diff --git a/packages/marshal/index.js b/packages/marshal/index.js index 6973cf84d7..0b92024186 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -29,6 +29,8 @@ export { unionRankCovers, } from './src/rankOrder.js'; +export { makeDotMembraneKit } from './src/dot-membrane.js'; + // eslint-disable-next-line import/export export * from './src/types.js'; diff --git a/packages/marshal/src/dot-membrane.js b/packages/marshal/src/dot-membrane.js index b8144d0a9f..c61942425a 100644 --- a/packages/marshal/src/dot-membrane.js +++ b/packages/marshal/src/dot-membrane.js @@ -2,152 +2,182 @@ /// import { E } from '@endo/eventual-send'; -import { isObject, getInterfaceOf, Far, passStyleOf } from '@endo/pass-style'; +import { getMethodNames } from '@endo/eventual-send/utils.js'; +import { + isObject, + getInterfaceOf, + passStyleOf, + Remotable, +} from '@endo/pass-style'; import { Fail } from '@endo/errors'; import { makeMarshal } from './marshal.js'; -const { fromEntries } = Object; -const { ownKeys } = Reflect; +const { fromEntries, defineProperties } = Object; -// TODO(erights): Add Converter type -/** @param {any} [mirrorConverter] */ -const makeConverter = (mirrorConverter = undefined) => { - /** @type {WeakMap=} */ - let mineToYours = new WeakMap(); - let optReasonString; - const myRevoke = reasonString => { - assert.typeof(reasonString, 'string'); - mineToYours = undefined; - optReasonString = reasonString; - if (optInnerRevoke) { - optInnerRevoke(reasonString); - } - }; - const convertMineToYours = (mine, _optIface = undefined) => { - if (mineToYours === undefined) { - throw harden(ReferenceError(`Revoked: ${optReasonString}`)); - } - if (mineToYours.has(mine)) { - return mineToYours.get(mine); - } - let yours; - const passStyle = passStyleOf(mine); - switch (passStyle) { - case 'promise': { - let yourResolve; - let yourReject; - yours = new Promise((res, rej) => { - yourResolve = res; - yourReject = rej; - }); - E.when( - mine, - myFulfillment => yourResolve(pass(myFulfillment)), - myReason => yourReject(pass(myReason)), - ) - .catch(metaReason => - // This can happen if myFulfillment or myReason is not passable. - // TODO verify that metaReason must be my-side-safe, or rather, - // that the passing of it is your-side-safe. - yourReject(pass(metaReason)), - ) - .catch(metaMetaReason => - // In case metaReason itself doesn't pass - yourReject(metaMetaReason), - ); - break; +/** + * @param {import('@endo/pass-style').Passable} blueTarget + * @param {import('./types.js').CapData[]} [optYellowLog] + */ +export const makeDotMembraneKit = (blueTarget, optYellowLog = undefined) => { + // TODO(erights): Add Converter type + /** + * @param {any} [mirrorConverter] + */ + const makeConverter = (mirrorConverter = undefined) => { + const myColor = mirrorConverter ? 'blue' : 'yellow'; + /** @type {WeakMap=} */ + let memoMineToYours = new WeakMap(); + let optReasonString; + const myRevoke = reasonString => { + assert.typeof(reasonString, 'string'); + memoMineToYours = undefined; + optReasonString = reasonString; + if (optBlueRevoke) { + // In this case, myRevoke is the yellowRevoke + optBlueRevoke(reasonString); + } + }; + const convertMineToYours = (mine, _optIface = undefined) => { + if (memoMineToYours === undefined) { + throw harden(ReferenceError(`Revoked: ${optReasonString}`)); } - case 'remotable': { - /** @param {PropertyKey} [optVerb] */ - const myMethodToYours = - (optVerb = undefined) => - (...yourArgs) => { - // We use mineIf rather than mine so that mine is not accessible - // after revocation. This gives the correct error behavior, - // but may not actually enable mine to be gc'ed, depending on - // the JS engine. - // TODO Could rewrite to keep scopes more separate, so post-revoke - // gc works more often. - const mineIf = passBack(yours); + if (memoMineToYours.has(mine)) { + return memoMineToYours.get(mine); + } + let yours; + const passStyle = passStyleOf(mine); + switch (passStyle) { + case 'promise': { + let yourResolve; + let yourReject; + yours = harden( + new Promise((res, rej) => { + yourResolve = res; + yourReject = rej; + }), + ); + E.when( + mine, + myFulfillment => yourResolve(mineToYours(myFulfillment)), + myReason => yourReject(mineToYours(myReason)), + ) + .catch(metaReason => + // This can happen if myFulfillment or myReason is not passable. + // TODO verify that metaReason must be my-side-safe, or rather, + // that the passing of it is your-side-safe. + yourReject(mineToYours(metaReason)), + ) + .catch(metaMetaReason => + // In case metaReason itself doesn't mineToYours + yourReject(metaMetaReason), + ); + break; + } + case 'remotable': { + /** @param {PropertyKey} [optVerb] */ + const myMethodToYours = (optVerb = undefined) => { + const yourMethod = (...yourArgs) => { + // We use mineIf rather than mine so that mine is not accessible + // after revocation. This gives the correct error behavior, + // but may not actually enable mine to be gc'ed, depending on + // the JS engine. + // TODO Could rewrite to keep scopes more separate, so post-revoke + // gc works more often. + const mineIf = yourToMine(yours); - assert(!isObject(optVerb)); - const myArgs = passBack(harden(yourArgs)); - let myResult; + assert(!isObject(optVerb)); + const myArgs = yourToMine(harden(yourArgs)); + let myResult; - try { - myResult = - optVerb === undefined - ? mineIf(...myArgs) - : mineIf[optVerb](...myArgs); - } catch (myReason) { - throw pass(myReason); + try { + myResult = optVerb + ? mineIf[optVerb](...myArgs) + : mineIf(...myArgs); + } catch (myReason) { + throw mineToYours(harden(myReason)); + } + return mineToYours(harden(myResult)); + }; + if (optVerb) { + defineProperties(yourMethod, { + name: { value: String(optVerb) }, + length: { value: Number(mine[optVerb].length || 0) }, + }); + } else { + defineProperties(yourMethod, { + name: { value: String(mine.name || 'anon') }, + length: { value: Number(mine.length || 0) }, + }); } - return pass(myResult); + return yourMethod; }; - const iface = pass(getInterfaceOf(mine)) || 'unlabeled remotable'; - if (typeof mine === 'function') { - // NOTE: Assumes that a far function has no "static" methods. This - // is the current marshal design, but revisit this if we change our - // minds. - yours = Far(iface, myMethodToYours()); - } else { - const myMethodNames = ownKeys(mine); - const yourMethods = myMethodNames.map(name => [ - name, - myMethodToYours(name), - ]); - yours = Far(iface, fromEntries(yourMethods)); + const iface = String(getInterfaceOf(mine) || 'unlabeled remotable'); + if (typeof mine === 'function') { + // NOTE: Assumes that a far function has no "static" methods. This + // is the current marshal design, but revisit this if we change our + // minds. + yours = Remotable(iface, undefined, myMethodToYours()); + } else { + const myMethodNames = getMethodNames(mine); + const yourMethods = myMethodNames.map(name => [ + name, + myMethodToYours(name), + ]); + yours = Remotable(iface, undefined, fromEntries(yourMethods)); + } + break; + } + default: { + Fail`internal: Unrecognized passStyle ${passStyle}`; } - break; } - default: { - Fail`internal: Unrecognized passStyle ${passStyle}`; + memoMineToYours.set(mine, yours); + memoYoursToMine.set(yours, mine); + return yours; + }; + + const { toCapData: myToYellowCapData, fromCapData: yourFromYellowCapData } = + makeMarshal( + // convert from my value to a yellow slot. undefined is identity. + myColor === 'yellow' ? undefined : convertMineToYours, + // convert from a yellow slot to your value. undefined is identity. + myColor === 'blue' ? undefined : convertMineToYours, + { + serializeBodyFormat: 'smallcaps', + }, + ); + + const mineToYours = mine => { + const yellowCapData = myToYellowCapData(mine); + const yours = yourFromYellowCapData(yellowCapData); + if (optYellowLog) { + optYellowLog.push(yellowCapData); } + return yours; + }; + const converter = harden({ + memoMineToYours, + mineToYours, + yourToMine: target => yourToMine(target), + myRevoke, + }); + let optBlueRevoke; + if (mirrorConverter === undefined) { + assert(myColor === 'yellow'); + // in this case, converter is the yellowConverter + // and mirrorConverter will be the blueConverter + mirrorConverter = makeConverter(converter); + optBlueRevoke = mirrorConverter.myRevoke; } - mineToYours.set(mine, yours); - yoursToMine.set(yours, mine); - return yours; - }; - // We need to pass this while convertYoursToMine is still in temporal - // dead zone, so we wrap it in convertSlotToVal. - const convertSlotToVal = (slot, optIface = undefined) => - convertYoursToMine(slot, optIface); - const { serialize: mySerialize, unserialize: myUnserialize } = makeMarshal( - convertMineToYours, - convertSlotToVal, - ); - const pass = mine => { - const myCapData = mySerialize(mine); - const yours = yourUnserialize(myCapData); - return yours; + const { memoMineToYours: memoYoursToMine, mineToYours: yourToMine } = + mirrorConverter; + return converter; }; - const converter = harden({ - mineToYours, - convertMineToYours, - myUnserialize, - pass, - wrap: target => passBack(target), - myRevoke, - }); - let optInnerRevoke; - if (mirrorConverter === undefined) { - mirrorConverter = makeConverter(converter); - optInnerRevoke = mirrorConverter.myRevoke; - } - const { - mineToYours: yoursToMine, - convertMineToYours: convertYoursToMine, - myUnserialize: yourUnserialize, - pass: passBack, - } = mirrorConverter; - return converter; -}; -export const makeDotMembraneKit = target => { - const converter = makeConverter(); + const yellowConverter = makeConverter(); return harden({ - proxy: converter.wrap(target), - revoke: converter.myRevoke, + yellowProxy: yellowConverter.yourToMine(blueTarget), + yellowRevoke: yellowConverter.myRevoke, }); }; harden(makeDotMembraneKit); diff --git a/packages/marshal/test/test-dot-membrane.js b/packages/marshal/test/test-dot-membrane.js index 045b540217..5153c4b980 100644 --- a/packages/marshal/test/test-dot-membrane.js +++ b/packages/marshal/test/test-dot-membrane.js @@ -4,22 +4,84 @@ import { test } from './prepare-test-env-ava.js'; import { Far } from '@endo/pass-style'; import { makeDotMembraneKit } from '../src/dot-membrane.js'; -test('test dot-membrane basics', t => { +test('test dot-membrane basics', async t => { /** @type {any} */ let blueState; - const blueSetState = Far('blueSetState', newState => { + const blueSetState = Far('blueSetState', async (newState, blueInP) => { blueState = newState; + await blueInP; + return Far('blueObj', { + getBlueState() { + return blueState; + }, + }); }); - const { proxy: yellowSetState, revoke } = makeDotMembraneKit(blueSetState); + const yellowLog = []; + const { yellowProxy: yellowSetState, yellowRevoke } = makeDotMembraneKit( + blueSetState, + yellowLog, + ); + t.not(blueSetState, yellowSetState); const yellow88 = [88]; const yellow99 = [99]; - yellowSetState(yellow88); + const yellowInP = Promise.resolve('wait for it'); + const yellowObjP = yellowSetState(yellow88, yellowInP); assert(blueState); t.is(blueState[0], 88); t.not(blueState, yellow88); - revoke('Halt!'); + const yellowObj = await yellowObjP; + // eslint-disable-next-line no-underscore-dangle + const methodNames = yellowObj.__getMethodNames__(); + yellowRevoke('Halt!'); t.throws(() => yellowSetState(yellow99), { message: /Revoked: Halt!/, }); + t.is(blueState[0], 88); + t.deepEqual(methodNames, ['__getMethodNames__', 'getBlueState']); + t.deepEqual(yellowLog, [ + { + body: '#"$0.Alleged: blueSetState"', + slots: [yellowSetState], + }, + { + body: '#"$0.Alleged: blueSetState"', + slots: [yellowSetState], + }, + { + body: '#[[88],"&0"]', + slots: [yellowInP], + }, + { + body: '#"&0"', + slots: [yellowObjP], + }, + { + body: '#"wait for it"', + slots: [], + }, + { + body: '#"$0.Alleged: blueObj"', + slots: [yellowObj], + }, + { + body: '#"$0.Alleged: blueObj"', + slots: [yellowObj], + }, + { + body: '#[]', + slots: [], + }, + { + body: '#["__getMethodNames__","getBlueState"]', + slots: [], + }, + ]); + // Needed because t.deepEqual sees blueSetState equal to yellowSetState + t.is(yellowLog[0].slots[0], yellowSetState); + t.is(yellowLog[1].slots[0], yellowSetState); + t.is(yellowLog[2].slots[0], yellowInP); + t.is(yellowLog[3].slots[0], yellowObjP); + t.is(yellowLog[5].slots[0], yellowObj); + t.is(yellowLog[6].slots[0], yellowObj); });