@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
package/src/types.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {PromiseKit} from '@endo/promise-kit'
|
|
3
|
+
* @import {Passable} from '@endo/pass-style'
|
|
4
|
+
* @import {Zone} from '@agoric/base-zone'
|
|
5
|
+
* @import {Vow, VowTools} from '@agoric/vow'
|
|
6
|
+
* @import {LogStore} from './log-store.js'
|
|
7
|
+
* @import {Bijection} from './bijection.js'
|
|
8
|
+
* @import {ReplayMembrane} from './replay-membrane.js'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {'Running' |
|
|
13
|
+
* 'Sleeping' |
|
|
14
|
+
* 'Replaying' |
|
|
15
|
+
* 'Failed' |
|
|
16
|
+
* 'Done'
|
|
17
|
+
* } FlowState
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @template {Passable} [T=Passable]
|
|
22
|
+
* @typedef {T} Guest
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @template {Passable} [T=Passable]
|
|
27
|
+
* @typedef {T} Host
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A HostVow must be durably storable. It corresponds to an
|
|
32
|
+
* ephemeral guest promise.
|
|
33
|
+
*
|
|
34
|
+
* @template {Passable} [T=Passable]
|
|
35
|
+
* @typedef {Host<Vow<T>>} HostVow
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {(...activationArgs: Guest[]) => Guest<Promise>} GuestAsyncFunc
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {(...activationArgs: Host[]) => HostVow} HostAsyncFuncWrapper
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} PreparationOptions
|
|
48
|
+
* @property {VowTools} [vowTools]
|
|
49
|
+
* @property {() => LogStore} [makeLogStore]
|
|
50
|
+
* @property {() => Bijection} [makeBijection]
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {'return'|'throw'} OutcomeKind
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {{kind: 'return', result: any}
|
|
59
|
+
* | {kind: 'throw', problem: any}
|
|
60
|
+
* } Outcome
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @template {WeakKey} [S=WeakKey]
|
|
65
|
+
* @template {any} [V=any]
|
|
66
|
+
* @typedef {object} Ephemera
|
|
67
|
+
* @property {(self: S) => V} for
|
|
68
|
+
* @property {(self: S) => void} resetFor
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This is the typedef for the membrane log entries we currently implement.
|
|
73
|
+
* See comment below for the commented-out typedef for the full
|
|
74
|
+
* membrane log entry, which we do not yet support.
|
|
75
|
+
*
|
|
76
|
+
* @typedef {[ // ///////////////// From Host to Guest /////////////////////////
|
|
77
|
+
* op: 'doFulfill',
|
|
78
|
+
* vow: HostVow,
|
|
79
|
+
* fulfillment: Host,
|
|
80
|
+
* ] | [
|
|
81
|
+
* op: 'doReject',
|
|
82
|
+
* vow: HostVow,
|
|
83
|
+
* reason: Host,
|
|
84
|
+
* ] | [
|
|
85
|
+
* op: 'doReturn',
|
|
86
|
+
* callIndex: number,
|
|
87
|
+
* result: Host,
|
|
88
|
+
* ] | [
|
|
89
|
+
* op: 'doThrow',
|
|
90
|
+
* callIndex: number,
|
|
91
|
+
* problem: Host,
|
|
92
|
+
* ] | [ // ///////////////////// From Guest to Host /////////////////////////
|
|
93
|
+
* op: 'checkCall',
|
|
94
|
+
* target: Host,
|
|
95
|
+
* optVerb: PropertyKey|undefined,
|
|
96
|
+
* args: Host[],
|
|
97
|
+
* callIndex: number
|
|
98
|
+
* ]} LogEntry
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* This would be the typedef for the full membrane log, if we supported
|
|
103
|
+
* - the guest sending guest-promises and guest-remotables to the host
|
|
104
|
+
* - the guest using `E` to eventual-send to guest wrappers of host
|
|
105
|
+
* vows and remotables.
|
|
106
|
+
*
|
|
107
|
+
* at-typedef {[ // ///////////////// From Host to Guest ///////////////////////
|
|
108
|
+
* op: 'doFulfill',
|
|
109
|
+
* vow: HostVow,
|
|
110
|
+
* fulfillment: Host,
|
|
111
|
+
* ] | [
|
|
112
|
+
* op: 'doReject',
|
|
113
|
+
* vow: HostVow,
|
|
114
|
+
* reason: Host,
|
|
115
|
+
* ] | [
|
|
116
|
+
* op: 'doCall',
|
|
117
|
+
* target: Host,
|
|
118
|
+
* optVerb: PropertyKey|undefined,
|
|
119
|
+
* args: Host[],
|
|
120
|
+
* callIndex: number
|
|
121
|
+
* ] | [
|
|
122
|
+
* op: 'doSend',
|
|
123
|
+
* target: Host,
|
|
124
|
+
* optVerb: PropertyKey|undefined,
|
|
125
|
+
* args: Host[],
|
|
126
|
+
* callIndex: number
|
|
127
|
+
* ] | [
|
|
128
|
+
* op: 'doReturn',
|
|
129
|
+
* callIndex: number,
|
|
130
|
+
* result: Host,
|
|
131
|
+
* ] | [
|
|
132
|
+
* op: 'doThrow',
|
|
133
|
+
* callIndex: number,
|
|
134
|
+
* problem: Host,
|
|
135
|
+
* ] | [ // ///////////////////// From Guest to Host /////////////////////////
|
|
136
|
+
* op: 'checkFulfill',
|
|
137
|
+
* vow: HostVow,
|
|
138
|
+
* fulfillment: Host,
|
|
139
|
+
* ] | [
|
|
140
|
+
* op: 'checkReject',
|
|
141
|
+
* vow: HostVow,
|
|
142
|
+
* reason: Host,
|
|
143
|
+
* ] | [
|
|
144
|
+
* op: 'checkCall',
|
|
145
|
+
* target: Host,
|
|
146
|
+
* optVerb: PropertyKey|undefined,
|
|
147
|
+
* args: Host[],
|
|
148
|
+
* callIndex: number
|
|
149
|
+
* ] | [
|
|
150
|
+
* op: 'checkSend',
|
|
151
|
+
* target: Host,
|
|
152
|
+
* optVerb: PropertyKey|undefined,
|
|
153
|
+
* args: Host[],
|
|
154
|
+
* callIndex: number
|
|
155
|
+
* ] | [
|
|
156
|
+
* op: 'checkReturn',
|
|
157
|
+
* callIndex: number,
|
|
158
|
+
* result: Host,
|
|
159
|
+
* ] | [
|
|
160
|
+
* op: 'checkThrow',
|
|
161
|
+
* callIndex: number,
|
|
162
|
+
* problem: Host,
|
|
163
|
+
* ]} LogEntry
|
|
164
|
+
*/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The purpose of this test file is to demonstrate
|
|
2
|
+
// https://github.com/Agoric/agoric-sdk/issues/9377
|
|
3
|
+
// as the `test.serial.failing` test at the end.
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line import/order
|
|
6
|
+
import {
|
|
7
|
+
test,
|
|
8
|
+
getBaggage,
|
|
9
|
+
annihilate,
|
|
10
|
+
nextLife,
|
|
11
|
+
} from './prepare-test-env-ava.js';
|
|
12
|
+
|
|
13
|
+
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
|
|
14
|
+
import { prepareVowTools } from '@agoric/vow';
|
|
15
|
+
import { makeDurableZone } from '@agoric/zone/durable.js';
|
|
16
|
+
|
|
17
|
+
import { prepareAsyncFlowTools } from '../src/async-flow.js';
|
|
18
|
+
|
|
19
|
+
const neverSettlesP = new Promise(() => {});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {any} t
|
|
23
|
+
* @param {Zone} zone
|
|
24
|
+
*/
|
|
25
|
+
const testPlay1 = async (t, zone) => {
|
|
26
|
+
const vowTools = prepareVowTools(zone);
|
|
27
|
+
const { asyncFlow } = prepareAsyncFlowTools(zone, {
|
|
28
|
+
vowTools,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const guestFunc = async () => neverSettlesP;
|
|
32
|
+
|
|
33
|
+
const wrapperFunc = asyncFlow(zone, 'guestFunc', guestFunc);
|
|
34
|
+
|
|
35
|
+
wrapperFunc();
|
|
36
|
+
t.pass();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {any} t
|
|
41
|
+
* @param {Zone} zone
|
|
42
|
+
*/
|
|
43
|
+
const testPlay2 = async (t, zone) => {
|
|
44
|
+
const vowTools = prepareVowTools(zone);
|
|
45
|
+
const { asyncFlow } = prepareAsyncFlowTools(zone, {
|
|
46
|
+
vowTools,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const guestFunc = async () => neverSettlesP;
|
|
50
|
+
|
|
51
|
+
t.notThrows(() => asyncFlow(zone, 'guestFunc', guestFunc));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {any} t
|
|
56
|
+
* @param {Zone} zone
|
|
57
|
+
*/
|
|
58
|
+
const testPlay3 = async (t, zone) => {
|
|
59
|
+
const vowTools = prepareVowTools(zone);
|
|
60
|
+
const { asyncFlow, allWokenP } = prepareAsyncFlowTools(zone, {
|
|
61
|
+
vowTools,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await null;
|
|
65
|
+
|
|
66
|
+
const guestFunc = async () => neverSettlesP;
|
|
67
|
+
t.notThrows(() => asyncFlow(zone, 'guestFunc', guestFunc));
|
|
68
|
+
t.notThrowsAsync(
|
|
69
|
+
() => allWokenP,
|
|
70
|
+
'will actually throw due to crank bug #9377',
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
test.serial('test durable first-crank hazard 1', async t => {
|
|
75
|
+
annihilate();
|
|
76
|
+
const zone1 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
77
|
+
await testPlay1(t, zone1);
|
|
78
|
+
|
|
79
|
+
await eventLoopIteration();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test.serial('test durable first-crank hazard 2', async t => {
|
|
83
|
+
nextLife();
|
|
84
|
+
const zone2 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
85
|
+
await testPlay2(t, zone2);
|
|
86
|
+
|
|
87
|
+
await eventLoopIteration();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test.serial.failing('test durable first-crank hazard 3', async t => {
|
|
91
|
+
nextLife();
|
|
92
|
+
const zone3 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
93
|
+
await testPlay3(t, zone3);
|
|
94
|
+
|
|
95
|
+
return eventLoopIteration();
|
|
96
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// eslint-disable-next-line import/order
|
|
2
|
+
import { test, getBaggage, annihilate } from './prepare-test-env-ava.js';
|
|
3
|
+
|
|
4
|
+
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
|
|
5
|
+
import { prepareVowTools } from '@agoric/vow';
|
|
6
|
+
import { makeHeapZone } from '@agoric/zone/heap.js';
|
|
7
|
+
import { makeVirtualZone } from '@agoric/zone/virtual.js';
|
|
8
|
+
import { makeDurableZone } from '@agoric/zone/durable.js';
|
|
9
|
+
|
|
10
|
+
import { prepareAsyncFlowTools } from '../src/async-flow.js';
|
|
11
|
+
|
|
12
|
+
const { apply } = Reflect;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} t
|
|
16
|
+
* @param {Zone} zone
|
|
17
|
+
*/
|
|
18
|
+
const testPlay = async (t, zone) => {
|
|
19
|
+
const vowTools = prepareVowTools(zone);
|
|
20
|
+
const { asyncFlow } = prepareAsyncFlowTools(zone, {
|
|
21
|
+
vowTools,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { guestFunc } = {
|
|
25
|
+
async guestFunc() {
|
|
26
|
+
t.is(this, undefined);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const wrapperFunc = asyncFlow(zone, 'guestFunc', guestFunc);
|
|
31
|
+
|
|
32
|
+
// Demonstrates that even if something is passed as `this` to the wrapperFunc,
|
|
33
|
+
// the guestFunc will run with `this` bound to `undefined`.
|
|
34
|
+
apply(wrapperFunc, 'bogus', []);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
test.serial('test heap no guest this', async t => {
|
|
38
|
+
annihilate();
|
|
39
|
+
const zone = makeHeapZone('heapRoot');
|
|
40
|
+
await testPlay(t, zone);
|
|
41
|
+
|
|
42
|
+
await eventLoopIteration();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.serial('test virtual no guest this', async t => {
|
|
46
|
+
annihilate();
|
|
47
|
+
const zone = makeVirtualZone('virtualRoot');
|
|
48
|
+
await testPlay(t, zone);
|
|
49
|
+
|
|
50
|
+
await eventLoopIteration();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test.serial('test durable no guest this', async t => {
|
|
54
|
+
annihilate();
|
|
55
|
+
const zone = makeDurableZone(getBaggage(), 'durableRoot');
|
|
56
|
+
await testPlay(t, zone);
|
|
57
|
+
|
|
58
|
+
await eventLoopIteration();
|
|
59
|
+
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// eslint-disable-next-line import/order
|
|
2
|
+
import {
|
|
3
|
+
test,
|
|
4
|
+
getBaggage,
|
|
5
|
+
annihilate,
|
|
6
|
+
nextLife,
|
|
7
|
+
} from './prepare-test-env-ava.js';
|
|
8
|
+
|
|
9
|
+
import { Fail } from '@endo/errors';
|
|
10
|
+
import { passStyleOf } from '@endo/pass-style';
|
|
11
|
+
import { makeCopyMap } from '@endo/patterns';
|
|
12
|
+
import { makePromiseKit } from '@endo/promise-kit';
|
|
13
|
+
import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
|
|
14
|
+
import { isVow } from '@agoric/vow/src/vow-utils.js';
|
|
15
|
+
import { prepareVowTools } from '@agoric/vow';
|
|
16
|
+
import { makeHeapZone } from '@agoric/zone/heap.js';
|
|
17
|
+
import { makeVirtualZone } from '@agoric/zone/virtual.js';
|
|
18
|
+
import { makeDurableZone } from '@agoric/zone/durable.js';
|
|
19
|
+
|
|
20
|
+
import { prepareAsyncFlowTools } from '../src/async-flow.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @import {AsyncFlow} from '../src/async-flow.js'
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Zone} zone
|
|
28
|
+
* @param {number} [k]
|
|
29
|
+
*/
|
|
30
|
+
const prepareOrchestra = (zone, k = 1) =>
|
|
31
|
+
zone.exoClass(
|
|
32
|
+
'Orchestra',
|
|
33
|
+
undefined,
|
|
34
|
+
(factor, vow, resolver) => ({ factor, vow, resolver }),
|
|
35
|
+
{
|
|
36
|
+
scale(n) {
|
|
37
|
+
const { state } = this;
|
|
38
|
+
return k * state.factor * n;
|
|
39
|
+
},
|
|
40
|
+
vow() {
|
|
41
|
+
const { state } = this;
|
|
42
|
+
return state.vow;
|
|
43
|
+
},
|
|
44
|
+
resolve(x) {
|
|
45
|
+
const { state } = this;
|
|
46
|
+
state.resolver.resolve(x);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/** @typedef {ReturnType<ReturnType<prepareOrchestra>>} Orchestra */
|
|
52
|
+
|
|
53
|
+
const firstLogLen = 7;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {any} t
|
|
57
|
+
* @param {Zone} zone
|
|
58
|
+
*/
|
|
59
|
+
const testFirstPlay = async (t, zone) => {
|
|
60
|
+
t.log('firstPlay started');
|
|
61
|
+
const vowTools = prepareVowTools(zone);
|
|
62
|
+
const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, {
|
|
63
|
+
vowTools,
|
|
64
|
+
});
|
|
65
|
+
const makeOrchestra = prepareOrchestra(zone);
|
|
66
|
+
const { makeVowKit } = vowTools;
|
|
67
|
+
|
|
68
|
+
const { vow: v1, resolver: r1 } = zone.makeOnce('v1', () => makeVowKit());
|
|
69
|
+
const { vow: v2, resolver: r2 } = zone.makeOnce('v2', () => makeVowKit());
|
|
70
|
+
const { vow: v3, resolver: _r3 } = zone.makeOnce('v3', () => makeVowKit());
|
|
71
|
+
const hOrch7 = zone.makeOnce('hOrch7', () => makeOrchestra(7, v2, r2));
|
|
72
|
+
|
|
73
|
+
// purposely violate rule that guestMethod is closed.
|
|
74
|
+
const { promise: promiseStep, resolve: resolveStep } = makePromiseKit();
|
|
75
|
+
|
|
76
|
+
const { guestMethod } = {
|
|
77
|
+
async guestMethod(gOrch7, g1, _p3) {
|
|
78
|
+
t.log(' firstPlay about to await g1');
|
|
79
|
+
t.is(await g1, 'x');
|
|
80
|
+
const p2 = gOrch7.vow();
|
|
81
|
+
const prod = gOrch7.scale(3);
|
|
82
|
+
t.is(prod, 21);
|
|
83
|
+
|
|
84
|
+
let gErr;
|
|
85
|
+
try {
|
|
86
|
+
gOrch7.scale(9n);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
gErr = e;
|
|
89
|
+
}
|
|
90
|
+
t.is(gErr.name, 'TypeError');
|
|
91
|
+
|
|
92
|
+
resolveStep(true);
|
|
93
|
+
t.log(' firstPlay to hang awaiting p2');
|
|
94
|
+
// awaiting a promise that won't be resolved until next incarnation
|
|
95
|
+
await p2;
|
|
96
|
+
t.fail('must not reach here in first incarnation');
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const wrapperFunc = asyncFlow(zone, 'AsyncFlow1', guestMethod);
|
|
101
|
+
|
|
102
|
+
const outcomeV = zone.makeOnce('outcomeV', () => wrapperFunc(hOrch7, v1, v3));
|
|
103
|
+
|
|
104
|
+
t.true(isVow(outcomeV));
|
|
105
|
+
r1.resolve('x');
|
|
106
|
+
|
|
107
|
+
const flow = zone.makeOnce('flow', () =>
|
|
108
|
+
adminAsyncFlow.getFlowForOutcomeVow(outcomeV),
|
|
109
|
+
);
|
|
110
|
+
t.is(passStyleOf(flow), 'remotable');
|
|
111
|
+
|
|
112
|
+
await promiseStep;
|
|
113
|
+
|
|
114
|
+
const logDump = flow.dump();
|
|
115
|
+
t.is(logDump.length, firstLogLen);
|
|
116
|
+
t.deepEqual(logDump, [
|
|
117
|
+
['doFulfill', v1, 'x'],
|
|
118
|
+
['checkCall', hOrch7, 'vow', [], 1],
|
|
119
|
+
['doReturn', 1, v2],
|
|
120
|
+
['checkCall', hOrch7, 'scale', [3], 3],
|
|
121
|
+
['doReturn', 3, 21],
|
|
122
|
+
['checkCall', hOrch7, 'scale', [9n], 5],
|
|
123
|
+
[
|
|
124
|
+
'doThrow',
|
|
125
|
+
5,
|
|
126
|
+
TypeError('Cannot mix BigInt and other types, use explicit conversions'),
|
|
127
|
+
],
|
|
128
|
+
]);
|
|
129
|
+
t.log('firstPlay done');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {any} t
|
|
134
|
+
* @param {Zone} zone
|
|
135
|
+
*/
|
|
136
|
+
const testBadReplay = async (t, zone) => {
|
|
137
|
+
t.log('badReplay started');
|
|
138
|
+
const vowTools = prepareVowTools(zone);
|
|
139
|
+
const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, {
|
|
140
|
+
vowTools,
|
|
141
|
+
});
|
|
142
|
+
prepareOrchestra(zone);
|
|
143
|
+
const { when } = vowTools;
|
|
144
|
+
const hOrch7 = /** @type {Orchestra} */ (
|
|
145
|
+
zone.makeOnce('hOrch7', () => Fail`need hOrch7`)
|
|
146
|
+
);
|
|
147
|
+
// purposely violate rule that guestMethod is closed.
|
|
148
|
+
const { promise: promiseStep, resolve: resolveStep } = makePromiseKit();
|
|
149
|
+
|
|
150
|
+
const { guestMethod } = {
|
|
151
|
+
async guestMethod(gOrch7, g1, _p3) {
|
|
152
|
+
t.log(' badReplay about to await g1');
|
|
153
|
+
await g1;
|
|
154
|
+
gOrch7.vow();
|
|
155
|
+
resolveStep(true);
|
|
156
|
+
// This is a replay error
|
|
157
|
+
gOrch7.scale(4);
|
|
158
|
+
t.fail('badReplay must not reach here');
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// `asyncFlow` can be used simply to re-prepare the guest function
|
|
163
|
+
// by ignoring the returned wrapper function. If the wrapper function is
|
|
164
|
+
// invoked, that would be a *new* activation with a new outcome and
|
|
165
|
+
// flow, and would have nothing to do with the existing one.
|
|
166
|
+
asyncFlow(zone, 'AsyncFlow1', guestMethod);
|
|
167
|
+
|
|
168
|
+
const outcomeV = /** @type {Vow} */ (
|
|
169
|
+
zone.makeOnce('outcomeV', () => Fail`need outcomeV`)
|
|
170
|
+
);
|
|
171
|
+
const flow = /** @type {AsyncFlow} */ (
|
|
172
|
+
zone.makeOnce('flow', () => Fail`need flow`)
|
|
173
|
+
);
|
|
174
|
+
const flow1 = adminAsyncFlow.getFlowForOutcomeVow(outcomeV);
|
|
175
|
+
t.is(flow, flow1);
|
|
176
|
+
t.is(passStyleOf(flow), 'remotable');
|
|
177
|
+
|
|
178
|
+
// This unblocks `await p2;` but only after the replay failure is fixed in
|
|
179
|
+
// the next incarnation.
|
|
180
|
+
hOrch7.resolve('y');
|
|
181
|
+
await promiseStep;
|
|
182
|
+
t.is(await when(hOrch7.vow()), 'y');
|
|
183
|
+
|
|
184
|
+
const logDump = flow.dump();
|
|
185
|
+
t.is(logDump.length, firstLogLen);
|
|
186
|
+
|
|
187
|
+
const replayProblem = flow.getOptFatalProblem();
|
|
188
|
+
t.true(replayProblem instanceof Error);
|
|
189
|
+
|
|
190
|
+
t.deepEqual(
|
|
191
|
+
adminAsyncFlow.getFailures(),
|
|
192
|
+
makeCopyMap([[flow, replayProblem]]),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
t.log(' badReplay failures', flow.getOptFatalProblem());
|
|
196
|
+
t.log('badReplay done');
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {any} t
|
|
201
|
+
* @param {Zone} zone
|
|
202
|
+
*/
|
|
203
|
+
const testGoodReplay = async (t, zone) => {
|
|
204
|
+
t.log('goodReplay started');
|
|
205
|
+
const vowTools = prepareVowTools(zone);
|
|
206
|
+
const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, {
|
|
207
|
+
vowTools,
|
|
208
|
+
});
|
|
209
|
+
prepareOrchestra(zone, 2); // Note change in new behavior
|
|
210
|
+
const { when } = vowTools;
|
|
211
|
+
const hOrch7 = /** @type {Orchestra} */ (
|
|
212
|
+
zone.makeOnce('hOrch7', () => Fail`need hOrch7`)
|
|
213
|
+
);
|
|
214
|
+
// purposely violate rule that guestMethod is closed.
|
|
215
|
+
const { promise: promiseStep, resolve: resolveStep } = makePromiseKit();
|
|
216
|
+
|
|
217
|
+
const { guestMethod } = {
|
|
218
|
+
async guestMethod(gOrch7, g1, p3) {
|
|
219
|
+
t.log(' goodReplay about to await g1');
|
|
220
|
+
await g1;
|
|
221
|
+
const p2 = gOrch7.vow();
|
|
222
|
+
const prod = gOrch7.scale(3);
|
|
223
|
+
t.is(prod, 21);
|
|
224
|
+
|
|
225
|
+
let gErr;
|
|
226
|
+
try {
|
|
227
|
+
gOrch7.scale(9n);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
gErr = e;
|
|
230
|
+
}
|
|
231
|
+
t.is(gErr.name, 'TypeError');
|
|
232
|
+
|
|
233
|
+
resolveStep(true);
|
|
234
|
+
t.log(' goodReplay about to await p2');
|
|
235
|
+
// awaiting a promise that won't be resolved until this incarnation
|
|
236
|
+
await p2;
|
|
237
|
+
t.log(' goodReplay woke up!');
|
|
238
|
+
const prod2 = gOrch7.scale(3);
|
|
239
|
+
// same question. different answer
|
|
240
|
+
t.is(prod2, 42);
|
|
241
|
+
t.log('about to await p3');
|
|
242
|
+
await p3;
|
|
243
|
+
t.log('p3 settled');
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// `asyncFlow` can be used simply to re-prepare the guest function
|
|
248
|
+
// by ignoring the returned wrapper function. If the wrapper function is
|
|
249
|
+
// invoked, that would be a *new* activation with a new outcome and
|
|
250
|
+
// flow, and would have nothing to do with the existing one.
|
|
251
|
+
asyncFlow(zone, 'AsyncFlow1', guestMethod);
|
|
252
|
+
|
|
253
|
+
const outcomeV = /** @type {Vow} */ (
|
|
254
|
+
zone.makeOnce('outcomeV', () => Fail`need outcomeV`)
|
|
255
|
+
);
|
|
256
|
+
const flow = /** @type {AsyncFlow} */ (
|
|
257
|
+
zone.makeOnce('flow', () => Fail`need flow`)
|
|
258
|
+
);
|
|
259
|
+
const flow1 = adminAsyncFlow.getFlowForOutcomeVow(outcomeV);
|
|
260
|
+
t.is(flow, flow1);
|
|
261
|
+
t.is(passStyleOf(flow), 'remotable');
|
|
262
|
+
|
|
263
|
+
const v2 = hOrch7.vow();
|
|
264
|
+
t.is(await when(v2), 'y');
|
|
265
|
+
await eventLoopIteration();
|
|
266
|
+
|
|
267
|
+
await promiseStep;
|
|
268
|
+
|
|
269
|
+
const { vow: v1 } = zone.makeOnce('v1', () => Fail`need v1`);
|
|
270
|
+
const { resolver: r3 } = zone.makeOnce('v3', () => Fail`need v3`);
|
|
271
|
+
|
|
272
|
+
const logDump = flow.dump();
|
|
273
|
+
t.is(logDump.length, firstLogLen + 3);
|
|
274
|
+
t.deepEqual(logDump, [
|
|
275
|
+
['doFulfill', v1, 'x'],
|
|
276
|
+
['checkCall', hOrch7, 'vow', [], 1],
|
|
277
|
+
['doReturn', 1, v2],
|
|
278
|
+
['checkCall', hOrch7, 'scale', [3], 3],
|
|
279
|
+
['doReturn', 3, 21],
|
|
280
|
+
['checkCall', hOrch7, 'scale', [9n], 5],
|
|
281
|
+
[
|
|
282
|
+
'doThrow',
|
|
283
|
+
5,
|
|
284
|
+
TypeError('Cannot mix BigInt and other types, use explicit conversions'),
|
|
285
|
+
],
|
|
286
|
+
// new stuff
|
|
287
|
+
['doFulfill', v2, 'y'],
|
|
288
|
+
['checkCall', hOrch7, 'scale', [3], firstLogLen + 1],
|
|
289
|
+
// same question. different answer
|
|
290
|
+
['doReturn', firstLogLen + 1, 42],
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
// @ts-expect-error TS doesn't know it is a resolver
|
|
294
|
+
r3.resolve('done');
|
|
295
|
+
await eventLoopIteration();
|
|
296
|
+
|
|
297
|
+
t.is(await when(outcomeV), undefined);
|
|
298
|
+
t.deepEqual(flow.dump(), []);
|
|
299
|
+
|
|
300
|
+
t.log('goodReplay done', await promiseStep);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {any} t
|
|
305
|
+
* @param {Zone} zone
|
|
306
|
+
*/
|
|
307
|
+
const testAfterPlay = async (t, zone) => {
|
|
308
|
+
t.log('testAfterPlay started');
|
|
309
|
+
const vowTools = prepareVowTools(zone);
|
|
310
|
+
const { asyncFlow, adminAsyncFlow } = prepareAsyncFlowTools(zone, {
|
|
311
|
+
vowTools,
|
|
312
|
+
});
|
|
313
|
+
prepareOrchestra(zone);
|
|
314
|
+
|
|
315
|
+
const { guestMethod } = {
|
|
316
|
+
async guestMethod(_gOrch7, _gP, _p3) {
|
|
317
|
+
t.fail('Must not replay this');
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// `asyncFlow` can be used simply to re-prepare the guest function
|
|
322
|
+
// by ignoring the returned wrapper function. If the wrapper function is
|
|
323
|
+
// invoked, that would be a *new* activation with a new outcome and
|
|
324
|
+
// flow, and would have nothing to do with the existing one.
|
|
325
|
+
asyncFlow(zone, 'AsyncFlow1', guestMethod);
|
|
326
|
+
|
|
327
|
+
const outcomeV = /** @type {Vow} */ (
|
|
328
|
+
zone.makeOnce('outcomeV', () => Fail`need outcomeV`)
|
|
329
|
+
);
|
|
330
|
+
const flow = /** @type {AsyncFlow} */ (
|
|
331
|
+
zone.makeOnce('flow', () => Fail`need flow`)
|
|
332
|
+
);
|
|
333
|
+
t.is(passStyleOf(flow), 'remotable');
|
|
334
|
+
|
|
335
|
+
t.throws(() => adminAsyncFlow.getFlowForOutcomeVow(outcomeV), {
|
|
336
|
+
message:
|
|
337
|
+
'key "[Alleged: VowInternalsKit vowV0]" not found in collection "flowForOutcomeVow"',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Even this doesn't wake it up.
|
|
341
|
+
flow.wake();
|
|
342
|
+
await eventLoopIteration();
|
|
343
|
+
|
|
344
|
+
t.log('testAfterDoneReplay done');
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
test.serial('test heap async-flow', async t => {
|
|
348
|
+
const zone = makeHeapZone('heapRoot');
|
|
349
|
+
return testFirstPlay(t, zone);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test.serial('test virtual async-flow', async t => {
|
|
353
|
+
annihilate();
|
|
354
|
+
const zone = makeVirtualZone('virtualRoot');
|
|
355
|
+
return testFirstPlay(t, zone);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test.serial('test durable async-flow', async t => {
|
|
359
|
+
annihilate();
|
|
360
|
+
const zone1 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
361
|
+
await testFirstPlay(t, zone1);
|
|
362
|
+
|
|
363
|
+
await eventLoopIteration();
|
|
364
|
+
|
|
365
|
+
nextLife();
|
|
366
|
+
const zone2 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
367
|
+
await testBadReplay(t, zone2);
|
|
368
|
+
|
|
369
|
+
await eventLoopIteration();
|
|
370
|
+
|
|
371
|
+
nextLife();
|
|
372
|
+
const zone3 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
373
|
+
await testGoodReplay(t, zone3);
|
|
374
|
+
|
|
375
|
+
await eventLoopIteration();
|
|
376
|
+
|
|
377
|
+
nextLife();
|
|
378
|
+
const zone4 = makeDurableZone(getBaggage(), 'durableRoot');
|
|
379
|
+
return testAfterPlay(t, zone4);
|
|
380
|
+
});
|