From e12bf19e35867f3ea69f45054decb024a75fc2b4 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 00:47:05 +0100 Subject: web: add event store, fix all those bugs --- web/src/js/store/settingstore.js | 28 +++++++++ web/src/js/store/store.js | 123 +++++++++++++++++++++++++++++++++++++++ web/src/js/store/view.js | 87 +++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 web/src/js/store/settingstore.js create mode 100644 web/src/js/store/store.js create mode 100644 web/src/js/store/view.js (limited to 'web/src/js/store') diff --git a/web/src/js/store/settingstore.js b/web/src/js/store/settingstore.js new file mode 100644 index 00000000..7eef9b8f --- /dev/null +++ b/web/src/js/store/settingstore.js @@ -0,0 +1,28 @@ +function _SettingsStore() { + EventEmitter.call(this); + + //FIXME: What do we do if we haven't requested anything from the server yet? + this.settings = { + version: "0.12", + showEventLog: true, + mode: "transparent", + }; +} +_.extend(_SettingsStore.prototype, EventEmitter.prototype, { + getAll: function () { + return this.settings; + }, + handle: function (action) { + switch (action.type) { + case ActionTypes.UPDATE_SETTINGS: + this.settings = action.settings; + this.emit("change"); + break; + default: + return; + } + } +}); + +var SettingsStore = new _SettingsStore(); +AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); diff --git a/web/src/js/store/store.js b/web/src/js/store/store.js new file mode 100644 index 00000000..da288a5d --- /dev/null +++ b/web/src/js/store/store.js @@ -0,0 +1,123 @@ +function Store() { + this._views = []; + this.reset(); +} +_.extend(Store.prototype, { + add: function (elem) { + if (elem.id in this._pos_map) { + return; + } + + this._pos_map[elem.id] = this._list.length; + this._list.push(elem); + for (var i = 0; i < this._views.length; i++) { + this._views[i].add(elem); + } + }, + update: function (elem) { + if (!(elem.id in this._pos_map)) { + return; + } + + this._list[this._pos_map[elem.id]] = elem; + for (var i = 0; i < this._views.length; i++) { + this._views[i].update(elem); + } + }, + remove: function (elem_id) { + if (!(elem.id in this._pos_map)) { + return; + } + + this._list.splice(this._pos_map[elem_id], 1); + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].remove(elem_id); + } + }, + reset: function (elems) { + this._list = elems || []; + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].recalculate(this._list); + } + }, + _build_map: function () { + this._pos_map = {}; + for (var i = 0; i < this._list.length; i++) { + var elem = this._list[i]; + this._pos_map[elem.id] = i; + } + }, + get: function (elem_id) { + return this._list[this._pos_map[elem_id]]; + }, + index: function(elem_id) { + return this._pos_map[elem_id]; + } +}); + + +function LiveStore(type) { + Store.call(this); + this.type = type; + + this._updates_before_fetch = undefined; + this._fetchxhr = false; + + this.handle = this.handle.bind(this); + AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } +} +_.extend(LiveStore.prototype, Store.prototype, { + handle: function (event) { + if (event.type === ActionTypes.CONNECTION_OPEN) { + return this.fetch(); + } + if (event.type === this.type) { + if (event.cmd === StoreCmds.RESET) { + this.fetch(); + } else if (this._updates_before_fetch) { + console.log("defer update", event); + this._updates_before_fetch.push(event); + } else { + this[event.cmd](event.data); + } + } + }, + close: function () { + AppDispatcher.unregister(this.handle); + }, + fetch: function () { + console.log("fetch " + this.type); + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._fetchxhr = $.getJSON("/" + this.type, this.handle_fetch.bind(this)); + this._updates_before_fetch = []; // (JS: empty array is true) + }, + handle_fetch: function (data) { + this._fetchxhr = false; + console.log(this.type + " fetched.", this._updates_before_fetch); + this.reset(data.list); + var updates = this._updates_before_fetch; + this._updates_before_fetch = false; + for (var i = 0; i < updates.length; i++) { + this.handle(updates[i]); + } + }, +}); + + +function FlowStore() { + return new LiveStore(ActionTypes.FLOW_STORE); +} + + +function EventLogStore() { + return new LiveStore(ActionTypes.EVENT_STORE); +} \ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js new file mode 100644 index 00000000..261429b2 --- /dev/null +++ b/web/src/js/store/view.js @@ -0,0 +1,87 @@ +function SortByStoreOrder(elem) { + return this.store.index(elem.id); +} + +var default_sort = SortByStoreOrder; +var default_filt = function(elem){ + return true; +}; + +function StoreView(store, filt, sortfun) { + EventEmitter.call(this); + filt = filt || default_filt; + sortfun = sortfun || default_sort; + + this.store = store; + this.store._views.push(this); + this.recalculate(this.store._list, filt, sortfun); +} + +_.extend(StoreView.prototype, EventEmitter.prototype, { + close: function () { + this.store._views = _.without(this.store._views, this); + }, + recalculate: function (elems, filt, sortfun) { + if (filt) { + this.filt = filt; + } + if (sortfun) { + this.sortfun = sortfun.bind(this); + } + + this.list = elems.filter(this.filt); + this.list.sort(function (a, b) { + return this.sortfun(a) - this.sortfun(b); + }.bind(this)); + this.emit("recalculate"); + }, + index: function (elem) { + return _.sortedIndex(this.list, elem, this.sortfun); + }, + add: function (elem) { + if (this.filt(elem)) { + var idx = this.index(elem); + if (idx === this.list.length) { //happens often, .push is way faster. + this.list.push(elem); + } else { + this.list.splice(idx, 0, elem); + } + this.emit("add", elem, idx); + } + }, + update: function (elem) { + var idx; + var i = this.list.length; + // Search from the back, we usually update the latest entries. + while (i--) { + if (this.list[i].id === elem.id) { + idx = i; + break; + } + } + + if (idx === -1) { //not contained in list + this.add(elem); + } else if (!this.filt(elem)) { + this.remove(elem.id); + } else { + if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed + this.remove(this.list[idx]); + this.add(elem); + } else { + this.list[idx] = elem; + this.emit("update", elem, idx); + } + } + }, + remove: function (elem_id) { + var idx = this.list.length; + while (idx--) { + if (this.list[idx].id === elem_id) { + this.list.splice(idx, 1); + this.emit("remove", elem_id, idx); + break; + } + } + } +}); \ No newline at end of file -- cgit v1.2.3 From 7e40b8ab09d6d605307342fbfa21129ca15ff055 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 15:25:40 +0100 Subject: web: implement settings store, modularize store --- web/src/js/store/settingstore.js | 28 ---------- web/src/js/store/store.js | 115 ++++++++++++++++++++++++++------------- web/src/js/store/view.js | 18 +++++- 3 files changed, 93 insertions(+), 68 deletions(-) (limited to 'web/src/js/store') diff --git a/web/src/js/store/settingstore.js b/web/src/js/store/settingstore.js index 7eef9b8f..e69de29b 100644 --- a/web/src/js/store/settingstore.js +++ b/web/src/js/store/settingstore.js @@ -1,28 +0,0 @@ -function _SettingsStore() { - EventEmitter.call(this); - - //FIXME: What do we do if we haven't requested anything from the server yet? - this.settings = { - version: "0.12", - showEventLog: true, - mode: "transparent", - }; -} -_.extend(_SettingsStore.prototype, EventEmitter.prototype, { - getAll: function () { - return this.settings; - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.UPDATE_SETTINGS: - this.settings = action.settings; - this.emit("change"); - break; - default: - return; - } - } -}); - -var SettingsStore = new _SettingsStore(); -AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); diff --git a/web/src/js/store/store.js b/web/src/js/store/store.js index da288a5d..0f94e496 100644 --- a/web/src/js/store/store.js +++ b/web/src/js/store/store.js @@ -1,65 +1,68 @@ -function Store() { - this._views = []; +function ListStore() { + EventEmitter.call(this); this.reset(); } -_.extend(Store.prototype, { +_.extend(ListStore.prototype, EventEmitter.prototype, { add: function (elem) { if (elem.id in this._pos_map) { return; } - - this._pos_map[elem.id] = this._list.length; - this._list.push(elem); - for (var i = 0; i < this._views.length; i++) { - this._views[i].add(elem); - } + this._pos_map[elem.id] = this.list.length; + this.list.push(elem); + this.emit("add", elem); }, update: function (elem) { if (!(elem.id in this._pos_map)) { return; } - - this._list[this._pos_map[elem.id]] = elem; - for (var i = 0; i < this._views.length; i++) { - this._views[i].update(elem); - } + this.list[this._pos_map[elem.id]] = elem; + this.emit("update", elem); }, remove: function (elem_id) { if (!(elem.id in this._pos_map)) { return; } - - this._list.splice(this._pos_map[elem_id], 1); + this.list.splice(this._pos_map[elem_id], 1); this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].remove(elem_id); - } + this.emit("remove", elem_id); }, reset: function (elems) { - this._list = elems || []; + this.list = elems || []; this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].recalculate(this._list); - } + this.emit("recalculate", this.list); }, _build_map: function () { this._pos_map = {}; - for (var i = 0; i < this._list.length; i++) { - var elem = this._list[i]; + for (var i = 0; i < this.list.length; i++) { + var elem = this.list[i]; this._pos_map[elem.id] = i; } }, get: function (elem_id) { - return this._list[this._pos_map[elem_id]]; + return this.list[this._pos_map[elem_id]]; }, - index: function(elem_id) { + index: function (elem_id) { return this._pos_map[elem_id]; } }); -function LiveStore(type) { - Store.call(this); +function DictStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(DictStore.prototype, EventEmitter.prototype, { + update: function (dict) { + _.merge(this.dict, dict); + this.emit("recalculate", this.dict); + }, + reset: function (dict) { + this.dict = dict || {}; + this.emit("recalculate", this.dict); + } +}); + +function LiveStoreMixin(type) { this.type = type; this._updates_before_fetch = undefined; @@ -73,7 +76,7 @@ function LiveStore(type) { this.fetch(); } } -_.extend(LiveStore.prototype, Store.prototype, { +_.extend(LiveStoreMixin.prototype, { handle: function (event) { if (event.type === ActionTypes.CONNECTION_OPEN) { return this.fetch(); @@ -92,18 +95,28 @@ _.extend(LiveStore.prototype, Store.prototype, { close: function () { AppDispatcher.unregister(this.handle); }, - fetch: function () { + fetch: function (data) { console.log("fetch " + this.type); if (this._fetchxhr) { this._fetchxhr.abort(); } - this._fetchxhr = $.getJSON("/" + this.type, this.handle_fetch.bind(this)); - this._updates_before_fetch = []; // (JS: empty array is true) + this._updates_before_fetch = []; // (JS: empty array is true) + if (data) { + this.handle_fetch(data); + } else { + this._fetchxhr = $.getJSON("/" + this.type) + .done(function (message) { + this.handle_fetch(message.data); + }.bind(this)) + .fail(function () { + EventLogActions.add_event("Could not fetch " + this.type); + }.bind(this)); + } }, handle_fetch: function (data) { this._fetchxhr = false; console.log(this.type + " fetched.", this._updates_before_fetch); - this.reset(data.list); + this.reset(data); var updates = this._updates_before_fetch; this._updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { @@ -112,12 +125,40 @@ _.extend(LiveStore.prototype, Store.prototype, { }, }); +function LiveListStore(type) { + ListStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype); + +function LiveDictStore(type) { + DictStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype); + function FlowStore() { - return new LiveStore(ActionTypes.FLOW_STORE); + return new LiveListStore(ActionTypes.FLOW_STORE); } +function SettingsStore() { + return new LiveDictStore(ActionTypes.SETTINGS_STORE); +} function EventLogStore() { - return new LiveStore(ActionTypes.EVENT_STORE); -} \ No newline at end of file + LiveListStore.call(this, ActionTypes.EVENT_STORE); +} +_.extend(EventLogStore.prototype, LiveListStore.prototype, { + fetch: function(){ + LiveListStore.prototype.fetch.apply(this, arguments); + + // Make sure to display updates even if fetching all events failed. + // This way, we can send "fetch failed" log messages to the log. + if(this._fetchxhr){ + this._fetchxhr.fail(function(){ + this.handle_fetch(null); + }.bind(this)); + } + } +}); \ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js index 261429b2..56bc4dbd 100644 --- a/web/src/js/store/view.js +++ b/web/src/js/store/view.js @@ -13,13 +13,25 @@ function StoreView(store, filt, sortfun) { sortfun = sortfun || default_sort; this.store = store; - this.store._views.push(this); - this.recalculate(this.store._list, filt, sortfun); + + this.add = this.add.bind(this); + this.update = this.update.bind(this); + this.remove = this.remove.bind(this); + this.recalculate = this.recalculate.bind(this); + this.store.addListener("add", this.add); + this.store.addListener("update", this.update); + this.store.addListener("remove", this.remove); + this.store.addListener("recalculate", this.recalculate); + + this.recalculate(this.store.list, filt, sortfun); } _.extend(StoreView.prototype, EventEmitter.prototype, { close: function () { - this.store._views = _.without(this.store._views, this); + this.store.removeListener("add", this.add); + this.store.removeListener("update", this.update); + this.store.removeListener("remove", this.remove); + this.store.removeListener("recalculate", this.recalculate); }, recalculate: function (elems, filt, sortfun) { if (filt) { -- cgit v1.2.3