inngest 4.2.5 → 4.3.0
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/CHANGELOG.md +18 -0
- package/api/api.cjs +1 -1
- package/api/api.js +1 -1
- package/components/Inngest.cjs +1 -1
- package/components/Inngest.d.cts +7 -1
- package/components/Inngest.d.cts.map +1 -1
- package/components/Inngest.d.ts +7 -1
- package/components/Inngest.d.ts.map +1 -1
- package/components/Inngest.js +1 -1
- package/components/InngestCommHandler.cjs +60 -28
- package/components/InngestCommHandler.cjs.map +1 -1
- package/components/InngestCommHandler.d.cts.map +1 -1
- package/components/InngestCommHandler.d.ts.map +1 -1
- package/components/InngestCommHandler.js +60 -28
- package/components/InngestCommHandler.js.map +1 -1
- package/components/InngestStepTools.cjs +3 -7
- package/components/InngestStepTools.cjs.map +1 -1
- package/components/InngestStepTools.d.cts +10 -7
- package/components/InngestStepTools.d.cts.map +1 -1
- package/components/InngestStepTools.d.ts +10 -7
- package/components/InngestStepTools.d.ts.map +1 -1
- package/components/InngestStepTools.js +3 -7
- package/components/InngestStepTools.js.map +1 -1
- package/components/connect/strategies/core/connection.cjs +91 -11
- package/components/connect/strategies/core/connection.cjs.map +1 -1
- package/components/connect/strategies/core/connection.js +91 -11
- package/components/connect/strategies/core/connection.js.map +1 -1
- package/components/connect/strategies/core/heartbeat.cjs +2 -1
- package/components/connect/strategies/core/heartbeat.cjs.map +1 -1
- package/components/connect/strategies/core/heartbeat.js +2 -1
- package/components/connect/strategies/core/heartbeat.js.map +1 -1
- package/components/connect/strategies/core/requestProcessor.cjs +27 -4
- package/components/connect/strategies/core/requestProcessor.cjs.map +1 -1
- package/components/connect/strategies/core/requestProcessor.js +27 -4
- package/components/connect/strategies/core/requestProcessor.js.map +1 -1
- package/components/connect/strategies/core/types.cjs +21 -0
- package/components/connect/strategies/core/types.cjs.map +1 -0
- package/components/connect/strategies/core/types.js +20 -0
- package/components/connect/strategies/core/types.js.map +1 -0
- package/components/connect/types.cjs.map +1 -1
- package/components/connect/types.d.cts +18 -0
- package/components/connect/types.d.cts.map +1 -1
- package/components/connect/types.d.ts +18 -0
- package/components/connect/types.d.ts.map +1 -1
- package/components/connect/types.js.map +1 -1
- package/components/execution/engine.cjs +1 -1
- package/components/execution/engine.js +1 -1
- package/components/execution/otel/middleware.d.cts +12 -6
- package/components/execution/otel/middleware.d.cts.map +1 -1
- package/components/execution/otel/middleware.d.ts +12 -6
- package/components/execution/otel/middleware.d.ts.map +1 -1
- package/components/middleware/manager.cjs +1 -1
- package/components/middleware/manager.cjs.map +1 -1
- package/components/middleware/manager.js +2 -2
- package/components/middleware/manager.js.map +1 -1
- package/components/middleware/utils.cjs +4 -3
- package/components/middleware/utils.cjs.map +1 -1
- package/components/middleware/utils.js +4 -3
- package/components/middleware/utils.js.map +1 -1
- package/components/realtime/types.d.cts +4 -4
- package/components/realtime/types.d.cts.map +1 -1
- package/components/realtime/types.d.ts +4 -4
- package/components/realtime/types.d.ts.map +1 -1
- package/helpers/consts.cjs +1 -0
- package/helpers/consts.cjs.map +1 -1
- package/helpers/consts.d.cts +1 -0
- package/helpers/consts.d.cts.map +1 -1
- package/helpers/consts.d.ts +1 -0
- package/helpers/consts.d.ts.map +1 -1
- package/helpers/consts.js +1 -0
- package/helpers/consts.js.map +1 -1
- package/helpers/env.cjs +2 -1
- package/helpers/env.cjs.map +1 -1
- package/helpers/env.js +2 -1
- package/helpers/env.js.map +1 -1
- package/helpers/net.cjs +3 -2
- package/helpers/net.cjs.map +1 -1
- package/helpers/net.js +3 -2
- package/helpers/net.js.map +1 -1
- package/helpers/strings.cjs +15 -5
- package/helpers/strings.cjs.map +1 -1
- package/helpers/strings.d.cts.map +1 -1
- package/helpers/strings.d.ts.map +1 -1
- package/helpers/strings.js +15 -6
- package/helpers/strings.js.map +1 -1
- package/helpers/temporal.cjs +2 -0
- package/helpers/temporal.d.ts +3 -0
- package/helpers/temporal.d.ts.map +1 -1
- package/helpers/temporal.js +1 -1
- package/package.json +1 -1
- package/react.d.cts.map +1 -1
- package/types.cjs.map +1 -1
- package/types.d.cts +5 -4
- package/types.d.cts.map +1 -1
- package/types.d.ts +5 -4
- package/types.d.ts.map +1 -1
- package/types.js.map +1 -1
- package/version.cjs +1 -1
- package/version.cjs.map +1 -1
- package/version.d.cts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
|
@@ -5,6 +5,7 @@ import { ConnectionState } from "../../types.js";
|
|
|
5
5
|
import { resolveApiBaseUrl } from "../../../../helpers/url.js";
|
|
6
6
|
import { parseConnectMessage } from "../../messages.js";
|
|
7
7
|
import { establishConnection } from "./handshake.js";
|
|
8
|
+
import { WAKE_REASON } from "./types.js";
|
|
8
9
|
import { HeartbeatManager } from "./heartbeat.js";
|
|
9
10
|
import { RequestProcessor } from "./requestProcessor.js";
|
|
10
11
|
import { StatusReporter } from "./statusReporter.js";
|
|
@@ -34,7 +35,7 @@ import { WaitGroup } from "@jpwilliams/waitgroup";
|
|
|
34
35
|
* - Manages a single heartbeat interval targeting the active connection
|
|
35
36
|
* - Handles reconnection, drain, and shutdown as state changes
|
|
36
37
|
*/
|
|
37
|
-
var ConnectionCore = class {
|
|
38
|
+
var ConnectionCore = class ConnectionCore {
|
|
38
39
|
config;
|
|
39
40
|
callbacks;
|
|
40
41
|
_activeConnection;
|
|
@@ -50,8 +51,12 @@ var ConnectionCore = class {
|
|
|
50
51
|
_lastMessageReceivedAt;
|
|
51
52
|
excludeGateways = /* @__PURE__ */ new Set();
|
|
52
53
|
wakeSignal;
|
|
54
|
+
pendingWakeReasons = [];
|
|
53
55
|
hasConnectedBefore = false;
|
|
56
|
+
shutdownDumpInterval;
|
|
57
|
+
static SHUTDOWN_DUMP_INTERVAL_MS = 6e4;
|
|
54
58
|
loopPromise;
|
|
59
|
+
closePromise;
|
|
55
60
|
resolveFirstReady;
|
|
56
61
|
rejectFirstReady;
|
|
57
62
|
useSigningKey;
|
|
@@ -86,7 +91,7 @@ var ConnectionCore = class {
|
|
|
86
91
|
return self.config.appIds;
|
|
87
92
|
}
|
|
88
93
|
};
|
|
89
|
-
const wakeSignalRef = { wake: () => this.wake() };
|
|
94
|
+
const wakeSignalRef = { wake: (reason) => this.wake(reason) };
|
|
90
95
|
const self = this;
|
|
91
96
|
this.heartbeatManager = new HeartbeatManager(accessor, wakeSignalRef, callbacks.logger);
|
|
92
97
|
this.heartbeatManager.onHeartbeatSent = () => {
|
|
@@ -142,14 +147,21 @@ var ConnectionCore = class {
|
|
|
142
147
|
* connection closed).
|
|
143
148
|
*/
|
|
144
149
|
async close() {
|
|
150
|
+
if (this.closePromise) return this.closePromise;
|
|
151
|
+
this.closePromise = this.closeOnce();
|
|
152
|
+
return this.closePromise;
|
|
153
|
+
}
|
|
154
|
+
async closeOnce() {
|
|
145
155
|
const inFlightCount = Object.keys(this._inProgressRequests.requestLeases).length;
|
|
146
156
|
this.callbacks.logger.info({ inFlightCount }, "Shutting down, waiting for in-flight requests");
|
|
147
157
|
this._shutdownRequested = true;
|
|
158
|
+
this.dumpInFlightForShutdown("drain-start");
|
|
159
|
+
this.startShutdownInFlightDumpTimer();
|
|
148
160
|
if (this._activeConnection?.ws.readyState === WebSocket.OPEN) {
|
|
149
161
|
this._activeConnection.ws.send(ensureUnsharedArrayBuffer(ConnectMessage.encode(ConnectMessage.create({ kind: GatewayMessageType.WORKER_PAUSE })).finish()));
|
|
150
162
|
this.callbacks.logger.info({ connectionId: this._activeConnection.id }, "Sent WORKER_PAUSE, draining");
|
|
151
163
|
}
|
|
152
|
-
this.wake();
|
|
164
|
+
this.wake(WAKE_REASON.ShutdownRequested);
|
|
153
165
|
if (this.loopPromise) await this.loopPromise;
|
|
154
166
|
this.callbacks.logger.info("Connection closed");
|
|
155
167
|
}
|
|
@@ -168,9 +180,10 @@ var ConnectionCore = class {
|
|
|
168
180
|
resolve
|
|
169
181
|
};
|
|
170
182
|
}
|
|
171
|
-
wake() {
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
183
|
+
wake(reason = WAKE_REASON.Unknown) {
|
|
184
|
+
const shouldResolve = this.pendingWakeReasons.length === 0;
|
|
185
|
+
this.pendingWakeReasons.push(reason);
|
|
186
|
+
if (shouldResolve) this.wakeSignal.resolve();
|
|
174
187
|
}
|
|
175
188
|
switchAuthKey() {
|
|
176
189
|
const switchToFallback = this.useSigningKey === this.config.hashedSigningKey;
|
|
@@ -180,8 +193,63 @@ var ConnectionCore = class {
|
|
|
180
193
|
hasInFlightRequests() {
|
|
181
194
|
return Object.keys(this._inProgressRequests.requestLeases).length > 0;
|
|
182
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Debug-level "still draining" dump emitted at drain start and periodically
|
|
198
|
+
* thereafter while in-flight requests are holding the shutdown. One summary
|
|
199
|
+
* line plus one line per request carrying `requestId`, `runId`, `stepId`,
|
|
200
|
+
* `functionSlug`, `ageMs`, and `sinceLastLeaseExtendMs`. Does not affect
|
|
201
|
+
* info/warn logs.
|
|
202
|
+
*
|
|
203
|
+
* `requestLeases` drives the reconcile-loop exit gate, so use it as the
|
|
204
|
+
* single source of truth for the in-flight set; `requestMeta` carries the
|
|
205
|
+
* enrichment fields and is kept in sync alongside the lease map.
|
|
206
|
+
*/
|
|
207
|
+
dumpInFlightForShutdown(reason) {
|
|
208
|
+
const leaseIds = Object.keys(this._inProgressRequests.requestLeases);
|
|
209
|
+
if (leaseIds.length === 0) return;
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const ages = [];
|
|
212
|
+
for (const id of leaseIds) {
|
|
213
|
+
const m = this._inProgressRequests.requestMeta[id];
|
|
214
|
+
if (m?.leaseAcquiredAt) ages.push(now - m.leaseAcquiredAt);
|
|
215
|
+
}
|
|
216
|
+
this.callbacks.logger.debug({
|
|
217
|
+
reason,
|
|
218
|
+
inFlightCount: leaseIds.length,
|
|
219
|
+
oldestAgeMs: ages.length > 0 ? Math.max(...ages) : void 0
|
|
220
|
+
}, "Shutdown: still draining");
|
|
221
|
+
for (const id of leaseIds) {
|
|
222
|
+
const m = this._inProgressRequests.requestMeta[id];
|
|
223
|
+
if (!m) continue;
|
|
224
|
+
this.callbacks.logger.debug({
|
|
225
|
+
reason,
|
|
226
|
+
requestId: m.requestId,
|
|
227
|
+
runId: m.runId,
|
|
228
|
+
stepId: m.stepId,
|
|
229
|
+
functionSlug: m.functionSlug,
|
|
230
|
+
appId: m.appId,
|
|
231
|
+
ageMs: m.leaseAcquiredAt ? now - m.leaseAcquiredAt : void 0,
|
|
232
|
+
sinceLastLeaseExtendMs: m.leaseLastExtendedAt ? now - m.leaseLastExtendedAt : void 0
|
|
233
|
+
}, "Shutdown: still draining in-flight request");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
startShutdownInFlightDumpTimer() {
|
|
237
|
+
if (this.shutdownDumpInterval) return;
|
|
238
|
+
this.shutdownDumpInterval = setInterval(() => {
|
|
239
|
+
if (!this._shutdownRequested) return;
|
|
240
|
+
this.dumpInFlightForShutdown("periodic");
|
|
241
|
+
this.wake(WAKE_REASON.ShutdownStillPending);
|
|
242
|
+
}, ConnectionCore.SHUTDOWN_DUMP_INTERVAL_MS);
|
|
243
|
+
}
|
|
244
|
+
stopShutdownInFlightDumpTimer() {
|
|
245
|
+
if (this.shutdownDumpInterval) {
|
|
246
|
+
clearInterval(this.shutdownDumpInterval);
|
|
247
|
+
this.shutdownDumpInterval = void 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
183
250
|
async reconcileLoop(initialAttempt) {
|
|
184
251
|
let attempt = initialAttempt;
|
|
252
|
+
this.callbacks.logger.debug({ initialAttempt }, "Reconcile loop entered");
|
|
185
253
|
while (true) {
|
|
186
254
|
if (this._shutdownRequested && !this.hasInFlightRequests()) break;
|
|
187
255
|
if (!this._activeConnection || this._activeConnection.dead) {
|
|
@@ -218,7 +286,10 @@ var ConnectionCore = class {
|
|
|
218
286
|
if (this._shutdownRequested) {
|
|
219
287
|
conn.ws.send(ensureUnsharedArrayBuffer(ConnectMessage.encode(ConnectMessage.create({ kind: GatewayMessageType.WORKER_PAUSE })).finish()));
|
|
220
288
|
this.callbacks.logger.info({ connectionId: conn.id }, "Sent WORKER_PAUSE on reconnect during shutdown");
|
|
221
|
-
} else
|
|
289
|
+
} else {
|
|
290
|
+
conn.ws.send(ensureUnsharedArrayBuffer(ConnectMessage.encode(ConnectMessage.create({ kind: GatewayMessageType.WORKER_READY })).finish()));
|
|
291
|
+
this.callbacks.logger.info({ connectionId: conn.id }, "Sent WORKER_READY");
|
|
292
|
+
}
|
|
222
293
|
this.callbacks.onConnectionActive?.(this.useSigningKey);
|
|
223
294
|
this.resolveFirstReady?.();
|
|
224
295
|
this.resolveFirstReady = void 0;
|
|
@@ -251,15 +322,24 @@ var ConnectionCore = class {
|
|
|
251
322
|
continue;
|
|
252
323
|
}
|
|
253
324
|
}
|
|
254
|
-
await this.wakeSignal.promise;
|
|
325
|
+
if (this.pendingWakeReasons.length === 0) await this.wakeSignal.promise;
|
|
326
|
+
const reasons = this.pendingWakeReasons;
|
|
327
|
+
this.pendingWakeReasons = [];
|
|
328
|
+
this.resetWakeSignal();
|
|
255
329
|
this.callbacks.logger.debug({
|
|
330
|
+
reasons,
|
|
256
331
|
shutdownRequested: this._shutdownRequested,
|
|
257
332
|
hasActiveConnection: !!this._activeConnection,
|
|
258
333
|
activeConnectionDead: this._activeConnection?.dead
|
|
259
334
|
}, "Reconcile loop woken");
|
|
260
335
|
}
|
|
336
|
+
this.callbacks.logger.debug({
|
|
337
|
+
shutdownRequested: this._shutdownRequested,
|
|
338
|
+
inFlightCount: Object.keys(this._inProgressRequests.requestLeases).length
|
|
339
|
+
}, "Reconcile loop exiting");
|
|
261
340
|
this.heartbeatManager.stop();
|
|
262
341
|
this.statusReporter.stop();
|
|
342
|
+
this.stopShutdownInFlightDumpTimer();
|
|
263
343
|
this._activeConnection?.close();
|
|
264
344
|
this._activeConnection = void 0;
|
|
265
345
|
this._drainingConnection?.close();
|
|
@@ -283,7 +363,7 @@ var ConnectionCore = class {
|
|
|
283
363
|
conn.dead = true;
|
|
284
364
|
this.excludeGateways.add(gatewayGroup);
|
|
285
365
|
if (this._activeConnection?.id === connectionId) this._activeConnection = void 0;
|
|
286
|
-
this.wake();
|
|
366
|
+
this.wake(WAKE_REASON.WsError);
|
|
287
367
|
};
|
|
288
368
|
ws.onclose = (ev) => {
|
|
289
369
|
if (conn.dead) return;
|
|
@@ -298,7 +378,7 @@ var ConnectionCore = class {
|
|
|
298
378
|
conn.dead = true;
|
|
299
379
|
this.excludeGateways.add(gatewayGroup);
|
|
300
380
|
if (this._activeConnection?.id === connectionId) this._activeConnection = void 0;
|
|
301
|
-
this.wake();
|
|
381
|
+
this.wake(WAKE_REASON.WsClose);
|
|
302
382
|
};
|
|
303
383
|
ws.onmessage = async (event) => {
|
|
304
384
|
this._lastMessageReceivedAt = Date.now();
|
|
@@ -312,7 +392,7 @@ var ConnectionCore = class {
|
|
|
312
392
|
}, "Gateway draining, opening new connection");
|
|
313
393
|
this._drainingConnection = this._activeConnection;
|
|
314
394
|
this._activeConnection = void 0;
|
|
315
|
-
this.wake();
|
|
395
|
+
this.wake(WAKE_REASON.GatewayClosing);
|
|
316
396
|
return;
|
|
317
397
|
}
|
|
318
398
|
if (connectMessage.kind === GatewayMessageType.GATEWAY_HEARTBEAT) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["resolve: () => void"],"sources":["../../../../../src/components/connect/strategies/core/connection.ts"],"sourcesContent":["/**\n * Shared connection core logic used by both SameThreadStrategy and\n * WorkerThreadStrategy.\n *\n * This module uses a **reconcile loop** that continuously ensures a live\n * WebSocket connection is open. Reconnection, drain, and shutdown are\n * expressed as state changes that wake the loop rather than recursive\n * calls or callback-driven control flow.\n *\n * Domain-specific logic is delegated to focused sub-modules:\n * - {@link HeartbeatManager} — periodic heartbeat pings\n * - {@link RequestProcessor} — executor requests, lease extensions, reply ACKs\n * - {@link establishConnection} — HTTP start + WebSocket handshake\n */\n\nimport { WaitGroup } from \"@jpwilliams/waitgroup\";\nimport { resolveApiBaseUrl } from \"../../../../helpers/url.ts\";\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport type { GatewayExecutorRequestData } from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n gatewayMessageTypeToJSON,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport { parseConnectMessage } from \"../../messages.ts\";\nimport {\n type ConnectDebugState,\n ConnectionState,\n type InFlightRequest,\n} from \"../../types.ts\";\nimport {\n AuthError,\n ConnectionLimitError,\n expBackoff,\n ReconnectError,\n waitWithCancel,\n} from \"../../util.ts\";\nimport { establishConnection } from \"./handshake.ts\";\nimport { HeartbeatManager } from \"./heartbeat.ts\";\nimport { RequestProcessor } from \"./requestProcessor.ts\";\nimport { StatusReporter } from \"./statusReporter.ts\";\nimport type { BaseConnectionConfig } from \"./types.ts\";\n\n/**\n * Connection object representing an active WebSocket connection.\n */\nexport interface Connection {\n id: string;\n ws: WebSocket;\n pendingHeartbeats: number;\n /** When true the connection is considered unusable and the reconcile loop\n * will establish a replacement. */\n dead: boolean;\n heartbeatIntervalMs: number;\n extendLeaseIntervalMs: number;\n statusIntervalMs: number;\n /** Timestamp (ms) when the connection was established. */\n connectedAt: number;\n /** Disable all handlers and close the underlying WebSocket. */\n close(): void;\n}\n\n/**\n * Configuration for the connection core.\n * Extends BaseConnectionConfig with connection-specific options.\n */\nexport interface ConnectionCoreConfig extends BaseConnectionConfig {\n instanceId?: string;\n maxWorkerConcurrency?: number;\n gatewayUrl?: string;\n appIds: string[];\n}\n\n/**\n * Callbacks for connection core events.\n */\nexport interface ConnectionCoreCallbacks {\n logger: Logger;\n onStateChange: (state: ConnectionState) => void;\n getState: () => ConnectionState;\n handleExecutionRequest: (\n request: GatewayExecutorRequestData,\n ) => Promise<Uint8Array>;\n onReplyAck?: (requestId: string) => void;\n onBufferResponse?: (requestId: string, responseBytes: Uint8Array) => void;\n onConnectionActive?: (signingKey: string | undefined) => void;\n}\n\n/**\n * Core connection manager that handles WebSocket connection lifecycle,\n * handshake, heartbeat, lease extension, and reconnection.\n *\n * Uses a reconcile loop that:\n * - Ensures a WebSocket connection is always open\n * - Manages a single heartbeat interval targeting the active connection\n * - Handles reconnection, drain, and shutdown as state changes\n */\nexport class ConnectionCore {\n private config: ConnectionCoreConfig;\n private callbacks: ConnectionCoreCallbacks;\n\n // Exposed via ConnectionAccessor for sub-modules\n private _activeConnection: Connection | undefined;\n private _drainingConnection: Connection | undefined;\n private _shutdownRequested = false;\n private _inProgressRequests: {\n wg: WaitGroup;\n requestLeases: Record<string, string>;\n requestMeta: Record<string, InFlightRequest>;\n } = {\n wg: new WaitGroup(),\n requestLeases: {},\n requestMeta: {},\n };\n\n private _lastHeartbeatSentAt: number | undefined;\n private _lastHeartbeatReceivedAt: number | undefined;\n private _lastMessageReceivedAt: number | undefined;\n\n private excludeGateways: Set<string> = new Set();\n\n // Wake signal for the reconcile loop\n private wakeSignal: { promise: Promise<void>; resolve: () => void };\n\n // Whether we've ever successfully connected (used to distinguish\n // CONNECTING from RECONNECTING state transitions).\n private hasConnectedBefore = false;\n\n // Loop promise — resolved when the reconcile loop exits\n private loopPromise: Promise<void> | undefined;\n\n // First-ready resolution — resolves start() when first connection is ready\n private resolveFirstReady: (() => void) | undefined;\n private rejectFirstReady: ((err: unknown) => void) | undefined;\n\n // Signing key management\n private useSigningKey: string | undefined;\n\n // Sub-modules\n private readonly heartbeatManager: HeartbeatManager;\n private readonly statusReporter: StatusReporter;\n private readonly requestProcessor: RequestProcessor;\n\n constructor(\n config: ConnectionCoreConfig,\n callbacks: ConnectionCoreCallbacks,\n ) {\n this.config = config;\n this.callbacks = callbacks;\n this.useSigningKey = config.hashedSigningKey;\n\n // Initialize the wake signal\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n this.wakeSignal = { promise, resolve: resolve! };\n\n // Build a ConnectionAccessor view for sub-modules\n const accessor = {\n get activeConnection() {\n return self._activeConnection;\n },\n get drainingConnection() {\n return self._drainingConnection;\n },\n get shutdownRequested() {\n return self._shutdownRequested;\n },\n get inProgressRequests() {\n return self._inProgressRequests;\n },\n get appIds() {\n return self.config.appIds;\n },\n };\n\n const wakeSignalRef = { wake: () => this.wake() };\n\n const self = this;\n\n this.heartbeatManager = new HeartbeatManager(\n accessor,\n wakeSignalRef,\n callbacks.logger,\n );\n this.heartbeatManager.onHeartbeatSent = () => {\n this._lastHeartbeatSentAt = Date.now();\n };\n\n this.statusReporter = new StatusReporter(accessor, callbacks.logger);\n\n this.requestProcessor = new RequestProcessor(\n accessor,\n wakeSignalRef,\n callbacks,\n callbacks.logger,\n );\n }\n\n get connectionId(): string | undefined {\n return this._activeConnection?.id;\n }\n\n /**\n * Wait for all in-progress requests to complete.\n */\n async waitForInProgress(): Promise<void> {\n await this._inProgressRequests.wg.wait();\n }\n\n /**\n * Return a snapshot of debug/health information for this connection.\n */\n getDebugState(): ConnectDebugState {\n return {\n state: this.callbacks.getState(),\n activeConnectionId: this._activeConnection?.id,\n drainingConnectionId: this._drainingConnection?.id,\n lastHeartbeatSentAt: this._lastHeartbeatSentAt,\n lastHeartbeatReceivedAt: this._lastHeartbeatReceivedAt,\n lastMessageReceivedAt: this._lastMessageReceivedAt,\n shutdownRequested: this._shutdownRequested,\n inFlightRequestCount: Object.keys(this._inProgressRequests.requestLeases)\n .length,\n inFlightRequests: Object.values(this._inProgressRequests.requestMeta),\n };\n }\n\n /**\n * Start the reconcile loop. Resolves when the first connection is active.\n * The loop continues running in the background.\n */\n async start(attempt = 0): Promise<void> {\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\"WebSockets not supported in current environment\");\n }\n\n const state = this.callbacks.getState();\n if (state === ConnectionState.CLOSED) {\n throw new Error(\"Connection already closed\");\n }\n\n const firstReadyPromise = new Promise<void>((resolve, reject) => {\n this.resolveFirstReady = resolve;\n this.rejectFirstReady = reject;\n });\n\n this.loopPromise = this.reconcileLoop(attempt);\n\n // If the loop ends before firstReady resolves, propagate any error\n this.loopPromise.catch((err) => {\n this.rejectFirstReady?.(err);\n });\n\n await firstReadyPromise;\n }\n\n /**\n * Request graceful shutdown. Resolves when fully closed (in-flight done,\n * connection closed).\n */\n async close(): Promise<void> {\n const inFlightCount = Object.keys(\n this._inProgressRequests.requestLeases,\n ).length;\n this.callbacks.logger.info(\n { inFlightCount },\n \"Shutting down, waiting for in-flight requests\",\n );\n this._shutdownRequested = true;\n\n if (this._activeConnection?.ws.readyState === WebSocket.OPEN) {\n this._activeConnection.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_PAUSE,\n }),\n ).finish(),\n ),\n );\n this.callbacks.logger.info(\n { connectionId: this._activeConnection.id },\n \"Sent WORKER_PAUSE, draining\",\n );\n }\n\n this.wake();\n\n if (this.loopPromise) {\n await this.loopPromise;\n }\n\n this.callbacks.logger.info(\"Connection closed\");\n }\n\n async getApiBaseUrl(): Promise<string> {\n return resolveApiBaseUrl({\n apiBaseUrl: this.config.apiBaseUrl,\n mode: this.config.mode,\n });\n }\n\n // ---------------------------------------------------------------------------\n // Wake signal\n // ---------------------------------------------------------------------------\n\n private resetWakeSignal(): void {\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n this.wakeSignal = { promise, resolve: resolve! };\n }\n\n private wake(): void {\n this.wakeSignal.resolve();\n this.resetWakeSignal();\n }\n\n // ---------------------------------------------------------------------------\n // Signing key management\n // ---------------------------------------------------------------------------\n\n private switchAuthKey(): void {\n const switchToFallback =\n this.useSigningKey === this.config.hashedSigningKey;\n if (switchToFallback) {\n this.callbacks.logger.debug(\"Switching to fallback signing key\");\n }\n this.useSigningKey = switchToFallback\n ? this.config.hashedFallbackKey\n : this.config.hashedSigningKey;\n }\n\n // ---------------------------------------------------------------------------\n // In-flight helpers\n // ---------------------------------------------------------------------------\n\n private hasInFlightRequests(): boolean {\n return Object.keys(this._inProgressRequests.requestLeases).length > 0;\n }\n\n // ---------------------------------------------------------------------------\n // Reconcile loop\n // ---------------------------------------------------------------------------\n\n private async reconcileLoop(initialAttempt: number): Promise<void> {\n let attempt = initialAttempt;\n\n while (true) {\n // Exit condition: shutdown requested + no in-flight requests\n if (this._shutdownRequested && !this.hasInFlightRequests()) {\n break;\n }\n\n // Ensure we have a live connection\n if (!this._activeConnection || this._activeConnection.dead) {\n this.callbacks.logger.debug(\n {\n hasActiveConnection: !!this._activeConnection,\n activeConnectionDead: this._activeConnection?.dead,\n hasDrainingConnection: !!this._drainingConnection,\n drainingConnectionId: this._drainingConnection?.id,\n },\n \"No active connection\",\n );\n\n if (this.hasConnectedBefore) {\n this.callbacks.logger.info({ attempt }, \"Reconnecting\");\n } else {\n this.callbacks.logger.info(\"Connecting\");\n }\n\n this.callbacks.onStateChange(\n this.hasConnectedBefore\n ? ConnectionState.RECONNECTING\n : ConnectionState.CONNECTING,\n );\n\n try {\n const { conn, gatewayGroup } = await establishConnection(\n this.config,\n this.useSigningKey,\n attempt,\n this.excludeGateways,\n this.callbacks.logger,\n );\n\n // Attach post-handshake handlers\n this.attachHandlers(conn, gatewayGroup);\n\n // Clean up draining connection after new one is ready\n if (this._drainingConnection) {\n this.callbacks.logger.info(\n {\n oldConnectionId: this._drainingConnection.id,\n newConnectionId: conn.id,\n },\n \"Replaced draining connection\",\n );\n this._drainingConnection.close();\n this._drainingConnection = undefined;\n }\n\n this._activeConnection = conn;\n this.heartbeatManager.updateInterval(conn.heartbeatIntervalMs);\n this.statusReporter.updateInterval(conn.statusIntervalMs);\n attempt = 0;\n this.hasConnectedBefore = true;\n this.callbacks.logger.info(\n { connectionId: conn.id, gatewayGroup },\n \"Connection active\",\n );\n this.callbacks.onStateChange(ConnectionState.ACTIVE);\n\n if (this._shutdownRequested) {\n // Reconnected during shutdown to keep in-flight requests alive.\n // Send WORKER_PAUSE instead of WORKER_READY so no new work is routed.\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_PAUSE,\n }),\n ).finish(),\n ),\n );\n this.callbacks.logger.info(\n { connectionId: conn.id },\n \"Sent WORKER_PAUSE on reconnect during shutdown\",\n );\n } else {\n // Signal the gateway that we're ready to receive requests.\n // This must happen after ACTIVE so the gateway doesn't route\n // requests before handlers are fully attached.\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_READY,\n }),\n ).finish(),\n ),\n );\n }\n\n // Flush any buffered responses via HTTP now that we're active.\n this.callbacks.onConnectionActive?.(this.useSigningKey);\n\n this.resolveFirstReady?.();\n this.resolveFirstReady = undefined;\n this.rejectFirstReady = undefined;\n } catch (err) {\n if (!(err instanceof ReconnectError)) throw err;\n\n attempt = err.attempt + 1;\n if (err instanceof AuthError) this.switchAuthKey();\n if (err instanceof ConnectionLimitError) {\n this.callbacks.logger.error(\"Max concurrent connections reached\");\n }\n\n // Gateway is draining, we should retry much faster\n if (err.message?.includes(\"connect_gateway_closing\")) {\n const jitter = 500 + Math.random() * 1000;\n this.callbacks.logger.info(\n { attempt, delay: Math.round(jitter), error: err.message },\n \"Gateway draining, retrying\",\n );\n const cancelled = await waitWithCancel(jitter, () => {\n return this._shutdownRequested && !this.hasInFlightRequests();\n });\n if (cancelled) break;\n continue;\n }\n\n const delay = expBackoff(attempt);\n this.callbacks.logger.info(\n { attempt, delay },\n \"Reconnecting after failure\",\n );\n\n const cancelled = await waitWithCancel(delay, () => {\n return this._shutdownRequested && !this.hasInFlightRequests();\n });\n if (cancelled) break;\n continue;\n }\n }\n\n // Wait for something to change\n await this.wakeSignal.promise;\n this.callbacks.logger.debug(\n {\n shutdownRequested: this._shutdownRequested,\n hasActiveConnection: !!this._activeConnection,\n activeConnectionDead: this._activeConnection?.dead,\n },\n \"Reconcile loop woken\",\n );\n }\n\n // Teardown\n this.heartbeatManager.stop();\n this.statusReporter.stop();\n this._activeConnection?.close();\n this._activeConnection = undefined;\n this._drainingConnection?.close();\n this._drainingConnection = undefined;\n }\n\n // ---------------------------------------------------------------------------\n // Post-handshake handler attachment\n // ---------------------------------------------------------------------------\n\n /**\n * Wire up error, close, and message handlers on a newly-handshaked connection.\n */\n private attachHandlers(conn: Connection, gatewayGroup: string): void {\n const { ws } = conn;\n const connectionId = conn.id;\n\n // Error/close handlers: mark connection as dead and wake the loop\n ws.onerror = (ev) => {\n if (conn.dead) return;\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.warn(\n {\n connectionId,\n gatewayGroup,\n uptimeMs,\n error: (ev as ErrorEvent)?.message,\n },\n \"Connection lost (error)\",\n );\n conn.dead = true;\n this.excludeGateways.add(gatewayGroup);\n if (this._activeConnection?.id === connectionId) {\n this._activeConnection = undefined;\n }\n this.wake();\n };\n\n ws.onclose = (ev) => {\n if (conn.dead) return;\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.warn(\n {\n connectionId,\n gatewayGroup,\n uptimeMs,\n code: ev.code,\n reason: ev.reason,\n },\n \"Connection lost (close)\",\n );\n conn.dead = true;\n this.excludeGateways.add(gatewayGroup);\n if (this._activeConnection?.id === connectionId) {\n this._activeConnection = undefined;\n }\n this.wake();\n };\n\n // Message handler for post-handshake messages\n ws.onmessage = async (event) => {\n this._lastMessageReceivedAt = Date.now();\n\n const messageBytes = new Uint8Array(event.data as ArrayBuffer);\n const connectMessage = parseConnectMessage(messageBytes);\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_CLOSING) {\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.info(\n { connectionId: conn.id, gatewayGroup, uptimeMs },\n \"Gateway draining, opening new connection\",\n );\n // Move current connection to draining, clear active so the loop\n // establishes a replacement.\n this._drainingConnection = this._activeConnection;\n this._activeConnection = undefined;\n this.wake();\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_HEARTBEAT) {\n this._lastHeartbeatReceivedAt = Date.now();\n conn.pendingHeartbeats = 0;\n this.callbacks.logger.debug(\n { connectionId },\n \"Handled gateway heartbeat\",\n );\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_EXECUTOR_REQUEST) {\n await this.requestProcessor.handleExecutorRequest(connectMessage, conn);\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.WORKER_REPLY_ACK) {\n this.requestProcessor.handleReplyAck(connectMessage, connectionId);\n return;\n }\n\n if (\n connectMessage.kind ===\n GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE_ACK\n ) {\n this.requestProcessor.handleExtendLeaseAck(\n connectMessage,\n connectionId,\n );\n return;\n }\n\n this.callbacks.logger.warn(\n {\n kind: gatewayMessageTypeToJSON(connectMessage.kind),\n rawKind: connectMessage.kind,\n state: this.callbacks.getState(),\n connectionId,\n },\n \"Unexpected message type\",\n );\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection.js","names":["resolve: () => void","ages: number[]"],"sources":["../../../../../src/components/connect/strategies/core/connection.ts"],"sourcesContent":["/**\n * Shared connection core logic used by both SameThreadStrategy and\n * WorkerThreadStrategy.\n *\n * This module uses a **reconcile loop** that continuously ensures a live\n * WebSocket connection is open. Reconnection, drain, and shutdown are\n * expressed as state changes that wake the loop rather than recursive\n * calls or callback-driven control flow.\n *\n * Domain-specific logic is delegated to focused sub-modules:\n * - {@link HeartbeatManager} — periodic heartbeat pings\n * - {@link RequestProcessor} — executor requests, lease extensions, reply ACKs\n * - {@link establishConnection} — HTTP start + WebSocket handshake\n */\n\nimport { WaitGroup } from \"@jpwilliams/waitgroup\";\nimport { resolveApiBaseUrl } from \"../../../../helpers/url.ts\";\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport type { GatewayExecutorRequestData } from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n gatewayMessageTypeToJSON,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport { parseConnectMessage } from \"../../messages.ts\";\nimport {\n type ConnectDebugState,\n ConnectionState,\n type InFlightRequest,\n} from \"../../types.ts\";\nimport {\n AuthError,\n ConnectionLimitError,\n expBackoff,\n ReconnectError,\n waitWithCancel,\n} from \"../../util.ts\";\nimport { establishConnection } from \"./handshake.ts\";\nimport { HeartbeatManager } from \"./heartbeat.ts\";\nimport { RequestProcessor } from \"./requestProcessor.ts\";\nimport { StatusReporter } from \"./statusReporter.ts\";\nimport type { BaseConnectionConfig } from \"./types.ts\";\nimport { WAKE_REASON, type WakeReason } from \"./types.ts\";\n\n/**\n * Connection object representing an active WebSocket connection.\n */\nexport interface Connection {\n id: string;\n ws: WebSocket;\n pendingHeartbeats: number;\n /** When true the connection is considered unusable and the reconcile loop\n * will establish a replacement. */\n dead: boolean;\n heartbeatIntervalMs: number;\n extendLeaseIntervalMs: number;\n statusIntervalMs: number;\n /** Timestamp (ms) when the connection was established. */\n connectedAt: number;\n /** Disable all handlers and close the underlying WebSocket. */\n close(): void;\n}\n\n/**\n * Configuration for the connection core.\n * Extends BaseConnectionConfig with connection-specific options.\n */\nexport interface ConnectionCoreConfig extends BaseConnectionConfig {\n instanceId?: string;\n maxWorkerConcurrency?: number;\n gatewayUrl?: string;\n appIds: string[];\n}\n\n/**\n * Callbacks for connection core events.\n */\nexport interface ConnectionCoreCallbacks {\n logger: Logger;\n onStateChange: (state: ConnectionState) => void;\n getState: () => ConnectionState;\n handleExecutionRequest: (\n request: GatewayExecutorRequestData,\n ) => Promise<Uint8Array>;\n onReplyAck?: (requestId: string) => void;\n onBufferResponse?: (requestId: string, responseBytes: Uint8Array) => void;\n onConnectionActive?: (signingKey: string | undefined) => void;\n}\n\n/**\n * Core connection manager that handles WebSocket connection lifecycle,\n * handshake, heartbeat, lease extension, and reconnection.\n *\n * Uses a reconcile loop that:\n * - Ensures a WebSocket connection is always open\n * - Manages a single heartbeat interval targeting the active connection\n * - Handles reconnection, drain, and shutdown as state changes\n */\nexport class ConnectionCore {\n private config: ConnectionCoreConfig;\n private callbacks: ConnectionCoreCallbacks;\n\n // Exposed via ConnectionAccessor for sub-modules\n private _activeConnection: Connection | undefined;\n private _drainingConnection: Connection | undefined;\n private _shutdownRequested = false;\n private _inProgressRequests: {\n wg: WaitGroup;\n requestLeases: Record<string, string>;\n requestMeta: Record<string, InFlightRequest>;\n } = {\n wg: new WaitGroup(),\n requestLeases: {},\n requestMeta: {},\n };\n\n private _lastHeartbeatSentAt: number | undefined;\n private _lastHeartbeatReceivedAt: number | undefined;\n private _lastMessageReceivedAt: number | undefined;\n\n private excludeGateways: Set<string> = new Set();\n\n // Wake signal for the reconcile loop\n private wakeSignal: { promise: Promise<void>; resolve: () => void };\n // Reasons accumulated since the last loop wake. Read + cleared by the loop.\n private pendingWakeReasons: WakeReason[] = [];\n\n // Whether we've ever successfully connected (used to distinguish\n // CONNECTING from RECONNECTING state transitions).\n private hasConnectedBefore = false;\n\n // Shutdown diagnostics: periodic \"still draining\" dump logged while the\n // drain is outstanding. Started in close(), cleared in teardown.\n private shutdownDumpInterval: ReturnType<typeof setInterval> | undefined;\n\n // Cadence for the periodic \"still draining\" debug dump.\n private static readonly SHUTDOWN_DUMP_INTERVAL_MS = 60_000;\n\n // Loop promise — resolved when the reconcile loop exits\n private loopPromise: Promise<void> | undefined;\n private closePromise: Promise<void> | undefined;\n\n // First-ready resolution — resolves start() when first connection is ready\n private resolveFirstReady: (() => void) | undefined;\n private rejectFirstReady: ((err: unknown) => void) | undefined;\n\n // Signing key management\n private useSigningKey: string | undefined;\n\n // Sub-modules\n private readonly heartbeatManager: HeartbeatManager;\n private readonly statusReporter: StatusReporter;\n private readonly requestProcessor: RequestProcessor;\n\n constructor(\n config: ConnectionCoreConfig,\n callbacks: ConnectionCoreCallbacks,\n ) {\n this.config = config;\n this.callbacks = callbacks;\n this.useSigningKey = config.hashedSigningKey;\n\n // Initialize the wake signal\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n this.wakeSignal = { promise, resolve: resolve! };\n\n // Build a ConnectionAccessor view for sub-modules\n const accessor = {\n get activeConnection() {\n return self._activeConnection;\n },\n get drainingConnection() {\n return self._drainingConnection;\n },\n get shutdownRequested() {\n return self._shutdownRequested;\n },\n get inProgressRequests() {\n return self._inProgressRequests;\n },\n get appIds() {\n return self.config.appIds;\n },\n };\n\n const wakeSignalRef = { wake: (reason?: WakeReason) => this.wake(reason) };\n\n const self = this;\n\n this.heartbeatManager = new HeartbeatManager(\n accessor,\n wakeSignalRef,\n callbacks.logger,\n );\n this.heartbeatManager.onHeartbeatSent = () => {\n this._lastHeartbeatSentAt = Date.now();\n };\n\n this.statusReporter = new StatusReporter(accessor, callbacks.logger);\n\n this.requestProcessor = new RequestProcessor(\n accessor,\n wakeSignalRef,\n callbacks,\n callbacks.logger,\n );\n }\n\n get connectionId(): string | undefined {\n return this._activeConnection?.id;\n }\n\n /**\n * Wait for all in-progress requests to complete.\n */\n async waitForInProgress(): Promise<void> {\n await this._inProgressRequests.wg.wait();\n }\n\n /**\n * Return a snapshot of debug/health information for this connection.\n */\n getDebugState(): ConnectDebugState {\n return {\n state: this.callbacks.getState(),\n activeConnectionId: this._activeConnection?.id,\n drainingConnectionId: this._drainingConnection?.id,\n lastHeartbeatSentAt: this._lastHeartbeatSentAt,\n lastHeartbeatReceivedAt: this._lastHeartbeatReceivedAt,\n lastMessageReceivedAt: this._lastMessageReceivedAt,\n shutdownRequested: this._shutdownRequested,\n inFlightRequestCount: Object.keys(this._inProgressRequests.requestLeases)\n .length,\n inFlightRequests: Object.values(this._inProgressRequests.requestMeta),\n };\n }\n\n /**\n * Start the reconcile loop. Resolves when the first connection is active.\n * The loop continues running in the background.\n */\n async start(attempt = 0): Promise<void> {\n if (typeof WebSocket === \"undefined\") {\n throw new Error(\"WebSockets not supported in current environment\");\n }\n\n const state = this.callbacks.getState();\n if (state === ConnectionState.CLOSED) {\n throw new Error(\"Connection already closed\");\n }\n\n const firstReadyPromise = new Promise<void>((resolve, reject) => {\n this.resolveFirstReady = resolve;\n this.rejectFirstReady = reject;\n });\n\n this.loopPromise = this.reconcileLoop(attempt);\n\n // If the loop ends before firstReady resolves, propagate any error\n this.loopPromise.catch((err) => {\n this.rejectFirstReady?.(err);\n });\n\n await firstReadyPromise;\n }\n\n /**\n * Request graceful shutdown. Resolves when fully closed (in-flight done,\n * connection closed).\n */\n async close(): Promise<void> {\n if (this.closePromise) return this.closePromise;\n\n this.closePromise = this.closeOnce();\n return this.closePromise;\n }\n\n private async closeOnce(): Promise<void> {\n const inFlightCount = Object.keys(\n this._inProgressRequests.requestLeases,\n ).length;\n this.callbacks.logger.info(\n { inFlightCount },\n \"Shutting down, waiting for in-flight requests\",\n );\n // Flip the shutdown flag before starting any timers so the periodic\n // dump guard (`if (!this._shutdownRequested) return`) cannot observe a\n // stale `false` on its first tick.\n this._shutdownRequested = true;\n // Verbose per-request dump (debug-only) at drain start so operators can\n // immediately see which runs are holding the shutdown.\n this.dumpInFlightForShutdown(\"drain-start\");\n this.startShutdownInFlightDumpTimer();\n\n if (this._activeConnection?.ws.readyState === WebSocket.OPEN) {\n this._activeConnection.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_PAUSE,\n }),\n ).finish(),\n ),\n );\n this.callbacks.logger.info(\n { connectionId: this._activeConnection.id },\n \"Sent WORKER_PAUSE, draining\",\n );\n }\n\n this.wake(WAKE_REASON.ShutdownRequested);\n\n if (this.loopPromise) {\n await this.loopPromise;\n }\n\n this.callbacks.logger.info(\"Connection closed\");\n }\n\n async getApiBaseUrl(): Promise<string> {\n return resolveApiBaseUrl({\n apiBaseUrl: this.config.apiBaseUrl,\n mode: this.config.mode,\n });\n }\n\n // ---------------------------------------------------------------------------\n // Wake signal\n // ---------------------------------------------------------------------------\n\n private resetWakeSignal(): void {\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n this.wakeSignal = { promise, resolve: resolve! };\n }\n\n private wake(reason: WakeReason = WAKE_REASON.Unknown): void {\n // Only the first pending wake needs to resolve the parked loop; later\n // wakes are accumulated and consumed together on the next iteration.\n const shouldResolve = this.pendingWakeReasons.length === 0;\n this.pendingWakeReasons.push(reason);\n if (shouldResolve) {\n this.wakeSignal.resolve();\n }\n }\n\n // ---------------------------------------------------------------------------\n // Signing key management\n // ---------------------------------------------------------------------------\n\n private switchAuthKey(): void {\n const switchToFallback =\n this.useSigningKey === this.config.hashedSigningKey;\n if (switchToFallback) {\n this.callbacks.logger.debug(\"Switching to fallback signing key\");\n }\n this.useSigningKey = switchToFallback\n ? this.config.hashedFallbackKey\n : this.config.hashedSigningKey;\n }\n\n // ---------------------------------------------------------------------------\n // In-flight helpers\n // ---------------------------------------------------------------------------\n\n private hasInFlightRequests(): boolean {\n return Object.keys(this._inProgressRequests.requestLeases).length > 0;\n }\n\n /**\n * Debug-level \"still draining\" dump emitted at drain start and periodically\n * thereafter while in-flight requests are holding the shutdown. One summary\n * line plus one line per request carrying `requestId`, `runId`, `stepId`,\n * `functionSlug`, `ageMs`, and `sinceLastLeaseExtendMs`. Does not affect\n * info/warn logs.\n *\n * `requestLeases` drives the reconcile-loop exit gate, so use it as the\n * single source of truth for the in-flight set; `requestMeta` carries the\n * enrichment fields and is kept in sync alongside the lease map.\n */\n private dumpInFlightForShutdown(reason: string): void {\n const leaseIds = Object.keys(this._inProgressRequests.requestLeases);\n if (leaseIds.length === 0) return;\n const now = Date.now();\n const ages: number[] = [];\n for (const id of leaseIds) {\n const m = this._inProgressRequests.requestMeta[id];\n if (m?.leaseAcquiredAt) ages.push(now - m.leaseAcquiredAt);\n }\n\n this.callbacks.logger.debug(\n {\n reason,\n inFlightCount: leaseIds.length,\n oldestAgeMs: ages.length > 0 ? Math.max(...ages) : undefined,\n },\n \"Shutdown: still draining\",\n );\n\n for (const id of leaseIds) {\n const m = this._inProgressRequests.requestMeta[id];\n if (!m) continue;\n this.callbacks.logger.debug(\n {\n reason,\n requestId: m.requestId,\n runId: m.runId,\n stepId: m.stepId,\n functionSlug: m.functionSlug,\n appId: m.appId,\n ageMs: m.leaseAcquiredAt ? now - m.leaseAcquiredAt : undefined,\n sinceLastLeaseExtendMs: m.leaseLastExtendedAt\n ? now - m.leaseLastExtendedAt\n : undefined,\n },\n \"Shutdown: still draining in-flight request\",\n );\n }\n }\n\n private startShutdownInFlightDumpTimer(): void {\n if (this.shutdownDumpInterval) return;\n this.shutdownDumpInterval = setInterval(() => {\n if (!this._shutdownRequested) return;\n this.dumpInFlightForShutdown(\"periodic\");\n // Wake the loop so its \"Reconcile loop woken\" line emits a fresh\n // state snapshot alongside the in-flight dump. Loop will park again\n // immediately if nothing has changed.\n this.wake(WAKE_REASON.ShutdownStillPending);\n }, ConnectionCore.SHUTDOWN_DUMP_INTERVAL_MS);\n }\n\n private stopShutdownInFlightDumpTimer(): void {\n if (this.shutdownDumpInterval) {\n clearInterval(this.shutdownDumpInterval);\n this.shutdownDumpInterval = undefined;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Reconcile loop\n // ---------------------------------------------------------------------------\n\n private async reconcileLoop(initialAttempt: number): Promise<void> {\n let attempt = initialAttempt;\n\n this.callbacks.logger.debug({ initialAttempt }, \"Reconcile loop entered\");\n\n while (true) {\n // Exit condition: shutdown requested + no in-flight requests\n if (this._shutdownRequested && !this.hasInFlightRequests()) {\n break;\n }\n\n // Ensure we have a live connection\n if (!this._activeConnection || this._activeConnection.dead) {\n this.callbacks.logger.debug(\n {\n hasActiveConnection: !!this._activeConnection,\n activeConnectionDead: this._activeConnection?.dead,\n hasDrainingConnection: !!this._drainingConnection,\n drainingConnectionId: this._drainingConnection?.id,\n },\n \"No active connection\",\n );\n\n if (this.hasConnectedBefore) {\n this.callbacks.logger.info({ attempt }, \"Reconnecting\");\n } else {\n this.callbacks.logger.info(\"Connecting\");\n }\n\n this.callbacks.onStateChange(\n this.hasConnectedBefore\n ? ConnectionState.RECONNECTING\n : ConnectionState.CONNECTING,\n );\n\n try {\n const { conn, gatewayGroup } = await establishConnection(\n this.config,\n this.useSigningKey,\n attempt,\n this.excludeGateways,\n this.callbacks.logger,\n );\n\n // Attach post-handshake handlers\n this.attachHandlers(conn, gatewayGroup);\n\n // Clean up draining connection after new one is ready\n if (this._drainingConnection) {\n this.callbacks.logger.info(\n {\n oldConnectionId: this._drainingConnection.id,\n newConnectionId: conn.id,\n },\n \"Replaced draining connection\",\n );\n this._drainingConnection.close();\n this._drainingConnection = undefined;\n }\n\n this._activeConnection = conn;\n this.heartbeatManager.updateInterval(conn.heartbeatIntervalMs);\n this.statusReporter.updateInterval(conn.statusIntervalMs);\n attempt = 0;\n this.hasConnectedBefore = true;\n this.callbacks.logger.info(\n { connectionId: conn.id, gatewayGroup },\n \"Connection active\",\n );\n this.callbacks.onStateChange(ConnectionState.ACTIVE);\n\n if (this._shutdownRequested) {\n // Reconnected during shutdown to keep in-flight requests alive.\n // Send WORKER_PAUSE instead of WORKER_READY so no new work is routed.\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_PAUSE,\n }),\n ).finish(),\n ),\n );\n this.callbacks.logger.info(\n { connectionId: conn.id },\n \"Sent WORKER_PAUSE on reconnect during shutdown\",\n );\n } else {\n // Signal the gateway that we're ready to receive requests.\n // This must happen after ACTIVE so the gateway doesn't route\n // requests before handlers are fully attached.\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_READY,\n }),\n ).finish(),\n ),\n );\n this.callbacks.logger.info(\n { connectionId: conn.id },\n \"Sent WORKER_READY\",\n );\n }\n\n // Flush any buffered responses via HTTP now that we're active.\n this.callbacks.onConnectionActive?.(this.useSigningKey);\n\n this.resolveFirstReady?.();\n this.resolveFirstReady = undefined;\n this.rejectFirstReady = undefined;\n } catch (err) {\n if (!(err instanceof ReconnectError)) throw err;\n\n attempt = err.attempt + 1;\n if (err instanceof AuthError) this.switchAuthKey();\n if (err instanceof ConnectionLimitError) {\n this.callbacks.logger.error(\"Max concurrent connections reached\");\n }\n\n // Gateway is draining, we should retry much faster\n if (err.message?.includes(\"connect_gateway_closing\")) {\n const jitter = 500 + Math.random() * 1000;\n this.callbacks.logger.info(\n { attempt, delay: Math.round(jitter), error: err.message },\n \"Gateway draining, retrying\",\n );\n const cancelled = await waitWithCancel(jitter, () => {\n return this._shutdownRequested && !this.hasInFlightRequests();\n });\n if (cancelled) break;\n continue;\n }\n\n const delay = expBackoff(attempt);\n this.callbacks.logger.info(\n { attempt, delay },\n \"Reconnecting after failure\",\n );\n\n const cancelled = await waitWithCancel(delay, () => {\n return this._shutdownRequested && !this.hasInFlightRequests();\n });\n if (cancelled) break;\n continue;\n }\n }\n\n // Wait for something to change. If a wake fired while this loop was\n // doing async work above, pendingWakeReasons is already populated; don't\n // wait on the replacement wakeSignal or the wake can be missed.\n if (this.pendingWakeReasons.length === 0) {\n await this.wakeSignal.promise;\n }\n const reasons = this.pendingWakeReasons;\n this.pendingWakeReasons = [];\n this.resetWakeSignal();\n this.callbacks.logger.debug(\n {\n reasons,\n shutdownRequested: this._shutdownRequested,\n hasActiveConnection: !!this._activeConnection,\n activeConnectionDead: this._activeConnection?.dead,\n },\n \"Reconcile loop woken\",\n );\n }\n\n this.callbacks.logger.debug(\n {\n shutdownRequested: this._shutdownRequested,\n inFlightCount: Object.keys(this._inProgressRequests.requestLeases)\n .length,\n },\n \"Reconcile loop exiting\",\n );\n\n // Teardown\n this.heartbeatManager.stop();\n this.statusReporter.stop();\n this.stopShutdownInFlightDumpTimer();\n this._activeConnection?.close();\n this._activeConnection = undefined;\n this._drainingConnection?.close();\n this._drainingConnection = undefined;\n }\n\n // ---------------------------------------------------------------------------\n // Post-handshake handler attachment\n // ---------------------------------------------------------------------------\n\n /**\n * Wire up error, close, and message handlers on a newly-handshaked connection.\n */\n private attachHandlers(conn: Connection, gatewayGroup: string): void {\n const { ws } = conn;\n const connectionId = conn.id;\n\n // Error/close handlers: mark connection as dead and wake the loop\n ws.onerror = (ev) => {\n if (conn.dead) return;\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.warn(\n {\n connectionId,\n gatewayGroup,\n uptimeMs,\n error: (ev as ErrorEvent)?.message,\n },\n \"Connection lost (error)\",\n );\n conn.dead = true;\n this.excludeGateways.add(gatewayGroup);\n if (this._activeConnection?.id === connectionId) {\n this._activeConnection = undefined;\n }\n this.wake(WAKE_REASON.WsError);\n };\n\n ws.onclose = (ev) => {\n if (conn.dead) return;\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.warn(\n {\n connectionId,\n gatewayGroup,\n uptimeMs,\n code: ev.code,\n reason: ev.reason,\n },\n \"Connection lost (close)\",\n );\n conn.dead = true;\n this.excludeGateways.add(gatewayGroup);\n if (this._activeConnection?.id === connectionId) {\n this._activeConnection = undefined;\n }\n this.wake(WAKE_REASON.WsClose);\n };\n\n // Message handler for post-handshake messages\n ws.onmessage = async (event) => {\n this._lastMessageReceivedAt = Date.now();\n\n const messageBytes = new Uint8Array(event.data as ArrayBuffer);\n const connectMessage = parseConnectMessage(messageBytes);\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_CLOSING) {\n const uptimeMs = Date.now() - conn.connectedAt;\n this.callbacks.logger.info(\n { connectionId: conn.id, gatewayGroup, uptimeMs },\n \"Gateway draining, opening new connection\",\n );\n // Move current connection to draining, clear active so the loop\n // establishes a replacement.\n this._drainingConnection = this._activeConnection;\n this._activeConnection = undefined;\n this.wake(WAKE_REASON.GatewayClosing);\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_HEARTBEAT) {\n this._lastHeartbeatReceivedAt = Date.now();\n conn.pendingHeartbeats = 0;\n this.callbacks.logger.debug(\n { connectionId },\n \"Handled gateway heartbeat\",\n );\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.GATEWAY_EXECUTOR_REQUEST) {\n await this.requestProcessor.handleExecutorRequest(connectMessage, conn);\n return;\n }\n\n if (connectMessage.kind === GatewayMessageType.WORKER_REPLY_ACK) {\n this.requestProcessor.handleReplyAck(connectMessage, connectionId);\n return;\n }\n\n if (\n connectMessage.kind ===\n GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE_ACK\n ) {\n this.requestProcessor.handleExtendLeaseAck(\n connectMessage,\n connectionId,\n );\n return;\n }\n\n this.callbacks.logger.warn(\n {\n kind: gatewayMessageTypeToJSON(connectMessage.kind),\n rawKind: connectMessage.kind,\n state: this.callbacks.getState(),\n connectionId,\n },\n \"Unexpected message type\",\n );\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_connect = require('../../../../proto/src/components/connect/protobuf/connect.cjs');
|
|
2
2
|
const require_buffer = require('../../buffer.cjs');
|
|
3
|
+
const require_types = require('./types.cjs');
|
|
3
4
|
|
|
4
5
|
//#region src/components/connect/strategies/core/heartbeat.ts
|
|
5
6
|
var HeartbeatManager = class {
|
|
@@ -36,7 +37,7 @@ var HeartbeatManager = class {
|
|
|
36
37
|
if (conn.pendingHeartbeats >= 2) {
|
|
37
38
|
this.logger.warn({ connectionId: conn.id }, "Consecutive heartbeats missed, reconnecting");
|
|
38
39
|
conn.dead = true;
|
|
39
|
-
this.wakeSignal.wake();
|
|
40
|
+
this.wakeSignal.wake(require_types.WAKE_REASON.HeartbeatMissed);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
conn.pendingHeartbeats++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.cjs","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","logger: Logger","ensureUnsharedArrayBuffer","ConnectMessage","GatewayMessageType"],"sources":["../../../../../src/components/connect/strategies/core/heartbeat.ts"],"sourcesContent":["/**\n * Heartbeat management for the active WebSocket connection.\n *\n * Sends periodic heartbeat pings and marks the connection as dead when\n * two consecutive heartbeats go unacknowledged, waking the reconcile loop\n * to trigger reconnection.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport type
|
|
1
|
+
{"version":3,"file":"heartbeat.cjs","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","logger: Logger","WAKE_REASON","ensureUnsharedArrayBuffer","ConnectMessage","GatewayMessageType"],"sources":["../../../../../src/components/connect/strategies/core/heartbeat.ts"],"sourcesContent":["/**\n * Heartbeat management for the active WebSocket connection.\n *\n * Sends periodic heartbeat pings and marks the connection as dead when\n * two consecutive heartbeats go unacknowledged, waking the reconcile loop\n * to trigger reconnection.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport {\n type ConnectionAccessor,\n WAKE_REASON,\n type WakeSignal,\n} from \"./types.ts\";\n\nexport class HeartbeatManager {\n private interval: ReturnType<typeof setInterval> | undefined;\n private intervalMs = 10_000;\n onHeartbeatSent: (() => void) | undefined;\n\n constructor(\n private readonly accessor: ConnectionAccessor,\n private readonly wakeSignal: WakeSignal,\n private readonly logger: Logger,\n ) {}\n\n /**\n * Update the heartbeat interval. Restarts the timer if the interval changed\n * or if it wasn't running yet.\n */\n updateInterval(ms: number): void {\n if (ms === this.intervalMs && this.interval) return;\n this.intervalMs = ms;\n this.stop();\n this.start();\n }\n\n /** Stop the heartbeat timer. */\n stop(): void {\n clearInterval(this.interval);\n this.interval = undefined;\n }\n\n private start(): void {\n if (this.interval) return;\n this.interval = setInterval(() => this.tick(), this.intervalMs);\n }\n\n private tick(): void {\n const conn = this.accessor.activeConnection;\n if (!conn || conn.ws.readyState !== WebSocket.OPEN) return;\n\n if (conn.pendingHeartbeats >= 2) {\n this.logger.warn(\n { connectionId: conn.id },\n \"Consecutive heartbeats missed, reconnecting\",\n );\n conn.dead = true;\n this.wakeSignal.wake(WAKE_REASON.HeartbeatMissed);\n return;\n }\n\n conn.pendingHeartbeats++;\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_HEARTBEAT,\n }),\n ).finish(),\n ),\n );\n\n this.logger.debug({ connectionId: conn.id }, \"Heartbeat sent\");\n this.onHeartbeatSent?.();\n }\n}\n"],"mappings":";;;;;AAoBA,IAAa,mBAAb,MAA8B;CAC5B,AAAQ;CACR,AAAQ,aAAa;CACrB;CAEA,YACE,AAAiBA,UACjB,AAAiBC,YACjB,AAAiBC,QACjB;EAHiB;EACA;EACA;;;;;;CAOnB,eAAe,IAAkB;AAC/B,MAAI,OAAO,KAAK,cAAc,KAAK,SAAU;AAC7C,OAAK,aAAa;AAClB,OAAK,MAAM;AACX,OAAK,OAAO;;;CAId,OAAa;AACX,gBAAc,KAAK,SAAS;AAC5B,OAAK,WAAW;;CAGlB,AAAQ,QAAc;AACpB,MAAI,KAAK,SAAU;AACnB,OAAK,WAAW,kBAAkB,KAAK,MAAM,EAAE,KAAK,WAAW;;CAGjE,AAAQ,OAAa;EACnB,MAAM,OAAO,KAAK,SAAS;AAC3B,MAAI,CAAC,QAAQ,KAAK,GAAG,eAAe,UAAU,KAAM;AAEpD,MAAI,KAAK,qBAAqB,GAAG;AAC/B,QAAK,OAAO,KACV,EAAE,cAAc,KAAK,IAAI,EACzB,8CACD;AACD,QAAK,OAAO;AACZ,QAAK,WAAW,KAAKC,0BAAY,gBAAgB;AACjD;;AAGF,OAAK;AACL,OAAK,GAAG,KACNC,yCACEC,+BAAe,OACbA,+BAAe,OAAO,EACpB,MAAMC,mCAAmB,kBAC1B,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AAED,OAAK,OAAO,MAAM,EAAE,cAAc,KAAK,IAAI,EAAE,iBAAiB;AAC9D,OAAK,mBAAmB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConnectMessage, GatewayMessageType } from "../../../../proto/src/components/connect/protobuf/connect.js";
|
|
2
2
|
import { ensureUnsharedArrayBuffer } from "../../buffer.js";
|
|
3
|
+
import { WAKE_REASON } from "./types.js";
|
|
3
4
|
|
|
4
5
|
//#region src/components/connect/strategies/core/heartbeat.ts
|
|
5
6
|
var HeartbeatManager = class {
|
|
@@ -36,7 +37,7 @@ var HeartbeatManager = class {
|
|
|
36
37
|
if (conn.pendingHeartbeats >= 2) {
|
|
37
38
|
this.logger.warn({ connectionId: conn.id }, "Consecutive heartbeats missed, reconnecting");
|
|
38
39
|
conn.dead = true;
|
|
39
|
-
this.wakeSignal.wake();
|
|
40
|
+
this.wakeSignal.wake(WAKE_REASON.HeartbeatMissed);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
conn.pendingHeartbeats++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.js","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","logger: Logger"],"sources":["../../../../../src/components/connect/strategies/core/heartbeat.ts"],"sourcesContent":["/**\n * Heartbeat management for the active WebSocket connection.\n *\n * Sends periodic heartbeat pings and marks the connection as dead when\n * two consecutive heartbeats go unacknowledged, waking the reconcile loop\n * to trigger reconnection.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport type
|
|
1
|
+
{"version":3,"file":"heartbeat.js","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","logger: Logger"],"sources":["../../../../../src/components/connect/strategies/core/heartbeat.ts"],"sourcesContent":["/**\n * Heartbeat management for the active WebSocket connection.\n *\n * Sends periodic heartbeat pings and marks the connection as dead when\n * two consecutive heartbeats go unacknowledged, waking the reconcile loop\n * to trigger reconnection.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n GatewayMessageType,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport {\n type ConnectionAccessor,\n WAKE_REASON,\n type WakeSignal,\n} from \"./types.ts\";\n\nexport class HeartbeatManager {\n private interval: ReturnType<typeof setInterval> | undefined;\n private intervalMs = 10_000;\n onHeartbeatSent: (() => void) | undefined;\n\n constructor(\n private readonly accessor: ConnectionAccessor,\n private readonly wakeSignal: WakeSignal,\n private readonly logger: Logger,\n ) {}\n\n /**\n * Update the heartbeat interval. Restarts the timer if the interval changed\n * or if it wasn't running yet.\n */\n updateInterval(ms: number): void {\n if (ms === this.intervalMs && this.interval) return;\n this.intervalMs = ms;\n this.stop();\n this.start();\n }\n\n /** Stop the heartbeat timer. */\n stop(): void {\n clearInterval(this.interval);\n this.interval = undefined;\n }\n\n private start(): void {\n if (this.interval) return;\n this.interval = setInterval(() => this.tick(), this.intervalMs);\n }\n\n private tick(): void {\n const conn = this.accessor.activeConnection;\n if (!conn || conn.ws.readyState !== WebSocket.OPEN) return;\n\n if (conn.pendingHeartbeats >= 2) {\n this.logger.warn(\n { connectionId: conn.id },\n \"Consecutive heartbeats missed, reconnecting\",\n );\n conn.dead = true;\n this.wakeSignal.wake(WAKE_REASON.HeartbeatMissed);\n return;\n }\n\n conn.pendingHeartbeats++;\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_HEARTBEAT,\n }),\n ).finish(),\n ),\n );\n\n this.logger.debug({ connectionId: conn.id }, \"Heartbeat sent\");\n this.onHeartbeatSent?.();\n }\n}\n"],"mappings":";;;;;AAoBA,IAAa,mBAAb,MAA8B;CAC5B,AAAQ;CACR,AAAQ,aAAa;CACrB;CAEA,YACE,AAAiBA,UACjB,AAAiBC,YACjB,AAAiBC,QACjB;EAHiB;EACA;EACA;;;;;;CAOnB,eAAe,IAAkB;AAC/B,MAAI,OAAO,KAAK,cAAc,KAAK,SAAU;AAC7C,OAAK,aAAa;AAClB,OAAK,MAAM;AACX,OAAK,OAAO;;;CAId,OAAa;AACX,gBAAc,KAAK,SAAS;AAC5B,OAAK,WAAW;;CAGlB,AAAQ,QAAc;AACpB,MAAI,KAAK,SAAU;AACnB,OAAK,WAAW,kBAAkB,KAAK,MAAM,EAAE,KAAK,WAAW;;CAGjE,AAAQ,OAAa;EACnB,MAAM,OAAO,KAAK,SAAS;AAC3B,MAAI,CAAC,QAAQ,KAAK,GAAG,eAAe,UAAU,KAAM;AAEpD,MAAI,KAAK,qBAAqB,GAAG;AAC/B,QAAK,OAAO,KACV,EAAE,cAAc,KAAK,IAAI,EACzB,8CACD;AACD,QAAK,OAAO;AACZ,QAAK,WAAW,KAAK,YAAY,gBAAgB;AACjD;;AAGF,OAAK;AACL,OAAK,GAAG,KACN,0BACE,eAAe,OACb,eAAe,OAAO,EACpB,MAAM,mBAAmB,kBAC1B,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AAED,OAAK,OAAO,MAAM,EAAE,cAAc,KAAK,IAAI,EAAE,iBAAiB;AAC9D,OAAK,mBAAmB"}
|
|
@@ -2,6 +2,7 @@ const require_connect = require('../../../../proto/src/components/connect/protob
|
|
|
2
2
|
const require_buffer = require('../../buffer.cjs');
|
|
3
3
|
const require_types = require('../../types.cjs');
|
|
4
4
|
const require_messages = require('../../messages.cjs');
|
|
5
|
+
const require_types$1 = require('./types.cjs');
|
|
5
6
|
|
|
6
7
|
//#region src/components/connect/strategies/core/requestProcessor.ts
|
|
7
8
|
function toError(value) {
|
|
@@ -67,6 +68,7 @@ var RequestProcessor = class {
|
|
|
67
68
|
})).finish()));
|
|
68
69
|
this.accessor.inProgressRequests.wg.add(1);
|
|
69
70
|
this.accessor.inProgressRequests.requestLeases[gatewayExecutorRequest.requestId] = gatewayExecutorRequest.leaseId;
|
|
71
|
+
const leaseAcquiredAt = Date.now();
|
|
70
72
|
this.accessor.inProgressRequests.requestMeta[gatewayExecutorRequest.requestId] = {
|
|
71
73
|
requestId: gatewayExecutorRequest.requestId,
|
|
72
74
|
runId: gatewayExecutorRequest.runId,
|
|
@@ -74,7 +76,9 @@ var RequestProcessor = class {
|
|
|
74
76
|
appId: gatewayExecutorRequest.appId,
|
|
75
77
|
envId: gatewayExecutorRequest.envId,
|
|
76
78
|
functionSlug: gatewayExecutorRequest.functionSlug,
|
|
77
|
-
accountId: gatewayExecutorRequest.accountId
|
|
79
|
+
accountId: gatewayExecutorRequest.accountId,
|
|
80
|
+
leaseAcquiredAt,
|
|
81
|
+
leaseLastExtendedAt: leaseAcquiredAt
|
|
78
82
|
};
|
|
79
83
|
const inFlightCount = Object.keys(this.accessor.inProgressRequests.requestLeases).length;
|
|
80
84
|
this.logger.debug({
|
|
@@ -98,7 +102,11 @@ var RequestProcessor = class {
|
|
|
98
102
|
};
|
|
99
103
|
this.logger.debug({
|
|
100
104
|
connectionId: latestConn.id,
|
|
101
|
-
leaseId: currentLeaseId
|
|
105
|
+
leaseId: currentLeaseId,
|
|
106
|
+
requestId: gatewayExecutorRequest.requestId,
|
|
107
|
+
functionSlug: gatewayExecutorRequest.functionSlug,
|
|
108
|
+
runId: gatewayExecutorRequest.runId,
|
|
109
|
+
stepId: gatewayExecutorRequest.stepId
|
|
102
110
|
}, "Extending lease");
|
|
103
111
|
if (latestConn.ws.readyState !== WebSocket.OPEN) {
|
|
104
112
|
this.logger.warn({
|
|
@@ -123,6 +131,8 @@ var RequestProcessor = class {
|
|
|
123
131
|
leaseId: currentLeaseId
|
|
124
132
|
})).finish()
|
|
125
133
|
})).finish()));
|
|
134
|
+
const meta = this.accessor.inProgressRequests.requestMeta[gatewayExecutorRequest.requestId];
|
|
135
|
+
if (meta) meta.leaseLastExtendedAt = Date.now();
|
|
126
136
|
} catch (err) {
|
|
127
137
|
this.logger.warn({
|
|
128
138
|
connectionId: latestConn.id,
|
|
@@ -169,7 +179,7 @@ var RequestProcessor = class {
|
|
|
169
179
|
requestId: gatewayExecutorRequest.requestId,
|
|
170
180
|
remainingInFlight
|
|
171
181
|
}, "Request finished");
|
|
172
|
-
if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) this.wakeSignal.wake();
|
|
182
|
+
if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) this.wakeSignal.wake(require_types$1.WAKE_REASON.RequestFinishedOnShutdown);
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
/** Handle a reply ACK from the gateway. */
|
|
@@ -184,13 +194,24 @@ var RequestProcessor = class {
|
|
|
184
194
|
/** Handle a lease extension ACK from the gateway. */
|
|
185
195
|
handleExtendLeaseAck(connectMessage, connectionId) {
|
|
186
196
|
const extendLeaseAck = require_connect.WorkerRequestExtendLeaseAckData.decode(connectMessage.payload);
|
|
197
|
+
const hasLease = Object.hasOwn(this.accessor.inProgressRequests.requestLeases, extendLeaseAck.requestId);
|
|
198
|
+
const meta = this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];
|
|
199
|
+
if (!hasLease || !meta) {
|
|
200
|
+
this.logger.debug({
|
|
201
|
+
connectionId,
|
|
202
|
+
requestId: extendLeaseAck.requestId,
|
|
203
|
+
newLeaseId: extendLeaseAck.newLeaseId,
|
|
204
|
+
hadLease: hasLease,
|
|
205
|
+
hadMeta: !!meta
|
|
206
|
+
}, "Ignoring extend lease ack for non-in-flight request");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
187
209
|
this.logger.debug({
|
|
188
210
|
connectionId,
|
|
189
211
|
newLeaseId: extendLeaseAck.newLeaseId
|
|
190
212
|
}, "Received extend lease ack");
|
|
191
213
|
if (extendLeaseAck.newLeaseId) this.accessor.inProgressRequests.requestLeases[extendLeaseAck.requestId] = extendLeaseAck.newLeaseId;
|
|
192
214
|
else {
|
|
193
|
-
const meta = this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];
|
|
194
215
|
this.logger.error({
|
|
195
216
|
connectionId,
|
|
196
217
|
requestId: extendLeaseAck.requestId,
|
|
@@ -199,6 +220,8 @@ var RequestProcessor = class {
|
|
|
199
220
|
stepId: meta?.stepId
|
|
200
221
|
}, "Lease lost: the server did not renew the lease for this request. Another worker may have claimed it. The in-progress execution will continue but its result may be discarded.");
|
|
201
222
|
delete this.accessor.inProgressRequests.requestLeases[extendLeaseAck.requestId];
|
|
223
|
+
delete this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];
|
|
224
|
+
if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) this.wakeSignal.wake(require_types$1.WAKE_REASON.LeaseLostOnShutdown);
|
|
202
225
|
}
|
|
203
226
|
}
|
|
204
227
|
hasInFlightRequests() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requestProcessor.cjs","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","callbacks: ConnectionCoreCallbacks","logger: Logger","ConnectionState","parseGatewayExecutorRequest","ensureUnsharedArrayBuffer","ConnectMessage","GatewayMessageType","WorkerRequestAckData","extendLeaseInterval: ReturnType<typeof setInterval> | undefined","WorkerRequestExtendLeaseData","parseWorkerReplyAck","WorkerRequestExtendLeaseAckData"],"sources":["../../../../../src/components/connect/strategies/core/requestProcessor.ts"],"sourcesContent":["/**\n * Processes incoming executor requests, manages lease extensions, and handles\n * reply acknowledgements.\n *\n * Extracted from ConnectionCore so the reconcile loop orchestrator only\n * dispatches messages to this module rather than containing the full\n * execution flow inline.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n type ConnectMessage as ConnectMessageType,\n type GatewayExecutorRequestData,\n GatewayMessageType,\n WorkerRequestAckData,\n WorkerRequestExtendLeaseAckData,\n WorkerRequestExtendLeaseData,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport {\n parseGatewayExecutorRequest,\n parseWorkerReplyAck,\n} from \"../../messages.ts\";\nimport { ConnectionState } from \"../../types.ts\";\nimport type { Connection, ConnectionCoreCallbacks } from \"./connection.ts\";\nimport type { ConnectionAccessor, WakeSignal } from \"./types.ts\";\n\nfunction toError(value: unknown): Error {\n if (value instanceof Error) {\n return value;\n }\n return new Error(String(value));\n}\n\nexport class RequestProcessor {\n constructor(\n private readonly accessor: ConnectionAccessor,\n private readonly wakeSignal: WakeSignal,\n private readonly callbacks: ConnectionCoreCallbacks,\n private readonly logger: Logger,\n ) {}\n\n /** Handle an incoming executor request. */\n async handleExecutorRequest(\n connectMessage: ConnectMessageType,\n conn: Connection,\n ): Promise<void> {\n const currentState = this.callbacks.getState();\n if (currentState !== ConnectionState.ACTIVE) {\n this.logger.warn(\n { connectionId: conn.id },\n \"Received request while not active, skipping\",\n );\n return;\n }\n\n const gatewayExecutorRequest = parseGatewayExecutorRequest(\n connectMessage.payload,\n );\n\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"Received gateway executor request\",\n );\n\n if (\n typeof gatewayExecutorRequest.appName !== \"string\" ||\n gatewayExecutorRequest.appName.length === 0\n ) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No app name in request, skipping\",\n );\n return;\n }\n\n if (!this.accessor.appIds.includes(gatewayExecutorRequest.appName)) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No request handler found for app, skipping\",\n );\n return;\n }\n\n // Send ACK\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_ACK,\n payload: WorkerRequestAckData.encode(\n WorkerRequestAckData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n runId: gatewayExecutorRequest.runId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n\n this.accessor.inProgressRequests.wg.add(1);\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ] = gatewayExecutorRequest.leaseId;\n this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ] = {\n requestId: gatewayExecutorRequest.requestId,\n runId: gatewayExecutorRequest.runId,\n stepId: gatewayExecutorRequest.stepId,\n appId: gatewayExecutorRequest.appId,\n envId: gatewayExecutorRequest.envId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n accountId: gatewayExecutorRequest.accountId,\n };\n\n const inFlightCount = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n inFlightCount,\n },\n \"Request acknowledged\",\n );\n\n const startedAt = Date.now();\n\n // Start lease extension interval\n const originalWs = conn.ws;\n const originalConnectionId = conn.id;\n let extendLeaseInterval: ReturnType<typeof setInterval> | undefined;\n extendLeaseInterval = setInterval(() => {\n const currentLeaseId =\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n if (!currentLeaseId) {\n clearInterval(extendLeaseInterval);\n return;\n }\n\n // Use the current live connection's WebSocket for lease extensions.\n // During a drain, the original WebSocket may be closed by the gateway\n // while the request is still in flight.\n const latestConn = {\n ws: this.accessor.activeConnection?.ws ?? originalWs,\n id: this.accessor.activeConnection?.id ?? originalConnectionId,\n };\n\n this.logger.debug(\n { connectionId: latestConn.id, leaseId: currentLeaseId },\n \"Extending lease\",\n );\n\n if (latestConn.ws.readyState !== WebSocket.OPEN) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Cannot extend lease, no open WebSocket available\",\n );\n return;\n }\n\n try {\n latestConn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE,\n payload: WorkerRequestExtendLeaseData.encode(\n WorkerRequestExtendLeaseData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n runId: gatewayExecutorRequest.runId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n leaseId: currentLeaseId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n } catch (err) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n err: toError(err),\n },\n \"Failed to send lease extension\",\n );\n }\n }, conn.extendLeaseIntervalMs);\n\n try {\n const responseBytes = await this.callbacks.handleExecutionRequest(\n gatewayExecutorRequest,\n );\n\n const durationMs = Date.now() - startedAt;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n durationMs,\n },\n \"Request execution completed\",\n );\n\n if (!this.accessor.activeConnection) {\n this.logger.warn(\n { requestId: gatewayExecutorRequest.requestId },\n \"No current WebSocket, buffering response\",\n );\n if (this.callbacks.onBufferResponse) {\n this.callbacks.onBufferResponse(\n gatewayExecutorRequest.requestId,\n responseBytes,\n );\n }\n return;\n }\n\n this.logger.debug(\n {\n connectionId: this.accessor.activeConnection.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Sending worker reply\",\n );\n\n this.accessor.activeConnection.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REPLY,\n payload: responseBytes,\n }),\n ).finish(),\n ),\n );\n } catch (err) {\n const durationMs = Date.now() - startedAt;\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n durationMs,\n err: toError(err),\n },\n \"Execution error\",\n );\n } finally {\n this.accessor.inProgressRequests.wg.done();\n delete this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n delete this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ];\n clearInterval(extendLeaseInterval);\n\n const remainingInFlight = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n remainingInFlight,\n },\n \"Request finished\",\n );\n\n // Wake the loop if shutdown is pending and this was the last request\n if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) {\n this.wakeSignal.wake();\n }\n }\n }\n\n /** Handle a reply ACK from the gateway. */\n handleReplyAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const replyAck = parseWorkerReplyAck(connectMessage.payload);\n\n this.logger.debug(\n { connectionId, requestId: replyAck.requestId },\n \"Acknowledging reply ack\",\n );\n\n this.callbacks.onReplyAck?.(replyAck.requestId);\n }\n\n /** Handle a lease extension ACK from the gateway. */\n handleExtendLeaseAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const extendLeaseAck = WorkerRequestExtendLeaseAckData.decode(\n connectMessage.payload,\n );\n\n this.logger.debug(\n { connectionId, newLeaseId: extendLeaseAck.newLeaseId },\n \"Received extend lease ack\",\n );\n\n if (extendLeaseAck.newLeaseId) {\n this.accessor.inProgressRequests.requestLeases[extendLeaseAck.requestId] =\n extendLeaseAck.newLeaseId;\n } else {\n const meta =\n this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];\n\n this.logger.error(\n {\n connectionId,\n requestId: extendLeaseAck.requestId,\n functionSlug: meta?.functionSlug,\n runId: meta?.runId,\n stepId: meta?.stepId,\n },\n \"Lease lost: the server did not renew the lease for this request. \" +\n \"Another worker may have claimed it. The in-progress execution \" +\n \"will continue but its result may be discarded.\",\n );\n delete this.accessor.inProgressRequests.requestLeases[\n extendLeaseAck.requestId\n ];\n }\n }\n\n private hasInFlightRequests(): boolean {\n return (\n Object.keys(this.accessor.inProgressRequests.requestLeases).length > 0\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"requestProcessor.cjs","names":["accessor: ConnectionAccessor","wakeSignal: WakeSignal","callbacks: ConnectionCoreCallbacks","logger: Logger","ConnectionState","parseGatewayExecutorRequest","ensureUnsharedArrayBuffer","ConnectMessage","GatewayMessageType","WorkerRequestAckData","extendLeaseInterval: ReturnType<typeof setInterval> | undefined","WorkerRequestExtendLeaseData","WAKE_REASON","parseWorkerReplyAck","WorkerRequestExtendLeaseAckData"],"sources":["../../../../../src/components/connect/strategies/core/requestProcessor.ts"],"sourcesContent":["/**\n * Processes incoming executor requests, manages lease extensions, and handles\n * reply acknowledgements.\n *\n * Extracted from ConnectionCore so the reconcile loop orchestrator only\n * dispatches messages to this module rather than containing the full\n * execution flow inline.\n */\n\nimport type { Logger } from \"../../../../middleware/logger.ts\";\nimport {\n ConnectMessage,\n type ConnectMessage as ConnectMessageType,\n GatewayMessageType,\n WorkerRequestAckData,\n WorkerRequestExtendLeaseAckData,\n WorkerRequestExtendLeaseData,\n} from \"../../../../proto/src/components/connect/protobuf/connect.ts\";\nimport { ensureUnsharedArrayBuffer } from \"../../buffer.ts\";\nimport {\n parseGatewayExecutorRequest,\n parseWorkerReplyAck,\n} from \"../../messages.ts\";\nimport { ConnectionState } from \"../../types.ts\";\nimport type { Connection, ConnectionCoreCallbacks } from \"./connection.ts\";\nimport {\n type ConnectionAccessor,\n WAKE_REASON,\n type WakeSignal,\n} from \"./types.ts\";\n\nfunction toError(value: unknown): Error {\n if (value instanceof Error) {\n return value;\n }\n return new Error(String(value));\n}\n\nexport class RequestProcessor {\n constructor(\n private readonly accessor: ConnectionAccessor,\n private readonly wakeSignal: WakeSignal,\n private readonly callbacks: ConnectionCoreCallbacks,\n private readonly logger: Logger,\n ) {}\n\n /** Handle an incoming executor request. */\n async handleExecutorRequest(\n connectMessage: ConnectMessageType,\n conn: Connection,\n ): Promise<void> {\n const currentState = this.callbacks.getState();\n if (currentState !== ConnectionState.ACTIVE) {\n this.logger.warn(\n { connectionId: conn.id },\n \"Received request while not active, skipping\",\n );\n return;\n }\n\n const gatewayExecutorRequest = parseGatewayExecutorRequest(\n connectMessage.payload,\n );\n\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"Received gateway executor request\",\n );\n\n if (\n typeof gatewayExecutorRequest.appName !== \"string\" ||\n gatewayExecutorRequest.appName.length === 0\n ) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No app name in request, skipping\",\n );\n return;\n }\n\n if (!this.accessor.appIds.includes(gatewayExecutorRequest.appName)) {\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n appId: gatewayExecutorRequest.appId,\n appName: gatewayExecutorRequest.appName,\n functionSlug: gatewayExecutorRequest.functionSlug,\n stepId: gatewayExecutorRequest.stepId,\n connectionId: conn.id,\n },\n \"No request handler found for app, skipping\",\n );\n return;\n }\n\n // Send ACK\n conn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_ACK,\n payload: WorkerRequestAckData.encode(\n WorkerRequestAckData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n runId: gatewayExecutorRequest.runId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n\n this.accessor.inProgressRequests.wg.add(1);\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ] = gatewayExecutorRequest.leaseId;\n const leaseAcquiredAt = Date.now();\n this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ] = {\n requestId: gatewayExecutorRequest.requestId,\n runId: gatewayExecutorRequest.runId,\n stepId: gatewayExecutorRequest.stepId,\n appId: gatewayExecutorRequest.appId,\n envId: gatewayExecutorRequest.envId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n accountId: gatewayExecutorRequest.accountId,\n leaseAcquiredAt,\n leaseLastExtendedAt: leaseAcquiredAt,\n };\n\n const inFlightCount = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n inFlightCount,\n },\n \"Request acknowledged\",\n );\n\n const startedAt = Date.now();\n\n // Start lease extension interval\n const originalWs = conn.ws;\n const originalConnectionId = conn.id;\n let extendLeaseInterval: ReturnType<typeof setInterval> | undefined;\n extendLeaseInterval = setInterval(() => {\n const currentLeaseId =\n this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n if (!currentLeaseId) {\n clearInterval(extendLeaseInterval);\n return;\n }\n\n // Use the current live connection's WebSocket for lease extensions.\n // During a drain, the original WebSocket may be closed by the gateway\n // while the request is still in flight.\n const latestConn = {\n ws: this.accessor.activeConnection?.ws ?? originalWs,\n id: this.accessor.activeConnection?.id ?? originalConnectionId,\n };\n\n this.logger.debug(\n {\n connectionId: latestConn.id,\n leaseId: currentLeaseId,\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n runId: gatewayExecutorRequest.runId,\n stepId: gatewayExecutorRequest.stepId,\n },\n \"Extending lease\",\n );\n\n if (latestConn.ws.readyState !== WebSocket.OPEN) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Cannot extend lease, no open WebSocket available\",\n );\n return;\n }\n\n try {\n latestConn.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REQUEST_EXTEND_LEASE,\n payload: WorkerRequestExtendLeaseData.encode(\n WorkerRequestExtendLeaseData.create({\n accountId: gatewayExecutorRequest.accountId,\n envId: gatewayExecutorRequest.envId,\n appId: gatewayExecutorRequest.appId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n requestId: gatewayExecutorRequest.requestId,\n stepId: gatewayExecutorRequest.stepId,\n runId: gatewayExecutorRequest.runId,\n userTraceCtx: gatewayExecutorRequest.userTraceCtx,\n systemTraceCtx: gatewayExecutorRequest.systemTraceCtx,\n leaseId: currentLeaseId,\n }),\n ).finish(),\n }),\n ).finish(),\n ),\n );\n const meta =\n this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ];\n if (meta) meta.leaseLastExtendedAt = Date.now();\n } catch (err) {\n this.logger.warn(\n {\n connectionId: latestConn.id,\n requestId: gatewayExecutorRequest.requestId,\n err: toError(err),\n },\n \"Failed to send lease extension\",\n );\n }\n }, conn.extendLeaseIntervalMs);\n\n try {\n const responseBytes = await this.callbacks.handleExecutionRequest(\n gatewayExecutorRequest,\n );\n\n const durationMs = Date.now() - startedAt;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n functionSlug: gatewayExecutorRequest.functionSlug,\n durationMs,\n },\n \"Request execution completed\",\n );\n\n if (!this.accessor.activeConnection) {\n this.logger.warn(\n { requestId: gatewayExecutorRequest.requestId },\n \"No current WebSocket, buffering response\",\n );\n if (this.callbacks.onBufferResponse) {\n this.callbacks.onBufferResponse(\n gatewayExecutorRequest.requestId,\n responseBytes,\n );\n }\n return;\n }\n\n this.logger.debug(\n {\n connectionId: this.accessor.activeConnection.id,\n requestId: gatewayExecutorRequest.requestId,\n },\n \"Sending worker reply\",\n );\n\n this.accessor.activeConnection.ws.send(\n ensureUnsharedArrayBuffer(\n ConnectMessage.encode(\n ConnectMessage.create({\n kind: GatewayMessageType.WORKER_REPLY,\n payload: responseBytes,\n }),\n ).finish(),\n ),\n );\n } catch (err) {\n const durationMs = Date.now() - startedAt;\n this.logger.warn(\n {\n requestId: gatewayExecutorRequest.requestId,\n durationMs,\n err: toError(err),\n },\n \"Execution error\",\n );\n } finally {\n this.accessor.inProgressRequests.wg.done();\n delete this.accessor.inProgressRequests.requestLeases[\n gatewayExecutorRequest.requestId\n ];\n delete this.accessor.inProgressRequests.requestMeta[\n gatewayExecutorRequest.requestId\n ];\n clearInterval(extendLeaseInterval);\n\n const remainingInFlight = Object.keys(\n this.accessor.inProgressRequests.requestLeases,\n ).length;\n this.logger.debug(\n {\n requestId: gatewayExecutorRequest.requestId,\n remainingInFlight,\n },\n \"Request finished\",\n );\n\n // Wake the loop if shutdown is pending and this was the last request\n if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) {\n this.wakeSignal.wake(WAKE_REASON.RequestFinishedOnShutdown);\n }\n }\n }\n\n /** Handle a reply ACK from the gateway. */\n handleReplyAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const replyAck = parseWorkerReplyAck(connectMessage.payload);\n\n this.logger.debug(\n { connectionId, requestId: replyAck.requestId },\n \"Acknowledging reply ack\",\n );\n\n this.callbacks.onReplyAck?.(replyAck.requestId);\n }\n\n /** Handle a lease extension ACK from the gateway. */\n handleExtendLeaseAck(\n connectMessage: ConnectMessageType,\n connectionId: string,\n ): void {\n const extendLeaseAck = WorkerRequestExtendLeaseAckData.decode(\n connectMessage.payload,\n );\n\n // Late lease-extend ACKs can arrive after a request has already cleaned up\n // both maps in the completion or lease-lost path. This handler stays\n // synchronous, so once we observe the request missing from either map we\n // can safely ignore the ACK instead of recreating a stale lease entry.\n const hasLease = Object.hasOwn(\n this.accessor.inProgressRequests.requestLeases,\n extendLeaseAck.requestId,\n );\n const meta =\n this.accessor.inProgressRequests.requestMeta[extendLeaseAck.requestId];\n if (!hasLease || !meta) {\n this.logger.debug(\n {\n connectionId,\n requestId: extendLeaseAck.requestId,\n newLeaseId: extendLeaseAck.newLeaseId,\n hadLease: hasLease,\n hadMeta: !!meta,\n },\n \"Ignoring extend lease ack for non-in-flight request\",\n );\n return;\n }\n\n this.logger.debug(\n { connectionId, newLeaseId: extendLeaseAck.newLeaseId },\n \"Received extend lease ack\",\n );\n\n if (extendLeaseAck.newLeaseId) {\n this.accessor.inProgressRequests.requestLeases[extendLeaseAck.requestId] =\n extendLeaseAck.newLeaseId;\n } else {\n this.logger.error(\n {\n connectionId,\n requestId: extendLeaseAck.requestId,\n functionSlug: meta?.functionSlug,\n runId: meta?.runId,\n stepId: meta?.stepId,\n },\n \"Lease lost: the server did not renew the lease for this request. \" +\n \"Another worker may have claimed it. The in-progress execution \" +\n \"will continue but its result may be discarded.\",\n );\n delete this.accessor.inProgressRequests.requestLeases[\n extendLeaseAck.requestId\n ];\n // Also drop meta so the shutdown dump helper and debug-state snapshot\n // don't report a request we've explicitly released the lease for.\n delete this.accessor.inProgressRequests.requestMeta[\n extendLeaseAck.requestId\n ];\n\n // If this was the last in-flight request and a shutdown has been\n // requested, wake the reconcile loop so close() can observe the\n // empty lease map and exit. Without this, the loop stays parked on\n // wakeSignal.promise because the finally-block decrement in\n // handleExecutorRequest never runs (user code is still hanging).\n if (this.accessor.shutdownRequested && !this.hasInFlightRequests()) {\n this.wakeSignal.wake(WAKE_REASON.LeaseLostOnShutdown);\n }\n }\n }\n\n private hasInFlightRequests(): boolean {\n return (\n Object.keys(this.accessor.inProgressRequests.requestLeases).length > 0\n );\n }\n}\n"],"mappings":";;;;;;;AA+BA,SAAS,QAAQ,OAAuB;AACtC,KAAI,iBAAiB,MACnB,QAAO;AAET,QAAO,IAAI,MAAM,OAAO,MAAM,CAAC;;AAGjC,IAAa,mBAAb,MAA8B;CAC5B,YACE,AAAiBA,UACjB,AAAiBC,YACjB,AAAiBC,WACjB,AAAiBC,QACjB;EAJiB;EACA;EACA;EACA;;;CAInB,MAAM,sBACJ,gBACA,MACe;AAEf,MADqB,KAAK,UAAU,UAAU,KACzBC,8BAAgB,QAAQ;AAC3C,QAAK,OAAO,KACV,EAAE,cAAc,KAAK,IAAI,EACzB,8CACD;AACD;;EAGF,MAAM,yBAAyBC,6CAC7B,eAAe,QAChB;AAED,OAAK,OAAO,MACV;GACE,WAAW,uBAAuB;GAClC,OAAO,uBAAuB;GAC9B,SAAS,uBAAuB;GAChC,cAAc,uBAAuB;GACrC,QAAQ,uBAAuB;GAC/B,cAAc,KAAK;GACpB,EACD,oCACD;AAED,MACE,OAAO,uBAAuB,YAAY,YAC1C,uBAAuB,QAAQ,WAAW,GAC1C;AACA,QAAK,OAAO,KACV;IACE,WAAW,uBAAuB;IAClC,OAAO,uBAAuB;IAC9B,cAAc,uBAAuB;IACrC,QAAQ,uBAAuB;IAC/B,cAAc,KAAK;IACpB,EACD,mCACD;AACD;;AAGF,MAAI,CAAC,KAAK,SAAS,OAAO,SAAS,uBAAuB,QAAQ,EAAE;AAClE,QAAK,OAAO,KACV;IACE,WAAW,uBAAuB;IAClC,OAAO,uBAAuB;IAC9B,SAAS,uBAAuB;IAChC,cAAc,uBAAuB;IACrC,QAAQ,uBAAuB;IAC/B,cAAc,KAAK;IACpB,EACD,6CACD;AACD;;AAIF,OAAK,GAAG,KACNC,yCACEC,+BAAe,OACbA,+BAAe,OAAO;GACpB,MAAMC,mCAAmB;GACzB,SAASC,qCAAqB,OAC5BA,qCAAqB,OAAO;IAC1B,WAAW,uBAAuB;IAClC,OAAO,uBAAuB;IAC9B,OAAO,uBAAuB;IAC9B,cAAc,uBAAuB;IACrC,WAAW,uBAAuB;IAClC,QAAQ,uBAAuB;IAC/B,cAAc,uBAAuB;IACrC,gBAAgB,uBAAuB;IACvC,OAAO,uBAAuB;IAC/B,CAAC,CACH,CAAC,QAAQ;GACX,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AAED,OAAK,SAAS,mBAAmB,GAAG,IAAI,EAAE;AAC1C,OAAK,SAAS,mBAAmB,cAC/B,uBAAuB,aACrB,uBAAuB;EAC3B,MAAM,kBAAkB,KAAK,KAAK;AAClC,OAAK,SAAS,mBAAmB,YAC/B,uBAAuB,aACrB;GACF,WAAW,uBAAuB;GAClC,OAAO,uBAAuB;GAC9B,QAAQ,uBAAuB;GAC/B,OAAO,uBAAuB;GAC9B,OAAO,uBAAuB;GAC9B,cAAc,uBAAuB;GACrC,WAAW,uBAAuB;GAClC;GACA,qBAAqB;GACtB;EAED,MAAM,gBAAgB,OAAO,KAC3B,KAAK,SAAS,mBAAmB,cAClC,CAAC;AACF,OAAK,OAAO,MACV;GACE,WAAW,uBAAuB;GAClC,cAAc,uBAAuB;GACrC;GACD,EACD,uBACD;EAED,MAAM,YAAY,KAAK,KAAK;EAG5B,MAAM,aAAa,KAAK;EACxB,MAAM,uBAAuB,KAAK;EAClC,IAAIC;AACJ,wBAAsB,kBAAkB;GACtC,MAAM,iBACJ,KAAK,SAAS,mBAAmB,cAC/B,uBAAuB;AAE3B,OAAI,CAAC,gBAAgB;AACnB,kBAAc,oBAAoB;AAClC;;GAMF,MAAM,aAAa;IACjB,IAAI,KAAK,SAAS,kBAAkB,MAAM;IAC1C,IAAI,KAAK,SAAS,kBAAkB,MAAM;IAC3C;AAED,QAAK,OAAO,MACV;IACE,cAAc,WAAW;IACzB,SAAS;IACT,WAAW,uBAAuB;IAClC,cAAc,uBAAuB;IACrC,OAAO,uBAAuB;IAC9B,QAAQ,uBAAuB;IAChC,EACD,kBACD;AAED,OAAI,WAAW,GAAG,eAAe,UAAU,MAAM;AAC/C,SAAK,OAAO,KACV;KACE,cAAc,WAAW;KACzB,WAAW,uBAAuB;KACnC,EACD,mDACD;AACD;;AAGF,OAAI;AACF,eAAW,GAAG,KACZJ,yCACEC,+BAAe,OACbA,+BAAe,OAAO;KACpB,MAAMC,mCAAmB;KACzB,SAASG,6CAA6B,OACpCA,6CAA6B,OAAO;MAClC,WAAW,uBAAuB;MAClC,OAAO,uBAAuB;MAC9B,OAAO,uBAAuB;MAC9B,cAAc,uBAAuB;MACrC,WAAW,uBAAuB;MAClC,QAAQ,uBAAuB;MAC/B,OAAO,uBAAuB;MAC9B,cAAc,uBAAuB;MACrC,gBAAgB,uBAAuB;MACvC,SAAS;MACV,CAAC,CACH,CAAC,QAAQ;KACX,CAAC,CACH,CAAC,QAAQ,CACX,CACF;IACD,MAAM,OACJ,KAAK,SAAS,mBAAmB,YAC/B,uBAAuB;AAE3B,QAAI,KAAM,MAAK,sBAAsB,KAAK,KAAK;YACxC,KAAK;AACZ,SAAK,OAAO,KACV;KACE,cAAc,WAAW;KACzB,WAAW,uBAAuB;KAClC,KAAK,QAAQ,IAAI;KAClB,EACD,iCACD;;KAEF,KAAK,sBAAsB;AAE9B,MAAI;GACF,MAAM,gBAAgB,MAAM,KAAK,UAAU,uBACzC,uBACD;GAED,MAAM,aAAa,KAAK,KAAK,GAAG;AAChC,QAAK,OAAO,MACV;IACE,WAAW,uBAAuB;IAClC,cAAc,uBAAuB;IACrC;IACD,EACD,8BACD;AAED,OAAI,CAAC,KAAK,SAAS,kBAAkB;AACnC,SAAK,OAAO,KACV,EAAE,WAAW,uBAAuB,WAAW,EAC/C,2CACD;AACD,QAAI,KAAK,UAAU,iBACjB,MAAK,UAAU,iBACb,uBAAuB,WACvB,cACD;AAEH;;AAGF,QAAK,OAAO,MACV;IACE,cAAc,KAAK,SAAS,iBAAiB;IAC7C,WAAW,uBAAuB;IACnC,EACD,uBACD;AAED,QAAK,SAAS,iBAAiB,GAAG,KAChCL,yCACEC,+BAAe,OACbA,+BAAe,OAAO;IACpB,MAAMC,mCAAmB;IACzB,SAAS;IACV,CAAC,CACH,CAAC,QAAQ,CACX,CACF;WACM,KAAK;GACZ,MAAM,aAAa,KAAK,KAAK,GAAG;AAChC,QAAK,OAAO,KACV;IACE,WAAW,uBAAuB;IAClC;IACA,KAAK,QAAQ,IAAI;IAClB,EACD,kBACD;YACO;AACR,QAAK,SAAS,mBAAmB,GAAG,MAAM;AAC1C,UAAO,KAAK,SAAS,mBAAmB,cACtC,uBAAuB;AAEzB,UAAO,KAAK,SAAS,mBAAmB,YACtC,uBAAuB;AAEzB,iBAAc,oBAAoB;GAElC,MAAM,oBAAoB,OAAO,KAC/B,KAAK,SAAS,mBAAmB,cAClC,CAAC;AACF,QAAK,OAAO,MACV;IACE,WAAW,uBAAuB;IAClC;IACD,EACD,mBACD;AAGD,OAAI,KAAK,SAAS,qBAAqB,CAAC,KAAK,qBAAqB,CAChE,MAAK,WAAW,KAAKI,4BAAY,0BAA0B;;;;CAMjE,eACE,gBACA,cACM;EACN,MAAM,WAAWC,qCAAoB,eAAe,QAAQ;AAE5D,OAAK,OAAO,MACV;GAAE;GAAc,WAAW,SAAS;GAAW,EAC/C,0BACD;AAED,OAAK,UAAU,aAAa,SAAS,UAAU;;;CAIjD,qBACE,gBACA,cACM;EACN,MAAM,iBAAiBC,gDAAgC,OACrD,eAAe,QAChB;EAMD,MAAM,WAAW,OAAO,OACtB,KAAK,SAAS,mBAAmB,eACjC,eAAe,UAChB;EACD,MAAM,OACJ,KAAK,SAAS,mBAAmB,YAAY,eAAe;AAC9D,MAAI,CAAC,YAAY,CAAC,MAAM;AACtB,QAAK,OAAO,MACV;IACE;IACA,WAAW,eAAe;IAC1B,YAAY,eAAe;IAC3B,UAAU;IACV,SAAS,CAAC,CAAC;IACZ,EACD,sDACD;AACD;;AAGF,OAAK,OAAO,MACV;GAAE;GAAc,YAAY,eAAe;GAAY,EACvD,4BACD;AAED,MAAI,eAAe,WACjB,MAAK,SAAS,mBAAmB,cAAc,eAAe,aAC5D,eAAe;OACZ;AACL,QAAK,OAAO,MACV;IACE;IACA,WAAW,eAAe;IAC1B,cAAc,MAAM;IACpB,OAAO,MAAM;IACb,QAAQ,MAAM;IACf,EACD,gLAGD;AACD,UAAO,KAAK,SAAS,mBAAmB,cACtC,eAAe;AAIjB,UAAO,KAAK,SAAS,mBAAmB,YACtC,eAAe;AAQjB,OAAI,KAAK,SAAS,qBAAqB,CAAC,KAAK,qBAAqB,CAChE,MAAK,WAAW,KAAKF,4BAAY,oBAAoB;;;CAK3D,AAAQ,sBAA+B;AACrC,SACE,OAAO,KAAK,KAAK,SAAS,mBAAmB,cAAc,CAAC,SAAS"}
|