better-race 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -31
- package/dist/index.d.mts +57 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +87 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
[](https://github.com/lumberjacque/better-race/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/better-race)
|
|
7
7
|
[](https://www.npmjs.com/package/better-race)
|
|
8
|
-
[](
|
|
8
|
+
[](https://github.com/lumberjacque/better-race/actions/workflows/ci.yml)
|
|
9
9
|
[](#public-types)
|
|
10
10
|
[](#installation)
|
|
11
|
-
[](https://deno.bundlejs.com/?q=better-race&treeshake=%5B%2A%5D)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
`ESM-only` · `zero runtime dependencies` · `tree-shakeable` · `AbortSignal`-native
|
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
```ts
|
|
19
19
|
import { race } from "better-race";
|
|
20
20
|
|
|
21
|
-
const winner = await race(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
21
|
+
const winner = await race(
|
|
22
|
+
{
|
|
23
|
+
eu: ({ signal }) => fetch("https://eu.example.com/user/42", { signal }),
|
|
24
|
+
us: ({ signal }) => fetch("https://us.example.com/user/42", { signal }),
|
|
25
|
+
},
|
|
26
|
+
{ abortLosers: true },
|
|
27
|
+
);
|
|
25
28
|
|
|
26
|
-
if (winner.key === "
|
|
27
|
-
winner.value; //
|
|
29
|
+
if (winner.key === "eu") {
|
|
30
|
+
winner.value; // the Response from the EU replica
|
|
28
31
|
}
|
|
29
32
|
```
|
|
30
33
|
|
|
@@ -33,8 +36,8 @@ if (winner.key === "api") {
|
|
|
33
36
|
Native `Promise.race()` returns a value union but drops its source:
|
|
34
37
|
|
|
35
38
|
```ts
|
|
36
|
-
const value = await Promise.race([
|
|
37
|
-
//
|
|
39
|
+
const value = await Promise.race([readEuReplica(), readUsReplica()]);
|
|
40
|
+
// EuUser | UsUser
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
You can restore the source by manually wrapping every promise. `better-race` makes that wrapper the default, preserves the relation as a discriminated union, and can abort cooperative losers when that saves work.
|
|
@@ -54,30 +57,33 @@ Pass an object whose string keys name tasks. A task can return a value, a promis
|
|
|
54
57
|
```ts
|
|
55
58
|
import { race } from "better-race";
|
|
56
59
|
|
|
57
|
-
const winner = await race(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
60
|
+
const winner = await race(
|
|
61
|
+
{
|
|
62
|
+
eu: ({ signal }) => euReplica.get("user-42", { signal }),
|
|
63
|
+
us: ({ signal }) => usReplica.get("user-42", { signal }),
|
|
64
|
+
apac: ({ signal }) => apacReplica.get("user-42", { signal }),
|
|
65
|
+
},
|
|
66
|
+
{ abortLosers: true },
|
|
67
|
+
);
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
The result is inferred as:
|
|
65
71
|
|
|
66
72
|
```ts
|
|
67
|
-
{ key: "
|
|
68
|
-
| { key: "
|
|
69
|
-
| { key: "
|
|
73
|
+
{ key: "eu"; value: EuUser }
|
|
74
|
+
| { key: "us"; value: UsUser }
|
|
75
|
+
| { key: "apac"; value: ApacUser }
|
|
70
76
|
```
|
|
71
77
|
|
|
72
78
|
Narrow the key and TypeScript narrows the value:
|
|
73
79
|
|
|
74
80
|
```ts
|
|
75
|
-
if (winner.key === "
|
|
76
|
-
winner.value; //
|
|
77
|
-
} else if (winner.key === "
|
|
78
|
-
winner.value; //
|
|
81
|
+
if (winner.key === "eu") {
|
|
82
|
+
winner.value; // EuUser
|
|
83
|
+
} else if (winner.key === "us") {
|
|
84
|
+
winner.value; // UsUser
|
|
79
85
|
} else {
|
|
80
|
-
winner.value; //
|
|
86
|
+
winner.value; // ApacUser
|
|
81
87
|
}
|
|
82
88
|
```
|
|
83
89
|
|
|
@@ -100,8 +106,8 @@ By default, losers keep running, exactly like `Promise.race()`:
|
|
|
100
106
|
|
|
101
107
|
```ts
|
|
102
108
|
const winner = await race({
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
eu: ({ signal }) => fetch("https://eu.example.com/user/42", { signal }),
|
|
110
|
+
us: ({ signal }) => fetch("https://us.example.com/user/42", { signal }),
|
|
105
111
|
});
|
|
106
112
|
```
|
|
107
113
|
|
|
@@ -160,13 +166,107 @@ import type { RaceContext, RaceOptions, RaceResult, RaceTask, RaceTasks } from "
|
|
|
160
166
|
|
|
161
167
|
`RaceContext` deliberately contains only `signal`. There are no framework adapters, schedulers, retries, timeouts, hooks, or hidden global state.
|
|
162
168
|
|
|
169
|
+
## `raceUntil(tasks, options)`
|
|
170
|
+
|
|
171
|
+
`raceUntil()` starts every task concurrently, but settles only when a fulfilled value passes `accept`. It is for “first usable result” cases where an early `null`, stale response, or unsuitable value should not end the race. It is not a replacement for a normal cache-first lookup: use it only when starting every candidate is an intentional latency or resilience trade-off.
|
|
172
|
+
|
|
173
|
+
### Keyed, cancellable `Promise.any()`
|
|
174
|
+
|
|
175
|
+
Use an always-accepting predicate when the first fulfilled result should win. In
|
|
176
|
+
that mode, `raceUntil()` is a keyed, cancellable alternative to `Promise.any()`:
|
|
177
|
+
rejections are skipped while another task can still fulfil, and the winner keeps
|
|
178
|
+
its task key.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const winner = await raceUntil(
|
|
182
|
+
{
|
|
183
|
+
cache: () => readCache(),
|
|
184
|
+
api: ({ signal }) => fetchUser({ signal }),
|
|
185
|
+
},
|
|
186
|
+
{ accept: () => true, abortLosers: true },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (winner.key === "api") {
|
|
190
|
+
winner.value; // User
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
If every task rejects, `raceUntil()` rejects with `NoAcceptedResultError`, which
|
|
195
|
+
extends `AggregateError`. Unlike native `Promise.any()`, its `rejections`
|
|
196
|
+
property retains each task key alongside the original rejection reason.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { raceUntil } from "better-race";
|
|
200
|
+
|
|
201
|
+
type User = { id: string };
|
|
202
|
+
|
|
203
|
+
const winner = await raceUntil(
|
|
204
|
+
{
|
|
205
|
+
memory: () => memoryCache.get("user-42"), // User | null
|
|
206
|
+
redis: () => redisCache.get("user-42"), // User | null
|
|
207
|
+
database: ({ signal }) => fetchUser("user-42", { signal }), // User
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
accept: (value): value is User => value !== null,
|
|
211
|
+
abortLosers: true,
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
winner.key; // "memory" | "redis" | "database"
|
|
216
|
+
winner.value; // User
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Semantics
|
|
220
|
+
|
|
221
|
+
- All tasks start in object property order before any fulfilment, rejection, or acceptance is processed.
|
|
222
|
+
- A fulfilled value for which `accept(value)` is `false` is declined; pending tasks keep racing.
|
|
223
|
+
- A task rejection is recorded and ignored while another task could still provide an accepted value.
|
|
224
|
+
- The first accepted value wins. With `abortLosers: true`, every pending loser receives an abort signal; otherwise it keeps running.
|
|
225
|
+
- If every task settles without an accepted value, `raceUntil()` rejects with `NoAcceptedResultError`.
|
|
226
|
+
- If `accept` throws, `raceUntil()` rejects with that exact error. External abort behaves exactly like `race()` and aborts all pending tasks with the caller’s original `signal.reason`.
|
|
227
|
+
|
|
228
|
+
`NoAcceptedResultError` extends `AggregateError`. Its `errors` array retains original rejection reasons, and its `rejections` property keeps each reason coupled to the task key:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
try {
|
|
232
|
+
await raceUntil(tasks, { accept: isUsable });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof NoAcceptedResultError) {
|
|
235
|
+
error.rejections; // readonly { key: string; reason: unknown }[]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Type narrowing
|
|
241
|
+
|
|
242
|
+
Use an explicit type predicate when the result must be narrowed on TypeScript 5.4 and newer:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
accept: (value): value is User => value !== null;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
A plain boolean callback is always valid, but preserves each task’s original value type. The keyed relationship remains intact in both forms.
|
|
249
|
+
|
|
250
|
+
### Public types
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import {
|
|
254
|
+
NoAcceptedResultError,
|
|
255
|
+
raceUntil,
|
|
256
|
+
type RaceUntilOptions,
|
|
257
|
+
type RaceUntilRejection,
|
|
258
|
+
type RaceUntilResult,
|
|
259
|
+
} from "better-race";
|
|
260
|
+
```
|
|
261
|
+
|
|
163
262
|
## Use cases
|
|
164
263
|
|
|
165
|
-
-
|
|
264
|
+
- Query independent read replicas when lower tail latency is worth redundant read work.
|
|
166
265
|
- Query independent replicas and return the fastest response.
|
|
167
266
|
- Race a preferred endpoint against a fallback endpoint, then abort the fallback.
|
|
168
267
|
- Preserve source information for metrics, tracing, or structured logging.
|
|
169
268
|
- Write compact TypeScript that narrows the result without hand-written wrapper objects.
|
|
269
|
+
- Query several stores in parallel until one returns a usable, non-null record.
|
|
170
270
|
|
|
171
271
|
## Examples
|
|
172
272
|
|
|
@@ -176,6 +276,7 @@ The executable examples are in [`examples/`](./examples):
|
|
|
176
276
|
- [`abort-losers.ts`](./examples/abort-losers.ts)
|
|
177
277
|
- [`external-abort.ts`](./examples/external-abort.ts)
|
|
178
278
|
- [`rejection-semantics.ts`](./examples/rejection-semantics.ts)
|
|
279
|
+
- [`until-accepted-result.ts`](./examples/until-accepted-result.ts)
|
|
179
280
|
|
|
180
281
|
CI compiles and executes these examples against the packed package, not the source tree.
|
|
181
282
|
|
|
@@ -184,20 +285,39 @@ CI compiles and executes these examples against the packed package, not the sour
|
|
|
184
285
|
A race is not a dependency graph: every task starts immediately. The important moment is the first settlement.
|
|
185
286
|
|
|
186
287
|
```text
|
|
187
|
-
Race Timeline —
|
|
288
|
+
Race Timeline — EU replica wins with abortLosers: true
|
|
188
289
|
|
|
189
290
|
Task │ 0ms 72ms 400ms
|
|
190
291
|
───────────┼─────────────────────┼──────────────────────────────────────────
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
292
|
+
eu │ ███████████████████ ● fulfilled winner
|
|
293
|
+
us │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
|
|
294
|
+
apac │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
|
|
194
295
|
↑
|
|
195
296
|
first settlement wins
|
|
196
297
|
|
|
197
298
|
Legend: █ active fulfilled work · ▓ active rejected work · ▒ active work aborted by the race
|
|
198
299
|
```
|
|
199
300
|
|
|
200
|
-
|
|
301
|
+
`raceUntil()` keeps the same concurrent start, but the line marks the first **accepted** value rather than the first settlement:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
RaceUntil Timeline — wait for an accepted result
|
|
305
|
+
|
|
306
|
+
Task │ Outcome │ Decision │ Timeline
|
|
307
|
+
───────────┼──────────────────┼─────────────────────────────┼────────────────────────────────────
|
|
308
|
+
memory │ null @ 24ms │ accept → false · decline │ ███ ▧ declined
|
|
309
|
+
redis │ Error @ 68ms │ record rejection · continue │ ████████ ▓ recorded
|
|
310
|
+
database │ User @ 176ms │ accept → true · winner │ ██████████████████████ ● accepted
|
|
311
|
+
backup │ pending @ 176ms │ abortLosers → abort │ ██████████████████████ ▒ cancelled
|
|
312
|
+
|
|
313
|
+
Legend: █ active task work · ▧ fulfilled candidate declined by accept · ▓ recorded rejection · ● accepted winner · ▒ cancelled loser
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Open [`playground/race-lab.html`](./playground/race-lab.html) directly in a browser to explore an animated version. It contains concrete `race()` and `raceUntil()` scenarios; the page is a visual prototype, not a shipped package artifact. Vitest and the packed-consumer test verify the runtime contract.
|
|
317
|
+
|
|
318
|
+
## Quality checks
|
|
319
|
+
|
|
320
|
+
The [CI workflow](https://github.com/lumberjacque/better-race/actions/workflows/ci.yml) runs the V8 coverage report on Node 22, 24, and 26. The configured threshold is exactly 100% for statements, branches, functions, and lines, so a green CI run is a verifiable guarantee rather than a decorative badge. Open the latest `Node 22` job to inspect its full report.
|
|
201
321
|
|
|
202
322
|
## Development
|
|
203
323
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/** A task rejection recorded while a `raceUntil()` call kept looking for an accepted value. */
|
|
3
|
+
interface RaceUntilRejection {
|
|
4
|
+
/** The task whose promise rejected. */
|
|
5
|
+
readonly key: string;
|
|
6
|
+
/** The original rejection reason from that task. */
|
|
7
|
+
readonly reason: unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when every `raceUntil()` task has settled but no fulfilled value was
|
|
11
|
+
* accepted. Rejections are preserved in both `errors` and `rejections`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class NoAcceptedResultError extends AggregateError {
|
|
14
|
+
/** The rejected tasks, including their keys and original reasons. */
|
|
15
|
+
readonly rejections: readonly RaceUntilRejection[];
|
|
16
|
+
constructor(rejections: readonly RaceUntilRejection[]);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
1
19
|
//#region src/types.d.ts
|
|
2
20
|
/** Context supplied to every task in a race. */
|
|
3
21
|
interface RaceContext {
|
|
@@ -25,6 +43,11 @@ interface RaceOptions {
|
|
|
25
43
|
*/
|
|
26
44
|
readonly abortLosers?: boolean;
|
|
27
45
|
}
|
|
46
|
+
/** Options that control which fulfilled values may win a {@link raceUntil}. */
|
|
47
|
+
interface RaceUntilOptions<TValue = unknown> extends RaceOptions {
|
|
48
|
+
/** Returns `true` when this fulfilled value should win the race. */
|
|
49
|
+
readonly accept: (value: TValue) => boolean;
|
|
50
|
+
}
|
|
28
51
|
type StringKeyOf<T> = Extract<keyof T, string>;
|
|
29
52
|
/**
|
|
30
53
|
* The discriminated union returned by {@link race}. Narrowing `key` narrows
|
|
@@ -36,6 +59,16 @@ type RaceResult<T extends RaceTasks> = { [K in StringKeyOf<T>]: {
|
|
|
36
59
|
}; }[StringKeyOf<T>];
|
|
37
60
|
/** @internal Rejects an empty object at compile time. */
|
|
38
61
|
type NonEmptyTasks<T extends RaceTasks> = keyof T extends never ? never : T;
|
|
62
|
+
/** The union of all values that tasks in a race can fulfil with. */
|
|
63
|
+
type RaceValue<T extends RaceTasks> = Awaited<ReturnType<T[StringKeyOf<T>]>>;
|
|
64
|
+
/**
|
|
65
|
+
* The keyed result returned by {@link raceUntil} when `accept` is a type
|
|
66
|
+
* predicate. Each task value is narrowed independently while keeping its key.
|
|
67
|
+
*/
|
|
68
|
+
type RaceUntilResult<T extends RaceTasks, Accepted> = { [K in StringKeyOf<T>]: {
|
|
69
|
+
readonly key: K;
|
|
70
|
+
readonly value: Extract<Awaited<ReturnType<T[K]>>, Accepted>;
|
|
71
|
+
}; }[StringKeyOf<T>];
|
|
39
72
|
//#endregion
|
|
40
73
|
//#region src/race.d.ts
|
|
41
74
|
/**
|
|
@@ -66,5 +99,28 @@ type NonEmptyTasks<T extends RaceTasks> = keyof T extends never ? never : T;
|
|
|
66
99
|
*/
|
|
67
100
|
export declare function race<const T extends RaceTasks>(tasks: NonEmptyTasks<T>, options?: RaceOptions): Promise<RaceResult<T>>;
|
|
68
101
|
//#endregion
|
|
69
|
-
|
|
102
|
+
//#region src/race-until.d.ts
|
|
103
|
+
type TypeGuardOptions<TValue, Accepted extends TValue> = RaceUntilOptions<TValue> & {
|
|
104
|
+
readonly accept: (value: TValue) => value is Accepted;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Races named tasks until a fulfilled value is accepted.
|
|
108
|
+
*
|
|
109
|
+
* Every task starts in object property order before any settlement is
|
|
110
|
+
* processed. Fulfilled values that `accept` rejects and task rejections are
|
|
111
|
+
* ignored while pending tasks remain. The first accepted value resolves with
|
|
112
|
+
* its keyed result. When every task settles without an accepted value, the
|
|
113
|
+
* promise rejects with {@link NoAcceptedResultError}.
|
|
114
|
+
*
|
|
115
|
+
* A type-predicate `accept` function narrows each keyed value in the result.
|
|
116
|
+
* Set `abortLosers` to abort cooperative pending tasks after acceptance; pass
|
|
117
|
+
* `signal` to cancel the whole pending race externally.
|
|
118
|
+
*
|
|
119
|
+
* @throws {TypeError} When `tasks` is empty or contains a non-function value.
|
|
120
|
+
* @throws {NoAcceptedResultError} When no fulfilled value is accepted.
|
|
121
|
+
*/
|
|
122
|
+
export declare function raceUntil<const T extends RaceTasks, Accepted extends RaceValue<T>>(tasks: NonEmptyTasks<T>, options: TypeGuardOptions<RaceValue<T>, Accepted>): Promise<RaceUntilResult<T, Accepted>>;
|
|
123
|
+
export declare function raceUntil<const T extends RaceTasks>(tasks: NonEmptyTasks<T>, options: RaceUntilOptions<RaceValue<T>>): Promise<RaceResult<T>>;
|
|
124
|
+
//#endregion
|
|
125
|
+
export type { RaceContext, RaceOptions, RaceResult, RaceTask, RaceTasks, RaceUntilOptions, RaceUntilRejection, RaceUntilResult, RaceValue };
|
|
70
126
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/race.ts"],"mappings":";;UACiB;;;;;WAKN,QAAQ;;;KAIP,SAAS,MAAM,SAAS,gBAAgB,IAAI,YAAY;;KAGxD,YAAY,SAAS,eAAe;;UAG/B;;;;;;WAMN,SAAS;;;;;WAMT;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/race.ts","../src/race-until.ts"],"mappings":";;UACiB;;WAEN;;WAGA;;;;;;qBAOE,8BAA8B;;WAEhC,qBAAqB;EAElB,YAAA,qBAAqB;;;;;UChBlB;;;;;WAKN,QAAQ;;;KAIP,SAAS,MAAM,SAAS,gBAAgB,IAAI,YAAY;;KAGxD,YAAY,SAAS,eAAe;;UAG/B;;;;;;WAMN,SAAS;;;;;WAMT;;;UAGM,iBAAiB,0BAA0B;;WAEjD,SAAS,OAAO;;KAGtB,YAAY,KAAK,cAAc;;;;;KAMxB,WAAW,UAAU,gBAC9B,KAAK,YAAY;WACP,KAAK;WACL,OAAO,QAAQ,WAAW,EAAE;KAEvC,YAAY;;KAGF,cAAc,UAAU,mBAAmB,0BAA0B;;KAErE,UAAU,UAAU,aAAa,QAAQ,WAAW,EAAE,YAAY;;;;;KAMlE,gBAAgB,UAAU,WAAW,eAC9C,KAAK,YAAY;WACP,KAAK;WACL,OAAO,QAAQ,QAAQ,WAAW,EAAE,MAAM;KAErD,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBC1BE,WAAW,UAAU,WACnC,OAAO,cAAc,IACrB,UAAS,cACR,QAAQ,WAAW;;;KC1BjB,iBAAiB,QAAQ,iBAAiB,UAAU,iBAAiB;WAC/D,SAAS,OAAO,WAAW,SAAS;;;;;;;;;;;;;;;;;;wBAmB/B,gBAAgB,UAAU,WAAW,iBAAiB,UAAU,IAC9E,OAAO,cAAc,IACrB,SAAS,iBAAiB,UAAU,IAAI,YACvC,QAAQ,gBAAgB,GAAG;wBACd,gBAAgB,UAAU,WACxC,OAAO,cAAc,IACrB,SAAS,iBAAiB,UAAU,MACnC,QAAQ,WAAW"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when every `raceUntil()` task has settled but no fulfilled value was
|
|
4
|
+
* accepted. Rejections are preserved in both `errors` and `rejections`.
|
|
5
|
+
*/
|
|
6
|
+
var NoAcceptedResultError = class extends AggregateError {
|
|
7
|
+
/** The rejected tasks, including their keys and original reasons. */
|
|
8
|
+
rejections;
|
|
9
|
+
constructor(rejections) {
|
|
10
|
+
super(rejections.map(({ reason }) => reason), "raceUntil() completed without an accepted result.");
|
|
11
|
+
this.name = "NoAcceptedResultError";
|
|
12
|
+
this.rejections = rejections;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
1
16
|
//#region src/race.ts
|
|
2
17
|
/**
|
|
3
18
|
* Races named tasks and returns both the first settled task's key and value.
|
|
@@ -70,6 +85,77 @@ function race(tasks, options = {}) {
|
|
|
70
85
|
});
|
|
71
86
|
}
|
|
72
87
|
//#endregion
|
|
73
|
-
|
|
88
|
+
//#region src/race-until.ts
|
|
89
|
+
function raceUntil(tasks, options) {
|
|
90
|
+
const entries = Object.entries(tasks);
|
|
91
|
+
if (entries.length === 0) throw new TypeError("raceUntil() requires at least one task.");
|
|
92
|
+
for (const [key, task] of entries) if (typeof task !== "function") throw new TypeError(`raceUntil() task "${key}" must be a function.`);
|
|
93
|
+
if (options.signal?.aborted) return Promise.reject(options.signal.reason);
|
|
94
|
+
const controllers = new Map(entries.map(([key]) => [key, new AbortController()]));
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let settled = false;
|
|
97
|
+
let remaining = entries.length;
|
|
98
|
+
const rejections = [];
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
101
|
+
};
|
|
102
|
+
const abortTasks = (winnerKey) => {
|
|
103
|
+
for (const [key, controller] of controllers) if (key !== winnerKey) controller.abort();
|
|
104
|
+
};
|
|
105
|
+
const settleAccepted = (key, value) => {
|
|
106
|
+
settled = true;
|
|
107
|
+
cleanup();
|
|
108
|
+
if (options.abortLosers) abortTasks(key);
|
|
109
|
+
resolve({
|
|
110
|
+
key,
|
|
111
|
+
value
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const settleFailure = (reason) => {
|
|
115
|
+
settled = true;
|
|
116
|
+
cleanup();
|
|
117
|
+
reject(reason);
|
|
118
|
+
};
|
|
119
|
+
const settleWithoutAcceptedValue = () => {
|
|
120
|
+
settled = true;
|
|
121
|
+
cleanup();
|
|
122
|
+
reject(new NoAcceptedResultError(rejections));
|
|
123
|
+
};
|
|
124
|
+
const onExternalAbort = () => {
|
|
125
|
+
settled = true;
|
|
126
|
+
cleanup();
|
|
127
|
+
abortTasks();
|
|
128
|
+
reject(options.signal?.reason);
|
|
129
|
+
};
|
|
130
|
+
options.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
131
|
+
for (const [key, task] of entries) {
|
|
132
|
+
const context = { signal: controllers.get(key).signal };
|
|
133
|
+
Promise.resolve().then(() => task(context)).then((value) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
try {
|
|
136
|
+
if (options.accept(value)) {
|
|
137
|
+
settleAccepted(key, value);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
} catch (reason) {
|
|
141
|
+
settleFailure(reason);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
remaining -= 1;
|
|
145
|
+
if (remaining === 0) settleWithoutAcceptedValue();
|
|
146
|
+
}, (reason) => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
rejections.push({
|
|
149
|
+
key,
|
|
150
|
+
reason
|
|
151
|
+
});
|
|
152
|
+
remaining -= 1;
|
|
153
|
+
if (remaining === 0) settleWithoutAcceptedValue();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { NoAcceptedResultError, race, raceUntil };
|
|
74
160
|
|
|
75
161
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/race.ts"],"sourcesContent":["import type {\n NonEmptyTasks,\n RaceContext,\n RaceOptions,\n RaceResult,\n RaceTask,\n RaceTasks,\n} from \"./types.js\";\n\ntype TaskEntry<T extends RaceTasks> = readonly [Extract<keyof T, string>, RaceTask<unknown>];\n\n/**\n * Races named tasks and returns both the first settled task's key and value.\n *\n * All tasks are started in object property order before any settlement is\n * processed. The first task to fulfil resolves with its keyed result; the\n * first task to reject rejects with its original reason, just like\n * `Promise.race()`.\n *\n * Set `abortLosers` to abort cooperative losers after the race settles. Pass\n * `signal` to cancel a still-pending race externally. A task must use the\n * supplied `AbortSignal` for cancellation to stop its underlying work.\n *\n * @example\n * ```ts\n * const winner = await race({\n * cache: () => readCache(),\n * api: ({ signal }) => fetchUser({ signal }),\n * });\n *\n * if (winner.key === \"api\") {\n * winner.value; // inferred from fetchUser\n * }\n * ```\n *\n * @throws {TypeError} When `tasks` is empty or contains a non-function value.\n */\nexport function race<const T extends RaceTasks>(\n tasks: NonEmptyTasks<T>,\n options: RaceOptions = {},\n): Promise<RaceResult<T>> {\n const entries = Object.entries(tasks) as unknown as TaskEntry<T>[];\n\n if (entries.length === 0) {\n throw new TypeError(\"race() requires at least one task.\");\n }\n\n for (const [key, task] of entries) {\n if (typeof task !== \"function\") {\n throw new TypeError(`race() task \"${key}\" must be a function.`);\n }\n }\n\n if (options.signal?.aborted) {\n return Promise.reject(options.signal.reason);\n }\n\n const controllers = new Map<string, AbortController>(\n entries.map(([key]) => [key, new AbortController()]),\n );\n\n return new Promise<RaceResult<T>>((resolve, reject) => {\n let settled = false;\n\n const cleanup = (): void => {\n options.signal?.removeEventListener(\"abort\", onExternalAbort);\n };\n\n const abortTasks = (winnerKey?: string): void => {\n for (const [key, controller] of controllers) {\n if (key !== winnerKey) {\n controller.abort();\n }\n }\n };\n\n const settleFulfilled = (key: string, value: unknown): void => {\n if (settled) return;\n\n settled = true;\n cleanup();\n if (options.abortLosers) abortTasks(key);\n resolve({ key, value } as RaceResult<T>);\n };\n\n const settleRejected = (key: string, reason: unknown): void => {\n if (settled) return;\n\n settled = true;\n cleanup();\n if (options.abortLosers) abortTasks(key);\n reject(reason);\n };\n\n const onExternalAbort = (): void => {\n settled = true;\n cleanup();\n abortTasks();\n reject(options.signal?.reason);\n };\n\n options.signal?.addEventListener(\"abort\", onExternalAbort, { once: true });\n\n for (const [key, task] of entries) {\n const controller = controllers.get(key)!;\n\n const context: RaceContext = { signal: controller.signal };\n Promise.resolve()\n .then(() => task(context))\n .then(\n (value) => settleFulfilled(key, value),\n (reason: unknown) => settleRejected(key, reason),\n );\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,KACd,OACA,UAAuB,CAAC,GACA;CACxB,MAAM,UAAU,OAAO,QAAQ,KAAK;CAEpC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,UAAU,oCAAoC;CAG1D,KAAK,MAAM,CAAC,KAAK,SAAS,SACxB,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,UAAU,gBAAgB,IAAI,sBAAsB;CAIlE,IAAI,QAAQ,QAAQ,SAClB,OAAO,QAAQ,OAAO,QAAQ,OAAO,MAAM;CAG7C,MAAM,cAAc,IAAI,IACtB,QAAQ,KAAK,CAAC,SAAS,CAAC,KAAK,IAAI,gBAAgB,CAAC,CAAC,CACrD;CAEA,OAAO,IAAI,SAAwB,SAAS,WAAW;EACrD,IAAI,UAAU;EAEd,MAAM,gBAAsB;GAC1B,QAAQ,QAAQ,oBAAoB,SAAS,eAAe;EAC9D;EAEA,MAAM,cAAc,cAA6B;GAC/C,KAAK,MAAM,CAAC,KAAK,eAAe,aAC9B,IAAI,QAAQ,WACV,WAAW,MAAM;EAGvB;EAEA,MAAM,mBAAmB,KAAa,UAAyB;GAC7D,IAAI,SAAS;GAEb,UAAU;GACV,QAAQ;GACR,IAAI,QAAQ,aAAa,WAAW,GAAG;GACvC,QAAQ;IAAE;IAAK;GAAM,CAAkB;EACzC;EAEA,MAAM,kBAAkB,KAAa,WAA0B;GAC7D,IAAI,SAAS;GAEb,UAAU;GACV,QAAQ;GACR,IAAI,QAAQ,aAAa,WAAW,GAAG;GACvC,OAAO,MAAM;EACf;EAEA,MAAM,wBAA8B;GAClC,UAAU;GACV,QAAQ;GACR,WAAW;GACX,OAAO,QAAQ,QAAQ,MAAM;EAC/B;EAEA,QAAQ,QAAQ,iBAAiB,SAAS,iBAAiB,EAAE,MAAM,KAAK,CAAC;EAEzE,KAAK,MAAM,CAAC,KAAK,SAAS,SAAS;GAGjC,MAAM,UAAuB,EAAE,QAFZ,YAAY,IAAI,GAEa,CAAC,CAAC,OAAO;GACzD,QAAQ,QAAQ,CAAC,CACd,WAAW,KAAK,OAAO,CAAC,CAAC,CACzB,MACE,UAAU,gBAAgB,KAAK,KAAK,IACpC,WAAoB,eAAe,KAAK,MAAM,CACjD;EACJ;CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/errors.ts","../src/race.ts","../src/race-until.ts"],"sourcesContent":["/** A task rejection recorded while a `raceUntil()` call kept looking for an accepted value. */\nexport interface RaceUntilRejection {\n /** The task whose promise rejected. */\n readonly key: string;\n\n /** The original rejection reason from that task. */\n readonly reason: unknown;\n}\n\n/**\n * Thrown when every `raceUntil()` task has settled but no fulfilled value was\n * accepted. Rejections are preserved in both `errors` and `rejections`.\n */\nexport class NoAcceptedResultError extends AggregateError {\n /** The rejected tasks, including their keys and original reasons. */\n readonly rejections: readonly RaceUntilRejection[];\n\n constructor(rejections: readonly RaceUntilRejection[]) {\n super(\n rejections.map(({ reason }) => reason),\n \"raceUntil() completed without an accepted result.\",\n );\n this.name = \"NoAcceptedResultError\";\n this.rejections = rejections;\n }\n}\n","import type {\n NonEmptyTasks,\n RaceContext,\n RaceOptions,\n RaceResult,\n RaceTask,\n RaceTasks,\n} from \"./types.js\";\n\ntype TaskEntry<T extends RaceTasks> = readonly [Extract<keyof T, string>, RaceTask<unknown>];\n\n/**\n * Races named tasks and returns both the first settled task's key and value.\n *\n * All tasks are started in object property order before any settlement is\n * processed. The first task to fulfil resolves with its keyed result; the\n * first task to reject rejects with its original reason, just like\n * `Promise.race()`.\n *\n * Set `abortLosers` to abort cooperative losers after the race settles. Pass\n * `signal` to cancel a still-pending race externally. A task must use the\n * supplied `AbortSignal` for cancellation to stop its underlying work.\n *\n * @example\n * ```ts\n * const winner = await race({\n * cache: () => readCache(),\n * api: ({ signal }) => fetchUser({ signal }),\n * });\n *\n * if (winner.key === \"api\") {\n * winner.value; // inferred from fetchUser\n * }\n * ```\n *\n * @throws {TypeError} When `tasks` is empty or contains a non-function value.\n */\nexport function race<const T extends RaceTasks>(\n tasks: NonEmptyTasks<T>,\n options: RaceOptions = {},\n): Promise<RaceResult<T>> {\n const entries = Object.entries(tasks) as unknown as TaskEntry<T>[];\n\n if (entries.length === 0) {\n throw new TypeError(\"race() requires at least one task.\");\n }\n\n for (const [key, task] of entries) {\n if (typeof task !== \"function\") {\n throw new TypeError(`race() task \"${key}\" must be a function.`);\n }\n }\n\n if (options.signal?.aborted) {\n return Promise.reject(options.signal.reason);\n }\n\n const controllers = new Map<string, AbortController>(\n entries.map(([key]) => [key, new AbortController()]),\n );\n\n return new Promise<RaceResult<T>>((resolve, reject) => {\n let settled = false;\n\n const cleanup = (): void => {\n options.signal?.removeEventListener(\"abort\", onExternalAbort);\n };\n\n const abortTasks = (winnerKey?: string): void => {\n for (const [key, controller] of controllers) {\n if (key !== winnerKey) {\n controller.abort();\n }\n }\n };\n\n const settleFulfilled = (key: string, value: unknown): void => {\n if (settled) return;\n\n settled = true;\n cleanup();\n if (options.abortLosers) abortTasks(key);\n resolve({ key, value } as RaceResult<T>);\n };\n\n const settleRejected = (key: string, reason: unknown): void => {\n if (settled) return;\n\n settled = true;\n cleanup();\n if (options.abortLosers) abortTasks(key);\n reject(reason);\n };\n\n const onExternalAbort = (): void => {\n settled = true;\n cleanup();\n abortTasks();\n reject(options.signal?.reason);\n };\n\n options.signal?.addEventListener(\"abort\", onExternalAbort, { once: true });\n\n for (const [key, task] of entries) {\n const controller = controllers.get(key)!;\n\n const context: RaceContext = { signal: controller.signal };\n Promise.resolve()\n .then(() => task(context))\n .then(\n (value) => settleFulfilled(key, value),\n (reason: unknown) => settleRejected(key, reason),\n );\n }\n });\n}\n","import { NoAcceptedResultError, type RaceUntilRejection } from \"./errors.js\";\nimport type {\n NonEmptyTasks,\n RaceContext,\n RaceResult,\n RaceTask,\n RaceTasks,\n RaceUntilOptions,\n RaceUntilResult,\n RaceValue,\n} from \"./types.js\";\n\ntype TaskEntry<T extends RaceTasks> = readonly [Extract<keyof T, string>, RaceTask<unknown>];\n\ntype TypeGuardOptions<TValue, Accepted extends TValue> = RaceUntilOptions<TValue> & {\n readonly accept: (value: TValue) => value is Accepted;\n};\n\n/**\n * Races named tasks until a fulfilled value is accepted.\n *\n * Every task starts in object property order before any settlement is\n * processed. Fulfilled values that `accept` rejects and task rejections are\n * ignored while pending tasks remain. The first accepted value resolves with\n * its keyed result. When every task settles without an accepted value, the\n * promise rejects with {@link NoAcceptedResultError}.\n *\n * A type-predicate `accept` function narrows each keyed value in the result.\n * Set `abortLosers` to abort cooperative pending tasks after acceptance; pass\n * `signal` to cancel the whole pending race externally.\n *\n * @throws {TypeError} When `tasks` is empty or contains a non-function value.\n * @throws {NoAcceptedResultError} When no fulfilled value is accepted.\n */\nexport function raceUntil<const T extends RaceTasks, Accepted extends RaceValue<T>>(\n tasks: NonEmptyTasks<T>,\n options: TypeGuardOptions<RaceValue<T>, Accepted>,\n): Promise<RaceUntilResult<T, Accepted>>;\nexport function raceUntil<const T extends RaceTasks>(\n tasks: NonEmptyTasks<T>,\n options: RaceUntilOptions<RaceValue<T>>,\n): Promise<RaceResult<T>>;\nexport function raceUntil<const T extends RaceTasks>(\n tasks: NonEmptyTasks<T>,\n options: RaceUntilOptions<RaceValue<T>>,\n): Promise<RaceResult<T>> {\n const entries = Object.entries(tasks) as unknown as TaskEntry<T>[];\n\n if (entries.length === 0) {\n throw new TypeError(\"raceUntil() requires at least one task.\");\n }\n\n for (const [key, task] of entries) {\n if (typeof task !== \"function\") {\n throw new TypeError(`raceUntil() task \"${key}\" must be a function.`);\n }\n }\n\n if (options.signal?.aborted) {\n return Promise.reject(options.signal.reason);\n }\n\n const controllers = new Map<string, AbortController>(\n entries.map(([key]) => [key, new AbortController()]),\n );\n\n return new Promise<RaceResult<T>>((resolve, reject) => {\n let settled = false;\n let remaining = entries.length;\n const rejections: RaceUntilRejection[] = [];\n\n const cleanup = (): void => {\n options.signal?.removeEventListener(\"abort\", onExternalAbort);\n };\n\n const abortTasks = (winnerKey?: string): void => {\n for (const [key, controller] of controllers) {\n if (key !== winnerKey) controller.abort();\n }\n };\n\n const settleAccepted = (key: string, value: unknown): void => {\n settled = true;\n cleanup();\n if (options.abortLosers) abortTasks(key);\n resolve({ key, value } as RaceResult<T>);\n };\n\n const settleFailure = (reason: unknown): void => {\n settled = true;\n cleanup();\n reject(reason);\n };\n\n const settleWithoutAcceptedValue = (): void => {\n settled = true;\n cleanup();\n reject(new NoAcceptedResultError(rejections));\n };\n\n const onExternalAbort = (): void => {\n settled = true;\n cleanup();\n abortTasks();\n reject(options.signal?.reason);\n };\n\n options.signal?.addEventListener(\"abort\", onExternalAbort, { once: true });\n\n for (const [key, task] of entries) {\n const controller = controllers.get(key)!;\n const context: RaceContext = { signal: controller.signal };\n\n Promise.resolve()\n .then(() => task(context))\n .then(\n (value) => {\n if (settled) return;\n\n try {\n if (options.accept(value as RaceValue<T>)) {\n settleAccepted(key, value);\n return;\n }\n } catch (reason) {\n settleFailure(reason);\n return;\n }\n\n remaining -= 1;\n if (remaining === 0) settleWithoutAcceptedValue();\n },\n (reason: unknown) => {\n if (settled) return;\n\n rejections.push({ key, reason });\n remaining -= 1;\n if (remaining === 0) settleWithoutAcceptedValue();\n },\n );\n }\n });\n}\n"],"mappings":"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"}
|