Skip to content

Commit

Permalink
feat(marshal): dot-membrane logs for replay
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Mar 9, 2024
1 parent 0a5c555 commit 99c9ae1
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 135 deletions.
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
290 changes: 160 additions & 130 deletions packages/marshal/src/dot-membrane.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,152 +2,182 @@
/// <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
* @param {[string, import('./types.js').CapData<any>][]} [optYellowLog]
*/
export const makeDotMembraneKit = (blueTarget, optYellowLog = undefined) => {
// 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;
}),
);
E.when(
mine,
myFulfillment => yourResolve(mineToYours(myFulfillment)),
myReason => yourReject(mineToYours(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(mineToYours(metaReason)),
)
.catch(metaMetaReason =>
// In case metaReason itself doesn't mineToYours
yourReject(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) {
throw mineToYours(harden(myReason));
}
return mineToYours(harden(myResult));
};
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);
if (optYellowLog) {
optYellowLog.push([myColor, 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

0 comments on commit 99c9ae1

Please sign in to comment.