@evolu/common 7.4.0 → 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 +1064 -630
- 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 -823
- 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 +209 -282
- 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 -529
- package/src/Instances.ts +0 -90
- 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 -960
package/dist/src/Task.d.ts
CHANGED
|
@@ -1,580 +1,2420 @@
|
|
|
1
|
-
import { Result } from "./Result.js";
|
|
2
|
-
import { Duration } from "./Time.js";
|
|
3
|
-
import { PositiveInt } from "./Type.js";
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Promise using {@link Result}.
|
|
2
|
+
* JavaScript-native structured concurrency.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { type NonEmptyReadonlyArray } from "./Array.js";
|
|
7
|
+
import { type ConsoleDep } from "./Console.js";
|
|
8
|
+
import type { RandomBytesDep } from "./Crypto.js";
|
|
9
|
+
import { type LookupOption } from "./Lookup.js";
|
|
10
|
+
import type { RandomDep } from "./Random.js";
|
|
11
|
+
import { type Ref } from "./Ref.js";
|
|
12
|
+
import type { Done, Result } from "./Result.js";
|
|
13
|
+
import type { Schedule, ScheduleStep } from "./Schedule.js";
|
|
14
|
+
import type { Duration, TimeDep } from "./Time.js";
|
|
15
|
+
import { Id, type InferType, type Name, NonNegativeInt, PositiveInt, type Typed } from "./Type.js";
|
|
16
|
+
import { type Awaitable, type Callback, type CallbackWithTeardown, type Int1To100, type NewKeys, type Predicate } from "./Types.js";
|
|
17
|
+
/**
|
|
18
|
+
* JavaScript-native structured concurrency.
|
|
10
19
|
*
|
|
11
|
-
*
|
|
20
|
+
* Structured concurrency is a simple idea: async operations form a tree where
|
|
21
|
+
* no child can outlive its parent — ending a parent aborts its children and
|
|
22
|
+
* waits for them to complete. This eliminates resource leaking and "fire and
|
|
23
|
+
* forget" bugs.
|
|
12
24
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* and AbortError is added to the error union with precise type safety.
|
|
25
|
+
* - **Automatic cancellation** — abort propagates to all descendants
|
|
26
|
+
* - **Guaranteed cleanup** — resources always cleaned up
|
|
27
|
+
* - **Observable state** — inspect what’s running and why
|
|
17
28
|
*
|
|
18
|
-
*
|
|
19
|
-
* passing it through. However, there are valid cases where we don't want to
|
|
20
|
-
* abort because we need some atomic unit to complete. For simple scripts and
|
|
21
|
-
* tests, omitting context is fine.
|
|
29
|
+
* Evolu implements structured concurrency with these types:
|
|
22
30
|
*
|
|
23
|
-
*
|
|
31
|
+
* - **{@link Task}** — a function that takes Run and returns {@link Awaitable}
|
|
32
|
+
* (sync or async) {@link Result}
|
|
33
|
+
* - **{@link Run}** — a callable object that runs Tasks, manages their lifecycle,
|
|
34
|
+
* provides dependencies, and creates Fibers
|
|
35
|
+
* - **{@link Fiber}** — awaitable, abortable/disposable handle to a running Task
|
|
24
36
|
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
* - {@link retry} - Retry failed Tasks with configurable backoff
|
|
37
|
+
* Evolu's structured concurrency core is minimal — one function with
|
|
38
|
+
* closed-over state and a few attached properties and helper methods using
|
|
39
|
+
* native APIs.
|
|
29
40
|
*
|
|
30
41
|
* ### Example
|
|
31
42
|
*
|
|
32
43
|
* ```ts
|
|
33
|
-
*
|
|
34
|
-
*
|
|
44
|
+
* // A dependency — wraps native fetch for testability.
|
|
45
|
+
* interface NativeFetchDep {
|
|
46
|
+
* readonly fetch: typeof globalThis.fetch;
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* interface FetchError extends Typed<"FetchError"> {
|
|
35
50
|
* readonly error: unknown;
|
|
36
51
|
* }
|
|
37
52
|
*
|
|
38
|
-
* // Task
|
|
39
|
-
* const fetch =
|
|
40
|
-
*
|
|
53
|
+
* // A Task wrapping native fetch — adds abortability.
|
|
54
|
+
* const fetch =
|
|
55
|
+
* (url: string): Task<Response, FetchError, NativeFetchDep> =>
|
|
56
|
+
* ({ deps, signal }) =>
|
|
41
57
|
* tryAsync(
|
|
42
|
-
* () =>
|
|
43
|
-
* (error): FetchError =>
|
|
44
|
-
*
|
|
58
|
+
* () => deps.fetch(url, { signal }),
|
|
59
|
+
* (error): FetchError | AbortError => {
|
|
60
|
+
* if (AbortError.is(error)) return error;
|
|
61
|
+
* return { type: "FetchError", error };
|
|
62
|
+
* },
|
|
63
|
+
* );
|
|
64
|
+
*
|
|
65
|
+
* // In a composition root…
|
|
66
|
+
* const deps: NativeFetchDep = {
|
|
67
|
+
* fetch: globalThis.fetch.bind(globalThis),
|
|
68
|
+
* };
|
|
69
|
+
*
|
|
70
|
+
* // Create a Run with dependencies.
|
|
71
|
+
* await using run = createRun(deps);
|
|
72
|
+
*
|
|
73
|
+
* // Running a Task returns a Fiber that can be awaited.
|
|
74
|
+
* const result = await run(fetch("/users/123"));
|
|
75
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
76
|
+
* Result<Response, FetchError | AbortError>
|
|
77
|
+
* >();
|
|
78
|
+
*
|
|
79
|
+
* // A Fiber can also be aborted (or disposed with `using`).
|
|
80
|
+
* const fiber = run(fetch("/users/456"));
|
|
81
|
+
* fiber.abort();
|
|
82
|
+
*
|
|
83
|
+
* // When this block ends, `await using` disposes the Run — aborting all Fibers.
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* In composition roots, prefer Evolu platform `createRun` adapters when one
|
|
87
|
+
* exists. `@evolu/web`, `@evolu/nodejs`, and `@evolu/react-native` build on the
|
|
88
|
+
* common {@link createRun} and add platform-specific global error handling.
|
|
89
|
+
*
|
|
90
|
+
* ## Composition
|
|
91
|
+
*
|
|
92
|
+
* | Category | Helper | Description |
|
|
93
|
+
* | ---------- | ------------------ | ----------------------------------- |
|
|
94
|
+
* | Collection | {@link all} | fail-fast on first error |
|
|
95
|
+
* | | {@link allSettled} | complete all regardless of failures |
|
|
96
|
+
* | | {@link map} | values to Tasks, fail-fast |
|
|
97
|
+
* | | {@link mapSettled} | values to Tasks, complete all |
|
|
98
|
+
* | Timing | {@link sleep} | pause execution |
|
|
99
|
+
* | | {@link timeout} | time-bounded execution |
|
|
100
|
+
* | | {@link repeat} | repeat with schedule |
|
|
101
|
+
* | | {@link yieldNow} | yield to event loop |
|
|
102
|
+
* | Racing | {@link race} | first to complete wins |
|
|
103
|
+
* | | {@link any} | first success wins |
|
|
104
|
+
* | Resilience | {@link retry} | retry with backoff |
|
|
105
|
+
* | Interop | {@link callback} | wrap callback APIs |
|
|
106
|
+
* | | {@link fetch} | HTTP requests with abort handling |
|
|
107
|
+
*
|
|
108
|
+
* Collection helpers run sequentially by default. Use {@link concurrently} to
|
|
109
|
+
* run Tasks concurrently. Note helpers like {@link race} always run
|
|
110
|
+
* concurrently; sequential execution wouldn't make sense for their semantics.
|
|
111
|
+
*
|
|
112
|
+
* ## Style
|
|
113
|
+
*
|
|
114
|
+
* Imperative code is the preferred way to compose sequential {@link Task}
|
|
115
|
+
* operations inside another Task.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* const user = await run(fetchUser(id));
|
|
119
|
+
* if (!user.ok) return user;
|
|
120
|
+
*
|
|
121
|
+
* const profile = await run(fetchProfile(user.value.id));
|
|
122
|
+
* if (!profile.ok) return profile;
|
|
123
|
+
*
|
|
124
|
+
* return ok({ user: user.value, profile: profile.value });
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* This is an intentional style choice. Evolu keeps helpers for operations with
|
|
128
|
+
* distinct semantics that plain control flow does not express well, such as
|
|
129
|
+
* concurrency, racing, retries, timeouts, and collection processing. It
|
|
130
|
+
* intentionally does not provide generic chain, flatMap, or pipe-style helpers
|
|
131
|
+
* for ordinary sequential Task composition, because that would duplicate plain
|
|
132
|
+
* control flow and create API ambiguity. While it may seem verbose, it is
|
|
133
|
+
* explicit, transparent, and avoids pipes and nested helpers, which are harder
|
|
134
|
+
* to debug.
|
|
135
|
+
*
|
|
136
|
+
* ### Building a better fetch
|
|
137
|
+
*
|
|
138
|
+
* Use {@link timeout} to prevent hanging:
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const fetchWithTimeout = (url: string) => timeout(fetch(url), "30s");
|
|
142
|
+
*
|
|
143
|
+
* expectTypeOf(fetchWithTimeout).toEqualTypeOf<
|
|
144
|
+
* (
|
|
145
|
+
* url: string,
|
|
146
|
+
* ) => Task<Response, FetchError | TimeoutError, NativeFetchDep>
|
|
147
|
+
* >();
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* Add {@link retry} for resilience:
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const fetchWithRetry = (url: string) =>
|
|
154
|
+
* retry(
|
|
155
|
+
* fetchWithTimeout(url),
|
|
156
|
+
* // A jittered, capped, limited exponential backoff.
|
|
157
|
+
* jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
45
158
|
* );
|
|
46
159
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
160
|
+
* expectTypeOf(fetchWithRetry).toEqualTypeOf<
|
|
161
|
+
* (
|
|
162
|
+
* url: string,
|
|
163
|
+
* ) => Task<
|
|
164
|
+
* Response,
|
|
165
|
+
* RetryError<FetchError | TimeoutError>,
|
|
166
|
+
* NativeFetchDep
|
|
167
|
+
* >
|
|
168
|
+
* >();
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* Run composed tasks with {@link concurrently} and {@link map}:
|
|
172
|
+
*
|
|
173
|
+
* ```ts
|
|
174
|
+
* await using run = createRun();
|
|
175
|
+
*
|
|
176
|
+
* const urls = [
|
|
177
|
+
* "https://api.example.com/users",
|
|
178
|
+
* "https://api.example.com/posts",
|
|
179
|
+
* "https://api.example.com/comments",
|
|
180
|
+
* ];
|
|
181
|
+
*
|
|
182
|
+
* // At most 2 concurrent requests
|
|
183
|
+
* const result = await run(concurrently(2, map(urls, fetchWithRetry)));
|
|
184
|
+
*
|
|
185
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
186
|
+
* Result<
|
|
187
|
+
* readonly Response[],
|
|
188
|
+
* AbortError | RetryError<TimeoutError | FetchError>
|
|
189
|
+
* >
|
|
190
|
+
* >();
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* ## Dependency Injection
|
|
194
|
+
*
|
|
195
|
+
* Assumes familiarity with
|
|
196
|
+
* {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}. Task
|
|
197
|
+
* DI is the same but without manually passing deps.
|
|
198
|
+
*
|
|
199
|
+
* Tasks declare dependencies via the `D` type parameter and access them via
|
|
200
|
+
* `run.deps`:
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* const fetchUser =
|
|
204
|
+
* (id: UserId): Task<User, FetchUserError, FetchDep> =>
|
|
205
|
+
* async (run) => {
|
|
206
|
+
* const { fetch } = run.deps;
|
|
207
|
+
* // ...
|
|
208
|
+
* };
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* Provide dependencies when creating a Run:
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* const deps: FetchDep = {
|
|
215
|
+
* fetch: globalThis.fetch.bind(globalThis),
|
|
216
|
+
* };
|
|
217
|
+
*
|
|
218
|
+
* await using run = createRun(deps);
|
|
219
|
+
* await run(fetchUser(123));
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* For runtime-created dependencies, use {@link Run#addDeps}.
|
|
223
|
+
*
|
|
224
|
+
* ### Built-in dependencies
|
|
225
|
+
*
|
|
226
|
+
* {@link createRun} provides default {@link RunDeps} available to all Tasks
|
|
227
|
+
* without declaring `D`:
|
|
228
|
+
*
|
|
229
|
+
* - {@link Console} — logging with hierarchical context via `child()`
|
|
230
|
+
* - {@link Time} — current time
|
|
231
|
+
* - {@link Random} — random number generation
|
|
232
|
+
* - {@link RandomBytes} — cryptographic random bytes
|
|
233
|
+
*
|
|
234
|
+
* For example, using `Console`:
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* const myTask: Task<void> = async (run) => {
|
|
238
|
+
* const { console } = run.deps;
|
|
239
|
+
* console.log("started");
|
|
240
|
+
* // ...
|
|
241
|
+
* };
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* Custom Console with formatted output:
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* const deps = {
|
|
248
|
+
* console: createConsole({
|
|
249
|
+
* formatter: createConsoleFormatter()({
|
|
250
|
+
* timestampFormat: "absolute",
|
|
251
|
+
* }),
|
|
252
|
+
* }),
|
|
253
|
+
* };
|
|
254
|
+
*
|
|
255
|
+
* await using run = createRun(deps);
|
|
256
|
+
*
|
|
257
|
+
* const console = run.deps.console.child("main");
|
|
258
|
+
*
|
|
259
|
+
* console.log("started");
|
|
260
|
+
* // 21:20:25.588 [main] started
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* For testing, use {@link testCreateRun} to get deterministic, controllable
|
|
264
|
+
* implementations of all RunDeps.
|
|
265
|
+
*
|
|
266
|
+
* ## Resource management
|
|
267
|
+
*
|
|
268
|
+
* Evolu uses standard JavaScript
|
|
269
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
|
|
270
|
+
*
|
|
271
|
+
* Use the
|
|
272
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncDisposableStack | AsyncDisposableStack}
|
|
273
|
+
* for async resource ownership.
|
|
274
|
+
*
|
|
275
|
+
* ### Example
|
|
276
|
+
*
|
|
277
|
+
* ```ts
|
|
278
|
+
* await using stack = new AsyncDisposableStack();
|
|
279
|
+
*
|
|
280
|
+
* const fooResult = await run(createFoo());
|
|
281
|
+
* if (!fooResult.ok) return fooResult;
|
|
282
|
+
* const foo = stack.use(fooResult.value);
|
|
283
|
+
*
|
|
284
|
+
* stack.defer(async () => {
|
|
285
|
+
* await foo.close();
|
|
286
|
+
* });
|
|
287
|
+
* stack.adopt(session, async (session) => {
|
|
288
|
+
* await session.logout();
|
|
289
|
+
* });
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* ## Awaitable
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* type Awaitable<T> = T | PromiseLike<T>;
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* Even though {@link Task} returns {@link Awaitable} (allowing sync or async
|
|
299
|
+
* results), the {@link Run} itself is always async. This is a deliberate design
|
|
300
|
+
* choice:
|
|
301
|
+
*
|
|
302
|
+
* - **Sync** → {@link Result}, native `using` / `DisposableStack`
|
|
303
|
+
* - **Async** → {@link Task}, {@link Run}, {@link Fiber}, `await using` /
|
|
304
|
+
* `AsyncDisposableStack`
|
|
305
|
+
*
|
|
306
|
+
* Benefits:
|
|
307
|
+
*
|
|
308
|
+
* - **No API ambiguity** — Task means async, Result means sync
|
|
309
|
+
* - **Zero overhead** — sync code stays with zero overhead
|
|
310
|
+
*
|
|
311
|
+
* While a unified sync/async API is technically possible — with
|
|
312
|
+
* {@link isPromiseLike} detection and two-phase disposal (sync first, async if
|
|
313
|
+
* needed, and a flag for callers) — Evolu prefers plain functions for sync code
|
|
314
|
+
* because most operations involve I/O, which is inherently async, and when we
|
|
315
|
+
* need sync, it's for simplicity (ideally no dependencies) and performance
|
|
316
|
+
* (zero abstraction overhead).
|
|
317
|
+
*
|
|
318
|
+
* Sync functions should be fast, so there's no need to monitor them. They
|
|
319
|
+
* should take values, not dependencies — following the
|
|
320
|
+
* {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
|
|
321
|
+
* pattern where impure code gathers data, pure functions process it, and impure
|
|
322
|
+
* code performs effects with the result. Sync functions taking deps often
|
|
323
|
+
* indicate a design that could be improved — for example, a function taking
|
|
324
|
+
* {@link Random} could instead accept {@link RandomNumber} as a value.
|
|
325
|
+
*
|
|
326
|
+
* Slow sync operations (parsing large JSON, sorting millions of items, complex
|
|
327
|
+
* cryptography) belong in workers. The async boundary to the worker is a
|
|
328
|
+
* {@link Task} with full monitoring — timeout, cancellation, tracing. The sync
|
|
329
|
+
* code inside the worker needs no monitoring; the async call to the worker
|
|
330
|
+
* provides it.
|
|
331
|
+
*
|
|
332
|
+
* ## Glossary
|
|
333
|
+
*
|
|
334
|
+
* - **Cleanup** — generic umbrella term when the exact lifecycle operation is not
|
|
335
|
+
* important.
|
|
336
|
+
* - **Dispose / disposal** — owner-driven cleanup via JavaScript resource
|
|
337
|
+
* management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
338
|
+
* `AsyncDisposableStack`).
|
|
339
|
+
* - **Create** — construct a new value or a resource.
|
|
340
|
+
* - **Acquire** — obtain a usable resource. Acquisition may create a new
|
|
341
|
+
* resource, borrow one, open one, or take a lease/lock.
|
|
342
|
+
* - **Release** — relinquish a previously acquired resource or lease. Release
|
|
343
|
+
* pairs with acquire and need not mean disposal; examples include unlock,
|
|
344
|
+
* logout, or returning a pooled resource.
|
|
345
|
+
*
|
|
346
|
+
* ## FAQ
|
|
347
|
+
*
|
|
348
|
+
* ### Why can `Task<T, never, D>` still return `AbortError`?
|
|
349
|
+
*
|
|
350
|
+
* The `E` type parameter represents domain errors, not abort control flow.
|
|
351
|
+
*
|
|
352
|
+
* `AbortError` comes from the {@link Run} runtime. A Task can still return it:
|
|
353
|
+
*
|
|
354
|
+
* - Before execution, when the parent or root {@link Run} is already stopped
|
|
355
|
+
* - During execution, when an abortable Task is aborted
|
|
356
|
+
* - At settlement, when abort was requested before the Task result was observed
|
|
357
|
+
*
|
|
358
|
+
* So `Task<T, never, D>` means "no domain errors", not "cannot fail at all".
|
|
359
|
+
*
|
|
360
|
+
* This also applies to {@link unabortable}. `unabortable(task)` only prevents
|
|
361
|
+
* abort from interrupting the Task after it has started running. It cannot
|
|
362
|
+
* force execution to start on a parent or root {@link Run} that is already
|
|
363
|
+
* disposing or settled, so it can still return {@link AbortError} before
|
|
364
|
+
* execution begins. If that abort would indicate a lifecycle bug in your code,
|
|
365
|
+
* use {@link assertNotAborted} to crash immediately instead of threading the
|
|
366
|
+
* impossible case through domain logic.
|
|
367
|
+
*
|
|
368
|
+
* ### How do I type an anonymous Task callback?
|
|
369
|
+
*
|
|
370
|
+
* For one-off inline Tasks, put the type arguments on the {@link Run} call:
|
|
371
|
+
*
|
|
372
|
+
* ```ts
|
|
373
|
+
* run<void, never>(async (run) => {
|
|
374
|
+
* const result = await run(waitForLeader);
|
|
375
|
+
* if (!result.ok) return result;
|
|
376
|
+
*
|
|
377
|
+
* doSomething(result.value);
|
|
378
|
+
* return ok();
|
|
379
|
+
* });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* This constrains the callback to the same shape as `Task<void, never, D>`
|
|
383
|
+
* without extracting a named constant.
|
|
384
|
+
*
|
|
385
|
+
* This pattern is useful for child Tasks started at the call site, especially
|
|
386
|
+
* when success has no meaningful value and domain errors are not expected. In
|
|
387
|
+
* that case, `void, never` documents an abort-only Task whose result can be
|
|
388
|
+
* ignored while the parent {@link Run} still owns its lifecycle.
|
|
389
|
+
*
|
|
390
|
+
* If the Task is reused, exported, or deserves a name, prefer an explicit
|
|
391
|
+
* `const task: Task<T, E, D> = async (run) => { ... }` instead.
|
|
392
|
+
*
|
|
393
|
+
* ### Where is fork and join?
|
|
394
|
+
*
|
|
395
|
+
* For those familiar with other structured concurrency implementations:
|
|
396
|
+
*
|
|
397
|
+
* - **Fork** — `run(task)` creates a {@link Fiber}
|
|
398
|
+
* - **Join** — `await fiber` waits for completion
|
|
399
|
+
*
|
|
400
|
+
* @group Core Types
|
|
401
|
+
*/
|
|
402
|
+
export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<T, E | AbortError>>;
|
|
403
|
+
/**
|
|
404
|
+
* Shorthand for a {@link Task} with `any` type parameters.
|
|
405
|
+
*
|
|
406
|
+
* @group Type utilities
|
|
407
|
+
*/
|
|
408
|
+
export type AnyTask = Task<any, any, any>;
|
|
409
|
+
/**
|
|
410
|
+
* Extracts the value type from a {@link Task}.
|
|
411
|
+
*
|
|
412
|
+
* @group Type utilities
|
|
413
|
+
*/
|
|
414
|
+
export type InferTaskOk<R extends AnyTask> = R extends Task<infer T, any, any> ? T : never;
|
|
415
|
+
/**
|
|
416
|
+
* Extracts the error type from a {@link Task}.
|
|
417
|
+
*
|
|
418
|
+
* @group Type utilities
|
|
419
|
+
*/
|
|
420
|
+
export type InferTaskErr<R extends AnyTask> = R extends Task<any, infer E, any> ? E : never;
|
|
421
|
+
/**
|
|
422
|
+
* Extracts the deps type from a {@link Task}.
|
|
423
|
+
*
|
|
424
|
+
* @group Type utilities
|
|
425
|
+
*/
|
|
426
|
+
export type InferTaskDeps<R extends AnyTask> = R extends Task<any, any, infer D> ? D : never;
|
|
427
|
+
/**
|
|
428
|
+
* A {@link Task} that can complete with a value, signal done, or fail.
|
|
429
|
+
*
|
|
430
|
+
* Forms a pair with {@link NextResult}:
|
|
431
|
+
*
|
|
432
|
+
* - `Result<A, E>` → `NextResult<A, E, D>`
|
|
433
|
+
* - `Task<T, E>` → `NextTask<T, E, D>`
|
|
434
|
+
*
|
|
435
|
+
* Use for pull-based protocols like iterators where `Done<D>` signals normal
|
|
436
|
+
* completion rather than an error.
|
|
437
|
+
*
|
|
438
|
+
* @group Core Types
|
|
439
|
+
*/
|
|
440
|
+
export type NextTask<T, E = never, D = void> = Task<T, E | Done<D>>;
|
|
441
|
+
/**
|
|
442
|
+
* Extracts the done value type from a {@link NextTask}.
|
|
443
|
+
*
|
|
444
|
+
* @group Type utilities
|
|
445
|
+
*/
|
|
446
|
+
export type InferTaskDone<T extends AnyTask> = InferTaskErr<T> extends infer Errors ? Errors extends Done<infer D> ? D : never : never;
|
|
447
|
+
/**
|
|
448
|
+
* Error returned when a {@link Task} is aborted via
|
|
449
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | AbortSignal}.
|
|
450
|
+
*
|
|
451
|
+
* This is structured-concurrency control flow, not a domain error. It plays a
|
|
452
|
+
* role similar to an interrupt: in most code, propagate it unchanged or ignore
|
|
453
|
+
* it when cleanup is already handled by the runtime.
|
|
454
|
+
*
|
|
455
|
+
* The `reason` field is `unknown` by design — use typed errors for business
|
|
456
|
+
* logic. If you need to inspect the reason, use type guards like
|
|
457
|
+
* `RaceLostError.is(reason)`.
|
|
458
|
+
*
|
|
459
|
+
* When a piece of logic must continue once started, wrap it with
|
|
460
|
+
* {@link unabortable}. That prevents ordinary mid-flight interruption, but it
|
|
461
|
+
* does not erase {@link AbortError} from the type because the Task can still be
|
|
462
|
+
* rejected before it starts, for example on an already-stopped {@link Run}. In
|
|
463
|
+
* those cases, abort usually indicates a lifecycle bug, so use
|
|
464
|
+
* `assertNotAborted` if you want to fail fast.
|
|
465
|
+
*
|
|
466
|
+
* @group Core Types
|
|
467
|
+
*/
|
|
468
|
+
export declare const AbortError: import("./Type.js").TypedType<"AbortError", {
|
|
469
|
+
reason: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
470
|
+
}>;
|
|
471
|
+
export interface AbortError extends InferType<typeof AbortError> {
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Runs a {@link Task} with
|
|
475
|
+
* {@link https://en.wikipedia.org/wiki/Structured_concurrency | structured concurrency}
|
|
476
|
+
* semantics.
|
|
477
|
+
*
|
|
478
|
+
* Each `Run` forms a Task tree: child Tasks are bound to it, abort propagates
|
|
479
|
+
* through that tree, and state is observable via snapshots and events.
|
|
480
|
+
*
|
|
481
|
+
* `Run` is a callable object — callable because it's convenient to run Tasks as
|
|
482
|
+
* `run(task)`, and an object because it holds state.
|
|
483
|
+
*
|
|
484
|
+
* Calling `run(task)` creates a child `Run`, passes it to the Task, and returns
|
|
485
|
+
* a {@link Fiber}. The child is tracked in `getChildren()`/events while running,
|
|
486
|
+
* then disposed and removed when settled.
|
|
487
|
+
*
|
|
488
|
+
* Before Task execution, `run(task)` applies two short-circuit checks:
|
|
489
|
+
*
|
|
490
|
+
* - If this Run is not `Running`, the child is aborted with
|
|
491
|
+
* {@link runStoppedError} and the Task is replaced with `err(AbortError)`.
|
|
492
|
+
* - If this Run's signal is already aborted and the child is abortable
|
|
493
|
+
* (`abortMask === 0`), the child is aborted with the same reason and the Task
|
|
494
|
+
* is replaced with `err(AbortError)`.
|
|
495
|
+
*
|
|
496
|
+
* After execution, the child stores both values: `outcome` (what the Task
|
|
497
|
+
* returned) and `result` (what callers observe). If the child signal is aborted
|
|
498
|
+
* at settlement time, `result` is forced to `err(AbortError)` even when
|
|
499
|
+
* `outcome` is `ok(...)`.
|
|
500
|
+
*
|
|
501
|
+
* That's the whole mechanism: {@link Task} is a function that takes a `Run` and
|
|
502
|
+
* returns an {@link Awaitable} {@link Result}. `run(task)` runs the Task via
|
|
503
|
+
* `Promise.try(task, run)` with aforementioned logic.
|
|
504
|
+
*
|
|
505
|
+
* @group Core Types
|
|
506
|
+
* @see {@link createRun}
|
|
507
|
+
* @see {@link Task}
|
|
508
|
+
*/
|
|
509
|
+
export interface Run<D = unknown> extends AsyncDisposable {
|
|
510
|
+
/** Runs a {@link Task} and returns a {@link Fiber} handle. */
|
|
511
|
+
<T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
|
|
512
|
+
/**
|
|
513
|
+
* Runs a {@link Task} and throws if the returned {@link Result} is an error.
|
|
514
|
+
*
|
|
515
|
+
* Use this where failure should crash the current flow instead of being
|
|
516
|
+
* handled locally.
|
|
517
|
+
*
|
|
518
|
+
* This is the async equivalent of {@link getOrThrow}. It runs the Task, awaits
|
|
519
|
+
* its {@link Result}, and returns the value on success.
|
|
520
|
+
*
|
|
521
|
+
* **When to use:**
|
|
522
|
+
*
|
|
523
|
+
* - Application startup or composition-root setup where errors must stop the
|
|
524
|
+
* program immediately. In Evolu apps, errors are handled by
|
|
525
|
+
* platform-specific `createRun` adapters at the app boundary.
|
|
526
|
+
* - Module-level constants
|
|
527
|
+
* - Test setup with values that are expected to be valid
|
|
528
|
+
*
|
|
529
|
+
* Prefer `await run(task)` with an explicit `if (!result.ok)` check in
|
|
530
|
+
* ordinary application logic where the caller can recover, retry, or choose a
|
|
531
|
+
* different flow.
|
|
532
|
+
*
|
|
533
|
+
* Throws: `Error` with the original Task error attached as `cause`.
|
|
534
|
+
*/
|
|
535
|
+
readonly orThrow: <T, E>(task: Task<T, E, D>) => Promise<T>;
|
|
536
|
+
/** Unique {@link Id} for this Run. */
|
|
537
|
+
readonly id: Id;
|
|
538
|
+
/** The parent {@link Run}, if this Run was created as a child. */
|
|
539
|
+
readonly parent: Run<D> | null;
|
|
540
|
+
/** @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal */
|
|
541
|
+
readonly signal: AbortSignal;
|
|
542
|
+
/** The abort mask depth. `0` means abortable, `>= 1` means unabortable. */
|
|
543
|
+
readonly abortMask: AbortMask;
|
|
544
|
+
/**
|
|
545
|
+
* Registers a callback to run when abort is requested.
|
|
546
|
+
*
|
|
547
|
+
* This is a convenience wrapper around subscribing to this Run's abort
|
|
548
|
+
* signal. The callback receives the abort reason extracted from
|
|
549
|
+
* {@link AbortError.reason} rather than the whole {@link AbortError}.
|
|
550
|
+
*
|
|
551
|
+
* If already aborted, the callback is invoked immediately. For
|
|
552
|
+
* {@link unabortable} Tasks, the callback is never invoked because their
|
|
553
|
+
* signal never aborts.
|
|
554
|
+
*
|
|
555
|
+
* Intentionally synchronous and not awaited. The callback runs in the abort
|
|
556
|
+
* request path, which may already be transitioning this Run to `Disposing` or
|
|
557
|
+
* `Settled`, so it is too late to start normal Tasks from there.
|
|
558
|
+
*
|
|
559
|
+
* Use for immediate abort-time reactions such as removing listeners, clearing
|
|
560
|
+
* timers, removing waiters from queues, or resolving pending promises. Do not
|
|
561
|
+
* use it for awaited cleanup or resource ownership. For that, use standard
|
|
562
|
+
* JavaScript resource management with `AsyncDisposableStack`.
|
|
563
|
+
*/
|
|
564
|
+
readonly onAbort: (callback: Callback<unknown>) => void;
|
|
565
|
+
/** Returns the current {@link RunState}. */
|
|
566
|
+
readonly getState: () => RunState;
|
|
567
|
+
/** Returns the current child {@link Fiber}s. */
|
|
568
|
+
readonly getChildren: () => ReadonlySet<Fiber<any, any, D>>;
|
|
569
|
+
/**
|
|
570
|
+
* Creates a memoized {@link RunSnapshot} of this Run.
|
|
571
|
+
*
|
|
572
|
+
* Use for monitoring, debugging, or building UI that visualizes Task trees.
|
|
573
|
+
*
|
|
574
|
+
* ### Example
|
|
575
|
+
*
|
|
576
|
+
* ```ts
|
|
577
|
+
* // React integration with useSyncExternalStore
|
|
578
|
+
* const useRunSnapshot = (run: Run) =>
|
|
579
|
+
* useSyncExternalStore(
|
|
580
|
+
* (callback) => {
|
|
581
|
+
* run.onEvent = callback;
|
|
582
|
+
* return () => {
|
|
583
|
+
* run.onEvent = undefined;
|
|
584
|
+
* };
|
|
585
|
+
* },
|
|
586
|
+
* () => run.snapshot(),
|
|
587
|
+
* );
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
readonly snapshot: () => RunSnapshot;
|
|
591
|
+
/**
|
|
592
|
+
* Callback for monitoring Run events.
|
|
593
|
+
*
|
|
594
|
+
* Called when this Run or any descendant emits a {@link RunEvent}. Events
|
|
595
|
+
* bubble up through parent runs, enabling centralized monitoring. Only
|
|
596
|
+
* emitted when {@link RunConfig.eventsEnabled} is `true`.
|
|
597
|
+
*/
|
|
598
|
+
onEvent: ((event: RunEvent) => void) | undefined;
|
|
599
|
+
/**
|
|
600
|
+
* The root {@link Run} of this Task tree.
|
|
601
|
+
*
|
|
602
|
+
* It is called `daemon` because that is how it should be used: for
|
|
603
|
+
* long-running work that must not be disposed when the current Task settles.
|
|
604
|
+
* Normal child Runs are disposed by their parent when they settle. The root
|
|
605
|
+
* Run has no parent, so work started with `run.daemon(task)` is attached to
|
|
606
|
+
* that root Run instead of the current Run and keeps running until the root
|
|
607
|
+
* Run is disposed manually.
|
|
608
|
+
*
|
|
609
|
+
* In application code, that usually means disposing the root Run on process
|
|
610
|
+
* shutdown in Node.js or when another platform-specific lifecycle hook is
|
|
611
|
+
* available. Browsers do not provide a fully reliable app termination hook.
|
|
612
|
+
*
|
|
613
|
+
* ### Example
|
|
614
|
+
*
|
|
615
|
+
* ```ts
|
|
616
|
+
* const myTask: Task<void, never> = async (run) => {
|
|
617
|
+
* // Aborted when myTask ends
|
|
618
|
+
* run(helperTask);
|
|
619
|
+
*
|
|
620
|
+
* // Outlives myTask, aborted when the root Run is disposed
|
|
621
|
+
* const backgroundFiber = run.daemon(backgroundSync);
|
|
622
|
+
*
|
|
623
|
+
* // Can still be aborted manually if needed
|
|
624
|
+
* backgroundFiber.abort();
|
|
625
|
+
*
|
|
626
|
+
* return ok();
|
|
627
|
+
* };
|
|
628
|
+
* ```
|
|
629
|
+
*
|
|
630
|
+
* For a long-lived reusable {@link Run}, use {@link Run.create}.
|
|
631
|
+
*/
|
|
632
|
+
readonly daemon: Run<D>;
|
|
633
|
+
/**
|
|
634
|
+
* Creates a {@link Run} from this Run.
|
|
635
|
+
*
|
|
636
|
+
* Like {@link createRun}, the returned Run is daemon: it stays running until
|
|
637
|
+
* disposed. Unlike {@link createRun}, it shares the same Deps as this Run.
|
|
638
|
+
*
|
|
639
|
+
* Use this for long-lived disposable resources that need to own async work.
|
|
640
|
+
* The resource creates one internal Run with `run.create()` and uses that Run
|
|
641
|
+
* for all of its work. Disposing the resource then disposes that internal
|
|
642
|
+
* Run, which aborts in-flight child Tasks, waits for them to settle, and
|
|
643
|
+
* rejects later calls through it.
|
|
644
|
+
*
|
|
645
|
+
* Typical examples are database clients, connection pools, workers, or other
|
|
646
|
+
* reusable resources with async methods and an async dispose operation.
|
|
647
|
+
*
|
|
648
|
+
* To run a single Task as daemon, use {@link Run.daemon}.
|
|
649
|
+
*/
|
|
650
|
+
readonly create: () => Run<D>;
|
|
651
|
+
/** Returns the dependencies passed to {@link createRun}. */
|
|
652
|
+
readonly deps: RunDeps & D;
|
|
653
|
+
/**
|
|
654
|
+
* @see {@link Concurrency}
|
|
655
|
+
* @see {@link concurrently}
|
|
656
|
+
*/
|
|
657
|
+
readonly concurrency: Concurrency;
|
|
658
|
+
/**
|
|
659
|
+
* Adds additional dependencies to this Run and returns it.
|
|
660
|
+
*
|
|
661
|
+
* Use for runtime-created dependencies — dependencies that cannot be created
|
|
662
|
+
* in the composition root (e.g., app start).
|
|
663
|
+
*
|
|
664
|
+
* ### Example
|
|
665
|
+
*
|
|
666
|
+
* ```ts
|
|
667
|
+
* // One-shot
|
|
668
|
+
* await run.addDeps({ db })(getUser(123));
|
|
669
|
+
*
|
|
670
|
+
* // Multiple deps at once
|
|
671
|
+
* await run.addDeps({ db, cache })(task);
|
|
672
|
+
*
|
|
673
|
+
* // Reusable — config comes from outside (message, file, etc.)
|
|
674
|
+
* type DbWorkerDeps = DbDep; // or DbDep & CacheDep & ...
|
|
675
|
+
*
|
|
676
|
+
* const init =
|
|
677
|
+
* (config: Config): Task<void, InitError, CreateDbDep> =>
|
|
678
|
+
* async (run) => {
|
|
679
|
+
* const { createDb } = run.deps;
|
|
680
|
+
* await using stack = new AsyncDisposableStack();
|
|
681
|
+
*
|
|
682
|
+
* const db = stack.use(await run.orThrow(startApp()));
|
|
683
|
+
* if (!db.ok) return db;
|
|
684
|
+
*
|
|
685
|
+
* const runWithDb = run.addDeps({ db: db.value });
|
|
686
|
+
*
|
|
687
|
+
* await runWithDb(getUser(123));
|
|
688
|
+
* await runWithDb(insertUser(user));
|
|
689
|
+
* return ok();
|
|
690
|
+
* };
|
|
691
|
+
* ```
|
|
692
|
+
*
|
|
693
|
+
* ## FAQ
|
|
694
|
+
*
|
|
695
|
+
* ### How does it work?
|
|
696
|
+
*
|
|
697
|
+
* This is the whole implementation:
|
|
698
|
+
*
|
|
699
|
+
* ```ts
|
|
700
|
+
* run.addDeps = <E extends NewKeys<E, D>>(newDeps: E): Run<D & E> => {
|
|
701
|
+
* depsRef.modify((currentDeps) => {
|
|
702
|
+
* const duplicate = Object.keys(newDeps).find(
|
|
703
|
+
* (k) => k in currentDeps,
|
|
704
|
+
* );
|
|
705
|
+
* assert(!duplicate, `Dependency '${duplicate}' already added.`);
|
|
706
|
+
* return [undefined, { ...currentDeps, ...newDeps }];
|
|
707
|
+
* });
|
|
708
|
+
* return self as unknown as Run<D & E>;
|
|
709
|
+
* };
|
|
710
|
+
* ```
|
|
711
|
+
*
|
|
712
|
+
* Dependencies are stored in a shared {@link Ref}, so `addDeps` propagates to
|
|
713
|
+
* all runs. The runtime assertion ensures dependencies are created once —
|
|
714
|
+
* automatic deduplication would mask poor design (dependencies should have a
|
|
715
|
+
* single, clear point of creation).
|
|
716
|
+
*/
|
|
717
|
+
readonly addDeps: <E extends NewKeys<E, D>>(extraDeps: E) => Run<D & E>;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* `Fiber` is a handle to a running {@link Task} that can be awaited, aborted, or
|
|
721
|
+
* disposed.
|
|
722
|
+
*
|
|
723
|
+
* ### Example
|
|
724
|
+
*
|
|
725
|
+
* ```ts
|
|
726
|
+
* await using run = createRun();
|
|
727
|
+
*
|
|
728
|
+
* // Await to get Result
|
|
729
|
+
* const result = await run(fetchData);
|
|
49
730
|
*
|
|
50
|
-
* //
|
|
51
|
-
* const
|
|
731
|
+
* // Abort manually
|
|
732
|
+
* const fiber = run(longRunningTask);
|
|
733
|
+
* fiber.abort();
|
|
734
|
+
* const aborted = await fiber; // Result contains AbortError (unless unabortable)
|
|
52
735
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
736
|
+
* // Auto-abort with `using`
|
|
737
|
+
* {
|
|
738
|
+
* using background = run(backgroundSync);
|
|
739
|
+
* await someOtherWork();
|
|
740
|
+
* } // background.abort() called automatically here
|
|
741
|
+
*
|
|
742
|
+
* // Run child tasks in fiber's scope
|
|
743
|
+
* fiber.run(childTask);
|
|
744
|
+
*
|
|
745
|
+
* // Monitor via the Run
|
|
746
|
+
* fiber.run.onEvent = (event) => {
|
|
747
|
+
* // handle event
|
|
748
|
+
* };
|
|
749
|
+
* ```
|
|
750
|
+
*
|
|
751
|
+
* Because `Fiber` is a {@link PromiseLike} object, Fibers can be composed with
|
|
752
|
+
* `Promise.all`, `Promise.race`, etc.
|
|
753
|
+
*
|
|
754
|
+
* Microtask timing: Run wraps the Task's promise with `.then` and `.finally`,
|
|
755
|
+
* which adds microtasks between Task completion and Fiber settlement. Do not
|
|
756
|
+
* write code that relies on a specific number of microtask yields between
|
|
757
|
+
* Tasks. Use explicit synchronization primitives instead.
|
|
758
|
+
*
|
|
759
|
+
* @group Core Types
|
|
760
|
+
*/
|
|
761
|
+
export interface Fiber<T = unknown, E = unknown, D = unknown> extends PromiseLike<Result<T, E | AbortError>>, Disposable {
|
|
762
|
+
readonly then: PromiseLike<Result<T, E | AbortError>>["then"];
|
|
763
|
+
/**
|
|
764
|
+
* A {@link Run} of this Fiber.
|
|
765
|
+
*
|
|
766
|
+
* Tasks run via this Run are aborted when the Fiber ends.
|
|
767
|
+
*
|
|
768
|
+
* ### Example
|
|
769
|
+
*
|
|
770
|
+
* ```ts
|
|
771
|
+
* const fiber = run(longRunningTask);
|
|
772
|
+
*
|
|
773
|
+
* // helperTask is aborted when longRunningTask ends
|
|
774
|
+
* fiber.run(helperTask);
|
|
775
|
+
*
|
|
776
|
+
* // Monitor this Fiber's Run
|
|
777
|
+
* fiber.run.onEvent = (event) => {
|
|
778
|
+
* console.log(event);
|
|
779
|
+
* };
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
782
|
+
readonly run: Run<D>;
|
|
783
|
+
/**
|
|
784
|
+
* Requests abort for this Fiber (and any child it started).
|
|
785
|
+
*
|
|
786
|
+
* ### Example
|
|
787
|
+
*
|
|
788
|
+
* ```ts
|
|
789
|
+
* const fiber = run(fetchData);
|
|
790
|
+
* fiber.abort();
|
|
791
|
+
* const result = await fiber; // err(AbortError)
|
|
792
|
+
* ```
|
|
793
|
+
*
|
|
794
|
+
* When abort is requested, the Fiber's result becomes {@link AbortError} even
|
|
795
|
+
* if the Task completed successfully. This keeps behavior predictable —
|
|
796
|
+
* calling `abort()` always yields `AbortError`.
|
|
797
|
+
*
|
|
798
|
+
* The optional reason is stored in `AbortError.reason`. Since any value can
|
|
799
|
+
* be passed, abort reasons are `unknown` — use typed errors for business
|
|
800
|
+
* logic. To inspect the reason, use type guards like
|
|
801
|
+
* `RaceLostError.is(reason)`.
|
|
802
|
+
*
|
|
803
|
+
* Abort is idempotent — calling multiple times has no additional effect
|
|
804
|
+
* beyond the first call.
|
|
805
|
+
*/
|
|
806
|
+
abort(reason?: unknown): void;
|
|
807
|
+
/** Returns the current {@link RunState} of this Fiber's {@link Run}. */
|
|
808
|
+
getState(): RunState<T, E>;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Extracts the value type from a {@link Fiber}.
|
|
812
|
+
*
|
|
813
|
+
* @group Type utilities
|
|
814
|
+
*/
|
|
815
|
+
export type InferFiberOk<F extends Fiber<any, any, any>> = F extends Fiber<infer T, any, any> ? T : never;
|
|
816
|
+
/**
|
|
817
|
+
* Extracts the error type from a {@link Fiber}.
|
|
818
|
+
*
|
|
819
|
+
* @group Type utilities
|
|
820
|
+
*/
|
|
821
|
+
export type InferFiberErr<F extends Fiber<any, any, any>> = F extends Fiber<any, infer E, any> ? E : never;
|
|
822
|
+
/**
|
|
823
|
+
* Extracts the deps type from a {@link Fiber}.
|
|
824
|
+
*
|
|
825
|
+
* @group Type utilities
|
|
826
|
+
*/
|
|
827
|
+
export type InferFiberDeps<F extends Fiber<any, any, any>> = F extends Fiber<any, any, infer D> ? D : never;
|
|
828
|
+
/**
|
|
829
|
+
* Abort mask depth for a {@link Run} or {@link Fiber}.
|
|
830
|
+
*
|
|
831
|
+
* - `0` — abortable (default)
|
|
832
|
+
* - `>= 1` — inside {@link unabortable}, abort requests are ignored
|
|
833
|
+
*
|
|
834
|
+
* The mask tracks nested unabortable regions. When abort is requested, the
|
|
835
|
+
* signal only propagates if `mask === 0`.
|
|
836
|
+
*
|
|
837
|
+
* - {@link unabortable} increments the mask — Task becomes protected
|
|
838
|
+
* - {@link unabortableMask} provides `restore` to restore the previous mask
|
|
839
|
+
* - Tasks inherit their parent's mask by default
|
|
840
|
+
*
|
|
841
|
+
* This enables nested resource lifecycle patterns where each level can have its
|
|
842
|
+
* own abortable section while outer acquisitions remain protected.
|
|
843
|
+
*
|
|
844
|
+
* UI/debugging tools can use this to visually distinguish protected Tasks
|
|
845
|
+
* (e.g., different icon or color) and explain why abort requests are ignored.
|
|
846
|
+
*
|
|
847
|
+
* @group Abort masking
|
|
848
|
+
*/
|
|
849
|
+
export declare const AbortMask: import("./Type.js").BrandType<import("./Type.js").BrandType<import("./Type.js").Type<"Brand", number & import("./Brand.js").Brand<"Int">, number, import("./Type.js").IntError, number, import("./Type.js").NumberError>, "NonNegative", import("./Type.js").NonNegativeError, import("./Type.js").NumberError | import("./Type.js").IntError>, "AbortMask", import("./Type.js").BrandWithoutRefineError<"AbortMask", import("./Type.js").NumberError | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, never>;
|
|
850
|
+
export type AbortMask = typeof AbortMask.Type;
|
|
851
|
+
/**
|
|
852
|
+
* Maximum number of concurrent Tasks.
|
|
853
|
+
*
|
|
854
|
+
* Default is 1 (sequential). Use 1-100 as a literal or {@link PositiveInt} for
|
|
855
|
+
* larger values.
|
|
856
|
+
*
|
|
857
|
+
* @group Concurrency primitives
|
|
858
|
+
* @see {@link concurrently}
|
|
859
|
+
* @see {@link createSemaphore}
|
|
860
|
+
*/
|
|
861
|
+
export type Concurrency = Int1To100 | PositiveInt;
|
|
862
|
+
/**
|
|
863
|
+
* The lifecycle state of a {@link Run}.
|
|
864
|
+
*
|
|
865
|
+
* - `Running` — Task running, no result yet
|
|
866
|
+
* - `Disposing` — abort requested, waiting for children to settle
|
|
867
|
+
* - `Settled` — settled with result and outcome
|
|
868
|
+
*
|
|
869
|
+
* @group Core Types
|
|
870
|
+
*/
|
|
871
|
+
export type RunState<T = unknown, E = unknown> = RunStateRunning | RunStateDisposing | RunStateSettled<T, E>;
|
|
872
|
+
export interface RunStateRunning extends Typed<"Running"> {
|
|
873
|
+
}
|
|
874
|
+
export interface RunStateDisposing extends Typed<"Disposing"> {
|
|
875
|
+
}
|
|
876
|
+
export interface RunStateSettled<T = unknown, E = unknown> extends Typed<"Settled"> {
|
|
877
|
+
/**
|
|
878
|
+
* The Run's completion value.
|
|
879
|
+
*
|
|
880
|
+
* If abort was requested, this is {@link AbortError} even if the Task
|
|
881
|
+
* completed successfully — see `outcome` for what the Task actually
|
|
882
|
+
* returned.
|
|
883
|
+
*/
|
|
884
|
+
readonly result: Result<T, E>;
|
|
885
|
+
/**
|
|
886
|
+
* What the Task actually returned.
|
|
887
|
+
*
|
|
888
|
+
* Unlike `result`, not overridden by abort.
|
|
889
|
+
*/
|
|
890
|
+
readonly outcome: Result<T, E>;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* {@link RunSnapshot} state Type.
|
|
894
|
+
*
|
|
895
|
+
* @group Monitoring
|
|
896
|
+
*/
|
|
897
|
+
export declare const RunSnapshotState: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
|
|
898
|
+
result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
899
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
900
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
901
|
+
}>, import("./Type.js").ObjectType<{
|
|
902
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
903
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
904
|
+
}>]>;
|
|
905
|
+
outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
906
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
907
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
908
|
+
}>, import("./Type.js").ObjectType<{
|
|
909
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
910
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
911
|
+
}>]>;
|
|
912
|
+
}>]>;
|
|
913
|
+
export type RunSnapshotState = typeof RunSnapshotState.Type;
|
|
914
|
+
/**
|
|
915
|
+
* A recursive snapshot of a {@link Run} tree.
|
|
916
|
+
*
|
|
917
|
+
* Snapshots use structural sharing — unchanged subtrees return the same object
|
|
918
|
+
* reference. This is useful for UI libraries like React that leverage
|
|
919
|
+
* referential transparency to skip re-rendering unchanged parts. Snapshots are
|
|
920
|
+
* computed on demand rather than pushed on every change. Push would require
|
|
921
|
+
* O(depth) new snapshot objects per mutation.
|
|
922
|
+
*
|
|
923
|
+
* @group Core Types
|
|
924
|
+
* @see {@link Run.snapshot}
|
|
925
|
+
*/
|
|
926
|
+
export interface RunSnapshot {
|
|
927
|
+
/** The {@link Run.id} this snapshot represents. */
|
|
928
|
+
readonly id: Id;
|
|
929
|
+
/** The current lifecycle state. */
|
|
930
|
+
readonly state: RunSnapshotState;
|
|
931
|
+
/** Child snapshots in run order. */
|
|
932
|
+
readonly children: ReadonlyArray<RunSnapshot>;
|
|
933
|
+
/** The abort mask depth. `0` means abortable, `>= 1` means unabortable. */
|
|
934
|
+
readonly abortMask: AbortMask;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* The event-specific payload of a {@link RunEvent}.
|
|
938
|
+
*
|
|
939
|
+
* @group Monitoring
|
|
940
|
+
*/
|
|
941
|
+
export declare const RunEventData: import("./Type.js").UnionType<[import("./Type.js").TypedType<"ChildAdded", {
|
|
942
|
+
childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
|
|
943
|
+
}>, import("./Type.js").TypedType<"ChildRemoved", {
|
|
944
|
+
childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
|
|
945
|
+
}>, import("./Type.js").TypedType<"StateChanged", {
|
|
946
|
+
state: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
|
|
947
|
+
result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
948
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
949
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
950
|
+
}>, import("./Type.js").ObjectType<{
|
|
951
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
952
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
953
|
+
}>]>;
|
|
954
|
+
outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
955
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
956
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
957
|
+
}>, import("./Type.js").ObjectType<{
|
|
958
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
959
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
960
|
+
}>]>;
|
|
961
|
+
}>]>;
|
|
962
|
+
}>]>;
|
|
963
|
+
export type RunEventData = typeof RunEventData.Type;
|
|
964
|
+
/**
|
|
965
|
+
* Events emitted by a {@link Run} for monitoring and debugging.
|
|
966
|
+
*
|
|
967
|
+
* Events bubble up through parent runs, enabling centralized monitoring at the
|
|
968
|
+
* root. Use with {@link Run.onEvent} to track Run lifecycle.
|
|
969
|
+
*
|
|
970
|
+
* @group Monitoring
|
|
971
|
+
*/
|
|
972
|
+
export declare const RunEvent: import("./Type.js").ObjectType<{
|
|
973
|
+
id: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
|
|
974
|
+
timestamp: import("./Type.js").BrandType<import("./Type.js").BrandType<import("./Type.js").Type<"Brand", number & import("./Brand.js").Brand<"Int"> & import("./Brand.js").Brand<"NonNegative">, number, import("./Type.js").NonNegativeError, number & import("./Brand.js").Brand<"Int">, import("./Type.js").NumberError | import("./Type.js").IntError>, "LessThan281474976710655", import("./Type.js").LessThanError<281474976710655>, import("./Type.js").NumberError | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, "Millis", import("./Type.js").BrandWithoutRefineError<"Millis", import("./Type.js").NumberError | import("./Type.js").LessThanError<281474976710655> | import("./Type.js").NonNegativeError | import("./Type.js").IntError>, never>;
|
|
975
|
+
data: import("./Type.js").UnionType<[import("./Type.js").TypedType<"ChildAdded", {
|
|
976
|
+
childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
|
|
977
|
+
}>, import("./Type.js").TypedType<"ChildRemoved", {
|
|
978
|
+
childId: import("./Type.js").BrandType<import("./Type.js").Type<"String", string, string, import("./Type.js").StringError, string, import("./Type.js").StringError>, "Id", import("./Type.js").IdError, import("./Type.js").StringError>;
|
|
979
|
+
}>, import("./Type.js").TypedType<"StateChanged", {
|
|
980
|
+
state: import("./Type.js").UnionType<[import("./Type.js").TypedType<"Running", Record<never, never>>, import("./Type.js").TypedType<"Disposing", Record<never, never>>, import("./Type.js").TypedType<"Settled", {
|
|
981
|
+
result: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
982
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
983
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
984
|
+
}>, import("./Type.js").ObjectType<{
|
|
985
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
986
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
987
|
+
}>]>;
|
|
988
|
+
outcome: import("./Type.js").UnionType<[import("./Type.js").ObjectType<{
|
|
989
|
+
ok: import("./Type.js").LiteralType<true>;
|
|
990
|
+
value: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
991
|
+
}>, import("./Type.js").ObjectType<{
|
|
992
|
+
ok: import("./Type.js").LiteralType<false>;
|
|
993
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
994
|
+
}>]>;
|
|
995
|
+
}>]>;
|
|
996
|
+
}>]>;
|
|
997
|
+
}>;
|
|
998
|
+
export interface RunEvent extends InferType<typeof RunEvent> {
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Configuration for {@link Run} behavior.
|
|
1002
|
+
*
|
|
1003
|
+
* @group Monitoring
|
|
1004
|
+
*/
|
|
1005
|
+
export interface RunConfig {
|
|
1006
|
+
/**
|
|
1007
|
+
* Whether to emit {@link RunEvent}s.
|
|
1008
|
+
*
|
|
1009
|
+
* Use a {@link Ref} to enable/disable at runtime without recreating the Run.
|
|
1010
|
+
* Disabled by default for zero overhead in production.
|
|
1011
|
+
*/
|
|
1012
|
+
readonly eventsEnabled: Ref<boolean>;
|
|
1013
|
+
}
|
|
1014
|
+
export interface RunConfigDep {
|
|
1015
|
+
readonly runConfig: RunConfig;
|
|
1016
|
+
}
|
|
1017
|
+
/** Default deps provided by {@link createRun}. */
|
|
1018
|
+
export type RunDeps = ConsoleDep & RandomBytesDep & RandomDep & TimeDep & Partial<RunConfigDep>;
|
|
1019
|
+
/**
|
|
1020
|
+
* Factory type for creating root {@link Run} instances.
|
|
1021
|
+
*
|
|
1022
|
+
* @group Creating Run
|
|
1023
|
+
*/
|
|
1024
|
+
export interface CreateRun<BaseDeps> {
|
|
1025
|
+
(): Run<BaseDeps>;
|
|
1026
|
+
<D>(deps: D): Run<BaseDeps & D>;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Creates root {@link Run}.
|
|
1030
|
+
*
|
|
1031
|
+
* The root Run is also the daemon Run: it stays running until disposed. Child
|
|
1032
|
+
* Runs created by `run(task)` are disposed by their parent once they settle.
|
|
1033
|
+
*
|
|
1034
|
+
* Call once per entry point (main thread, worker, etc.) and dispose on
|
|
1035
|
+
* shutdown. All Tasks run as descendants of this root Run.
|
|
1036
|
+
*
|
|
1037
|
+
* This common {@link createRun} is platform-agnostic. At application entry
|
|
1038
|
+
* points, prefer the platform adapter when one exists. `@evolu/web` adds
|
|
1039
|
+
* browser `error` and `unhandledrejection` handlers, `@evolu/nodejs` adds
|
|
1040
|
+
* Node.js `uncaughtException`, `unhandledRejection`, and graceful shutdown
|
|
1041
|
+
* handling, and `@evolu/react-native` adds React Native global error handling.
|
|
1042
|
+
*
|
|
1043
|
+
* {@link RunDeps} provides default dependencies:
|
|
1044
|
+
*
|
|
1045
|
+
* - {@link Time}
|
|
1046
|
+
* - {@link Console}
|
|
1047
|
+
* - {@link Random}
|
|
1048
|
+
* - {@link RandomBytes}
|
|
1049
|
+
*
|
|
1050
|
+
* ### Example
|
|
1051
|
+
*
|
|
1052
|
+
* ```ts
|
|
1053
|
+
* // App entry point
|
|
1054
|
+
* await using run = createRun();
|
|
1055
|
+
*
|
|
1056
|
+
* const result = await run(fetchData);
|
|
1057
|
+
* ```
|
|
1058
|
+
*
|
|
1059
|
+
* ### Example with custom dependencies
|
|
1060
|
+
*
|
|
1061
|
+
* ```ts
|
|
1062
|
+
* // Define dependency interfaces
|
|
1063
|
+
* interface Config {
|
|
1064
|
+
* readonly apiUrl: string;
|
|
1065
|
+
* }
|
|
1066
|
+
*
|
|
1067
|
+
* interface ConfigDep {
|
|
1068
|
+
* readonly config: Config;
|
|
1069
|
+
* }
|
|
1070
|
+
*
|
|
1071
|
+
* // Task declares its dependencies via the D type parameter
|
|
1072
|
+
* const fetchUser =
|
|
1073
|
+
* (id: string): Task<User, FetchError, ConfigDep> =>
|
|
1074
|
+
* async (run) => {
|
|
1075
|
+
* const { config } = run.deps;
|
|
1076
|
+
* const response = await fetch(`${config.apiUrl}/users/${id}`);
|
|
1077
|
+
* // ...
|
|
1078
|
+
* };
|
|
1079
|
+
*
|
|
1080
|
+
* // Composition root: create a Run with custom deps
|
|
1081
|
+
* type AppDeps = RunDeps & ConfigDep;
|
|
1082
|
+
*
|
|
1083
|
+
* const appDeps: AppDeps = {
|
|
1084
|
+
* ...testCreateDeps(), // or spread individual deps
|
|
1085
|
+
* config: { apiUrl: "https://api.example.com" },
|
|
1086
|
+
* };
|
|
1087
|
+
*
|
|
1088
|
+
* await using run = createRun(appDeps);
|
|
1089
|
+
*
|
|
1090
|
+
* // Run type is inferred from the deps argument
|
|
1091
|
+
* const result = await run(fetchUser("123"));
|
|
1092
|
+
*
|
|
1093
|
+
* // TypeScript catches missing deps at compile time:
|
|
1094
|
+
* // await using run2 = createRun(); // Run<RunDeps>
|
|
1095
|
+
* // run2(fetchUser("123")); // Error: Property 'config' is missing
|
|
1096
|
+
* ```
|
|
1097
|
+
*
|
|
1098
|
+
* @group Creating Run
|
|
1099
|
+
*/
|
|
1100
|
+
export declare const createRun: CreateRun<RunDeps>;
|
|
1101
|
+
/**
|
|
1102
|
+
* Abort reason indicating a {@link Run} can no longer start new Tasks.
|
|
1103
|
+
*
|
|
1104
|
+
* Covers both disposing and settled Runs.
|
|
1105
|
+
*
|
|
1106
|
+
* @group Creating Run
|
|
1107
|
+
*/
|
|
1108
|
+
export declare const RunStoppedError: import("./Type.js").TypedType<"RunStoppedError", Record<never, never>>;
|
|
1109
|
+
export interface RunStoppedError extends InferType<typeof RunStoppedError> {
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Shared {@link RunStoppedError} instance used as the default
|
|
1113
|
+
* {@link AbortError.reason} when a Task is started on a non-running {@link Run}.
|
|
1114
|
+
*
|
|
1115
|
+
* @group Creating Run
|
|
1116
|
+
*/
|
|
1117
|
+
export declare const runStoppedError: RunStoppedError;
|
|
1118
|
+
/**
|
|
1119
|
+
* Makes a {@link Task} unabortable.
|
|
1120
|
+
*
|
|
1121
|
+
* Once started, an unabortable Task always completes — abort requests are
|
|
1122
|
+
* ignored and `signal.aborted` remains `false`.
|
|
1123
|
+
*
|
|
1124
|
+
* If the parent {@link Run} is already disposing or settled, `run(task)`
|
|
1125
|
+
* short-circuits before task execution and returns `err(AbortError)` with
|
|
1126
|
+
* {@link runStoppedError} as reason. So `unabortable` means “do not interrupt
|
|
1127
|
+
* this Task once it has started”, not “remove AbortError from its type”.
|
|
1128
|
+
*
|
|
1129
|
+
* When that pre-start abort would be a programmer error, assert it explicitly
|
|
1130
|
+
* with `assertNotAborted` after awaiting the result.
|
|
1131
|
+
*
|
|
1132
|
+
* ### Example
|
|
1133
|
+
*
|
|
1134
|
+
* ```ts
|
|
1135
|
+
* await using run = createRun();
|
|
1136
|
+
*
|
|
1137
|
+
* const events: Array<string> = [];
|
|
1138
|
+
* const canComplete = Promise.withResolvers<void>();
|
|
1139
|
+
* let signalAbortedInAnalytics = true;
|
|
1140
|
+
*
|
|
1141
|
+
* // Simulate async analytics API (abortable by default)
|
|
1142
|
+
* const sendToAnalytics =
|
|
1143
|
+
* (event: number): Task<void, never> =>
|
|
1144
|
+
* async ({ signal }) => {
|
|
1145
|
+
* await canComplete.promise;
|
|
1146
|
+
* signalAbortedInAnalytics = signal.aborted;
|
|
1147
|
+
* events.push(`sent ${event}`);
|
|
1148
|
+
* return ok();
|
|
1149
|
+
* };
|
|
1150
|
+
*
|
|
1151
|
+
* // Important events must be sent even if the user navigates away
|
|
1152
|
+
* const trackImportantEvent = (event: number) =>
|
|
1153
|
+
* unabortable(sendToAnalytics(event));
|
|
1154
|
+
*
|
|
1155
|
+
* // User clicks, we start tracking (Task runs until first await)
|
|
1156
|
+
* const fiber = run(trackImportantEvent(123));
|
|
1157
|
+
*
|
|
1158
|
+
* // User navigates away (abort requested while task is running)
|
|
1159
|
+
* fiber.abort();
|
|
1160
|
+
* canComplete.resolve();
|
|
1161
|
+
*
|
|
1162
|
+
* const result = await fiber;
|
|
1163
|
+
*
|
|
1164
|
+
* expect(signalAbortedInAnalytics).toBe(false);
|
|
1165
|
+
* // Analytics was sent despite abort
|
|
1166
|
+
* expect(events).toEqual(["sent 123"]);
|
|
1167
|
+
* expect(result).toEqual(ok());
|
|
1168
|
+
* ```
|
|
1169
|
+
*
|
|
1170
|
+
* @group Abort masking
|
|
1171
|
+
*/
|
|
1172
|
+
export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
1173
|
+
/**
|
|
1174
|
+
* Like {@link unabortable}, but provides `restore` to restore abortability for
|
|
1175
|
+
* specific tasks.
|
|
1176
|
+
*
|
|
1177
|
+
* Tasks inherit abort masking from their parent. This means:
|
|
1178
|
+
*
|
|
1179
|
+
* - Tasks run inside `unabortableMask` are unabortable by default
|
|
1180
|
+
* - Tasks wrapped with `restore()` restore the previous abortability
|
|
1181
|
+
*
|
|
1182
|
+
* @group Abort masking
|
|
1183
|
+
*/
|
|
1184
|
+
export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2>(task: Task<T2, E2, D>) => Task<T2, E2, D>) => Task<T, E, D>) => Task<T, E, D>;
|
|
1185
|
+
/**
|
|
1186
|
+
* Runs tasks concurrently instead of sequentially.
|
|
1187
|
+
*
|
|
1188
|
+
* Sets the {@link Concurrency} level for a {@link Task}, which helpers like
|
|
1189
|
+
* {@link all}, {@link map}, etc. use to control how many tasks run at once.
|
|
1190
|
+
*
|
|
1191
|
+
* By default, tasks run sequentially (one at a time) to encourage thinking
|
|
1192
|
+
* about concurrency explicitly.
|
|
1193
|
+
*
|
|
1194
|
+
* For tuple-based calls like `all([taskA, taskB, taskC])` with a known small
|
|
1195
|
+
* number of tasks, omit the limit (runs unlimited). For arrays of unknown
|
|
1196
|
+
* length, always specify a limit.
|
|
1197
|
+
*
|
|
1198
|
+
* Concurrency is inherited by child tasks and can be overridden at any level.
|
|
1199
|
+
* Composition helpers should respect inherited concurrency — they should not
|
|
1200
|
+
* override it with a fixed number unless semantically required (like
|
|
1201
|
+
* {@link race}). Helpers with a recommended concurrency should export it for use
|
|
1202
|
+
* with `concurrently`.
|
|
1203
|
+
*
|
|
1204
|
+
* ### Example
|
|
1205
|
+
*
|
|
1206
|
+
* ```ts
|
|
1207
|
+
* // Unlimited (omit the limit)
|
|
1208
|
+
* run(concurrently(all([fetchA, fetchB, fetchC])));
|
|
1209
|
+
*
|
|
1210
|
+
* // Limited — at most 5 tasks run at a time
|
|
1211
|
+
* run(concurrently(5, all(tasks)));
|
|
1212
|
+
* run(concurrently(5, map(userIds, fetchUser)));
|
|
1213
|
+
*
|
|
1214
|
+
* // Inherited — inner all() uses parent's limit
|
|
1215
|
+
* const pipeline = concurrently(5, async (run) => {
|
|
1216
|
+
* const users = await run(map(userIds, fetchUser)); // uses 5
|
|
1217
|
+
* if (!users.ok) return users;
|
|
1218
|
+
* return run(map(users.value, enrichUser)); // also uses 5
|
|
1219
|
+
* });
|
|
1220
|
+
* ```
|
|
1221
|
+
*
|
|
1222
|
+
* @group Composition
|
|
1223
|
+
*/
|
|
1224
|
+
export declare function concurrently<T, E, D = unknown>(concurrency: Concurrency, task: Task<T, E, D>): Task<T, E, D>;
|
|
1225
|
+
/** Unlimited. */
|
|
1226
|
+
export declare function concurrently<T, E, D = unknown>(task: Task<T, E, D>): Task<T, E, D>;
|
|
1227
|
+
/**
|
|
1228
|
+
* Yields execution to allow other work to proceed.
|
|
1229
|
+
*
|
|
1230
|
+
* Long-running JavaScript blocks the main thread. In browsers, this makes the
|
|
1231
|
+
* UI unresponsive (user interactions, animations). In Node.js, it prevents I/O
|
|
1232
|
+
* callbacks, timers, and other requests from being handled. Inserting yield
|
|
1233
|
+
* points lets the runtime process high-priority work between chunks of code.
|
|
1234
|
+
*
|
|
1235
|
+
* Uses `scheduler.yield()` in browsers for optimal main thread scheduling,
|
|
1236
|
+
* falls back to `setImmediate` in Node.js, or `setTimeout` elsewhere.
|
|
1237
|
+
*
|
|
1238
|
+
* ### Example
|
|
1239
|
+
*
|
|
1240
|
+
* ```ts
|
|
1241
|
+
* const processLargeArray: Task<void, never> = async (run) => {
|
|
1242
|
+
* const { time } = run.deps;
|
|
1243
|
+
* let lastYield = time.now();
|
|
1244
|
+
*
|
|
1245
|
+
* for (const item of largeArray) {
|
|
1246
|
+
* processItem(item);
|
|
1247
|
+
*
|
|
1248
|
+
* // Yield periodically to keep UI responsive
|
|
1249
|
+
* if (time.now() - lastYield > msLongTask) {
|
|
1250
|
+
* const r = await run(yieldNow);
|
|
1251
|
+
* if (!r.ok) return r;
|
|
1252
|
+
* lastYield = time.now();
|
|
1253
|
+
* }
|
|
1254
|
+
* }
|
|
1255
|
+
*
|
|
1256
|
+
* return ok();
|
|
1257
|
+
* };
|
|
1258
|
+
* ```
|
|
1259
|
+
*
|
|
1260
|
+
* Recursive tasks also benefit from periodic yields — without them, deep
|
|
1261
|
+
* recursion overflows the call stack:
|
|
1262
|
+
*
|
|
1263
|
+
* ```ts
|
|
1264
|
+
* const processRecursive =
|
|
1265
|
+
* (count: number, index: number, sum: number): Task<number> =>
|
|
1266
|
+
* async (run) => {
|
|
1267
|
+
* if (index >= count) return ok(sum);
|
|
1268
|
+
*
|
|
1269
|
+
* // Yield periodically to break synchronous call chains.
|
|
1270
|
+
* if (index > 0 && index % 1000 === 0) {
|
|
1271
|
+
* const y = await run(yieldNow);
|
|
1272
|
+
* if (!y.ok) return y;
|
|
1273
|
+
* }
|
|
1274
|
+
*
|
|
1275
|
+
* // Direct tail-call: no fiber overhead, stack-safe thanks to yieldNow.
|
|
1276
|
+
* return await processRecursive(count, index + 1, sum + index)(run);
|
|
1277
|
+
* };
|
|
1278
|
+
* ```
|
|
1279
|
+
*
|
|
1280
|
+
* @group Composition
|
|
1281
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield
|
|
1282
|
+
* @see https://web.dev/articles/optimize-long-tasks
|
|
1283
|
+
*/
|
|
1284
|
+
export declare const yieldNow: Task<void>;
|
|
1285
|
+
/**
|
|
1286
|
+
* Creates a {@link Task} from a callback-based API.
|
|
1287
|
+
*
|
|
1288
|
+
* Use this to wrap callback-style APIs (event listeners, Node.js callbacks,
|
|
1289
|
+
* etc.) into Tasks with proper abort handling.
|
|
1290
|
+
*
|
|
1291
|
+
* Optionally return a teardown function that runs on abort.
|
|
1292
|
+
*
|
|
1293
|
+
* ### Example
|
|
1294
|
+
*
|
|
1295
|
+
* ```ts
|
|
1296
|
+
* // The sleep helper is implemented using callback:
|
|
1297
|
+
* const sleep = (duration: Duration): Task<void> =>
|
|
1298
|
+
* callback(({ ok, deps: { time } }) => {
|
|
1299
|
+
* const id = time.setTimeout(ok, durationToMillis(duration));
|
|
1300
|
+
* return () => time.clearTimeout(id);
|
|
1301
|
+
* });
|
|
1302
|
+
*
|
|
1303
|
+
* // Wrap an event listener — use signal directly
|
|
1304
|
+
* const waitForClick = (element: HTMLElement): Task<MouseEvent> =>
|
|
1305
|
+
* callback(({ ok, signal }) => {
|
|
1306
|
+
* element.addEventListener("click", ok, { once: true, signal });
|
|
1307
|
+
* });
|
|
1308
|
+
*
|
|
1309
|
+
* // Wrap Node.js callback API
|
|
1310
|
+
* const readFile = (path: string): Task<string, NodeJS.ErrnoException> =>
|
|
1311
|
+
* callback(({ ok, err }) => {
|
|
1312
|
+
* fs.readFile(path, "utf8", (error, data) => {
|
|
1313
|
+
* if (error) err(error);
|
|
1314
|
+
* else ok(data);
|
|
1315
|
+
* });
|
|
1316
|
+
* });
|
|
1317
|
+
* ```
|
|
1318
|
+
*
|
|
1319
|
+
* @group Composition
|
|
1320
|
+
*/
|
|
1321
|
+
export declare const callback: <T, E = never>(callback: CallbackWithTeardown<{
|
|
1322
|
+
readonly ok: Callback<T>;
|
|
1323
|
+
readonly err: Callback<E>;
|
|
1324
|
+
readonly signal: AbortSignal;
|
|
1325
|
+
readonly deps: RunDeps;
|
|
1326
|
+
}>) => Task<T, E>;
|
|
1327
|
+
/**
|
|
1328
|
+
* Pauses execution for a specified duration.
|
|
1329
|
+
*
|
|
1330
|
+
* ### Example
|
|
1331
|
+
*
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* const task: Task<void> = async (run) => {
|
|
1334
|
+
* console.log("Starting...");
|
|
1335
|
+
* await run(sleep("1s"));
|
|
1336
|
+
* console.log("Done after 1 second");
|
|
1337
|
+
* return ok();
|
|
1338
|
+
* };
|
|
1339
|
+
* ```
|
|
1340
|
+
*
|
|
1341
|
+
* @group Composition
|
|
1342
|
+
*/
|
|
1343
|
+
export declare const sleep: (duration: Duration) => Task<void>;
|
|
1344
|
+
/**
|
|
1345
|
+
* Returns a {@link Task} that completes first.
|
|
1346
|
+
*
|
|
1347
|
+
* Like
|
|
1348
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race | Promise.race},
|
|
1349
|
+
* the first Task to complete (whether success or failure) wins. All other Tasks
|
|
1350
|
+
* are aborted. Use {@link any} if you need the first Task to succeed instead.
|
|
1351
|
+
*
|
|
1352
|
+
* Requires a non-empty array — racing zero Tasks has no meaningful result
|
|
1353
|
+
* (there's no "first to complete" without participants). This is enforced at
|
|
1354
|
+
* compile time for non-empty tuple types. For other arrays, guard with
|
|
1355
|
+
* {@link isNonEmptyArray}:
|
|
1356
|
+
*
|
|
1357
|
+
* ```ts
|
|
1358
|
+
* if (isNonEmptyArray(tasks)) {
|
|
1359
|
+
* await run(race(tasks));
|
|
1360
|
+
* }
|
|
1361
|
+
* ```
|
|
1362
|
+
*
|
|
1363
|
+
* ### Example
|
|
1364
|
+
*
|
|
1365
|
+
* ```ts
|
|
1366
|
+
* const fast: Task<string> = () => ok("fast");
|
|
1367
|
+
* const slow: Task<string> = async (run) => {
|
|
1368
|
+
* await run(sleep("10ms"));
|
|
1369
|
+
* return ok("slow");
|
|
1370
|
+
* };
|
|
1371
|
+
*
|
|
1372
|
+
* // First wins, others are aborted.
|
|
1373
|
+
* const result = await run(race([fast, slow])); // ok("fast")
|
|
1374
|
+
* ```
|
|
1375
|
+
*
|
|
1376
|
+
* Always runs with unlimited concurrency — a sequential race makes no sense
|
|
1377
|
+
* since the first Task would always "win".
|
|
1378
|
+
*
|
|
1379
|
+
* @group Composition
|
|
1380
|
+
*/
|
|
1381
|
+
export declare const race: <T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, { abortReason, }?: {
|
|
1382
|
+
/** Abort reason for losing tasks. Defaults to {@link raceLostError}. */
|
|
1383
|
+
abortReason?: unknown;
|
|
1384
|
+
}) => Task<InferTaskOk<T[number]>, InferTaskErr<T[number]>, InferTaskDeps<T[number]>>;
|
|
1385
|
+
/**
|
|
1386
|
+
* Abort reason for tasks that lose a {@link race}.
|
|
1387
|
+
*
|
|
1388
|
+
* @group Composition
|
|
1389
|
+
*/
|
|
1390
|
+
export declare const RaceLostError: import("./Type.js").TypedType<"RaceLostError", Record<never, never>>;
|
|
1391
|
+
export interface RaceLostError extends InferType<typeof RaceLostError> {
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* {@link RaceLostError} used as abort reason in {@link race}.
|
|
1395
|
+
*
|
|
1396
|
+
* @group Composition
|
|
1397
|
+
*/
|
|
1398
|
+
export declare const raceLostError: RaceLostError;
|
|
1399
|
+
/**
|
|
1400
|
+
* Wraps a {@link Task} with a time limit.
|
|
1401
|
+
*
|
|
1402
|
+
* Returns {@link TimeoutError} if the Task doesn't complete within the specified
|
|
1403
|
+
* duration. The original Task is aborted when the timeout fires.
|
|
1404
|
+
*
|
|
1405
|
+
* ### Example
|
|
1406
|
+
*
|
|
1407
|
+
* ```ts
|
|
1408
|
+
* const fetchWithTimeout = timeout(fetchData, "5s");
|
|
1409
|
+
*
|
|
1410
|
+
* const result = await run(fetchWithTimeout);
|
|
1411
|
+
* if (!result.ok && result.error.type === "TimeoutError") {
|
|
1412
|
+
* console.log("Request timed out");
|
|
1413
|
+
* }
|
|
1414
|
+
* ```
|
|
1415
|
+
*
|
|
1416
|
+
* @group Composition
|
|
1417
|
+
*/
|
|
1418
|
+
export declare const timeout: <T, E, D = unknown>(task: Task<T, E, D>, duration: Duration, { abortReason, }?: {
|
|
1419
|
+
/**
|
|
1420
|
+
* Abort reason for the task when timeout fires. Defaults to
|
|
1421
|
+
* {@link timeoutError}.
|
|
1422
|
+
*/
|
|
1423
|
+
abortReason?: unknown;
|
|
1424
|
+
}) => Task<T, E | TimeoutError, D>;
|
|
1425
|
+
/**
|
|
1426
|
+
* Typed error returned by {@link timeout} when a task exceeds its time limit.
|
|
1427
|
+
*
|
|
1428
|
+
* @group Composition
|
|
1429
|
+
*/
|
|
1430
|
+
export declare const TimeoutError: import("./Type.js").TypedType<"TimeoutError", Record<never, never>>;
|
|
1431
|
+
export interface TimeoutError extends InferType<typeof TimeoutError> {
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* {@link TimeoutError} used as abort reason in {@link timeout}.
|
|
1435
|
+
*
|
|
1436
|
+
* @group Composition
|
|
1437
|
+
*/
|
|
1438
|
+
export declare const timeoutError: TimeoutError;
|
|
1439
|
+
/**
|
|
1440
|
+
* Options for {@link retry}.
|
|
1441
|
+
*
|
|
1442
|
+
* @group Composition
|
|
1443
|
+
*/
|
|
1444
|
+
export interface RetryOptions<E, Output> {
|
|
1445
|
+
/** Predicate to determine if error is retryable. Defaults to all errors. */
|
|
1446
|
+
readonly retryable?: Predicate<E>;
|
|
1447
|
+
/**
|
|
1448
|
+
* Callback invoked before each retry attempt with error, retry attempt
|
|
1449
|
+
* number, schedule output, and delay.
|
|
1450
|
+
*/
|
|
1451
|
+
readonly onRetry?: (attempt: RetryAttempt<E, Output>) => void;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Info passed to {@link retry} {@link RetryOptions.onRetry} callback.
|
|
1455
|
+
*
|
|
1456
|
+
* @group Composition
|
|
1457
|
+
*/
|
|
1458
|
+
export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
|
|
1459
|
+
readonly error: E;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Error returned when all retry attempts are exhausted.
|
|
1463
|
+
*
|
|
1464
|
+
* @group Composition
|
|
1465
|
+
*/
|
|
1466
|
+
export interface RetryError<E> extends Typed<"RetryError"> {
|
|
1467
|
+
/** The error from the final attempt. */
|
|
1468
|
+
readonly cause: E;
|
|
1469
|
+
/** Total attempts made (initial + retries). */
|
|
1470
|
+
readonly attempts: PositiveInt;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Wraps a {@link Task} with retry logic.
|
|
1474
|
+
*
|
|
1475
|
+
* Retries the Task according to the {@link Schedule}'s rules. Use
|
|
1476
|
+
* {@link RetryOptions.retryable} to filter which errors should trigger retries.
|
|
1477
|
+
*
|
|
1478
|
+
* All non-abort errors are wrapped in {@link RetryError}:
|
|
1479
|
+
*
|
|
1480
|
+
* - Task succeeds → `ok(value)`
|
|
1481
|
+
* - Task returns {@link AbortError} → `err(AbortError)` — passed through, no
|
|
1482
|
+
* retry, no wrapping
|
|
1483
|
+
* - Task returns any other error → retry until schedule exhausted or `retryable`
|
|
1484
|
+
* returns false → `err(RetryError)` with `cause` = the last error
|
|
1485
|
+
*
|
|
1486
|
+
* The `RetryError` is informative: "I tried N times, here's why I finally gave
|
|
1487
|
+
* up" — and `cause` contains the actual underlying error.
|
|
1488
|
+
*
|
|
1489
|
+
* ### Example
|
|
1490
|
+
*
|
|
1491
|
+
* ```ts
|
|
1492
|
+
* import {
|
|
1493
|
+
* exponential,
|
|
1494
|
+
* jitter,
|
|
1495
|
+
* maxDelay,
|
|
1496
|
+
* retry,
|
|
1497
|
+
* take,
|
|
1498
|
+
* } from "@evolu/common";
|
|
1499
|
+
*
|
|
1500
|
+
* const fetchWithRetry = retry(
|
|
1501
|
+
* fetchData,
|
|
1502
|
+
* // A jittered, capped, limited exponential backoff.
|
|
1503
|
+
* jitter(1)(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
1504
|
+
* );
|
|
1505
|
+
*
|
|
1506
|
+
* const result = await run(fetchWithRetry);
|
|
1507
|
+
* if (!result.ok) {
|
|
1508
|
+
* if (AbortError.is(result.error)) {
|
|
1509
|
+
* // Was aborted externally
|
|
1510
|
+
* } else {
|
|
1511
|
+
* // RetryError — failed after retrying
|
|
1512
|
+
* console.log(`Failed after ${result.error.attempts} attempts`);
|
|
1513
|
+
* console.log(`Last error:`, result.error.cause);
|
|
1514
|
+
* }
|
|
1515
|
+
* }
|
|
1516
|
+
* ```
|
|
1517
|
+
*
|
|
1518
|
+
* The schedule receives the error as input, enabling error-aware strategies
|
|
1519
|
+
* like stopping on fatal errors:
|
|
1520
|
+
*
|
|
1521
|
+
* ```ts
|
|
1522
|
+
* import { whileScheduleInput } from "@evolu/common";
|
|
1523
|
+
*
|
|
1524
|
+
* // Don't retry fatal errors
|
|
1525
|
+
* const smartRetry = retry(
|
|
1526
|
+
* fetchData,
|
|
1527
|
+
* whileScheduleInput((e: FetchError) => e.type !== "FatalError")(
|
|
1528
|
+
* take(5)(spaced("1s")),
|
|
1529
|
+
* ),
|
|
1530
|
+
* );
|
|
1531
|
+
* ```
|
|
1532
|
+
*
|
|
1533
|
+
* @group Composition
|
|
1534
|
+
* @see {@link RetryOptions}
|
|
1535
|
+
*/
|
|
1536
|
+
export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, E>, { retryable, onRetry, }?: RetryOptions<E, Output>) => Task<T, RetryError<E>, D>;
|
|
1537
|
+
/**
|
|
1538
|
+
* Options for {@link repeat}.
|
|
1539
|
+
*
|
|
1540
|
+
* @group Composition
|
|
1541
|
+
*/
|
|
1542
|
+
export interface RepeatOptions<T, Output> {
|
|
1543
|
+
/** Predicate to determine if value is repeatable. Defaults to all values. */
|
|
1544
|
+
readonly repeatable?: Predicate<T>;
|
|
1545
|
+
/**
|
|
1546
|
+
* Callback invoked before each repeat with value, repeat attempt number,
|
|
1547
|
+
* schedule output, and delay.
|
|
1548
|
+
*/
|
|
1549
|
+
readonly onRepeat?: (attempt: RepeatAttempt<T, Output>) => void;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Info passed to {@link repeat} {@link RepeatOptions.onRepeat} callback.
|
|
1553
|
+
*
|
|
1554
|
+
* @group Composition
|
|
1555
|
+
*/
|
|
1556
|
+
export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
1557
|
+
readonly value: T;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Repeats a {@link Task} according to a {@link Schedule}.
|
|
1561
|
+
*
|
|
1562
|
+
* Runs the Task, then checks the schedule to determine if it should repeat. The
|
|
1563
|
+
* schedule controls how many repetitions occur and the delay between them.
|
|
1564
|
+
* Continues until the schedule returns `Err(Done<void>)` or the Task fails.
|
|
1565
|
+
*
|
|
1566
|
+
* With `take(n)`, the task runs n+1 times (initial run plus n repetitions).
|
|
1567
|
+
*
|
|
1568
|
+
* Also works with {@link NextTask} — when the Task returns `Err(Done<D>)`,
|
|
1569
|
+
* repeat stops and propagates the done signal.
|
|
1570
|
+
*
|
|
1571
|
+
* ### Example
|
|
1572
|
+
*
|
|
1573
|
+
* ```ts
|
|
1574
|
+
* import { fixed, take } from "@evolu/common/schedule";
|
|
1575
|
+
* import { repeat } from "@evolu/common";
|
|
1576
|
+
*
|
|
1577
|
+
* // Heartbeat every 30 seconds (runs forever until aborted)
|
|
1578
|
+
* const heartbeat = repeat(sendHeartbeat, fixed("30s"));
|
|
1579
|
+
*
|
|
1580
|
+
* // Poll 4 times total (initial + 3 repetitions), 1 second apart
|
|
1581
|
+
* const poll = repeat(checkStatus, take(3)(fixed("1s")));
|
|
1582
|
+
*
|
|
1583
|
+
* // Process queue items until empty (NextTask pattern)
|
|
1584
|
+
* const processQueue: NextTask<Item, ProcessError, void> = async (run) => {
|
|
1585
|
+
* const item = queue.dequeue();
|
|
1586
|
+
* if (!item) return err(done()); // Queue empty, stop
|
|
1587
|
+
* await process(item);
|
|
1588
|
+
* return ok(item);
|
|
1589
|
+
* };
|
|
1590
|
+
*
|
|
1591
|
+
* const result = await run(repeat(processQueue, fixed("100ms")));
|
|
1592
|
+
* if (!result.ok && result.error.type === "Done") {
|
|
1593
|
+
* console.log("Queue exhausted");
|
|
1594
|
+
* }
|
|
1595
|
+
* ```
|
|
1596
|
+
*
|
|
1597
|
+
* @group Composition
|
|
1598
|
+
*/
|
|
1599
|
+
export declare const repeat: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, T>, { repeatable, onRepeat, }?: RepeatOptions<T, Output>) => Task<T, E, D>;
|
|
1600
|
+
/**
|
|
1601
|
+
* A value that can be resolved later.
|
|
1602
|
+
*
|
|
1603
|
+
* Similar to
|
|
1604
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers | Promise.withResolvers},
|
|
1605
|
+
* but integrated with {@link Task} and {@link Run} for cancellation support.
|
|
1606
|
+
*
|
|
1607
|
+
* Use for bridging callback-based APIs or coordinating between Tasks.
|
|
1608
|
+
*
|
|
1609
|
+
* Disposing aborts all waiting Tasks with an {@link AbortError} with
|
|
1610
|
+
* {@link deferredDisposedError} reason.
|
|
1611
|
+
*
|
|
1612
|
+
* ### Example
|
|
1613
|
+
*
|
|
1614
|
+
* ```ts
|
|
1615
|
+
* const deferred = createDeferred<string, MyError>();
|
|
1616
|
+
*
|
|
1617
|
+
* // Start waiting for the value
|
|
1618
|
+
* const fiber = run(deferred.task);
|
|
1619
|
+
*
|
|
1620
|
+
* // Resolve from elsewhere (callback, another task, etc.)
|
|
1621
|
+
* deferred.resolve(ok("value"));
|
|
1622
|
+
*
|
|
1623
|
+
* const result = await fiber; // ok("value")
|
|
1624
|
+
* ```
|
|
1625
|
+
*
|
|
1626
|
+
* @group Concurrency primitives
|
|
1627
|
+
* @see {@link createDeferred}
|
|
1628
|
+
*/
|
|
1629
|
+
export interface Deferred<T, E = never> extends Disposable {
|
|
1630
|
+
/** A {@link Task} that waits until {@link Deferred.resolve} is called. */
|
|
1631
|
+
readonly task: Task<T, E | DeferredDisposedError>;
|
|
1632
|
+
/** Resolves the value. Returns `true` once, then `false`. */
|
|
1633
|
+
readonly resolve: (result: Result<T, E | AbortError | DeferredDisposedError>) => boolean;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Creates a {@link Deferred}.
|
|
1637
|
+
*
|
|
1638
|
+
* @group Concurrency primitives
|
|
1639
|
+
*/
|
|
1640
|
+
export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
|
|
1641
|
+
/**
|
|
1642
|
+
* Abort reason used when a {@link Deferred} is disposed.
|
|
1643
|
+
*
|
|
1644
|
+
* @group Concurrency primitives
|
|
1645
|
+
*/
|
|
1646
|
+
export declare const DeferredDisposedError: import("./Type.js").TypedType<"DeferredDisposedError", Record<never, never>>;
|
|
1647
|
+
export interface DeferredDisposedError extends InferType<typeof DeferredDisposedError> {
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* {@link DeferredDisposedError} used as abort reason in {@link createDeferred}.
|
|
1651
|
+
*
|
|
1652
|
+
* @group Concurrency primitives
|
|
1653
|
+
*/
|
|
1654
|
+
export declare const deferredDisposedError: DeferredDisposedError;
|
|
1655
|
+
/**
|
|
1656
|
+
* A blocking {@link Task} — like a gate.
|
|
56
1657
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* retry(
|
|
60
|
-
* {
|
|
61
|
-
* retries: PositiveInt.orThrow(3),
|
|
62
|
-
* initialDelay: "100ms",
|
|
63
|
-
* },
|
|
64
|
-
* fetchWithTimeout(url),
|
|
65
|
-
* );
|
|
1658
|
+
* - **Closed**: Tasks wait.
|
|
1659
|
+
* - **Open**: Tasks proceed.
|
|
66
1660
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* ) => Task<
|
|
70
|
-
* Response,
|
|
71
|
-
* TimeoutError | FetchError | RetryError<TimeoutError | FetchError>
|
|
72
|
-
* >;
|
|
1661
|
+
* Use it to pause execution based on a condition. Unlike a {@link Deferred}
|
|
1662
|
+
* (which triggers once), a {@link Gate} can be opened and closed repeatedly.
|
|
73
1663
|
*
|
|
74
|
-
*
|
|
1664
|
+
* Disposing aborts all waiting Tasks with {@link deferredDisposedError}.
|
|
75
1665
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* // Usage
|
|
89
|
-
* const results = await Promise.all(
|
|
90
|
-
* [
|
|
91
|
-
* "https://api.example.com/users",
|
|
92
|
-
* "https://api.example.com/posts",
|
|
93
|
-
* "https://api.example.com/comments",
|
|
94
|
-
* ]
|
|
95
|
-
* .map(fetchWithPermit)
|
|
96
|
-
* .map((task) => task()),
|
|
97
|
-
* );
|
|
1666
|
+
* @group Concurrency primitives
|
|
1667
|
+
* @see {@link createGate}
|
|
1668
|
+
*/
|
|
1669
|
+
export interface Gate<D = unknown> extends Disposable {
|
|
1670
|
+
readonly wait: Task<void, DeferredDisposedError, D>;
|
|
1671
|
+
readonly open: () => void;
|
|
1672
|
+
readonly close: () => void;
|
|
1673
|
+
readonly isOpen: () => boolean;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Creates a {@link Gate} that starts closed.
|
|
98
1677
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* Response,
|
|
102
|
-
* | AbortError
|
|
103
|
-
* | TimeoutError
|
|
104
|
-
* | FetchError
|
|
105
|
-
* | RetryError<TimeoutError | FetchError>
|
|
106
|
-
* >
|
|
107
|
-
* >;
|
|
1678
|
+
* Useful for "stop/go" logic where multiple Tasks need to wait for a state
|
|
1679
|
+
* change.
|
|
108
1680
|
*
|
|
109
|
-
*
|
|
110
|
-
* for (const result of results) {
|
|
111
|
-
* if (result.ok) {
|
|
112
|
-
* // Process successful response
|
|
113
|
-
* const response = result.value;
|
|
114
|
-
* expect(response).toBeInstanceOf(Response);
|
|
115
|
-
* } else {
|
|
116
|
-
* // Handle error (TimeoutError, FetchError, RetryError, or AbortError)
|
|
117
|
-
* expect(result.error).toBeDefined();
|
|
118
|
-
* }
|
|
119
|
-
* }
|
|
1681
|
+
* ### Example
|
|
120
1682
|
*
|
|
121
|
-
*
|
|
122
|
-
* const
|
|
123
|
-
* const cancelableTask = fetchWithPermit("https://api.example.com/data");
|
|
1683
|
+
* ```ts
|
|
1684
|
+
* const networkGate = createGate();
|
|
124
1685
|
*
|
|
125
|
-
* //
|
|
126
|
-
* const
|
|
1686
|
+
* // Pause processing when offline
|
|
1687
|
+
* const onOffline = () => networkGate.close();
|
|
127
1688
|
*
|
|
128
|
-
* //
|
|
129
|
-
*
|
|
130
|
-
* controller.abort("User cancelled");
|
|
131
|
-
* }, 1000);
|
|
1689
|
+
* // Resume processing when online
|
|
1690
|
+
* const onOnline = () => networkGate.open();
|
|
132
1691
|
*
|
|
133
|
-
* const
|
|
134
|
-
*
|
|
1692
|
+
* const syncLoop = async (run) => {
|
|
1693
|
+
* while (true) {
|
|
1694
|
+
* // Blocks here whenever the gate is closed
|
|
1695
|
+
* await run(networkGate.wait);
|
|
1696
|
+
* await run(uploadNextItem);
|
|
1697
|
+
* }
|
|
1698
|
+
* };
|
|
135
1699
|
* ```
|
|
136
1700
|
*
|
|
137
|
-
*
|
|
1701
|
+
* @group Concurrency primitives
|
|
1702
|
+
*/
|
|
1703
|
+
export declare const createGate: <D = unknown>() => Gate<D>;
|
|
1704
|
+
/**
|
|
1705
|
+
* A semaphore that limits the number of concurrent {@link Task}s.
|
|
1706
|
+
*
|
|
1707
|
+
* For mutual exclusion (limiting to exactly one {@link Task}), use {@link Mutex}
|
|
1708
|
+
* instead.
|
|
138
1709
|
*
|
|
139
|
-
*
|
|
140
|
-
* dependencies and `TaskContext` for execution context like cancellation. Usage
|
|
141
|
-
* follows the pattern: deps → arguments → execution context.
|
|
1710
|
+
* @group Concurrency primitives
|
|
142
1711
|
*/
|
|
143
|
-
export interface
|
|
1712
|
+
export interface Semaphore extends Disposable {
|
|
144
1713
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* Provide a context with an AbortSignal to enable cancellation. When called
|
|
148
|
-
* without a signal, {@link AbortError} cannot occur and the error type narrows
|
|
149
|
-
* accordingly.
|
|
1714
|
+
* Executes a {@link Task} while holding a semaphore permit.
|
|
150
1715
|
*
|
|
151
|
-
*
|
|
1716
|
+
* The Task waits until a permit is available. If the semaphore is disposed
|
|
1717
|
+
* while waiting or running, the Task is aborted with an {@link AbortError}
|
|
1718
|
+
* whose reason is {@link semaphoreDisposedError}.
|
|
1719
|
+
*/
|
|
1720
|
+
readonly withPermit: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
1721
|
+
/**
|
|
1722
|
+
* Executes a {@link Task} while holding a specified number of permits.
|
|
152
1723
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* readonly error: unknown;
|
|
157
|
-
* }
|
|
158
|
-
*
|
|
159
|
-
* // Task version of fetch with proper error handling and cancellation support.
|
|
160
|
-
* const fetch = (url: string) =>
|
|
161
|
-
* toTask((context) =>
|
|
162
|
-
* tryAsync(
|
|
163
|
-
* () => globalThis.fetch(url, { signal: context?.signal ?? null }),
|
|
164
|
-
* (error): FetchError => ({ type: "FetchError", error }),
|
|
165
|
-
* ),
|
|
166
|
-
* );
|
|
1724
|
+
* If insufficient permits are available, waits in FIFO order until permits
|
|
1725
|
+
* become available. If disposed while waiting or running, the Task is aborted
|
|
1726
|
+
* with {@link semaphoreDisposedError}.
|
|
167
1727
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
1728
|
+
* Use this for weighted concurrency where a Task represents a resource
|
|
1729
|
+
* demand, not just "one more Task". One permit is one resource unit.
|
|
170
1730
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
1731
|
+
* Example: with capacity `10`, a lightweight operation can reserve `1` permit
|
|
1732
|
+
* while a heavy operation reserves `4` permits. This models shared budgets
|
|
1733
|
+
* such as DB connections, API credits, memory/CPU buckets, or batch
|
|
1734
|
+
* processing slots.
|
|
173
1735
|
*
|
|
174
|
-
*
|
|
175
|
-
* const controller = new AbortController();
|
|
176
|
-
* const result2 = await fetch("https://api.example.com/data")(
|
|
177
|
-
* controller,
|
|
178
|
-
* );
|
|
179
|
-
* expectTypeOf(result2).toEqualTypeOf<
|
|
180
|
-
* Result<Response, FetchError | AbortError>
|
|
181
|
-
* >();
|
|
182
|
-
* ```
|
|
1736
|
+
* {@link Semaphore.withPermit} is equivalent to `withPermits(1)`.
|
|
183
1737
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1738
|
+
readonly withPermits: <T, E, D>(permits: Concurrency) => (task: Task<T, E, D>) => Task<T, E, D>;
|
|
1739
|
+
/** Returns the current semaphore state for monitoring/debugging. */
|
|
1740
|
+
readonly snapshot: () => SemaphoreSnapshot;
|
|
187
1741
|
}
|
|
188
|
-
/**
|
|
189
|
-
export interface
|
|
190
|
-
/**
|
|
191
|
-
readonly
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
readonly
|
|
196
|
-
|
|
1742
|
+
/** Snapshot returned by {@link Semaphore.snapshot}. */
|
|
1743
|
+
export interface SemaphoreSnapshot {
|
|
1744
|
+
/** Total permits configured at creation. */
|
|
1745
|
+
readonly permits: Concurrency;
|
|
1746
|
+
/** Currently held permits. */
|
|
1747
|
+
readonly taken: NonNegativeInt;
|
|
1748
|
+
/** Number of currently waiting Tasks. */
|
|
1749
|
+
readonly waiting: NonNegativeInt;
|
|
1750
|
+
/** Currently available permits. */
|
|
1751
|
+
readonly available: NonNegativeInt;
|
|
1752
|
+
/** Whether no permits are taken and no Tasks are waiting. */
|
|
1753
|
+
readonly isIdle: boolean;
|
|
1754
|
+
/** Whether the semaphore has been disposed. */
|
|
1755
|
+
readonly disposed: boolean;
|
|
197
1756
|
}
|
|
198
1757
|
/**
|
|
199
|
-
*
|
|
1758
|
+
* Creates a {@link Semaphore} that limits concurrent {@link Task}s.
|
|
200
1759
|
*
|
|
201
1760
|
* ### Example
|
|
202
1761
|
*
|
|
203
1762
|
* ```ts
|
|
204
|
-
*
|
|
205
|
-
* readonly type: "FetchError";
|
|
206
|
-
* readonly error: unknown;
|
|
207
|
-
* }
|
|
208
|
-
*
|
|
209
|
-
* // Task version of fetch with proper error handling and cancellation support.
|
|
210
|
-
* const fetch = (url: string) =>
|
|
211
|
-
* toTask((context) =>
|
|
212
|
-
* tryAsync(
|
|
213
|
-
* () => globalThis.fetch(url, { signal: context?.signal ?? null }),
|
|
214
|
-
* (error): FetchError => ({ type: "FetchError", error }),
|
|
215
|
-
* ),
|
|
216
|
-
* );
|
|
1763
|
+
* await using run = createRun();
|
|
217
1764
|
*
|
|
218
|
-
*
|
|
219
|
-
* fetch satisfies (url: string) => Task<Response, FetchError>;
|
|
1765
|
+
* const semaphore = createSemaphore(PositiveInt.orThrow(2));
|
|
220
1766
|
*
|
|
221
|
-
* const
|
|
222
|
-
*
|
|
1767
|
+
* const fetchUser =
|
|
1768
|
+
* (id: string): Task<string> =>
|
|
1769
|
+
* async (run) => {
|
|
1770
|
+
* const { console } = run.deps;
|
|
1771
|
+
* console.log("[demo]", "start", id);
|
|
1772
|
+
* const slept = await run(sleep("10ms"));
|
|
1773
|
+
* if (!slept.ok) return slept;
|
|
1774
|
+
* console.log("[demo]", "end", id);
|
|
1775
|
+
* return ok(`user:${id}`);
|
|
1776
|
+
* };
|
|
1777
|
+
*
|
|
1778
|
+
* const fetchWithPermit = (id: string) =>
|
|
1779
|
+
* semaphore.withPermit(fetchUser(id));
|
|
1780
|
+
*
|
|
1781
|
+
* await Promise.all([
|
|
1782
|
+
* run(fetchWithPermit("1")),
|
|
1783
|
+
* run(fetchWithPermit("2")),
|
|
1784
|
+
* run(fetchWithPermit("3")),
|
|
1785
|
+
* ]);
|
|
223
1786
|
*
|
|
224
|
-
* //
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
1787
|
+
* // [demo] start 1
|
|
1788
|
+
* // [demo] start 2
|
|
1789
|
+
* // [demo] end 1
|
|
1790
|
+
* // [demo] start 3
|
|
1791
|
+
* // [demo] end 2
|
|
1792
|
+
* // [demo] end 3
|
|
228
1793
|
* ```
|
|
1794
|
+
*
|
|
1795
|
+
* @group Concurrency primitives
|
|
1796
|
+
*/
|
|
1797
|
+
export declare const createSemaphore: (permits: Concurrency) => Semaphore;
|
|
1798
|
+
/**
|
|
1799
|
+
* Abort reason used when a {@link Semaphore} is disposed.
|
|
1800
|
+
*
|
|
1801
|
+
* @group Concurrency primitives
|
|
1802
|
+
*/
|
|
1803
|
+
export declare const SemaphoreDisposedError: import("./Type.js").TypedType<"SemaphoreDisposedError", Record<never, never>>;
|
|
1804
|
+
export interface SemaphoreDisposedError extends InferType<typeof SemaphoreDisposedError> {
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* {@link SemaphoreDisposedError} used as abort reason in {@link createSemaphore}.
|
|
1808
|
+
*
|
|
1809
|
+
* @group Concurrency primitives
|
|
1810
|
+
*/
|
|
1811
|
+
export declare const semaphoreDisposedError: SemaphoreDisposedError;
|
|
1812
|
+
/**
|
|
1813
|
+
* A keyed {@link Semaphore} registry.
|
|
1814
|
+
*
|
|
1815
|
+
* Provides semaphore operations per key while preserving the same API shape as
|
|
1816
|
+
* {@link Semaphore}.
|
|
1817
|
+
*
|
|
1818
|
+
* By default, {@link createSemaphoreByKey} uses reference identity for keys,
|
|
1819
|
+
* matching native `Map`. Callers may instead provide a {@link Lookup lookup} so
|
|
1820
|
+
* logical equality is based on a derived stable key.
|
|
1821
|
+
*
|
|
1822
|
+
* @group Concurrency primitives
|
|
1823
|
+
*/
|
|
1824
|
+
export interface SemaphoreByKey<K = unknown> extends Disposable {
|
|
1825
|
+
/**
|
|
1826
|
+
* Executes a {@link Task} while holding one permit for a specific key.
|
|
1827
|
+
*
|
|
1828
|
+
* Behaves like {@link Semaphore.withPermit}, scoped to `key`.
|
|
1829
|
+
*/
|
|
1830
|
+
readonly withPermit: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
|
|
1831
|
+
/**
|
|
1832
|
+
* Executes a {@link Task} while holding permits for a specific key.
|
|
1833
|
+
*
|
|
1834
|
+
* Behaves like {@link Semaphore.withPermits}, scoped to `key`.
|
|
1835
|
+
*/
|
|
1836
|
+
readonly withPermits: <T, E, D>(key: K, permits: Concurrency) => (task: Task<T, E, D>) => Task<T, E, D>;
|
|
1837
|
+
/** Returns current semaphore state for a key, or `null` if absent. */
|
|
1838
|
+
readonly snapshot: (key: K) => SemaphoreSnapshot | null;
|
|
1839
|
+
}
|
|
1840
|
+
/** Options for {@link createSemaphoreByKey}. */
|
|
1841
|
+
export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<K, L> {
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Creates a {@link SemaphoreByKey}.
|
|
1845
|
+
*
|
|
1846
|
+
* Each key gets its own semaphore with the same permit capacity.
|
|
1847
|
+
*
|
|
1848
|
+
* @group Concurrency primitives
|
|
229
1849
|
*/
|
|
230
|
-
export declare
|
|
1850
|
+
export declare function createSemaphoreByKey<K = unknown>(permits: Concurrency): SemaphoreByKey<K>;
|
|
1851
|
+
export declare function createSemaphoreByKey<K, L>(permits: Concurrency, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
|
|
231
1852
|
/**
|
|
232
|
-
*
|
|
1853
|
+
* A mutex (mutual exclusion) that ensures only one {@link Task} runs at a time.
|
|
1854
|
+
*
|
|
1855
|
+
* This is a specialized version of a {@link Semaphore} with a permit count of 1.
|
|
233
1856
|
*
|
|
234
1857
|
* ### Example
|
|
235
1858
|
*
|
|
236
1859
|
* ```ts
|
|
237
|
-
*
|
|
238
|
-
*
|
|
1860
|
+
* await using run = createRun();
|
|
1861
|
+
*
|
|
1862
|
+
* const mutex = createMutex();
|
|
1863
|
+
*
|
|
1864
|
+
* const task =
|
|
1865
|
+
* (id: string): Task<void> =>
|
|
1866
|
+
* async (run) => {
|
|
1867
|
+
* const { console } = run.deps;
|
|
1868
|
+
* console.log("start", id);
|
|
1869
|
+
* await run(sleep("10ms"));
|
|
1870
|
+
* console.log("end", id);
|
|
1871
|
+
* return ok();
|
|
1872
|
+
* };
|
|
1873
|
+
*
|
|
1874
|
+
* await Promise.all([
|
|
1875
|
+
* run(mutex.withLock(task("1"))),
|
|
1876
|
+
* run(mutex.withLock(task("2"))),
|
|
1877
|
+
* ]);
|
|
239
1878
|
*
|
|
240
|
-
* //
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
1879
|
+
* // start 1
|
|
1880
|
+
* // end 1
|
|
1881
|
+
* // start 2
|
|
1882
|
+
* // end 2
|
|
244
1883
|
* ```
|
|
1884
|
+
*
|
|
1885
|
+
* @group Concurrency primitives
|
|
245
1886
|
*/
|
|
246
|
-
export
|
|
247
|
-
/**
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
1887
|
+
export interface Mutex extends Disposable {
|
|
1888
|
+
/**
|
|
1889
|
+
* Executes a {@link Task} while holding the mutex lock.
|
|
1890
|
+
*
|
|
1891
|
+
* Only one Task can hold the lock at a time. Other Tasks wait until the lock
|
|
1892
|
+
* is released.
|
|
1893
|
+
*/
|
|
1894
|
+
readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
1895
|
+
/** Returns the current mutex state for monitoring/debugging. */
|
|
1896
|
+
readonly snapshot: () => SemaphoreSnapshot;
|
|
251
1897
|
}
|
|
252
1898
|
/**
|
|
253
|
-
*
|
|
1899
|
+
* Creates a {@link Mutex}.
|
|
254
1900
|
*
|
|
255
|
-
*
|
|
1901
|
+
* @group Concurrency primitives
|
|
1902
|
+
*/
|
|
1903
|
+
export declare const createMutex: () => Mutex;
|
|
1904
|
+
/**
|
|
1905
|
+
* A keyed {@link Mutex} registry.
|
|
256
1906
|
*
|
|
257
|
-
*
|
|
258
|
-
* interface FetchError {
|
|
259
|
-
* readonly type: "FetchError";
|
|
260
|
-
* readonly error: unknown;
|
|
261
|
-
* }
|
|
1907
|
+
* Provides mutex operations per key.
|
|
262
1908
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* tryAsync(
|
|
267
|
-
* () => globalThis.fetch(url, { signal: context?.signal ?? null }),
|
|
268
|
-
* (error): FetchError => ({ type: "FetchError", error }),
|
|
269
|
-
* ),
|
|
270
|
-
* );
|
|
1909
|
+
* By default, {@link createMutexByKey} uses reference identity for keys,
|
|
1910
|
+
* matching native `Map`. Callers may instead provide a {@link Lookup lookup} so
|
|
1911
|
+
* logical equality is based on a derived stable key.
|
|
271
1912
|
*
|
|
272
|
-
*
|
|
273
|
-
|
|
1913
|
+
* @group Concurrency primitives
|
|
1914
|
+
*/
|
|
1915
|
+
export interface MutexByKey<K = unknown> extends Disposable {
|
|
1916
|
+
/**
|
|
1917
|
+
* Executes a {@link Task} while holding the mutex lock for a specific key.
|
|
1918
|
+
*
|
|
1919
|
+
* Behaves like {@link Mutex.withLock}, scoped to `key`.
|
|
1920
|
+
*/
|
|
1921
|
+
readonly withLock: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
|
|
1922
|
+
/** Returns the current mutex state for `key`, or `null` if absent. */
|
|
1923
|
+
readonly snapshot: (key: K) => SemaphoreSnapshot | null;
|
|
1924
|
+
}
|
|
1925
|
+
/** Options for {@link createMutexByKey}. */
|
|
1926
|
+
export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Creates a {@link MutexByKey}.
|
|
274
1930
|
*
|
|
275
|
-
*
|
|
1931
|
+
* @group Concurrency primitives
|
|
1932
|
+
*/
|
|
1933
|
+
export declare function createMutexByKey<K = unknown>(): MutexByKey<K>;
|
|
1934
|
+
export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<K, L>): MutexByKey<K>;
|
|
1935
|
+
/**
|
|
1936
|
+
* {@link Ref} protected by a {@link Mutex}.
|
|
276
1937
|
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
1938
|
+
* `MutexRef` serializes all operations through an internal {@link Mutex}. Reads,
|
|
1939
|
+
* writes, and updates observe one consistent value transition at a time. If the
|
|
1940
|
+
* update fails or is aborted, the previous value is preserved.
|
|
279
1941
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1942
|
+
* Typical use cases are small coordinators such as caches, session values,
|
|
1943
|
+
* in-memory registries, and counters whose transitions need to run {@link Task}s
|
|
1944
|
+
* atomically.
|
|
1945
|
+
*
|
|
1946
|
+
* @group Concurrency primitives
|
|
1947
|
+
*/
|
|
1948
|
+
export interface MutexRef<T> extends Disposable {
|
|
1949
|
+
/** Returns the current value. */
|
|
1950
|
+
readonly get: Task<T>;
|
|
1951
|
+
/** Sets the current value. */
|
|
1952
|
+
readonly set: (value: T) => Task<void>;
|
|
1953
|
+
/** Sets the current value and returns the previous value. */
|
|
1954
|
+
readonly getAndSet: (value: T) => Task<T>;
|
|
1955
|
+
/** Sets the current value and returns it. */
|
|
1956
|
+
readonly setAndGet: (value: T) => Task<T>;
|
|
1957
|
+
/** Updates the current value. */
|
|
1958
|
+
readonly update: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<void, E, D>;
|
|
1959
|
+
/** Updates the current value and returns the previous value. */
|
|
1960
|
+
readonly getAndUpdate: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<T, E, D>;
|
|
1961
|
+
/** Updates the current value and returns it. */
|
|
1962
|
+
readonly updateAndGet: <E = never, D = unknown>(updater: (current: T) => Task<T, E, D>) => Task<T, E, D>;
|
|
1963
|
+
/** Modifies the current value and returns a computed result. */
|
|
1964
|
+
readonly modify: <R, E = never, D = unknown>(modifier: (current: T) => Task<readonly [result: R, nextValue: T], E, D>) => Task<R, E, D>;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Creates a {@link MutexRef} with the given initial immutable value.
|
|
1968
|
+
*
|
|
1969
|
+
* @group Concurrency primitives
|
|
1970
|
+
*/
|
|
1971
|
+
export declare const createMutexRef: <T>(initialValue: T) => MutexRef<T>;
|
|
1972
|
+
/**
|
|
1973
|
+
* Cross-platform leader lock abstraction.
|
|
1974
|
+
*
|
|
1975
|
+
* `lock` returns a {@link Task} that waits until leadership is acquired and
|
|
1976
|
+
* yields a lease.
|
|
1977
|
+
*
|
|
1978
|
+
* Returns {@link AsyncDisposable} lease. Dispose it to release leadership.
|
|
1979
|
+
*
|
|
1980
|
+
* @group Concurrency primitives
|
|
1981
|
+
*/
|
|
1982
|
+
export interface LeaderLock {
|
|
1983
|
+
readonly lock: (name: Name) => Task<AsyncDisposable>;
|
|
1984
|
+
}
|
|
1985
|
+
/** @group Concurrency primitives */
|
|
1986
|
+
export interface LeaderLockDep {
|
|
1987
|
+
readonly leaderLock: LeaderLock;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Creates an in-process {@link LeaderLock}.
|
|
1991
|
+
*
|
|
1992
|
+
* Uses one {@link Mutex} per {@link Name}. Suitable for runtimes without a
|
|
1993
|
+
* cross-process lock manager (for example in-memory worker tests or React
|
|
1994
|
+
* Native).
|
|
1995
|
+
*
|
|
1996
|
+
* @group Concurrency primitives
|
|
290
1997
|
*/
|
|
291
|
-
export declare const
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1998
|
+
export declare const createInMemoryLeaderLock: () => LeaderLock;
|
|
1999
|
+
/**
|
|
2000
|
+
* Options for {@link all}, {@link allSettled}, {@link map}, and {@link mapSettled}.
|
|
2001
|
+
*
|
|
2002
|
+
* @group Composition
|
|
2003
|
+
*/
|
|
2004
|
+
export interface CollectOptions<Collect extends boolean = true> {
|
|
296
2005
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
2006
|
+
* Whether to collect results. When `false`, returns `Task<void, E, D>`.
|
|
2007
|
+
*
|
|
2008
|
+
* @default true
|
|
300
2009
|
*/
|
|
301
|
-
readonly
|
|
302
|
-
/** Maximum delay between retries. */
|
|
303
|
-
readonly maxDelay?: Duration;
|
|
304
|
-
/** Exponential backoff multiplier. */
|
|
305
|
-
readonly factor?: number;
|
|
306
|
-
/** Random jitter factor (0-1) to prevent thundering herd. */
|
|
307
|
-
readonly jitter?: number;
|
|
2010
|
+
readonly collect?: Collect;
|
|
308
2011
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
2012
|
+
* Custom reason for aborting remaining tasks on failure.
|
|
2013
|
+
*
|
|
2014
|
+
* By default, uses the helper's default abort error.
|
|
311
2015
|
*/
|
|
312
|
-
readonly
|
|
313
|
-
/** Callback invoked before each retry attempt. */
|
|
314
|
-
readonly onRetry?: (error: E, attempt: number, delay: number) => void;
|
|
315
|
-
}
|
|
316
|
-
/** Error returned when {@link retry} exhausts all retry attempts. */
|
|
317
|
-
export interface RetryError<E> {
|
|
318
|
-
readonly type: "RetryError";
|
|
319
|
-
readonly cause: E;
|
|
320
|
-
readonly attempts: number;
|
|
2016
|
+
readonly abortReason?: unknown;
|
|
321
2017
|
}
|
|
322
2018
|
/**
|
|
323
|
-
*
|
|
2019
|
+
* Fails fast on first error across multiple {@link Task}s.
|
|
2020
|
+
*
|
|
2021
|
+
* Sequential by default — use {@link concurrently} to run concurrently.
|
|
324
2022
|
*
|
|
325
2023
|
* ### Example
|
|
326
2024
|
*
|
|
327
2025
|
* ```ts
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
2026
|
+
* const result = await run(all([fetchUser, fetchPosts, fetchComments]));
|
|
2027
|
+
* if (!result.ok) return result;
|
|
2028
|
+
* const [user, posts, comments] = result.value;
|
|
2029
|
+
* ```
|
|
332
2030
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*
|
|
2031
|
+
* @group Composition
|
|
2032
|
+
* @see {@link CollectOptions}
|
|
2033
|
+
*/
|
|
2034
|
+
export declare function all<const T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, options?: CollectOptions): Task<{
|
|
2035
|
+
[K in keyof T]: InferTaskOk<T[K]>;
|
|
2036
|
+
}, InferTaskErr<T[number]>, InferTaskDeps<T[number]>>;
|
|
2037
|
+
/**
|
|
2038
|
+
* Returns object with same keys.
|
|
341
2039
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
2040
|
+
* ```ts
|
|
2041
|
+
* const result = await run(all({ user: fetchUser, posts: fetchPosts }));
|
|
2042
|
+
* if (!result.ok) return result;
|
|
2043
|
+
* const { user, posts } = result.value;
|
|
2044
|
+
* ```
|
|
2045
|
+
*/
|
|
2046
|
+
export declare function all<T extends Readonly<Record<string, AnyTask>>>(tasks: T, options?: CollectOptions): Task<{
|
|
2047
|
+
[P in keyof T]: InferTaskOk<T[P]>;
|
|
2048
|
+
}, [
|
|
2049
|
+
keyof T
|
|
2050
|
+
] extends [never] ? never : InferTaskErr<T[keyof T]>, [
|
|
2051
|
+
keyof T
|
|
2052
|
+
] extends [never] ? unknown : InferTaskDeps<T[keyof T]>>;
|
|
2053
|
+
/**
|
|
2054
|
+
* For dynamic or generated task lists.
|
|
344
2055
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
2056
|
+
* ```ts
|
|
2057
|
+
* const urls: ReadonlyArray<string> = getUrls();
|
|
2058
|
+
* const result = await run(map(urls, fetchUrl));
|
|
2059
|
+
* if (!result.ok) return result;
|
|
2060
|
+
* // result.value: ReadonlyArray<Response>
|
|
2061
|
+
* ```
|
|
2062
|
+
*/
|
|
2063
|
+
export declare function all<T, E, D>(tasks: Iterable<Task<T, E, D>>, options?: CollectOptions): Task<ReadonlyArray<T>, E, D>;
|
|
2064
|
+
/**
|
|
2065
|
+
* Guarantees non-empty result.
|
|
347
2066
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
2067
|
+
* ```ts
|
|
2068
|
+
* const tasks: NonEmptyReadonlyArray<Task<Response, FetchError>> = [
|
|
2069
|
+
* fetchUrl("/a"),
|
|
2070
|
+
* fetchUrl("/b"),
|
|
2071
|
+
* ];
|
|
2072
|
+
* const result = await run(all(tasks));
|
|
2073
|
+
* if (!result.ok) return result;
|
|
2074
|
+
* // result.value: NonEmptyReadonlyArray<Response>
|
|
2075
|
+
* ```
|
|
2076
|
+
*/
|
|
2077
|
+
export declare function all<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: CollectOptions): Task<NonEmptyReadonlyArray<T>, E, D>;
|
|
2078
|
+
/**
|
|
2079
|
+
* Run for side effects only.
|
|
350
2080
|
*
|
|
351
|
-
*
|
|
352
|
-
* const
|
|
353
|
-
*
|
|
354
|
-
* controller,
|
|
355
|
-
* );
|
|
356
|
-
* result2 satisfies Result<
|
|
357
|
-
* Response,
|
|
358
|
-
* FetchError | RetryError<FetchError> | AbortError
|
|
359
|
-
* >;
|
|
2081
|
+
* ```ts
|
|
2082
|
+
* const result = await run(all(tasks, { collect: false }));
|
|
2083
|
+
* // result.value: void
|
|
360
2084
|
* ```
|
|
361
2085
|
*/
|
|
362
|
-
export declare
|
|
2086
|
+
export declare function all<T, E, D>(tasks: Iterable<Task<T, E, D>> | Readonly<Record<string, Task<T, E, D>>>, options: CollectOptions<false>): Task<void, E, D>;
|
|
363
2087
|
/**
|
|
364
|
-
*
|
|
2088
|
+
* Abort reason used by {@link all} when aborting remaining tasks.
|
|
365
2089
|
*
|
|
366
|
-
*
|
|
367
|
-
* {@link Mutex} instead.
|
|
2090
|
+
* Used when a Task fails and other Tasks need to be aborted.
|
|
368
2091
|
*
|
|
369
|
-
* @
|
|
2092
|
+
* @group Composition
|
|
370
2093
|
*/
|
|
371
|
-
export
|
|
372
|
-
|
|
373
|
-
* Executes a Task while holding a semaphore permit.
|
|
374
|
-
*
|
|
375
|
-
* The Task will wait until a permit is available before executing. Supports
|
|
376
|
-
* cancellation via AbortSignal - if the signal is aborted while waiting for a
|
|
377
|
-
* permit or during execution, the Task is cancelled and permits are properly
|
|
378
|
-
* released.
|
|
379
|
-
*/
|
|
380
|
-
readonly withPermit: <T, E>(task: Task<T, E>) => Task<T, E | AbortError>;
|
|
2094
|
+
export declare const AllAbortError: import("./Type.js").TypedType<"AllAbortError", Record<never, never>>;
|
|
2095
|
+
export interface AllAbortError extends InferType<typeof AllAbortError> {
|
|
381
2096
|
}
|
|
382
2097
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
2098
|
+
* {@link AllAbortError} used as abort reason in {@link all}.
|
|
2099
|
+
*
|
|
2100
|
+
* @group Composition
|
|
2101
|
+
*/
|
|
2102
|
+
export declare const allAbortError: AllAbortError;
|
|
2103
|
+
/**
|
|
2104
|
+
* Completes all {@link Task}s regardless of individual failures.
|
|
385
2105
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
2106
|
+
* Like
|
|
2107
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled | Promise.allSettled},
|
|
2108
|
+
* all Tasks run to completion regardless of individual failures. Returns an
|
|
2109
|
+
* array of {@link Result}s preserving the original order.
|
|
389
2110
|
*
|
|
390
|
-
*
|
|
391
|
-
* {@link createMutex} instead.
|
|
2111
|
+
* Sequential by default. Use {@link concurrently} for concurrent execution.
|
|
392
2112
|
*
|
|
393
2113
|
* ### Example
|
|
394
2114
|
*
|
|
395
2115
|
* ```ts
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* const events: Array<string> = [];
|
|
2116
|
+
* const results = await run(
|
|
2117
|
+
* allSettled([fetchUser, fetchPosts, fetchComments]),
|
|
2118
|
+
* );
|
|
2119
|
+
* if (!results.ok) return results; // Only AbortError
|
|
401
2120
|
*
|
|
402
|
-
* const
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
2121
|
+
* for (const result of results.value) {
|
|
2122
|
+
* if (result.ok) {
|
|
2123
|
+
* console.log("Success:", result.value);
|
|
2124
|
+
* } else {
|
|
2125
|
+
* console.log("Failed:", result.error);
|
|
2126
|
+
* }
|
|
2127
|
+
* }
|
|
2128
|
+
* ```
|
|
406
2129
|
*
|
|
407
|
-
*
|
|
2130
|
+
* @group Composition
|
|
2131
|
+
* @see {@link CollectOptions}
|
|
2132
|
+
*/
|
|
2133
|
+
export declare function allSettled<const T extends readonly [AnyTask, ...ReadonlyArray<AnyTask>]>(tasks: T, options?: CollectOptions): Task<{
|
|
2134
|
+
[K in keyof T]: Result<InferTaskOk<T[K]>, InferTaskErr<T[K]> | AbortError>;
|
|
2135
|
+
}, never, InferTaskDeps<T[number]>>;
|
|
2136
|
+
/**
|
|
2137
|
+
* Returns object with same keys.
|
|
408
2138
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
2139
|
+
* ```ts
|
|
2140
|
+
* const results = await run(
|
|
2141
|
+
* allSettled({ user: fetchUser, posts: fetchPosts }),
|
|
2142
|
+
* );
|
|
2143
|
+
* if (!results.ok) return results;
|
|
2144
|
+
* const { user, posts } = results.value; // Each is Result<T, E>
|
|
2145
|
+
* ```
|
|
2146
|
+
*/
|
|
2147
|
+
export declare function allSettled<T extends Readonly<Record<string, AnyTask>>>(tasks: T, options?: CollectOptions): Task<{
|
|
2148
|
+
[P in keyof T]: Result<InferTaskOk<T[P]>, InferTaskErr<T[P]> | AbortError>;
|
|
2149
|
+
}, never, [
|
|
2150
|
+
keyof T
|
|
2151
|
+
] extends [never] ? unknown : InferTaskDeps<T[keyof T]>>;
|
|
2152
|
+
/**
|
|
2153
|
+
* For dynamic or generated task lists.
|
|
413
2154
|
*
|
|
414
|
-
*
|
|
415
|
-
* const
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
|
|
421
|
-
|
|
2155
|
+
* ```ts
|
|
2156
|
+
* const urls: ReadonlyArray<string> = getUrls();
|
|
2157
|
+
* const results = await run(allSettled(tasks));
|
|
2158
|
+
* if (!results.ok) return results;
|
|
2159
|
+
* // results.value: ReadonlyArray<Result<Response, FetchError | AbortError>>
|
|
2160
|
+
* ```
|
|
2161
|
+
*/
|
|
2162
|
+
export declare function allSettled<T, E, D>(tasks: Iterable<Task<T, E, D>>, options?: CollectOptions): Task<ReadonlyArray<Result<T, E | AbortError>>, never, D>;
|
|
2163
|
+
/**
|
|
2164
|
+
* Guarantees non-empty result.
|
|
422
2165
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* "end 2 (concurrent: 2)",
|
|
432
|
-
* "start 5 (concurrent: 3)",
|
|
433
|
-
* "end 3 (concurrent: 2)",
|
|
434
|
-
* "end 4 (concurrent: 1)",
|
|
435
|
-
* "end 5 (concurrent: 0)",
|
|
436
|
-
* ]
|
|
437
|
-
* `);
|
|
2166
|
+
* ```ts
|
|
2167
|
+
* const tasks: NonEmptyReadonlyArray<Task<Response, FetchError>> = [
|
|
2168
|
+
* fetchUrl("/a"),
|
|
2169
|
+
* fetchUrl("/b"),
|
|
2170
|
+
* ];
|
|
2171
|
+
* const results = await run(allSettled(tasks));
|
|
2172
|
+
* if (!results.ok) return results;
|
|
2173
|
+
* // results.value: NonEmptyReadonlyArray<Result<Response, FetchError | AbortError>>
|
|
438
2174
|
* ```
|
|
439
2175
|
*/
|
|
440
|
-
export declare
|
|
2176
|
+
export declare function allSettled<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: CollectOptions): Task<NonEmptyReadonlyArray<Result<T, E | AbortError>>, never, D>;
|
|
441
2177
|
/**
|
|
442
|
-
*
|
|
2178
|
+
* Run for side effects only.
|
|
443
2179
|
*
|
|
444
|
-
*
|
|
2180
|
+
* ```ts
|
|
2181
|
+
* const result = await run(allSettled(tasks, { collect: false }));
|
|
2182
|
+
* // result.value: void
|
|
2183
|
+
* ```
|
|
2184
|
+
*/
|
|
2185
|
+
export declare function allSettled<T, E, D>(tasks: Iterable<Task<T, E, D>> | Readonly<Record<string, Task<T, E, D>>>, options: CollectOptions<false>): Task<void, never, D>;
|
|
2186
|
+
/**
|
|
2187
|
+
* Abort reason used by {@link allSettled} when aborted externally.
|
|
445
2188
|
*
|
|
446
|
-
* @
|
|
2189
|
+
* @group Composition
|
|
447
2190
|
*/
|
|
448
|
-
export
|
|
449
|
-
|
|
450
|
-
* Executes a Task while holding the mutex lock.
|
|
451
|
-
*
|
|
452
|
-
* Only one Task can hold the lock at a time. Other Tasks will wait until the
|
|
453
|
-
* lock is released. Supports cancellation via AbortSignal.
|
|
454
|
-
*/
|
|
455
|
-
readonly withLock: <T, E>(task: Task<T, E>) => Task<T, E | AbortError>;
|
|
2191
|
+
export declare const AllSettledAbortError: import("./Type.js").TypedType<"AllSettledAbortError", Record<never, never>>;
|
|
2192
|
+
export interface AllSettledAbortError extends InferType<typeof AllSettledAbortError> {
|
|
456
2193
|
}
|
|
457
2194
|
/**
|
|
458
|
-
*
|
|
2195
|
+
* {@link AllSettledAbortError} used as abort reason in {@link allSettled}.
|
|
459
2196
|
*
|
|
460
|
-
*
|
|
461
|
-
|
|
2197
|
+
* @group Composition
|
|
2198
|
+
*/
|
|
2199
|
+
export declare const allSettledAbortError: AllSettledAbortError;
|
|
2200
|
+
/**
|
|
2201
|
+
* Maps values to {@link Task}s, failing fast on first error.
|
|
2202
|
+
*
|
|
2203
|
+
* Sequential by default — use {@link concurrently} for concurrent execution.
|
|
462
2204
|
*
|
|
463
2205
|
* ### Example
|
|
464
2206
|
*
|
|
465
2207
|
* ```ts
|
|
466
|
-
* const
|
|
2208
|
+
* const result = await run(map(userIds, fetchUser));
|
|
2209
|
+
* if (!result.ok) return result;
|
|
2210
|
+
* // result.value: ReadonlyArray<User>
|
|
2211
|
+
* ```
|
|
467
2212
|
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
*
|
|
474
|
-
* );
|
|
2213
|
+
* @group Composition
|
|
2214
|
+
* @see {@link CollectOptions}
|
|
2215
|
+
*/
|
|
2216
|
+
export declare function map<A, T, E, D>(items: Iterable<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<ReadonlyArray<T>, E, D>;
|
|
2217
|
+
/**
|
|
2218
|
+
* Returns object with same keys.
|
|
475
2219
|
*
|
|
476
|
-
*
|
|
477
|
-
* const
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* mutex.withLock(updateTask(3))(),
|
|
481
|
-
* ]);
|
|
2220
|
+
* ```ts
|
|
2221
|
+
* const result = await run(map({ a: 1, b: 2 }, (n) => double(n)));
|
|
2222
|
+
* if (!result.ok) return result;
|
|
2223
|
+
* // result.value: { a: number, b: number }
|
|
482
2224
|
* ```
|
|
483
2225
|
*/
|
|
484
|
-
export declare
|
|
2226
|
+
export declare function map<A, T, E, D, K extends string>(items: Readonly<Record<K, A>>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<Readonly<Record<K, T>>, E, D>;
|
|
485
2227
|
/**
|
|
486
|
-
*
|
|
487
|
-
* navigation) have completed.
|
|
2228
|
+
* Guarantees non-empty result.
|
|
488
2229
|
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
2230
|
+
* ```ts
|
|
2231
|
+
* const ids: NonEmptyReadonlyArray<UserId> = [id1, id2];
|
|
2232
|
+
* const result = await run(map(ids, fetchUser));
|
|
2233
|
+
* if (!result.ok) return result;
|
|
2234
|
+
* // result.value: NonEmptyReadonlyArray<User>
|
|
2235
|
+
* ```
|
|
2236
|
+
*/
|
|
2237
|
+
export declare function map<A, T, E, D>(items: NonEmptyReadonlyArray<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<NonEmptyReadonlyArray<T>, E, D>;
|
|
2238
|
+
/**
|
|
2239
|
+
* Run for side effects only.
|
|
2240
|
+
*
|
|
2241
|
+
* ```ts
|
|
2242
|
+
* const result = await run(map(userIds, sendEmail, { collect: false }));
|
|
2243
|
+
* // result.value: void
|
|
2244
|
+
* ```
|
|
2245
|
+
*/
|
|
2246
|
+
export declare function map<A, T, E, D>(items: Iterable<A> | Readonly<Record<string, A>>, task: (a: A) => Task<T, E, D>, options: CollectOptions<false>): Task<void, E, D>;
|
|
2247
|
+
/**
|
|
2248
|
+
* Abort reason used by {@link map} when aborting remaining tasks.
|
|
2249
|
+
*
|
|
2250
|
+
* @group Composition
|
|
2251
|
+
*/
|
|
2252
|
+
export declare const MapAbortError: import("./Type.js").TypedType<"MapAbortError", Record<never, never>>;
|
|
2253
|
+
export interface MapAbortError extends InferType<typeof MapAbortError> {
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* {@link MapAbortError} used as abort reason in {@link map}.
|
|
2257
|
+
*
|
|
2258
|
+
* @group Composition
|
|
2259
|
+
*/
|
|
2260
|
+
export declare const mapAbortError: MapAbortError;
|
|
2261
|
+
/**
|
|
2262
|
+
* Maps values to {@link Task}s, completing all regardless of failures.
|
|
2263
|
+
*
|
|
2264
|
+
* Returns an array of {@link Result}s preserving the original order. Sequential
|
|
2265
|
+
* by default — use {@link concurrently} for concurrent execution.
|
|
491
2266
|
*
|
|
492
2267
|
* ### Example
|
|
493
2268
|
*
|
|
494
2269
|
* ```ts
|
|
495
|
-
* const
|
|
496
|
-
*
|
|
497
|
-
* return ok();
|
|
498
|
-
* });
|
|
2270
|
+
* const results = await run(mapSettled(userIds, fetchUser));
|
|
2271
|
+
* if (!results.ok) return results; // Only AbortError
|
|
499
2272
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
2273
|
+
* for (const result of results.value) {
|
|
2274
|
+
* if (result.ok) {
|
|
2275
|
+
* console.log("Success:", result.value);
|
|
2276
|
+
* } else {
|
|
2277
|
+
* console.log("Failed:", result.error);
|
|
2278
|
+
* }
|
|
2279
|
+
* }
|
|
502
2280
|
* ```
|
|
2281
|
+
*
|
|
2282
|
+
* @group Composition
|
|
2283
|
+
* @see {@link CollectOptions}
|
|
503
2284
|
*/
|
|
504
|
-
export declare
|
|
2285
|
+
export declare function mapSettled<A, T, E, D>(items: Iterable<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<ReadonlyArray<Result<T, E | AbortError>>, never, D>;
|
|
505
2286
|
/**
|
|
506
|
-
*
|
|
2287
|
+
* Returns object with same keys.
|
|
507
2288
|
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
2289
|
+
* ```ts
|
|
2290
|
+
* const results = await run(mapSettled({ a: 1, b: 2 }, (n) => double(n)));
|
|
2291
|
+
* if (!results.ok) return results;
|
|
2292
|
+
* // results.value: { a: Result<number, E>, b: Result<number, E> }
|
|
2293
|
+
* ```
|
|
2294
|
+
*/
|
|
2295
|
+
export declare function mapSettled<A, T, E, D, K extends string>(items: Readonly<Record<K, A>>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<Readonly<Record<K, Result<T, E | AbortError>>>, never, D>;
|
|
2296
|
+
/**
|
|
2297
|
+
* Guarantees non-empty result.
|
|
511
2298
|
*
|
|
512
|
-
*
|
|
2299
|
+
* ```ts
|
|
2300
|
+
* const ids: NonEmptyReadonlyArray<UserId> = [id1, id2];
|
|
2301
|
+
* const results = await run(mapSettled(ids, fetchUser));
|
|
2302
|
+
* if (!results.ok) return results;
|
|
2303
|
+
* // results.value: NonEmptyReadonlyArray<Result<User, FetchError | AbortError>>
|
|
2304
|
+
* ```
|
|
2305
|
+
*/
|
|
2306
|
+
export declare function mapSettled<A, T, E, D>(items: NonEmptyReadonlyArray<A>, task: (a: A) => Task<T, E, D>, options?: CollectOptions): Task<NonEmptyReadonlyArray<Result<T, E | AbortError>>, never, D>;
|
|
2307
|
+
/**
|
|
2308
|
+
* Run for side effects only.
|
|
513
2309
|
*
|
|
514
|
-
*
|
|
2310
|
+
* ```ts
|
|
2311
|
+
* const result = await run(
|
|
2312
|
+
* mapSettled(userIds, sendEmail, { collect: false }),
|
|
2313
|
+
* );
|
|
2314
|
+
* // result.value: void
|
|
2315
|
+
* ```
|
|
2316
|
+
*/
|
|
2317
|
+
export declare function mapSettled<A, T, E, D>(items: Iterable<A> | Readonly<Record<string, A>>, task: (a: A) => Task<T, E, D>, options: CollectOptions<false>): Task<void, never, D>;
|
|
2318
|
+
/**
|
|
2319
|
+
* Returns the first {@link Task} that succeeds.
|
|
515
2320
|
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* 3. **Return `T | PromiseLike<T>`** - Calling code can check the value and only
|
|
521
|
-
* `await` when needed, avoiding microtask overhead for sync cases
|
|
2321
|
+
* Like
|
|
2322
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
|
|
2323
|
+
* the first Task to succeed wins. All other Tasks are aborted. If all Tasks
|
|
2324
|
+
* fail, returns the last error (by input order).
|
|
522
2325
|
*
|
|
523
|
-
*
|
|
2326
|
+
* Sequential by default. Use {@link concurrently} for concurrent execution.
|
|
524
2327
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
2328
|
+
* Think of it like `Array.prototype.some()` — it stops on the first success.
|
|
2329
|
+
* This is in contrast to {@link race}, which returns the first task to complete
|
|
2330
|
+
* (whether success or failure).
|
|
528
2331
|
*
|
|
529
2332
|
* ### Example
|
|
530
2333
|
*
|
|
531
2334
|
* ```ts
|
|
532
|
-
* //
|
|
533
|
-
* const
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* // Caller can optimize based on actual behavior
|
|
540
|
-
* const result = getData(id);
|
|
541
|
-
* const data = isAsync(result) ? await result : result;
|
|
2335
|
+
* // Try multiple endpoints concurrently, first success wins
|
|
2336
|
+
* const result = await run(
|
|
2337
|
+
* concurrently(
|
|
2338
|
+
* any([fetchFromPrimary, fetchFromSecondary, fetchFromTertiary]),
|
|
2339
|
+
* ),
|
|
2340
|
+
* );
|
|
542
2341
|
* ```
|
|
543
2342
|
*
|
|
544
|
-
*
|
|
2343
|
+
* @group Composition
|
|
2344
|
+
*/
|
|
2345
|
+
export declare function any<T, E, D>(tasks: NonEmptyReadonlyArray<Task<T, E, D>>, options?: {
|
|
2346
|
+
/** How to choose an error if all tasks fail. */
|
|
2347
|
+
allFailed?: AnyAllFailed;
|
|
2348
|
+
}): Task<T, E, D>;
|
|
2349
|
+
/**
|
|
2350
|
+
* Tie-breaker for {@link any} when all tasks fail.
|
|
2351
|
+
*
|
|
2352
|
+
* Used only when no task succeeds.
|
|
2353
|
+
*
|
|
2354
|
+
* - `"input"` returns the error from the last task in the input array. This is
|
|
2355
|
+
* stable under concurrency and generally produces deterministic tests.
|
|
2356
|
+
* - `"completion"` returns the error from the Task that finished last. This
|
|
2357
|
+
* reflects timing but can vary across runs when task timing varies.
|
|
2358
|
+
*
|
|
2359
|
+
* ### Example
|
|
2360
|
+
*
|
|
2361
|
+
* ```ts
|
|
2362
|
+
* await using run = createRun();
|
|
2363
|
+
* const result = await run(
|
|
2364
|
+
* concurrently(any([a, b, c], { allFailed: "completion" })),
|
|
2365
|
+
* );
|
|
2366
|
+
* ```
|
|
2367
|
+
*/
|
|
2368
|
+
export type AnyAllFailed = "input" | "completion";
|
|
2369
|
+
/**
|
|
2370
|
+
* Abort reason used by {@link any} when aborting remaining tasks.
|
|
545
2371
|
*
|
|
546
|
-
*
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
2372
|
+
* @group Composition
|
|
2373
|
+
*/
|
|
2374
|
+
export declare const AnyAbortError: import("./Type.js").TypedType<"AnyAbortError", Record<never, never>>;
|
|
2375
|
+
export interface AnyAbortError extends InferType<typeof AnyAbortError> {
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* {@link AnyAbortError} used as abort reason in {@link any}.
|
|
552
2379
|
*
|
|
553
|
-
*
|
|
2380
|
+
* @group Composition
|
|
2381
|
+
*/
|
|
2382
|
+
export declare const anyAbortError: AnyAbortError;
|
|
2383
|
+
/**
|
|
2384
|
+
* Error returned when a {@link fetch} {@link Task} fails.
|
|
554
2385
|
*
|
|
555
|
-
*
|
|
556
|
-
* when possible (e.g., mutex with available permit, retry on first success).
|
|
2386
|
+
* @group Composition
|
|
557
2387
|
*/
|
|
558
|
-
export
|
|
2388
|
+
export declare const FetchError: import("./Type.js").TypedType<"FetchError", {
|
|
2389
|
+
error: import("./Type.js").Type<"Unknown", unknown, unknown, never, unknown, never>;
|
|
2390
|
+
}>;
|
|
2391
|
+
export interface FetchError extends InferType<typeof FetchError> {
|
|
2392
|
+
}
|
|
559
2393
|
/**
|
|
560
|
-
*
|
|
2394
|
+
* Creates a {@link Task} that wraps the native
|
|
2395
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}.
|
|
561
2396
|
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
2397
|
+
* Handles cross-browser abort behavior — WebKit throws a `DOMException` with
|
|
2398
|
+
* message "Fetch is aborted" instead of propagating `signal.reason`. This
|
|
2399
|
+
* helper normalizes the behavior to always return {@link AbortError}.
|
|
564
2400
|
*
|
|
565
2401
|
* ### Example
|
|
566
2402
|
*
|
|
567
2403
|
* ```ts
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
* return fetchData(id); // Async path
|
|
572
|
-
* };
|
|
2404
|
+
* await using run = createRun();
|
|
2405
|
+
*
|
|
2406
|
+
* const result = await run(fetch("https://api.example.com/users"));
|
|
573
2407
|
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
2408
|
+
* if (!result.ok) {
|
|
2409
|
+
* // Handle FetchError or AbortError
|
|
2410
|
+
* }
|
|
2411
|
+
*
|
|
2412
|
+
* // Compose with timeout and retry
|
|
2413
|
+
* const fetchWithRetry = (url: string) =>
|
|
2414
|
+
* retry(timeout(fetch(url), "10s"), retryStrategyAws);
|
|
577
2415
|
* ```
|
|
2416
|
+
*
|
|
2417
|
+
* @group Composition
|
|
578
2418
|
*/
|
|
579
|
-
export declare const
|
|
2419
|
+
export declare const fetch: (input: RequestInfo | URL, init?: RequestInit) => Task<Response, FetchError>;
|
|
580
2420
|
//# sourceMappingURL=Task.d.ts.map
|