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(marshal): dot-membrane logs for replay #2130

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
2 changes: 2 additions & 0 deletions packages/marshal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
293 changes: 162 additions & 131 deletions packages/marshal/src/dot-membrane.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,152 +2,183 @@
/// <reference types="ses"/>

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<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;
/**
* @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<any,any>=} */
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);
Loading
Loading