Genel bakış

Race

Run keyed behaviors and cancel losers after the first settled child.

parallelcancellation

Install

$
Terminal window
pnpm dlx shadcn@latest add https://archyn.net/r/race.json

Copy the files below into the target paths shown in each tab.

ts@lib/flow/race.ts
import { err, ok, type Result } from "./core/result";
import {
type BehaviorRecord,
type EmptyRaceError,
type RaceError,
type RaceOutput,
type RecordContext,
type RecordInput,
abortChildren,
normalizedConcurrency,
raceFailure,
raceSuccess,
runChild,
} from "./core/record";
import {
type Behavior,
type RuntimeContext,
type RuntimeError,
resultFromSignal,
thrown,
} from "./core/runtime";
/**
* Runs keyed behaviors and returns the first settled child.
*
* On success, the returned value includes the winning key and value. On failure, the returned error
* includes the failing key and error. Remaining active children are aborted.
*
* @typeParam B - Behavior record type.
* @param behaviors - Keyed behaviors to race with the same input.
* @param options - Optional concurrency limit.
* @returns A behavior producing a keyed success or keyed failure.
*
* @example
* ```ts
* const fastestProfile = race({
* primary: loadFromPrimary,
* replica: loadFromReplica,
* });
* ```
*/
export function race<const B extends BehaviorRecord>(
behaviors: B,
options?: { concurrency?: number },
): Behavior<RecordInput<B>, RaceOutput<B>, RaceError<B> | EmptyRaceError, RecordContext<B>> {
return async (input, context) => {
if (context.signal.aborted) {
return resultFromSignal(context.signal);
}
return runRace(behaviors, input, context, options?.concurrency);
};
}
function runRace<const B extends BehaviorRecord>(
behaviors: B,
input: RecordInput<B>,
context: RuntimeContext<RecordContext<B>>,
concurrency: number | undefined,
): Promise<Result<RaceOutput<B>, RaceError<B> | EmptyRaceError | RuntimeError>> {
const keys = Object.keys(behaviors) as Array<keyof B>;
const childControllers: AbortController[] = [];
return new Promise((resolve) => {
let nextIndex = 0;
let active = 0;
let settled = false;
const limit = normalizedConcurrency(concurrency, keys.length);
const finish = (
result: Result<RaceOutput<B>, RaceError<B> | EmptyRaceError | RuntimeError>,
) => {
if (settled) {
return;
}
settled = true;
context.signal.removeEventListener("abort", onAbort);
abortChildren(childControllers, result.ok ? undefined : result.error);
resolve(result);
};
const onAbort = () => finish(resultFromSignal(context.signal));
context.signal.addEventListener("abort", onAbort, { once: true });
const startNext = () => {
if (settled) {
return;
}
while (active < limit && nextIndex < keys.length) {
const key = keys[nextIndex];
if (key === undefined) {
break;
}
const behavior = behaviors[key];
if (behavior === undefined) {
break;
}
nextIndex += 1;
active += 1;
runChild(
behavior as unknown as Behavior<RecordInput<B>, unknown, unknown, RecordContext<B>>,
input,
context,
childControllers,
)
.then((result) => {
active -= 1;
if (settled) {
return;
}
if (result.ok) {
finish(ok(raceSuccess(key, result.value) as RaceOutput<B>));
return;
}
finish(err(raceFailure(key, result.error) as RaceError<B>));
})
.catch((error: unknown) => {
active -= 1;
const failure = thrown(error);
finish(err(raceFailure(key, failure.error) as RaceError<B>));
});
}
};
if (keys.length === 0) {
finish(err({ kind: "empty_race" }));
return;
}
startNext();
});
}

Update the import paths to match your project setup.

Run keyed Behavior values and settle with the first child result.

Usage

race starts child Behavior values and returns the first success or failure, tagged with the child key that settled first. It aborts the remaining active children.

Use it when:

  • Use the first available data source.
  • Race alternative strategies and keep the winning key.
  • Stop slower work after the first useful result or failure.
