Skip to content

Commit

Permalink
feat(daemon): Add partial followLocatorNames()
Browse files Browse the repository at this point in the history
Adds an initial implementation of `followLocatorNames()`, which
publishes the extant names for a specific id, if any. A test is added
for the same.
  • Loading branch information
rekmarks committed Mar 30, 2024
1 parent dfea431 commit 1929957
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 19 deletions.
20 changes: 20 additions & 0 deletions packages/daemon/src/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -216,6 +232,7 @@ export const makeDirectoryMaker = ({
identify,
locate,
reverseLocate,
followLocatorNames,
list,
listIdentifiers,
followNames,
Expand Down Expand Up @@ -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()),
},
);
Expand Down
7 changes: 6 additions & 1 deletion packages/daemon/src/guest.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const makeGuestMaker = ({
list,
listIdentifiers,
followNames,
followLocatorNames,
lookup,
reverseLookup,
write,
Expand Down Expand Up @@ -100,6 +101,7 @@ export const makeGuestMaker = ({
reverseLocate,
list,
listIdentifiers,
followLocatorNames,
followNames,
lookup,
reverseLookup,
Expand All @@ -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({
Expand Down
7 changes: 6 additions & 1 deletion packages/daemon/src/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export const makeHostMaker = ({
list,
listIdentifiers,
followNames,
followLocatorNames,
reverseLookup,
write,
remove,
Expand Down Expand Up @@ -491,6 +492,7 @@ export const makeHostMaker = ({
reverseLocate,
list,
listIdentifiers,
followLocatorNames,
followNames,
lookup,
reverseLookup,
Expand Down Expand Up @@ -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 });
Expand Down
4 changes: 4 additions & 0 deletions packages/daemon/src/multimap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};

Expand Down Expand Up @@ -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),
Expand Down
21 changes: 21 additions & 0 deletions packages/daemon/src/pet-sitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -69,6 +89,7 @@ export const makePetSitter = (petStore, specialNames) => {
identifyLocal,
reverseIdentify,
list,
followId,
followNames,
write,
remove,
Expand Down
44 changes: 33 additions & 11 deletions packages/daemon/src/pet-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ export const makePetStoreMaker = (filePowers, locator) => {
const makePetStoreAtPath = async (petNameDirectoryPath, assertValidName) => {
/** @type {import('./types.js').BidirectionalMultimap<string, string>} */
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<string, ReturnType<typeof makeIdChangeTopic>>} */
const idsToTopics = new Map();

/** @param {string} petName */
const read = async petName => {
const petNamePath = filePowers.joinPath(petNameDirectoryPath, petName);
Expand Down Expand Up @@ -77,18 +82,17 @@ 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),
});
};

/**
* @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)}`);
Expand All @@ -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']} */
Expand Down Expand Up @@ -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
Expand All @@ -186,6 +207,7 @@ export const makePetStoreMaker = (filePowers, locator) => {
identifyLocal,
reverseIdentify,
list,
followId,
followNames,
write,
remove,
Expand Down
40 changes: 40 additions & 0 deletions packages/daemon/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PetStoreNameDiff>;

export type IdChangesTopic = Topic<PetStoreIdDiff>;

export interface PetStore {
has(petName: string): boolean;
identifyLocal(petName: string): string | undefined;
list(): Array<string>;
/**
* Subscribe to all name changes. First publishes all existing names in alphabetical order.
* Then publishes diffs as names are added and removed.
*/
followNames(): AsyncGenerator<PetStoreNameDiff, undefined, undefined>;
/**
* 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<PetStoreIdDiff, undefined, undefined>;
write(petName: string, id: string): Promise<void>;
remove(petName: string): Promise<void>;
rename(fromPetName: string, toPetName: string): Promise<void>;
Expand All @@ -386,11 +404,21 @@ export interface PetStore {
reverseIdentify(id: string): Array<string>;
}

/**
* `add` and `remove` are locators.
*/
export type LocatorDiff =
| { add: string; names: string[] }
| { remove: string; names?: string[] };

export interface NameHub {
has(...petNamePath: string[]): Promise<boolean>;
identify(...petNamePath: string[]): Promise<string | undefined>;
locate(...petNamePath: string[]): Promise<string | undefined>;
reverseLocate(locator: string): Promise<string[]>;
followLocatorNames(
locator: string,
): AsyncGenerator<LocatorDiff, undefined, undefined>;
list(...petNamePath: string[]): Promise<Array<string>>;
listIdentifiers(...petNamePath: string[]): Promise<Array<string>>;
followNames(
Expand Down Expand Up @@ -902,6 +930,12 @@ export type Multimap<K, V> = {
* @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;
};

/**
Expand Down Expand Up @@ -930,6 +964,12 @@ export type BidirectionalMultimap<K, V> = {
*/
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.
Expand Down
Loading

0 comments on commit 1929957

Please sign in to comment.