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(async-flow): endowments #9566

Merged
merged 12 commits into from
Jun 25, 2024
2 changes: 1 addition & 1 deletion packages/async-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"license": "Apache-2.0",
"dependencies": {
"@agoric/base-zone": "^0.1.0",
"@agoric/internal": "^0.3.2",
"@agoric/store": "^0.9.2",
"@agoric/vow": "^0.1.0",
"@endo/pass-style": "^1.4.0",
Expand All @@ -36,7 +37,6 @@
"@endo/promise-kit": "^1.1.2"
},
"devDependencies": {
"@agoric/internal": "^0.3.2",
"@agoric/swingset-liveslots": "^0.10.2",
"@agoric/zone": "^0.2.2",
"@endo/env-options": "^1.1.4",
Expand Down
30 changes: 17 additions & 13 deletions packages/async-flow/src/async-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import { prepareVowTools, toPassableCap, VowShape } from '@agoric/vow';
import { makeReplayMembrane } from './replay-membrane.js';
import { prepareLogStore } from './log-store.js';
import { prepareBijection } from './bijection.js';
import { prepareEndowmentTools } from './endowments.js';
import { LogEntryShape, FlowStateShape } from './type-guards.js';

/**
* @import {WeakMapStore} from '@agoric/store'
* @import {PromiseKit} from '@endo/promise-kit'
* @import {Zone} from '@agoric/base-zone'
* @import {MapStore} from '@agoric/store';
* @import {LogStore} from '../src/log-store.js';
* @import {Bijection} from '../src/bijection.js';
* @import {FlowState, GuestAsyncFunc, HostAsyncFuncWrapper, PreparationOptions} from '../src/types.js';
* @import {ReplayMembrane} from '../src/replay-membrane.js';
* @import {FlowState, GuestAsyncFunc, HostAsyncFuncWrapper, PreparationOptions} from '../src/types.js'
* @import {ReplayMembrane} from '../src/replay-membrane.js'
*/

const { defineProperties } = Object;
Expand Down Expand Up @@ -53,7 +50,11 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
const {
vowTools = prepareVowTools(outerZone),
makeLogStore = prepareLogStore(outerZone),
makeBijection = prepareBijection(outerZone),
endowmentTools: { prepareEndowment, unwrap } = prepareEndowmentTools(
outerZone,
{ vowTools },
),
makeBijection = prepareBijection(outerZone, unwrap),
} = outerOptions;
const { watch, makeVowKit } = vowTools;

Expand Down Expand Up @@ -177,7 +178,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
eagerWakers.delete(flow);
}

const wakeWatch = vowish => {
const watchWake = vowish => {
// Extra paranoid because we're getting
// "promise watcher must be a virtual object"
// in the general vicinity.
Expand All @@ -188,13 +189,13 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
watch(vowish, wakeWatcher);
};
const panic = err => admin.panic(err);
const membrane = makeReplayMembrane(
const membrane = makeReplayMembrane({
log,
bijection,
vowTools,
wakeWatch,
watchWake,
panic,
);
});
initMembrane(flow, membrane);
const guestArgs = membrane.hostToGuest(activationArgs);

Expand Down Expand Up @@ -225,7 +226,9 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
// gating condition, the next line could grow the bijection
// of a failed flow, subverting other gating checks on bijection
// membership.
bijection.init(guestResultP, outcomeKit.vow);
const g = bijection.unwrapInit(guestResultP, outcomeKit.vow);
g === guestResultP ||
Fail`internal: promises should not be unwrapped ${g}`;
}
// log is driven at first by guestAyncFunc interaction through the
// membrane with the host activationArgs. At the end of its first
Expand Down Expand Up @@ -422,7 +425,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
const asyncFlowKit = internalMakeAsyncFlowKit(activationArgs);
const { flow } = asyncFlowKit;

const vow = toPassableCap(flow.getOutcome());
const vow = flow.getOutcome();
flowForOutcomeVowKey.init(toPassableCap(vow), flow);
flow.restart();
return asyncFlowKit;
Expand Down Expand Up @@ -484,6 +487,7 @@ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
asyncFlow,
adminAsyncFlow,
allWokenP,
prepareEndowment,
});
};
harden(prepareAsyncFlowTools);
Expand Down
64 changes: 48 additions & 16 deletions packages/async-flow/src/bijection.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { b, Fail } from '@endo/errors';
import { M } from '@endo/patterns';
import { Far } from '@endo/pass-style';
import { Far, isPassable } from '@endo/pass-style';
import { toPassableCap } from '@agoric/vow';
import { makeEphemera } from './ephemera.js';

/**
* @import {PromiseKit} from '@endo/promise-kit'
* @import {PassableCap} from '@endo/pass-style'
* @import {Zone} from '@agoric/base-zone'
* @import {Vow} from '@agoric/vow'
* @import {Ephemera} from './types.js';
*/

