forked from mystiq/hydrogen-web
handle key collisions in JoinedMap
This commit is contained in:
parent
20f4474eb6
commit
4e3127c4cf
1 changed files with 191 additions and 23 deletions
|
@ -22,27 +22,33 @@ export class JoinedMap extends BaseObservableMap {
|
|||
this._sources = sources;
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
this.emitAdd(key, value);
|
||||
onAdd(source, key, value) {
|
||||
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||
const occludingValue = this._getValueFromOccludedSources(source, key);
|
||||
if (occludingValue !== undefined) {
|
||||
// adding a value that will occlude another one should
|
||||
// first emit a remove
|
||||
this.emitRemove(key, occludingValue);
|
||||
}
|
||||
this.emitAdd(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
onRemove(key, value) {
|
||||
this.emitRemove(key, value);
|
||||
onRemove(source, key, value) {
|
||||
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||
this.emitRemove(key, value);
|
||||
const occludedValue = this._getValueFromOccludedSources(source, key);
|
||||
if (occludedValue !== undefined) {
|
||||
// removing a value that so far occluded another one should
|
||||
// emit an add for the occluded value after the removal
|
||||
this.emitAdd(key, occludedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate(key, value, params) {
|
||||
this.emitUpdate(key, value, params);
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._subscriptions = this._sources.map(source => source.subscribe(this));
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
for (const s of this._subscriptions) {
|
||||
s();
|
||||
onUpdate(source, key, value, params) {
|
||||
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||
this.emitUpdate(key, value, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +56,49 @@ export class JoinedMap extends BaseObservableMap {
|
|||
this.emitReset();
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._subscriptions = this._sources.map(source => new SourceSubscriptionHandler(source, this).subscribe());
|
||||
super.onSubscribeFirst();
|
||||
}
|
||||
|
||||
_isKeyAtSourceOccluded(source, key) {
|
||||
// sources that come first in the sources array can
|
||||
// hide the keys in later sources, to prevent events
|
||||
// being emitted for the same key and different values,
|
||||
// so check the key is not present in earlier sources
|
||||
const index = this._sources.indexOf(source);
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (this._sources[i].get(key) !== undefined) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the value that the given source and key occlude, if any
|
||||
_getValueFromOccludedSources(source, key) {
|
||||
// sources that come first in the sources array can
|
||||
// hide the keys in later sources, to prevent events
|
||||
// being emitted for the same key and different values,
|
||||
// so check the key is not present in earlier sources
|
||||
const index = this._sources.indexOf(source);
|
||||
for (let i = index + 1; i < this._sources.length; i += 1) {
|
||||
const source = this._sources[i];
|
||||
const occludedValue = source.get(key);
|
||||
if (occludedValue !== undefined) {
|
||||
return occludedValue;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
super.onUnsubscribeLast();
|
||||
for (const s of this._subscriptions) {
|
||||
s.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return new JoinedIterator(this._sources);
|
||||
}
|
||||
|
@ -74,6 +123,7 @@ class JoinedIterator {
|
|||
this._sources = sources;
|
||||
this._sourceIndex = -1;
|
||||
this._currentIterator = null;
|
||||
this._encounteredKeys = new Set();
|
||||
}
|
||||
|
||||
next() {
|
||||
|
@ -91,22 +141,140 @@ class JoinedIterator {
|
|||
this._currentIterator = null;
|
||||
continue;
|
||||
} else {
|
||||
result = sourceResult;
|
||||
const key = sourceResult.value[0];
|
||||
if (!this._encounteredKeys.has(key)) {
|
||||
this._encounteredKeys.add(key);
|
||||
result = sourceResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SourceSubscriptionHandler {
|
||||
constructor(source, joinedMap) {
|
||||
this._source = source;
|
||||
this._joinedMap = joinedMap;
|
||||
this._subscription = null;
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
this._source.subscribe(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._subscription = this._subscription();
|
||||
}
|
||||
|
||||
onAdd(key, value) {
|
||||
this._joinedMap.onAdd(this._source, key, value);
|
||||
}
|
||||
|
||||
onRemove(key, value) {
|
||||
this._joinedMap.onRemove(this._source, key, value);
|
||||
}
|
||||
|
||||
onUpdate(key, value, params) {
|
||||
this._joinedMap.onUpdate(this._source, key, value, params);
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this._joinedMap.onReset(this._source);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import { ObservableMap } from "./ObservableMap.js";
|
||||
|
||||
export function tests() {
|
||||
|
||||
function observeMap(map) {
|
||||
const events = [];
|
||||
map.subscribe({
|
||||
onAdd(key, value) { events.push({type: "add", key, value}); },
|
||||
onRemove(key, value) { events.push({type: "remove", key, value}); },
|
||||
onUpdate(key, value, params) { events.push({type: "update", key, value, params}); }
|
||||
});
|
||||
return events;
|
||||
}
|
||||
|
||||
return {
|
||||
"joined iterator": assert => {
|
||||
const it = new JoinedIterator([[1, 2], [3, 4]]);
|
||||
assert.equal(it.next().value, 1);
|
||||
assert.equal(it.next().value, 2);
|
||||
assert.equal(it.next().value, 3);
|
||||
assert.equal(it.next().value, 4);
|
||||
const firstKV = ["a", 1];
|
||||
const secondKV = ["b", 2];
|
||||
const thirdKV = ["c", 3];
|
||||
const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]);
|
||||
assert.equal(it.next().value, firstKV);
|
||||
assert.equal(it.next().value, secondKV);
|
||||
assert.equal(it.next().value, thirdKV);
|
||||
assert.equal(it.next().done, true);
|
||||
},
|
||||
"prevent key collision during iteration": assert => {
|
||||
const first = new ObservableMap();
|
||||
const second = new ObservableMap();
|
||||
const join = new JoinedMap([first, second]);
|
||||
second.add("a", 2);
|
||||
second.add("b", 3);
|
||||
first.add("a", 1);
|
||||
const it = join[Symbol.iterator]();
|
||||
assert.deepEqual(it.next().value, ["a", 1]);
|
||||
assert.deepEqual(it.next().value, ["b", 3]);
|
||||
assert.equal(it.next().done, true);
|
||||
},
|
||||
"adding occluded key doesn't emit add": assert => {
|
||||
const first = new ObservableMap();
|
||||
const second = new ObservableMap();
|
||||
const join = new JoinedMap([first, second]);
|
||||
const events = observeMap(join);
|
||||
first.add("a", 1);
|
||||
second.add("a", 2);
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].type, "add");
|
||||
assert.equal(events[0].key, "a");
|
||||
assert.equal(events[0].value, 1);
|
||||
},
|
||||
"updating occluded key doesn't emit update": assert => {
|
||||
const first = new ObservableMap();
|
||||
const second = new ObservableMap();
|
||||
const join = new JoinedMap([first, second]);
|
||||
first.add("a", 1);
|
||||
second.add("a", 2);
|
||||
const events = observeMap(join);
|
||||
second.update("a", 3);
|
||||
assert.equal(events.length, 0);
|
||||
},
|
||||
"removal of occluding key emits add after remove": assert => {
|
||||
const first = new ObservableMap();
|
||||
const second = new ObservableMap();
|
||||
const join = new JoinedMap([first, second]);
|
||||
first.add("a", 1);
|
||||
second.add("a", 2);
|
||||
const events = observeMap(join);
|
||||
first.remove("a");
|
||||
assert.equal(events.length, 2);
|
||||
assert.equal(events[0].type, "remove");
|
||||
assert.equal(events[0].key, "a");
|
||||
assert.equal(events[0].value, 1);
|
||||
assert.equal(events[1].type, "add");
|
||||
assert.equal(events[1].key, "a");
|
||||
assert.equal(events[1].value, 2);
|
||||
},
|
||||
"adding occluding key emits remove first": assert => {
|
||||
const first = new ObservableMap();
|
||||
const second = new ObservableMap();
|
||||
const join = new JoinedMap([first, second]);
|
||||
second.add("a", 2);
|
||||
const events = observeMap(join);
|
||||
first.add("a", 1);
|
||||
assert.equal(events.length, 2);
|
||||
assert.equal(events[0].type, "remove");
|
||||
assert.equal(events[0].key, "a");
|
||||
assert.equal(events[0].value, 2);
|
||||
assert.equal(events[1].type, "add");
|
||||
assert.equal(events[1].key, "a");
|
||||
assert.equal(events[1].value, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue