@evolu/common 8.1.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Function.d.ts +1 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Http.d.ts +9 -0
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/Number.d.ts +66 -6
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +11 -3
- package/dist/src/Object.d.ts +1 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts +1 -0
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +2 -0
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Resource.d.ts +1 -0
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Result.d.ts +383 -548
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +296 -46
- package/dist/src/Schedule.d.ts +2 -2
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/StackTrace.d.ts +1 -0
- package/dist/src/StackTrace.d.ts.map +1 -1
- package/dist/src/Task.d.ts +978 -1061
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1010 -159
- package/dist/src/Time.d.ts +71 -34
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +77 -1
- package/dist/src/Type.d.ts +1740 -795
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1634 -211
- package/dist/src/Types.d.ts +0 -24
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/intl/_en.d.ts +126 -0
- package/dist/src/intl/_en.d.ts.map +1 -0
- package/dist/src/intl/_en.js +267 -0
- package/dist/src/intl/ar.d.ts +117 -0
- package/dist/src/intl/ar.d.ts.map +1 -0
- package/dist/src/intl/ar.js +245 -0
- package/dist/src/intl/bn.d.ts +117 -0
- package/dist/src/intl/bn.d.ts.map +1 -0
- package/dist/src/intl/bn.js +258 -0
- package/dist/src/intl/ca.d.ts +117 -0
- package/dist/src/intl/ca.d.ts.map +1 -0
- package/dist/src/intl/ca.js +258 -0
- package/dist/src/intl/cs.d.ts +30 -7
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +65 -28
- package/dist/src/intl/da.d.ts +117 -0
- package/dist/src/intl/da.d.ts.map +1 -0
- package/dist/src/intl/da.js +258 -0
- package/dist/src/intl/de.d.ts +117 -0
- package/dist/src/intl/de.d.ts.map +1 -0
- package/dist/src/intl/de.js +258 -0
- package/dist/src/intl/el.d.ts +117 -0
- package/dist/src/intl/el.d.ts.map +1 -0
- package/dist/src/intl/el.js +265 -0
- package/dist/src/intl/es.d.ts +117 -0
- package/dist/src/intl/es.d.ts.map +1 -0
- package/dist/src/intl/es.js +258 -0
- package/dist/src/intl/fa.d.ts +117 -0
- package/dist/src/intl/fa.d.ts.map +1 -0
- package/dist/src/intl/fa.js +258 -0
- package/dist/src/intl/fi.d.ts +117 -0
- package/dist/src/intl/fi.d.ts.map +1 -0
- package/dist/src/intl/fi.js +258 -0
- package/dist/src/intl/fil.d.ts +117 -0
- package/dist/src/intl/fil.d.ts.map +1 -0
- package/dist/src/intl/fil.js +258 -0
- package/dist/src/intl/fr.d.ts +117 -0
- package/dist/src/intl/fr.d.ts.map +1 -0
- package/dist/src/intl/fr.js +258 -0
- package/dist/src/intl/he.d.ts +117 -0
- package/dist/src/intl/he.d.ts.map +1 -0
- package/dist/src/intl/he.js +258 -0
- package/dist/src/intl/hi.d.ts +117 -0
- package/dist/src/intl/hi.d.ts.map +1 -0
- package/dist/src/intl/hi.js +258 -0
- package/dist/src/intl/hr.d.ts +117 -0
- package/dist/src/intl/hr.d.ts.map +1 -0
- package/dist/src/intl/hr.js +265 -0
- package/dist/src/intl/hu.d.ts +63 -0
- package/dist/src/intl/hu.d.ts.map +1 -0
- package/dist/src/intl/hu.js +204 -0
- package/dist/src/intl/id.d.ts +117 -0
- package/dist/src/intl/id.d.ts.map +1 -0
- package/dist/src/intl/id.js +258 -0
- package/dist/src/intl/index.d.ts +42 -0
- package/dist/src/intl/index.d.ts.map +1 -1
- package/dist/src/intl/index.js +42 -0
- package/dist/src/intl/it.d.ts +117 -0
- package/dist/src/intl/it.d.ts.map +1 -0
- package/dist/src/intl/it.js +258 -0
- package/dist/src/intl/ja.d.ts +117 -0
- package/dist/src/intl/ja.d.ts.map +1 -0
- package/dist/src/intl/ja.js +258 -0
- package/dist/src/intl/ko.d.ts +117 -0
- package/dist/src/intl/ko.d.ts.map +1 -0
- package/dist/src/intl/ko.js +258 -0
- package/dist/src/intl/ml.d.ts +117 -0
- package/dist/src/intl/ml.d.ts.map +1 -0
- package/dist/src/intl/ml.js +258 -0
- package/dist/src/intl/mr.d.ts +117 -0
- package/dist/src/intl/mr.d.ts.map +1 -0
- package/dist/src/intl/mr.js +258 -0
- package/dist/src/intl/ms.d.ts +117 -0
- package/dist/src/intl/ms.d.ts.map +1 -0
- package/dist/src/intl/ms.js +245 -0
- package/dist/src/intl/nb.d.ts +63 -0
- package/dist/src/intl/nb.d.ts.map +1 -0
- package/dist/src/intl/nb.js +191 -0
- package/dist/src/intl/nl.d.ts +117 -0
- package/dist/src/intl/nl.d.ts.map +1 -0
- package/dist/src/intl/nl.js +258 -0
- package/dist/src/intl/pa.d.ts +117 -0
- package/dist/src/intl/pa.d.ts.map +1 -0
- package/dist/src/intl/pa.js +258 -0
- package/dist/src/intl/pl.d.ts +117 -0
- package/dist/src/intl/pl.d.ts.map +1 -0
- package/dist/src/intl/pl.js +242 -0
- package/dist/src/intl/pt-BR.d.ts +117 -0
- package/dist/src/intl/pt-BR.d.ts.map +1 -0
- package/dist/src/intl/pt-BR.js +258 -0
- package/dist/src/intl/pt.d.ts +117 -0
- package/dist/src/intl/pt.d.ts.map +1 -0
- package/dist/src/intl/pt.js +246 -0
- package/dist/src/intl/ro.d.ts +117 -0
- package/dist/src/intl/ro.d.ts.map +1 -0
- package/dist/src/intl/ro.js +258 -0
- package/dist/src/intl/sk.d.ts +117 -0
- package/dist/src/intl/sk.d.ts.map +1 -0
- package/dist/src/intl/sk.js +244 -0
- package/dist/src/intl/sl.d.ts +117 -0
- package/dist/src/intl/sl.d.ts.map +1 -0
- package/dist/src/intl/sl.js +258 -0
- package/dist/src/intl/sv.d.ts +117 -0
- package/dist/src/intl/sv.d.ts.map +1 -0
- package/dist/src/intl/sv.js +258 -0
- package/dist/src/intl/sw.d.ts +63 -0
- package/dist/src/intl/sw.d.ts.map +1 -0
- package/dist/src/intl/sw.js +191 -0
- package/dist/src/intl/ta.d.ts +117 -0
- package/dist/src/intl/ta.d.ts.map +1 -0
- package/dist/src/intl/ta.js +258 -0
- package/dist/src/intl/te.d.ts +117 -0
- package/dist/src/intl/te.d.ts.map +1 -0
- package/dist/src/intl/te.js +258 -0
- package/dist/src/intl/th.d.ts +117 -0
- package/dist/src/intl/th.d.ts.map +1 -0
- package/dist/src/intl/th.js +258 -0
- package/dist/src/intl/tr.d.ts +117 -0
- package/dist/src/intl/tr.d.ts.map +1 -0
- package/dist/src/intl/tr.js +255 -0
- package/dist/src/intl/uk.d.ts +117 -0
- package/dist/src/intl/uk.d.ts.map +1 -0
- package/dist/src/intl/uk.js +258 -0
- package/dist/src/intl/ur.d.ts +117 -0
- package/dist/src/intl/ur.d.ts.map +1 -0
- package/dist/src/intl/ur.js +245 -0
- package/dist/src/intl/vi.d.ts +117 -0
- package/dist/src/intl/vi.d.ts.map +1 -0
- package/dist/src/intl/vi.js +255 -0
- package/dist/src/intl/zh-CN.d.ts +117 -0
- package/dist/src/intl/zh-CN.d.ts.map +1 -0
- package/dist/src/intl/zh-CN.js +258 -0
- package/dist/src/intl/zh-TW.d.ts +117 -0
- package/dist/src/intl/zh-TW.d.ts.map +1 -0
- package/dist/src/intl/zh-TW.js +258 -0
- package/dist/src/local-first/Owner.d.ts +156 -62
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +119 -20
- package/package.json +1 -1
- package/src/Function.ts +2 -0
- package/src/Http.ts +9 -0
- package/src/Number.ts +85 -14
- package/src/Object.ts +2 -0
- package/src/RefCount.ts +2 -0
- package/src/Relation.ts +4 -0
- package/src/Resource.ts +2 -0
- package/src/Result.ts +397 -560
- package/src/Schedule.ts +1 -1
- package/src/StackTrace.ts +2 -0
- package/src/Task.ts +1145 -1225
- package/src/Time.ts +110 -62
- package/src/Type.ts +2842 -835
- package/src/Types.ts +0 -49
- package/src/intl/_en.ts +497 -0
- package/src/intl/ar.ts +417 -0
- package/src/intl/bn.ts +486 -0
- package/src/intl/ca.ts +488 -0
- package/src/intl/cs.ts +131 -32
- package/src/intl/da.ts +488 -0
- package/src/intl/de.ts +488 -0
- package/src/intl/el.ts +494 -0
- package/src/intl/es.ts +488 -0
- package/src/intl/fa.ts +487 -0
- package/src/intl/fi.ts +487 -0
- package/src/intl/fil.ts +488 -0
- package/src/intl/fr.ts +488 -0
- package/src/intl/he.ts +486 -0
- package/src/intl/hi.ts +487 -0
- package/src/intl/hr.ts +494 -0
- package/src/intl/hu.ts +387 -0
- package/src/intl/id.ts +487 -0
- package/src/intl/index.ts +42 -0
- package/src/intl/it.ts +488 -0
- package/src/intl/ja.ts +488 -0
- package/src/intl/ko.ts +487 -0
- package/src/intl/ml.ts +487 -0
- package/src/intl/mr.ts +487 -0
- package/src/intl/ms.ts +410 -0
- package/src/intl/nb.ts +364 -0
- package/src/intl/nl.ts +488 -0
- package/src/intl/pa.ts +487 -0
- package/src/intl/pl.ts +450 -0
- package/src/intl/pt-BR.ts +487 -0
- package/src/intl/pt.ts +424 -0
- package/src/intl/ro.ts +488 -0
- package/src/intl/sk.ts +412 -0
- package/src/intl/sl.ts +487 -0
- package/src/intl/sv.ts +488 -0
- package/src/intl/sw.ts +324 -0
- package/src/intl/ta.ts +487 -0
- package/src/intl/te.ts +487 -0
- package/src/intl/th.ts +484 -0
- package/src/intl/tr.ts +487 -0
- package/src/intl/uk.ts +488 -0
- package/src/intl/ur.ts +410 -0
- package/src/intl/vi.ts +435 -0
- package/src/intl/zh-CN.ts +482 -0
- package/src/intl/zh-TW.ts +482 -0
- package/src/local-first/Owner.ts +156 -62
package/dist/src/Task.d.ts
CHANGED
|
@@ -1,192 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ## Intro
|
|
3
3
|
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
import { type NonEmptyReadonlyArray } from "./Array.ts";
|
|
7
|
-
import type { Brand } from "./Brand.ts";
|
|
8
|
-
import { type ConsoleDep, type TestConsoleDep } from "./Console.ts";
|
|
9
|
-
import { type RandomBytesDep } from "./Crypto.ts";
|
|
10
|
-
import type { NativeFetchDep } from "./Http.ts";
|
|
11
|
-
import { type LeakDetectorDep, type TestLeakDetectorDep } from "./LeakDetector.ts";
|
|
12
|
-
import { type LookupOption } from "./Lookup.ts";
|
|
13
|
-
import { type Option } from "./Option.ts";
|
|
14
|
-
import { type RandomDep, type RandomLibDep } from "./Random.ts";
|
|
15
|
-
import { type Ref } from "./Ref.ts";
|
|
16
|
-
import { type Done, type Result } from "./Result.ts";
|
|
17
|
-
import type { Schedule, ScheduleStep } from "./Schedule.ts";
|
|
18
|
-
import { type Millis, type PositiveDuration, type TimeDep, type TestTimeDep } from "./Time.ts";
|
|
19
|
-
import { type InferType, NonNegativeInt, PositiveInt, String, Unknown, UnknownResult, type Id, type Int1To100OrPositiveInt, type ObjectType, type RecordType, type Typed, type TypedType } from "./Type.ts";
|
|
20
|
-
import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
21
|
-
/**
|
|
22
4
|
* JavaScript-native structured concurrency.
|
|
23
5
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* it completes, and abort follows the tree: aborting a parent requests abort of
|
|
30
|
-
* all its descendants, while races and fail-fast operations also abort
|
|
31
|
-
* remaining sibling branches. This prevents detached work and gives
|
|
32
|
-
* cancellation, failure, and cleanup explicit ownership. The tree also makes
|
|
33
|
-
* running work and its ownership observable.
|
|
6
|
+
* Structured concurrency makes ownership of asynchronous work explicit.
|
|
7
|
+
* Operations form a tree where every child belongs to a parent. A parent waits
|
|
8
|
+
* for its children before it completes, and abort follows the tree: aborting a
|
|
9
|
+
* parent requests abort of all its descendants. Races and fail-fast operations
|
|
10
|
+
* also abort their remaining sibling branches.
|
|
34
11
|
*
|
|
35
12
|
* With plain {@link AbortController} code, these guarantees depend on call-site
|
|
36
13
|
* discipline: someone must remember the `finally` that aborts started work and
|
|
37
|
-
* the await that waits for
|
|
38
|
-
* registers every child before it starts, and the parent settles
|
|
39
|
-
* child cleanup finishes
|
|
40
|
-
*
|
|
41
|
-
* Evolu
|
|
42
|
-
*
|
|
43
|
-
* {@link
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* aborted, while `run.abortable(task)` catches abort and returns it as an
|
|
60
|
-
* {@link Err}. If the Task itself throws or rejects, that is a defect. A defect
|
|
61
|
-
* panics the Run tree, and a Fiber rejects with AbortError whose reason is
|
|
62
|
-
* {@link PanicAbortReason}; an AbortableFiber returns that AbortError as an
|
|
63
|
-
* Err.
|
|
64
|
-
*
|
|
65
|
-
* The core is intentionally small: ordinary Task functions, a callable Run with
|
|
66
|
-
* closed-over state, Promise-backed Fibers, AbortSignal propagation, and
|
|
67
|
-
* JavaScript resource management. That minimal model still covers abort,
|
|
68
|
-
* cleanup, panic, dependency injection, monitoring, concurrency, and resource
|
|
69
|
-
* bracketing. The code is covered by carefully written, readable tests, so they
|
|
70
|
-
* serve as documentation too.
|
|
71
|
-
*
|
|
72
|
-
* ## Example
|
|
73
|
-
*
|
|
74
|
-
* This intentionally naive wrapper is useful for learning Task dependencies,
|
|
75
|
-
* Result errors, and native AbortSignal interop. Do not copy it as a production
|
|
76
|
-
* fetch helper: a Response is not a plain value — its unread body is tied to
|
|
77
|
-
* the request signal, which aborts when the Task settles. The returned Response
|
|
78
|
-
* escapes the scope that keeps it alive. Evolu's {@link fetch} exists to close
|
|
79
|
-
* this gap.
|
|
80
|
-
*
|
|
81
|
-
* `nativeFetch` is already a {@link RunDefaultDeps | default dependency}; this
|
|
82
|
-
* example declares the same shape locally to demonstrate how Tasks declare
|
|
83
|
-
* capabilities, and passes it to {@link createRun} to show that default
|
|
84
|
-
* dependencies can be replaced like custom ones.
|
|
14
|
+
* the await that waits for cleanup. {@link Run} makes both structural:
|
|
15
|
+
* `run(task)` registers every child before it starts, and the parent settles
|
|
16
|
+
* only after child cleanup finishes.
|
|
17
|
+
*
|
|
18
|
+
* Evolu models structured concurrency with ordinary JavaScript:
|
|
19
|
+
*
|
|
20
|
+
* - A {@link Task} describes an asynchronous operation and its dependencies.
|
|
21
|
+
* - A {@link Run} starts Tasks and owns their lifetimes.
|
|
22
|
+
* - A {@link Fiber} is the Promise-backed handle returned when a Run starts a
|
|
23
|
+
* Task.
|
|
24
|
+
* - An {@link AbortableFiber} adds explicit abort and async disposal.
|
|
25
|
+
*
|
|
26
|
+
* The runtime core is deliberately small: ordinary functions, a callable Run
|
|
27
|
+
* with closed-over state, Promise-backed Fibers, {@link AbortSignal}
|
|
28
|
+
* propagation, and JavaScript resource management. Together, these primitives
|
|
29
|
+
* provide abort, cleanup, defect handling, dependency injection, monitoring,
|
|
30
|
+
* concurrency, and resource bracketing.
|
|
31
|
+
*
|
|
32
|
+
* Tasks return domain success or failure as {@link Result}. Abort is control
|
|
33
|
+
* flow represented by {@link AbortError}. If a Task throws or rejects with
|
|
34
|
+
* anything else, that is a defect: the root Run reports it and shuts down its
|
|
35
|
+
* tree so code does not continue in a potentially invalid state.
|
|
85
36
|
*
|
|
86
37
|
* ```ts
|
|
87
38
|
* import {
|
|
88
|
-
* AbortError,
|
|
89
39
|
* createRun,
|
|
90
40
|
* err,
|
|
91
41
|
* ok,
|
|
92
|
-
* type AbortableFiber,
|
|
93
42
|
* type Result,
|
|
94
43
|
* type Task,
|
|
44
|
+
* type Typed,
|
|
95
45
|
* } from "@evolu/common";
|
|
96
46
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* readonly
|
|
47
|
+
* interface User {
|
|
48
|
+
* readonly id: string;
|
|
49
|
+
* readonly name: string;
|
|
100
50
|
* }
|
|
101
51
|
*
|
|
102
|
-
* interface
|
|
103
|
-
* readonly
|
|
104
|
-
* readonly error: unknown;
|
|
52
|
+
* interface Db {
|
|
53
|
+
* readonly usersById: ReadonlyMap<string, User>;
|
|
105
54
|
* }
|
|
106
55
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* async ({ deps, signal }) => {
|
|
111
|
-
* try {
|
|
112
|
-
* const response = await deps.nativeFetch(url, { signal });
|
|
113
|
-
* return ok(response);
|
|
114
|
-
* } catch (error) {
|
|
115
|
-
* if (AbortError.is(error)) throw error;
|
|
116
|
-
* return err({ type: "NaiveFetchError", error });
|
|
117
|
-
* }
|
|
118
|
-
* };
|
|
119
|
-
*
|
|
120
|
-
* const nativeFetch: typeof globalThis.fetch = async (input, init) => {
|
|
121
|
-
* if (String(input).endsWith("/123")) {
|
|
122
|
-
* return new Response(null, { status: 204 });
|
|
123
|
-
* }
|
|
124
|
-
*
|
|
125
|
-
* return new Promise<Response>((_resolve, reject) => {
|
|
126
|
-
* const { signal } = init ?? {};
|
|
127
|
-
* if (signal?.aborted) {
|
|
128
|
-
* reject(signal.reason);
|
|
129
|
-
* return;
|
|
130
|
-
* }
|
|
131
|
-
* signal?.addEventListener("abort", () => reject(signal.reason), {
|
|
132
|
-
* once: true,
|
|
133
|
-
* });
|
|
134
|
-
* });
|
|
135
|
-
* };
|
|
136
|
-
*
|
|
137
|
-
* // Provide dependencies at the composition root.
|
|
138
|
-
* const deps: NativeFetchDep = {
|
|
139
|
-
* nativeFetch,
|
|
140
|
-
* };
|
|
141
|
-
*
|
|
142
|
-
* // Create a Run with those dependencies.
|
|
143
|
-
* await using run = createRun(deps);
|
|
56
|
+
* interface DbDep {
|
|
57
|
+
* readonly db: Db;
|
|
58
|
+
* }
|
|
144
59
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* // So what is naive about it? The Response ok value.
|
|
152
|
-
* // Wrong: the Task settled, so its Run disposed and aborted `signal`.
|
|
153
|
-
* // The Response body is a live resource tied to that signal. Whether
|
|
154
|
-
* // this read fails immediately or appears to work depends on the
|
|
155
|
-
* // runtime and on how much of the body was already buffered — it is
|
|
156
|
-
* // timing-dependent behavior, not an API you can rely on.
|
|
157
|
-
* // await result.value.json();
|
|
158
|
-
*
|
|
159
|
-
* // Abort works when native fetch rejects with signal.reason. Some hosts use
|
|
160
|
-
* // their own abort error, which this naive wrapper does not normalize.
|
|
161
|
-
* const fiber = run.abortable(naiveFetch("/users/456"));
|
|
162
|
-
* fiber.abort();
|
|
163
|
-
* const abortResult = await fiber;
|
|
164
|
-
* expectTypeOf(fiber).toEqualTypeOf<
|
|
165
|
-
* AbortableFiber<Response, NaiveFetchError, NativeFetchDep>
|
|
166
|
-
* >();
|
|
167
|
-
* assert(!abortResult.ok);
|
|
168
|
-
* expect(AbortError.is(abortResult.error)).toBe(true);
|
|
169
|
-
* ```
|
|
60
|
+
* const getUser =
|
|
61
|
+
* (id: string): Task<User, UserNotFoundError, DbDep> =>
|
|
62
|
+
* (run) => {
|
|
63
|
+
* const user = run.deps.db.usersById.get(id);
|
|
64
|
+
* return user ? ok(user) : err({ type: "UserNotFound", id });
|
|
65
|
+
* };
|
|
170
66
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
67
|
+
* // Typed declares the `type` discriminant without repeating the property.
|
|
68
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
69
|
+
* readonly id: string;
|
|
70
|
+
* }
|
|
174
71
|
*
|
|
175
|
-
*
|
|
176
|
-
* import {
|
|
177
|
-
* createRun,
|
|
178
|
-
* fetch,
|
|
179
|
-
* type FetchError,
|
|
180
|
-
* type Result,
|
|
181
|
-
* } from "@evolu/common";
|
|
72
|
+
* const user: User = { id: "user-1", name: "Ada" };
|
|
182
73
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* await using run = createRun({
|
|
74
|
+
* // Provide dependencies at the composition root. `await using` disposes the
|
|
75
|
+
* // Run and waits for its child Tasks before leaving this scope.
|
|
76
|
+
* await using run = createRun({
|
|
77
|
+
* db: { usersById: new Map([[user.id, user]]) },
|
|
78
|
+
* });
|
|
186
79
|
*
|
|
187
|
-
* const
|
|
188
|
-
* expectTypeOf(
|
|
189
|
-
* expectOk(
|
|
80
|
+
* const result = await run(getUser(user.id));
|
|
81
|
+
* expectTypeOf(result).toEqualTypeOf<Result<User, UserNotFoundError>>();
|
|
82
|
+
* expectOk(result, user);
|
|
190
83
|
* ```
|
|
191
84
|
*
|
|
192
85
|
* In composition roots, prefer the lifecycle API from the matching Evolu
|
|
@@ -198,76 +91,99 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
198
91
|
*
|
|
199
92
|
* ## Composition
|
|
200
93
|
*
|
|
201
|
-
* | Category | Helper | Description
|
|
202
|
-
* | ------------ | ------------------------- |
|
|
203
|
-
* | Collection | {@link all} | Return Ok values or stop on first Err
|
|
204
|
-
* | | {@link allSettled} | Return every Task Result
|
|
205
|
-
* | | {@link each} | Handle each Task Result
|
|
206
|
-
* | Interop | {@link callback} | Wrap callback APIs
|
|
207
|
-
* | | {@link fetch} | Native fetch with bounded Response use
|
|
208
|
-
* | Timing | {@link sleep} | Pause execution
|
|
209
|
-
* | | {@link timeout} | Time-bounded execution
|
|
210
|
-
* | Resilience | {@link retry} | Retry domain errors with a schedule
|
|
211
|
-
* | Repetition | {@link repeat} | Repeat successes with a schedule
|
|
212
|
-
* | Racing | {@link any} | First Ok wins
|
|
213
|
-
* | | {@link race} | First settled Result wins
|
|
214
|
-
* | | {@link firstN} | First n Ok values win
|
|
215
|
-
* | | {@link firstNSettled} | First n Results win
|
|
216
|
-
* | Scheduling | {@link prioritized} | Assign scheduler priority
|
|
217
|
-
* | | {@link yieldNow} | Yield to the host scheduler
|
|
218
|
-
* | Lifetime | {@link daemon} | Run under root ownership
|
|
219
|
-
* |
|
|
220
|
-
* |
|
|
221
|
-
* | | {@link
|
|
94
|
+
* | Category | Helper | Description |
|
|
95
|
+
* | ------------ | ------------------------- | ----------------------------------------------------- |
|
|
96
|
+
* | Collection | {@link all} | Return {@link Ok} values or stop on first {@link Err} |
|
|
97
|
+
* | | {@link allSettled} | Return every Task Result |
|
|
98
|
+
* | | {@link each} | Handle each Task Result |
|
|
99
|
+
* | Interop | {@link callback} | Wrap callback APIs |
|
|
100
|
+
* | | {@link fetch} | Native fetch with bounded Response use |
|
|
101
|
+
* | Timing | {@link sleep} | Pause execution |
|
|
102
|
+
* | | {@link timeout} | Time-bounded execution |
|
|
103
|
+
* | Resilience | {@link retry} | Retry domain errors with a schedule |
|
|
104
|
+
* | Repetition | {@link repeat} | Repeat successes with a schedule |
|
|
105
|
+
* | Racing | {@link any} | First Ok wins |
|
|
106
|
+
* | | {@link race} | First settled Result wins |
|
|
107
|
+
* | | {@link firstN} | First n Ok values win |
|
|
108
|
+
* | | {@link firstNSettled} | First n Results win |
|
|
109
|
+
* | Scheduling | {@link prioritized} | Assign scheduler priority |
|
|
110
|
+
* | | {@link yieldNow} | Yield to the host scheduler |
|
|
111
|
+
* | Lifetime | {@link daemon} | Run under root ownership |
|
|
112
|
+
* | | {@link acquireUseRelease} | Bracket acquire, use, and release |
|
|
113
|
+
* | Abortability | {@link unabortable} | Mask abort after a Task starts |
|
|
114
|
+
* | | {@link unabortableMask} | Mask abort and selectively restore it |
|
|
222
115
|
*
|
|
223
116
|
* Helpers that process multiple Tasks run sequentially by default. Use a
|
|
224
117
|
* `concurrency` option to run more than one Task at a time.
|
|
225
118
|
*
|
|
119
|
+
* ### Sequential composition
|
|
120
|
+
*
|
|
226
121
|
* For ordinary sequential composition, use imperative code:
|
|
227
122
|
*
|
|
228
123
|
* ```ts
|
|
229
|
-
* import {
|
|
124
|
+
* import {
|
|
125
|
+
* createRun,
|
|
126
|
+
* err,
|
|
127
|
+
* ok,
|
|
128
|
+
* type Result,
|
|
129
|
+
* type Task,
|
|
130
|
+
* type Typed,
|
|
131
|
+
* } from "@evolu/common";
|
|
230
132
|
*
|
|
231
133
|
* interface User {
|
|
232
134
|
* readonly id: string;
|
|
233
135
|
* readonly profileId: string;
|
|
234
136
|
* }
|
|
137
|
+
*
|
|
235
138
|
* interface Profile {
|
|
236
139
|
* readonly id: string;
|
|
237
140
|
* }
|
|
238
141
|
*
|
|
239
|
-
* const
|
|
240
|
-
* (id: string): Task<User,
|
|
142
|
+
* const getUser =
|
|
143
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
241
144
|
* () =>
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
145
|
+
* id === "user-1"
|
|
146
|
+
* ? ok({ id, profileId: "profile-1" })
|
|
147
|
+
* : err({ type: "UserNotFound", id });
|
|
148
|
+
*
|
|
149
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {
|
|
150
|
+
* readonly id: string;
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* const getProfile =
|
|
154
|
+
* (id: string): Task<Profile, ProfileNotFoundError> =>
|
|
245
155
|
* () =>
|
|
246
|
-
*
|
|
156
|
+
* id === "profile-1"
|
|
157
|
+
* ? ok({ id })
|
|
158
|
+
* : err({ type: "ProfileNotFound", id });
|
|
159
|
+
*
|
|
160
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {
|
|
161
|
+
* readonly id: string;
|
|
162
|
+
* }
|
|
247
163
|
*
|
|
248
|
-
* const
|
|
164
|
+
* const getUserWithProfile =
|
|
249
165
|
* (
|
|
250
166
|
* id: string,
|
|
251
167
|
* ): Task<
|
|
252
168
|
* { readonly user: User; readonly profile: Profile },
|
|
253
|
-
*
|
|
169
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
254
170
|
* > =>
|
|
255
171
|
* async (run) => {
|
|
256
|
-
* const user = await run(
|
|
172
|
+
* const user = await run(getUser(id));
|
|
257
173
|
* if (!user.ok) return user;
|
|
258
174
|
*
|
|
259
|
-
* const profile = await run(
|
|
175
|
+
* const profile = await run(getProfile(user.value.profileId));
|
|
260
176
|
* if (!profile.ok) return profile;
|
|
261
177
|
*
|
|
262
178
|
* return ok({ user: user.value, profile: profile.value });
|
|
263
179
|
* };
|
|
264
180
|
*
|
|
265
181
|
* await using run = createRun();
|
|
266
|
-
* const result = await run(
|
|
182
|
+
* const result = await run(getUserWithProfile("user-1"));
|
|
267
183
|
* expectTypeOf(result).toEqualTypeOf<
|
|
268
184
|
* Result<
|
|
269
185
|
* { readonly user: User; readonly profile: Profile },
|
|
270
|
-
*
|
|
186
|
+
* UserNotFoundError | ProfileNotFoundError
|
|
271
187
|
* >
|
|
272
188
|
* >();
|
|
273
189
|
* expectOk(result, {
|
|
@@ -279,33 +195,13 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
279
195
|
* Evolu intentionally avoids pipe APIs, chainable methods, and generator-based
|
|
280
196
|
* effect DSLs. Plain async/await with early returns is easier to read, review,
|
|
281
197
|
* and debug, and it lets TypeScript narrow Result values through ordinary
|
|
282
|
-
* control flow.
|
|
283
|
-
* in the {@link Result} FAQ.
|
|
198
|
+
* control flow.
|
|
284
199
|
*
|
|
285
|
-
* ###
|
|
200
|
+
* ### Resilient fetch
|
|
286
201
|
*
|
|
287
202
|
* {@link fetch} with a body mode already returns a plain value, so resilience is
|
|
288
|
-
* ordinary Task composition.
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* ```ts
|
|
292
|
-
* import {
|
|
293
|
-
* fetch,
|
|
294
|
-
* timeout,
|
|
295
|
-
* type FetchError,
|
|
296
|
-
* type Task,
|
|
297
|
-
* type TimeoutError,
|
|
298
|
-
* } from "@evolu/common";
|
|
299
|
-
*
|
|
300
|
-
* const fetchWithTimeout = (url: string) =>
|
|
301
|
-
* timeout(fetch(url, "text"), "30s");
|
|
302
|
-
*
|
|
303
|
-
* expectTypeOf(fetchWithTimeout).returns.toEqualTypeOf<
|
|
304
|
-
* Task<string, FetchError | TimeoutError>
|
|
305
|
-
* >();
|
|
306
|
-
* ```
|
|
307
|
-
*
|
|
308
|
-
* Add {@link retry} for recoverable domain failures:
|
|
203
|
+
* ordinary Task composition. Combine {@link timeout} and {@link retry} to bound
|
|
204
|
+
* each attempt and retry recoverable domain failures:
|
|
309
205
|
*
|
|
310
206
|
* ```ts
|
|
311
207
|
* import {
|
|
@@ -322,12 +218,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
322
218
|
* type TimeoutError,
|
|
323
219
|
* } from "@evolu/common";
|
|
324
220
|
*
|
|
325
|
-
* const fetchWithTimeout = (url: string) =>
|
|
326
|
-
* timeout(fetch(url, "text"), "30s");
|
|
327
|
-
*
|
|
328
221
|
* const fetchWithRetry = (url: string) =>
|
|
329
222
|
* retry(
|
|
330
|
-
*
|
|
223
|
+
* timeout(fetch(url, "text"), "30s"),
|
|
331
224
|
* // A jittered, capped, limited exponential backoff.
|
|
332
225
|
* jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
|
|
333
226
|
* );
|
|
@@ -337,7 +230,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
337
230
|
* >();
|
|
338
231
|
* ```
|
|
339
232
|
*
|
|
340
|
-
*
|
|
233
|
+
* ### Concurrent composition
|
|
234
|
+
*
|
|
235
|
+
* Run composed Tasks with a `concurrency` option and {@link all}:
|
|
341
236
|
*
|
|
342
237
|
* ```ts
|
|
343
238
|
* import { all, createRun, ok, sleep, type Task } from "@evolu/common";
|
|
@@ -367,23 +262,21 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
367
262
|
* expect(maxActiveRequests).toBe(2);
|
|
368
263
|
* ```
|
|
369
264
|
*
|
|
370
|
-
* ## Concurrency Primitives
|
|
371
|
-
*
|
|
372
265
|
* Task helpers compose Tasks; concurrency primitives are stateful objects that
|
|
373
266
|
* coordinate Tasks across call sites. Create them with their `createX`
|
|
374
267
|
* factories and share them where coordination is needed.
|
|
375
268
|
*
|
|
376
|
-
* | Primitive | Description
|
|
377
|
-
* | ---------------------- |
|
|
378
|
-
* | {@link Deferred} | One-shot value resolved from outside
|
|
379
|
-
* | {@link Gate} | Block and release Tasks repeatedly
|
|
380
|
-
* | {@link Semaphore} | Limit concurrent Tasks with permits
|
|
381
|
-
* | {@link Mutex} | Run Tasks one at a time
|
|
382
|
-
* | {@link SemaphoreByKey} | Per-key permits with automatic cleanup
|
|
383
|
-
* | {@link MutexByKey} | Per-key one-at-a-time execution
|
|
384
|
-
* | {@link MutexRef} | Ref with serialized Task transitions
|
|
269
|
+
* | Primitive | Description |
|
|
270
|
+
* | ---------------------- | -------------------------------------------- |
|
|
271
|
+
* | {@link Deferred} | One-shot value resolved from outside |
|
|
272
|
+
* | {@link Gate} | Block and release Tasks repeatedly |
|
|
273
|
+
* | {@link Semaphore} | Limit concurrent Tasks with permits |
|
|
274
|
+
* | {@link Mutex} | Run Tasks one at a time |
|
|
275
|
+
* | {@link SemaphoreByKey} | Per-key permits with automatic cleanup |
|
|
276
|
+
* | {@link MutexByKey} | Per-key one-at-a-time execution |
|
|
277
|
+
* | {@link MutexRef} | {@link Ref} with serialized Task transitions |
|
|
385
278
|
*
|
|
386
|
-
* ## Dependency
|
|
279
|
+
* ## Dependency injection
|
|
387
280
|
*
|
|
388
281
|
* Task DI is
|
|
389
282
|
* {@link https://www.evolu.dev/docs/dependency-injection | Evolu Pure DI}
|
|
@@ -397,7 +290,42 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
397
290
|
* Use normal Task arguments for per-call values and `D` for capabilities,
|
|
398
291
|
* resources, or services shared by all code running inside a Run.
|
|
399
292
|
*
|
|
400
|
-
*
|
|
293
|
+
* ```ts
|
|
294
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
295
|
+
*
|
|
296
|
+
* interface GreetingFormatter {
|
|
297
|
+
* readonly format: (name: string) => string;
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* interface GreetingFormatterDep {
|
|
301
|
+
* readonly greetingFormatter: GreetingFormatter;
|
|
302
|
+
* }
|
|
303
|
+
*
|
|
304
|
+
* const greet =
|
|
305
|
+
* (name: string): Task<string, never, GreetingFormatterDep> =>
|
|
306
|
+
* (run) =>
|
|
307
|
+
* ok(run.deps.greetingFormatter.format(name));
|
|
308
|
+
*
|
|
309
|
+
* const formal: GreetingFormatter = {
|
|
310
|
+
* format: (name) => `Hello, ${name}`,
|
|
311
|
+
* };
|
|
312
|
+
* const casual: GreetingFormatter = {
|
|
313
|
+
* format: (name) => `Hi, ${name}`,
|
|
314
|
+
* };
|
|
315
|
+
*
|
|
316
|
+
* await using run = createRun({ greetingFormatter: formal });
|
|
317
|
+
*
|
|
318
|
+
* // Root dependencies are inherited.
|
|
319
|
+
* expectOk(await run(greet("Ada")), "Hello, Ada");
|
|
320
|
+
*
|
|
321
|
+
* // Child-specific dependencies replace the root's custom dependencies.
|
|
322
|
+
* expectOk(
|
|
323
|
+
* await run(greet("Ada"), { greetingFormatter: casual }),
|
|
324
|
+
* "Hi, Ada",
|
|
325
|
+
* );
|
|
326
|
+
* ```
|
|
327
|
+
*
|
|
328
|
+
* ### Default dependencies
|
|
401
329
|
*
|
|
402
330
|
* {@link createRun} provides default {@link RunDefaultDeps} available to all
|
|
403
331
|
* Tasks without declaring `D`:
|
|
@@ -410,62 +338,10 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
410
338
|
* - {@link ReportDefect} — defect reporting
|
|
411
339
|
* - {@link Time} — current time
|
|
412
340
|
*
|
|
413
|
-
* For example, using Console:
|
|
414
|
-
*
|
|
415
|
-
* ```ts
|
|
416
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
417
|
-
*
|
|
418
|
-
* const myTask: Task<void> = async (run) => {
|
|
419
|
-
* const { console } = run.deps;
|
|
420
|
-
* console.log("started");
|
|
421
|
-
* return ok();
|
|
422
|
-
* };
|
|
423
|
-
*
|
|
424
|
-
* expectTypeOf(myTask).toEqualTypeOf<Task<void>>();
|
|
425
|
-
* await using run = createRun();
|
|
426
|
-
* expectOk(await run(myTask), undefined);
|
|
427
|
-
* ```
|
|
428
|
-
*
|
|
429
|
-
* Custom Console with formatted output:
|
|
430
|
-
*
|
|
431
|
-
* ```ts
|
|
432
|
-
* import {
|
|
433
|
-
* createConsoleArrayOutput,
|
|
434
|
-
* createConsole,
|
|
435
|
-
* createConsoleFormatter,
|
|
436
|
-
* createRun,
|
|
437
|
-
* type ConsoleEntry,
|
|
438
|
-
* } from "@evolu/common";
|
|
439
|
-
*
|
|
440
|
-
* const entries: Array<ConsoleEntry> = [];
|
|
441
|
-
* const formatter = createConsoleFormatter()({
|
|
442
|
-
* timestampFormat: "absolute",
|
|
443
|
-
* });
|
|
444
|
-
* const deps = {
|
|
445
|
-
* console: createConsole({
|
|
446
|
-
* output: createConsoleArrayOutput(entries),
|
|
447
|
-
* formatter,
|
|
448
|
-
* }),
|
|
449
|
-
* };
|
|
450
|
-
*
|
|
451
|
-
* await using run = createRun(deps);
|
|
452
|
-
* const console = run.deps.console.child("main");
|
|
453
|
-
*
|
|
454
|
-
* console.log("started");
|
|
455
|
-
* expect(console.name).toBe("main");
|
|
456
|
-
* expect(entries).toHaveLength(1);
|
|
457
|
-
* expect(entries[0]?.path).toEqual(["main"]);
|
|
458
|
-
* assert(entries[0]);
|
|
459
|
-
* const formattedArgs = formatter(entries[0]);
|
|
460
|
-
* // 21:20:25.588 [main] started
|
|
461
|
-
* expect(formattedArgs[0]).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3} \[main\]$/);
|
|
462
|
-
* expect(formattedArgs[1]).toBe("started");
|
|
463
|
-
* ```
|
|
464
|
-
*
|
|
465
341
|
* For testing, use {@link testCreateRun} to get deterministic, controllable
|
|
466
342
|
* implementations of all RunDefaultDeps.
|
|
467
343
|
*
|
|
468
|
-
* ## Resource
|
|
344
|
+
* ## Resource management
|
|
469
345
|
*
|
|
470
346
|
* JavaScript provides standard
|
|
471
347
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management | resource management}.
|
|
@@ -479,7 +355,7 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
479
355
|
* - Closure-held state bounded by a reusable {@link DisposableRun}:
|
|
480
356
|
* {@link DisposableRun.defer}
|
|
481
357
|
*
|
|
482
|
-
* ### Returning
|
|
358
|
+
* ### Returning resources from Tasks
|
|
483
359
|
*
|
|
484
360
|
* A Task that successfully returns a disposable resource transfers ownership of
|
|
485
361
|
* a live resource to its caller. The resource must remain live after the Task
|
|
@@ -494,32 +370,33 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
494
370
|
* `undefined` should represent valid absence, not failure.
|
|
495
371
|
*
|
|
496
372
|
* ```ts
|
|
497
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
373
|
+
* import { createRun, ok, type Task, type Typed } from "@evolu/common";
|
|
498
374
|
*
|
|
499
|
-
* interface ConnectionError {
|
|
500
|
-
* readonly type: "ConnectionError";
|
|
501
|
-
* }
|
|
502
375
|
* interface Socket extends AsyncDisposable {
|
|
503
376
|
* readonly send: (message: string) => string;
|
|
504
377
|
* }
|
|
378
|
+
*
|
|
505
379
|
* interface Connection extends AsyncDisposable {
|
|
506
380
|
* readonly send: (message: string) => string;
|
|
507
381
|
* }
|
|
508
382
|
*
|
|
509
383
|
* let socketDisposed = false;
|
|
510
|
-
* const openSocket: Task<Socket,
|
|
384
|
+
* const openSocket: Task<Socket, ConnectionFailedError> = () =>
|
|
511
385
|
* ok({
|
|
512
386
|
* send: (message) => message,
|
|
513
387
|
* [Symbol.asyncDispose]: async () => {
|
|
514
388
|
* socketDisposed = true;
|
|
515
389
|
* },
|
|
516
390
|
* });
|
|
391
|
+
*
|
|
392
|
+
* interface ConnectionFailedError extends Typed<"ConnectionFailed"> {}
|
|
393
|
+
*
|
|
517
394
|
* const handshake =
|
|
518
|
-
* (_socket: Socket): Task<void,
|
|
395
|
+
* (_socket: Socket): Task<void, ConnectionFailedError> =>
|
|
519
396
|
* () =>
|
|
520
397
|
* ok();
|
|
521
398
|
*
|
|
522
|
-
* const createConnection: Task<Connection,
|
|
399
|
+
* const createConnection: Task<Connection, ConnectionFailedError> = async (
|
|
523
400
|
* run,
|
|
524
401
|
* ) => {
|
|
525
402
|
* await using disposer = new AsyncDisposableStack();
|
|
@@ -547,95 +424,15 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
547
424
|
* expect(socketDisposed).toBe(true);
|
|
548
425
|
* ```
|
|
549
426
|
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* ```ts
|
|
554
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
555
|
-
*
|
|
556
|
-
* interface Foo extends AsyncDisposable {
|
|
557
|
-
* readonly value: string;
|
|
558
|
-
* }
|
|
559
|
-
*
|
|
560
|
-
* let disposed = false;
|
|
561
|
-
* const createFoo = (): Task<Foo> => () =>
|
|
562
|
-
* ok({
|
|
563
|
-
* value: "foo",
|
|
564
|
-
* [Symbol.asyncDispose]: async () => {
|
|
565
|
-
* disposed = true;
|
|
566
|
-
* },
|
|
567
|
-
* });
|
|
568
|
-
*
|
|
569
|
-
* await using run = createRun();
|
|
570
|
-
* {
|
|
571
|
-
* await using foo = await run.ok(createFoo());
|
|
572
|
-
* expect(foo.value).toBe("foo");
|
|
573
|
-
* }
|
|
574
|
-
* expect(disposed).toBe(true);
|
|
575
|
-
* ```
|
|
576
|
-
*
|
|
577
|
-
* Use {@link acquireUseRelease} when acquisition and release are separate
|
|
578
|
-
* operations rather than a disposable value:
|
|
579
|
-
*
|
|
580
|
-
* ```ts
|
|
581
|
-
* import {
|
|
582
|
-
* acquireUseRelease,
|
|
583
|
-
* createRun,
|
|
584
|
-
* ok,
|
|
585
|
-
* type Task,
|
|
586
|
-
* } from "@evolu/common";
|
|
587
|
-
*
|
|
588
|
-
* interface User {
|
|
589
|
-
* readonly id: string;
|
|
590
|
-
* readonly name: string;
|
|
591
|
-
* }
|
|
592
|
-
*
|
|
593
|
-
* interface Connection {
|
|
594
|
-
* readonly loadUser: (id: string) => User;
|
|
595
|
-
* }
|
|
596
|
-
*
|
|
597
|
-
* const openConnection: Task<Connection> = () =>
|
|
598
|
-
* ok({ loadUser: (id) => ({ id, name: "Ada" }) });
|
|
599
|
-
*
|
|
600
|
-
* const loadUser =
|
|
601
|
-
* (connection: Connection): Task<User> =>
|
|
602
|
-
* () =>
|
|
603
|
-
* ok(connection.loadUser("user-1"));
|
|
604
|
-
*
|
|
605
|
-
* let connectionClosed = false;
|
|
606
|
-
* const closeConnection =
|
|
607
|
-
* (_connection: Connection): Task<void> =>
|
|
608
|
-
* () => {
|
|
609
|
-
* connectionClosed = true;
|
|
610
|
-
* return ok();
|
|
611
|
-
* };
|
|
612
|
-
*
|
|
613
|
-
* const queryUser = acquireUseRelease(
|
|
614
|
-
* openConnection,
|
|
615
|
-
* loadUser,
|
|
616
|
-
* closeConnection,
|
|
617
|
-
* );
|
|
618
|
-
*
|
|
619
|
-
* await using run = createRun();
|
|
620
|
-
* const result = await run(queryUser);
|
|
621
|
-
* expectOk(result, { id: "user-1", name: "Ada" });
|
|
622
|
-
* expect(connectionClosed).toBe(true);
|
|
623
|
-
* ```
|
|
427
|
+
* Use {@link Run.ok} with `await using` when an infallible Task returns a
|
|
428
|
+
* disposable value. Use {@link acquireUseRelease} when acquisition and release
|
|
429
|
+
* are separate operations rather than a disposable value.
|
|
624
430
|
*
|
|
625
431
|
* ## Awaitable
|
|
626
432
|
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* type Awaitable<T> = T | PromiseLike<T>;
|
|
631
|
-
*
|
|
632
|
-
* expectTypeOf<Awaitable<number>>().toEqualTypeOf<
|
|
633
|
-
* EvoluAwaitable<number>
|
|
634
|
-
* >();
|
|
635
|
-
* ```
|
|
636
|
-
*
|
|
637
|
-
* Even though {@link Task} returns {@link Awaitable}, allowing sync or async
|
|
638
|
-
* results, {@link Run} is always async. This is a deliberate design choice:
|
|
433
|
+
* A {@link Task} returns {@link Awaitable}, so its body may produce a
|
|
434
|
+
* {@link Result} immediately or asynchronously. {@link Run} is always async and
|
|
435
|
+
* returns a {@link Fiber}; callers use the same ownership model either way.
|
|
639
436
|
*
|
|
640
437
|
* - **Sync** → {@link Result}, native `using` / `DisposableStack`
|
|
641
438
|
* - **Async** → Task, Run, {@link Fiber}, `await using` / `AsyncDisposableStack`
|
|
@@ -644,39 +441,36 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
644
441
|
* decomposition. Calling `run(task)` always creates a child Run by design. Use
|
|
645
442
|
* ordinary promises when an async operation does not need its own Run.
|
|
646
443
|
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* Sync functions should be fast, so there is no need to monitor them. They
|
|
660
|
-
* should take values, not dependencies — following the
|
|
444
|
+
* A unified sync/async effect API is technically possible. It can detect
|
|
445
|
+
* Promise-like values with {@link isPromiseLike}, dispose synchronous resources
|
|
446
|
+
* first, continue with asynchronous disposal when necessary, and track whether
|
|
447
|
+
* callers must await the result. Evolu deliberately keeps the two models
|
|
448
|
+
* separate instead: plain functions and Result for synchronous code, Task and
|
|
449
|
+
* Run for asynchronous ownership. Most effects involve inherently asynchronous
|
|
450
|
+
* I/O, while synchronous code benefits from a simpler API and no Task
|
|
451
|
+
* overhead.
|
|
452
|
+
*
|
|
453
|
+
* Keep synchronous computation as plain functions returning Result. Prefer
|
|
454
|
+
* passing values rather than dependencies, following the
|
|
661
455
|
* {@link https://blog.ploeh.dk/2017/02/02/dependency-rejection/ | impure/pure/impure sandwich}
|
|
662
456
|
* pattern where impure code gathers data, pure functions process it, and impure
|
|
663
|
-
* code performs effects with the result.
|
|
664
|
-
*
|
|
665
|
-
* {@link Random} could instead accept {@link RandomNumber} as a value.
|
|
457
|
+
* code performs effects with the result. For example, a pure function can
|
|
458
|
+
* accept a {@link RandomNumber} value instead of depending on {@link Random}.
|
|
666
459
|
*
|
|
667
|
-
*
|
|
668
|
-
* or complex cryptography belong in
|
|
669
|
-
*
|
|
670
|
-
* monitoring.
|
|
671
|
-
* call to the worker provides it.
|
|
460
|
+
* Large CPU-bound operations, such as parsing large JSON, sorting millions of
|
|
461
|
+
* items, or complex cryptography, belong in a worker. Model the asynchronous
|
|
462
|
+
* call to that worker as a Task so Run can provide timeout, abort, cleanup, and
|
|
463
|
+
* monitoring.
|
|
672
464
|
*
|
|
673
465
|
* ## Glossary
|
|
674
466
|
*
|
|
675
|
-
* - **Defect** — a thrown
|
|
467
|
+
* - **Defect** — a thrown or rejected value other than {@link AbortError}, rather
|
|
468
|
+
* than a declared {@link Result} error.
|
|
676
469
|
* - **Outcome** — a Fiber's settlement: resolution with the Task {@link Result},
|
|
677
|
-
* or rejection with {@link AbortError}.
|
|
678
|
-
*
|
|
679
|
-
*
|
|
470
|
+
* or rejection with {@link AbortError}. The original defect is reported
|
|
471
|
+
* through {@link ReportDefectDep} whether or not the Fiber is observed; the
|
|
472
|
+
* Fiber boundary represents the panic with AbortError whose reason is
|
|
473
|
+
* {@link PanicAbortReason}.
|
|
680
474
|
* - **Create** — construct a new value or a resource.
|
|
681
475
|
* - **Acquire** — obtain a usable resource. Acquisition may create a new
|
|
682
476
|
* resource, borrow one, open one, or take a lease/lock.
|
|
@@ -684,7 +478,7 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
684
478
|
* pairs with acquire and need not mean disposal; examples include unlock,
|
|
685
479
|
* logout, or returning a pooled resource.
|
|
686
480
|
* - **Dispose / disposal** — owner-driven resource finalization via JavaScript
|
|
687
|
-
* management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
481
|
+
* resource management (`Symbol.dispose`, `Symbol.asyncDispose`, `using`,
|
|
688
482
|
* `AsyncDisposableStack`).
|
|
689
483
|
*
|
|
690
484
|
* ## FAQ
|
|
@@ -722,12 +516,11 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
722
516
|
*
|
|
723
517
|
* ### What should Task code do with defects?
|
|
724
518
|
*
|
|
725
|
-
* Nothing. Once a defect
|
|
726
|
-
* Tasks are aborted, and the Run tree shuts down. If
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
* be useful anyway.
|
|
519
|
+
* Nothing. Once a defect reaches the {@link Run}, it is too late: the root Run
|
|
520
|
+
* panics, running Tasks are aborted, and the Run tree shuts down. If an
|
|
521
|
+
* operation can throw or reject for a recoverable reason, wrap that operation
|
|
522
|
+
* with {@link trySync} or {@link tryAsync} so the failure becomes a typed
|
|
523
|
+
* {@link Result} error. Let unrecoverable failures propagate as defects.
|
|
731
524
|
*
|
|
732
525
|
* ### Why does a defect panic the whole Run tree?
|
|
733
526
|
*
|
|
@@ -770,7 +563,9 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
770
563
|
*
|
|
771
564
|
* Task follows the opposite approach: Tasks are ordinary async functions, Run
|
|
772
565
|
* owns lifetimes and scoped context, {@link Result} carries expected domain
|
|
773
|
-
* errors, and defects keep real exceptions with real stacks.
|
|
566
|
+
* errors, and defects keep real exceptions with real stacks. Result propagation
|
|
567
|
+
* is explicit at each async boundary, so TypeScript narrows it through ordinary
|
|
568
|
+
* control flow and readers can see where an error is handled or returned.
|
|
774
569
|
*
|
|
775
570
|
* ### Are recursive Tasks stack-safe?
|
|
776
571
|
*
|
|
@@ -790,33 +585,32 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
790
585
|
* readonly children: ReadonlyArray<TreeNode>;
|
|
791
586
|
* }
|
|
792
587
|
*
|
|
793
|
-
* const visited: Array<string> = [];
|
|
794
|
-
* const visit = (node: TreeNode): void => {
|
|
795
|
-
* visited.push(node.value);
|
|
796
|
-
* };
|
|
797
|
-
*
|
|
798
588
|
* const visitTree =
|
|
799
|
-
* (root: TreeNode): Task<
|
|
589
|
+
* (root: TreeNode): Task<ReadonlyArray<string>> =>
|
|
800
590
|
* () => {
|
|
801
591
|
* const remaining = [root];
|
|
592
|
+
* const visited: Array<string> = [];
|
|
802
593
|
*
|
|
803
594
|
* while (remaining.length > 0) {
|
|
804
595
|
* const node = remaining.pop();
|
|
805
596
|
* if (!node) continue;
|
|
806
|
-
*
|
|
597
|
+
* visited.push(node.value);
|
|
807
598
|
* for (const child of node.children) remaining.push(child);
|
|
808
599
|
* }
|
|
809
600
|
*
|
|
810
|
-
* return ok();
|
|
601
|
+
* return ok(visited);
|
|
811
602
|
* };
|
|
812
603
|
*
|
|
813
|
-
* const tree: TreeNode = {
|
|
814
|
-
* value: "root",
|
|
815
|
-
* children: [{ value: "child", children: [] }],
|
|
816
|
-
* };
|
|
817
604
|
* await using run = createRun();
|
|
818
|
-
* expectOk(
|
|
819
|
-
*
|
|
605
|
+
* expectOk(
|
|
606
|
+
* await run(
|
|
607
|
+
* visitTree({
|
|
608
|
+
* value: "root",
|
|
609
|
+
* children: [{ value: "child", children: [] }],
|
|
610
|
+
* }),
|
|
611
|
+
* ),
|
|
612
|
+
* ["root", "child"],
|
|
613
|
+
* );
|
|
820
614
|
* ```
|
|
821
615
|
*
|
|
822
616
|
* Task favors direct native execution, `async`/`await`, and native tooling over
|
|
@@ -825,35 +619,12 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
825
619
|
* periodically await {@link yieldNow} for cooperative scheduling, and move
|
|
826
620
|
* CPU-bound work to a worker.
|
|
827
621
|
*
|
|
828
|
-
* ###
|
|
829
|
-
*
|
|
830
|
-
* Task uses native TypeScript control flow so each async boundary and error
|
|
831
|
-
* propagation point is visible.
|
|
832
|
-
*
|
|
833
|
-
* ```ts
|
|
834
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
835
|
-
*
|
|
836
|
-
* const loadUser: Task<string, "LoadUserError"> = () => ok("Ada");
|
|
837
|
-
* const greetUser: Task<string, "LoadUserError"> = async (run) => {
|
|
838
|
-
* const user = await run(loadUser);
|
|
839
|
-
* if (!user.ok) return user;
|
|
840
|
-
* return ok(`Hello, ${user.value}`);
|
|
841
|
-
* };
|
|
842
|
-
*
|
|
843
|
-
* await using run = createRun();
|
|
844
|
-
* expectOk(await run(greetUser), "Hello, Ada");
|
|
845
|
-
* ```
|
|
846
|
-
*
|
|
847
|
-
* This is slightly more verbose than fluent or generator-based syntax, but it's
|
|
848
|
-
* simple to read, easy to debug, friendly to TypeScript narrowing, and works
|
|
849
|
-
* well with generated code.
|
|
622
|
+
* ### Should a Task be called directly?
|
|
850
623
|
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* child disposal boundaries. It is reserved for Task internals that explicitly
|
|
856
|
-
* need same-Run execution; use `run(task)` in application code.
|
|
624
|
+
* Only inside Task internals that explicitly require same-Run execution. A
|
|
625
|
+
* direct call, `task(run)`, uses the current Run instead of creating a child
|
|
626
|
+
* Run, so it bypasses child lifetime tracking, scheduling metadata, and child
|
|
627
|
+
* disposal boundaries. Application code should use `run(task)`.
|
|
857
628
|
*
|
|
858
629
|
* ### Where are fork and join?
|
|
859
630
|
*
|
|
@@ -872,6 +643,32 @@ import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
|
872
643
|
* emitted by TypeScript; the polyfills provide the runtime resource-management
|
|
873
644
|
* globals.
|
|
874
645
|
*
|
|
646
|
+
* @module
|
|
647
|
+
*/
|
|
648
|
+
import { type NonEmptyReadonlyArray } from "./Array.ts";
|
|
649
|
+
import type { Brand } from "./Brand.ts";
|
|
650
|
+
import { type ConsoleDep, type TestConsoleDep } from "./Console.ts";
|
|
651
|
+
import { type RandomBytesDep } from "./Crypto.ts";
|
|
652
|
+
import type { NativeFetchDep } from "./Http.ts";
|
|
653
|
+
import { type LeakDetectorDep, type TestLeakDetectorDep } from "./LeakDetector.ts";
|
|
654
|
+
import { type LookupOption } from "./Lookup.ts";
|
|
655
|
+
import { type Int1To100OrPositiveInt } from "./Number.ts";
|
|
656
|
+
import { type Option } from "./Option.ts";
|
|
657
|
+
import { type RandomDep, type RandomLibDep } from "./Random.ts";
|
|
658
|
+
import { type Ref } from "./Ref.ts";
|
|
659
|
+
import { type Done, type Result } from "./Result.ts";
|
|
660
|
+
import type { Schedule, ScheduleStep } from "./Schedule.ts";
|
|
661
|
+
import { type Millis, type PositiveDuration, type TimeDep, type TestTimeDep } from "./Time.ts";
|
|
662
|
+
import { type InferType, NonNegativeInt, PositiveInt, String, Unknown, UnknownResult, type Id, type ObjectType, type RecordType, type Typed, type TypedType } from "./Type.ts";
|
|
663
|
+
import type { Awaitable, ParameterIntersection, Predicate } from "./Types.ts";
|
|
664
|
+
/**
|
|
665
|
+
* An operation run by {@link Run} that returns a {@link Result} synchronously or
|
|
666
|
+
* asynchronously and declares its dependencies through `D`.
|
|
667
|
+
*
|
|
668
|
+
* Its return type is {@link Awaitable}.
|
|
669
|
+
*
|
|
670
|
+
* See the {@link @evolu/common!Task | Task overview}.
|
|
671
|
+
*
|
|
875
672
|
* @group Core
|
|
876
673
|
*/
|
|
877
674
|
export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<T, E>>;
|
|
@@ -882,13 +679,13 @@ export type Task<T, E = never, D = unknown> = (run: Run<D>) => Awaitable<Result<
|
|
|
882
679
|
*/
|
|
883
680
|
export type AnyTask = Task<any, any, any>;
|
|
884
681
|
/**
|
|
885
|
-
* Extracts the Ok value type from a {@link Task}.
|
|
682
|
+
* Extracts the {@link Ok} value type from a {@link Task}.
|
|
886
683
|
*
|
|
887
684
|
* @group Type utilities
|
|
888
685
|
*/
|
|
889
686
|
export type InferTaskOk<TTask extends AnyTask> = TTask extends Task<infer T, any, any> ? T : never;
|
|
890
687
|
/**
|
|
891
|
-
* Extracts the Result error type from a {@link Task}.
|
|
688
|
+
* Extracts the {@link Result} error type from a {@link Task}.
|
|
892
689
|
*
|
|
893
690
|
* @group Type utilities
|
|
894
691
|
*/
|
|
@@ -900,10 +697,11 @@ export type InferTaskErr<TTask extends AnyTask> = TTask extends Task<any, infer
|
|
|
900
697
|
*/
|
|
901
698
|
export type InferTaskDeps<TTask extends AnyTask> = TTask extends Task<any, any, infer D> ? D : never;
|
|
902
699
|
/**
|
|
903
|
-
* A {@link Task} that can return a value, signal done, or return a Result
|
|
700
|
+
* A {@link Task} that can return a value, signal done, or return a {@link Result}
|
|
701
|
+
* error.
|
|
904
702
|
*
|
|
905
|
-
* Use for pull-based protocols where
|
|
906
|
-
* than an error.
|
|
703
|
+
* Use for pull-based protocols where {@link Done | Done<D>} signals normal
|
|
704
|
+
* completion rather than an error.
|
|
907
705
|
*
|
|
908
706
|
* @group Core
|
|
909
707
|
*/
|
|
@@ -918,8 +716,8 @@ export type InferTaskDone<TTask extends AnyTask> = InferTaskErr<TTask> extends i
|
|
|
918
716
|
* A {@link Task} whose error type is not `never`.
|
|
919
717
|
*
|
|
920
718
|
* Used by {@link Run.orThrow} to accept only Tasks that can return a declared
|
|
921
|
-
* {@link Err}. Tasks without declared Result errors should use
|
|
922
|
-
* instead.
|
|
719
|
+
* {@link Err}. Tasks without declared {@link Result} errors should use
|
|
720
|
+
* {@link Run.ok} instead.
|
|
923
721
|
*
|
|
924
722
|
* @group Type utilities
|
|
925
723
|
*/
|
|
@@ -942,7 +740,7 @@ export type TaskWithError<TTask extends AnyTask> = TTask & ([InferTaskErr<TTask>
|
|
|
942
740
|
* the child Fiber from the parent. If a parent Task returns before awaiting or
|
|
943
741
|
* returning a child Fiber, cleanup still waits for the child. A child defect
|
|
944
742
|
* during that cleanup panics and aborts the root Run, but the parent Fiber
|
|
945
|
-
* keeps the Result already returned by the parent Task.
|
|
743
|
+
* keeps the {@link Result} already returned by the parent Task.
|
|
946
744
|
*
|
|
947
745
|
* Disposing a Run requests abort and prevents new child Tasks from starting.
|
|
948
746
|
* Async disposal waits for current children to settle. Abort requests propagate
|
|
@@ -981,9 +779,7 @@ export interface Run<D = unknown> {
|
|
|
981
779
|
* defects are still reported. Use {@link Run.daemon} for work that should
|
|
982
780
|
* outlive the current Task.
|
|
983
781
|
*
|
|
984
|
-
* The
|
|
985
|
-
* Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
|
|
986
|
-
* assignable alternatives.
|
|
782
|
+
* The Task uses this Run's current dependencies.
|
|
987
783
|
*
|
|
988
784
|
* The Fiber rejects when the Task observes abort by throwing
|
|
989
785
|
* {@link AbortError}. It also rejects with AbortError whose reason is
|
|
@@ -1003,25 +799,32 @@ export interface Run<D = unknown> {
|
|
|
1003
799
|
* interface Db {
|
|
1004
800
|
* readonly name: string;
|
|
1005
801
|
* }
|
|
802
|
+
*
|
|
1006
803
|
* interface DbDep {
|
|
1007
804
|
* readonly db: Db;
|
|
1008
805
|
* }
|
|
1009
806
|
*
|
|
1010
807
|
* const db: Db = { name: "main" };
|
|
1011
808
|
* const loadUser: Task<string> = () => ok("Ada");
|
|
1012
|
-
* const saveUser: Task<void, never, DbDep> = (
|
|
1013
|
-
* expect(deps.db).toBe(db);
|
|
809
|
+
* const saveUser: Task<void, never, DbDep> = (run) => {
|
|
810
|
+
* expect(run.deps.db).toBe(db);
|
|
1014
811
|
* return ok();
|
|
1015
812
|
* };
|
|
1016
813
|
*
|
|
1017
|
-
* await using run = createRun();
|
|
814
|
+
* await using run = createRun({ db });
|
|
1018
815
|
* const userResult = await run(loadUser);
|
|
1019
|
-
* const savedResult = await run(saveUser
|
|
816
|
+
* const savedResult = await run(saveUser);
|
|
1020
817
|
* expectOk(userResult, "Ada");
|
|
1021
818
|
* expectOk(savedResult, undefined);
|
|
1022
819
|
* ```
|
|
1023
820
|
*/
|
|
1024
821
|
<T, E>(task: Task<T, E, D>): Fiber<T, E, D>;
|
|
822
|
+
/**
|
|
823
|
+
* Starts a {@link Task} with replacement custom dependencies.
|
|
824
|
+
*
|
|
825
|
+
* Default deps ({@link RunDefaultDeps}) are inherited unless replaced with
|
|
826
|
+
* assignable alternatives.
|
|
827
|
+
*/
|
|
1025
828
|
<T, E, Deps extends object>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): Fiber<T, E, Deps>;
|
|
1026
829
|
/**
|
|
1027
830
|
* Runs a {@link Task} whose error type is not `never` and throws if the
|
|
@@ -1030,18 +833,59 @@ export interface Run<D = unknown> {
|
|
|
1030
833
|
* This is the Task equivalent of {@link getOrThrow}. Use it where a declared
|
|
1031
834
|
* Result error should crash the current flow instead of being handled
|
|
1032
835
|
* locally.
|
|
836
|
+
*
|
|
837
|
+
* ### Example
|
|
838
|
+
*
|
|
839
|
+
* ```ts
|
|
840
|
+
* import { createRun, ok, type Task, type Typed } from "@evolu/common";
|
|
841
|
+
*
|
|
842
|
+
* const loadConfig: Task<string, ConfigInvalidError> = () =>
|
|
843
|
+
* ok("config");
|
|
844
|
+
*
|
|
845
|
+
* interface ConfigInvalidError extends Typed<"ConfigInvalid"> {}
|
|
846
|
+
*
|
|
847
|
+
* await using run = createRun();
|
|
848
|
+
* expect(await run.orThrow(loadConfig)).toBe("config");
|
|
849
|
+
* ```
|
|
1033
850
|
*/
|
|
1034
851
|
readonly orThrow: {
|
|
1035
852
|
<TTask extends Task<any, any, D>>(task: TaskWithError<TTask>): Promise<InferTaskOk<TTask>>;
|
|
1036
853
|
<Deps extends object, TTask extends Task<any, any, Deps>>(task: TaskWithError<TTask>, deps: RunCustomDeps<Deps>): Promise<InferTaskOk<TTask>>;
|
|
1037
854
|
};
|
|
1038
855
|
/**
|
|
1039
|
-
* Runs a {@link Task} whose error type is `never` and returns its Ok
|
|
856
|
+
* Runs a {@link Task} whose error type is `never` and returns its {@link Ok}
|
|
857
|
+
* value.
|
|
1040
858
|
*
|
|
1041
859
|
* This is the Task equivalent of {@link getOk}.
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
860
|
+
*
|
|
861
|
+
* ### Example
|
|
862
|
+
*
|
|
863
|
+
* ```ts
|
|
864
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
865
|
+
*
|
|
866
|
+
* interface Resource extends AsyncDisposable {
|
|
867
|
+
* readonly value: string;
|
|
868
|
+
* }
|
|
869
|
+
*
|
|
870
|
+
* let disposed = false;
|
|
871
|
+
* const openResource: Task<Resource> = () =>
|
|
872
|
+
* ok({
|
|
873
|
+
* value: "resource",
|
|
874
|
+
* [Symbol.asyncDispose]: async () => {
|
|
875
|
+
* disposed = true;
|
|
876
|
+
* },
|
|
877
|
+
* });
|
|
878
|
+
*
|
|
879
|
+
* await using run = createRun();
|
|
880
|
+
* {
|
|
881
|
+
* await using resource = await run.ok(openResource);
|
|
882
|
+
* expect(resource.value).toBe("resource");
|
|
883
|
+
* }
|
|
884
|
+
* expect(disposed).toBe(true);
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
readonly ok: {
|
|
888
|
+
<T>(task: Task<T, never, D>): Promise<T>;
|
|
1045
889
|
<T, Deps extends object>(task: Task<T, never, Deps>, deps: RunCustomDeps<Deps>): Promise<T>;
|
|
1046
890
|
};
|
|
1047
891
|
/**
|
|
@@ -1073,8 +917,9 @@ export interface Run<D = unknown> {
|
|
|
1073
917
|
* interface DbDep {
|
|
1074
918
|
* readonly db: { readonly name: string };
|
|
1075
919
|
* }
|
|
920
|
+
*
|
|
1076
921
|
* const db = { name: "main" };
|
|
1077
|
-
* const loadUser: Task<string,
|
|
922
|
+
* const loadUser: Task<string, never, DbDep> = async (run) => {
|
|
1078
923
|
* await run.ok(sleep("1s"));
|
|
1079
924
|
* return ok(run.deps.db.name);
|
|
1080
925
|
* };
|
|
@@ -1082,7 +927,7 @@ export interface Run<D = unknown> {
|
|
|
1082
927
|
* await using run = createRun();
|
|
1083
928
|
* const fiber = run.abortable(loadUser, { db });
|
|
1084
929
|
* expectTypeOf(fiber).toEqualTypeOf<
|
|
1085
|
-
* AbortableFiber<string,
|
|
930
|
+
* AbortableFiber<string, never, DbDep>
|
|
1086
931
|
* >();
|
|
1087
932
|
* fiber.abort();
|
|
1088
933
|
* const userResult = await fiber;
|
|
@@ -1100,8 +945,8 @@ export interface Run<D = unknown> {
|
|
|
1100
945
|
* Normal child Runs are disposed after their Task settles. Tasks started by
|
|
1101
946
|
* `run.daemon` detach their lifetime from the current Task and attach to the
|
|
1102
947
|
* root Run, so they keep running until they settle or the root Run is
|
|
1103
|
-
* disposed. Calling `.abort()` or async-disposing the returned Fiber
|
|
1104
|
-
* abort. Keep the returned Fiber for lifetime control.
|
|
948
|
+
* disposed. Calling `.abort()` or async-disposing the returned {@link Fiber}
|
|
949
|
+
* requests abort. Keep the returned Fiber for lifetime control.
|
|
1105
950
|
*
|
|
1106
951
|
* The daemon receives deps derived from the Run that starts it, not from the
|
|
1107
952
|
* root Run: `deps` replace that Run's custom deps for the daemon Task, while
|
|
@@ -1118,7 +963,9 @@ export interface Run<D = unknown> {
|
|
|
1118
963
|
* un-aborted, because detached work must not spawn under a scope that is
|
|
1119
964
|
* shutting down.
|
|
1120
965
|
*
|
|
1121
|
-
*
|
|
966
|
+
* For a long-lived reusable {@link Run}, use {@link Run.create}.
|
|
967
|
+
*
|
|
968
|
+
* ### Abort masks
|
|
1122
969
|
*
|
|
1123
970
|
* ```ts
|
|
1124
971
|
* import { createRun, ok, unabortable, type Task } from "@evolu/common";
|
|
@@ -1146,9 +993,7 @@ export interface Run<D = unknown> {
|
|
|
1146
993
|
* expectOk(await run(syncParent), ["synced", "synced"]);
|
|
1147
994
|
* ```
|
|
1148
995
|
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* ### Example
|
|
996
|
+
* ### Aborting a daemon
|
|
1152
997
|
*
|
|
1153
998
|
* ```ts
|
|
1154
999
|
* import {
|
|
@@ -1162,6 +1007,7 @@ export interface Run<D = unknown> {
|
|
|
1162
1007
|
* interface DbDep {
|
|
1163
1008
|
* readonly db: { readonly name: string };
|
|
1164
1009
|
* }
|
|
1010
|
+
*
|
|
1165
1011
|
* const db = { name: "main" };
|
|
1166
1012
|
* const syncUsers: Task<void, never, DbDep> = async (run) => {
|
|
1167
1013
|
* await run.ok(sleep("1s"));
|
|
@@ -1176,6 +1022,8 @@ export interface Run<D = unknown> {
|
|
|
1176
1022
|
* expect(AbortError.is(syncResult.error)).toBe(true);
|
|
1177
1023
|
* ```
|
|
1178
1024
|
*
|
|
1025
|
+
* ### Disposing a daemon
|
|
1026
|
+
*
|
|
1179
1027
|
* ```ts
|
|
1180
1028
|
* import { createRun, ok, waitForAbort, type Task } from "@evolu/common";
|
|
1181
1029
|
*
|
|
@@ -1202,11 +1050,11 @@ export interface Run<D = unknown> {
|
|
|
1202
1050
|
<T, E, Deps extends object>(task: Task<T, E, Deps>, deps: RunCustomDeps<Deps>): AbortableFiber<T, E, Deps>;
|
|
1203
1051
|
};
|
|
1204
1052
|
/**
|
|
1205
|
-
* Creates a {@link DisposableRun} attached to the root Run with this
|
|
1206
|
-
* deps.
|
|
1053
|
+
* Creates a {@link DisposableRun} attached to the root {@link Run} with this
|
|
1054
|
+
* Run's deps.
|
|
1207
1055
|
*
|
|
1208
1056
|
* Use it when you need a Run that can be reused across multiple operations.
|
|
1209
|
-
* For a single long-lived Task, use {@link Run.daemon}.
|
|
1057
|
+
* For a single long-lived {@link Task}, use {@link Run.daemon}.
|
|
1210
1058
|
*
|
|
1211
1059
|
* Use deps to replace the created Run's custom deps. Default deps
|
|
1212
1060
|
* ({@link RunDefaultDeps}) are inherited unless explicitly replaced with
|
|
@@ -1225,11 +1073,12 @@ export interface Run<D = unknown> {
|
|
|
1225
1073
|
* interface DbDep {
|
|
1226
1074
|
* readonly db: { readonly users: Array<string> };
|
|
1227
1075
|
* }
|
|
1076
|
+
*
|
|
1228
1077
|
* const db = { users: ["Ada"] };
|
|
1229
|
-
* const loadUser: Task<string, never, DbDep> = (
|
|
1230
|
-
* ok(deps.db.users[0] ?? "Unknown");
|
|
1231
|
-
* const saveUser: Task<void, never, DbDep> = (
|
|
1232
|
-
* deps.db.users.push("Grace");
|
|
1078
|
+
* const loadUser: Task<string, never, DbDep> = (run) =>
|
|
1079
|
+
* ok(run.deps.db.users[0] ?? "Unknown");
|
|
1080
|
+
* const saveUser: Task<void, never, DbDep> = (run) => {
|
|
1081
|
+
* run.deps.db.users.push("Grace");
|
|
1233
1082
|
* return ok();
|
|
1234
1083
|
* };
|
|
1235
1084
|
*
|
|
@@ -1250,7 +1099,7 @@ export interface Run<D = unknown> {
|
|
|
1250
1099
|
readonly id: Id;
|
|
1251
1100
|
/** The parent {@link Run}, if this Run was created as a child. */
|
|
1252
1101
|
readonly parent: Run | null;
|
|
1253
|
-
/** Dependencies available to the Task, including {@link RunDefaultDeps}. */
|
|
1102
|
+
/** Dependencies available to the {@link Task}, including {@link RunDefaultDeps}. */
|
|
1254
1103
|
readonly deps: RunDefaultDeps & D;
|
|
1255
1104
|
/**
|
|
1256
1105
|
* Abort signal for the {@link Task}.
|
|
@@ -1326,7 +1175,7 @@ export interface Run<D = unknown> {
|
|
|
1326
1175
|
/**
|
|
1327
1176
|
* Callback for monitoring Run events emitted by this Run or descendants.
|
|
1328
1177
|
*
|
|
1329
|
-
* Event handlers are observers, not part of Task control flow. Handler
|
|
1178
|
+
* Event handlers are observers, not part of {@link Task} control flow. Handler
|
|
1330
1179
|
* defects are reported via {@link ReportDefectDep.reportDefect}; they do not
|
|
1331
1180
|
* panic the root Run or change Run state.
|
|
1332
1181
|
*
|
|
@@ -1351,52 +1200,21 @@ export type RunCustomDeps<D extends object> = D & {
|
|
|
1351
1200
|
*
|
|
1352
1201
|
* {@link createRun} creates a root DisposableRun. {@link Run.create} creates one
|
|
1353
1202
|
* attached to that root, typically to give a reusable resource its own
|
|
1354
|
-
* lifetime. A DisposableRun owns its child
|
|
1355
|
-
* registered with {@link DisposableRun.defer}; disposing it shuts down
|
|
1203
|
+
* lifetime. A DisposableRun owns its child {@link Task}s and closure-held
|
|
1204
|
+
* cleanup registered with {@link DisposableRun.defer}; disposing it shuts down
|
|
1205
|
+
* both.
|
|
1356
1206
|
*
|
|
1357
1207
|
* Sync disposal starts shutdown without waiting. Async disposal waits for child
|
|
1358
1208
|
* Tasks and registered cleanup to finish.
|
|
1359
1209
|
*
|
|
1360
|
-
* Use {@link createRun} at composition roots such as app, server, worker, or
|
|
1361
|
-
* test entry points. The common factory is platform-agnostic; platform adapters
|
|
1362
|
-
* can wrap it to add global error handling or shutdown integration.
|
|
1363
|
-
*
|
|
1364
|
-
* ### Example
|
|
1365
|
-
*
|
|
1366
|
-
* ```ts
|
|
1367
|
-
* import { createRun, ok, type Task } from "@evolu/common";
|
|
1368
|
-
*
|
|
1369
|
-
* await using run = createRun();
|
|
1370
|
-
* const loadData: Task<string> = () => ok("data");
|
|
1371
|
-
*
|
|
1372
|
-
* expectOk(await run(loadData), "data");
|
|
1373
|
-
* ```
|
|
1374
|
-
*
|
|
1375
|
-
* ### Example with custom dependencies
|
|
1376
|
-
*
|
|
1377
|
-
* ```ts
|
|
1378
|
-
* import { createRun, type DisposableRun } from "@evolu/common";
|
|
1379
|
-
*
|
|
1380
|
-
* interface ConfigDep {
|
|
1381
|
-
* readonly config: { readonly apiUrl: string };
|
|
1382
|
-
* }
|
|
1383
|
-
*
|
|
1384
|
-
* await using run = createRun<ConfigDep>({
|
|
1385
|
-
* config: { apiUrl: "https://api.example.com" },
|
|
1386
|
-
* });
|
|
1387
|
-
*
|
|
1388
|
-
* expectTypeOf(run).toEqualTypeOf<DisposableRun<ConfigDep>>();
|
|
1389
|
-
* expect(run.deps.config.apiUrl).toBe("https://api.example.com");
|
|
1390
|
-
* ```
|
|
1391
|
-
*
|
|
1392
1210
|
* @group Core
|
|
1393
1211
|
*/
|
|
1394
1212
|
export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDisposable {
|
|
1395
1213
|
/**
|
|
1396
1214
|
* Registers closure-held cleanup owned by this Run.
|
|
1397
1215
|
*
|
|
1398
|
-
* Finalizers run in LIFO order after child
|
|
1399
|
-
* async disposal. The Run is in `Aborted` state while they run and
|
|
1216
|
+
* Finalizers run in LIFO order after child {@link Task}s settle and are
|
|
1217
|
+
* awaited by async disposal. The Run is in `Aborted` state while they run and
|
|
1400
1218
|
* transitions to `Settled` afterward, so a finalizer cannot start Tasks on
|
|
1401
1219
|
* it. Use `using` for resources owned by a Task stack frame; use `defer` for
|
|
1402
1220
|
* closure-held state whose lifetime is bounded by a reusable DisposableRun.
|
|
@@ -1407,6 +1225,23 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
|
|
|
1407
1225
|
* with the same already-reported {@link AbortError}.
|
|
1408
1226
|
*
|
|
1409
1227
|
* Calling `defer` after disposal starts is a programmer error.
|
|
1228
|
+
*
|
|
1229
|
+
* ### Example
|
|
1230
|
+
*
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* import { createRun } from "@evolu/common";
|
|
1233
|
+
*
|
|
1234
|
+
* let connectionClosed = false;
|
|
1235
|
+
* {
|
|
1236
|
+
* await using run = createRun();
|
|
1237
|
+
* run.defer(() => {
|
|
1238
|
+
* connectionClosed = true;
|
|
1239
|
+
* });
|
|
1240
|
+
*
|
|
1241
|
+
* expect(connectionClosed).toBe(false);
|
|
1242
|
+
* }
|
|
1243
|
+
* expect(connectionClosed).toBe(true);
|
|
1244
|
+
* ```
|
|
1410
1245
|
*/
|
|
1411
1246
|
readonly defer: (finalizer: () => Awaitable<void>) => void;
|
|
1412
1247
|
/**
|
|
@@ -1422,8 +1257,8 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
|
|
|
1422
1257
|
* {@link ReportDefectDep}. The original defect is available as
|
|
1423
1258
|
* `abortError.reason.defect` for diagnostics. The first panic records the
|
|
1424
1259
|
* AbortError as the root Run's aborted exit and starts root disposal, which
|
|
1425
|
-
* aborts running
|
|
1426
|
-
* running Tasks to settle. Later panics still report and return their own
|
|
1260
|
+
* aborts running {@link Task}s, prevents new Tasks from starting, and waits
|
|
1261
|
+
* for running Tasks to settle. Later panics still report and return their own
|
|
1427
1262
|
* AbortError, but do not replace the root Run exit.
|
|
1428
1263
|
*/
|
|
1429
1264
|
readonly panic: (defect: unknown) => AbortError;
|
|
@@ -1431,8 +1266,8 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
|
|
|
1431
1266
|
/**
|
|
1432
1267
|
* A Promise-backed handle to a {@link Task} started by a {@link Run}.
|
|
1433
1268
|
*
|
|
1434
|
-
* Await a Fiber to use the Task Result in the current control flow. The
|
|
1435
|
-
* resolves with the Task
|
|
1269
|
+
* Await a Fiber to use the Task {@link Result} in the current control flow. The
|
|
1270
|
+
* Fiber resolves with the Task Result when the Task returns normally. A Fiber
|
|
1436
1271
|
* returned by `run(task)` rejects with {@link AbortError} when the Task observes
|
|
1437
1272
|
* abort or when a defect panics the Run tree. Panic uses
|
|
1438
1273
|
* {@link PanicAbortReason}; the original defect is available on the reason for
|
|
@@ -1487,6 +1322,7 @@ export interface DisposableRun<D = unknown> extends Run<D>, Disposable, AsyncDis
|
|
|
1487
1322
|
* @group Core
|
|
1488
1323
|
*/
|
|
1489
1324
|
export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<Result<T, E>> {
|
|
1325
|
+
/** The child {@link Run} that executes this Fiber's {@link Task}. */
|
|
1490
1326
|
readonly run: Run<D>;
|
|
1491
1327
|
}
|
|
1492
1328
|
/**
|
|
@@ -1496,13 +1332,13 @@ export interface Fiber<T = unknown, E = unknown, D = unknown> extends Promise<Re
|
|
|
1496
1332
|
*/
|
|
1497
1333
|
export type AnyFiber = Fiber<any, any, any>;
|
|
1498
1334
|
/**
|
|
1499
|
-
* Extracts the Ok value type from a {@link Fiber}.
|
|
1335
|
+
* Extracts the {@link Ok} value type from a {@link Fiber}.
|
|
1500
1336
|
*
|
|
1501
1337
|
* @group Type utilities
|
|
1502
1338
|
*/
|
|
1503
1339
|
export type InferFiberOk<TFiber extends AnyFiber> = TFiber extends Fiber<infer T, any, any> ? T : never;
|
|
1504
1340
|
/**
|
|
1505
|
-
* Extracts the Result error type from a {@link Fiber}.
|
|
1341
|
+
* Extracts the {@link Result} error type from a {@link Fiber}.
|
|
1506
1342
|
*
|
|
1507
1343
|
* @group Type utilities
|
|
1508
1344
|
*/
|
|
@@ -1517,8 +1353,8 @@ export type InferFiberDeps<TFiber extends AnyFiber> = TFiber extends Fiber<any,
|
|
|
1517
1353
|
* A {@link Fiber} with explicit abort and async-disposal controls.
|
|
1518
1354
|
*
|
|
1519
1355
|
* Calling `.abort()` requests abort for the Fiber's child {@link Run}. If the
|
|
1520
|
-
* Task observes abort or a defect panics the Run tree, the Fiber
|
|
1521
|
-
* an {@link Err} containing the {@link AbortError}. Panic uses
|
|
1356
|
+
* {@link Task} observes abort or a defect panics the Run tree, the Fiber
|
|
1357
|
+
* resolves with an {@link Err} containing the {@link AbortError}. Panic uses
|
|
1522
1358
|
* {@link PanicAbortReason}; the original defect is available on the reason for
|
|
1523
1359
|
* diagnostics.
|
|
1524
1360
|
*
|
|
@@ -1556,6 +1392,7 @@ export type InferFiberDeps<TFiber extends AnyFiber> = TFiber extends Fiber<any,
|
|
|
1556
1392
|
* @group Core
|
|
1557
1393
|
*/
|
|
1558
1394
|
export interface AbortableFiber<T = unknown, E = unknown, D = unknown> extends Fiber<T, E | AbortError, D>, AsyncDisposable {
|
|
1395
|
+
/** Requests abort with an optional {@link AbortReason}. */
|
|
1559
1396
|
readonly abort: (reason?: AbortReason) => void;
|
|
1560
1397
|
}
|
|
1561
1398
|
/**
|
|
@@ -1566,15 +1403,15 @@ export interface AbortableFiber<T = unknown, E = unknown, D = unknown> extends F
|
|
|
1566
1403
|
* scopes. Abort requests are still recorded, but the Run's observed abort
|
|
1567
1404
|
* signal is aborted only when the mask is `0`.
|
|
1568
1405
|
*
|
|
1569
|
-
* Plain child
|
|
1570
|
-
* mask for the wrapped Task, and `unabortableMask` provides
|
|
1571
|
-
* selected child Tasks with the previous mask.
|
|
1406
|
+
* Plain child {@link Task}s inherit their parent's mask. `unabortable`
|
|
1407
|
+
* increments the mask for the wrapped Task, and `unabortableMask` provides
|
|
1408
|
+
* `restore` to run selected child Tasks with the previous mask.
|
|
1572
1409
|
*
|
|
1573
1410
|
* @group Abortability
|
|
1574
1411
|
*/
|
|
1575
1412
|
export type AbortMask = NonNegativeInt & Brand<"AbortMask">;
|
|
1576
1413
|
/**
|
|
1577
|
-
*
|
|
1414
|
+
* Runtime Type for structured data explaining why a {@link Run} was aborted.
|
|
1578
1415
|
*
|
|
1579
1416
|
* A reason has a `type` discriminant and optional structured data, so abort
|
|
1580
1417
|
* causes can carry typed domain data. Well-known reasons are
|
|
@@ -1586,14 +1423,19 @@ export type AbortMask = NonNegativeInt & Brand<"AbortMask">;
|
|
|
1586
1423
|
export declare const AbortReason: ObjectType<{
|
|
1587
1424
|
readonly type: typeof String;
|
|
1588
1425
|
}, RecordType<typeof String, typeof Unknown>>;
|
|
1426
|
+
/**
|
|
1427
|
+
* Structured data explaining why a {@link Run} was aborted.
|
|
1428
|
+
*
|
|
1429
|
+
* @group Core
|
|
1430
|
+
*/
|
|
1589
1431
|
export interface AbortReason extends InferType<typeof AbortReason> {
|
|
1590
1432
|
}
|
|
1591
1433
|
/**
|
|
1592
|
-
*
|
|
1434
|
+
* Runtime Type for structured-concurrency abort control flow.
|
|
1593
1435
|
*
|
|
1594
|
-
* AbortError is thrown to stop Task execution when a Run observes
|
|
1595
|
-
* request. AbortableFiber catches AbortError and returns it as
|
|
1596
|
-
* error, so abort can be handled as an ordinary Task outcome.
|
|
1436
|
+
* AbortError is thrown to stop {@link Task} execution when a {@link Run} observes
|
|
1437
|
+
* an abort request. {@link AbortableFiber} catches AbortError and returns it as
|
|
1438
|
+
* a {@link Result} error, so abort can be handled as an ordinary Task outcome.
|
|
1597
1439
|
*
|
|
1598
1440
|
* The reason explains why the Run was aborted. It can be an explicit abort
|
|
1599
1441
|
* reason, {@link runDisposedAbortReason} for normal Run cleanup, or
|
|
@@ -1615,6 +1457,11 @@ export interface AbortReason extends InferType<typeof AbortReason> {
|
|
|
1615
1457
|
export declare const AbortError: TypedType<"AbortError", {
|
|
1616
1458
|
readonly reason: typeof AbortReason;
|
|
1617
1459
|
}>;
|
|
1460
|
+
/**
|
|
1461
|
+
* Structured-concurrency abort control-flow value.
|
|
1462
|
+
*
|
|
1463
|
+
* @group Core
|
|
1464
|
+
*/
|
|
1618
1465
|
export interface AbortError extends InferType<typeof AbortError> {
|
|
1619
1466
|
}
|
|
1620
1467
|
/**
|
|
@@ -1626,9 +1473,9 @@ export declare const createAbortError: (reason: AbortReason) => AbortError;
|
|
|
1626
1473
|
/**
|
|
1627
1474
|
* Final outcome recorded by a {@link Run}.
|
|
1628
1475
|
*
|
|
1629
|
-
* A Run exit is an outer {@link Result}. {@link Ok} means the Task
|
|
1630
|
-
* Result; {@link Err} means the Run aborted with {@link AbortError}.
|
|
1631
|
-
* recorded as an AbortError whose reason is {@link PanicAbortReason}.
|
|
1476
|
+
* A Run exit is an outer {@link Result}. {@link Ok} means the {@link Task}
|
|
1477
|
+
* returned a Result; {@link Err} means the Run aborted with {@link AbortError}.
|
|
1478
|
+
* Panic is recorded as an AbortError whose reason is {@link PanicAbortReason}.
|
|
1632
1479
|
*
|
|
1633
1480
|
* @group Core
|
|
1634
1481
|
*/
|
|
@@ -1688,7 +1535,7 @@ export interface RunStateSettled extends Typed<"Settled">, RunAbortState {
|
|
|
1688
1535
|
* same object reference. This lets UI and debugging tools compare snapshots by
|
|
1689
1536
|
* identity and skip unchanged branches.
|
|
1690
1537
|
*
|
|
1691
|
-
* @group
|
|
1538
|
+
* @group Monitoring
|
|
1692
1539
|
* @see {@link Run.snapshot}
|
|
1693
1540
|
*/
|
|
1694
1541
|
export interface RunSnapshot {
|
|
@@ -1708,7 +1555,7 @@ export interface RunSnapshot {
|
|
|
1708
1555
|
*/
|
|
1709
1556
|
export type RunEventData = RunEventDataChildAdded | RunEventDataChildRemoved | RunEventDataStateChanged;
|
|
1710
1557
|
/**
|
|
1711
|
-
* A child Run was added to the emitting Run.
|
|
1558
|
+
* A child {@link Run} was added to the emitting Run.
|
|
1712
1559
|
*
|
|
1713
1560
|
* @group Monitoring
|
|
1714
1561
|
*/
|
|
@@ -1717,7 +1564,7 @@ export interface RunEventDataChildAdded extends Typed<"ChildAdded"> {
|
|
|
1717
1564
|
readonly childId: Id;
|
|
1718
1565
|
}
|
|
1719
1566
|
/**
|
|
1720
|
-
* A child Run was removed from the emitting Run.
|
|
1567
|
+
* A child {@link Run} was removed from the emitting Run.
|
|
1721
1568
|
*
|
|
1722
1569
|
* @group Monitoring
|
|
1723
1570
|
*/
|
|
@@ -1753,9 +1600,9 @@ export interface RunEvent {
|
|
|
1753
1600
|
/**
|
|
1754
1601
|
* Shared abort reason used for ordinary {@link Run} cleanup.
|
|
1755
1602
|
*
|
|
1756
|
-
* Disposal requests abort so child
|
|
1757
|
-
* settle. This reason distinguishes that cleanup path from explicit
|
|
1758
|
-
* {@link PanicAbortReason}.
|
|
1603
|
+
* Disposal requests abort so child {@link Task}s stop while the Run waits for
|
|
1604
|
+
* them to settle. This reason distinguishes that cleanup path from explicit
|
|
1605
|
+
* abort and {@link PanicAbortReason}.
|
|
1759
1606
|
*
|
|
1760
1607
|
* @group Run
|
|
1761
1608
|
*/
|
|
@@ -1774,7 +1621,7 @@ export declare const explicitAbortReason: {
|
|
|
1774
1621
|
/**
|
|
1775
1622
|
* Shared abort reason for tests that need a non-production abort reason.
|
|
1776
1623
|
*
|
|
1777
|
-
* @group
|
|
1624
|
+
* @group Testing
|
|
1778
1625
|
*/
|
|
1779
1626
|
export declare const testAbortReason: {
|
|
1780
1627
|
readonly type: "TestAbortReason";
|
|
@@ -1782,25 +1629,24 @@ export declare const testAbortReason: {
|
|
|
1782
1629
|
/**
|
|
1783
1630
|
* Shared {@link AbortError} for tests, created from {@link testAbortReason}.
|
|
1784
1631
|
*
|
|
1785
|
-
* @group
|
|
1632
|
+
* @group Testing
|
|
1786
1633
|
*/
|
|
1787
1634
|
export declare const testAbortError: AbortError;
|
|
1788
1635
|
/**
|
|
1789
|
-
*
|
|
1636
|
+
* Abort reason recorded when a defect panics the root {@link Run}.
|
|
1790
1637
|
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
* flow, not a defect.
|
|
1638
|
+
* A defect is a thrown or rejected value other than {@link AbortError}.
|
|
1639
|
+
* Recoverable domain errors belong in {@link Result}. Bugs and unrecoverable
|
|
1640
|
+
* failures, such as storage engine errors the {@link Task} cannot usefully
|
|
1641
|
+
* handle, may throw or reject.
|
|
1796
1642
|
*
|
|
1797
1643
|
* {@link Run.onEvent} handler defects are different: event handlers are
|
|
1798
1644
|
* monitoring code, so their defects are reported globally but do not panic the
|
|
1799
1645
|
* root Run.
|
|
1800
1646
|
*
|
|
1801
|
-
* When Run observes a defect, it aborts the root Run and starts disposal
|
|
1647
|
+
* When {@link Run} observes a defect, it aborts the root Run and starts disposal
|
|
1802
1648
|
* immediately. This prevents later Tasks from starting after the defect. A
|
|
1803
|
-
* Fiber rejects with AbortError whose reason is PanicAbortReason; an
|
|
1649
|
+
* {@link Fiber} rejects with AbortError whose reason is PanicAbortReason; an
|
|
1804
1650
|
* {@link AbortableFiber} returns that AbortError as an {@link Err}.
|
|
1805
1651
|
*
|
|
1806
1652
|
* @group Core
|
|
@@ -1841,10 +1687,10 @@ export interface RunConfigDep {
|
|
|
1841
1687
|
/**
|
|
1842
1688
|
* Reports a defect.
|
|
1843
1689
|
*
|
|
1844
|
-
* Run uses this dependency in two cases: {@link DisposableRun.panic}
|
|
1845
|
-
* {@link AbortError} whose reason is {@link PanicAbortReason}, and
|
|
1846
|
-
* monitoring reports observer defects without panicking the Run. The
|
|
1847
|
-
* panic defect is available at `abortError.reason.defect`.
|
|
1690
|
+
* {@link Run} uses this dependency in two cases: {@link DisposableRun.panic}
|
|
1691
|
+
* reports the {@link AbortError} whose reason is {@link PanicAbortReason}, and
|
|
1692
|
+
* event monitoring reports observer defects without panicking the Run. The
|
|
1693
|
+
* original panic defect is available at `abortError.reason.defect`.
|
|
1848
1694
|
*
|
|
1849
1695
|
* @group Run
|
|
1850
1696
|
*/
|
|
@@ -1873,9 +1719,9 @@ export declare const reportDefectAfterMicrotask: ReportDefect;
|
|
|
1873
1719
|
/**
|
|
1874
1720
|
* Default dependencies provided by {@link createRun}.
|
|
1875
1721
|
*
|
|
1876
|
-
* Root
|
|
1877
|
-
* detection, native fetch, randomness, error reporting, time, and optional
|
|
1878
|
-
* monitoring configuration.
|
|
1722
|
+
* Root {@link Run}s include platform-independent implementations for console,
|
|
1723
|
+
* leak detection, native fetch, randomness, error reporting, time, and optional
|
|
1724
|
+
* Run monitoring configuration.
|
|
1879
1725
|
*
|
|
1880
1726
|
* The {@link LeakDetector} is enabled only in development builds; production
|
|
1881
1727
|
* uses a no-op implementation.
|
|
@@ -1895,14 +1741,39 @@ export declare const createRunDefaultDeps: () => RunDefaultDeps;
|
|
|
1895
1741
|
* @group Run
|
|
1896
1742
|
*/
|
|
1897
1743
|
export interface CreateRun {
|
|
1898
|
-
/** Creates a root Run with only {@link RunDefaultDeps}. */
|
|
1744
|
+
/** Creates a root {@link Run} with only {@link RunDefaultDeps}. */
|
|
1899
1745
|
(): DisposableRun;
|
|
1900
|
-
/**
|
|
1746
|
+
/**
|
|
1747
|
+
* Creates a root {@link Run} with custom deps merged over
|
|
1748
|
+
* {@link RunDefaultDeps}.
|
|
1749
|
+
*/
|
|
1901
1750
|
<D extends object>(deps: RunCustomDeps<D>): DisposableRun<D>;
|
|
1902
1751
|
}
|
|
1903
1752
|
/**
|
|
1904
1753
|
* Creates a root {@link DisposableRun}.
|
|
1905
1754
|
*
|
|
1755
|
+
* Use at composition roots such as app, server, worker, or test entry points.
|
|
1756
|
+
* The common factory is platform-agnostic; platform adapters can wrap it to add
|
|
1757
|
+
* global error handling or shutdown integration.
|
|
1758
|
+
*
|
|
1759
|
+
* ### Example
|
|
1760
|
+
*
|
|
1761
|
+
* ```ts
|
|
1762
|
+
* import { createRun, ok, type Task } from "@evolu/common";
|
|
1763
|
+
*
|
|
1764
|
+
* interface ConfigDep {
|
|
1765
|
+
* readonly config: { readonly apiUrl: string };
|
|
1766
|
+
* }
|
|
1767
|
+
*
|
|
1768
|
+
* const loadApiUrl: Task<string, never, ConfigDep> = (run) =>
|
|
1769
|
+
* ok(run.deps.config.apiUrl);
|
|
1770
|
+
*
|
|
1771
|
+
* await using run = createRun({
|
|
1772
|
+
* config: { apiUrl: "https://api.example.com" },
|
|
1773
|
+
* });
|
|
1774
|
+
* expectOk(await run(loadApiUrl), "https://api.example.com");
|
|
1775
|
+
* ```
|
|
1776
|
+
*
|
|
1906
1777
|
* @group Run
|
|
1907
1778
|
*/
|
|
1908
1779
|
export declare const createRun: CreateRun;
|
|
@@ -1926,7 +1797,11 @@ export declare const createRun: CreateRun;
|
|
|
1926
1797
|
* @group Testing
|
|
1927
1798
|
*/
|
|
1928
1799
|
export type TestRunDefaultDeps = Omit<RunDefaultDeps, "console" | "leakDetector" | "reportDefect" | "time"> & TestConsoleDep & TestLeakDetectorDep & TestReportDefectDep & TestTimeDep & RandomLibDep;
|
|
1929
|
-
/**
|
|
1800
|
+
/**
|
|
1801
|
+
* Provides a test {@link Run} with deterministic default dependencies.
|
|
1802
|
+
*
|
|
1803
|
+
* @group Testing
|
|
1804
|
+
*/
|
|
1930
1805
|
export interface TestRunDep<D = unknown> {
|
|
1931
1806
|
readonly run: Run<TestRunDefaultDeps & D>;
|
|
1932
1807
|
}
|
|
@@ -1974,9 +1849,39 @@ export declare const testCreateDeps: (options?: {
|
|
|
1974
1849
|
/**
|
|
1975
1850
|
* Creates a root {@link DisposableRun} with {@link TestRunDefaultDeps}.
|
|
1976
1851
|
*
|
|
1852
|
+
* ### Example
|
|
1853
|
+
*
|
|
1854
|
+
* ```ts
|
|
1855
|
+
* import { ok, testCreateRun, type Task } from "@evolu/common";
|
|
1856
|
+
*
|
|
1857
|
+
* const readTime: Task<number> = (run) => ok(run.deps.time.now());
|
|
1858
|
+
*
|
|
1859
|
+
* await using run = testCreateRun();
|
|
1860
|
+
* expectOk(await run(readTime), 0);
|
|
1861
|
+
* ```
|
|
1862
|
+
*
|
|
1977
1863
|
* @group Testing
|
|
1978
1864
|
*/
|
|
1979
1865
|
export declare function testCreateRun(deps?: TestRunDefaultDeps): DisposableRun<TestRunDefaultDeps>;
|
|
1866
|
+
/**
|
|
1867
|
+
* Merges custom dependencies into {@link TestRunDefaultDeps}.
|
|
1868
|
+
*
|
|
1869
|
+
* ### Example
|
|
1870
|
+
*
|
|
1871
|
+
* ```ts
|
|
1872
|
+
* import { ok, testCreateRun, type Task } from "@evolu/common";
|
|
1873
|
+
*
|
|
1874
|
+
* interface FeatureDep {
|
|
1875
|
+
* readonly feature: { readonly enabled: boolean };
|
|
1876
|
+
* }
|
|
1877
|
+
*
|
|
1878
|
+
* const isFeatureEnabled: Task<boolean, never, FeatureDep> = (run) =>
|
|
1879
|
+
* ok(run.deps.feature.enabled);
|
|
1880
|
+
*
|
|
1881
|
+
* await using run = testCreateRun({ feature: { enabled: true } });
|
|
1882
|
+
* expectOk(await run(isFeatureEnabled), true);
|
|
1883
|
+
* ```
|
|
1884
|
+
*/
|
|
1980
1885
|
export declare function testCreateRun<D extends object>(deps: RunCustomDeps<D>): DisposableRun<TestRunDefaultDeps & D>;
|
|
1981
1886
|
/**
|
|
1982
1887
|
* A readonly record whose values are {@link Task}s.
|
|
@@ -1985,24 +1890,26 @@ export declare function testCreateRun<D extends object>(deps: RunCustomDeps<D>):
|
|
|
1985
1890
|
*/
|
|
1986
1891
|
export type TaskRecord = Readonly<Record<string, AnyTask>>;
|
|
1987
1892
|
/**
|
|
1988
|
-
* Extracts the dependency intersection required by a readonly Task
|
|
1893
|
+
* Extracts the dependency intersection required by a readonly {@link Task}
|
|
1894
|
+
* array.
|
|
1989
1895
|
*
|
|
1990
1896
|
* @group Type utilities
|
|
1991
1897
|
*/
|
|
1992
1898
|
export type InferTasksDeps<TTasks extends ReadonlyArray<AnyTask>> = ParameterIntersection<TTasks[number] extends infer TTask ? TTask extends AnyTask ? (deps: InferTaskDeps<TTask>) => void : never : never>;
|
|
1993
1899
|
/**
|
|
1994
|
-
* Extracts the dependency intersection required by a Task record.
|
|
1900
|
+
* Extracts the dependency intersection required by a {@link Task} record.
|
|
1995
1901
|
*
|
|
1996
1902
|
* @group Type utilities
|
|
1997
1903
|
*/
|
|
1998
1904
|
export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<ReadonlyArray<TTasks[keyof TTasks]>>;
|
|
1999
1905
|
/**
|
|
2000
|
-
* Options shared by Task collection helpers.
|
|
1906
|
+
* Options shared by {@link Task} collection helpers.
|
|
2001
1907
|
*
|
|
2002
|
-
* `concurrency` controls how many Tasks run at once. It defaults to `1`.
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
1908
|
+
* `concurrency` controls how many Tasks run at once. It defaults to `1`. For
|
|
1909
|
+
* CPU-bound Tasks backed by workers or parallel native operations, a platform
|
|
1910
|
+
* `availableParallelism()` result is often a good limit. For network or
|
|
1911
|
+
* database Tasks, choose a limit based on the transport, server, connection
|
|
1912
|
+
* pool, and rate limits.
|
|
2006
1913
|
*
|
|
2007
1914
|
* Keep concurrency bounded. In rare cases where running every Task concurrently
|
|
2008
1915
|
* is safe, use {@link maxPositiveInt}.
|
|
@@ -2010,6 +1917,7 @@ export type InferTaskRecordDeps<TTasks extends TaskRecord> = InferTasksDeps<Read
|
|
|
2010
1917
|
* @group Collection
|
|
2011
1918
|
*/
|
|
2012
1919
|
export interface TaskCollectionOptions {
|
|
1920
|
+
/** Maximum number of {@link Task}s run concurrently. Defaults to `1`. */
|
|
2013
1921
|
readonly concurrency?: Int1To100OrPositiveInt;
|
|
2014
1922
|
}
|
|
2015
1923
|
/**
|
|
@@ -2018,11 +1926,12 @@ export interface TaskCollectionOptions {
|
|
|
2018
1926
|
* @group Collection
|
|
2019
1927
|
*/
|
|
2020
1928
|
export interface AllOptions extends TaskCollectionOptions {
|
|
2021
|
-
/** Disables collecting Ok values. */
|
|
1929
|
+
/** Disables collecting {@link Ok} values. */
|
|
2022
1930
|
readonly collect: false;
|
|
2023
1931
|
}
|
|
2024
1932
|
/**
|
|
2025
|
-
* Maps a Task array or record to the Ok values produced by its
|
|
1933
|
+
* Maps a {@link Task} array or record to the {@link Ok} values produced by its
|
|
1934
|
+
* Tasks.
|
|
2026
1935
|
*
|
|
2027
1936
|
* The mapped type is homomorphic, so tuples preserve their shape and records
|
|
2028
1937
|
* preserve their keys.
|
|
@@ -2033,7 +1942,7 @@ export type InferTasksOk<TTasks> = {
|
|
|
2033
1942
|
readonly [K in keyof TTasks]: TTasks[K] extends AnyTask ? InferTaskOk<TTasks[K]> : never;
|
|
2034
1943
|
};
|
|
2035
1944
|
/**
|
|
2036
|
-
* Runs
|
|
1945
|
+
* Runs {@link Task}s until all return {@link Ok} or one returns {@link Err}.
|
|
2037
1946
|
*
|
|
2038
1947
|
* Returns Ok with all values when every Task returns Ok. Stops on the first
|
|
2039
1948
|
* Err; remaining running Tasks are aborted. Sequential by default; pass a
|
|
@@ -2051,8 +1960,8 @@ export type InferTasksOk<TTasks> = {
|
|
|
2051
1960
|
*
|
|
2052
1961
|
* Similar to
|
|
2053
1962
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all | Promise.all},
|
|
2054
|
-
* but runs Tasks, returns Result values, and aborts remaining Tasks on
|
|
2055
|
-
* first Err.
|
|
1963
|
+
* but runs Tasks, returns {@link Result} values, and aborts remaining Tasks on
|
|
1964
|
+
* the first Err.
|
|
2056
1965
|
*
|
|
2057
1966
|
* ### Example
|
|
2058
1967
|
*
|
|
@@ -2064,48 +1973,35 @@ export type InferTasksOk<TTasks> = {
|
|
|
2064
1973
|
* ok,
|
|
2065
1974
|
* type Result,
|
|
2066
1975
|
* type Task,
|
|
1976
|
+
* type Typed,
|
|
2067
1977
|
* } from "@evolu/common";
|
|
2068
1978
|
*
|
|
2069
|
-
* interface User {
|
|
2070
|
-
* readonly id: string;
|
|
2071
|
-
* }
|
|
2072
|
-
* interface Post {
|
|
2073
|
-
* readonly id: string;
|
|
2074
|
-
* }
|
|
2075
|
-
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
2076
|
-
* const fetchPosts: Task<ReadonlyArray<Post>> = () =>
|
|
2077
|
-
* ok([{ id: "post-1" }]);
|
|
2078
|
-
* await using run = createRun();
|
|
2079
|
-
*
|
|
2080
|
-
* const dashboard = await run(all([fetchUser, fetchPosts]));
|
|
2081
|
-
* expectTypeOf(dashboard).toEqualTypeOf<
|
|
2082
|
-
* Result<readonly [User, ReadonlyArray<Post>]>
|
|
2083
|
-
* >();
|
|
2084
|
-
* expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
|
|
2085
|
-
*
|
|
2086
|
-
* // Skip collecting Ok values when they aren't needed.
|
|
2087
|
-
* interface SaveUserError {
|
|
2088
|
-
* readonly type: "SaveUserError";
|
|
2089
|
-
* readonly userId: string;
|
|
2090
|
-
* }
|
|
2091
1979
|
* const savedUserIds: Array<string> = [];
|
|
2092
1980
|
* const saveUser =
|
|
2093
|
-
* (id: string): Task<number,
|
|
1981
|
+
* (id: string): Task<number, SaveUserFailedError> =>
|
|
2094
1982
|
* () => {
|
|
2095
1983
|
* if (id === "missing") {
|
|
2096
|
-
* return err({ type: "
|
|
1984
|
+
* return err({ type: "SaveUserFailed", userId: id });
|
|
2097
1985
|
* }
|
|
2098
1986
|
* savedUserIds.push(id);
|
|
2099
1987
|
* return ok(1);
|
|
2100
1988
|
* };
|
|
1989
|
+
*
|
|
1990
|
+
* interface SaveUserFailedError extends Typed<"SaveUserFailed"> {
|
|
1991
|
+
* readonly userId: string;
|
|
1992
|
+
* }
|
|
1993
|
+
*
|
|
1994
|
+
* await using run = createRun();
|
|
2101
1995
|
* const saveResult = await run(
|
|
2102
|
-
* all(["user-1", "missing", "user-3"],
|
|
1996
|
+
* all([saveUser("user-1"), saveUser("missing"), saveUser("user-3")], {
|
|
2103
1997
|
* collect: false,
|
|
2104
1998
|
* }),
|
|
2105
1999
|
* );
|
|
2106
|
-
* expectTypeOf(saveResult).toEqualTypeOf<
|
|
2000
|
+
* expectTypeOf(saveResult).toEqualTypeOf<
|
|
2001
|
+
* Result<void, SaveUserFailedError>
|
|
2002
|
+
* >();
|
|
2107
2003
|
* expectErr(saveResult, {
|
|
2108
|
-
* type: "
|
|
2004
|
+
* type: "SaveUserFailed",
|
|
2109
2005
|
* userId: "missing",
|
|
2110
2006
|
* });
|
|
2111
2007
|
* expect(savedUserIds).toEqual(["user-1"]);
|
|
@@ -2114,11 +2010,45 @@ export type InferTasksOk<TTasks> = {
|
|
|
2114
2010
|
* @group Collection
|
|
2115
2011
|
*/
|
|
2116
2012
|
export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options: AllOptions): Task<void, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
|
|
2117
|
-
/** Runs a Task record without collecting its Ok values. */
|
|
2013
|
+
/** Runs a {@link Task} record without collecting its {@link Ok} values. */
|
|
2118
2014
|
export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, options: AllOptions): Task<void, InferTaskErr<TTasks[keyof TTasks]>, InferTaskRecordDeps<TTasks>>;
|
|
2015
|
+
/**
|
|
2016
|
+
* Runs a {@link Task} array and preserves its shape.
|
|
2017
|
+
*
|
|
2018
|
+
* ### Example
|
|
2019
|
+
*
|
|
2020
|
+
* ```ts
|
|
2021
|
+
* import {
|
|
2022
|
+
* all,
|
|
2023
|
+
* createRun,
|
|
2024
|
+
* ok,
|
|
2025
|
+
* type Result,
|
|
2026
|
+
* type Task,
|
|
2027
|
+
* } from "@evolu/common";
|
|
2028
|
+
*
|
|
2029
|
+
* interface User {
|
|
2030
|
+
* readonly id: string;
|
|
2031
|
+
* }
|
|
2032
|
+
*
|
|
2033
|
+
* interface Post {
|
|
2034
|
+
* readonly id: string;
|
|
2035
|
+
* }
|
|
2036
|
+
*
|
|
2037
|
+
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
2038
|
+
* const fetchPosts: Task<ReadonlyArray<Post>> = () =>
|
|
2039
|
+
* ok([{ id: "post-1" }]);
|
|
2040
|
+
*
|
|
2041
|
+
* await using run = createRun();
|
|
2042
|
+
* const dashboard = await run(all([fetchUser, fetchPosts]));
|
|
2043
|
+
* expectTypeOf(dashboard).toEqualTypeOf<
|
|
2044
|
+
* Result<readonly [User, ReadonlyArray<Post>]>
|
|
2045
|
+
* >();
|
|
2046
|
+
* expectOk(dashboard, [{ id: "user-1" }, [{ id: "post-1" }]]);
|
|
2047
|
+
* ```
|
|
2048
|
+
*/
|
|
2119
2049
|
export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksOk<TTasks>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
|
|
2120
2050
|
/**
|
|
2121
|
-
* Runs a Task record and preserves its keys.
|
|
2051
|
+
* Runs a {@link Task} record and preserves its keys.
|
|
2122
2052
|
*
|
|
2123
2053
|
* ### Example
|
|
2124
2054
|
*
|
|
@@ -2134,6 +2064,7 @@ export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks:
|
|
|
2134
2064
|
* interface User {
|
|
2135
2065
|
* readonly id: string;
|
|
2136
2066
|
* }
|
|
2067
|
+
*
|
|
2137
2068
|
* interface Post {
|
|
2138
2069
|
* readonly id: string;
|
|
2139
2070
|
* }
|
|
@@ -2155,8 +2086,13 @@ export declare function all<const TTasks extends ReadonlyArray<AnyTask>>(tasks:
|
|
|
2155
2086
|
* ```
|
|
2156
2087
|
*/
|
|
2157
2088
|
export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksOk<TTasks>, InferTaskErr<TTasks[keyof TTasks]>, InferTaskRecordDeps<TTasks>>;
|
|
2089
|
+
/** Maps an array to {@link Task}s without collecting their {@link Ok} values. */
|
|
2090
|
+
export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2091
|
+
/** Maps record values to {@link Task}s without collecting their {@link Ok} values. */
|
|
2092
|
+
export declare function all<const TValues extends Readonly<Record<string, unknown>>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2158
2093
|
/**
|
|
2159
|
-
* Maps an array to
|
|
2094
|
+
* Maps an array to {@link Task}s and collects their {@link Ok} values in the same
|
|
2095
|
+
* shape.
|
|
2160
2096
|
*
|
|
2161
2097
|
* ### Example
|
|
2162
2098
|
*
|
|
@@ -2172,6 +2108,7 @@ export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, opti
|
|
|
2172
2108
|
* interface User {
|
|
2173
2109
|
* readonly id: string;
|
|
2174
2110
|
* }
|
|
2111
|
+
*
|
|
2175
2112
|
* const loadUser =
|
|
2176
2113
|
* (id: string): Task<User> =>
|
|
2177
2114
|
* () =>
|
|
@@ -2193,14 +2130,11 @@ export declare function all<const TTasks extends TaskRecord>(tasks: TTasks, opti
|
|
|
2193
2130
|
* expectOk(result, [{ id: "user-1" }, { id: "user-2" }]);
|
|
2194
2131
|
* ```
|
|
2195
2132
|
*/
|
|
2196
|
-
export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2197
|
-
/** Maps record values to Tasks without collecting their Ok values. */
|
|
2198
|
-
export declare function all<const TValues extends Readonly<Record<string, unknown>>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[keyof TValues], key: keyof TValues) => TTask, options: AllOptions): Task<void, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2199
2133
|
export declare function all<const TValues extends ReadonlyArray<unknown>, TTask extends AnyTask>(values: TValues, fn: (value: TValues[number], index: number) => TTask, options?: TaskCollectionOptions): Task<{
|
|
2200
2134
|
readonly [K in keyof TValues]: InferTaskOk<TTask>;
|
|
2201
2135
|
}, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2202
2136
|
/**
|
|
2203
|
-
* Maps record values to
|
|
2137
|
+
* Maps record values to {@link Task}s and preserves the record's keys.
|
|
2204
2138
|
*
|
|
2205
2139
|
* ### Example
|
|
2206
2140
|
*
|
|
@@ -2216,6 +2150,7 @@ export declare function all<const TValues extends ReadonlyArray<unknown>, TTask
|
|
|
2216
2150
|
* interface User {
|
|
2217
2151
|
* readonly id: string;
|
|
2218
2152
|
* }
|
|
2153
|
+
*
|
|
2219
2154
|
* const loadUser =
|
|
2220
2155
|
* (id: string): Task<User> =>
|
|
2221
2156
|
* () =>
|
|
@@ -2249,7 +2184,8 @@ export declare function all<const TValues extends Readonly<Record<string, unknow
|
|
|
2249
2184
|
readonly [K in keyof TValues]: InferTaskOk<TTask>;
|
|
2250
2185
|
}, InferTaskErr<TTask>, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2251
2186
|
/**
|
|
2252
|
-
* Maps a Task array or record to the Result values produced by
|
|
2187
|
+
* Maps a {@link Task} array or record to the {@link Result} values produced by
|
|
2188
|
+
* its Tasks.
|
|
2253
2189
|
*
|
|
2254
2190
|
* The mapped type is homomorphic, so tuples preserve their shape and records
|
|
2255
2191
|
* preserve their keys.
|
|
@@ -2260,7 +2196,7 @@ export type InferTasksSettled<TTasks> = {
|
|
|
2260
2196
|
readonly [K in keyof TTasks]: TTasks[K] extends AnyTask ? Result<InferTaskOk<TTasks[K]>, InferTaskErr<TTasks[K]>> : never;
|
|
2261
2197
|
};
|
|
2262
2198
|
/**
|
|
2263
|
-
* Runs all
|
|
2199
|
+
* Runs all {@link Task}s and returns every Task {@link Result}.
|
|
2264
2200
|
*
|
|
2265
2201
|
* Unlike {@link all}, {@link Err} Results do not stop later Tasks.
|
|
2266
2202
|
*
|
|
@@ -2287,14 +2223,14 @@ export type InferTasksSettled<TTasks> = {
|
|
|
2287
2223
|
* ok,
|
|
2288
2224
|
* type Result,
|
|
2289
2225
|
* type Task,
|
|
2226
|
+
* type Typed,
|
|
2290
2227
|
* } from "@evolu/common";
|
|
2291
2228
|
*
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2229
|
+
* const loadProfile: Task<string, ProfileNotFoundError> = () =>
|
|
2230
|
+
* err({ type: "ProfileNotFound" });
|
|
2231
|
+
*
|
|
2232
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
|
|
2295
2233
|
*
|
|
2296
|
-
* const loadProfile: Task<string, LoadError> = () =>
|
|
2297
|
-
* err({ type: "LoadError" });
|
|
2298
2234
|
* let activityLoaded = false;
|
|
2299
2235
|
* const loadActivity: Task<ReadonlyArray<string>> = () => {
|
|
2300
2236
|
* activityLoaded = true;
|
|
@@ -2305,11 +2241,14 @@ export type InferTasksSettled<TTasks> = {
|
|
|
2305
2241
|
* const results = await run(allSettled([loadProfile, loadActivity]));
|
|
2306
2242
|
* expectTypeOf(results).toEqualTypeOf<
|
|
2307
2243
|
* Result<
|
|
2308
|
-
* readonly [
|
|
2244
|
+
* readonly [
|
|
2245
|
+
* Result<string, ProfileNotFoundError>,
|
|
2246
|
+
* Result<ReadonlyArray<string>>,
|
|
2247
|
+
* ]
|
|
2309
2248
|
* >
|
|
2310
2249
|
* >();
|
|
2311
2250
|
* expectOk(results, [
|
|
2312
|
-
* { ok: false, error: { type: "
|
|
2251
|
+
* { ok: false, error: { type: "ProfileNotFound" } },
|
|
2313
2252
|
* { ok: true, value: ["signed-in"] },
|
|
2314
2253
|
* ]);
|
|
2315
2254
|
* // Unlike all, a later Task still runs after an Err.
|
|
@@ -2320,7 +2259,7 @@ export type InferTasksSettled<TTasks> = {
|
|
|
2320
2259
|
*/
|
|
2321
2260
|
export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksSettled<TTasks>, never, InferTasksDeps<TTasks>>;
|
|
2322
2261
|
/**
|
|
2323
|
-
* Runs a Task record and preserves its keys.
|
|
2262
|
+
* Runs a {@link Task} record and preserves its keys.
|
|
2324
2263
|
*
|
|
2325
2264
|
* ### Example
|
|
2326
2265
|
*
|
|
@@ -2332,17 +2271,18 @@ export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
2332
2271
|
* ok,
|
|
2333
2272
|
* type Result,
|
|
2334
2273
|
* type Task,
|
|
2274
|
+
* type Typed,
|
|
2335
2275
|
* } from "@evolu/common";
|
|
2336
2276
|
*
|
|
2337
2277
|
* interface User {
|
|
2338
2278
|
* readonly id: string;
|
|
2339
2279
|
* }
|
|
2340
|
-
*
|
|
2341
|
-
* readonly type: "LoadError";
|
|
2342
|
-
* }
|
|
2280
|
+
*
|
|
2343
2281
|
* const fetchUser: Task<User> = () => ok({ id: "user-1" });
|
|
2344
|
-
* const fetchProfile: Task<string,
|
|
2345
|
-
* err({ type: "
|
|
2282
|
+
* const fetchProfile: Task<string, ProfileNotFoundError> = () =>
|
|
2283
|
+
* err({ type: "ProfileNotFound" });
|
|
2284
|
+
*
|
|
2285
|
+
* interface ProfileNotFoundError extends Typed<"ProfileNotFound"> {}
|
|
2346
2286
|
*
|
|
2347
2287
|
* await using run = createRun();
|
|
2348
2288
|
* const results = await run(
|
|
@@ -2352,18 +2292,18 @@ export declare function allSettled<const TTasks extends ReadonlyArray<AnyTask>>(
|
|
|
2352
2292
|
* expectTypeOf(results).toEqualTypeOf<
|
|
2353
2293
|
* Result<{
|
|
2354
2294
|
* readonly user: Result<User>;
|
|
2355
|
-
* readonly profile: Result<string,
|
|
2295
|
+
* readonly profile: Result<string, ProfileNotFoundError>;
|
|
2356
2296
|
* }>
|
|
2357
2297
|
* >();
|
|
2358
2298
|
* expectOk(results, {
|
|
2359
2299
|
* user: { ok: true, value: { id: "user-1" } },
|
|
2360
|
-
* profile: { ok: false, error: { type: "
|
|
2300
|
+
* profile: { ok: false, error: { type: "ProfileNotFound" } },
|
|
2361
2301
|
* });
|
|
2362
2302
|
* ```
|
|
2363
2303
|
*/
|
|
2364
2304
|
export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTasks, options?: TaskCollectionOptions): Task<InferTasksSettled<TTasks>, never, InferTaskRecordDeps<TTasks>>;
|
|
2365
2305
|
/**
|
|
2366
|
-
* Maps an array to
|
|
2306
|
+
* Maps an array to {@link Task}s and preserves its shape.
|
|
2367
2307
|
*
|
|
2368
2308
|
* ### Example
|
|
2369
2309
|
*
|
|
@@ -2375,18 +2315,19 @@ export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTask
|
|
|
2375
2315
|
* ok,
|
|
2376
2316
|
* type Result,
|
|
2377
2317
|
* type Task,
|
|
2318
|
+
* type Typed,
|
|
2378
2319
|
* } from "@evolu/common";
|
|
2379
2320
|
*
|
|
2380
2321
|
* interface User {
|
|
2381
2322
|
* readonly id: string;
|
|
2382
2323
|
* }
|
|
2383
|
-
*
|
|
2384
|
-
* readonly type: "LoadError";
|
|
2385
|
-
* }
|
|
2324
|
+
*
|
|
2386
2325
|
* const loadUser =
|
|
2387
|
-
* (id: string): Task<User,
|
|
2326
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
2388
2327
|
* () =>
|
|
2389
|
-
* id === "missing" ? err({ type: "
|
|
2328
|
+
* id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
|
|
2329
|
+
*
|
|
2330
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {}
|
|
2390
2331
|
*
|
|
2391
2332
|
* const userIds = ["user-1", "missing"] as const;
|
|
2392
2333
|
* const indexes: Array<number> = [];
|
|
@@ -2401,11 +2342,16 @@ export declare function allSettled<const TTasks extends TaskRecord>(tasks: TTask
|
|
|
2401
2342
|
* await using run = createRun();
|
|
2402
2343
|
* const results = await run(loadUsers);
|
|
2403
2344
|
* expectTypeOf(results).toEqualTypeOf<
|
|
2404
|
-
* Result<
|
|
2345
|
+
* Result<
|
|
2346
|
+
* readonly [
|
|
2347
|
+
* Result<User, UserNotFoundError>,
|
|
2348
|
+
* Result<User, UserNotFoundError>,
|
|
2349
|
+
* ]
|
|
2350
|
+
* >
|
|
2405
2351
|
* >();
|
|
2406
2352
|
* expectOk(results, [
|
|
2407
2353
|
* { ok: true, value: { id: "user-1" } },
|
|
2408
|
-
* { ok: false, error: { type: "
|
|
2354
|
+
* { ok: false, error: { type: "UserNotFound" } },
|
|
2409
2355
|
* ]);
|
|
2410
2356
|
* ```
|
|
2411
2357
|
*/
|
|
@@ -2413,7 +2359,7 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
|
|
|
2413
2359
|
readonly [K in keyof TValues]: Result<InferTaskOk<TTask>, InferTaskErr<TTask>>;
|
|
2414
2360
|
}, never, InferTasksDeps<ReadonlyArray<TTask>>>;
|
|
2415
2361
|
/**
|
|
2416
|
-
* Maps record values to
|
|
2362
|
+
* Maps record values to {@link Task}s and preserves the record's keys.
|
|
2417
2363
|
*
|
|
2418
2364
|
* ### Example
|
|
2419
2365
|
*
|
|
@@ -2425,18 +2371,19 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
|
|
|
2425
2371
|
* ok,
|
|
2426
2372
|
* type Result,
|
|
2427
2373
|
* type Task,
|
|
2374
|
+
* type Typed,
|
|
2428
2375
|
* } from "@evolu/common";
|
|
2429
2376
|
*
|
|
2430
2377
|
* interface User {
|
|
2431
2378
|
* readonly id: string;
|
|
2432
2379
|
* }
|
|
2433
|
-
*
|
|
2434
|
-
* readonly type: "LoadError";
|
|
2435
|
-
* }
|
|
2380
|
+
*
|
|
2436
2381
|
* const loadUser =
|
|
2437
|
-
* (id: string): Task<User,
|
|
2382
|
+
* (id: string): Task<User, UserNotFoundError> =>
|
|
2438
2383
|
* () =>
|
|
2439
|
-
* id === "missing" ? err({ type: "
|
|
2384
|
+
* id === "missing" ? err({ type: "UserNotFound" }) : ok({ id });
|
|
2385
|
+
*
|
|
2386
|
+
* interface UserNotFoundError extends Typed<"UserNotFound"> {}
|
|
2440
2387
|
*
|
|
2441
2388
|
* const userIdsByRole = { admin: "user-1", reviewer: "missing" } as const;
|
|
2442
2389
|
* const roles: Array<keyof typeof userIdsByRole> = [];
|
|
@@ -2452,13 +2399,13 @@ export declare function allSettled<const TValues extends ReadonlyArray<unknown>,
|
|
|
2452
2399
|
* const results = await run(loadUsersByRole);
|
|
2453
2400
|
* expectTypeOf(results).toEqualTypeOf<
|
|
2454
2401
|
* Result<{
|
|
2455
|
-
* readonly admin: Result<User,
|
|
2456
|
-
* readonly reviewer: Result<User,
|
|
2402
|
+
* readonly admin: Result<User, UserNotFoundError>;
|
|
2403
|
+
* readonly reviewer: Result<User, UserNotFoundError>;
|
|
2457
2404
|
* }>
|
|
2458
2405
|
* >();
|
|
2459
2406
|
* expectOk(results, {
|
|
2460
2407
|
* admin: { ok: true, value: { id: "user-1" } },
|
|
2461
|
-
* reviewer: { ok: false, error: { type: "
|
|
2408
|
+
* reviewer: { ok: false, error: { type: "UserNotFound" } },
|
|
2462
2409
|
* });
|
|
2463
2410
|
* ```
|
|
2464
2411
|
*/
|
|
@@ -2472,10 +2419,11 @@ export declare function allSettled<const TValues extends Readonly<Record<string,
|
|
|
2472
2419
|
* callbacks. Resolve with `ok(value)` or `err(error)`, or reject with a defect,
|
|
2473
2420
|
* to complete the Task. Settlement is one-shot: the first `resolve` or `reject`
|
|
2474
2421
|
* wins, and later settlement calls are ignored, matching Promise semantics.
|
|
2475
|
-
* When `reject` wins settlement, the defect panics the Run tree and is
|
|
2476
|
-
* at Fiber boundaries as {@link AbortError} with
|
|
2477
|
-
* Rejecting AbortError is reserved for Task abort
|
|
2478
|
-
* `run(task)` rejects with it, and `run.abortable(task)`
|
|
2422
|
+
* When `reject` wins settlement, the defect panics the {@link Run} tree and is
|
|
2423
|
+
* observed at {@link Fiber} boundaries as {@link AbortError} with
|
|
2424
|
+
* {@link PanicAbortReason}. Rejecting AbortError is reserved for Task abort
|
|
2425
|
+
* control flow: direct `run(task)` rejects with it, and `run.abortable(task)`
|
|
2426
|
+
* returns it as an {@link Err}.
|
|
2479
2427
|
*
|
|
2480
2428
|
* This helper is a callback bridge. If `reject` forwards an Error created in a
|
|
2481
2429
|
* separate async chain, V8 cannot reconstruct the caller's zero-cost async
|
|
@@ -2490,10 +2438,10 @@ export declare function allSettled<const TValues extends Readonly<Record<string,
|
|
|
2490
2438
|
* Optionally return a synchronous cleanup callback. It runs when the Task Run
|
|
2491
2439
|
* signal aborts, including normal completion and explicit abort. The callback
|
|
2492
2440
|
* must not throw. Cleanup defects panic the root Run; if the callback Task
|
|
2493
|
-
* already settled, its Fiber keeps the original Result while the root
|
|
2494
|
-
* reports the panic. Cleanup must be synchronous; returned promises are
|
|
2495
|
-
* awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
2496
|
-
* with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
2441
|
+
* already settled, its Fiber keeps the original {@link Result} while the root
|
|
2442
|
+
* still reports the panic. Cleanup must be synchronous; returned promises are
|
|
2443
|
+
* not awaited. For async cleanup, prefer {@link acquireUseRelease}, `await
|
|
2444
|
+
* using` with {@link AsyncDisposableStack}, or a Task that owns the resource
|
|
2497
2445
|
* explicitly.
|
|
2498
2446
|
*
|
|
2499
2447
|
* If setup can throw after acquiring any resource, use a local
|
|
@@ -2537,25 +2485,20 @@ export declare const callback: <T, E = never, D = unknown>(fn: (options: {
|
|
|
2537
2485
|
/**
|
|
2538
2486
|
* Pauses execution for a specified {@link PositiveDuration}.
|
|
2539
2487
|
*
|
|
2540
|
-
* Aborting the Task clears the scheduled timeout.
|
|
2488
|
+
* Aborting the {@link Task} clears the scheduled timeout.
|
|
2541
2489
|
*
|
|
2542
|
-
*
|
|
2543
|
-
*/
|
|
2544
|
-
export declare const sleep: (duration: PositiveDuration) => Task<void>;
|
|
2545
|
-
/**
|
|
2546
|
-
* Typed error returned by {@link timeout} when a Task exceeds its duration.
|
|
2490
|
+
* ### Example
|
|
2547
2491
|
*
|
|
2548
|
-
*
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
2492
|
+
* ```ts
|
|
2493
|
+
* import { createRun, sleep } from "@evolu/common";
|
|
2494
|
+
*
|
|
2495
|
+
* await using run = createRun();
|
|
2496
|
+
* expectOk(await run(sleep("1ms")), undefined);
|
|
2497
|
+
* ```
|
|
2555
2498
|
*
|
|
2556
2499
|
* @group Timing
|
|
2557
2500
|
*/
|
|
2558
|
-
export declare const
|
|
2501
|
+
export declare const sleep: (duration: PositiveDuration) => Task<void>;
|
|
2559
2502
|
/**
|
|
2560
2503
|
* Limits how long a {@link Task} may run.
|
|
2561
2504
|
*
|
|
@@ -2587,6 +2530,26 @@ export declare const timeoutError: TimeoutError;
|
|
|
2587
2530
|
* @group Timing
|
|
2588
2531
|
*/
|
|
2589
2532
|
export declare const timeout: <T, E, D = unknown>(task: Task<T, E, D>, duration: PositiveDuration) => Task<T, E | TimeoutError, D>;
|
|
2533
|
+
/**
|
|
2534
|
+
* Runtime Type for the error returned by {@link timeout} when a {@link Task}
|
|
2535
|
+
* exceeds its duration.
|
|
2536
|
+
*
|
|
2537
|
+
* @group Timing
|
|
2538
|
+
*/
|
|
2539
|
+
export declare const TimeoutError: TypedType<"TimeoutError">;
|
|
2540
|
+
/**
|
|
2541
|
+
* Error returned by {@link timeout} when a {@link Task} exceeds its duration.
|
|
2542
|
+
*
|
|
2543
|
+
* @group Timing
|
|
2544
|
+
*/
|
|
2545
|
+
export interface TimeoutError extends InferType<typeof TimeoutError> {
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* The {@link TimeoutError} instance returned by {@link timeout}.
|
|
2549
|
+
*
|
|
2550
|
+
* @group Timing
|
|
2551
|
+
*/
|
|
2552
|
+
export declare const timeoutError: TimeoutError;
|
|
2590
2553
|
/**
|
|
2591
2554
|
* Options for {@link retry}.
|
|
2592
2555
|
*
|
|
@@ -2599,7 +2562,7 @@ export interface RetryOptions<E, Output> {
|
|
|
2599
2562
|
* Returning `false` stops retrying and returns {@link RetryError}. The
|
|
2600
2563
|
* original error is stored as `lastError` instead of returned directly. The
|
|
2601
2564
|
* predicate must not throw: a thrown exception is a defect that panics the
|
|
2602
|
-
* Run tree.
|
|
2565
|
+
* {@link Run} tree.
|
|
2603
2566
|
*/
|
|
2604
2567
|
readonly shouldRetry?: Predicate<E>;
|
|
2605
2568
|
/**
|
|
@@ -2608,8 +2571,8 @@ export interface RetryOptions<E, Output> {
|
|
|
2608
2571
|
* `attempt` is the failed attempt that caused this retry, not the upcoming
|
|
2609
2572
|
* attempt number. The callback runs after {@link RetryOptions.shouldRetry} and
|
|
2610
2573
|
* the {@link Schedule} approve a retry, and before the retry delay. The
|
|
2611
|
-
* callback must not throw: a thrown exception is a defect that panics the
|
|
2612
|
-
* tree.
|
|
2574
|
+
* callback must not throw: a thrown exception is a defect that panics the
|
|
2575
|
+
* {@link Run} tree.
|
|
2613
2576
|
*/
|
|
2614
2577
|
readonly onRetry?: (attempt: RetryAttempt<E, Output>) => void;
|
|
2615
2578
|
}
|
|
@@ -2623,40 +2586,23 @@ export interface RetryAttempt<E, Output> extends ScheduleStep<Output> {
|
|
|
2623
2586
|
readonly error: E;
|
|
2624
2587
|
}
|
|
2625
2588
|
/**
|
|
2626
|
-
*
|
|
2627
|
-
*
|
|
2628
|
-
* @group Resilience
|
|
2629
|
-
*/
|
|
2630
|
-
export interface RetryError<E> extends Typed<"RetryError"> {
|
|
2631
|
-
/** The final domain error that stopped retrying. */
|
|
2632
|
-
readonly lastError: E;
|
|
2633
|
-
/** The number of attempts that were started. */
|
|
2634
|
-
readonly attempts: PositiveInt;
|
|
2635
|
-
}
|
|
2636
|
-
/**
|
|
2637
|
-
* Error type returned by {@link retry}.
|
|
2638
|
-
*
|
|
2639
|
-
* @group Resilience
|
|
2640
|
-
*/
|
|
2641
|
-
export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? never : RetryError<Exclude<E, AbortError>>) | Extract<E, AbortError>;
|
|
2642
|
-
/**
|
|
2643
|
-
* Retries a Task according to a {@link Schedule}.
|
|
2589
|
+
* Retries a {@link Task} according to a {@link Schedule}.
|
|
2644
2590
|
*
|
|
2645
2591
|
* Use `retry` for failure recovery: it repeats after {@link Err} and wraps the
|
|
2646
2592
|
* final domain error. Use {@link repeat} for success-driven loops: it repeats
|
|
2647
|
-
* after {@link Ok} and returns the Task's natural Result.
|
|
2593
|
+
* after {@link Ok} and returns the Task's natural {@link Result}.
|
|
2648
2594
|
*
|
|
2649
2595
|
* {@link AbortError} passes through unchanged when returned as a Result error,
|
|
2650
|
-
* such as from {@link daemon}. Abort from `run(task)` remains Fiber
|
|
2651
|
-
* flow. Other errors are domain errors: retrying continues while the
|
|
2652
|
-
* yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
2653
|
-
* retrying stops, `retry` returns {@link RetryError} with the final
|
|
2654
|
-
* as `lastError`.
|
|
2596
|
+
* such as from {@link daemon}. Abort from `run(task)` remains {@link Fiber}
|
|
2597
|
+
* control flow. Other errors are domain errors: retrying continues while the
|
|
2598
|
+
* schedule yields another delay and {@link RetryOptions.shouldRetry} returns
|
|
2599
|
+
* `true`. When retrying stops, `retry` returns {@link RetryError} with the final
|
|
2600
|
+
* domain error as `lastError`.
|
|
2655
2601
|
*
|
|
2656
2602
|
* AbortError recognition is structural. Domain errors, especially values parsed
|
|
2657
2603
|
* from untrusted input, must not use the reserved AbortError shape.
|
|
2658
2604
|
*
|
|
2659
|
-
* ###
|
|
2605
|
+
* ### Retrying failures
|
|
2660
2606
|
*
|
|
2661
2607
|
* ```ts
|
|
2662
2608
|
* import {
|
|
@@ -2667,57 +2613,81 @@ export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? neve
|
|
|
2667
2613
|
* type Result,
|
|
2668
2614
|
* type RetryTaskError,
|
|
2669
2615
|
* type Task,
|
|
2616
|
+
* type Typed,
|
|
2670
2617
|
* } from "@evolu/common";
|
|
2671
2618
|
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
* interface FetchDataError {
|
|
2675
|
-
* readonly type: "FetchDataError";
|
|
2676
|
-
* }
|
|
2619
|
+
* const fetchData: Task<string, ServiceUnavailableError> = () =>
|
|
2620
|
+
* err({ type: "ServiceUnavailable" });
|
|
2677
2621
|
*
|
|
2678
|
-
*
|
|
2679
|
-
* err({ type: "FetchDataError" });
|
|
2622
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
2680
2623
|
*
|
|
2681
2624
|
* const fetchWithRetry = retry(fetchData, recurs(2));
|
|
2682
2625
|
*
|
|
2626
|
+
* await using run = createRun();
|
|
2683
2627
|
* const result = await run(fetchWithRetry);
|
|
2684
2628
|
* expectTypeOf(result).toEqualTypeOf<
|
|
2685
|
-
* Result<string, RetryTaskError<
|
|
2629
|
+
* Result<string, RetryTaskError<ServiceUnavailableError>>
|
|
2686
2630
|
* >();
|
|
2687
2631
|
* expectErr(result, {
|
|
2688
2632
|
* type: "RetryError",
|
|
2689
2633
|
* attempts: 3,
|
|
2690
|
-
* lastError: { type: "
|
|
2634
|
+
* lastError: { type: "ServiceUnavailable" },
|
|
2691
2635
|
* });
|
|
2692
2636
|
* ```
|
|
2693
2637
|
*
|
|
2694
|
-
* ###
|
|
2638
|
+
* ### Filtering retries
|
|
2695
2639
|
*
|
|
2696
2640
|
* ```ts
|
|
2697
|
-
* import {
|
|
2641
|
+
* import {
|
|
2642
|
+
* createRun,
|
|
2643
|
+
* err,
|
|
2644
|
+
* recurs,
|
|
2645
|
+
* retry,
|
|
2646
|
+
* type Task,
|
|
2647
|
+
* type Typed,
|
|
2648
|
+
* } from "@evolu/common";
|
|
2698
2649
|
*
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
*
|
|
2650
|
+
* const fetchData: Task<
|
|
2651
|
+
* string,
|
|
2652
|
+
* TemporaryFailureError | PermanentFailureError
|
|
2653
|
+
* > = () => err({ type: "PermanentFailure" });
|
|
2654
|
+
*
|
|
2655
|
+
* interface TemporaryFailureError extends Typed<"TemporaryFailure"> {}
|
|
2702
2656
|
*
|
|
2703
|
-
*
|
|
2704
|
-
* err({ type: "FatalError" });
|
|
2657
|
+
* interface PermanentFailureError extends Typed<"PermanentFailure"> {}
|
|
2705
2658
|
*
|
|
2706
2659
|
* const fetchWithRetry = retry(fetchData, recurs(5), {
|
|
2707
|
-
* shouldRetry: (error) => error.type !== "
|
|
2660
|
+
* shouldRetry: (error) => error.type !== "PermanentFailure",
|
|
2708
2661
|
* });
|
|
2709
2662
|
*
|
|
2710
2663
|
* await using run = createRun();
|
|
2711
2664
|
* expectErr(await run(fetchWithRetry), {
|
|
2712
2665
|
* type: "RetryError",
|
|
2713
2666
|
* attempts: 1,
|
|
2714
|
-
* lastError: { type: "
|
|
2667
|
+
* lastError: { type: "PermanentFailure" },
|
|
2715
2668
|
* });
|
|
2716
2669
|
* ```
|
|
2717
2670
|
*
|
|
2718
2671
|
* @group Resilience
|
|
2719
2672
|
*/
|
|
2720
2673
|
export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, Exclude<E, AbortError>>, { shouldRetry, onRetry, }?: RetryOptions<Exclude<E, AbortError>, Output>) => Task<T, RetryTaskError<E>, D>;
|
|
2674
|
+
/**
|
|
2675
|
+
* Error returned by {@link retry} when retrying stops after a domain error.
|
|
2676
|
+
*
|
|
2677
|
+
* @group Resilience
|
|
2678
|
+
*/
|
|
2679
|
+
export interface RetryError<E> extends Typed<"RetryError"> {
|
|
2680
|
+
/** The final domain error that stopped retrying. */
|
|
2681
|
+
readonly lastError: E;
|
|
2682
|
+
/** The number of attempts that were started. */
|
|
2683
|
+
readonly attempts: PositiveInt;
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Error type returned by {@link retry}.
|
|
2687
|
+
*
|
|
2688
|
+
* @group Resilience
|
|
2689
|
+
*/
|
|
2690
|
+
export type RetryTaskError<E> = ([Exclude<E, AbortError>] extends [never] ? never : RetryError<Exclude<E, AbortError>>) | Extract<E, AbortError>;
|
|
2721
2691
|
/**
|
|
2722
2692
|
* Options for {@link repeat}.
|
|
2723
2693
|
*
|
|
@@ -2725,10 +2695,10 @@ export declare const retry: <T, E, D = unknown, Output = unknown>(task: Task<T,
|
|
|
2725
2695
|
*/
|
|
2726
2696
|
export interface RepeatOptions<T, Output> {
|
|
2727
2697
|
/**
|
|
2728
|
-
* Decides whether an Ok value should schedule another repeat.
|
|
2698
|
+
* Decides whether an {@link Ok} value should schedule another repeat.
|
|
2729
2699
|
*
|
|
2730
2700
|
* The predicate must not throw: a thrown exception is a defect that panics
|
|
2731
|
-
* the Run tree.
|
|
2701
|
+
* the {@link Run} tree.
|
|
2732
2702
|
*/
|
|
2733
2703
|
readonly shouldRepeat?: Predicate<T>;
|
|
2734
2704
|
/**
|
|
@@ -2738,7 +2708,7 @@ export interface RepeatOptions<T, Output> {
|
|
|
2738
2708
|
* upcoming attempt number. The callback runs after
|
|
2739
2709
|
* {@link RepeatOptions.shouldRepeat} and the {@link Schedule} approve a repeat,
|
|
2740
2710
|
* and before the repeat delay. The callback must not throw: a thrown
|
|
2741
|
-
* exception is a defect that panics the Run tree.
|
|
2711
|
+
* exception is a defect that panics the {@link Run} tree.
|
|
2742
2712
|
*/
|
|
2743
2713
|
readonly onRepeat?: (attempt: RepeatAttempt<T, Output>) => void;
|
|
2744
2714
|
}
|
|
@@ -2748,17 +2718,18 @@ export interface RepeatOptions<T, Output> {
|
|
|
2748
2718
|
* @group Repetition
|
|
2749
2719
|
*/
|
|
2750
2720
|
export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
2751
|
-
/** The Ok value returned by the completed attempt. */
|
|
2721
|
+
/** The {@link Ok} value returned by the completed attempt. */
|
|
2752
2722
|
readonly value: T;
|
|
2753
2723
|
}
|
|
2754
2724
|
/**
|
|
2755
|
-
* Repeats a Task according to a {@link Schedule}.
|
|
2725
|
+
* Repeats a {@link Task} according to a {@link Schedule}.
|
|
2756
2726
|
*
|
|
2757
2727
|
* Runs the Task once, then repeats while the Task returns {@link Ok}, the
|
|
2758
2728
|
* schedule yields another delay, and {@link RepeatOptions.shouldRepeat} returns
|
|
2759
|
-
* `true`. When repeating stops, `repeat` returns the last successful
|
|
2760
|
-
* the Task returns {@link Err}, including {@link Done} from a
|
|
2761
|
-
* `repeat` returns that error without scheduling another
|
|
2729
|
+
* `true`. When repeating stops, `repeat` returns the last successful
|
|
2730
|
+
* {@link Result}. If the Task returns {@link Err}, including {@link Done} from a
|
|
2731
|
+
* {@link NextTask}, `repeat` returns that error without scheduling another
|
|
2732
|
+
* attempt.
|
|
2762
2733
|
*
|
|
2763
2734
|
* Use `repeat` for success-driven loops such as polling or consuming a
|
|
2764
2735
|
* NextTask: it repeats after Ok and returns the Task's natural Result. Use
|
|
@@ -2767,7 +2738,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
2767
2738
|
*
|
|
2768
2739
|
* With `take(n)`, the Task runs n+1 times: the initial attempt plus n repeats.
|
|
2769
2740
|
*
|
|
2770
|
-
* ###
|
|
2741
|
+
* ### Repeating successes
|
|
2771
2742
|
*
|
|
2772
2743
|
* ```ts
|
|
2773
2744
|
* import { createRun, ok, recurs, repeat, type Task } from "@evolu/common";
|
|
@@ -2785,7 +2756,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
2785
2756
|
* expect(attempts).toBe(4);
|
|
2786
2757
|
* ```
|
|
2787
2758
|
*
|
|
2788
|
-
* ###
|
|
2759
|
+
* ### Stopping with Done
|
|
2789
2760
|
*
|
|
2790
2761
|
* ```ts
|
|
2791
2762
|
* import {
|
|
@@ -2798,8 +2769,6 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
2798
2769
|
* type NextTask,
|
|
2799
2770
|
* } from "@evolu/common";
|
|
2800
2771
|
*
|
|
2801
|
-
* await using run = createRun();
|
|
2802
|
-
*
|
|
2803
2772
|
* interface Item {
|
|
2804
2773
|
* readonly id: string;
|
|
2805
2774
|
* }
|
|
@@ -2811,6 +2780,7 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
2811
2780
|
* return item ? ok(item) : err(done());
|
|
2812
2781
|
* };
|
|
2813
2782
|
*
|
|
2783
|
+
* await using run = createRun();
|
|
2814
2784
|
* const result = await run(repeat(processQueue, spaced("1ms")));
|
|
2815
2785
|
* expectErr(result, done());
|
|
2816
2786
|
* expect(queue).toEqual([]);
|
|
@@ -2820,28 +2790,29 @@ export interface RepeatAttempt<T, Output> extends ScheduleStep<Output> {
|
|
|
2820
2790
|
*/
|
|
2821
2791
|
export declare const repeat: <T, E, D = unknown, Output = unknown>(task: Task<T, E, D>, schedule: Schedule<Output, T>, { shouldRepeat, onRepeat }?: RepeatOptions<T, Output>) => Task<T, E, D>;
|
|
2822
2792
|
/**
|
|
2823
|
-
* Extracts the Result type produced by one Task in a non-empty
|
|
2793
|
+
* Extracts the {@link Result} type produced by one {@link Task} in a non-empty
|
|
2794
|
+
* Task array.
|
|
2824
2795
|
*
|
|
2825
|
-
* @
|
|
2796
|
+
* @group Type utilities
|
|
2826
2797
|
*/
|
|
2827
2798
|
export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Result<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>>;
|
|
2828
2799
|
/**
|
|
2829
|
-
* Runs
|
|
2800
|
+
* Runs {@link Task}s until one returns {@link Ok} or all return {@link Err}.
|
|
2830
2801
|
*
|
|
2831
|
-
* Use {@link race} to return the first settled Result instead, whether Ok
|
|
2832
|
-
* {@link Err}.
|
|
2802
|
+
* Use {@link race} to return the first settled {@link Result} instead, whether Ok
|
|
2803
|
+
* or {@link Err}.
|
|
2833
2804
|
*
|
|
2834
|
-
* Returns the first {@link Ok} Result.
|
|
2835
|
-
* returns Ok, returns the last Err by
|
|
2836
|
-
* order. Other Err results are discarded;
|
|
2837
|
-
* every error.
|
|
2805
|
+
* Returns the first {@link Ok} Result. Queued Tasks are not started, and other
|
|
2806
|
+
* running Tasks are aborted. If no Task returns Ok, returns the last Err by
|
|
2807
|
+
* input order, regardless of completion order. Other Err results are discarded;
|
|
2808
|
+
* use {@link allSettled} when you need every error.
|
|
2838
2809
|
*
|
|
2839
2810
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
2840
2811
|
* at a time.
|
|
2841
2812
|
*
|
|
2842
2813
|
* Similar to
|
|
2843
2814
|
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any | Promise.any},
|
|
2844
|
-
* but
|
|
2815
|
+
* but runs Tasks, returns Result values, and stops after the first Ok.
|
|
2845
2816
|
*
|
|
2846
2817
|
* ### Example
|
|
2847
2818
|
*
|
|
@@ -2853,10 +2824,14 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
|
|
|
2853
2824
|
* ok,
|
|
2854
2825
|
* type Result,
|
|
2855
2826
|
* type Task,
|
|
2827
|
+
* type Typed,
|
|
2856
2828
|
* } from "@evolu/common";
|
|
2857
2829
|
*
|
|
2858
|
-
* const unavailable: Task<string,
|
|
2859
|
-
* err("
|
|
2830
|
+
* const unavailable: Task<string, ServiceUnavailableError> = () =>
|
|
2831
|
+
* err({ type: "ServiceUnavailable" });
|
|
2832
|
+
*
|
|
2833
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
2834
|
+
*
|
|
2860
2835
|
* let fallbackStarted = false;
|
|
2861
2836
|
* const fallback: Task<string> = () => {
|
|
2862
2837
|
* fallbackStarted = true;
|
|
@@ -2866,7 +2841,9 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
|
|
|
2866
2841
|
* await using run = createRun();
|
|
2867
2842
|
* const result = await run(any([unavailable, fallback]));
|
|
2868
2843
|
*
|
|
2869
|
-
* expectTypeOf(result).toEqualTypeOf<
|
|
2844
|
+
* expectTypeOf(result).toEqualTypeOf<
|
|
2845
|
+
* Result<string, ServiceUnavailableError>
|
|
2846
|
+
* >();
|
|
2870
2847
|
* expectOk(result, "fallback");
|
|
2871
2848
|
* expect(fallbackStarted).toBe(true);
|
|
2872
2849
|
* ```
|
|
@@ -2875,7 +2852,7 @@ export type InferTasksResult<TTasks extends NonEmptyReadonlyArray<AnyTask>> = Re
|
|
|
2875
2852
|
*/
|
|
2876
2853
|
export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, options?: TaskCollectionOptions) => Task<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
|
|
2877
2854
|
/**
|
|
2878
|
-
* Runs
|
|
2855
|
+
* Runs {@link Task}s until the first Task settles.
|
|
2879
2856
|
*
|
|
2880
2857
|
* Returns the first Task {@link Result} to settle, whether {@link Ok} or
|
|
2881
2858
|
* {@link Err}.
|
|
@@ -2896,7 +2873,7 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
|
|
|
2896
2873
|
* arrays whose emptiness is only known at runtime, guard with
|
|
2897
2874
|
* {@link isNonEmptyArray}:
|
|
2898
2875
|
*
|
|
2899
|
-
* ###
|
|
2876
|
+
* ### Runtime arrays
|
|
2900
2877
|
*
|
|
2901
2878
|
* ```ts
|
|
2902
2879
|
* import {
|
|
@@ -2915,7 +2892,7 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
|
|
|
2915
2892
|
* }
|
|
2916
2893
|
* ```
|
|
2917
2894
|
*
|
|
2918
|
-
* ###
|
|
2895
|
+
* ### First settlement
|
|
2919
2896
|
*
|
|
2920
2897
|
* ```ts
|
|
2921
2898
|
* import {
|
|
@@ -2949,12 +2926,12 @@ export declare const any: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks:
|
|
|
2949
2926
|
*/
|
|
2950
2927
|
export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks) => Task<InferTaskOk<TTasks[number]>, InferTaskErr<TTasks[number]>, InferTasksDeps<TTasks>>;
|
|
2951
2928
|
/**
|
|
2952
|
-
* Runs
|
|
2929
|
+
* Runs {@link Task}s until `count` Tasks return {@link Ok} or all Tasks settle.
|
|
2953
2930
|
*
|
|
2954
2931
|
* Returns {@link Ok} with Ok values in settlement order, not input order.
|
|
2955
|
-
* {@link Err}
|
|
2956
|
-
*
|
|
2957
|
-
* the Ok values that did settle.
|
|
2932
|
+
* {@link Err} {@link Result}s are ignored. When `count` Ok values have settled,
|
|
2933
|
+
* queued Tasks are not started and remaining running Tasks are aborted. If
|
|
2934
|
+
* fewer than `count` Tasks return Ok, returns the Ok values that did settle.
|
|
2958
2935
|
*
|
|
2959
2936
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
2960
2937
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -2970,17 +2947,23 @@ export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks
|
|
|
2970
2947
|
* ok,
|
|
2971
2948
|
* sleep,
|
|
2972
2949
|
* type Task,
|
|
2950
|
+
* type Typed,
|
|
2973
2951
|
* } from "@evolu/common";
|
|
2974
2952
|
*
|
|
2975
2953
|
* let slowCompleted = false;
|
|
2976
|
-
* const slow: Task<string
|
|
2954
|
+
* const slow: Task<string> = async (run) => {
|
|
2977
2955
|
* await run.ok(sleep("10ms"));
|
|
2978
2956
|
* slowCompleted = true;
|
|
2979
2957
|
* return ok("slow");
|
|
2980
2958
|
* };
|
|
2959
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
2960
|
+
* err({ type: "ServiceUnavailable" });
|
|
2961
|
+
*
|
|
2962
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
2963
|
+
*
|
|
2981
2964
|
* const tasks = [
|
|
2982
2965
|
* slow,
|
|
2983
|
-
*
|
|
2966
|
+
* unavailable,
|
|
2984
2967
|
* () => ok("fast-1"),
|
|
2985
2968
|
* () => ok("fast-2"),
|
|
2986
2969
|
* ] as const;
|
|
@@ -2996,11 +2979,12 @@ export declare const race: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks
|
|
|
2996
2979
|
*/
|
|
2997
2980
|
export declare const firstN: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, count: Int1To100OrPositiveInt, options?: TaskCollectionOptions) => Task<ReadonlyArray<InferTaskOk<TTasks[number]>>, never, InferTasksDeps<TTasks>>;
|
|
2998
2981
|
/**
|
|
2999
|
-
* Runs
|
|
2982
|
+
* Runs {@link Task}s until `count` Tasks settle or all Tasks settle.
|
|
3000
2983
|
*
|
|
3001
2984
|
* Returns {@link Ok} with Task {@link Result}s in settlement order, not input
|
|
3002
|
-
* order. When `count` Results have settled,
|
|
3003
|
-
* fewer than `count` Tasks settle,
|
|
2985
|
+
* order. When `count` Results have settled, queued Tasks are not started and
|
|
2986
|
+
* remaining running Tasks are aborted. If fewer than `count` Tasks settle,
|
|
2987
|
+
* returns the Results that did settle.
|
|
3004
2988
|
*
|
|
3005
2989
|
* Sequential by default; pass a `concurrency` option to run more than one Task
|
|
3006
2990
|
* at a time. The count uses {@link Int1To100OrPositiveInt}: pass `1` to `100` as
|
|
@@ -3016,25 +3000,27 @@ export declare const firstN: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tas
|
|
|
3016
3000
|
* ok,
|
|
3017
3001
|
* sleep,
|
|
3018
3002
|
* type Task,
|
|
3003
|
+
* type Typed,
|
|
3019
3004
|
* } from "@evolu/common";
|
|
3020
3005
|
*
|
|
3021
3006
|
* let slowCompleted = false;
|
|
3022
|
-
* const slow: Task<string
|
|
3007
|
+
* const slow: Task<string> = async (run) => {
|
|
3023
3008
|
* await run.ok(sleep("10ms"));
|
|
3024
3009
|
* slowCompleted = true;
|
|
3025
3010
|
* return ok("slow");
|
|
3026
3011
|
* };
|
|
3027
|
-
* const
|
|
3028
|
-
*
|
|
3029
|
-
*
|
|
3030
|
-
*
|
|
3031
|
-
*
|
|
3012
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
3013
|
+
* err({ type: "ServiceUnavailable" });
|
|
3014
|
+
*
|
|
3015
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
3016
|
+
*
|
|
3017
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
3032
3018
|
* await using run = createRun();
|
|
3033
3019
|
*
|
|
3034
3020
|
* // Err and Ok both count, and Results use settlement order.
|
|
3035
3021
|
* const result = await run(firstNSettled(tasks, 2, { concurrency: 3 }));
|
|
3036
3022
|
* expectOk(result, [
|
|
3037
|
-
* { ok: false, error: "
|
|
3023
|
+
* { ok: false, error: { type: "ServiceUnavailable" } },
|
|
3038
3024
|
* { ok: true, value: "fast" },
|
|
3039
3025
|
* ]);
|
|
3040
3026
|
* expect(slowCompleted).toBe(false);
|
|
@@ -3046,43 +3032,44 @@ export declare const firstNSettled: <TTasks extends NonEmptyReadonlyArray<AnyTas
|
|
|
3046
3032
|
/**
|
|
3047
3033
|
* Decision returned by an {@link each} result handler.
|
|
3048
3034
|
*
|
|
3049
|
-
* `continue` allows queued
|
|
3035
|
+
* `continue` allows queued {@link Task}s to start when concurrency capacity is
|
|
3050
3036
|
* available. `stop` prevents queued Tasks from starting and aborts already
|
|
3051
|
-
* running Tasks through structured Run disposal.
|
|
3037
|
+
* running Tasks through structured {@link Run} disposal.
|
|
3052
3038
|
*
|
|
3053
|
-
* @group
|
|
3039
|
+
* @group Collection
|
|
3054
3040
|
*/
|
|
3055
3041
|
export type EachDecision = "continue" | "stop";
|
|
3056
3042
|
/**
|
|
3057
|
-
* Handles one settled Task Result from {@link each}.
|
|
3043
|
+
* Handles one settled {@link Task} {@link Result} from {@link each}.
|
|
3058
3044
|
*
|
|
3059
3045
|
* The index is the original input index. Callback order follows settlement
|
|
3060
3046
|
* order, not input order.
|
|
3061
3047
|
*
|
|
3062
|
-
* @group
|
|
3048
|
+
* @group Collection
|
|
3063
3049
|
*/
|
|
3064
3050
|
export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (result: InferTasksResult<TTasks>, index: number) => EachDecision;
|
|
3065
3051
|
/**
|
|
3066
|
-
* Runs
|
|
3067
|
-
* {@link Result} as it settles.
|
|
3052
|
+
* Runs {@link Task}s under a concurrency limit and calls `onResult` for each
|
|
3053
|
+
* Task {@link Result} as it settles.
|
|
3068
3054
|
*
|
|
3069
|
-
* `onResult` receives the Result and the original input index; call
|
|
3070
|
-
* settlement order, not input order. Returning `continue` lets queued
|
|
3071
|
-
* start when capacity is available. Returning `stop` prevents queued
|
|
3072
|
-
* starting and aborts already-running Tasks through structured
|
|
3073
|
-
* `each` still waits for them to settle before
|
|
3055
|
+
* `onResult` receives the {@link Result} and the original input index; call
|
|
3056
|
+
* order is settlement order, not input order. Returning `continue` lets queued
|
|
3057
|
+
* Tasks start when capacity is available. Returning `stop` prevents queued
|
|
3058
|
+
* Tasks from starting and aborts already-running Tasks through structured
|
|
3059
|
+
* {@link Run} disposal — `each` still waits for them to settle before
|
|
3060
|
+
* returning.
|
|
3074
3061
|
*
|
|
3075
3062
|
* `each` is the scheduling primitive under the collection helpers. Each one is
|
|
3076
3063
|
* a small `onResult` policy:
|
|
3077
3064
|
*
|
|
3078
|
-
* | Helper | Policy
|
|
3079
|
-
* | --------------------- |
|
|
3080
|
-
* | {@link all} | Collect values, stop on the first Err |
|
|
3081
|
-
* | {@link allSettled} | Collect every Result, never stop
|
|
3082
|
-
* | {@link any} | Stop on the first Ok |
|
|
3083
|
-
* | {@link race} | Stop on the first settled Result
|
|
3084
|
-
* | {@link firstN} | Stop after n Ok values
|
|
3085
|
-
* | {@link firstNSettled} | Stop after n Results
|
|
3065
|
+
* | Helper | Policy |
|
|
3066
|
+
* | --------------------- | --------------------------------------------- |
|
|
3067
|
+
* | {@link all} | Collect values, stop on the first {@link Err} |
|
|
3068
|
+
* | {@link allSettled} | Collect every Result, never stop |
|
|
3069
|
+
* | {@link any} | Stop on the first {@link Ok} |
|
|
3070
|
+
* | {@link race} | Stop on the first settled Result |
|
|
3071
|
+
* | {@link firstN} | Stop after n Ok values |
|
|
3072
|
+
* | {@link firstNSettled} | Stop after n Results |
|
|
3086
3073
|
*
|
|
3087
3074
|
* Use `each` directly to build a collection policy the helpers don't cover. For
|
|
3088
3075
|
* example, keep the first successful value together with its original input
|
|
@@ -3098,6 +3085,7 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
|
|
|
3098
3085
|
* ok,
|
|
3099
3086
|
* sleep,
|
|
3100
3087
|
* type Task,
|
|
3088
|
+
* type Typed,
|
|
3101
3089
|
* } from "@evolu/common";
|
|
3102
3090
|
*
|
|
3103
3091
|
* let slowCompleted = false;
|
|
@@ -3106,11 +3094,12 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
|
|
|
3106
3094
|
* slowCompleted = true;
|
|
3107
3095
|
* return ok("slow");
|
|
3108
3096
|
* };
|
|
3109
|
-
* const
|
|
3110
|
-
*
|
|
3111
|
-
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3097
|
+
* const unavailable: Task<never, ServiceUnavailableError> = () =>
|
|
3098
|
+
* err({ type: "ServiceUnavailable" });
|
|
3099
|
+
*
|
|
3100
|
+
* interface ServiceUnavailableError extends Typed<"ServiceUnavailable"> {}
|
|
3101
|
+
*
|
|
3102
|
+
* const tasks = [slow, unavailable, () => ok("fast")] as const;
|
|
3114
3103
|
* let first: readonly [string, number] | undefined;
|
|
3115
3104
|
* await using run = createRun();
|
|
3116
3105
|
* const result = await run(
|
|
@@ -3144,14 +3133,13 @@ export type EachCallback<TTasks extends NonEmptyReadonlyArray<AnyTask>> = (resul
|
|
|
3144
3133
|
* building on `each` preserves diagnostics that a hand-rolled scheduling loop
|
|
3145
3134
|
* typically loses.
|
|
3146
3135
|
*
|
|
3147
|
-
* @group
|
|
3136
|
+
* @group Collection
|
|
3148
3137
|
*/
|
|
3149
3138
|
export declare const each: <TTasks extends NonEmptyReadonlyArray<AnyTask>>(tasks: TTasks, onResult: EachCallback<TTasks>, options?: TaskCollectionOptions) => Task<void, never, InferTasksDeps<TTasks>>;
|
|
3150
3139
|
/**
|
|
3151
|
-
* Scheduler priority for
|
|
3140
|
+
* Scheduler priority for {@link Task}s started through a native scheduler.
|
|
3152
3141
|
*
|
|
3153
|
-
*
|
|
3154
|
-
* added in a future release.
|
|
3142
|
+
* A Task's priority is static while it runs.
|
|
3155
3143
|
*
|
|
3156
3144
|
* @group Scheduling
|
|
3157
3145
|
*/
|
|
@@ -3188,9 +3176,9 @@ export declare const prioritized: <T, E, D = unknown>(priority: TaskPriority, ta
|
|
|
3188
3176
|
* Yields execution to the host scheduler.
|
|
3189
3177
|
*
|
|
3190
3178
|
* Uses native `scheduler.yield()` when available, `setImmediate` when
|
|
3191
|
-
* available, and `setTimeout` elsewhere. Because this is a Task, `await
|
|
3192
|
-
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
3193
|
-
* monitoring.
|
|
3179
|
+
* available, and `setTimeout` elsewhere. Because this is a {@link Task}, `await
|
|
3180
|
+
* run(yieldNow)` is an explicit abortable checkpoint and is visible in
|
|
3181
|
+
* {@link Run} monitoring.
|
|
3194
3182
|
*
|
|
3195
3183
|
* For example, call it periodically in a long-running synchronous loop to let
|
|
3196
3184
|
* the host process rendering, input, and other scheduled work.
|
|
@@ -3241,12 +3229,13 @@ export declare const yieldNow: Task<void>;
|
|
|
3241
3229
|
* interface ServerDep {
|
|
3242
3230
|
* readonly port: number;
|
|
3243
3231
|
* }
|
|
3232
|
+
*
|
|
3244
3233
|
* interface Server extends AsyncDisposable {}
|
|
3245
3234
|
*
|
|
3246
3235
|
* const serverStarted = Promise.withResolvers<void>();
|
|
3247
3236
|
* let serverStopped = false;
|
|
3248
|
-
* const startServer: Task<Server, never, ServerDep> = (
|
|
3249
|
-
* expect(deps.port).toBe(3000);
|
|
3237
|
+
* const startServer: Task<Server, never, ServerDep> = (run) => {
|
|
3238
|
+
* expect(run.deps.port).toBe(3000);
|
|
3250
3239
|
* serverStarted.resolve();
|
|
3251
3240
|
* return ok({
|
|
3252
3241
|
* [Symbol.asyncDispose]: async () => {
|
|
@@ -3296,7 +3285,7 @@ export declare const waitForAbort: Task<never>;
|
|
|
3296
3285
|
* Do not wrap a Task that keeps using a resource the caller may release after
|
|
3297
3286
|
* this wrapper returns, unless the Task reliably observes abort before using
|
|
3298
3287
|
* that resource. The daemon Task can continue after the caller stops waiting.
|
|
3299
|
-
* Later domain
|
|
3288
|
+
* Later domain {@link Err} results from the daemon Task are discarded after the
|
|
3300
3289
|
* caller stops waiting. Defects from the daemon Task remain visible to the root
|
|
3301
3290
|
* Run: if it later throws or rejects, the root Run still panics and reports the
|
|
3302
3291
|
* defect.
|
|
@@ -3306,10 +3295,10 @@ export declare const waitForAbort: Task<never>;
|
|
|
3306
3295
|
* to settle, keeping cleanup and late defects inside the caller's lifetime. A
|
|
3307
3296
|
* Task that ignores abort can keep them waiting.
|
|
3308
3297
|
*
|
|
3309
|
-
* `run.abortable(task)` returns an owned child Fiber and requests abort
|
|
3310
|
-
* that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
3311
|
-
* current Run aborts. {@link unabortable} masks abort for a Task that
|
|
3312
|
-
* finish once started; daemon lets a Task outlive the caller.
|
|
3298
|
+
* `run.abortable(task)` returns an owned child {@link Fiber} and requests abort
|
|
3299
|
+
* through that Fiber; `daemon(task)` starts a daemon child and stops waiting
|
|
3300
|
+
* when the current Run aborts. {@link unabortable} masks abort for a Task that
|
|
3301
|
+
* must finish once started; daemon lets a Task outlive the caller.
|
|
3313
3302
|
*
|
|
3314
3303
|
* Because the Task starts with {@link Run.daemon}, a recorded abort request
|
|
3315
3304
|
* returns AbortError before the Task starts — including a request masked by
|
|
@@ -3356,15 +3345,10 @@ export declare const waitForAbort: Task<never>;
|
|
|
3356
3345
|
* import { createRun, ok, type Result, type Task } from "@evolu/common";
|
|
3357
3346
|
*
|
|
3358
3347
|
* type ResultValue = string;
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
* }
|
|
3362
|
-
* const createPromiseReturningResult = (): Promise<
|
|
3363
|
-
* Result<ResultValue, MyError>
|
|
3364
|
-
* > => Promise.resolve(ok("value"));
|
|
3348
|
+
* const createPromiseReturningResult = (): Promise<Result<ResultValue>> =>
|
|
3349
|
+
* Promise.resolve(ok("value"));
|
|
3365
3350
|
*
|
|
3366
|
-
* const task: Task<ResultValue
|
|
3367
|
-
* createPromiseReturningResult();
|
|
3351
|
+
* const task: Task<ResultValue> = () => createPromiseReturningResult();
|
|
3368
3352
|
*
|
|
3369
3353
|
* await using run = createRun();
|
|
3370
3354
|
* expectOk(await run(task), "value");
|
|
@@ -3377,12 +3361,9 @@ export declare const waitForAbort: Task<never>;
|
|
|
3377
3361
|
* import { ok, type Result, type Task } from "@evolu/common";
|
|
3378
3362
|
*
|
|
3379
3363
|
* type ResultValue = string;
|
|
3380
|
-
* interface MyError {
|
|
3381
|
-
* readonly type: "MyError";
|
|
3382
|
-
* }
|
|
3383
3364
|
* let promiseStarted = false;
|
|
3384
3365
|
* const createPromiseReturningResult = (): Promise<
|
|
3385
|
-
* Result<ResultValue
|
|
3366
|
+
* Result<ResultValue>
|
|
3386
3367
|
* > => {
|
|
3387
3368
|
* promiseStarted = true;
|
|
3388
3369
|
* return Promise.resolve(ok("value"));
|
|
@@ -3390,10 +3371,10 @@ export declare const waitForAbort: Task<never>;
|
|
|
3390
3371
|
*
|
|
3391
3372
|
* // Wrong: the Promise starts now, before a Run starts the Task.
|
|
3392
3373
|
* const promise = createPromiseReturningResult();
|
|
3393
|
-
* const task: Task<ResultValue
|
|
3374
|
+
* const task: Task<ResultValue> = () => promise;
|
|
3394
3375
|
*
|
|
3395
3376
|
* expect(promiseStarted).toBe(true);
|
|
3396
|
-
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue
|
|
3377
|
+
* expectTypeOf(task).toEqualTypeOf<Task<ResultValue>>();
|
|
3397
3378
|
* ```
|
|
3398
3379
|
*
|
|
3399
3380
|
* @group Lifetime
|
|
@@ -3405,8 +3386,8 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
|
|
|
3405
3386
|
* Abort requests are masked while the Task runs, so `run.signal.aborted`
|
|
3406
3387
|
* remains false inside the Task. This does not force the Task to start after an
|
|
3407
3388
|
* abort request has already reached its Run; unabortable means the Task is not
|
|
3408
|
-
* interrupted once it has started. Disposing the enclosing Run still
|
|
3409
|
-
* the Task to settle.
|
|
3389
|
+
* interrupted once it has started. Disposing the enclosing {@link Run} still
|
|
3390
|
+
* waits for the Task to settle.
|
|
3410
3391
|
*
|
|
3411
3392
|
* Apply at most one abort behavior helper to a Task: do not wrap the same Task
|
|
3412
3393
|
* with both unabortable and restore, or apply either helper more than once.
|
|
@@ -3418,10 +3399,10 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
|
|
|
3418
3399
|
*
|
|
3419
3400
|
* const commitStarted = Promise.withResolvers<void>();
|
|
3420
3401
|
* const finishCommit = Promise.withResolvers<void>();
|
|
3421
|
-
* const commit: Task<string> = unabortable(async (
|
|
3402
|
+
* const commit: Task<string> = unabortable(async (run) => {
|
|
3422
3403
|
* commitStarted.resolve();
|
|
3423
3404
|
* await finishCommit.promise;
|
|
3424
|
-
* expect(signal.aborted).toBe(false);
|
|
3405
|
+
* expect(run.signal.aborted).toBe(false);
|
|
3425
3406
|
* return ok("committed");
|
|
3426
3407
|
* });
|
|
3427
3408
|
*
|
|
@@ -3438,8 +3419,8 @@ export declare const daemon: <T, E, D = unknown>(task: Task<T, E, D>) => Task<T,
|
|
|
3438
3419
|
*/
|
|
3439
3420
|
export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
3440
3421
|
/**
|
|
3441
|
-
* Like {@link unabortable}, but provides `restore` for child
|
|
3442
|
-
* run with the previous abort mask.
|
|
3422
|
+
* Like {@link unabortable}, but provides `restore` for child {@link Task}s that
|
|
3423
|
+
* should run with the previous abort mask.
|
|
3443
3424
|
*
|
|
3444
3425
|
* Use this for acquire/use/release flows where acquire and release must finish
|
|
3445
3426
|
* once started, while use should remain abortable. Child Tasks inherit the mask
|
|
@@ -3467,8 +3448,6 @@ export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D
|
|
|
3467
3448
|
* type Task,
|
|
3468
3449
|
* } from "@evolu/common";
|
|
3469
3450
|
*
|
|
3470
|
-
* await using run = createRun();
|
|
3471
|
-
*
|
|
3472
3451
|
* interface Resource {
|
|
3473
3452
|
* readonly id: string;
|
|
3474
3453
|
* }
|
|
@@ -3485,13 +3464,14 @@ export declare const unabortable: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D
|
|
|
3485
3464
|
* let released = false;
|
|
3486
3465
|
* const release =
|
|
3487
3466
|
* (_resource: Resource): Task<void> =>
|
|
3488
|
-
* (
|
|
3467
|
+
* (run) => {
|
|
3489
3468
|
* // Release inherits the mask even after abort was requested.
|
|
3490
|
-
* expect(signal.aborted).toBe(false);
|
|
3469
|
+
* expect(run.signal.aborted).toBe(false);
|
|
3491
3470
|
* released = true;
|
|
3492
3471
|
* return ok();
|
|
3493
3472
|
* };
|
|
3494
3473
|
*
|
|
3474
|
+
* await using run = createRun();
|
|
3495
3475
|
* const fiber = run.abortable(
|
|
3496
3476
|
* unabortableMask((restore) => async (run) => {
|
|
3497
3477
|
* // Acquire with abort masked.
|
|
@@ -3544,6 +3524,7 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
|
|
|
3544
3524
|
* err,
|
|
3545
3525
|
* ok,
|
|
3546
3526
|
* type Task,
|
|
3527
|
+
* type Typed,
|
|
3547
3528
|
* } from "@evolu/common";
|
|
3548
3529
|
*
|
|
3549
3530
|
* interface Connection {
|
|
@@ -3554,9 +3535,14 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
|
|
|
3554
3535
|
* const openConnection: Task<Connection> = () =>
|
|
3555
3536
|
* ok({ user: "Ada", isAvailable: false });
|
|
3556
3537
|
* const loadUser =
|
|
3557
|
-
* (connection: Connection): Task<string,
|
|
3538
|
+
* (connection: Connection): Task<string, UserUnavailableError> =>
|
|
3558
3539
|
* () =>
|
|
3559
|
-
* connection.isAvailable
|
|
3540
|
+
* connection.isAvailable
|
|
3541
|
+
* ? ok(connection.user)
|
|
3542
|
+
* : err({ type: "UserUnavailable" });
|
|
3543
|
+
*
|
|
3544
|
+
* interface UserUnavailableError extends Typed<"UserUnavailable"> {}
|
|
3545
|
+
*
|
|
3560
3546
|
* let connectionClosed = false;
|
|
3561
3547
|
* const closeConnection =
|
|
3562
3548
|
* (_connection: Connection): Task<void> =>
|
|
@@ -3572,26 +3558,38 @@ export declare const unabortableMask: <T, E, D = unknown>(fn: (restore: <T2, E2,
|
|
|
3572
3558
|
* );
|
|
3573
3559
|
*
|
|
3574
3560
|
* await using run = createRun();
|
|
3575
|
-
* expectErr(await run(queryUser), "
|
|
3561
|
+
* expectErr(await run(queryUser), { type: "UserUnavailable" });
|
|
3576
3562
|
* // Release still runs when use returns a domain error.
|
|
3577
3563
|
* expect(connectionClosed).toBe(true);
|
|
3578
3564
|
* ```
|
|
3579
3565
|
*
|
|
3580
|
-
* @group
|
|
3566
|
+
* @group Lifetime
|
|
3581
3567
|
*/
|
|
3582
3568
|
export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire = unknown, DUse = unknown, DRelease = unknown>(acquire: Task<Resource, EAcquire, DAcquire>, use: (resource: Resource) => Task<T, EUse, DUse>, release: (resource: Resource) => Task<void, never, DRelease>) => Task<T, EAcquire | EUse, DAcquire & DUse & DRelease>;
|
|
3583
3569
|
/**
|
|
3584
3570
|
* A one-shot value resolved from outside the waiting {@link Task}.
|
|
3585
3571
|
*
|
|
3586
|
-
* Use Deferred when Task code must wait for a Result completed by an
|
|
3587
|
-
* callback or another Task. Deferred is `Promise.withResolvers` with
|
|
3588
|
-
* semantics: each waiter uses its waiting Run lifetime, can abort
|
|
3572
|
+
* Use Deferred when Task code must wait for a {@link Result} completed by an
|
|
3573
|
+
* external callback or another Task. Deferred is `Promise.withResolvers` with
|
|
3574
|
+
* Task semantics: each waiter uses its waiting {@link Run} lifetime, can abort
|
|
3589
3575
|
* independently, appears in Run observability, and settles with Result-based
|
|
3590
3576
|
* errors.
|
|
3591
3577
|
*
|
|
3592
3578
|
* The Deferred resolves once. Later calls to {@link Deferred.resolve} return
|
|
3593
3579
|
* `false` and do not change the Result.
|
|
3594
3580
|
*
|
|
3581
|
+
* @group Concurrency primitives
|
|
3582
|
+
* @see {@link createDeferred}
|
|
3583
|
+
*/
|
|
3584
|
+
export interface Deferred<T, E = never> {
|
|
3585
|
+
/** Waits until {@link Deferred.resolve} resolves the Deferred. */
|
|
3586
|
+
readonly task: Task<T, E>;
|
|
3587
|
+
/** Resolves the Deferred, returning whether this call completed it. */
|
|
3588
|
+
readonly resolve: (result: Result<T, E>) => boolean;
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Creates a {@link Deferred}.
|
|
3592
|
+
*
|
|
3595
3593
|
* ### Example
|
|
3596
3594
|
*
|
|
3597
3595
|
* ```ts
|
|
@@ -3618,7 +3616,7 @@ export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire =
|
|
|
3618
3616
|
* expectOk(await run(deferred.task), "ready");
|
|
3619
3617
|
* ```
|
|
3620
3618
|
*
|
|
3621
|
-
* ###
|
|
3619
|
+
* ### Aborting a waiter
|
|
3622
3620
|
*
|
|
3623
3621
|
* ```ts
|
|
3624
3622
|
* import { AbortError, createDeferred, createRun } from "@evolu/common";
|
|
@@ -3635,22 +3633,10 @@ export declare const acquireUseRelease: <Resource, T, EAcquire, EUse, DAcquire =
|
|
|
3635
3633
|
* ```
|
|
3636
3634
|
*
|
|
3637
3635
|
* @group Concurrency primitives
|
|
3638
|
-
* @see {@link createDeferred}
|
|
3639
|
-
*/
|
|
3640
|
-
export interface Deferred<T, E = never> {
|
|
3641
|
-
/** Waits until {@link Deferred.resolve} resolves the Deferred. */
|
|
3642
|
-
readonly task: Task<T, E>;
|
|
3643
|
-
/** Resolves the Deferred, returning whether this call completed it. */
|
|
3644
|
-
readonly resolve: (result: Result<T, E>) => boolean;
|
|
3645
|
-
}
|
|
3646
|
-
/**
|
|
3647
|
-
* Creates a {@link Deferred}.
|
|
3648
|
-
*
|
|
3649
|
-
* @group Concurrency primitives
|
|
3650
3636
|
*/
|
|
3651
3637
|
export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
|
|
3652
3638
|
/**
|
|
3653
|
-
* A reusable gate for blocking and releasing
|
|
3639
|
+
* A reusable gate for blocking and releasing {@link Task}s.
|
|
3654
3640
|
*
|
|
3655
3641
|
* - **Closed**: Tasks wait.
|
|
3656
3642
|
* - **Open**: Tasks proceed.
|
|
@@ -3664,6 +3650,24 @@ export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
|
|
|
3664
3650
|
* {@link createGate} creates a closed Gate by default. Pass `isOpen: true` when
|
|
3665
3651
|
* work should proceed immediately.
|
|
3666
3652
|
*
|
|
3653
|
+
* @group Concurrency primitives
|
|
3654
|
+
* @see {@link createGate}
|
|
3655
|
+
*/
|
|
3656
|
+
export interface Gate {
|
|
3657
|
+
/** Waits while the gate is closed. */
|
|
3658
|
+
readonly wait: Task<void>;
|
|
3659
|
+
/** Opens the gate, releasing all waiters. Returns false when already open. */
|
|
3660
|
+
readonly open: () => boolean;
|
|
3661
|
+
/** Closes the gate. Returns false when already closed. */
|
|
3662
|
+
readonly close: () => boolean;
|
|
3663
|
+
/** Releases the current closed wait cycle. Returns false when already open. */
|
|
3664
|
+
readonly release: () => boolean;
|
|
3665
|
+
/** Returns whether the gate is open. */
|
|
3666
|
+
readonly isOpen: () => boolean;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Creates a {@link Gate}.
|
|
3670
|
+
*
|
|
3667
3671
|
* ### Example
|
|
3668
3672
|
*
|
|
3669
3673
|
* ```ts
|
|
@@ -3693,30 +3697,12 @@ export declare const createDeferred: <T, E = never>() => Deferred<T, E>;
|
|
|
3693
3697
|
* ```
|
|
3694
3698
|
*
|
|
3695
3699
|
* @group Concurrency primitives
|
|
3696
|
-
* @see {@link createGate}
|
|
3697
|
-
*/
|
|
3698
|
-
export interface Gate {
|
|
3699
|
-
/** Waits while the gate is closed. */
|
|
3700
|
-
readonly wait: Task<void>;
|
|
3701
|
-
/** Opens the gate, releasing all waiters. Returns false when already open. */
|
|
3702
|
-
readonly open: () => boolean;
|
|
3703
|
-
/** Closes the gate. Returns false when already closed. */
|
|
3704
|
-
readonly close: () => boolean;
|
|
3705
|
-
/** Releases the current closed wait cycle. Returns false when already open. */
|
|
3706
|
-
readonly release: () => boolean;
|
|
3707
|
-
/** Returns whether the gate is open. */
|
|
3708
|
-
readonly isOpen: () => boolean;
|
|
3709
|
-
}
|
|
3710
|
-
/**
|
|
3711
|
-
* Creates a {@link Gate}.
|
|
3712
|
-
*
|
|
3713
|
-
* @group Concurrency primitives
|
|
3714
3700
|
*/
|
|
3715
3701
|
export declare const createGate: ({ isOpen, }?: {
|
|
3716
3702
|
isOpen?: boolean;
|
|
3717
3703
|
}) => Gate;
|
|
3718
3704
|
/**
|
|
3719
|
-
* Coordinates concurrent
|
|
3705
|
+
* Coordinates concurrent {@link Task}s by acquiring and releasing permits.
|
|
3720
3706
|
*
|
|
3721
3707
|
* Use {@link Semaphore.withPermit} or {@link Semaphore.withPermits} to acquire
|
|
3722
3708
|
* permits for one Task and release them when it settles. Use
|
|
@@ -3735,45 +3721,6 @@ export declare const createGate: ({ isOpen, }?: {
|
|
|
3735
3721
|
* already holding permits consumes additional permits and can wait if not
|
|
3736
3722
|
* enough permits are available.
|
|
3737
3723
|
*
|
|
3738
|
-
* ### Example
|
|
3739
|
-
*
|
|
3740
|
-
* ```ts
|
|
3741
|
-
* import {
|
|
3742
|
-
* createRun,
|
|
3743
|
-
* createSemaphore,
|
|
3744
|
-
* getOk,
|
|
3745
|
-
* ok,
|
|
3746
|
-
* sleep,
|
|
3747
|
-
* type Task,
|
|
3748
|
-
* } from "@evolu/common";
|
|
3749
|
-
*
|
|
3750
|
-
* await using run = createRun();
|
|
3751
|
-
*
|
|
3752
|
-
* const semaphore = createSemaphore(2);
|
|
3753
|
-
* let activeSaves = 0;
|
|
3754
|
-
* let maxActiveSaves = 0;
|
|
3755
|
-
*
|
|
3756
|
-
* const saveUser =
|
|
3757
|
-
* (id: string): Task<string> =>
|
|
3758
|
-
* async (run) => {
|
|
3759
|
-
* activeSaves += 1;
|
|
3760
|
-
* maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
|
|
3761
|
-
* await run.ok(sleep("10ms"));
|
|
3762
|
-
* activeSaves -= 1;
|
|
3763
|
-
* return ok(`saved:${id}`);
|
|
3764
|
-
* };
|
|
3765
|
-
*
|
|
3766
|
-
* const results = await Promise.all([
|
|
3767
|
-
* run(semaphore.withPermit(saveUser("1"))),
|
|
3768
|
-
* run(semaphore.withPermit(saveUser("2"))),
|
|
3769
|
-
* run(semaphore.withPermit(saveUser("3"))),
|
|
3770
|
-
* ]);
|
|
3771
|
-
*
|
|
3772
|
-
* const savedUsers = results.map(getOk);
|
|
3773
|
-
* expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
|
|
3774
|
-
* expect(maxActiveSaves).toBe(2);
|
|
3775
|
-
* ```
|
|
3776
|
-
*
|
|
3777
3724
|
* @group Concurrency primitives
|
|
3778
3725
|
* @see {@link createSemaphore}
|
|
3779
3726
|
*/
|
|
@@ -3793,8 +3740,8 @@ export interface Semaphore {
|
|
|
3793
3740
|
/**
|
|
3794
3741
|
* Acquires permits and returns an owned {@link SemaphorePermit}.
|
|
3795
3742
|
*
|
|
3796
|
-
* The Task waits until enough permits are available. Dispose or
|
|
3797
|
-
* returned permit to make them available again.
|
|
3743
|
+
* The {@link Task} waits until enough permits are available. Dispose or
|
|
3744
|
+
* release the returned permit to make them available again.
|
|
3798
3745
|
*
|
|
3799
3746
|
* When the request exceeds the current total permit count, the Task remains
|
|
3800
3747
|
* pending until {@link Semaphore.resize} increases capacity or the Task is
|
|
@@ -3894,6 +3841,45 @@ export interface SemaphoreSnapshot {
|
|
|
3894
3841
|
/**
|
|
3895
3842
|
* Creates a {@link Semaphore}.
|
|
3896
3843
|
*
|
|
3844
|
+
* ### Example
|
|
3845
|
+
*
|
|
3846
|
+
* ```ts
|
|
3847
|
+
* import {
|
|
3848
|
+
* createRun,
|
|
3849
|
+
* createSemaphore,
|
|
3850
|
+
* getOk,
|
|
3851
|
+
* ok,
|
|
3852
|
+
* sleep,
|
|
3853
|
+
* type Task,
|
|
3854
|
+
* } from "@evolu/common";
|
|
3855
|
+
*
|
|
3856
|
+
* await using run = createRun();
|
|
3857
|
+
*
|
|
3858
|
+
* const semaphore = createSemaphore(2);
|
|
3859
|
+
* let activeSaves = 0;
|
|
3860
|
+
* let maxActiveSaves = 0;
|
|
3861
|
+
*
|
|
3862
|
+
* const saveUser =
|
|
3863
|
+
* (id: string): Task<string> =>
|
|
3864
|
+
* async (run) => {
|
|
3865
|
+
* activeSaves += 1;
|
|
3866
|
+
* maxActiveSaves = Math.max(maxActiveSaves, activeSaves);
|
|
3867
|
+
* await run.ok(sleep("10ms"));
|
|
3868
|
+
* activeSaves -= 1;
|
|
3869
|
+
* return ok(`saved:${id}`);
|
|
3870
|
+
* };
|
|
3871
|
+
*
|
|
3872
|
+
* const results = await Promise.all([
|
|
3873
|
+
* run(semaphore.withPermit(saveUser("1"))),
|
|
3874
|
+
* run(semaphore.withPermit(saveUser("2"))),
|
|
3875
|
+
* run(semaphore.withPermit(saveUser("3"))),
|
|
3876
|
+
* ]);
|
|
3877
|
+
*
|
|
3878
|
+
* const savedUsers = results.map(getOk);
|
|
3879
|
+
* expect(savedUsers).toEqual(["saved:1", "saved:2", "saved:3"]);
|
|
3880
|
+
* expect(maxActiveSaves).toBe(2);
|
|
3881
|
+
* ```
|
|
3882
|
+
*
|
|
3897
3883
|
* @group Concurrency primitives
|
|
3898
3884
|
*/
|
|
3899
3885
|
export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, { policy, }?: {
|
|
@@ -3905,11 +3891,22 @@ export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, {
|
|
|
3905
3891
|
policy?: SemaphorePolicy;
|
|
3906
3892
|
}) => Semaphore;
|
|
3907
3893
|
/**
|
|
3908
|
-
* Runs
|
|
3894
|
+
* Runs {@link Task}s one at a time.
|
|
3909
3895
|
*
|
|
3910
3896
|
* `Mutex` is non-reentrant. A Task that tries to acquire the same Mutex while
|
|
3911
3897
|
* already holding it waits on itself and will not progress.
|
|
3912
3898
|
*
|
|
3899
|
+
* @group Concurrency primitives
|
|
3900
|
+
*/
|
|
3901
|
+
export interface Mutex {
|
|
3902
|
+
/** Runs a {@link Task} while holding the lock. */
|
|
3903
|
+
readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
3904
|
+
/** Returns the current lock state for monitoring and debugging. */
|
|
3905
|
+
readonly snapshot: () => SemaphoreSnapshot;
|
|
3906
|
+
}
|
|
3907
|
+
/**
|
|
3908
|
+
* Creates a {@link Mutex}.
|
|
3909
|
+
*
|
|
3913
3910
|
* ### Example
|
|
3914
3911
|
*
|
|
3915
3912
|
* ```ts
|
|
@@ -3943,20 +3940,9 @@ export declare const createSemaphore: (initialPermits: Int1To100OrPositiveInt, {
|
|
|
3943
3940
|
*
|
|
3944
3941
|
* @group Concurrency primitives
|
|
3945
3942
|
*/
|
|
3946
|
-
export interface Mutex {
|
|
3947
|
-
/** Runs a {@link Task} while holding the lock. */
|
|
3948
|
-
readonly withLock: <T, E, D>(task: Task<T, E, D>) => Task<T, E, D>;
|
|
3949
|
-
/** Returns the current lock state for monitoring and debugging. */
|
|
3950
|
-
readonly snapshot: () => SemaphoreSnapshot;
|
|
3951
|
-
}
|
|
3952
|
-
/**
|
|
3953
|
-
* Creates a {@link Mutex}.
|
|
3954
|
-
*
|
|
3955
|
-
* @group Concurrency primitives
|
|
3956
|
-
*/
|
|
3957
3943
|
export declare const createMutex: () => Mutex;
|
|
3958
3944
|
/**
|
|
3959
|
-
* Coordinates concurrent
|
|
3945
|
+
* Coordinates concurrent {@link Task}s independently for each key.
|
|
3960
3946
|
*
|
|
3961
3947
|
* `SemaphoreByKey` intentionally exposes only Task-scoped acquisition helpers,
|
|
3962
3948
|
* not the complete {@link Semaphore} API. Methods like {@link Semaphore.take} and
|
|
@@ -3969,54 +3955,6 @@ export declare const createMutex: () => Mutex;
|
|
|
3969
3955
|
* ownership should be tied to one Task lifetime and idle keys can be forgotten
|
|
3970
3956
|
* automatically.
|
|
3971
3957
|
*
|
|
3972
|
-
* ### Example
|
|
3973
|
-
*
|
|
3974
|
-
* ```ts
|
|
3975
|
-
* import {
|
|
3976
|
-
* createGate,
|
|
3977
|
-
* createRun,
|
|
3978
|
-
* createSemaphoreByKey,
|
|
3979
|
-
* getOk,
|
|
3980
|
-
* ok,
|
|
3981
|
-
* type Task,
|
|
3982
|
-
* } from "@evolu/common";
|
|
3983
|
-
*
|
|
3984
|
-
* // Each host gets an independent two-download limit.
|
|
3985
|
-
* const downloadsByHost = createSemaphoreByKey<string>(2);
|
|
3986
|
-
* const finishDownloads = createGate();
|
|
3987
|
-
* const firstBatchStarted = Promise.withResolvers<void>();
|
|
3988
|
-
* const started: Array<string> = [];
|
|
3989
|
-
* const download = (host: string, file: string): Task<string> =>
|
|
3990
|
-
* downloadsByHost.withPermit(host, async (run) => {
|
|
3991
|
-
* started.push(`${host}/${file}`);
|
|
3992
|
-
* if (started.length === 3) firstBatchStarted.resolve();
|
|
3993
|
-
* await run.ok(finishDownloads.wait);
|
|
3994
|
-
* return ok(`${host}/${file}`);
|
|
3995
|
-
* });
|
|
3996
|
-
*
|
|
3997
|
-
* await using run = createRun();
|
|
3998
|
-
* const downloads = [
|
|
3999
|
-
* run(download("a.example", "1.json")),
|
|
4000
|
-
* run(download("a.example", "2.json")),
|
|
4001
|
-
* run(download("a.example", "3.json")),
|
|
4002
|
-
* run(download("b.example", "1.json")),
|
|
4003
|
-
* ];
|
|
4004
|
-
* await firstBatchStarted.promise;
|
|
4005
|
-
* expect(started).toEqual([
|
|
4006
|
-
* "a.example/1.json",
|
|
4007
|
-
* "a.example/2.json",
|
|
4008
|
-
* "b.example/1.json",
|
|
4009
|
-
* ]);
|
|
4010
|
-
*
|
|
4011
|
-
* finishDownloads.open();
|
|
4012
|
-
* expect((await Promise.all(downloads)).map(getOk)).toEqual([
|
|
4013
|
-
* "a.example/1.json",
|
|
4014
|
-
* "a.example/2.json",
|
|
4015
|
-
* "a.example/3.json",
|
|
4016
|
-
* "b.example/1.json",
|
|
4017
|
-
* ]);
|
|
4018
|
-
* ```
|
|
4019
|
-
*
|
|
4020
3958
|
* @group Concurrency primitives
|
|
4021
3959
|
*/
|
|
4022
3960
|
export interface SemaphoreByKey<K = unknown> {
|
|
@@ -4039,56 +3977,40 @@ export interface CreateSemaphoreByKeyOptions<K, L = K> extends LookupOption<K, L
|
|
|
4039
3977
|
/**
|
|
4040
3978
|
* Creates a {@link SemaphoreByKey}.
|
|
4041
3979
|
*
|
|
4042
|
-
* @group Concurrency primitives
|
|
4043
|
-
*/
|
|
4044
|
-
export declare function createSemaphoreByKey<K = unknown>(initialPermits: Int1To100OrPositiveInt, options?: CreateSemaphoreByKeyOptions<K, unknown>): SemaphoreByKey<K>;
|
|
4045
|
-
export declare function createSemaphoreByKey<K, L>(initialPermits: Int1To100OrPositiveInt, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
|
|
4046
|
-
/**
|
|
4047
|
-
* Runs Tasks one at a time independently for each key, like {@link Mutex}.
|
|
4048
|
-
*
|
|
4049
3980
|
* ### Example
|
|
4050
3981
|
*
|
|
4051
3982
|
* ```ts
|
|
4052
3983
|
* import {
|
|
4053
|
-
* createGate,
|
|
4054
|
-
* createMutexByKey,
|
|
4055
3984
|
* createRun,
|
|
3985
|
+
* createSemaphoreByKey,
|
|
4056
3986
|
* ok,
|
|
4057
3987
|
* type Task,
|
|
4058
3988
|
* } from "@evolu/common";
|
|
4059
3989
|
*
|
|
4060
|
-
*
|
|
4061
|
-
* const
|
|
4062
|
-
* const
|
|
4063
|
-
*
|
|
4064
|
-
* const balancesByAccount = new Map<string, number>();
|
|
4065
|
-
* const deposit = (account: string, amount: number): Task<number> =>
|
|
4066
|
-
* accountLocks.withLock(account, async (run) => {
|
|
4067
|
-
* started.push(account);
|
|
4068
|
-
* if (started.length === 2) firstBatchStarted.resolve();
|
|
4069
|
-
* await run.ok(finishDeposits.wait);
|
|
4070
|
-
* const balance = (balancesByAccount.get(account) ?? 0) + amount;
|
|
4071
|
-
* balancesByAccount.set(account, balance);
|
|
4072
|
-
* return ok(balance);
|
|
4073
|
-
* });
|
|
3990
|
+
* // Each host gets an independent two-download limit.
|
|
3991
|
+
* const downloadsByHost = createSemaphoreByKey<string>(2);
|
|
3992
|
+
* const download = (host: string, file: string): Task<string> =>
|
|
3993
|
+
* downloadsByHost.withPermit(host, () => ok(`${host}/${file}`));
|
|
4074
3994
|
*
|
|
4075
3995
|
* await using run = createRun();
|
|
4076
|
-
*
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4079
|
-
*
|
|
4080
|
-
*
|
|
4081
|
-
* expect(started).toEqual(["checking", "savings"]);
|
|
4082
|
-
*
|
|
4083
|
-
* finishDeposits.open();
|
|
4084
|
-
* expectOk(await first, 2);
|
|
4085
|
-
* expectOk(await second, 5);
|
|
4086
|
-
* expectOk(await third, 4);
|
|
4087
|
-
* expect(started).toEqual(["checking", "savings", "checking"]);
|
|
3996
|
+
* expectOk(
|
|
3997
|
+
* await run(download("a.example", "index.json")),
|
|
3998
|
+
* "a.example/index.json",
|
|
3999
|
+
* );
|
|
4000
|
+
* expect(downloadsByHost.isIdle("a.example")).toBe(true);
|
|
4088
4001
|
* ```
|
|
4089
4002
|
*
|
|
4090
4003
|
* @group Concurrency primitives
|
|
4091
4004
|
*/
|
|
4005
|
+
export declare function createSemaphoreByKey<K = unknown>(initialPermits: Int1To100OrPositiveInt, options?: CreateSemaphoreByKeyOptions<K, unknown>): SemaphoreByKey<K>;
|
|
4006
|
+
/** Creates a {@link SemaphoreByKey} with custom logical key lookup. */
|
|
4007
|
+
export declare function createSemaphoreByKey<K, L>(initialPermits: Int1To100OrPositiveInt, options: CreateSemaphoreByKeyOptions<K, L>): SemaphoreByKey<K>;
|
|
4008
|
+
/**
|
|
4009
|
+
* Runs {@link Task}s one at a time independently for each key, like
|
|
4010
|
+
* {@link Mutex}.
|
|
4011
|
+
*
|
|
4012
|
+
* @group Concurrency primitives
|
|
4013
|
+
*/
|
|
4092
4014
|
export interface MutexByKey<K = unknown> {
|
|
4093
4015
|
/** Runs a {@link Task} while holding the lock for the key. */
|
|
4094
4016
|
readonly withLock: <T, E, D>(key: K, task: Task<T, E, D>) => Task<T, E, D>;
|
|
@@ -4107,9 +4029,35 @@ export interface CreateMutexByKeyOptions<K, L = K> extends LookupOption<K, L> {
|
|
|
4107
4029
|
/**
|
|
4108
4030
|
* Creates a {@link MutexByKey}.
|
|
4109
4031
|
*
|
|
4032
|
+
* ### Example
|
|
4033
|
+
*
|
|
4034
|
+
* ```ts
|
|
4035
|
+
* import {
|
|
4036
|
+
* createMutexByKey,
|
|
4037
|
+
* createRun,
|
|
4038
|
+
* ok,
|
|
4039
|
+
* type Task,
|
|
4040
|
+
* } from "@evolu/common";
|
|
4041
|
+
*
|
|
4042
|
+
* const accountLocks = createMutexByKey<string>();
|
|
4043
|
+
* const balancesByAccount = new Map<string, number>();
|
|
4044
|
+
* const deposit = (account: string, amount: number): Task<number> =>
|
|
4045
|
+
* accountLocks.withLock(account, () => {
|
|
4046
|
+
* const balance = (balancesByAccount.get(account) ?? 0) + amount;
|
|
4047
|
+
* balancesByAccount.set(account, balance);
|
|
4048
|
+
* return ok(balance);
|
|
4049
|
+
* });
|
|
4050
|
+
*
|
|
4051
|
+
* await using run = createRun();
|
|
4052
|
+
* expectOk(await run(deposit("checking", 2)), 2);
|
|
4053
|
+
* expectOk(await run(deposit("checking", 3)), 5);
|
|
4054
|
+
* expect(accountLocks.isIdle("checking")).toBe(true);
|
|
4055
|
+
* ```
|
|
4056
|
+
*
|
|
4110
4057
|
* @group Concurrency primitives
|
|
4111
4058
|
*/
|
|
4112
4059
|
export declare function createMutexByKey<K = unknown>(options?: CreateMutexByKeyOptions<K, unknown>): MutexByKey<K>;
|
|
4060
|
+
/** Creates a {@link MutexByKey} with custom logical key lookup. */
|
|
4113
4061
|
export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<K, L>): MutexByKey<K>;
|
|
4114
4062
|
/**
|
|
4115
4063
|
* {@link Ref} protected by a {@link Mutex}.
|
|
@@ -4122,57 +4070,13 @@ export declare function createMutexByKey<K, L>(options: CreateMutexByKeyOptions<
|
|
|
4122
4070
|
* internal Mutex, so calling another method on the same MutexRef from inside
|
|
4123
4071
|
* one of them waits on itself and will not progress.
|
|
4124
4072
|
*
|
|
4125
|
-
* Use it for state whose transitions are
|
|
4126
|
-
* Plain Ref cannot express that — between a sync read and a
|
|
4127
|
-
* concurrent transition can interleave and get lost.
|
|
4073
|
+
* Use it for state whose transitions are {@link Task}s: atomic async
|
|
4074
|
+
* read-modify-write. Plain Ref cannot express that — between a sync read and a
|
|
4075
|
+
* later write, a concurrent transition can interleave and get lost.
|
|
4128
4076
|
*
|
|
4129
|
-
* `MutexRef` operations are Tasks and incur normal Run lifecycle
|
|
4130
|
-
* {@link Ref} instead for synchronous state transitions,
|
|
4131
|
-
* allocation-sensitive hot paths.
|
|
4132
|
-
*
|
|
4133
|
-
* ### Example
|
|
4134
|
-
*
|
|
4135
|
-
* ```ts
|
|
4136
|
-
* import {
|
|
4137
|
-
* createGate,
|
|
4138
|
-
* createMutexRef,
|
|
4139
|
-
* createRun,
|
|
4140
|
-
* ok,
|
|
4141
|
-
* type Task,
|
|
4142
|
-
* } from "@evolu/common";
|
|
4143
|
-
*
|
|
4144
|
-
* await using run = createRun();
|
|
4145
|
-
*
|
|
4146
|
-
* const finishRefresh = createGate();
|
|
4147
|
-
* const refreshStarted = Promise.withResolvers<void>();
|
|
4148
|
-
* let fetchTokenCalls = 0;
|
|
4149
|
-
* const fetchToken: Task<string> = async (run) => {
|
|
4150
|
-
* fetchTokenCalls += 1;
|
|
4151
|
-
* refreshStarted.resolve();
|
|
4152
|
-
* await run.ok(finishRefresh.wait);
|
|
4153
|
-
* return ok("fresh-token");
|
|
4154
|
-
* };
|
|
4155
|
-
*
|
|
4156
|
-
* const tokenRef = createMutexRef<string | null>(null);
|
|
4157
|
-
*
|
|
4158
|
-
* // Concurrent callers never trigger duplicate refreshes: the first caller
|
|
4159
|
-
* // runs fetchToken under the lock; later callers reuse the stored token.
|
|
4160
|
-
* const getToken = tokenRef.updateAndGet((current) =>
|
|
4161
|
-
* current === null ? fetchToken : () => ok(current),
|
|
4162
|
-
* );
|
|
4163
|
-
*
|
|
4164
|
-
* const first = run(getToken);
|
|
4165
|
-
* await refreshStarted.promise;
|
|
4166
|
-
* const second = run(getToken);
|
|
4167
|
-
* finishRefresh.open();
|
|
4168
|
-
*
|
|
4169
|
-
* const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
4170
|
-
* expectTypeOf(getToken).toEqualTypeOf<Task<string | null>>();
|
|
4171
|
-
* expectOk(firstResult, "fresh-token");
|
|
4172
|
-
* expectOk(secondResult, "fresh-token");
|
|
4173
|
-
* expect(fetchTokenCalls).toBe(1);
|
|
4174
|
-
* expectOk(await run(tokenRef.get), "fresh-token");
|
|
4175
|
-
* ```
|
|
4077
|
+
* `MutexRef` operations are Tasks and incur normal {@link Run} lifecycle
|
|
4078
|
+
* overhead. Use {@link Ref} instead for synchronous state transitions,
|
|
4079
|
+
* especially on allocation-sensitive hot paths.
|
|
4176
4080
|
*
|
|
4177
4081
|
* @group Concurrency primitives
|
|
4178
4082
|
* @see {@link createMutexRef}
|
|
@@ -4200,6 +4104,19 @@ export interface MutexRef<T> {
|
|
|
4200
4104
|
/**
|
|
4201
4105
|
* Creates a {@link MutexRef}.
|
|
4202
4106
|
*
|
|
4107
|
+
* ### Example
|
|
4108
|
+
*
|
|
4109
|
+
* ```ts
|
|
4110
|
+
* import { createMutexRef, createRun, ok } from "@evolu/common";
|
|
4111
|
+
*
|
|
4112
|
+
* const counter = createMutexRef(0);
|
|
4113
|
+
* const increment = counter.updateAndGet((value) => () => ok(value + 1));
|
|
4114
|
+
*
|
|
4115
|
+
* await using run = createRun();
|
|
4116
|
+
* expectOk(await run(increment), 1);
|
|
4117
|
+
* expectOk(await run(counter.get), 1);
|
|
4118
|
+
* ```
|
|
4119
|
+
*
|
|
4203
4120
|
* @group Concurrency primitives
|
|
4204
4121
|
*/
|
|
4205
4122
|
export declare const createMutexRef: <T>(initialValue: T) => MutexRef<T>;
|