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.

319 lines (317 loc) • 10.9 kB
import { createDeferredPromise } from "../../../helpers/promises.js"; import { Realtime } from "../types.js"; import { StreamFanout } from "./StreamFanout.js"; import Debug from "debug"; //#region src/components/realtime/subscribe/TokenSubscription.ts const extractSchema = (topicEntry) => { if (!topicEntry || typeof topicEntry !== "object") return; if ("schema" in topicEntry && topicEntry.schema) return topicEntry.schema; }; var TokenSubscription = class { #apiBaseUrl; #channelId; #debug = Debug("inngest:realtime"); #encoder = new TextEncoder(); #fanout = new StreamFanout(); #running = false; #topics; #ws = null; #signingKey; #signingKeyFallback; #validate; #getSubscriptionToken; #chunkStreams = /* @__PURE__ */ new Map(); token; constructor(options) { this.token = options.token; this.#apiBaseUrl = options.apiBaseUrl; this.#signingKey = options.signingKey; this.#signingKeyFallback = options.signingKeyFallback; this.#validate = options.validate ?? true; this.#getSubscriptionToken = options.getSubscriptionToken; const channel = this.token.channel; if (typeof channel === "string") { this.#channelId = channel; this.#topics = new Map(this.token.topics.map((name) => [name, void 0])); } else { this.#channelId = channel.name; this.#topics = new Map(this.token.topics.map((name) => [name, channel.topics?.[name]])); } } getWsUrl(token) { const path = "/v1/realtime/connect"; let url; if (this.#apiBaseUrl) url = new URL(path, this.#apiBaseUrl); else url = new URL(path, "https://api.inngest.com/"); url.protocol = url.protocol === "http:" ? "ws:" : "wss:"; url.searchParams.set("token", token); return url; } isExpectedChannel(channel) { if (channel === this.#channelId) return true; this.#debug(`Received message for unexpected channel "${channel}" (expected "${this.#channelId}")`); return false; } async connect() { this.#debug(`Establishing connection to channel "${this.#channelId}" with topics ${JSON.stringify([...this.#topics.keys()])}...`); if (typeof WebSocket === "undefined") throw new Error("WebSockets not supported in current environment"); let key = this.token.key; if (!key) { this.#debug("No subscription token key passed; attempting to retrieve one automatically..."); key = await this.lazilyGetSubscriptionToken(); if (!key) throw new Error("No subscription token key passed and failed to retrieve one automatically"); } const ret = createDeferredPromise(); let isConnectSettled = false; let hasOpened = false; const resolveConnect = () => { if (isConnectSettled) return; isConnectSettled = true; ret.resolve(); }; const rejectConnect = (err) => { if (isConnectSettled) return; isConnectSettled = true; ret.reject(err); }; try { this.#running = true; this.#ws = new WebSocket(this.getWsUrl(key)); this.#ws.onopen = () => { this.#debug("WebSocket connection established"); hasOpened = true; resolveConnect(); }; this.#ws.onmessage = async (event) => { let parsedJson; try { parsedJson = JSON.parse(event.data); } catch (err) { this.#debug("Received non-JSON message:", err); return; } const parseRes = await Realtime.messageSchema.safeParseAsync(parsedJson); if (!parseRes.success) { this.#debug("Received invalid message:", parseRes.error); return; } const msg = parseRes.data; if (!this.#running) { this.#debug(`Received message on channel "${msg.channel}" for topic "${msg.topic}" but stream is closed`); return; } switch (msg.kind) { case "data": { if (!msg.channel) { this.#debug(`Received message on channel "${msg.channel}" with no channel`); return; } if (!this.isExpectedChannel(msg.channel)) return; if (!msg.topic) { this.#debug(`Received message on channel "${msg.channel}" with no topic`); return; } if (!this.#topics.has(msg.topic)) { this.#debug(`Received message on channel "${msg.channel}" for unknown topic "${msg.topic}"`); return; } const schema = extractSchema(this.#topics.get(msg.topic)); if (this.#validate && schema) { const validateRes = await schema["~standard"].validate(msg.data); if (validateRes.issues) { console.error(`Received message on channel "${msg.channel}" for topic "${msg.topic}" that failed schema validation:`, validateRes.issues); return; } msg.data = validateRes.value; } this.#debug(`Received message on channel "${msg.channel}" for topic "${msg.topic}":`, msg.data); return this.#fanout.write({ channel: msg.channel, topic: msg.topic, data: msg.data, fnId: msg.fn_id, createdAt: msg.created_at || /* @__PURE__ */ new Date(), runId: msg.run_id, kind: "data", envId: msg.env_id }); } case "run": if (msg.channel && !this.isExpectedChannel(msg.channel)) return; this.#debug(`Received run lifecycle message on "${msg.channel}"`); return this.#fanout.write({ channel: msg.channel, topic: msg.topic, data: msg.data, fnId: msg.fn_id, createdAt: msg.created_at || /* @__PURE__ */ new Date(), runId: msg.run_id, kind: "run", envId: msg.env_id }); case "datastream-start": { if (!msg.channel || !msg.topic) { this.#debug(`Received message on channel "${msg.channel}" with no channel or topic`); return; } if (!this.isExpectedChannel(msg.channel)) return; const streamId = msg.data; if (typeof streamId !== "string" || !streamId) { this.#debug(`Received message on channel "${msg.channel}" with no stream ID`); return; } if (this.#chunkStreams.has(streamId)) { this.#debug(`Received message on channel "${msg.channel}" to create stream ID "${streamId}" that already exists`); return; } const stream = new ReadableStream({ start: (controller) => { this.#chunkStreams.set(streamId, { stream, controller }); }, cancel: () => { this.#chunkStreams.delete(streamId); } }); this.#debug(`Created stream ID "${streamId}" on channel "${msg.channel}"`); return this.#fanout.write({ channel: msg.channel, topic: msg.topic, kind: "datastream-start", data: streamId, streamId, fnId: msg.fn_id, runId: msg.run_id, stream }); } case "datastream-end": { if (!msg.channel || !msg.topic) { this.#debug(`Received message on channel "${msg.channel}" with no channel or topic`); return; } if (!this.isExpectedChannel(msg.channel)) return; const endStreamId = msg.data; if (typeof endStreamId !== "string" || !endStreamId) { this.#debug(`Received message on channel "${msg.channel}" with no stream ID`); return; } const endStream = this.#chunkStreams.get(endStreamId); if (!endStream) { this.#debug(`Received message on channel "${msg.channel}" to close stream ID "${endStreamId}" that doesn't exist`); return; } endStream.controller.close(); this.#chunkStreams.delete(endStreamId); this.#debug(`Closed stream ID "${endStreamId}" on channel "${msg.channel}"`); return this.#fanout.write({ channel: msg.channel, topic: msg.topic, kind: "datastream-end", data: endStreamId, streamId: endStreamId, fnId: msg.fn_id, runId: msg.run_id, stream: endStream.stream }); } case "chunk": { if (!msg.channel || !msg.topic) { this.#debug(`Received message on channel "${msg.channel}" with no channel or topic`); return; } if (!this.isExpectedChannel(msg.channel)) return; if (!msg.stream_id) { this.#debug(`Received message on channel "${msg.channel}" with no stream ID`); return; } const chunkStream = this.#chunkStreams.get(msg.stream_id); if (!chunkStream) { this.#debug(`Received message on channel "${msg.channel}" for unknown stream ID "${msg.stream_id}"`); return; } this.#debug(`Received chunk on channel "${msg.channel}" for stream ID "${msg.stream_id}":`, msg.data); chunkStream.controller.enqueue(msg.data); return this.#fanout.write({ channel: msg.channel, topic: msg.topic, kind: "chunk", data: msg.data, streamId: msg.stream_id, fnId: msg.fn_id, runId: msg.run_id, stream: chunkStream.stream }); } default: this.#debug(`Received message on channel "${msg.channel}" with unhandled kind "${msg.kind}"`); return; } }; this.#ws.onerror = (event) => { console.error("WebSocket error observed:", event); rejectConnect(event); }; this.#ws.onclose = (event) => { this.#debug("WebSocket closed:", event.reason); if (!hasOpened) rejectConnect(/* @__PURE__ */ new Error(`WebSocket closed before opening${event.reason ? `: ${event.reason}` : ""}`)); this.close(); }; } catch (err) { this.#running = false; ret.reject(err); } return ret.promise; } async lazilyGetSubscriptionToken() { const channelId = this.#channelId; if (!channelId) throw new Error("Channel ID is required to create a subscription token"); if (this.#getSubscriptionToken) return this.#getSubscriptionToken(channelId, this.token.topics); throw new Error("No getSubscriptionToken handler provided. Pass an Inngest client or provide a token key."); } close(reason = "Userland closed connection") { if (!this.#running) return; this.#debug("close() called; closing connection..."); this.#running = false; this.#ws?.close(1e3, reason); this.#ws = null; for (const { controller } of this.#chunkStreams.values()) try { controller.close(); } catch {} this.#chunkStreams.clear(); this.#debug(`Closing ${this.#fanout.size()} streams...`); this.#fanout.close(); } getJsonStream() { return this.#fanout.createStream(); } getEncodedStream() { return this.#fanout.createStream((chunk) => { return this.#encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`); }); } useCallback(callback, stream = this.getJsonStream(), onError) { (async () => { const reader = stream.getReader(); try { while (this.#running) { const { done, value } = await reader.read(); if (done || !this.#running) break; try { await callback(value); } catch (err) { if (onError) onError(err); else console.error("Realtime subscription callback failed:", err); } } } finally { reader.releaseLock(); } })(); } }; //#endregion export { TokenSubscription }; //# sourceMappingURL=TokenSubscription.js.map