From 51859d4bb481cae126550f595f227a6ee1aaeb67 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Fri, 8 Mar 2024 19:30:55 -0800 Subject: [PATCH] feat(internal): durable dot-membrane --- packages/internal/package.json | 4 + packages/internal/src/durable-membrane.js | 156 ++++++++++++++++++ .../internal/test/prepare-test-env-ava.js | 7 + .../internal/test/test-durable-membrane.js | 27 +++ 4 files changed, 194 insertions(+) create mode 100644 packages/internal/src/durable-membrane.js create mode 100644 packages/internal/test/prepare-test-env-ava.js create mode 100644 packages/internal/test/test-durable-membrane.js 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); +});