@evolu/common 8.0.0-next.4 → 8.0.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/dist/src/Array.d.ts +279 -173
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +105 -101
- package/dist/src/Assert.d.ts +44 -44
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +44 -32
- package/dist/src/BigInt.d.ts +6 -3
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -2
- package/dist/src/Brand.d.ts +27 -40
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Buffer.d.ts +27 -11
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Cache.d.ts +9 -4
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +8 -3
- package/dist/src/Callbacks.d.ts +30 -19
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Console.d.ts +145 -66
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +91 -43
- package/dist/src/Crypto.d.ts +85 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +44 -20
- package/dist/src/Eq.d.ts +87 -21
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +67 -19
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- package/dist/src/Function.d.ts +119 -58
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +86 -43
- package/dist/src/Http.d.ts +394 -0
- package/dist/src/Http.d.ts.map +1 -0
- package/dist/src/Http.js +102 -0
- package/dist/src/Identicon.d.ts +16 -7
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +14 -5
- package/dist/src/LeakDetector.d.ts +88 -0
- package/dist/src/LeakDetector.d.ts.map +1 -0
- package/dist/src/LeakDetector.js +86 -0
- package/dist/src/LockManager.d.ts +18 -9
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +7 -8
- package/dist/src/Lookup.d.ts +43 -13
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +28 -8
- package/dist/src/Microtask.d.ts +1 -1
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +3 -7
- package/dist/src/Number.d.ts +49 -13
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +34 -5
- package/dist/src/Object.d.ts +160 -40
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +92 -46
- package/dist/src/Option.d.ts +10 -7
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +9 -6
- package/dist/src/Order.d.ts +21 -21
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +18 -21
- package/dist/src/Platform.d.ts +31 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +98 -0
- package/dist/src/Random.d.ts +32 -17
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +12 -8
- package/dist/src/Redacted.d.ts +43 -29
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +14 -3
- package/dist/src/Ref.d.ts +11 -3
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +2 -2
- package/dist/src/RefCount.d.ts +2 -6
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +33 -90
- package/dist/src/Relation.d.ts +70 -5
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +77 -2
- package/dist/src/Resource.d.ts +590 -155
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +592 -343
- package/dist/src/Result.d.ts +838 -191
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +163 -102
- package/dist/src/Schedule.d.ts +684 -231
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +766 -295
- package/dist/src/Set.d.ts +89 -40
- package/dist/src/Set.d.ts.map +1 -1
- package/dist/src/Set.js +50 -29
- package/dist/src/Sqlite.d.ts +29 -72
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +41 -46
- package/dist/src/StackTrace.d.ts +40 -0
- package/dist/src/StackTrace.d.ts.map +1 -0
- package/dist/src/StackTrace.js +140 -0
- package/dist/src/Store.d.ts +5 -4
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +5 -4
- package/dist/src/String.d.ts +7 -1
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +20 -7
- package/dist/src/Task.d.ts +3572 -1770
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1879 -1274
- package/dist/src/Test.d.ts +17 -125
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +17 -68
- package/dist/src/Time.d.ts +108 -49
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +172 -73
- package/dist/src/Type.d.ts +3542 -1926
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +3070 -1784
- package/dist/src/Types.d.ts +200 -108
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +39 -8
- package/dist/src/WebSocket.d.ts +48 -23
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +15 -6
- package/dist/src/Worker.d.ts +94 -36
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +58 -24
- package/dist/src/index.d.ts +55 -52
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/cs.d.ts +94 -0
- package/dist/src/intl/cs.d.ts.map +1 -0
- package/dist/src/intl/cs.js +220 -0
- package/dist/src/intl/index.d.ts +7 -0
- package/dist/src/intl/index.d.ts.map +1 -0
- package/dist/src/intl/index.js +6 -0
- package/dist/src/local-first/Db.d.ts +12 -8
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -132
- package/dist/src/local-first/Error.d.ts +4 -4
- package/dist/src/local-first/Evolu.d.ts +338 -92
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +47 -55
- package/dist/src/local-first/LocalAuth.d.ts +4 -4
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/Owner.d.ts +61 -24
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +45 -8
- package/dist/src/local-first/Protocol.d.ts +44 -31
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +36 -13
- package/dist/src/local-first/Query.d.ts +147 -62
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +112 -58
- package/dist/src/local-first/Relay.d.ts +44 -14
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +92 -41
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +25 -8
- package/dist/src/local-first/Shared.d.ts +23 -21
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +100 -57
- package/dist/src/local-first/Storage.d.ts +53 -32
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +194 -234
- package/dist/src/local-first/Timestamp.d.ts +21 -21
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +2 -2
- package/dist/src/local-first/index.d.ts +11 -11
- package/package.json +14 -13
- package/src/Array.ts +306 -205
- package/src/Assert.ts +54 -69
- package/src/BigInt.ts +6 -3
- package/src/Brand.ts +27 -40
- package/src/Buffer.ts +28 -12
- package/src/Cache.ts +9 -4
- package/src/Callbacks.ts +30 -19
- package/src/Console.ts +156 -84
- package/src/Crypto.ts +83 -41
- package/src/Eq.ts +95 -22
- package/src/Error.ts +8 -4
- package/src/Function.ts +167 -59
- package/src/Http.ts +549 -0
- package/src/Identicon.ts +16 -7
- package/src/LeakDetector.ts +182 -0
- package/src/LockManager.ts +43 -39
- package/src/Lookup.ts +62 -28
- package/src/Microtask.ts +19 -23
- package/src/Number.ts +64 -12
- package/src/Object.ts +173 -49
- package/src/Option.ts +10 -7
- package/src/Order.ts +23 -21
- package/src/Platform.ts +136 -0
- package/src/Polyfills.ts +6 -12
- package/src/Random.ts +32 -19
- package/src/Redacted.ts +44 -30
- package/src/Ref.ts +14 -6
- package/src/RefCount.ts +38 -80
- package/src/Relation.ts +202 -10
- package/src/Resource.ts +1199 -591
- package/src/Result.ts +930 -271
- package/src/Schedule.ts +859 -343
- package/src/Set.ts +89 -40
- package/src/Sqlite.ts +144 -138
- package/src/StackTrace.ts +230 -0
- package/src/Store.ts +8 -7
- package/src/String.ts +22 -5
- package/src/Task.ts +5101 -2832
- package/src/Test.ts +20 -168
- package/src/Time.ts +293 -90
- package/src/Type.ts +10034 -4117
- package/src/Types.ts +227 -110
- package/src/WebSocket.ts +64 -36
- package/src/Worker.ts +180 -72
- package/src/index.ts +55 -52
- package/src/intl/cs.ts +388 -0
- package/src/intl/index.ts +7 -0
- package/src/local-first/Db.ts +152 -179
- package/src/local-first/Error.ts +4 -4
- package/src/local-first/Evolu.ts +495 -250
- package/src/local-first/LocalAuth.ts +6 -12
- package/src/local-first/Owner.ts +58 -21
- package/src/local-first/Protocol.ts +49 -31
- package/src/local-first/Query.ts +146 -61
- package/src/local-first/Relay.ts +51 -21
- package/src/local-first/Schema.ts +109 -51
- package/src/local-first/Shared.ts +266 -226
- package/src/local-first/Storage.ts +266 -264
- package/src/local-first/Timestamp.ts +25 -18
- package/src/local-first/index.ts +11 -11
- package/dist/src/Skiplist.d.ts +0 -28
- package/dist/src/Skiplist.d.ts.map +0 -1
- package/dist/src/Skiplist.js +0 -64
- package/dist/src/Tracer.d.ts +0 -48
- package/dist/src/Tracer.d.ts.map +0 -1
- package/dist/src/Tracer.js +0 -6
- package/src/Skiplist.ts +0 -108
- package/src/Tracer.ts +0 -54
package/dist/src/Task.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JavaScript-native structured concurrency.
|
|
3
|
-
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
1
|
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
7
2
|
if (value !== null && value !== void 0) {
|
|
8
3
|
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
@@ -55,1261 +50,2118 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
55
50
|
var e = new Error(message);
|
|
56
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
57
52
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
/**
|
|
54
|
+
* JavaScript-native structured concurrency.
|
|
55
|
+
*
|
|
56
|
+
* @module
|
|
57
|
+
*/
|
|
58
|
+
import { emptyArray, isNonEmptyArray, mapArray, } from "./Array.js";
|
|
59
|
+
import { assert, assertNonEmptyReadonlyArray, assertNonNullable, assertNotDisposed, } from "./Assert.js";
|
|
60
|
+
import { createConsole, testCreateConsole, } from "./Console.js";
|
|
61
|
+
import { createRandomBytes, testCreateRandomBytes, } from "./Crypto.js";
|
|
62
62
|
import { eqArrayStrict } from "./Eq.js";
|
|
63
|
-
import {
|
|
63
|
+
import { constTrue, constVoid, identity } from "./Function.js";
|
|
64
|
+
import { createLeakDetector, noopLeakDetector, testCreateLeakDetector, } from "./LeakDetector.js";
|
|
64
65
|
import { createLookupMap } from "./Lookup.js";
|
|
65
|
-
import { decrement, increment } from "./Number.js";
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
66
|
+
import { decrement, increment, incrementPositiveInt } from "./Number.js";
|
|
67
|
+
import { emptyRecord, mapObject, objectFromEntries, objectToEntries, } from "./Object.js";
|
|
68
|
+
import { none, some } from "./Option.js";
|
|
69
|
+
import { isDev } from "./Platform.js";
|
|
70
|
+
import { createRandom, testCreateRandom, testCreateRandomLib, } from "./Random.js";
|
|
68
71
|
import { createRef } from "./Ref.js";
|
|
69
|
-
import { err, getOrThrow, ok,
|
|
70
|
-
import {
|
|
71
|
-
import { createTime,
|
|
72
|
-
import {
|
|
73
|
-
import {} from "./Types.js";
|
|
72
|
+
import { err, getOk, getOrThrow, ok, } from "./Result.js";
|
|
73
|
+
import { emptySet } from "./Set.js";
|
|
74
|
+
import { createTime, PositiveMillis, testCreateTime, } from "./Time.js";
|
|
75
|
+
import { assertType, createId, maxPositiveInt, NonNegativeInt, object, onePositiveInt, PositiveInt, record, String, typed, Unknown, UnknownResult, } from "./Type.js";
|
|
74
76
|
/**
|
|
75
|
-
*
|
|
76
|
-
* {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | AbortSignal}.
|
|
77
|
-
*
|
|
78
|
-
* This is structured-concurrency control flow, not a domain error. It plays a
|
|
79
|
-
* role similar to an interrupt: in most code, propagate it unchanged or ignore
|
|
80
|
-
* it when cleanup is already handled by the runtime.
|
|
81
|
-
*
|
|
82
|
-
* The `reason` field is `unknown` by design — use typed errors for business
|
|
83
|
-
* logic. If you need to inspect the reason, use type guards like
|
|
84
|
-
* `RaceLostError.is(reason)`.
|
|
77
|
+
* Typed object explaining why a {@link Run} was aborted.
|
|
85
78
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* those cases, abort usually indicates a lifecycle bug, so use
|
|
91
|
-
* `assertNotAborted` if you want to fail fast.
|
|
79
|
+
* A reason has a `type` discriminant and optional structured data, so abort
|
|
80
|
+
* causes can carry typed domain data. Well-known reasons are
|
|
81
|
+
* {@link explicitAbortReason}, {@link runDisposedAbortReason}, and
|
|
82
|
+
* {@link PanicAbortReason}.
|
|
92
83
|
*
|
|
93
|
-
* @group Core
|
|
84
|
+
* @group Core
|
|
94
85
|
*/
|
|
95
|
-
export const
|
|
96
|
-
|
|
97
|
-
});
|
|
86
|
+
export const AbortReason = /*#__PURE__*/ object({ type: String },
|
|
87
|
+
/*#__PURE__*/ record(String, Unknown));
|
|
98
88
|
/**
|
|
99
|
-
*
|
|
89
|
+
* Typed object representing structured-concurrency abort control flow.
|
|
100
90
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
91
|
+
* AbortError is thrown to stop Task execution when a Run observes an abort
|
|
92
|
+
* request. AbortableFiber catches AbortError and returns it as a {@link Result}
|
|
93
|
+
* error, so abort can be handled as an ordinary Task outcome.
|
|
103
94
|
*
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
95
|
+
* The reason explains why the Run was aborted. It can be an explicit abort
|
|
96
|
+
* reason, {@link runDisposedAbortReason} for normal Run cleanup, or
|
|
97
|
+
* {@link PanicAbortReason} when a defect panicked the Run tree. The original
|
|
98
|
+
* defect is available as `panicAbortReason.defect` for diagnostics.
|
|
106
99
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* - Tasks inherit their parent's mask by default
|
|
100
|
+
* AbortError is reserved for Task abort control flow. Do not throw or reject
|
|
101
|
+
* with AbortError for domain errors; return a Result error instead.
|
|
110
102
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
103
|
+
* Helpers that abort their own child Tasks should catch or normalize AbortError
|
|
104
|
+
* before it escapes the helper boundary. The reason carries typed domain data.
|
|
113
105
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
106
|
+
* WebKit fetch rejects with its own abort error instead of `signal.reason`.
|
|
107
|
+
* Native wrappers should treat `signal.reason` as the source of truth and
|
|
108
|
+
* normalize aborts to AbortError.
|
|
116
109
|
*
|
|
117
|
-
* @group
|
|
110
|
+
* @group Core
|
|
118
111
|
*/
|
|
119
|
-
export const
|
|
112
|
+
export const AbortError = /*#__PURE__*/ typed("AbortError", {
|
|
113
|
+
reason: AbortReason,
|
|
114
|
+
});
|
|
120
115
|
/**
|
|
121
|
-
* {@link
|
|
116
|
+
* Creates an {@link AbortError} from an {@link AbortReason}.
|
|
122
117
|
*
|
|
123
|
-
* @group
|
|
118
|
+
* @group Core
|
|
124
119
|
*/
|
|
125
|
-
export const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
result: UnknownResult,
|
|
130
|
-
outcome: UnknownResult,
|
|
131
|
-
}));
|
|
120
|
+
export const createAbortError = (reason) => ({
|
|
121
|
+
type: "AbortError",
|
|
122
|
+
reason,
|
|
123
|
+
});
|
|
132
124
|
/**
|
|
133
|
-
*
|
|
125
|
+
* Shared abort reason used for ordinary {@link Run} cleanup.
|
|
126
|
+
*
|
|
127
|
+
* Disposal requests abort so child Tasks stop while the Run waits for them to
|
|
128
|
+
* settle. This reason distinguishes that cleanup path from explicit abort and
|
|
129
|
+
* {@link PanicAbortReason}.
|
|
134
130
|
*
|
|
135
|
-
* @group
|
|
131
|
+
* @group Run
|
|
136
132
|
*/
|
|
137
|
-
export const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
export const runDisposedAbortReason = {
|
|
134
|
+
type: "RunDisposedAbortReason",
|
|
135
|
+
};
|
|
136
|
+
const runDisposedAbortError = createAbortError(runDisposedAbortReason);
|
|
141
137
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* Events bubble up through parent runs, enabling centralized monitoring at the
|
|
145
|
-
* root. Use with {@link Run.onEvent} to track Run lifecycle.
|
|
138
|
+
* Shared abort reason used when callers explicitly request abort without a more
|
|
139
|
+
* specific reason.
|
|
146
140
|
*
|
|
147
|
-
* @group
|
|
141
|
+
* @group Run
|
|
148
142
|
*/
|
|
149
|
-
export const
|
|
150
|
-
|
|
151
|
-
timestamp: Millis,
|
|
152
|
-
data: RunEventData,
|
|
153
|
-
});
|
|
154
|
-
// TODO:
|
|
155
|
-
// Partial<TracerConfigDep> & // TODO:
|
|
156
|
-
// Partial<TracerDep>; // TODO:
|
|
157
|
-
const runDeps = {
|
|
158
|
-
console: createConsole(),
|
|
159
|
-
randomBytes: createRandomBytes(),
|
|
160
|
-
random: createRandom(),
|
|
161
|
-
time: createTime(),
|
|
143
|
+
export const explicitAbortReason = {
|
|
144
|
+
type: "ExplicitAbortReason",
|
|
162
145
|
};
|
|
163
146
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* The root Run is also the daemon Run: it stays running until disposed. Child
|
|
167
|
-
* Runs created by `run(task)` are disposed by their parent once they settle.
|
|
168
|
-
*
|
|
169
|
-
* Call once per entry point (main thread, worker, etc.) and dispose on
|
|
170
|
-
* shutdown. All Tasks run as descendants of this root Run.
|
|
171
|
-
*
|
|
172
|
-
* This common {@link createRun} is platform-agnostic. At application entry
|
|
173
|
-
* points, prefer the platform adapter when one exists. `@evolu/web` adds
|
|
174
|
-
* browser `error` and `unhandledrejection` handlers, `@evolu/nodejs` adds
|
|
175
|
-
* Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
|
|
176
|
-
* handling, and `@evolu/react-native` adds React Native global error handling.
|
|
177
|
-
*
|
|
178
|
-
* {@link RunDeps} provides default dependencies:
|
|
179
|
-
*
|
|
180
|
-
* - {@link Time}
|
|
181
|
-
* - {@link Console}
|
|
182
|
-
* - {@link Random}
|
|
183
|
-
* - {@link RandomBytes}
|
|
184
|
-
*
|
|
185
|
-
* ### Example
|
|
186
|
-
*
|
|
187
|
-
* ```ts
|
|
188
|
-
* // App entry point
|
|
189
|
-
* await using run = createRun();
|
|
190
|
-
*
|
|
191
|
-
* const result = await run(fetchData);
|
|
192
|
-
* ```
|
|
193
|
-
*
|
|
194
|
-
* ### Example with custom dependencies
|
|
147
|
+
* Shared abort reason for tests that need a non-production abort reason.
|
|
195
148
|
*
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
149
|
+
* @group Run
|
|
150
|
+
*/
|
|
151
|
+
export const testAbortReason = {
|
|
152
|
+
type: "TestAbortReason",
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Shared {@link AbortError} for tests, created from {@link testAbortReason}.
|
|
201
156
|
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
|
|
157
|
+
* @group Run
|
|
158
|
+
*/
|
|
159
|
+
export const testAbortError = /*#__PURE__*/ createAbortError(testAbortReason);
|
|
160
|
+
/**
|
|
161
|
+
* Creates a {@link PanicAbortReason} from a defect.
|
|
205
162
|
*
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
*
|
|
163
|
+
* @group Core
|
|
164
|
+
*/
|
|
165
|
+
export const createPanicAbortReason = (defect) => ({
|
|
166
|
+
type: "PanicAbortReason",
|
|
167
|
+
defect,
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Default {@link ReportDefect} for platform-independent {@link createRun}.
|
|
214
171
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
172
|
+
* Throws the reported defect from a queued microtask. This is a fallback for
|
|
173
|
+
* platforms without native global error reporting. Browser adapters use the
|
|
174
|
+
* native
|
|
175
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/reportError | reportError}
|
|
176
|
+
* API; other platform adapters can use platform-specific reporting and preserve
|
|
177
|
+
* nested panic defects as cause or detail data.
|
|
217
178
|
*
|
|
218
|
-
*
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
179
|
+
* @group Run
|
|
180
|
+
*/
|
|
181
|
+
export const reportDefectAfterMicrotask = (defect) => {
|
|
182
|
+
queueMicrotask(() => {
|
|
183
|
+
throw defect;
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Creates {@link RunDefaultDeps}.
|
|
222
188
|
*
|
|
223
|
-
*
|
|
189
|
+
* @group Run
|
|
190
|
+
*/
|
|
191
|
+
export const createRunDefaultDeps = () => {
|
|
192
|
+
const console = createConsole();
|
|
193
|
+
return {
|
|
194
|
+
console,
|
|
195
|
+
leakDetector: isDev ? createLeakDetector({ console }) : noopLeakDetector,
|
|
196
|
+
nativeFetch: globalThis.fetch.bind(globalThis),
|
|
197
|
+
randomBytes: createRandomBytes(),
|
|
198
|
+
random: createRandom(),
|
|
199
|
+
reportDefect: reportDefectAfterMicrotask,
|
|
200
|
+
time: createTime(),
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Creates a root {@link DisposableRun}.
|
|
224
205
|
*
|
|
225
|
-
*
|
|
226
|
-
|
|
206
|
+
* @group Run
|
|
207
|
+
*/
|
|
208
|
+
export const createRun = (deps) => createRunInternal({
|
|
209
|
+
...createRunDefaultDeps(),
|
|
210
|
+
...deps,
|
|
211
|
+
});
|
|
212
|
+
/**
|
|
213
|
+
* Creates {@link TestReportDefect}.
|
|
227
214
|
*
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
* @group Testing
|
|
216
|
+
*/
|
|
217
|
+
export const testCreateReportDefect = () => {
|
|
218
|
+
const defects = [];
|
|
219
|
+
const nextWaiters = [];
|
|
220
|
+
let nextIndex = 0;
|
|
221
|
+
const next = () => {
|
|
222
|
+
if (nextIndex < defects.length) {
|
|
223
|
+
const defect = defects[nextIndex];
|
|
224
|
+
nextIndex += 1;
|
|
225
|
+
return Promise.resolve(defect);
|
|
226
|
+
}
|
|
227
|
+
const nextDefect = Promise.withResolvers();
|
|
228
|
+
nextWaiters.push(nextDefect.resolve);
|
|
229
|
+
return nextDefect.promise;
|
|
230
|
+
};
|
|
231
|
+
const getDefects = () => [...defects];
|
|
232
|
+
const getDefectsSnapshot = () => {
|
|
233
|
+
assert(nextWaiters.length === 0, "getDefectsSnapshot must not be called while reportDefect.next() is pending");
|
|
234
|
+
const snapshot = [...defects];
|
|
235
|
+
defects.length = 0;
|
|
236
|
+
nextIndex = 0;
|
|
237
|
+
return snapshot;
|
|
238
|
+
};
|
|
239
|
+
const clearDefects = () => {
|
|
240
|
+
assert(nextWaiters.length === 0, "clearDefects must not be called while reportDefect.next() is pending");
|
|
241
|
+
defects.length = 0;
|
|
242
|
+
nextIndex = 0;
|
|
243
|
+
};
|
|
244
|
+
return Object.assign((defect) => {
|
|
245
|
+
defects.push(defect);
|
|
246
|
+
const resolveNext = nextWaiters.shift();
|
|
247
|
+
if (!resolveNext)
|
|
248
|
+
return;
|
|
249
|
+
nextIndex += 1;
|
|
250
|
+
resolveNext(defect);
|
|
251
|
+
}, { getDefects, next, getDefectsSnapshot, clearDefects });
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Creates {@link TestRunDefaultDeps}.
|
|
232
255
|
*
|
|
233
|
-
* @group
|
|
256
|
+
* @group Testing
|
|
234
257
|
*/
|
|
235
|
-
export const
|
|
236
|
-
const
|
|
237
|
-
|
|
258
|
+
export const testCreateDeps = (options) => {
|
|
259
|
+
const seed = options?.seed ?? "evolu";
|
|
260
|
+
const console = testCreateConsole();
|
|
261
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
262
|
+
const random = testCreateRandom(seed);
|
|
263
|
+
const randomLib = testCreateRandomLib(seed);
|
|
264
|
+
const randomBytes = testCreateRandomBytes({ randomLib });
|
|
265
|
+
const nativeFetch = () => {
|
|
266
|
+
throw new Error("Provide a nativeFetch test double");
|
|
267
|
+
};
|
|
268
|
+
const reportDefect = testCreateReportDefect();
|
|
269
|
+
const time = testCreateTime();
|
|
270
|
+
return {
|
|
271
|
+
console,
|
|
272
|
+
leakDetector,
|
|
273
|
+
nativeFetch,
|
|
274
|
+
randomBytes,
|
|
275
|
+
random,
|
|
276
|
+
randomLib,
|
|
277
|
+
reportDefect,
|
|
278
|
+
time,
|
|
279
|
+
};
|
|
238
280
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
281
|
+
export function testCreateRun(deps) {
|
|
282
|
+
return createRunInternal({
|
|
283
|
+
...testCreateDeps(),
|
|
284
|
+
...deps,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const globalWithScheduler = globalThis;
|
|
288
|
+
const globalWithSetImmediate = globalThis;
|
|
289
|
+
const taskMetaSymbol = Symbol("evolu.Task.meta");
|
|
290
|
+
const abortableMask = 0;
|
|
291
|
+
const runningRunState = { type: "Running" };
|
|
292
|
+
const createRunInternal = (deps, parent, rootRun, taskMeta) => {
|
|
293
|
+
const abortBehavior = taskMeta?.abortBehavior;
|
|
294
|
+
if (abortBehavior !== undefined && abortBehavior !== "unabortable") {
|
|
295
|
+
assert(parent?.restoreTokens.has(abortBehavior.restoreToken) === true, "restore is only valid inside the unabortableMask that created it");
|
|
252
296
|
}
|
|
297
|
+
// Plain Tasks inherit the parent's mask. `unabortable` increments at start
|
|
298
|
+
// time because its concrete mask is relative to that parent. `restore` is
|
|
299
|
+
// validated by its token above, then uses its captured concrete mask.
|
|
300
|
+
const parentAbortMask = parent?.abortMask ?? abortableMask;
|
|
301
|
+
const abortMask = abortBehavior === undefined
|
|
302
|
+
? parentAbortMask
|
|
303
|
+
: abortBehavior === "unabortable"
|
|
304
|
+
? increment(parentAbortMask)
|
|
305
|
+
: abortBehavior.abortMask;
|
|
306
|
+
let state = runningRunState;
|
|
307
|
+
let exit;
|
|
308
|
+
let snapshot;
|
|
309
|
+
// Keyed by the child Run for snapshot() and removal; the value is the done
|
|
310
|
+
// promise disposal waits on.
|
|
311
|
+
const children = new Map();
|
|
312
|
+
// Invariant: requestController records every abort request immediately;
|
|
313
|
+
// signalController only exposes an abort when this Run's mask allows it.
|
|
314
|
+
// Code that must reject new work after shutdown reads requestAbortSignal.
|
|
253
315
|
const requestController = new AbortController();
|
|
254
316
|
const signalController = new AbortController();
|
|
255
|
-
let
|
|
256
|
-
let
|
|
257
|
-
let
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
317
|
+
let disposePromise;
|
|
318
|
+
let finalizers;
|
|
319
|
+
let finalizerAbortError;
|
|
320
|
+
const reportDefect = (reported) => {
|
|
321
|
+
try {
|
|
322
|
+
deps.reportDefect(reported);
|
|
323
|
+
}
|
|
324
|
+
catch (reporterDefect) {
|
|
325
|
+
reportDefectAfterMicrotask(new AggregateError([reported, reporterDefect], "ReportDefect failed while reporting a defect"));
|
|
326
|
+
}
|
|
264
327
|
};
|
|
265
|
-
if (parent) {
|
|
266
|
-
subscribeToAbort(parent.requestSignal, () => requestAbort(parent.requestSignal.reason), { signal: requestController.signal });
|
|
267
|
-
}
|
|
268
328
|
const emitEvent = (data) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
329
|
+
// Run events are observability-only. Handler and event-construction defects
|
|
330
|
+
// are reported, but must not panic the Run or interrupt Task settlement.
|
|
331
|
+
try {
|
|
332
|
+
if (!deps.runConfig?.eventsEnabled.get())
|
|
333
|
+
return;
|
|
334
|
+
const event = {
|
|
335
|
+
id: run.id,
|
|
336
|
+
timestamp: root.deps.time.now(),
|
|
337
|
+
data,
|
|
338
|
+
};
|
|
339
|
+
for (let node = run; node; node = node.parent) {
|
|
340
|
+
try {
|
|
341
|
+
node.onEvent?.(event);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
reportDefect(error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
275
347
|
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
reportDefect(error);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
// Reads abort reasons from the Run-owned controllers, which are only
|
|
353
|
+
// aborted with AbortError.
|
|
354
|
+
const currentAbort = () => ({
|
|
355
|
+
request: requestController.signal.reason.reason,
|
|
356
|
+
observed: signalController.signal.aborted
|
|
357
|
+
? signalController.signal.reason.reason
|
|
358
|
+
: null,
|
|
359
|
+
});
|
|
360
|
+
const commitState = (nextState) => {
|
|
361
|
+
state = nextState;
|
|
362
|
+
emitEvent({ type: "StateChanged", state });
|
|
363
|
+
};
|
|
364
|
+
const requestAbort = (reason = explicitAbortReason) => {
|
|
365
|
+
if (requestController.signal.aborted)
|
|
366
|
+
return;
|
|
367
|
+
const abortError = createAbortError(reason);
|
|
368
|
+
requestController.abort(abortError);
|
|
369
|
+
if (abortMask === abortableMask)
|
|
370
|
+
signalController.abort(abortError);
|
|
371
|
+
commitState({ type: "Aborted", abort: currentAbort() });
|
|
276
372
|
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
373
|
+
// The first provided exit claims the Run exit; later exits are ignored. A
|
|
374
|
+
// Run disposed without an exit stays claimable (e.g. by a panic during
|
|
375
|
+
// disposal) until children settle, then the exit defaults to ok(ok()) —
|
|
376
|
+
// root and Run.create Runs have no parent Task to supply one, and a settled
|
|
377
|
+
// Run must record an exit. A panic that claims the exit during disposal is
|
|
378
|
+
// not reflected in the already-aborted controllers, so RunStateSettled.abort
|
|
379
|
+
// can keep runDisposedAbortReason while exit records the panic.
|
|
380
|
+
const dispose = (nextExit) => {
|
|
381
|
+
exit ??= nextExit;
|
|
382
|
+
if (disposePromise)
|
|
383
|
+
return disposePromise;
|
|
384
|
+
const settle = () => {
|
|
385
|
+
exit ??= ok(ok());
|
|
386
|
+
commitState({ type: "Settled", abort: currentAbort(), exit });
|
|
387
|
+
return exit;
|
|
388
|
+
};
|
|
389
|
+
const finalizersToDispose = finalizers;
|
|
390
|
+
if (finalizersToDispose) {
|
|
391
|
+
disposePromise = Promise.all(children.values()).then(async () => {
|
|
392
|
+
try {
|
|
393
|
+
await finalizersToDispose.disposeAsync();
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
finalizerAbortError = root.panic(error);
|
|
397
|
+
exit ??= err(finalizerAbortError);
|
|
398
|
+
}
|
|
399
|
+
return settle();
|
|
400
|
+
});
|
|
282
401
|
}
|
|
283
|
-
else
|
|
284
|
-
|
|
285
|
-
childRun.requestAbort(signalController.signal.reason);
|
|
286
|
-
task = () => err(signalController.signal.reason);
|
|
402
|
+
else {
|
|
403
|
+
disposePromise = Promise.all(children.values()).then(settle);
|
|
287
404
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}), {
|
|
296
|
-
run: childRun,
|
|
297
|
-
abort: (reason) => {
|
|
298
|
-
childRun.requestAbort(createAbortError(reason));
|
|
299
|
-
},
|
|
300
|
-
getState: () => childRun.getState(),
|
|
301
|
-
[Symbol.dispose]: () => {
|
|
302
|
-
childFiber.abort();
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
children = addToSet(children, childFiber);
|
|
306
|
-
emitEvent({ type: "ChildAdded", childId: childRun.id });
|
|
307
|
-
return childFiber;
|
|
405
|
+
const abortError = exit?.ok === false ? exit.error : runDisposedAbortError;
|
|
406
|
+
const { aborted } = signalController.signal;
|
|
407
|
+
requestController.abort(abortError);
|
|
408
|
+
signalController.abort(abortError);
|
|
409
|
+
if (!aborted)
|
|
410
|
+
commitState({ type: "Aborted", abort: currentAbort() });
|
|
411
|
+
return disposePromise;
|
|
308
412
|
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
run.snapshot = () => {
|
|
328
|
-
const childSnapshots = Array.from(children).map((fiber) => fiber.run.snapshot());
|
|
329
|
-
if (snapshot?.state !== state ||
|
|
330
|
-
!eqArrayStrict(snapshot.children, childSnapshots)) {
|
|
331
|
-
snapshot = {
|
|
332
|
-
id,
|
|
333
|
-
state: state,
|
|
334
|
-
children: childSnapshots,
|
|
335
|
-
abortMask,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
return snapshot;
|
|
413
|
+
// Custom deps replace parent custom deps, so defaults must be picked from
|
|
414
|
+
// the merged deps by key. `satisfies RunDefaultDeps` fails to compile when
|
|
415
|
+
// a newly added required default dep is missing here; optional ones like
|
|
416
|
+
// runConfig must be added manually.
|
|
417
|
+
const createChildDeps = (taskDeps) => taskDeps === undefined
|
|
418
|
+
? deps
|
|
419
|
+
: {
|
|
420
|
+
...{
|
|
421
|
+
console: deps.console,
|
|
422
|
+
leakDetector: deps.leakDetector,
|
|
423
|
+
nativeFetch: deps.nativeFetch,
|
|
424
|
+
randomBytes: deps.randomBytes,
|
|
425
|
+
random: deps.random,
|
|
426
|
+
reportDefect: deps.reportDefect,
|
|
427
|
+
time: deps.time,
|
|
428
|
+
...(deps.runConfig && { runConfig: deps.runConfig }),
|
|
429
|
+
},
|
|
430
|
+
...taskDeps,
|
|
339
431
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
run
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return self;
|
|
432
|
+
const run = ((task, taskDeps, { abortable = false } = {}) => {
|
|
433
|
+
assertNotDisposed({ disposed: !!disposePromise });
|
|
434
|
+
const taskMeta = task[taskMetaSymbol];
|
|
435
|
+
const taskRun = createRunInternal(createChildDeps(taskDeps), run, root, taskMeta);
|
|
436
|
+
// Disposal uses a separate promise because the Fiber starts immediately and
|
|
437
|
+
// gets its rejection handler only after the Task stack is captured.
|
|
438
|
+
const done = Promise.withResolvers();
|
|
439
|
+
children.set(taskRun, done.promise);
|
|
440
|
+
emitEvent({ type: "ChildAdded", childId: taskRun.id });
|
|
441
|
+
// Wired before the Task starts so an already-aborted parent prevents the
|
|
442
|
+
// start via startSignal.throwIfAborted below.
|
|
443
|
+
const abortFromParent = () => {
|
|
444
|
+
taskRun.requestAbort(run.requestAbortSignal.reason.reason);
|
|
354
445
|
};
|
|
355
|
-
run
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
disposingPromise = Promise.allSettled((run.ownTaskSettled
|
|
362
|
-
? [run.ownTaskSettled.promise, ...children]
|
|
363
|
-
: children))
|
|
364
|
-
.then(lazyVoid)
|
|
365
|
-
.finally(() => {
|
|
366
|
-
/**
|
|
367
|
-
* Root and daemon Runs have no own Task, so
|
|
368
|
-
* `run.handleTaskFulfilled` never populates their terminal values.
|
|
369
|
-
* In that case disposal publishes `ok()` for both `result` and
|
|
370
|
-
* `outcome`. Task-backed Runs normally reach this point with both
|
|
371
|
-
* values already set.
|
|
372
|
-
*/
|
|
373
|
-
[result, outcome] = [result ?? ok(), outcome ?? ok()];
|
|
374
|
-
state = { type: "Settled", result, outcome };
|
|
375
|
-
emitEvent({ type: "StateChanged", state });
|
|
446
|
+
if (run.requestAbortSignal.aborted)
|
|
447
|
+
abortFromParent();
|
|
448
|
+
else {
|
|
449
|
+
run.requestAbortSignal.addEventListener("abort", abortFromParent, {
|
|
450
|
+
once: true,
|
|
451
|
+
signal: taskRun.requestAbortSignal,
|
|
376
452
|
});
|
|
377
|
-
|
|
453
|
+
}
|
|
454
|
+
// The Fiber is runTask's native async-function promise. Keep runTask named:
|
|
455
|
+
// async stack traces use it as the visible Run boundary frame.
|
|
456
|
+
const runTask = async () => {
|
|
457
|
+
let exit;
|
|
458
|
+
try {
|
|
459
|
+
const startSignal = taskMeta?.abortBehavior === "unabortable"
|
|
460
|
+
? taskRun.requestAbortSignal
|
|
461
|
+
: taskRun.signal;
|
|
462
|
+
const scheduler = globalWithScheduler.scheduler;
|
|
463
|
+
let result;
|
|
464
|
+
if (taskMeta?.priority && scheduler?.postTask) {
|
|
465
|
+
result = await scheduler.postTask(() => {
|
|
466
|
+
startSignal.throwIfAborted();
|
|
467
|
+
// eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
|
|
468
|
+
return task(taskRun);
|
|
469
|
+
}, {
|
|
470
|
+
priority: taskMeta.priority,
|
|
471
|
+
signal: startSignal,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
startSignal.throwIfAborted();
|
|
476
|
+
// eslint-disable-next-line evolu/no-direct-task-call -- The executor invokes the Task with its child Run.
|
|
477
|
+
result = await task(taskRun);
|
|
478
|
+
}
|
|
479
|
+
// Full Result validation is dev-only; production checks only the
|
|
480
|
+
// boolean `ok` discriminant. This catches common malformed returns
|
|
481
|
+
// without full validation cost.
|
|
482
|
+
assert(isDev
|
|
483
|
+
? UnknownResult.is(result)
|
|
484
|
+
: typeof result
|
|
485
|
+
?.ok === "boolean", "Task must return Result.");
|
|
486
|
+
exit = ok(result);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
exit = err(AbortError.is(error) ? error : root.panic(error));
|
|
490
|
+
}
|
|
491
|
+
// Internal Child Run disposal cannot reject; finalizer defects become Err
|
|
492
|
+
// exits. Plain Task Runs settle from the Task exit; only a DisposableRun
|
|
493
|
+
// from Run.create can record an earlier exit.
|
|
494
|
+
const taskExit = await taskRun.dispose(exit);
|
|
495
|
+
// The child is removed once its Run has disposed. Parent bookkeeping
|
|
496
|
+
// tracks internal lifetime, not Fiber observers.
|
|
497
|
+
children.delete(taskRun);
|
|
498
|
+
emitEvent({ type: "ChildRemoved", childId: taskRun.id });
|
|
499
|
+
done.resolve();
|
|
500
|
+
if (taskExit.ok)
|
|
501
|
+
return taskExit.value;
|
|
502
|
+
if (abortable)
|
|
503
|
+
return err(taskExit.error);
|
|
504
|
+
// Invariant: the Run tree is every Fiber's final handler: defects are
|
|
505
|
+
// reported, aborts are control flow, and forgotten Fibers are a lint
|
|
506
|
+
// concern. Attach this late so V8 captures the defect stack before any
|
|
507
|
+
// catch observes the Fiber.
|
|
508
|
+
void fiber.catch(constVoid);
|
|
509
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error -- AbortError is Task abort control flow; aborts intentionally carry no stack.
|
|
510
|
+
throw taskExit.error;
|
|
378
511
|
};
|
|
379
|
-
|
|
380
|
-
run
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
512
|
+
const fiber = runTask();
|
|
513
|
+
fiber.run = taskRun;
|
|
514
|
+
if (abortable) {
|
|
515
|
+
const abortableFiber = fiber;
|
|
516
|
+
abortableFiber.abort = taskRun.requestAbort;
|
|
517
|
+
abortableFiber[Symbol.asyncDispose] = async () => {
|
|
518
|
+
abortableFiber.abort();
|
|
519
|
+
await abortableFiber;
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return fiber;
|
|
523
|
+
});
|
|
524
|
+
const root = rootRun ?? run;
|
|
525
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return -- Internal overload assignments implement public generics via TaskInternal. */
|
|
526
|
+
run.orThrow = (async (task, taskDeps) => getOrThrow(await run(task, taskDeps)));
|
|
527
|
+
run.ok = (async (task, taskDeps) => getOk((await run(task, taskDeps))));
|
|
528
|
+
/* eslint-enable @typescript-eslint/no-unsafe-return */
|
|
529
|
+
run.abortable = ((task, deps) => run(task, deps, { abortable: true }));
|
|
530
|
+
run.daemon = ((task, taskDeps) => {
|
|
531
|
+
assertNotDisposed({ disposed: !!disposePromise });
|
|
532
|
+
// Invariant: daemons detach to the root lifetime, so they must never start
|
|
533
|
+
// after this Run records abort, even if a mask hides it from run.signal.
|
|
534
|
+
run.requestAbortSignal.throwIfAborted();
|
|
535
|
+
return root.abortable(task, createChildDeps(taskDeps));
|
|
536
|
+
});
|
|
537
|
+
run.create = ((runDeps) => run.daemon(async (run) => {
|
|
538
|
+
await run.abortable(waitForAbort);
|
|
539
|
+
return ok();
|
|
540
|
+
}, runDeps).run);
|
|
541
|
+
run.id = createId(deps);
|
|
542
|
+
run.parent = parent ?? null;
|
|
543
|
+
run.deps = deps;
|
|
544
|
+
run.signal = signalController.signal;
|
|
545
|
+
run.requestAbortSignal = requestController.signal;
|
|
546
|
+
run.onAbort = (callback) => {
|
|
547
|
+
const onAbort = () => {
|
|
548
|
+
try {
|
|
549
|
+
callback(run.signal.reason);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
// Abort callbacks run outside a Fiber result channel.
|
|
553
|
+
root.panic(error);
|
|
554
|
+
}
|
|
390
555
|
};
|
|
391
|
-
run.
|
|
392
|
-
|
|
556
|
+
if (run.signal.aborted) {
|
|
557
|
+
onAbort();
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
// A user listener on run.signal calling stopImmediatePropagation could
|
|
561
|
+
// suppress this listener (see the run.signal JSDoc). Switch to
|
|
562
|
+
// addAbortCallback if/when https://github.com/whatwg/dom/pull/1425 lands
|
|
563
|
+
// (free structural fix); use a private signal exposed via AbortSignal.any
|
|
564
|
+
// only if a real-world report ever surfaces (per-Run cost).
|
|
565
|
+
run.signal.addEventListener("abort", onAbort, { once: true });
|
|
566
|
+
return {
|
|
567
|
+
[Symbol.dispose]: () => {
|
|
568
|
+
run.signal.removeEventListener("abort", onAbort);
|
|
569
|
+
},
|
|
393
570
|
};
|
|
571
|
+
};
|
|
572
|
+
run.getState = () => state;
|
|
573
|
+
run.snapshot = () => {
|
|
574
|
+
const childSnapshots = Array.from(children.keys(), (childRun) => childRun.snapshot());
|
|
575
|
+
if (snapshot?.state !== state ||
|
|
576
|
+
!eqArrayStrict(snapshot.children, childSnapshots)) {
|
|
577
|
+
snapshot = {
|
|
578
|
+
id: run.id,
|
|
579
|
+
state,
|
|
580
|
+
children: childSnapshots,
|
|
581
|
+
abortMask,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return snapshot;
|
|
585
|
+
};
|
|
586
|
+
run.onEvent = undefined;
|
|
587
|
+
run.defer = (finalizer) => {
|
|
588
|
+
assertNotDisposed({ disposed: !!disposePromise });
|
|
589
|
+
(finalizers ??= new AsyncDisposableStack()).defer(finalizer);
|
|
590
|
+
};
|
|
591
|
+
run.abort = (reason = explicitAbortReason) => {
|
|
592
|
+
// Unlike panic, abort cannot claim the exit after disposal starts.
|
|
593
|
+
if (disposePromise)
|
|
594
|
+
return;
|
|
595
|
+
void dispose(err(createAbortError(reason)));
|
|
596
|
+
};
|
|
597
|
+
run.panic = (defect) => {
|
|
598
|
+
const abortError = createAbortError(createPanicAbortReason(defect));
|
|
599
|
+
reportDefect(abortError);
|
|
600
|
+
// Internal disposal waits for Run children and does not reject. The first
|
|
601
|
+
// disposal wins, so a later panic does not replace the root Run exit.
|
|
602
|
+
void root.dispose(err(abortError));
|
|
603
|
+
return abortError;
|
|
604
|
+
};
|
|
605
|
+
run[Symbol.dispose] = () => {
|
|
606
|
+
void dispose();
|
|
607
|
+
};
|
|
608
|
+
run[Symbol.asyncDispose] = async () => {
|
|
609
|
+
await dispose();
|
|
610
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error -- AbortError is Task abort control flow; rethrowing it avoids reporting the finalizer defect twice in Task code.
|
|
611
|
+
if (finalizerAbortError)
|
|
612
|
+
throw finalizerAbortError;
|
|
613
|
+
};
|
|
614
|
+
// Internal
|
|
615
|
+
run.abortMask = abortMask;
|
|
616
|
+
run.restoreTokens = parent?.restoreTokens ?? emptySet;
|
|
617
|
+
run.requestAbort = requestAbort;
|
|
618
|
+
run.dispose = dispose;
|
|
619
|
+
return run;
|
|
620
|
+
};
|
|
621
|
+
const withTaskMeta = (meta) => (task) => {
|
|
622
|
+
const taskInternal = task;
|
|
623
|
+
assert(meta.abortBehavior === undefined ||
|
|
624
|
+
taskInternal[taskMetaSymbol]?.abortBehavior === undefined, "abort behavior helpers cannot wrap the same Task");
|
|
625
|
+
// eslint-disable-next-line evolu/no-direct-task-call -- Preserve the wrapped Task's child Run.
|
|
626
|
+
const wrapped = (run) => task(run);
|
|
627
|
+
const taskMeta = taskInternal[taskMetaSymbol];
|
|
628
|
+
wrapped[taskMetaSymbol] = taskMeta ? { ...taskMeta, ...meta } : meta;
|
|
629
|
+
return wrapped;
|
|
630
|
+
};
|
|
631
|
+
export function all(input, fnOrOptions, options) {
|
|
632
|
+
const fn = typeof fnOrOptions === "function" ? fnOrOptions : undefined;
|
|
633
|
+
const collectionOptions = typeof fnOrOptions === "function" ? options : fnOrOptions;
|
|
634
|
+
const collectValues = collectionOptions === undefined ||
|
|
635
|
+
!("collect" in collectionOptions) ||
|
|
636
|
+
collectionOptions.collect !== false;
|
|
637
|
+
return collect("all", fn ? mapInput(input, fn) : input, collectionOptions, collectValues);
|
|
638
|
+
}
|
|
639
|
+
export function allSettled(input, fnOrOptions, options) {
|
|
640
|
+
const fn = typeof fnOrOptions === "function" ? fnOrOptions : undefined;
|
|
641
|
+
return collect("allSettled", fn ? mapInput(input, fn) : input, typeof fnOrOptions === "function" ? options : fnOrOptions);
|
|
642
|
+
}
|
|
643
|
+
const collect = (type, input, options, collectValues = true) => async (run) => {
|
|
644
|
+
let tasks;
|
|
645
|
+
let getValue;
|
|
646
|
+
if (Array.isArray(input)) {
|
|
647
|
+
if (!isNonEmptyArray(input)) {
|
|
648
|
+
return collectValues ? ok(emptyArray) : ok();
|
|
649
|
+
}
|
|
650
|
+
tasks = input;
|
|
651
|
+
getValue = identity;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const entries = objectToEntries(input);
|
|
655
|
+
if (!isNonEmptyArray(entries)) {
|
|
656
|
+
return collectValues ? ok(emptyRecord) : ok();
|
|
657
|
+
}
|
|
658
|
+
tasks = mapArray(entries, ([, task]) => task);
|
|
659
|
+
getValue = (values) => objectFromEntries(entries.map(([key], index) => [key, values[index]]));
|
|
394
660
|
}
|
|
395
|
-
|
|
661
|
+
const values = collectValues ? [] : undefined;
|
|
662
|
+
let firstErr;
|
|
663
|
+
await run(each(tasks, (result, index) => {
|
|
664
|
+
if (type === "allSettled") {
|
|
665
|
+
values[index] = result;
|
|
666
|
+
return "continue";
|
|
667
|
+
}
|
|
668
|
+
if (!result.ok) {
|
|
669
|
+
firstErr = result;
|
|
670
|
+
return "stop";
|
|
671
|
+
}
|
|
672
|
+
if (values)
|
|
673
|
+
values[index] = result.value;
|
|
674
|
+
return "continue";
|
|
675
|
+
}, options));
|
|
676
|
+
return firstErr ?? (values ? ok(getValue(values)) : ok());
|
|
396
677
|
};
|
|
397
|
-
const
|
|
678
|
+
const mapInput = (input, fn) => Array.isArray(input)
|
|
679
|
+
? mapArray(input, (value, index) => fn(value, index))
|
|
680
|
+
: mapObject(input, fn);
|
|
398
681
|
/**
|
|
399
|
-
*
|
|
682
|
+
* Creates a {@link Task} from a callback-based API.
|
|
683
|
+
*
|
|
684
|
+
* Use this to wrap callback-style APIs such as event listeners and Node.js
|
|
685
|
+
* callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
|
|
686
|
+
* to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
|
|
687
|
+
* wins, and later settlement calls are ignored, matching Promise semantics.
|
|
688
|
+
* When `reject` wins settlement, the defect panics the Run tree and is observed
|
|
689
|
+
* at Fiber boundaries as {@link AbortError} with {@link PanicAbortReason}.
|
|
690
|
+
* Rejecting AbortError is reserved for Task abort control flow: direct
|
|
691
|
+
* `run(task)` rejects with it, and `run.abortable(task)` returns it as an Err.
|
|
692
|
+
*
|
|
693
|
+
* This helper is a callback bridge. If `reject` forwards an Error created in a
|
|
694
|
+
* separate async chain, V8 cannot reconstruct the caller's zero-cost async
|
|
695
|
+
* stack through this bridge. Prefer native promise APIs and `await` when the
|
|
696
|
+
* wrapped operation already has a promise-shaped API.
|
|
697
|
+
*
|
|
698
|
+
* One-shot settlement applies only to `resolve` and `reject`. A synchronous
|
|
699
|
+
* throw from the setup function is a defect that panics the Run tree even after
|
|
700
|
+
* `resolve` was called — setup throws are bugs and must not be masked by an
|
|
701
|
+
* earlier resolution.
|
|
702
|
+
*
|
|
703
|
+
* Optionally return a synchronous cleanup callback. It runs when the Task Run
|
|
704
|
+
* signal aborts, including normal completion and explicit abort. The callback
|
|
705
|
+
* must not throw. Cleanup defects panic the root Run; if the callback Task
|
|
706
|
+
* already settled, its Fiber keeps the original Result while the root still
|
|
707
|
+
* reports the panic. Cleanup must be synchronous; returned promises are not
|
|
708
|
+
* awaited. For async cleanup, prefer {@link acquireUseRelease}, `await using`
|
|
709
|
+
* with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
710
|
+
* explicitly.
|
|
711
|
+
*
|
|
712
|
+
* If setup can throw after acquiring any resource, use a local
|
|
713
|
+
* {@link DisposableStack} before returning cleanup. Register each cleanup as
|
|
714
|
+
* soon as the resource is acquired, then move the disposer and return a cleanup
|
|
715
|
+
* callback. If setup throws before cleanup is returned, the local `using`
|
|
716
|
+
* disposal releases already-acquired resources.
|
|
400
717
|
*
|
|
401
|
-
*
|
|
718
|
+
* ### Example
|
|
719
|
+
*
|
|
720
|
+
* ```ts
|
|
721
|
+
* import { callback, createRun, ok, type Task } from "@evolu/common";
|
|
722
|
+
*
|
|
723
|
+
* const listeners = new Set<(message: string) => void>();
|
|
724
|
+
* const subscribe = (
|
|
725
|
+
* listener: (message: string) => void,
|
|
726
|
+
* ): (() => void) => {
|
|
727
|
+
* listeners.add(listener);
|
|
728
|
+
* return () => listeners.delete(listener);
|
|
729
|
+
* };
|
|
730
|
+
* const nextMessage: Task<string> = callback(({ resolve }) =>
|
|
731
|
+
* subscribe((message) => resolve(ok(message))),
|
|
732
|
+
* );
|
|
733
|
+
*
|
|
734
|
+
* await using run = createRun();
|
|
735
|
+
* const fiber = run(nextMessage);
|
|
736
|
+
* expect(listeners.size).toBe(1);
|
|
737
|
+
* for (const listener of listeners) listener("ready");
|
|
738
|
+
* expectOk(await fiber, "ready");
|
|
739
|
+
* // The callback cleanup unsubscribes after settlement.
|
|
740
|
+
* expect(listeners.size).toBe(0);
|
|
741
|
+
* ```
|
|
402
742
|
*
|
|
403
|
-
* @group
|
|
743
|
+
* @group Interop
|
|
404
744
|
*/
|
|
405
|
-
export const
|
|
745
|
+
export const callback = (fn) => (run) => {
|
|
746
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
747
|
+
const cleanup = fn({ run, resolve, reject });
|
|
748
|
+
// The Task Run aborts during disposal, so this also runs callback cleanup
|
|
749
|
+
// after normal settlement. The abort listener is once-only.
|
|
750
|
+
run.onAbort((abortError) => {
|
|
751
|
+
reject(abortError);
|
|
752
|
+
cleanup?.();
|
|
753
|
+
});
|
|
754
|
+
return promise;
|
|
755
|
+
};
|
|
406
756
|
/**
|
|
407
|
-
*
|
|
408
|
-
* {@link AbortError.reason} when a Task is started on a non-running {@link Run}.
|
|
757
|
+
* Pauses execution for a specified {@link PositiveDuration}.
|
|
409
758
|
*
|
|
410
|
-
*
|
|
759
|
+
* Aborting the Task clears the scheduled timeout.
|
|
760
|
+
*
|
|
761
|
+
* @group Timing
|
|
411
762
|
*/
|
|
412
|
-
export const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const createAbortError = (reason) => ({
|
|
416
|
-
type: "AbortError",
|
|
417
|
-
reason,
|
|
418
|
-
});
|
|
419
|
-
const subscribeToAbort = (signal, handler, options) => {
|
|
420
|
-
if (signal.aborted)
|
|
421
|
-
handler();
|
|
422
|
-
else
|
|
423
|
-
signal.addEventListener("abort", handler, options);
|
|
424
|
-
};
|
|
425
|
-
const runStoppedAbortError = createAbortError(runStoppedError);
|
|
426
|
-
const isAbortable = AbortMask.orThrow(0);
|
|
427
|
-
const abortBehaviorSymbol = Symbol("evolu.Task.abortBehavior");
|
|
428
|
-
const getAbortBehavior = (task) => task[abortBehaviorSymbol];
|
|
429
|
-
const abortBehavior = (behavior) => (task) => Object.assign((run) => run(task), {
|
|
430
|
-
[abortBehaviorSymbol]: behavior,
|
|
763
|
+
export const sleep = (duration) => callback(({ run: { deps }, resolve }) => {
|
|
764
|
+
const id = deps.time.setTimeout(() => resolve(ok()), duration);
|
|
765
|
+
return () => deps.time.clearTimeout(id);
|
|
431
766
|
});
|
|
432
767
|
/**
|
|
433
|
-
*
|
|
768
|
+
* Typed error returned by {@link timeout} when a Task exceeds its duration.
|
|
434
769
|
*
|
|
435
|
-
*
|
|
436
|
-
|
|
770
|
+
* @group Timing
|
|
771
|
+
*/
|
|
772
|
+
export const TimeoutError =
|
|
773
|
+
/*#__PURE__*/ typed("TimeoutError");
|
|
774
|
+
/**
|
|
775
|
+
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
437
776
|
*
|
|
438
|
-
*
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
777
|
+
* @group Timing
|
|
778
|
+
*/
|
|
779
|
+
export const timeoutError = { type: "TimeoutError" };
|
|
780
|
+
/**
|
|
781
|
+
* Limits how long a {@link Task} may run.
|
|
442
782
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
783
|
+
* Returns the Task {@link Result} when it settles within the duration.
|
|
784
|
+
* Otherwise, the Task is aborted and {@link TimeoutError} is returned. A Task
|
|
785
|
+
* that doesn't observe abort delays the TimeoutError until it settles; when
|
|
786
|
+
* that wait is unacceptable, wrap the Task with {@link daemon}:
|
|
787
|
+
* `timeout(daemon(task), duration)`.
|
|
445
788
|
*
|
|
446
789
|
* ### Example
|
|
447
790
|
*
|
|
448
791
|
* ```ts
|
|
792
|
+
* import {
|
|
793
|
+
* createRun,
|
|
794
|
+
* timeout,
|
|
795
|
+
* timeoutError,
|
|
796
|
+
* waitForAbort,
|
|
797
|
+
* type Result,
|
|
798
|
+
* type TimeoutError,
|
|
799
|
+
* } from "@evolu/common";
|
|
800
|
+
*
|
|
449
801
|
* await using run = createRun();
|
|
450
802
|
*
|
|
451
|
-
* const
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
* // Simulate async analytics API (abortable by default)
|
|
456
|
-
* const sendToAnalytics =
|
|
457
|
-
* (event: number): Task<void, never> =>
|
|
458
|
-
* async ({ signal }) => {
|
|
459
|
-
* await canComplete.promise;
|
|
460
|
-
* signalAbortedInAnalytics = signal.aborted;
|
|
461
|
-
* events.push(`sent ${event}`);
|
|
462
|
-
* return ok();
|
|
463
|
-
* };
|
|
803
|
+
* const result = await run(timeout(waitForAbort, "1ms"));
|
|
804
|
+
* expectTypeOf(result).toEqualTypeOf<Result<never, TimeoutError>>();
|
|
805
|
+
* expectErr(result, timeoutError);
|
|
806
|
+
* ```
|
|
464
807
|
*
|
|
465
|
-
*
|
|
466
|
-
|
|
467
|
-
|
|
808
|
+
* @group Timing
|
|
809
|
+
*/
|
|
810
|
+
export const timeout = (task, duration) => race([
|
|
811
|
+
task,
|
|
812
|
+
async (run) => {
|
|
813
|
+
await run.ok(sleep(duration));
|
|
814
|
+
return err(timeoutError);
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
/**
|
|
818
|
+
* Retries a Task according to a {@link Schedule}.
|
|
468
819
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
820
|
+
* Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
|
|
821
|
+
* final domain error. Use {@link repeat} for success-driven loops: it repeats
|
|
822
|
+
* after {@link Ok} and returns the Task's natural Result.
|
|
471
823
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
824
|
+
* {@link AbortError} passes through unchanged when returned as a Result error,
|
|
825
|
+
* such as from {@link daemon}. Abort from `run(task)` remains Fiber control
|
|
826
|
+
* flow. Other errors are domain errors: retrying continues while the schedule
|
|
827
|
+
* yields another delay and {@link RetryOptions.shouldRetry} returns `true`. When
|
|
828
|
+
* retrying stops, `retry` returns {@link RetryError} with the final domain error
|
|
829
|
+
* as `lastError`.
|
|
475
830
|
*
|
|
476
|
-
*
|
|
831
|
+
* AbortError recognition is structural. Domain errors, especially values parsed
|
|
832
|
+
* from untrusted input, must not use the reserved AbortError shape.
|
|
477
833
|
*
|
|
478
|
-
*
|
|
479
|
-
* // Analytics was sent despite abort
|
|
480
|
-
* expect(events).toEqual(["sent 123"]);
|
|
481
|
-
* expect(result).toEqual(ok());
|
|
482
|
-
* ```
|
|
834
|
+
* ### Example
|
|
483
835
|
*
|
|
484
|
-
*
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
*
|
|
490
|
-
*
|
|
836
|
+
* ```ts
|
|
837
|
+
* import {
|
|
838
|
+
* createRun,
|
|
839
|
+
* err,
|
|
840
|
+
* recurs,
|
|
841
|
+
* retry,
|
|
842
|
+
* type Result,
|
|
843
|
+
* type RetryTaskError,
|
|
844
|
+
* type Task,
|
|
845
|
+
* } from "@evolu/common";
|
|
491
846
|
*
|
|
492
|
-
*
|
|
847
|
+
* await using run = createRun();
|
|
493
848
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
849
|
+
* interface FetchDataError {
|
|
850
|
+
* readonly type: "FetchDataError";
|
|
851
|
+
* }
|
|
496
852
|
*
|
|
497
|
-
*
|
|
498
|
-
|
|
499
|
-
export const unabortableMask = (fn) => unabortable((run) => fn(abortBehavior(AbortMask.orThrow(decrement(run.abortMask))))(run));
|
|
500
|
-
const defaultConcurrency = 1;
|
|
501
|
-
const concurrencyBehaviorSymbol = Symbol("evolu.Task.concurrencyBehavior");
|
|
502
|
-
const getConcurrencyBehavior = (task) => task[concurrencyBehaviorSymbol];
|
|
503
|
-
export function concurrently(concurrencyOrTask, taskOrFallback) {
|
|
504
|
-
const isTask = isFunction(concurrencyOrTask);
|
|
505
|
-
const task = isTask ? concurrencyOrTask : taskOrFallback;
|
|
506
|
-
return Object.assign((run) => run(task), {
|
|
507
|
-
[concurrencyBehaviorSymbol]: isTask ? maxPositiveInt : concurrencyOrTask,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Yields execution to allow other work to proceed.
|
|
853
|
+
* const fetchData: Task<string, FetchDataError> = () =>
|
|
854
|
+
* err({ type: "FetchDataError" });
|
|
512
855
|
*
|
|
513
|
-
*
|
|
514
|
-
* UI unresponsive (user interactions, animations). In Node.js, it prevents I/O
|
|
515
|
-
* callbacks, timers, and other requests from being handled. Inserting yield
|
|
516
|
-
* points lets the runtime process high-priority work between chunks of code.
|
|
856
|
+
* const fetchWithRetry = retry(fetchData, recurs(2));
|
|
517
857
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
858
|
+
* const result = await run(fetchWithRetry);
|
|
859
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
860
|
+
* Result<string, RetryTaskError<FetchDataError>>
|
|
861
|
+
* >();
|
|
862
|
+
* expectErr(result, {
|
|
863
|
+
* type: "RetryError",
|
|
864
|
+
* attempts: 3,
|
|
865
|
+
* lastError: { type: "FetchDataError" },
|
|
866
|
+
* });
|
|
867
|
+
* ```
|
|
520
868
|
*
|
|
521
869
|
* ### Example
|
|
522
870
|
*
|
|
523
871
|
* ```ts
|
|
524
|
-
*
|
|
525
|
-
* const { time } = run.deps;
|
|
526
|
-
* let lastYield = time.now();
|
|
527
|
-
*
|
|
528
|
-
* for (const item of largeArray) {
|
|
529
|
-
* processItem(item);
|
|
530
|
-
*
|
|
531
|
-
* // Yield periodically to keep UI responsive
|
|
532
|
-
* if (time.now() - lastYield > msLongTask) {
|
|
533
|
-
* const r = await run(yieldNow);
|
|
534
|
-
* if (!r.ok) return r;
|
|
535
|
-
* lastYield = time.now();
|
|
536
|
-
* }
|
|
537
|
-
* }
|
|
872
|
+
* import { createRun, err, recurs, retry, type Task } from "@evolu/common";
|
|
538
873
|
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
* Recursive tasks also benefit from periodic yields — without them, deep
|
|
544
|
-
* recursion overflows the call stack:
|
|
874
|
+
* interface FetchDataError {
|
|
875
|
+
* readonly type: "RecoverableError" | "FatalError";
|
|
876
|
+
* }
|
|
545
877
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
* (count: number, index: number, sum: number): Task<number> =>
|
|
549
|
-
* async (run) => {
|
|
550
|
-
* if (index >= count) return ok(sum);
|
|
878
|
+
* const fetchData: Task<string, FetchDataError> = () =>
|
|
879
|
+
* err({ type: "FatalError" });
|
|
551
880
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* if (!y.ok) return y;
|
|
556
|
-
* }
|
|
881
|
+
* const fetchWithRetry = retry(fetchData, recurs(5), {
|
|
882
|
+
* shouldRetry: (error) => error.type !== "FatalError",
|
|
883
|
+
* });
|
|
557
884
|
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
885
|
+
* await using run = createRun();
|
|
886
|
+
* expectErr(await run(fetchWithRetry), {
|
|
887
|
+
* type: "RetryError",
|
|
888
|
+
* attempts: 1,
|
|
889
|
+
* lastError: { type: "FatalError" },
|
|
890
|
+
* });
|
|
561
891
|
* ```
|
|
562
892
|
*
|
|
563
|
-
* @group
|
|
564
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield
|
|
565
|
-
* @see https://web.dev/articles/optimize-long-tasks
|
|
893
|
+
* @group Resilience
|
|
566
894
|
*/
|
|
567
|
-
export const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
895
|
+
export const retry = (task, schedule, { shouldRetry = constTrue, onRetry, } = {}) => async (run) => {
|
|
896
|
+
const step = schedule(run.deps);
|
|
897
|
+
let attempt = onePositiveInt;
|
|
898
|
+
const createRetryError = (lastError) => ({
|
|
899
|
+
type: "RetryError",
|
|
900
|
+
lastError,
|
|
901
|
+
attempts: attempt,
|
|
902
|
+
});
|
|
903
|
+
for (;;) {
|
|
904
|
+
const result = await run(task);
|
|
905
|
+
if (result.ok)
|
|
906
|
+
return result;
|
|
907
|
+
if (AbortError.is(result.error))
|
|
908
|
+
return result;
|
|
909
|
+
const error = result.error;
|
|
910
|
+
if (!shouldRetry(error))
|
|
911
|
+
return err(createRetryError(error));
|
|
912
|
+
const next = step(error);
|
|
913
|
+
if (!next.ok)
|
|
914
|
+
return err(createRetryError(error));
|
|
915
|
+
const [output, delay] = next.value;
|
|
916
|
+
onRetry?.({ error, attempt, output, delay });
|
|
917
|
+
attempt = incrementPositiveInt(attempt);
|
|
918
|
+
if (delay > 0)
|
|
919
|
+
await run.ok(sleep(PositiveMillis.orThrow(delay)));
|
|
920
|
+
}
|
|
921
|
+
};
|
|
575
922
|
/**
|
|
576
|
-
*
|
|
923
|
+
* Repeats a Task according to a {@link Schedule}.
|
|
577
924
|
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
925
|
+
* Runs the Task once, then repeats while the Task returns {@link Ok}, the
|
|
926
|
+
* schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
|
|
927
|
+
* `true`. When repeating stops, `repeat` returns the last successful Result. If
|
|
928
|
+
* the Task returns {@link Err}, including {@link Done} from a {@link NextTask},
|
|
929
|
+
* `repeat` returns that error without scheduling another attempt.
|
|
580
930
|
*
|
|
581
|
-
*
|
|
931
|
+
* Use `repeat` for success-driven loops such as polling or consuming a
|
|
932
|
+
* NextTask: it repeats after Ok and returns the Task's natural Result. Use
|
|
933
|
+
* {@link retry} for failure recovery: it repeats after Err and wraps the final
|
|
934
|
+
* domain error in {@link RetryError}.
|
|
582
935
|
*
|
|
583
|
-
*
|
|
584
|
-
* Start the promise work inside the callback, and aborting the Task stops
|
|
585
|
-
* waiting for it immediately.
|
|
936
|
+
* With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
|
|
586
937
|
*
|
|
587
938
|
* ### Example
|
|
588
939
|
*
|
|
589
940
|
* ```ts
|
|
590
|
-
*
|
|
591
|
-
* const sleep = (duration: Duration): Task<void> =>
|
|
592
|
-
* callback(({ ok, deps: { time } }) => {
|
|
593
|
-
* const id = time.setTimeout(ok, durationToMillis(duration));
|
|
594
|
-
* return () => time.clearTimeout(id);
|
|
595
|
-
* });
|
|
941
|
+
* import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
|
|
596
942
|
*
|
|
597
|
-
*
|
|
598
|
-
* const
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
943
|
+
* let attempts = 0;
|
|
944
|
+
* const checkStatus: Task<string> = () => {
|
|
945
|
+
* attempts += 1;
|
|
946
|
+
* return ok("pending");
|
|
947
|
+
* };
|
|
602
948
|
*
|
|
603
|
-
*
|
|
604
|
-
* const readFile = (path: string): Task<string, NodeJS.ErrnoException> =>
|
|
605
|
-
* callback(({ ok, err }) => {
|
|
606
|
-
* fs.readFile(path, "utf8", (error, data) => {
|
|
607
|
-
* if (error) err(error);
|
|
608
|
-
* else ok(data);
|
|
609
|
-
* });
|
|
610
|
-
* });
|
|
949
|
+
* const poll = repeat(checkStatus, recurs(3));
|
|
611
950
|
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
* callback(({ ok, err }) => {
|
|
616
|
-
* void promiseApi().then(ok, err);
|
|
617
|
-
* });
|
|
951
|
+
* await using run = createRun();
|
|
952
|
+
* expectOk(await run(poll), "pending");
|
|
953
|
+
* expect(attempts).toBe(4);
|
|
618
954
|
* ```
|
|
619
955
|
*
|
|
620
|
-
* @group Composition
|
|
621
|
-
*/
|
|
622
|
-
export const callback = (callback) => (run) => new Promise((resolve) => {
|
|
623
|
-
const teardown = callback({
|
|
624
|
-
ok: (value) => resolve(ok(value)),
|
|
625
|
-
err: (error) => resolve(err(error)),
|
|
626
|
-
signal: run.signal,
|
|
627
|
-
deps: run.deps,
|
|
628
|
-
});
|
|
629
|
-
run.onAbort((reason) => {
|
|
630
|
-
if (teardown)
|
|
631
|
-
teardown();
|
|
632
|
-
resolve(err(createAbortError(reason)));
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
/**
|
|
636
|
-
* Pauses execution for a specified duration.
|
|
637
|
-
*
|
|
638
956
|
* ### Example
|
|
639
957
|
*
|
|
640
958
|
* ```ts
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
959
|
+
* import {
|
|
960
|
+
* createRun,
|
|
961
|
+
* done,
|
|
962
|
+
* err,
|
|
963
|
+
* ok,
|
|
964
|
+
* repeat,
|
|
965
|
+
* spaced,
|
|
966
|
+
* type NextTask,
|
|
967
|
+
* } from "@evolu/common";
|
|
648
968
|
*
|
|
649
|
-
*
|
|
650
|
-
*/
|
|
651
|
-
export const sleep = (duration) => callback(({ ok, deps: { time } }) => {
|
|
652
|
-
const id = time.setTimeout(ok, durationToMillis(duration));
|
|
653
|
-
return () => time.clearTimeout(id);
|
|
654
|
-
});
|
|
655
|
-
/**
|
|
656
|
-
* Returns a {@link Task} that completes first.
|
|
969
|
+
* await using run = createRun();
|
|
657
970
|
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* are aborted. Use {@link any} if you need the first Task to succeed instead.
|
|
971
|
+
* interface Item {
|
|
972
|
+
* readonly id: string;
|
|
973
|
+
* }
|
|
662
974
|
*
|
|
663
|
-
*
|
|
664
|
-
* (there's no "first to complete" without participants). This is enforced at
|
|
665
|
-
* compile time for non-empty tuple types. For other arrays, guard with
|
|
666
|
-
* {@link isNonEmptyArray}:
|
|
975
|
+
* const queue: Array<Item> = [{ id: "item-1" }];
|
|
667
976
|
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* }
|
|
977
|
+
* const processQueue: NextTask<Item> = () => {
|
|
978
|
+
* const item = queue.shift();
|
|
979
|
+
* return item ? ok(item) : err(done());
|
|
980
|
+
* };
|
|
981
|
+
*
|
|
982
|
+
* const result = await run(repeat(processQueue, spaced("1ms")));
|
|
983
|
+
* expectErr(result, done());
|
|
984
|
+
* expect(queue).toEqual([]);
|
|
672
985
|
* ```
|
|
673
986
|
*
|
|
674
|
-
*
|
|
987
|
+
* @group Repetition
|
|
988
|
+
*/
|
|
989
|
+
export const repeat = (task, schedule, { shouldRepeat = constTrue, onRepeat } = {}) => async (run) => {
|
|
990
|
+
const step = schedule(run.deps);
|
|
991
|
+
let attempt = onePositiveInt;
|
|
992
|
+
for (;;) {
|
|
993
|
+
const result = await run(task);
|
|
994
|
+
if (!result.ok)
|
|
995
|
+
return result;
|
|
996
|
+
if (!shouldRepeat(result.value))
|
|
997
|
+
return result;
|
|
998
|
+
const next = step(result.value);
|
|
999
|
+
if (!next.ok)
|
|
1000
|
+
return result;
|
|
1001
|
+
const [output, delay] = next.value;
|
|
1002
|
+
onRepeat?.({ value: result.value, attempt, output, delay });
|
|
1003
|
+
attempt = incrementPositiveInt(attempt);
|
|
1004
|
+
if (delay > 0)
|
|
1005
|
+
await run.ok(sleep(PositiveMillis.orThrow(delay)));
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
/**
|
|
1009
|
+
* Runs Tasks until one returns {@link Ok} or all return {@link Err}.
|
|
1010
|
+
*
|
|
1011
|
+
* Use {@link race} to return the first settled Result instead, whether Ok or
|
|
1012
|
+
* {@link Err}.
|
|
1013
|
+
*
|
|
1014
|
+
* Returns the first {@link Ok} Result. Losing Tasks are aborted. If no Task
|
|
1015
|
+
* returns Ok, returns the last Err by input order, regardless of completion
|
|
1016
|
+
* order. Other Err results are discarded; use {@link allSettled} when you need
|
|
1017
|
+
* every error.
|
|
1018
|
+
*
|
|
1019
|
+
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1020
|
+
* at a time.
|
|
1021
|
+
*
|
|
1022
|
+
* Similar to
|
|
1023
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
|
|
1024
|
+
* but races Tasks, returns Result values, and aborts losers.
|
|
1025
|
+
*
|
|
1026
|
+
* ### Example
|
|
675
1027
|
*
|
|
676
1028
|
* ```ts
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
1029
|
+
* import {
|
|
1030
|
+
* any,
|
|
1031
|
+
* createRun,
|
|
1032
|
+
* err,
|
|
1033
|
+
* ok,
|
|
1034
|
+
* type Result,
|
|
1035
|
+
* type Task,
|
|
1036
|
+
* } from "@evolu/common";
|
|
1037
|
+
*
|
|
1038
|
+
* const unavailable: Task<string, "Unavailable"> = () =>
|
|
1039
|
+
* err("Unavailable");
|
|
1040
|
+
* let fallbackStarted = false;
|
|
1041
|
+
* const fallback: Task<string> = () => {
|
|
1042
|
+
* fallbackStarted = true;
|
|
1043
|
+
* return ok("fallback");
|
|
681
1044
|
* };
|
|
682
1045
|
*
|
|
683
|
-
*
|
|
684
|
-
* const result = await run(
|
|
685
|
-
* ```
|
|
1046
|
+
* await using run = createRun();
|
|
1047
|
+
* const result = await run(any([unavailable, fallback]));
|
|
686
1048
|
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
1049
|
+
* expectTypeOf(result).toEqualTypeOf<Result<string, "Unavailable">>();
|
|
1050
|
+
* expectOk(result, "fallback");
|
|
1051
|
+
* expect(fallbackStarted).toBe(true);
|
|
1052
|
+
* ```
|
|
689
1053
|
*
|
|
690
|
-
* @group
|
|
1054
|
+
* @group Racing
|
|
691
1055
|
*/
|
|
692
|
-
export const
|
|
1056
|
+
export const any = (tasks, options) => async (run) => {
|
|
1057
|
+
let firstOk;
|
|
1058
|
+
let lastErr;
|
|
1059
|
+
let lastErrIndex = -1;
|
|
1060
|
+
await run(each(tasks, (result, index) => {
|
|
1061
|
+
if (result.ok) {
|
|
1062
|
+
firstOk = result;
|
|
1063
|
+
return "stop";
|
|
1064
|
+
}
|
|
1065
|
+
if (index > lastErrIndex) {
|
|
1066
|
+
lastErrIndex = index;
|
|
1067
|
+
lastErr = result;
|
|
1068
|
+
}
|
|
1069
|
+
return "continue";
|
|
1070
|
+
}, options));
|
|
1071
|
+
if (firstOk)
|
|
1072
|
+
return firstOk;
|
|
1073
|
+
assertNonNullable(lastErr);
|
|
1074
|
+
return lastErr;
|
|
1075
|
+
};
|
|
693
1076
|
/**
|
|
694
|
-
*
|
|
1077
|
+
* Runs Tasks until the first Task settles.
|
|
695
1078
|
*
|
|
696
|
-
* @
|
|
697
|
-
|
|
698
|
-
export const RaceLostError = /*#__PURE__*/ typed("RaceLostError");
|
|
699
|
-
/**
|
|
700
|
-
* {@link RaceLostError} used as abort reason in {@link race}.
|
|
1079
|
+
* Returns the first Task {@link Result} to settle, whether {@link Ok} or
|
|
1080
|
+
* {@link Err}.
|
|
701
1081
|
*
|
|
702
|
-
* @
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
*
|
|
1082
|
+
* Use {@link any} to wait for the first Ok instead.
|
|
1083
|
+
*
|
|
1084
|
+
* Losing Tasks are aborted.
|
|
1085
|
+
*
|
|
1086
|
+
* Tasks always run concurrently because racing sequentially would be
|
|
1087
|
+
* meaningless.
|
|
707
1088
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
1089
|
+
* Similar to
|
|
1090
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race | Promise.race},
|
|
1091
|
+
* but races Tasks, returns Result values, and aborts losers.
|
|
1092
|
+
*
|
|
1093
|
+
* Requires a non-empty array: zero Tasks have no meaningful first settled
|
|
1094
|
+
* Result. This is enforced at compile time for non-empty tuple types. For
|
|
1095
|
+
* arrays whose emptiness is only known at runtime, guard with
|
|
1096
|
+
* {@link isNonEmptyArray}:
|
|
710
1097
|
*
|
|
711
1098
|
* ### Example
|
|
712
1099
|
*
|
|
713
1100
|
* ```ts
|
|
714
|
-
*
|
|
1101
|
+
* import {
|
|
1102
|
+
* createRun,
|
|
1103
|
+
* isNonEmptyArray,
|
|
1104
|
+
* ok,
|
|
1105
|
+
* race,
|
|
1106
|
+
* type Task,
|
|
1107
|
+
* } from "@evolu/common";
|
|
715
1108
|
*
|
|
716
|
-
* const
|
|
717
|
-
*
|
|
718
|
-
*
|
|
1109
|
+
* const tasks: ReadonlyArray<Task<string>> = [() => ok("first")];
|
|
1110
|
+
* await using run = createRun();
|
|
1111
|
+
* if (isNonEmptyArray(tasks)) {
|
|
1112
|
+
* const result = await run(race(tasks));
|
|
1113
|
+
* expectOk(result, "first");
|
|
719
1114
|
* }
|
|
720
1115
|
* ```
|
|
721
1116
|
*
|
|
722
|
-
*
|
|
723
|
-
*/
|
|
724
|
-
export const timeout = (task, duration, { abortReason = timeoutError, } = {}) => race([
|
|
725
|
-
task,
|
|
726
|
-
async (run) => {
|
|
727
|
-
await run(sleep(duration));
|
|
728
|
-
return err(timeoutError);
|
|
729
|
-
},
|
|
730
|
-
], { abortReason });
|
|
731
|
-
/**
|
|
732
|
-
* Typed error returned by {@link timeout} when a task exceeds its time limit.
|
|
1117
|
+
* ### Example
|
|
733
1118
|
*
|
|
734
|
-
*
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
*
|
|
1119
|
+
* ```ts
|
|
1120
|
+
* import {
|
|
1121
|
+
* createRun,
|
|
1122
|
+
* ok,
|
|
1123
|
+
* race,
|
|
1124
|
+
* sleep,
|
|
1125
|
+
* type Result,
|
|
1126
|
+
* type Task,
|
|
1127
|
+
* } from "@evolu/common";
|
|
739
1128
|
*
|
|
740
|
-
*
|
|
1129
|
+
* await using run = createRun();
|
|
1130
|
+
*
|
|
1131
|
+
* const fast: Task<string> = () => ok("fast");
|
|
1132
|
+
* let slowCompleted = false;
|
|
1133
|
+
* const slow: Task<string> = async (run) => {
|
|
1134
|
+
* await run.ok(sleep("10ms"));
|
|
1135
|
+
* slowCompleted = true;
|
|
1136
|
+
* return ok("slow");
|
|
1137
|
+
* };
|
|
1138
|
+
*
|
|
1139
|
+
* // Input order does not matter: the first settled Result wins, and the
|
|
1140
|
+
* // still-running loser is aborted.
|
|
1141
|
+
* const result = await run(race([slow, fast]));
|
|
1142
|
+
* expectTypeOf(result).toEqualTypeOf<Result<string>>();
|
|
1143
|
+
* expectOk(result, "fast");
|
|
1144
|
+
* expect(slowCompleted).toBe(false);
|
|
1145
|
+
* ```
|
|
1146
|
+
*
|
|
1147
|
+
* @group Racing
|
|
741
1148
|
*/
|
|
742
|
-
export const
|
|
1149
|
+
export const race = (tasks) => async (run) => {
|
|
1150
|
+
assertNonEmptyReadonlyArray(tasks);
|
|
1151
|
+
let firstResult;
|
|
1152
|
+
await run(each(tasks, (result) => {
|
|
1153
|
+
firstResult = result;
|
|
1154
|
+
return "stop";
|
|
1155
|
+
}, { concurrency: tasks.length }));
|
|
1156
|
+
assertNonNullable(firstResult);
|
|
1157
|
+
return firstResult;
|
|
1158
|
+
};
|
|
743
1159
|
/**
|
|
744
|
-
*
|
|
1160
|
+
* Runs Tasks until `count` Tasks return {@link Ok} or all Tasks settle.
|
|
1161
|
+
*
|
|
1162
|
+
* Returns {@link Ok} with Ok values in settlement order, not input order.
|
|
1163
|
+
* {@link Err} Results are ignored. When `count` Ok values have settled,
|
|
1164
|
+
* remaining Tasks are aborted. If fewer than `count` Tasks return Ok, returns
|
|
1165
|
+
* the Ok values that did settle.
|
|
745
1166
|
*
|
|
746
|
-
*
|
|
747
|
-
* {@link
|
|
1167
|
+
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1168
|
+
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
1169
|
+
* a literal, or a validated {@link PositiveInt} for larger values.
|
|
748
1170
|
*
|
|
749
|
-
*
|
|
1171
|
+
* ### Example
|
|
750
1172
|
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
1173
|
+
* ```ts
|
|
1174
|
+
* import {
|
|
1175
|
+
* createRun,
|
|
1176
|
+
* err,
|
|
1177
|
+
* firstN,
|
|
1178
|
+
* ok,
|
|
1179
|
+
* sleep,
|
|
1180
|
+
* type Task,
|
|
1181
|
+
* } from "@evolu/common";
|
|
756
1182
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
1183
|
+
* let slowCompleted = false;
|
|
1184
|
+
* const slow: Task<string, "Failed"> = async (run) => {
|
|
1185
|
+
* await run.ok(sleep("10ms"));
|
|
1186
|
+
* slowCompleted = true;
|
|
1187
|
+
* return ok("slow");
|
|
1188
|
+
* };
|
|
1189
|
+
* const tasks = [
|
|
1190
|
+
* slow,
|
|
1191
|
+
* () => err("Failed" as const),
|
|
1192
|
+
* () => ok("fast-1"),
|
|
1193
|
+
* () => ok("fast-2"),
|
|
1194
|
+
* ] as const;
|
|
1195
|
+
* await using run = createRun();
|
|
1196
|
+
*
|
|
1197
|
+
* // Errs do not count. After two Ok values settle, the slow Task is aborted.
|
|
1198
|
+
* const result = await run(firstN(tasks, 2, { concurrency: 4 }));
|
|
1199
|
+
* expectOk(result, ["fast-1", "fast-2"]);
|
|
1200
|
+
* expect(slowCompleted).toBe(false);
|
|
1201
|
+
* ```
|
|
1202
|
+
*
|
|
1203
|
+
* @group Racing
|
|
1204
|
+
*/
|
|
1205
|
+
export const firstN = (tasks, count, options) => async (run) => {
|
|
1206
|
+
assertType(PositiveInt, count);
|
|
1207
|
+
const values = [];
|
|
1208
|
+
await run(each(tasks, (result) => {
|
|
1209
|
+
if (result.ok)
|
|
1210
|
+
values.push(result.value);
|
|
1211
|
+
return values.length < count ? "continue" : "stop";
|
|
1212
|
+
}, options));
|
|
1213
|
+
return ok(values);
|
|
1214
|
+
};
|
|
1215
|
+
/**
|
|
1216
|
+
* Runs Tasks until `count` Tasks settle or all Tasks settle.
|
|
1217
|
+
*
|
|
1218
|
+
* Returns {@link Ok} with Task {@link Result}s in settlement order, not input
|
|
1219
|
+
* order. When `count` Results have settled, remaining Tasks are aborted. If
|
|
1220
|
+
* fewer than `count` Tasks settle, returns the Results that did settle.
|
|
1221
|
+
*
|
|
1222
|
+
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1223
|
+
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
1224
|
+
* a literal, or a validated {@link PositiveInt} for larger values.
|
|
759
1225
|
*
|
|
760
1226
|
* ### Example
|
|
761
1227
|
*
|
|
762
1228
|
* ```ts
|
|
763
1229
|
* import {
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
1230
|
+
* createRun,
|
|
1231
|
+
* err,
|
|
1232
|
+
* firstNSettled,
|
|
1233
|
+
* ok,
|
|
1234
|
+
* sleep,
|
|
1235
|
+
* type Task,
|
|
769
1236
|
* } from "@evolu/common";
|
|
770
1237
|
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* );
|
|
1238
|
+
* let slowCompleted = false;
|
|
1239
|
+
* const slow: Task<string, "Failed"> = async (run) => {
|
|
1240
|
+
* await run.ok(sleep("10ms"));
|
|
1241
|
+
* slowCompleted = true;
|
|
1242
|
+
* return ok("slow");
|
|
1243
|
+
* };
|
|
1244
|
+
* const tasks = [
|
|
1245
|
+
* slow,
|
|
1246
|
+
* () => err("Failed" as const),
|
|
1247
|
+
* () => ok("fast"),
|
|
1248
|
+
* ] as const;
|
|
1249
|
+
* await using run = createRun();
|
|
776
1250
|
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* console.log(`Last error:`, result.error.cause);
|
|
785
|
-
* }
|
|
786
|
-
* }
|
|
1251
|
+
* // Err and Ok both count, and Results use settlement order.
|
|
1252
|
+
* const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
|
|
1253
|
+
* expectOk(result, [
|
|
1254
|
+
* { ok: false, error: "Failed" },
|
|
1255
|
+
* { ok: true, value: "fast" },
|
|
1256
|
+
* ]);
|
|
1257
|
+
* expect(slowCompleted).toBe(false);
|
|
787
1258
|
* ```
|
|
788
1259
|
*
|
|
789
|
-
*
|
|
790
|
-
|
|
1260
|
+
* @group Racing
|
|
1261
|
+
*/
|
|
1262
|
+
export const firstNSettled = (tasks, count, options) => async (run) => {
|
|
1263
|
+
assertType(PositiveInt, count);
|
|
1264
|
+
const results = [];
|
|
1265
|
+
await run(each(tasks, (result) => {
|
|
1266
|
+
results.push(result);
|
|
1267
|
+
return results.length < count ? "continue" : "stop";
|
|
1268
|
+
}, options));
|
|
1269
|
+
return ok(results);
|
|
1270
|
+
};
|
|
1271
|
+
/**
|
|
1272
|
+
* Runs Tasks under a concurrency limit and calls `onResult` for each Task
|
|
1273
|
+
* {@link Result} as it settles.
|
|
1274
|
+
*
|
|
1275
|
+
* `onResult` receives the Result and the original input index; call order is
|
|
1276
|
+
* settlement order, not input order. Returning `continue` lets queued Tasks
|
|
1277
|
+
* start when capacity is available. Returning `stop` prevents queued Tasks from
|
|
1278
|
+
* starting and aborts already-running Tasks through structured Run disposal —
|
|
1279
|
+
* `each` still waits for them to settle before returning.
|
|
1280
|
+
*
|
|
1281
|
+
* `each` is the scheduling primitive under the collection helpers. Each one is
|
|
1282
|
+
* a small `onResult` policy:
|
|
1283
|
+
*
|
|
1284
|
+
* | Helper | Policy |
|
|
1285
|
+
* | --------------------- | ------------------------------------- |
|
|
1286
|
+
* | {@link all} | Collect values, stop on the first Err |
|
|
1287
|
+
* | {@link allSettled} | Collect every Result, never stop |
|
|
1288
|
+
* | {@link any} | Stop on the first Ok |
|
|
1289
|
+
* | {@link race} | Stop on the first settled Result |
|
|
1290
|
+
* | {@link firstN} | Stop after n Ok values |
|
|
1291
|
+
* | {@link firstNSettled} | Stop after n Results |
|
|
1292
|
+
*
|
|
1293
|
+
* Use `each` directly to build a collection policy the helpers don't cover. For
|
|
1294
|
+
* example, keep the first successful value together with its original input
|
|
1295
|
+
* index:
|
|
1296
|
+
*
|
|
1297
|
+
* ### Example
|
|
791
1298
|
*
|
|
792
1299
|
* ```ts
|
|
793
|
-
* import {
|
|
1300
|
+
* import {
|
|
1301
|
+
* createRun,
|
|
1302
|
+
* each,
|
|
1303
|
+
* err,
|
|
1304
|
+
* ok,
|
|
1305
|
+
* sleep,
|
|
1306
|
+
* type Task,
|
|
1307
|
+
* } from "@evolu/common";
|
|
794
1308
|
*
|
|
795
|
-
*
|
|
796
|
-
* const
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
1309
|
+
* let slowCompleted = false;
|
|
1310
|
+
* const slow: Task<string> = async (run) => {
|
|
1311
|
+
* await run.ok(sleep("10ms"));
|
|
1312
|
+
* slowCompleted = true;
|
|
1313
|
+
* return ok("slow");
|
|
1314
|
+
* };
|
|
1315
|
+
* const tasks = [
|
|
1316
|
+
* slow,
|
|
1317
|
+
* () => err("Unavailable" as const),
|
|
1318
|
+
* () => ok("fast"),
|
|
1319
|
+
* ] as const;
|
|
1320
|
+
* let first: readonly [string, number] | undefined;
|
|
1321
|
+
* await using run = createRun();
|
|
1322
|
+
* const result = await run(
|
|
1323
|
+
* each(
|
|
1324
|
+
* tasks,
|
|
1325
|
+
* (result, index) => {
|
|
1326
|
+
* if (!result.ok) return "continue";
|
|
1327
|
+
* first = [result.value, index];
|
|
1328
|
+
* return "stop";
|
|
1329
|
+
* },
|
|
1330
|
+
* { concurrency: 2 },
|
|
800
1331
|
* ),
|
|
801
1332
|
* );
|
|
1333
|
+
*
|
|
1334
|
+
* expectOk(result, undefined);
|
|
1335
|
+
* expect(first).toEqual(["fast", 2]);
|
|
1336
|
+
* expect(slowCompleted).toBe(false);
|
|
802
1337
|
* ```
|
|
803
1338
|
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
1339
|
+
* `onResult` is a synchronous scheduling decision, not a place to do work. It
|
|
1340
|
+
* runs in the scheduler's own continuation, bracketed by abort checks, and its
|
|
1341
|
+
* return value gates whether queued Tasks may start. For async work per result,
|
|
1342
|
+
* put it inside the Task itself — the Task is the async slot — or start a
|
|
1343
|
+
* supervised side effect with `void run(task)` from inside the callback and
|
|
1344
|
+
* keep the decision synchronous. Like {@link RetryOptions.shouldRetry} and
|
|
1345
|
+
* {@link RetryOptions.onRetry}, `onResult` must not throw: a thrown exception is
|
|
1346
|
+
* a defect that panics the Run tree.
|
|
1347
|
+
*
|
|
1348
|
+
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
1349
|
+
* at a time. Defects from child Tasks keep caller-linked async stack traces;
|
|
1350
|
+
* building on `each` preserves diagnostics that a hand-rolled scheduling loop
|
|
1351
|
+
* typically loses.
|
|
1352
|
+
*
|
|
1353
|
+
* @group Concurrency
|
|
806
1354
|
*/
|
|
807
|
-
export const
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1355
|
+
export const each = (tasks, onResult, options = {}) => async (run) => {
|
|
1356
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1357
|
+
try {
|
|
1358
|
+
// Guard against hanging on an empty array.
|
|
1359
|
+
assertNonEmptyReadonlyArray(tasks);
|
|
1360
|
+
const parked = new Promise(constVoid);
|
|
1361
|
+
const wake = Promise.withResolvers();
|
|
1362
|
+
const _ = __addDisposableResource(env_1, run.onAbort(wake.reject), false);
|
|
1363
|
+
let stopped = false;
|
|
1364
|
+
let nextIndex = 0;
|
|
1365
|
+
const workerCount = Math.min(options.concurrency ?? onePositiveInt, tasks.length);
|
|
1366
|
+
let active = workerCount;
|
|
1367
|
+
// This topology is measured by StackTrace.test.ts ("pool parked" and
|
|
1368
|
+
// "pool parked wake reject"). Defects and stacks must travel await/
|
|
1369
|
+
// combinator edges, not resolver/then bridges; V8 drops parent frames when
|
|
1370
|
+
// a started worker is re-raced, JSC drops them through nested combinators.
|
|
1371
|
+
// Workers are direct race arms and never resolve — non-panic child
|
|
1372
|
+
// AbortErrors reject a worker arm; panics and caller abort reject the wake
|
|
1373
|
+
// via run.onAbort; drain and stop resolve it. `parked` must stay per-call:
|
|
1374
|
+
// a shared never-settling promise would retain worker reactions forever.
|
|
1375
|
+
const eachWorker = async () => {
|
|
1376
|
+
try {
|
|
1377
|
+
while (!stopped && nextIndex < tasks.length) {
|
|
1378
|
+
const index = nextIndex;
|
|
1379
|
+
nextIndex += 1;
|
|
1380
|
+
const result = await run(tasks[index]);
|
|
1381
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- stopped can flip across the await via sibling workers
|
|
1382
|
+
if (stopped)
|
|
1383
|
+
break;
|
|
1384
|
+
run.signal.throwIfAborted();
|
|
1385
|
+
const decision = onResult(result, index);
|
|
1386
|
+
run.signal.throwIfAborted();
|
|
1387
|
+
if (decision === "stop") {
|
|
1388
|
+
stopped = true;
|
|
1389
|
+
wake.resolve();
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
820
1392
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
error
|
|
824
|
-
attempt: PositiveInt.orThrow(decrement(attempt)),
|
|
825
|
-
output,
|
|
826
|
-
delay,
|
|
827
|
-
});
|
|
828
|
-
if (delay > 0) {
|
|
829
|
-
const sleepResult = await run(sleep(delay));
|
|
830
|
-
if (!sleepResult.ok)
|
|
831
|
-
return sleepResult;
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
stopped = true;
|
|
1395
|
+
throw error;
|
|
832
1396
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
return
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1397
|
+
active -= 1;
|
|
1398
|
+
if (active === 0)
|
|
1399
|
+
wake.resolve();
|
|
1400
|
+
return await parked;
|
|
1401
|
+
};
|
|
1402
|
+
await Promise.race([
|
|
1403
|
+
...Array.from({ length: workerCount }, eachWorker),
|
|
1404
|
+
wake.promise,
|
|
1405
|
+
]);
|
|
1406
|
+
return ok();
|
|
1407
|
+
}
|
|
1408
|
+
catch (e_1) {
|
|
1409
|
+
env_1.error = e_1;
|
|
1410
|
+
env_1.hasError = true;
|
|
1411
|
+
}
|
|
1412
|
+
finally {
|
|
1413
|
+
__disposeResources(env_1);
|
|
848
1414
|
}
|
|
849
1415
|
};
|
|
850
1416
|
/**
|
|
851
|
-
*
|
|
1417
|
+
* Assigns static scheduler priority to a {@link Task}.
|
|
1418
|
+
*
|
|
1419
|
+
* {@link Run} uses the priority with native `scheduler.postTask` when available.
|
|
1420
|
+
* Platforms without the Scheduler API run the Task normally. Use a Scheduler
|
|
1421
|
+
* API polyfill such as
|
|
1422
|
+
* {@link https://github.com/GoogleChromeLabs/scheduler-polyfill | scheduler-polyfill}
|
|
1423
|
+
* for hosts like Safari and React Native when priority-aware scheduling is
|
|
1424
|
+
* needed.
|
|
852
1425
|
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* Continues until the schedule returns `Err(Done<void>)` or the Task fails.
|
|
1426
|
+
* `scheduler.postTask` must reject queued aborts with `AbortSignal.reason`.
|
|
1427
|
+
* Other host-specific abort objects are treated as defects.
|
|
856
1428
|
*
|
|
857
|
-
*
|
|
1429
|
+
* ### Example
|
|
1430
|
+
*
|
|
1431
|
+
* ```ts
|
|
1432
|
+
* import { createRun, ok, prioritized, type Task } from "@evolu/common";
|
|
1433
|
+
*
|
|
1434
|
+
* const rebuildSearchIndex: Task<string> = () => ok("indexed");
|
|
1435
|
+
* const backgroundIndexing = prioritized("background", rebuildSearchIndex);
|
|
858
1436
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1437
|
+
* await using run = createRun();
|
|
1438
|
+
* expectOk(await run(backgroundIndexing), "indexed");
|
|
1439
|
+
* ```
|
|
1440
|
+
*
|
|
1441
|
+
* @group Scheduling
|
|
1442
|
+
*/
|
|
1443
|
+
export const prioritized = (priority, task) => withTaskMeta({ priority })(task);
|
|
1444
|
+
/**
|
|
1445
|
+
* Yields execution to the host scheduler.
|
|
1446
|
+
*
|
|
1447
|
+
* Uses native `scheduler.yield()` when available, `setImmediate` when
|
|
1448
|
+
* available, and `setTimeout` elsewhere. Because this is a Task, `await
|
|
1449
|
+
* run(yieldNow)` is an explicit abortable checkpoint and is visible in Run
|
|
1450
|
+
* monitoring.
|
|
1451
|
+
*
|
|
1452
|
+
* For example, call it periodically in a long-running synchronous loop to let
|
|
1453
|
+
* the host process rendering, input, and other scheduled work.
|
|
861
1454
|
*
|
|
862
1455
|
* ### Example
|
|
863
1456
|
*
|
|
864
1457
|
* ```ts
|
|
865
|
-
* import {
|
|
866
|
-
* import { repeat } from "@evolu/common";
|
|
1458
|
+
* import { createRun, ok, yieldNow, type Task } from "@evolu/common";
|
|
867
1459
|
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
1460
|
+
* const sumTo =
|
|
1461
|
+
* (count: number): Task<number> =>
|
|
1462
|
+
* async (run) => {
|
|
1463
|
+
* let sum = 0;
|
|
870
1464
|
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
1465
|
+
* for (let index = 0; index < count; index++) {
|
|
1466
|
+
* if (index > 0 && index % 1000 === 0) await run.ok(yieldNow);
|
|
1467
|
+
* sum += index;
|
|
1468
|
+
* }
|
|
873
1469
|
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
* const item = queue.dequeue();
|
|
877
|
-
* if (!item) return err(done()); // Queue empty, stop
|
|
878
|
-
* await process(item);
|
|
879
|
-
* return ok(item);
|
|
880
|
-
* };
|
|
1470
|
+
* return ok(sum);
|
|
1471
|
+
* };
|
|
881
1472
|
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* console.log("Queue exhausted");
|
|
885
|
-
* }
|
|
1473
|
+
* await using run = createRun();
|
|
1474
|
+
* expectOk(await run(sumTo(1001)), 500500);
|
|
886
1475
|
* ```
|
|
887
1476
|
*
|
|
888
|
-
* @group
|
|
1477
|
+
* @group Scheduling
|
|
889
1478
|
*/
|
|
890
|
-
export const
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
value: result.value,
|
|
907
|
-
attempt,
|
|
908
|
-
output,
|
|
909
|
-
delay,
|
|
1479
|
+
export const yieldNow = async (run) => {
|
|
1480
|
+
const scheduler = globalWithScheduler.scheduler;
|
|
1481
|
+
if (scheduler?.yield) {
|
|
1482
|
+
await scheduler.yield();
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
// Intentionally bypasses deps.time: yielding is a host-scheduler concern,
|
|
1486
|
+
// not logical time, and fake time would block awaiting code in tests. The
|
|
1487
|
+
// timer is not cleared on abort; throwIfAborted below observes abort after
|
|
1488
|
+
// the tick, delaying settlement by at most one timer tick.
|
|
1489
|
+
await new Promise((resolve) => {
|
|
1490
|
+
const immediate = globalWithSetImmediate.setImmediate;
|
|
1491
|
+
if (immediate)
|
|
1492
|
+
immediate(resolve);
|
|
1493
|
+
else
|
|
1494
|
+
setTimeout(resolve, 0);
|
|
910
1495
|
});
|
|
911
|
-
attempt = PositiveInt.orThrow(increment(attempt));
|
|
912
|
-
if (delay > 0) {
|
|
913
|
-
const sleepResult = await run(sleep(delay));
|
|
914
|
-
if (!sleepResult.ok)
|
|
915
|
-
return sleepResult;
|
|
916
|
-
}
|
|
917
1496
|
}
|
|
918
|
-
|
|
1497
|
+
run.signal.throwIfAborted();
|
|
1498
|
+
return ok();
|
|
919
1499
|
};
|
|
1500
|
+
// Abortability
|
|
920
1501
|
/**
|
|
921
|
-
*
|
|
1502
|
+
* Waits until the current {@link Run} aborts, then rejects with its
|
|
1503
|
+
* {@link AbortError}.
|
|
922
1504
|
*
|
|
923
|
-
*
|
|
924
|
-
|
|
925
|
-
export const createDeferred = () => {
|
|
926
|
-
let resolved = null;
|
|
927
|
-
const resolvers = new Set();
|
|
928
|
-
const resolve = (result) => {
|
|
929
|
-
if (resolved !== null)
|
|
930
|
-
return false;
|
|
931
|
-
resolved = result;
|
|
932
|
-
for (const resolver of resolvers)
|
|
933
|
-
resolver(result);
|
|
934
|
-
resolvers.clear();
|
|
935
|
-
return true;
|
|
936
|
-
};
|
|
937
|
-
return {
|
|
938
|
-
task: (run) => {
|
|
939
|
-
if (resolved !== null)
|
|
940
|
-
return resolved;
|
|
941
|
-
return new Promise((resolvePromise) => {
|
|
942
|
-
const resolve = (result) => {
|
|
943
|
-
resolvers.delete(resolve);
|
|
944
|
-
resolvePromise(result);
|
|
945
|
-
};
|
|
946
|
-
resolvers.add(resolve);
|
|
947
|
-
run.onAbort((reason) => {
|
|
948
|
-
resolve(err(createAbortError(reason)));
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
},
|
|
952
|
-
resolve,
|
|
953
|
-
[Symbol.dispose]: () => {
|
|
954
|
-
resolve(err(deferredDisposedError));
|
|
955
|
-
},
|
|
956
|
-
};
|
|
957
|
-
};
|
|
958
|
-
/**
|
|
959
|
-
* Abort reason used when a {@link Deferred} is disposed.
|
|
1505
|
+
* Use it to keep long-running services and Run-owned resources alive until
|
|
1506
|
+
* their owning Run shuts down.
|
|
960
1507
|
*
|
|
961
|
-
*
|
|
962
|
-
*/
|
|
963
|
-
export const DeferredDisposedError = /*#__PURE__*/ typed("DeferredDisposedError");
|
|
964
|
-
/**
|
|
965
|
-
* {@link DeferredDisposedError} used as abort reason in {@link createDeferred}.
|
|
1508
|
+
* ### Example
|
|
966
1509
|
*
|
|
967
|
-
*
|
|
1510
|
+
* ```ts
|
|
1511
|
+
* import {
|
|
1512
|
+
* AbortError,
|
|
1513
|
+
* createRun,
|
|
1514
|
+
* ok,
|
|
1515
|
+
* waitForAbort,
|
|
1516
|
+
* type Task,
|
|
1517
|
+
* } from "@evolu/common";
|
|
1518
|
+
*
|
|
1519
|
+
* interface ServerDep {
|
|
1520
|
+
* readonly port: number;
|
|
1521
|
+
* }
|
|
1522
|
+
* interface Server extends AsyncDisposable {}
|
|
1523
|
+
*
|
|
1524
|
+
* const serverStarted = Promise.withResolvers<void>();
|
|
1525
|
+
* let serverStopped = false;
|
|
1526
|
+
* const startServer: Task<Server, never, ServerDep> = ({ deps }) => {
|
|
1527
|
+
* expect(deps.port).toBe(3000);
|
|
1528
|
+
* serverStarted.resolve();
|
|
1529
|
+
* return ok({
|
|
1530
|
+
* [Symbol.asyncDispose]: async () => {
|
|
1531
|
+
* serverStopped = true;
|
|
1532
|
+
* },
|
|
1533
|
+
* });
|
|
1534
|
+
* };
|
|
1535
|
+
*
|
|
1536
|
+
* const serve = (): Task<never, never, ServerDep> => async (run) => {
|
|
1537
|
+
* await using server = await run.ok(startServer);
|
|
1538
|
+
* return await run(waitForAbort);
|
|
1539
|
+
* };
|
|
1540
|
+
*
|
|
1541
|
+
* expectTypeOf(serve).returns.toEqualTypeOf<
|
|
1542
|
+
* Task<never, never, ServerDep>
|
|
1543
|
+
* >();
|
|
1544
|
+
*
|
|
1545
|
+
* await using run = createRun();
|
|
1546
|
+
* const fiber = run.abortable(serve(), { port: 3000 });
|
|
1547
|
+
* await serverStarted.promise;
|
|
1548
|
+
* fiber.abort();
|
|
1549
|
+
*
|
|
1550
|
+
* const result = await fiber;
|
|
1551
|
+
* assert(!result.ok);
|
|
1552
|
+
* expect(AbortError.is(result.error)).toBe(true);
|
|
1553
|
+
* expect(serverStopped).toBe(true);
|
|
1554
|
+
* ```
|
|
1555
|
+
*
|
|
1556
|
+
* @group Abortability
|
|
968
1557
|
*/
|
|
969
|
-
export const
|
|
970
|
-
|
|
1558
|
+
export const waitForAbort = async (run) => {
|
|
1559
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
1560
|
+
try {
|
|
1561
|
+
const aborted = Promise.withResolvers();
|
|
1562
|
+
const _ = __addDisposableResource(env_2, run.onAbort(aborted.reject), false);
|
|
1563
|
+
return await aborted.promise;
|
|
1564
|
+
}
|
|
1565
|
+
catch (e_2) {
|
|
1566
|
+
env_2.error = e_2;
|
|
1567
|
+
env_2.hasError = true;
|
|
1568
|
+
}
|
|
1569
|
+
finally {
|
|
1570
|
+
__disposeResources(env_2);
|
|
1571
|
+
}
|
|
971
1572
|
};
|
|
972
1573
|
/**
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1574
|
+
* Starts a {@link Task} with {@link Run.daemon} and waits until it settles or the
|
|
1575
|
+
* current Run aborts.
|
|
1576
|
+
*
|
|
1577
|
+
* When the current Run aborts, this helper requests abort for the daemon Task
|
|
1578
|
+
* and returns {@link AbortError} without waiting for that daemon Task to observe
|
|
1579
|
+
* abort, clean up, or settle. This makes the wait abortable, not the underlying
|
|
1580
|
+
* execution. The daemon Task continues under root Run ownership until it
|
|
1581
|
+
* settles, observes abort, or the root Run is disposed.
|
|
1582
|
+
*
|
|
1583
|
+
* This is not a replacement for direct {@link AbortSignal} support in operations
|
|
1584
|
+
* that can observe abort, such as {@link fetch}, timers that accept a signal, or
|
|
1585
|
+
* callback APIs that accept a signal. Use it as an escape hatch for Tasks that
|
|
1586
|
+
* ignore abort when an abort request must stop waiting immediately.
|
|
1587
|
+
*
|
|
1588
|
+
* Do not wrap a Task that keeps using a resource the caller may release after
|
|
1589
|
+
* this wrapper returns, unless the Task reliably observes abort before using
|
|
1590
|
+
* that resource. The daemon Task can continue after the caller stops waiting.
|
|
1591
|
+
* Later domain `Err` results from the daemon Task are discarded after the
|
|
1592
|
+
* caller stops waiting. Defects from the daemon Task remain visible to the root
|
|
1593
|
+
* Run: if it later throws or rejects, the root Run still panics and reports the
|
|
1594
|
+
* defect.
|
|
1595
|
+
*
|
|
1596
|
+
* Compose with {@link race} or {@link timeout} when the losing Task must not
|
|
1597
|
+
* delay the winner. Those helpers normally abort losing Tasks and wait for them
|
|
1598
|
+
* to settle, keeping cleanup and late defects inside the caller's lifetime. A
|
|
1599
|
+
* Task that ignores abort can keep them waiting.
|
|
1600
|
+
*
|
|
1601
|
+
* `run.abortable(task)` returns an owned child Fiber and requests abort through
|
|
1602
|
+
* that Fiber; `daemon(task)` starts a daemon child and stops waiting when the
|
|
1603
|
+
* current Run aborts. {@link unabortable} masks abort for a Task that must
|
|
1604
|
+
* finish once started; daemon lets a Task outlive the caller.
|
|
1605
|
+
*
|
|
1606
|
+
* Because the Task starts with {@link Run.daemon}, a recorded abort request
|
|
1607
|
+
* returns AbortError before the Task starts — including a request masked by
|
|
1608
|
+
* {@link unabortable}, even though `run.signal` stays un-aborted inside the
|
|
1609
|
+
* mask. Inside a masked body, wrapping a Task with `daemon` opts the wait back
|
|
1610
|
+
* into abort observation; omit the wrapper when the mask should keep the Task
|
|
1611
|
+
* running.
|
|
977
1612
|
*
|
|
978
1613
|
* ### Example
|
|
979
1614
|
*
|
|
980
1615
|
* ```ts
|
|
981
|
-
*
|
|
1616
|
+
* import {
|
|
1617
|
+
* createRun,
|
|
1618
|
+
* daemon,
|
|
1619
|
+
* ok,
|
|
1620
|
+
* timeout,
|
|
1621
|
+
* type Task,
|
|
1622
|
+
* } from "@evolu/common";
|
|
982
1623
|
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
1624
|
+
* let finished = false;
|
|
1625
|
+
* let finishTask = (): void => {};
|
|
1626
|
+
* const taskNotUsingAbort: Task<string> = () =>
|
|
1627
|
+
* new Promise((resolve) => {
|
|
1628
|
+
* finishTask = () => {
|
|
1629
|
+
* finished = true;
|
|
1630
|
+
* resolve(ok("done"));
|
|
1631
|
+
* };
|
|
1632
|
+
* });
|
|
985
1633
|
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
1634
|
+
* {
|
|
1635
|
+
* await using run = createRun();
|
|
1636
|
+
* const result = await run(timeout(daemon(taskNotUsingAbort), "1ms"));
|
|
1637
|
+
* assert(!result.ok);
|
|
1638
|
+
* expect(result.error.type).toBe("TimeoutError");
|
|
1639
|
+
* expect(finished).toBe(false);
|
|
1640
|
+
* finishTask();
|
|
1641
|
+
* }
|
|
1642
|
+
* expect(finished).toBe(true);
|
|
1643
|
+
* ```
|
|
988
1644
|
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
1645
|
+
* Promise-producing operations should start inside the Task, not before it.
|
|
1646
|
+
*
|
|
1647
|
+
* ```ts
|
|
1648
|
+
* import { createRun, ok, type Result, type Task } from "@evolu/common";
|
|
1649
|
+
*
|
|
1650
|
+
* type ResultValue = string;
|
|
1651
|
+
* interface MyError {
|
|
1652
|
+
* readonly type: "MyError";
|
|
1653
|
+
* }
|
|
1654
|
+
* const createPromiseReturningResult = (): Promise<
|
|
1655
|
+
* Result<ResultValue, MyError>
|
|
1656
|
+
* > => Promise.resolve(ok("value"));
|
|
1657
|
+
*
|
|
1658
|
+
* const task: Task<ResultValue, MyError> = () =>
|
|
1659
|
+
* createPromiseReturningResult();
|
|
1660
|
+
*
|
|
1661
|
+
* await using run = createRun();
|
|
1662
|
+
* expectOk(await run(task), "value");
|
|
1663
|
+
* ```
|
|
1664
|
+
*
|
|
1665
|
+
* Do not reuse an already-running Promise. It started outside the Task, so the
|
|
1666
|
+
* Run cannot own its lifetime or request abort before it begins.
|
|
1667
|
+
*
|
|
1668
|
+
* ```ts
|
|
1669
|
+
* import { ok, type Result, type Task } from "@evolu/common";
|
|
1670
|
+
*
|
|
1671
|
+
* type ResultValue = string;
|
|
1672
|
+
* interface MyError {
|
|
1673
|
+
* readonly type: "MyError";
|
|
1674
|
+
* }
|
|
1675
|
+
* let promiseStarted = false;
|
|
1676
|
+
* const createPromiseReturningResult = (): Promise<
|
|
1677
|
+
* Result<ResultValue, MyError>
|
|
1678
|
+
* > => {
|
|
1679
|
+
* promiseStarted = true;
|
|
1680
|
+
* return Promise.resolve(ok("value"));
|
|
995
1681
|
* };
|
|
1682
|
+
*
|
|
1683
|
+
* // Wrong: the Promise starts now, before a Run starts the Task.
|
|
1684
|
+
* const promise = createPromiseReturningResult();
|
|
1685
|
+
* const task: Task<ResultValue, MyError> = () => promise;
|
|
1686
|
+
*
|
|
1687
|
+
* expect(promiseStarted).toBe(true);
|
|
1688
|
+
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue, MyError>>();
|
|
996
1689
|
* ```
|
|
997
1690
|
*
|
|
998
|
-
* @group
|
|
1691
|
+
* @group Lifetime
|
|
999
1692
|
*/
|
|
1000
|
-
export const
|
|
1001
|
-
const
|
|
1693
|
+
export const daemon = (task) => async (run) => {
|
|
1694
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1002
1695
|
try {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
open: () => {
|
|
1019
|
-
assertNotDisposed(disposables);
|
|
1020
|
-
if (isOpen)
|
|
1021
|
-
return;
|
|
1022
|
-
isOpen = true;
|
|
1023
|
-
deferred.resolve(ok());
|
|
1024
|
-
},
|
|
1025
|
-
close: () => {
|
|
1026
|
-
assertNotDisposed(disposables);
|
|
1027
|
-
if (!isOpen)
|
|
1028
|
-
return;
|
|
1029
|
-
isOpen = false;
|
|
1030
|
-
deferred = createDeferred();
|
|
1031
|
-
},
|
|
1032
|
-
isOpen: () => {
|
|
1033
|
-
assertNotDisposed(disposables);
|
|
1034
|
-
return isOpen;
|
|
1035
|
-
},
|
|
1036
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
1037
|
-
};
|
|
1696
|
+
let fiber;
|
|
1697
|
+
try {
|
|
1698
|
+
fiber = run.daemon(task);
|
|
1699
|
+
}
|
|
1700
|
+
catch (error) {
|
|
1701
|
+
if (AbortError.is(error))
|
|
1702
|
+
return err(error);
|
|
1703
|
+
throw error;
|
|
1704
|
+
}
|
|
1705
|
+
const aborted = Promise.withResolvers();
|
|
1706
|
+
const _ = __addDisposableResource(env_3, run.onAbort((abortError) => {
|
|
1707
|
+
fiber.abort(abortError.reason);
|
|
1708
|
+
aborted.resolve(err(abortError));
|
|
1709
|
+
}), false);
|
|
1710
|
+
return await Promise.race([fiber, aborted.promise]);
|
|
1038
1711
|
}
|
|
1039
|
-
catch (
|
|
1040
|
-
|
|
1041
|
-
|
|
1712
|
+
catch (e_3) {
|
|
1713
|
+
env_3.error = e_3;
|
|
1714
|
+
env_3.hasError = true;
|
|
1042
1715
|
}
|
|
1043
1716
|
finally {
|
|
1044
|
-
__disposeResources(
|
|
1717
|
+
__disposeResources(env_3);
|
|
1045
1718
|
}
|
|
1046
1719
|
};
|
|
1047
1720
|
/**
|
|
1048
|
-
*
|
|
1721
|
+
* Makes a {@link Task} unabortable after it starts.
|
|
1722
|
+
*
|
|
1723
|
+
* Abort requests are masked while the Task runs, so `run.signal.aborted`
|
|
1724
|
+
* remains false inside the Task. This does not force the Task to start after an
|
|
1725
|
+
* abort request has already reached its Run; unabortable means the Task is not
|
|
1726
|
+
* interrupted once it has started. Disposing the enclosing Run still waits for
|
|
1727
|
+
* the Task to settle.
|
|
1728
|
+
*
|
|
1729
|
+
* Apply at most one abort behavior helper to a Task: do not wrap the same Task
|
|
1730
|
+
* with both unabortable and restore, or apply either helper more than once.
|
|
1731
|
+
*
|
|
1732
|
+
* ### Example
|
|
1733
|
+
*
|
|
1734
|
+
* ```ts
|
|
1735
|
+
* import { createRun, ok, unabortable, type Task } from "@evolu/common";
|
|
1736
|
+
*
|
|
1737
|
+
* const commitStarted = Promise.withResolvers<void>();
|
|
1738
|
+
* const finishCommit = Promise.withResolvers<void>();
|
|
1739
|
+
* const commit: Task<string> = unabortable(async ({ signal }) => {
|
|
1740
|
+
* commitStarted.resolve();
|
|
1741
|
+
* await finishCommit.promise;
|
|
1742
|
+
* expect(signal.aborted).toBe(false);
|
|
1743
|
+
* return ok("committed");
|
|
1744
|
+
* });
|
|
1745
|
+
*
|
|
1746
|
+
* await using run = createRun();
|
|
1747
|
+
* const fiber = run.abortable(commit);
|
|
1748
|
+
* await commitStarted.promise;
|
|
1749
|
+
* fiber.abort();
|
|
1750
|
+
* finishCommit.resolve();
|
|
1751
|
+
*
|
|
1752
|
+
* expectOk(await fiber, "committed");
|
|
1753
|
+
* ```
|
|
1754
|
+
*
|
|
1755
|
+
* @group Abortability
|
|
1756
|
+
*/
|
|
1757
|
+
export const unabortable = /*#__PURE__*/ withTaskMeta({
|
|
1758
|
+
abortBehavior: "unabortable",
|
|
1759
|
+
});
|
|
1760
|
+
/**
|
|
1761
|
+
* Like {@link unabortable}, but provides `restore` for child Tasks that should
|
|
1762
|
+
* run with the previous abort mask.
|
|
1763
|
+
*
|
|
1764
|
+
* Use this for acquire/use/release flows where acquire and release must finish
|
|
1765
|
+
* once started, while use should remain abortable. Child Tasks inherit the mask
|
|
1766
|
+
* unless they are wrapped with `restore()` before scheduling.
|
|
1767
|
+
*
|
|
1768
|
+
* An abort request before the mask Task starts prevents entering the mask. Once
|
|
1769
|
+
* the body starts, plain child Tasks inherit the mask, so acquire and release
|
|
1770
|
+
* can run after abort. Put release operations directly in the original mask's
|
|
1771
|
+
* `finally`; do not wrap release in a nested `unabortableMask`, which is a new
|
|
1772
|
+
* critical-section entry and may not start after abort.
|
|
1773
|
+
*
|
|
1774
|
+
* {@link AsyncDisposableStack} is often enough for ordinary acquire/release. Use
|
|
1775
|
+
* it inside `unabortableMask` when masked acquire/release spans multiple or
|
|
1776
|
+
* conditional resources.
|
|
1049
1777
|
*
|
|
1050
1778
|
* ### Example
|
|
1051
1779
|
*
|
|
1052
1780
|
* ```ts
|
|
1781
|
+
* import {
|
|
1782
|
+
* AbortError,
|
|
1783
|
+
* createRun,
|
|
1784
|
+
* ok,
|
|
1785
|
+
* unabortableMask,
|
|
1786
|
+
* waitForAbort,
|
|
1787
|
+
* type Task,
|
|
1788
|
+
* } from "@evolu/common";
|
|
1789
|
+
*
|
|
1053
1790
|
* await using run = createRun();
|
|
1054
1791
|
*
|
|
1055
|
-
*
|
|
1792
|
+
* interface Resource {
|
|
1793
|
+
* readonly id: string;
|
|
1794
|
+
* }
|
|
1056
1795
|
*
|
|
1057
|
-
* const
|
|
1058
|
-
*
|
|
1796
|
+
* const acquire: Task<Resource> = () => ok({ id: "resource-1" });
|
|
1797
|
+
* const operationStarted = Promise.withResolvers<void>();
|
|
1798
|
+
* const operate =
|
|
1799
|
+
* (resource: Resource): Task<never> =>
|
|
1059
1800
|
* async (run) => {
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1801
|
+
* expect(resource.id).toBe("resource-1");
|
|
1802
|
+
* operationStarted.resolve();
|
|
1803
|
+
* return await run(waitForAbort);
|
|
1804
|
+
* };
|
|
1805
|
+
* let released = false;
|
|
1806
|
+
* const release =
|
|
1807
|
+
* (_resource: Resource): Task<void> =>
|
|
1808
|
+
* ({ signal }) => {
|
|
1809
|
+
* // Release inherits the mask even after abort was requested.
|
|
1810
|
+
* expect(signal.aborted).toBe(false);
|
|
1811
|
+
* released = true;
|
|
1812
|
+
* return ok();
|
|
1066
1813
|
* };
|
|
1067
1814
|
*
|
|
1068
|
-
* const
|
|
1069
|
-
*
|
|
1815
|
+
* const fiber = run.abortable(
|
|
1816
|
+
* unabortableMask((restore) => async (run) => {
|
|
1817
|
+
* // Acquire with abort masked.
|
|
1818
|
+
* const resource = await run.ok(acquire);
|
|
1819
|
+
*
|
|
1820
|
+
* try {
|
|
1821
|
+
* // Use with the previous abort mask restored.
|
|
1822
|
+
* return await run(restore(operate(resource)));
|
|
1823
|
+
* } finally {
|
|
1824
|
+
* // Release with abort masked.
|
|
1825
|
+
* await run.ok(release(resource));
|
|
1826
|
+
* }
|
|
1827
|
+
* }),
|
|
1828
|
+
* );
|
|
1070
1829
|
*
|
|
1071
|
-
* await
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1830
|
+
* await operationStarted.promise;
|
|
1831
|
+
* fiber.abort();
|
|
1832
|
+
* const result = await fiber;
|
|
1833
|
+
* assert(!result.ok);
|
|
1834
|
+
* expect(AbortError.is(result.error)).toBe(true);
|
|
1835
|
+
* expect(released).toBe(true);
|
|
1836
|
+
* ```
|
|
1837
|
+
*
|
|
1838
|
+
* @group Abortability
|
|
1839
|
+
*/
|
|
1840
|
+
export const unabortableMask = (fn) => unabortable((run) => {
|
|
1841
|
+
const runInternal = run;
|
|
1842
|
+
// Only verifies the Run is masked. A direct call inside an already masked
|
|
1843
|
+
// Run passes undetected; direct calls bypass Run semantics by design.
|
|
1844
|
+
assert(runInternal.abortMask > abortableMask, "unabortableMask requires a masked Run; use run(task), not a direct call");
|
|
1845
|
+
const restoreToken = Symbol();
|
|
1846
|
+
// The token is local to this Task Run; descendant Runs inherit the token
|
|
1847
|
+
// set so helpers can receive restore while the mask Task is alive.
|
|
1848
|
+
// Each set is bounded by its Run lifetime and is intentionally not pruned.
|
|
1849
|
+
runInternal.restoreTokens = new Set(runInternal.restoreTokens).add(restoreToken);
|
|
1850
|
+
return run(fn(withTaskMeta({
|
|
1851
|
+
abortBehavior: {
|
|
1852
|
+
abortMask: decrement(runInternal.abortMask),
|
|
1853
|
+
restoreToken,
|
|
1854
|
+
},
|
|
1855
|
+
})));
|
|
1856
|
+
});
|
|
1857
|
+
/**
|
|
1858
|
+
* Runs acquire, use, and release as one bracketed {@link Task}.
|
|
1859
|
+
*
|
|
1860
|
+
* Acquire and release are masked once the helper starts, while use runs with
|
|
1861
|
+
* the previous abort mask restored. If acquire returns an error, release is not
|
|
1862
|
+
* run. Release must not return recoverable errors; handle expected cleanup
|
|
1863
|
+
* failures inside the release Task.
|
|
1076
1864
|
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1865
|
+
* Use this when a resource, lease, lock, or session must always be released
|
|
1866
|
+
* after it is acquired, even when use fails or is aborted.
|
|
1867
|
+
*
|
|
1868
|
+
* Prefer native `using`, `await using`, or {@link AsyncDisposableStack} for
|
|
1869
|
+
* owned values that implement {@link Disposable} or {@link AsyncDisposable}. Use
|
|
1870
|
+
* `acquireUseRelease` when acquisition must be balanced with a separate release
|
|
1871
|
+
* operation, such as unlocking, returning a pooled value, releasing a lease, or
|
|
1872
|
+
* logging out of a session.
|
|
1873
|
+
*
|
|
1874
|
+
* ### Example
|
|
1875
|
+
*
|
|
1876
|
+
* ```ts
|
|
1877
|
+
* import {
|
|
1878
|
+
* acquireUseRelease,
|
|
1879
|
+
* createRun,
|
|
1880
|
+
* err,
|
|
1881
|
+
* ok,
|
|
1882
|
+
* type Task,
|
|
1883
|
+
* } from "@evolu/common";
|
|
1884
|
+
*
|
|
1885
|
+
* interface Connection {
|
|
1886
|
+
* readonly user: string;
|
|
1887
|
+
* readonly isAvailable: boolean;
|
|
1888
|
+
* }
|
|
1889
|
+
*
|
|
1890
|
+
* const openConnection: Task<Connection> = () =>
|
|
1891
|
+
* ok({ user: "Ada", isAvailable: false });
|
|
1892
|
+
* const loadUser =
|
|
1893
|
+
* (connection: Connection): Task<string, "Unavailable"> =>
|
|
1894
|
+
* () =>
|
|
1895
|
+
* connection.isAvailable ? ok(connection.user) : err("Unavailable");
|
|
1896
|
+
* let connectionClosed = false;
|
|
1897
|
+
* const closeConnection =
|
|
1898
|
+
* (_connection: Connection): Task<void> =>
|
|
1899
|
+
* () => {
|
|
1900
|
+
* connectionClosed = true;
|
|
1901
|
+
* return ok();
|
|
1902
|
+
* };
|
|
1903
|
+
*
|
|
1904
|
+
* const queryUser = acquireUseRelease(
|
|
1905
|
+
* openConnection,
|
|
1906
|
+
* loadUser,
|
|
1907
|
+
* (connection) => closeConnection(connection),
|
|
1908
|
+
* );
|
|
1909
|
+
*
|
|
1910
|
+
* await using run = createRun();
|
|
1911
|
+
* expectErr(await run(queryUser), "Unavailable");
|
|
1912
|
+
* // Release still runs when use returns a domain error.
|
|
1913
|
+
* expect(connectionClosed).toBe(true);
|
|
1083
1914
|
* ```
|
|
1084
1915
|
*
|
|
1085
|
-
* @group
|
|
1916
|
+
* @group Abortability
|
|
1086
1917
|
*/
|
|
1087
|
-
export const
|
|
1088
|
-
const
|
|
1918
|
+
export const acquireUseRelease = (acquire, use, release) => unabortableMask((restore) => async (run) => {
|
|
1919
|
+
const resourceResult = await run(acquire);
|
|
1920
|
+
if (!resourceResult.ok)
|
|
1921
|
+
return resourceResult;
|
|
1089
1922
|
try {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const waiters = [];
|
|
1093
|
-
let taken = zeroNonNegativeInt;
|
|
1094
|
-
disposer.defer(() => {
|
|
1095
|
-
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1096
|
-
try {
|
|
1097
|
-
const disposer = __addDisposableResource(env_3, new DisposableStack(), false);
|
|
1098
|
-
for (const fiber of fibers) {
|
|
1099
|
-
disposer.adopt(fiber, (fiber) => {
|
|
1100
|
-
fiber.abort(semaphoreDisposedError);
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
for (const waiter of waiters) {
|
|
1104
|
-
waiter.resolve(err(semaphoreDisposedAbortError));
|
|
1105
|
-
}
|
|
1106
|
-
waiters.length = 0;
|
|
1107
|
-
}
|
|
1108
|
-
catch (e_3) {
|
|
1109
|
-
env_3.error = e_3;
|
|
1110
|
-
env_3.hasError = true;
|
|
1111
|
-
}
|
|
1112
|
-
finally {
|
|
1113
|
-
__disposeResources(env_3);
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
const disposables = disposer.move();
|
|
1117
|
-
const withPermits = (requestedPermits) => (task) => async (run) => {
|
|
1118
|
-
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
1119
|
-
try {
|
|
1120
|
-
const requested = PositiveInt.orThrow(requestedPermits);
|
|
1121
|
-
assert(requested <= permits, "Requested permits must not exceed semaphore capacity.");
|
|
1122
|
-
if (disposables.disposed)
|
|
1123
|
-
return err(semaphoreDisposedAbortError);
|
|
1124
|
-
if (waiters.length > 0 || taken + requested > permits) {
|
|
1125
|
-
const waiter = Promise.withResolvers();
|
|
1126
|
-
const waiting = {
|
|
1127
|
-
permits: requested,
|
|
1128
|
-
resolve: waiter.resolve,
|
|
1129
|
-
};
|
|
1130
|
-
waiters.push(waiting);
|
|
1131
|
-
run.onAbort((reason) => {
|
|
1132
|
-
const i = waiters.indexOf(waiting);
|
|
1133
|
-
if (i >= 0)
|
|
1134
|
-
waiters.splice(i, 1);
|
|
1135
|
-
waiter.resolve(err(createAbortError(reason)));
|
|
1136
|
-
});
|
|
1137
|
-
const permit = await waiter.promise;
|
|
1138
|
-
if (!permit.ok)
|
|
1139
|
-
return permit;
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
taken = NonNegativeInt.orThrow(taken + requested);
|
|
1143
|
-
}
|
|
1144
|
-
let fiber = null;
|
|
1145
|
-
const _ = __addDisposableResource(env_4, {
|
|
1146
|
-
[Symbol.dispose]: () => {
|
|
1147
|
-
if (fiber)
|
|
1148
|
-
fibers.delete(fiber);
|
|
1149
|
-
taken = NonNegativeInt.orThrow(taken - requested);
|
|
1150
|
-
while (waiters.length > 0) {
|
|
1151
|
-
const waiter = waiters[0];
|
|
1152
|
-
if (taken + waiter.permits > permits)
|
|
1153
|
-
break;
|
|
1154
|
-
waiters.shift();
|
|
1155
|
-
taken = NonNegativeInt.orThrow(taken + waiter.permits);
|
|
1156
|
-
waiter.resolve(ok());
|
|
1157
|
-
}
|
|
1158
|
-
},
|
|
1159
|
-
}, false);
|
|
1160
|
-
fiber = run(task);
|
|
1161
|
-
fibers.add(fiber);
|
|
1162
|
-
return await fiber;
|
|
1163
|
-
}
|
|
1164
|
-
catch (e_4) {
|
|
1165
|
-
env_4.error = e_4;
|
|
1166
|
-
env_4.hasError = true;
|
|
1167
|
-
}
|
|
1168
|
-
finally {
|
|
1169
|
-
__disposeResources(env_4);
|
|
1170
|
-
}
|
|
1171
|
-
};
|
|
1172
|
-
return {
|
|
1173
|
-
withPermits,
|
|
1174
|
-
withPermit: (task) => withPermits(1)(task),
|
|
1175
|
-
snapshot: () => {
|
|
1176
|
-
assertNotDisposed(disposables);
|
|
1177
|
-
return {
|
|
1178
|
-
permits,
|
|
1179
|
-
taken,
|
|
1180
|
-
waiting: NonNegativeInt.orThrow(waiters.length),
|
|
1181
|
-
available: NonNegativeInt.orThrow(permits - taken),
|
|
1182
|
-
isIdle: taken === 0 && waiters.length === 0,
|
|
1183
|
-
disposed: disposables.disposed,
|
|
1184
|
-
};
|
|
1185
|
-
},
|
|
1186
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
catch (e_2) {
|
|
1190
|
-
env_2.error = e_2;
|
|
1191
|
-
env_2.hasError = true;
|
|
1923
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- `use` is an acquireUseRelease callback, not a React Hook.
|
|
1924
|
+
return await run(restore(use(resourceResult.value)));
|
|
1192
1925
|
}
|
|
1193
1926
|
finally {
|
|
1194
|
-
|
|
1927
|
+
await run.ok(release(resourceResult.value));
|
|
1195
1928
|
}
|
|
1196
|
-
};
|
|
1929
|
+
});
|
|
1197
1930
|
/**
|
|
1198
|
-
*
|
|
1931
|
+
* Creates a {@link Deferred}.
|
|
1199
1932
|
*
|
|
1200
1933
|
* @group Concurrency primitives
|
|
1201
1934
|
*/
|
|
1202
|
-
export const
|
|
1935
|
+
export const createDeferred = () => {
|
|
1936
|
+
let resolvedResult;
|
|
1937
|
+
const waiters = new Set();
|
|
1938
|
+
return {
|
|
1939
|
+
task: callback(({ resolve }) => {
|
|
1940
|
+
if (resolvedResult) {
|
|
1941
|
+
resolve(resolvedResult);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
waiters.add(resolve);
|
|
1945
|
+
return () => {
|
|
1946
|
+
waiters.delete(resolve);
|
|
1947
|
+
};
|
|
1948
|
+
}),
|
|
1949
|
+
resolve: (result) => {
|
|
1950
|
+
if (resolvedResult)
|
|
1951
|
+
return false;
|
|
1952
|
+
resolvedResult = result;
|
|
1953
|
+
for (const wait of waiters)
|
|
1954
|
+
wait(result);
|
|
1955
|
+
waiters.clear();
|
|
1956
|
+
return true;
|
|
1957
|
+
},
|
|
1958
|
+
};
|
|
1959
|
+
};
|
|
1203
1960
|
/**
|
|
1204
|
-
*
|
|
1961
|
+
* Creates a {@link Gate}.
|
|
1205
1962
|
*
|
|
1206
1963
|
* @group Concurrency primitives
|
|
1207
1964
|
*/
|
|
1208
|
-
export const
|
|
1209
|
-
|
|
1965
|
+
export const createGate = ({ isOpen = false, } = {}) => {
|
|
1966
|
+
let deferred = createDeferred();
|
|
1967
|
+
if (isOpen)
|
|
1968
|
+
deferred.resolve(ok());
|
|
1969
|
+
return {
|
|
1970
|
+
// Direct same-Run delegation is intentional so wait observes the current deferred.
|
|
1971
|
+
// eslint-disable-next-line evolu/no-direct-task-call
|
|
1972
|
+
wait: (run) => deferred.task(run),
|
|
1973
|
+
open: () => {
|
|
1974
|
+
if (isOpen)
|
|
1975
|
+
return false;
|
|
1976
|
+
isOpen = true;
|
|
1977
|
+
deferred.resolve(ok());
|
|
1978
|
+
return true;
|
|
1979
|
+
},
|
|
1980
|
+
close: () => {
|
|
1981
|
+
if (!isOpen)
|
|
1982
|
+
return false;
|
|
1983
|
+
isOpen = false;
|
|
1984
|
+
deferred = createDeferred();
|
|
1985
|
+
return true;
|
|
1986
|
+
},
|
|
1987
|
+
release: () => {
|
|
1988
|
+
if (isOpen)
|
|
1989
|
+
return false;
|
|
1990
|
+
deferred.resolve(ok());
|
|
1991
|
+
deferred = createDeferred();
|
|
1992
|
+
return true;
|
|
1993
|
+
},
|
|
1994
|
+
isOpen: () => isOpen,
|
|
1995
|
+
};
|
|
1210
1996
|
};
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1997
|
+
/**
|
|
1998
|
+
* Creates a {@link Semaphore}.
|
|
1999
|
+
*
|
|
2000
|
+
* @group Concurrency primitives
|
|
2001
|
+
*/
|
|
2002
|
+
export const createSemaphore = (initialPermits, { policy = "fifo", } = {}) => {
|
|
2003
|
+
const isGreedy = policy === "greedy";
|
|
2004
|
+
let permits = PositiveInt.orThrow(initialPermits);
|
|
2005
|
+
let taken = NonNegativeInt.orThrow(0);
|
|
2006
|
+
const waiters = new Set();
|
|
2007
|
+
const hasFreePermits = (requested) => taken + requested <= permits;
|
|
2008
|
+
const canTake = (requested) => hasFreePermits(requested) && (isGreedy || waiters.size === 0);
|
|
2009
|
+
const grant = (requested, leakDetector) => {
|
|
2010
|
+
taken = NonNegativeInt.orThrow(taken + requested);
|
|
2011
|
+
let released = false;
|
|
2012
|
+
const handle = {};
|
|
2013
|
+
const release = () => {
|
|
2014
|
+
if (released)
|
|
2015
|
+
return false;
|
|
2016
|
+
released = true;
|
|
2017
|
+
leakDetector.untrack(handle);
|
|
2018
|
+
taken = NonNegativeInt.orThrow(taken - requested);
|
|
2019
|
+
releaseWaiters();
|
|
2020
|
+
return true;
|
|
2021
|
+
};
|
|
2022
|
+
const permit = {
|
|
2023
|
+
permits: requested,
|
|
2024
|
+
release,
|
|
2025
|
+
[Symbol.dispose]: release,
|
|
2026
|
+
};
|
|
2027
|
+
leakDetector.track(permit, { name: "SemaphorePermit", isLeaked: () => !released }, handle);
|
|
2028
|
+
return permit;
|
|
2029
|
+
};
|
|
2030
|
+
const releaseWaiters = () => {
|
|
2031
|
+
for (const waiter of waiters) {
|
|
2032
|
+
if (!hasFreePermits(waiter.permits)) {
|
|
2033
|
+
if (!isGreedy)
|
|
2034
|
+
break;
|
|
2035
|
+
continue;
|
|
1236
2036
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
2037
|
+
waiters.delete(waiter);
|
|
2038
|
+
waiter.resolve(ok(grant(waiter.permits, waiter.leakDetector)));
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
const take = (requestedPermits) => callback(({ run: { deps }, resolve }) => {
|
|
2042
|
+
const requested = PositiveInt.orThrow(requestedPermits);
|
|
2043
|
+
if (canTake(requested)) {
|
|
2044
|
+
resolve(ok(grant(requested, deps.leakDetector)));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const waiter = {
|
|
2048
|
+
permits: requested,
|
|
2049
|
+
leakDetector: deps.leakDetector,
|
|
2050
|
+
resolve,
|
|
2051
|
+
};
|
|
2052
|
+
waiters.add(waiter);
|
|
2053
|
+
return () => {
|
|
2054
|
+
if (waiters.delete(waiter))
|
|
2055
|
+
releaseWaiters();
|
|
2056
|
+
};
|
|
2057
|
+
});
|
|
2058
|
+
const withPermits = (requestedPermits) => (task) => async (run) => {
|
|
2059
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
2060
|
+
try {
|
|
2061
|
+
const _permit = __addDisposableResource(env_4, await run.ok(take(requestedPermits)), false);
|
|
2062
|
+
return await run(task);
|
|
2063
|
+
}
|
|
2064
|
+
catch (e_4) {
|
|
2065
|
+
env_4.error = e_4;
|
|
2066
|
+
env_4.hasError = true;
|
|
2067
|
+
}
|
|
2068
|
+
finally {
|
|
2069
|
+
__disposeResources(env_4);
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
const isIdle = () => taken === 0 && waiters.size === 0;
|
|
2073
|
+
return {
|
|
2074
|
+
withPermit: (task) => withPermits(1)(task),
|
|
2075
|
+
withPermits,
|
|
2076
|
+
withPermitsIfAvailable: (requestedPermits) => (task) => async (run) => {
|
|
2077
|
+
const env_5 = { stack: [], error: void 0, hasError: false };
|
|
1241
2078
|
try {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
[Symbol.dispose]: () => {
|
|
1251
|
-
if (semaphoresByKey.get(key) !== semaphore)
|
|
1252
|
-
return;
|
|
1253
|
-
const snapshot = semaphore.snapshot();
|
|
1254
|
-
if (snapshot.isIdle) {
|
|
1255
|
-
semaphoresByKey.delete(key);
|
|
1256
|
-
semaphore[Symbol.dispose]();
|
|
1257
|
-
}
|
|
1258
|
-
},
|
|
1259
|
-
}, false);
|
|
1260
|
-
return await run(semaphore.withPermits(requestedPermits)(task));
|
|
2079
|
+
const requested = PositiveInt.orThrow(requestedPermits);
|
|
2080
|
+
if (!canTake(requested))
|
|
2081
|
+
return ok(none);
|
|
2082
|
+
const _permit = __addDisposableResource(env_5, grant(requested, run.deps.leakDetector), false);
|
|
2083
|
+
const result = await run(task);
|
|
2084
|
+
if (!result.ok)
|
|
2085
|
+
return result;
|
|
2086
|
+
return ok(some(result.value));
|
|
1261
2087
|
}
|
|
1262
|
-
catch (
|
|
1263
|
-
|
|
1264
|
-
|
|
2088
|
+
catch (e_5) {
|
|
2089
|
+
env_5.error = e_5;
|
|
2090
|
+
env_5.hasError = true;
|
|
1265
2091
|
}
|
|
1266
2092
|
finally {
|
|
1267
|
-
__disposeResources(
|
|
2093
|
+
__disposeResources(env_5);
|
|
1268
2094
|
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
2095
|
+
},
|
|
2096
|
+
take,
|
|
2097
|
+
resize: (newPermits) => {
|
|
2098
|
+
permits = PositiveInt.orThrow(newPermits);
|
|
2099
|
+
releaseWaiters();
|
|
2100
|
+
},
|
|
2101
|
+
snapshot: () => ({
|
|
2102
|
+
policy,
|
|
2103
|
+
permits,
|
|
2104
|
+
taken,
|
|
2105
|
+
waiters: Array.from(waiters, ({ permits }) => ({ permits })),
|
|
2106
|
+
available: NonNegativeInt.orThrow(!isGreedy && waiters.size > 0 ? 0 : Math.max(0, permits - taken)),
|
|
2107
|
+
isIdle: isIdle(),
|
|
2108
|
+
}),
|
|
2109
|
+
isIdle,
|
|
2110
|
+
};
|
|
2111
|
+
};
|
|
1288
2112
|
/**
|
|
1289
2113
|
* Creates a {@link Mutex}.
|
|
1290
2114
|
*
|
|
1291
2115
|
* @group Concurrency primitives
|
|
1292
2116
|
*/
|
|
1293
2117
|
export const createMutex = () => {
|
|
1294
|
-
const semaphore = createSemaphore(
|
|
2118
|
+
const semaphore = createSemaphore(1);
|
|
1295
2119
|
return {
|
|
1296
2120
|
withLock: semaphore.withPermit,
|
|
1297
2121
|
snapshot: semaphore.snapshot,
|
|
1298
|
-
[Symbol.dispose]: semaphore[Symbol.dispose],
|
|
1299
2122
|
};
|
|
1300
2123
|
};
|
|
2124
|
+
export function createSemaphoreByKey(initialPermits, { lookup = identity } = {}) {
|
|
2125
|
+
const semaphoresByKey = createLookupMap({ lookup });
|
|
2126
|
+
const getSemaphore = (key) => semaphoresByKey.getOrInsertComputed(key, () => createSemaphore(initialPermits));
|
|
2127
|
+
const withPermits = (key, requestedPermits) => (task) => async (run) => {
|
|
2128
|
+
const semaphore = getSemaphore(key);
|
|
2129
|
+
try {
|
|
2130
|
+
const env_6 = { stack: [], error: void 0, hasError: false };
|
|
2131
|
+
try {
|
|
2132
|
+
const _permit = __addDisposableResource(env_6, await run.ok(semaphore.take(requestedPermits)), false);
|
|
2133
|
+
return await run(task);
|
|
2134
|
+
}
|
|
2135
|
+
catch (e_6) {
|
|
2136
|
+
env_6.error = e_6;
|
|
2137
|
+
env_6.hasError = true;
|
|
2138
|
+
}
|
|
2139
|
+
finally {
|
|
2140
|
+
__disposeResources(env_6);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
finally {
|
|
2144
|
+
if (semaphore.isIdle())
|
|
2145
|
+
semaphoresByKey.delete(key);
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
return {
|
|
2149
|
+
withPermit: (key, task) => withPermits(key, 1)(task),
|
|
2150
|
+
withPermits,
|
|
2151
|
+
isIdle: (key) => semaphoresByKey.get(key)?.isIdle() ?? true,
|
|
2152
|
+
snapshot: (key) => semaphoresByKey.get(key)?.snapshot() ?? null,
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
1301
2155
|
export function createMutexByKey({ lookup = identity, } = {}) {
|
|
1302
|
-
const semaphoreByKey = createSemaphoreByKey(
|
|
1303
|
-
lookup,
|
|
1304
|
-
});
|
|
2156
|
+
const semaphoreByKey = createSemaphoreByKey(1, { lookup });
|
|
1305
2157
|
return {
|
|
1306
2158
|
withLock: (key, task) => semaphoreByKey.withPermit(key, task),
|
|
2159
|
+
isIdle: semaphoreByKey.isIdle,
|
|
1307
2160
|
snapshot: semaphoreByKey.snapshot,
|
|
1308
|
-
[Symbol.dispose]: semaphoreByKey[Symbol.dispose],
|
|
1309
2161
|
};
|
|
1310
2162
|
}
|
|
1311
2163
|
/**
|
|
1312
|
-
* Creates a {@link MutexRef}
|
|
2164
|
+
* Creates a {@link MutexRef}.
|
|
1313
2165
|
*
|
|
1314
2166
|
* @group Concurrency primitives
|
|
1315
2167
|
*/
|
|
@@ -1340,287 +2192,40 @@ export const createMutexRef = (initialValue) => {
|
|
|
1340
2192
|
return ok(previousValue);
|
|
1341
2193
|
}),
|
|
1342
2194
|
updateAndGet: (updater) => mutex.withLock(async (run) => {
|
|
1343
|
-
const
|
|
1344
|
-
const nextValue = await run(updater(currentValue));
|
|
2195
|
+
const nextValue = await run(updater(ref.get()));
|
|
1345
2196
|
if (!nextValue.ok)
|
|
1346
2197
|
return nextValue;
|
|
1347
2198
|
ref.set(nextValue.value);
|
|
1348
2199
|
return ok(nextValue.value);
|
|
1349
2200
|
}),
|
|
1350
2201
|
modify: (modifier) => mutex.withLock(async (run) => {
|
|
1351
|
-
const
|
|
1352
|
-
if (!
|
|
1353
|
-
return
|
|
1354
|
-
const [
|
|
1355
|
-
ref.set(
|
|
1356
|
-
return ok(
|
|
2202
|
+
const result = await run(modifier(ref.get()));
|
|
2203
|
+
if (!result.ok)
|
|
2204
|
+
return result;
|
|
2205
|
+
const [value, nextValue] = result.value;
|
|
2206
|
+
ref.set(nextValue);
|
|
2207
|
+
return ok(value);
|
|
1357
2208
|
}),
|
|
1358
|
-
|
|
2209
|
+
snapshot: mutex.snapshot,
|
|
1359
2210
|
};
|
|
1360
2211
|
};
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* Abort reason used by {@link allSettled} when aborted externally.
|
|
1383
|
-
*
|
|
1384
|
-
* @group Composition
|
|
1385
|
-
*/
|
|
1386
|
-
export const AllSettledAbortError = /*#__PURE__*/ typed("AllSettledAbortError");
|
|
1387
|
-
/**
|
|
1388
|
-
* {@link AllSettledAbortError} used as abort reason in {@link allSettled}.
|
|
1389
|
-
*
|
|
1390
|
-
* @group Composition
|
|
1391
|
-
*/
|
|
1392
|
-
export const allSettledAbortError = {
|
|
1393
|
-
type: "AllSettledAbortError",
|
|
1394
|
-
};
|
|
1395
|
-
export function map(items, fn, { abortReason = mapAbortError, ...options } = {}) {
|
|
1396
|
-
const mapped = mapInput(items, fn);
|
|
1397
|
-
return all(mapped, {
|
|
1398
|
-
...options,
|
|
1399
|
-
abortReason,
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Abort reason used by {@link map} when aborting remaining tasks.
|
|
1404
|
-
*
|
|
1405
|
-
* @group Composition
|
|
1406
|
-
*/
|
|
1407
|
-
export const MapAbortError = /*#__PURE__*/ typed("MapAbortError");
|
|
1408
|
-
/**
|
|
1409
|
-
* {@link MapAbortError} used as abort reason in {@link map}.
|
|
1410
|
-
*
|
|
1411
|
-
* @group Composition
|
|
1412
|
-
*/
|
|
1413
|
-
export const mapAbortError = {
|
|
1414
|
-
type: "MapAbortError",
|
|
1415
|
-
};
|
|
1416
|
-
export function mapSettled(items, task, options) {
|
|
1417
|
-
const mapped = mapInput(items, task);
|
|
1418
|
-
return allSettled(mapped, options);
|
|
1419
|
-
}
|
|
1420
|
-
export function any(tasks, options) {
|
|
1421
|
-
const { allFailed = "input" } = options ?? {};
|
|
1422
|
-
return pool(tasks, {
|
|
1423
|
-
stopOn: "success",
|
|
1424
|
-
collect: false,
|
|
1425
|
-
abortReason: anyAbortError,
|
|
1426
|
-
allFailed,
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Abort reason used by {@link any} when aborting remaining tasks.
|
|
1431
|
-
*
|
|
1432
|
-
* @group Composition
|
|
1433
|
-
*/
|
|
1434
|
-
export const AnyAbortError = /*#__PURE__*/ typed("AnyAbortError");
|
|
1435
|
-
/**
|
|
1436
|
-
* {@link AnyAbortError} used as abort reason in {@link any}.
|
|
1437
|
-
*
|
|
1438
|
-
* @group Composition
|
|
1439
|
-
*/
|
|
1440
|
-
export const anyAbortError = { type: "AnyAbortError" };
|
|
1441
|
-
/** Shared implementation for {@link all} and {@link allSettled}. */
|
|
1442
|
-
const collect = (type, input, { collect = true, abortReason = type === "all" ? allAbortError : allSettledAbortError, } = {}) => {
|
|
1443
|
-
const stopOn = type === "all" ? "error" : null;
|
|
1444
|
-
if (isIterable(input)) {
|
|
1445
|
-
const array = arrayFrom(input);
|
|
1446
|
-
if (!isNonEmptyArray(array))
|
|
1447
|
-
return () => ok(collect ? emptyArray : undefined);
|
|
1448
|
-
return pool(array, {
|
|
1449
|
-
stopOn,
|
|
1450
|
-
collect,
|
|
1451
|
-
abortReason,
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
const keys = [];
|
|
1455
|
-
const taskArray = [];
|
|
1456
|
-
for (const key in input) {
|
|
1457
|
-
keys.push(key);
|
|
1458
|
-
taskArray.push(input[key]);
|
|
1459
|
-
}
|
|
1460
|
-
if (keys.length === 0)
|
|
1461
|
-
return () => ok(collect ? emptyRecord : undefined);
|
|
1462
|
-
return async (run) => {
|
|
1463
|
-
const result = await run(pool(taskArray, { stopOn, collect, abortReason }));
|
|
1464
|
-
if (!result.ok)
|
|
1465
|
-
return result;
|
|
1466
|
-
if (!collect)
|
|
1467
|
-
return ok();
|
|
1468
|
-
const record = createRecord();
|
|
1469
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1470
|
-
record[keys[i]] = result.value[i];
|
|
1471
|
-
}
|
|
1472
|
-
return ok(record);
|
|
1473
|
-
};
|
|
1474
|
-
};
|
|
1475
|
-
const mapInput = (input, fn) => isIterable(input) ? mapArray(arrayFrom(input), fn) : mapObject(input, fn);
|
|
1476
|
-
function pool(tasksIterable, { stopOn = null, collect, abortReason, allFailed, }) {
|
|
1477
|
-
const tasks = arrayFrom(tasksIterable);
|
|
1478
|
-
const { length } = tasks;
|
|
1479
|
-
if (length === 0)
|
|
1480
|
-
return () => ok(emptyArray);
|
|
1481
|
-
return async (run) => {
|
|
1482
|
-
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
1483
|
-
try {
|
|
1484
|
-
const results = collect ? new Array(length) : null;
|
|
1485
|
-
const aborted = Promise.withResolvers();
|
|
1486
|
-
const stopSignal = stopOn ? Promise.withResolvers() : null;
|
|
1487
|
-
let index = 0;
|
|
1488
|
-
let stopped = null;
|
|
1489
|
-
let lastResult = null;
|
|
1490
|
-
let lastIndexResult = null;
|
|
1491
|
-
const worker = async (run) => {
|
|
1492
|
-
while (index < length && (stopOn ? !stopped : true)) {
|
|
1493
|
-
const i = index++;
|
|
1494
|
-
const result = (await run(tasks[i]));
|
|
1495
|
-
lastResult = result;
|
|
1496
|
-
if (i === length - 1)
|
|
1497
|
-
lastIndexResult = result;
|
|
1498
|
-
if (!stopOn) {
|
|
1499
|
-
if (results)
|
|
1500
|
-
results[i] = result;
|
|
1501
|
-
continue;
|
|
1502
|
-
}
|
|
1503
|
-
const stop = stopOn === "first" ||
|
|
1504
|
-
(stopOn === "error" && !result.ok) ||
|
|
1505
|
-
(stopOn === "success" && result.ok);
|
|
1506
|
-
if (!stop) {
|
|
1507
|
-
if (results)
|
|
1508
|
-
results[i] = result.value;
|
|
1509
|
-
continue;
|
|
1510
|
-
}
|
|
1511
|
-
if (!stopped) {
|
|
1512
|
-
stopped = result;
|
|
1513
|
-
abortWorkers(!result.ok && AbortError.is(result.error)
|
|
1514
|
-
? result.error.reason
|
|
1515
|
-
: abortReason);
|
|
1516
|
-
stopSignal?.resolve();
|
|
1517
|
-
}
|
|
1518
|
-
break;
|
|
1519
|
-
}
|
|
1520
|
-
return ok();
|
|
1521
|
-
};
|
|
1522
|
-
let workersAborted = false;
|
|
1523
|
-
const abortWorkers = (reason) => {
|
|
1524
|
-
if (workersAborted)
|
|
1525
|
-
return;
|
|
1526
|
-
workersAborted = true;
|
|
1527
|
-
for (const worker of workers)
|
|
1528
|
-
worker.abort(reason);
|
|
1529
|
-
};
|
|
1530
|
-
const workerCount = Math.min(run.concurrency, length);
|
|
1531
|
-
const workers = arrayFrom(workerCount, () => run.daemon(worker));
|
|
1532
|
-
const _ = __addDisposableResource(env_8, new DisposableStack(), false);
|
|
1533
|
-
_.defer(() => {
|
|
1534
|
-
abortWorkers(abortReason);
|
|
1535
|
-
});
|
|
1536
|
-
run.onAbort((reason) => {
|
|
1537
|
-
abortWorkers(reason);
|
|
1538
|
-
aborted.resolve();
|
|
1539
|
-
});
|
|
1540
|
-
const waitFor = [Promise.all(workers), aborted.promise];
|
|
1541
|
-
if (stopSignal)
|
|
1542
|
-
waitFor.push(stopSignal.promise);
|
|
1543
|
-
await Promise.race(waitFor);
|
|
1544
|
-
if (run.signal.aborted) {
|
|
1545
|
-
return err(run.signal.reason);
|
|
1546
|
-
}
|
|
1547
|
-
if (!stopOn)
|
|
1548
|
-
return results ? ok(results) : ok();
|
|
1549
|
-
if (stopped)
|
|
1550
|
-
return stopped;
|
|
1551
|
-
if (results)
|
|
1552
|
-
return ok(results);
|
|
1553
|
-
// For all/allSettled/map/mapSettled with collect: false (no allFailed handler)
|
|
1554
|
-
if (!allFailed)
|
|
1555
|
-
return ok();
|
|
1556
|
-
return allFailed === "completion" ? lastResult : lastIndexResult;
|
|
1557
|
-
}
|
|
1558
|
-
catch (e_8) {
|
|
1559
|
-
env_8.error = e_8;
|
|
1560
|
-
env_8.hasError = true;
|
|
1561
|
-
}
|
|
1562
|
-
finally {
|
|
1563
|
-
__disposeResources(env_8);
|
|
1564
|
-
}
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Error returned when a {@link fetch} {@link Task} fails.
|
|
1569
|
-
*
|
|
1570
|
-
* @group Composition
|
|
1571
|
-
*/
|
|
1572
|
-
export const FetchError = /*#__PURE__*/ typed("FetchError", { error: Unknown });
|
|
1573
|
-
/**
|
|
1574
|
-
* Creates a {@link Task} that wraps the native
|
|
1575
|
-
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}.
|
|
1576
|
-
*
|
|
1577
|
-
* Handles cross-browser abort behavior — WebKit throws a `DOMException` with
|
|
1578
|
-
* message "Fetch is aborted" instead of propagating `signal.reason`. This
|
|
1579
|
-
* helper normalizes the behavior to always return {@link AbortError}.
|
|
1580
|
-
*
|
|
1581
|
-
* ### Example
|
|
1582
|
-
*
|
|
1583
|
-
* ```ts
|
|
1584
|
-
* await using run = createRun();
|
|
1585
|
-
*
|
|
1586
|
-
* const result = await run(fetch("https://api.example.com/users"));
|
|
1587
|
-
*
|
|
1588
|
-
* if (!result.ok) {
|
|
1589
|
-
* // Handle FetchError or AbortError
|
|
1590
|
-
* }
|
|
1591
|
-
*
|
|
1592
|
-
* // Compose with timeout and retry
|
|
1593
|
-
* const fetchWithRetry = (url: string) =>
|
|
1594
|
-
* retry(timeout(fetch(url), "10s"), retryStrategyAws);
|
|
1595
|
-
* ```
|
|
1596
|
-
*
|
|
1597
|
-
* @group Composition
|
|
1598
|
-
*/
|
|
1599
|
-
export const fetch = (input, init) => ({ signal }) => tryAsync(() => globalThis.fetch(input, { ...init, signal }), (error) => {
|
|
1600
|
-
if (AbortError.is(error))
|
|
1601
|
-
return error;
|
|
1602
|
-
// WebKit throws DOMException with message "Fetch is aborted" instead of
|
|
1603
|
-
// propagating signal.reason. Detect this and create AbortError.
|
|
1604
|
-
if (signal.aborted &&
|
|
1605
|
-
error instanceof Error &&
|
|
1606
|
-
error.message === "Fetch is aborted") {
|
|
1607
|
-
return createAbortError(signal.reason);
|
|
1608
|
-
}
|
|
1609
|
-
return { type: "FetchError", error };
|
|
1610
|
-
});
|
|
1611
|
-
// TODO: Prioritized Task Scheduling API integration
|
|
1612
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
|
|
1613
|
-
//
|
|
1614
|
-
// - `run(task, { priority })` - Fiber-level priority via TaskController
|
|
1615
|
-
// (extends AbortController), so the fiber's signal gets priority and all
|
|
1616
|
-
// nested work inherits it.
|
|
1617
|
-
// - `fiber.setPriority(priority)` - Dynamic priority changes mid-flight via
|
|
1618
|
-
// TaskController.setPriority().
|
|
1619
|
-
//
|
|
1620
|
-
// Note: scheduler.yield() inherits priority from enclosing postTask, so
|
|
1621
|
-
// yieldNow doesn't need a priority argument — just run the fiber at priority.
|
|
1622
|
-
//
|
|
1623
|
-
// Safari doesn't support it yet, Node.js probably never will (use setImmediate).
|
|
1624
|
-
// For Safari, scheduler-polyfill can be used.
|
|
1625
|
-
// https://www.npmjs.com/package/scheduler-polyfill
|
|
1626
|
-
// TODO: Do we really need specialized aborts?
|
|
2212
|
+
// TODO: Add Run observability after Task migration.
|
|
2213
|
+
// - Structured logging with levels, inherited log annotations, JSON output,
|
|
2214
|
+
// filtering, and pluggable log sinks.
|
|
2215
|
+
// - Tracing spans with names, timing, parent-child relationships, attributes,
|
|
2216
|
+
// error status, and helpers for annotating the current or child spans.
|
|
2217
|
+
// - Metrics for counters, gauges, histograms, and operation durations.
|
|
2218
|
+
// - Resource metadata for service name, service version, deployment
|
|
2219
|
+
// environment, and user-provided attributes.
|
|
2220
|
+
// - Exporters for production telemetry backends, including OTLP-compatible
|
|
2221
|
+
// logs, traces, and metrics.
|
|
2222
|
+
// - Transferable snapshots for local and worker monitoring. Current snapshots
|
|
2223
|
+
// can contain arbitrary Result values and Error objects, so serialization must
|
|
2224
|
+
// replace non-transferable values with safe diagnostics.
|
|
2225
|
+
// - Incremental snapshot invalidation so polling unchanged large Run trees
|
|
2226
|
+
// avoids recursive traversal and child-array allocation.
|
|
2227
|
+
// - Run labels and structured annotations for rendering useful snapshot trees
|
|
2228
|
+
// instead of anonymous ids.
|
|
2229
|
+
// - Snapshot and trace views should preserve ownership boundaries, so reusable
|
|
2230
|
+
// resources and long-lived operations appear as labeled subtrees instead of
|
|
2231
|
+
// unrelated child operations.
|