forked from mystiq/hydrogen-web
restructure observable collections and fixes for sort, filter and map
This commit is contained in:
parent
9e6c69c121
commit
618c4ffe20
10 changed files with 84 additions and 272 deletions
|
@ -27,4 +27,6 @@ export default class BaseObservableCollection {
|
|||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
// Add iterator over handlers here
|
||||
}
|
||||
|
|
22
src/observable/index.js
Normal file
22
src/observable/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import SortedMapList from "./list/SortedMapList.js";
|
||||
import FilteredMap from "./map/FilteredMap.js";
|
||||
import MappedMap from "./map/MappedMap.js";
|
||||
import BaseObservableMap from "./map/BaseObservableMap.js";
|
||||
// re-export "root" (of chain) collections
|
||||
export { default as ObservableMap } from "./map/ObservableMap.js";
|
||||
|
||||
// avoid circular dependency between these classes
|
||||
// and BaseObservableMap (as they extend it)
|
||||
Object.assign(BaseObservableMap.prototype, {
|
||||
asSortedList(comparator) {
|
||||
return new SortedMapList(this, comparator);
|
||||
},
|
||||
|
||||
mapValues(mapper) {
|
||||
return new MappedMap(this, mapper);
|
||||
},
|
||||
|
||||
filterValues(filter) {
|
||||
return new FilteredMap(this, filter);
|
||||
}
|
||||
});
|
|
@ -14,9 +14,9 @@ export default class BaseObservableList extends BaseObservableCollection {
|
|||
}
|
||||
}
|
||||
|
||||
emitChange(index, value, params) {
|
||||
emitUpdate(index, value, params) {
|
||||
for(let h of this._handlers) {
|
||||
h.onChange(index, value, params);
|
||||
h.onUpdate(index, value, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import BaseObservableList from "../BaseObservableList.js";
|
||||
import BaseObservableList from "./BaseObservableList.js";
|
||||
|
||||
|
||||
/*
|
||||
|
||||
when a value changes, it sorting order can change. It would still be at the old index prior to firing an onChange event.
|
||||
when a value changes, it sorting order can change. It would still be at the old index prior to firing an onUpdate event.
|
||||
So how do you know where it was before it changed, if not by going over all values?
|
||||
|
||||
how to make this fast?
|
||||
|
@ -52,73 +52,81 @@ function sortedIndex(array, value, comparator) {
|
|||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
|
||||
// does not assume whether or not the values are reference
|
||||
// types modified outside of the collection (and affecting sort order) or not
|
||||
|
||||
// no duplicates allowed for now
|
||||
export default class SortOperator extends BaseObservableList {
|
||||
constructor(sourceMap, comparator, getKey) {
|
||||
export default class SortedMapList extends BaseObservableList {
|
||||
constructor(sourceMap, comparator) {
|
||||
super();
|
||||
this._sourceMap = sourceMap;
|
||||
this._comparator = comparator;
|
||||
this._getKey = getKey;
|
||||
this._sortedValues = [];
|
||||
this._sortedPairs = null;
|
||||
this._mapSubscription = null;
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||
this._sortedValues.splice(idx, 0, value);
|
||||
const idx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||
this._sortedPairs.splice(idx, 0, {key, value});
|
||||
this.emitAdd(idx, value);
|
||||
}
|
||||
|
||||
onRemove(key, value) {
|
||||
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||
this._sortedValues.splice(idx, 1);
|
||||
const idx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||
// assert key === this._sortedPairs[idx].key;
|
||||
this._sortedPairs.splice(idx, 1);
|
||||
this.emitRemove(idx, value);
|
||||
}
|
||||
|
||||
onChange(key, value, params) {
|
||||
const idxIfSortUnchanged = sortedIndex(this._sortedValues, value, this._comparator);
|
||||
if (this._getKey(this._sortedValues[idxIfSortUnchanged]) === key) {
|
||||
this.emitChange(idxIfSortUnchanged, value, params);
|
||||
} else {
|
||||
// TODO: slow!? See above for idea with BST to speed this up if we need to
|
||||
const oldIdx = this._sortedValues.find(v => this._getKey(v) === key);
|
||||
this._sortedValues.splice(oldIdx, 1);
|
||||
const newIdx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||
this._sortedValues.splice(newIdx, 0, value);
|
||||
onUpdate(key, value, params) {
|
||||
// TODO: suboptimal for performance, see above for idea with BST to speed this up if we need to
|
||||
const oldIdx = this._sortedPairs.findIndex(p => p.key === key);
|
||||
// neccesary to remove item from array before
|
||||
// doing sortedIndex as it relies on being sorted
|
||||
this._sortedPairs.splice(oldIdx, 1);
|
||||
const newIdx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||
this._sortedPairs.splice(newIdx, 0, {key, value});
|
||||
if (oldIdx !== newIdx) {
|
||||
this.emitMove(oldIdx, newIdx, value);
|
||||
this.emitChange(newIdx, value, params);
|
||||
}
|
||||
this.emitUpdate(newIdx, value, params);
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this._sortedValues = [];
|
||||
this._sortedPairs = [];
|
||||
this.emitReset();
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._mapSubscription = this._sourceMap.subscribe(this);
|
||||
this._sortedValues = new Array(this._sourceMap.size);
|
||||
this._sortedPairs = new Array(this._sourceMap.size);
|
||||
let i = 0;
|
||||
for (let [, value] of this._sourceMap) {
|
||||
this._sortedValues[i] = value;
|
||||
this._sortedPairs[i] = value;
|
||||
++i;
|
||||
}
|
||||
this._sortedValues.sort(this._comparator);
|
||||
this._sortedPairs.sort(this._comparator);
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
this._sortedValues = null;
|
||||
this._sortedPairs = null;
|
||||
this._mapSubscription = this._mapSubscription();
|
||||
}
|
||||
|
||||
get(index) {
|
||||
return this._sourceMap[index];
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._sourceMap.size;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._sortedValues;
|
||||
return this._sortedPairs;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +147,7 @@ export function tests() {
|
|||
assert.equal(idx, 1);
|
||||
},
|
||||
|
||||
test_sortIndex_likeArraySort(assert) {
|
||||
test_sortIndex_comparator_Array_compatible(assert) {
|
||||
const a = [5, 1, 8, 2];
|
||||
const cmp = (a, b) => a - b;
|
||||
a.sort(cmp);
|
|
@ -25,12 +25,4 @@ export default class BaseObservableMap extends BaseObservableCollection {
|
|||
h.onRemove(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// map(mapper, updater) {
|
||||
// return new MapOperator(this, mapper, updater);
|
||||
// }
|
||||
|
||||
// sort(comparator) {
|
||||
// return new SortOperator(this, comparator);
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Operator from "../Operator.js";
|
||||
import BaseObservableMap from "./BaseObservableMap.js";
|
||||
|
||||
export default class MapOperator extends Operator {
|
||||
export default class FilteredMap extends BaseObservableMap {
|
||||
constructor(source, mapper, updater) {
|
||||
super(source);
|
||||
super();
|
||||
this._source = source;
|
||||
this._mapper = mapper;
|
||||
this._updater = updater;
|
||||
this._mappedValues = new Map();
|
|
@ -1,15 +1,19 @@
|
|||
import Operator from "../Operator.js";
|
||||
import BaseObservableMap from "./BaseObservableMap.js";
|
||||
|
||||
export default class FilterOperator extends Operator {
|
||||
constructor(source, mapper, updater) {
|
||||
super(source);
|
||||
export default class MappedMap extends BaseObservableMap {
|
||||
constructor(source, mapper) {
|
||||
super();
|
||||
this._source = source;
|
||||
this._mapper = mapper;
|
||||
this._updater = updater;
|
||||
this._mappedValues = new Map();
|
||||
this._updater = (key, params) => { // this should really be (value, params) but can't make that work for now
|
||||
const value = this._mappedValues.get(key);
|
||||
this.onUpdate(key, value, params);
|
||||
};
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
const mappedValue = this._mapper(value);
|
||||
const mappedValue = this._mapper(value, this._updater);
|
||||
this._mappedValues.set(key, mappedValue);
|
||||
this.emitAdd(key, mappedValue);
|
||||
}
|
||||
|
@ -21,26 +25,27 @@ export default class FilterOperator extends Operator {
|
|||
}
|
||||
}
|
||||
|
||||
onChange(key, value, params) {
|
||||
onUpdate(key, value, params) {
|
||||
const mappedValue = this._mappedValues.get(key);
|
||||
if (mappedValue !== undefined) {
|
||||
const newParams = this._updater(value, params);
|
||||
if (newParams !== undefined) {
|
||||
this.emitChange(key, mappedValue, newParams);
|
||||
this.emitUpdate(key, mappedValue, newParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._subscription = this._source.subscribe(this);
|
||||
for (let [key, value] of this._source) {
|
||||
const mappedValue = this._mapper(value);
|
||||
const mappedValue = this._mapper(value, this._updater);
|
||||
this._mappedValues.set(key, mappedValue);
|
||||
}
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
this._subscription = this._subscription();
|
||||
this._mappedValues.clear();
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import BaseObservableMap from "./map/BaseObservableMap.js";
|
||||
import BaseObservableMap from "./BaseObservableMap.js";
|
||||
|
||||
export default class ObservableMap extends BaseObservableMap {
|
||||
constructor(keyFn, initialValues) {
|
||||
|
@ -57,6 +57,7 @@ export default class ObservableMap extends BaseObservableMap {
|
|||
}
|
||||
}
|
||||
|
||||
//#ifdef TESTS
|
||||
export function tests() {
|
||||
return {
|
||||
test_add(assert) {
|
||||
|
@ -142,3 +143,4 @@ export function tests() {
|
|||
},
|
||||
}
|
||||
}
|
||||
//#endif
|
|
@ -1,22 +0,0 @@
|
|||
// import BaseObservableMap from "./BaseObservableMap.js";
|
||||
|
||||
export default class Operator /* extends BaseObservableMap */ {
|
||||
constructor(source) {
|
||||
// super();
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._sourceSubscription = this._source.subscribe(this);
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
this._sourceSubscription();
|
||||
this._sourceSubscription = null;
|
||||
}
|
||||
|
||||
onRemove(key, value) {}
|
||||
onAdd(key, value) {}
|
||||
onChange(key, value, params) {}
|
||||
onReset() {}
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
class ObservableMap {
|
||||
constructor() {
|
||||
this._handlers = new Set();
|
||||
}
|
||||
|
||||
emitReset() {
|
||||
for(let h of this._handlers) {
|
||||
h.onReset();
|
||||
}
|
||||
}
|
||||
// we need batch events, mostly on index based collection though?
|
||||
// maybe we should get started without?
|
||||
emitAdd(key, value) {
|
||||
for(let h of this._handlers) {
|
||||
h.onAdd(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
emitChange(key, value, ...params) {
|
||||
for(let h of this._handlers) {
|
||||
h.onChange(key, value, ...params);
|
||||
}
|
||||
}
|
||||
|
||||
emitRemove(key, value) {
|
||||
for(let h of this._handlers) {
|
||||
h.onRemove(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
|
||||
}
|
||||
|
||||
subscribe(handler) {
|
||||
this._handlers.add(handler);
|
||||
if (this._handlers.length === 1) {
|
||||
this.onSubscribeFirst();
|
||||
}
|
||||
return () => {
|
||||
if (handler) {
|
||||
this._handlers.delete(this._handler);
|
||||
if (this._handlers.length === 0) {
|
||||
this.onUnsubscribeLast();
|
||||
}
|
||||
handler = null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Operator extends ObservableMap {
|
||||
constructor(source) {
|
||||
super();
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._sourceSubscription = this._source.subscribe(this);
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
this._sourceSubscription();
|
||||
this._sourceSubscription = null;
|
||||
}
|
||||
|
||||
onRemove(key, value) {}
|
||||
onAdd(key, value) {}
|
||||
onChange(key, value, params) {}
|
||||
onReset() {}
|
||||
}
|
||||
|
||||
export default class ObservableMapCollection extends ObservableMap {
|
||||
constructor(initialValues) {
|
||||
super();
|
||||
this._values = new Map(initialValues);
|
||||
}
|
||||
|
||||
updated(key, params) {
|
||||
const value = this._values.get(key);
|
||||
if (value !== undefined) {
|
||||
this._values.add(key, value);
|
||||
this.emitChange(key, value, params);
|
||||
return true;
|
||||
}
|
||||
return false; // or return existing value?
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
if (!this._values.has(key)) {
|
||||
this._values.add(key, value);
|
||||
this.emitAdd(key, value);
|
||||
return true;
|
||||
}
|
||||
return false; // or return existing value?
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
const value = this._values.get(key);
|
||||
if (value !== undefined) {
|
||||
this._values.delete(key);
|
||||
this.emitRemove(key, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._values.clear();
|
||||
this.emitReset();
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this._values.get(key);
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._values.entries()[Symbol.iterator];
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableMapOperator extends Operator {
|
||||
constructor(source, mapper, updater) {
|
||||
super(source);
|
||||
this._mapper = mapper;
|
||||
this._updater = updater;
|
||||
this._mappedValues = new Map();
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
const mappedValue = this._mapper(value);
|
||||
this._mappedValues.set(key, mappedValue);
|
||||
this.emitAdd(key, mappedValue);
|
||||
}
|
||||
|
||||
onRemove(key, _value) {
|
||||
const mappedValue = this._mappedValues.get(key);
|
||||
if (this._mappedValues.delete(key)) {
|
||||
this.emitRemove(key, mappedValue);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(key, value, params) {
|
||||
const mappedValue = this._mappedValues.get(key);
|
||||
if (mappedValue !== undefined) {
|
||||
const newParams = this._updater(value, params);
|
||||
if (newParams !== undefined) {
|
||||
this.emitChange(key, mappedValue, newParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
for (let [key, value] of this._source) {
|
||||
const mappedValue = this._mapper(value);
|
||||
this._mappedValues.set(key, mappedValue);
|
||||
}
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
this._mappedValues.clear();
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this._mappedValues.clear();
|
||||
this.emitReset();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._mappedValues.entries()[Symbol.iterator];
|
||||
}
|
||||
}
|
||||
|
||||
class FilterOperator extends ObservableMapOperator {
|
||||
|
||||
}
|
||||
|
||||
class SortSet {
|
||||
constructor(ObservableMap) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
test_for_of(assert) {
|
||||
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue