better-race 0.2.0-next.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.
Files changed (2) hide show
  1. package/README.md +60 -29
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -18,13 +18,16 @@
18
18
  ```ts
19
19
  import { race } from "better-race";
20
20
 
21
- const winner = await race({
22
- cache: () => readCache(),
23
- api: ({ signal }) => fetchUser({ signal }),
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 === "api") {
27
- winner.value; // exactly the return type of fetchUser
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([readCache(), fetchUser()]);
37
- // CachedUser | ApiUser
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
- memory: () => memoryCache.get("user-42"),
59
- replica: ({ signal }) => replicaClient.get("user-42", { signal }),
60
- primary: ({ signal }) => primaryClient.get("user-42", { signal }),
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: "memory"; value: MemoryUser }
68
- | { key: "replica"; value: ReplicaUser }
69
- | { key: "primary"; value: PrimaryUser }
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 === "memory") {
76
- winner.value; // MemoryUser
77
- } else if (winner.key === "replica") {
78
- winner.value; // ReplicaUser
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; // PrimaryUser
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
- cache: readCache,
104
- network: ({ signal }) => fetch("/user", { signal }),
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
 
@@ -162,7 +168,32 @@ import type { RaceContext, RaceOptions, RaceResult, RaceTask, RaceTasks } from "
162
168
 
163
169
  ## `raceUntil(tasks, options)`
164
170
 
165
- `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.
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.
166
197
 
167
198
  ```ts
168
199
  import { raceUntil } from "better-race";
@@ -230,7 +261,7 @@ import {
230
261
 
231
262
  ## Use cases
232
263
 
233
- - Read from a memory cache, distributed cache, and primary store simultaneously.
264
+ - Query independent read replicas when lower tail latency is worth redundant read work.
234
265
  - Query independent replicas and return the fastest response.
235
266
  - Race a preferred endpoint against a fallback endpoint, then abort the fallback.
236
267
  - Preserve source information for metrics, tracing, or structured logging.
@@ -254,13 +285,13 @@ CI compiles and executes these examples against the packed package, not the sour
254
285
  A race is not a dependency graph: every task starts immediately. The important moment is the first settlement.
255
286
 
256
287
  ```text
257
- Race Timeline — cache wins with abortLosers: true
288
+ Race Timeline — EU replica wins with abortLosers: true
258
289
 
259
290
  Task │ 0ms 72ms 400ms
260
291
  ───────────┼─────────────────────┼──────────────────────────────────────────
261
- cache │ ███████████████████ ● fulfilled winner
262
- api │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
263
- replica │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
292
+ eu │ ███████████████████ ● fulfilled winner
293
+ us │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
294
+ apac │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
264
295
 
265
296
  first settlement wins
266
297
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-race",
3
- "version": "0.2.0-next.0",
3
+ "version": "0.2.0",
4
4
  "description": "A better Promise.race() for TypeScript: keyed results and optional cancellation.",
5
5
  "keywords": [
6
6
  "abortsignal",