Channel
import { Channel } from "jsr:@ende/async";class
class Channel<T> implements AsyncIterable<T>, Disposable, AsyncDisposableAn async channel for communicating between concurrent tasks with optional bounded buffering and backpressure.
- FIFO order: values are received in the order they were sent. When multiple senders or receivers are suspended, they are served in the order they arrived.
- Backpressure:
Channel.sendsuspends when the buffer is full (or always, when unbuffered) until a receiver consumes a value. - Close asymmetry:
Channel.closeaccepts an optional reason. Pending and futureChannel.receivecalls reject with that reason (or a freshChannelClosedErrorwhen no reason was supplied). PendingChannel.sendcalls always reject with aChannelClosedErrorcarrying the unsent value, regardless of the close reason. undefinedvalues:undefinedis a valid channel value, so non-blocking receives use theChannelReceiveResultdiscriminated union rather thanT | undefined.- Multiple consumers: concurrent
Channel.receivecalls and multipleChannel.toReadableStreaminstances each consume values FIFO; every value is delivered to exactly one consumer.
Example: Basic producer/consumer
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 4 });
await ch.send(1);
await ch.send(2);
ch.close();
const values: number[] = [];
for await (const v of ch) {
values.push(v);
}
assertEquals(values, [1, 2]);
Example: Using await using for automatic cleanup
import { Channel } from "@ende/async/channel";
import { assert } from "@ende/assert";
let ref: Channel<string>;
{
await using ch = new Channel<string>({ capacity: 8 });
ref = ch;
await ch.send("hello");
}
assert(ref.closed);
Constructors
constructor
new Channel(options: ChannelOptions = [UNSUPPORTED])Creates a new channel.
options: ChannelOptions = [UNSUPPORTED]Channel options. Defaults to an unbuffered (rendezvous) channel with capacity
0.
Methods
send
send(value: T, options?: ChannelSendOptions): Promise<void>Sends a value into the channel. The returned promise resolves when the
value is buffered or handed to a waiting receiver. Suspends if the buffer
is full (or if unbuffered, suspends until a receiver calls
Channel.receive).
value: TThe value to send into the channel.
options?: ChannelSendOptionsOptional settings for the send operation.
Returns
A promise that resolves when the value has been accepted.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 1 });
await ch.send(42);
assertEquals(ch.size, 1);
ch.close();
Example: Cancelling with an AbortSignal
import { Channel } from "@ende/async/channel";
import { assertRejects } from "@ende/assert";
const ch = new Channel<number>();
const controller = new AbortController();
const p = ch.send(42, { signal: controller.signal });
controller.abort(new Error("cancelled"));
await assertRejects(() => p, Error, "cancelled");
receive
receive(options?: ChannelReceiveOptions): Promise<T>Receives a value from the channel. Suspends if the buffer is empty. Multiple concurrent calls are supported; each value is delivered to exactly one receiver in FIFO order.
options?: ChannelReceiveOptionsOptional settings for the receive operation.
Returns
A promise that resolves with the next value from the channel.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 1 });
await ch.send(42);
assertEquals(await ch.receive(), 42);
ch.close();
Example: Cancelling with an AbortSignal
import { Channel } from "@ende/async/channel";
import { assertRejects } from "@ende/assert";
const ch = new Channel<number>();
const controller = new AbortController();
const p = ch.receive({ signal: controller.signal });
controller.abort(new Error("cancelled"));
await assertRejects(() => p, Error, "cancelled");
trySend
trySend(value: T): booleanNon-blocking send. Does not throw.
value: TThe value to send.
Returns
true if the value was delivered (buffered, or handed directly
to a waiting receiver in the unbuffered case). false if the buffer is
full, no receiver is waiting (unbuffered), or the channel is closed.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assert, assertFalse } from "@ende/assert";
const ch = new Channel<number>({ capacity: 1 });
assert(ch.trySend(1));
assertFalse(ch.trySend(2));
tryReceive
tryReceive(): ChannelReceiveResult<T>Non-blocking receive. Discriminate on the state field to determine the
outcome without ambiguity, even when T itself can be undefined.
Returns
A ChannelReceiveResult — { state: "ok", value }
if a value was available, { state: "empty" } if the channel is open
but no value is ready, or { state: "closed" } if the channel has
been closed and no buffered values remain.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 1 });
await ch.send(42);
assertEquals(ch.tryReceive(), { state: "ok", value: 42 });
assertEquals(ch.tryReceive(), { state: "empty" });
ch.close();
assertEquals(ch.tryReceive(), { state: "closed" });
close
close(): voidCloses the channel. Idempotent — calling close() on an already-closed
channel is a no-op. Pending send() calls reject with
ChannelClosedError. Pending receive() calls drain remaining
buffered values, then reject.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assert } from "@ende/assert";
const ch = new Channel<number>();
ch.close();
assert(ch.closed);
close(reason: unknown): voidCloses the channel with a reason. All pending and future receive()
calls reject with reason after draining buffered values. Pending
send() calls reject with ChannelClosedError (the reason is
intentionally not propagated to senders, so they can recover the unsent
value via ChannelClosedError.value). Idempotent.
reason: unknownThe reason to reject pending and future receivers with.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertRejects } from "@ende/assert";
const ch = new Channel<number>();
ch.close(new Error("upstream failure"));
await assertRejects(() => ch.receive(), Error, "upstream failure");
close(...args: [unknown] | []): voidclosed
get closed(): booleanWhether the channel has been closed.
Returns
true if the channel is closed, false otherwise.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assert, assertFalse } from "@ende/assert";
const ch = new Channel<number>();
assertFalse(ch.closed);
ch.close();
assert(ch.closed);
size
get size(): numberNumber of values currently buffered. Informational only — the value is inherently racy and should not be used for send/receive control flow.
Returns
The number of buffered values.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 4 });
await ch.send(1);
await ch.send(2);
assertEquals(ch.size, 2);
ch.close();
capacity
get capacity(): numberMaximum buffer capacity (0 for unbuffered).
Returns
The maximum buffer capacity.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 8 });
assertEquals(ch.capacity, 8);
ch.close();
[Symbol.asyncIterator]
[Symbol.asyncIterator](): AsyncIterableIterator<T>Async iteration drains the channel until it is closed and empty.
Returns
An async iterator that yields values from the channel.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 4 });
await ch.send(1);
await ch.send(2);
ch.close();
const values: number[] = [];
for await (const v of ch) {
values.push(v);
}
assertEquals(values, [1, 2]);
toReadableStream
toReadableStream(): ReadableStream<T>Creates a ReadableStream that yields values from this
channel. The stream closes when the channel closes after draining
buffered values. If the channel was closed with a reason, the stream
errors with that reason. Cancelling the stream closes the channel; a
non-undefined cancel reason is forwarded to Channel.close
so other consumers observe it.
Each call returns an independent consumer. If multiple streams (or
streams alongside direct Channel.receive calls or async
iteration) consume from the same channel concurrently, values are
distributed FIFO and each value is delivered to exactly one consumer.
Returns
A readable stream of channel values.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assertEquals } from "@ende/assert";
const ch = new Channel<number>({ capacity: 4 });
await ch.send(1);
await ch.send(2);
ch.close();
const values = await Array.fromAsync(ch.toReadableStream());
assertEquals(values, [1, 2]);
[Symbol.dispose]
[Symbol.dispose](): voidCalls Channel.close. Enables using for automatic cleanup.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assert } from "@ende/assert";
const ch = new Channel<number>();
ch[Symbol.dispose]();
assert(ch.closed);
[Symbol.asyncDispose]
[Symbol.asyncDispose](): Promise<void>Calls Channel.close. Enables await using for automatic
cleanup in async contexts.
Example: Usage
import { Channel } from "@ende/async/channel";
import { assert } from "@ende/assert";
const ch = new Channel<number>();
await ch[Symbol.asyncDispose]();
assert(ch.closed);