Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions packages/daemon/src/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,12 @@ const makeDaemonCore = async (
['bundle', formula.bundle],
];
case 'peer':
return [['networks', formula.networks]];
return /** @type {Array<[string, FormulaIdentifier]>} */ ([
['networks', formula.networks],
...(formula.syncedStore === undefined
? []
: [['syncedStore', formula.syncedStore]]),
]);
case 'handle':
return [['agent', formula.agent]];
case 'mail-hub':
Expand Down Expand Up @@ -2951,6 +2956,28 @@ const makeDaemonCore = async (
return /** @type {FormulaIdentifier} */ (peerId);
};

/** @type {DaemonCore['setPeerSyncedStore']} */
const setPeerSyncedStore = async (peerId, syncedStoreId) => {
const peerFormula = await getFormulaForId(peerId);
if (peerFormula.type !== 'peer') {
throw makeError(X`Formula ${q(peerId)} is not a peer`);
}
if (peerFormula.syncedStore === syncedStoreId) {
return;
}
/** @type {PeerFormula} */
const updatedFormula = harden({
...peerFormula,
syncedStore: syncedStoreId,
});
const { number: peerNumber } = parseId(peerId);
await persistencePowers.writeFormula(peerNumber, updatedFormula);
await withFormulaGraphLock(async () => {
formulaForId.set(peerId, updatedFormula);
formulaGraph.onFormulaAdded(peerId, updatedFormula);
});
};

/** @type {DaemonCore['cancelValue']} */
const cancelValue = async (id, reason) => {
// Wait for any in-flight graph operation (formulation, collection)
Expand Down Expand Up @@ -4483,11 +4510,15 @@ const makeDaemonCore = async (
/** @type {FormulaNumber} */ ('0'.repeat(64)),
peerId, // store dependency (peer keeps alive)
);
await setPeerSyncedStore(peerId, syncedStoreId);

// Write the guest handle locator into the synced store.
// Write the guest handle locator into a hidden synced-store entry.
// Hidden entries use special-name keys and are filtered from ordinary
// user-facing listing operations.
const peerHandleEntry = /** @type {PetName} */ ('@peer-handle');
const guestHandleLocatorStr = formatLocator(guestHandleId, 'remote');
await E(syncedStoreValue).storeLocator(
/** @type {PetName} */ (guestName),
peerHandleEntry,
guestHandleLocatorStr,
);

Expand All @@ -4505,33 +4536,21 @@ const makeDaemonCore = async (
hostHandleExternalId,
'handle',
);
await E(syncedStoreValue).storeLocator(
/** @type {PetName} */ (hostNameFromGuest),
hostHandleLocatorStr,
);
const selfHandleEntry = /** @type {PetName} */ ('@self-handle');
await E(syncedStoreValue).storeLocator(selfHandleEntry, hostHandleLocatorStr);
}

