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.
Files changed (103) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/api/api.cjs +1 -1
  3. package/api/api.js +1 -1
  4. package/components/Inngest.cjs +1 -1
  5. package/components/Inngest.d.cts +7 -1
  6. package/components/Inngest.d.cts.map +1 -1
  7. package/components/Inngest.d.ts +7 -1
  8. package/components/Inngest.d.ts.map +1 -1
  9. package/components/Inngest.js +1 -1
  10. package/components/InngestCommHandler.cjs +60 -28
  11. package/components/InngestCommHandler.cjs.map +1 -1
  12. package/components/InngestCommHandler.d.cts.map +1 -1
  13. package/components/InngestCommHandler.d.ts.map +1 -1
  14. package/components/InngestCommHandler.js +60 -28
  15. package/components/InngestCommHandler.js.map +1 -1
  16. package/components/InngestStepTools.cjs +3 -7
  17. package/components/InngestStepTools.cjs.map +1 -1
  18. package/components/InngestStepTools.d.cts +10 -7
  19. package/components/InngestStepTools.d.cts.map +1 -1
  20. package/components/InngestStepTools.d.ts +10 -7
  21. package/components/InngestStepTools.d.ts.map +1 -1
  22. package/components/InngestStepTools.js +3 -7
  23. package/components/InngestStepTools.js.map +1 -1
  24. package/components/connect/strategies/core/connection.cjs +91 -11
  25. package/components/connect/strategies/core/connection.cjs.map +1 -1
  26. package/components/connect/strategies/core/connection.js +91 -11
  27. package/components/connect/strategies/core/connection.js.map +1 -1
  28. package/components/connect/strategies/core/heartbeat.cjs +2 -1
  29. package/components/connect/strategies/core/heartbeat.cjs.map +1 -1
  30. package/components/connect/strategies/core/heartbeat.js +2 -1
  31. package/components/connect/strategies/core/heartbeat.js.map +1 -1
  32. package/components/connect/strategies/core/requestProcessor.cjs +27 -4
  33. package/components/connect/strategies/core/requestProcessor.cjs.map +1 -1
  34. package/components/connect/strategies/core/requestProcessor.js +27 -4
  35. package/components/connect/strategies/core/requestProcessor.js.map +1 -1
  36. package/components/connect/strategies/core/types.cjs +21 -0
  37. package/components/connect/strategies/core/types.cjs.map +1 -0
  38. package/components/connect/strategies/core/types.js +20 -0
  39. package/components/connect/strategies/core/types.js.map +1 -0
  40. package/components/connect/types.cjs.map +1 -1
  41. package/components/connect/types.d.cts +18 -0
  42. package/components/connect/types.d.cts.map +1 -1
  43. package/components/connect/types.d.ts +18 -0
  44. package/components/connect/types.d.ts.map +1 -1
  45. package/components/connect/types.js.map +1 -1
  46. package/components/execution/engine.cjs +1 -1
  47. package/components/execution/engine.js +1 -1
  48. package/components/execution/otel/middleware.d.cts +12 -6
  49. package/components/execution/otel/middleware.d.cts.map +1 -1
  50. package/components/execution/otel/middleware.d.ts +12 -6
  51. package/components/execution/otel/middleware.d.ts.map +1 -1
  52. package/components/middleware/manager.cjs +1 -1
  53. package/components/middleware/manager.cjs.map +1 -1
  54. package/components/middleware/manager.js +2 -2
  55. package/components/middleware/manager.js.map +1 -1
  56. package/components/middleware/utils.cjs +4 -3
  57. package/components/middleware/utils.cjs.map +1 -1
  58. package/components/middleware/utils.js +4 -3
  59. package/components/middleware/utils.js.map +1 -1
  60. package/components/realtime/types.d.cts +4 -4
  61. package/components/realtime/types.d.cts.map +1 -1
  62. package/components/realtime/types.d.ts +4 -4
  63. package/components/realtime/types.d.ts.map +1 -1
  64. package/helpers/consts.cjs +1 -0
  65. package/helpers/consts.cjs.map +1 -1
  66. package/helpers/consts.d.cts +1 -0
  67. package/helpers/consts.d.cts.map +1 -1
  68. package/helpers/consts.d.ts +1 -0
  69. package/helpers/consts.d.ts.map +1 -1
  70. package/helpers/consts.js +1 -0
  71. package/helpers/consts.js.map +1 -1
  72. package/helpers/env.cjs +2 -1
  73. package/helpers/env.cjs.map +1 -1
  74. package/helpers/env.js +2 -1
  75. package/helpers/env.js.map +1 -1
  76. package/helpers/net.cjs +3 -2
  77. package/helpers/net.cjs.map +1 -1
  78. package/helpers/net.js +3 -2
  79. package/helpers/net.js.map +1 -1
  80. package/helpers/strings.cjs +15 -5
  81. package/helpers/strings.cjs.map +1 -1
  82. package/helpers/strings.d.cts.map +1 -1
  83. package/helpers/strings.d.ts.map +1 -1
  84. package/helpers/strings.js +15 -6
  85. package/helpers/strings.js.map +1 -1
  86. package/helpers/temporal.cjs +2 -0
  87. package/helpers/temporal.d.ts +3 -0
  88. package/helpers/temporal.d.ts.map +1 -1
  89. package/helpers/temporal.js +1 -1
  90. package/package.json +1 -1
  91. package/react.d.cts.map +1 -1
  92. package/types.cjs.map +1 -1
  93. package/types.d.cts +5 -4
  94. package/types.d.cts.map +1 -1
  95. package/types.d.ts +5 -4
  96. package/types.d.ts.map +1 -1
  97. package/types.js.map +1 -1
  98. package/version.cjs +1 -1
  99. package/version.cjs.map +1 -1
  100. package/version.d.cts +1 -1
  101. package/version.d.ts +1 -1
  102. package/version.js +1 -1
  103. 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.wakeSignal.resolve();
