restructure observable collections and fixes for sort, filter and map

This commit is contained in:
Bruno Windels 2019-02-26 20:48:57 +01:00
parent 9e6c69c121
commit 618c4ffe20
10 changed files with 84 additions and 272 deletions

View file

@ -27,4 +27,6 @@ export default class BaseObservableCollection {
return null; return null;
}; };
} }
// Add iterator over handlers here
} }

22
src/observable/index.js Normal file
View 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);
}
});

View file

@ -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);
} }
} }

View file

@ -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);

View file

@ -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);
// }
} }

View file

@ -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();

View file

@ -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();
} }

View file

@ -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

View file

@ -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() {}
}

View file

@ -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) {
},
};
}