better-race 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShimG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # better-race
2
+
3
+ > A better `Promise.race()` for TypeScript: keyed results, precise narrowing, and optional cooperative cancellation.
4
+
5
+ [![CI](https://github.com/lumberjacque/better-race/actions/workflows/ci.yml/badge.svg)](https://github.com/lumberjacque/better-race/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/better-race?logo=npm&label=npm&color=cb3837)](https://www.npmjs.com/package/better-race)
7
+ [![npm downloads](https://img.shields.io/npm/dw/better-race?logo=npm&label=downloads)](https://www.npmjs.com/package/better-race)
8
+ [![coverage: 100%](https://img.shields.io/badge/coverage-100%25-21c55d?logo=vitest&logoColor=white)](#quality-checks)
9
+ [![TypeScript ≥5.4](https://img.shields.io/badge/TypeScript-%E2%89%A55.4-3178c6?logo=typescript&logoColor=white)](#public-types)
10
+ [![Node ≥22](https://img.shields.io/badge/Node-%E2%89%A522-339933?logo=nodedotjs&logoColor=white)](#installation)
11
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/better-race?label=minzipped)](https://bundlephobia.com/package/better-race)
12
+ [![license: MIT](https://img.shields.io/github/license/lumberjacque/better-race)](LICENSE)
13
+
14
+ `ESM-only` · `zero runtime dependencies` · `tree-shakeable` · `AbortSignal`-native
15
+
16
+ `Promise.race()` tells you the first value that settled. `better-race` also tells you **which task produced it**, while keeping that key and value connected in TypeScript.
17
+
18
+ ```ts
19
+ import { race } from "better-race";
20
+
21
+ const winner = await race({
22
+ cache: () => readCache(),
23
+ api: ({ signal }) => fetchUser({ signal }),
24
+ });
25
+
26
+ if (winner.key === "api") {
27
+ winner.value; // exactly the return type of fetchUser
28
+ }
29
+ ```
30
+
31
+ ## Why?
32
+
33
+ Native `Promise.race()` returns a value union but drops its source:
34
+
35
+ ```ts
36
+ const value = await Promise.race([readCache(), fetchUser()]);
37
+ // CachedUser | ApiUser
38
+ ```
39
+
40
+ 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.
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ npm install better-race
46
+ ```
47
+
48
+ `better-race` is ESM-only, targets ES2022, and supports Node.js 22 or newer. It is framework-agnostic: it uses only promises and the standard `AbortSignal` API.
49
+
50
+ ## `race(tasks, options?)`
51
+
52
+ Pass an object whose string keys name tasks. A task can return a value, a promise, or any `PromiseLike` value. It receives a context containing an `AbortSignal`, which it may ignore.
53
+
54
+ ```ts
55
+ import { race } from "better-race";
56
+
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
+ });
62
+ ```
63
+
64
+ The result is inferred as:
65
+
66
+ ```ts
67
+ { key: "memory"; value: MemoryUser }
68
+ | { key: "replica"; value: ReplicaUser }
69
+ | { key: "primary"; value: PrimaryUser }
70
+ ```
71
+
72
+ Narrow the key and TypeScript narrows the value:
73
+
74
+ ```ts
75
+ if (winner.key === "memory") {
76
+ winner.value; // MemoryUser
77
+ } else if (winner.key === "replica") {
78
+ winner.value; // ReplicaUser
79
+ } else {
80
+ winner.value; // PrimaryUser
81
+ }
82
+ ```
83
+
84
+ ### Semantics
85
+
86
+ `race()` intentionally follows the mental model of `Promise.race()`.
87
+
88
+ - Every task starts in object property order before any settlement is processed.
89
+ - The first task to settle wins, whether it fulfils or rejects.
90
+ - A first rejection rejects with the **original reason**; it is never wrapped.
91
+ - A synchronous task result works. A synchronous throw becomes a rejected race.
92
+ - `race({})` is rejected at the call site with a clear `TypeError`; TypeScript also rejects it.
93
+ - Only own, enumerable string keys are considered.
94
+
95
+ There is no priority system. Property order defines predictable startup order only—not a tie-breaker.
96
+
97
+ ### Optional loser cancellation
98
+
99
+ By default, losers keep running, exactly like `Promise.race()`:
100
+
101
+ ```ts
102
+ const winner = await race({
103
+ cache: readCache,
104
+ network: ({ signal }) => fetch("/user", { signal }),
105
+ });
106
+ ```
107
+
108
+ Set `abortLosers: true` to abort every still-running loser after the first settlement, including when the winning task rejects:
109
+
110
+ ```ts
111
+ const winner = await race(
112
+ {
113
+ primary: ({ signal }) => fetch("/primary", { signal }),
114
+ backup: ({ signal }) => fetch("/backup", { signal }),
115
+ },
116
+ { abortLosers: true },
117
+ );
118
+ ```
119
+
120
+ Cancellation is cooperative. Passing a signal does not magically stop synchronous CPU work or code that ignores it. `fetch`, many database clients, and your own signal-aware functions can react to it.
121
+
122
+ The winning task's signal is never aborted by `abortLosers`.
123
+
124
+ ### External cancellation
125
+
126
+ Pass a caller-owned signal to stop a pending race:
127
+
128
+ ```ts
129
+ const controller = new AbortController();
130
+
131
+ const winner = await race(
132
+ {
133
+ primary: ({ signal }) => fetch("/primary", { signal }),
134
+ backup: ({ signal }) => fetch("/backup", { signal }),
135
+ },
136
+ { signal: controller.signal, abortLosers: true },
137
+ );
138
+
139
+ controller.abort(new Error("User navigated away"));
140
+ ```
141
+
142
+ If the external signal is already aborted, no task starts. If it aborts while the race is pending, every task signal is aborted and the returned promise rejects with the exact `signal.reason`.
143
+
144
+ Once a task wins, the external listener is removed. With the default `abortLosers: false`, an external abort later does **not** retroactively cancel a loser that was deliberately allowed to continue.
145
+
146
+ ### Options
147
+
148
+ ```ts
149
+ interface RaceOptions {
150
+ signal?: AbortSignal;
151
+ abortLosers?: boolean; // false by default
152
+ }
153
+ ```
154
+
155
+ ### Public types
156
+
157
+ ```ts
158
+ import type { RaceContext, RaceOptions, RaceResult, RaceTask, RaceTasks } from "better-race";
159
+ ```
160
+
161
+ `RaceContext` deliberately contains only `signal`. There are no framework adapters, schedulers, retries, timeouts, hooks, or hidden global state.
162
+
163
+ ## Use cases
164
+
165
+ - Read from a memory cache, distributed cache, and primary store simultaneously.
166
+ - Query independent replicas and return the fastest response.
167
+ - Race a preferred endpoint against a fallback endpoint, then abort the fallback.
168
+ - Preserve source information for metrics, tracing, or structured logging.
169
+ - Write compact TypeScript that narrows the result without hand-written wrapper objects.
170
+
171
+ ## Examples
172
+
173
+ The executable examples are in [`examples/`](./examples):
174
+
175
+ - [`basic-keyed-result.ts`](./examples/basic-keyed-result.ts)
176
+ - [`abort-losers.ts`](./examples/abort-losers.ts)
177
+ - [`external-abort.ts`](./examples/external-abort.ts)
178
+ - [`rejection-semantics.ts`](./examples/rejection-semantics.ts)
179
+
180
+ CI compiles and executes these examples against the packed package, not the source tree.
181
+
182
+ ## Visual semantics
183
+
184
+ A race is not a dependency graph: every task starts immediately. The important moment is the first settlement.
185
+
186
+ ```text
187
+ Race Timeline — cache wins with abortLosers: true
188
+
189
+ Task │ 0ms 72ms 400ms
190
+ ───────────┼─────────────────────┼──────────────────────────────────────────
191
+ cache │ ███████████████████ ● fulfilled winner
192
+ api │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
193
+ replica │ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ × abort signal received
194
+
195
+ first settlement wins
196
+
197
+ Legend: █ active fulfilled work · ▓ active rejected work · ▒ active work aborted by the race
198
+ ```
199
+
200
+ Open [`playground/race-lab.html`](./playground/race-lab.html) directly in a browser to explore an animated version. It contains four concrete scenarios: fulfilment, first rejection, continuing losers, and external abort. The page is a visual prototype, not a shipped package artifact; Vitest and the packed-consumer test verify the runtime contract.
201
+
202
+ ## Development
203
+
204
+ ```sh
205
+ npm install
206
+ npm run verify
207
+ ```
208
+
209
+ `verify` runs formatting, linting, typechecking, 100% runtime coverage, type tests, a production build, and a packed-consumer test.
210
+
211
+ Useful commands:
212
+
213
+ ```sh
214
+ npm run format # Format source with oxfmt
215
+ npm run lint # Lint with oxlint
216
+ npm run test:coverage # Runtime tests with strict 100% coverage
217
+ npm run test:types # Compile-time inference tests
218
+ npm run build # ESM package and declaration output
219
+ npm run test:pack # Test the tarball from a clean consumer
220
+ ```
221
+
222
+ Husky runs formatting for staged files plus typechecking and tests before commits. Commit messages use the Conventional Commits specification.
223
+
224
+ ## Contributing and security
225
+
226
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) and [SECURITY.md](./SECURITY.md). This project is released under the [MIT License](./LICENSE).
@@ -0,0 +1,70 @@
1
+ //#region src/types.d.ts
2
+ /** Context supplied to every task in a race. */
3
+ interface RaceContext {
4
+ /**
5
+ * A signal that is aborted when the caller cancels a pending race or, when
6
+ * enabled, when another task settles first.
7
+ */
8
+ readonly signal: AbortSignal;
9
+ }
10
+ /** A synchronous or asynchronous operation that participates in a race. */
11
+ type RaceTask<T> = (context: RaceContext) => T | PromiseLike<T>;
12
+ /** A named collection of tasks accepted by {@link race}. */
13
+ type RaceTasks = Readonly<Record<string, RaceTask<unknown>>>;
14
+ /** Options that control cancellation for {@link race}. */
15
+ interface RaceOptions {
16
+ /**
17
+ * Cancels every still-running task while the race is pending. The race
18
+ * rejects with this signal's reason. Once a task wins, this signal is
19
+ * detached and can no longer affect continuing losers.
20
+ */
21
+ readonly signal?: AbortSignal;
22
+ /**
23
+ * Abort still-running losers when the first task settles. Defaults to
24
+ * `false`, matching the non-cancelling behaviour of `Promise.race()`.
25
+ */
26
+ readonly abortLosers?: boolean;
27
+ }
28
+ type StringKeyOf<T> = Extract<keyof T, string>;
29
+ /**
30
+ * The discriminated union returned by {@link race}. Narrowing `key` narrows
31
+ * `value` to the exact value type returned by that task.
32
+ */
33
+ type RaceResult<T extends RaceTasks> = { [K in StringKeyOf<T>]: {
34
+ readonly key: K;
35
+ readonly value: Awaited<ReturnType<T[K]>>;
36
+ }; }[StringKeyOf<T>];
37
+ /** @internal Rejects an empty object at compile time. */
38
+ type NonEmptyTasks<T extends RaceTasks> = keyof T extends never ? never : T;
39
+ //#endregion
40
+ //#region src/race.d.ts
41
+ /**
42
+ * Races named tasks and returns both the first settled task's key and value.
43
+ *
44
+ * All tasks are started in object property order before any settlement is
45
+ * processed. The first task to fulfil resolves with its keyed result; the
46
+ * first task to reject rejects with its original reason, just like
47
+ * `Promise.race()`.
48
+ *
49
+ * Set `abortLosers` to abort cooperative losers after the race settles. Pass
50
+ * `signal` to cancel a still-pending race externally. A task must use the
51
+ * supplied `AbortSignal` for cancellation to stop its underlying work.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const winner = await race({
56
+ * cache: () => readCache(),
57
+ * api: ({ signal }) => fetchUser({ signal }),
58
+ * });
59
+ *
60
+ * if (winner.key === "api") {
61
+ * winner.value; // inferred from fetchUser
62
+ * }
63
+ * ```
64
+ *
65
+ * @throws {TypeError} When `tasks` is empty or contains a non-function value.
66
+ */
67
+ export declare function race<const T extends RaceTasks>(tasks: NonEmptyTasks<T>, options?: RaceOptions): Promise<RaceResult<T>>;
68
+ //#endregion
69
+ export type { RaceContext, RaceOptions, RaceResult, RaceTask, RaceTasks };
70
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +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;;KAGN,YAAY,KAAK,cAAc;;;;;KAMxB,WAAW,UAAU,gBAC9B,KAAK,YAAY;WACP,KAAK;WACL,OAAO,QAAQ,WAAW,EAAE;KAEvC,YAAY;;KAGF,cAAc,UAAU,mBAAmB,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBCRjE,WAAW,UAAU,WACnC,OAAO,cAAc,IACrB,UAAS,cACR,QAAQ,WAAW"}
package/dist/index.mjs ADDED
@@ -0,0 +1,75 @@
1
+ //#region src/race.ts
2
+ /**
3
+ * Races named tasks and returns both the first settled task's key and value.
4
+ *
5
+ * All tasks are started in object property order before any settlement is
6
+ * processed. The first task to fulfil resolves with its keyed result; the
7
+ * first task to reject rejects with its original reason, just like
8
+ * `Promise.race()`.
9
+ *
10
+ * Set `abortLosers` to abort cooperative losers after the race settles. Pass
11
+ * `signal` to cancel a still-pending race externally. A task must use the
12
+ * supplied `AbortSignal` for cancellation to stop its underlying work.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const winner = await race({
17
+ * cache: () => readCache(),
18
+ * api: ({ signal }) => fetchUser({ signal }),
19
+ * });
20
+ *
21
+ * if (winner.key === "api") {
22
+ * winner.value; // inferred from fetchUser
23
+ * }
24
+ * ```
25
+ *
26
+ * @throws {TypeError} When `tasks` is empty or contains a non-function value.
27
+ */
28
+ function race(tasks, options = {}) {
29
+ const entries = Object.entries(tasks);
30
+ if (entries.length === 0) throw new TypeError("race() requires at least one task.");
31
+ for (const [key, task] of entries) if (typeof task !== "function") throw new TypeError(`race() task "${key}" must be a function.`);
32
+ if (options.signal?.aborted) return Promise.reject(options.signal.reason);
33
+ const controllers = new Map(entries.map(([key]) => [key, new AbortController()]));
34
+ return new Promise((resolve, reject) => {
35
+ let settled = false;
36
+ const cleanup = () => {
37
+ options.signal?.removeEventListener("abort", onExternalAbort);
38
+ };
39
+ const abortTasks = (winnerKey) => {
40
+ for (const [key, controller] of controllers) if (key !== winnerKey) controller.abort();
41
+ };
42
+ const settleFulfilled = (key, value) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ cleanup();
46
+ if (options.abortLosers) abortTasks(key);
47
+ resolve({
48
+ key,
49
+ value
50
+ });
51
+ };
52
+ const settleRejected = (key, reason) => {
53
+ if (settled) return;
54
+ settled = true;
55
+ cleanup();
56
+ if (options.abortLosers) abortTasks(key);
57
+ reject(reason);
58
+ };
59
+ const onExternalAbort = () => {
60
+ settled = true;
61
+ cleanup();
62
+ abortTasks();
63
+ reject(options.signal?.reason);
64
+ };
65
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
66
+ for (const [key, task] of entries) {
67
+ const context = { signal: controllers.get(key).signal };
68
+ Promise.resolve().then(() => task(context)).then((value) => settleFulfilled(key, value), (reason) => settleRejected(key, reason));
69
+ }
70
+ });
71
+ }
72
+ //#endregion
73
+ export { race };
74
+
75
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "better-race",
3
+ "version": "0.1.0",
4
+ "description": "A better Promise.race() for TypeScript: keyed results and optional cancellation.",
5
+ "keywords": [
6
+ "abortsignal",
7
+ "async",
8
+ "cancellation",
9
+ "promise",
10
+ "race",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/lumberjacque/better-race#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/lumberjacque/better-race/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/lumberjacque/better-race.git"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "types": "./dist/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.mts",
33
+ "import": "./dist/index.mjs"
34
+ }
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "format": "oxfmt --write .",
39
+ "format:check": "oxfmt --check .",
40
+ "lint": "oxlint .",
41
+ "lint:ci": "oxlint --deny-warnings .",
42
+ "typecheck": "tsc --project tsconfig.json --noEmit",
43
+ "test": "vitest run",
44
+ "test:coverage": "vitest run --coverage",
45
+ "test:types": "vitest --typecheck",
46
+ "test:pack": "node scripts/test-pack.mjs",
47
+ "verify": "node scripts/verify.mjs",
48
+ "version": "changeset version",
49
+ "publish": "node scripts/publish.mjs",
50
+ "prepare": "husky"
51
+ },
52
+ "devDependencies": {
53
+ "@changesets/cli": "^3.0.2",
54
+ "@commitlint/cli": "^21.2.2",
55
+ "@commitlint/config-conventional": "^21.2.2",
56
+ "@types/node": "^26.4.1",
57
+ "@vitest/coverage-v8": "^5.0.0",
58
+ "husky": "^9.1.7",
59
+ "lint-staged": "^17.4.1",
60
+ "oxfmt": "^0.66.0",
61
+ "oxlint": "^1.81.0",
62
+ "tsdown": "^0.23.0",
63
+ "typescript": "^5.9.3",
64
+ "vitest": "^5.0.0"
65
+ },
66
+ "lint-staged": {
67
+ "*.{js,cjs,mjs,ts,tsx,json,jsonc,md,yml,yaml,html,css}": "oxfmt --write"
68
+ },
69
+ "engines": {
70
+ "node": ">=22"
71
+ }
72
+ }