import { race } from "@/lib/flow/race";
import { task } from "@/lib/flow/task";
const fastestProfile = race({
edge: task(() => fetch("/api/edge-profile").then((r) => r.json())),
origin: task(() => fetch("/api/profile").then((r) => r.json())),
});

Behavior

race starts children up to the concurrency limit and settles as soon as one child succeeds or fails.

The first failure is wrapped with its key. Empty races return empty_race. Parent cancellation aborts children and returns Cancelled.

Types

The result is keyed so callers know which behavior settled first.

Import from @/lib/flow/race.

Composition Notes

Use race for replicas, fallbacks, and first-available data sources.

Do not use race when slower successful results are still required; use allSettled.

Source

ts@lib/flow/race.ts
import { err, ok, type Result } from "./core/result";
import {
type BehaviorRecord,
type EmptyRaceError,
type RaceError,
type RaceOutput,
type RecordContext,
type RecordInput,
abortChildren,
normalizedConcurrency,
raceFailure,
raceSuccess,
runChild,
} from "./core/record";
import {
type Behavior,
type RuntimeContext,
type RuntimeError,
resultFromSignal,
thrown,
} from "./core/runtime";
/**
* Runs keyed behaviors and returns the first settled child.
*
* On success, the returned value includes the winning key and value. On failure, the returned error
* includes the failing key and error. Remaining active children are aborted.
*
* @typeParam B - Behavior record type.
* @param behaviors - Keyed behaviors to race with the same input.
* @param options - Optional concurrency limit.
* @returns A behavior producing a keyed success or keyed failure.
*
* @example
* ```ts
* const fastestProfile = race({
* primary: loadFromPrimary,
* replica: loadFromReplica,
* });
* ```
*/
export function race<const B extends BehaviorRecord>(
behaviors: B,
options?: { concurrency?: number },
): Behavior<RecordInput<B>, RaceOutput<B>, RaceError<B> | EmptyRaceError, RecordContext<B>> {
return async (input, context) => {
if (context.signal.aborted) {
return resultFromSignal(context.signal);
}
return runRace(behaviors, input, context, options?.concurrency);
};
}
function runRace<const B extends BehaviorRecord>(
behaviors: B,
input: RecordInput<B>,
context: RuntimeContext<RecordContext<B>>,
concurrency: number | undefined,
): Promise<Result<RaceOutput<B>, RaceError<B> | EmptyRaceError | RuntimeError>> {
const keys = Object.keys(behaviors) as Array<keyof B>;
const childControllers: AbortController[] = [];
return new Promise((resolve) => {
let nextIndex = 0;
let active = 0;
let settled = false;
const limit = normalizedConcurrency(concurrency, keys.length);
const finish = (
result: Result<RaceOutput<B>, RaceError<B> | EmptyRaceError | RuntimeError>,
) => {
if (settled) {
return;
}
settled = true;
context.signal.removeEventListener("abort", onAbort);
abortChildren(childControllers, result.ok ? undefined : result.error);
resolve(result);
};
const onAbort = () => finish(resultFromSignal(context.signal));
context.signal.addEventListener("abort", onAbort, { once: true });
const startNext = () => {
if (settled) {
return;
}
while (active < limit && nextIndex < keys.length) {
const key = keys[nextIndex];
if (key === undefined) {
break;
}
const behavior = behaviors[key];
if (behavior === undefined) {
break;
}
nextIndex += 1;
active += 1;
runChild(
behavior as unknown as Behavior<RecordInput<B>, unknown, unknown, RecordContext<B>>,
input,
context,
childControllers,
)
.then((result) => {
active -= 1;
if (settled) {
return;
}
if (result.ok) {
finish(ok(raceSuccess(key, result.value) as RaceOutput<B>));
return;
}
finish(err(raceFailure(key, result.error) as RaceError<B>));
})
.catch((error: unknown) => {
active -= 1;
const failure = thrown(error);
finish(err(raceFailure(key, failure.error) as RaceError<B>));
});
}
};
if (keys.length === 0) {
finish(err({ kind: "empty_race" }));
return;
}
startNext();
});
}