// Create a local guest backed by the synced store.
/** @type {DeferredTasks<AgentDeferredTaskParams>} */
const guestTasks = makeDeferredTasks();
const { id: localGuestId } = await formulateGuest(
await formulateGuest(
hostAgentId,
hostHandleId,
guestTasks,
`guest:${guestName}`,
syncedStoreId,
);

// Name the local guest handle inside @pins so that incarnating
// it transitively incarnates the guest and its synced pet store.
const localGuestFormula = /** @type {GuestFormula} */ (
await getFormulaForId(localGuestId)
);
await E(hostAgent).storeIdentifier(
/** @type {NamePath} */ (['@pins', `guest-${guestName}`]),
localGuestFormula.handle,
);

// Store the remote guest handle under guestName for mail delivery.
// Use storeLocator so the directory properly internalizes the
// remote formula identifier for peer resolution.
Expand Down Expand Up @@ -4690,6 +4709,7 @@ const makeDaemonCore = async (
formulateScratchMount,
formulateInvitation,
formulateSyncedPetStore,
setPeerSyncedStore,
formulateDirectoryForStore,
getPeerIdForNodeIdentifier,
getAllNetworkAddresses,
Expand Down Expand Up @@ -4813,6 +4833,9 @@ const makeDaemonCore = async (
harden({
NODE: formula.node,
ADDRESSES: formula.addresses,
...(formula.syncedStore === undefined
? {}
: { SYNCED_STORE: formula.syncedStore }),
}),
);
}
Expand Down
116 changes: 85 additions & 31 deletions packages/daemon/src/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
assertPetNamePath,
assertName,
assertNamePath,
isPetName,
namePathFrom,
} from './pet-name.js';
import {
Expand All @@ -21,7 +22,7 @@ import {
parseId,
formatId,
} from './formula-identifier.js';
import { addressesFromLocator, formatLocator } from './locator.js';
import { addressesFromLocator, formatLocator, LOCAL_NODE } from './locator.js';
import { toHex, fromHex } from './hex.js';
import { makePetSitter } from './pet-sitter.js';

Expand Down Expand Up @@ -71,6 +72,7 @@ const normalizeHostOrGuestOptions = opts => {
* @param {DaemonCore['formulateScratchMount']} args.formulateScratchMount
* @param {DaemonCore['formulateInvitation']} args.formulateInvitation
* @param {DaemonCore['formulateSyncedPetStore']} args.formulateSyncedPetStore
* @param {DaemonCore['setPeerSyncedStore']} args.setPeerSyncedStore
* @param {DaemonCore['formulateDirectoryForStore']} args.formulateDirectoryForStore
* @param {DaemonCore['getPeerIdForNodeIdentifier']} args.getPeerIdForNodeIdentifier
* @param {DaemonCore['formulateChannel']} args.formulateChannel
Expand Down Expand Up @@ -105,6 +107,7 @@ export const makeHostMaker = ({
formulateScratchMount,
formulateInvitation,
formulateSyncedPetStore,
setPeerSyncedStore,
formulateDirectoryForStore,
getPeerIdForNodeIdentifier,
formulateChannel,
Expand Down Expand Up @@ -198,13 +201,76 @@ export const makeHostMaker = ({
isLocalKey,
getNetworkAddresses,
);
/**
* Derive an objective, deterministic hidden export-table key.
* Use the externalized identifier (node:number) so the key is
* stable across process restarts and independent of local formula
* identifier normalization.
*
* @param {FormulaIdentifier} externalizedId
* @returns {Name}
*/
const makeExportRetentionNameForId = externalizedId => {
const { node, number } = parseId(externalizedId);
return /** @type {Name} */ (
`@export-${node.slice(0, 16)}-${number.slice(0, 16)}`
);
};

/**
* Track formulas sent to a remote peer in that peer's hidden synced table.
* These entries retain formulas even if the exporter never names them
* in their own local pet store.
*
* @param {FormulaIdentifier} recipientId
* @param {FormulaIdentifier[]} ids
*/
const trackSentIdentifiers = async (recipientId, ids) => {
const { node: recipientNode } = parseId(recipientId);
if (recipientNode === LOCAL_NODE || isLocalKey(recipientNode)) {
return;
}

const peerNames = specialStore
.reverseIdentify(recipientId)
.filter(name => isPetName(name));

for (const peerName of peerNames) {
let syncedStore;
try {
syncedStore = await getSyncedStore(peerName);
} catch {
continue;
}
for (const id of ids) {
const { node } = parseId(id);
if (node !== LOCAL_NODE && !isLocalKey(node)) {
continue;
}
const formulaType = await getTypeForId(id);
const { number } = parseId(id);
const externalizedId = formatId({
number,
node: agentNodeNumber,
});
const locator = formatLocator(externalizedId, formulaType);
await E(syncedStore).storeLocator(
makeExportRetentionNameForId(id),
locator,
);
}
return;
}
};

const mailbox = await makeMailbox({
petStore: specialStore,
agentNodeNumber,
mailboxStore: mailboxController,
directory,
selfId: handleId,
context,
trackSentIdentifiers,
});
const { petStore, handle } = mailbox;
const getEndoBootstrap = async () => provide(endoId, 'endo');
Expand Down Expand Up @@ -881,30 +947,19 @@ export const makeHostMaker = ({
/** @type {import('./types.js').FormulaNumber} */ (syncedStoreNumber),
peerId, // store dependency
);
await setPeerSyncedStore(peerId, syncedStoreId);

// Create a local guest backed by the synced store.
/** @type {import('./types.js').DeferredTasks<import('./types.js').AgentDeferredTaskParams>} */
const guestTasks = makeDeferredTasks();
const { id: localGuestId } = await formulateGuest(
await formulateGuest(
hostId,
handleId,
guestTasks,
`guest:${guestName}`,
syncedStoreId,
);

// Look up the local guest's handle from its formula so we can
// name it. Incarnating the handle transitively incarnates the
// guest and its synced pet store, starting synchronisation.
const localGuestFormula =
/** @type {import('./types.js').GuestFormula} */ (
await getFormulaForId(localGuestId)
);
await E(directory).storeIdentifier(
['@pins', `guest-${guestName}`],
localGuestFormula.handle,
);

// Store the remote handle under guestName for mail delivery.
const remoteHandleId = formatId({
number: /** @type {import('./types.js').FormulaNumber} */ (
Expand All @@ -918,31 +973,30 @@ export const makeHostMaker = ({

/** @type {EndoHost['registerSyncedStore']} */
const registerSyncedStore = async (_petName, _syncedStoreId) => {
// No-op: the synced store is now discovered via the formula
// graph (guest handle → guest → petStore). Retained for
// interface compatibility.
// No-op: synced stores are attached to peer formulas.
// Retained for interface compatibility.
};

/** @type {EndoHost['getSyncedStore']} */
const getSyncedStore = async petName => {
// Traverse the formula graph:
// @pins/guest-<name> → local guest handle
// → handle formula.agent → guest formula
// → guest formula.petStore → synced store
const localHandleId = await E(directory).identify(
'@pins',
`guest-${petName}`,
);
if (localHandleId === undefined) {
const remoteHandleId = await E(directory).identify(petName);
if (remoteHandleId === undefined) {
throw new Error(`No synced store for ${q(petName)}`);
}
const handleFormula = /** @type {import('./types.js').HandleFormula} */ (
await getFormulaForId(/** @type {FormulaIdentifier} */ (localHandleId))
const { node: remoteNode } = parseId(
/** @type {FormulaIdentifier} */ (remoteHandleId),
);
const guestFormula = /** @type {import('./types.js').GuestFormula} */ (
await getFormulaForId(handleFormula.agent)
if (remoteNode === LOCAL_NODE || isLocalKey(remoteNode)) {
throw new Error(`No synced store for non-peer ${q(petName)}`);
}
const peerId = await getPeerIdForNodeIdentifier(
/** @type {NodeNumber} */ (remoteNode),
);
return provide(guestFormula.petStore, 'synced-pet-store');
const peerFormula = await getFormulaForId(peerId);
if (peerFormula.type !== 'peer' || peerFormula.syncedStore === undefined) {
throw new Error(`No synced store for ${q(petName)}`);
}
return provide(peerFormula.syncedStore, 'synced-pet-store');
};

/** @type {EndoHost['cancel']} */
Expand Down
10 changes: 10 additions & 0 deletions packages/daemon/src/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const makeMailboxMaker = ({
mailboxStore,
directory,
context,
trackSentIdentifiers = async (_recipientId, _ids) => {},
}) => {
const { number: selfNumber } = parseId(localSelfId);
const selfId = formatId({
Expand Down Expand Up @@ -862,6 +863,7 @@ export const makeMailboxMaker = ({
return /** @type {FormulaIdentifier} */ (id);
}),
);
await trackSentIdentifiers(/** @type {FormulaIdentifier} */ (toId), ids);

/** @type {import('./types.js').FormulaNumber | undefined} */
let replyTo;
Expand Down Expand Up @@ -940,6 +942,10 @@ export const makeMailboxMaker = ({
return /** @type {FormulaIdentifier} */ (id);
}),
);
await trackSentIdentifiers(
/** @type {FormulaIdentifier} */ (otherId),
ids,
);

const message = harden({
type: /** @type {const} */ ('package'),
Expand Down Expand Up @@ -1314,6 +1320,10 @@ export const makeMailboxMaker = ({
throw new Error(`Unknown pet name ${q(petNameOrPath)}`);
}
assertValidId(valueId);
await trackSentIdentifiers(
/** @type {FormulaIdentifier} */ (otherId),
[/** @type {FormulaIdentifier} */ (valueId)],
);

const messageId = /** @type {import('./types.js').FormulaNumber} */ (
await randomHex256()
Expand Down
Loading
Loading