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.
- package/README.md +60 -29
- 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
|
-
|
|
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
|
|
|
@@ -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
|
-
-
|
|
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 —
|
|
288
|
+
Race Timeline — EU replica wins with abortLosers: true
|
|
258
289
|
|
|
259
290
|
Task │ 0ms 72ms 400ms
|
|
260
291
|
───────────┼─────────────────────┼──────────────────────────────────────────
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
292
|
+
eu │ ███████████████████ ● fulfilled winner
|
|
293
|
+
us │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
|
|
294
|
+
apac │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
|
|
264
295
|
↑
|
|
265
296
|
first settlement wins
|
|
266
297
|
|