workadventure/front/src/Stores/Utils/MapStore.ts
David Négrier 0e68490e75 Refactoring with a MapStore
A great deal of the complexity of the current code is that we must chain
2 reactive values (one in the map "GameScene.MapPlayersByKey" and one in
the snapshot store).

The new generic MapStore class can be used to listen to stores inside a map.
When the store inside the map, or the map itself is modified, the
resulting store is updated.
2021-12-14 14:47:25 +01:00

123 lines
3.9 KiB
TypeScript

import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store";
import { get, readable, writable } from "svelte/store";
/**
* Is it a Map? Is it a Store? No! It's a MapStore!
*
* The MapStore behaves just like a regular JS Map, but... it is also a regular Svelte store.
*
* As a bonus, you can also get a store on any given key of the map.
*
* For instance:
*
* const mapStore = new MapStore<string, string>();
* mapStore.getStore('foo').subscribe((value) => {
* console.log('Foo key has been written to the store. New value: ', value);
* });
* mapStore.set('foo', 'bar');
*
*
* Even better, if the items stored in map contain stores, you can directly get the store to those values:
*
* const mapStore = new MapStore<string, {
* nestedStore: Readable<string>
* }>();
*
* mapStore.getNestedStore('foo', item => item.nestedStore).subscribe((value) => {
* console.log('Foo key has been written to the store or the nested store has been updated. New value: ', value);
* });
* mapStore.set('foo', {
* nestedStore: writable('bar')
* });
* // Whenever the nested store is updated OR the 'foo' key is overwritten, the store returned by mapStore.getNestedStore
* // will be triggered.
*/
export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
private readonly store = writable(this);
private readonly storesByKey = new Map<K, Writable<V | undefined>>();
subscribe(run: Subscriber<Map<K, V>>, invalidate?: (value?: Map<K, V>) => void): Unsubscriber {
return this.store.subscribe(run, invalidate);
}
clear() {
super.clear();
this.store.set(this);
this.storesByKey.forEach((store) => {
store.set(undefined);
});
}
delete(key: K): boolean {
const result = super.delete(key);
if (result) {
this.store.set(this);
this.storesByKey.get(key)?.set(undefined);
}
return result;
}
set(key: K, value: V): this {
super.set(key, value);
this.store.set(this);
this.storesByKey.get(key)?.set(value);
return this;
}
getStore(key: K): Readable<V | undefined> {
const store = writable(this.get(key), () => {
return () => {
// No more subscribers!
this.storesByKey.delete(key);
};
});
this.storesByKey.set(key, store);
return store;
}
/**
* Returns an "inner" store inside a value stored in the map.
*/
getNestedStore<T>(key: K, accessor: (value: V) => Readable<T> | undefined): Readable<T | undefined> {
const initVal = this.get(key);
let initStore: Readable<T> | undefined;
let initStoreValue: T | undefined;
if (initVal) {
initStore = accessor(initVal);
if (initStore !== undefined) {
initStoreValue = get(initStore);
}
}
return readable(initStoreValue, (set) => {
const storeByKey = this.getStore(key);
let unsubscribeDeepStore: Unsubscriber | undefined;
const unsubscribe = storeByKey.subscribe((newMapValue) => {
if (unsubscribeDeepStore) {
unsubscribeDeepStore();
}
if (newMapValue === undefined) {
set(undefined);
} else {
const deepValueStore = accessor(newMapValue);
if (deepValueStore !== undefined) {
set(get(deepValueStore));
unsubscribeDeepStore = deepValueStore.subscribe((value) => {
set(value);
});
}
}
});
return () => {
unsubscribe();
if (unsubscribeDeepStore) {
unsubscribeDeepStore();
}
};
});
}
}