@langchain/langgraph
Version:
208 lines (207 loc) • 6.54 kB
JavaScript
//#region src/stream/stream-channel.ts
/**
* StreamChannel — projection channel for local or remote streaming.
*
* A `StreamChannel<T>` is an append-only async stream with independent
* cursors. Local channels stay in-process only. Remote channels declare a
* protocol channel name; when registered with a {@link StreamMux} (via a
* transformer's `init()` return value), every {@link push} is automatically
* forwarded as a {@link ProtocolEvent} on `custom:<channelName>` — making the
* data available both in-process (via `run.extensions`) and to remote clients
* (via `session.subscribe("custom:<channelName>")`).
*
* Lifecycle (`close` / `fail`) is managed by the mux automatically;
* transformers do not need to call them.
*/
/**
* Branded symbol placed on every {@link StreamChannel} instance.
*
* Uses `Symbol.for` so the same symbol is shared across multiple
* copies of this package that may coexist in a dependency graph
* (e.g. when a user app imports `@langchain/langgraph` directly and a
* wrapping library like `langchain` bundles its own copy). Using a
* symbol brand instead of `instanceof` lets channels created against
* one copy of the class be recognised by a mux from another.
* @internal
*/
const STREAM_CHANNEL_BRAND = Symbol.for("langgraph.stream_channel");
/**
* A projection channel for {@link StreamTransformer}s.
*
* Implements `AsyncIterable<T>` so it can be iterated directly by
* in-process consumers via `run.extensions.<key>`. Channels created with
* {@link StreamChannel.remote} or `new StreamChannel(name)` are also
* auto-forwarded to remote clients.
*
* @typeParam T - The type of items pushed into the channel.
*/
var StreamChannel = class StreamChannel {
/** @internal Brand used by {@link StreamChannel.isInstance}. */
[STREAM_CHANNEL_BRAND] = true;
/** Protocol channel name used for auto-forwarded events, if remote. */
channelName;
#items = [];
#waiters = [];
#done = false;
#error;
#onPush;
constructor(name) {
this.channelName = name;
}
/**
* Create an in-process-only channel. Values remain available through
* `run.extensions.<key>` but are not forwarded to remote clients.
*/
static local() {
return new StreamChannel();
}
/**
* Create a channel whose pushes are forwarded to remote clients under
* the given protocol channel name.
*/
static remote(name) {
return new StreamChannel(name);
}
/**
* Brand-based type guard that recognises any {@link StreamChannel}
* instance, even ones originating from a different copy of this
* package. Prefer this over `instanceof StreamChannel` when code
* may observe channels that were constructed elsewhere.
*/
static isInstance(value) {
return typeof value === "object" && value !== null && STREAM_CHANNEL_BRAND in value && value[STREAM_CHANNEL_BRAND] === true;
}
/**
* Append an item to the channel. If this is a remote channel wired to a
* mux, the item is also injected into the main protocol event stream under
* {@link channelName}.
*/
push(item) {
this.#items.push(item);
this.#wake();
this.#onPush?.(item);
}
/**
* Returns an async iterator starting at position {@link startAt}. Each call
* returns an independent cursor so multiple consumers can iterate the same
* channel concurrently.
*/
iterate(startAt = 0) {
let cursor = startAt;
return { next: async () => {
while (true) {
if (cursor < this.#items.length) return {
value: this.#items[cursor++],
done: false
};
if (this.#done) {
if (this.#error) throw this.#error;
return {
value: void 0,
done: true
};
}
await new Promise((resolve) => this.#waiters.push(resolve));
}
} };
}
/**
* Creates an {@link AsyncIterable} backed by this channel, starting from
* {@link startAt}.
*/
toAsyncIterable(startAt = 0) {
return { [Symbol.asyncIterator]: () => this.iterate(startAt) };
}
/**
* Creates a web {@link ReadableStream} that emits channel items as
* Server-Sent Events. Useful for returning a channel directly from
* `new Response(channel.toEventStream())`.
*/
toEventStream(options = {}) {
const encoder = new TextEncoder();
const iterator = this.iterate(options.startAt);
const event = options.event ?? this.channelName;
const serialize = options.serialize ?? ((item) => JSON.stringify(item) ?? "null");
return new ReadableStream({
async pull(controller) {
try {
const next = await iterator.next();
if (next.done) {
controller.close();
return;
}
const lines = [];
if (event != null) lines.push(`event: ${event}`);
for (const line of serialize(next.value).split(/\r\n|\r|\n/)) lines.push(`data: ${line}`);
controller.enqueue(encoder.encode(`${lines.join("\n")}\n\n`));
} catch (error) {
controller.error(error);
}
},
async cancel() {
await iterator.return?.();
}
});
}
/**
* Returns the item at the given zero-based index.
*
* @throws {RangeError} If the index is out of bounds.
*/
get(index) {
if (index < 0 || index >= this.#items.length) throw new RangeError(`StreamChannel index ${index} out of bounds (size=${this.#items.length})`);
return this.#items[index];
}
/** The number of items currently buffered in the channel. */
get size() {
return this.#items.length;
}
/** Whether the channel has been closed or failed. */
get done() {
return this.#done;
}
/** Mark the channel as complete after all buffered items are consumed. */
close() {
this.#done = true;
this.#wake();
}
/** Mark the channel as failed after all buffered items are consumed. */
fail(err) {
this.#error = err;
this.#done = true;
this.#wake();
}
/** @internal Called by the mux to wire auto-forwarding. */
_wire(fn) {
this.#onPush = fn;
}
/** @internal Called by the mux on normal completion. */
_close() {
this.close();
}
/** @internal Called by the mux on failure. */
_fail(err) {
this.fail(err);
}
[Symbol.asyncIterator]() {
return this.iterate();
}
#wake() {
const waiters = this.#waiters.splice(0);
for (const w of waiters) w();
}
};
/**
* Type guard that tests whether a value is a {@link StreamChannel}.
*
* Uses a symbol brand rather than `instanceof` so channels built
* against a different copy of this package (e.g. one bundled by the
* `langchain` umbrella package) are still recognised.
*/
function isStreamChannel(value) {
return StreamChannel.isInstance(value);
}
//#endregion
exports.StreamChannel = StreamChannel;
exports.isStreamChannel = isStreamChannel;
//# sourceMappingURL=stream-channel.cjs.map