@agoric/vow 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.
@@ -0,0 +1,220 @@
1
+ // @ts-check
2
+
3
+ import { M } from '@endo/patterns';
4
+ import { PromiseWatcherI } from '@agoric/base-zone';
5
+
6
+ const { Fail, bare, details: X } = assert;
7
+
8
+ /**
9
+ * @import {MapStore} from '@agoric/store/src/types.js';
10
+ * @import {Zone} from '@agoric/base-zone';
11
+ * @import {Watch} from './watch.js';
12
+ * @import {When} from './when.js';
13
+ * @import {VowKit, AsPromiseFunction, IsRetryableReason, EVow} from './types.js';
14
+ */
15
+
16
+ const VowShape = M.tagged(
17
+ 'Vow',
18
+ harden({
19
+ vowV0: M.remotable('VowV0'),
20
+ }),
21
+ );
22
+
23
+ /**
24
+ * Like `provideLazy`, but accepts non-Passable values.
25
+ *
26
+ * @param {WeakMap} map
27
+ * @param {any} key
28
+ * @param {(key: any) => any} makeValue
29
+ */
30
+ const provideLazyMap = (map, key, makeValue) => {
31
+ if (!map.has(key)) {
32
+ map.set(key, makeValue(key));
33
+ }
34
+ return map.get(key);
35
+ };
36
+
37
+ /**
38
+ * @param {Zone} zone
39
+ * @param {object} powers
40
+ * @param {Watch} powers.watch
41
+ * @param {When} powers.when
42
+ * @param {() => VowKit<any>} powers.makeVowKit
43
+ * @param {IsRetryableReason} powers.isRetryableReason
44
+ */
45
+ export const prepareWatchUtils = (
46
+ zone,
47
+ { watch, when, makeVowKit, isRetryableReason },
48
+ ) => {
49
+ const detached = zone.detached();
50
+ const utilsToNonStorableResults = new WeakMap();
51
+
52
+ const makeWatchUtilsKit = zone.exoClassKit(
53
+ 'WatchUtils',
54
+ {
55
+ utils: M.interface('Utils', {
56
+ all: M.call(M.arrayOf(M.any())).returns(VowShape),
57
+ asPromise: M.call(M.raw()).rest(M.raw()).returns(M.promise()),
58
+ }),
59
+ watcher: M.interface('Watcher', {
60
+ onFulfilled: M.call(M.any()).rest(M.any()).returns(M.any()),
61
+ onRejected: M.call(M.any()).rest(M.any()).returns(M.any()),
62
+ }),
63
+ retryRejectionPromiseWatcher: PromiseWatcherI,
64
+ },
65
+ () => {
66
+ /**
67
+ * @typedef {object} VowState
68
+ * @property {number} remaining
69
+ * @property {MapStore<number, any>} resultsMap
70
+ * @property {VowKit['resolver']} resolver
71
+ */
72
+ /** @type {MapStore<bigint, VowState>} */
73
+ const idToVowState = detached.mapStore('idToVowState');
74
+
75
+ return {
76
+ nextId: 0n,
77
+ idToVowState,
78
+ };
79
+ },
80
+ {
81
+ utils: {
82
+ /**
83
+ * @param {EVow<unknown>[]} vows
84
+ */
85
+ all(vows) {
86
+ const { nextId: id, idToVowState } = this.state;
87
+ /** @type {VowKit<any[]>} */
88
+ const kit = makeVowKit();
89
+
90
+ // Preserve the order of the vow results.
91
+ for (let index = 0; index < vows.length; index += 1) {
92
+ watch(vows[index], this.facets.watcher, {
93
+ id,
94
+ index,
95
+ numResults: vows.length,
96
+ });
97
+ }
98
+
99
+ if (vows.length > 0) {
100
+ // Save the state until rejection or all fulfilled.
101
+ this.state.nextId += 1n;
102
+ idToVowState.init(
103
+ id,
104
+ harden({
105
+ resolver: kit.resolver,
106
+ remaining: vows.length,
107
+ resultsMap: detached.mapStore('resultsMap'),
108
+ }),
109
+ );
110
+ const idToNonStorableResults = provideLazyMap(
111
+ utilsToNonStorableResults,
112
+ this.facets.utils,
113
+ () => new Map(),
114
+ );
115
+ idToNonStorableResults.set(id, new Map());
116
+ } else {
117
+ // Base case: nothing to wait for.
118
+ kit.resolver.resolve(harden([]));
119
+ }
120
+ return kit.vow;
121
+ },
122
+ /** @type {AsPromiseFunction} */
123
+ asPromise(specimenP, ...watcherArgs) {
124
+ // Watch the specimen in case it is an ephemeral promise.
125
+ const vow = watch(specimenP, ...watcherArgs);
126
+ const promise = when(vow);
127
+ // Watch the ephemeral result promise to ensure that if its settlement is
128
+ // lost due to upgrade of this incarnation, we will at least cause an
129
+ // unhandled rejection in the new incarnation.
130
+ zone.watchPromise(promise, this.facets.retryRejectionPromiseWatcher);
131
+
132
+ return promise;
133
+ },
134
+ },
135
+ watcher: {
136
+ onFulfilled(value, { id, index, numResults }) {
137
+ const { idToVowState } = this.state;
138
+ if (!idToVowState.has(id)) {
139
+ // Resolution of the returned vow happened already.
140
+ return;
141
+ }
142
+ const { remaining, resultsMap, resolver } = idToVowState.get(id);
143
+ const idToNonStorableResults = provideLazyMap(
144
+ utilsToNonStorableResults,
145
+ this.facets.utils,
146
+ () => new Map(),
147
+ );
148
+ const nonStorableResults = provideLazyMap(
149
+ idToNonStorableResults,
150
+ id,
151
+ () => new Map(),
152
+ );
153
+
154
+ // Capture the fulfilled value.
155
+ if (zone.isStorable(value)) {
156
+ resultsMap.init(index, value);
157
+ } else {
158
+ nonStorableResults.set(index, value);
159
+ }
160
+ const vowState = harden({
161
+ remaining: remaining - 1,
162
+ resultsMap,
163
+ resolver,
164
+ });
165
+ if (vowState.remaining > 0) {
166
+ idToVowState.set(id, vowState);
167
+ return;
168
+ }
169
+ // We're done! Extract the array.
170
+ idToVowState.delete(id);
171
+ const results = new Array(numResults);
172
+ let numLost = 0;
173
+ for (let i = 0; i < numResults; i += 1) {
174
+ if (nonStorableResults.has(i)) {
175
+ results[i] = nonStorableResults.get(i);
176
+ } else if (resultsMap.has(i)) {
177
+ results[i] = resultsMap.get(i);
178
+ } else {
179
+ numLost += 1;
180
+ }
181
+ }
182
+ if (numLost > 0) {
183
+ resolver.reject(
184
+ assert.error(X`${numLost} unstorable results were lost`),
185
+ );
186
+ } else {
187
+ resolver.resolve(harden(results));
188
+ }
189
+ },
190
+ onRejected(value, { id, index: _index, numResults: _numResults }) {
191
+ const { idToVowState } = this.state;
192
+ if (!idToVowState.has(id)) {
193
+ // First rejection wins.
194
+ return;
195
+ }
196
+ const { resolver } = idToVowState.get(id);
197
+ idToVowState.delete(id);
198
+ resolver.reject(value);
199
+ },
200
+ },
201
+ retryRejectionPromiseWatcher: {
202
+ onFulfilled(_result) {},
203
+ onRejected(reason, failedOp) {
204
+ if (isRetryableReason(reason, undefined)) {
205
+ Fail`Pending ${bare(failedOp)} could not retry; ${reason}`;
206
+ }
207
+ },
208
+ },
209
+ },
210
+ );
211
+
212
+ const makeWatchUtil = () => {
213
+ const { utils } = makeWatchUtilsKit();
214
+ return harden(utils);
215
+ };
216
+
217
+ return makeWatchUtil;
218
+ };
219
+
220
+ harden(prepareWatchUtils);
package/src/watch.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export function prepareWatch(zone: Zone, makeVowKit: () => VowKit<any>, isRetryableReason?: ((reason: any, lastValue: any) => any) | undefined): <T = any, TResult1 = T, TResult2 = never, C extends any[] = any[]>(specimenP: EVow<T>, watcher?: Watcher<T, TResult1, TResult2, C> | undefined, ...watcherArgs: C) => Vow<Exclude<TResult1, void> | Exclude<TResult2, void> extends never ? TResult1 : Exclude<TResult1, void> | Exclude<TResult2, void>>;
2
+ export type Watch = ReturnType<typeof prepareWatch>;
3
+ import type { Zone } from '@agoric/base-zone';
4
+ import type { VowKit } from './types.js';
5
+ import type { EVow } from './types.js';
6
+ import type { Watcher } from './types.js';
7
+ import type { Vow } from './types.js';
8
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["watch.js"],"names":[],"mappings":"AA2JO,mCAJI,IAAI,cACJ,MAAM,OAAO,GAAG,CAAC,gCACR,GAAG,aAAa,GAAG,KAAK,GAAG,iBAe/B,CAAC,QACD,QAAQ,MACR,QAAQ,UACA,CAAC,SAAT,GAAG,EAAG,qBACT,KAAK,CAAC,CAAC,2EAEP,CAAC,yIAoBb;oBAIa,UAAU,CAAC,OAAO,YAAY,CAAC;0BA/LJ,mBAAmB;4BACyB,YAAY;0BAAZ,YAAY;6BAAZ,YAAY;yBAAZ,YAAY"}
package/src/watch.js ADDED
@@ -0,0 +1,199 @@
1
+ // @ts-check
2
+ import { PromiseWatcherI } from '@agoric/base-zone';
3
+ import { getVowPayload, basicE } from './vow-utils.js';
4
+
5
+ const { apply } = Reflect;
6
+
7
+ /**
8
+ * @import { PromiseWatcher, Zone } from '@agoric/base-zone';
9
+ * @import { ERef, EVow, IsRetryableReason, Vow, VowKit, VowResolver, Watcher } from './types.js';
10
+ */
11
+
12
+ /**
13
+ * @param {Zone} zone
14
+ */
15
+ const makeWatchNextStep =
16
+ zone =>
17
+ /**
18
+ * If the specimen is a vow, obtain a fresh shortened promise from it,
19
+ * otherwise coerce the non-vow specimen to a promise. Then, associate a
20
+ * (usually durable) watcher object with the promise.
21
+ *
22
+ * @param {any} specimen
23
+ * @param {PromiseWatcher} promiseWatcher
24
+ */
25
+ (specimen, promiseWatcher) => {
26
+ let promise;
27
+ const payload = getVowPayload(specimen);
28
+ if (payload) {
29
+ promise = basicE(payload.vowV0).shorten();
30
+ } else {
31
+ promise = basicE.resolve(specimen);
32
+ }
33
+ zone.watchPromise(promise, promiseWatcher);
34
+ };
35
+
36
+ /**
37
+ * @param {VowResolver | undefined} resolver
38
+ * @param {Watcher<unknown, unknown, unknown> | undefined} watcher
39
+ * @param {keyof Required<Watcher>} wcb
40
+ * @param {unknown} value
41
+ * @param {unknown[]} [watcherArgs]
42
+ */
43
+ const settle = (resolver, watcher, wcb, value, watcherArgs = []) => {
44
+ try {
45
+ let chainedValue = value;
46
+ const w = watcher && watcher[wcb];
47
+ if (w) {
48
+ chainedValue = apply(w, watcher, [value, ...watcherArgs]);
49
+ } else if (wcb === 'onRejected') {
50
+ throw value;
51
+ }
52
+ resolver && resolver.resolve(chainedValue);
53
+ } catch (e) {
54
+ if (resolver) {
55
+ resolver.reject(e);
56
+ } else {
57
+ // for host's unhandled rejection handler to catch
58
+ throw e;
59
+ }
60
+ }
61
+ };
62
+
63
+ /**
64
+ * @param {Zone} zone
65
+ * @param {IsRetryableReason} isRetryableReason
66
+ * @param {ReturnType<typeof makeWatchNextStep>} watchNextStep
67
+ */
68
+ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => {
69
+ // We use an ephemeral WeakSet for the previously seen vows in a watch operation
70
+ // While watch is durable, it suffices to detect the cycle in a single incarnation
71
+ /** @type {WeakMap<PromiseWatcher, WeakSet<any>>} */
72
+ const watcherSeenPayloads = new WeakMap();
73
+
74
+ /** @param {PromiseWatcher} watcher */
75
+ const getSeenPayloads = watcher => {
76
+ let seenPayloads = watcherSeenPayloads.get(watcher);
77
+ if (!seenPayloads) {
78
+ seenPayloads = new WeakSet();
79
+ watcherSeenPayloads.set(watcher, seenPayloads);
80
+ }
81
+ return seenPayloads;
82
+ };
83
+
84
+ return zone.exoClass(
85
+ 'PromiseWatcher',
86
+ PromiseWatcherI,
87
+ /**
88
+ * @template [T=any]
89
+ * @template [TResult1=T]
90
+ * @template [TResult2=never]
91
+ * @param {VowResolver<TResult1 | TResult2>} resolver
92
+ * @param {Watcher<T, TResult1, TResult2>} [watcher]
93
+ * @param {unknown[]} [watcherArgs]
94
+ */
95
+ (resolver, watcher, watcherArgs) => {
96
+ const state = {
97
+ vow: /** @type {unknown} */ (undefined),
98
+ priorRetryValue: /** @type {any} */ (undefined),
99
+ resolver,
100
+ watcher,
101
+ watcherArgs: harden(watcherArgs),
102
+ };
103
+ return /** @type {Partial<typeof state>} */ (state);
104
+ },
105
+ {
106
+ /** @type {Required<PromiseWatcher>['onFulfilled']} */
107
+ onFulfilled(value) {
108
+ const { watcher, watcherArgs, resolver } = this.state;
109
+ const payload = getVowPayload(value);
110
+ if (payload) {
111
+ const seenPayloads = getSeenPayloads(this.self);
112
+ // TODO: rely on endowed helper to get storable cap from payload
113
+ if (seenPayloads.has(payload.vowV0)) {
114
+ return this.self.onRejected(Error('Vow resolution cycle detected'));
115
+ }
116
+ seenPayloads.add(payload.vowV0);
117
+ // We've been shortened, so reflect our state accordingly, and go again.
118
+ this.state.vow = value;
119
+ watchNextStep(value, this.self);
120
+ return;
121
+ }
122
+ watcherSeenPayloads.delete(this.self);
123
+ this.state.priorRetryValue = undefined;
124
+ this.state.watcher = undefined;
125
+ this.state.resolver = undefined;
126
+ settle(resolver, watcher, 'onFulfilled', value, watcherArgs);
127
+ },
128
+ /** @type {Required<PromiseWatcher>['onRejected']} */
129
+ onRejected(reason) {
130
+ const { vow, watcher, watcherArgs, resolver, priorRetryValue } =
131
+ this.state;
132
+ if (vow) {
133
+ const retryValue = isRetryableReason(reason, priorRetryValue);
134
+ if (retryValue) {
135
+ // Retry the same specimen.
136
+ this.state.priorRetryValue = retryValue;
137
+ watchNextStep(vow, this.self);
138
+ return;
139
+ }
140
+ }
141
+ watcherSeenPayloads.delete(this.self);
142
+ this.state.priorRetryValue = undefined;
143
+ this.state.resolver = undefined;
144
+ this.state.watcher = undefined;
145
+ settle(resolver, watcher, 'onRejected', reason, watcherArgs);
146
+ },
147
+ },
148
+ );
149
+ };
150
+
151
+ /**
152
+ * @param {Zone} zone
153
+ * @param {() => VowKit<any>} makeVowKit
154
+ * @param {(reason: any, lastValue: any) => any} [isRetryableReason]
155
+ */
156
+ export const prepareWatch = (
157
+ zone,
158
+ makeVowKit,
159
+ isRetryableReason = (_reason, _lastValue) => undefined,
160
+ ) => {
161
+ const watchNextStep = makeWatchNextStep(zone);
162
+ const makePromiseWatcher = preparePromiseWatcher(
163
+ zone,
164
+ isRetryableReason,
165
+ watchNextStep,
166
+ );
167
+
168
+ /**
169
+ * @template [T=any]
170
+ * @template [TResult1=T]
171
+ * @template [TResult2=never]
172
+ * @template {any[]} [C=any[]] watcher args
173
+ * @param {EVow<T>} specimenP
174
+ * @param {Watcher<T, TResult1, TResult2, C>} [watcher]
175
+ * @param {C} watcherArgs
176
+ */
177
+ const watch = (specimenP, watcher, ...watcherArgs) => {
178
+ /** @typedef {Exclude<TResult1, void> | Exclude<TResult2, void>} Voidless */
179
+ /** @typedef {Voidless extends never ? TResult1 : Voidless} Narrowest */
180
+ /** @type {VowKit<Narrowest>} */
181
+ const { resolver, vow } = makeVowKit();
182
+
183
+ // Create a promise watcher to track vows, retrying upon rejection as
184
+ // controlled by `isRetryableReason`.
185
+ const promiseWatcher = makePromiseWatcher(resolver, watcher, watcherArgs);
186
+
187
+ // Coerce the specimen to a promise, and start the watcher cycle.
188
+ zone.watchPromise(basicE.resolve(specimenP), promiseWatcher);
189
+
190
+ return vow;
191
+ };
192
+ harden(watch);
193
+
194
+ return watch;
195
+ };
196
+
197
+ harden(prepareWatch);
198
+
199
+ /** @typedef {ReturnType<typeof prepareWatch>} Watch */
package/src/when.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export function makeWhen(isRetryableReason?: IsRetryableReason | undefined): <T, TResult1 = EUnwrap<T>, TResult2 = never>(specimenP: T, onFulfilled?: ((value: EUnwrap<T>) => TResult1 | PromiseLike<TResult1>) | undefined, onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined) => Promise<TResult1 | TResult2>;
2
+ export type When = ReturnType<typeof makeWhen>;
3
+ import type { IsRetryableReason } from './types.js';
4
+ import type { EUnwrap } from './types.js';
5
+ //# sourceMappingURL=when.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"when.d.ts","sourceRoot":"","sources":["when.js"],"names":[],"mappings":"AAQO,8EAMQ,CAAC,EACA,QAAQ,eACR,QAAQ,qBACX,CAAC,yBACO,QAAQ,CAAC,CAAC,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,sCAC9C,GAAG,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,kBAC/C,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAmD1C;mBAIa,UAAU,CAAC,OAAO,QAAQ,CAAC;uCAxEO,YAAY;6BAAZ,YAAY"}
package/src/when.js ADDED
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+ import { getVowPayload, basicE } from './vow-utils.js';
3
+
4
+ /** @import { IsRetryableReason, EUnwrap } from './types.js' */
5
+
6
+ /**
7
+ * @param {IsRetryableReason} [isRetryableReason]
8
+ */
9
+ export const makeWhen = (
10
+ isRetryableReason = /** @type {IsRetryableReason} */ (() => false),
11
+ ) => {
12
+ /**
13
+ * Shorten `specimenP` until we achieve a final result.
14
+ *
15
+ * @template T
16
+ * @template [TResult1=EUnwrap<T>]
17
+ * @template [TResult2=never]
18
+ * @param {T} specimenP value to unwrap
19
+ * @param {(value: EUnwrap<T>) => TResult1 | PromiseLike<TResult1>} [onFulfilled]
20
+ * @param {(reason: any) => TResult2 | PromiseLike<TResult2>} [onRejected]
21
+ * @returns {Promise<TResult1 | TResult2>}
22
+ */
23
+ const when = async (specimenP, onFulfilled, onRejected) => {
24
+ // Ensure we don't run until a subsequent turn.
25
+ await null;
26
+
27
+ // Ensure we have a presence that won't be disconnected later.
28
+ let result = await specimenP;
29
+ let payload = getVowPayload(result);
30
+ let priorRetryValue;
31
+ const seenPayloads = new WeakSet();
32
+ while (payload) {
33
+ // TODO: rely on endowed helpers for getting storable cap and performing
34
+ // shorten "next step"
35
+ const { vowV0 } = payload;
36
+ if (seenPayloads.has(vowV0)) {
37
+ throw Error('Vow resolution cycle detected');
38
+ }
39
+ result = await basicE(vowV0)
40
+ .shorten()
41
+ .then(
42
+ res => {
43
+ seenPayloads.add(vowV0);
44
+ priorRetryValue = undefined;
45
+ return res;
46
+ },
47
+ e => {
48
+ const nextValue = isRetryableReason(e, priorRetryValue);
49
+ if (nextValue) {
50
+ // Shorten the same specimen to try again.
51
+ priorRetryValue = nextValue;
52
+ return result;
53
+ }
54
+ throw e;
55
+ },
56
+ );
57
+ // Advance to the next vow.
58
+ payload = getVowPayload(result);
59
+ }
60
+
61
+ const unwrapped = /** @type {EUnwrap<T>} */ (result);
62
+
63
+ // We've extracted the final result.
64
+ if (onFulfilled == null && onRejected == null) {
65
+ return /** @type {TResult1} */ (unwrapped);
66
+ }
67
+ return basicE.resolve(unwrapped).then(onFulfilled, onRejected);
68
+ };
69
+ harden(when);
70
+
71
+ return when;
72
+ };
73
+
74
+ harden(makeWhen);
75
+
76
+ /** @typedef {ReturnType<typeof makeWhen>} When */
package/vat.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @file specialization of the `@agoric/vow` package for the vat disconnection rejections produced by
3
+ * the SwingSet kernel.
4
+ */
5
+
6
+ // @ts-check
7
+ import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
8
+ import { makeHeapZone } from '@agoric/base-zone/heap.js';
9
+ import { prepareVowTools as rawPrepareVowTools } from './src/index.js';
10
+
11
+ export { EVow$, OrVow$ } from './src/index.js';
12
+
13
+ /** @type {import('./src/types.js').IsRetryableReason} */
14
+ const isRetryableReason = (reason, priorRetryValue) => {
15
+ if (
16
+ isUpgradeDisconnection(reason) &&
17
+ (!priorRetryValue ||
18
+ reason.incarnationNumber > priorRetryValue.incarnationNumber)
19
+ ) {
20
+ return reason;
21
+ }
22
+ return undefined;
23
+ };
24
+
25
+ export const defaultPowers = harden({
26
+ isRetryableReason,
27
+ });
28
+
29
+ /**
30
+ * Produce SwingSet-compatible vowTools, with an arbitrary Zone type
31
+ *
32
+ * @type {typeof rawPrepareVowTools}
33
+ */
34
+ export const prepareVowTools = (zone, powers = {}) =>
35
+ rawPrepareVowTools(zone, { ...defaultPowers, ...powers });
36
+
37
+ /**
38
+ * `vowTools` that are not durable, but are useful in non-durable clients that
39
+ * need to consume vows from other SwingSet vats.
40
+ */
41
+ export const heapVowTools = prepareVowTools(makeHeapZone());
42
+
43
+ export const { E: heapVowE } = heapVowTools;