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
JavaScript
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