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;
|
this._sources = sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(source, key, value) {
|
||||||
this.emitAdd(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) {
|
onRemove(source, key, value) {
|
||||||
this.emitRemove(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) {
|
onUpdate(source, key, value, params) {
|
||||||
this.emitUpdate(key, value, params);
|
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||||
}
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +56,49 @@ export class JoinedMap extends BaseObservableMap {
|
||||||
this.emitReset();
|
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]() {
|
[Symbol.iterator]() {
|
||||||
return new JoinedIterator(this._sources);
|
return new JoinedIterator(this._sources);
|
||||||
}
|
}
|
||||||
|
@ -74,6 +123,7 @@ class JoinedIterator {
|
||||||
this._sources = sources;
|
this._sources = sources;
|
||||||
this._sourceIndex = -1;
|
this._sourceIndex = -1;
|
||||||
this._currentIterator = null;
|
this._currentIterator = null;
|
||||||
|
this._encounteredKeys = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
|
@ -91,22 +141,140 @@ class JoinedIterator {
|
||||||
this._currentIterator = null;
|
this._currentIterator = null;
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
result = sourceResult;
|
const key = sourceResult.value[0];
|
||||||
|
if (!this._encounteredKeys.has(key)) {
|
||||||
|
this._encounteredKeys.add(key);
|
||||||
|
result = sourceResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
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() {
|
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 {
|
return {
|
||||||
"joined iterator": assert => {
|
"joined iterator": assert => {
|
||||||
const it = new JoinedIterator([[1, 2], [3, 4]]);
|
const firstKV = ["a", 1];
|
||||||
assert.equal(it.next().value, 1);
|
const secondKV = ["b", 2];
|
||||||
assert.equal(it.next().value, 2);
|
const thirdKV = ["c", 3];
|
||||||
assert.equal(it.next().value, 3);
|
const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]);
|
||||||
assert.equal(it.next().value, 4);
|
assert.equal(it.next().value, firstKV);
|
||||||
|
assert.equal(it.next().value, secondKV);
|
||||||
|
assert.equal(it.next().value, thirdKV);
|
||||||
assert.equal(it.next().done, true);
|
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