UNPKG

inngest

Version:

Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.

830 lines • 41.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.connect = exports.DEFAULT_SHUTDOWN_SIGNALS = void 0; const waitgroup_1 = require("@jpwilliams/waitgroup"); const debug_1 = __importDefault(require("debug")); const ms_1 = __importDefault(require("ms")); const consts_js_1 = require("../../helpers/consts.js"); const env_js_1 = require("../../helpers/env.js"); const functions_js_1 = require("../../helpers/functions.js"); const strings_js_1 = require("../../helpers/strings.js"); const connect_js_1 = require("../../proto/src/components/connect/protobuf/connect.js"); const version_js_1 = require("../../version.js"); const InngestExecution_js_1 = require("../execution/InngestExecution.js"); const InngestCommHandler_js_1 = require("../InngestCommHandler.js"); const buffer_js_1 = require("./buffer.js"); const messages_js_1 = require("./messages.js"); const os_js_1 = require("./os.js"); const types_js_1 = require("./types.js"); Object.defineProperty(exports, "DEFAULT_SHUTDOWN_SIGNALS", { enumerable: true, get: function () { return types_js_1.DEFAULT_SHUTDOWN_SIGNALS; } }); const util_js_1 = require("./util.js"); const ResponseAcknowlegeDeadline = 5000; const InngestBranchEnvironmentSigningKeyPrefix = "signkey-branch-"; const ConnectWebSocketProtocol = "v0.connect.inngest.com"; class WebSocketWorkerConnection { constructor(options) { var _a; /** * The current state of the connection. */ this.state = types_js_1.ConnectionState.CONNECTING; this.inProgressRequests = { wg: new waitgroup_1.WaitGroup(), requestLeases: {}, }; /** * A set of gateways to exclude from the connection. */ this.excludeGateways = new Set(); if (!Array.isArray(options.apps) || options.apps.length === 0 || !options.apps[0]) { throw new Error("No apps provided"); } this.inngest = options.apps[0].client; for (const app of options.apps) { if (app.client.env !== this.inngest.env) { throw new Error(`All apps must be configured to the same environment. ${app.client.id} is configured to ${app.client.env} but ${this.inngest.id} is configured to ${this.inngest.env}`); } } this.options = this.applyDefaults(options); this._inngestEnv = (_a = this.inngest.env) !== null && _a !== void 0 ? _a : (0, env_js_1.getEnvironmentName)(); this.debug = (0, debug_1.default)("inngest:connect"); this.messageBuffer = new buffer_js_1.MessageBuffer(this.inngest); this.closingPromise = new Promise((resolve) => { this.resolveClosingPromise = resolve; }); } get functions() { var _a; const functions = {}; for (const app of this.options.apps) { if (functions[app.client.id]) { throw new Error(`Duplicate app id: ${app.client.id}`); } const client = app.client; functions[app.client.id] = { client: app.client, functions: (_a = app.functions) !== null && _a !== void 0 ? _a : client.funcs, }; } return functions; } applyDefaults(opts) { const options = Object.assign({}, opts); if (!Array.isArray(options.handleShutdownSignals)) { options.handleShutdownSignals = types_js_1.DEFAULT_SHUTDOWN_SIGNALS; } const env = (0, env_js_1.allProcessEnv)(); options.signingKey = options.signingKey || env[consts_js_1.envKeys.InngestSigningKey]; options.signingKeyFallback = options.signingKeyFallback || env[consts_js_1.envKeys.InngestSigningKeyFallback]; return options; } // eslint-disable-next-line @typescript-eslint/require-await async close() { var _a; // Remove the shutdown signal handler if (this.cleanupShutdownSignal) { this.cleanupShutdownSignal(); this.cleanupShutdownSignal = undefined; } this.state = types_js_1.ConnectionState.CLOSING; this.debug("Cleaning up connection resources"); if (this.currentConnection) { await this.currentConnection.cleanup(); this.currentConnection = undefined; } this.debug("Connection closed"); this.debug("Waiting for in-flight requests to complete"); await this.inProgressRequests.wg.wait(); this.debug("Flushing messages before closing"); try { await this.messageBuffer.flush(this._hashedSigningKey); } catch (err) { this.debug("Failed to flush messages, using fallback key", err); await this.messageBuffer.flush(this._hashedFallbackKey); } this.state = types_js_1.ConnectionState.CLOSED; (_a = this.resolveClosingPromise) === null || _a === void 0 ? void 0 : _a.call(this); this.debug("Fully closed"); return this.closed; } /** * A promise that resolves when the connection is closed on behalf of the * user by calling `close()` or when a shutdown signal is received. */ get closed() { if (!this.closingPromise) { throw new Error("No connection established"); } return this.closingPromise; } /** * The current connection ID of the worker. */ get connectionId() { if (!this.currentConnection) { throw new Error("Connection not prepared"); } return this.currentConnection.id; } /** * Establish a persistent connection to the gateway. */ async connect(attempt = 0, path = []) { if (typeof WebSocket === "undefined") { throw new Error("WebSockets not supported in current environment"); } if (this.state === types_js_1.ConnectionState.CLOSING || this.state === types_js_1.ConnectionState.CLOSED) { throw new Error("Connection already closed"); } this.debug("Establishing connection", { attempt }); if (this.inngest["mode"].isCloud && !this.options.signingKey) { throw new Error("Signing key is required"); } this._hashedSigningKey = this.options.signingKey ? (0, strings_js_1.hashSigningKey)(this.options.signingKey) : undefined; if (this.options.signingKey && this.options.signingKey.startsWith(InngestBranchEnvironmentSigningKeyPrefix) && !this._inngestEnv) { throw new Error("Environment is required when using branch environment signing keys"); } if (this.options.signingKeyFallback) { this._hashedFallbackKey = (0, strings_js_1.hashSigningKey)(this.options.signingKeyFallback); } try { await this.messageBuffer.flush(this._hashedSigningKey); } catch (err) { this.debug("Failed to flush messages, using fallback key", err); await this.messageBuffer.flush(this._hashedFallbackKey); } const capabilities = { trust_probe: "v1", connect: "v1", }; const functionConfigs = {}; for (const [appId, { client, functions }] of Object.entries(this.functions)) { functionConfigs[appId] = { client: client, functions: functions.flatMap((f) => f["getConfig"]({ baseUrl: new URL("wss://connect"), appPrefix: client.id, isConnect: true, })), }; } this.debug("Prepared sync data", { functionSlugs: Object.entries(functionConfigs).map(([appId, { functions }]) => { return JSON.stringify({ appId, functions: functions.map((f) => ({ id: f.id, stepUrls: Object.values(f.steps).map((s) => s.runtime["url"]), })), }); }), }); const data = { manualReadinessAck: false, marshaledCapabilities: JSON.stringify(capabilities), apps: Object.entries(functionConfigs).map(([appId, { client, functions }]) => ({ appName: appId, appVersion: client.appVersion, functions: new TextEncoder().encode(JSON.stringify(functions)), })), }; const requestHandlers = {}; for (const [appId, { client, functions }] of Object.entries(this.functions)) { const inngestCommHandler = new InngestCommHandler_js_1.InngestCommHandler({ client: client, functions: functions, frameworkName: "connect", signingKey: this.options.signingKey, signingKeyFallback: this.options.signingKeyFallback, skipSignatureValidation: true, handler: (msg) => { const asString = new TextDecoder().decode(msg.requestPayload); const parsed = (0, functions_js_1.parseFnData)(JSON.parse(asString)); const userTraceCtx = (0, util_js_1.parseTraceCtx)(msg.userTraceCtx); return { body() { return parsed; }, method() { return "POST"; }, headers(key) { var _a, _b; switch (key) { case consts_js_1.headerKeys.ContentLength.toString(): return asString.length.toString(); case consts_js_1.headerKeys.InngestExpectedServerKind.toString(): return "connect"; case consts_js_1.headerKeys.RequestVersion.toString(): return parsed.version.toString(); case consts_js_1.headerKeys.Signature.toString(): // Note: Signature is disabled for connect return null; case consts_js_1.headerKeys.TraceParent.toString(): return (_a = userTraceCtx === null || userTraceCtx === void 0 ? void 0 : userTraceCtx.traceParent) !== null && _a !== void 0 ? _a : null; case consts_js_1.headerKeys.TraceState.toString(): return (_b = userTraceCtx === null || userTraceCtx === void 0 ? void 0 : userTraceCtx.traceState) !== null && _b !== void 0 ? _b : null; default: return null; } }, transformResponse({ body, headers, status }) { var _a; let sdkResponseStatus = connect_js_1.SDKResponseStatus.DONE; switch (status) { case 200: sdkResponseStatus = connect_js_1.SDKResponseStatus.DONE; break; case 206: sdkResponseStatus = connect_js_1.SDKResponseStatus.NOT_COMPLETED; break; case 500: sdkResponseStatus = connect_js_1.SDKResponseStatus.ERROR; break; } return connect_js_1.SDKResponse.create({ requestId: msg.requestId, accountId: msg.accountId, envId: msg.envId, appId: msg.appId, status: sdkResponseStatus, body: new TextEncoder().encode(body), noRetry: headers[consts_js_1.headerKeys.NoRetry] === "true", retryAfter: headers[consts_js_1.headerKeys.RetryAfter], sdkVersion: `inngest-js:v${version_js_1.version}`, requestVersion: parseInt((_a = headers[consts_js_1.headerKeys.RequestVersion]) !== null && _a !== void 0 ? _a : InngestExecution_js_1.PREFERRED_EXECUTION_VERSION.toString(), 10), systemTraceCtx: msg.systemTraceCtx, userTraceCtx: msg.userTraceCtx, runId: msg.runId, }); }, url() { const baseUrl = new URL("http://connect.inngest.com"); baseUrl.searchParams.set(consts_js_1.queryKeys.FnId, msg.functionSlug); if (msg.stepId) { baseUrl.searchParams.set(consts_js_1.queryKeys.StepId, msg.stepId); } return baseUrl; }, }; }, }); const requestHandler = inngestCommHandler.createHandler(); requestHandlers[appId] = requestHandler; } if (this.options.handleShutdownSignals && this.options.handleShutdownSignals.length > 0) { this.setupShutdownSignal(this.options.handleShutdownSignals); } let useSigningKey = this._hashedSigningKey; while (![types_js_1.ConnectionState.CLOSING, types_js_1.ConnectionState.CLOSED].includes(this.state)) { // Clean up any previous connection state // Note: Never reset the message buffer, as there may be pending/unsent messages { // Flush any pending messages await this.messageBuffer.flush(useSigningKey); } try { await this.prepareConnection(requestHandlers, useSigningKey, data, attempt, [...path]); return; } catch (err) { this.debug("Failed to connect", err); if (!(err instanceof util_js_1.ReconnectError)) { throw err; } attempt = err.attempt; if (err instanceof util_js_1.AuthError) { const switchToFallback = useSigningKey === this._hashedSigningKey; if (switchToFallback) { this.debug("Switching to fallback signing key"); } useSigningKey = switchToFallback ? this._hashedFallbackKey : this._hashedSigningKey; } if (err instanceof util_js_1.ConnectionLimitError) { console.error("You have reached the maximum number of concurrent connections. Please disconnect other active workers to continue."); // Continue reconnecting, do not throw. } const delay = (0, util_js_1.expBackoff)(attempt); this.debug("Reconnecting in", delay, "ms"); const cancelled = await (0, util_js_1.waitWithCancel)(delay, () => this.state === types_js_1.ConnectionState.CLOSING || this.state === types_js_1.ConnectionState.CLOSED); if (cancelled) { this.debug("Reconnect backoff cancelled"); break; } attempt++; } } this.debug("Exiting connect loop"); } async sendStartRequest(hashedSigningKey, attempt) { const msg = (0, messages_js_1.createStartRequest)(Array.from(this.excludeGateways)); const headers = Object.assign({ "Content-Type": "application/protobuf" }, (hashedSigningKey ? { Authorization: `Bearer ${hashedSigningKey}` } : {})); if (this._inngestEnv) { headers[consts_js_1.headerKeys.Environment] = this._inngestEnv; } // refactor this to a more universal spot const targetUrl = await this.inngest["inngestApi"]["getTargetUrl"]("/v0/connect/start"); let resp; try { resp = await fetch(targetUrl, { method: "POST", body: msg, headers: headers, }); } catch (err) { const errMsg = err instanceof Error ? err.message : "Unknown error"; throw new util_js_1.ReconnectError(`Failed initial API handshake request to ${targetUrl.toString()}, ${errMsg}`, attempt); } if (!resp.ok) { if (resp.status === 401) { throw new util_js_1.AuthError(`Failed initial API handshake request to ${targetUrl.toString()}${this._inngestEnv ? ` (env: ${this._inngestEnv})` : ""}, ${await resp.text()}`, attempt); } if (resp.status === 429) { throw new util_js_1.ConnectionLimitError(attempt); } throw new util_js_1.ReconnectError(`Failed initial API handshake request to ${targetUrl.toString()}, ${await resp.text()}`, attempt); } const startResp = await (0, messages_js_1.parseStartResponse)(resp); return startResp; } async prepareConnection(requestHandlers, hashedSigningKey, data, attempt, path = []) { let closed = false; this.debug("Preparing connection", { attempt, path, }); const startedAt = new Date(); const startResp = await this.sendStartRequest(hashedSigningKey, attempt); const connectionId = startResp.connectionId; path.push(connectionId); let resolveWebsocketConnected; // eslint-disable-next-line @typescript-eslint/no-explicit-any let rejectWebsocketConnected; const websocketConnectedPromise = new Promise((resolve, reject) => { resolveWebsocketConnected = resolve; rejectWebsocketConnected = reject; }); const connectTimeout = setTimeout(() => { this.excludeGateways.add(startResp.gatewayGroup); rejectWebsocketConnected === null || rejectWebsocketConnected === void 0 ? void 0 : rejectWebsocketConnected(new util_js_1.ReconnectError(`Connection ${connectionId} timed out`, attempt)); }, 10000); let finalEndpoint = startResp.gatewayEndpoint; if (this.options.rewriteGatewayEndpoint) { const rewritten = this.options.rewriteGatewayEndpoint(startResp.gatewayEndpoint); this.debug("Rewriting gateway endpoint", { original: startResp.gatewayEndpoint, rewritten, }); finalEndpoint = rewritten; } this.debug(`Connecting to gateway`, { endpoint: finalEndpoint, gatewayGroup: startResp.gatewayGroup, connectionId, }); const ws = new WebSocket(finalEndpoint, [ConnectWebSocketProtocol]); ws.binaryType = "arraybuffer"; let onConnectionError; { onConnectionError = (error) => { // Only process the first error per connection if (closed) { this.debug(`Connection error while initializing but already in closed state, skipping`, { connectionId, }); return; } closed = true; this.debug(`Connection error in connecting state, rejecting promise`, { connectionId, }); this.excludeGateways.add(startResp.gatewayGroup); clearTimeout(connectTimeout); // Make sure to close the WebSocket if it's still open ws.onerror = () => { }; ws.onclose = () => { }; ws.close(4001, // incomplete setup (0, connect_js_1.workerDisconnectReasonToJSON)(connect_js_1.WorkerDisconnectReason.UNEXPECTED)); rejectWebsocketConnected === null || rejectWebsocketConnected === void 0 ? void 0 : rejectWebsocketConnected(new util_js_1.ReconnectError(`Error while connecting (${connectionId}): ${error instanceof Error ? error.message : "Unknown error"}`, attempt)); }; ws.onerror = (err) => onConnectionError(err); ws.onclose = (ev) => { void onConnectionError(new util_js_1.ReconnectError(`Connection ${connectionId} closed: ${ev.reason}`, attempt)); }; } /** * The current setup state of the connection. */ const setupState = { receivedGatewayHello: false, sentWorkerConnect: false, receivedConnectionReady: false, }; let heartbeatIntervalMs; let extendLeaseIntervalMs; ws.onmessage = async (event) => { const messageBytes = new Uint8Array(event.data); const connectMessage = (0, messages_js_1.parseConnectMessage)(messageBytes); this.debug(`Received message: ${(0, connect_js_1.gatewayMessageTypeToJSON)(connectMessage.kind)}`, { connectionId, }); if (!setupState.receivedGatewayHello) { if (connectMessage.kind !== connect_js_1.GatewayMessageType.GATEWAY_HELLO) { void onConnectionError(new util_js_1.ReconnectError(`Expected hello message, got ${(0, connect_js_1.gatewayMessageTypeToJSON)(connectMessage.kind)}`, attempt)); return; } setupState.receivedGatewayHello = true; } if (!setupState.sentWorkerConnect) { const workerConnectRequestMsg = connect_js_1.WorkerConnectRequestData.create({ connectionId: startResp.connectionId, environment: this._inngestEnv, platform: (0, env_js_1.getPlatformName)(Object.assign({}, (0, env_js_1.allProcessEnv)())), sdkVersion: `v${version_js_1.version}`, sdkLanguage: "typescript", framework: "connect", workerManualReadinessAck: data.manualReadinessAck, systemAttributes: await (0, os_js_1.retrieveSystemAttributes)(), authData: { sessionToken: startResp.sessionToken, syncToken: startResp.syncToken, }, apps: data.apps, capabilities: new TextEncoder().encode(data.marshaledCapabilities), startedAt: startedAt, instanceId: this.options.instanceId || (await (0, os_js_1.getHostname)()), }); const workerConnectRequestMsgBytes = connect_js_1.WorkerConnectRequestData.encode(workerConnectRequestMsg).finish(); ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_CONNECT, payload: workerConnectRequestMsgBytes, })).finish()); setupState.sentWorkerConnect = true; return; } if (!setupState.receivedConnectionReady) { if (connectMessage.kind !== connect_js_1.GatewayMessageType.GATEWAY_CONNECTION_READY) { void onConnectionError(new util_js_1.ReconnectError(`Expected ready message, got ${(0, connect_js_1.gatewayMessageTypeToJSON)(connectMessage.kind)}`, attempt)); return; } const readyPayload = connect_js_1.GatewayConnectionReadyData.decode(connectMessage.payload); setupState.receivedConnectionReady = true; // The intervals should be supplied by the gateway, but we should fall back just in case heartbeatIntervalMs = readyPayload.heartbeatInterval.length > 0 ? (0, ms_1.default)(readyPayload.heartbeatInterval) : 10000; extendLeaseIntervalMs = readyPayload.extendLeaseInterval.length > 0 ? (0, ms_1.default)(readyPayload.extendLeaseInterval) : 5000; resolveWebsocketConnected === null || resolveWebsocketConnected === void 0 ? void 0 : resolveWebsocketConnected(); return; } this.debug("Unexpected message type during setup", { kind: (0, connect_js_1.gatewayMessageTypeToJSON)(connectMessage.kind), rawKind: connectMessage.kind, attempt, setupState: setupState, state: this.state, connectionId, }); }; await websocketConnectedPromise; clearTimeout(connectTimeout); this.state = types_js_1.ConnectionState.ACTIVE; this.excludeGateways.delete(startResp.gatewayGroup); attempt = 0; const conn = { id: connectionId, ws, cleanup: () => { if (closed) { return; } closed = true; ws.onerror = () => { }; ws.onclose = () => { }; ws.close(); }, pendingHeartbeats: 0, }; this.currentConnection = conn; this.debug(`Connection ready (${connectionId})`); // Flag to prevent connecting twice in draining scenario: // 1. We're already draining and repeatedly trying to connect while keeping the old connection open // 2. The gateway closes the old connection after a timeout, causing a connection error (which would also trigger a new connection) let isDraining = false; { onConnectionError = async (error) => { // Only process the first error per connection if (closed) { this.debug(`Connection error but already in closed state, skipping`, { connectionId, }); return; } closed = true; await conn.cleanup(); // Don't attempt to reconnect if we're already closing or closed if (this.state === types_js_1.ConnectionState.CLOSING || this.state === types_js_1.ConnectionState.CLOSED) { this.debug(`Connection error (${connectionId}) but already closing or closed, skipping`); return; } this.state = types_js_1.ConnectionState.RECONNECTING; this.excludeGateways.add(startResp.gatewayGroup); // If this connection is draining and got closed unexpectedly, there's already a new connection being established if (isDraining) { this.debug(`Connection error (${connectionId}) but already draining, skipping`); return; } this.debug(`Connection error (${connectionId})`, error); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.connect(attempt + 1, [...path, "onConnectionError"]); }; ws.onerror = (err) => onConnectionError(err); ws.onclose = (ev) => { void onConnectionError(new util_js_1.ReconnectError(`Connection closed: ${ev.reason}`, attempt)); }; } ws.onmessage = async (event) => { const messageBytes = new Uint8Array(event.data); const connectMessage = (0, messages_js_1.parseConnectMessage)(messageBytes); if (connectMessage.kind === connect_js_1.GatewayMessageType.GATEWAY_CLOSING) { isDraining = true; this.debug("Received draining message", { connectionId }); try { this.debug("Setting up new connection while keeping previous connection open", { connectionId }); // Wait for new conn to be successfully established await this.connect(0, [...path]); // Clean up the old connection await conn.cleanup(); } catch (err) { this.debug("Failed to reconnect after receiving draining message", { connectionId, }); // Clean up the old connection await conn.cleanup(); void onConnectionError(new util_js_1.ReconnectError(`Failed to reconnect after receiving draining message (${connectionId})`, attempt)); } return; } if (connectMessage.kind === connect_js_1.GatewayMessageType.GATEWAY_HEARTBEAT) { conn.pendingHeartbeats = 0; this.debug("Handled gateway heartbeat", { connectionId, }); return; } if (connectMessage.kind === connect_js_1.GatewayMessageType.GATEWAY_EXECUTOR_REQUEST) { if (this.state !== types_js_1.ConnectionState.ACTIVE) { this.debug("Received request while not active, skipping", { connectionId, }); return; } const gatewayExecutorRequest = (0, messages_js_1.parseGatewayExecutorRequest)(connectMessage.payload); this.debug("Received gateway executor request", { requestId: gatewayExecutorRequest.requestId, appId: gatewayExecutorRequest.appId, appName: gatewayExecutorRequest.appName, functionSlug: gatewayExecutorRequest.functionSlug, stepId: gatewayExecutorRequest.stepId, connectionId, }); if (typeof gatewayExecutorRequest.appName !== "string" || gatewayExecutorRequest.appName.length === 0) { this.debug("No app name in request, skipping", { requestId: gatewayExecutorRequest.requestId, appId: gatewayExecutorRequest.appId, functionSlug: gatewayExecutorRequest.functionSlug, stepId: gatewayExecutorRequest.stepId, connectionId, }); return; } const requestHandler = requestHandlers[gatewayExecutorRequest.appName]; if (!requestHandler) { this.debug("No request handler found for app, skipping", { requestId: gatewayExecutorRequest.requestId, appId: gatewayExecutorRequest.appId, appName: gatewayExecutorRequest.appName, functionSlug: gatewayExecutorRequest.functionSlug, stepId: gatewayExecutorRequest.stepId, connectionId, }); return; } // Ack received request ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_REQUEST_ACK, payload: connect_js_1.WorkerRequestAckData.encode(connect_js_1.WorkerRequestAckData.create({ accountId: gatewayExecutorRequest.accountId, envId: gatewayExecutorRequest.envId, appId: gatewayExecutorRequest.appId, functionSlug: gatewayExecutorRequest.functionSlug, requestId: gatewayExecutorRequest.requestId, stepId: gatewayExecutorRequest.stepId, userTraceCtx: gatewayExecutorRequest.userTraceCtx, systemTraceCtx: gatewayExecutorRequest.systemTraceCtx, runId: gatewayExecutorRequest.runId, })).finish(), })).finish()); this.inProgressRequests.wg.add(1); this.inProgressRequests.requestLeases[gatewayExecutorRequest.requestId] = gatewayExecutorRequest.leaseId; let extendLeaseInterval; try { extendLeaseInterval = setInterval(() => { if (extendLeaseIntervalMs === undefined) { return; } // Only extend lease if it's still set on the request const currentLeaseId = this.inProgressRequests.requestLeases[gatewayExecutorRequest.requestId]; if (!currentLeaseId) { clearInterval(extendLeaseInterval); return; } this.debug("extending lease", { connectionId, leaseId: currentLeaseId, }); // Send extend lease request ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE, payload: connect_js_1.WorkerRequestExtendLeaseData.encode(connect_js_1.WorkerRequestExtendLeaseData.create({ accountId: gatewayExecutorRequest.accountId, envId: gatewayExecutorRequest.envId, appId: gatewayExecutorRequest.appId, functionSlug: gatewayExecutorRequest.functionSlug, requestId: gatewayExecutorRequest.requestId, stepId: gatewayExecutorRequest.stepId, runId: gatewayExecutorRequest.runId, userTraceCtx: gatewayExecutorRequest.userTraceCtx, systemTraceCtx: gatewayExecutorRequest.systemTraceCtx, leaseId: currentLeaseId, })).finish(), })).finish()); }, extendLeaseIntervalMs); const res = await requestHandler(gatewayExecutorRequest); this.debug("Sending worker reply", { connectionId, requestId: gatewayExecutorRequest.requestId, }); this.messageBuffer.addPending(res, ResponseAcknowlegeDeadline); if (!this.currentConnection) { this.debug("No current WebSocket, buffering response", { connectionId, requestId: gatewayExecutorRequest.requestId, }); this.messageBuffer.append(res); return; } // Send reply back to gateway this.currentConnection.ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_REPLY, payload: connect_js_1.SDKResponse.encode(res).finish(), })).finish()); } finally { this.inProgressRequests.wg.done(); delete this.inProgressRequests.requestLeases[gatewayExecutorRequest.requestId]; clearInterval(extendLeaseInterval); } return; } if (connectMessage.kind === connect_js_1.GatewayMessageType.WORKER_REPLY_ACK) { const replyAck = (0, messages_js_1.parseWorkerReplyAck)(connectMessage.payload); this.debug("Acknowledging reply ack", { connectionId, requestId: replyAck.requestId, }); this.messageBuffer.acknowledgePending(replyAck.requestId); return; } if (connectMessage.kind === connect_js_1.GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE_ACK) { const extendLeaseAck = connect_js_1.WorkerRequestExtendLeaseAckData.decode(connectMessage.payload); this.debug("received extend lease ack", { connectionId, newLeaseId: extendLeaseAck.newLeaseId, }); if (extendLeaseAck.newLeaseId) { this.inProgressRequests.requestLeases[extendLeaseAck.requestId] = extendLeaseAck.newLeaseId; } else { this.debug("unable to extend lease", { connectionId, requestId: extendLeaseAck.requestId, }); delete this.inProgressRequests.requestLeases[extendLeaseAck.requestId]; } return; } this.debug("Unexpected message type", { kind: (0, connect_js_1.gatewayMessageTypeToJSON)(connectMessage.kind), rawKind: connectMessage.kind, attempt, setupState: setupState, state: this.state, connectionId, }); }; let heartbeatInterval = undefined; if (heartbeatIntervalMs !== undefined) { heartbeatInterval = setInterval(() => { if (heartbeatIntervalMs === undefined) { return; } // Check if we've missed 2 consecutive heartbeats if (conn.pendingHeartbeats >= 2) { this.debug("Gateway heartbeat missed"); void onConnectionError(new util_js_1.ReconnectError(`Consecutive gateway heartbeats missed (${connectionId})`, attempt)); return; } this.debug("Sending worker heartbeat", { connectionId, }); // Send worker heartbeat conn.pendingHeartbeats++; ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_HEARTBEAT, })).finish()); }, heartbeatIntervalMs); } conn.cleanup = () => { var _a; this.debug("Cleaning up worker heartbeat", { connectionId, }); clearInterval(heartbeatInterval); if (closed) { return; } closed = true; this.debug("Cleaning up connection", { connectionId }); if (ws.readyState === WebSocket.OPEN) { this.debug("Sending pause message", { connectionId }); ws.send(connect_js_1.ConnectMessage.encode(connect_js_1.ConnectMessage.create({ kind: connect_js_1.GatewayMessageType.WORKER_PAUSE, })).finish()); } this.debug("Closing connection", { connectionId }); ws.onerror = () => { }; ws.onclose = () => { }; ws.close(1000, (0, connect_js_1.workerDisconnectReasonToJSON)(connect_js_1.WorkerDisconnectReason.WORKER_SHUTDOWN)); if (((_a = this.currentConnection) === null || _a === void 0 ? void 0 : _a.id) === connectionId) { this.currentConnection = undefined; } }; return conn; } setupShutdownSignal(signals) { if (this.cleanupShutdownSignal) { return; } this.debug(`Setting up shutdown signal handler for ${signals.join(", ")}`); const cleanupShutdownHandlers = (0, os_js_1.onShutdown)(signals, () => { this.debug("Received shutdown signal, closing connection"); void this.close(); }); this.cleanupShutdownSignal = () => { this.debug("Cleaning up shutdown signal handler"); cleanupShutdownHandlers(); }; } } const connect = async (options // eslint-disable-next-line @typescript-eslint/require-await ) => { if (options.apps.length === 0) { throw new Error("No apps provided"); } const conn = new WebSocketWorkerConnection(options); await conn.connect(); return conn; }; exports.connect = connect; //# sourceMappingURL=index.js.map