@evolu/common 7.4.1 → 8.0.0-next.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/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +851 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -824
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +206 -277
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -526
- package/src/Instances.ts +0 -92
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -949
package/dist/src/Task.js
CHANGED
|
@@ -1,466 +1,1606 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript-native structured concurrency.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
7
|
+
if (value !== null && value !== void 0) {
|
|
8
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
9
|
+
var dispose, inner;
|
|
10
|
+
if (async) {
|
|
11
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
12
|
+
dispose = value[Symbol.asyncDispose];
|
|
13
|
+
}
|
|
14
|
+
if (dispose === void 0) {
|
|
15
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
16
|
+
dispose = value[Symbol.dispose];
|
|
17
|
+
if (async) inner = dispose;
|
|
18
|
+
}
|
|
19
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
20
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
21
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
22
|
+
}
|
|
23
|
+
else if (async) {
|
|
24
|
+
env.stack.push({ async: true });
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
29
|
+
return function (env) {
|
|
30
|
+
function fail(e) {
|
|
31
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
32
|
+
env.hasError = true;
|
|
33
|
+
}
|
|
34
|
+
var r, s = 0;
|
|
35
|
+
function next() {
|
|
36
|
+
while (r = env.stack.pop()) {
|
|
37
|
+
try {
|
|
38
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
39
|
+
if (r.dispose) {
|
|
40
|
+
var result = r.dispose.call(r.value);
|
|
41
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
42
|
+
}
|
|
43
|
+
else s |= 1;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
fail(e);
|
|
47
|
+
}
|
|
25
48
|
}
|
|
26
|
-
s.
|
|
49
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
50
|
+
if (env.hasError) throw env.error;
|
|
27
51
|
}
|
|
28
|
-
return
|
|
52
|
+
return next();
|
|
29
53
|
};
|
|
30
|
-
}
|
|
54
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
55
|
+
var e = new Error(message);
|
|
56
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
57
|
+
});
|
|
58
|
+
import { arrayFrom, emptyArray, isNonEmptyArray, mapArray, } from "./Array.js";
|
|
59
|
+
import { assert } from "./Assert.js";
|
|
60
|
+
import { createConsole } from "./Console.js";
|
|
61
|
+
import { createRandomBytes } from "./Crypto.js";
|
|
62
|
+
import { eqArrayStrict } from "./Eq.js";
|
|
63
|
+
import { identity, lazyTrue, lazyVoid } from "./Function.js";
|
|
64
|
+
import { createLookupMap } from "./Lookup.js";
|
|
65
|
+
import { decrement, increment } from "./Number.js";
|
|
66
|
+
import { createRecord, emptyRecord, isFunction, isIterable, mapObject, } from "./Object.js";
|
|
67
|
+
import { createRandom } from "./Random.js";
|
|
68
|
+
import { createRef } from "./Ref.js";
|
|
69
|
+
import { err, getOrThrow, ok, tryAsync } from "./Result.js";
|
|
70
|
+
import { addToSet, deleteFromSet, emptySet } from "./Set.js";
|
|
71
|
+
import { createTime, durationToMillis, Millis } from "./Time.js";
|
|
72
|
+
import { brand, createId, Id, maxPositiveInt, NonNegativeInt, object, onePositiveInt, PositiveInt, typed, union, Unknown, UnknownResult, zeroNonNegativeInt, } from "./Type.js";
|
|
73
|
+
import {} from "./Types.js";
|
|
31
74
|
/**
|
|
32
|
-
*
|
|
75
|
+
* Error returned when a {@link Task} is aborted via
|
|
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)`.
|
|
85
|
+
*
|
|
86
|
+
* When a piece of logic must continue once started, wrap it with
|
|
87
|
+
* {@link unabortable}. That prevents ordinary mid-flight interruption, but it
|
|
88
|
+
* does not erase {@link AbortError} from the type because the Task can still be
|
|
89
|
+
* rejected before it starts, for example on an already-stopped {@link Run}. In
|
|
90
|
+
* those cases, abort usually indicates a lifecycle bug, so use
|
|
91
|
+
* `assertNotAborted` if you want to fail fast.
|
|
92
|
+
*
|
|
93
|
+
* @group Core Types
|
|
94
|
+
*/
|
|
95
|
+
export const AbortError = /*#__PURE__*/ typed("AbortError", {
|
|
96
|
+
reason: Unknown,
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Abort mask depth for a {@link Run} or {@link Fiber}.
|
|
100
|
+
*
|
|
101
|
+
* - `0` — abortable (default)
|
|
102
|
+
* - `>= 1` — inside {@link unabortable}, abort requests are ignored
|
|
103
|
+
*
|
|
104
|
+
* The mask tracks nested unabortable regions. When abort is requested, the
|
|
105
|
+
* signal only propagates if `mask === 0`.
|
|
106
|
+
*
|
|
107
|
+
* - {@link unabortable} increments the mask — Task becomes protected
|
|
108
|
+
* - {@link unabortableMask} provides `restore` to restore the previous mask
|
|
109
|
+
* - Tasks inherit their parent's mask by default
|
|
110
|
+
*
|
|
111
|
+
* This enables nested resource lifecycle patterns where each level can have its
|
|
112
|
+
* own abortable section while outer acquisitions remain protected.
|
|
113
|
+
*
|
|
114
|
+
* UI/debugging tools can use this to visually distinguish protected Tasks
|
|
115
|
+
* (e.g., different icon or color) and explain why abort requests are ignored.
|
|
33
116
|
*
|
|
34
|
-
*
|
|
35
|
-
* Otherwise, returns just the internal signal.
|
|
117
|
+
* @group Abort masking
|
|
36
118
|
*/
|
|
37
|
-
const
|
|
38
|
-
? AbortSignal.any([context.signal, internalSignal])
|
|
39
|
-
: internalSignal;
|
|
119
|
+
export const AbortMask = /*#__PURE__*/ brand("AbortMask", NonNegativeInt);
|
|
40
120
|
/**
|
|
41
|
-
*
|
|
121
|
+
* {@link RunSnapshot} state Type.
|
|
122
|
+
*
|
|
123
|
+
* @group Monitoring
|
|
124
|
+
*/
|
|
125
|
+
export const RunSnapshotState = /*#__PURE__*/ union(
|
|
126
|
+
/*#__PURE__*/ typed("Running"),
|
|
127
|
+
/*#__PURE__*/ typed("Disposing"),
|
|
128
|
+
/*#__PURE__*/ typed("Settled", {
|
|
129
|
+
result: UnknownResult,
|
|
130
|
+
outcome: UnknownResult,
|
|
131
|
+
}));
|
|
132
|
+
/**
|
|
133
|
+
* The event-specific payload of a {@link RunEvent}.
|
|
134
|
+
*
|
|
135
|
+
* @group Monitoring
|
|
136
|
+
*/
|
|
137
|
+
export const RunEventData = /*#__PURE__*/ union(
|
|
138
|
+
/*#__PURE__*/ typed("ChildAdded", { childId: Id }),
|
|
139
|
+
/*#__PURE__*/ typed("ChildRemoved", { childId: Id }),
|
|
140
|
+
/*#__PURE__*/ typed("StateChanged", { state: RunSnapshotState }));
|
|
141
|
+
/**
|
|
142
|
+
* Events emitted by a {@link Run} for monitoring and debugging.
|
|
143
|
+
*
|
|
144
|
+
* Events bubble up through parent runs, enabling centralized monitoring at the
|
|
145
|
+
* root. Use with {@link Run.onEvent} to track Run lifecycle.
|
|
146
|
+
*
|
|
147
|
+
* @group Monitoring
|
|
148
|
+
*/
|
|
149
|
+
export const RunEvent = /*#__PURE__*/ object({
|
|
150
|
+
id: Id,
|
|
151
|
+
timestamp: Millis,
|
|
152
|
+
data: RunEventData,
|
|
153
|
+
});
|
|
154
|
+
// TODO:
|
|
155
|
+
// Partial<TracerConfigDep> & // TODO:
|
|
156
|
+
// Partial<TracerDep>; // TODO:
|
|
157
|
+
const runDeps = {
|
|
158
|
+
console: createConsole(),
|
|
159
|
+
randomBytes: createRandomBytes(),
|
|
160
|
+
random: createRandom(),
|
|
161
|
+
time: createTime(),
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Creates root {@link Run}.
|
|
165
|
+
*
|
|
166
|
+
* The root Run is also the daemon Run: it stays running until disposed. Child
|
|
167
|
+
* Runs created by `run(task)` are disposed by their parent once they settle.
|
|
168
|
+
*
|
|
169
|
+
* Call once per entry point (main thread, worker, etc.) and dispose on
|
|
170
|
+
* shutdown. All Tasks run as descendants of this root Run.
|
|
171
|
+
*
|
|
172
|
+
* This common {@link createRun} is platform-agnostic. At application entry
|
|
173
|
+
* points, prefer the platform adapter when one exists. `@evolu/web` adds
|
|
174
|
+
* browser `error` and `unhandledrejection` handlers, `@evolu/nodejs` adds
|
|
175
|
+
* Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
|
|
176
|
+
* handling, and `@evolu/react-native` adds React Native global error handling.
|
|
177
|
+
*
|
|
178
|
+
* {@link RunDeps} provides default dependencies:
|
|
179
|
+
*
|
|
180
|
+
* - {@link Time}
|
|
181
|
+
* - {@link Console}
|
|
182
|
+
* - {@link Random}
|
|
183
|
+
* - {@link RandomBytes}
|
|
42
184
|
*
|
|
43
185
|
* ### Example
|
|
44
186
|
*
|
|
45
187
|
* ```ts
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
188
|
+
* // App entry point
|
|
189
|
+
* await using run = createRun();
|
|
190
|
+
*
|
|
191
|
+
* const result = await run(fetchData);
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* ### Example with custom dependencies
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* // Define dependency interfaces
|
|
198
|
+
* interface Config {
|
|
199
|
+
* readonly apiUrl: string;
|
|
200
|
+
* }
|
|
201
|
+
*
|
|
202
|
+
* interface ConfigDep {
|
|
203
|
+
* readonly config: Config;
|
|
49
204
|
* }
|
|
50
205
|
*
|
|
51
|
-
* // Task
|
|
52
|
-
* const
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* //
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* const
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
206
|
+
* // Task declares its dependencies via the D type parameter
|
|
207
|
+
* const fetchUser =
|
|
208
|
+
* (id: string): Task<User, FetchError, ConfigDep> =>
|
|
209
|
+
* async (run) => {
|
|
210
|
+
* const { config } = run.deps;
|
|
211
|
+
* const response = await fetch(`${config.apiUrl}/users/${id}`);
|
|
212
|
+
* // ...
|
|
213
|
+
* };
|
|
214
|
+
*
|
|
215
|
+
* // Composition root: create a Run with custom deps
|
|
216
|
+
* type AppDeps = RunDeps & ConfigDep;
|
|
217
|
+
*
|
|
218
|
+
* const appDeps: AppDeps = {
|
|
219
|
+
* ...testCreateDeps(), // or spread individual deps
|
|
220
|
+
* config: { apiUrl: "https://api.example.com" },
|
|
221
|
+
* };
|
|
222
|
+
*
|
|
223
|
+
* await using run = createRun(appDeps);
|
|
224
|
+
*
|
|
225
|
+
* // Run type is inferred from the deps argument
|
|
226
|
+
* const result = await run(fetchUser("123"));
|
|
227
|
+
*
|
|
228
|
+
* // TypeScript catches missing deps at compile time:
|
|
229
|
+
* // await using run2 = createRun(); // Run<RunDeps>
|
|
230
|
+
* // run2(fetchUser("123")); // Error: Property 'config' is missing
|
|
70
231
|
* ```
|
|
232
|
+
*
|
|
233
|
+
* @group Creating Run
|
|
71
234
|
*/
|
|
72
|
-
export const
|
|
73
|
-
|
|
74
|
-
((
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
235
|
+
export const createRun = (deps) => {
|
|
236
|
+
const mergedDeps = { ...runDeps, ...deps };
|
|
237
|
+
return createRunInternal(createRef(mergedDeps))();
|
|
238
|
+
};
|
|
239
|
+
const createRunInternal = (depsRef) => (parent, daemon, abortBehavior, concurrencyBehavior) => {
|
|
240
|
+
const parentMask = parent?.abortMask ?? isAbortable;
|
|
241
|
+
let abortMask;
|
|
242
|
+
switch (abortBehavior) {
|
|
243
|
+
case undefined:
|
|
244
|
+
abortMask = parentMask;
|
|
245
|
+
break;
|
|
246
|
+
case "unabortable":
|
|
247
|
+
abortMask = increment(parentMask);
|
|
248
|
+
break;
|
|
249
|
+
default:
|
|
250
|
+
assert(abortBehavior <= parentMask, "restore used outside its unabortableMask");
|
|
251
|
+
abortMask = abortBehavior;
|
|
80
252
|
}
|
|
81
|
-
|
|
82
|
-
|
|
253
|
+
const requestController = new AbortController();
|
|
254
|
+
const signalController = new AbortController();
|
|
255
|
+
let state = running;
|
|
256
|
+
let result;
|
|
257
|
+
let outcome;
|
|
258
|
+
let children = emptySet;
|
|
259
|
+
const requestAbort = (reason) => {
|
|
260
|
+
const abortError = reason;
|
|
261
|
+
if (abortMask === isAbortable)
|
|
262
|
+
signalController.abort(abortError);
|
|
263
|
+
requestController.abort(abortError);
|
|
264
|
+
};
|
|
265
|
+
if (parent) {
|
|
266
|
+
subscribeToAbort(parent.requestSignal, () => requestAbort(parent.requestSignal.reason), { signal: requestController.signal });
|
|
83
267
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
268
|
+
const emitEvent = (data) => {
|
|
269
|
+
const deps = depsRef.get();
|
|
270
|
+
if (!deps.runConfig?.eventsEnabled.get())
|
|
271
|
+
return;
|
|
272
|
+
const e = { id: self.id, timestamp: deps.time.now(), data };
|
|
273
|
+
for (let node = self; node; node = node.parent) {
|
|
274
|
+
node.onEvent?.(e);
|
|
275
|
+
}
|
|
88
276
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
277
|
+
const run = (task) => {
|
|
278
|
+
const childRun = createRunInternal(depsRef)(self, daemon ?? self, getAbortBehavior(task), getConcurrencyBehavior(task));
|
|
279
|
+
if (state !== running) {
|
|
280
|
+
childRun.requestAbort(runStoppedAbortError);
|
|
281
|
+
task = () => err(runStoppedAbortError);
|
|
282
|
+
}
|
|
283
|
+
else if (signalController.signal.aborted &&
|
|
284
|
+
childRun.abortMask === isAbortable) {
|
|
285
|
+
childRun.requestAbort(signalController.signal.reason);
|
|
286
|
+
task = () => err(signalController.signal.reason);
|
|
287
|
+
}
|
|
288
|
+
const childFiber = Object.assign(Promise.try(task, childRun)
|
|
289
|
+
.then(childRun.handleTaskFulfilled)
|
|
290
|
+
.finally(childRun.handleTaskSettled)
|
|
291
|
+
.finally(childRun[Symbol.asyncDispose])
|
|
292
|
+
.finally(() => {
|
|
293
|
+
children = deleteFromSet(children, childFiber);
|
|
294
|
+
emitEvent({ type: "ChildRemoved", childId: childRun.id });
|
|
295
|
+
}), {
|
|
296
|
+
run: childRun,
|
|
297
|
+
abort: (reason) => {
|
|
298
|
+
childRun.requestAbort(createAbortError(reason));
|
|
299
|
+
},
|
|
300
|
+
getState: () => childRun.getState(),
|
|
301
|
+
[Symbol.dispose]: () => {
|
|
302
|
+
childFiber.abort();
|
|
303
|
+
},
|
|
110
304
|
});
|
|
111
|
-
|
|
305
|
+
children = addToSet(children, childFiber);
|
|
306
|
+
emitEvent({ type: "ChildAdded", childId: childRun.id });
|
|
307
|
+
return childFiber;
|
|
112
308
|
};
|
|
309
|
+
const self = run;
|
|
310
|
+
{
|
|
311
|
+
const run = self;
|
|
312
|
+
const id = createId(depsRef.get());
|
|
313
|
+
let snapshot = null;
|
|
314
|
+
let disposingPromise = null;
|
|
315
|
+
run.orThrow = async (task) => getOrThrow(await self(task));
|
|
316
|
+
run.id = id;
|
|
317
|
+
run.parent = parent ?? null;
|
|
318
|
+
run.signal = signalController.signal;
|
|
319
|
+
run.abortMask = abortMask;
|
|
320
|
+
run.onAbort = (callback) => {
|
|
321
|
+
if (abortMask !== isAbortable)
|
|
322
|
+
return;
|
|
323
|
+
subscribeToAbort(signalController.signal, () => callback(signalController.signal.reason.reason), { once: true, signal: requestController.signal });
|
|
324
|
+
};
|
|
325
|
+
run.getState = () => state;
|
|
326
|
+
run.getChildren = () => children;
|
|
327
|
+
run.snapshot = () => {
|
|
328
|
+
const childSnapshots = Array.from(children).map((fiber) => fiber.run.snapshot());
|
|
329
|
+
if (snapshot?.state !== state ||
|
|
330
|
+
!eqArrayStrict(snapshot.children, childSnapshots)) {
|
|
331
|
+
snapshot = {
|
|
332
|
+
id,
|
|
333
|
+
state: state,
|
|
334
|
+
children: childSnapshots,
|
|
335
|
+
abortMask,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return snapshot;
|
|
339
|
+
};
|
|
340
|
+
run.daemon = daemon ?? self;
|
|
341
|
+
run.create = () => run.daemon(createDeferred().task).run;
|
|
342
|
+
Object.defineProperty(run, "deps", { get: depsRef.get });
|
|
343
|
+
run.concurrency =
|
|
344
|
+
concurrencyBehavior ?? parent?.concurrency ?? defaultConcurrency;
|
|
345
|
+
run.addDeps = (newDeps) => {
|
|
346
|
+
depsRef.modify((currentDeps) => {
|
|
347
|
+
const duplicate = Object.keys(newDeps).find((k) => k in currentDeps);
|
|
348
|
+
assert(!duplicate, `Dependency '${duplicate}' already added. ` +
|
|
349
|
+
`This assert ensures dependencies are created once. ` +
|
|
350
|
+
`Automatic deduplication would mask bugs.`);
|
|
351
|
+
return [undefined, { ...currentDeps, ...newDeps }];
|
|
352
|
+
});
|
|
353
|
+
return self;
|
|
354
|
+
};
|
|
355
|
+
run[Symbol.asyncDispose] = () => {
|
|
356
|
+
if (disposingPromise)
|
|
357
|
+
return disposingPromise;
|
|
358
|
+
state = { type: "Disposing" };
|
|
359
|
+
emitEvent({ type: "StateChanged", state });
|
|
360
|
+
requestAbort(runStoppedAbortError);
|
|
361
|
+
disposingPromise = Promise.allSettled((run.ownTaskSettled
|
|
362
|
+
? [run.ownTaskSettled.promise, ...children]
|
|
363
|
+
: children))
|
|
364
|
+
.then(lazyVoid)
|
|
365
|
+
.finally(() => {
|
|
366
|
+
/**
|
|
367
|
+
* Root and daemon Runs have no own Task, so
|
|
368
|
+
* `run.handleTaskFulfilled` never populates their terminal values.
|
|
369
|
+
* In that case disposal publishes `ok()` for both `result` and
|
|
370
|
+
* `outcome`. Task-backed Runs normally reach this point with both
|
|
371
|
+
* values already set.
|
|
372
|
+
*/
|
|
373
|
+
[result, outcome] = [result ?? ok(), outcome ?? ok()];
|
|
374
|
+
state = { type: "Settled", result, outcome };
|
|
375
|
+
emitEvent({ type: "StateChanged", state });
|
|
376
|
+
});
|
|
377
|
+
return disposingPromise;
|
|
378
|
+
};
|
|
379
|
+
// Internal
|
|
380
|
+
run.ownTaskSettled = parent ? Promise.withResolvers() : null;
|
|
381
|
+
run.requestAbort = requestAbort;
|
|
382
|
+
run.requestSignal = requestController.signal;
|
|
383
|
+
run.handleTaskFulfilled = (taskOutcome) => {
|
|
384
|
+
const taskResult = run.signal.aborted
|
|
385
|
+
? err(run.signal.reason)
|
|
386
|
+
: taskOutcome;
|
|
387
|
+
result = taskResult;
|
|
388
|
+
outcome = taskOutcome;
|
|
389
|
+
return taskResult;
|
|
390
|
+
};
|
|
391
|
+
run.handleTaskSettled = () => {
|
|
392
|
+
run.ownTaskSettled?.resolve();
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return self;
|
|
396
|
+
};
|
|
397
|
+
const running = { type: "Running" };
|
|
398
|
+
/**
|
|
399
|
+
* Abort reason indicating a {@link Run} can no longer start new Tasks.
|
|
400
|
+
*
|
|
401
|
+
* Covers both disposing and settled Runs.
|
|
402
|
+
*
|
|
403
|
+
* @group Creating Run
|
|
404
|
+
*/
|
|
405
|
+
export const RunStoppedError = /*#__PURE__*/ typed("RunStoppedError");
|
|
406
|
+
/**
|
|
407
|
+
* Shared {@link RunStoppedError} instance used as the default
|
|
408
|
+
* {@link AbortError.reason} when a Task is started on a non-running {@link Run}.
|
|
409
|
+
*
|
|
410
|
+
* @group Creating Run
|
|
411
|
+
*/
|
|
412
|
+
export const runStoppedError = {
|
|
413
|
+
type: "RunStoppedError",
|
|
414
|
+
};
|
|
415
|
+
const createAbortError = (reason) => ({
|
|
416
|
+
type: "AbortError",
|
|
417
|
+
reason,
|
|
418
|
+
});
|
|
419
|
+
const subscribeToAbort = (signal, handler, options) => {
|
|
420
|
+
if (signal.aborted)
|
|
421
|
+
handler();
|
|
422
|
+
else
|
|
423
|
+
signal.addEventListener("abort", handler, options);
|
|
424
|
+
};
|
|
425
|
+
const runStoppedAbortError = createAbortError(runStoppedError);
|
|
426
|
+
const isAbortable = AbortMask.orThrow(0);
|
|
427
|
+
const abortBehaviorSymbol = Symbol("evolu.Task.abortBehavior");
|
|
428
|
+
const getAbortBehavior = (task) => task[abortBehaviorSymbol];
|
|
429
|
+
const abortBehavior = (behavior) => (task) => Object.assign((run) => run(task), {
|
|
430
|
+
[abortBehaviorSymbol]: behavior,
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* Makes a {@link Task} unabortable.
|
|
434
|
+
*
|
|
435
|
+
* Once started, an unabortable Task always completes — abort requests are
|
|
436
|
+
* ignored and `signal.aborted` remains `false`.
|
|
437
|
+
*
|
|
438
|
+
* If the parent {@link Run} is already disposing or settled, `run(task)`
|
|
439
|
+
* short-circuits before task execution and returns `err(AbortError)` with
|
|
440
|
+
* {@link runStoppedError} as reason. So `unabortable` means “do not interrupt
|
|
441
|
+
* this Task once it has started”, not “remove AbortError from its type”.
|
|
442
|
+
*
|
|
443
|
+
* When that pre-start abort would be a programmer error, assert it explicitly
|
|
444
|
+
* with `assertNotAborted` after awaiting the result.
|
|
445
|
+
*
|
|
446
|
+
* ### Example
|
|
447
|
+
*
|
|
448
|
+
* ```ts
|
|
449
|
+
* await using run = createRun();
|
|
450
|
+
*
|
|
451
|
+
* const events: Array<string> = [];
|
|
452
|
+
* const canComplete = Promise.withResolvers<void>();
|
|
453
|
+
* let signalAbortedInAnalytics = true;
|
|
454
|
+
*
|
|
455
|
+
* // Simulate async analytics API (abortable by default)
|
|
456
|
+
* const sendToAnalytics =
|
|
457
|
+
* (event: number): Task<void, never> =>
|
|
458
|
+
* async ({ signal }) => {
|
|
459
|
+
* await canComplete.promise;
|
|
460
|
+
* signalAbortedInAnalytics = signal.aborted;
|
|
461
|
+
* events.push(`sent ${event}`);
|
|
462
|
+
* return ok();
|
|
463
|
+
* };
|
|
464
|
+
*
|
|
465
|
+
* // Important events must be sent even if the user navigates away
|
|
466
|
+
* const trackImportantEvent = (event: number) =>
|
|
467
|
+
* unabortable(sendToAnalytics(event));
|
|
468
|
+
*
|
|
469
|
+
* // User clicks, we start tracking (Task runs until first await)
|
|
470
|
+
* const fiber = run(trackImportantEvent(123));
|
|
471
|
+
*
|
|
472
|
+
* // User navigates away (abort requested while task is running)
|
|
473
|
+
* fiber.abort();
|
|
474
|
+
* canComplete.resolve();
|
|
475
|
+
*
|
|
476
|
+
* const result = await fiber;
|
|
477
|
+
*
|
|
478
|
+
* expect(signalAbortedInAnalytics).toBe(false);
|
|
479
|
+
* // Analytics was sent despite abort
|
|
480
|
+
* expect(events).toEqual(["sent 123"]);
|
|
481
|
+
* expect(result).toEqual(ok());
|
|
482
|
+
* ```
|
|
483
|
+
*
|
|
484
|
+
* @group Abort masking
|
|
485
|
+
*/
|
|
486
|
+
// TODO: Clear AbortError from unabortable task results.
|
|
487
|
+
export const unabortable = /*#__PURE__*/ abortBehavior("unabortable");
|
|
488
|
+
/**
|
|
489
|
+
* Like {@link unabortable}, but provides `restore` to restore abortability for
|
|
490
|
+
* specific tasks.
|
|
491
|
+
*
|
|
492
|
+
* Tasks inherit abort masking from their parent. This means:
|
|
493
|
+
*
|
|
494
|
+
* - Tasks run inside `unabortableMask` are unabortable by default
|
|
495
|
+
* - Tasks wrapped with `restore()` restore the previous abortability
|
|
496
|
+
*
|
|
497
|
+
* @group Abort masking
|
|
498
|
+
*/
|
|
499
|
+
export const unabortableMask = (fn) => unabortable((run) => fn(abortBehavior(AbortMask.orThrow(decrement(run.abortMask))))(run));
|
|
500
|
+
const defaultConcurrency = 1;
|
|
501
|
+
const concurrencyBehaviorSymbol = Symbol("evolu.Task.concurrencyBehavior");
|
|
502
|
+
const getConcurrencyBehavior = (task) => task[concurrencyBehaviorSymbol];
|
|
503
|
+
export function concurrently(concurrencyOrTask, taskOrFallback) {
|
|
504
|
+
const isTask = isFunction(concurrencyOrTask);
|
|
505
|
+
const task = isTask ? concurrencyOrTask : taskOrFallback;
|
|
506
|
+
return Object.assign((run) => run(task), {
|
|
507
|
+
[concurrencyBehaviorSymbol]: isTask ? maxPositiveInt : concurrencyOrTask,
|
|
508
|
+
});
|
|
113
509
|
}
|
|
114
510
|
/**
|
|
115
|
-
*
|
|
511
|
+
* Yields execution to allow other work to proceed.
|
|
512
|
+
*
|
|
513
|
+
* Long-running JavaScript blocks the main thread. In browsers, this makes the
|
|
514
|
+
* UI unresponsive (user interactions, animations). In Node.js, it prevents I/O
|
|
515
|
+
* callbacks, timers, and other requests from being handled. Inserting yield
|
|
516
|
+
* points lets the runtime process high-priority work between chunks of code.
|
|
517
|
+
*
|
|
518
|
+
* Uses `scheduler.yield()` in browsers for optimal main thread scheduling,
|
|
519
|
+
* falls back to `setImmediate` in Node.js, or `setTimeout` elsewhere.
|
|
116
520
|
*
|
|
117
521
|
* ### Example
|
|
118
522
|
*
|
|
119
523
|
* ```ts
|
|
120
|
-
* const
|
|
121
|
-
*
|
|
524
|
+
* const processLargeArray: Task<void, never> = async (run) => {
|
|
525
|
+
* const { time } = run.deps;
|
|
526
|
+
* let lastYield = time.now();
|
|
527
|
+
*
|
|
528
|
+
* for (const item of largeArray) {
|
|
529
|
+
* processItem(item);
|
|
530
|
+
*
|
|
531
|
+
* // Yield periodically to keep UI responsive
|
|
532
|
+
* if (time.now() - lastYield > msLongTask) {
|
|
533
|
+
* const r = await run(yieldNow);
|
|
534
|
+
* if (!r.ok) return r;
|
|
535
|
+
* lastYield = time.now();
|
|
536
|
+
* }
|
|
537
|
+
* }
|
|
122
538
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* const result2 = await wait("10ms")(controller);
|
|
126
|
-
* result2 satisfies Result<void, AbortError>;
|
|
539
|
+
* return ok();
|
|
540
|
+
* };
|
|
127
541
|
* ```
|
|
542
|
+
*
|
|
543
|
+
* Recursive tasks also benefit from periodic yields — without them, deep
|
|
544
|
+
* recursion overflows the call stack:
|
|
545
|
+
*
|
|
546
|
+
* ```ts
|
|
547
|
+
* const processRecursive =
|
|
548
|
+
* (count: number, index: number, sum: number): Task<number> =>
|
|
549
|
+
* async (run) => {
|
|
550
|
+
* if (index >= count) return ok(sum);
|
|
551
|
+
*
|
|
552
|
+
* // Yield periodically to break synchronous call chains.
|
|
553
|
+
* if (index > 0 && index % 1000 === 0) {
|
|
554
|
+
* const y = await run(yieldNow);
|
|
555
|
+
* if (!y.ok) return y;
|
|
556
|
+
* }
|
|
557
|
+
*
|
|
558
|
+
* // Direct tail-call: no fiber overhead, stack-safe thanks to yieldNow.
|
|
559
|
+
* return await processRecursive(count, index + 1, sum + index)(run);
|
|
560
|
+
* };
|
|
561
|
+
* ```
|
|
562
|
+
*
|
|
563
|
+
* @group Composition
|
|
564
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield
|
|
565
|
+
* @see https://web.dev/articles/optimize-long-tasks
|
|
128
566
|
*/
|
|
129
|
-
export const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}));
|
|
567
|
+
export const yieldNow = () => tryAsync(() => yieldImpl(), // TODO: yieldImpl(run.signal)
|
|
568
|
+
(reason) => createAbortError(reason));
|
|
569
|
+
const scheduler = globalThis.scheduler;
|
|
570
|
+
const yieldImpl = typeof scheduler?.yield === "function"
|
|
571
|
+
? () => scheduler.yield()
|
|
572
|
+
: typeof setImmediate !== "undefined"
|
|
573
|
+
? () => new Promise((resolve) => setImmediate(resolve))
|
|
574
|
+
: () => new Promise((r) => setTimeout(r, 0)); // Safari
|
|
138
575
|
/**
|
|
139
|
-
*
|
|
576
|
+
* Creates a {@link Task} from a callback-based API.
|
|
577
|
+
*
|
|
578
|
+
* Use this to wrap callback-style APIs (event listeners, Node.js callbacks,
|
|
579
|
+
* etc.) into Tasks with proper abort handling.
|
|
580
|
+
*
|
|
581
|
+
* Optionally return a teardown function that runs on abort.
|
|
140
582
|
*
|
|
141
583
|
* ### Example
|
|
142
584
|
*
|
|
143
585
|
* ```ts
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
586
|
+
* // The sleep helper is implemented using callback:
|
|
587
|
+
* const sleep = (duration: Duration): Task<void> =>
|
|
588
|
+
* callback(({ ok, deps: { time } }) => {
|
|
589
|
+
* const id = time.setTimeout(ok, durationToMillis(duration));
|
|
590
|
+
* return () => time.clearTimeout(id);
|
|
591
|
+
* });
|
|
148
592
|
*
|
|
149
|
-
* //
|
|
150
|
-
* const
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* (error): FetchError => ({ type: "FetchError", error }),
|
|
155
|
-
* ),
|
|
156
|
-
* );
|
|
593
|
+
* // Wrap an event listener — use signal directly
|
|
594
|
+
* const waitForClick = (element: HTMLElement): Task<MouseEvent> =>
|
|
595
|
+
* callback(({ ok, signal }) => {
|
|
596
|
+
* element.addEventListener("click", ok, { once: true, signal });
|
|
597
|
+
* });
|
|
157
598
|
*
|
|
158
|
-
* //
|
|
159
|
-
*
|
|
599
|
+
* // Wrap Node.js callback API
|
|
600
|
+
* const readFile = (path: string): Task<string, NodeJS.ErrnoException> =>
|
|
601
|
+
* callback(({ ok, err }) => {
|
|
602
|
+
* fs.readFile(path, "utf8", (error, data) => {
|
|
603
|
+
* if (error) err(error);
|
|
604
|
+
* else ok(data);
|
|
605
|
+
* });
|
|
606
|
+
* });
|
|
607
|
+
* ```
|
|
160
608
|
*
|
|
161
|
-
*
|
|
609
|
+
* @group Composition
|
|
610
|
+
*/
|
|
611
|
+
export const callback = (callback) => (run) => new Promise((resolve) => {
|
|
612
|
+
const teardown = callback({
|
|
613
|
+
ok: (value) => resolve(ok(value)),
|
|
614
|
+
err: (error) => resolve(err(error)),
|
|
615
|
+
signal: run.signal,
|
|
616
|
+
deps: run.deps,
|
|
617
|
+
});
|
|
618
|
+
run.onAbort((reason) => {
|
|
619
|
+
if (teardown)
|
|
620
|
+
teardown();
|
|
621
|
+
resolve(err(createAbortError(reason)));
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
/**
|
|
625
|
+
* Pauses execution for a specified duration.
|
|
162
626
|
*
|
|
163
|
-
*
|
|
164
|
-
* result1 satisfies Result<Response, FetchError | TimeoutError>;
|
|
627
|
+
* ### Example
|
|
165
628
|
*
|
|
166
|
-
*
|
|
167
|
-
* const
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* );
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* FetchError | TimeoutError | AbortError
|
|
174
|
-
* >;
|
|
629
|
+
* ```ts
|
|
630
|
+
* const task: Task<void> = async (run) => {
|
|
631
|
+
* console.log("Starting...");
|
|
632
|
+
* await run(sleep("1s"));
|
|
633
|
+
* console.log("Done after 1 second");
|
|
634
|
+
* return ok();
|
|
635
|
+
* };
|
|
175
636
|
* ```
|
|
637
|
+
*
|
|
638
|
+
* @group Composition
|
|
176
639
|
*/
|
|
177
|
-
export const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const signal = combineSignal(context, timeoutSignal);
|
|
181
|
-
const result = await task({ signal });
|
|
182
|
-
if (timeoutSignal.aborted) {
|
|
183
|
-
return err({ type: "TimeoutError", timeoutMs });
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
640
|
+
export const sleep = (duration) => callback(({ ok, deps: { time } }) => {
|
|
641
|
+
const id = time.setTimeout(ok, durationToMillis(duration));
|
|
642
|
+
return () => time.clearTimeout(id);
|
|
186
643
|
});
|
|
187
644
|
/**
|
|
188
|
-
*
|
|
645
|
+
* Returns a {@link Task} that completes first.
|
|
646
|
+
*
|
|
647
|
+
* Like
|
|
648
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race | Promise.race},
|
|
649
|
+
* the first Task to complete (whether success or failure) wins. All other Tasks
|
|
650
|
+
* are aborted. Use {@link any} if you need the first Task to succeed instead.
|
|
651
|
+
*
|
|
652
|
+
* Requires a non-empty array — racing zero Tasks has no meaningful result
|
|
653
|
+
* (there's no "first to complete" without participants). This is enforced at
|
|
654
|
+
* compile time for non-empty tuple types. For other arrays, guard with
|
|
655
|
+
* {@link isNonEmptyArray}:
|
|
656
|
+
*
|
|
657
|
+
* ```ts
|
|
658
|
+
* if (isNonEmptyArray(tasks)) {
|
|
659
|
+
* await run(race(tasks));
|
|
660
|
+
* }
|
|
661
|
+
* ```
|
|
662
|
+
*
|
|
663
|
+
* ### Example
|
|
664
|
+
*
|
|
665
|
+
* ```ts
|
|
666
|
+
* const fast: Task<string> = () => ok("fast");
|
|
667
|
+
* const slow: Task<string> = async (run) => {
|
|
668
|
+
* await run(sleep("10ms"));
|
|
669
|
+
* return ok("slow");
|
|
670
|
+
* };
|
|
671
|
+
*
|
|
672
|
+
* // First wins, others are aborted.
|
|
673
|
+
* const result = await run(race([fast, slow])); // ok("fast")
|
|
674
|
+
* ```
|
|
675
|
+
*
|
|
676
|
+
* Always runs with unlimited concurrency — a sequential race makes no sense
|
|
677
|
+
* since the first Task would always "win".
|
|
678
|
+
*
|
|
679
|
+
* @group Composition
|
|
680
|
+
*/
|
|
681
|
+
export const race = (tasks, { abortReason = raceLostError, } = {}) => concurrently(pool(tasks, { stopOn: "first", collect: false, abortReason }));
|
|
682
|
+
/**
|
|
683
|
+
* Abort reason for tasks that lose a {@link race}.
|
|
684
|
+
*
|
|
685
|
+
* @group Composition
|
|
686
|
+
*/
|
|
687
|
+
export const RaceLostError = /*#__PURE__*/ typed("RaceLostError");
|
|
688
|
+
/**
|
|
689
|
+
* {@link RaceLostError} used as abort reason in {@link race}.
|
|
690
|
+
*
|
|
691
|
+
* @group Composition
|
|
692
|
+
*/
|
|
693
|
+
export const raceLostError = { type: "RaceLostError" };
|
|
694
|
+
/**
|
|
695
|
+
* Wraps a {@link Task} with a time limit.
|
|
696
|
+
*
|
|
697
|
+
* Returns {@link TimeoutError} if the Task doesn't complete within the specified
|
|
698
|
+
* duration. The original Task is aborted when the timeout fires.
|
|
189
699
|
*
|
|
190
700
|
* ### Example
|
|
191
701
|
*
|
|
192
702
|
* ```ts
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
703
|
+
* const fetchWithTimeout = timeout(fetchData, "5s");
|
|
704
|
+
*
|
|
705
|
+
* const result = await run(fetchWithTimeout);
|
|
706
|
+
* if (!result.ok && result.error.type === "TimeoutError") {
|
|
707
|
+
* console.log("Request timed out");
|
|
196
708
|
* }
|
|
709
|
+
* ```
|
|
197
710
|
*
|
|
198
|
-
*
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
711
|
+
* @group Composition
|
|
712
|
+
*/
|
|
713
|
+
export const timeout = (task, duration, { abortReason = timeoutError, } = {}) => race([
|
|
714
|
+
task,
|
|
715
|
+
async (run) => {
|
|
716
|
+
await run(sleep(duration));
|
|
717
|
+
return err(timeoutError);
|
|
718
|
+
},
|
|
719
|
+
], { abortReason });
|
|
720
|
+
/**
|
|
721
|
+
* Typed error returned by {@link timeout} when a task exceeds its time limit.
|
|
206
722
|
*
|
|
207
|
-
*
|
|
208
|
-
|
|
723
|
+
* @group Composition
|
|
724
|
+
*/
|
|
725
|
+
export const TimeoutError = /*#__PURE__*/ typed("TimeoutError");
|
|
726
|
+
/**
|
|
727
|
+
* {@link TimeoutError} used as abort reason in {@link timeout}.
|
|
209
728
|
*
|
|
210
|
-
*
|
|
211
|
-
|
|
729
|
+
* @group Composition
|
|
730
|
+
*/
|
|
731
|
+
export const timeoutError = { type: "TimeoutError" };
|
|
732
|
+
/**
|
|
733
|
+
* Wraps a {@link Task} with retry logic.
|
|
734
|
+
*
|
|
735
|
+
* Retries the Task according to the {@link Schedule}'s rules. Use
|
|
736
|
+
* {@link RetryOptions.retryable} to filter which errors should trigger retries.
|
|
737
|
+
*
|
|
738
|
+
* All non-abort errors are wrapped in {@link RetryError}:
|
|
739
|
+
*
|
|
740
|
+
* - Task succeeds → `ok(value)`
|
|
741
|
+
* - Task returns {@link AbortError} → `err(AbortError)` — passed through, no
|
|
742
|
+
* retry, no wrapping
|
|
743
|
+
* - Task returns any other error → retry until schedule exhausted or `retryable`
|
|
744
|
+
* returns false → `err(RetryError)` with `cause` = the last error
|
|
745
|
+
*
|
|
746
|
+
* The `RetryError` is informative: "I tried N times, here's why I finally gave
|
|
747
|
+
* up" — and `cause` contains the actual underlying error.
|
|
212
748
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
749
|
+
* ### Example
|
|
750
|
+
*
|
|
751
|
+
* ```ts
|
|
752
|
+
* import {
|
|
753
|
+
* exponential,
|
|
754
|
+
* jitter,
|
|
755
|
+
* maxDelay,
|
|
756
|
+
* retry,
|
|
757
|
+
* take,
|
|
758
|
+
* } from "@evolu/common";
|
|
759
|
+
*
|
|
760
|
+
* const fetchWithRetry = retry(
|
|
761
|
+
* fetchData,
|
|
762
|
+
* // A jittered, capped, limited exponential backoff.
|
|
763
|
+
* jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
764
|
+
* );
|
|
765
|
+
*
|
|
766
|
+
* const result = await run(fetchWithRetry);
|
|
767
|
+
* if (!result.ok) {
|
|
768
|
+
* if (AbortError.is(result.error)) {
|
|
769
|
+
* // Was aborted externally
|
|
770
|
+
* } else {
|
|
771
|
+
* // RetryError — failed after retrying
|
|
772
|
+
* console.log(`Failed after ${result.error.attempts} attempts`);
|
|
773
|
+
* console.log(`Last error:`, result.error.cause);
|
|
774
|
+
* }
|
|
775
|
+
* }
|
|
776
|
+
* ```
|
|
777
|
+
*
|
|
778
|
+
* The schedule receives the error as input, enabling error-aware strategies
|
|
779
|
+
* like stopping on fatal errors:
|
|
215
780
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
781
|
+
* ```ts
|
|
782
|
+
* import { whileScheduleInput } from "@evolu/common";
|
|
783
|
+
*
|
|
784
|
+
* // Don't retry fatal errors
|
|
785
|
+
* const smartRetry = retry(
|
|
786
|
+
* fetchData,
|
|
787
|
+
* whileScheduleInput((e: FetchError) => e.type !== "FatalError")(
|
|
788
|
+
* take(5)(spaced("1s")),
|
|
789
|
+
* ),
|
|
220
790
|
* );
|
|
221
|
-
* result2 satisfies Result<
|
|
222
|
-
* Response,
|
|
223
|
-
* FetchError | RetryError<FetchError> | AbortError
|
|
224
|
-
* >;
|
|
225
791
|
* ```
|
|
792
|
+
*
|
|
793
|
+
* @group Composition
|
|
794
|
+
* @see {@link RetryOptions}
|
|
226
795
|
*/
|
|
227
|
-
export const retry = (
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
796
|
+
export const retry = (task, schedule, { retryable = lazyTrue, onRetry, } = {}) => async (run) => {
|
|
797
|
+
const step = schedule(run.deps);
|
|
798
|
+
let attempt = onePositiveInt;
|
|
799
|
+
let error;
|
|
800
|
+
for (;;) {
|
|
801
|
+
if (error !== undefined) {
|
|
802
|
+
const scheduleResult = step(error);
|
|
803
|
+
if (!scheduleResult.ok) {
|
|
804
|
+
return err({
|
|
805
|
+
type: "RetryError",
|
|
806
|
+
cause: error,
|
|
807
|
+
attempts: PositiveInt.orThrow(decrement(attempt)),
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
const [output, delay] = scheduleResult.value;
|
|
811
|
+
onRetry?.({
|
|
812
|
+
error,
|
|
813
|
+
attempt: PositiveInt.orThrow(decrement(attempt)),
|
|
814
|
+
output,
|
|
815
|
+
delay,
|
|
816
|
+
});
|
|
817
|
+
if (delay > 0) {
|
|
818
|
+
const sleepResult = await run(sleep(delay));
|
|
819
|
+
if (!sleepResult.ok)
|
|
820
|
+
return sleepResult;
|
|
821
|
+
}
|
|
237
822
|
}
|
|
238
|
-
|
|
239
|
-
if (
|
|
823
|
+
const result = await run(task);
|
|
824
|
+
if (result.ok)
|
|
825
|
+
return result;
|
|
826
|
+
if (AbortError.is(result.error))
|
|
240
827
|
return err(result.error);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (attempt > maxRetries || !retryable(result.error)) {
|
|
828
|
+
error = result.error;
|
|
829
|
+
if (!retryable(error)) {
|
|
244
830
|
return err({
|
|
245
831
|
type: "RetryError",
|
|
246
|
-
cause:
|
|
832
|
+
cause: error,
|
|
247
833
|
attempts: attempt,
|
|
248
834
|
});
|
|
249
835
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
836
|
+
attempt = PositiveInt.orThrow(increment(attempt));
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Repeats a {@link Task} according to a {@link Schedule}.
|
|
841
|
+
*
|
|
842
|
+
* Runs the Task, then checks the schedule to determine if it should repeat. The
|
|
843
|
+
* schedule controls how many repetitions occur and the delay between them.
|
|
844
|
+
* Continues until the schedule returns `Err(Done<void>)` or the Task fails.
|
|
845
|
+
*
|
|
846
|
+
* With `take(n)`, the task runs n+1 times (initial run plus n repetitions).
|
|
847
|
+
*
|
|
848
|
+
* Also works with {@link NextTask} — when the Task returns `Err(Done<D>)`,
|
|
849
|
+
* repeat stops and propagates the done signal.
|
|
850
|
+
*
|
|
851
|
+
* ### Example
|
|
852
|
+
*
|
|
853
|
+
* ```ts
|
|
854
|
+
* import { fixed, take } from "@evolu/common/schedule";
|
|
855
|
+
* import { repeat } from "@evolu/common";
|
|
856
|
+
*
|
|
857
|
+
* // Heartbeat every 30 seconds (runs forever until aborted)
|
|
858
|
+
* const heartbeat = repeat(sendHeartbeat, fixed("30s"));
|
|
859
|
+
*
|
|
860
|
+
* // Poll 4 times total (initial + 3 repetitions), 1 second apart
|
|
861
|
+
* const poll = repeat(checkStatus, take(3)(fixed("1s")));
|
|
862
|
+
*
|
|
863
|
+
* // Process queue items until empty (NextTask pattern)
|
|
864
|
+
* const processQueue: NextTask<Item, ProcessError, void> = async (run) => {
|
|
865
|
+
* const item = queue.dequeue();
|
|
866
|
+
* if (!item) return err(done()); // Queue empty, stop
|
|
867
|
+
* await process(item);
|
|
868
|
+
* return ok(item);
|
|
869
|
+
* };
|
|
870
|
+
*
|
|
871
|
+
* const result = await run(repeat(processQueue, fixed("100ms")));
|
|
872
|
+
* if (!result.ok && result.error.type === "Done") {
|
|
873
|
+
* console.log("Queue exhausted");
|
|
874
|
+
* }
|
|
875
|
+
* ```
|
|
876
|
+
*
|
|
877
|
+
* @group Composition
|
|
878
|
+
*/
|
|
879
|
+
export const repeat = (task, schedule, { repeatable = lazyTrue, onRepeat, } = {}) => async (run) => {
|
|
880
|
+
const step = schedule(run.deps);
|
|
881
|
+
let lastResult;
|
|
882
|
+
let attempt = onePositiveInt;
|
|
883
|
+
for (;;) {
|
|
884
|
+
const result = await run(task);
|
|
885
|
+
if (!result.ok)
|
|
886
|
+
return result;
|
|
887
|
+
lastResult = result;
|
|
888
|
+
if (!repeatable(result.value))
|
|
889
|
+
return lastResult;
|
|
890
|
+
const next = step(result.value);
|
|
891
|
+
if (!next.ok)
|
|
892
|
+
break;
|
|
893
|
+
const [output, delay] = next.value;
|
|
894
|
+
onRepeat?.({
|
|
895
|
+
value: result.value,
|
|
896
|
+
attempt,
|
|
897
|
+
output,
|
|
898
|
+
delay,
|
|
899
|
+
});
|
|
900
|
+
attempt = PositiveInt.orThrow(increment(attempt));
|
|
901
|
+
if (delay > 0) {
|
|
902
|
+
const sleepResult = await run(sleep(delay));
|
|
903
|
+
if (!sleepResult.ok)
|
|
904
|
+
return sleepResult;
|
|
266
905
|
}
|
|
267
906
|
}
|
|
268
|
-
|
|
907
|
+
return lastResult;
|
|
908
|
+
};
|
|
269
909
|
/**
|
|
270
|
-
* Creates a
|
|
271
|
-
* count.
|
|
910
|
+
* Creates a {@link Deferred}.
|
|
272
911
|
*
|
|
273
|
-
*
|
|
274
|
-
|
|
275
|
-
|
|
912
|
+
* @group Concurrency primitives
|
|
913
|
+
*/
|
|
914
|
+
export const createDeferred = () => {
|
|
915
|
+
let resolved = null;
|
|
916
|
+
const resolvers = new Set();
|
|
917
|
+
const resolve = (result) => {
|
|
918
|
+
if (resolved !== null)
|
|
919
|
+
return false;
|
|
920
|
+
resolved = result;
|
|
921
|
+
for (const resolver of resolvers)
|
|
922
|
+
resolver(result);
|
|
923
|
+
resolvers.clear();
|
|
924
|
+
return true;
|
|
925
|
+
};
|
|
926
|
+
return {
|
|
927
|
+
task: (run) => {
|
|
928
|
+
if (resolved !== null)
|
|
929
|
+
return resolved;
|
|
930
|
+
return new Promise((resolvePromise) => {
|
|
931
|
+
const resolve = (result) => {
|
|
932
|
+
resolvers.delete(resolve);
|
|
933
|
+
resolvePromise(result);
|
|
934
|
+
};
|
|
935
|
+
resolvers.add(resolve);
|
|
936
|
+
run.onAbort((reason) => {
|
|
937
|
+
resolve(err(createAbortError(reason)));
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
},
|
|
941
|
+
resolve,
|
|
942
|
+
[Symbol.dispose]: () => {
|
|
943
|
+
resolve(err(deferredDisposedError));
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
/**
|
|
948
|
+
* Abort reason used when a {@link Deferred} is disposed.
|
|
276
949
|
*
|
|
277
|
-
*
|
|
278
|
-
|
|
950
|
+
* @group Concurrency primitives
|
|
951
|
+
*/
|
|
952
|
+
export const DeferredDisposedError = /*#__PURE__*/ typed("DeferredDisposedError");
|
|
953
|
+
/**
|
|
954
|
+
* {@link DeferredDisposedError} used as abort reason in {@link createDeferred}.
|
|
955
|
+
*
|
|
956
|
+
* @group Concurrency primitives
|
|
957
|
+
*/
|
|
958
|
+
export const deferredDisposedError = {
|
|
959
|
+
type: "DeferredDisposedError",
|
|
960
|
+
};
|
|
961
|
+
/**
|
|
962
|
+
* Creates a {@link Gate} that starts closed.
|
|
963
|
+
*
|
|
964
|
+
* Useful for "stop/go" logic where multiple Tasks need to wait for a state
|
|
965
|
+
* change.
|
|
279
966
|
*
|
|
280
967
|
* ### Example
|
|
281
968
|
*
|
|
282
969
|
* ```ts
|
|
283
|
-
*
|
|
284
|
-
* const semaphore = createSemaphore(PositiveInt.orThrow(3));
|
|
970
|
+
* const networkGate = createGate();
|
|
285
971
|
*
|
|
286
|
-
*
|
|
287
|
-
* const
|
|
972
|
+
* // Pause processing when offline
|
|
973
|
+
* const onOffline = () => networkGate.close();
|
|
288
974
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* currentConcurrent++;
|
|
292
|
-
* events.push(`start ${id} (concurrent: ${currentConcurrent})`);
|
|
975
|
+
* // Resume processing when online
|
|
976
|
+
* const onOnline = () => networkGate.open();
|
|
293
977
|
*
|
|
294
|
-
*
|
|
978
|
+
* const syncLoop = async (run) => {
|
|
979
|
+
* while (true) {
|
|
980
|
+
* // Blocks here whenever the gate is closed
|
|
981
|
+
* await run(networkGate.wait);
|
|
982
|
+
* await run(uploadNextItem);
|
|
983
|
+
* }
|
|
984
|
+
* };
|
|
985
|
+
* ```
|
|
295
986
|
*
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
987
|
+
* @group Concurrency primitives
|
|
988
|
+
*/
|
|
989
|
+
export const createGate = () => {
|
|
990
|
+
let isOpen = false;
|
|
991
|
+
let disposed = false;
|
|
992
|
+
let deferred = createDeferred();
|
|
993
|
+
return {
|
|
994
|
+
wait: (run) => {
|
|
995
|
+
if (disposed)
|
|
996
|
+
return err(deferredDisposedError);
|
|
997
|
+
if (isOpen)
|
|
998
|
+
return ok();
|
|
999
|
+
return run(deferred.task);
|
|
1000
|
+
},
|
|
1001
|
+
open: () => {
|
|
1002
|
+
if (disposed || isOpen)
|
|
1003
|
+
return;
|
|
1004
|
+
isOpen = true;
|
|
1005
|
+
deferred.resolve(ok());
|
|
1006
|
+
},
|
|
1007
|
+
close: () => {
|
|
1008
|
+
if (disposed || !isOpen)
|
|
1009
|
+
return;
|
|
1010
|
+
isOpen = false;
|
|
1011
|
+
deferred = createDeferred();
|
|
1012
|
+
},
|
|
1013
|
+
isOpen: () => isOpen,
|
|
1014
|
+
[Symbol.dispose]: () => {
|
|
1015
|
+
if (disposed)
|
|
1016
|
+
return;
|
|
1017
|
+
disposed = true;
|
|
1018
|
+
deferred[Symbol.dispose]();
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
};
|
|
1022
|
+
/**
|
|
1023
|
+
* Creates a {@link Semaphore} that limits concurrent {@link Task}s.
|
|
1024
|
+
*
|
|
1025
|
+
* ### Example
|
|
1026
|
+
*
|
|
1027
|
+
* ```ts
|
|
1028
|
+
* await using run = createRun();
|
|
1029
|
+
*
|
|
1030
|
+
* const semaphore = createSemaphore(PositiveInt.orThrow(2));
|
|
300
1031
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
1032
|
+
* const fetchUser =
|
|
1033
|
+
* (id: string): Task<string> =>
|
|
1034
|
+
* async (run) => {
|
|
1035
|
+
* const { console } = run.deps;
|
|
1036
|
+
* console.log("[demo]", "start", id);
|
|
1037
|
+
* const slept = await run(sleep("10ms"));
|
|
1038
|
+
* if (!slept.ok) return slept;
|
|
1039
|
+
* console.log("[demo]", "end", id);
|
|
1040
|
+
* return ok(`user:${id}`);
|
|
1041
|
+
* };
|
|
1042
|
+
*
|
|
1043
|
+
* const fetchWithPermit = (id: string) =>
|
|
1044
|
+
* semaphore.withPermit(fetchUser(id));
|
|
1045
|
+
*
|
|
1046
|
+
* await Promise.all([
|
|
1047
|
+
* run(fetchWithPermit("1")),
|
|
1048
|
+
* run(fetchWithPermit("2")),
|
|
1049
|
+
* run(fetchWithPermit("3")),
|
|
308
1050
|
* ]);
|
|
309
1051
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* "end 1 (concurrent: 2)",
|
|
317
|
-
* "start 4 (concurrent: 3)",
|
|
318
|
-
* "end 2 (concurrent: 2)",
|
|
319
|
-
* "start 5 (concurrent: 3)",
|
|
320
|
-
* "end 3 (concurrent: 2)",
|
|
321
|
-
* "end 4 (concurrent: 1)",
|
|
322
|
-
* "end 5 (concurrent: 0)",
|
|
323
|
-
* ]
|
|
324
|
-
* `);
|
|
1052
|
+
* // [demo] start 1
|
|
1053
|
+
* // [demo] start 2
|
|
1054
|
+
* // [demo] end 1
|
|
1055
|
+
* // [demo] start 3
|
|
1056
|
+
* // [demo] end 2
|
|
1057
|
+
* // [demo] end 3
|
|
325
1058
|
* ```
|
|
1059
|
+
*
|
|
1060
|
+
* @group Concurrency primitives
|
|
326
1061
|
*/
|
|
327
|
-
export const createSemaphore = (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1062
|
+
export const createSemaphore = (permits) => {
|
|
1063
|
+
const fibers = new Set();
|
|
1064
|
+
const waiters = [];
|
|
1065
|
+
let taken = zeroNonNegativeInt;
|
|
1066
|
+
let disposed = false;
|
|
1067
|
+
const withPermits = (requestedPermits) => (task) => async (run) => {
|
|
1068
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
1069
|
+
try {
|
|
1070
|
+
const requested = PositiveInt.orThrow(requestedPermits);
|
|
1071
|
+
assert(requested <= permits, "Requested permits must not exceed semaphore capacity.");
|
|
1072
|
+
if (disposed)
|
|
1073
|
+
return err(semaphoreDisposedAbortError);
|
|
1074
|
+
if (waiters.length > 0 || taken + requested > permits) {
|
|
1075
|
+
const waiter = Promise.withResolvers();
|
|
1076
|
+
const waiting = {
|
|
1077
|
+
permits: requested,
|
|
1078
|
+
resolve: waiter.resolve,
|
|
1079
|
+
};
|
|
1080
|
+
waiters.push(waiting);
|
|
1081
|
+
run.onAbort((reason) => {
|
|
1082
|
+
const i = waiters.indexOf(waiting);
|
|
1083
|
+
if (i >= 0)
|
|
1084
|
+
waiters.splice(i, 1);
|
|
1085
|
+
waiter.resolve(err(createAbortError(reason)));
|
|
1086
|
+
});
|
|
1087
|
+
const permit = await waiter.promise;
|
|
1088
|
+
if (!permit.ok)
|
|
1089
|
+
return permit;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
taken = NonNegativeInt.orThrow(taken + requested);
|
|
1093
|
+
}
|
|
1094
|
+
let fiber = null;
|
|
1095
|
+
const _ = __addDisposableResource(env_1, {
|
|
1096
|
+
[Symbol.dispose]: () => {
|
|
1097
|
+
if (fiber)
|
|
1098
|
+
fibers.delete(fiber);
|
|
1099
|
+
taken = NonNegativeInt.orThrow(taken - requested);
|
|
1100
|
+
while (waiters.length > 0) {
|
|
1101
|
+
const waiter = waiters[0];
|
|
1102
|
+
if (taken + waiter.permits > permits)
|
|
1103
|
+
break;
|
|
1104
|
+
waiters.shift();
|
|
1105
|
+
taken = NonNegativeInt.orThrow(taken + waiter.permits);
|
|
1106
|
+
waiter.resolve(ok());
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
}, false);
|
|
1110
|
+
fiber = run(task);
|
|
1111
|
+
fibers.add(fiber);
|
|
1112
|
+
return await fiber;
|
|
336
1113
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
};
|
|
341
|
-
const release = () => {
|
|
342
|
-
if (isNonEmptyArray(waitingQueue)) {
|
|
343
|
-
shiftArray(waitingQueue)();
|
|
1114
|
+
catch (e_1) {
|
|
1115
|
+
env_1.error = e_1;
|
|
1116
|
+
env_1.hasError = true;
|
|
344
1117
|
}
|
|
345
|
-
|
|
346
|
-
|
|
1118
|
+
finally {
|
|
1119
|
+
__disposeResources(env_1);
|
|
347
1120
|
}
|
|
348
1121
|
};
|
|
349
1122
|
return {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const signal = combineSignal(context, semaphoreController.signal);
|
|
360
|
-
const result = await task({ signal });
|
|
361
|
-
release();
|
|
362
|
-
return result;
|
|
1123
|
+
withPermits,
|
|
1124
|
+
withPermit: (task) => withPermits(1)(task),
|
|
1125
|
+
snapshot: () => ({
|
|
1126
|
+
permits,
|
|
1127
|
+
taken,
|
|
1128
|
+
waiting: NonNegativeInt.orThrow(waiters.length),
|
|
1129
|
+
available: NonNegativeInt.orThrow(permits - taken),
|
|
1130
|
+
isIdle: taken === 0 && waiters.length === 0,
|
|
1131
|
+
disposed,
|
|
363
1132
|
}),
|
|
364
1133
|
[Symbol.dispose]: () => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1134
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
1135
|
+
try {
|
|
1136
|
+
if (disposed)
|
|
1137
|
+
return;
|
|
1138
|
+
disposed = true;
|
|
1139
|
+
const stack = __addDisposableResource(env_2, new DisposableStack(), false);
|
|
1140
|
+
for (const fiber of fibers) {
|
|
1141
|
+
stack.adopt(fiber, (fiber) => {
|
|
1142
|
+
fiber.abort(semaphoreDisposedError);
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
for (const waiter of waiters) {
|
|
1146
|
+
waiter.resolve(err(semaphoreDisposedAbortError));
|
|
1147
|
+
}
|
|
1148
|
+
waiters.length = 0;
|
|
1149
|
+
}
|
|
1150
|
+
catch (e_2) {
|
|
1151
|
+
env_2.error = e_2;
|
|
1152
|
+
env_2.hasError = true;
|
|
1153
|
+
}
|
|
1154
|
+
finally {
|
|
1155
|
+
__disposeResources(env_2);
|
|
373
1156
|
}
|
|
374
1157
|
},
|
|
375
1158
|
};
|
|
376
1159
|
};
|
|
377
1160
|
/**
|
|
378
|
-
*
|
|
1161
|
+
* Abort reason used when a {@link Semaphore} is disposed.
|
|
379
1162
|
*
|
|
380
|
-
*
|
|
381
|
-
|
|
1163
|
+
* @group Concurrency primitives
|
|
1164
|
+
*/
|
|
1165
|
+
export const SemaphoreDisposedError = /*#__PURE__*/ typed("SemaphoreDisposedError");
|
|
1166
|
+
/**
|
|
1167
|
+
* {@link SemaphoreDisposedError} used as abort reason in {@link createSemaphore}.
|
|
382
1168
|
*
|
|
383
|
-
*
|
|
1169
|
+
* @group Concurrency primitives
|
|
1170
|
+
*/
|
|
1171
|
+
export const semaphoreDisposedError = {
|
|
1172
|
+
type: "SemaphoreDisposedError",
|
|
1173
|
+
};
|
|
1174
|
+
const semaphoreDisposedAbortError = createAbortError(semaphoreDisposedError);
|
|
1175
|
+
export function createSemaphoreByKey(permits, { lookup = identity } = {}) {
|
|
1176
|
+
const semaphoresByKey = createLookupMap({
|
|
1177
|
+
lookup,
|
|
1178
|
+
});
|
|
1179
|
+
let disposed = false;
|
|
1180
|
+
const withPermits = (key, requestedPermits) => (task) => async (run) => {
|
|
1181
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1182
|
+
try {
|
|
1183
|
+
if (disposed)
|
|
1184
|
+
return err(semaphoreDisposedAbortError);
|
|
1185
|
+
let semaphore = semaphoresByKey.get(key);
|
|
1186
|
+
if (!semaphore) {
|
|
1187
|
+
semaphore = createSemaphore(permits);
|
|
1188
|
+
semaphoresByKey.set(key, semaphore);
|
|
1189
|
+
}
|
|
1190
|
+
const _ = __addDisposableResource(env_3, {
|
|
1191
|
+
[Symbol.dispose]: () => {
|
|
1192
|
+
const snapshot = semaphore.snapshot();
|
|
1193
|
+
if (snapshot.isIdle) {
|
|
1194
|
+
semaphoresByKey.delete(key);
|
|
1195
|
+
semaphore[Symbol.dispose]();
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
}, false);
|
|
1199
|
+
return await run(semaphore.withPermits(requestedPermits)(task));
|
|
1200
|
+
}
|
|
1201
|
+
catch (e_3) {
|
|
1202
|
+
env_3.error = e_3;
|
|
1203
|
+
env_3.hasError = true;
|
|
1204
|
+
}
|
|
1205
|
+
finally {
|
|
1206
|
+
__disposeResources(env_3);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
return {
|
|
1210
|
+
withPermit: (key, task) => withPermits(key, 1)(task),
|
|
1211
|
+
withPermits,
|
|
1212
|
+
snapshot: (key) => semaphoresByKey.get(key)?.snapshot() ?? null,
|
|
1213
|
+
[Symbol.dispose]: () => {
|
|
1214
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
1215
|
+
try {
|
|
1216
|
+
if (disposed)
|
|
1217
|
+
return;
|
|
1218
|
+
disposed = true;
|
|
1219
|
+
const stack = __addDisposableResource(env_4, new DisposableStack(), false);
|
|
1220
|
+
for (const semaphore of semaphoresByKey.values()) {
|
|
1221
|
+
stack.use(semaphore);
|
|
1222
|
+
}
|
|
1223
|
+
semaphoresByKey.clear();
|
|
1224
|
+
}
|
|
1225
|
+
catch (e_4) {
|
|
1226
|
+
env_4.error = e_4;
|
|
1227
|
+
env_4.hasError = true;
|
|
1228
|
+
}
|
|
1229
|
+
finally {
|
|
1230
|
+
__disposeResources(env_4);
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Creates a {@link Mutex}.
|
|
384
1237
|
*
|
|
385
|
-
*
|
|
386
|
-
* const mutex = createMutex();
|
|
387
|
-
*
|
|
388
|
-
* const updateTask = (id: number) =>
|
|
389
|
-
* toTask((context) =>
|
|
390
|
-
* tryAsync(
|
|
391
|
-
* () => updateSharedResource(id, context),
|
|
392
|
-
* (error): UpdateError => ({ type: "UpdateError", error }),
|
|
393
|
-
* ),
|
|
394
|
-
* );
|
|
395
|
-
*
|
|
396
|
-
* // These Tasks will execute one at a time
|
|
397
|
-
* const results = await Promise.all([
|
|
398
|
-
* mutex.withLock(updateTask(1))(),
|
|
399
|
-
* mutex.withLock(updateTask(2))(),
|
|
400
|
-
* mutex.withLock(updateTask(3))(),
|
|
401
|
-
* ]);
|
|
402
|
-
* ```
|
|
1238
|
+
* @group Concurrency primitives
|
|
403
1239
|
*/
|
|
404
1240
|
export const createMutex = () => {
|
|
405
|
-
const
|
|
1241
|
+
const semaphore = createSemaphore(onePositiveInt);
|
|
1242
|
+
return {
|
|
1243
|
+
withLock: semaphore.withPermit,
|
|
1244
|
+
snapshot: semaphore.snapshot,
|
|
1245
|
+
[Symbol.dispose]: semaphore[Symbol.dispose],
|
|
1246
|
+
};
|
|
1247
|
+
};
|
|
1248
|
+
export function createMutexByKey({ lookup = identity, } = {}) {
|
|
1249
|
+
const semaphoreByKey = createSemaphoreByKey(onePositiveInt, {
|
|
1250
|
+
lookup,
|
|
1251
|
+
});
|
|
1252
|
+
return {
|
|
1253
|
+
withLock: (key, task) => semaphoreByKey.withPermit(key, task),
|
|
1254
|
+
snapshot: semaphoreByKey.snapshot,
|
|
1255
|
+
[Symbol.dispose]: semaphoreByKey[Symbol.dispose],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Creates a {@link MutexRef} with the given initial immutable value.
|
|
1260
|
+
*
|
|
1261
|
+
* @group Concurrency primitives
|
|
1262
|
+
*/
|
|
1263
|
+
export const createMutexRef = (initialValue) => {
|
|
1264
|
+
const ref = createRef(initialValue);
|
|
1265
|
+
const mutex = createMutex();
|
|
406
1266
|
return {
|
|
407
|
-
|
|
1267
|
+
get: mutex.withLock(() => ok(ref.get())),
|
|
1268
|
+
set: (value) => mutex.withLock(() => {
|
|
1269
|
+
ref.set(value);
|
|
1270
|
+
return ok();
|
|
1271
|
+
}),
|
|
1272
|
+
getAndSet: (value) => mutex.withLock(() => ok(ref.getAndSet(value))),
|
|
1273
|
+
setAndGet: (value) => mutex.withLock(() => ok(ref.setAndGet(value))),
|
|
1274
|
+
update: (updater) => mutex.withLock(async (run) => {
|
|
1275
|
+
const nextValue = await run(updater(ref.get()));
|
|
1276
|
+
if (!nextValue.ok)
|
|
1277
|
+
return nextValue;
|
|
1278
|
+
ref.set(nextValue.value);
|
|
1279
|
+
return ok();
|
|
1280
|
+
}),
|
|
1281
|
+
getAndUpdate: (updater) => mutex.withLock(async (run) => {
|
|
1282
|
+
const previousValue = ref.get();
|
|
1283
|
+
const nextValue = await run(updater(previousValue));
|
|
1284
|
+
if (!nextValue.ok)
|
|
1285
|
+
return nextValue;
|
|
1286
|
+
ref.set(nextValue.value);
|
|
1287
|
+
return ok(previousValue);
|
|
1288
|
+
}),
|
|
1289
|
+
updateAndGet: (updater) => mutex.withLock(async (run) => {
|
|
1290
|
+
const currentValue = ref.get();
|
|
1291
|
+
const nextValue = await run(updater(currentValue));
|
|
1292
|
+
if (!nextValue.ok)
|
|
1293
|
+
return nextValue;
|
|
1294
|
+
ref.set(nextValue.value);
|
|
1295
|
+
return ok(nextValue.value);
|
|
1296
|
+
}),
|
|
1297
|
+
modify: (modifier) => mutex.withLock(async (run) => {
|
|
1298
|
+
const nextValue = await run(modifier(ref.get()));
|
|
1299
|
+
if (!nextValue.ok)
|
|
1300
|
+
return nextValue;
|
|
1301
|
+
const [result, updatedValue] = nextValue.value;
|
|
1302
|
+
ref.set(updatedValue);
|
|
1303
|
+
return ok(result);
|
|
1304
|
+
}),
|
|
408
1305
|
[Symbol.dispose]: mutex[Symbol.dispose],
|
|
409
1306
|
};
|
|
410
1307
|
};
|
|
411
1308
|
/**
|
|
412
|
-
*
|
|
413
|
-
* navigation) have completed.
|
|
1309
|
+
* Creates an in-process {@link LeaderLock}.
|
|
414
1310
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
1311
|
+
* Uses one {@link Mutex} per {@link Name}. Suitable for runtimes without a
|
|
1312
|
+
* cross-process lock manager (for example in-memory worker tests or React
|
|
1313
|
+
* Native).
|
|
417
1314
|
*
|
|
418
|
-
*
|
|
1315
|
+
* @group Concurrency primitives
|
|
1316
|
+
*/
|
|
1317
|
+
export const createInMemoryLeaderLock = () => {
|
|
1318
|
+
const mutexByName = createMutexByKey();
|
|
1319
|
+
return {
|
|
1320
|
+
lock: (name) => async (run) => {
|
|
1321
|
+
const leaseRun = run.create();
|
|
1322
|
+
const released = createDeferred();
|
|
1323
|
+
const acquired = createDeferred();
|
|
1324
|
+
void leaseRun(mutexByName.withLock(name, async (run) => {
|
|
1325
|
+
acquired.resolve(ok());
|
|
1326
|
+
await run(released.task);
|
|
1327
|
+
return ok();
|
|
1328
|
+
}));
|
|
1329
|
+
const acquiredResult = await run(acquired.task);
|
|
1330
|
+
if (!acquiredResult.ok) {
|
|
1331
|
+
assert(AbortError.is(acquiredResult.error), "Leader lock acquisition deferred must not be disposed.");
|
|
1332
|
+
void leaseRun[Symbol.asyncDispose]();
|
|
1333
|
+
return err(acquiredResult.error);
|
|
1334
|
+
}
|
|
1335
|
+
return ok({
|
|
1336
|
+
[Symbol.asyncDispose]: leaseRun[Symbol.asyncDispose],
|
|
1337
|
+
});
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
};
|
|
1341
|
+
export function all(input, options) {
|
|
1342
|
+
return collect("all", input, options);
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Abort reason used by {@link all} when aborting remaining tasks.
|
|
419
1346
|
*
|
|
420
|
-
*
|
|
421
|
-
* const processDataTask: Task<void, ProcessError> = toTask(async () => {
|
|
422
|
-
* // Heavy processing work
|
|
423
|
-
* return ok();
|
|
424
|
-
* });
|
|
1347
|
+
* Used when a Task fails and other Tasks need to be aborted.
|
|
425
1348
|
*
|
|
426
|
-
*
|
|
427
|
-
|
|
428
|
-
|
|
1349
|
+
* @group Composition
|
|
1350
|
+
*/
|
|
1351
|
+
export const AllAbortError = /*#__PURE__*/ typed("AllAbortError");
|
|
1352
|
+
/**
|
|
1353
|
+
* {@link AllAbortError} used as abort reason in {@link all}.
|
|
1354
|
+
*
|
|
1355
|
+
* @group Composition
|
|
1356
|
+
*/
|
|
1357
|
+
export const allAbortError = { type: "AllAbortError" };
|
|
1358
|
+
export function allSettled(input, options) {
|
|
1359
|
+
return collect("allSettled", input, options);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Abort reason used by {@link allSettled} when aborted externally.
|
|
1363
|
+
*
|
|
1364
|
+
* @group Composition
|
|
429
1365
|
*/
|
|
430
|
-
export const
|
|
431
|
-
|
|
432
|
-
|
|
1366
|
+
export const AllSettledAbortError = /*#__PURE__*/ typed("AllSettledAbortError");
|
|
1367
|
+
/**
|
|
1368
|
+
* {@link AllSettledAbortError} used as abort reason in {@link allSettled}.
|
|
1369
|
+
*
|
|
1370
|
+
* @group Composition
|
|
1371
|
+
*/
|
|
1372
|
+
export const allSettledAbortError = {
|
|
1373
|
+
type: "AllSettledAbortError",
|
|
1374
|
+
};
|
|
1375
|
+
export function map(items, fn, { abortReason = mapAbortError, ...options } = {}) {
|
|
1376
|
+
const mapped = mapInput(items, fn);
|
|
1377
|
+
return all(mapped, {
|
|
1378
|
+
...options,
|
|
1379
|
+
abortReason,
|
|
433
1380
|
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Abort reason used by {@link map} when aborting remaining tasks.
|
|
1384
|
+
*
|
|
1385
|
+
* @group Composition
|
|
1386
|
+
*/
|
|
1387
|
+
export const MapAbortError = /*#__PURE__*/ typed("MapAbortError");
|
|
1388
|
+
/**
|
|
1389
|
+
* {@link MapAbortError} used as abort reason in {@link map}.
|
|
1390
|
+
*
|
|
1391
|
+
* @group Composition
|
|
1392
|
+
*/
|
|
1393
|
+
export const mapAbortError = {
|
|
1394
|
+
type: "MapAbortError",
|
|
1395
|
+
};
|
|
1396
|
+
export function mapSettled(items, task, options) {
|
|
1397
|
+
const mapped = mapInput(items, task);
|
|
1398
|
+
return allSettled(mapped, options);
|
|
1399
|
+
}
|
|
1400
|
+
export function any(tasks, options) {
|
|
1401
|
+
const { allFailed = "input" } = options ?? {};
|
|
1402
|
+
return pool(tasks, {
|
|
1403
|
+
stopOn: "success",
|
|
1404
|
+
collect: false,
|
|
1405
|
+
abortReason: anyAbortError,
|
|
1406
|
+
allFailed,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Abort reason used by {@link any} when aborting remaining tasks.
|
|
1411
|
+
*
|
|
1412
|
+
* @group Composition
|
|
1413
|
+
*/
|
|
1414
|
+
export const AnyAbortError = /*#__PURE__*/ typed("AnyAbortError");
|
|
1415
|
+
/**
|
|
1416
|
+
* {@link AnyAbortError} used as abort reason in {@link any}.
|
|
1417
|
+
*
|
|
1418
|
+
* @group Composition
|
|
1419
|
+
*/
|
|
1420
|
+
export const anyAbortError = { type: "AnyAbortError" };
|
|
1421
|
+
/** Shared implementation for {@link all} and {@link allSettled}. */
|
|
1422
|
+
const collect = (type, input, { collect = true, abortReason = type === "all" ? allAbortError : allSettledAbortError, } = {}) => {
|
|
1423
|
+
const stopOn = type === "all" ? "error" : null;
|
|
1424
|
+
if (isIterable(input)) {
|
|
1425
|
+
const array = arrayFrom(input);
|
|
1426
|
+
if (!isNonEmptyArray(array))
|
|
1427
|
+
return () => ok(collect ? emptyArray : undefined);
|
|
1428
|
+
return pool(array, {
|
|
1429
|
+
stopOn,
|
|
1430
|
+
collect,
|
|
1431
|
+
abortReason,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
const keys = [];
|
|
1435
|
+
const taskArray = [];
|
|
1436
|
+
for (const key in input) {
|
|
1437
|
+
keys.push(key);
|
|
1438
|
+
taskArray.push(input[key]);
|
|
1439
|
+
}
|
|
1440
|
+
if (keys.length === 0)
|
|
1441
|
+
return () => ok(collect ? emptyRecord : undefined);
|
|
1442
|
+
return async (run) => {
|
|
1443
|
+
const result = await run(pool(taskArray, { stopOn, collect, abortReason }));
|
|
1444
|
+
if (!result.ok)
|
|
1445
|
+
return result;
|
|
1446
|
+
if (!collect)
|
|
1447
|
+
return ok();
|
|
1448
|
+
const record = createRecord();
|
|
1449
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1450
|
+
record[keys[i]] = result.value[i];
|
|
1451
|
+
}
|
|
1452
|
+
return ok(record);
|
|
1453
|
+
};
|
|
1454
|
+
};
|
|
1455
|
+
const mapInput = (input, fn) => isIterable(input) ? mapArray(arrayFrom(input), fn) : mapObject(input, fn);
|
|
1456
|
+
function pool(tasksIterable, { stopOn = null, collect, abortReason, allFailed, }) {
|
|
1457
|
+
const tasks = arrayFrom(tasksIterable);
|
|
1458
|
+
const { length } = tasks;
|
|
1459
|
+
if (length === 0)
|
|
1460
|
+
return () => ok(emptyArray);
|
|
1461
|
+
return async (run) => {
|
|
1462
|
+
const env_5 = { stack: [], error: void 0, hasError: false };
|
|
1463
|
+
try {
|
|
1464
|
+
const results = collect ? new Array(length) : null;
|
|
1465
|
+
const aborted = Promise.withResolvers();
|
|
1466
|
+
const stopSignal = stopOn ? Promise.withResolvers() : null;
|
|
1467
|
+
let index = 0;
|
|
1468
|
+
let stopped = null;
|
|
1469
|
+
let lastResult = null;
|
|
1470
|
+
let lastIndexResult = null;
|
|
1471
|
+
const worker = async (run) => {
|
|
1472
|
+
while (index < length && (stopOn ? !stopped : true)) {
|
|
1473
|
+
const i = index++;
|
|
1474
|
+
const result = (await run(tasks[i]));
|
|
1475
|
+
lastResult = result;
|
|
1476
|
+
if (i === length - 1)
|
|
1477
|
+
lastIndexResult = result;
|
|
1478
|
+
if (!stopOn) {
|
|
1479
|
+
if (results)
|
|
1480
|
+
results[i] = result;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const stop = stopOn === "first" ||
|
|
1484
|
+
(stopOn === "error" && !result.ok) ||
|
|
1485
|
+
(stopOn === "success" && result.ok);
|
|
1486
|
+
if (!stop) {
|
|
1487
|
+
if (results)
|
|
1488
|
+
results[i] = result.value;
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (!stopped) {
|
|
1492
|
+
stopped = result;
|
|
1493
|
+
abortWorkers(!result.ok && AbortError.is(result.error)
|
|
1494
|
+
? result.error.reason
|
|
1495
|
+
: abortReason);
|
|
1496
|
+
stopSignal?.resolve();
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
return ok();
|
|
1501
|
+
};
|
|
1502
|
+
let workersAborted = false;
|
|
1503
|
+
const abortWorkers = (reason) => {
|
|
1504
|
+
if (workersAborted)
|
|
1505
|
+
return;
|
|
1506
|
+
workersAborted = true;
|
|
1507
|
+
for (const worker of workers)
|
|
1508
|
+
worker.abort(reason);
|
|
1509
|
+
};
|
|
1510
|
+
const workerCount = Math.min(run.concurrency, length);
|
|
1511
|
+
const workers = arrayFrom(workerCount, () => run.daemon(worker));
|
|
1512
|
+
const _ = __addDisposableResource(env_5, new DisposableStack(), false);
|
|
1513
|
+
_.defer(() => {
|
|
1514
|
+
abortWorkers(abortReason);
|
|
1515
|
+
});
|
|
1516
|
+
run.onAbort((reason) => {
|
|
1517
|
+
abortWorkers(reason);
|
|
1518
|
+
aborted.resolve();
|
|
1519
|
+
});
|
|
1520
|
+
const waitFor = [Promise.all(workers), aborted.promise];
|
|
1521
|
+
if (stopSignal)
|
|
1522
|
+
waitFor.push(stopSignal.promise);
|
|
1523
|
+
await Promise.race(waitFor);
|
|
1524
|
+
if (run.signal.aborted) {
|
|
1525
|
+
return err(run.signal.reason);
|
|
1526
|
+
}
|
|
1527
|
+
if (!stopOn)
|
|
1528
|
+
return results ? ok(results) : ok();
|
|
1529
|
+
if (stopped)
|
|
1530
|
+
return stopped;
|
|
1531
|
+
if (results)
|
|
1532
|
+
return ok(results);
|
|
1533
|
+
// For all/allSettled/map/mapSettled with collect: false (no allFailed handler)
|
|
1534
|
+
if (!allFailed)
|
|
1535
|
+
return ok();
|
|
1536
|
+
return allFailed === "completion" ? lastResult : lastIndexResult;
|
|
1537
|
+
}
|
|
1538
|
+
catch (e_5) {
|
|
1539
|
+
env_5.error = e_5;
|
|
1540
|
+
env_5.hasError = true;
|
|
1541
|
+
}
|
|
1542
|
+
finally {
|
|
1543
|
+
__disposeResources(env_5);
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Error returned when a {@link fetch} {@link Task} fails.
|
|
1549
|
+
*
|
|
1550
|
+
* @group Composition
|
|
1551
|
+
*/
|
|
1552
|
+
export const FetchError = /*#__PURE__*/ typed("FetchError", { error: Unknown });
|
|
438
1553
|
/**
|
|
439
|
-
*
|
|
1554
|
+
* Creates a {@link Task} that wraps the native
|
|
1555
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}.
|
|
440
1556
|
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
1557
|
+
* Handles cross-browser abort behavior — WebKit throws a `DOMException` with
|
|
1558
|
+
* message "Fetch is aborted" instead of propagating `signal.reason`. This
|
|
1559
|
+
* helper normalizes the behavior to always return {@link AbortError}.
|
|
443
1560
|
*
|
|
444
1561
|
* ### Example
|
|
445
1562
|
*
|
|
446
1563
|
* ```ts
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* return fetchData(id); // Async path
|
|
451
|
-
* };
|
|
1564
|
+
* await using run = createRun();
|
|
1565
|
+
*
|
|
1566
|
+
* const result = await run(fetch("https://api.example.com/users"));
|
|
452
1567
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
1568
|
+
* if (!result.ok) {
|
|
1569
|
+
* // Handle FetchError or AbortError
|
|
1570
|
+
* }
|
|
1571
|
+
*
|
|
1572
|
+
* // Compose with timeout and retry
|
|
1573
|
+
* const fetchWithRetry = (url: string) =>
|
|
1574
|
+
* retry(timeout(fetch(url), "10s"), retryStrategyAws);
|
|
456
1575
|
* ```
|
|
1576
|
+
*
|
|
1577
|
+
* @group Composition
|
|
457
1578
|
*/
|
|
458
|
-
export const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1579
|
+
export const fetch = (input, init) => ({ signal }) => tryAsync(() => globalThis.fetch(input, { ...init, signal }), (error) => {
|
|
1580
|
+
if (AbortError.is(error))
|
|
1581
|
+
return error;
|
|
1582
|
+
// WebKit throws DOMException with message "Fetch is aborted" instead of
|
|
1583
|
+
// propagating signal.reason. Detect this and create AbortError.
|
|
1584
|
+
if (signal.aborted &&
|
|
1585
|
+
error instanceof Error &&
|
|
1586
|
+
error.message === "Fetch is aborted") {
|
|
1587
|
+
return createAbortError(signal.reason);
|
|
1588
|
+
}
|
|
1589
|
+
return { type: "FetchError", error };
|
|
1590
|
+
});
|
|
1591
|
+
// TODO: Prioritized Task Scheduling API integration
|
|
1592
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
|
|
1593
|
+
//
|
|
1594
|
+
// - `run(task, { priority })` - Fiber-level priority via TaskController
|
|
1595
|
+
// (extends AbortController), so the fiber's signal gets priority and all
|
|
1596
|
+
// nested work inherits it.
|
|
1597
|
+
// - `fiber.setPriority(priority)` - Dynamic priority changes mid-flight via
|
|
1598
|
+
// TaskController.setPriority().
|
|
1599
|
+
//
|
|
1600
|
+
// Note: scheduler.yield() inherits priority from enclosing postTask, so
|
|
1601
|
+
// yieldNow doesn't need a priority argument — just run the fiber at priority.
|
|
1602
|
+
//
|
|
1603
|
+
// Safari doesn't support it yet, Node.js probably never will (use setImmediate).
|
|
1604
|
+
// For Safari, scheduler-polyfill can be used.
|
|
1605
|
+
// https://www.npmjs.com/package/scheduler-polyfill
|
|
1606
|
+
// TODO: Do we really need specialized aborts?
|