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

9449 membrane types #9685

Merged
merged 20 commits into from
Jul 11, 2024
Merged
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
1 change: 1 addition & 0 deletions packages/async-flow/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './src/async-flow.js';
export * from './src/types.js';
export { makeStateRecord } from './src/endowments.js';
7 changes: 4 additions & 3 deletions packages/async-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
"@agoric/internal": "^0.3.2",
"@agoric/store": "^0.9.2",
"@agoric/vow": "^0.1.0",
"@endo/pass-style": "^1.4.0",
"@endo/common": "^1.2.2",
"@endo/errors": "^1.2.2",
"@endo/eventual-send": "^1.2.2",
"@endo/marshal": "^1.5.0",
"@endo/pass-style": "^1.4.0",
"@endo/patterns": "^1.4.0",
"@endo/promise-kit": "^1.1.2"
},
Expand All @@ -41,7 +41,8 @@
"@agoric/zone": "^0.2.2",
"@endo/env-options": "^1.1.4",
"@endo/ses-ava": "^1.2.2",
"ava": "^5.3.0"
"ava": "^5.3.0",
"tsd": "^0.31.1"
},
"publishConfig": {
"access": "public"
Expand All @@ -60,6 +61,6 @@
"workerThreads": false
},
"typeCoverage": {
"atLeast": 77.83
"atLeast": 77.32
}
}
72 changes: 6 additions & 66 deletions packages/async-flow/src/replay-membrane.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
/* eslint-disable no-use-before-define */
import { isVow } from '@agoric/vow/src/vow-utils.js';
import { heapVowE } from '@agoric/vow/vat.js';
import { throwLabeled } from '@endo/common/throw-labeled.js';
import { Fail, X, b, makeError, q } from '@endo/errors';
import { E } from '@endo/eventual-send';
import { getMethodNames } from '@endo/eventual-send/utils.js';
import { throwLabeled } from '@endo/common/throw-labeled.js';
import { objectMap } from '@endo/common/object-map.js';
import {
Far,
Remotable,
getInterfaceOf,
getTag,
makeTagged,
passStyleOf,
} from '@endo/pass-style';
import { heapVowE } from '@agoric/vow/vat.js';
import { isVow } from '@agoric/vow/src/vow-utils.js';
import { makeEquate } from './equate.js';
import { Far, Remotable, getInterfaceOf } from '@endo/pass-style';
import { makeConvertKit } from './convert.js';
import { makeEquate } from './equate.js';

/**
* @import {PromiseKit} from '@endo/promise-kit'
Expand Down Expand Up @@ -43,7 +35,7 @@ export const makeReplayMembrane = ({
watchWake,
panic,
}) => {
const { when, watch, makeVowKit } = vowTools;
const { when, makeVowKit } = vowTools;

const equate = makeEquate(bijection);

Expand Down Expand Up @@ -137,63 +129,12 @@ export const makeReplayMembrane = ({

// ///////////// Guest to Host or consume log ////////////////////////////////

/**
* The host is not supposed to expose host-side promises to the membrane,
* since they cannot be stored durably or survive upgrade. We cannot just
* automatically wrap any such host promises with host vows, because that
* would mask upgrade hazards if an upgrade happens before the vow settles.
* However, during the transition, the current host APIs called by
* orchestration still return many promises. We want to generate diagnostics
* when we encounter them, but for now, automatically convert them to
* host vow anyway, just so integration testing can proceed to reveal
* additional problems beyond these.
*
* @param {Passable} h
*/
const tolerateHostPromiseToVow = h => {
const passStyle = passStyleOf(h);
switch (passStyle) {
case 'promise': {
const e = Error('where warning happened');
console.log('Warning for now: vow expected, not promise', h, e);
// TODO remove this stopgap. Here for now because host-side
// promises are everywhere!
// Note: A good place to set a breakpoint, or to uncomment the
// `debugger;` line, to work around bundling.
// debugger;
return watch(h);
}
case 'copyRecord': {
const o = /** @type {object} */ (h);
return objectMap(o, tolerateHostPromiseToVow);
}
case 'copyArray': {
const a = /** @type {Array} */ (h);
return harden(a.map(tolerateHostPromiseToVow));
}
case 'tagged': {
const t = /** @type {CopyTagged} */ (h);
if (isVow(t)) {
return h;
}
return makeTagged(getTag(t), tolerateHostPromiseToVow(t.payload));
}
default: {
return h;
}
}
};

