UNPKG

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.

245 lines (243 loc) 7.27 kB
const require_als = require('./execution/als.cjs'); const require_streaming = require('./execution/streaming.cjs'); //#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 require_als.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(require_streaming.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(require_streaming.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 = require_streaming.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(require_streaming.buildSseRedirectEvent(data)); } /** * Write a succeeded result event and close the writer. Internal use only. */ closeSucceeded(response) { let sseEvent; try { sseEvent = require_streaming.buildSseSucceededEvent(response); } catch { sseEvent = require_streaming.buildSseFailedEvent("Failed to serialize result"); } this.closeWriter(sseEvent); } /** * Write a failed result event and close the writer. Internal use only. */ closeFailed(error) { this.closeWriter(require_streaming.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 require_als.getAsyncCtxSync()?.execution?.stream; }; const getDeferredStreamTooling = async () => { return (await require_als.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 exports.Stream = Stream; exports.stream = stream; //# sourceMappingURL=StreamTools.cjs.map