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);
});