@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +201 -0
  3. package/README.md +40 -0
  4. package/docs/async-flow-states.key +0 -0
  5. package/docs/async-flow-states.md +15 -0
  6. package/docs/async-flow-states.png +0 -0
  7. package/index.d.ts +3 -0
  8. package/index.d.ts.map +1 -0
  9. package/index.js +2 -0
  10. package/package.json +66 -0
  11. package/src/async-flow.d.ts +94 -0
  12. package/src/async-flow.d.ts.map +1 -0
  13. package/src/async-flow.js +520 -0
  14. package/src/bijection.d.ts +31 -0
  15. package/src/bijection.d.ts.map +1 -0
  16. package/src/bijection.js +207 -0
  17. package/src/convert.d.ts +6 -0
  18. package/src/convert.d.ts.map +1 -0
  19. package/src/convert.js +133 -0
  20. package/src/endowments.d.ts +16 -0
  21. package/src/endowments.d.ts.map +1 -0
  22. package/src/endowments.js +292 -0
  23. package/src/ephemera.d.ts +3 -0
  24. package/src/ephemera.d.ts.map +1 -0
  25. package/src/ephemera.js +39 -0
  26. package/src/equate.d.ts +2 -0
  27. package/src/equate.d.ts.map +1 -0
  28. package/src/equate.js +123 -0
  29. package/src/log-store.d.ts +27 -0
  30. package/src/log-store.d.ts.map +1 -0
  31. package/src/log-store.js +169 -0
  32. package/src/replay-membrane.d.ts +40 -0
  33. package/src/replay-membrane.d.ts.map +1 -0
  34. package/src/replay-membrane.js +752 -0
  35. package/src/type-guards.d.ts +4 -0
  36. package/src/type-guards.d.ts.map +1 -0
  37. package/src/type-guards.js +68 -0
  38. package/src/types.d.ts +67 -0
  39. package/src/types.d.ts.map +1 -0
  40. package/src/types.js +196 -0
  41. package/test/async-flow-crank.test.js +102 -0
  42. package/test/async-flow-early-completion.test.js +203 -0
  43. package/test/async-flow-no-this.js +65 -0
  44. package/test/async-flow.test.js +383 -0
  45. package/test/bad-host.test.js +210 -0
  46. package/test/bijection.test.js +124 -0
  47. package/test/convert.test.js +132 -0
  48. package/test/endowments.test.js +157 -0
  49. package/test/equate.test.js +120 -0
  50. package/test/log-store.test.js +120 -0
  51. package/test/prepare-test-env-ava.js +28 -0
  52. package/test/replay-membrane-eventual.test.js +217 -0
  53. package/test/replay-membrane-settlement.test.js +173 -0
  54. package/test/replay-membrane-zombie.test.js +187 -0
  55. package/test/replay-membrane.test.js +297 -0
  56. package/tsconfig.build.json +11 -0
  57. package/tsconfig.json +13 -0
  58. 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 */