From 1929957b7ce4144acff0992104e818b20ddc9ea7 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 29 Mar 2024 18:04:49 -0700 Subject: [PATCH] feat(daemon): Add partial followLocatorNames() Adds an initial implementation of `followLocatorNames()`, which publishes the extant names for a specific id, if any. A test is added for the same. --- packages/daemon/src/directory.js | 20 ++++++++++++ packages/daemon/src/guest.js | 7 ++++- packages/daemon/src/host.js | 7 ++++- packages/daemon/src/multimap.js | 4 +++ packages/daemon/src/pet-sitter.js | 21 +++++++++++++ packages/daemon/src/pet-store.js | 44 ++++++++++++++++++++------- packages/daemon/src/types.d.ts | 40 ++++++++++++++++++++++++ packages/daemon/test/test-endo.js | 26 ++++++++++++---- packages/daemon/test/test-multimap.js | 33 ++++++++++++++++++++ 9 files changed, 183 insertions(+), 19 deletions(-) diff --git a/packages/daemon/src/directory.js b/packages/daemon/src/directory.js index 679a800dc6..bc7b80b692 100644 --- a/packages/daemon/src/directory.js +++ b/packages/daemon/src/directory.js @@ -106,6 +106,22 @@ export const makeDirectoryMaker = ({ return petStore.reverseIdentify(id); }; + /** @type {import('./types.js').EndoDirectory['followLocatorNames']} */ + const followLocatorNames = async function* followLocatorNames(locator) { + const id = idFromLocator(locator); + for await (const idDiff of petStore.followId(id)) { + /** @type {any} */ + const locatorDiff = { + ...idDiff, + ...(Object.hasOwn(idDiff, 'add') + ? { add: locator } + : { remove: locator }), + }; + + yield /** @type {import('./types.js').LocatorDiff} */ locatorDiff; + } + }; + /** @type {import('./types.js').EndoDirectory['list']} */ const list = async (...petNamePath) => { if (petNamePath.length === 0) { @@ -216,6 +232,7 @@ export const makeDirectoryMaker = ({ identify, locate, reverseLocate, + followLocatorNames, list, listIdentifiers, followNames, @@ -248,6 +265,9 @@ export const makeDirectoryMaker = ({ M.interface('EndoDirectory', {}, { defaultGuards: 'passable' }), { ...directory, + /** @param {string} locator */ + followLocatorNames: locator => + makeIteratorRef(directory.followLocatorNames(locator)), followNames: () => makeIteratorRef(directory.followNames()), }, ); diff --git a/packages/daemon/src/guest.js b/packages/daemon/src/guest.js index 05d5527080..d84566b8e9 100644 --- a/packages/daemon/src/guest.js +++ b/packages/daemon/src/guest.js @@ -70,6 +70,7 @@ export const makeGuestMaker = ({ list, listIdentifiers, followNames, + followLocatorNames, lookup, reverseLookup, write, @@ -100,6 +101,7 @@ export const makeGuestMaker = ({ reverseLocate, list, listIdentifiers, + followLocatorNames, followNames, lookup, reverseLookup, @@ -124,8 +126,11 @@ export const makeGuestMaker = ({ M.interface('EndoGuest', {}, { defaultGuards: 'passable' }), { ...guest, - followNames: () => makeIteratorRef(guest.followNames()), + /** @param {string} locator */ + followLocatorNames: locator => + makeIteratorRef(guest.followLocatorNames(locator)), followMessages: () => makeIteratorRef(guest.followMessages()), + followNames: () => makeIteratorRef(guest.followNames()), }, ); const internal = harden({ diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index 3d8601c8ae..ff87fdaf5a 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -462,6 +462,7 @@ export const makeHostMaker = ({ list, listIdentifiers, followNames, + followLocatorNames, reverseLookup, write, remove, @@ -491,6 +492,7 @@ export const makeHostMaker = ({ reverseLocate, list, listIdentifiers, + followLocatorNames, followNames, lookup, reverseLookup, @@ -527,8 +529,11 @@ export const makeHostMaker = ({ M.interface('EndoHost', {}, { defaultGuards: 'passable' }), { ...host, - followNames: () => makeIteratorRef(host.followNames()), + /** @param {string} locator */ + followLocatorNames: locator => + makeIteratorRef(host.followLocatorNames(locator)), followMessages: () => makeIteratorRef(host.followMessages()), + followNames: () => makeIteratorRef(host.followNames()), }, ); const internal = harden({ receive, respond, petStore }); diff --git a/packages/daemon/src/multimap.js b/packages/daemon/src/multimap.js index 8f6d976010..2d667cadb4 100644 --- a/packages/daemon/src/multimap.js +++ b/packages/daemon/src/multimap.js @@ -36,6 +36,8 @@ const internalMakeMultimap = mapConstructor => { get: key => map.get(key)?.keys().next().value, getAll: key => Array.from(map.get(key) ?? []), + + has: key => map.has(key), }; }; @@ -92,6 +94,8 @@ export const makeBidirectionalMultimap = () => { return keyForValues.deleteAll(key); }, + has: key => keyForValues.has(key), + hasValue: value => valueForKey.has(value), get: key => keyForValues.get(key), diff --git a/packages/daemon/src/pet-sitter.js b/packages/daemon/src/pet-sitter.js index f387fc7386..2c6f16179b 100644 --- a/packages/daemon/src/pet-sitter.js +++ b/packages/daemon/src/pet-sitter.js @@ -51,6 +51,26 @@ export const makePetSitter = (petStore, specialNames) => { yield* petStore.followNames(); }; + /** @type {import('./types.js').PetStore['followId']} */ + const followId = async function* currentAndSubsequentIds(id) { + const subscription = petStore.followId(id); + + const idSpecialName = Object.entries(specialNames) + .filter(([_, specialId]) => specialId === id) + .map(([specialName, _]) => specialName) + .shift(); + + if (typeof idSpecialName === 'string') { + // The first published event contains the existing names for the id, if any. + const { value: existingNames } = await subscription.next(); + existingNames?.names?.push(idSpecialName); + existingNames?.names?.sort(); + yield /** @type {import('./types.js').PetStoreIdDiff} */ (existingNames); + } + + yield* subscription; + }; + /** @type {import('./types.js').PetStore['reverseIdentify']} */ const reverseIdentify = id => { const names = Array.from(petStore.reverseIdentify(id)); @@ -69,6 +89,7 @@ export const makePetSitter = (petStore, specialNames) => { identifyLocal, reverseIdentify, list, + followId, followNames, write, remove, diff --git a/packages/daemon/src/pet-store.js b/packages/daemon/src/pet-store.js index 99787dc70e..6d7a1837a7 100644 --- a/packages/daemon/src/pet-store.js +++ b/packages/daemon/src/pet-store.js @@ -19,9 +19,14 @@ export const makePetStoreMaker = (filePowers, locator) => { const makePetStoreAtPath = async (petNameDirectoryPath, assertValidName) => { /** @type {import('./types.js').BidirectionalMultimap} */ const idsToPetNames = makeBidirectionalMultimap(); - /** @type {import('./types.js').Topic<({ add: string, value: import('./types.js').IdRecord } | { remove: string })>} */ + /** @type {import('./types.js').NameChangesTopic} */ const nameChangesTopic = makeChangeTopic(); + /** @returns {import('./types.js').IdChangesTopic} */ + const makeIdChangeTopic = () => makeChangeTopic(); + /** @type {Map>} */ + const idsToTopics = new Map(); + /** @param {string} petName */ const read = async petName => { const petNamePath = filePowers.joinPath(petNameDirectoryPath, petName); @@ -77,10 +82,9 @@ export const makePetStoreMaker = (filePowers, locator) => { const petNamePath = filePowers.joinPath(petNameDirectoryPath, petName); const petNameText = `${formulaIdentifier}\n`; await filePowers.writeFileText(petNamePath, petNameText); - const formulaIdentifierRecord = parseId(formulaIdentifier); nameChangesTopic.publisher.next({ add: petName, - value: formulaIdentifierRecord, + value: parseId(formulaIdentifier), }); }; @@ -88,7 +92,7 @@ export const makePetStoreMaker = (filePowers, locator) => { * @param {string} petName * @returns {import('./types.js').IdRecord} */ - const formulaIdentifierRecordForName = petName => { + const idRecordForName = petName => { const formulaIdentifier = idsToPetNames.getKey(petName); if (formulaIdentifier === undefined) { throw new Error(`Formula does not exist for pet name ${q(petName)}`); @@ -101,15 +105,33 @@ export const makePetStoreMaker = (filePowers, locator) => { /** @type {import('./types.js').PetStore['followNames']} */ const followNames = async function* currentAndSubsequentNames() { - const changes = nameChangesTopic.subscribe(); + const subscription = nameChangesTopic.subscribe(); for (const name of idsToPetNames.getAll().sort()) { - const formulaIdentifierRecord = formulaIdentifierRecordForName(name); - yield /** @type {{ add: string, value: import('./types.js').IdRecord }} */ ({ + yield /** @type {import('./types.js').PetStoreNameDiff} */ ({ add: name, - value: formulaIdentifierRecord, + value: idRecordForName(name), }); } - yield* changes; + yield* subscription; + }; + + /** @type {import('./types.js').PetStore['followId']} */ + const followId = async function* currentAndSubsequentIds(id) { + if (!idsToTopics.has(id)) { + idsToTopics.set(id, makeIdChangeTopic()); + } + const idTopic = /** @type {import('./types.js').IdChangesTopic} */ ( + idsToTopics.get(id) + ); + const subscription = idTopic.subscribe(); + + const existingNames = idsToPetNames.getAllFor(id).sort(); + yield /** @type {import('./types.js').PetStoreIdDiff} */ ({ + add: parseId(id), + names: existingNames, + }); + + yield* subscription; }; /** @type {import('./types.js').PetStore['remove']} */ @@ -162,10 +184,9 @@ export const makePetStoreMaker = (filePowers, locator) => { // Update the mapping for the pet name. idsToPetNames.add(formulaIdentifier, toName); - const formulaIdentifierRecord = parseId(formulaIdentifier); nameChangesTopic.publisher.next({ add: toName, - value: formulaIdentifierRecord, + value: parseId(formulaIdentifier), }); nameChangesTopic.publisher.next({ remove: fromName }); // TODO consider retaining a backlog of overwritten names for recovery @@ -186,6 +207,7 @@ export const makePetStoreMaker = (filePowers, locator) => { identifyLocal, reverseIdentify, list, + followId, followNames, write, remove, diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index ebaef6734f..b2eb67d428 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -371,11 +371,29 @@ export type PetStoreNameDiff = | { add: string; value: IdRecord } | { remove: string }; +export type PetStoreIdDiff = + | { add: IdRecord; names: string[] } + | { remove: IdRecord; names?: string[] }; + +export type NameChangesTopic = Topic; + +export type IdChangesTopic = Topic; + export interface PetStore { has(petName: string): boolean; identifyLocal(petName: string): string | undefined; list(): Array; + /** + * Subscribe to all name changes. First publishes all existing names in alphabetical order. + * Then publishes diffs as names are added and removed. + */ followNames(): AsyncGenerator; + /** + * Subscribe to name changes for the specified id. First publishes the existing names for the id. + * Then publishes diffs as names are added and removed, or if the id is itself removed. + * @throws If attempting to follow an id with no names. + */ + followId(id: string): AsyncGenerator; write(petName: string, id: string): Promise; remove(petName: string): Promise; rename(fromPetName: string, toPetName: string): Promise; @@ -386,11 +404,21 @@ export interface PetStore { reverseIdentify(id: string): Array; } +/** + * `add` and `remove` are locators. + */ +export type LocatorDiff = + | { add: string; names: string[] } + | { remove: string; names?: string[] }; + export interface NameHub { has(...petNamePath: string[]): Promise; identify(...petNamePath: string[]): Promise; locate(...petNamePath: string[]): Promise; reverseLocate(locator: string): Promise; + followLocatorNames( + locator: string, + ): AsyncGenerator; list(...petNamePath: string[]): Promise>; listIdentifiers(...petNamePath: string[]): Promise>; followNames( @@ -902,6 +930,12 @@ export type Multimap = { * @returns An array of all values associated with the key. */ getAll(key: K): V[]; + + /** + * @param key - The key whose presence to check for. + * @returns `true` if the key is present and `false` otherwise. + */ + has(key: K): boolean; }; /** @@ -930,6 +964,12 @@ export type BidirectionalMultimap = { */ deleteAll(key: K): boolean; + /** + * @param key - The key whose presence to check for. + * @returns `true` if the key is present and `false` otherwise. + */ + has(key: K): boolean; + /** * @param value - The value whose presence to check for. * @returns `true` if the value is present and `false` otherwise. diff --git a/packages/daemon/test/test-endo.js b/packages/daemon/test/test-endo.js index 05a39f3e83..e3cad1a110 100644 --- a/packages/daemon/test/test-endo.js +++ b/packages/daemon/test/test-endo.js @@ -33,7 +33,7 @@ const { raw } = String; const dirname = url.fileURLToPath(new URL('..', import.meta.url)).toString(); /** - * @param {ReturnType} asyncIterator - The iterator to take from. + * @param {AsyncIterator} asyncIterator - The iterator to take from. * @param {number} count - The number of values to retrieve. */ const takeCount = async (asyncIterator, count) => { @@ -590,7 +590,7 @@ test('guest facet receives a message for host', async t => { ); }); -test('name changes subscription first publishes existing names', async t => { +test('followNames first publishes existing names', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); @@ -601,7 +601,7 @@ test('name changes subscription first publishes existing names', async t => { t.deepEqual(values.map(value => value.add).sort(), [...existingNames].sort()); }); -test('name changes subscription publishes new names', async t => { +test('followNames publishes new names', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); @@ -613,7 +613,7 @@ test('name changes subscription publishes new names', async t => { t.is(value.add, 'ten'); }); -test('name changes subscription publishes removed names', async t => { +test('followNames publishes removed names', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); @@ -627,7 +627,7 @@ test('name changes subscription publishes removed names', async t => { t.is(value.remove, 'ten'); }); -test('name changes subscription publishes renamed names', async t => { +test('followNames publishes renamed names', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); @@ -644,7 +644,7 @@ test('name changes subscription publishes renamed names', async t => { t.is(value.remove, 'ten'); }); -test('name changes subscription does not notify of redundant pet store writes', async t => { +test('followNames does not notify of redundant pet store writes', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); @@ -663,6 +663,20 @@ test('name changes subscription does not notify of redundant pet store writes', t.is(value.add, 'eleven'); }); +test('followLocatorNames first publishes existing names', async t => { + const { cancelled, locator } = await prepareLocator(t); + const { host } = await makeHost(locator, cancelled); + + await E(host).evaluate('MAIN', '10', [], [], 'ten'); + + const tenLocator = await E(host).locate('ten'); + const tenLocatorSub = makeRefIterator( + await E(host).followLocatorNames(tenLocator), + ); + const { value } = await tenLocatorSub.next(); + t.deepEqual(value, { add: tenLocator, names: ['ten'] }); +}); + test('direct cancellation', async t => { const { cancelled, locator } = await prepareLocator(t); const { host } = await makeHost(locator, cancelled); diff --git a/packages/daemon/test/test-multimap.js b/packages/daemon/test/test-multimap.js index cb2b8e5207..bf59485cc3 100644 --- a/packages/daemon/test/test-multimap.js +++ b/packages/daemon/test/test-multimap.js @@ -59,6 +59,23 @@ import { t.deepEqual(multimap.getAll(key2), []); }); + test(`${mapName}: has`, t => { + const multimap = multimapConstructor(); + const key1 = {}; + const key2 = {}; + const key3 = {}; + const value1 = 'foo'; + const value2 = 'bar'; + + multimap.add(key1, value1); + multimap.add(key1, value2); + multimap.add(key2, value1); + + t.is(multimap.has(key1), true); + t.is(multimap.has(key2), true); + t.is(multimap.has(key3), false); + }); + test(`${mapName}: delete`, t => { const multimap = multimapConstructor(); const key = {}; @@ -190,6 +207,22 @@ test('multi-bimap: getKey', t => { t.is(bimap.getKey(value3), undefined); }); +test('multi-bimap: has', t => { + const bimap = makeBidirectionalMultimap(); + const key1 = 'foo'; + const key2 = 'bar'; + const key3 = 'baz'; + const value1 = {}; + const value2 = {}; + + bimap.add(key1, value1); + bimap.add(key2, value2); + + t.is(bimap.has(key1), true); + t.is(bimap.has(key2), true); + t.is(bimap.has(key3), false); +}); + test('multi-bimap: hasValue', t => { const bimap = makeBidirectionalMultimap(); const key1 = 'foo';