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