@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.
- 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 +2 -0
- package/index.d.ts.map +1 -0
- package/index.js +1 -0
- package/package.json +67 -0
- package/src/async-flow.d.ts +87 -0
- package/src/async-flow.d.ts.map +1 -0
- package/src/async-flow.js +502 -0
- package/src/bijection.d.ts +28 -0
- package/src/bijection.d.ts.map +1 -0
- package/src/bijection.js +132 -0
- package/src/convert.d.ts +5 -0
- package/src/convert.d.ts.map +1 -0
- package/src/convert.js +131 -0
- package/src/ephemera.d.ts +2 -0
- package/src/ephemera.d.ts.map +1 -0
- package/src/ephemera.js +35 -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 +25 -0
- package/src/log-store.d.ts.map +1 -0
- package/src/log-store.js +165 -0
- package/src/replay-membrane.d.ts +71 -0
- package/src/replay-membrane.d.ts.map +1 -0
- package/src/replay-membrane.js +435 -0
- package/src/type-guards.d.ts +4 -0
- package/src/type-guards.d.ts.map +1 -0
- package/src/type-guards.js +54 -0
- package/src/types.d.ts +70 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +164 -0
- package/test/async-flow-crank.test.js +96 -0
- package/test/async-flow-no-this.js +59 -0
- package/test/async-flow.test.js +380 -0
- package/test/bad-host.test.js +205 -0
- package/test/bijection.test.js +118 -0
- package/test/convert.test.js +127 -0
- package/test/equate.test.js +116 -0
- package/test/log-store.test.js +112 -0
- package/test/prepare-test-env-ava.js +28 -0
- package/test/replay-membrane-settlement.test.js +154 -0
- package/test/replay-membrane-zombie.test.js +158 -0
- package/test/replay-membrane.test.js +271 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +13 -0
- 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"}
|
package/src/bijection.js
ADDED
|
@@ -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
|
+
*/
|
package/src/convert.d.ts
ADDED
|
@@ -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"}
|