The problem.pipeline.execute() waits for one value. A streaming call (LLM tokens, a large download, server-sent
events) produces many, and a failure halfway through is different from a failure at the start:
Before the first chunk, nothing has reached the user. The call can be retried, hedged, timed out or replaced by a fallback like any other.
After the first chunk, the user has seen part of the answer. Retrying would send those chunks again; a fallback would
splice a second answer onto half of the first.
pipeline.executeStream(factory) draws that line for you: the pipeline protects everything up to the first chunk, and from
then on the stream is committed to the caller.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Runs a streaming call (an AsyncIterable or a ReadableStream) through the pipeline. Everything up to the first chunk is
protected: retries, timeouts (time to first chunk), breaker, limiters. Once the first chunk arrives the stream is
committed: later errors reach the consumer and are not retried or replaced by a fallback, which would repeat data
already delivered. Breaking out of the loop closes the underlying stream.
executeStream(({
signal: AbortSignal
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
signal }) =>
functionopenCompletion(prompt:string, options: {
signal:AbortSignal;
}):Promise<AsyncIterable<string>>
openCompletion('Summarize…', {
signal: AbortSignal
signal }))) {
functionrender(token:string):void
render(
consttoken:string
token);
}
The factory returns an AsyncIterable or a ReadableStream (or a promise of one). executeStream returns an async
generator you consume with for await. It accepts the same options as execute (signal, operationKey, idempotent…).
The pipeline runs the factory and reads the first chunk inside execute(). That execute() returns as soon as the first
chunk arrives, so every strategy sees the stream only up to that point:
Strategy
Before the first chunk
After the first chunk
Retry
Retries a failed connect or a stream that fails before yielding. A stream that throws before its first chunk is closed first.
Not retried. The error reaches your for await.
Timeout
Time to first chunk. A slow first token is a TimeoutRejectedError.
Off: its timer was cleared when execute() returned.
Circuit breaker
Counts connect failures and first-chunk timeouts.
Mid-stream errors are not counted.
Fallback
Answers instead of the stream: a value is yielded as one chunk, a stream is iterated.
Never replaces a stream that already yielded.
Cache
Not supported: a cached stream can only be read once, so the second hit throws a TypeError.
—
Concurrency limiter, rate limiter
Hold a permit until the first chunk.
The permit is released: a long stream doesn’t occupy a slot.
Hedging
Starts a second stream if the first is slow to produce a chunk.
The first stream to yield wins. See the caution below.
Two consequences worth knowing:
A limiter does not bound concurrent streams. If you need “at most 20 open streams”, count them yourself around the
for await, or wrap the whole loop in execute() with a concurrency limiter
(then the pipeline protects the entire stream, but retries would repeat chunks, so put no retry in that pipeline).
Fallbacks only cover the connect phase. If the stream fails after its first chunk, the error reaches your loop;
switch sources there only if nothing was yielded yet (example below).
The pipeline timeout only bounds the wait for the first chunk. For a limit on the whole stream, or on the gap between chunks,
use the source’s own signal:
const
consttotal:AbortSignal
total=
var AbortSignal: {
new ():AbortSignal;
prototype:AbortSignal;
abort(reason?:any):AbortSignal;
any(signals:AbortSignal[]):AbortSignal;
timeout(milliseconds:number):AbortSignal;
}
The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.
Runs a streaming call (an AsyncIterable or a ReadableStream) through the pipeline. Everything up to the first chunk is
protected: retries, timeouts (time to first chunk), breaker, limiters. Once the first chunk arrives the stream is
committed: later errors reach the consumer and are not retried or replaced by a fallback, which would repeat data
already delivered. Breaking out of the loop closes the underlying stream.
executeStream(
({
signal: AbortSignal
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
signal }) =>
functionopenCompletion(prompt:string, options: {
signal:AbortSignal;
}):Promise<AsyncIterable<string>>
openCompletion('Summarize…', {
signal: AbortSignal
signal:
var AbortSignal: {
new ():AbortSignal;
prototype:AbortSignal;
abort(reason?:any):AbortSignal;
any(signals:AbortSignal[]):AbortSignal;
timeout(milliseconds:number):AbortSignal;
}
The AbortSignal interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.
AbortSignal.any([signal, total]) reaches the source both before the first chunk (the pipeline’s own timeouts and
retries abort signal) and after it (total still runs). For an idle timeout between chunks, reset a timer per chunk in
your loop and break when it fires.
Runs a streaming call (an AsyncIterable or a ReadableStream) through the pipeline. Everything up to the first chunk is
protected: retries, timeouts (time to first chunk), breaker, limiters. Once the first chunk arrives the stream is
committed: later errors reach the consumer and are not retried or replaced by a fallback, which would repeat data
already delivered. Breaking out of the loop closes the underlying stream.
executeStream(({
signal: AbortSignal
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
Runs a streaming call (an AsyncIterable or a ReadableStream) through the pipeline. Everything up to the first chunk is
protected: retries, timeouts (time to first chunk), breaker, limiters. Once the first chunk arrives the stream is
committed: later errors reach the consumer and are not retried or replaced by a fallback, which would repeat data
already delivered. Breaking out of the loop closes the underlying stream.
executeStream(({
signal: AbortSignal
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
executeStream reads a ReadableStream with its reader when the stream isn’t async-iterable, so a fetch body works
directly. Check the status inside the factory, so a 503 counts as a failure before the first chunk and is retried:
Runs a streaming call (an AsyncIterable or a ReadableStream) through the pipeline. Everything up to the first chunk is
protected: retries, timeouts (time to first chunk), breaker, limiters. Once the first chunk arrives the stream is
committed: later errors reach the consumer and are not retried or replaced by a fallback, which would repeat data
already delivered. Breaking out of the loop closes the underlying stream.
executeStream(async ({
signal: AbortSignal
Aborted when the caller cancels, or when a strategy (timeout, hedging) gives up on this execution.
The ok read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.
A streaming call that changes state (starts a job, charges credits per request) must not run twice. Pass idempotent: false:
retries and hedging then only repeat attempts that never started (rejected by a limiter or an open circuit).
llm.executeStream(({ signal }) =>startExport(jobSpec, signal), { idempotent: false });