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;
|
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) {
|
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?
|
So how do you know where it was before it changed, if not by going over all values?
|
||||||
|
|
||||||
how to make this fast?
|
how to make this fast?
|
||||||
|
@ -52,73 +52,81 @@ function sortedIndex(array, value, comparator) {
|
||||||
}
|
}
|
||||||
return high;
|
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
|
// no duplicates allowed for now
|
||||||
export default class SortOperator extends BaseObservableList {
|
export default class SortedMapList extends BaseObservableList {
|
||||||
constructor(sourceMap, comparator, getKey) {
|
constructor(sourceMap, comparator) {
|
||||||
super();
|
super();
|
||||||
this._sourceMap = sourceMap;
|
this._sourceMap = sourceMap;
|
||||||
this._comparator = comparator;
|
this._comparator = comparator;
|
||||||
this._getKey = getKey;
|
this._sortedPairs = null;
|
||||||
this._sortedValues = [];
|
this._mapSubscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key, value) {
|
||||||
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
const idx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||||
this._sortedValues.splice(idx, 0, value);
|
this._sortedPairs.splice(idx, 0, {key, value});
|
||||||
this.emitAdd(idx, value);
|
this.emitAdd(idx, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key, value) {
|
onRemove(key, value) {
|
||||||
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
const idx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||||
this._sortedValues.splice(idx, 1);
|
// assert key === this._sortedPairs[idx].key;
|
||||||
|
this._sortedPairs.splice(idx, 1);
|
||||||
this.emitRemove(idx, value);
|
this.emitRemove(idx, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(key, value, params) {
|
onUpdate(key, value, params) {
|
||||||
const idxIfSortUnchanged = sortedIndex(this._sortedValues, value, this._comparator);
|
// TODO: suboptimal for performance, see above for idea with BST to speed this up if we need to
|
||||||
if (this._getKey(this._sortedValues[idxIfSortUnchanged]) === key) {
|
const oldIdx = this._sortedPairs.findIndex(p => p.key === key);
|
||||||
this.emitChange(idxIfSortUnchanged, value, params);
|
// neccesary to remove item from array before
|
||||||
} else {
|
// doing sortedIndex as it relies on being sorted
|
||||||
// TODO: slow!? See above for idea with BST to speed this up if we need to
|
this._sortedPairs.splice(oldIdx, 1);
|
||||||
const oldIdx = this._sortedValues.find(v => this._getKey(v) === key);
|
const newIdx = sortedIndex(this._sortedPairs, value, this._comparator);
|
||||||
this._sortedValues.splice(oldIdx, 1);
|
this._sortedPairs.splice(newIdx, 0, {key, value});
|
||||||
const newIdx = sortedIndex(this._sortedValues, value, this._comparator);
|
if (oldIdx !== newIdx) {
|
||||||
this._sortedValues.splice(newIdx, 0, value);
|
|
||||||
this.emitMove(oldIdx, newIdx, value);
|
this.emitMove(oldIdx, newIdx, value);
|
||||||
this.emitChange(newIdx, value, params);
|
|
||||||
}
|
}
|
||||||
|
this.emitUpdate(newIdx, value, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset() {
|
||||||
this._sortedValues = [];
|
this._sortedPairs = [];
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst() {
|
||||||
this._mapSubscription = this._sourceMap.subscribe(this);
|
this._mapSubscription = this._sourceMap.subscribe(this);
|
||||||
this._sortedValues = new Array(this._sourceMap.size);
|
this._sortedPairs = new Array(this._sourceMap.size);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let [, value] of this._sourceMap) {
|
for (let [, value] of this._sourceMap) {
|
||||||
this._sortedValues[i] = value;
|
this._sortedPairs[i] = value;
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
this._sortedValues.sort(this._comparator);
|
this._sortedPairs.sort(this._comparator);
|
||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast() {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this._sortedValues = null;
|
this._sortedPairs = null;
|
||||||
this._mapSubscription = this._mapSubscription();
|
this._mapSubscription = this._mapSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(index) {
|
||||||
|
return this._sourceMap[index];
|
||||||
|
}
|
||||||
|
|
||||||
get length() {
|
get length() {
|
||||||
return this._sourceMap.size;
|
return this._sourceMap.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return this._sortedValues;
|
return this._sortedPairs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +147,7 @@ export function tests() {
|
||||||
assert.equal(idx, 1);
|
assert.equal(idx, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_sortIndex_likeArraySort(assert) {
|
test_sortIndex_comparator_Array_compatible(assert) {
|
||||||
const a = [5, 1, 8, 2];
|
const a = [5, 1, 8, 2];
|
||||||
const cmp = (a, b) => a - b;
|
const cmp = (a, b) => a - b;
|
||||||
a.sort(cmp);
|
a.sort(cmp);
|
|
@ -25,12 +25,4 @@ export default class BaseObservableMap extends BaseObservableCollection {
|
||||||
h.onRemove(key, value);
|
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) {
|
constructor(source, mapper, updater) {
|
||||||
super(source);
|
super();
|
||||||
|
this._source = source;
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
this._updater = updater;
|
this._updater = updater;
|
||||||
this._mappedValues = new Map();
|
this._mappedValues = new Map();
|
|
@ -1,15 +1,19 @@
|
||||||
import Operator from "../Operator.js";
|
import BaseObservableMap from "./BaseObservableMap.js";
|
||||||
|
|
||||||
export default class FilterOperator extends Operator {
|
export default class MappedMap extends BaseObservableMap {
|
||||||
constructor(source, mapper, updater) {
|
constructor(source, mapper) {
|
||||||
super(source);
|
super();
|
||||||
|
this._source = source;
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
this._updater = updater;
|
|
||||||
this._mappedValues = new Map();
|
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) {
|
onAdd(key, value) {
|
||||||
const mappedValue = this._mapper(value);
|
const mappedValue = this._mapper(value, this._updater);
|
||||||
this._mappedValues.set(key, mappedValue);
|
this._mappedValues.set(key, mappedValue);
|
||||||
this.emitAdd(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);
|
const mappedValue = this._mappedValues.get(key);
|
||||||
if (mappedValue !== undefined) {
|
if (mappedValue !== undefined) {
|
||||||
const newParams = this._updater(value, params);
|
const newParams = this._updater(value, params);
|
||||||
if (newParams !== undefined) {
|
if (newParams !== undefined) {
|
||||||
this.emitChange(key, mappedValue, newParams);
|
this.emitUpdate(key, mappedValue, newParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst() {
|
||||||
|
this._subscription = this._source.subscribe(this);
|
||||||
for (let [key, value] of this._source) {
|
for (let [key, value] of this._source) {
|
||||||
const mappedValue = this._mapper(value);
|
const mappedValue = this._mapper(value, this._updater);
|
||||||
this._mappedValues.set(key, mappedValue);
|
this._mappedValues.set(key, mappedValue);
|
||||||
}
|
}
|
||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast() {
|
||||||
super.onUnsubscribeLast();
|
this._subscription = this._subscription();
|
||||||
this._mappedValues.clear();
|
this._mappedValues.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import BaseObservableMap from "./map/BaseObservableMap.js";
|
import BaseObservableMap from "./BaseObservableMap.js";
|
||||||
|
|
||||||
export default class ObservableMap extends BaseObservableMap {
|
export default class ObservableMap extends BaseObservableMap {
|
||||||
constructor(keyFn, initialValues) {
|
constructor(keyFn, initialValues) {
|
||||||
|
@ -57,6 +57,7 @@ export default class ObservableMap extends BaseObservableMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#ifdef TESTS
|
||||||
export function tests() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
test_add(assert) {
|
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