const performCall = (hostTarget, optVerb, hostArgs, callIndex) => {
let hostResult;
try {
hostResult = optVerb
? hostTarget[optVerb](...hostArgs)
: hostTarget(...hostArgs);
// This is a temporary kludge anyway. But note that it only
// catches the case where the promise is at the top of hostResult.
harden(hostResult);
hostResult = tolerateHostPromiseToVow(hostResult);
// Try converting here just to route the error correctly
hostToGuest(hostResult, `converting ${optVerb || 'host'} result`);
} catch (hostProblem) {
Expand Down Expand Up @@ -575,7 +516,6 @@ export const makeReplayMembrane = ({
* @returns {Promise}
*/
const makeGuestForHostVow = (hVow, promiseKey = undefined) => {
hVow = tolerateHostPromiseToVow(hVow);
isVow(hVow) || Fail`vow expected ${hVow}`;
const { promise, resolve, reject } = makeGuestPromiseKit();
promiseKey ??= promise;
Expand Down
191 changes: 191 additions & 0 deletions packages/async-flow/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import type { Passable } from '@endo/pass-style';
import type { Vow, VowTools } from '@agoric/vow';
import type { LogStore } from './log-store.js';
import type { Bijection } from './bijection.js';
import type { EndowmentTools } from './endowments.js';

export type FlowState =
| 'Running'
| 'Sleeping'
| 'Replaying'
| 'Failed'
| 'Done';

/**
* `T` defaults to `any`, not `Passable`, because unwrapped guests include
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realize this is an existing comment, but consider an @internal directive

Copy link
Member Author

@turadg turadg Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I added it there was a jsdoc lint error the tag should be empty

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does @internal mean/do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9690 fixes the lint error. I'm still not sure whether this should be @internal.

* non-passables, like unwrapped functions and unwrapped state records.
* (Unwrapped functions could be made into Remotables,
* but since they still could not be made durable, in this context
* it'd be pointless.)
*/
export type Guest<T extends unknown = any> = T;
export type Host<T extends Passable = Passable> = T;

/**
* A HostVow must be durably storable. It corresponds to an
* ephemeral guest promise.
*/
export type HostVow<T extends Passable = Passable> = Host<Vow<T>>;

export type GuestAsyncFunc = (
...activationArgs: Guest[]
) => Guest<Promise<any>>;

export type HostAsyncFuncWrapper = (...activationArgs: Host[]) => HostVow;

/**
* The function from the host as it will be available in the guest.
*
* Specifically, Vow return values are converted to Promises.
*/
export type GuestOf<F extends HostAsyncFuncWrapper> = F extends (
...args: infer A
) => Vow<infer R>
? (...args: A) => Promise<R>
: F;
Comment on lines +41 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow!

Thanks for translating all those type declarations into better .d.ts types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tsc did the work! yarn prepack

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL! Good to know.


/**
* Convert an entire Guest interface into what the host will implement.
*/
type HostInterface<T> = {
[K in keyof T]: HostOf<T[K]>;
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on these. Do we also need a GuestInterface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we define an interface it should be in the view of the guest.

/**
* The function the host must provide to match an interface the guest expects.
*
* Specifically, Promise return values are converted to Vows.
*/
export type HostOf<F> = F extends (...args: infer A) => Promise<infer R>
? (...args: A) => Vow<R extends Passable ? R : HostInterface<R>>
: F;

export type PreparationOptions = {
vowTools?: VowTools;
makeLogStore?: (() => LogStore) | undefined;
makeBijection?: (() => Bijection) | undefined;
endowmentTools?: EndowmentTools;
};
export type OutcomeKind = 'return' | 'throw';

export type Outcome =
| {
kind: 'return';
result: any;
}
| {
kind: 'throw';
problem: any;
};

export type Ephemera<S extends WeakKey = WeakKey, V extends unknown = any> = {
for: (self: S) => V;
resetFor: (self: S) => void;
};

/**
* This is the type alias for the membrane log entries we currently implement.
*
* @see {FutureLogEntry} below for the full membrane log entry, which we do not
* yet support.
*/
export type LogEntry =
| [
// ///////////////// From Host to Guest /////////////////////////
op: 'doFulfill',
vow: HostVow,
fulfillment: Host,
]
| [op: 'doReject', vow: HostVow, reason: Host]
| [op: 'doReturn', callIndex: number, result: Host]
| [op: 'doThrow', callIndex: number, problem: Host]
| [
// ///////////////////// From Guest to Host /////////////////////////
op: 'checkCall',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'checkSendOnly',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'checkSend',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
];

/**
* This would be the type alias for the full membrane log, if we supported:
* - the guest sending guest-promises and guest-remotables to the host
* - the guest using `E` to eventual-send to guest wrappers of the host
* vows and remotables.
*/
export type FutureLogEntry =
| [
// ///////////////// From Host to Guest ///////////////////////
op: 'doFulfill',
vow: HostVow,
fulfillment: Host,
]
| [op: 'doReject', vow: HostVow, reason: Host]
| [
op: 'doCall',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'doSendOnly',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'doSend',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [op: 'doReturn', callIndex: number, result: Host]
| [op: 'doThrow', callIndex: number, problem: Host]
| [
// ///////////////////// From Guest to Host /////////////////////////
op: 'checkFulfill',
vow: HostVow,
fulfillment: Host,
]
| [op: 'checkReject', vow: HostVow, reason: Host]
| [
op: 'checkCall',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'checkSendOnly',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [
op: 'checkSend',
target: Host,
optVerb: PropertyKey | undefined,
args: Host[],
callIndex: number,
]
| [op: 'checkReturn', callIndex: number, result: Host]
| [op: 'checkThrow', callIndex: number, problem: Host];
Loading
Loading