diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 569e7c43f9c..e675c02bbbe 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -1,13 +1,16 @@ /* eslint-disable no-use-before-define */ -import { Fail, b, q } from '@endo/errors'; +import { Fail, X, b, makeError, q } from '@endo/errors'; import { Far, Remotable, getInterfaceOf } from '@endo/pass-style'; import { E } from '@endo/eventual-send'; import { getMethodNames } from '@endo/eventual-send/utils.js'; -import { makePromiseKit } from '@endo/promise-kit'; import { makeEquate } from './equate.js'; import { makeConvertKit } from './convert.js'; -const { fromEntries, defineProperties } = Object; +/** + * @import {PromiseKit} from '@endo/promise-kit' + */ + +const { fromEntries, defineProperties, assign } = Object; /** * @param {LogStore} log @@ -31,6 +34,8 @@ export const makeReplayMembrane = ( let stopped = false; + const Panic = (template, ...args) => panic(makeError(X(template, ...args))); + // ////////////// Host or Interpreter to Guest /////////////////////////////// /** @@ -196,9 +201,61 @@ export const makeReplayMembrane = ( default: { // @ts-expect-error TS correctly knows this case would be outside // the type. But that's what we want to check. - throw Fail`unexpected outcome kind ${q(outcome.kind)}`; + throw Panic`unexpected outcome kind ${q(outcome.kind)}`; + } + } + }; + + // //////////////// Eventual Send //////////////////////////////////////////// + + const guestHandler = harden({ + applyMethod(guestTarget, optVerb, guestArgs, guestReturnedP) { + if (optVerb === undefined) { + throw Panic`guest eventual call not yet supported: ${guestTarget}(${b(guestArgs)}) -> ${b(guestReturnedP)}`; + } else { + throw Panic`guest eventual send not yet supported: ${guestTarget}.${b(optVerb)}(${b(guestArgs)}) -> ${b(guestReturnedP)}`; } + }, + applyFunction(guestTarget, guestArgs, guestReturnedP) { + return guestHandler.applyMethod( + guestTarget, + undefined, + guestArgs, + guestReturnedP, + ); + }, + get(guestTarget, prop, guestReturnedP) { + throw Panic`guest eventual get not yet supported: ${guestTarget}.${b(prop)} -> ${b(guestReturnedP)}`; + }, + }); + + const makeGuestPresence = (iface, methodEntries) => { + let guestPresence; + void new HandledPromise((_res, _rej, resolveWithPresence) => { + guestPresence = resolveWithPresence(guestHandler); + }); // no unfulfilledHandler + if (typeof guestPresence !== 'object') { + throw Fail`presence expected to be object ${guestPresence}`; } + assign(guestPresence, fromEntries(methodEntries)); + const result = Remotable(iface, undefined, guestPresence); + result === guestPresence || + Fail`Remotable expected to make presence in place: ${guestPresence} vs ${result}`; + return result; + }; + + /** + * @returns {PromiseKit} + */ + const makeGuestPromiseKit = () => { + let resolve; + let reject; + const promise = new HandledPromise((res, rej, _resPres) => { + resolve = res; + reject = rej; + }, guestHandler); + // @ts-expect-error TS cannot infer that it is a PromiseKit + return harden({ promise, resolve, reject }); }; // //////////////// Converters /////////////////////////////////////////////// @@ -246,9 +303,7 @@ export const makeReplayMembrane = ( name, makeGuestMethod(name), ]); - // TODO in order to support E *well*, - // use HandledPromise to make gRem a remote presence for hRem - gRem = Remotable(guestIface, undefined, fromEntries(guestMethods)); + gRem = makeGuestPresence(guestIface, guestMethods); } // See note at the top of the function to see why clearing the `hRem` // variable is safe, and what invariant the above code needs to maintain so @@ -258,10 +313,12 @@ export const makeReplayMembrane = ( }; harden(makeGuestForHostRemotable); + /** + * @param {Vow} hVow + * @returns {Promise} + */ const makeGuestForHostVow = hVow => { - // TODO in order to support E *well*, - // use HandledPromise to make `promise` a handled promise for hVow - const { promise, resolve, reject } = makePromiseKit(); + const { promise, resolve, reject } = makeGuestPromiseKit(); guestPromiseMap.set(promise, harden({ resolve, reject })); watchWake(hVow); diff --git a/packages/async-flow/test/replay-membrane-eventual.test.js b/packages/async-flow/test/replay-membrane-eventual.test.js new file mode 100644 index 00000000000..51d7ff72ae6 --- /dev/null +++ b/packages/async-flow/test/replay-membrane-eventual.test.js @@ -0,0 +1,91 @@ +// eslint-disable-next-line import/order +import { + test, + getBaggage, + annihilate, + nextLife, +} from './prepare-test-env-ava.js'; + +import { Fail } from '@endo/errors'; +import { prepareVowTools } from '@agoric/vow'; +import { E } from '@endo/eventual-send'; +// import E from '@agoric/vow/src/E.js'; +import { makeHeapZone } from '@agoric/zone/heap.js'; +import { makeVirtualZone } from '@agoric/zone/virtual.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +import { prepareLogStore } from '../src/log-store.js'; +import { prepareBijection } from '../src/bijection.js'; +import { makeReplayMembrane } from '../src/replay-membrane.js'; + +const watchWake = _vowish => {}; +const panic = problem => Fail`panic over ${problem}`; + +/** + * @param {Zone} zone + */ +const preparePingee = zone => + zone.exoClass('Pingee', undefined, () => ({}), { + ping(_str) {}, + }); + +/** + * @typedef {ReturnType>} Pingee + */ + +/** + * @param {any} t + * @param {Zone} zone + */ +const testFirstPlay = async (t, zone) => { + const vowTools = prepareVowTools(zone); + const makeLogStore = prepareLogStore(zone); + const makeBijection = prepareBijection(zone); + const makePingee = preparePingee(zone); + + const log = zone.makeOnce('log', () => makeLogStore()); + const bij = zone.makeOnce('bij', makeBijection); + + const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + + t.deepEqual(log.dump(), []); + + /** @type {Pingee} */ + const pingee = zone.makeOnce('pingee', () => makePingee()); + /** @type {Pingee} */ + const guestPingee = mem.hostToGuest(pingee); + t.deepEqual(log.dump(), []); + + const pingTestSendResult = t.throwsAsync(() => E(guestPingee).ping('send'), { + message: + 'panic over "[Error: guest eventual send not yet supported: \\"[Alleged: Pingee guest wrapper]\\".ping([\\"send\\"]) -> \\"[Promise]\\"]"', + }); + + guestPingee.ping('call'); + + await pingTestSendResult; + + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ]); +}; + +test.serial('test heap replay-membrane settlement', async t => { + const zone = makeHeapZone('heapRoot'); + return testFirstPlay(t, zone); +}); + +test.serial('test virtual replay-membrane settlement', async t => { + annihilate(); + const zone = makeVirtualZone('virtualRoot'); + return testFirstPlay(t, zone); +}); + +test.serial('test durable replay-membrane settlement', async t => { + annihilate(); + + nextLife(); + const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); + return testFirstPlay(t, zone1); +});