173
- this.resetWakeSignal();
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 conn.ws.send(ensureUnsharedArrayBuffer(ConnectMessage.encode(ConnectMessage.create({ kind: GatewayMessageType.WORKER_READY })).finish()));
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkGA,IAAa,iBAAb,MAA4B;CAC1B,AAAQ;CACR,AAAQ;CAGR,AAAQ;CACR,AAAQ;CACR,AAAQ,qBAAqB;CAC7B,AAAQ,sBAIJ;EACF,IAAI,IAAI,WAAW;EACnB,eAAe,EAAE;EACjB,aAAa,EAAE;EAChB;CAED,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,AAAQ,kCAA+B,IAAI,KAAK;CAGhD,AAAQ;CAIR,AAAQ,qBAAqB;CAG7B,AAAQ;CAGR,AAAQ;CACR,AAAQ;CAGR,AAAQ;CAGR,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YACE,QACA,WACA;AACA,OAAK,SAAS;AACd,OAAK,YAAY;AACjB,OAAK,gBAAgB,OAAO;EAG5B,IAAIA;AAIJ,OAAK,aAAa;GAAE,SAHJ,IAAI,SAAe,MAAM;AACvC,cAAU;KACV;GACoC;GAAU;EAGhD,MAAM,WAAW;GACf,IAAI,mBAAmB;AACrB,WAAO,KAAK;;GAEd,IAAI,qBAAqB;AACvB,WAAO,KAAK;;GAEd,IAAI,oBAAoB;AACtB,WAAO,KAAK;;GAEd,IAAI,qBAAqB;AACvB,WAAO,KAAK;;GAEd,IAAI,SAAS;AACX,WAAO,KAAK,OAAO;;GAEtB;EAED,MAAM,gBAAgB,EAAE,YAAY,KAAK,MAAM,EAAE;EAEjD,MAAM,OAAO;AAEb,OAAK,mBAAmB,IAAI,iBAC1B,UACA,eACA,UAAU,OACX;AACD,OAAK,iBAAiB,wBAAwB;AAC5C,QAAK,uBAAuB,KAAK,KAAK;;AAGxC,OAAK,iBAAiB,IAAI,eAAe,UAAU,UAAU,OAAO;AAEpE,OAAK,mBAAmB,IAAI,iBAC1B,UACA,eACA,WACA,UAAU,OACX;;CAGH,IAAI,eAAmC;AACrC,SAAO,KAAK,mBAAmB;;;;;CAMjC,MAAM,oBAAmC;AACvC,QAAM,KAAK,oBAAoB,GAAG,MAAM;;;;;CAM1C,gBAAmC;AACjC,SAAO;GACL,OAAO,KAAK,UAAU,UAAU;GAChC,oBAAoB,KAAK,mBAAmB;GAC5C,sBAAsB,KAAK,qBAAqB;GAChD,qBAAqB,KAAK;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB,KAAK;GAC5B,mBAAmB,KAAK;GACxB,sBAAsB,OAAO,KAAK,KAAK,oBAAoB,cAAc,CACtE;GACH,kBAAkB,OAAO,OAAO,KAAK,oBAAoB,YAAY;GACtE;;;;;;CAOH,MAAM,MAAM,UAAU,GAAkB;AACtC,MAAI,OAAO,cAAc,YACvB,OAAM,IAAI,MAAM,kDAAkD;AAIpE,MADc,KAAK,UAAU,UAAU,KACzB,gBAAgB,OAC5B,OAAM,IAAI,MAAM,4BAA4B;EAG9C,MAAM,oBAAoB,IAAI,SAAe,SAAS,WAAW;AAC/D,QAAK,oBAAoB;AACzB,QAAK,mBAAmB;IACxB;AAEF,OAAK,cAAc,KAAK,cAAc,QAAQ;AAG9C,OAAK,YAAY,OAAO,QAAQ;AAC9B,QAAK,mBAAmB,IAAI;IAC5B;AAEF,QAAM;;;;;;CAOR,MAAM,QAAuB;EAC3B,MAAM,gBAAgB,OAAO,KAC3B,KAAK,oBAAoB,cAC1B,CAAC;AACF,OAAK,UAAU,OAAO,KACpB,EAAE,eAAe,EACjB,gDACD;AACD,OAAK,qBAAqB;AAE1B,MAAI,KAAK,mBAAmB,GAAG,eAAe,UAAU,MAAM;AAC5D,QAAK,kBAAkB,GAAG,KACxB,0BACE,eAAe,OACb,eAAe,OAAO,EACpB,MAAM,mBAAmB,cAC1B,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AACD,QAAK,UAAU,OAAO,KACpB,EAAE,cAAc,KAAK,kBAAkB,IAAI,EAC3C,8BACD;;AAGH,OAAK,MAAM;AAEX,MAAI,KAAK,YACP,OAAM,KAAK;AAGb,OAAK,UAAU,OAAO,KAAK,oBAAoB;;CAGjD,MAAM,gBAAiC;AACrC,SAAO,kBAAkB;GACvB,YAAY,KAAK,OAAO;GACxB,MAAM,KAAK,OAAO;GACnB,CAAC;;CAOJ,AAAQ,kBAAwB;EAC9B,IAAIA;AAIJ,OAAK,aAAa;GAAE,SAHJ,IAAI,SAAe,MAAM;AACvC,cAAU;KACV;GACoC;GAAU;;CAGlD,AAAQ,OAAa;AACnB,OAAK,WAAW,SAAS;AACzB,OAAK,iBAAiB;;CAOxB,AAAQ,gBAAsB;EAC5B,MAAM,mBACJ,KAAK,kBAAkB,KAAK,OAAO;AACrC,MAAI,iBACF,MAAK,UAAU,OAAO,MAAM,oCAAoC;AAElE,OAAK,gBAAgB,mBACjB,KAAK,OAAO,oBACZ,KAAK,OAAO;;CAOlB,AAAQ,sBAA+B;AACrC,SAAO,OAAO,KAAK,KAAK,oBAAoB,cAAc,CAAC,SAAS;;CAOtE,MAAc,cAAc,gBAAuC;EACjE,IAAI,UAAU;AAEd,SAAO,MAAM;AAEX,OAAI,KAAK,sBAAsB,CAAC,KAAK,qBAAqB,CACxD;AAIF,OAAI,CAAC,KAAK,qBAAqB,KAAK,kBAAkB,MAAM;AAC1D,SAAK,UAAU,OAAO,MACpB;KACE,qBAAqB,CAAC,CAAC,KAAK;KAC5B,sBAAsB,KAAK,mBAAmB;KAC9C,uBAAuB,CAAC,CAAC,KAAK;KAC9B,sBAAsB,KAAK,qBAAqB;KACjD,EACD,uBACD;AAED,QAAI,KAAK,mBACP,MAAK,UAAU,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe;QAEvD,MAAK,UAAU,OAAO,KAAK,aAAa;AAG1C,SAAK,UAAU,cACb,KAAK,qBACD,gBAAgB,eAChB,gBAAgB,WACrB;AAED,QAAI;KACF,MAAM,EAAE,MAAM,iBAAiB,MAAM,oBACnC,KAAK,QACL,KAAK,eACL,SACA,KAAK,iBACL,KAAK,UAAU,OAChB;AAGD,UAAK,eAAe,MAAM,aAAa;AAGvC,SAAI,KAAK,qBAAqB;AAC5B,WAAK,UAAU,OAAO,KACpB;OACE,iBAAiB,KAAK,oBAAoB;OAC1C,iBAAiB,KAAK;OACvB,EACD,+BACD;AACD,WAAK,oBAAoB,OAAO;AAChC,WAAK,sBAAsB;;AAG7B,UAAK,oBAAoB;AACzB,UAAK,iBAAiB,eAAe,KAAK,oBAAoB;AAC9D,UAAK,eAAe,eAAe,KAAK,iBAAiB;AACzD,eAAU;AACV,UAAK,qBAAqB;AAC1B,UAAK,UAAU,OAAO,KACpB;MAAE,cAAc,KAAK;MAAI;MAAc,EACvC,oBACD;AACD,UAAK,UAAU,cAAc,gBAAgB,OAAO;AAEpD,SAAI,KAAK,oBAAoB;AAG3B,WAAK,GAAG,KACN,0BACE,eAAe,OACb,eAAe,OAAO,EACpB,MAAM,mBAAmB,cAC1B,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AACD,WAAK,UAAU,OAAO,KACpB,EAAE,cAAc,KAAK,IAAI,EACzB,iDACD;WAKD,MAAK,GAAG,KACN,0BACE,eAAe,OACb,eAAe,OAAO,EACpB,MAAM,mBAAmB,cAC1B,CAAC,CACH,CAAC,QAAQ,CACX,CACF;AAIH,UAAK,UAAU,qBAAqB,KAAK,cAAc;AAEvD,UAAK,qBAAqB;AAC1B,UAAK,oBAAoB;AACzB,UAAK,mBAAmB;aACjB,KAAK;AACZ,SAAI,EAAE,eAAe,gBAAiB,OAAM;AAE5C,eAAU,IAAI,UAAU;AACxB,SAAI,eAAe,UAAW,MAAK,eAAe;AAClD,SAAI,eAAe,qBACjB,MAAK,UAAU,OAAO,MAAM,qCAAqC;AAInE,SAAI,IAAI,SAAS,SAAS,0BAA0B,EAAE;MACpD,MAAM,SAAS,MAAM,KAAK,QAAQ,GAAG;AACrC,WAAK,UAAU,OAAO,KACpB;OAAE;OAAS,OAAO,KAAK,MAAM,OAAO;OAAE,OAAO,IAAI;OAAS,EAC1D,6BACD;AAID,UAHkB,MAAM,eAAe,cAAc;AACnD,cAAO,KAAK,sBAAsB,CAAC,KAAK,qBAAqB;QAC7D,CACa;AACf;;KAGF,MAAM,QAAQ,WAAW,QAAQ;AACjC,UAAK,UAAU,OAAO,KACpB;MAAE;MAAS;MAAO,EAClB,6BACD;AAKD,SAHkB,MAAM,eAAe,aAAa;AAClD,aAAO,KAAK,sBAAsB,CAAC,KAAK,qBAAqB;OAC7D,CACa;AACf;;;AAKJ,SAAM,KAAK,WAAW;AACtB,QAAK,UAAU,OAAO,MACpB;IACE,mBAAmB,KAAK;IACxB,qBAAqB,CAAC,CAAC,KAAK;IAC5B,sBAAsB,KAAK,mBAAmB;IAC/C,EACD,uBACD;;AAIH,OAAK,iBAAiB,MAAM;AAC5B,OAAK,eAAe,MAAM;AAC1B,OAAK,mBAAmB,OAAO;AAC/B,OAAK,oBAAoB;AACzB,OAAK,qBAAqB,OAAO;AACjC,OAAK,sBAAsB;;;;;CAU7B,AAAQ,eAAe,MAAkB,cAA4B;EACnE,MAAM,EAAE,OAAO;EACf,MAAM,eAAe,KAAK;AAG1B,KAAG,WAAW,OAAO;AACnB,OAAI,KAAK,KAAM;GACf,MAAM,WAAW,KAAK,KAAK,GAAG,KAAK;AACnC,QAAK,UAAU,OAAO,KACpB;IACE;IACA;IACA;IACA,OAAQ,IAAmB;IAC5B,EACD,0BACD;AACD,QAAK,OAAO;AACZ,QAAK,gBAAgB,IAAI,aAAa;AACtC,OAAI,KAAK,mBAAmB,OAAO,aACjC,MAAK,oBAAoB;AAE3B,QAAK,MAAM;;AAGb,KAAG,WAAW,OAAO;AACnB,OAAI,KAAK,KAAM;GACf,MAAM,WAAW,KAAK,KAAK,GAAG,KAAK;AACnC,QAAK,UAAU,OAAO,KACpB;IACE;IACA;IACA;IACA,MAAM,GAAG;IACT,QAAQ,GAAG;IACZ,EACD,0BACD;AACD,QAAK,OAAO;AACZ,QAAK,gBAAgB,IAAI,aAAa;AACtC,OAAI,KAAK,mBAAmB,OAAO,aACjC,MAAK,oBAAoB;AAE3B,QAAK,MAAM;;AAIb,KAAG,YAAY,OAAO,UAAU;AAC9B,QAAK,yBAAyB,KAAK,KAAK;GAGxC,MAAM,iBAAiB,oBADF,IAAI,WAAW,MAAM,KAAoB,CACN;AAExD,OAAI,eAAe,SAAS,mBAAmB,iBAAiB;IAC9D,MAAM,WAAW,KAAK,KAAK,GAAG,KAAK;AACnC,SAAK,UAAU,OAAO,KACpB;KAAE,cAAc,KAAK;KAAI;KAAc;KAAU,EACjD,2CACD;AAGD,SAAK,sBAAsB,KAAK;AAChC,SAAK,oBAAoB;AACzB,SAAK,MAAM;AACX;;AAGF,OAAI,eAAe,SAAS,mBAAmB,mBAAmB;AAChE,SAAK,2BAA2B,KAAK,KAAK;AAC1C,SAAK,oBAAoB;AACzB,SAAK,UAAU,OAAO,MACpB,EAAE,cAAc,EAChB,4BACD;AACD;;AAGF,OAAI,eAAe,SAAS,mBAAmB,0BAA0B;AACvE,UAAM,KAAK,iBAAiB,sBAAsB,gBAAgB,KAAK;AACvE;;AAGF,OAAI,eAAe,SAAS,mBAAmB,kBAAkB;AAC/D,SAAK,iBAAiB,eAAe,gBAAgB,aAAa;AAClE;;AAGF,OACE,eAAe,SACf,mBAAmB,iCACnB;AACA,SAAK,iBAAiB,qBACpB,gBACA,aACD;AACD;;AAGF,QAAK,UAAU,OAAO,KACpB;IACE,MAAM,yBAAyB,eAAe,KAAK;IACnD,SAAS,eAAe;IACxB,OAAO,KAAK,UAAU,UAAU;IAChC;IACD,EACD,0BACD"}
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 { ConnectionAccessor, WakeSignal } 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();\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":";;;;AAgBA,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,MAAM;AACtB;;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
+ {"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 { ConnectionAccessor, WakeSignal } 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();\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":";;;;AAgBA,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,MAAM;AACtB;;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"}
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":"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"}