handle key collisions in JoinedMap

This commit is contained in:
Bruno Windels 2021-04-22 13:28:14 +02:00
parent 20f4474eb6
commit 4e3127c4cf

View file

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