-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(internal): durable dot-membrane
- Loading branch information
Showing
4 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |