@agoric/async-flow 0.1.1-dev-16095c5.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 (52) 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 +2 -0
  8. package/index.d.ts.map +1 -0
  9. package/index.js +1 -0
  10. package/package.json +67 -0
  11. package/src/async-flow.d.ts +87 -0
  12. package/src/async-flow.d.ts.map +1 -0
  13. package/src/async-flow.js +502 -0
  14. package/src/bijection.d.ts +28 -0
  15. package/src/bijection.d.ts.map +1 -0
  16. package/src/bijection.js +132 -0
  17. package/src/convert.d.ts +5 -0
  18. package/src/convert.d.ts.map +1 -0
  19. package/src/convert.js +131 -0
  20. package/src/ephemera.d.ts +2 -0
  21. package/src/ephemera.d.ts.map +1 -0
  22. package/src/ephemera.js +35 -0
  23. package/src/equate.d.ts +2 -0
  24. package/src/equate.d.ts.map +1 -0
  25. package/src/equate.js +123 -0
  26. package/src/log-store.d.ts +25 -0
  27. package/src/log-store.d.ts.map +1 -0
  28. package/src/log-store.js +165 -0
  29. package/src/replay-membrane.d.ts +71 -0
  30. package/src/replay-membrane.d.ts.map +1 -0
  31. package/src/replay-membrane.js +435 -0
  32. package/src/type-guards.d.ts +4 -0
  33. package/src/type-guards.d.ts.map +1 -0
  34. package/src/type-guards.js +54 -0
  35. package/src/types.d.ts +70 -0
  36. package/src/types.d.ts.map +1 -0
  37. package/src/types.js +164 -0
  38. package/test/async-flow-crank.test.js +96 -0
  39. package/test/async-flow-no-this.js +59 -0
  40. package/test/async-flow.test.js +380 -0
  41. package/test/bad-host.test.js +205 -0
  42. package/test/bijection.test.js +118 -0
  43. package/test/convert.test.js +127 -0
  44. package/test/equate.test.js +116 -0
  45. package/test/log-store.test.js +112 -0
  46. package/test/prepare-test-env-ava.js +28 -0
  47. package/test/replay-membrane-settlement.test.js +154 -0
  48. package/test/replay-membrane-zombie.test.js +158 -0
  49. package/test/replay-membrane.test.js +271 -0
  50. package/tsconfig.build.json +11 -0
  51. package/tsconfig.json +13 -0
  52. package/typedoc.json +8 -0
@@ -0,0 +1,502 @@
1
+ import { annotateError, Fail, makeError, q, X } from '@endo/errors';
2
+ import { E } from '@endo/eventual-send';
3
+ import { M } from '@endo/patterns';
4
+ import { makeScalarWeakMapStore } from '@agoric/store';
5
+ import { PromiseWatcherI } from '@agoric/base-zone';
6
+ import { prepareVowTools, toPassableCap, VowShape } from '@agoric/vow';
7
+ import { makeReplayMembrane } from './replay-membrane.js';
8
+ import { prepareLogStore } from './log-store.js';
9
+ import { prepareBijection } from './bijection.js';
10
+ import { LogEntryShape, FlowStateShape } from './type-guards.js';
11
+
12
+ /**
13
+ * @import { WeakMapStore } from '@agoric/store'
14
+ */
15
+
16
+ const { defineProperties } = Object;
17
+
18
+ const AsyncFlowIKit = harden({
19
+ flow: M.interface('Flow', {
20
+ getFlowState: M.call().returns(FlowStateShape),
21
+ restart: M.call().optional(M.boolean()).returns(),
22
+ wake: M.call().returns(),
23
+ getOutcome: M.call().returns(VowShape),
24
+ dump: M.call().returns(M.arrayOf(LogEntryShape)),
25
+ getOptFatalProblem: M.call().returns(M.opt(M.error())),
26
+ }),
27
+ admin: M.interface('FlowAdmin', {
28
+ reset: M.call().returns(),
29
+ complete: M.call().returns(),
30
+ panic: M.call(M.error()).returns(M.not(M.any())), // only throws
31
+ }),
32
+ wakeWatcher: PromiseWatcherI,
33
+ });
34
+
35
+ const AdminAsyncFlowI = M.interface('AsyncFlowAdmin', {
36
+ getFailures: M.call().returns(M.mapOf(M.remotable('asyncFlow'), M.error())),
37
+ wakeAll: M.call().returns(),
38
+ getFlowForOutcomeVow: M.call(VowShape).returns(M.opt(M.remotable('flow'))),
39
+ });
40
+
41
+ /**
42
+ * @param {Zone} outerZone
43
+ * @param {PreparationOptions} [outerOptions]
44
+ */
45
+ export const prepareAsyncFlowTools = (outerZone, outerOptions = {}) => {
46
+ const {
47
+ vowTools = prepareVowTools(outerZone),
48
+ makeLogStore = prepareLogStore(outerZone),
49
+ makeBijection = prepareBijection(outerZone),
50
+ } = outerOptions;
51
+ const { watch, makeVowKit } = vowTools;
52
+
53
+ const failures = outerZone.mapStore('asyncFuncFailures', {
54
+ keyShape: M.remotable('flow'), // flowState === 'Failed'
55
+ valueShape: M.error(),
56
+ });
57
+
58
+ const eagerWakers = outerZone.setStore(`asyncFuncEagerWakers`, {
59
+ keyShape: M.remotable('flow'), // flowState !== 'Done'
60
+ });
61
+
62
+ /** @type WeakMapStore<AsyncFlow, ReplayMembrane> */
63
+ const membraneMap = makeScalarWeakMapStore('membraneFor', {
64
+ keyShape: M.remotable('flow'),
65
+ valueShape: M.remotable('membrane'),
66
+ });
67
+
68
+ const hasMembrane = flow => membraneMap.has(flow);
69
+ const getMembrane = flow => membraneMap.get(flow);
70
+ const initMembrane = (flow, membrane) => membraneMap.init(flow, membrane);
71
+ const deleteMembrane = flow => membraneMap.delete(flow);
72
+
73
+ /**
74
+ * So we can give out wrapper functions easily and recover flow objects
75
+ * for their activations later.
76
+ */
77
+ const flowForOutcomeVowKey = outerZone.mapStore('flowForOutcomeVow', {
78
+ keyShape: M.remotable('toPassableCap'),
79
+ valueShape: M.remotable('flow'), // flowState !== 'Done'
80
+ });
81
+
82
+ /**
83
+ * @param {Zone} zone
84
+ * @param {string} tag
85
+ * @param {GuestAsyncFunc} guestAsyncFunc
86
+ * @param {{ startEager?: boolean }} [options]
87
+ */
88
+ const prepareAsyncFlowKit = (zone, tag, guestAsyncFunc, options = {}) => {
89
+ typeof guestAsyncFunc === 'function' ||
90
+ Fail`guestAsyncFunc must be a callable function ${guestAsyncFunc}`;
91
+ const {
92
+ // May change default to false, once instances reliably wake up
93
+ startEager = true,
94
+ } = options;
95
+
96
+ const internalMakeAsyncFlowKit = zone.exoClassKit(
97
+ tag,
98
+ AsyncFlowIKit,
99
+ activationArgs => {
100
+ harden(activationArgs);
101
+ const log = makeLogStore();
102
+ const bijection = makeBijection();
103
+
104
+ return {
105
+ activationArgs, // replay starts by reactivating with these
106
+ log, // log to be accumulated or replayed
107
+ bijection, // membrane's guest-host mapping
108
+ outcomeKit: makeVowKit(), // outcome of activation as host vow
109
+ isDone: false, // persistently done
110
+ };
111
+ },
112
+ {
113
+ flow: {
114
+ /**
115
+ * @returns {FlowState}
116
+ */
117
+ getFlowState() {
118
+ const { state, facets } = this;
119
+ const { log, outcomeKit, isDone } = state;
120
+ const { flow } = facets;
121
+
122
+ if (isDone) {
123
+ !hasMembrane(flow) ||
124
+ Fail`Done flow must drop membrane ${flow} ${getMembrane(flow)}`;
125
+ !failures.has(flow) ||
126
+ Fail`Done flow must not be in failures ${flow} ${failures.get(flow)}`;
127
+ !eagerWakers.has(flow) ||
128
+ Fail`Done flow must not be in eagerWakers ${flow}`;
129
+ !flowForOutcomeVowKey.has(outcomeKit.vow) ||
130
+ Fail`Done flow must drop flow lookup from vow ${outcomeKit.vow}`;
131
+ (log.getIndex() === 0 && log.getLength() === 0) ||
132
+ Fail`Done flow must empty log ${flow} ${log}`;
133
+ return 'Done';
134
+ }
135
+ if (failures.has(flow)) {
136
+ return 'Failed';
137
+ }
138
+ if (!hasMembrane(flow)) {
139
+ log.getIndex() === 0 ||
140
+ Fail`Sleeping flow must play from log start ${flow} ${log.getIndex()}`;
141
+ return 'Sleeping';
142
+ }
143
+ if (log.isReplaying()) {
144
+ return 'Replaying';
145
+ }
146
+ return 'Running';
147
+ },
148
+
149
+ /**
150
+ * Calls the guest function, either for the initial run or at the
151
+ * start of a replay.
152
+ *
153
+ * @param {boolean} [eager]
154
+ */
155
+ restart(eager = startEager) {
156
+ const { state, facets } = this;
157
+ const { activationArgs, log, bijection, outcomeKit } = state;
158
+ const { flow, admin, wakeWatcher } = facets;
159
+
160
+ const startFlowState = flow.getFlowState();
161
+
162
+ startFlowState !== 'Done' ||
163
+ // separate line so I can set a breakpoint
164
+ Fail`Cannot restart a done flow ${flow}`;
165
+
166
+ admin.reset();
167
+ if (eager) {
168
+ eagerWakers.add(flow);
169
+ } else if (eagerWakers.has(flow)) {
170
+ eagerWakers.delete(flow);
171
+ }
172
+
173
+ const wakeWatch = vowish => {
174
+ // Extra paranoid because we're getting
175
+ // "promise watcher must be a virtual object"
176
+ // in the general vicinity.
177
+ zone.isStorable(vowish) ||
178
+ Fail`vowish must be storable in this zone (usually, must be durable): ${vowish}`;
179
+ zone.isStorable(wakeWatcher) ||
180
+ Fail`wakeWatcher must be storable in this zone (usually, must be durable): ${wakeWatcher}`;
181
+ watch(vowish, wakeWatcher);
182
+ };
183
+ const panic = err => admin.panic(err);
184
+ const membrane = makeReplayMembrane(
185
+ log,
186
+ bijection,
187
+ vowTools,
188
+ wakeWatch,
189
+ panic,
190
+ );
191
+ initMembrane(flow, membrane);
192
+ const guestArgs = membrane.hostToGuest(activationArgs);
193
+
194
+ const flowState = flow.getFlowState();
195
+ flowState === 'Running' ||
196
+ flowState === 'Replaying' ||
197
+ Fail`Restarted flow must be Running or Replaying ${flow}`;
198
+
199
+ // In case some host vows were settled before the guest makes
200
+ // the first call to a host object.
201
+ membrane.wake();
202
+
203
+ // We do *not* call the guestAsyncFunc by having the membrane make
204
+ // a host wrapper for the function. Rather, we special case this
205
+ // host-to-guest call by "manually" sending the arguments through
206
+ // and calling the guest function ourselves. Likewise, we
207
+ // special case the handling of the guestResultP, rather than
208
+ // ask the membrane to make a host vow for a guest promise.
209
+ // To support this special casing, we store additional replay
210
+ // data in this internal flow instance -- the host activationArgs
211
+ // and the host outcome vow kit.
212
+ const guestResultP = (async () =>
213
+ // async IFFE ensures guestResultP is a fresh promise
214
+ guestAsyncFunc(...guestArgs))();
215
+
216
+ if (flow.getFlowState() !== 'Failed') {
217
+ // If the flow fails, that resets the bijection. Without this
218
+ // gating condition, the next line could grow the bijection
219
+ // of a failed flow, subverting other gating checks on bijection
220
+ // membership.
221
+ bijection.init(guestResultP, outcomeKit.vow);
222
+ }
223
+ // log is driven at first by guestAyncFunc interaction through the
224
+ // membrane with the host activationArgs. At the end of its first
225
+ // turn, it returns a promise for its eventual guest result.
226
+ // It then proceeds to interact with the host through the membrane
227
+ // in further turns by `await`ing (or otherwise registering)
228
+ // on host vows turned into guest promises, and by calling
229
+ // the guest presence of other host objects.
230
+ //
231
+ // `bijection.hasGuest(guestResultP)` can be false in a delayed
232
+ // guest - to - host settling from a previous run.
233
+ // In that case, the bijection was reset and all guest caps
234
+ // created in the previous run were unregistered,
235
+ // including `guestResultP`.
236
+ void E.when(
237
+ guestResultP,
238
+ gFulfillment => {
239
+ if (bijection.hasGuest(guestResultP)) {
240
+ outcomeKit.resolver.resolve(
241
+ membrane.guestToHost(gFulfillment),
242
+ );
243
+ admin.complete();
244
+ }
245
+ },
246
+ guestReason => {
247
+ // The `guestResultP` might be a failure thrown by `panic`
248
+ // indicating a failure to replay. In that case, we must not
249
+ // settle the outcomeVow, since the outcome vow only represents
250
+ // the settled result of the async function itself.
251
+ // Fortunately, `panic` resets the bijection, again resulting
252
+ // in the `guestResultP` being absent from the bijection,
253
+ // so this leave the outcome vow unsettled, as it must.
254
+ if (bijection.hasGuest(guestResultP)) {
255
+ outcomeKit.resolver.reject(membrane.guestToHost(guestReason));
256
+ admin.complete();
257
+ }
258
+ },
259
+ );
260
+ },
261
+ wake() {
262
+ const { facets } = this;
263
+ const { flow } = facets;
264
+
265
+ const flowState = flow.getFlowState();
266
+ switch (flowState) {
267
+ case 'Done':
268
+ case 'Failed': {
269
+ return;
270
+ }
271
+ case 'Running':
272
+ case 'Replaying': {
273
+ // Safe to call membrane wake for a replaying or running flow
274
+ // because it is idempotent. membrane.wake already has reentrancy
275
+ // protection. Aside from harmless reentrancy, calling
276
+ // membrane.wake won't cause it to do anything that it would
277
+ // not have done on its own.
278
+ //
279
+ // An interesting edge case is that when the guest proceeds
280
+ // from a top-level doReturn or doThrow, while we're still in
281
+ // the guest turn, if somehow flow.wake were to be called then,
282
+ // and if the next thing in the replay log was a `doCall`
283
+ // (a future feature), then the `doCall` would call the guest
284
+ // while it was still in the middle of a "past" turn. However,
285
+ // this cannot happen because `flow` is host-side. For it to
286
+ // be called while the guest is active, the membrane's
287
+ // `callStack` would not be empty. membrane.wake checks and
288
+ // already throws an error in that case.
289
+ //
290
+ // More important, during a replay, no guest action can actually
291
+ // call host functions at all. Rather, the host is fully
292
+ // emulated from the log. So this case cannot arise.
293
+ //
294
+ // This analysis *assumes* that the guest function has no access
295
+ // to the flow outside the membrane, i.e., the "closed guest"
296
+ // assumption.
297
+ getMembrane(flow).wake();
298
+ return;
299
+ }
300
+ case 'Sleeping': {
301
+ flow.restart();
302
+ return;
303
+ }
304
+ default: {
305
+ // Should be a at-ts-expect-error that this case is unreachable
306
+ // which TS clearly knows anyway because it thinks the following
307
+ // `flowState` variable in this context has type `never`.
308
+ throw Fail`unexpected flowState ${q(flowState)}`;
309
+ }
310
+ }
311
+ },
312
+ getOutcome() {
313
+ const { state } = this;
314
+ const { outcomeKit } = state;
315
+ return outcomeKit.vow;
316
+ },
317
+ dump() {
318
+ const { state } = this;
319
+ const { log } = state;
320
+
321
+ return log.dump();
322
+ },
323
+ getOptFatalProblem() {
324
+ const { facets } = this;
325
+ const { flow } = facets;
326
+
327
+ return failures.has(flow) ? failures.get(flow) : undefined;
328
+ },
329
+ },
330
+ admin: {
331
+ reset() {
332
+ const { state, facets } = this;
333
+ const { bijection, log } = state;
334
+ const { flow } = facets;
335
+ !state.isDone || Fail`Cannot reset a done flow`;
336
+
337
+ if (failures.has(flow)) {
338
+ failures.delete(flow);
339
+ }
340
+ if (hasMembrane(flow)) {
341
+ getMembrane(flow).stop();
342
+ deleteMembrane(flow);
343
+ }
344
+ log.reset();
345
+ bijection.reset();
346
+ },
347
+ complete() {
348
+ const { state, facets } = this;
349
+ const { log } = state;
350
+ const { flow, admin } = facets;
351
+
352
+ admin.reset();
353
+ if (eagerWakers.has(flow)) {
354
+ eagerWakers.delete(flow);
355
+ }
356
+ flowForOutcomeVowKey.delete(toPassableCap(flow.getOutcome()));
357
+ state.isDone = true;
358
+ log.dispose();
359
+ flow.getFlowState() === 'Done' ||
360
+ Fail`Complete flow must be Done ${flow}`;
361
+ },
362
+ panic(fatalProblem) {
363
+ const { state, facets } = this;
364
+ const { bijection, log } = state;
365
+ const { flow } = facets;
366
+
367
+ if (failures.has(flow)) {
368
+ const prevErr = failures.get(flow);
369
+ annotateError(
370
+ prevErr,
371
+ X`doubly failed somehow with ${fatalProblem}`,
372
+ );
373
+ // prevErr likely to be the more relevant diagnostic to report
374
+ fatalProblem = prevErr;
375
+ } else {
376
+ failures.init(flow, fatalProblem);
377
+ }
378
+
379
+ if (hasMembrane(flow)) {
380
+ getMembrane(flow).stop();
381
+ deleteMembrane(flow);
382
+ }
383
+ log.reset();
384
+ bijection.reset();
385
+
386
+ flow.getFlowState() === 'Failed' ||
387
+ Fail`Panicked flow must be Failed ${flow}`;
388
+
389
+ // This is not an expected throw, so in theory arbitrary chaos
390
+ // may ensue from throwing it. But at this point
391
+ // we should have successfully isolated this activation from
392
+ // having any observable effects on the host, aside from
393
+ // console logging and
394
+ // resource exhaustion, including infinite loops
395
+ const err = makeError(
396
+ X`In a Failed state: see getFailures() or getOptFatalProblem() for more information`,
397
+ );
398
+ annotateError(err, X`due to ${fatalProblem}`);
399
+ throw err;
400
+ },
401
+ },
402
+ wakeWatcher: {
403
+ onFulfilled(_fulfillment) {
404
+ const { facets } = this;
405
+ facets.flow.wake();
406
+ },
407
+ onRejected(_fulfillment) {
408
+ const { facets } = this;
409
+ facets.flow.wake();
410
+ },
411
+ },
412
+ },
413
+ );
414
+ const makeAsyncFlowKit = activationArgs => {
415
+ const asyncFlowKit = internalMakeAsyncFlowKit(activationArgs);
416
+ const { flow } = asyncFlowKit;
417
+
418
+ const vow = toPassableCap(flow.getOutcome());
419
+ flowForOutcomeVowKey.init(toPassableCap(vow), flow);
420
+ flow.restart();
421
+ return asyncFlowKit;
422
+ };
423
+ return harden(makeAsyncFlowKit);
424
+ };
425
+
426
+ /**
427
+ * @param {Zone} zone
428
+ * @param {string} tag
429
+ * @param {GuestAsyncFunc} guestFunc
430
+ * @param {{ startEager?: boolean }} [options]
431
+ * @returns {HostAsyncFuncWrapper}
432
+ */
433
+ const asyncFlow = (zone, tag, guestFunc, options = undefined) => {
434
+ const makeAsyncFlowKit = prepareAsyncFlowKit(zone, tag, guestFunc, options);
435
+ const hostFuncName = `${tag}_hostFlow`;
436
+ const wrapperFunc = {
437
+ [hostFuncName](...args) {
438
+ const { flow } = makeAsyncFlowKit(args);
439
+ return flow.getOutcome();
440
+ },
441
+ }[hostFuncName];
442
+ defineProperties(wrapperFunc, {
443
+ length: { value: guestFunc.length },
444
+ });
445
+ return harden(wrapperFunc);
446
+ };
447
+
448
+ const adminAsyncFlow = outerZone.exo('AdminAsyncFlow', AdminAsyncFlowI, {
449
+ getFailures() {
450
+ return failures.snapshot();
451
+ },
452
+ wakeAll() {
453
+ // [...stuff.keys()] in order to snapshot before iterating
454
+ const failuresToRestart = [...failures.keys()];
455
+ const flowsToWake = [...eagerWakers.keys()];
456
+ for (const flow of failuresToRestart) {
457
+ flow.restart();
458
+ }
459
+ for (const flow of flowsToWake) {
460
+ flow.wake();
461
+ }
462
+ },
463
+ getFlowForOutcomeVow(outcomeVow) {
464
+ return flowForOutcomeVowKey.get(toPassableCap(outcomeVow));
465
+ },
466
+ });
467
+
468
+ // Cannot call this until everything is prepared, so postpone to a later
469
+ // turn. (Ideally, we'd postpone to a later crank because prepares are
470
+ // allowed anytime in the first crank. But there's currently no pleasant
471
+ // way to postpone to a later crank.)
472
+ // See https://github.com/Agoric/agoric-sdk/issues/9377
473
+ const allWokenP = E.when(null, () => adminAsyncFlow.wakeAll());
474
+
475
+ return harden({
476
+ prepareAsyncFlowKit,
477
+ asyncFlow,
478
+ adminAsyncFlow,
479
+ allWokenP,
480
+ });
481
+ };
482
+ harden(prepareAsyncFlowTools);
483
+
484
+ /**
485
+ * @typedef {ReturnType<prepareAsyncFlowTools>} AsyncFlowTools
486
+ */
487
+
488
+ /**
489
+ * @typedef {AsyncFlowTools['adminAsyncFlow']} AdminAsyncFlow
490
+ */
491
+
492
+ /**
493
+ * @typedef {ReturnType<AsyncFlowTools['prepareAsyncFlowKit']>} MakeAsyncFlowKit
494
+ */
495
+
496
+ /**
497
+ * @typedef {ReturnType<MakeAsyncFlowKit>} AsyncFlowKit
498
+ */
499
+
500
+ /**
501
+ * @typedef {AsyncFlowKit['flow']} AsyncFlow
502
+ */
@@ -0,0 +1,28 @@
1
+ export function prepareBijection(zone: Zone): () => import("@endo/exo").Guarded<{
2
+ reset(): void;
3
+ init(g: any, h: any): void;
4
+ hasGuest(g: any): boolean;
5
+ hasHost(h: any): boolean;
6
+ has(g: any, h: any): boolean;
7
+ guestToHost(g: any): any;
8
+ hostToGuest(h: any): any;
9
+ }>;
10
+ export type VowishStore = ReturnType<(name: string) => {
11
+ init: (k: any, v: any) => void;
12
+ has: (k: any) => boolean;
13
+ get: (k: any) => any;
14
+ } & import("@endo/pass-style").RemotableObject<`Alleged: ${string}`> & import("@endo/eventual-send").RemotableBrand<{}, {
15
+ init: (k: any, v: any) => void;
16
+ has: (k: any) => boolean;
17
+ get: (k: any) => any;
18
+ }>>;
19
+ export type Bijection = ReturnType<ReturnType<(zone: Zone) => () => import("@endo/exo").Guarded<{
20
+ reset(): void;
21
+ init(g: any, h: any): void;
22
+ hasGuest(g: any): boolean;
23
+ hasHost(h: any): boolean;
24
+ has(g: any, h: any): boolean;
25
+ guestToHost(g: any): any;
26
+ hostToGuest(h: any): any;
27
+ }>>>;
28
+ //# sourceMappingURL=bijection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bijection.d.ts","sourceRoot":"","sources":["bijection.js"],"names":[],"mappings":"AA0DO;;;;;;;;GAoEN;0BAzEa,UAAU,QAhCb,MAAM;;;;;;;;GAgCwB;wBA6E5B,UAAU,CAAC,UAAU;;;;;;;;GAAkB,CAAC"}
@@ -0,0 +1,132 @@
1
+ import { b, Fail } from '@endo/errors';
2
+ import { M } from '@endo/patterns';
3
+ import { Far } from '@endo/pass-style';
4
+ import { toPassableCap } from '@agoric/vow';
5
+ import { makeEphemera } from './ephemera.js';
6
+
7
+ const BijectionI = M.interface('Bijection', {
8
+ reset: M.call().returns(),
9
+ init: M.call(M.any(), M.any()).returns(),
10
+ hasGuest: M.call(M.any()).returns(M.boolean()),
11
+ hasHost: M.call(M.any()).returns(M.boolean()),
12
+ has: M.call(M.any(), M.any()).returns(M.boolean()),
13
+ guestToHost: M.call(M.any()).returns(M.any()),
14
+ hostToGuest: M.call(M.any()).returns(M.any()),
15
+ });
16
+
17
+ /**
18
+ * Makes a store like a WeakMapStore except that Promises and Vows can also be
19
+ * used as keys.
20
+ * NOTE: This depends on promise identity being stable!
21
+ *
22
+ * @param {string} name
23
+ */
24
+ const makeVowishStore = name => {
25
+ // This internal map could be (and was) a WeakMap. But there are various ways
26
+ // in which a WeakMap is more expensive than a Map. The main advantage is
27
+ // that a WeakMap can drop entries whose keys are not otherwise retained.
28
+ // But async-flow only uses a bijection together with a log-store that happens
29
+ // to durably retain all the host-side keys of the associated bijection, so
30
+ // this additional feature of the bijection is irrelevant. When the bijection
31
+ // is reset or revived in a new incarnation, these vowishStores will be gone
32
+ // anyway, dropping all the guest-side objects.
33
+ const map = new Map();
34
+
35
+ return Far(name, {
36
+ init: (k, v) => {
37
+ const k2 = toPassableCap(k);
38
+ !map.has(k2) ||
39
+ // separate line so I can set a breakpoint
40
+ Fail`${b(name)} key already bound: ${k} -> ${map.get(k2)} vs ${v}`;
41
+ map.set(k2, v);
42
+ },
43
+ has: k => map.has(toPassableCap(k)),
44
+ get: k => {
45
+ const k2 = toPassableCap(k);
46
+ map.has(k2) ||
47
+ // separate line so I can set a breakpoint
48
+ Fail`${b(name)} key not found: ${k}`;
49
+ return map.get(k2);
50
+ },
51
+ });
52
+ };
53
+
54
+ /** @typedef {ReturnType<makeVowishStore>} VowishStore */
55
+
56
+ /**
57
+ * @param {Zone} zone
58
+ */
59
+ export const prepareBijection = zone => {
60
+ /** @type {Ephemera<Bijection, VowishStore>} */
61
+ const g2h = makeEphemera(() => makeVowishStore('guestToHost'));
62
+ /** @type {Ephemera<Bijection, VowishStore>} */
63
+ const h2g = makeEphemera(() => makeVowishStore('hostToGuest'));
64
+
65
+ return zone.exoClass('Bijection', BijectionI, () => ({}), {
66
+ reset() {
67
+ const { self } = this;
68
+
69
+ g2h.resetFor(self);
70
+ h2g.resetFor(self);
71
+ },
72
+ init(g, h) {
73
+ const { self } = this;
74
+ const guestToHost = g2h.for(self);
75
+ const hostToGuest = h2g.for(self);
76
+
77
+ !hostToGuest.has(h) ||
78
+ Fail`hostToGuest key already bound: ${h} -> ${hostToGuest.get(h)} vs ${g}`;
79
+ guestToHost.init(g, h);
80
+ hostToGuest.init(h, g);
81
+ self.has(g, h) ||
82
+ // separate line so I can set a breakpoint
83
+ Fail`internal: ${g} <-> ${h}`;
84
+ },
85
+ hasGuest(g) {
86
+ const { self } = this;
87
+ const guestToHost = g2h.for(self);
88
+
89
+ return guestToHost.has(g);
90
+ },
91
+ hasHost(h) {
92
+ const { self } = this;
93
+ const hostToGuest = h2g.for(self);
94
+
95
+ return hostToGuest.has(h);
96
+ },
97
+ has(g, h) {
98
+ const { self } = this;
99
+ const guestToHost = g2h.for(self);
100
+ const hostToGuest = h2g.for(self);
101
+
102
+ if (guestToHost.has(g)) {
103
+ toPassableCap(guestToHost.get(g)) === toPassableCap(h) ||
104
+ Fail`internal: g->h ${g} -> ${h} vs ${guestToHost.get(g)}`;
105
+ hostToGuest.get(h) === g ||
106
+ Fail`internal h->g: ${h} -> ${g} vs ${hostToGuest.get(h)}`;
107
+ return true;
108
+ } else {
109
+ !hostToGuest.has(h) ||
110
+ Fail`internal: unexpected h->g ${h} -> ${hostToGuest.get(h)}`;
111
+ return false;
112
+ }
113
+ },
114
+ guestToHost(g) {
115
+ const { self } = this;
116
+ const guestToHost = g2h.for(self);
117
+
118
+ return guestToHost.get(g);
119
+ },
120
+ hostToGuest(h) {
121
+ const { self } = this;
122
+ const hostToGuest = h2g.for(self);
123
+
124
+ return hostToGuest.get(h);
125
+ },
126
+ });
127
+ };
128
+ harden(prepareBijection);
129
+
130
+ /**
131
+ * @typedef {ReturnType<ReturnType<prepareBijection>>} Bijection
132
+ */
@@ -0,0 +1,5 @@
1
+ export function makeConvertKit(bijection: any, makeGuestForHostRemotable: any, makeGuestForHostVow: any): {
2
+ guestToHost: (specimen: Passable, label?: string | undefined) => any;
3
+ hostToGuest: (specimen: Passable, label?: string | undefined) => any;
4
+ };
5
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["convert.js"],"names":[],"mappings":"AA2EO;;;EAsDN"}