const BijectionI = M.interface('Bijection', {
reset: M.call().returns(),
init: M.call(M.any(), M.any()).returns(),
hasGuest: M.call(M.any()).returns(M.boolean()),
unwrapInit: M.call(M.raw(), M.any()).returns(M.raw()),
hasGuest: M.call(M.raw()).returns(M.boolean()),
hasHost: M.call(M.any()).returns(M.boolean()),
has: M.call(M.any(), M.any()).returns(M.boolean()),
guestToHost: M.call(M.any()).returns(M.any()),
hostToGuest: M.call(M.any()).returns(M.any()),
has: M.call(M.raw(), M.any()).returns(M.boolean()),
guestToHost: M.call(M.raw()).returns(M.any()),
hostToGuest: M.call(M.any()).returns(M.raw()),
});

/**
* @param {unknown} k
*/
const toKey = k =>
// @ts-expect-error k specificity
isPassable(k) ? toPassableCap(k) : k;

/**
* Makes a store like a WeakMapStore except that Promises and Vows can also be
* used as keys.
Expand All @@ -40,15 +48,15 @@ const makeVowishStore = name => {

return Far(name, {
init: (k, v) => {
const k2 = toPassableCap(k);
const k2 = toKey(k);
!map.has(k2) ||
// separate line so I can set a breakpoint
Fail`${b(name)} key already bound: ${k} -> ${map.get(k2)} vs ${v}`;
map.set(k2, v);
},
has: k => map.has(toPassableCap(k)),
has: k => map.has(toKey(k)),
get: k => {
const k2 = toPassableCap(k);
const k2 = toKey(k);
map.has(k2) ||
// separate line so I can set a breakpoint
Fail`${b(name)} key not found: ${k}`;
Expand All @@ -61,34 +69,53 @@ const makeVowishStore = name => {

/**
* @param {Zone} zone
erights marked this conversation as resolved.
Show resolved Hide resolved
* @param {(hostWrapper: PassableCap | Vow, guestWrapper: PassableCap) => unknown} [unwrap]
* defaults to identity function on `guestWrapper` arg
*/
export const prepareBijection = zone => {
export const prepareBijection = (
zone,
unwrap = (_hostWrapper, guestWrapper) => guestWrapper,
) => {
/** @type {Ephemera<Bijection, VowishStore>} */
const g2h = makeEphemera(() => makeVowishStore('guestToHost'));
/** @type {Ephemera<Bijection, VowishStore>} */
const h2g = makeEphemera(() => makeVowishStore('hostToGuest'));

// Guest arguments are results are now unguarded, i.e., guarded by `M.raw()`,
erights marked this conversation as resolved.
Show resolved Hide resolved
// so that they can be non-passables. Therefore, we need to harden these
// here.
return zone.exoClass('Bijection', BijectionI, () => ({}), {
reset() {
const { self } = this;

g2h.resetFor(self);
h2g.resetFor(self);
},
init(g, h) {
unwrapInit(g, h) {
harden(g);
const { self } = this;
const guestToHost = g2h.for(self);
const hostToGuest = h2g.for(self);

const gUnwrapped = unwrap(h, g);
!hostToGuest.has(h) ||
Fail`hostToGuest key already bound: ${h} -> ${hostToGuest.get(h)} vs ${g}`;
guestToHost.init(g, h);
hostToGuest.init(h, g);
self.has(g, h) ||
Fail`hostToGuest key already bound: ${h} -> ${hostToGuest.get(h)} vs ${gUnwrapped}`;
guestToHost.init(gUnwrapped, h);
hostToGuest.init(h, gUnwrapped);
self.has(gUnwrapped, h) ||
// separate line so I can set a breakpoint
Fail`internal: ${g} <-> ${h}`;
if (g !== gUnwrapped) {
// When they are different, also map g to h without mapping h to g
!guestToHost.has(g) ||
// separate line so I can set a breakpoint
Fail`hidden guest wrapper already bound ${g}`;
guestToHost.init(g, h);
}
return gUnwrapped;
},
hasGuest(g) {
harden(g);
const { self } = this;
const guestToHost = g2h.for(self);

Expand All @@ -101,6 +128,7 @@ export const prepareBijection = zone => {
return hostToGuest.has(h);
},
has(g, h) {
harden(g);
const { self } = this;
const guestToHost = g2h.for(self);
const hostToGuest = h2g.for(self);
Expand All @@ -118,6 +146,7 @@ export const prepareBijection = zone => {
}
},
guestToHost(g) {
harden(g);
const { self } = this;
const guestToHost = g2h.for(self);

Expand All @@ -127,6 +156,9 @@ export const prepareBijection = zone => {
const { self } = this;
const hostToGuest = h2g.for(self);

// Even though result is unguarded, i.e., guarded by `M.raw()`, don't
// need to harden here because was already harden when added to
// collection.
return hostToGuest.get(h);
},
});
Expand Down
6 changes: 2 additions & 4 deletions packages/async-flow/src/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,14 @@ export const makeConvertKit = (
return bijection.hostToGuest(hRem);
}
const gRem = makeGuestForHostRemotable(hRem);
bijection.init(gRem, hRem);
return gRem;
return bijection.unwrapInit(gRem, hRem);
},
hVow => {
if (bijection.hasHost(hVow)) {
return bijection.hostToGuest(hVow);
}
const gP = makeGuestForHostVow(hVow);
bijection.init(gP, hVow);
return gP;
return bijection.unwrapInit(gP, hVow);
},
hErr => {
const gErr = harden(
Expand Down
Loading
Loading