Skip to content

Commit

Permalink
feat(daemon): Add reverseLocate() and followLocatorNameChanges()
Browse files Browse the repository at this point in the history
…(merge #2195)

Adds `reverseLocate()` and `followLocatorNameChanges()` to the daemon's
directory. To match this new method, the directory's `followChanges()`
and the pet store's underlying `follow()` are renamed to
`followNameChanges()`.

The directory's `reverseLocate(locator)` and
`followLocatorNameChanges(locator)` are backed up by the pet store's
`reverseIdentify(id)` and `followIdNameChanges(id)`, respectively. The
implementation of the former is straightforward. The implementation of
the latter required the introduction of a new pubsub topic,
`idChangesTopic`, and a map from ids to `idChangesTopic` subscriptions
(one for each id).

Since the pet store is unaware of whether ids exist,
`followIdNameChanges()` will naively create a subscription that first
publishes an empty array of names, and then any subsequent names that
are added for that id. `followLocatorNameChanges()` inherits this
property.

The current implementation suffers from the limitation that locator name
change subscriptions cannot be ended. A follow-up may introduce the
ability for subscribers to cancel their subscription. Locator
subscriptions can also be a target for our future garbage collector,
which can delete subscriptions for unreachable locators. Cancelling a
locator should not end the subscription, because we consider the
liveness of a value to be orthogonal to the subscriber's interest in
changes to its names.
  • Loading branch information
rekmarks authored Apr 17, 2024
2 parents 3457c7e + 37bb657 commit d521532
Show file tree
Hide file tree
Showing 14 changed files with 573 additions and 161 deletions.
2 changes: 1 addition & 1 deletion packages/cli/demo/cat.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ const inventoryComponent = async ($parent, $end, powers) => {
$parent.insertBefore($ul, $end);

const $names = new Map();
for await (const change of makeRefIterator(E(powers).followChanges())) {
for await (const change of makeRefIterator(E(powers).followNameChanges())) {
if ('add' in change) {
const name = change.add;

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const list = async ({ directoryPath, follow, json }) =>
agent = E(agent).lookup(...directoryPath.split('.'));
}
if (follow) {
const topic = await E(agent).followNames();
const topic = await E(agent).followNameChanges();
const iterator = makeRefIterator(topic);
if (json) {
for await (const change of iterator) {
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ const makeDaemonCore = async (
has: disallowedFn,
identify: disallowedFn,
list: disallowedFn,
followChanges: disallowedFn,
followNameChanges: disallowedFn,
lookup: disallowedFn,
reverseLookup: disallowedFn,
write: disallowedFn,
Expand Down
45 changes: 38 additions & 7 deletions packages/daemon/src/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { E } from '@endo/far';
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { makeIteratorRef } from './reader-ref.js';
import { formatLocator } from './locator.js';
import { formatLocator, idFromLocator } from './locator.js';

const { quote: q } = assert;

Expand Down Expand Up @@ -97,6 +97,30 @@ export const makeDirectoryMaker = ({
return formatLocator(id, formulaType);
};

/** @type {import('./types.js').EndoDirectory['reverseLocate']} */
const reverseLocate = async locator => {
const id = idFromLocator(locator);
return petStore.reverseIdentify(id);
};

/** @type {import('./types.js').EndoDirectory['followLocatorNameChanges']} */
const followLocatorNameChanges = async function* followLocatorNameChanges(
locator,
) {
const id = idFromLocator(locator);
for await (const idNameChange of petStore.followIdNameChanges(id)) {
/** @type {any} */
const locatorNameChange = {
...idNameChange,
...(Object.hasOwn(idNameChange, 'add')
? { add: locator }
: { remove: locator }),
};

yield /** @type {import('./types.js').LocatorNameChange} */ locatorNameChange;
}
};

/** @type {import('./types.js').EndoDirectory['list']} */
const list = async (...petNamePath) => {
if (petNamePath.length === 0) {
Expand All @@ -123,16 +147,18 @@ export const makeDirectoryMaker = ({
return harden(Array.from(identities).sort());
};

/** @type {import('./types.js').EndoDirectory['followChanges']} */
const followChanges = async function* followChanges(...petNamePath) {
/** @type {import('./types.js').EndoDirectory['followNameChanges']} */
const followNameChanges = async function* followNameChanges(
...petNamePath
) {
if (petNamePath.length === 0) {
yield* petStore.follow();
yield* petStore.followNameChanges();
return;
}
const hub = /** @type {import('./types.js').NameHub} */ (
await lookup(...petNamePath)
);
yield* hub.followChanges();
yield* hub.followNameChanges();
};

/** @type {import('./types.js').EndoDirectory['remove']} */
Expand Down Expand Up @@ -206,9 +232,11 @@ export const makeDirectoryMaker = ({
has,
identify,
locate,
reverseLocate,
followLocatorNameChanges,
list,
listIdentifiers,
followChanges,
followNameChanges,
lookup,
reverseLookup,
write,
Expand Down Expand Up @@ -236,7 +264,10 @@ export const makeDirectoryMaker = ({
M.interface('EndoDirectory', {}, { defaultGuards: 'passable' }),
{
...directory,
followChanges: () => makeIteratorRef(directory.followChanges()),
/** @param {string} locator */
followLocatorNameChanges: locator =>
makeIteratorRef(directory.followLocatorNameChanges(locator)),
followNameChanges: () => makeIteratorRef(directory.followNameChanges()),
},
);
};
Expand Down
13 changes: 10 additions & 3 deletions packages/daemon/src/guest.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ export const makeGuestMaker = ({ provide, makeMailbox, makeDirectoryNode }) => {
has,
identify,
locate,
reverseLocate,
list,
listIdentifiers,
followChanges,
followNameChanges,
followLocatorNameChanges,
lookup,
reverseLookup,
write,
Expand Down Expand Up @@ -85,9 +87,11 @@ export const makeGuestMaker = ({ provide, makeMailbox, makeDirectoryNode }) => {
identify,
reverseIdentify,
locate,
reverseLocate,
list,
listIdentifiers,
followChanges,
followLocatorNameChanges,
followNameChanges,
lookup,
reverseLookup,
write,
Expand All @@ -113,8 +117,11 @@ export const makeGuestMaker = ({ provide, makeMailbox, makeDirectoryNode }) => {
M.interface('EndoGuest', {}, { defaultGuards: 'passable' }),
{
...guest,
followChanges: () => makeIteratorRef(guest.followChanges()),
/** @param {string} locator */
followLocatorNameChanges: locator =>
makeIteratorRef(guest.followLocatorNameChanges(locator)),
followMessages: () => makeIteratorRef(guest.followMessages()),
followNameChanges: () => makeIteratorRef(guest.followNameChanges()),
},
);
};
Expand Down
13 changes: 10 additions & 3 deletions packages/daemon/src/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,11 @@ export const makeHostMaker = ({
identify,
lookup,
locate,
reverseLocate,
list,
listIdentifiers,
followChanges,
followNameChanges,
followLocatorNameChanges,
reverseLookup,
write,
remove,
Expand All @@ -498,9 +500,11 @@ export const makeHostMaker = ({
identify,
reverseIdentify,
locate,
reverseLocate,
list,
listIdentifiers,
followChanges,
followLocatorNameChanges,
followNameChanges,
lookup,
reverseLookup,
write,
Expand Down Expand Up @@ -538,8 +542,11 @@ export const makeHostMaker = ({
M.interface('EndoHost', {}, { defaultGuards: 'passable' }),
{
...host,
followChanges: () => makeIteratorRef(host.followChanges()),
/** @param {string} locator */
followLocatorNameChanges: locator =>
makeIteratorRef(host.followLocatorNameChanges(locator)),
followMessages: () => makeIteratorRef(host.followMessages()),
followNameChanges: () => makeIteratorRef(host.followNameChanges()),
},
);

Expand Down
36 changes: 23 additions & 13 deletions packages/daemon/src/locator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check

import { parseId, isValidNumber } from './formula-identifier.js';
import { formatId, isValidNumber, parseId } from './formula-identifier.js';
import { isValidFormulaType } from './formula-type.js';

const { quote: q } = assert;
Expand Down Expand Up @@ -28,48 +28,50 @@ const isValidLocatorType = allegedType =>
*/
const assertValidLocatorType = allegedType => {
if (!isValidLocatorType(allegedType)) {
assert.Fail`Unrecognized locator type ${q(allegedType)}`;
throw assert.error(`Unrecognized locator type ${q(allegedType)}`);
}
};

/** @param {string} allegedLocator */
/**
* @param {string} allegedLocator
* @returns {{ formulaType: string, node: string, number: string }}
*/
export const parseLocator = allegedLocator => {
const errorPrefix = `Invalid locator ${q(allegedLocator)}:`;

if (!URL.canParse(allegedLocator)) {
assert.Fail`${errorPrefix} Invalid URL.`;
throw assert.error(`${errorPrefix} Invalid URL.`);
}
const url = new URL(allegedLocator);

if (!allegedLocator.startsWith('endo://')) {
assert.Fail`${errorPrefix} Invalid protocol.`;
throw assert.error(`${errorPrefix} Invalid protocol.`);
}

const node = url.host;
if (!isValidNumber(node)) {
assert.Fail`${errorPrefix} Invalid node identifier.`;
throw assert.error(`${errorPrefix} Invalid node identifier.`);
}

if (
url.searchParams.size !== 2 ||
!url.searchParams.has('id') ||
!url.searchParams.has('type')
) {
assert.Fail`${errorPrefix} Invalid search params.`;
throw assert.error(`${errorPrefix} Invalid search params.`);
}

const id = url.searchParams.get('id');
if (id === null || !isValidNumber(id)) {
assert.Fail`${errorPrefix} Invalid id.`;
const number = url.searchParams.get('id');
if (number === null || !isValidNumber(number)) {
throw assert.error(`${errorPrefix} Invalid id.`);
}

const formulaType = url.searchParams.get('type');
if (formulaType === null || !isValidLocatorType(formulaType)) {
assert.Fail`${errorPrefix} Invalid type.`;
throw assert.error(`${errorPrefix} Invalid type.`);
}

/** @type {{ formulaType: string, node: string, id: string }} */
return { formulaType, node, id };
return { formulaType, node, number };
};

/** @param {string} allegedLocator */
Expand All @@ -94,3 +96,11 @@ export const formatLocator = (id, formulaType) => {

return url.toString();
};

/**
* @param {string} locator
*/
export const idFromLocator = locator => {
const { number, node } = parseLocator(locator);
return formatId({ number, node });
};
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,

getAllFor: 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
28 changes: 24 additions & 4 deletions packages/daemon/src/pet-sitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,35 @@ export const makePetSitter = (petStore, specialNames) => {
const list = () =>
harden([...Object.keys(specialNames).sort(), ...petStore.list()]);

/** @type {import('./types.js').PetStore['follow']} */
const follow = async function* currentAndSubsequentNames() {
/** @type {import('./types.js').PetStore['followNameChanges']} */
const followNameChanges = async function* currentAndSubsequentNames() {
for (const name of Object.keys(specialNames).sort()) {
const idRecord = idRecordForName(name);
yield /** @type {{ add: string, value: import('./types.js').IdRecord }} */ ({
add: name,
value: idRecord,
});
}
yield* petStore.follow();
yield* petStore.followNameChanges();
};

/** @type {import('./types.js').PetStore['followIdNameChanges']} */
const followIdNameChanges = async function* currentAndSubsequentIds(id) {
const subscription = petStore.followIdNameChanges(id);

const idSpecialNames = Object.entries(specialNames)
.filter(([_, specialId]) => specialId === id)
.map(([specialName, _]) => specialName);

// The first published event contains the existing names for the id, if any.
const { value: existingNames } = await subscription.next();
existingNames?.names?.unshift(...idSpecialNames);
existingNames?.names?.sort();
yield /** @type {import('./types.js').PetStoreIdNameChange} */ (
existingNames
);

yield* subscription;
};

/** @type {import('./types.js').PetStore['reverseIdentify']} */
Expand All @@ -77,7 +96,8 @@ export const makePetSitter = (petStore, specialNames) => {
identifyLocal,
reverseIdentify,
list,
follow,
followIdNameChanges,
followNameChanges,
write,
remove,
rename,
Expand Down
Loading

0 comments on commit d521532

Please sign in to comment.