Skip to content

Commit

Permalink
feat(internal): durable dot-membrane
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Mar 9, 2024
1 parent 5d5ee65 commit 51859d4
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -34,6 +37,7 @@
},
"devDependencies": {
"@endo/init": "^1.0.4",
"@endo/ses-ava": "^1.1.2",
"ava": "^5.3.0",
"tsd": "^0.30.4"
},
Expand Down
156 changes: 156 additions & 0 deletions packages/internal/src/durable-membrane.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* eslint-disable no-use-before-define */
/// <reference types="ses"/>

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<any,any>?} */
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);
7 changes: 7 additions & 0 deletions packages/internal/test/prepare-test-env-ava.js
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 27 additions & 0 deletions packages/internal/test/test-durable-membrane.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 51859d4

Please sign in to comment.