@agoric/async-flow 0.1.1-calypso-dev-84eb287.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/LICENSE +201 -0
- package/README.md +40 -0
- package/docs/async-flow-states.key +0 -0
- package/docs/async-flow-states.md +15 -0
- package/docs/async-flow-states.png +0 -0
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -0
- package/index.js +2 -0
- package/package.json +66 -0
- package/src/async-flow.d.ts +94 -0
- package/src/async-flow.d.ts.map +1 -0
- package/src/async-flow.js +520 -0
- package/src/bijection.d.ts +31 -0
- package/src/bijection.d.ts.map +1 -0
- package/src/bijection.js +207 -0
- package/src/convert.d.ts +6 -0
- package/src/convert.d.ts.map +1 -0
- package/src/convert.js +133 -0
- package/src/endowments.d.ts +16 -0
- package/src/endowments.d.ts.map +1 -0
- package/src/endowments.js +292 -0
- package/src/ephemera.d.ts +3 -0
- package/src/ephemera.d.ts.map +1 -0
- package/src/ephemera.js +39 -0
- package/src/equate.d.ts +2 -0
- package/src/equate.d.ts.map +1 -0
- package/src/equate.js +123 -0
- package/src/log-store.d.ts +27 -0
- package/src/log-store.d.ts.map +1 -0
- package/src/log-store.js +169 -0
- package/src/replay-membrane.d.ts +40 -0
- package/src/replay-membrane.d.ts.map +1 -0
- package/src/replay-membrane.js +752 -0
- package/src/type-guards.d.ts +4 -0
- package/src/type-guards.d.ts.map +1 -0
- package/src/type-guards.js +68 -0
- package/src/types.d.ts +67 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +196 -0
- package/test/async-flow-crank.test.js +102 -0
- package/test/async-flow-early-completion.test.js +203 -0
- package/test/async-flow-no-this.js +65 -0
- package/test/async-flow.test.js +383 -0
- package/test/bad-host.test.js +210 -0
- package/test/bijection.test.js +124 -0
- package/test/convert.test.js +132 -0
- package/test/endowments.test.js +157 -0
- package/test/equate.test.js +120 -0
- package/test/log-store.test.js +120 -0
- package/test/prepare-test-env-ava.js +28 -0
- package/test/replay-membrane-eventual.test.js +217 -0
- package/test/replay-membrane-settlement.test.js +173 -0
- package/test/replay-membrane-zombie.test.js +187 -0
- package/test/replay-membrane.test.js +297 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +13 -0
- package/typedoc.json +8 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/* eslint-disable no-use-before-define */
|
|
2
|
+
import { Fail, X, b, makeError, q } from '@endo/errors';
|
|
3
|
+
import {
|
|
4
|
+
Far,
|
|
5
|
+
Remotable,
|
|
6
|
+
getInterfaceOf,
|
|
7
|
+
getTag,
|
|
8
|
+
makeTagged,
|
|
9
|
+
passStyleOf,
|
|
10
|
+
} from '@endo/pass-style';
|
|
11
|
+
import { E } from '@endo/eventual-send';
|
|
12
|
+
import { throwLabeled } from '@endo/common/throw-labeled.js';
|
|
13
|
+
import { heapVowE } from '@agoric/vow/vat.js';
|
|
14
|
+
import { getMethodNames } from '@endo/eventual-send/utils.js';
|
|
15
|
+
import { objectMap } from '@endo/common/object-map.js';
|
|
16
|
+
import { isVow } from '@agoric/vow/src/vow-utils.js';
|
|
17
|
+
import { makeEquate } from './equate.js';
|
|
18
|
+
import { makeConvertKit } from './convert.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @import {PromiseKit} from '@endo/promise-kit'
|
|
22
|
+
* @import {Passable, PassableCap, CopyTagged} from '@endo/pass-style'
|
|
23
|
+
* @import {Vow, VowTools, VowKit} from '@agoric/vow'
|
|
24
|
+
* @import {LogStore} from '../src/log-store.js';
|
|
25
|
+
* @import {Bijection} from '../src/bijection.js';
|
|
26
|
+
* @import {Host, HostVow, LogEntry, Outcome} from '../src/types.js';
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { fromEntries, defineProperties, assign } = Object;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} arg
|
|
33
|
+
* @param {LogStore} arg.log
|
|
34
|
+
* @param {Bijection} arg.bijection
|
|
35
|
+
* @param {VowTools} arg.vowTools
|
|
36
|
+
* @param {(vowish: Promise | Vow) => void} arg.watchWake
|
|
37
|
+
* @param {(problem: Error) => never} arg.panic
|
|
38
|
+
*/
|
|
39
|
+
export const makeReplayMembrane = ({
|
|
40
|
+
log,
|
|
41
|
+
bijection,
|
|
42
|
+
vowTools,
|
|
43
|
+
watchWake,
|
|
44
|
+
panic,
|
|
45
|
+
}) => {
|
|
46
|
+
const { when, watch, makeVowKit } = vowTools;
|
|
47
|
+
|
|
48
|
+
const equate = makeEquate(bijection);
|
|
49
|
+
|
|
50
|
+
const guestPromiseMap = new WeakMap();
|
|
51
|
+
|
|
52
|
+
let stopped = false;
|
|
53
|
+
|
|
54
|
+
const Panic = (template, ...args) => panic(makeError(X(template, ...args)));
|
|
55
|
+
|
|
56
|
+
// ////////////// Host or Interpreter to Guest ///////////////////////////////
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When replaying, this comes from interpreting the log.
|
|
60
|
+
* Otherwise, it is triggered by a watcher watching hostVow,
|
|
61
|
+
* that must also log it.
|
|
62
|
+
*
|
|
63
|
+
* @param {HostVow} hostVow
|
|
64
|
+
* @param {Host} hostFulfillment
|
|
65
|
+
*/
|
|
66
|
+
const doFulfill = (hostVow, hostFulfillment) => {
|
|
67
|
+
const guestPromise = hostToGuest(hostVow);
|
|
68
|
+
const status = guestPromiseMap.get(guestPromise);
|
|
69
|
+
if (!status || status === 'settled') {
|
|
70
|
+
Fail`doFulfill should only be called on a registered unresolved promise`;
|
|
71
|
+
}
|
|
72
|
+
const guestFulfillment = hostToGuest(hostFulfillment);
|
|
73
|
+
status.resolve(guestFulfillment);
|
|
74
|
+
guestPromiseMap.set(guestPromise, 'settled');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* When replaying, this comes from interpreting the log.
|
|
79
|
+
* Otherwise, it is triggered by a watcher watching hostVow,
|
|
80
|
+
* that must also log it.
|
|
81
|
+
*
|
|
82
|
+
* @param {HostVow} hostVow
|
|
83
|
+
* @param {Host} hostReason
|
|
84
|
+
*/
|
|
85
|
+
const doReject = (hostVow, hostReason) => {
|
|
86
|
+
const guestPromise = hostToGuest(hostVow);
|
|
87
|
+
const status = guestPromiseMap.get(guestPromise);
|
|
88
|
+
if (!status || status === 'settled') {
|
|
89
|
+
Fail`doReject should only be called on a registered unresolved promise`;
|
|
90
|
+
}
|
|
91
|
+
const guestReason = hostToGuest(hostReason);
|
|
92
|
+
status.reject(guestReason);
|
|
93
|
+
guestPromiseMap.set(guestPromise, 'settled');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* When replaying, after the guest thinks it has called a host method,
|
|
98
|
+
* triggering `checkCall`, that host method emulator consumes one of
|
|
99
|
+
* these entries from the log to return what it is supposed to.
|
|
100
|
+
* It returns an Outcome describing either a throw or return, because we
|
|
101
|
+
* reserve the actual throw channels for replay errors and internal
|
|
102
|
+
* errors.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} callIndex
|
|
105
|
+
* @param {Host} hostResult
|
|
106
|
+
* @returns {Outcome}
|
|
107
|
+
*/
|
|
108
|
+
const doReturn = (callIndex, hostResult) => {
|
|
109
|
+
unnestInterpreter(callIndex);
|
|
110
|
+
const guestResult = hostToGuest(hostResult);
|
|
111
|
+
return harden({
|
|
112
|
+
kind: 'return',
|
|
113
|
+
result: guestResult,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* When replaying, after the guest thinks it has called a host method,
|
|
119
|
+
* triggering `checkCall`, that host method emulator consumes one of
|
|
120
|
+
* these entries from the log to return what it is supposed to.
|
|
121
|
+
* It returns an Outcome describing either a throw or return, because we
|
|
122
|
+
* reserve the actual throw channels for replay errors and internal
|
|
123
|
+
* errors.
|
|
124
|
+
*
|
|
125
|
+
* @param {number} callIndex
|
|
126
|
+
* @param {Host} hostProblem
|
|
127
|
+
* @returns {Outcome}
|
|
128
|
+
*/
|
|
129
|
+
const doThrow = (callIndex, hostProblem) => {
|
|
130
|
+
unnestInterpreter(callIndex);
|
|
131
|
+
const guestProblem = hostToGuest(hostProblem);
|
|
132
|
+
return harden({
|
|
133
|
+
kind: 'throw',
|
|
134
|
+
problem: guestProblem,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ///////////// Guest to Host or consume log ////////////////////////////////
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The host is not supposed to expose host-side promises to the membrane,
|
|
142
|
+
* since they cannot be stored durably or survive upgrade. We cannot just
|
|
143
|
+
* automatically wrap any such host promises with host vows, because that
|
|
144
|
+
* would mask upgrade hazards if an upgrade happens before the vow settles.
|
|
145
|
+
* However, during the transition, the current host APIs called by
|
|
146
|
+
* orchestration still return many promises. We want to generate diagnostics
|
|
147
|
+
* when we encounter them, but for now, automatically convert them to
|
|
148
|
+
* host vow anyway, just so integration testing can proceed to reveal
|
|
149
|
+
* additional problems beyond these.
|
|
150
|
+
*
|
|
151
|
+
* @param {Passable} h
|
|
152
|
+
*/
|
|
153
|
+
const tolerateHostPromiseToVow = h => {
|
|
154
|
+
const passStyle = passStyleOf(h);
|
|
155
|
+
switch (passStyle) {
|
|
156
|
+
case 'promise': {
|
|
157
|
+
const e = Error('where warning happened');
|
|
158
|
+
console.log('Warning for now: vow expected, not promise', h, e);
|
|
159
|
+
// TODO remove this stopgap. Here for now because host-side
|
|
160
|
+
// promises are everywhere!
|
|
161
|
+
// Note: A good place to set a breakpoint, or to uncomment the
|
|
162
|
+
// `debugger;` line, to work around bundling.
|
|
163
|
+
// debugger;
|
|
164
|
+
return watch(h);
|
|
165
|
+
}
|
|
166
|
+
case 'copyRecord': {
|
|
167
|
+
const o = /** @type {object} */ (h);
|
|
168
|
+
return objectMap(o, tolerateHostPromiseToVow);
|
|
169
|
+
}
|
|
170
|
+
case 'copyArray': {
|
|
171
|
+
const a = /** @type {Array} */ (h);
|
|
172
|
+
return harden(a.map(tolerateHostPromiseToVow));
|
|
173
|
+
}
|
|
174
|
+
case 'tagged': {
|
|
175
|
+
const t = /** @type {CopyTagged} */ (h);
|
|
176
|
+
if (isVow(t)) {
|
|
177
|
+
return h;
|
|
178
|
+
}
|
|
179
|
+
return makeTagged(getTag(t), tolerateHostPromiseToVow(t.payload));
|
|
180
|
+
}
|
|
181
|
+
default: {
|
|
182
|
+
return h;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const performCall = (hostTarget, optVerb, hostArgs, callIndex) => {
|
|
188
|
+
let hostResult;
|
|
189
|
+
try {
|
|
190
|
+
hostResult = optVerb
|
|
191
|
+
? hostTarget[optVerb](...hostArgs)
|
|
192
|
+
: hostTarget(...hostArgs);
|
|
193
|
+
// This is a temporary kludge anyway. But note that it only
|
|
194
|
+
// catches the case where the promise is at the top of hostResult.
|
|
195
|
+
harden(hostResult);
|
|
196
|
+
hostResult = tolerateHostPromiseToVow(hostResult);
|
|
197
|
+
// Try converting here just to route the error correctly
|
|
198
|
+
hostToGuest(hostResult, `converting ${optVerb || 'host'} result`);
|
|
199
|
+
} catch (hostProblem) {
|
|
200
|
+
return logDo(nestDispatch, harden(['doThrow', callIndex, hostProblem]));
|
|
201
|
+
}
|
|
202
|
+
return logDo(nestDispatch, harden(['doReturn', callIndex, hostResult]));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const guestCallsHost = (guestTarget, optVerb, guestArgs, callIndex) => {
|
|
206
|
+
if (stopped || !bijection.hasGuest(guestTarget)) {
|
|
207
|
+
// This happens in a delayed guest-to-host call from a previous run.
|
|
208
|
+
// In that case, the bijection was reset and all guest caps
|
|
209
|
+
// created in the previous run were unregistered,
|
|
210
|
+
// including guestTarget.
|
|
211
|
+
// Throwing an error back to the old guest caller may cause
|
|
212
|
+
// it to proceed in all sorts of crazy ways. But that old run
|
|
213
|
+
// should now be isolated and unable to cause any observable effects.
|
|
214
|
+
// Well, except for resource exhaustion including infinite loops,
|
|
215
|
+
// which would be a genuine problem.
|
|
216
|
+
//
|
|
217
|
+
// Console logging of unhandled rejections, errors thrown to the top
|
|
218
|
+
// of the event loop, or anything else are not problematic effects.
|
|
219
|
+
// At this level of abstraction, we don't consider console logging
|
|
220
|
+
// activity to be observable. Thus, it is also ok for the guest
|
|
221
|
+
// function, which should otherwise be closed, to
|
|
222
|
+
// capture (lexically "close over") the `console`.
|
|
223
|
+
const extraDiagnostic =
|
|
224
|
+
callStack.length === 0
|
|
225
|
+
? ''
|
|
226
|
+
: // This case should only happen when the callStack is empty
|
|
227
|
+
` with non-empty callstack ${q(callStack)};`;
|
|
228
|
+
Fail`Called from a previous run: ${guestTarget}${b(extraDiagnostic)}`;
|
|
229
|
+
}
|
|
230
|
+
/** @type {Outcome} */
|
|
231
|
+
let outcome;
|
|
232
|
+
try {
|
|
233
|
+
const guestEntry = harden([
|
|
234
|
+
'checkCall',
|
|
235
|
+
guestTarget,
|
|
236
|
+
optVerb,
|
|
237
|
+
guestArgs,
|
|
238
|
+
callIndex,
|
|
239
|
+
]);
|
|
240
|
+
if (log.isReplaying()) {
|
|
241
|
+
const entry = log.nextEntry();
|
|
242
|
+
equate(
|
|
243
|
+
guestEntry,
|
|
244
|
+
entry,
|
|
245
|
+
`replay ${callIndex}:
|
|
246
|
+
${q(guestEntry)}
|
|
247
|
+
vs ${q(entry)}
|
|
248
|
+
`,
|
|
249
|
+
);
|
|
250
|
+
outcome = /** @type {Outcome} */ (nestInterpreter(callIndex));
|
|
251
|
+
} else {
|
|
252
|
+
const entry = guestToHost(guestEntry);
|
|
253
|
+
log.pushEntry(entry);
|
|
254
|
+
const [_, ...args] = entry;
|
|
255
|
+
nestInterpreter(callIndex);
|
|
256
|
+
outcome = performCall(...args);
|
|
257
|
+
}
|
|
258
|
+
} catch (fatalError) {
|
|
259
|
+
throw panic(fatalError);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
switch (outcome.kind) {
|
|
263
|
+
case 'return': {
|
|
264
|
+
return outcome.result;
|
|
265
|
+
}
|
|
266
|
+
case 'throw': {
|
|
267
|
+
throw outcome.problem;
|
|
268
|
+
}
|
|
269
|
+
default: {
|
|
270
|
+
// @ts-expect-error TS correctly knows this case would be outside
|
|
271
|
+
// the type. But that's what we want to check.
|
|
272
|
+
throw Panic`unexpected outcome kind ${q(outcome.kind)}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// //////////////// Eventual Send ////////////////////////////////////////////
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {PassableCap} hostTarget
|
|
281
|
+
* @param {string | undefined} optVerb
|
|
282
|
+
* @param {Passable[]} hostArgs
|
|
283
|
+
*/
|
|
284
|
+
const performSendOnly = (hostTarget, optVerb, hostArgs) => {
|
|
285
|
+
try {
|
|
286
|
+
optVerb
|
|
287
|
+
? heapVowE.sendOnly(hostTarget)[optVerb](...hostArgs)
|
|
288
|
+
: // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
|
289
|
+
// @ts-ignore once we changed this from E to heapVowE,
|
|
290
|
+
// typescript started complaining that heapVowE(hostTarget)
|
|
291
|
+
// is not callable. I'm not sure if this is a just a typing bug
|
|
292
|
+
// in heapVowE or also reflects a runtime deficiency. But this
|
|
293
|
+
// case it not used yet anyway. We disable it
|
|
294
|
+
// with at-ts-ignore rather than at-ts-expect-error because
|
|
295
|
+
// the dependency-graph tests complains that the latter is unused.
|
|
296
|
+
heapVowE.sendOnly(hostTarget)(...hostArgs);
|
|
297
|
+
} catch (hostProblem) {
|
|
298
|
+
throw Panic`internal: eventual sendOnly synchrously failed ${hostProblem}`;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {PassableCap} hostTarget
|
|
304
|
+
* @param {string | undefined} optVerb
|
|
305
|
+
* @param {Passable[]} hostArgs
|
|
306
|
+
* @param {number} callIndex
|
|
307
|
+
* @param {VowKit} hostResultKit
|
|
308
|
+
* @param {Promise} guestReturnedP
|
|
309
|
+
* @returns {Outcome}
|
|
310
|
+
*/
|
|
311
|
+
const performSend = (
|
|
312
|
+
hostTarget,
|
|
313
|
+
optVerb,
|
|
314
|
+
hostArgs,
|
|
315
|
+
callIndex,
|
|
316
|
+
hostResultKit,
|
|
317
|
+
guestReturnedP,
|
|
318
|
+
) => {
|
|
319
|
+
const { vow, resolver } = hostResultKit;
|
|
320
|
+
try {
|
|
321
|
+
const hostPromise = optVerb
|
|
322
|
+
? heapVowE(hostTarget)[optVerb](...hostArgs)
|
|
323
|
+
: // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
|
324
|
+
// @ts-ignore once we changed this from E to heapVowE,
|
|
325
|
+
// typescript started complaining that heapVowE(hostTarget)
|
|
326
|
+
// is not callable. I'm not sure if this is a just a typing bug
|
|
327
|
+
// in heapVowE or also reflects a runtime deficiency. But this
|
|
328
|
+
// case it not used yet anyway. We disable it
|
|
329
|
+
// with at-ts-ignore rather than at-ts-expect-error because
|
|
330
|
+
// the dependency-graph tests complains that the latter is unused.
|
|
331
|
+
heapVowE(hostTarget)(...hostArgs);
|
|
332
|
+
resolver.resolve(hostPromise); // TODO does this always work?
|
|
333
|
+
} catch (hostProblem) {
|
|
334
|
+
throw Panic`internal: eventual send synchrously failed ${hostProblem}`;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const entry = harden(['doReturn', callIndex, vow]);
|
|
338
|
+
log.pushEntry(entry);
|
|
339
|
+
const guestPromise = makeGuestForHostVow(vow, guestReturnedP);
|
|
340
|
+
// Note that `guestPromise` is not registered in the bijection since
|
|
341
|
+
// guestReturnedP is already the guest for vow. Rather, the handler
|
|
342
|
+
// returns guestPromise to resolve guestReturnedP to guestPromise.
|
|
343
|
+
doReturn(callIndex, vow);
|
|
344
|
+
return harden({
|
|
345
|
+
kind: 'return',
|
|
346
|
+
result: guestPromise,
|
|
347
|
+
});
|
|
348
|
+
} catch (problem) {
|
|
349
|
+
throw panic(problem);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const guestHandler = harden({
|
|
354
|
+
applyMethodSendOnly(guestTarget, optVerb, guestArgs) {
|
|
355
|
+
const callIndex = log.getIndex();
|
|
356
|
+
if (stopped || !bijection.hasGuest(guestTarget)) {
|
|
357
|
+
Fail`Sent from a previous run: ${guestTarget}`;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const guestEntry = harden([
|
|
361
|
+
'checkSendOnly',
|
|
362
|
+
guestTarget,
|
|
363
|
+
optVerb,
|
|
364
|
+
guestArgs,
|
|
365
|
+
callIndex,
|
|
366
|
+
]);
|
|
367
|
+
if (log.isReplaying()) {
|
|
368
|
+
const entry = log.nextEntry();
|
|
369
|
+
try {
|
|
370
|
+
equate(guestEntry, entry);
|
|
371
|
+
} catch (equateErr) {
|
|
372
|
+
// TODO consider Richard Gibson's suggestion for a better way
|
|
373
|
+
// to keep track of the error labeling.
|
|
374
|
+
throwLabeled(
|
|
375
|
+
equateErr,
|
|
376
|
+
`replay ${callIndex}:
|
|
377
|
+
${q(guestEntry)}
|
|
378
|
+
vs ${q(entry)}
|
|
379
|
+
`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
const entry = guestToHost(guestEntry);
|
|
384
|
+
log.pushEntry(entry);
|
|
385
|
+
const [_op, hostTarget, _optVerb, hostArgs, _callIndex] = entry;
|
|
386
|
+
performSendOnly(hostTarget, optVerb, hostArgs);
|
|
387
|
+
}
|
|
388
|
+
} catch (fatalError) {
|
|
389
|
+
throw panic(fatalError);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
applyMethod(guestTarget, optVerb, guestArgs, guestReturnedP) {
|
|
393
|
+
const callIndex = log.getIndex();
|
|
394
|
+
if (stopped || !bijection.hasGuest(guestTarget)) {
|
|
395
|
+
Fail`Sent from a previous run: ${guestTarget}`;
|
|
396
|
+
}
|
|
397
|
+
const hostResultKit = makeVowKit();
|
|
398
|
+
const g = bijection.unwrapInit(guestReturnedP, hostResultKit.vow);
|
|
399
|
+
g === guestReturnedP ||
|
|
400
|
+
Fail`internal: guestReturnedP should not unwrap: ${g} vs ${guestReturnedP}`;
|
|
401
|
+
/** @type {Outcome} */
|
|
402
|
+
let outcome;
|
|
403
|
+
try {
|
|
404
|
+
const guestEntry = harden([
|
|
405
|
+
'checkSend',
|
|
406
|
+
guestTarget,
|
|
407
|
+
optVerb,
|
|
408
|
+
guestArgs,
|
|
409
|
+
callIndex,
|
|
410
|
+
]);
|
|
411
|
+
if (log.isReplaying()) {
|
|
412
|
+
const entry = log.nextEntry();
|
|
413
|
+
try {
|
|
414
|
+
equate(guestEntry, entry);
|
|
415
|
+
} catch (equateErr) {
|
|
416
|
+
// TODO consider Richard Gibson's suggestion for a better way
|
|
417
|
+
// to keep track of the error labeling.
|
|
418
|
+
throwLabeled(
|
|
419
|
+
equateErr,
|
|
420
|
+
`replay ${callIndex}:
|
|
421
|
+
${q(guestEntry)}
|
|
422
|
+
vs ${q(entry)}
|
|
423
|
+
`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
outcome = /** @type {Outcome} */ (nestInterpreter(callIndex));
|
|
427
|
+
} else {
|
|
428
|
+
const entry = guestToHost(guestEntry);
|
|
429
|
+
log.pushEntry(entry);
|
|
430
|
+
const [_op, hostTarget, _optVerb, hostArgs, _callIndex] = entry;
|
|
431
|
+
nestInterpreter(callIndex);
|
|
432
|
+
outcome = performSend(
|
|
433
|
+
hostTarget,
|
|
434
|
+
optVerb,
|
|
435
|
+
hostArgs,
|
|
436
|
+
callIndex,
|
|
437
|
+
hostResultKit,
|
|
438
|
+
guestReturnedP,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
} catch (fatalError) {
|
|
442
|
+
throw panic(fatalError);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
switch (outcome.kind) {
|
|
446
|
+
case 'return': {
|
|
447
|
+
return outcome.result;
|
|
448
|
+
}
|
|
449
|
+
case 'throw': {
|
|
450
|
+
throw outcome.problem;
|
|
451
|
+
}
|
|
452
|
+
default: {
|
|
453
|
+
// @ts-expect-error TS correctly knows this case would be outside
|
|
454
|
+
// the type. But that's what we want to check.
|
|
455
|
+
throw Panic`unexpected outcome kind ${q(outcome.kind)}`;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
applyFunctionSendOnly(guestTarget, guestArgs) {
|
|
460
|
+
return guestHandler.applyMethodSendOnly(
|
|
461
|
+
guestTarget,
|
|
462
|
+
undefined,
|
|
463
|
+
guestArgs,
|
|
464
|
+
);
|
|
465
|
+
},
|
|
466
|
+
applyFunction(guestTarget, guestArgs, guestReturnedP) {
|
|
467
|
+
return guestHandler.applyMethod(
|
|
468
|
+
guestTarget,
|
|
469
|
+
undefined,
|
|
470
|
+
guestArgs,
|
|
471
|
+
guestReturnedP,
|
|
472
|
+
);
|
|
473
|
+
},
|
|
474
|
+
getSendOnly(guestTarget, prop) {
|
|
475
|
+
throw Panic`guest eventual getSendOnly not yet supported: ${guestTarget}.${b(prop)}`;
|
|
476
|
+
},
|
|
477
|
+
get(guestTarget, prop, guestReturnedP) {
|
|
478
|
+
throw Panic`guest eventual get not yet supported: ${guestTarget}.${b(prop)} -> ${b(guestReturnedP)}`;
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const makeGuestPresence = (iface, methodEntries) => {
|
|
483
|
+
let guestPresence;
|
|
484
|
+
void new HandledPromise((_res, _rej, resolveWithPresence) => {
|
|
485
|
+
guestPresence = resolveWithPresence(guestHandler);
|
|
486
|
+
}); // no unfulfilledHandler
|
|
487
|
+
if (typeof guestPresence !== 'object') {
|
|
488
|
+
throw Fail`presence expected to be object ${guestPresence}`;
|
|
489
|
+
}
|
|
490
|
+
assign(guestPresence, fromEntries(methodEntries));
|
|
491
|
+
const result = Remotable(iface, undefined, guestPresence);
|
|
492
|
+
result === guestPresence ||
|
|
493
|
+
Fail`Remotable expected to make presence in place: ${guestPresence} vs ${result}`;
|
|
494
|
+
return result;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @returns {PromiseKit<any>}
|
|
499
|
+
*/
|
|
500
|
+
const makeGuestPromiseKit = () => {
|
|
501
|
+
let resolve;
|
|
502
|
+
let reject;
|
|
503
|
+
const promise = new HandledPromise((res, rej, _resPres) => {
|
|
504
|
+
resolve = res;
|
|
505
|
+
reject = rej;
|
|
506
|
+
}, guestHandler);
|
|
507
|
+
// @ts-expect-error TS cannot infer that it is a PromiseKit
|
|
508
|
+
return harden({ promise, resolve, reject });
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// //////////////// Converters ///////////////////////////////////////////////
|
|
512
|
+
|
|
513
|
+
const makeGuestForHostRemotable = hRem => {
|
|
514
|
+
// Nothing here that captures `hRem` should make any use of it after the
|
|
515
|
+
// `makeGuestForHostRemotable` returns. This invariant enables
|
|
516
|
+
// `makeGuestForHostRemotable` to clear the `hRem` variable just before
|
|
517
|
+
// it returns, so any implementation-level capture of the variable does
|
|
518
|
+
// not inadvertently retain the host remotable which was the original
|
|
519
|
+
// value of the `hRem` variable.
|
|
520
|
+
let gRem;
|
|
521
|
+
/** @param {PropertyKey} [optVerb] */
|
|
522
|
+
const makeGuestMethod = (optVerb = undefined) => {
|
|
523
|
+
const guestMethod = (...guestArgs) => {
|
|
524
|
+
const callIndex = log.getIndex();
|
|
525
|
+
return guestCallsHost(gRem, optVerb, guestArgs, callIndex);
|
|
526
|
+
};
|
|
527
|
+
if (optVerb) {
|
|
528
|
+
defineProperties(guestMethod, {
|
|
529
|
+
name: { value: String(hRem[optVerb].name || optVerb) },
|
|
530
|
+
length: { value: Number(hRem[optVerb].length || 0) },
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
defineProperties(guestMethod, {
|
|
534
|
+
name: { value: String(hRem.name || 'anon') },
|
|
535
|
+
length: { value: Number(hRem.length || 0) },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return guestMethod;
|
|
539
|
+
};
|
|
540
|
+
const iface = String(getInterfaceOf(hRem) || 'remotable');
|
|
541
|
+
const guestIface = `${iface} guest wrapper`; // just for debugging clarity
|
|
542
|
+
if (typeof hRem === 'function') {
|
|
543
|
+
// NOTE: Assumes that a far function has no "static" methods. This
|
|
544
|
+
// is the current marshal design, but revisit this if we change our
|
|
545
|
+
// minds.
|
|
546
|
+
gRem = Remotable(guestIface, undefined, makeGuestMethod());
|
|
547
|
+
// NOTE: If we ever do support that, probably all we need
|
|
548
|
+
// to do is remove the following `throw Fail` line.
|
|
549
|
+
throw Fail`host far functions not yet passable`;
|
|
550
|
+
} else {
|
|
551
|
+
const methodNames = getMethodNames(hRem);
|
|
552
|
+
const guestMethods = methodNames.map(name => [
|
|
553
|
+
name,
|
|
554
|
+
makeGuestMethod(name),
|
|
555
|
+
]);
|
|
556
|
+
gRem = makeGuestPresence(guestIface, guestMethods);
|
|
557
|
+
}
|
|
558
|
+
// See note at the top of the function to see why clearing the `hRem`
|
|
559
|
+
// variable is safe, and what invariant the above code needs to maintain so
|
|
560
|
+
// that it remains safe.
|
|
561
|
+
hRem = undefined;
|
|
562
|
+
return gRem;
|
|
563
|
+
};
|
|
564
|
+
harden(makeGuestForHostRemotable);
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @param {Vow} hVow
|
|
568
|
+
* @param {Promise} [promiseKey]
|
|
569
|
+
* If provided, use this promise as the key in the guestPromiseMap
|
|
570
|
+
* rather than the returned promise. This only happens when the
|
|
571
|
+
* promiseKey ends up forwarded to the returned promise anyway, so
|
|
572
|
+
* associating it with this resolve/reject pair is not incorrect.
|
|
573
|
+
* It is needed when `promiseKey` is also entered into the bijection
|
|
574
|
+
* paired with hVow.
|
|
575
|
+
* @returns {Promise}
|
|
576
|
+
*/
|
|
577
|
+
const makeGuestForHostVow = (hVow, promiseKey = undefined) => {
|
|
578
|
+
hVow = tolerateHostPromiseToVow(hVow);
|
|
579
|
+
isVow(hVow) || Fail`vow expected ${hVow}`;
|
|
580
|
+
const { promise, resolve, reject } = makeGuestPromiseKit();
|
|
581
|
+
promiseKey ??= promise;
|
|
582
|
+
guestPromiseMap.set(promiseKey, harden({ resolve, reject }));
|
|
583
|
+
|
|
584
|
+
watchWake(hVow);
|
|
585
|
+
|
|
586
|
+
// The replay membrane is the only component inserting entries into
|
|
587
|
+
// the log. In particular, the flow's vow durable watcher does not log the
|
|
588
|
+
// settlement outcome, and instead it's the responsibility of the
|
|
589
|
+
// membrane's ephemeral handler. Because of this, the membrane's handler
|
|
590
|
+
// must be careful to:
|
|
591
|
+
// - Be added to the vow if the settlement has not yet been recorded in
|
|
592
|
+
// the log.
|
|
593
|
+
// - Insert a single settlement outcome in the log for the given vow.
|
|
594
|
+
//
|
|
595
|
+
// In practice the former is accomplished by a handler always being
|
|
596
|
+
// added to the host vow when creating a guest promise, and the
|
|
597
|
+
// handler checking after replay is complete, whether the guest promise
|
|
598
|
+
// is already settled (by the log replay) or not. The latter is
|
|
599
|
+
// accomplished by checking that the membrane has not been stopped
|
|
600
|
+
// before updating the log.
|
|
601
|
+
|
|
602
|
+
void when(
|
|
603
|
+
hVow,
|
|
604
|
+
async hostFulfillment => {
|
|
605
|
+
await log.promiseReplayDone(); // should never reject
|
|
606
|
+
if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') {
|
|
607
|
+
/** @type {LogEntry} */
|
|
608
|
+
const entry = harden(['doFulfill', hVow, hostFulfillment]);
|
|
609
|
+
log.pushEntry(entry);
|
|
610
|
+
try {
|
|
611
|
+
interpretOne(topDispatch, entry);
|
|
612
|
+
} catch {
|
|
613
|
+
// interpretOne does its own try/catch/panic, so failure would
|
|
614
|
+
// already be registered. Here, just return to avoid the
|
|
615
|
+
// Unhandled rejection.
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
async hostReason => {
|
|
620
|
+
await log.promiseReplayDone(); // should never reject
|
|
621
|
+
if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') {
|
|
622
|
+
/** @type {LogEntry} */
|
|
623
|
+
const entry = harden(['doReject', hVow, hostReason]);
|
|
624
|
+
log.pushEntry(entry);
|
|
625
|
+
try {
|
|
626
|
+
interpretOne(topDispatch, entry);
|
|
627
|
+
} catch {
|
|
628
|
+
// interpretOne does its own try/catch/panic, so failure would
|
|
629
|
+
// already be registered. Here, just return to avoid the
|
|
630
|
+
// Unhandled rejection.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
return promise;
|
|
636
|
+
};
|
|
637
|
+
harden(makeGuestForHostVow);
|
|
638
|
+
|
|
639
|
+
const { guestToHost, hostToGuest } = makeConvertKit(
|
|
640
|
+
bijection,
|
|
641
|
+
makeGuestForHostRemotable,
|
|
642
|
+
makeGuestForHostVow,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
// /////////////////////////////// Interpreter ///////////////////////////////
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* These are the only ones that are driven from the interpreter loop
|
|
649
|
+
*/
|
|
650
|
+
const topDispatch = harden({
|
|
651
|
+
doFulfill,
|
|
652
|
+
doReject,
|
|
653
|
+
// doCall, // unimplemented in the current plan
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* These are the only ones that are driven from the interpreter loop
|
|
658
|
+
*/
|
|
659
|
+
const nestDispatch = harden({
|
|
660
|
+
// doCall, // unimplemented in the current plan
|
|
661
|
+
doReturn,
|
|
662
|
+
doThrow,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const interpretOne = (dispatch, [op, ...args]) => {
|
|
666
|
+
try {
|
|
667
|
+
op in dispatch ||
|
|
668
|
+
// separate line so I can set a breakpoint
|
|
669
|
+
Fail`unexpected dispatch op: ${q(op)}`;
|
|
670
|
+
return dispatch[op](...args);
|
|
671
|
+
} catch (problem) {
|
|
672
|
+
throw panic(problem);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const logDo = (dispatch, entry) => {
|
|
677
|
+
log.pushEntry(entry);
|
|
678
|
+
return interpretOne(dispatch, entry);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const callStack = [];
|
|
682
|
+
|
|
683
|
+
let unnestFlag = false;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* @param {number} callIndex
|
|
687
|
+
* @returns {Outcome | undefined}
|
|
688
|
+
*/
|
|
689
|
+
const nestInterpreter = callIndex => {
|
|
690
|
+
callStack.push(callIndex);
|
|
691
|
+
while (log.isReplaying() && !stopped) {
|
|
692
|
+
const entry = log.nextEntry();
|
|
693
|
+
const optOutcome = interpretOne(nestDispatch, entry);
|
|
694
|
+
if (unnestFlag) {
|
|
695
|
+
optOutcome ||
|
|
696
|
+
// separate line so I can set a breakpoint
|
|
697
|
+
Fail`only unnest with an outcome: ${q(entry[0])}`;
|
|
698
|
+
unnestFlag = false;
|
|
699
|
+
return optOutcome;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
unnestFlag = false;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* @param {number} callIndex
|
|
707
|
+
*/
|
|
708
|
+
const unnestInterpreter = callIndex => {
|
|
709
|
+
!stopped ||
|
|
710
|
+
Fail`This membrane stopped. Restart with new membrane ${replayMembrane}`;
|
|
711
|
+
callStack.length >= 1 ||
|
|
712
|
+
// separate line so I can set a breakpoint
|
|
713
|
+
Fail`Unmatched unnest: ${q(callIndex)}`;
|
|
714
|
+
const i = callStack.pop();
|
|
715
|
+
i === callIndex ||
|
|
716
|
+
// separate line so I can set a breakpoint
|
|
717
|
+
Fail`Unexpected unnest: ${q(callIndex)} vs ${q(i)}`;
|
|
718
|
+
unnestFlag = true;
|
|
719
|
+
if (callStack.length === 0) {
|
|
720
|
+
void E.when(undefined, wake);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const wake = () => {
|
|
725
|
+
while (log.isReplaying() && !stopped) {
|
|
726
|
+
callStack.length === 0 ||
|
|
727
|
+
Fail`wake only with empty callStack: ${q(callStack)}`;
|
|
728
|
+
const entry = log.peekEntry();
|
|
729
|
+
const op = entry[0];
|
|
730
|
+
if (!(op in topDispatch)) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
void log.nextEntry();
|
|
734
|
+
interpretOne(topDispatch, entry);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const stop = () => {
|
|
739
|
+
stopped = true;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const replayMembrane = Far('replayMembrane', {
|
|
743
|
+
hostToGuest,
|
|
744
|
+
guestToHost,
|
|
745
|
+
wake,
|
|
746
|
+
stop,
|
|
747
|
+
});
|
|
748
|
+
return replayMembrane;
|
|
749
|
+
};
|
|
750
|
+
harden(makeReplayMembrane);
|
|
751
|
+
|
|
752
|
+
/** @typedef {ReturnType<makeReplayMembrane>} ReplayMembrane */
|