diff --git a/packages/daemon/src/daemon.js b/packages/daemon/src/daemon.js index f469d8542b..cd5eeb1672 100644 --- a/packages/daemon/src/daemon.js +++ b/packages/daemon/src/daemon.js @@ -896,10 +896,14 @@ const makeDaemonCore = async ( }; /** - * @type {import('./types.js').DaemonCore['incarnateHandle']} + * Incarnates a `handle` formula and synchronously adds it to the formula graph. + * The returned promise is resolved after the formula is persisted. + * + * @param {string} formulaNumber - The formula number of the handle to incarnate. + * @param {string} targetFormulaIdentifier - The formula identifier of the handle's target. + * @returns {import('./types.js').IncarnateResult} The incarnated handle. */ - const incarnateHandle = async targetFormulaIdentifier => { - const formulaNumber = await randomHex512(); + const incarnateNumberedHandle = (formulaNumber, targetFormulaIdentifier) => { /** @type {import('./types.js').HandleFormula} */ const formula = { type: 'handle', @@ -911,10 +915,13 @@ const makeDaemonCore = async ( }; /** - * @type {import('./types.js').DaemonCore['incarnatePetStore']} + * Incarnates a `pet-store` formula and synchronously adds it to the formula graph. + * The returned promise is resolved after the formula is persisted. + * + * @param {string} formulaNumber - The formula number of the pet store to incarnate. + * @returns {import('./types.js').IncarnateResult} The incarnated pet store. */ - const incarnatePetStore = async specifiedFormulaNumber => { - const formulaNumber = specifiedFormulaNumber ?? (await randomHex512()); + const incarnateNumberedPetStore = async formulaNumber => { /** @type {import('./types.js').PetStoreFormula} */ const formula = { type: 'pet-store', @@ -929,7 +936,7 @@ const makeDaemonCore = async ( */ const incarnateDirectory = async () => { const { formulaIdentifier: petStoreFormulaIdentifier } = - await incarnatePetStore(); + await incarnateNumberedPetStore(await randomHex512()); const formulaNumber = await randomHex512(); /** @type {import('./types.js').DirectoryFormula} */ const formula = { @@ -941,25 +948,12 @@ const makeDaemonCore = async ( ); }; - /** - * @type {import('./types.js').DaemonCore['incarnateWorker']} - */ - const incarnateWorker = async () => { - const formulaNumber = await randomHex512(); - /** @type {import('./types.js').WorkerFormula} */ - const formula = { - type: 'worker', - }; - return /** @type {import('./types').IncarnateResult} */ ( - provideValueForNumberedFormula(formula.type, formulaNumber, formula) - ); - }; - /** * Incarnates a `worker` formula and synchronously adds it to the formula graph. * The returned promise is resolved after the formula is persisted. + * * @param {string} formulaNumber - The worker formula number. - * @returns {Promise<{ formulaIdentifier: string, value: import('./types').EndoWorker }>} + * @returns {ReturnType} */ const incarnateNumberedWorker = formulaNumber => { /** @type {import('./types.js').WorkerFormula} */ @@ -972,6 +966,14 @@ const makeDaemonCore = async ( ); }; + /** + * @type {import('./types.js').DaemonCore['incarnateWorker']} + */ + const incarnateWorker = async () => { + const formulaNumber = await formulaGraphMutex.enqueue(randomHex512); + return incarnateNumberedWorker(formulaNumber); + }; + /** @type {import('./types.js').DaemonCore['incarnateHost']} */ const incarnateHost = async ( endoFormulaIdentifier, @@ -983,10 +985,10 @@ const makeDaemonCore = async ( let workerFormulaIdentifier = specifiedWorkerFormulaIdentifier; if (workerFormulaIdentifier === undefined) { ({ formulaIdentifier: workerFormulaIdentifier } = - await incarnateWorker()); + await incarnateNumberedWorker(await randomHex512())); } const { formulaIdentifier: storeFormulaIdentifier } = - await incarnatePetStore(); + await incarnateNumberedPetStore(await randomHex512()); const { formulaIdentifier: inspectorFormulaIdentifier } = // eslint-disable-next-line no-use-before-define await incarnatePetInspector(storeFormulaIdentifier); @@ -1006,12 +1008,41 @@ const makeDaemonCore = async ( }; /** @type {import('./types.js').DaemonCore['incarnateGuest']} */ - const incarnateGuest = async hostHandleFormulaIdentifier => { - const formulaNumber = await randomHex512(); - const { formulaIdentifier: storeFormulaIdentifier } = - await incarnatePetStore(); - const { formulaIdentifier: workerFormulaIdentifier } = - await incarnateWorker(); + const incarnateGuest = async (hostFormulaIdentifier, deferredTasks) => { + const { + guestFormulaNumber, + hostHandleFormulaIdentifier, + storeFormulaIdentifier, + workerFormulaIdentifier, + } = await formulaGraphMutex.enqueue(async () => { + const formulaNumber = await randomHex512(); + const hostHandle = await incarnateNumberedHandle( + await randomHex512(), + hostFormulaIdentifier, + ); + const storeIncarnation = await incarnateNumberedPetStore( + await randomHex512(), + ); + const workerIncarnation = await incarnateNumberedWorker( + await randomHex512(), + ); + + await deferredTasks.execute({ + guestFormulaIdentifier: serializeFormulaIdentifier({ + type: 'guest', + number: formulaNumber, + node: ownNodeIdentifier, + }), + }); + + return harden({ + guestFormulaNumber: formulaNumber, + hostHandleFormulaIdentifier: hostHandle.formulaIdentifier, + storeFormulaIdentifier: storeIncarnation.formulaIdentifier, + workerFormulaIdentifier: workerIncarnation.formulaIdentifier, + }); + }); + /** @type {import('./types.js').GuestFormula} */ const formula = { type: 'guest', @@ -1020,7 +1051,7 @@ const makeDaemonCore = async ( worker: workerFormulaIdentifier, }; return /** @type {import('./types').IncarnateResult} */ ( - provideValueForNumberedFormula(formula.type, formulaNumber, formula) + provideValueForNumberedFormula(formula.type, guestFormulaNumber, formula) ); }; @@ -1030,7 +1061,7 @@ const makeDaemonCore = async ( source, codeNames, endowmentFormulaIdsOrPaths, - hooks, + deferredTasks, specifiedWorkerFormulaIdentifier, ) => { const { @@ -1073,7 +1104,7 @@ const makeDaemonCore = async ( evalFormulaIdentifier: ownFormulaIdentifier, }); - await Promise.all(hooks.map(hook => hook(identifiers))); + await deferredTasks.execute(identifiers); return identifiers; }); @@ -1265,13 +1296,13 @@ const makeDaemonCore = async ( }); const { formulaIdentifier: defaultHostWorkerFormulaIdentifier } = - await incarnateWorker(); + await incarnateNumberedWorker(await randomHex512()); const { formulaIdentifier: networksDirectoryFormulaIdentifier } = await incarnateNetworksDirectory(); const { formulaIdentifier: leastAuthorityFormulaIdentifier } = await incarnateLeastAuthority(); const { formulaIdentifier: newPeersFormulaIdentifier } = - await incarnatePetStore(peersFormulaNumber); + await incarnateNumberedPetStore(peersFormulaNumber); if (newPeersFormulaIdentifier !== peersFormulaIdentifier) { throw new Error( `Peers PetStore formula identifier did not match expected value.`, @@ -1444,7 +1475,6 @@ const makeDaemonCore = async ( incarnateUnconfined, incarnateBundle, incarnateWebBundle, - incarnateHandle, storeReaderRef, makeMailbox, makeDirectoryNode, @@ -1597,8 +1627,6 @@ const makeDaemonCore = async ( incarnateLeastAuthority, incarnateNetworksDirectory, incarnateLoopbackNetwork, - incarnateHandle, - incarnatePetStore, incarnateDirectory, incarnateWorker, incarnateHost, diff --git a/packages/daemon/src/deferred-tasks.js b/packages/daemon/src/deferred-tasks.js new file mode 100644 index 0000000000..a815dcbd9e --- /dev/null +++ b/packages/daemon/src/deferred-tasks.js @@ -0,0 +1,16 @@ +/** + * @returns {import('./types.js').DeferredTasks} + */ +export const makeDeferredTasks = () => { + /** @type {import('./types.js').DeferredTask[]} */ + const tasks = []; + + return { + execute: async param => { + await Promise.all(tasks.map(task => task(param))); + }, + push: task => { + tasks.push(task); + }, + }; +}; diff --git a/packages/daemon/src/host.js b/packages/daemon/src/host.js index 5c11d93440..93f8f3a90e 100644 --- a/packages/daemon/src/host.js +++ b/packages/daemon/src/host.js @@ -4,6 +4,7 @@ import { E, Far } from '@endo/far'; import { makeIteratorRef } from './reader-ref.js'; import { assertPetName, petNamePathFrom } from './pet-name.js'; import { makePetSitter } from './pet-sitter.js'; +import { makeDeferredTasks } from './deferred-tasks.js'; const { quote: q } = assert; @@ -19,7 +20,6 @@ const { quote: q } = assert; * @param {import('./types.js').DaemonCore['incarnateUnconfined']} args.incarnateUnconfined * @param {import('./types.js').DaemonCore['incarnateBundle']} args.incarnateBundle * @param {import('./types.js').DaemonCore['incarnateWebBundle']} args.incarnateWebBundle - * @param {import('./types.js').DaemonCore['incarnateHandle']} args.incarnateHandle * @param {import('./types.js').DaemonCore['storeReaderRef']} args.storeReaderRef * @param {import('./types.js').DaemonCore['getAllNetworkAddresses']} args.getAllNetworkAddresses * @param {import('./types.js').MakeMailbox} args.makeMailbox @@ -37,7 +37,6 @@ export const makeHostMaker = ({ incarnateUnconfined, incarnateBundle, incarnateWebBundle, - incarnateHandle, storeReaderRef, getAllNetworkAddresses, makeMailbox, @@ -98,22 +97,18 @@ export const makeHostMaker = ({ }; /** - * @returns {Promise<{ formulaIdentifier: string, value: import('./types').ExternalHandle }>} - */ - const makeNewHandleForSelf = () => { - return incarnateHandle(hostFormulaIdentifier); - }; - - /** - * @param {import('./types.js').Controller} newController - * @param {Record} introducedNames + * @param {string} formulaIdentifier - The guest or host formula identifier. + * @param {Record} introducedNames - The names to introduce. * @returns {Promise} */ - const introduceNamesToNewHostOrGuest = async ( - newController, + const introduceNamesToParty = async ( + formulaIdentifier, introducedNames, ) => { - const { petStore: newPetStore } = await newController.internal; + /** @type {import('./types.js').Controller} */ + const controller = + provideControllerForFormulaIdentifier(formulaIdentifier); + const { petStore: newPetStore } = await controller.internal; await Promise.all( Object.entries(introducedNames).map(async ([parentName, childName]) => { const introducedFormulaIdentifier = @@ -132,48 +127,48 @@ export const makeHostMaker = ({ * @returns {Promise<{formulaIdentifier: string, value: Promise}>} */ const makeGuest = async (petName, { introducedNames = {} } = {}) => { - /** @type {string | undefined} */ - let formulaIdentifier; if (petName !== undefined) { - formulaIdentifier = petStore.identifyLocal(petName); - } + const formulaIdentifier = petStore.identifyLocal(petName); + if (formulaIdentifier !== undefined) { + if (formulaIdentifier.startsWith('guest:')) { + throw new Error( + `Existing pet name does not designate a guest powers capability: ${q( + petName, + )}`, + ); + } - if (formulaIdentifier === undefined) { - const { formulaIdentifier: hostHandleFormulaIdentifier } = - await makeNewHandleForSelf(); - const { value, formulaIdentifier: guestFormulaIdentifier } = - // Behold, recursion: - // eslint-disable-next-line no-use-before-define - await incarnateGuest(hostHandleFormulaIdentifier); - if (petName !== undefined) { - assertPetName(petName); - await petStore.write(petName, guestFormulaIdentifier); + // TODO: Should this be awaited? + introduceNamesToParty(formulaIdentifier, introducedNames); + const guestController = + provideControllerForFormulaIdentifier(formulaIdentifier); + return { + formulaIdentifier, + value: /** @type {Promise} */ ( + guestController.external + ), + }; } - - return { - value: Promise.resolve(value), - formulaIdentifier: guestFormulaIdentifier, - }; - } else if (!formulaIdentifier.startsWith('guest:')) { - throw new Error( - `Existing pet name does not designate a guest powers capability: ${q( - petName, - )}`, - ); } - const newGuestController = - /** @type {import('./types.js').Controller<>} */ ( - provideControllerForFormulaIdentifier(formulaIdentifier) + /** @type {import('./types.js').DeferredTasks} */ + const tasks = makeDeferredTasks(); + if (petName !== undefined) { + tasks.push(identifiers => + petStore.write(petName, identifiers.guestFormulaIdentifier), ); - if (introducedNames !== undefined) { - introduceNamesToNewHostOrGuest(newGuestController, introducedNames); } + + const { value, formulaIdentifier } = + // Behold, recursion: + // eslint-disable-next-line no-use-before-define + await incarnateGuest(hostFormulaIdentifier, tasks); + // TODO: Should this be awaited? + introduceNamesToParty(formulaIdentifier, introducedNames); + return { + value: Promise.resolve(value), formulaIdentifier, - value: /** @type {Promise} */ ( - newGuestController.external - ), }; }; @@ -247,10 +242,10 @@ export const makeHostMaker = ({ /** * @param {string | 'MAIN' | 'NEW'} workerName - * @param {(hook: import('./types.js').EvalFormulaHook) => void} addHook + * @param {import('./types.js').DeferredTasks<{ workerFormulaIdentifier: string }>['push']} deferTask * @returns {string | undefined} */ - const provideWorkerFormulaIdentifierSync = (workerName, addHook) => { + const provideWorkerFormulaIdentifierSync = (workerName, deferTask) => { if (workerName === 'MAIN') { return mainWorkerFormulaIdentifier; } else if (workerName === 'NEW') { @@ -260,7 +255,7 @@ export const makeHostMaker = ({ assertPetName(workerName); const workerFormulaIdentifier = petStore.identifyLocal(workerName); if (workerFormulaIdentifier === undefined) { - addHook(identifiers => + deferTask(identifiers => petStore.write(workerName, identifiers.workerFormulaIdentifier), ); return undefined; @@ -280,7 +275,7 @@ export const makeHostMaker = ({ )); if (guestFormulaIdentifier === undefined) { throw new Error( - `panic: provideGuest must return an guest with a corresponding formula identifier`, + `panic: makeGuest must return a guest with a corresponding formula identifier`, ); } } @@ -308,16 +303,12 @@ export const makeHostMaker = ({ throw new Error('Evaluator requires one pet name for each code name'); } - /** @type {import('./types.js').EvalFormulaHook[]} */ - const hooks = []; - /** @type {(hook: import('./types.js').EvalFormulaHook) => void} */ - const addHook = hook => { - hooks.push(hook); - }; + /** @type {import('./types.js').DeferredTasks} */ + const tasks = makeDeferredTasks(); const workerFormulaIdentifier = provideWorkerFormulaIdentifierSync( workerName, - addHook, + tasks.push, ); /** @type {(string | string[])[]} */ @@ -341,7 +332,7 @@ export const makeHostMaker = ({ ); if (resultName !== undefined) { - addHook(identifiers => + tasks.push(identifiers => petStore.write(resultName, identifiers.evalFormulaIdentifier), ); } @@ -351,7 +342,7 @@ export const makeHostMaker = ({ source, codeNames, endowmentFormulaIdsOrPaths, - hooks, + tasks, workerFormulaIdentifier, ); return value; @@ -472,13 +463,12 @@ export const makeHostMaker = ({ )}`, ); } + + introduceNamesToParty(formulaIdentifier, introducedNames); const newHostController = /** @type {import('./types.js').Controller<>} */ ( provideControllerForFormulaIdentifier(formulaIdentifier) ); - if (introducedNames !== undefined) { - introduceNamesToNewHostOrGuest(newHostController, introducedNames); - } return { formulaIdentifier, value: /** @type {Promise} */ ( diff --git a/packages/daemon/src/pet-store.js b/packages/daemon/src/pet-store.js index 8bfcf31fef..113b648f81 100644 --- a/packages/daemon/src/pet-store.js +++ b/packages/daemon/src/pet-store.js @@ -85,12 +85,14 @@ export const makePetStoreMaker = (filePowers, locator) => { assertValidName(petName); assertValidFormulaIdentifier(formulaIdentifier, petName); + // TODO: Return early if the formula identifier is the same. if (petNames.has(petName)) { // Perform cleanup on the overwritten pet name. const formulaPetNames = formulaIdentifiers.get(petName); if (formulaPetNames !== undefined) { formulaPetNames.delete(petName); } + // TODO: Should this only happen if something is actually deleted? changesTopic.publisher.next({ remove: petName }); } diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index feeb13595b..d27a3edb6a 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -92,6 +92,10 @@ type GuestFormula = { worker: string; }; +export type GuestDeferredTaskParams = { + guestFormulaIdentifier: string; +}; + type LeastAuthorityFormula = { type: 'least-authority'; }; @@ -105,13 +109,11 @@ type EvalFormula = { // TODO formula slots }; -export type EvalFormulaHook = ( - identifiers: Readonly<{ - endowmentFormulaIdentifiers: string[]; - evalFormulaIdentifier: string; - workerFormulaIdentifier: string; - }>, -) => Promise; +export type EvalDeferredTaskParams = { + endowmentFormulaIdentifiers: string[]; + evalFormulaIdentifier: string; + workerFormulaIdentifier: string; +}; type ReadableBlobFormula = { type: 'readable-blob'; @@ -539,14 +541,14 @@ export interface EndoHost extends EndoDirectory { addPeerInfo(peerInfo: PeerInfo): Promise; } -export interface InternalEndoHost { +export interface InternalEndoParty { receive: Mail['receive']; respond: Mail['respond']; petStore: PetStore; } export interface EndoHostController - extends Controller, InternalEndoHost> {} + extends Controller, InternalEndoParty> {} export type EndoInspector = { lookup: (petName: Record) => Promise; @@ -706,6 +708,20 @@ export type DaemonicPowers = { }; type IncarnateResult = Promise<{ formulaIdentifier: string; value: T }>; + +export type DeferredTask> = ( + formulaIdentifiers: Readonly, +) => Promise; + +/** + * A collection of deferred tasks (i.e. async functions) that can be executed in + * parallel. + */ +export type DeferredTasks> = { + execute(identifiers: Readonly): Promise; + push(value: DeferredTask): void; +}; + export interface DaemonCore { nodeIdentifier: string; provideValueForFormulaIdentifier: ( @@ -730,9 +746,6 @@ export interface DaemonCore { specifiedFormulaNumber: string, ) => IncarnateResult; incarnateWorker: () => IncarnateResult; - incarnatePetStore: ( - specifiedFormulaNumber?: string, - ) => IncarnateResult; incarnateDirectory: () => IncarnateResult; incarnatePetInspector: ( petStoreFormulaIdentifier: string, @@ -744,7 +757,8 @@ export interface DaemonCore { specifiedWorkerFormulaIdentifier?: string | undefined, ) => IncarnateResult; incarnateGuest: ( - hostHandleFormulaIdenfitier: string, + hostFormulaIdentifier: string, + deferredTasks: DeferredTasks, ) => IncarnateResult; incarnateReadableBlob: ( contentSha512: string, @@ -754,7 +768,7 @@ export interface DaemonCore { source: string, codeNames: string[], endowmentFormulaIdsOrPaths: (string | string[])[], - hooks: EvalFormulaHook[], + deferredTasks: DeferredTasks, specifiedWorkerFormulaIdentifier?: string, ) => IncarnateResult; incarnateUnconfined: ( @@ -775,9 +789,6 @@ export interface DaemonCore { powersFormulaIdentifier: string, bundleFormulaIdentifier: string, ) => IncarnateResult; - incarnateHandle: ( - targetFormulaIdentifier: string, - ) => IncarnateResult; incarnatePeer: ( networksFormulaIdentifier: string, addresses: Array, diff --git a/packages/daemon/test/test-deferred-tasks.js b/packages/daemon/test/test-deferred-tasks.js new file mode 100644 index 0000000000..9c6a09a5dc --- /dev/null +++ b/packages/daemon/test/test-deferred-tasks.js @@ -0,0 +1,20 @@ +import test from 'ava'; +import { makeDeferredTasks } from '../src/deferred-tasks.js'; + +test('execute', async t => { + const tasks = makeDeferredTasks(); + const results = []; + tasks.push(async () => { + results.push(1); + }); + tasks.push(async () => { + results.push(2); + }); + tasks.push(async () => { + results.push(3); + }); + + await tasks.execute(); + + t.deepEqual(results.sort(), [1, 2, 3]); +});