Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(internal): durable dot-membrane #9053

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
Loading