@evolu/common 8.0.0-next.5 → 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 +39 -44
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +38 -31
- 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 +3 -3
- package/dist/src/Error.d.ts.map +1 -1
- 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 +39 -45
- 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 +3428 -1629
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1876 -1279
- package/dist/src/Test.d.ts +17 -124
- 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 -100
- 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 +107 -130
- 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 +85 -42
- 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 +48 -68
- 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 +5 -2
- 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 +142 -137
- package/src/StackTrace.ts +230 -0
- package/src/Store.ts +8 -7
- package/src/String.ts +22 -5
- package/src/Task.ts +5109 -2845
- package/src/Test.ts +20 -167
- package/src/Time.ts +293 -90
- package/src/Type.ts +10034 -4117
- package/src/Types.ts +227 -101
- 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 +161 -190
- 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 +253 -214
- 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,1268 +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 runDefaultDeps = {
|
|
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 RunDefaultDeps} 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
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
};
|
|
280
|
+
};
|
|
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");
|
|
249
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.
|
|
250
315
|
const requestController = new AbortController();
|
|
251
316
|
const signalController = new AbortController();
|
|
252
|
-
let
|
|
253
|
-
let
|
|
254
|
-
let
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
260
327
|
};
|
|
261
|
-
if (parent) {
|
|
262
|
-
subscribeToAbort(parent.requestSignal, () => requestAbort(parent.requestSignal.reason), { signal: requestController.signal });
|
|
263
|
-
}
|
|
264
328
|
const emitEvent = (data) => {
|
|
265
|
-
|
|
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
|
+
}
|
|
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)
|
|
266
366
|
return;
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
367
|
+
const abortError = createAbortError(reason);
|
|
368
|
+
requestController.abort(abortError);
|
|
369
|
+
if (abortMask === abortableMask)
|
|
370
|
+
signalController.abort(abortError);
|
|
371
|
+
commitState({ type: "Aborted", abort: currentAbort() });
|
|
372
|
+
};
|
|
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
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
disposePromise = Promise.all(children.values()).then(settle);
|
|
270
404
|
}
|
|
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;
|
|
271
412
|
};
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
...{
|
|
276
421
|
console: deps.console,
|
|
422
|
+
leakDetector: deps.leakDetector,
|
|
423
|
+
nativeFetch: deps.nativeFetch,
|
|
277
424
|
randomBytes: deps.randomBytes,
|
|
278
425
|
random: deps.random,
|
|
426
|
+
reportDefect: deps.reportDefect,
|
|
279
427
|
time: deps.time,
|
|
280
|
-
...(deps.runConfig && {
|
|
281
|
-
runConfig: deps.runConfig,
|
|
282
|
-
}),
|
|
283
|
-
...taskDeps,
|
|
284
|
-
}, self, daemon ?? self, getAbortBehavior(task), getConcurrencyBehavior(task));
|
|
285
|
-
if (state !== running) {
|
|
286
|
-
childRun.requestAbort(runStoppedAbortError);
|
|
287
|
-
task = () => err(runStoppedAbortError);
|
|
288
|
-
}
|
|
289
|
-
else if (signalController.signal.aborted &&
|
|
290
|
-
childRun.abortMask === isAbortable) {
|
|
291
|
-
const abortError = signalController.signal.reason;
|
|
292
|
-
childRun.requestAbort(abortError);
|
|
293
|
-
task = () => err(abortError);
|
|
294
|
-
}
|
|
295
|
-
const childFiber = Object.assign(Promise.try(task, childRun)
|
|
296
|
-
.then(childRun.handleTaskFulfilled)
|
|
297
|
-
.finally(childRun.handleTaskSettled)
|
|
298
|
-
.finally(childRun[Symbol.asyncDispose])
|
|
299
|
-
.finally(() => {
|
|
300
|
-
children = deleteFromSet(children, childFiber);
|
|
301
|
-
emitEvent({ type: "ChildRemoved", childId: childRun.id });
|
|
302
|
-
}), {
|
|
303
|
-
run: childRun,
|
|
304
|
-
abort: (reason) => {
|
|
305
|
-
childRun.requestAbort(createAbortError(reason));
|
|
306
|
-
},
|
|
307
|
-
getState: () => childRun.getState(),
|
|
308
|
-
[Symbol.dispose]: () => {
|
|
309
|
-
childFiber.abort();
|
|
428
|
+
...(deps.runConfig && { runConfig: deps.runConfig }),
|
|
310
429
|
},
|
|
311
|
-
|
|
312
|
-
children = addToSet(children, childFiber);
|
|
313
|
-
emitEvent({ type: "ChildAdded", childId: childRun.id });
|
|
314
|
-
return childFiber;
|
|
315
|
-
};
|
|
316
|
-
let snapshot = null;
|
|
317
|
-
let disposingPromise = null;
|
|
318
|
-
const self = run;
|
|
319
|
-
{
|
|
320
|
-
const run = self;
|
|
321
|
-
const id = createId(deps);
|
|
322
|
-
async function orThrow(task, taskDeps) {
|
|
323
|
-
const result = taskDeps === undefined ? await self(task) : await self(task, taskDeps);
|
|
324
|
-
return getOrThrow(result);
|
|
325
|
-
}
|
|
326
|
-
run.orThrow = orThrow;
|
|
327
|
-
run.id = id;
|
|
328
|
-
run.parent = parent ?? null;
|
|
329
|
-
run.signal = signalController.signal;
|
|
330
|
-
run.abortMask = abortMask;
|
|
331
|
-
run.onAbort = (callback) => {
|
|
332
|
-
if (abortMask !== isAbortable)
|
|
333
|
-
return;
|
|
334
|
-
subscribeToAbort(signalController.signal, () => callback(signalController.signal.reason.reason), { once: true, signal: requestController.signal });
|
|
335
|
-
};
|
|
336
|
-
run.getState = () => state;
|
|
337
|
-
run.getChildren = () => children;
|
|
338
|
-
run.snapshot = () => {
|
|
339
|
-
const childSnapshots = Array.from(children).map((fiber) => fiber.run.snapshot());
|
|
340
|
-
if (snapshot?.state !== state ||
|
|
341
|
-
!eqArrayStrict(snapshot.children, childSnapshots)) {
|
|
342
|
-
snapshot = {
|
|
343
|
-
id,
|
|
344
|
-
state,
|
|
345
|
-
children: childSnapshots,
|
|
346
|
-
abortMask,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
return snapshot;
|
|
430
|
+
...taskDeps,
|
|
350
431
|
};
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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);
|
|
356
445
|
};
|
|
357
|
-
run.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
state = { type: "Disposing" };
|
|
364
|
-
emitEvent({ type: "StateChanged", state });
|
|
365
|
-
requestAbort(runStoppedAbortError);
|
|
366
|
-
disposingPromise = Promise.allSettled((self.ownTaskSettled
|
|
367
|
-
? [self.ownTaskSettled.promise, ...children]
|
|
368
|
-
: children))
|
|
369
|
-
.then(lazyVoid)
|
|
370
|
-
.finally(() => {
|
|
371
|
-
/**
|
|
372
|
-
* Root and daemon Runs have no own Task, so `run.handleTaskFulfilled`
|
|
373
|
-
* never populates their terminal values. In that case disposal
|
|
374
|
-
* publishes `ok()` for both `result` and `outcome`. Task-backed Runs
|
|
375
|
-
* normally reach this point with both values already set.
|
|
376
|
-
*/
|
|
377
|
-
[result, outcome] = [result ?? ok(), outcome ?? ok()];
|
|
378
|
-
state = { type: "Settled", result, outcome };
|
|
379
|
-
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,
|
|
380
452
|
});
|
|
381
|
-
|
|
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;
|
|
382
511
|
};
|
|
383
|
-
|
|
384
|
-
run
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
}
|
|
394
555
|
};
|
|
395
|
-
run.
|
|
396
|
-
|
|
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
|
+
},
|
|
397
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]]));
|
|
398
660
|
}
|
|
399
|
-
|
|
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());
|
|
400
677
|
};
|
|
401
|
-
const
|
|
678
|
+
const mapInput = (input, fn) => Array.isArray(input)
|
|
679
|
+
? mapArray(input, (value, index) => fn(value, index))
|
|
680
|
+
: mapObject(input, fn);
|
|
402
681
|
/**
|
|
403
|
-
*
|
|
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.
|
|
717
|
+
*
|
|
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
|
+
* );
|
|
404
733
|
*
|
|
405
|
-
*
|
|
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
|
+
* ```
|
|
406
742
|
*
|
|
407
|
-
* @group
|
|
743
|
+
* @group Interop
|
|
408
744
|
*/
|
|
409
|
-
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
|
+
};
|
|
410
756
|
/**
|
|
411
|
-
*
|
|
412
|
-
*
|
|
757
|
+
* Pauses execution for a specified {@link PositiveDuration}.
|
|
758
|
+
*
|
|
759
|
+
* Aborting the Task clears the scheduled timeout.
|
|
413
760
|
*
|
|
414
|
-
* @group
|
|
761
|
+
* @group Timing
|
|
415
762
|
*/
|
|
416
|
-
export const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const createAbortError = (reason) => ({
|
|
420
|
-
type: "AbortError",
|
|
421
|
-
reason,
|
|
422
|
-
});
|
|
423
|
-
const subscribeToAbort = (signal, handler, options) => {
|
|
424
|
-
if (signal.aborted)
|
|
425
|
-
handler();
|
|
426
|
-
else
|
|
427
|
-
signal.addEventListener("abort", handler, options);
|
|
428
|
-
};
|
|
429
|
-
const runStoppedAbortError = createAbortError(runStoppedError);
|
|
430
|
-
const isAbortable = AbortMask.orThrow(0);
|
|
431
|
-
const abortBehaviorSymbol = Symbol("evolu.Task.abortBehavior");
|
|
432
|
-
const getAbortBehavior = (task) => task[abortBehaviorSymbol];
|
|
433
|
-
const abortBehavior = (behavior) => (task) => Object.assign((run) => run(task), {
|
|
434
|
-
[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);
|
|
435
766
|
});
|
|
436
767
|
/**
|
|
437
|
-
*
|
|
768
|
+
* Typed error returned by {@link timeout} when a Task exceeds its duration.
|
|
438
769
|
*
|
|
439
|
-
*
|
|
440
|
-
|
|
770
|
+
* @group Timing
|
|
771
|
+
*/
|
|
772
|
+
export const TimeoutError =
|
|
773
|
+
/*#__PURE__*/ typed("TimeoutError");
|
|
774
|
+
/**
|
|
775
|
+
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
441
776
|
*
|
|
442
|
-
*
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
*
|
|
777
|
+
* @group Timing
|
|
778
|
+
*/
|
|
779
|
+
export const timeoutError = { type: "TimeoutError" };
|
|
780
|
+
/**
|
|
781
|
+
* Limits how long a {@link Task} may run.
|
|
447
782
|
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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)`.
|
|
452
788
|
*
|
|
453
789
|
* ### Example
|
|
454
790
|
*
|
|
455
791
|
* ```ts
|
|
792
|
+
* import {
|
|
793
|
+
* createRun,
|
|
794
|
+
* timeout,
|
|
795
|
+
* timeoutError,
|
|
796
|
+
* waitForAbort,
|
|
797
|
+
* type Result,
|
|
798
|
+
* type TimeoutError,
|
|
799
|
+
* } from "@evolu/common";
|
|
800
|
+
*
|
|
456
801
|
* await using run = createRun();
|
|
457
802
|
*
|
|
458
|
-
* const
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* // Simulate async analytics API (abortable by default)
|
|
463
|
-
* const sendToAnalytics =
|
|
464
|
-
* (event: number): Task<void, never> =>
|
|
465
|
-
* async ({ signal }) => {
|
|
466
|
-
* await canComplete.promise;
|
|
467
|
-
* signalAbortedInAnalytics = signal.aborted;
|
|
468
|
-
* events.push(`sent ${event}`);
|
|
469
|
-
* return ok();
|
|
470
|
-
* };
|
|
803
|
+
* const result = await run(timeout(waitForAbort, "1ms"));
|
|
804
|
+
* expectTypeOf(result).toEqualTypeOf<Result<never, TimeoutError>>();
|
|
805
|
+
* expectErr(result, timeoutError);
|
|
806
|
+
* ```
|
|
471
807
|
*
|
|
472
|
-
*
|
|
473
|
-
|
|
474
|
-
|
|
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}.
|
|
475
819
|
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
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.
|
|
478
823
|
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
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`.
|
|
482
830
|
*
|
|
483
|
-
*
|
|
831
|
+
* AbortError recognition is structural. Domain errors, especially values parsed
|
|
832
|
+
* from untrusted input, must not use the reserved AbortError shape.
|
|
484
833
|
*
|
|
485
|
-
*
|
|
486
|
-
* // Analytics was sent despite abort
|
|
487
|
-
* expect(events).toEqual(["sent 123"]);
|
|
488
|
-
* expect(result).toEqual(ok());
|
|
489
|
-
* ```
|
|
834
|
+
* ### Example
|
|
490
835
|
*
|
|
491
|
-
*
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*
|
|
497
|
-
*
|
|
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";
|
|
498
846
|
*
|
|
499
|
-
*
|
|
847
|
+
* await using run = createRun();
|
|
500
848
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
849
|
+
* interface FetchDataError {
|
|
850
|
+
* readonly type: "FetchDataError";
|
|
851
|
+
* }
|
|
503
852
|
*
|
|
504
|
-
*
|
|
505
|
-
|
|
506
|
-
export const unabortableMask = (fn) => unabortable((run) => fn(abortBehavior(AbortMask.orThrow(decrement(run.abortMask))))(run));
|
|
507
|
-
const defaultConcurrency = 1;
|
|
508
|
-
const concurrencyBehaviorSymbol = Symbol("evolu.Task.concurrencyBehavior");
|
|
509
|
-
const getConcurrencyBehavior = (task) => task[concurrencyBehaviorSymbol];
|
|
510
|
-
export function concurrently(concurrencyOrTask, taskOrFallback) {
|
|
511
|
-
const isTask = isFunction(concurrencyOrTask);
|
|
512
|
-
const task = isTask ? concurrencyOrTask : taskOrFallback;
|
|
513
|
-
return Object.assign((run) => run(task), {
|
|
514
|
-
[concurrencyBehaviorSymbol]: isTask ? maxPositiveInt : concurrencyOrTask,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Yields execution to allow other work to proceed.
|
|
853
|
+
* const fetchData: Task<string, FetchDataError> = () =>
|
|
854
|
+
* err({ type: "FetchDataError" });
|
|
519
855
|
*
|
|
520
|
-
*
|
|
521
|
-
* UI unresponsive (user interactions, animations). In Node.js, it prevents I/O
|
|
522
|
-
* callbacks, timers, and other requests from being handled. Inserting yield
|
|
523
|
-
* points lets the runtime process high-priority work between chunks of code.
|
|
856
|
+
* const fetchWithRetry = retry(fetchData, recurs(2));
|
|
524
857
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
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
|
+
* ```
|
|
527
868
|
*
|
|
528
869
|
* ### Example
|
|
529
870
|
*
|
|
530
871
|
* ```ts
|
|
531
|
-
*
|
|
532
|
-
* const { time } = run.deps;
|
|
533
|
-
* let lastYield = time.now();
|
|
534
|
-
*
|
|
535
|
-
* for (const item of largeArray) {
|
|
536
|
-
* processItem(item);
|
|
537
|
-
*
|
|
538
|
-
* // Yield periodically to keep UI responsive
|
|
539
|
-
* if (time.now() - lastYield > msLongTask) {
|
|
540
|
-
* const r = await run(yieldNow);
|
|
541
|
-
* if (!r.ok) return r;
|
|
542
|
-
* lastYield = time.now();
|
|
543
|
-
* }
|
|
544
|
-
* }
|
|
545
|
-
*
|
|
546
|
-
* return ok();
|
|
547
|
-
* };
|
|
548
|
-
* ```
|
|
872
|
+
* import { createRun, err, recurs, retry, type Task } from "@evolu/common";
|
|
549
873
|
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
874
|
+
* interface FetchDataError {
|
|
875
|
+
* readonly type: "RecoverableError" | "FatalError";
|
|
876
|
+
* }
|
|
552
877
|
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* (count: number, index: number, sum: number): Task<number> =>
|
|
556
|
-
* async (run) => {
|
|
557
|
-
* if (index >= count) return ok(sum);
|
|
878
|
+
* const fetchData: Task<string, FetchDataError> = () =>
|
|
879
|
+
* err({ type: "FatalError" });
|
|
558
880
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* if (!y.ok) return y;
|
|
563
|
-
* }
|
|
881
|
+
* const fetchWithRetry = retry(fetchData, recurs(5), {
|
|
882
|
+
* shouldRetry: (error) => error.type !== "FatalError",
|
|
883
|
+
* });
|
|
564
884
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
885
|
+
* await using run = createRun();
|
|
886
|
+
* expectErr(await run(fetchWithRetry), {
|
|
887
|
+
* type: "RetryError",
|
|
888
|
+
* attempts: 1,
|
|
889
|
+
* lastError: { type: "FatalError" },
|
|
890
|
+
* });
|
|
568
891
|
* ```
|
|
569
892
|
*
|
|
570
|
-
* @group
|
|
571
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield
|
|
572
|
-
* @see https://web.dev/articles/optimize-long-tasks
|
|
893
|
+
* @group Resilience
|
|
573
894
|
*/
|
|
574
|
-
export const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
+
};
|
|
582
922
|
/**
|
|
583
|
-
*
|
|
923
|
+
* Repeats a Task according to a {@link Schedule}.
|
|
584
924
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
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.
|
|
587
930
|
*
|
|
588
|
-
*
|
|
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}.
|
|
589
935
|
*
|
|
590
|
-
*
|
|
591
|
-
* Start the promise work inside the callback, and aborting the Task stops
|
|
592
|
-
* waiting for it immediately.
|
|
936
|
+
* With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
|
|
593
937
|
*
|
|
594
938
|
* ### Example
|
|
595
939
|
*
|
|
596
940
|
* ```ts
|
|
597
|
-
*
|
|
598
|
-
* const sleep = (duration: Duration): Task<void> =>
|
|
599
|
-
* callback(({ ok, deps: { time } }) => {
|
|
600
|
-
* const id = time.setTimeout(ok, durationToMillis(duration));
|
|
601
|
-
* return () => time.clearTimeout(id);
|
|
602
|
-
* });
|
|
941
|
+
* import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
|
|
603
942
|
*
|
|
604
|
-
*
|
|
605
|
-
* const
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
943
|
+
* let attempts = 0;
|
|
944
|
+
* const checkStatus: Task<string> = () => {
|
|
945
|
+
* attempts += 1;
|
|
946
|
+
* return ok("pending");
|
|
947
|
+
* };
|
|
609
948
|
*
|
|
610
|
-
*
|
|
611
|
-
* const readFile = (path: string): Task<string, NodeJS.ErrnoException> =>
|
|
612
|
-
* callback(({ ok, err }) => {
|
|
613
|
-
* fs.readFile(path, "utf8", (error, data) => {
|
|
614
|
-
* if (error) err(error);
|
|
615
|
-
* else ok(data);
|
|
616
|
-
* });
|
|
617
|
-
* });
|
|
949
|
+
* const poll = repeat(checkStatus, recurs(3));
|
|
618
950
|
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* callback(({ ok, err }) => {
|
|
623
|
-
* void promiseApi().then(ok, err);
|
|
624
|
-
* });
|
|
951
|
+
* await using run = createRun();
|
|
952
|
+
* expectOk(await run(poll), "pending");
|
|
953
|
+
* expect(attempts).toBe(4);
|
|
625
954
|
* ```
|
|
626
955
|
*
|
|
627
|
-
* @group Composition
|
|
628
|
-
*/
|
|
629
|
-
export const callback = (callback) => (run) => new Promise((resolve) => {
|
|
630
|
-
const teardown = callback({
|
|
631
|
-
ok: (value) => resolve(ok(value)),
|
|
632
|
-
err: (error) => resolve(err(error)),
|
|
633
|
-
signal: run.signal,
|
|
634
|
-
deps: run.deps,
|
|
635
|
-
});
|
|
636
|
-
run.onAbort((reason) => {
|
|
637
|
-
if (teardown)
|
|
638
|
-
teardown();
|
|
639
|
-
resolve(err(createAbortError(reason)));
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
/**
|
|
643
|
-
* Pauses execution for a specified duration.
|
|
644
|
-
*
|
|
645
956
|
* ### Example
|
|
646
957
|
*
|
|
647
958
|
* ```ts
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
959
|
+
* import {
|
|
960
|
+
* createRun,
|
|
961
|
+
* done,
|
|
962
|
+
* err,
|
|
963
|
+
* ok,
|
|
964
|
+
* repeat,
|
|
965
|
+
* spaced,
|
|
966
|
+
* type NextTask,
|
|
967
|
+
* } from "@evolu/common";
|
|
655
968
|
*
|
|
656
|
-
*
|
|
657
|
-
*/
|
|
658
|
-
export const sleep = (duration) => callback(({ ok, deps: { time } }) => {
|
|
659
|
-
const id = time.setTimeout(ok, durationToMillis(duration));
|
|
660
|
-
return () => time.clearTimeout(id);
|
|
661
|
-
});
|
|
662
|
-
/**
|
|
663
|
-
* Returns a {@link Task} that completes first.
|
|
969
|
+
* await using run = createRun();
|
|
664
970
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* are aborted. Use {@link any} if you need the first Task to succeed instead.
|
|
971
|
+
* interface Item {
|
|
972
|
+
* readonly id: string;
|
|
973
|
+
* }
|
|
669
974
|
*
|
|
670
|
-
*
|
|
671
|
-
* (there's no "first to complete" without participants). This is enforced at
|
|
672
|
-
* compile time for non-empty tuple types. For other arrays, guard with
|
|
673
|
-
* {@link isNonEmptyArray}:
|
|
975
|
+
* const queue: Array<Item> = [{ id: "item-1" }];
|
|
674
976
|
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* }
|
|
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([]);
|
|
679
985
|
* ```
|
|
680
986
|
*
|
|
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
|
+
*
|
|
681
1026
|
* ### Example
|
|
682
1027
|
*
|
|
683
1028
|
* ```ts
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
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");
|
|
688
1044
|
* };
|
|
689
1045
|
*
|
|
690
|
-
*
|
|
691
|
-
* const result = await run(
|
|
692
|
-
* ```
|
|
1046
|
+
* await using run = createRun();
|
|
1047
|
+
* const result = await run(any([unavailable, fallback]));
|
|
693
1048
|
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
1049
|
+
* expectTypeOf(result).toEqualTypeOf<Result<string, "Unavailable">>();
|
|
1050
|
+
* expectOk(result, "fallback");
|
|
1051
|
+
* expect(fallbackStarted).toBe(true);
|
|
1052
|
+
* ```
|
|
696
1053
|
*
|
|
697
|
-
* @group
|
|
1054
|
+
* @group Racing
|
|
698
1055
|
*/
|
|
699
|
-
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
|
+
};
|
|
700
1076
|
/**
|
|
701
|
-
*
|
|
1077
|
+
* Runs Tasks until the first Task settles.
|
|
702
1078
|
*
|
|
703
|
-
* @
|
|
704
|
-
|
|
705
|
-
export const RaceLostError = /*#__PURE__*/ typed("RaceLostError");
|
|
706
|
-
/**
|
|
707
|
-
* {@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}.
|
|
708
1081
|
*
|
|
709
|
-
* @
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Wraps a {@link Task} with a time limit.
|
|
1082
|
+
* Use {@link any} to wait for the first Ok instead.
|
|
1083
|
+
*
|
|
1084
|
+
* Losing Tasks are aborted.
|
|
714
1085
|
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
1086
|
+
* Tasks always run concurrently because racing sequentially would be
|
|
1087
|
+
* meaningless.
|
|
1088
|
+
*
|
|
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}:
|
|
717
1097
|
*
|
|
718
1098
|
* ### Example
|
|
719
1099
|
*
|
|
720
1100
|
* ```ts
|
|
721
|
-
*
|
|
1101
|
+
* import {
|
|
1102
|
+
* createRun,
|
|
1103
|
+
* isNonEmptyArray,
|
|
1104
|
+
* ok,
|
|
1105
|
+
* race,
|
|
1106
|
+
* type Task,
|
|
1107
|
+
* } from "@evolu/common";
|
|
722
1108
|
*
|
|
723
|
-
* const
|
|
724
|
-
*
|
|
725
|
-
*
|
|
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");
|
|
726
1114
|
* }
|
|
727
1115
|
* ```
|
|
728
1116
|
*
|
|
729
|
-
*
|
|
730
|
-
*/
|
|
731
|
-
export const timeout = (task, duration, { abortReason = timeoutError, } = {}) => race([
|
|
732
|
-
task,
|
|
733
|
-
async (run) => {
|
|
734
|
-
await run(sleep(duration));
|
|
735
|
-
return err(timeoutError);
|
|
736
|
-
},
|
|
737
|
-
], { abortReason });
|
|
738
|
-
/**
|
|
739
|
-
* Typed error returned by {@link timeout} when a task exceeds its time limit.
|
|
1117
|
+
* ### Example
|
|
740
1118
|
*
|
|
741
|
-
*
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
*
|
|
1119
|
+
* ```ts
|
|
1120
|
+
* import {
|
|
1121
|
+
* createRun,
|
|
1122
|
+
* ok,
|
|
1123
|
+
* race,
|
|
1124
|
+
* sleep,
|
|
1125
|
+
* type Result,
|
|
1126
|
+
* type Task,
|
|
1127
|
+
* } from "@evolu/common";
|
|
1128
|
+
*
|
|
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
|
+
* };
|
|
746
1138
|
*
|
|
747
|
-
*
|
|
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
|
|
748
1148
|
*/
|
|
749
|
-
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
|
+
};
|
|
750
1159
|
/**
|
|
751
|
-
*
|
|
1160
|
+
* Runs Tasks until `count` Tasks return {@link Ok} or all Tasks settle.
|
|
752
1161
|
*
|
|
753
|
-
*
|
|
754
|
-
* {@link
|
|
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.
|
|
755
1166
|
*
|
|
756
|
-
*
|
|
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.
|
|
757
1170
|
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
1171
|
+
* ### Example
|
|
1172
|
+
*
|
|
1173
|
+
* ```ts
|
|
1174
|
+
* import {
|
|
1175
|
+
* createRun,
|
|
1176
|
+
* err,
|
|
1177
|
+
* firstN,
|
|
1178
|
+
* ok,
|
|
1179
|
+
* sleep,
|
|
1180
|
+
* type Task,
|
|
1181
|
+
* } from "@evolu/common";
|
|
763
1182
|
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
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.
|
|
766
1225
|
*
|
|
767
1226
|
* ### Example
|
|
768
1227
|
*
|
|
769
1228
|
* ```ts
|
|
770
1229
|
* import {
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
1230
|
+
* createRun,
|
|
1231
|
+
* err,
|
|
1232
|
+
* firstNSettled,
|
|
1233
|
+
* ok,
|
|
1234
|
+
* sleep,
|
|
1235
|
+
* type Task,
|
|
776
1236
|
* } from "@evolu/common";
|
|
777
1237
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* );
|
|
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();
|
|
783
1250
|
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* console.log(`Last error:`, result.error.cause);
|
|
792
|
-
* }
|
|
793
|
-
* }
|
|
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);
|
|
794
1258
|
* ```
|
|
795
1259
|
*
|
|
796
|
-
*
|
|
797
|
-
|
|
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
|
|
798
1298
|
*
|
|
799
1299
|
* ```ts
|
|
800
|
-
* import {
|
|
1300
|
+
* import {
|
|
1301
|
+
* createRun,
|
|
1302
|
+
* each,
|
|
1303
|
+
* err,
|
|
1304
|
+
* ok,
|
|
1305
|
+
* sleep,
|
|
1306
|
+
* type Task,
|
|
1307
|
+
* } from "@evolu/common";
|
|
801
1308
|
*
|
|
802
|
-
*
|
|
803
|
-
* const
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
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 },
|
|
807
1331
|
* ),
|
|
808
1332
|
* );
|
|
1333
|
+
*
|
|
1334
|
+
* expectOk(result, undefined);
|
|
1335
|
+
* expect(first).toEqual(["fast", 2]);
|
|
1336
|
+
* expect(slowCompleted).toBe(false);
|
|
809
1337
|
* ```
|
|
810
1338
|
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
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
|
|
813
1354
|
*/
|
|
814
|
-
export const
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
+
}
|
|
827
1392
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
error
|
|
831
|
-
attempt: PositiveInt.orThrow(decrement(attempt)),
|
|
832
|
-
output,
|
|
833
|
-
delay,
|
|
834
|
-
});
|
|
835
|
-
if (delay > 0) {
|
|
836
|
-
const sleepResult = await run(sleep(delay));
|
|
837
|
-
if (!sleepResult.ok)
|
|
838
|
-
return sleepResult;
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
stopped = true;
|
|
1395
|
+
throw error;
|
|
839
1396
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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);
|
|
855
1414
|
}
|
|
856
1415
|
};
|
|
857
1416
|
/**
|
|
858
|
-
*
|
|
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.
|
|
859
1425
|
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* 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.
|
|
863
1428
|
*
|
|
864
|
-
*
|
|
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);
|
|
865
1436
|
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
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.
|
|
868
1454
|
*
|
|
869
1455
|
* ### Example
|
|
870
1456
|
*
|
|
871
1457
|
* ```ts
|
|
872
|
-
* import {
|
|
873
|
-
* import { repeat } from "@evolu/common";
|
|
1458
|
+
* import { createRun, ok, yieldNow, type Task } from "@evolu/common";
|
|
874
1459
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
1460
|
+
* const sumTo =
|
|
1461
|
+
* (count: number): Task<number> =>
|
|
1462
|
+
* async (run) => {
|
|
1463
|
+
* let sum = 0;
|
|
877
1464
|
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
1465
|
+
* for (let index = 0; index < count; index++) {
|
|
1466
|
+
* if (index > 0 && index % 1000 === 0) await run.ok(yieldNow);
|
|
1467
|
+
* sum += index;
|
|
1468
|
+
* }
|
|
880
1469
|
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* const item = queue.dequeue();
|
|
884
|
-
* if (!item) return err(done()); // Queue empty, stop
|
|
885
|
-
* await process(item);
|
|
886
|
-
* return ok(item);
|
|
887
|
-
* };
|
|
1470
|
+
* return ok(sum);
|
|
1471
|
+
* };
|
|
888
1472
|
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
* console.log("Queue exhausted");
|
|
892
|
-
* }
|
|
1473
|
+
* await using run = createRun();
|
|
1474
|
+
* expectOk(await run(sumTo(1001)), 500500);
|
|
893
1475
|
* ```
|
|
894
1476
|
*
|
|
895
|
-
* @group
|
|
1477
|
+
* @group Scheduling
|
|
896
1478
|
*/
|
|
897
|
-
export const
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
value: result.value,
|
|
914
|
-
attempt,
|
|
915
|
-
output,
|
|
916
|
-
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);
|
|
917
1495
|
});
|
|
918
|
-
attempt = PositiveInt.orThrow(increment(attempt));
|
|
919
|
-
if (delay > 0) {
|
|
920
|
-
const sleepResult = await run(sleep(delay));
|
|
921
|
-
if (!sleepResult.ok)
|
|
922
|
-
return sleepResult;
|
|
923
|
-
}
|
|
924
1496
|
}
|
|
925
|
-
|
|
1497
|
+
run.signal.throwIfAborted();
|
|
1498
|
+
return ok();
|
|
926
1499
|
};
|
|
1500
|
+
// Abortability
|
|
927
1501
|
/**
|
|
928
|
-
*
|
|
1502
|
+
* Waits until the current {@link Run} aborts, then rejects with its
|
|
1503
|
+
* {@link AbortError}.
|
|
929
1504
|
*
|
|
930
|
-
*
|
|
931
|
-
|
|
932
|
-
export const createDeferred = () => {
|
|
933
|
-
let resolved = null;
|
|
934
|
-
const resolvers = new Set();
|
|
935
|
-
const resolve = (result) => {
|
|
936
|
-
if (resolved !== null)
|
|
937
|
-
return false;
|
|
938
|
-
resolved = result;
|
|
939
|
-
for (const resolver of resolvers)
|
|
940
|
-
resolver(result);
|
|
941
|
-
resolvers.clear();
|
|
942
|
-
return true;
|
|
943
|
-
};
|
|
944
|
-
return {
|
|
945
|
-
task: (run) => {
|
|
946
|
-
if (resolved !== null)
|
|
947
|
-
return resolved;
|
|
948
|
-
return new Promise((resolvePromise) => {
|
|
949
|
-
const resolve = (result) => {
|
|
950
|
-
resolvers.delete(resolve);
|
|
951
|
-
resolvePromise(result);
|
|
952
|
-
};
|
|
953
|
-
resolvers.add(resolve);
|
|
954
|
-
run.onAbort((reason) => {
|
|
955
|
-
resolve(err(createAbortError(reason)));
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
},
|
|
959
|
-
resolve,
|
|
960
|
-
[Symbol.dispose]: () => {
|
|
961
|
-
resolve(err(deferredDisposedError));
|
|
962
|
-
},
|
|
963
|
-
};
|
|
964
|
-
};
|
|
965
|
-
/**
|
|
966
|
-
* 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.
|
|
967
1507
|
*
|
|
968
|
-
*
|
|
969
|
-
*/
|
|
970
|
-
export const DeferredDisposedError = /*#__PURE__*/ typed("DeferredDisposedError");
|
|
971
|
-
/**
|
|
972
|
-
* {@link DeferredDisposedError} used as abort reason in {@link createDeferred}.
|
|
1508
|
+
* ### Example
|
|
973
1509
|
*
|
|
974
|
-
*
|
|
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
|
|
975
1557
|
*/
|
|
976
|
-
export const
|
|
977
|
-
|
|
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
|
+
}
|
|
978
1572
|
};
|
|
979
1573
|
/**
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
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.
|
|
984
1612
|
*
|
|
985
1613
|
* ### Example
|
|
986
1614
|
*
|
|
987
1615
|
* ```ts
|
|
988
|
-
*
|
|
1616
|
+
* import {
|
|
1617
|
+
* createRun,
|
|
1618
|
+
* daemon,
|
|
1619
|
+
* ok,
|
|
1620
|
+
* timeout,
|
|
1621
|
+
* type Task,
|
|
1622
|
+
* } from "@evolu/common";
|
|
989
1623
|
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
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
|
+
* });
|
|
992
1633
|
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
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
|
+
* ```
|
|
995
1644
|
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
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"));
|
|
1002
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>>();
|
|
1003
1689
|
* ```
|
|
1004
1690
|
*
|
|
1005
|
-
* @group
|
|
1691
|
+
* @group Lifetime
|
|
1006
1692
|
*/
|
|
1007
|
-
export const
|
|
1008
|
-
const
|
|
1693
|
+
export const daemon = (task) => async (run) => {
|
|
1694
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1009
1695
|
try {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
open: () => {
|
|
1026
|
-
assertNotDisposed(disposables);
|
|
1027
|
-
if (isOpen)
|
|
1028
|
-
return;
|
|
1029
|
-
isOpen = true;
|
|
1030
|
-
deferred.resolve(ok());
|
|
1031
|
-
},
|
|
1032
|
-
close: () => {
|
|
1033
|
-
assertNotDisposed(disposables);
|
|
1034
|
-
if (!isOpen)
|
|
1035
|
-
return;
|
|
1036
|
-
isOpen = false;
|
|
1037
|
-
deferred = createDeferred();
|
|
1038
|
-
},
|
|
1039
|
-
isOpen: () => {
|
|
1040
|
-
assertNotDisposed(disposables);
|
|
1041
|
-
return isOpen;
|
|
1042
|
-
},
|
|
1043
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
1044
|
-
};
|
|
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]);
|
|
1045
1711
|
}
|
|
1046
|
-
catch (
|
|
1047
|
-
|
|
1048
|
-
|
|
1712
|
+
catch (e_3) {
|
|
1713
|
+
env_3.error = e_3;
|
|
1714
|
+
env_3.hasError = true;
|
|
1049
1715
|
}
|
|
1050
1716
|
finally {
|
|
1051
|
-
__disposeResources(
|
|
1717
|
+
__disposeResources(env_3);
|
|
1052
1718
|
}
|
|
1053
1719
|
};
|
|
1054
1720
|
/**
|
|
1055
|
-
*
|
|
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.
|
|
1056
1731
|
*
|
|
1057
1732
|
* ### Example
|
|
1058
1733
|
*
|
|
1059
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
|
+
*
|
|
1060
1746
|
* await using run = createRun();
|
|
1747
|
+
* const fiber = run.abortable(commit);
|
|
1748
|
+
* await commitStarted.promise;
|
|
1749
|
+
* fiber.abort();
|
|
1750
|
+
* finishCommit.resolve();
|
|
1061
1751
|
*
|
|
1062
|
-
*
|
|
1752
|
+
* expectOk(await fiber, "committed");
|
|
1753
|
+
* ```
|
|
1063
1754
|
*
|
|
1064
|
-
*
|
|
1065
|
-
|
|
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.
|
|
1777
|
+
*
|
|
1778
|
+
* ### Example
|
|
1779
|
+
*
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* import {
|
|
1782
|
+
* AbortError,
|
|
1783
|
+
* createRun,
|
|
1784
|
+
* ok,
|
|
1785
|
+
* unabortableMask,
|
|
1786
|
+
* waitForAbort,
|
|
1787
|
+
* type Task,
|
|
1788
|
+
* } from "@evolu/common";
|
|
1789
|
+
*
|
|
1790
|
+
* await using run = createRun();
|
|
1791
|
+
*
|
|
1792
|
+
* interface Resource {
|
|
1793
|
+
* readonly id: string;
|
|
1794
|
+
* }
|
|
1795
|
+
*
|
|
1796
|
+
* const acquire: Task<Resource> = () => ok({ id: "resource-1" });
|
|
1797
|
+
* const operationStarted = Promise.withResolvers<void>();
|
|
1798
|
+
* const operate =
|
|
1799
|
+
* (resource: Resource): Task<never> =>
|
|
1066
1800
|
* async (run) => {
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
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();
|
|
1073
1813
|
* };
|
|
1074
1814
|
*
|
|
1075
|
-
* const
|
|
1076
|
-
*
|
|
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
|
+
* );
|
|
1077
1829
|
*
|
|
1078
|
-
* await
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
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.
|
|
1083
1864
|
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
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);
|
|
1090
1914
|
* ```
|
|
1091
1915
|
*
|
|
1092
|
-
* @group
|
|
1916
|
+
* @group Abortability
|
|
1093
1917
|
*/
|
|
1094
|
-
export const
|
|
1095
|
-
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;
|
|
1096
1922
|
try {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const waiters = [];
|
|
1100
|
-
let taken = zeroNonNegativeInt;
|
|
1101
|
-
disposer.defer(() => {
|
|
1102
|
-
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1103
|
-
try {
|
|
1104
|
-
const disposer = __addDisposableResource(env_3, new DisposableStack(), false);
|
|
1105
|
-
for (const fiber of fibers) {
|
|
1106
|
-
disposer.adopt(fiber, (fiber) => {
|
|
1107
|
-
fiber.abort(semaphoreDisposedError);
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
for (const waiter of waiters) {
|
|
1111
|
-
waiter.resolve(err(semaphoreDisposedAbortError));
|
|
1112
|
-
}
|
|
1113
|
-
waiters.length = 0;
|
|
1114
|
-
}
|
|
1115
|
-
catch (e_3) {
|
|
1116
|
-
env_3.error = e_3;
|
|
1117
|
-
env_3.hasError = true;
|
|
1118
|
-
}
|
|
1119
|
-
finally {
|
|
1120
|
-
__disposeResources(env_3);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
const disposables = disposer.move();
|
|
1124
|
-
const withPermits = (requestedPermits) => (task) => async (run) => {
|
|
1125
|
-
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
1126
|
-
try {
|
|
1127
|
-
const requested = PositiveInt.orThrow(requestedPermits);
|
|
1128
|
-
assert(requested <= permits, "Requested permits must not exceed semaphore capacity.");
|
|
1129
|
-
if (disposables.disposed)
|
|
1130
|
-
return err(semaphoreDisposedAbortError);
|
|
1131
|
-
if (waiters.length > 0 || taken + requested > permits) {
|
|
1132
|
-
const waiter = Promise.withResolvers();
|
|
1133
|
-
const waiting = {
|
|
1134
|
-
permits: requested,
|
|
1135
|
-
resolve: waiter.resolve,
|
|
1136
|
-
};
|
|
1137
|
-
waiters.push(waiting);
|
|
1138
|
-
run.onAbort((reason) => {
|
|
1139
|
-
const i = waiters.indexOf(waiting);
|
|
1140
|
-
if (i >= 0)
|
|
1141
|
-
waiters.splice(i, 1);
|
|
1142
|
-
waiter.resolve(err(createAbortError(reason)));
|
|
1143
|
-
});
|
|
1144
|
-
const permit = await waiter.promise;
|
|
1145
|
-
if (!permit.ok)
|
|
1146
|
-
return permit;
|
|
1147
|
-
}
|
|
1148
|
-
else {
|
|
1149
|
-
taken = NonNegativeInt.orThrow(taken + requested);
|
|
1150
|
-
}
|
|
1151
|
-
let fiber = null;
|
|
1152
|
-
const _ = __addDisposableResource(env_4, {
|
|
1153
|
-
[Symbol.dispose]: () => {
|
|
1154
|
-
if (fiber)
|
|
1155
|
-
fibers.delete(fiber);
|
|
1156
|
-
taken = NonNegativeInt.orThrow(taken - requested);
|
|
1157
|
-
while (waiters.length > 0) {
|
|
1158
|
-
const waiter = waiters[0];
|
|
1159
|
-
if (taken + waiter.permits > permits)
|
|
1160
|
-
break;
|
|
1161
|
-
waiters.shift();
|
|
1162
|
-
taken = NonNegativeInt.orThrow(taken + waiter.permits);
|
|
1163
|
-
waiter.resolve(ok());
|
|
1164
|
-
}
|
|
1165
|
-
},
|
|
1166
|
-
}, false);
|
|
1167
|
-
fiber = run(task);
|
|
1168
|
-
fibers.add(fiber);
|
|
1169
|
-
return await fiber;
|
|
1170
|
-
}
|
|
1171
|
-
catch (e_4) {
|
|
1172
|
-
env_4.error = e_4;
|
|
1173
|
-
env_4.hasError = true;
|
|
1174
|
-
}
|
|
1175
|
-
finally {
|
|
1176
|
-
__disposeResources(env_4);
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1179
|
-
return {
|
|
1180
|
-
withPermits,
|
|
1181
|
-
withPermit: (task) => withPermits(1)(task),
|
|
1182
|
-
snapshot: () => {
|
|
1183
|
-
assertNotDisposed(disposables);
|
|
1184
|
-
return {
|
|
1185
|
-
permits,
|
|
1186
|
-
taken,
|
|
1187
|
-
waiting: NonNegativeInt.orThrow(waiters.length),
|
|
1188
|
-
available: NonNegativeInt.orThrow(permits - taken),
|
|
1189
|
-
isIdle: taken === 0 && waiters.length === 0,
|
|
1190
|
-
disposed: disposables.disposed,
|
|
1191
|
-
};
|
|
1192
|
-
},
|
|
1193
|
-
[Symbol.dispose]: () => disposables.dispose(),
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
catch (e_2) {
|
|
1197
|
-
env_2.error = e_2;
|
|
1198
|
-
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)));
|
|
1199
1925
|
}
|
|
1200
1926
|
finally {
|
|
1201
|
-
|
|
1927
|
+
await run.ok(release(resourceResult.value));
|
|
1202
1928
|
}
|
|
1203
|
-
};
|
|
1929
|
+
});
|
|
1204
1930
|
/**
|
|
1205
|
-
*
|
|
1931
|
+
* Creates a {@link Deferred}.
|
|
1206
1932
|
*
|
|
1207
1933
|
* @group Concurrency primitives
|
|
1208
1934
|
*/
|
|
1209
|
-
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
|
+
};
|
|
1210
1960
|
/**
|
|
1211
|
-
*
|
|
1961
|
+
* Creates a {@link Gate}.
|
|
1212
1962
|
*
|
|
1213
1963
|
* @group Concurrency primitives
|
|
1214
1964
|
*/
|
|
1215
|
-
export const
|
|
1216
|
-
|
|
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
|
+
};
|
|
1217
1996
|
};
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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;
|
|
1243
2036
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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 };
|
|
1248
2078
|
try {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
[Symbol.dispose]: () => {
|
|
1258
|
-
if (semaphoresByKey.get(key) !== semaphore)
|
|
1259
|
-
return;
|
|
1260
|
-
const snapshot = semaphore.snapshot();
|
|
1261
|
-
if (snapshot.isIdle) {
|
|
1262
|
-
semaphoresByKey.delete(key);
|
|
1263
|
-
semaphore[Symbol.dispose]();
|
|
1264
|
-
}
|
|
1265
|
-
},
|
|
1266
|
-
}, false);
|
|
1267
|
-
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));
|
|
1268
2087
|
}
|
|
1269
|
-
catch (
|
|
1270
|
-
|
|
1271
|
-
|
|
2088
|
+
catch (e_5) {
|
|
2089
|
+
env_5.error = e_5;
|
|
2090
|
+
env_5.hasError = true;
|
|
1272
2091
|
}
|
|
1273
2092
|
finally {
|
|
1274
|
-
__disposeResources(
|
|
2093
|
+
__disposeResources(env_5);
|
|
1275
2094
|
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
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
|
+
};
|
|
1295
2112
|
/**
|
|
1296
2113
|
* Creates a {@link Mutex}.
|
|
1297
2114
|
*
|
|
1298
2115
|
* @group Concurrency primitives
|
|
1299
2116
|
*/
|
|
1300
2117
|
export const createMutex = () => {
|
|
1301
|
-
const semaphore = createSemaphore(
|
|
2118
|
+
const semaphore = createSemaphore(1);
|
|
1302
2119
|
return {
|
|
1303
2120
|
withLock: semaphore.withPermit,
|
|
1304
2121
|
snapshot: semaphore.snapshot,
|
|
1305
|
-
[Symbol.dispose]: semaphore[Symbol.dispose],
|
|
1306
2122
|
};
|
|
1307
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
|
+
}
|
|
1308
2155
|
export function createMutexByKey({ lookup = identity, } = {}) {
|
|
1309
|
-
const semaphoreByKey = createSemaphoreByKey(
|
|
1310
|
-
lookup,
|
|
1311
|
-
});
|
|
2156
|
+
const semaphoreByKey = createSemaphoreByKey(1, { lookup });
|
|
1312
2157
|
return {
|
|
1313
2158
|
withLock: (key, task) => semaphoreByKey.withPermit(key, task),
|
|
2159
|
+
isIdle: semaphoreByKey.isIdle,
|
|
1314
2160
|
snapshot: semaphoreByKey.snapshot,
|
|
1315
|
-
[Symbol.dispose]: semaphoreByKey[Symbol.dispose],
|
|
1316
2161
|
};
|
|
1317
2162
|
}
|
|
1318
2163
|
/**
|
|
1319
|
-
* Creates a {@link MutexRef}
|
|
2164
|
+
* Creates a {@link MutexRef}.
|
|
1320
2165
|
*
|
|
1321
2166
|
* @group Concurrency primitives
|
|
1322
2167
|
*/
|
|
@@ -1347,288 +2192,40 @@ export const createMutexRef = (initialValue) => {
|
|
|
1347
2192
|
return ok(previousValue);
|
|
1348
2193
|
}),
|
|
1349
2194
|
updateAndGet: (updater) => mutex.withLock(async (run) => {
|
|
1350
|
-
const
|
|
1351
|
-
const nextValue = await run(updater(currentValue));
|
|
2195
|
+
const nextValue = await run(updater(ref.get()));
|
|
1352
2196
|
if (!nextValue.ok)
|
|
1353
2197
|
return nextValue;
|
|
1354
2198
|
ref.set(nextValue.value);
|
|
1355
2199
|
return ok(nextValue.value);
|
|
1356
2200
|
}),
|
|
1357
2201
|
modify: (modifier) => mutex.withLock(async (run) => {
|
|
1358
|
-
const
|
|
1359
|
-
if (!
|
|
1360
|
-
return
|
|
1361
|
-
const [
|
|
1362
|
-
ref.set(
|
|
1363
|
-
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);
|
|
1364
2208
|
}),
|
|
1365
|
-
|
|
2209
|
+
snapshot: mutex.snapshot,
|
|
1366
2210
|
};
|
|
1367
2211
|
};
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
/**
|
|
1389
|
-
* Abort reason used by {@link allSettled} when aborted externally.
|
|
1390
|
-
*
|
|
1391
|
-
* @group Composition
|
|
1392
|
-
*/
|
|
1393
|
-
export const AllSettledAbortError = /*#__PURE__*/ typed("AllSettledAbortError");
|
|
1394
|
-
/**
|
|
1395
|
-
* {@link AllSettledAbortError} used as abort reason in {@link allSettled}.
|
|
1396
|
-
*
|
|
1397
|
-
* @group Composition
|
|
1398
|
-
*/
|
|
1399
|
-
export const allSettledAbortError = {
|
|
1400
|
-
type: "AllSettledAbortError",
|
|
1401
|
-
};
|
|
1402
|
-
export function map(items, fn, { abortReason = mapAbortError, ...options } = {}) {
|
|
1403
|
-
const mapped = mapInput(items, fn);
|
|
1404
|
-
return all(mapped, {
|
|
1405
|
-
...options,
|
|
1406
|
-
abortReason,
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
/**
|
|
1410
|
-
* Abort reason used by {@link map} when aborting remaining tasks.
|
|
1411
|
-
*
|
|
1412
|
-
* @group Composition
|
|
1413
|
-
*/
|
|
1414
|
-
export const MapAbortError = /*#__PURE__*/ typed("MapAbortError");
|
|
1415
|
-
/**
|
|
1416
|
-
* {@link MapAbortError} used as abort reason in {@link map}.
|
|
1417
|
-
*
|
|
1418
|
-
* @group Composition
|
|
1419
|
-
*/
|
|
1420
|
-
export const mapAbortError = {
|
|
1421
|
-
type: "MapAbortError",
|
|
1422
|
-
};
|
|
1423
|
-
export function mapSettled(items, task, options) {
|
|
1424
|
-
const mapped = mapInput(items, task);
|
|
1425
|
-
return allSettled(mapped, options);
|
|
1426
|
-
}
|
|
1427
|
-
export function any(tasks, options) {
|
|
1428
|
-
const { allFailed = "input" } = options ?? {};
|
|
1429
|
-
return pool(tasks, {
|
|
1430
|
-
stopOn: "success",
|
|
1431
|
-
collect: false,
|
|
1432
|
-
abortReason: anyAbortError,
|
|
1433
|
-
allFailed,
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Abort reason used by {@link any} when aborting remaining tasks.
|
|
1438
|
-
*
|
|
1439
|
-
* @group Composition
|
|
1440
|
-
*/
|
|
1441
|
-
export const AnyAbortError = /*#__PURE__*/ typed("AnyAbortError");
|
|
1442
|
-
/**
|
|
1443
|
-
* {@link AnyAbortError} used as abort reason in {@link any}.
|
|
1444
|
-
*
|
|
1445
|
-
* @group Composition
|
|
1446
|
-
*/
|
|
1447
|
-
export const anyAbortError = { type: "AnyAbortError" };
|
|
1448
|
-
/** Shared implementation for {@link all} and {@link allSettled}. */
|
|
1449
|
-
const collect = (type, input, { collect = true, abortReason = type === "all" ? allAbortError : allSettledAbortError, } = {}) => {
|
|
1450
|
-
const stopOn = type === "all" ? "error" : null;
|
|
1451
|
-
if (isIterable(input)) {
|
|
1452
|
-
const tasks = input;
|
|
1453
|
-
const array = arrayFrom(tasks);
|
|
1454
|
-
if (!isNonEmptyArray(array))
|
|
1455
|
-
return () => ok(collect ? emptyArray : undefined);
|
|
1456
|
-
return pool(array, {
|
|
1457
|
-
stopOn,
|
|
1458
|
-
collect,
|
|
1459
|
-
abortReason,
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
const keys = [];
|
|
1463
|
-
const taskArray = [];
|
|
1464
|
-
for (const key in input) {
|
|
1465
|
-
keys.push(key);
|
|
1466
|
-
taskArray.push(input[key]);
|
|
1467
|
-
}
|
|
1468
|
-
if (keys.length === 0)
|
|
1469
|
-
return () => ok(collect ? emptyRecord : undefined);
|
|
1470
|
-
return async (run) => {
|
|
1471
|
-
const result = await run(pool(taskArray, { stopOn, collect, abortReason }));
|
|
1472
|
-
if (!result.ok)
|
|
1473
|
-
return result;
|
|
1474
|
-
if (!collect)
|
|
1475
|
-
return ok();
|
|
1476
|
-
const record = createRecord();
|
|
1477
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1478
|
-
record[keys[i]] = result.value[i];
|
|
1479
|
-
}
|
|
1480
|
-
return ok(record);
|
|
1481
|
-
};
|
|
1482
|
-
};
|
|
1483
|
-
const mapInput = (input, fn) => isIterable(input) ? mapArray(arrayFrom(input), fn) : mapObject(input, fn);
|
|
1484
|
-
function pool(tasksIterable, { stopOn = null, collect, abortReason, allFailed, }) {
|
|
1485
|
-
const tasks = arrayFrom(tasksIterable);
|
|
1486
|
-
const { length } = tasks;
|
|
1487
|
-
if (length === 0)
|
|
1488
|
-
return () => ok(emptyArray);
|
|
1489
|
-
return async (run) => {
|
|
1490
|
-
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
1491
|
-
try {
|
|
1492
|
-
const results = collect ? new Array(length) : null;
|
|
1493
|
-
const aborted = Promise.withResolvers();
|
|
1494
|
-
const stopSignal = stopOn ? Promise.withResolvers() : null;
|
|
1495
|
-
let index = 0;
|
|
1496
|
-
let stopped = null;
|
|
1497
|
-
let lastResult = null;
|
|
1498
|
-
let lastIndexResult = null;
|
|
1499
|
-
const worker = async (run) => {
|
|
1500
|
-
while (index < length && (stopOn ? !stopped : true)) {
|
|
1501
|
-
const i = index++;
|
|
1502
|
-
const result = (await run(tasks[i]));
|
|
1503
|
-
lastResult = result;
|
|
1504
|
-
if (i === length - 1)
|
|
1505
|
-
lastIndexResult = result;
|
|
1506
|
-
if (!stopOn) {
|
|
1507
|
-
if (results)
|
|
1508
|
-
results[i] = result;
|
|
1509
|
-
continue;
|
|
1510
|
-
}
|
|
1511
|
-
const stop = stopOn === "first" ||
|
|
1512
|
-
(stopOn === "error" && !result.ok) ||
|
|
1513
|
-
(stopOn === "success" && result.ok);
|
|
1514
|
-
if (!stop) {
|
|
1515
|
-
if (results)
|
|
1516
|
-
results[i] = result.value;
|
|
1517
|
-
continue;
|
|
1518
|
-
}
|
|
1519
|
-
if (!stopped) {
|
|
1520
|
-
stopped = result;
|
|
1521
|
-
abortWorkers(!result.ok && AbortError.is(result.error)
|
|
1522
|
-
? result.error.reason
|
|
1523
|
-
: abortReason);
|
|
1524
|
-
stopSignal?.resolve();
|
|
1525
|
-
}
|
|
1526
|
-
break;
|
|
1527
|
-
}
|
|
1528
|
-
return ok();
|
|
1529
|
-
};
|
|
1530
|
-
let workersAborted = false;
|
|
1531
|
-
const abortWorkers = (reason) => {
|
|
1532
|
-
if (workersAborted)
|
|
1533
|
-
return;
|
|
1534
|
-
workersAborted = true;
|
|
1535
|
-
for (const worker of workers)
|
|
1536
|
-
worker.abort(reason);
|
|
1537
|
-
};
|
|
1538
|
-
const workerCount = Math.min(run.concurrency, length);
|
|
1539
|
-
const workers = arrayFrom(workerCount, () => run.daemon(worker, run.deps));
|
|
1540
|
-
const _ = __addDisposableResource(env_8, new DisposableStack(), false);
|
|
1541
|
-
_.defer(() => {
|
|
1542
|
-
abortWorkers(abortReason);
|
|
1543
|
-
});
|
|
1544
|
-
run.onAbort((reason) => {
|
|
1545
|
-
abortWorkers(reason);
|
|
1546
|
-
aborted.resolve();
|
|
1547
|
-
});
|
|
1548
|
-
const waitFor = [Promise.all(workers), aborted.promise];
|
|
1549
|
-
if (stopSignal)
|
|
1550
|
-
waitFor.push(stopSignal.promise);
|
|
1551
|
-
await Promise.race(waitFor);
|
|
1552
|
-
if (run.signal.aborted) {
|
|
1553
|
-
return err(run.signal.reason);
|
|
1554
|
-
}
|
|
1555
|
-
if (!stopOn)
|
|
1556
|
-
return results ? ok(results) : ok();
|
|
1557
|
-
if (stopped)
|
|
1558
|
-
return stopped;
|
|
1559
|
-
if (results)
|
|
1560
|
-
return ok(results);
|
|
1561
|
-
// For all/allSettled/map/mapSettled with collect: false (no allFailed handler)
|
|
1562
|
-
if (!allFailed)
|
|
1563
|
-
return ok();
|
|
1564
|
-
return allFailed === "completion" ? lastResult : lastIndexResult;
|
|
1565
|
-
}
|
|
1566
|
-
catch (e_8) {
|
|
1567
|
-
env_8.error = e_8;
|
|
1568
|
-
env_8.hasError = true;
|
|
1569
|
-
}
|
|
1570
|
-
finally {
|
|
1571
|
-
__disposeResources(env_8);
|
|
1572
|
-
}
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
/**
|
|
1576
|
-
* Error returned when a {@link fetch} {@link Task} fails.
|
|
1577
|
-
*
|
|
1578
|
-
* @group Composition
|
|
1579
|
-
*/
|
|
1580
|
-
export const FetchError = /*#__PURE__*/ typed("FetchError", { error: Unknown });
|
|
1581
|
-
/**
|
|
1582
|
-
* Creates a {@link Task} that wraps the native
|
|
1583
|
-
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}.
|
|
1584
|
-
*
|
|
1585
|
-
* Handles cross-browser abort behavior — WebKit throws a `DOMException` with
|
|
1586
|
-
* message "Fetch is aborted" instead of propagating `signal.reason`. This
|
|
1587
|
-
* helper normalizes the behavior to always return {@link AbortError}.
|
|
1588
|
-
*
|
|
1589
|
-
* ### Example
|
|
1590
|
-
*
|
|
1591
|
-
* ```ts
|
|
1592
|
-
* await using run = createRun();
|
|
1593
|
-
*
|
|
1594
|
-
* const result = await run(fetch("https://api.example.com/users"));
|
|
1595
|
-
*
|
|
1596
|
-
* if (!result.ok) {
|
|
1597
|
-
* // Handle FetchError or AbortError
|
|
1598
|
-
* }
|
|
1599
|
-
*
|
|
1600
|
-
* // Compose with timeout and retry
|
|
1601
|
-
* const fetchWithRetry = (url: string) =>
|
|
1602
|
-
* retry(timeout(fetch(url), "10s"), retryStrategyAws);
|
|
1603
|
-
* ```
|
|
1604
|
-
*
|
|
1605
|
-
* @group Composition
|
|
1606
|
-
*/
|
|
1607
|
-
export const fetch = (input, init) => ({ signal }) => tryAsync(() => globalThis.fetch(input, { ...init, signal }), (error) => {
|
|
1608
|
-
if (AbortError.is(error))
|
|
1609
|
-
return error;
|
|
1610
|
-
// WebKit throws DOMException with message "Fetch is aborted" instead of
|
|
1611
|
-
// propagating signal.reason. Detect this and create AbortError.
|
|
1612
|
-
if (signal.aborted &&
|
|
1613
|
-
error instanceof Error &&
|
|
1614
|
-
error.message === "Fetch is aborted") {
|
|
1615
|
-
return createAbortError(signal.reason);
|
|
1616
|
-
}
|
|
1617
|
-
return { type: "FetchError", error };
|
|
1618
|
-
});
|
|
1619
|
-
// TODO: Prioritized Task Scheduling API integration
|
|
1620
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
|
|
1621
|
-
//
|
|
1622
|
-
// - `run(task, { priority })` - Fiber-level priority via TaskController
|
|
1623
|
-
// (extends AbortController), so the fiber's signal gets priority and all
|
|
1624
|
-
// nested work inherits it.
|
|
1625
|
-
// - `fiber.setPriority(priority)` - Dynamic priority changes mid-flight via
|
|
1626
|
-
// TaskController.setPriority().
|
|
1627
|
-
//
|
|
1628
|
-
// Note: scheduler.yield() inherits priority from enclosing postTask, so
|
|
1629
|
-
// yieldNow doesn't need a priority argument — just run the fiber at priority.
|
|
1630
|
-
//
|
|
1631
|
-
// Safari doesn't support it yet, Node.js probably never will (use setImmediate).
|
|
1632
|
-
// For Safari, scheduler-polyfill can be used.
|
|
1633
|
-
// https://www.npmjs.com/package/scheduler-polyfill
|
|
1634
|
-
// 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.
|