UNPKG

@metamask/ocap-kernel

Version:
253 lines 9.57 kB
import { Fail } from "@endo/errors"; import { getBaseMethods } from "./base.mjs"; import { getQueueMethods } from "./queue.mjs"; import { insistVatId } from "../../types.mjs"; import { getRefCountMethods } from "./refcount.mjs"; import { makeKernelSlot } from "../utils/kernel-slots.mjs"; import { parseRef } from "../utils/parse-ref.mjs"; import { isPromiseRef } from "../utils/promise-ref.mjs"; /** * Create a promise store object that provides functionality for managing kernel promises. * * @param ctx - The store context. * @returns A promise store object that maps various persistent kernel data * structures onto `kv`. */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function getPromiseMethods(ctx) { const { incCounter, provideStoredQueue, getPrefixedKeys, refCountKey } = getBaseMethods(ctx.kv); const { enqueueRun } = getQueueMethods(ctx); const { incrementRefCount, decrementRefCount } = getRefCountMethods(ctx); /** * Create a new, unresolved kernel promise. The new promise will be born with * a reference count of 1 on the assumption that the promise has just been * imported from somewhere. * * @returns A tuple of the new promise's KRef and an object describing the * new promise itself. */ function initKernelPromise() { const kpr = { state: 'unresolved', subscribers: [], }; const kpid = getNextPromiseId(); provideStoredQueue(kpid, false); ctx.kv.set(`${kpid}.state`, 'unresolved'); ctx.kv.set(`${kpid}.subscribers`, '[]'); ctx.kv.set(refCountKey(kpid), '1'); return [kpid, kpr]; } /** * Fetch the descriptive record for a kernel promise. * * @param kpid - The KRef of the kernel promise of interest. * @returns An object describing the requested kernel promise. */ function getKernelPromise(kpid) { const { context, isPromise } = parseRef(kpid); assert(context === 'kernel' && isPromise); const state = ctx.kv.get(`${kpid}.state`); if (state === undefined) { throw Error(`unknown kernel promise ${kpid}`); } const result = { state }; switch (state) { case 'unresolved': { const decider = ctx.kv.get(`${kpid}.decider`); if (decider !== '' && decider !== undefined) { result.decider = decider; } const subscribers = ctx.kv.getRequired(`${kpid}.subscribers`); result.subscribers = JSON.parse(subscribers); break; } case 'fulfilled': case 'rejected': { result.value = JSON.parse(ctx.kv.getRequired(`${kpid}.value`)); break; } default: throw Error(`unknown state for ${kpid}: ${state}`); } return result; } /** * Expunge a kernel promise from the kernel's persistent state. * * @param kpid - The KRef of the kernel promise to delete. */ function deleteKernelPromise(kpid) { ctx.kv.delete(`${kpid}.state`); ctx.kv.delete(`${kpid}.decider`); ctx.kv.delete(`${kpid}.subscribers`); ctx.kv.delete(`${kpid}.value`); ctx.kv.delete(refCountKey(kpid)); provideStoredQueue(kpid).delete(); } /** * Obtain a KRef for the next unallocated kernel promise. * * @returns The next kpid use. */ function getNextPromiseId() { return makeKernelSlot('promise', incCounter(ctx.nextPromiseId)); } /** * Add a new subscriber to a kernel promise's collection of subscribers. * * @param vatId - The vat that is subscribing. * @param kpid - The KRef of the promise being subscribed to. */ function addPromiseSubscriber(vatId, kpid) { insistVatId(vatId); const kp = getKernelPromise(kpid); kp.state === 'unresolved' || Fail `attempt to add subscriber to resolved promise ${kpid}`; const tempSet = new Set(kp.subscribers); tempSet.add(vatId); const newSubscribers = Array.from(tempSet).sort(); const key = `${kpid}.subscribers`; ctx.kv.set(key, JSON.stringify(newSubscribers)); } /** * Assign a kernel promise's decider. * * @param kpid - The KRef of promise whose decider is being set. * @param vatId - The vat which will become the decider. */ function setPromiseDecider(kpid, vatId) { insistVatId(vatId); if (kpid) { ctx.kv.set(`${kpid}.decider`, vatId); } } /** * Record the resolution of a kernel promise. * * @param kpid - The ref of the promise being resolved. * @param rejected - True if the promise is being rejected, false if fulfilled. * @param value - The value the promise is being fulfilled to or rejected with. */ function resolveKernelPromise(kpid, rejected, value) { let idx = 0; for (const dataSlot of value.slots) { incrementRefCount(dataSlot, `resolve|${kpid}|s${idx}`); idx += 1; } const queue = provideStoredQueue(kpid, false); for (const message of getKernelPromiseMessageQueue(kpid)) { const messageItem = { type: 'send', target: kpid, message, }; enqueueRun(messageItem); } ctx.kv.set(`${kpid}.state`, rejected ? 'rejected' : 'fulfilled'); ctx.kv.set(`${kpid}.value`, JSON.stringify(value)); ctx.kv.delete(`${kpid}.decider`); ctx.kv.delete(`${kpid}.subscribers`); // Drop the baseline “decider” refcount now that the promise is settled. decrementRefCount(kpid, 'resolve|decider'); queue.delete(); } /** * Append a message to a promise's message queue. * * @param kpid - The KRef of the promise to enqueue on. * @param message - The message to enqueue. */ function enqueuePromiseMessage(kpid, message) { provideStoredQueue(kpid, false).enqueue(message); } /** * Fetch the messages in a kernel promise's message queue. * * @param kpid - The KRef of the kernel promise of interest. * @returns An array of all the messages in the given promise's message queue. */ function getKernelPromiseMessageQueue(kpid) { const result = []; const queue = provideStoredQueue(kpid, false); for (;;) { const message = queue.dequeue(); if (message) { result.push(message); } else { return result; } } } /** * Generator that yield the promises decided by a given vat. * * @param decider - The vat ID of the vat of interest. * * @yields the kpids of all the promises decided by `decider`. */ function* getPromisesByDecider(decider) { const basePrefix = `cle.${decider}.`; for (const key of getPrefixedKeys(`${basePrefix}p`)) { const kpid = ctx.kv.getRequired(key); const kp = getKernelPromise(kpid); if (kp.state === 'unresolved' && kp.decider === decider) { yield kpid; } } } /** * Given a promise that has just been resolved and the value it resolved to, * find all promises reachable (recursively) from the new resolution value * which are themselves already resolved. This will determine the set of * resolutions that subscribers to the original promise will need to be * notified of. * * This is needed because subscription to a promise carries with it an implied * subscription to any promises that appear in its resolution value -- these * subscriptions must be implied rather than explicit because they are * necessarily unknown at the time of the original promise was subscribed to. * * @param origKpid - The original promise to start from. * @param origValue - The value the original promise is resolved to. * @returns An array of the kpids of the promises whose values become visible * as a consequence of the resolution of `origKpid`. */ function getKpidsToRetire(origKpid, origValue) { const seen = new Set(); const scanPromise = (kpid, value) => { seen.add(kpid); if (value) { for (const slot of value.slots) { if (isPromiseRef(slot)) { if (!seen.has(slot)) { const promise = getKernelPromise(slot); if (promise.state !== 'unresolved') { if (promise.value) { scanPromise(slot, promise.value); } } } } } } }; scanPromise(origKpid, origValue); return Array.from(seen); } return { initKernelPromise, getKernelPromise, deleteKernelPromise, getNextPromiseId, addPromiseSubscriber, setPromiseDecider, resolveKernelPromise, enqueuePromiseMessage, getKernelPromiseMessageQueue, getPromisesByDecider, getKpidsToRetire, }; } //# sourceMappingURL=promise.mjs.map