diff --git a/packages/internal/package.json b/packages/internal/package.json
index e5fc76a1869..fcb8d80d1d3 100755
--- a/packages/internal/package.json
+++ b/packages/internal/package.json
@@ -23,9 +23,12 @@
"@agoric/assert": "^0.6.0",
"@agoric/base-zone": "^0.1.0",
"@endo/common": "^1.1.0",
+ "@endo/errors": "^1.1.0",
+ "@endo/eventual-send": "^1.1.2",
"@endo/far": "^1.0.4",
"@endo/init": "^1.0.4",
"@endo/marshal": "^1.3.0",
+ "@endo/pass-style": "^1.2.0",
"@endo/patterns": "^1.2.0",
"@endo/promise-kit": "^1.0.4",
"@endo/stream": "^1.1.0",
@@ -34,6 +37,7 @@
},
"devDependencies": {
"@endo/init": "^1.0.4",
+ "@endo/ses-ava": "^1.1.2",
"ava": "^5.3.0",
"tsd": "^0.30.4"
},
diff --git a/packages/internal/src/durable-membrane.js b/packages/internal/src/durable-membrane.js
new file mode 100644
index 00000000000..68caaecff4a
--- /dev/null
+++ b/packages/internal/src/durable-membrane.js
@@ -0,0 +1,156 @@
+/* eslint-disable no-use-before-define */
+///
+
+import { E } from '@endo/eventual-send';
+import { getMethodNames } from '@endo/eventual-send/utils.js';
+import { isObject, getInterfaceOf, Far, passStyleOf } from '@endo/pass-style';
+import { Fail } from '@endo/errors';
+import { makeMarshal } from '@endo/marshal';
+
+const { fromEntries } = 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;
+ }
+ 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);
+
+ assert(!isObject(optVerb));
+ const myArgs = passBack(harden(yourArgs));
+ let myResult;
+
+ try {
+ myResult =
+ optVerb === undefined
+ ? mineIf(...myArgs)
+ : mineIf[optVerb](...myArgs);
+ } catch (myReason) {
+ throw pass(myReason);
+ }
+ return pass(myResult);
+ };
+ 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 = getMethodNames(mine);
+ const yourMethods = myMethodNames.map(name => [
+ name,
+ myMethodToYours(name),
+ ]);
+ yours = Far(iface, fromEntries(yourMethods));
+ }
+ break;
+ }
+ default: {
+ Fail`internal: Unrecognized passStyle ${passStyle}`;
+ }
+ }
+ 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 { toCapData: mySerialize, fromCapData: myUnserialize } = makeMarshal(
+ convertMineToYours,
+ convertSlotToVal,
+ {
+ serializeBodyFormat: 'smallcaps',
+ },
+ );
+ const pass = mine => {
+ const myCapData = mySerialize(mine);
+ const yours = yourUnserialize(myCapData);
+ return yours;
+ };
+ 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();
+ return harden({
+ proxy: converter.wrap(target),
+ revoke: converter.myRevoke,
+ });
+};
+harden(makeDotMembraneKit);
diff --git a/packages/internal/test/prepare-test-env-ava.js b/packages/internal/test/prepare-test-env-ava.js
new file mode 100644
index 00000000000..6037b658cd9
--- /dev/null
+++ b/packages/internal/test/prepare-test-env-ava.js
@@ -0,0 +1,7 @@
+import '@endo/init/debug.js';
+
+import rawTest from 'ava';
+import { wrapTest } from '@endo/ses-ava';
+
+/** @type {typeof rawTest} */
+export const test = wrapTest(rawTest);
diff --git a/packages/internal/test/test-durable-membrane.js b/packages/internal/test/test-durable-membrane.js
new file mode 100644
index 00000000000..a8fd4033d11
--- /dev/null
+++ b/packages/internal/test/test-durable-membrane.js
@@ -0,0 +1,27 @@
+// TODO Why was this compaining? `prepare-test-env-ava.js` is not a test file.
+// eslint-disable-next-line ava/no-import-test-files
+import { test } from './prepare-test-env-ava.js';
+
+// eslint-disable-next-line import/order
+import { Far } from '@endo/pass-style';
+import { makeDotMembraneKit } from '../src/durable-membrane.js';
+
+test('test dot-membrane basics', t => {
+ /** @type {any} */
+ let blueState;
+ const blueSetState = Far('blueSetState', newState => {
+ blueState = newState;
+ });
+ const { proxy: yellowSetState, revoke } = makeDotMembraneKit(blueSetState);
+ const yellow88 = [88];
+ const yellow99 = [99];
+ yellowSetState(yellow88);
+ assert(blueState);
+ t.is(blueState[0], 88);
+ t.not(blueState, yellow88);
+ revoke('Halt!');
+ t.throws(() => yellowSetState(yellow99), {
+ message: /Revoked: Halt!/,
+ });
+ t.is(blueState[0], 88);
+});