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..11e79c1ce8 100644
--- a/packages/marshal/src/dot-membrane.js
+++ b/packages/marshal/src/dot-membrane.js
@@ -2,152 +2,183 @@
///
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
+ */
+export const makeDotMembraneKit = blueTarget => {
+ // 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;
+ }),
+ );
+ const myResolve = myFulfillment =>
+ yourResolve(mineToYours(myFulfillment));
+ const myReject = myReason => yourReject(mineToYours(myReason));
+ E.when(
+ mine,
+ myFulfillment => myResolve(myFulfillment),
+ myReason => myReject(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.
+ myReject(metaReason),
+ )
+ .catch(metaMetaReason =>
+ // In case metaReason itself doesn't mineToYours
+ myReject(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 = yoursToMine(yours);
- assert(!isObject(optVerb));
- const myArgs = passBack(harden(yourArgs));
- let myResult;
+ assert(!isObject(optVerb));
+ const myArgs = yoursToMine(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) {
+ const yourReason = mineToYours(harden(myReason));
+ throw yourReason;
+ }
+ const yourResult = mineToYours(harden(myResult));
+ return yourResult;
+ };
+ 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);
+ return yours;
+ };
+ const converter = harden({
+ memoMineToYours,
+ mineToYours,
+ yourToMine: target => yoursToMine(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: yoursToMine } =
+ 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/src/equalEnough.js b/packages/marshal/src/equalEnough.js
new file mode 100644
index 0000000000..abc7ed043b
--- /dev/null
+++ b/packages/marshal/src/equalEnough.js
@@ -0,0 +1,119 @@
+import { Fail, q } from '@endo/errors';
+import { passStyleOf } from '@endo/pass-style';
+import { compareRank } from './rankOrder.js';
+import { recordNames, recordValues } from './encodePassable.js';
+
+const { is } = Object;
+
+/**
+ * Since we're starting below the level where Checker is defined, try
+ * using a Rejector parameter directly.
+ *
+ * The pleasantness of this exercise shows we should have used Rejector
+ * parameters rather than Checker parameters all along. TODO we should seek
+ * to migrate existing uses of Checker to Rejector.
+ *
+ * TODO Also experiment with pleasant error path labeling.
+ *
+ * @typedef {false |
+ * ((template: TemplateStringsArray, ...subs: any[]) => false)
+ * } Rejector
+ */
+
+/**
+ * Normally, the `check*` function stays encapsulated
+ * and only the `is*` and `assert*` functions are exported.
+ * Here, we export the `check*` function too, because a caller is interested
+ * in distinguishing diagnostic rejection from internal errors.
+ *
+ * @param {import('@endo/pass-style').Passable} x
+ * @param {import('@endo/pass-style').Passable} y
+ * @param {Rejector} reject
+ * @returns {boolean}
+ */
+export const checkEqualEnough = (x, y, reject) => {
+ if (is(x, y)) {
+ return true;
+ }
+ harden(x);
+ harden(y);
+ if (compareRank(x, y) !== 0) {
+ return reject && reject`Unequal rank: ${x} vs ${y}`;
+ }
+ const passStyle = passStyleOf(x);
+ passStyle === passStyleOf(y) ||
+ Fail`internal: Same rank should imply same passStyle: ${q(
+ passStyle,
+ )} vs ${q(passStyleOf(y))}`;
+ switch (passStyle) {
+ case 'undefined':
+ case 'null': {
+ throw Fail`internal: x sameRank y should imply x is y: ${x} vs ${y}`;
+ }
+ case 'boolean':
+ case 'number':
+ case 'bigint':
+ case 'string':
+ case 'symbol':
+ case 'remotable': {
+ return reject && reject`Unequal: ${x} vs ${y}`;
+ }
+ case 'promise': {
+ // what ya gonna do?
+ return true;
+ }
+ case 'error': {
+ // TODO equal enough without comparing .message or other properties?
+ return (
+ x.name === y.name ||
+ (reject && reject`Different error names: ${q(x.name)} vs ${q(y.name)}`)
+ );
+ }
+ case 'copyArray': {
+ if (x.length !== y.length) {
+ return (
+ reject && reject`Different lengths: ${q(x.length)} vs ${q(y.length)}`
+ );
+ }
+ return x.every((xel, i) => checkEqualEnough(xel, y[i], reject));
+ }
+ case 'copyRecord': {
+ const xNames = recordNames(x);
+ const yNames = recordNames(y);
+ return (
+ checkEqualEnough(xNames, yNames, reject) &&
+ checkEqualEnough(
+ recordValues(x, xNames),
+ recordValues(y, xNames),
+ reject,
+ )
+ );
+ }
+ case 'tagged': {
+ return (
+ checkEqualEnough(x.tag, y.tag, reject) &&
+ checkEqualEnough(x.payload, y.payload, reject)
+ );
+ }
+ default: {
+ throw Fail`internal: Unexpected passStyle: ${q(passStyle)}`;
+ }
+ }
+};
+
+/**
+ * @param { import('@endo/pass-style').Passable } x
+ * @param { import('@endo/pass-style').Passable } y
+ * @returns { boolean }
+ */
+export const equalEnough = (x, y) => checkEqualEnough(x, y, false);
+harden(equalEnough);
+
+/**
+ * @param { import('@endo/pass-style').Passable } x
+ * @param { import('@endo/pass-style').Passable } y
+ */
+export const assertEqualEnough = (x, y) => {
+ checkEqualEnough(x, y, Fail);
+};
+harden(assertEqualEnough);
diff --git a/packages/marshal/src/host-log-membrane.js b/packages/marshal/src/host-log-membrane.js
new file mode 100644
index 0000000000..67d97dc51c
--- /dev/null
+++ b/packages/marshal/src/host-log-membrane.js
@@ -0,0 +1,312 @@
+/* eslint-disable no-use-before-define */
+import { E } from '@endo/eventual-send';
+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, defineProperties } = Object;
+
+/**
+ * @param {import('@endo/pass-style').Passable} guestTarget
+ * @param {any[]} hostLog
+ */
+export const makeHostLogMembraneKit = (guestTarget, hostLog) => {
+ /** @type {WeakMap | undefined} */
+ let memoH2G = new WeakMap();
+ /** @type {WeakMap | undefined} */
+ let memoG2H = new WeakMap();
+ let optReasonString;
+ const revoke = reasonString => {
+ assert.typeof(reasonString, 'string');
+ memoH2G = undefined;
+ memoG2H = undefined;
+ optReasonString = reasonString;
+ };
+
+ const convertCapH2G = (hostCap, _optIface = undefined) => {
+ if (memoH2G === undefined) {
+ throw harden(ReferenceError(`Revoked: ${optReasonString}`));
+ }
+ assert(memoG2H !== undefined);
+ if (memoH2G.has(hostCap)) {
+ return memoH2G.get(hostCap);
+ }
+ let guestCap;
+ const passStyle = passStyleOf(hostCap);
+ switch (passStyle) {
+ case 'promise': {
+ let guestResolve;
+ let guestReject;
+ guestCap = harden(
+ new Promise((res, rej) => {
+ guestResolve = res;
+ guestReject = rej;
+ }),
+ );
+ const hostResolve = hostFulfillment => {
+ hostLog.push(['doFulfill', hostCap, hostFulfillment]);
+ guestResolve(hostToGuest(hostFulfillment));
+ };
+ const hostReject = hostReason => {
+ hostLog.push(['doReject', hostCap, hostReason]);
+ guestReject(hostToGuest(hostReason));
+ };
+ E.when(
+ hostCap,
+ hostFulfillment => hostResolve(hostFulfillment),
+ hostReason => hostReject(hostReason),
+ )
+ .catch(metaReason =>
+ // This can happen if hostFulfillment or hostReason is not
+ // Passable.
+ // TODO verify that metaReason must be host-side-safe, or rather,
+ // that the passing of it is guest-side-safe.
+ hostReject(metaReason),
+ )
+ .catch(metaMetaReason =>
+ // In case metaReason itself doesn't hostToGuest
+ guestReject(metaMetaReason),
+ );
+ break;
+ }
+ case 'remotable': {
+ /** @param {PropertyKey} [optVerb] */
+ const makeGuestMethod = (optVerb = undefined) => {
+ const guestMethod = (...guestArgs) => {
+ // We use hostCapIf rather than hostCap so that hostCap is
+ // not accessible
+ // after revocation. This gives the correct error behavior,
+ // but may not actually enable hostCap 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 hostCapIf = guestToHost(guestCap);
+
+ assert(!isObject(optVerb));
+ const hostArgs = guestToHost(harden(guestArgs));
+ let hostResult;
+
+ const callLogIndex = hostLog.length;
+ hostLog.push([
+ 'checkCall',
+ hostCap,
+ optVerb,
+ hostArgs,
+ callLogIndex,
+ ]);
+
+ try {
+ hostResult = optVerb
+ ? hostCapIf[optVerb](...hostArgs)
+ : hostCapIf(...hostArgs);
+ } catch (hostReason) {
+ const guestReason = hostToGuest(harden(hostReason));
+ hostLog.push(['doThrow', callLogIndex, hostReason]);
+ throw guestReason;
+ }
+ const guestResult = hostToGuest(harden(hostResult));
+ hostLog.push(['doReturn', callLogIndex, hostResult]);
+ return guestResult;
+ };
+ if (optVerb) {
+ defineProperties(guestMethod, {
+ name: { value: String(optVerb) },
+ length: { value: Number(hostCap[optVerb].length || 0) },
+ });
+ } else {
+ defineProperties(guestMethod, {
+ name: { value: String(hostCap.name || 'anon') },
+ length: { value: Number(hostCap.length || 0) },
+ });
+ }
+ return guestMethod;
+ };
+ const iface = String(getInterfaceOf(hostCap) || 'unlabeled remotable');
+ if (typeof hostCap === '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.
+ guestCap = Remotable(iface, undefined, makeGuestMethod());
+ } else {
+ const methodNames = getMethodNames(hostCap);
+ const guestMethods = methodNames.map(name => [
+ name,
+ makeGuestMethod(name),
+ ]);
+ guestCap = Remotable(iface, undefined, fromEntries(guestMethods));
+ }
+ break;
+ }
+ default: {
+ Fail`internal: Unrecognized passStyle ${passStyle}`;
+ }
+ }
+ memoH2G.set(hostCap, guestCap);
+ memoG2H.set(guestCap, hostCap);
+ return guestCap;
+ };
+
+ const { toCapData: hostToHostCapData, fromCapData: guestFromHostCapData } =
+ makeMarshal(
+ undefined, // undefined is identity
+ convertCapH2G, // convert a host slot to a guest value
+ {
+ serializeBodyFormat: 'smallcaps',
+ },
+ );
+
+ const hostToGuest = hostVal => {
+ const hostCapData = hostToHostCapData(hostVal);
+ const guestVal = guestFromHostCapData(hostCapData);
+ return guestVal;
+ };
+
+ // ///////////////////////// Mirror image ////////////////////////////////////
+
+ const convertCapG2H = (guestCap, _optIface = undefined) => {
+ if (memoG2H === undefined) {
+ throw harden(ReferenceError(`Revoked: ${optReasonString}`));
+ }
+ assert(memoH2G !== undefined);
+ if (memoG2H.has(guestCap)) {
+ return memoG2H.get(guestCap);
+ }
+ let hostCap;
+ const passStyle = passStyleOf(guestCap);
+ switch (passStyle) {
+ case 'promise': {
+ let hostResolve;
+ let hostReject;
+ hostCap = harden(
+ new Promise((res, rej) => {
+ hostResolve = res;
+ hostReject = rej;
+ }),
+ );
+ const guestResolve = guestFulfillment => {
+ const hostFulfillment = guestToHost(guestFulfillment);
+ hostLog.push(['checkFulfill', hostCap, hostFulfillment]);
+ hostResolve(hostFulfillment);
+ };
+ const guestReject = guestReason => {
+ const hostReason = guestToHost(guestReason);
+ hostLog.push(['checkReject', hostCap, hostReason]);
+ hostReject(hostReason);
+ };
+ E.when(
+ guestCap,
+ guestFulfillment => guestResolve(guestFulfillment),
+ guestReason => guestReject(guestReason),
+ )
+ .catch(metaReason =>
+ // This can happen if guestFulfillment or guestReason is not
+ // passable.
+ // TODO verify that metaReason must be guest-side-safe, or rather,
+ // that the passing of it is host-side-safe.
+ guestReject(metaReason),
+ )
+ .catch(metaMetaReason =>
+ // In case metaReason itself doesn't guestToHost
+ hostReject(metaMetaReason),
+ );
+ break;
+ }
+ case 'remotable': {
+ /** @param {PropertyKey} [optVerb] */
+ const makeHostMethod = (optVerb = undefined) => {
+ const hostMethod = (...hostArgs) => {
+ // We use guestCapIf rather than guestCap so that guestCap is
+ // not accessible
+ // after revocation. This gives the correct error behavior,
+ // but may not actually enable guestCap 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 guestCapIf = hostToGuest(hostCap);
+
+ assert(!isObject(optVerb));
+ const guestArgs = hostToGuest(harden(hostArgs));
+ let guestResult;
+
+ const callLogIndex = hostLog.length;
+ hostLog.push(['doCall', hostCap, optVerb, hostArgs, callLogIndex]);
+
+ try {
+ guestResult = optVerb
+ ? guestCapIf[optVerb](...guestArgs)
+ : guestCapIf(...guestArgs);
+ } catch (guestReason) {
+ const yourReason = guestToHost(harden(guestReason));
+ hostLog.push(['checkThrow', callLogIndex, yourReason]);
+ throw yourReason;
+ }
+ const yourResult = guestToHost(harden(guestResult));
+ hostLog.push(['checkReturn', callLogIndex, yourResult]);
+ return yourResult;
+ };
+ if (optVerb) {
+ defineProperties(hostMethod, {
+ name: { value: String(optVerb) },
+ length: { value: Number(guestCap[optVerb].length || 0) },
+ });
+ } else {
+ defineProperties(hostMethod, {
+ name: { value: String(guestCap.name || 'anon') },
+ length: { value: Number(guestCap.length || 0) },
+ });
+ }
+ return hostMethod;
+ };
+ const iface = String(getInterfaceOf(guestCap) || 'unlabeled remotable');
+ if (typeof guestCap === '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.
+ hostCap = Remotable(iface, undefined, makeHostMethod());
+ } else {
+ const methodNames = getMethodNames(guestCap);
+ const hostMethods = methodNames.map(name => [
+ name,
+ makeHostMethod(name),
+ ]);
+ hostCap = Remotable(iface, undefined, fromEntries(hostMethods));
+ }
+ break;
+ }
+ default: {
+ Fail`internal: Unrecognized passStyle ${passStyle}`;
+ }
+ }
+ memoG2H.set(guestCap, hostCap);
+ memoH2G.set(hostCap, guestCap);
+ return hostCap;
+ };
+
+ const { toCapData: guestToHostCapData, fromCapData: hostFromHostCapData } =
+ makeMarshal(
+ // convert from my value to a yellow slot. undefined is identity.
+ convertCapG2H, // convert a guest value to a host slot
+ undefined, // undefined is identity
+ {
+ serializeBodyFormat: 'smallcaps',
+ },
+ );
+
+ const guestToHost = guestVal => {
+ const hostCapData = guestToHostCapData(guestVal);
+ const hostVal = hostFromHostCapData(hostCapData);
+ return hostVal;
+ };
+
+ return harden({
+ hostProxy: guestToHost(guestTarget),
+ revoke,
+ });
+};
+harden(makeHostLogMembraneKit);
diff --git a/packages/marshal/src/replay-bridge.js b/packages/marshal/src/replay-bridge.js
new file mode 100644
index 0000000000..42abc6e0db
--- /dev/null
+++ b/packages/marshal/src/replay-bridge.js
@@ -0,0 +1,95 @@
+import { Fail, X, annotateError, q } from '@endo/errors';
+import { isObject, passStyleOf } from '@endo/pass-style';
+import { compareRank } from './rankOrder.js';
+import { recordNames, recordValues } from './encodePassable.js';
+
+const { is } = Object;
+
+/**
+ * Since we're starting below the level where Checker is defined, try
+ * using a Rejector parameter directly.
+ *
+ * The pleasantness of this exercise shows we should have used Rejector
+ * parameters rather than Checker parameters all along. TODO we should seek
+ * to migrate existing uses of Checker to Rejector.
+ *
+ * TODO Also experiment with pleasant error path labeling.
+ */
+
+export const makeReplayBridge = (memoG2H, memoH2G) => {
+ const bind = (g, h) => {
+ memoG2H.set(g, h);
+ memoH2G.set(h, g);
+ };
+
+ const bridge = (g, h, _label) => {
+ if (isObject(g)) {
+ isObject(h) || Fail`isObject must be the same: ${g} vs ${h}`;
+ if (memoG2H.has(g)) {
+ memoG2H.get(g) === h || Fail`Unqual objects: ${g} vs ${h}`;
+ memoH2G.get(h) === g ||
+ Fail`internal: memos inconsistent on: ${g} vs ${h}`;
+ return;
+ } else {
+ !memoH2G.has(h) || Fail`internal: memos inconsistent on: ${g} vs ${h}`;
+ }
+ } else {
+ is(g, h) || Fail`Unqual primitive values: ${g} vs ${h}`;
+ return;
+ }
+ const passStyle = passStyleOf(g);
+ const hPassStyle = passStyleOf(h);
+ passStyle === hPassStyle ||
+ Fail`passStyles must be the same: ${q(passStyle)} vs ${q(hPassStyle)}`;
+ switch (passStyle) {
+ case 'copyArray': {
+ g.length === h.length ||
+ Fail`Unqual lengths: ${q(g.length)} vs ${q(h.length)}`;
+ g.forEach((gElement, i) => bridge(gElement, h[i], i));
+ bind(g, h);
+ return;
+ }
+ case 'copyRecord': {
+ const names = recordNames(g);
+ const hNames = recordNames(h);
+ compareRank(names, hNames) === 0 ||
+ Fail`Unequal property names: ${q(names)} vs ${q(hNames)}`;
+ const gValues = recordValues(g, names);
+ const hValues = recordValues(h, names);
+ gValues.forEach((gValue, i) => bridge(gValue, hValues[i], names[i]));
+ bind(g, h);
+ return;
+ }
+ case 'tagged': {
+ bridge(g.tag, h.tag, 'tag');
+ bridge(g.payload, h.payload, 'payload');
+ bind(g, h);
+ return;
+ }
+ case 'error': {
+ bridge(g.name, h.name, 'error name');
+ // Ok for everything else to differ
+ // but would be nice to warn on different errors
+ annotateError(g, X`guest for host error ${h}`);
+ annotateError(h, X`host for guest error ${g}`);
+ bind(g, h);
+ return;
+ }
+ case 'remotable': {
+ // Would be nice to want on different iface strings
+ bind(g, h);
+ return;
+ }
+ case 'promise': {
+ // Would be nice to want on different iface strings
+ bind(g, h);
+ return;
+ }
+ default: {
+ Fail`unrecognized passStyle ${q(passStyle)}`;
+ }
+ }
+ };
+ return harden(bridge);
+};
+harden(makeReplayBridge);
diff --git a/packages/marshal/src/replay-membrane.js b/packages/marshal/src/replay-membrane.js
new file mode 100644
index 0000000000..c1b671b26f
--- /dev/null
+++ b/packages/marshal/src/replay-membrane.js
@@ -0,0 +1,514 @@
+/* eslint-disable @endo/restrict-comparison-operands */
+/* eslint-disable no-use-before-define */
+import { E } from '@endo/eventual-send';
+import { getMethodNames } from '@endo/eventual-send/utils.js';
+import { getInterfaceOf, passStyleOf, Remotable } from '@endo/pass-style';
+import { X, makeError, q } from '@endo/errors';
+import { makeMarshal } from './marshal.js';
+import { assertEqualEnough, checkEqualEnough } from './equalEnough.js';
+
+const { fromEntries, defineProperties } = Object;
+
+/**
+ * @typedef {'return'|'throw'} OutcomeKind
+ */
+
+/**
+ * @typedef {{kind: 'return', result: any}
+ * | {kind: 'throw', reason: any}
+ * } Outcome
+ */
+
+/**
+ * @param {import('@endo/pass-style').Passable} guestTarget
+ * @param {any[]} hostLog
+ */
+export const makeReplayMembraneKit = async (guestTarget, hostLog) => {
+ /** @type {WeakMap | undefined} */
+ let memoH2G = new WeakMap();
+ /** @type {WeakMap | undefined} */
+ let memoG2H = new WeakMap();
+
+ let optReason;
+ /**
+ * @type {import('./equalEnough.js').Rejector}
+ */
+ const reject = (template, ...subs) => {
+ if (optReason === undefined) {
+ optReason = makeError(X(template, ...subs), ReferenceError);
+ memoH2G = undefined;
+ memoG2H = undefined;
+ }
+ throw optReason;
+ };
+
+ /**
+ * If < hostLog.length, the index of the next hostLog entry to interpret.
+ * > Q: Why did the program counter increment?
+ * > A: To get to the next instruction.
+ *
+ * Else must be === hostLog.length, in which case we're caught up
+ * and should switch into pushing new hostLog entries.
+ *
+ * @type {number} hostLogIndex
+ */
+ let hostLogIndex = 0;
+
+ const promiseMap = new WeakMap();
+
+ const doHostLog = entry => {
+ try {
+ if (hostLogIndex < hostLog.length) {
+ assertEqualEnough(entry, hostLog[hostLogIndex]);
+ } else {
+ hostLogIndex === hostLog.length ||
+ reject`internal: unexpected hostLogIndex: ${q(hostLogIndex)}`;
+ hostLog.push(entry);
+ }
+ } finally {
+ hostLogIndex += 1;
+ }
+ };
+
+ const checkHostLog = entry => {
+ try {
+ if (hostLogIndex < hostLog.length) {
+ checkEqualEnough(entry, hostLog[hostLogIndex], reject);
+ } else {
+ hostLogIndex === hostLog.length ||
+ reject`internal: unexpected hostLogIndex: ${q(hostLogIndex)}`;
+ hostLog.push(entry);
+ }
+ } finally {
+ hostLogIndex += 1;
+ }
+ };
+
+ // ////////////// Host or Interpreter to Guest ///////////////////////////////
+
+ const doFulfill = (hostPromise, hostFulfillment) => {
+ doHostLog(['doFulfill', hostPromise, hostFulfillment]);
+ promiseMap.get(hostPromise).hostResolve(hostFulfillment);
+ promiseMap.delete(hostPromise);
+ };
+
+ const doReject = (hostPromise, hostReason) => {
+ doHostLog(['doReject', hostPromise, hostReason]);
+ promiseMap.get(hostPromise).hostReject(hostReason);
+ promiseMap.delete(hostPromise);
+ };
+
+ /**
+ * @param {Remotable} hostCap
+ * @param {PropertyKey | undefined} optVerb
+ * @param {any[]} hostArgs
+ * @param {number} callIndex
+ * @returns {Outcome}
+ */
+ const doCall = (hostCap, optVerb, hostArgs, callIndex) => {
+ doHostLog(['doCall', hostCap, optVerb, hostArgs, callIndex]);
+ const guestCap = hostToGuest(hostCap);
+ const guestArgs = hostToGuest(hostArgs);
+ let guestResult;
+ try {
+ guestResult =
+ optVerb === undefined
+ ? guestCap(...guestArgs)
+ : guestCap[optVerb](...guestArgs);
+ } catch (guestReason) {
+ return checkThrow(callIndex, guestReason);
+ }
+ return checkReturn(callIndex, guestResult);
+ };
+
+ /**
+ * @param {number} callIndex
+ * @param {any} hostResult
+ * @returns {Outcome}
+ */
+ const doReturn = (callIndex, hostResult) => {
+ doHostLog(['doReturn', callIndex, hostResult]);
+ unnestInterpreter(callIndex);
+ const guestResult = hostToGuest(hostResult);
+ return harden({
+ kind: 'return',
+ result: guestResult,
+ });
+ };
+
+ /**
+ * @param {number} callIndex
+ * @param {any} hostReason
+ * @returns {Outcome}
+ */
+ const doThrow = (callIndex, hostReason) => {
+ doHostLog(['doThrow', callIndex, hostReason]);
+ unnestInterpreter(callIndex);
+ const guestReason = hostToGuest(hostReason);
+ return harden({
+ kind: 'throw',
+ reason: guestReason,
+ });
+ };
+
+ // ///////////// Guest to Host or consume log ////////////////////////////////
+
+ const checkFulfill = (guestPromise, guestFulfillment) => {
+ const hostPromise = guestToHost(guestPromise);
+ const hostFulfillment = guestToHost(guestFulfillment);
+ checkHostLog(['checkFulfill', hostPromise, hostFulfillment]);
+ promiseMap.get(hostPromise).hostResolve(hostFulfillment);
+ promiseMap.delete(hostPromise);
+ };
+
+ const checkReject = (guestPromise, guestReason) => {
+ const hostPromise = guestToHost(guestPromise);
+ const hostReason = guestToHost(guestReason);
+ checkHostLog(['checkReject', hostPromise, hostReason]);
+ promiseMap.get(hostPromise).hostReject(hostReason);
+ promiseMap.delete(hostPromise);
+ };
+
+ /**
+ * @param {Remotable} guestCap
+ * @param {PropertyKey | undefined} optVerb
+ * @param {any[]} guestArgs
+ * @param {number} callIndex
+ * @returns {Outcome}
+ */
+ const checkCall = (guestCap, optVerb, guestArgs, callIndex) => {
+ const hostCap = guestToHost(guestCap);
+ const hostArgs = guestToHost(guestArgs);
+ checkHostLog(['checkCall', hostCap, optVerb, hostArgs, callIndex]);
+ if (callIndex < hostLog.length) {
+ // Simulate everything that happens ending with matching
+ // doReturn or doThrow
+ return nestInterpreter(callIndex);
+ } else {
+ // Running for real. Actually call the host function or method
+ let hostResult;
+ try {
+ hostResult = optVerb
+ ? hostCap[optVerb](...hostArgs)
+ : hostCap(...hostArgs);
+ } catch (hostReason) {
+ return doThrow(callIndex, hostReason);
+ }
+ return doReturn(callIndex, hostResult);
+ }
+ };
+
+ /**
+ * @param {number} callIndex
+ * @param {any} guestResult
+ * @returns {Outcome}
+ */
+ const checkReturn = (callIndex, guestResult) => {
+ const hostResult = hostToGuest(guestResult);
+ checkHostLog(['checkReturn', callIndex, hostResult]);
+ return harden({
+ kind: 'return',
+ result: hostResult,
+ });
+ };
+
+ /**
+ * @param {number} callIndex
+ * @param {any} guestReason
+ * @returns {Outcome}
+ */
+ const checkThrow = (callIndex, guestReason) => {
+ const hostReason = hostToGuest(guestReason);
+ checkHostLog(['checkThrow', callIndex, hostReason]);
+ return harden({
+ kind: 'throw',
+ reason: hostReason,
+ });
+ };
+
+ // //////////////////////// Log interpreters /////////////////////////////////
+
+ const callIndexStack = [];
+
+ let unnestFlag = false;
+
+ const nestDispatch = harden({
+ doCall,
+ doReturn,
+ doThrow,
+ });
+
+ const interpretEntry = (dispatch, [verb, ...args]) => dispatch[verb](...args);
+
+ /**
+ * @param {number} callIndex
+ * @returns {Outcome}
+ */
+ const nestInterpreter = callIndex => {
+ callIndexStack.push(callIndex);
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ hostLogIndex < hostLog.length ||
+ reject`log ended too soon: ${hostLogIndex}`;
+ try {
+ // eslint-disable-next-line no-await-in-loop
+ const optOutcome = interpretEntry(nestDispatch, hostLog[hostLogIndex]);
+ if (unnestFlag) {
+ optOutcome || reject`only unnest with an outcome: ${q(hostLogIndex)}`;
+ unnestFlag = false;
+ return optOutcome;
+ }
+ } catch (problem) {
+ reject`Playback stopped due to ${q(problem)}`;
+ }
+ }
+ };
+
+ /**
+ * @param {number} callIndex
+ */
+ const unnestInterpreter = callIndex => {
+ const stackLen = callIndexStack.length;
+ (stackLen >= 1 && callIndexStack[stackLen - 1] === callIndex) ||
+ reject`Unexpected call stack close: ${q(callIndex)}`;
+ callIndexStack.pop();
+ if (hostLogIndex < hostLog.length) {
+ unnestFlag = true;
+ } else {
+ hostLogIndex === hostLog.length ||
+ reject`internal: unexpected hostLogIndex: ${q(hostLogIndex)}`;
+ }
+ };
+
+ const topDispatch = harden({
+ doFulfill,
+ doReject,
+ doCall,
+ checkFulfill,
+ checkReject,
+ });
+
+ const topInterpreter = async () => {
+ while (hostLogIndex < hostLog.length) {
+ try {
+ // eslint-disable-next-line no-await-in-loop
+ const optOutcome = interpretEntry(topDispatch, hostLog[hostLogIndex]);
+ if (unnestFlag) {
+ optOutcome || reject`only unnest with an outcome: ${q(hostLogIndex)}`;
+ unnestFlag = false;
+ return optOutcome;
+ }
+ } catch (problem) {
+ reject`Playback stopped due to ${q(problem)}`;
+ }
+ }
+ return undefined;
+ };
+
+ // ///////////////////////////////////////////////////////////////////////////
+
+ const convertCapH2G = (hostCap, _optIface = undefined) => {
+ if (memoH2G === undefined) {
+ throw optReason;
+ }
+ assert(memoG2H !== undefined);
+ if (memoH2G.has(hostCap)) {
+ return memoH2G.get(hostCap);
+ }
+ let guestCap;
+ const passStyle = passStyleOf(hostCap);
+ switch (passStyle) {
+ case 'promise': {
+ let guestResolve;
+ let guestReject;
+ guestCap = harden(
+ new Promise((res, rej) => {
+ guestResolve = res;
+ guestReject = rej;
+ }),
+ );
+ const hostResolve = hostFulfillment => {
+ guestResolve(hostToGuest(hostFulfillment));
+ };
+ const hostReject = hostReason => {
+ guestReject(hostToGuest(hostReason));
+ };
+ promiseMap.set(hostCap, harden({ hostResolve, hostReject }));
+ if (hostLogIndex >= hostLog.length) {
+ assert(hostLogIndex === hostLog.length);
+ // A real host, not the log replay
+ E.when(
+ hostCap,
+ hostFulfillment => doFulfill(hostCap, hostFulfillment),
+ hostReason => doReject(hostCap, hostReason),
+ );
+ }
+ break;
+ }
+ case 'remotable': {
+ /** @param {PropertyKey} [optVerb] */
+ const makeGuestMethod = (optVerb = undefined) => {
+ const guestMethod = (...guestArgs) => {
+ const callIndex = hostLogIndex;
+ return checkCall(guestCap, optVerb, guestArgs, callIndex);
+ };
+ if (optVerb) {
+ defineProperties(guestMethod, {
+ name: { value: String(optVerb) },
+ length: { value: Number(hostCap[optVerb].length || 0) },
+ });
+ } else {
+ defineProperties(guestMethod, {
+ name: { value: String(hostCap.name || 'anon') },
+ length: { value: Number(hostCap.length || 0) },
+ });
+ }
+ return guestMethod;
+ };
+ const iface = String(getInterfaceOf(hostCap) || 'unlabeled remotable');
+ if (typeof hostCap === '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.
+ guestCap = Remotable(iface, undefined, makeGuestMethod());
+ } else {
+ const methodNames = getMethodNames(hostCap);
+ const guestMethods = methodNames.map(name => [
+ name,
+ makeGuestMethod(name),
+ ]);
+ guestCap = Remotable(iface, undefined, fromEntries(guestMethods));
+ }
+ break;
+ }
+ default: {
+ reject`internal: Unrecognized passStyle ${passStyle}`;
+ }
+ }
+ memoH2G.set(hostCap, guestCap);
+ memoG2H.set(guestCap, hostCap);
+ return guestCap;
+ };
+
+ const { toCapData: hostToHostCapData, fromCapData: guestFromHostCapData } =
+ makeMarshal(
+ undefined, // undefined is identity
+ convertCapH2G, // convert a host slot to a guest value
+ {
+ serializeBodyFormat: 'smallcaps',
+ },
+ );
+
+ const hostToGuest = hostVal => {
+ const hostCapData = hostToHostCapData(harden(hostVal));
+ const guestVal = guestFromHostCapData(hostCapData);
+ return guestVal;
+ };
+
+ // ///////////////////////// Mirror image ////////////////////////////////////
+
+ const convertCapG2H = (guestCap, _optIface = undefined) => {
+ if (memoG2H === undefined) {
+ throw optReason;
+ }
+ assert(memoH2G !== undefined);
+ if (memoG2H.has(guestCap)) {
+ return memoG2H.get(guestCap);
+ }
+ let hostCap;
+ const passStyle = passStyleOf(guestCap);
+ switch (passStyle) {
+ case 'promise': {
+ let hostResolve;
+ let hostReject;
+ hostCap = harden(
+ new Promise((res, rej) => {
+ hostResolve = res;
+ hostReject = rej;
+ }),
+ );
+ promiseMap.set(hostCap, harden({ hostResolve, hostReject }));
+ E.when(
+ guestCap,
+ guestFulfillment => checkFulfill(guestCap, guestFulfillment),
+ guestReason => checkReject(guestCap, guestReason),
+ );
+ break;
+ }
+ case 'remotable': {
+ /** @param {PropertyKey} [optVerb] */
+ const makeHostMethod = (optVerb = undefined) => {
+ const hostMethod = (...hostArgs) => {
+ const callIndex = hostLogIndex;
+ return doCall(hostCap, optVerb, hostArgs, callIndex);
+ };
+ if (optVerb) {
+ defineProperties(hostMethod, {
+ name: { value: String(optVerb) },
+ length: { value: Number(guestCap[optVerb].length || 0) },
+ });
+ } else {
+ defineProperties(hostMethod, {
+ name: { value: String(guestCap.name || 'anon') },
+ length: { value: Number(guestCap.length || 0) },
+ });
+ }
+ return hostMethod;
+ };
+ const iface = String(getInterfaceOf(guestCap) || 'unlabeled remotable');
+ if (typeof guestCap === '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.
+ hostCap = Remotable(iface, undefined, makeHostMethod());
+ } else {
+ const methodNames = getMethodNames(guestCap);
+ const hostMethods = methodNames.map(name => [
+ name,
+ makeHostMethod(name),
+ ]);
+ hostCap = Remotable(iface, undefined, fromEntries(hostMethods));
+ }
+ break;
+ }
+ default: {
+ reject`internal: Unrecognized passStyle ${passStyle}`;
+ }
+ }
+ memoG2H.set(guestCap, hostCap);
+ memoH2G.set(hostCap, guestCap);
+ return hostCap;
+ };
+
+ const { toCapData: guestToHostCapData, fromCapData: hostFromHostCapData } =
+ makeMarshal(
+ // convert from my value to a yellow slot. undefined is identity.
+ convertCapG2H, // convert a guest value to a host slot
+ undefined, // undefined is identity
+ {
+ serializeBodyFormat: 'smallcaps',
+ },
+ );
+
+ const guestToHost = guestVal => {
+ const hostCapData = guestToHostCapData(harden(guestVal));
+ const hostVal = hostFromHostCapData(hostCapData);
+ return hostVal;
+ };
+
+ if (hostLogIndex < hostLog.length) {
+ const [firstVerb, firstArg, ..._rest] = hostLog[0];
+ firstVerb === 'doCall' ||
+ reject`First log entry be a "doCall", not ${q(firstVerb)}`;
+ const hostTarget = firstArg;
+ memoG2H.set(guestTarget, hostTarget);
+ memoH2G.set(hostTarget, guestTarget);
+ }
+
+ await topInterpreter();
+
+ return harden({
+ hostProxy: guestToHost(guestTarget),
+ });
+};
+harden(makeReplayMembraneKit);
diff --git a/packages/marshal/test/test-dot-membrane.js b/packages/marshal/test/test-dot-membrane.js
index b600d88913..a964b88ebc 100644
--- a/packages/marshal/test/test-dot-membrane.js
+++ b/packages/marshal/test/test-dot-membrane.js
@@ -3,22 +3,36 @@ import test from '@endo/ses-ava/prepare-endo.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 { yellowProxy: yellowSetState, yellowRevoke } =
+ makeDotMembraneKit(blueSetState);
+ 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']);
});
diff --git a/packages/marshal/test/test-replay-membrane.js b/packages/marshal/test/test-replay-membrane.js
new file mode 100644
index 0000000000..c5f083693f
--- /dev/null
+++ b/packages/marshal/test/test-replay-membrane.js
@@ -0,0 +1,90 @@
+import test from '@endo/ses-ava/prepare-endo.js';
+
+// eslint-disable-next-line import/order
+import { Far } from '@endo/pass-style';
+import { makeHostLogMembraneKit } from '../src/host-log-membrane.js';
+import { makeReplayMembraneKit } from '../src/replay-membrane.js';
+import { equalEnough } from '../src/equalEnough.js';
+
+test('test replay-membrane basics', async t => {
+ /** @type {any} */
+ let guestState;
+ const guestAsyncFuncR = Far(
+ 'guestSetState',
+ async (newGuestState, guestOrchestraW) => {
+ guestState = newGuestState;
+ const guestPauseWP = guestOrchestraW.getHostPauseP();
+ await guestPauseWP;
+ return Far('guestStateGetterR', {
+ getGuestState() {
+ return guestState;
+ },
+ });
+ },
+ );
+ const hostLog = [];
+ const { hostProxy: hostAsyncFuncW, revoke } = makeHostLogMembraneKit(
+ guestAsyncFuncR,
+ hostLog,
+ );
+ t.not(guestAsyncFuncR, hostAsyncFuncW);
+ const host88 = [88];
+ const host99 = [99];
+ const hostPauseRP = Promise.resolve('wait for it');
+ const hostOrchestraR = Far('hostOrchestra', {
+ getHostPauseP() {
+ return hostPauseRP;
+ },
+ });
+ const hostStateGetterWP = hostAsyncFuncW(host88, hostOrchestraR);
+ assert(guestState);
+ t.is(guestState[0], 88);
+ t.not(guestState, host88);
+ const hostStateGetterW = await hostStateGetterWP;
+ // eslint-disable-next-line no-underscore-dangle
+ const methodNames = hostStateGetterW.__getMethodNames__();
+ const hostState = hostStateGetterW.getGuestState();
+ t.is(hostState[0], 88);
+ revoke('Halt!');
+ t.throws(() => hostAsyncFuncW(host99), {
+ message: /Revoked: Halt!/,
+ });
+ t.throws(() => hostStateGetterW.getGuestState(), {
+ message: /Revoked: Halt!/,
+ });
+
+ t.is(guestState[0], 88);
+ t.deepEqual(methodNames, ['__getMethodNames__', 'getGuestState']);
+
+ const golden1 = harden([
+ ['doCall', hostAsyncFuncW, undefined, [[88], hostOrchestraR], 0],
+ ['checkCall', hostOrchestraR, 'getHostPauseP', [], 1],
+ ['doReturn', 1, hostPauseRP],
+ ['checkReturn', 0, hostStateGetterWP],
+ ['doFulfill', hostPauseRP, 'wait for it'],
+ ['checkFulfill', hostStateGetterWP, hostStateGetterW],
+ ['doCall', hostStateGetterW, '__getMethodNames__', [], 6],
+ ['checkReturn', 6, ['__getMethodNames__', 'getGuestState']],
+ ['doCall', hostStateGetterW, 'getGuestState', [], 8],
+ ['checkReturn', 8, [88]],
+ ]);
+ const golden2 = harden([
+ ['doCall', hostAsyncFuncW, undefined, [[88], hostOrchestraR], 0],
+ ['checkCall', hostOrchestraR, 'getHostPauseP', [], 1],
+ ['doReturn', 1, hostPauseRP],
+ ['checkReturn', 0, hostStateGetterWP],
+ ['doFulfill', hostPauseRP, 'wait for it'],
+ // ['checkFulfill', hostStateGetterWP, hostStateGetterW],
+ // ['doCall', hostStateGetterW, '__getMethodNames__', [], 6],
+ // ['checkReturn', 6, ['__getMethodNames__', 'getGuestState']],
+ // ['doCall', hostStateGetterW, 'getGuestState', [], 8],
+ // ['checkReturn', 8, [88]],
+ ]);
+ t.deepEqual(hostLog, golden1);
+ t.true(equalEnough(hostLog, golden1));
+
+ const { hostProxy: _hostSetState2 } = await makeReplayMembraneKit(
+ guestAsyncFuncR,
+ [...golden2],
+ );
+});