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.
198 lines (196 loc) • 5.9 kB
JavaScript
import { isRecord } from "../../helpers/types.js";
import { UnreachableError } from "../middleware/utils.js";
import { z } from "zod/v3";
//#region src/components/execution/streaming.ts
const sseMetadataSchema = z.object({
type: z.literal("inngest.metadata"),
runId: z.string()
});
const sseStreamSchema = z.object({
type: z.literal("inngest.stream"),
data: z.unknown(),
hashedStepId: z.string().optional()
});
const sseCommitSchema = z.object({
type: z.literal("inngest.commit"),
hashedStepId: z.string().nullable()
});
const sseRollbackSchema = z.object({
type: z.literal("inngest.rollback"),
hashedStepId: z.string().nullable()
});
const sseResultSchema = z.object({
type: z.literal("inngest.response"),
status: z.union([z.literal("succeeded"), z.literal("failed")]),
response: z.object({
body: z.string(),
headers: z.record(z.string()),
statusCode: z.number()
})
});
const sseRedirectSchema = z.object({
type: z.literal("inngest.redirect_info"),
runId: z.string(),
url: z.string()
});
/**
* Builds a single SSE event with the given event name and JSON-serialized data.
*
* `undefined` is normalized to `null` so that the `data:` field is always valid
* JSON (since `JSON.stringify(undefined)` returns the JS primitive `undefined`,
* not the string `"null"`).
*/
function buildSseEvent(event, data) {
return `event: ${event}\ndata: ${JSON.stringify(data ?? null)}\n\n`;
}
/**
* Builds an SSE metadata event string for a streaming response.
*
* The event follows the Server-Sent Events format and provides run context
* (run ID) to consumers of the stream.
*/
function buildSseMetadataEvent(runId) {
return buildSseEvent("inngest.metadata", { runId });
}
/**
* Builds an SSE stream event string for user-pushed data.
*
* Used by `stream.push()` and `stream.pipe()` to send arbitrary data to
* clients as part of a streaming response.
*/
function buildSseStreamEvent(data, hashedStepId) {
const payload = { data };
if (hashedStepId) payload.hashedStepId = hashedStepId;
return buildSseEvent("inngest.stream", payload);
}
/**
* Builds an `inngest.response` SSE event with status `succeeded`.
*/
function buildSseSucceededEvent(response) {
return buildSseEvent("inngest.response", {
status: "succeeded",
response
});
}
/**
* Builds an `inngest.response` SSE event with status `failed`.
*/
function buildSseFailedEvent(error) {
return buildSseEvent("inngest.response", {
status: "failed",
response: {
body: JSON.stringify(error),
statusCode: 500,
headers: { "content-type": "application/json" }
}
});
}
/**
* Builds an SSE redirect event telling the client that execution has switched
* to async mode and it should reconnect elsewhere to get remaining output.
*
* The `url` already contains the realtime JWT as a query parameter, so no
* separate token field is needed.
*/
function buildSseRedirectEvent(data) {
return buildSseEvent("inngest.redirect_info", data);
}
/**
* Returns a new `ReadableStream` that emits `prefix` first, then pipes
* through all chunks from the original `stream`.
*/
function prependToStream(prefix, stream) {
return new ReadableStream({ async start(controller) {
controller.enqueue(prefix);
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value);
}
controller.close();
} catch (err) {
controller.error(err);
} finally {
reader.releaseLock();
}
} });
}
/**
* Builds an `inngest.commit` SSE event indicating a step's data is committed.
*/
function buildSseCommitEvent(hashedStepId) {
return buildSseEvent("inngest.commit", { hashedStepId });
}
/**
* Builds an `inngest.rollback` SSE event indicating a step's data should be
* rolled back (e.g. step errored and will retry, or disconnect mid-step).
*/
function buildSseRollbackEvent(hashedStepId) {
return buildSseEvent("inngest.rollback", { hashedStepId });
}
/**
* Parses a `ReadableStream<Uint8Array>` as an SSE byte stream, yielding
* `RawSseEvent` objects for each complete event.
*/
async function* iterSse(body) {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() ?? "";
for (const part of parts) {
if (!part.trim()) continue;
let event = "message";
const dataLines = [];
for (const line of part.split("\n")) if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
const data = dataLines.join("\n");
yield {
event,
data
};
}
}
} finally {
reader.releaseLock();
}
}
const sseSchemasByEvent = {
"inngest.metadata": sseMetadataSchema,
"inngest.stream": sseStreamSchema,
"inngest.response": sseResultSchema,
"inngest.commit": sseCommitSchema,
"inngest.rollback": sseRollbackSchema,
"inngest.redirect_info": sseRedirectSchema
};
/**
* Converts a `RawSseEvent` into a typed `SseEvent`, or returns `undefined`
* if the event type is unrecognised or fails validation.
*/
function parseSseEvent(raw) {
const schema = sseSchemasByEvent[raw.event];
if (!schema) return;
let parsed;
try {
parsed = JSON.parse(raw.data);
} catch {
throw new UnreachableError("SSE data is not a valid JSON string");
}
if (!isRecord(parsed)) return;
const result = schema.safeParse({
...parsed,
type: raw.event
});
if (!result.success) throw new Error("Unknown SSE event", { cause: result.error });
return result.data;
}
//#endregion
export { buildSseCommitEvent, buildSseFailedEvent, buildSseMetadataEvent, buildSseRedirectEvent, buildSseRollbackEvent, buildSseStreamEvent, buildSseSucceededEvent, iterSse, parseSseEvent, prependToStream };
//# sourceMappingURL=streaming.js.map