inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
244 lines (242 loc) • 7.21 kB
JavaScript
import { getAsyncCtx, getAsyncCtxSync } from "./execution/als.js";
import { buildSseCommitEvent, buildSseFailedEvent, buildSseRedirectEvent, buildSseRollbackEvent, buildSseStreamEvent, buildSseSucceededEvent } from "./execution/streaming.js";
//#region src/components/StreamTools.ts
/**
* Wraps a `TransformStream<Uint8Array>` to provide push/pipe SSE streaming
* capabilities within an Inngest execution.
*
* @internal
*/
var Stream = class {
transform;
writer;
encoder = new TextEncoder();
_activated = false;
_errored = false;
writeChain = Promise.resolve();
/**
* Optional callback invoked the first time `push` or `pipe` is called.
* Used by the execution engine to fire a checkpoint that returns the SSE
* Response to the client immediately.
*/
onActivated;
/**
* Optional callback invoked when a write to the underlying stream fails
* (e.g. the client disconnected or the transform stream errored). Used by
* the execution engine to emit diagnostic logs.
*/
onWriteError;
constructor(opts) {
this.onActivated = opts?.onActivated;
this.onWriteError = opts?.onWriteError;
let readableStrategy;
try {
readableStrategy = new CountQueuingStrategy({ highWaterMark: 1024 });
} catch {}
this.transform = new TransformStream(void 0, void 0, readableStrategy);
this.writer = this.transform.writable.getWriter();
}
/**
* Whether `push` or `pipe` has been called at least once.
*/
get activated() {
return this._activated;
}
/**
* The readable side of the underlying transform stream. Consumers (i.e. the
* HTTP response) read SSE events from here.
*/
get readable() {
return this.transform.readable;
}
/**
* Resolve the current hashed step ID for stream events. Returns the
* executing step's hashed ID (read from ALS), or undefined if outside a step.
*/
currentHashedStepId() {
return getAsyncCtxSync()?.execution?.executingStep?.hashedId;
}
activate() {
if (!this._activated) {
this._activated = true;
this.onActivated?.();
}
}
/**
* Encode and write an SSE event string to the underlying writer.
*/
writeEncoded(sseEvent) {
return this.writer.write(this.encoder.encode(sseEvent));
}
/**
* Enqueue a pre-built SSE event string onto the write chain.
*/
enqueue(sseEvent) {
if (this._errored) return;
this.writeChain = this.writeChain.then(() => this.writeEncoded(sseEvent)).catch((err) => {
this._errored = true;
this.onWriteError?.(err);
});
}
/**
* Emit an `inngest.commit` SSE event indicating that uncommitted streamed data
* should be committed (i.e. will not be rolled back). Internal use only.
*/
commit(hashedStepId) {
this.enqueue(buildSseCommitEvent(hashedStepId));
}
/**
* Emit an `inngest.rollback` SSE event indicating the uncommitted streamed
* data should be discarded (e.g. step errored). Internal use only.
*/
rollback(hashedStepId) {
this.enqueue(buildSseRollbackEvent(hashedStepId));
}
/**
* Serialize `data` into an SSE stream event and enqueue it. Returns `false`
* if serialization fails (e.g. circular reference) so callers can skip.
*/
enqueueStreamEvent(data, hashedStepId) {
let sseEvent;
try {
sseEvent = buildSseStreamEvent(data, hashedStepId);
} catch {
return false;
}
this.enqueue(sseEvent);
return true;
}
/**
* Write a single SSE stream event containing `data`. The current step's
* hashed ID is automatically included as stepId for rollback tracking.
*/
push(data) {
this.activate();
this.enqueueStreamEvent(data, this.currentHashedStepId());
}
/**
* Pipe a source to the client, writing each chunk as an SSE stream event.
* Returns the concatenated content of all chunks.
*/
async pipe(source) {
this.activate();
let iterable;
if (source instanceof ReadableStream) iterable = this.readableToAsyncIterable(source);
else if (typeof source === "function") iterable = source();
else iterable = source;
return this.pipeIterable(iterable);
}
/**
* Adapt a ReadableStream into an AsyncIterable<string>. TypeScript's
* ReadableStream type doesn't declare Symbol.asyncIterator, so we use the
* reader API for type safety.
*/
async *readableToAsyncIterable(readable) {
const reader = readable.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield typeof value === "string" ? value : decoder.decode(value, { stream: true });
}
const final = decoder.decode();
if (final) yield final;
} finally {
reader.releaseLock();
}
}
/**
* Core pipe loop: iterate an async iterable, writing each chunk as an SSE
* stream event and collecting the concatenated result.
*/
async pipeIterable(source) {
const hashedStepId = this.currentHashedStepId();
const chunks = [];
for await (const chunk of source) {
if (this._errored) break;
chunks.push(chunk);
if (!this.enqueueStreamEvent(chunk, hashedStepId)) continue;
await this.writeChain;
}
return chunks.join("");
}
/**
* Write a redirect info event. Tells the client where to reconnect if the
* durable endpoint goes async. Does NOT close the writer — more stream
* events may follow before the durable endpoint actually switches to async
* mode. Internal use only.
*/
sendRedirectInfo(data) {
this.enqueue(buildSseRedirectEvent(data));
}
/**
* Write a succeeded result event and close the writer. Internal use only.
*/
closeSucceeded(response) {
let sseEvent;
try {
sseEvent = buildSseSucceededEvent(response);
} catch {
sseEvent = buildSseFailedEvent("Failed to serialize result");
}
this.closeWriter(sseEvent);
}
/**
* Write a failed result event and close the writer. Internal use only.
*/
closeFailed(error) {
this.closeWriter(buildSseFailedEvent(error));
}
/**
* Optionally write a final SSE event, then close the writer.
*/
closeWriter(finalEvent) {
this.writeChain = this.writeChain.then(async () => {
if (finalEvent) await this.writeEncoded(finalEvent);
await this.writer.close();
}).catch((err) => {
this.onWriteError?.(err);
});
}
/**
* Close the writer without writing a result event. Used when the durable endpoint goes
* async and the real result will arrive on the redirected stream.
*/
end() {
this.closeWriter();
}
};
/** Synchronous ALS lookup for the stream tools (fast path). */
const getStreamToolsSync = () => {
return getAsyncCtxSync()?.execution?.stream;
};
const getDeferredStreamTooling = async () => {
return (await getAsyncCtx())?.execution?.stream;
};
/**
* Stream tools that use ALS to resolve the current execution context.
* Outside an Inngest execution, `push()` is a no-op and `pipe()` resolves immediately.
*/
const stream = {
push: (data) => {
const syncStream = getStreamToolsSync();
if (syncStream) {
syncStream.push(data);
return;
}
getDeferredStreamTooling().then((s) => {
s?.push(data);
}).catch(() => {});
},
pipe: async (source) => {
const syncStream = getStreamToolsSync();
if (syncStream) return syncStream.pipe(source);
const s = await getDeferredStreamTooling();
if (s) return s.pipe(source);
return "";
}
};
//#endregion
export { Stream, stream };
//# sourceMappingURL=StreamTools.js.map