diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index f66407a7ca..8db27f75ff 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -14,6 +14,7 @@ import { assertPetName } from './pet-name.js'; import { makeContextMaker } from './context.js'; import { parseFormulaIdentifier } from './formula-identifier.js'; import { makeMutex } from './mutex.js'; +import { makeWeakMultimap } from './weak-multimap.js'; const delay = async (ms, cancelled) => { // Do not attempt to set up a timer if already cancelled. @@ -96,12 +97,15 @@ const makeDaemonCore = async ( * @type {Map} */ const controllerForFormulaIdentifier = new Map(); + /** * Reverse look-up, for answering "what is my name for this near or far * reference", and not for "what is my name for this promise". - * @type {WeakMap} + * @type {import('./types.js').WeakMultimap, string>} */ - const formulaIdentifierForRef = new WeakMap(); + const formulaIdentifierForRef = makeWeakMultimap(); + + /** @type {import('./types.js').WeakMultimap, string>['get']} */ const getFormulaIdentifierForRef = ref => formulaIdentifierForRef.get(ref); /** @@ -586,7 +590,7 @@ const makeDaemonCore = async ( context, external: E.get(partial).external.then(value => { if (typeof value === 'object' && value !== null) { - formulaIdentifierForRef.set(value, formulaIdentifier); + formulaIdentifierForRef.add(value, formulaIdentifier); } return value; }), @@ -669,7 +673,7 @@ const makeDaemonCore = async ( // Release the value to the public only after ensuring // we can reverse-lookup its nonce. if (typeof value === 'object' && value !== null) { - formulaIdentifierForRef.set(value, formulaIdentifier); + formulaIdentifierForRef.add(value, formulaIdentifier); } return value; }); diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index 90d48ebee0..05c4dbae08 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -359,7 +359,7 @@ export interface Mail { remove: PetStore['remove']; list: PetStore['list']; identifyLocal: PetStore['identifyLocal']; - reverseLookup: PetStore['reverseLookup']; + reverseLookup(value: unknown): Array; // Extended methods: lookup(...petNamePath: string[]): Promise; listSpecial(): Array; @@ -637,8 +637,44 @@ export type DaemonicPowers = { control: DaemonicControlPowers; }; -type Mutex = { +export type Mutex = { lock: () => Promise; unlock: () => void; enqueue: (asyncFn?: () => Promise) => Promise; }; + +/** + * A multimap backed by a WeakMap. Keys must be objects. + */ +export type WeakMultimap = { + /** + * @param key - The key to add a value for. + * @param value - The value to add. + */ + add(key: K, value: V): void; + + /** + * @param key - The key whose value to delete. + * @param value - The value to delete. + * @returns `true` if the key was found and the value was deleted, `false` otherwise. + */ + delete(key: K, value: V): boolean; + + /** + * @param key - The key whose values to delete + * @returns `true` if the key was found and its values were deleted, `false` otherwise. + */ + deleteAll(key: K): boolean; + + /** + * @param key - The key whose first value to retrieve + * @returns The first value associated with the key. + */ + get(key: K): V | undefined; + + /** + * @param key - The key whose values to retrieve. + * @returns An array of all values associated with the key. + */ + getAll(key: K): V[]; +}; diff --git a/packages/daemon/src/weak-multimap.js b/packages/daemon/src/weak-multimap.js new file mode 100644 index 0000000000..5051a03685 --- /dev/null +++ b/packages/daemon/src/weak-multimap.js @@ -0,0 +1,35 @@ +/** + * @returns {import('./types.js').WeakMultimap} + */ +export const makeWeakMultimap = () => { + /** @type {WeakMap>} */ + const map = new WeakMap(); + return { + add: (ref, formulaIdentifier) => { + let set = map.get(ref); + if (set === undefined) { + set = new Set(); + map.set(ref, set); + } + set.add(formulaIdentifier); + }, + + delete: (ref, formulaIdentifier) => { + const set = map.get(ref); + if (set !== undefined) { + const result = set.delete(formulaIdentifier); + if (set.size === 0) { + map.delete(ref); + } + return result; + } + return false; + }, + + deleteAll: ref => map.delete(ref), + + get: ref => map.get(ref)?.keys().next().value, + + getAll: ref => Array.from(map.get(ref) ?? []), + }; +}; diff --git a/packages/daemon/test/test-weak-multimap.js b/packages/daemon/test/test-weak-multimap.js new file mode 100644 index 0000000000..e985d7254b --- /dev/null +++ b/packages/daemon/test/test-weak-multimap.js @@ -0,0 +1,82 @@ +import test from 'ava'; +import { makeWeakMultimap } from '../src/weak-multimap.js'; + +test('add and get', t => { + const multimap = makeWeakMultimap(); + const ref = {}; + const value = 'foo'; + + multimap.add(ref, value); + t.is(multimap.get(ref), value); + + // Adding a value for a key should be idempotent. + multimap.add(ref, value); + t.is(multimap.get(ref), value); +}); + +test('add and get with multiple refs', t => { + const multimap = makeWeakMultimap(); + const ref1 = {}; + const ref2 = {}; + const value1 = 'foo'; + const value2 = 'bar'; + + multimap.add(ref1, value1); + multimap.add(ref1, value2); + multimap.add(ref2, value1); + + t.is(multimap.get(ref1), value1); + t.deepEqual(multimap.getAll(ref1), [value1, value2]); + t.is(multimap.get(ref2), value1); + t.deepEqual(multimap.getAll(ref2), [value1]); +}); + +test('getAll', t => { + const multimap = makeWeakMultimap(); + const ref = {}; + const value1 = 'foo'; + const value2 = 'bar'; + + multimap.add(ref, value1); + multimap.add(ref, value2); + t.deepEqual(multimap.getAll(ref), [value1, value2]); + + // Adding a value for a key should be idempotent. + multimap.add(ref, value1); + multimap.add(ref, value2); + t.deepEqual(multimap.getAll(ref), [value1, value2]); +}); + +test('delete', t => { + const multimap = makeWeakMultimap(); + const ref = {}; + const value = 'foo'; + + multimap.add(ref, value); + + t.is(multimap.get(ref), value); + t.is(multimap.delete(ref, value), true); + t.is(multimap.get(ref), undefined); + + // Deleting should be idempotent. + t.is(multimap.delete(ref, value), false); + t.is(multimap.get(ref), undefined); +}); + +test('deleteAll', t => { + const multimap = makeWeakMultimap(); + const ref = {}; + const value1 = 'foo'; + const value2 = 'bar'; + + multimap.add(ref, value1); + multimap.add(ref, value2); + + t.deepEqual(multimap.getAll(ref), [value1, value2]); + t.is(multimap.deleteAll(ref), true); + t.is(multimap.get(ref), undefined); + + // Deleting should be idempotent. + t.is(multimap.deleteAll(ref), false); + t.is(multimap.get(ref), undefined); +});