@bunworks/inngest-realtime 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunworks/inngest-realtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Realtime messaging для @bunworks",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest run",
|
|
12
|
+
"test:coverage": "vitest run --coverage",
|
|
12
13
|
"build": "tsc -p tsconfig.build.json && tsdown --config tsdown.config.ts",
|
|
13
14
|
"postversion": "bun run build",
|
|
14
15
|
"release": "cross-env DIST_DIR=dist node ../../scripts/release/publish.js",
|
|
15
16
|
"pack": "bun run build && mv $(npm pack ./dist --pack-destination . --silent) inngest-realtime.tgz",
|
|
16
|
-
"publish:patch": "npm version patch --no-git-tag-version && npm publish ./dist --access public"
|
|
17
|
+
"publish:patch": "npm version patch --no-git-tag-version && bun run build && npm publish ./dist --access public"
|
|
17
18
|
},
|
|
18
19
|
"exports": {
|
|
19
20
|
".": {
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"@types/debug": "^4.1.12",
|
|
71
72
|
"@types/node": "^25.2.2",
|
|
72
73
|
"@types/react": "^19.2.13",
|
|
74
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
73
75
|
"eslint": "^10.0.0",
|
|
74
76
|
"eslint-plugin-prettier": "^5.5.5",
|
|
75
77
|
"globals": "^17.3.0",
|
|
@@ -19,6 +19,7 @@ var TokenSubscription = class {
|
|
|
19
19
|
#encoder = new TextEncoder();
|
|
20
20
|
#fanout = new require_StreamFanout.StreamFanout();
|
|
21
21
|
#running = false;
|
|
22
|
+
#closed = false;
|
|
22
23
|
#topics;
|
|
23
24
|
#ws = null;
|
|
24
25
|
#signingKey;
|
|
@@ -72,8 +73,8 @@ var TokenSubscription = class {
|
|
|
72
73
|
* Establish WebSocket connection
|
|
73
74
|
*/
|
|
74
75
|
async connect() {
|
|
76
|
+
if (this.#closed) return;
|
|
75
77
|
if (this.#connectionPromise) return this.#connectionPromise;
|
|
76
|
-
if (!this.#running && this.#reconnectAttempts === 0) return;
|
|
77
78
|
this.#connectionPromise = this.#connect();
|
|
78
79
|
return this.#connectionPromise;
|
|
79
80
|
}
|
|
@@ -371,9 +372,10 @@ var TokenSubscription = class {
|
|
|
371
372
|
/**
|
|
372
373
|
* Close the connection and cleanup resources
|
|
373
374
|
*/
|
|
374
|
-
close(
|
|
375
|
-
if (
|
|
375
|
+
close() {
|
|
376
|
+
if (this.#closed) return;
|
|
376
377
|
this.#debug("close() called; closing connection...");
|
|
378
|
+
this.#closed = true;
|
|
377
379
|
this.#running = false;
|
|
378
380
|
if (this.#reconnectTimer) {
|
|
379
381
|
clearTimeout(this.#reconnectTimer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokenSubscription.cjs","names":["StreamFanout","#apiBaseUrl","#signingKey","#signingKeyFallback","#channelId","#topics","topic","getEnvVar","parseAsBoolean","#connectionPromise","#running","#reconnectAttempts","#connect","#ws","#debug","createDeferredPromise","#cleanupWebSocket","#handleMessage","#handleClose","#chunkStreams","#fanout","#maxReconnectAttempts","#reconnectDelay","#reconnectTimer","Realtime","#handleDataMessage","#handleDataStreamStart","#handleDataStreamEnd","#handleChunk","api","#encoder"],"sources":["../../src/subscribe/TokenSubscription.ts"],"sourcesContent":["import debug from \"debug\";\r\nimport { api } from \"../api\";\r\nimport { getEnvVar } from \"../env\";\r\nimport { topic } from \"../topic\";\r\nimport { Realtime } from \"../types\";\r\nimport { createDeferredPromise, parseAsBoolean } from \"../util\";\r\nimport { StreamFanout } from \"./StreamFanout\";\r\n\r\n/**\r\n * Realtime channel subscription via WebSocket\r\n */\r\nexport class TokenSubscription {\r\n #apiBaseUrl?: string;\r\n #channelId: string;\r\n #debug = debug(\"inngest:realtime\");\r\n #encoder = new TextEncoder();\r\n #fanout = new StreamFanout<Realtime.Message>();\r\n #running = false;\r\n #topics: Map<string, Realtime.Topic.Definition>;\r\n #ws: WebSocket | null = null;\r\n #signingKey: string | undefined;\r\n #signingKeyFallback: string | undefined;\r\n #reconnectAttempts = 0;\r\n #maxReconnectAttempts = 5;\r\n #reconnectDelay = 1000;\r\n #connectionPromise: Promise<void> | null = null;\r\n #reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Map of stream IDs to their streams and controllers\r\n */\r\n #chunkStreams = new Map<\r\n string,\r\n { stream: ReadableStream; controller: ReadableStreamDefaultController }\r\n >();\r\n\r\n constructor(\r\n /**\r\n * Subscription token\r\n */\r\n public token: Realtime.Subscribe.Token,\r\n apiBaseUrl: string | undefined,\r\n signingKey: string | undefined,\r\n signingKeyFallback: string | undefined,\r\n ) {\r\n this.#apiBaseUrl = apiBaseUrl;\r\n this.#signingKey = signingKey;\r\n this.#signingKeyFallback = signingKeyFallback;\r\n\r\n if (typeof token.channel === \"string\") {\r\n this.#channelId = token.channel;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n } else {\r\n this.#channelId = token.channel.name;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, token.channel.topics[name] ?? topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n }\r\n }\r\n\r\n private async getWsUrl(token: string): Promise<URL> {\r\n let url: URL;\r\n const path = \"/v1/realtime/connect\";\r\n const devEnvVar = getEnvVar(\"INNGEST_DEV\");\r\n\r\n if (this.#apiBaseUrl) {\r\n url = new URL(path, this.#apiBaseUrl);\r\n } else if (devEnvVar) {\r\n try {\r\n const devUrl = new URL(devEnvVar);\r\n url = new URL(path, devUrl);\r\n } catch {\r\n if (parseAsBoolean(devEnvVar)) {\r\n url = new URL(path, \"http://localhost:8288/\");\r\n } else {\r\n url = new URL(path, \"https://api.inngest.com/\");\r\n }\r\n }\r\n } else {\r\n url = new URL(\r\n path,\r\n getEnvVar(\"NODE_ENV\") === \"production\"\r\n ? \"https://api.inngest.com/\"\r\n : \"http://localhost:8288/\",\r\n );\r\n }\r\n\r\n url.protocol = url.protocol === \"http:\" ? \"ws:\" : \"wss:\";\r\n url.searchParams.set(\"token\", token);\r\n\r\n return url;\r\n }\r\n\r\n /**\r\n * Establish WebSocket connection\r\n */\r\n public async connect() {\r\n // Prevent multiple simultaneous connection attempts\r\n if (this.#connectionPromise) {\r\n return this.#connectionPromise;\r\n }\r\n\r\n // Don't connect if instance was closed\r\n if (!this.#running && this.#reconnectAttempts === 0) {\r\n return;\r\n }\r\n\r\n this.#connectionPromise = this.#connect();\r\n return this.#connectionPromise;\r\n }\r\n\r\n async #connect() {\r\n // Guard against concurrent connection attempts\r\n if (this.#running && this.#ws?.readyState === WebSocket.OPEN) {\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Establishing connection to channel \"${\r\n this.#channelId\r\n }\" with topics ${JSON.stringify([...this.#topics.keys()])}...`,\r\n );\r\n\r\n if (typeof WebSocket === \"undefined\") {\r\n throw new Error(\"WebSocket is not supported in current environment\");\r\n }\r\n\r\n let key = this.token.key;\r\n if (!key) {\r\n this.#debug(\r\n \"No subscription token key passed; attempting to retrieve one automatically...\",\r\n );\r\n\r\n key = (\r\n await this.lazilyGetSubscriptionToken({\r\n ...this.token,\r\n signingKey: this.#signingKey,\r\n signingKeyFallback: this.#signingKeyFallback,\r\n })\r\n ).key;\r\n\r\n if (!key) {\r\n throw new Error(\r\n \"No subscription token key provided and failed to retrieve one automatically\",\r\n );\r\n }\r\n }\r\n\r\n const ret = createDeferredPromise<void>();\r\n\r\n try {\r\n // Clean up existing connection if any\r\n if (this.#ws) {\r\n this.#cleanupWebSocket();\r\n }\r\n\r\n this.#ws = new WebSocket(await this.getWsUrl(key));\r\n\r\n this.#ws.onopen = () => {\r\n this.#debug(\"WebSocket connection established\");\r\n this.#reconnectAttempts = 0;\r\n this.#running = true;\r\n // Clear connection promise only after successful connection\r\n this.#connectionPromise = null;\r\n ret.resolve();\r\n };\r\n\r\n this.#ws.onmessage = async (event) => {\r\n await this.#handleMessage(event);\r\n };\r\n\r\n this.#ws.onerror = (event) => {\r\n this.#debug(\"WebSocket error observed:\", event);\r\n ret.reject(new Error(\"WebSocket connection error\"));\r\n };\r\n\r\n this.#ws.onclose = (event) => {\r\n this.#debug(\"WebSocket closed:\", event.code, event.reason);\r\n this.#handleClose(event);\r\n };\r\n } catch (err) {\r\n // Clear connection promise on error\r\n this.#connectionPromise = null;\r\n ret.reject(err);\r\n }\r\n\r\n return ret.promise;\r\n }\r\n\r\n #cleanupWebSocket() {\r\n if (!this.#ws) return;\r\n\r\n try {\r\n // Remove event listeners to prevent memory leaks\r\n this.#ws.onopen = null;\r\n this.#ws.onmessage = null;\r\n this.#ws.onerror = null;\r\n this.#ws.onclose = null;\r\n\r\n // Close connection if still open\r\n if (\r\n this.#ws.readyState === WebSocket.OPEN ||\r\n this.#ws.readyState === WebSocket.CONNECTING\r\n ) {\r\n this.#ws.close(1000, \"Cleaning up connection\");\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error cleaning up WebSocket:\", err);\r\n }\r\n\r\n this.#ws = null;\r\n }\r\n\r\n #handleClose(event: CloseEvent) {\r\n const wasRunning = this.#running;\r\n this.#running = false;\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n // Normal closure or user-initiated close\r\n if (event.code === 1000 || !wasRunning) {\r\n this.#debug(\"Connection closed normally\");\r\n // Clear connection promise on normal close\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n return;\r\n }\r\n\r\n // Attempt reconnection for abnormal closures\r\n if (this.#reconnectAttempts < this.#maxReconnectAttempts) {\r\n this.#reconnectAttempts++;\r\n const delay =\r\n this.#reconnectDelay * Math.pow(2, this.#reconnectAttempts - 1);\r\n\r\n this.#debug(\r\n `Attempting reconnection ${this.#reconnectAttempts}/${this.#maxReconnectAttempts} in ${delay}ms...`,\r\n );\r\n\r\n // Clear the old connection promise before attempting reconnection\r\n this.#connectionPromise = null;\r\n\r\n // Store timer ID so it can be cleared in close()\r\n this.#reconnectTimer = setTimeout(() => {\r\n this.#reconnectTimer = null;\r\n\r\n // connect() will set a new #connectionPromise, preventing races\r\n this.connect().catch((err) => {\r\n this.#debug(\"Reconnection failed:\", err);\r\n if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {\r\n this.#debug(\"Max reconnection attempts reached, closing streams\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n });\r\n }, delay);\r\n } else {\r\n this.#debug(\"Max reconnection attempts reached\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n }\r\n\r\n async #handleMessage(event: MessageEvent) {\r\n let parsedData;\r\n try {\r\n parsedData = JSON.parse(event.data as string);\r\n } catch (err) {\r\n this.#debug(\"Failed to parse JSON from WebSocket message:\", err);\r\n this.#debug(\"Raw payload:\", event.data);\r\n return;\r\n }\r\n\r\n const parseRes = await Realtime.messageSchema.safeParseAsync(parsedData);\r\n\r\n if (!parseRes.success) {\r\n this.#debug(\"Received invalid message:\", parseRes.error);\r\n return;\r\n }\r\n\r\n const msg = parseRes.data;\r\n\r\n if (!this.#running) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" but stream is closed`,\r\n );\r\n return;\r\n }\r\n\r\n switch (msg.kind) {\r\n case \"data\": {\r\n await this.#handleDataMessage(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-start\": {\r\n this.#handleDataStreamStart(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-end\": {\r\n this.#handleDataStreamEnd(msg);\r\n break;\r\n }\r\n\r\n case \"chunk\": {\r\n this.#handleChunk(msg);\r\n break;\r\n }\r\n\r\n case \"ping\": {\r\n // Respond to ping with pong to keep connection alive\r\n if (this.#ws?.readyState === WebSocket.OPEN) {\r\n this.#ws.send(JSON.stringify({ kind: \"pong\" }));\r\n }\r\n break;\r\n }\r\n\r\n case \"closing\": {\r\n this.#debug(\"Server is closing connection, will reconnect...\");\r\n break;\r\n }\r\n\r\n default: {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" with unhandled kind \"${msg.kind}\"`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n async #handleDataMessage(msg: any) {\r\n if (!msg.channel) {\r\n this.#debug(`Received message with no channel`);\r\n return;\r\n }\r\n\r\n if (!msg.topic) {\r\n this.#debug(`Received message on channel \"${msg.channel}\" with no topic`);\r\n return;\r\n }\r\n\r\n const topic = this.#topics.get(msg.topic);\r\n if (!topic) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for unknown topic \"${msg.topic}\"`,\r\n );\r\n return;\r\n }\r\n\r\n const schema = topic.getSchema();\r\n if (schema) {\r\n const validateRes = await schema[\"~standard\"].validate(msg.data);\r\n if (validateRes.issues) {\r\n console.error(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" that failed schema validation:`,\r\n validateRes.issues,\r\n );\r\n return;\r\n }\r\n\r\n msg.data = validateRes.value;\r\n }\r\n\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\":`,\r\n msg.data,\r\n );\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n data: msg.data,\r\n fnId: msg.fn_id,\r\n createdAt: msg.created_at || new Date(),\r\n runId: msg.run_id,\r\n kind: \"data\",\r\n envId: msg.env_id,\r\n });\r\n }\r\n\r\n #handleDataStreamStart(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-start with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-start with invalid stream ID`);\r\n return;\r\n }\r\n\r\n if (this.#chunkStreams.has(streamId)) {\r\n this.#debug(\r\n `Received datastream-start for stream ID \"${streamId}\" that already exists`,\r\n );\r\n return;\r\n }\r\n\r\n let holderStream: ReadableStream;\r\n let holderController: ReadableStreamDefaultController;\r\n\r\n holderStream = new ReadableStream({\r\n start: (controller) => {\r\n holderController = controller;\r\n this.#chunkStreams.set(streamId, {\r\n stream: holderStream,\r\n controller: holderController,\r\n });\r\n },\r\n\r\n cancel: () => {\r\n this.#chunkStreams.delete(streamId);\r\n },\r\n });\r\n\r\n this.#debug(`Created stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-start\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: holderStream,\r\n });\r\n }\r\n\r\n #handleDataStreamEnd(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-end with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-end with invalid stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(streamId);\r\n if (!stream) {\r\n this.#debug(\r\n `Received datastream-end for stream ID \"${streamId}\" that doesn't exist`,\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n\r\n this.#chunkStreams.delete(streamId);\r\n\r\n this.#debug(`Closed stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-end\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n #handleChunk(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received chunk with missing channel or topic`);\r\n return;\r\n }\r\n\r\n if (!msg.stream_id) {\r\n this.#debug(`Received chunk with no stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(msg.stream_id);\r\n if (!stream) {\r\n this.#debug(`Received chunk for unknown stream ID \"${msg.stream_id}\"`);\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Received chunk on channel \"${msg.channel}\" for stream ID \"${msg.stream_id}\":`,\r\n msg.data,\r\n );\r\n\r\n try {\r\n stream.controller.enqueue(msg.data);\r\n } catch (err) {\r\n this.#debug(`Error enqueueing chunk to stream ${msg.stream_id}:`, err);\r\n }\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"chunk\",\r\n data: msg.data,\r\n streamId: msg.stream_id,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n /**\r\n * Lazily get a subscription token if not provided\r\n */\r\n private async lazilyGetSubscriptionToken<\r\n const InputChannel extends Realtime.Channel | string,\r\n const InputTopics extends (keyof Realtime.Channel.InferTopics<\r\n Realtime.Channel.AsChannel<InputChannel>\r\n > &\r\n string)[],\r\n const TToken extends Realtime.Subscribe.Token<\r\n Realtime.Channel.AsChannel<InputChannel>,\r\n InputTopics\r\n >,\r\n >(\r\n /**\r\n * Subscription parameters\r\n */\r\n args: {\r\n /**\r\n * Channel ID or channel object\r\n */\r\n channel: Realtime.Subscribe.InferChannelInput<InputChannel>;\r\n\r\n /**\r\n * List of topics\r\n */\r\n topics: InputTopics;\r\n\r\n /**\r\n * Signing key for authentication\r\n */\r\n signingKey: string | undefined;\r\n\r\n /**\r\n * Fallback signing key\r\n */\r\n signingKeyFallback: string | undefined;\r\n },\r\n ): Promise<TToken> {\r\n const channelId =\r\n typeof args.channel === \"string\" ? args.channel : args.channel.name;\r\n\r\n if (!channelId) {\r\n throw new Error(\"Channel ID is required to create a subscription token\");\r\n }\r\n\r\n const key = await api.getSubscriptionToken({\r\n channel: channelId,\r\n topics: args.topics,\r\n signingKey: args.signingKey,\r\n signingKeyFallback: args.signingKeyFallback,\r\n apiBaseUrl: this.#apiBaseUrl,\r\n });\r\n\r\n const token = {\r\n channel: channelId,\r\n topics: args.topics,\r\n key,\r\n } as TToken;\r\n\r\n return token;\r\n }\r\n\r\n /**\r\n * Close the connection and cleanup resources\r\n */\r\n public close(\r\n /**\r\n * Reason for closing\r\n */\r\n reason = \"Userland closed connection\",\r\n ) {\r\n if (!this.#running && !this.#ws) {\r\n return;\r\n }\r\n\r\n this.#debug(\"close() called; closing connection...\");\r\n this.#running = false;\r\n\r\n // Clear any pending reconnection timer\r\n if (this.#reconnectTimer) {\r\n clearTimeout(this.#reconnectTimer);\r\n this.#reconnectTimer = null;\r\n }\r\n\r\n // Prevent reconnection attempts\r\n this.#reconnectAttempts = this.#maxReconnectAttempts;\r\n\r\n // Close WebSocket connection\r\n this.#cleanupWebSocket();\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n this.#debug(`Closing ${this.#fanout.size()} streams...`);\r\n this.#fanout.close();\r\n }\r\n\r\n /**\r\n * Get a new JSON stream from the subscription\r\n */\r\n public getJsonStream() {\r\n return this.#fanout.createStream();\r\n }\r\n\r\n /**\r\n * Get a new encoded stream (SSE-compatible) from the subscription\r\n */\r\n public getEncodedStream() {\r\n return this.#fanout.createStream((chunk) => {\r\n return this.#encoder.encode(`${JSON.stringify(chunk)}\\n`);\r\n });\r\n }\r\n\r\n /**\r\n * Use a callback to handle messages from the stream\r\n */\r\n public useCallback(\r\n callback: Realtime.Subscribe.Callback,\r\n stream: ReadableStream<Realtime.Message> = this.getJsonStream(),\r\n ) {\r\n void (async () => {\r\n // Explicitly get and manage the reader so that we can manually release\r\n // the lock if anything goes wrong or we're done with it.\r\n const reader = stream.getReader();\r\n try {\r\n while (this.#running) {\r\n const { done, value } = await reader.read();\r\n if (done || !this.#running) break;\r\n\r\n try {\r\n callback(value);\r\n } catch (err) {\r\n this.#debug(\"Error in callback:\", err);\r\n }\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error reading from stream:\", err);\r\n } finally {\r\n try {\r\n reader.releaseLock();\r\n } catch (err) {\r\n this.#debug(\"Error releasing reader lock:\", err);\r\n }\r\n }\r\n })();\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TokenSubscription.cjs","names":["StreamFanout","#apiBaseUrl","#signingKey","#signingKeyFallback","#channelId","#topics","topic","getEnvVar","parseAsBoolean","#closed","#connectionPromise","#connect","#running","#ws","#debug","createDeferredPromise","#cleanupWebSocket","#reconnectAttempts","#handleMessage","#handleClose","#chunkStreams","#fanout","#maxReconnectAttempts","#reconnectDelay","#reconnectTimer","Realtime","#handleDataMessage","#handleDataStreamStart","#handleDataStreamEnd","#handleChunk","api","#encoder"],"sources":["../../src/subscribe/TokenSubscription.ts"],"sourcesContent":["import debug from \"debug\";\r\nimport { api } from \"../api\";\r\nimport { getEnvVar } from \"../env\";\r\nimport { topic } from \"../topic\";\r\nimport { Realtime } from \"../types\";\r\nimport { createDeferredPromise, parseAsBoolean } from \"../util\";\r\nimport { StreamFanout } from \"./StreamFanout\";\r\n\r\n/**\r\n * Realtime channel subscription via WebSocket\r\n */\r\nexport class TokenSubscription {\r\n #apiBaseUrl?: string;\r\n #channelId: string;\r\n #debug = debug(\"inngest:realtime\");\r\n #encoder = new TextEncoder();\r\n #fanout = new StreamFanout<Realtime.Message>();\r\n #running = false;\r\n #closed = false;\r\n #topics: Map<string, Realtime.Topic.Definition>;\r\n #ws: WebSocket | null = null;\r\n #signingKey: string | undefined;\r\n #signingKeyFallback: string | undefined;\r\n #reconnectAttempts = 0;\r\n #maxReconnectAttempts = 5;\r\n #reconnectDelay = 1000;\r\n #connectionPromise: Promise<void> | null = null;\r\n #reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Map of stream IDs to their streams and controllers\r\n */\r\n #chunkStreams = new Map<\r\n string,\r\n { stream: ReadableStream; controller: ReadableStreamDefaultController }\r\n >();\r\n\r\n constructor(\r\n /**\r\n * Subscription token\r\n */\r\n public token: Realtime.Subscribe.Token,\r\n apiBaseUrl: string | undefined,\r\n signingKey: string | undefined,\r\n signingKeyFallback: string | undefined,\r\n ) {\r\n this.#apiBaseUrl = apiBaseUrl;\r\n this.#signingKey = signingKey;\r\n this.#signingKeyFallback = signingKeyFallback;\r\n\r\n if (typeof token.channel === \"string\") {\r\n this.#channelId = token.channel;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n } else {\r\n this.#channelId = token.channel.name;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, token.channel.topics[name] ?? topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n }\r\n }\r\n\r\n private async getWsUrl(token: string): Promise<URL> {\r\n let url: URL;\r\n const path = \"/v1/realtime/connect\";\r\n const devEnvVar = getEnvVar(\"INNGEST_DEV\");\r\n\r\n if (this.#apiBaseUrl) {\r\n url = new URL(path, this.#apiBaseUrl);\r\n } else if (devEnvVar) {\r\n try {\r\n const devUrl = new URL(devEnvVar);\r\n url = new URL(path, devUrl);\r\n } catch {\r\n if (parseAsBoolean(devEnvVar)) {\r\n url = new URL(path, \"http://localhost:8288/\");\r\n } else {\r\n url = new URL(path, \"https://api.inngest.com/\");\r\n }\r\n }\r\n } else {\r\n url = new URL(\r\n path,\r\n getEnvVar(\"NODE_ENV\") === \"production\"\r\n ? \"https://api.inngest.com/\"\r\n : \"http://localhost:8288/\",\r\n );\r\n }\r\n\r\n url.protocol = url.protocol === \"http:\" ? \"ws:\" : \"wss:\";\r\n url.searchParams.set(\"token\", token);\r\n\r\n return url;\r\n }\r\n\r\n /**\r\n * Establish WebSocket connection\r\n */\r\n public async connect() {\r\n // Don't connect if instance was explicitly closed\r\n if (this.#closed) {\r\n return;\r\n }\r\n\r\n // Prevent multiple simultaneous connection attempts\r\n if (this.#connectionPromise) {\r\n return this.#connectionPromise;\r\n }\r\n\r\n this.#connectionPromise = this.#connect();\r\n return this.#connectionPromise;\r\n }\r\n\r\n async #connect() {\r\n // Guard against concurrent connection attempts\r\n if (this.#running && this.#ws?.readyState === WebSocket.OPEN) {\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Establishing connection to channel \"${\r\n this.#channelId\r\n }\" with topics ${JSON.stringify([...this.#topics.keys()])}...`,\r\n );\r\n\r\n if (typeof WebSocket === \"undefined\") {\r\n throw new Error(\"WebSocket is not supported in current environment\");\r\n }\r\n\r\n let key = this.token.key;\r\n if (!key) {\r\n this.#debug(\r\n \"No subscription token key passed; attempting to retrieve one automatically...\",\r\n );\r\n\r\n key = (\r\n await this.lazilyGetSubscriptionToken({\r\n ...this.token,\r\n signingKey: this.#signingKey,\r\n signingKeyFallback: this.#signingKeyFallback,\r\n })\r\n ).key;\r\n\r\n if (!key) {\r\n throw new Error(\r\n \"No subscription token key provided and failed to retrieve one automatically\",\r\n );\r\n }\r\n }\r\n\r\n const ret = createDeferredPromise<void>();\r\n\r\n try {\r\n // Clean up existing connection if any\r\n if (this.#ws) {\r\n this.#cleanupWebSocket();\r\n }\r\n\r\n this.#ws = new WebSocket(await this.getWsUrl(key));\r\n\r\n this.#ws.onopen = () => {\r\n this.#debug(\"WebSocket connection established\");\r\n this.#reconnectAttempts = 0;\r\n this.#running = true;\r\n // Clear connection promise only after successful connection\r\n this.#connectionPromise = null;\r\n ret.resolve();\r\n };\r\n\r\n this.#ws.onmessage = async (event) => {\r\n await this.#handleMessage(event);\r\n };\r\n\r\n this.#ws.onerror = (event) => {\r\n this.#debug(\"WebSocket error observed:\", event);\r\n ret.reject(new Error(\"WebSocket connection error\"));\r\n };\r\n\r\n this.#ws.onclose = (event) => {\r\n this.#debug(\"WebSocket closed:\", event.code, event.reason);\r\n this.#handleClose(event);\r\n };\r\n } catch (err) {\r\n // Clear connection promise on error\r\n this.#connectionPromise = null;\r\n ret.reject(err);\r\n }\r\n\r\n return ret.promise;\r\n }\r\n\r\n #cleanupWebSocket() {\r\n if (!this.#ws) return;\r\n\r\n try {\r\n // Remove event listeners to prevent memory leaks\r\n this.#ws.onopen = null;\r\n this.#ws.onmessage = null;\r\n this.#ws.onerror = null;\r\n this.#ws.onclose = null;\r\n\r\n // Close connection if still open\r\n if (\r\n this.#ws.readyState === WebSocket.OPEN ||\r\n this.#ws.readyState === WebSocket.CONNECTING\r\n ) {\r\n this.#ws.close(1000, \"Cleaning up connection\");\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error cleaning up WebSocket:\", err);\r\n }\r\n\r\n this.#ws = null;\r\n }\r\n\r\n #handleClose(event: CloseEvent) {\r\n const wasRunning = this.#running;\r\n this.#running = false;\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n // Normal closure or user-initiated close\r\n if (event.code === 1000 || !wasRunning) {\r\n this.#debug(\"Connection closed normally\");\r\n // Clear connection promise on normal close\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n return;\r\n }\r\n\r\n // Attempt reconnection for abnormal closures\r\n if (this.#reconnectAttempts < this.#maxReconnectAttempts) {\r\n this.#reconnectAttempts++;\r\n const delay =\r\n this.#reconnectDelay * Math.pow(2, this.#reconnectAttempts - 1);\r\n\r\n this.#debug(\r\n `Attempting reconnection ${this.#reconnectAttempts}/${this.#maxReconnectAttempts} in ${delay}ms...`,\r\n );\r\n\r\n // Clear the old connection promise before attempting reconnection\r\n this.#connectionPromise = null;\r\n\r\n // Store timer ID so it can be cleared in close()\r\n this.#reconnectTimer = setTimeout(() => {\r\n this.#reconnectTimer = null;\r\n\r\n // connect() will set a new #connectionPromise, preventing races\r\n this.connect().catch((err) => {\r\n this.#debug(\"Reconnection failed:\", err);\r\n if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {\r\n this.#debug(\"Max reconnection attempts reached, closing streams\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n });\r\n }, delay);\r\n } else {\r\n this.#debug(\"Max reconnection attempts reached\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n }\r\n\r\n async #handleMessage(event: MessageEvent) {\r\n let parsedData;\r\n try {\r\n parsedData = JSON.parse(event.data as string);\r\n } catch (err) {\r\n this.#debug(\"Failed to parse JSON from WebSocket message:\", err);\r\n this.#debug(\"Raw payload:\", event.data);\r\n return;\r\n }\r\n\r\n const parseRes = await Realtime.messageSchema.safeParseAsync(parsedData);\r\n\r\n if (!parseRes.success) {\r\n this.#debug(\"Received invalid message:\", parseRes.error);\r\n return;\r\n }\r\n\r\n const msg = parseRes.data;\r\n\r\n if (!this.#running) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" but stream is closed`,\r\n );\r\n return;\r\n }\r\n\r\n switch (msg.kind) {\r\n case \"data\": {\r\n await this.#handleDataMessage(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-start\": {\r\n this.#handleDataStreamStart(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-end\": {\r\n this.#handleDataStreamEnd(msg);\r\n break;\r\n }\r\n\r\n case \"chunk\": {\r\n this.#handleChunk(msg);\r\n break;\r\n }\r\n\r\n case \"ping\": {\r\n // Respond to ping with pong to keep connection alive\r\n if (this.#ws?.readyState === WebSocket.OPEN) {\r\n this.#ws.send(JSON.stringify({ kind: \"pong\" }));\r\n }\r\n break;\r\n }\r\n\r\n case \"closing\": {\r\n this.#debug(\"Server is closing connection, will reconnect...\");\r\n break;\r\n }\r\n\r\n default: {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" with unhandled kind \"${msg.kind}\"`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n async #handleDataMessage(msg: any) {\r\n if (!msg.channel) {\r\n this.#debug(`Received message with no channel`);\r\n return;\r\n }\r\n\r\n if (!msg.topic) {\r\n this.#debug(`Received message on channel \"${msg.channel}\" with no topic`);\r\n return;\r\n }\r\n\r\n const topic = this.#topics.get(msg.topic);\r\n if (!topic) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for unknown topic \"${msg.topic}\"`,\r\n );\r\n return;\r\n }\r\n\r\n const schema = topic.getSchema();\r\n if (schema) {\r\n const validateRes = await schema[\"~standard\"].validate(msg.data);\r\n if (validateRes.issues) {\r\n console.error(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" that failed schema validation:`,\r\n validateRes.issues,\r\n );\r\n return;\r\n }\r\n\r\n msg.data = validateRes.value;\r\n }\r\n\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\":`,\r\n msg.data,\r\n );\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n data: msg.data,\r\n fnId: msg.fn_id,\r\n createdAt: msg.created_at || new Date(),\r\n runId: msg.run_id,\r\n kind: \"data\",\r\n envId: msg.env_id,\r\n });\r\n }\r\n\r\n #handleDataStreamStart(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-start with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-start with invalid stream ID`);\r\n return;\r\n }\r\n\r\n if (this.#chunkStreams.has(streamId)) {\r\n this.#debug(\r\n `Received datastream-start for stream ID \"${streamId}\" that already exists`,\r\n );\r\n return;\r\n }\r\n\r\n let holderStream: ReadableStream;\r\n let holderController: ReadableStreamDefaultController;\r\n\r\n holderStream = new ReadableStream({\r\n start: (controller) => {\r\n holderController = controller;\r\n this.#chunkStreams.set(streamId, {\r\n stream: holderStream,\r\n controller: holderController,\r\n });\r\n },\r\n\r\n cancel: () => {\r\n this.#chunkStreams.delete(streamId);\r\n },\r\n });\r\n\r\n this.#debug(`Created stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-start\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: holderStream,\r\n });\r\n }\r\n\r\n #handleDataStreamEnd(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-end with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-end with invalid stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(streamId);\r\n if (!stream) {\r\n this.#debug(\r\n `Received datastream-end for stream ID \"${streamId}\" that doesn't exist`,\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n\r\n this.#chunkStreams.delete(streamId);\r\n\r\n this.#debug(`Closed stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-end\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n #handleChunk(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received chunk with missing channel or topic`);\r\n return;\r\n }\r\n\r\n if (!msg.stream_id) {\r\n this.#debug(`Received chunk with no stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(msg.stream_id);\r\n if (!stream) {\r\n this.#debug(`Received chunk for unknown stream ID \"${msg.stream_id}\"`);\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Received chunk on channel \"${msg.channel}\" for stream ID \"${msg.stream_id}\":`,\r\n msg.data,\r\n );\r\n\r\n try {\r\n stream.controller.enqueue(msg.data);\r\n } catch (err) {\r\n this.#debug(`Error enqueueing chunk to stream ${msg.stream_id}:`, err);\r\n }\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"chunk\",\r\n data: msg.data,\r\n streamId: msg.stream_id,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n /**\r\n * Lazily get a subscription token if not provided\r\n */\r\n private async lazilyGetSubscriptionToken<\r\n const InputChannel extends Realtime.Channel | string,\r\n const InputTopics extends (keyof Realtime.Channel.InferTopics<\r\n Realtime.Channel.AsChannel<InputChannel>\r\n > &\r\n string)[],\r\n const TToken extends Realtime.Subscribe.Token<\r\n Realtime.Channel.AsChannel<InputChannel>,\r\n InputTopics\r\n >,\r\n >(\r\n /**\r\n * Subscription parameters\r\n */\r\n args: {\r\n /**\r\n * Channel ID or channel object\r\n */\r\n channel: Realtime.Subscribe.InferChannelInput<InputChannel>;\r\n\r\n /**\r\n * List of topics\r\n */\r\n topics: InputTopics;\r\n\r\n /**\r\n * Signing key for authentication\r\n */\r\n signingKey: string | undefined;\r\n\r\n /**\r\n * Fallback signing key\r\n */\r\n signingKeyFallback: string | undefined;\r\n },\r\n ): Promise<TToken> {\r\n const channelId =\r\n typeof args.channel === \"string\" ? args.channel : args.channel.name;\r\n\r\n if (!channelId) {\r\n throw new Error(\"Channel ID is required to create a subscription token\");\r\n }\r\n\r\n const key = await api.getSubscriptionToken({\r\n channel: channelId,\r\n topics: args.topics,\r\n signingKey: args.signingKey,\r\n signingKeyFallback: args.signingKeyFallback,\r\n apiBaseUrl: this.#apiBaseUrl,\r\n });\r\n\r\n const token = {\r\n channel: channelId,\r\n topics: args.topics,\r\n key,\r\n } as TToken;\r\n\r\n return token;\r\n }\r\n\r\n /**\r\n * Close the connection and cleanup resources\r\n */\r\n public close() {\r\n if (this.#closed) {\r\n return;\r\n }\r\n\r\n this.#debug(\"close() called; closing connection...\");\r\n this.#closed = true;\r\n this.#running = false;\r\n\r\n // Clear any pending reconnection timer\r\n if (this.#reconnectTimer) {\r\n clearTimeout(this.#reconnectTimer);\r\n this.#reconnectTimer = null;\r\n }\r\n\r\n // Prevent reconnection attempts\r\n this.#reconnectAttempts = this.#maxReconnectAttempts;\r\n\r\n // Close WebSocket connection\r\n this.#cleanupWebSocket();\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n this.#debug(`Closing ${this.#fanout.size()} streams...`);\r\n this.#fanout.close();\r\n }\r\n\r\n /**\r\n * Get a new JSON stream from the subscription\r\n */\r\n public getJsonStream() {\r\n return this.#fanout.createStream();\r\n }\r\n\r\n /**\r\n * Get a new encoded stream (SSE-compatible) from the subscription\r\n */\r\n public getEncodedStream() {\r\n return this.#fanout.createStream((chunk) => {\r\n return this.#encoder.encode(`${JSON.stringify(chunk)}\\n`);\r\n });\r\n }\r\n\r\n /**\r\n * Use a callback to handle messages from the stream\r\n */\r\n public useCallback(\r\n callback: Realtime.Subscribe.Callback,\r\n stream: ReadableStream<Realtime.Message> = this.getJsonStream(),\r\n ) {\r\n void (async () => {\r\n // Explicitly get and manage the reader so that we can manually release\r\n // the lock if anything goes wrong or we're done with it.\r\n const reader = stream.getReader();\r\n try {\r\n while (this.#running) {\r\n const { done, value } = await reader.read();\r\n if (done || !this.#running) break;\r\n\r\n try {\r\n callback(value);\r\n } catch (err) {\r\n this.#debug(\"Error in callback:\", err);\r\n }\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error reading from stream:\", err);\r\n } finally {\r\n try {\r\n reader.releaseLock();\r\n } catch (err) {\r\n this.#debug(\"Error releasing reader lock:\", err);\r\n }\r\n }\r\n })();\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;AAWA,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA,4BAAe,mBAAmB;CAClC,WAAW,IAAI,aAAa;CAC5B,UAAU,IAAIA,mCAAgC;CAC9C,WAAW;CACX,UAAU;CACV;CACA,MAAwB;CACxB;CACA;CACA,qBAAqB;CACrB,wBAAwB;CACxB,kBAAkB;CAClB,qBAA2C;CAC3C,kBAAwD;;;;CAKxD,gCAAgB,IAAI,KAGjB;CAEH,YAIE,AAAO,OACP,YACA,YACA,oBACA;EAJO;AAKP,QAAKC,aAAc;AACnB,QAAKC,aAAc;AACnB,QAAKC,qBAAsB;AAE3B,MAAI,OAAO,MAAM,YAAY,UAAU;AACrC,SAAKC,YAAa,MAAM;AAExB,SAAKC,SAAU,KAAK,MAAM,OAAO,QAE9B,KAAK,SAAS;AACf,QAAI,IAAI,MAAMC,oBAAM,KAAK,CAAC;AAE1B,WAAO;sBACN,IAAI,KAAwC,CAAC;SAC3C;AACL,SAAKF,YAAa,MAAM,QAAQ;AAEhC,SAAKC,SAAU,KAAK,MAAM,OAAO,QAE9B,KAAK,SAAS;AACf,QAAI,IAAI,MAAM,MAAM,QAAQ,OAAO,SAASC,oBAAM,KAAK,CAAC;AAExD,WAAO;sBACN,IAAI,KAAwC,CAAC;;;CAIpD,MAAc,SAAS,OAA6B;EAClD,IAAI;EACJ,MAAM,OAAO;EACb,MAAM,YAAYC,sBAAU,cAAc;AAE1C,MAAI,MAAKN,WACP,OAAM,IAAI,IAAI,MAAM,MAAKA,WAAY;WAC5B,UACT,KAAI;GACF,MAAM,SAAS,IAAI,IAAI,UAAU;AACjC,SAAM,IAAI,IAAI,MAAM,OAAO;UACrB;AACN,OAAIO,4BAAe,UAAU,CAC3B,OAAM,IAAI,IAAI,MAAM,yBAAyB;OAE7C,OAAM,IAAI,IAAI,MAAM,2BAA2B;;MAInD,OAAM,IAAI,IACR,MACAD,sBAAU,WAAW,KAAK,eACtB,6BACA,yBACL;AAGH,MAAI,WAAW,IAAI,aAAa,UAAU,QAAQ;AAClD,MAAI,aAAa,IAAI,SAAS,MAAM;AAEpC,SAAO;;;;;CAMT,MAAa,UAAU;AAErB,MAAI,MAAKE,OACP;AAIF,MAAI,MAAKC,kBACP,QAAO,MAAKA;AAGd,QAAKA,oBAAqB,MAAKC,SAAU;AACzC,SAAO,MAAKD;;CAGd,OAAMC,UAAW;AAEf,MAAI,MAAKC,WAAY,MAAKC,IAAK,eAAe,UAAU,KACtD;AAGF,QAAKC,MACH,uCACE,MAAKV,UACN,gBAAgB,KAAK,UAAU,CAAC,GAAG,MAAKC,OAAQ,MAAM,CAAC,CAAC,CAAC,KAC3D;AAED,MAAI,OAAO,cAAc,YACvB,OAAM,IAAI,MAAM,oDAAoD;EAGtE,IAAI,MAAM,KAAK,MAAM;AACrB,MAAI,CAAC,KAAK;AACR,SAAKS,MACH,gFACD;AAED,UACE,MAAM,KAAK,2BAA2B;IACpC,GAAG,KAAK;IACR,YAAY,MAAKZ;IACjB,oBAAoB,MAAKC;IAC1B,CAAC,EACF;AAEF,OAAI,CAAC,IACH,OAAM,IAAI,MACR,8EACD;;EAIL,MAAM,MAAMY,oCAA6B;AAEzC,MAAI;AAEF,OAAI,MAAKF,GACP,OAAKG,kBAAmB;AAG1B,SAAKH,KAAM,IAAI,UAAU,MAAM,KAAK,SAAS,IAAI,CAAC;AAElD,SAAKA,GAAI,eAAe;AACtB,UAAKC,MAAO,mCAAmC;AAC/C,UAAKG,oBAAqB;AAC1B,UAAKL,UAAW;AAEhB,UAAKF,oBAAqB;AAC1B,QAAI,SAAS;;AAGf,SAAKG,GAAI,YAAY,OAAO,UAAU;AACpC,UAAM,MAAKK,cAAe,MAAM;;AAGlC,SAAKL,GAAI,WAAW,UAAU;AAC5B,UAAKC,MAAO,6BAA6B,MAAM;AAC/C,QAAI,uBAAO,IAAI,MAAM,6BAA6B,CAAC;;AAGrD,SAAKD,GAAI,WAAW,UAAU;AAC5B,UAAKC,MAAO,qBAAqB,MAAM,MAAM,MAAM,OAAO;AAC1D,UAAKK,YAAa,MAAM;;WAEnB,KAAK;AAEZ,SAAKT,oBAAqB;AAC1B,OAAI,OAAO,IAAI;;AAGjB,SAAO,IAAI;;CAGb,oBAAoB;AAClB,MAAI,CAAC,MAAKG,GAAK;AAEf,MAAI;AAEF,SAAKA,GAAI,SAAS;AAClB,SAAKA,GAAI,YAAY;AACrB,SAAKA,GAAI,UAAU;AACnB,SAAKA,GAAI,UAAU;AAGnB,OACE,MAAKA,GAAI,eAAe,UAAU,QAClC,MAAKA,GAAI,eAAe,UAAU,WAElC,OAAKA,GAAI,MAAM,KAAM,yBAAyB;WAEzC,KAAK;AACZ,SAAKC,MAAO,gCAAgC,IAAI;;AAGlD,QAAKD,KAAM;;CAGb,aAAa,OAAmB;EAC9B,MAAM,aAAa,MAAKD;AACxB,QAAKA,UAAW;AAGhB,OAAK,MAAM,CAAC,UAAU,WAAW,MAAKQ,aAAc,SAAS,CAC3D,KAAI;AACF,UAAO,WAAW,OAAO;WAClB,KAAK;AACZ,SAAKN,MAAO,wBAAwB,SAAS,IAAI,IAAI;;AAGzD,QAAKM,aAAc,OAAO;AAG1B,MAAI,MAAM,SAAS,OAAQ,CAAC,YAAY;AACtC,SAAKN,MAAO,6BAA6B;AAEzC,SAAKJ,oBAAqB;AAC1B,SAAKW,OAAQ,OAAO;AACpB;;AAIF,MAAI,MAAKJ,oBAAqB,MAAKK,sBAAuB;AACxD,SAAKL;GACL,MAAM,QACJ,MAAKM,iBAAkB,KAAK,IAAI,GAAG,MAAKN,oBAAqB,EAAE;AAEjE,SAAKH,MACH,2BAA2B,MAAKG,kBAAmB,GAAG,MAAKK,qBAAsB,MAAM,MAAM,OAC9F;AAGD,SAAKZ,oBAAqB;AAG1B,SAAKc,iBAAkB,iBAAiB;AACtC,UAAKA,iBAAkB;AAGvB,SAAK,SAAS,CAAC,OAAO,QAAQ;AAC5B,WAAKV,MAAO,wBAAwB,IAAI;AACxC,SAAI,MAAKG,qBAAsB,MAAKK,sBAAuB;AACzD,YAAKR,MAAO,qDAAqD;AACjE,YAAKJ,oBAAqB;AAC1B,YAAKW,OAAQ,OAAO;;MAEtB;MACD,MAAM;SACJ;AACL,SAAKP,MAAO,oCAAoC;AAChD,SAAKJ,oBAAqB;AAC1B,SAAKW,OAAQ,OAAO;;;CAIxB,OAAMH,cAAe,OAAqB;EACxC,IAAI;AACJ,MAAI;AACF,gBAAa,KAAK,MAAM,MAAM,KAAe;WACtC,KAAK;AACZ,SAAKJ,MAAO,gDAAgD,IAAI;AAChE,SAAKA,MAAO,gBAAgB,MAAM,KAAK;AACvC;;EAGF,MAAM,WAAW,MAAMW,uBAAS,cAAc,eAAe,WAAW;AAExE,MAAI,CAAC,SAAS,SAAS;AACrB,SAAKX,MAAO,6BAA6B,SAAS,MAAM;AACxD;;EAGF,MAAM,MAAM,SAAS;AAErB,MAAI,CAAC,MAAKF,SAAU;AAClB,SAAKE,MACH,gCAAgC,IAAI,QAAQ,eAAe,IAAI,MAAM,wBACtE;AACD;;AAGF,UAAQ,IAAI,MAAZ;GACE,KAAK;AACH,UAAM,MAAKY,kBAAmB,IAAI;AAClC;GAGF,KAAK;AACH,UAAKC,sBAAuB,IAAI;AAChC;GAGF,KAAK;AACH,UAAKC,oBAAqB,IAAI;AAC9B;GAGF,KAAK;AACH,UAAKC,YAAa,IAAI;AACtB;GAGF,KAAK;AAEH,QAAI,MAAKhB,IAAK,eAAe,UAAU,KACrC,OAAKA,GAAI,KAAK,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;AAEjD;GAGF,KAAK;AACH,UAAKC,MAAO,kDAAkD;AAC9D;GAGF,QACE,OAAKA,MACH,gCAAgC,IAAI,QAAQ,yBAAyB,IAAI,KAAK,GAC/E;;;CAKP,OAAMY,kBAAmB,KAAU;AACjC,MAAI,CAAC,IAAI,SAAS;AAChB,SAAKZ,MAAO,mCAAmC;AAC/C;;AAGF,MAAI,CAAC,IAAI,OAAO;AACd,SAAKA,MAAO,gCAAgC,IAAI,QAAQ,iBAAiB;AACzE;;EAGF,MAAM,QAAQ,MAAKT,OAAQ,IAAI,IAAI,MAAM;AACzC,MAAI,CAAC,OAAO;AACV,SAAKS,MACH,gCAAgC,IAAI,QAAQ,uBAAuB,IAAI,MAAM,GAC9E;AACD;;EAGF,MAAM,SAAS,MAAM,WAAW;AAChC,MAAI,QAAQ;GACV,MAAM,cAAc,MAAM,OAAO,aAAa,SAAS,IAAI,KAAK;AAChE,OAAI,YAAY,QAAQ;AACtB,YAAQ,MACN,gCAAgC,IAAI,QAAQ,eAAe,IAAI,MAAM,mCACrE,YAAY,OACb;AACD;;AAGF,OAAI,OAAO,YAAY;;AAGzB,QAAKA,MACH,gCAAgC,IAAI,QAAQ,eAAe,IAAI,MAAM,KACrE,IAAI,KACL;AAED,QAAKO,OAAQ,MAAM;GACjB,SAAS,IAAI;GACb,OAAO,IAAI;GACX,MAAM,IAAI;GACV,MAAM,IAAI;GACV,WAAW,IAAI,8BAAc,IAAI,MAAM;GACvC,OAAO,IAAI;GACX,MAAM;GACN,OAAO,IAAI;GACZ,CAAC;;CAGJ,uBAAuB,KAAU;AAC/B,MAAI,CAAC,IAAI,WAAW,CAAC,IAAI,OAAO;AAC9B,SAAKP,MAAO,0DAA0D;AACtE;;EAGF,MAAM,WAAoB,IAAI;AAC9B,MAAI,OAAO,aAAa,YAAY,CAAC,UAAU;AAC7C,SAAKA,MAAO,mDAAmD;AAC/D;;AAGF,MAAI,MAAKM,aAAc,IAAI,SAAS,EAAE;AACpC,SAAKN,MACH,4CAA4C,SAAS,uBACtD;AACD;;EAGF,IAAI;EACJ,IAAI;AAEJ,iBAAe,IAAI,eAAe;GAChC,QAAQ,eAAe;AACrB,uBAAmB;AACnB,UAAKM,aAAc,IAAI,UAAU;KAC/B,QAAQ;KACR,YAAY;KACb,CAAC;;GAGJ,cAAc;AACZ,UAAKA,aAAc,OAAO,SAAS;;GAEtC,CAAC;AAEF,QAAKN,MAAO,sBAAsB,SAAS,gBAAgB,IAAI,QAAQ,GAAG;AAE1E,QAAKO,OAAQ,MAAM;GACjB,SAAS,IAAI;GACb,OAAO,IAAI;GACX,MAAM;GACN,MAAM;GACN;GACA,MAAM,IAAI;GACV,OAAO,IAAI;GACX,QAAQ;GACT,CAAC;;CAGJ,qBAAqB,KAAU;AAC7B,MAAI,CAAC,IAAI,WAAW,CAAC,IAAI,OAAO;AAC9B,SAAKP,MAAO,wDAAwD;AACpE;;EAGF,MAAM,WAAoB,IAAI;AAC9B,MAAI,OAAO,aAAa,YAAY,CAAC,UAAU;AAC7C,SAAKA,MAAO,iDAAiD;AAC7D;;EAGF,MAAM,SAAS,MAAKM,aAAc,IAAI,SAAS;AAC/C,MAAI,CAAC,QAAQ;AACX,SAAKN,MACH,0CAA0C,SAAS,sBACpD;AACD;;AAGF,MAAI;AACF,UAAO,WAAW,OAAO;WAClB,KAAK;AACZ,SAAKA,MAAO,wBAAwB,SAAS,IAAI,IAAI;;AAGvD,QAAKM,aAAc,OAAO,SAAS;AAEnC,QAAKN,MAAO,qBAAqB,SAAS,gBAAgB,IAAI,QAAQ,GAAG;AAEzE,QAAKO,OAAQ,MAAM;GACjB,SAAS,IAAI;GACb,OAAO,IAAI;GACX,MAAM;GACN,MAAM;GACN;GACA,MAAM,IAAI;GACV,OAAO,IAAI;GACX,QAAQ,OAAO;GAChB,CAAC;;CAGJ,aAAa,KAAU;AACrB,MAAI,CAAC,IAAI,WAAW,CAAC,IAAI,OAAO;AAC9B,SAAKP,MAAO,+CAA+C;AAC3D;;AAGF,MAAI,CAAC,IAAI,WAAW;AAClB,SAAKA,MAAO,mCAAmC;AAC/C;;EAGF,MAAM,SAAS,MAAKM,aAAc,IAAI,IAAI,UAAU;AACpD,MAAI,CAAC,QAAQ;AACX,SAAKN,MAAO,yCAAyC,IAAI,UAAU,GAAG;AACtE;;AAGF,QAAKA,MACH,8BAA8B,IAAI,QAAQ,mBAAmB,IAAI,UAAU,KAC3E,IAAI,KACL;AAED,MAAI;AACF,UAAO,WAAW,QAAQ,IAAI,KAAK;WAC5B,KAAK;AACZ,SAAKA,MAAO,oCAAoC,IAAI,UAAU,IAAI,IAAI;;AAGxE,QAAKO,OAAQ,MAAM;GACjB,SAAS,IAAI;GACb,OAAO,IAAI;GACX,MAAM;GACN,MAAM,IAAI;GACV,UAAU,IAAI;GACd,MAAM,IAAI;GACV,OAAO,IAAI;GACX,QAAQ,OAAO;GAChB,CAAC;;;;;CAMJ,MAAc,2BAcZ,MAqBiB;EACjB,MAAM,YACJ,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,KAAK,QAAQ;AAEjE,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wDAAwD;EAG1E,MAAM,MAAM,MAAMS,gBAAI,qBAAqB;GACzC,SAAS;GACT,QAAQ,KAAK;GACb,YAAY,KAAK;GACjB,oBAAoB,KAAK;GACzB,YAAY,MAAK7B;GAClB,CAAC;AAQF,SANc;GACZ,SAAS;GACT,QAAQ,KAAK;GACb;GACD;;;;;CAQH,AAAO,QAAQ;AACb,MAAI,MAAKQ,OACP;AAGF,QAAKK,MAAO,wCAAwC;AACpD,QAAKL,SAAU;AACf,QAAKG,UAAW;AAGhB,MAAI,MAAKY,gBAAiB;AACxB,gBAAa,MAAKA,eAAgB;AAClC,SAAKA,iBAAkB;;AAIzB,QAAKP,oBAAqB,MAAKK;AAG/B,QAAKN,kBAAmB;AAGxB,OAAK,MAAM,CAAC,UAAU,WAAW,MAAKI,aAAc,SAAS,CAC3D,KAAI;AACF,UAAO,WAAW,OAAO;WAClB,KAAK;AACZ,SAAKN,MAAO,wBAAwB,SAAS,IAAI,IAAI;;AAGzD,QAAKM,aAAc,OAAO;AAE1B,QAAKN,MAAO,WAAW,MAAKO,OAAQ,MAAM,CAAC,aAAa;AACxD,QAAKA,OAAQ,OAAO;;;;;CAMtB,AAAO,gBAAgB;AACrB,SAAO,MAAKA,OAAQ,cAAc;;;;;CAMpC,AAAO,mBAAmB;AACxB,SAAO,MAAKA,OAAQ,cAAc,UAAU;AAC1C,UAAO,MAAKU,QAAS,OAAO,GAAG,KAAK,UAAU,MAAM,CAAC,IAAI;IACzD;;;;;CAMJ,AAAO,YACL,UACA,SAA2C,KAAK,eAAe,EAC/D;AACA,GAAM,YAAY;GAGhB,MAAM,SAAS,OAAO,WAAW;AACjC,OAAI;AACF,WAAO,MAAKnB,SAAU;KACpB,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,SAAI,QAAQ,CAAC,MAAKA,QAAU;AAE5B,SAAI;AACF,eAAS,MAAM;cACR,KAAK;AACZ,YAAKE,MAAO,sBAAsB,IAAI;;;YAGnC,KAAK;AACZ,UAAKA,MAAO,8BAA8B,IAAI;aACtC;AACR,QAAI;AACF,YAAO,aAAa;aACb,KAAK;AACZ,WAAKA,MAAO,gCAAgC,IAAI;;;MAGlD"}
|
|
@@ -17,6 +17,7 @@ var TokenSubscription = class {
|
|
|
17
17
|
#encoder = new TextEncoder();
|
|
18
18
|
#fanout = new StreamFanout();
|
|
19
19
|
#running = false;
|
|
20
|
+
#closed = false;
|
|
20
21
|
#topics;
|
|
21
22
|
#ws = null;
|
|
22
23
|
#signingKey;
|
|
@@ -70,8 +71,8 @@ var TokenSubscription = class {
|
|
|
70
71
|
* Establish WebSocket connection
|
|
71
72
|
*/
|
|
72
73
|
async connect() {
|
|
74
|
+
if (this.#closed) return;
|
|
73
75
|
if (this.#connectionPromise) return this.#connectionPromise;
|
|
74
|
-
if (!this.#running && this.#reconnectAttempts === 0) return;
|
|
75
76
|
this.#connectionPromise = this.#connect();
|
|
76
77
|
return this.#connectionPromise;
|
|
77
78
|
}
|
|
@@ -369,9 +370,10 @@ var TokenSubscription = class {
|
|
|
369
370
|
/**
|
|
370
371
|
* Close the connection and cleanup resources
|
|
371
372
|
*/
|
|
372
|
-
close(
|
|
373
|
-
if (
|
|
373
|
+
close() {
|
|
374
|
+
if (this.#closed) return;
|
|
374
375
|
this.#debug("close() called; closing connection...");
|
|
376
|
+
this.#closed = true;
|
|
375
377
|
this.#running = false;
|
|
376
378
|
if (this.#reconnectTimer) {
|
|
377
379
|
clearTimeout(this.#reconnectTimer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokenSubscription.mjs","names":["#apiBaseUrl","#signingKey","#signingKeyFallback","#channelId","#topics","#connectionPromise","#running","#reconnectAttempts","#connect","#ws","#debug","#cleanupWebSocket","#handleMessage","#handleClose","#chunkStreams","#fanout","#maxReconnectAttempts","#reconnectDelay","#reconnectTimer","#handleDataMessage","#handleDataStreamStart","#handleDataStreamEnd","#handleChunk","#encoder"],"sources":["../../src/subscribe/TokenSubscription.ts"],"sourcesContent":["import debug from \"debug\";\r\nimport { api } from \"../api\";\r\nimport { getEnvVar } from \"../env\";\r\nimport { topic } from \"../topic\";\r\nimport { Realtime } from \"../types\";\r\nimport { createDeferredPromise, parseAsBoolean } from \"../util\";\r\nimport { StreamFanout } from \"./StreamFanout\";\r\n\r\n/**\r\n * Realtime channel subscription via WebSocket\r\n */\r\nexport class TokenSubscription {\r\n #apiBaseUrl?: string;\r\n #channelId: string;\r\n #debug = debug(\"inngest:realtime\");\r\n #encoder = new TextEncoder();\r\n #fanout = new StreamFanout<Realtime.Message>();\r\n #running = false;\r\n #topics: Map<string, Realtime.Topic.Definition>;\r\n #ws: WebSocket | null = null;\r\n #signingKey: string | undefined;\r\n #signingKeyFallback: string | undefined;\r\n #reconnectAttempts = 0;\r\n #maxReconnectAttempts = 5;\r\n #reconnectDelay = 1000;\r\n #connectionPromise: Promise<void> | null = null;\r\n #reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Map of stream IDs to their streams and controllers\r\n */\r\n #chunkStreams = new Map<\r\n string,\r\n { stream: ReadableStream; controller: ReadableStreamDefaultController }\r\n >();\r\n\r\n constructor(\r\n /**\r\n * Subscription token\r\n */\r\n public token: Realtime.Subscribe.Token,\r\n apiBaseUrl: string | undefined,\r\n signingKey: string | undefined,\r\n signingKeyFallback: string | undefined,\r\n ) {\r\n this.#apiBaseUrl = apiBaseUrl;\r\n this.#signingKey = signingKey;\r\n this.#signingKeyFallback = signingKeyFallback;\r\n\r\n if (typeof token.channel === \"string\") {\r\n this.#channelId = token.channel;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n } else {\r\n this.#channelId = token.channel.name;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, token.channel.topics[name] ?? topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n }\r\n }\r\n\r\n private async getWsUrl(token: string): Promise<URL> {\r\n let url: URL;\r\n const path = \"/v1/realtime/connect\";\r\n const devEnvVar = getEnvVar(\"INNGEST_DEV\");\r\n\r\n if (this.#apiBaseUrl) {\r\n url = new URL(path, this.#apiBaseUrl);\r\n } else if (devEnvVar) {\r\n try {\r\n const devUrl = new URL(devEnvVar);\r\n url = new URL(path, devUrl);\r\n } catch {\r\n if (parseAsBoolean(devEnvVar)) {\r\n url = new URL(path, \"http://localhost:8288/\");\r\n } else {\r\n url = new URL(path, \"https://api.inngest.com/\");\r\n }\r\n }\r\n } else {\r\n url = new URL(\r\n path,\r\n getEnvVar(\"NODE_ENV\") === \"production\"\r\n ? \"https://api.inngest.com/\"\r\n : \"http://localhost:8288/\",\r\n );\r\n }\r\n\r\n url.protocol = url.protocol === \"http:\" ? \"ws:\" : \"wss:\";\r\n url.searchParams.set(\"token\", token);\r\n\r\n return url;\r\n }\r\n\r\n /**\r\n * Establish WebSocket connection\r\n */\r\n public async connect() {\r\n // Prevent multiple simultaneous connection attempts\r\n if (this.#connectionPromise) {\r\n return this.#connectionPromise;\r\n }\r\n\r\n // Don't connect if instance was closed\r\n if (!this.#running && this.#reconnectAttempts === 0) {\r\n return;\r\n }\r\n\r\n this.#connectionPromise = this.#connect();\r\n return this.#connectionPromise;\r\n }\r\n\r\n async #connect() {\r\n // Guard against concurrent connection attempts\r\n if (this.#running && this.#ws?.readyState === WebSocket.OPEN) {\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Establishing connection to channel \"${\r\n this.#channelId\r\n }\" with topics ${JSON.stringify([...this.#topics.keys()])}...`,\r\n );\r\n\r\n if (typeof WebSocket === \"undefined\") {\r\n throw new Error(\"WebSocket is not supported in current environment\");\r\n }\r\n\r\n let key = this.token.key;\r\n if (!key) {\r\n this.#debug(\r\n \"No subscription token key passed; attempting to retrieve one automatically...\",\r\n );\r\n\r\n key = (\r\n await this.lazilyGetSubscriptionToken({\r\n ...this.token,\r\n signingKey: this.#signingKey,\r\n signingKeyFallback: this.#signingKeyFallback,\r\n })\r\n ).key;\r\n\r\n if (!key) {\r\n throw new Error(\r\n \"No subscription token key provided and failed to retrieve one automatically\",\r\n );\r\n }\r\n }\r\n\r\n const ret = createDeferredPromise<void>();\r\n\r\n try {\r\n // Clean up existing connection if any\r\n if (this.#ws) {\r\n this.#cleanupWebSocket();\r\n }\r\n\r\n this.#ws = new WebSocket(await this.getWsUrl(key));\r\n\r\n this.#ws.onopen = () => {\r\n this.#debug(\"WebSocket connection established\");\r\n this.#reconnectAttempts = 0;\r\n this.#running = true;\r\n // Clear connection promise only after successful connection\r\n this.#connectionPromise = null;\r\n ret.resolve();\r\n };\r\n\r\n this.#ws.onmessage = async (event) => {\r\n await this.#handleMessage(event);\r\n };\r\n\r\n this.#ws.onerror = (event) => {\r\n this.#debug(\"WebSocket error observed:\", event);\r\n ret.reject(new Error(\"WebSocket connection error\"));\r\n };\r\n\r\n this.#ws.onclose = (event) => {\r\n this.#debug(\"WebSocket closed:\", event.code, event.reason);\r\n this.#handleClose(event);\r\n };\r\n } catch (err) {\r\n // Clear connection promise on error\r\n this.#connectionPromise = null;\r\n ret.reject(err);\r\n }\r\n\r\n return ret.promise;\r\n }\r\n\r\n #cleanupWebSocket() {\r\n if (!this.#ws) return;\r\n\r\n try {\r\n // Remove event listeners to prevent memory leaks\r\n this.#ws.onopen = null;\r\n this.#ws.onmessage = null;\r\n this.#ws.onerror = null;\r\n this.#ws.onclose = null;\r\n\r\n // Close connection if still open\r\n if (\r\n this.#ws.readyState === WebSocket.OPEN ||\r\n this.#ws.readyState === WebSocket.CONNECTING\r\n ) {\r\n this.#ws.close(1000, \"Cleaning up connection\");\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error cleaning up WebSocket:\", err);\r\n }\r\n\r\n this.#ws = null;\r\n }\r\n\r\n #handleClose(event: CloseEvent) {\r\n const wasRunning = this.#running;\r\n this.#running = false;\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n // Normal closure or user-initiated close\r\n if (event.code === 1000 || !wasRunning) {\r\n this.#debug(\"Connection closed normally\");\r\n // Clear connection promise on normal close\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n return;\r\n }\r\n\r\n // Attempt reconnection for abnormal closures\r\n if (this.#reconnectAttempts < this.#maxReconnectAttempts) {\r\n this.#reconnectAttempts++;\r\n const delay =\r\n this.#reconnectDelay * Math.pow(2, this.#reconnectAttempts - 1);\r\n\r\n this.#debug(\r\n `Attempting reconnection ${this.#reconnectAttempts}/${this.#maxReconnectAttempts} in ${delay}ms...`,\r\n );\r\n\r\n // Clear the old connection promise before attempting reconnection\r\n this.#connectionPromise = null;\r\n\r\n // Store timer ID so it can be cleared in close()\r\n this.#reconnectTimer = setTimeout(() => {\r\n this.#reconnectTimer = null;\r\n\r\n // connect() will set a new #connectionPromise, preventing races\r\n this.connect().catch((err) => {\r\n this.#debug(\"Reconnection failed:\", err);\r\n if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {\r\n this.#debug(\"Max reconnection attempts reached, closing streams\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n });\r\n }, delay);\r\n } else {\r\n this.#debug(\"Max reconnection attempts reached\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n }\r\n\r\n async #handleMessage(event: MessageEvent) {\r\n let parsedData;\r\n try {\r\n parsedData = JSON.parse(event.data as string);\r\n } catch (err) {\r\n this.#debug(\"Failed to parse JSON from WebSocket message:\", err);\r\n this.#debug(\"Raw payload:\", event.data);\r\n return;\r\n }\r\n\r\n const parseRes = await Realtime.messageSchema.safeParseAsync(parsedData);\r\n\r\n if (!parseRes.success) {\r\n this.#debug(\"Received invalid message:\", parseRes.error);\r\n return;\r\n }\r\n\r\n const msg = parseRes.data;\r\n\r\n if (!this.#running) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" but stream is closed`,\r\n );\r\n return;\r\n }\r\n\r\n switch (msg.kind) {\r\n case \"data\": {\r\n await this.#handleDataMessage(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-start\": {\r\n this.#handleDataStreamStart(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-end\": {\r\n this.#handleDataStreamEnd(msg);\r\n break;\r\n }\r\n\r\n case \"chunk\": {\r\n this.#handleChunk(msg);\r\n break;\r\n }\r\n\r\n case \"ping\": {\r\n // Respond to ping with pong to keep connection alive\r\n if (this.#ws?.readyState === WebSocket.OPEN) {\r\n this.#ws.send(JSON.stringify({ kind: \"pong\" }));\r\n }\r\n break;\r\n }\r\n\r\n case \"closing\": {\r\n this.#debug(\"Server is closing connection, will reconnect...\");\r\n break;\r\n }\r\n\r\n default: {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" with unhandled kind \"${msg.kind}\"`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n async #handleDataMessage(msg: any) {\r\n if (!msg.channel) {\r\n this.#debug(`Received message with no channel`);\r\n return;\r\n }\r\n\r\n if (!msg.topic) {\r\n this.#debug(`Received message on channel \"${msg.channel}\" with no topic`);\r\n return;\r\n }\r\n\r\n const topic = this.#topics.get(msg.topic);\r\n if (!topic) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for unknown topic \"${msg.topic}\"`,\r\n );\r\n return;\r\n }\r\n\r\n const schema = topic.getSchema();\r\n if (schema) {\r\n const validateRes = await schema[\"~standard\"].validate(msg.data);\r\n if (validateRes.issues) {\r\n console.error(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" that failed schema validation:`,\r\n validateRes.issues,\r\n );\r\n return;\r\n }\r\n\r\n msg.data = validateRes.value;\r\n }\r\n\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\":`,\r\n msg.data,\r\n );\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n data: msg.data,\r\n fnId: msg.fn_id,\r\n createdAt: msg.created_at || new Date(),\r\n runId: msg.run_id,\r\n kind: \"data\",\r\n envId: msg.env_id,\r\n });\r\n }\r\n\r\n #handleDataStreamStart(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-start with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-start with invalid stream ID`);\r\n return;\r\n }\r\n\r\n if (this.#chunkStreams.has(streamId)) {\r\n this.#debug(\r\n `Received datastream-start for stream ID \"${streamId}\" that already exists`,\r\n );\r\n return;\r\n }\r\n\r\n let holderStream: ReadableStream;\r\n let holderController: ReadableStreamDefaultController;\r\n\r\n holderStream = new ReadableStream({\r\n start: (controller) => {\r\n holderController = controller;\r\n this.#chunkStreams.set(streamId, {\r\n stream: holderStream,\r\n controller: holderController,\r\n });\r\n },\r\n\r\n cancel: () => {\r\n this.#chunkStreams.delete(streamId);\r\n },\r\n });\r\n\r\n this.#debug(`Created stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-start\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: holderStream,\r\n });\r\n }\r\n\r\n #handleDataStreamEnd(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-end with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-end with invalid stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(streamId);\r\n if (!stream) {\r\n this.#debug(\r\n `Received datastream-end for stream ID \"${streamId}\" that doesn't exist`,\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n\r\n this.#chunkStreams.delete(streamId);\r\n\r\n this.#debug(`Closed stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-end\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n #handleChunk(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received chunk with missing channel or topic`);\r\n return;\r\n }\r\n\r\n if (!msg.stream_id) {\r\n this.#debug(`Received chunk with no stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(msg.stream_id);\r\n if (!stream) {\r\n this.#debug(`Received chunk for unknown stream ID \"${msg.stream_id}\"`);\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Received chunk on channel \"${msg.channel}\" for stream ID \"${msg.stream_id}\":`,\r\n msg.data,\r\n );\r\n\r\n try {\r\n stream.controller.enqueue(msg.data);\r\n } catch (err) {\r\n this.#debug(`Error enqueueing chunk to stream ${msg.stream_id}:`, err);\r\n }\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"chunk\",\r\n data: msg.data,\r\n streamId: msg.stream_id,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n /**\r\n * Lazily get a subscription token if not provided\r\n */\r\n private async lazilyGetSubscriptionToken<\r\n const InputChannel extends Realtime.Channel | string,\r\n const InputTopics extends (keyof Realtime.Channel.InferTopics<\r\n Realtime.Channel.AsChannel<InputChannel>\r\n > &\r\n string)[],\r\n const TToken extends Realtime.Subscribe.Token<\r\n Realtime.Channel.AsChannel<InputChannel>,\r\n InputTopics\r\n >,\r\n >(\r\n /**\r\n * Subscription parameters\r\n */\r\n args: {\r\n /**\r\n * Channel ID or channel object\r\n */\r\n channel: Realtime.Subscribe.InferChannelInput<InputChannel>;\r\n\r\n /**\r\n * List of topics\r\n */\r\n topics: InputTopics;\r\n\r\n /**\r\n * Signing key for authentication\r\n */\r\n signingKey: string | undefined;\r\n\r\n /**\r\n * Fallback signing key\r\n */\r\n signingKeyFallback: string | undefined;\r\n },\r\n ): Promise<TToken> {\r\n const channelId =\r\n typeof args.channel === \"string\" ? args.channel : args.channel.name;\r\n\r\n if (!channelId) {\r\n throw new Error(\"Channel ID is required to create a subscription token\");\r\n }\r\n\r\n const key = await api.getSubscriptionToken({\r\n channel: channelId,\r\n topics: args.topics,\r\n signingKey: args.signingKey,\r\n signingKeyFallback: args.signingKeyFallback,\r\n apiBaseUrl: this.#apiBaseUrl,\r\n });\r\n\r\n const token = {\r\n channel: channelId,\r\n topics: args.topics,\r\n key,\r\n } as TToken;\r\n\r\n return token;\r\n }\r\n\r\n /**\r\n * Close the connection and cleanup resources\r\n */\r\n public close(\r\n /**\r\n * Reason for closing\r\n */\r\n reason = \"Userland closed connection\",\r\n ) {\r\n if (!this.#running && !this.#ws) {\r\n return;\r\n }\r\n\r\n this.#debug(\"close() called; closing connection...\");\r\n this.#running = false;\r\n\r\n // Clear any pending reconnection timer\r\n if (this.#reconnectTimer) {\r\n clearTimeout(this.#reconnectTimer);\r\n this.#reconnectTimer = null;\r\n }\r\n\r\n // Prevent reconnection attempts\r\n this.#reconnectAttempts = this.#maxReconnectAttempts;\r\n\r\n // Close WebSocket connection\r\n this.#cleanupWebSocket();\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n this.#debug(`Closing ${this.#fanout.size()} streams...`);\r\n this.#fanout.close();\r\n }\r\n\r\n /**\r\n * Get a new JSON stream from the subscription\r\n */\r\n public getJsonStream() {\r\n return this.#fanout.createStream();\r\n }\r\n\r\n /**\r\n * Get a new encoded stream (SSE-compatible) from the subscription\r\n */\r\n public getEncodedStream() {\r\n return this.#fanout.createStream((chunk) => {\r\n return this.#encoder.encode(`${JSON.stringify(chunk)}\\n`);\r\n });\r\n }\r\n\r\n /**\r\n * Use a callback to handle messages from the stream\r\n */\r\n public useCallback(\r\n callback: Realtime.Subscribe.Callback,\r\n stream: ReadableStream<Realtime.Message> = this.getJsonStream(),\r\n ) {\r\n void (async () => {\r\n // Explicitly get and manage the reader so that we can manually release\r\n // the lock if anything goes wrong or we're done with it.\r\n const reader = stream.getReader();\r\n try {\r\n while (this.#running) {\r\n const { done, value } = await reader.read();\r\n if (done || !this.#running) break;\r\n\r\n try {\r\n callback(value);\r\n } catch (err) {\r\n this.#debug(\"Error in callback:\", err);\r\n }\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error reading from stream:\", err);\r\n } finally {\r\n try {\r\n reader.releaseLock();\r\n } catch (err) {\r\n this.#debug(\"Error releasing reader lock:\", err);\r\n }\r\n }\r\n })();\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TokenSubscription.mjs","names":["#apiBaseUrl","#signingKey","#signingKeyFallback","#channelId","#topics","#closed","#connectionPromise","#connect","#running","#ws","#debug","#cleanupWebSocket","#reconnectAttempts","#handleMessage","#handleClose","#chunkStreams","#fanout","#maxReconnectAttempts","#reconnectDelay","#reconnectTimer","#handleDataMessage","#handleDataStreamStart","#handleDataStreamEnd","#handleChunk","#encoder"],"sources":["../../src/subscribe/TokenSubscription.ts"],"sourcesContent":["import debug from \"debug\";\r\nimport { api } from \"../api\";\r\nimport { getEnvVar } from \"../env\";\r\nimport { topic } from \"../topic\";\r\nimport { Realtime } from \"../types\";\r\nimport { createDeferredPromise, parseAsBoolean } from \"../util\";\r\nimport { StreamFanout } from \"./StreamFanout\";\r\n\r\n/**\r\n * Realtime channel subscription via WebSocket\r\n */\r\nexport class TokenSubscription {\r\n #apiBaseUrl?: string;\r\n #channelId: string;\r\n #debug = debug(\"inngest:realtime\");\r\n #encoder = new TextEncoder();\r\n #fanout = new StreamFanout<Realtime.Message>();\r\n #running = false;\r\n #closed = false;\r\n #topics: Map<string, Realtime.Topic.Definition>;\r\n #ws: WebSocket | null = null;\r\n #signingKey: string | undefined;\r\n #signingKeyFallback: string | undefined;\r\n #reconnectAttempts = 0;\r\n #maxReconnectAttempts = 5;\r\n #reconnectDelay = 1000;\r\n #connectionPromise: Promise<void> | null = null;\r\n #reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n /**\r\n * Map of stream IDs to their streams and controllers\r\n */\r\n #chunkStreams = new Map<\r\n string,\r\n { stream: ReadableStream; controller: ReadableStreamDefaultController }\r\n >();\r\n\r\n constructor(\r\n /**\r\n * Subscription token\r\n */\r\n public token: Realtime.Subscribe.Token,\r\n apiBaseUrl: string | undefined,\r\n signingKey: string | undefined,\r\n signingKeyFallback: string | undefined,\r\n ) {\r\n this.#apiBaseUrl = apiBaseUrl;\r\n this.#signingKey = signingKey;\r\n this.#signingKeyFallback = signingKeyFallback;\r\n\r\n if (typeof token.channel === \"string\") {\r\n this.#channelId = token.channel;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n } else {\r\n this.#channelId = token.channel.name;\r\n\r\n this.#topics = this.token.topics.reduce<\r\n Map<string, Realtime.Topic.Definition>\r\n >((acc, name) => {\r\n acc.set(name, token.channel.topics[name] ?? topic(name));\r\n\r\n return acc;\r\n }, new Map<string, Realtime.Topic.Definition>());\r\n }\r\n }\r\n\r\n private async getWsUrl(token: string): Promise<URL> {\r\n let url: URL;\r\n const path = \"/v1/realtime/connect\";\r\n const devEnvVar = getEnvVar(\"INNGEST_DEV\");\r\n\r\n if (this.#apiBaseUrl) {\r\n url = new URL(path, this.#apiBaseUrl);\r\n } else if (devEnvVar) {\r\n try {\r\n const devUrl = new URL(devEnvVar);\r\n url = new URL(path, devUrl);\r\n } catch {\r\n if (parseAsBoolean(devEnvVar)) {\r\n url = new URL(path, \"http://localhost:8288/\");\r\n } else {\r\n url = new URL(path, \"https://api.inngest.com/\");\r\n }\r\n }\r\n } else {\r\n url = new URL(\r\n path,\r\n getEnvVar(\"NODE_ENV\") === \"production\"\r\n ? \"https://api.inngest.com/\"\r\n : \"http://localhost:8288/\",\r\n );\r\n }\r\n\r\n url.protocol = url.protocol === \"http:\" ? \"ws:\" : \"wss:\";\r\n url.searchParams.set(\"token\", token);\r\n\r\n return url;\r\n }\r\n\r\n /**\r\n * Establish WebSocket connection\r\n */\r\n public async connect() {\r\n // Don't connect if instance was explicitly closed\r\n if (this.#closed) {\r\n return;\r\n }\r\n\r\n // Prevent multiple simultaneous connection attempts\r\n if (this.#connectionPromise) {\r\n return this.#connectionPromise;\r\n }\r\n\r\n this.#connectionPromise = this.#connect();\r\n return this.#connectionPromise;\r\n }\r\n\r\n async #connect() {\r\n // Guard against concurrent connection attempts\r\n if (this.#running && this.#ws?.readyState === WebSocket.OPEN) {\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Establishing connection to channel \"${\r\n this.#channelId\r\n }\" with topics ${JSON.stringify([...this.#topics.keys()])}...`,\r\n );\r\n\r\n if (typeof WebSocket === \"undefined\") {\r\n throw new Error(\"WebSocket is not supported in current environment\");\r\n }\r\n\r\n let key = this.token.key;\r\n if (!key) {\r\n this.#debug(\r\n \"No subscription token key passed; attempting to retrieve one automatically...\",\r\n );\r\n\r\n key = (\r\n await this.lazilyGetSubscriptionToken({\r\n ...this.token,\r\n signingKey: this.#signingKey,\r\n signingKeyFallback: this.#signingKeyFallback,\r\n })\r\n ).key;\r\n\r\n if (!key) {\r\n throw new Error(\r\n \"No subscription token key provided and failed to retrieve one automatically\",\r\n );\r\n }\r\n }\r\n\r\n const ret = createDeferredPromise<void>();\r\n\r\n try {\r\n // Clean up existing connection if any\r\n if (this.#ws) {\r\n this.#cleanupWebSocket();\r\n }\r\n\r\n this.#ws = new WebSocket(await this.getWsUrl(key));\r\n\r\n this.#ws.onopen = () => {\r\n this.#debug(\"WebSocket connection established\");\r\n this.#reconnectAttempts = 0;\r\n this.#running = true;\r\n // Clear connection promise only after successful connection\r\n this.#connectionPromise = null;\r\n ret.resolve();\r\n };\r\n\r\n this.#ws.onmessage = async (event) => {\r\n await this.#handleMessage(event);\r\n };\r\n\r\n this.#ws.onerror = (event) => {\r\n this.#debug(\"WebSocket error observed:\", event);\r\n ret.reject(new Error(\"WebSocket connection error\"));\r\n };\r\n\r\n this.#ws.onclose = (event) => {\r\n this.#debug(\"WebSocket closed:\", event.code, event.reason);\r\n this.#handleClose(event);\r\n };\r\n } catch (err) {\r\n // Clear connection promise on error\r\n this.#connectionPromise = null;\r\n ret.reject(err);\r\n }\r\n\r\n return ret.promise;\r\n }\r\n\r\n #cleanupWebSocket() {\r\n if (!this.#ws) return;\r\n\r\n try {\r\n // Remove event listeners to prevent memory leaks\r\n this.#ws.onopen = null;\r\n this.#ws.onmessage = null;\r\n this.#ws.onerror = null;\r\n this.#ws.onclose = null;\r\n\r\n // Close connection if still open\r\n if (\r\n this.#ws.readyState === WebSocket.OPEN ||\r\n this.#ws.readyState === WebSocket.CONNECTING\r\n ) {\r\n this.#ws.close(1000, \"Cleaning up connection\");\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error cleaning up WebSocket:\", err);\r\n }\r\n\r\n this.#ws = null;\r\n }\r\n\r\n #handleClose(event: CloseEvent) {\r\n const wasRunning = this.#running;\r\n this.#running = false;\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n // Normal closure or user-initiated close\r\n if (event.code === 1000 || !wasRunning) {\r\n this.#debug(\"Connection closed normally\");\r\n // Clear connection promise on normal close\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n return;\r\n }\r\n\r\n // Attempt reconnection for abnormal closures\r\n if (this.#reconnectAttempts < this.#maxReconnectAttempts) {\r\n this.#reconnectAttempts++;\r\n const delay =\r\n this.#reconnectDelay * Math.pow(2, this.#reconnectAttempts - 1);\r\n\r\n this.#debug(\r\n `Attempting reconnection ${this.#reconnectAttempts}/${this.#maxReconnectAttempts} in ${delay}ms...`,\r\n );\r\n\r\n // Clear the old connection promise before attempting reconnection\r\n this.#connectionPromise = null;\r\n\r\n // Store timer ID so it can be cleared in close()\r\n this.#reconnectTimer = setTimeout(() => {\r\n this.#reconnectTimer = null;\r\n\r\n // connect() will set a new #connectionPromise, preventing races\r\n this.connect().catch((err) => {\r\n this.#debug(\"Reconnection failed:\", err);\r\n if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {\r\n this.#debug(\"Max reconnection attempts reached, closing streams\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n });\r\n }, delay);\r\n } else {\r\n this.#debug(\"Max reconnection attempts reached\");\r\n this.#connectionPromise = null;\r\n this.#fanout.close();\r\n }\r\n }\r\n\r\n async #handleMessage(event: MessageEvent) {\r\n let parsedData;\r\n try {\r\n parsedData = JSON.parse(event.data as string);\r\n } catch (err) {\r\n this.#debug(\"Failed to parse JSON from WebSocket message:\", err);\r\n this.#debug(\"Raw payload:\", event.data);\r\n return;\r\n }\r\n\r\n const parseRes = await Realtime.messageSchema.safeParseAsync(parsedData);\r\n\r\n if (!parseRes.success) {\r\n this.#debug(\"Received invalid message:\", parseRes.error);\r\n return;\r\n }\r\n\r\n const msg = parseRes.data;\r\n\r\n if (!this.#running) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" but stream is closed`,\r\n );\r\n return;\r\n }\r\n\r\n switch (msg.kind) {\r\n case \"data\": {\r\n await this.#handleDataMessage(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-start\": {\r\n this.#handleDataStreamStart(msg);\r\n break;\r\n }\r\n\r\n case \"datastream-end\": {\r\n this.#handleDataStreamEnd(msg);\r\n break;\r\n }\r\n\r\n case \"chunk\": {\r\n this.#handleChunk(msg);\r\n break;\r\n }\r\n\r\n case \"ping\": {\r\n // Respond to ping with pong to keep connection alive\r\n if (this.#ws?.readyState === WebSocket.OPEN) {\r\n this.#ws.send(JSON.stringify({ kind: \"pong\" }));\r\n }\r\n break;\r\n }\r\n\r\n case \"closing\": {\r\n this.#debug(\"Server is closing connection, will reconnect...\");\r\n break;\r\n }\r\n\r\n default: {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" with unhandled kind \"${msg.kind}\"`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n async #handleDataMessage(msg: any) {\r\n if (!msg.channel) {\r\n this.#debug(`Received message with no channel`);\r\n return;\r\n }\r\n\r\n if (!msg.topic) {\r\n this.#debug(`Received message on channel \"${msg.channel}\" with no topic`);\r\n return;\r\n }\r\n\r\n const topic = this.#topics.get(msg.topic);\r\n if (!topic) {\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for unknown topic \"${msg.topic}\"`,\r\n );\r\n return;\r\n }\r\n\r\n const schema = topic.getSchema();\r\n if (schema) {\r\n const validateRes = await schema[\"~standard\"].validate(msg.data);\r\n if (validateRes.issues) {\r\n console.error(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\" that failed schema validation:`,\r\n validateRes.issues,\r\n );\r\n return;\r\n }\r\n\r\n msg.data = validateRes.value;\r\n }\r\n\r\n this.#debug(\r\n `Received message on channel \"${msg.channel}\" for topic \"${msg.topic}\":`,\r\n msg.data,\r\n );\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n data: msg.data,\r\n fnId: msg.fn_id,\r\n createdAt: msg.created_at || new Date(),\r\n runId: msg.run_id,\r\n kind: \"data\",\r\n envId: msg.env_id,\r\n });\r\n }\r\n\r\n #handleDataStreamStart(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-start with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-start with invalid stream ID`);\r\n return;\r\n }\r\n\r\n if (this.#chunkStreams.has(streamId)) {\r\n this.#debug(\r\n `Received datastream-start for stream ID \"${streamId}\" that already exists`,\r\n );\r\n return;\r\n }\r\n\r\n let holderStream: ReadableStream;\r\n let holderController: ReadableStreamDefaultController;\r\n\r\n holderStream = new ReadableStream({\r\n start: (controller) => {\r\n holderController = controller;\r\n this.#chunkStreams.set(streamId, {\r\n stream: holderStream,\r\n controller: holderController,\r\n });\r\n },\r\n\r\n cancel: () => {\r\n this.#chunkStreams.delete(streamId);\r\n },\r\n });\r\n\r\n this.#debug(`Created stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-start\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: holderStream,\r\n });\r\n }\r\n\r\n #handleDataStreamEnd(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received datastream-end with missing channel or topic`);\r\n return;\r\n }\r\n\r\n const streamId: unknown = msg.data;\r\n if (typeof streamId !== \"string\" || !streamId) {\r\n this.#debug(`Received datastream-end with invalid stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(streamId);\r\n if (!stream) {\r\n this.#debug(\r\n `Received datastream-end for stream ID \"${streamId}\" that doesn't exist`,\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n\r\n this.#chunkStreams.delete(streamId);\r\n\r\n this.#debug(`Closed stream ID \"${streamId}\" on channel \"${msg.channel}\"`);\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"datastream-end\",\r\n data: streamId,\r\n streamId,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n #handleChunk(msg: any) {\r\n if (!msg.channel || !msg.topic) {\r\n this.#debug(`Received chunk with missing channel or topic`);\r\n return;\r\n }\r\n\r\n if (!msg.stream_id) {\r\n this.#debug(`Received chunk with no stream ID`);\r\n return;\r\n }\r\n\r\n const stream = this.#chunkStreams.get(msg.stream_id);\r\n if (!stream) {\r\n this.#debug(`Received chunk for unknown stream ID \"${msg.stream_id}\"`);\r\n return;\r\n }\r\n\r\n this.#debug(\r\n `Received chunk on channel \"${msg.channel}\" for stream ID \"${msg.stream_id}\":`,\r\n msg.data,\r\n );\r\n\r\n try {\r\n stream.controller.enqueue(msg.data);\r\n } catch (err) {\r\n this.#debug(`Error enqueueing chunk to stream ${msg.stream_id}:`, err);\r\n }\r\n\r\n this.#fanout.write({\r\n channel: msg.channel,\r\n topic: msg.topic,\r\n kind: \"chunk\",\r\n data: msg.data,\r\n streamId: msg.stream_id,\r\n fnId: msg.fn_id,\r\n runId: msg.run_id,\r\n stream: stream.stream,\r\n });\r\n }\r\n\r\n /**\r\n * Lazily get a subscription token if not provided\r\n */\r\n private async lazilyGetSubscriptionToken<\r\n const InputChannel extends Realtime.Channel | string,\r\n const InputTopics extends (keyof Realtime.Channel.InferTopics<\r\n Realtime.Channel.AsChannel<InputChannel>\r\n > &\r\n string)[],\r\n const TToken extends Realtime.Subscribe.Token<\r\n Realtime.Channel.AsChannel<InputChannel>,\r\n InputTopics\r\n >,\r\n >(\r\n /**\r\n * Subscription parameters\r\n */\r\n args: {\r\n /**\r\n * Channel ID or channel object\r\n */\r\n channel: Realtime.Subscribe.InferChannelInput<InputChannel>;\r\n\r\n /**\r\n * List of topics\r\n */\r\n topics: InputTopics;\r\n\r\n /**\r\n * Signing key for authentication\r\n */\r\n signingKey: string | undefined;\r\n\r\n /**\r\n * Fallback signing key\r\n */\r\n signingKeyFallback: string | undefined;\r\n },\r\n ): Promise<TToken> {\r\n const channelId =\r\n typeof args.channel === \"string\" ? args.channel : args.channel.name;\r\n\r\n if (!channelId) {\r\n throw new Error(\"Channel ID is required to create a subscription token\");\r\n }\r\n\r\n const key = await api.getSubscriptionToken({\r\n channel: channelId,\r\n topics: args.topics,\r\n signingKey: args.signingKey,\r\n signingKeyFallback: args.signingKeyFallback,\r\n apiBaseUrl: this.#apiBaseUrl,\r\n });\r\n\r\n const token = {\r\n channel: channelId,\r\n topics: args.topics,\r\n key,\r\n } as TToken;\r\n\r\n return token;\r\n }\r\n\r\n /**\r\n * Close the connection and cleanup resources\r\n */\r\n public close() {\r\n if (this.#closed) {\r\n return;\r\n }\r\n\r\n this.#debug(\"close() called; closing connection...\");\r\n this.#closed = true;\r\n this.#running = false;\r\n\r\n // Clear any pending reconnection timer\r\n if (this.#reconnectTimer) {\r\n clearTimeout(this.#reconnectTimer);\r\n this.#reconnectTimer = null;\r\n }\r\n\r\n // Prevent reconnection attempts\r\n this.#reconnectAttempts = this.#maxReconnectAttempts;\r\n\r\n // Close WebSocket connection\r\n this.#cleanupWebSocket();\r\n\r\n // Close all chunk streams\r\n for (const [streamId, stream] of this.#chunkStreams.entries()) {\r\n try {\r\n stream.controller.close();\r\n } catch (err) {\r\n this.#debug(`Error closing stream ${streamId}:`, err);\r\n }\r\n }\r\n this.#chunkStreams.clear();\r\n\r\n this.#debug(`Closing ${this.#fanout.size()} streams...`);\r\n this.#fanout.close();\r\n }\r\n\r\n /**\r\n * Get a new JSON stream from the subscription\r\n */\r\n public getJsonStream() {\r\n return this.#fanout.createStream();\r\n }\r\n\r\n /**\r\n * Get a new encoded stream (SSE-compatible) from the subscription\r\n */\r\n public getEncodedStream() {\r\n return this.#fanout.createStream((chunk) => {\r\n return this.#encoder.encode(`${JSON.stringify(chunk)}\\n`);\r\n });\r\n }\r\n\r\n /**\r\n * Use a callback to handle messages from the stream\r\n */\r\n public useCallback(\r\n callback: Realtime.Subscribe.Callback,\r\n stream: ReadableStream<Realtime.Message> = this.getJsonStream(),\r\n ) {\r\n void (async () => {\r\n // Explicitly get and manage the reader so that we can manually release\r\n // the lock if anything goes wrong or we're done with it.\r\n const reader = stream.getReader();\r\n try {\r\n while (this.#running) {\r\n const { done, value } = await reader.read();\r\n if (done || !this.#running) break;\r\n\r\n try {\r\n callback(value);\r\n } catch (err) {\r\n this.#debug(\"Error in callback:\", err);\r\n }\r\n }\r\n } catch (err) {\r\n this.#debug(\"Error reading from stream:\", err);\r\n } finally {\r\n try {\r\n reader.releaseLock();\r\n } catch (err) {\r\n this.#debug(\"Error releasing reader lock:\", err);\r\n }\r\n }\r\n })();\r\n }\r\n}\r\n"],"mappings":"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"}
|