@kronos-ts/axon-server 0.5.0 → 0.7.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 (68) hide show
  1. package/dist/axon-server-event-store.d.ts.map +1 -1
  2. package/dist/axon-server-event-store.js +72 -77
  3. package/dist/axon-server-event-store.js.map +1 -1
  4. package/dist/axon-server-snapshotting-event-store.d.ts +3 -3
  5. package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -1
  6. package/dist/axon-server-snapshotting-event-store.js +3 -2
  7. package/dist/axon-server-snapshotting-event-store.js.map +1 -1
  8. package/dist/axon-server.d.ts +15 -7
  9. package/dist/axon-server.d.ts.map +1 -1
  10. package/dist/axon-server.js +447 -275
  11. package/dist/axon-server.js.map +1 -1
  12. package/dist/bounded-read.d.ts +19 -0
  13. package/dist/bounded-read.d.ts.map +1 -0
  14. package/dist/bounded-read.js +39 -0
  15. package/dist/bounded-read.js.map +1 -0
  16. package/dist/connection.d.ts +13 -0
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +78 -40
  19. package/dist/connection.js.map +1 -1
  20. package/dist/control-plane.d.ts +9 -19
  21. package/dist/control-plane.d.ts.map +1 -1
  22. package/dist/control-plane.js +6 -37
  23. package/dist/control-plane.js.map +1 -1
  24. package/dist/event-processor-info.d.ts +5 -23
  25. package/dist/event-processor-info.d.ts.map +1 -1
  26. package/dist/event-processor-info.js +16 -20
  27. package/dist/event-processor-info.js.map +1 -1
  28. package/dist/flow-controlled-sender.d.ts.map +1 -1
  29. package/dist/flow-controlled-sender.js +37 -17
  30. package/dist/flow-controlled-sender.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/outbound-stream.d.ts +5 -9
  35. package/dist/outbound-stream.d.ts.map +1 -1
  36. package/dist/outbound-stream.js +65 -16
  37. package/dist/outbound-stream.js.map +1 -1
  38. package/dist/platform-service.d.ts +10 -1
  39. package/dist/platform-service.d.ts.map +1 -1
  40. package/dist/platform-service.js +70 -10
  41. package/dist/platform-service.js.map +1 -1
  42. package/dist/resilience.d.ts +2 -0
  43. package/dist/resilience.d.ts.map +1 -1
  44. package/dist/resilience.js +2 -1
  45. package/dist/resilience.js.map +1 -1
  46. package/dist/shutdown-latch.d.ts +1 -0
  47. package/dist/shutdown-latch.d.ts.map +1 -1
  48. package/dist/shutdown-latch.js +20 -1
  49. package/dist/shutdown-latch.js.map +1 -1
  50. package/dist/stream-recovery.d.ts +9 -0
  51. package/dist/stream-recovery.d.ts.map +1 -0
  52. package/dist/stream-recovery.js +70 -0
  53. package/dist/stream-recovery.js.map +1 -0
  54. package/package.json +2 -2
  55. package/src/axon-server-event-store.ts +77 -79
  56. package/src/axon-server-snapshotting-event-store.ts +10 -7
  57. package/src/axon-server.ts +417 -316
  58. package/src/bounded-read.ts +43 -0
  59. package/src/connection.ts +77 -45
  60. package/src/control-plane.ts +19 -63
  61. package/src/event-processor-info.ts +22 -43
  62. package/src/flow-controlled-sender.ts +33 -14
  63. package/src/index.ts +0 -1
  64. package/src/outbound-stream.ts +57 -27
  65. package/src/platform-service.ts +75 -11
  66. package/src/resilience.ts +6 -2
  67. package/src/shutdown-latch.ts +14 -1
  68. package/src/stream-recovery.ts +72 -0
@@ -1,3 +1,5 @@
1
+ import { streamRecovery } from "./stream-recovery.js";
2
+ import { messagingAdmission, messagingDeadline, positiveInteger } from "@kronos-ts/core";
1
3
  /**
2
4
  * The Axon Server command and query buses.
3
5
  *
@@ -15,7 +17,7 @@
15
17
  * axonServerQueryBus(localQueryBus(unitOfWork), axon), correlation)
16
18
  * ```
17
19
  *
18
- * Axon-specific protocol invariants are preserved byte-for-byte:
20
+ * Axon-specific protocol invariants:
19
21
  *
20
22
  * - CLIENT_SUPPORTS_STREAMING capability advertised on every dispatched
21
23
  * query via `defaultQueryInstructions(...)`;
@@ -26,7 +28,7 @@
26
28
  * `reestablishStreamBody`).
27
29
  */
28
30
  import { qualifiedNameToString, qualifiedNameFromString, generateIdentifier, } from "@kronos-ts/core";
29
- import { withRetry } from "./resilience.js";
31
+ import {} from "./resilience.js";
30
32
  import { applySubscriptionFilter, updateHandler, runAfterCommitOrImmediately, } from "@kronos-ts/core";
31
33
  import { contextView } from "./context-view.js";
32
34
  import { metadataToProto, metadataFromProto } from "./metadata-conversion.js";
@@ -34,9 +36,8 @@ import { outboundStream } from "./outbound-stream.js";
34
36
  import { mapErrorCode, AxonServerErrorCode } from "./errors.js";
35
37
  /** Default flow control settings — aligned with Java's 5000 permits. */
36
38
  const DEFAULT_PERMITS = 5000n;
37
- const DEFAULT_THRESHOLD = 2500n;
38
39
  /** Default query dispatch timeout — aligned with Java's one hour. */
39
- const DEFAULT_QUERY_TIMEOUT_MS = 3_600_000;
40
+ const DEFAULT_QUERY_TIMEOUT_MS = 30_000;
40
41
  /** Default command handler load factor — aligned with Java's 100. */
41
42
  const DEFAULT_LOAD_FACTOR = 100;
42
43
  // Processing instruction keys — aligned with proto ProcessingKey enum.
@@ -146,11 +147,18 @@ function createPayloadHelpers(serializer) {
146
147
  export function axonServerCommandBus(next, conn, options = {}) {
147
148
  const { connection, serializer, metadata: axonMetadata, } = contextView(conn, options.context ?? conn.connection.config.context);
148
149
  const shutdownLatch = conn.shutdown;
150
+ const requestTimeoutMs = positiveInteger(options.timeoutMs ?? 30000, "timeoutMs");
151
+ if (requestTimeoutMs > 2_147_483_647)
152
+ throw new RangeError("timeoutMs exceeds the timer range");
153
+ const inboundAdmission = messagingAdmission("inbound handlers", options.limits?.maxConcurrentHandlers ?? 128, options.limits?.observe);
154
+ const outboundAdmission = messagingAdmission("pending requests", options.limits?.maxPendingRequests ?? 1024, options.limits?.observe);
149
155
  const resilience = options.resilience ?? conn.resilience;
150
156
  const metadata = axonMetadata();
151
157
  const { serializePayload, deserializePayload } = createPayloadHelpers(serializer);
152
- const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS));
153
- const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
158
+ const PERMITS = BigInt(positiveInteger(options.flowControl?.permits ?? Number(DEFAULT_PERMITS), "flowControl.permits"));
159
+ const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Math.floor(Number(PERMITS) / 2));
160
+ if (THRESHOLD < 0n || THRESHOLD >= PERMITS)
161
+ throw new RangeError("refillThreshold must be between zero and permits - 1");
154
162
  const loadFactor = options.loadFactor ?? DEFAULT_LOAD_FACTOR;
155
163
  /**
156
164
  * The names this node announced to Axon Server. The handlers themselves live
@@ -162,14 +170,16 @@ export function axonServerCommandBus(next, conn, options = {}) {
162
170
  // Bidirectional stream for handler subscription + inbound command handling
163
171
  let outbound = outboundStream();
164
172
  let streamStarted = false;
173
+ let providerAbort = new AbortController();
174
+ connection.onDisconnect?.(() => { providerAbort.abort(); outbound.close(); });
165
175
  let permits = 0n;
166
176
  function ensureStreamStarted() {
167
177
  if (streamStarted)
168
178
  return;
169
179
  streamStarted = true;
170
180
  // Open stream using connection.commands (always gets current client after reconnect)
171
- const inbound = connection.commands.openStream(outbound.iterable, { metadata });
172
- processInboundCommands(inbound);
181
+ const inbound = connection.commands.openStream(outbound.iterable, { metadata, signal: providerAbort.signal });
182
+ void processInboundCommands(inbound, outbound);
173
183
  }
174
184
  function grantPermits() {
175
185
  outbound.send({
@@ -199,6 +209,8 @@ export function axonServerCommandBus(next, conn, options = {}) {
199
209
  * trigger a server-side stream error.
200
210
  */
201
211
  function reestablishStreamBody() {
212
+ providerAbort.abort();
213
+ providerAbort = new AbortController();
202
214
  outbound.close();
203
215
  outbound = outboundStream();
204
216
  streamStarted = false;
@@ -210,98 +222,106 @@ export function axonServerCommandBus(next, conn, options = {}) {
210
222
  // Permits AFTER subscriptions (Axon-specific ordering invariant)
211
223
  grantPermits();
212
224
  }
213
- async function reestablishStreamWithRetry() {
214
- if (shutdownLatch.shuttingDown)
215
- return;
216
- await withRetry(async () => reestablishStreamBody(), {
217
- event: "reconnect",
218
- ...resilience,
219
- });
220
- }
225
+ const recovery = streamRecovery(reestablishStreamBody, () => !shutdownLatch.shuttingDown && connection.state !== "closed" && connection.state !== "disconnected" && connection.state !== "reconnecting", resilience);
226
+ shutdownLatch.onShutdown(recovery.stop);
221
227
  // Auto-reestablish when the connection reconnects (e.g., after heartbeat timeout)
222
228
  connection.onReconnect(() => {
223
229
  if (!shutdownLatch.shuttingDown && streamStarted) {
224
- reestablishStreamWithRetry().catch((err) => {
225
- console.error("Axon Server command bus: reconnect retries exhausted", err);
226
- });
230
+ recovery.restart();
227
231
  }
228
232
  });
229
- async function processInboundCommands(inbound) {
233
+ async function handleInboundCommand(proto, responses) {
234
+ let activity;
235
+ let admission;
236
+ let responseSerialized;
237
+ let errorCode = "";
238
+ let errorMsg = "";
230
239
  try {
231
- for await (const message of inbound) {
232
- if (!message.command)
233
- continue;
234
- permits--;
235
- const proto = message.command;
236
- const commandName = proto.name;
237
- let resultPayload;
238
- let errorCode = "";
239
- let errorMsg = "";
240
- if (subscribedNames.has(commandName)) {
241
- try {
242
- const commandMessage = {
243
- kind: "command",
244
- identifier: proto.messageIdentifier,
245
- name: qualifiedNameFromString(commandName),
246
- payload: deserializePayload(proto.payload?.data),
247
- metadata: metadataFromProto(proto.metaData),
248
- timestamp: Number(proto.timestamp),
249
- };
250
- // Through the LOCAL BUS, so the caller's unit-of-work policy runs.
251
- // AF parity is preserved: `CommandProcessingTask` runs the next
252
- // segment without re-running dispatch interceptors, and a `next`
253
- // that happens to carry `correlation` re-applies a pair of `??` seeds
254
- // that are already set.
255
- resultPayload = await next.dispatch(commandMessage);
256
- }
257
- catch (err) {
258
- errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR;
259
- errorMsg = err instanceof Error ? err.message : String(err);
260
- }
240
+ try {
241
+ // Remote callers have no outbound dispatch activity on this adapter.
242
+ // Track the entire handling, including result serialization and enqueue.
243
+ // Registration also rejects new work once shutdown has begun.
244
+ activity = shutdownLatch.registerActivity();
245
+ admission = inboundAdmission.enter();
246
+ if (subscribedNames.has(proto.name)) {
247
+ const commandMessage = {
248
+ kind: "command",
249
+ identifier: proto.messageIdentifier,
250
+ name: qualifiedNameFromString(proto.name),
251
+ payload: deserializePayload(proto.payload?.data, proto.payload?.type, proto.payload?.revision),
252
+ metadata: metadataFromProto(proto.metaData ?? {}),
253
+ timestamp: Number(proto.timestamp),
254
+ };
255
+ // The local bus opens a fresh unit of work for EVERY wire command,
256
+ // including children of handlers running on this same connection.
257
+ const result = await next.dispatch(commandMessage);
258
+ responseSerialized = result !== undefined ? serializePayload("result", result) : undefined;
261
259
  }
262
260
  else {
263
261
  errorCode = AxonServerErrorCode.NO_HANDLER_FOR_COMMAND;
264
- errorMsg = `No next handler for command "${commandName}"`;
262
+ errorMsg = `No next handler for command "${proto.name}"`;
265
263
  }
266
- // Send response back to Axon Server
267
- outbound.send({
268
- commandResponse: {
269
- messageIdentifier: generateIdentifier(),
270
- requestIdentifier: proto.messageIdentifier,
271
- errorCode,
272
- errorMessage: errorCode
273
- ? {
274
- message: errorMsg,
275
- location: connection.config.componentName,
276
- details: [],
277
- errorCode,
278
- }
279
- : undefined,
280
- payload: resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
281
- metaData: {},
282
- processingInstructions: [],
283
- },
284
- instructionId: "",
264
+ }
265
+ catch (err) {
266
+ // Decode, handler, and result-encoding failures belong to this request;
267
+ // none should terminate the receive loop or reconnect the stream.
268
+ errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR;
269
+ errorMsg = err instanceof Error ? err.message : String(err);
270
+ }
271
+ // Capture the originating stream: a late handler must not send an old
272
+ // request's response on a replacement stream after reconnect.
273
+ responses.send({
274
+ commandResponse: {
275
+ messageIdentifier: generateIdentifier(),
276
+ requestIdentifier: proto.messageIdentifier,
277
+ errorCode,
278
+ errorMessage: errorCode
279
+ ? { message: errorMsg, location: connection.config.componentName, details: [], errorCode }
280
+ : undefined,
281
+ payload: responseSerialized,
282
+ metaData: {},
283
+ processingInstructions: [],
284
+ },
285
+ instructionId: "",
286
+ });
287
+ await responses.flush();
288
+ }
289
+ finally {
290
+ admission?.end();
291
+ activity?.end();
292
+ }
293
+ }
294
+ async function processInboundCommands(inbound, responses) {
295
+ try {
296
+ for await (const message of inbound) {
297
+ if (responses !== outbound)
298
+ return;
299
+ recovery.received();
300
+ if (message.instructionId)
301
+ responses.send({ ack: { instructionId: message.instructionId, success: true }, instructionId: "" });
302
+ permits--;
303
+ // Credits bound delivery batches, not unfinished handlers. Replenish
304
+ // on receipt: completion-based credits or a fixed handler semaphore can
305
+ // deadlock when every admitted parent is waiting for a queued child.
306
+ if (permits <= THRESHOLD && !shutdownLatch.shuttingDown)
307
+ grantPermits();
308
+ if (!message.command)
309
+ continue;
310
+ // Each invocation owns its response and shutdown activity. Keep reading
311
+ // while it awaits work so nested dispatch can return on this connection.
312
+ void handleInboundCommand(message.command, responses).catch((err) => {
313
+ console.error("Axon Server command bus: inbound response failed", err);
285
314
  });
286
- // Refill permits when running low
287
- if (permits <= THRESHOLD) {
288
- outbound.send({
289
- flowControl: { clientId: connection.config.clientId, permits: PERMITS },
290
- instructionId: "",
291
- });
292
- permits += PERMITS;
293
- }
294
315
  }
316
+ if (responses === outbound && !shutdownLatch.shuttingDown)
317
+ throw new Error("Inbound provider stream ended unexpectedly");
295
318
  }
296
319
  catch (err) {
297
- if (shutdownLatch.shuttingDown)
320
+ if (responses !== outbound || shutdownLatch.shuttingDown)
298
321
  return;
299
- if (String(err).includes("Connection dropped"))
322
+ if (connection.state === "reconnecting" || connection.state === "closed" || connection.state === "disconnected")
300
323
  return;
301
- console.error("Axon Server command bus: inbound stream error, attempting re-establishment via withRetry", err);
302
- await reestablishStreamWithRetry().catch((retryErr) => {
303
- console.error("Axon Server command bus: reconnect retries exhausted", retryErr);
304
- });
324
+ recovery.failed(err);
305
325
  }
306
326
  }
307
327
  return {
@@ -313,7 +333,11 @@ export function axonServerCommandBus(next, conn, options = {}) {
313
333
  // instead, so the task that handles it supplies the instant.
314
334
  const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
315
335
  const activity = shutdownLatch.registerActivity();
336
+ let admission;
337
+ let deadline;
316
338
  try {
339
+ admission = outboundAdmission.enter(unstamped.identifier);
340
+ deadline = messagingDeadline(requestTimeoutMs);
317
341
  const commandName = qualifiedNameToString(message.name);
318
342
  const response = await connection.commands.dispatch({
319
343
  messageIdentifier: message.identifier,
@@ -324,13 +348,15 @@ export function axonServerCommandBus(next, conn, options = {}) {
324
348
  processingInstructions: toProtoProcessingInstructions(message.metadata?.processingInstructions),
325
349
  clientId: connection.config.clientId,
326
350
  componentName: connection.config.componentName,
327
- }, { metadata });
351
+ }, { metadata, signal: deadline.signal });
328
352
  if (response.errorCode && response.errorCode !== "") {
329
353
  throw mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error");
330
354
  }
331
- return deserializePayload(response.payload?.data);
355
+ return deserializePayload(response.payload?.data, response.payload?.type, response.payload?.revision);
332
356
  }
333
357
  finally {
358
+ deadline?.close();
359
+ admission?.end();
334
360
  activity.end();
335
361
  }
336
362
  },
@@ -368,18 +394,26 @@ export function axonServerCommandBus(next, conn, options = {}) {
368
394
  * `scatterGather` and `subscriptionQuery`. Because the wrap is outside, the
369
395
  * shortcut branch gets identical correlation to the remote branch.
370
396
  *
371
- * KNOWN GAP: `subscriptionQuery` / `subscribeToUpdates` build their proto
372
- * straight from `message.metadata`, and `interceptingQueryBus` (in
373
- * `@kronos-ts/core`) forwards those two calls to the delegate without
374
- * running the dispatch chain. Closing that needs a core change.
397
+ * Subscription queries run the dispatch chain: `interceptingQueryBus` wraps
398
+ * `subscriptionQuery` / `subscribeToUpdates` with the same intercept the
399
+ * primary `query` gets, so the proto built from `message.metadata` already
400
+ * carries whatever the host's intercept stamped (pinned in core by
401
+ * `interception/__tests__/subscription-interception.test.ts`).
375
402
  */
376
403
  export function axonServerQueryBus(next, conn, options = {}) {
377
404
  const { connection, serializer, metadata: axonMetadata, } = contextView(conn, options.context ?? conn.connection.config.context);
378
405
  const shutdownLatch = conn.shutdown;
406
+ const requestTimeoutMs = positiveInteger(options.timeoutMs ?? 30000, "timeoutMs");
407
+ if (requestTimeoutMs > 2_147_483_647)
408
+ throw new RangeError("timeoutMs exceeds the timer range");
409
+ const inboundAdmission = messagingAdmission("inbound handlers", options.limits?.maxConcurrentHandlers ?? 128, options.limits?.observe);
410
+ const outboundAdmission = messagingAdmission("pending requests", options.limits?.maxPendingRequests ?? 1024, options.limits?.observe);
379
411
  const resilience = options.resilience ?? conn.resilience;
380
412
  const metadata = axonMetadata();
381
- const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS));
382
- const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
413
+ const PERMITS = BigInt(positiveInteger(options.flowControl?.permits ?? Number(DEFAULT_PERMITS), "flowControl.permits"));
414
+ const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Math.floor(Number(PERMITS) / 2));
415
+ if (THRESHOLD < 0n || THRESHOLD >= PERMITS)
416
+ throw new RangeError("refillThreshold must be between zero and permits - 1");
383
417
  const shortcutQueriesToLocalHandlers = options.shortcutQueriesToLocalHandlers ?? false;
384
418
  const queryTimeoutMs = options.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
385
419
  const { serializePayload, deserializePayload } = createPayloadHelpers(serializer);
@@ -394,15 +428,47 @@ export function axonServerQueryBus(next, conn, options = {}) {
394
428
  // to decide which subscriber IDs to target; the server forwards each response to the
395
429
  // exact subscriber.
396
430
  const handlerSubscriptions = new Map();
431
+ const responseCredits = new Map();
432
+ function cancelResponseCredits() {
433
+ for (const credit of responseCredits.values()) {
434
+ clearTimeout(credit.timer);
435
+ credit.cancel();
436
+ }
437
+ responseCredits.clear();
438
+ }
439
+ function responseCredit(identifier) {
440
+ const existing = responseCredits.get(identifier);
441
+ if (existing)
442
+ return existing;
443
+ if (responseCredits.size >= (options.limits?.maxPendingRequests ?? 1024))
444
+ return undefined;
445
+ let grant;
446
+ const ready = new Promise((resolve) => { grant = resolve; });
447
+ const credit = {
448
+ ready, grant, cancelled: false,
449
+ cancel() { this.cancelled = true; grant(); },
450
+ timer: setTimeout(() => {
451
+ credit.cancel();
452
+ if (responseCredits.get(identifier) === credit)
453
+ responseCredits.delete(identifier);
454
+ }, requestTimeoutMs),
455
+ };
456
+ credit.timer.unref?.();
457
+ responseCredits.set(identifier, credit);
458
+ return credit;
459
+ }
460
+ shutdownLatch.onShutdown(() => handlerSubscriptions.clear());
397
461
  let outbound = outboundStream();
398
462
  let streamStarted = false;
463
+ let providerAbort = new AbortController();
464
+ connection.onDisconnect?.(() => { cancelResponseCredits(); providerAbort.abort(); outbound.close(); });
399
465
  let permits = 0n;
400
466
  function ensureStreamStarted() {
401
467
  if (streamStarted)
402
468
  return;
403
469
  streamStarted = true;
404
- const inbound = connection.queries.openStream(outbound.iterable, { metadata });
405
- processInboundQueries(inbound);
470
+ const inbound = connection.queries.openStream(outbound.iterable, { metadata, signal: providerAbort.signal });
471
+ void processInboundQueries(inbound, outbound);
406
472
  }
407
473
  function grantQueryPermits() {
408
474
  outbound.send({
@@ -431,6 +497,8 @@ export function axonServerQueryBus(next, conn, options = {}) {
431
497
  * re-emitted BEFORE the permits frame.
432
498
  */
433
499
  function reestablishStreamBody() {
500
+ cancelResponseCredits();
501
+ handlerSubscriptions.clear();
434
502
  outbound.close();
435
503
  outbound = outboundStream();
436
504
  streamStarted = false;
@@ -440,183 +508,208 @@ export function axonServerQueryBus(next, conn, options = {}) {
440
508
  sendSubscribe(queryName);
441
509
  grantQueryPermits();
442
510
  }
443
- async function reestablishStreamWithRetry() {
444
- if (shutdownLatch.shuttingDown)
445
- return;
446
- await withRetry(async () => reestablishStreamBody(), {
447
- event: "reconnect",
448
- ...resilience,
449
- });
450
- }
511
+ const recovery = streamRecovery(reestablishStreamBody, () => !shutdownLatch.shuttingDown && connection.state !== "closed" && connection.state !== "disconnected" && connection.state !== "reconnecting", resilience);
512
+ shutdownLatch.onShutdown(recovery.stop);
451
513
  // Auto-reestablish when the connection reconnects (e.g., after heartbeat timeout)
452
514
  connection.onReconnect(() => {
453
515
  if (!shutdownLatch.shuttingDown && streamStarted) {
454
- reestablishStreamWithRetry().catch((err) => {
455
- console.error("Axon Server query bus: reconnect retries exhausted", err);
456
- });
516
+ recovery.restart();
457
517
  }
458
518
  });
459
- async function handleSubscriptionQueryRequest(req) {
460
- if (req.subscribe) {
461
- const sub = req.subscribe;
462
- const subId = sub.subscriptionIdentifier;
463
- const proto = sub.queryRequest;
464
- if (!subId || !proto)
519
+ async function handleInboundQuery(proto, responses, subId) {
520
+ let activity;
521
+ let admission;
522
+ let payload;
523
+ let subscriptionEntry;
524
+ let credit;
525
+ const supports = (key) => proto.processingInstructions?.some((instruction) => instruction.key === key && instruction.value?.booleanValue);
526
+ if (!subId && supports(7) && supports(8)) {
527
+ // Response credits may precede the query on Axon's provider stream.
528
+ // Retain those credits by request ID, within the same bounded table.
529
+ credit = responseCredit(proto.messageIdentifier);
530
+ if (!credit) {
531
+ responses.close();
465
532
  return;
466
- const queryName = proto.query;
467
- const payload = deserializePayload(proto.payload?.data, proto.payload?.type, proto.payload?.revision);
468
- handlerSubscriptions.set(subId, { queryName, payload });
469
- let resultPayload;
470
- let errorCode = "";
471
- let errorMsg = "";
472
- if (subscribedNames.has(queryName)) {
473
- try {
533
+ }
534
+ }
535
+ let errorCode = "";
536
+ let errorMsg = "";
537
+ try {
538
+ try {
539
+ activity = shutdownLatch.registerActivity();
540
+ admission = inboundAdmission.enter();
541
+ if (subscribedNames.has(proto.query)) {
474
542
  const queryMessage = {
475
543
  kind: "query",
476
544
  identifier: proto.messageIdentifier,
477
- name: qualifiedNameFromString(queryName),
478
- payload,
545
+ name: qualifiedNameFromString(proto.query),
546
+ payload: deserializePayload(proto.payload?.data, proto.payload?.type, proto.payload?.revision),
479
547
  metadata: metadataFromProto(proto.metaData ?? {}),
480
548
  timestamp: Number(proto.timestamp),
481
549
  };
482
- resultPayload = await next.query(queryMessage);
550
+ if (subId) {
551
+ subscriptionEntry = handlerSubscriptions.get(subId);
552
+ if (!subscriptionEntry)
553
+ return;
554
+ }
555
+ // Every wire query enters the local bus with a fresh unit of work.
556
+ const result = await next.query(queryMessage);
557
+ payload = result !== undefined ? serializePayload("result", result) : undefined;
483
558
  }
484
- catch (err) {
485
- errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
486
- errorMsg = err instanceof Error ? err.message : String(err);
559
+ else {
560
+ errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
561
+ errorMsg = `No next handler for query "${proto.query}"`;
487
562
  }
488
563
  }
489
- else {
490
- errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
491
- errorMsg = `No next handler for query "${queryName}"`;
564
+ catch (err) {
565
+ errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
566
+ errorMsg = err instanceof Error ? err.message : String(err);
492
567
  }
493
- const responseSerialized = resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined;
494
- outbound.send({
495
- subscriptionQueryResponse: {
496
- messageIdentifier: generateIdentifier(),
497
- subscriptionIdentifier: subId,
498
- initialResult: {
499
- messageIdentifier: generateIdentifier(),
500
- requestIdentifier: proto.messageIdentifier,
501
- errorCode,
502
- errorMessage: errorCode
503
- ? {
504
- message: errorMsg,
505
- location: connection.config.componentName,
506
- details: [],
507
- errorCode,
508
- }
509
- : undefined,
510
- payload: responseSerialized,
511
- metaData: {},
512
- processingInstructions: [],
568
+ // An unsubscribe or completion can overtake a slow initial handler.
569
+ if (subId && subscriptionEntry && handlerSubscriptions.get(subId) !== subscriptionEntry)
570
+ return;
571
+ if (subId && errorCode)
572
+ handlerSubscriptions.delete(subId);
573
+ if (credit) {
574
+ await credit.ready;
575
+ if (credit.cancelled)
576
+ return;
577
+ }
578
+ const response = {
579
+ messageIdentifier: generateIdentifier(),
580
+ requestIdentifier: proto.messageIdentifier,
581
+ errorCode,
582
+ errorMessage: errorCode
583
+ ? { message: errorMsg, location: connection.config.componentName, details: [], errorCode }
584
+ : undefined,
585
+ payload,
586
+ metaData: {},
587
+ processingInstructions: [],
588
+ };
589
+ if (subId) {
590
+ responses.send({
591
+ subscriptionQueryResponse: {
592
+ messageIdentifier: generateIdentifier(), subscriptionIdentifier: subId, initialResult: response,
513
593
  },
514
- },
515
- instructionId: "",
516
- });
517
- return;
594
+ instructionId: "",
595
+ });
596
+ }
597
+ else {
598
+ responses.send({ queryResponse: response, instructionId: "" });
599
+ responses.send({
600
+ queryComplete: { messageId: generateIdentifier(), requestId: proto.messageIdentifier },
601
+ instructionId: "",
602
+ });
603
+ }
604
+ await responses.flush();
518
605
  }
519
- if (req.unsubscribe) {
520
- handlerSubscriptions.delete(req.unsubscribe.subscriptionIdentifier);
606
+ finally {
607
+ if (credit)
608
+ clearTimeout(credit.timer);
609
+ if (credit && responseCredits.get(proto.messageIdentifier) === credit)
610
+ responseCredits.delete(proto.messageIdentifier);
611
+ admission?.end();
612
+ activity?.end();
521
613
  }
522
- // flowControl + getInitialResult are not tracked per-sub; ignored for now.
523
614
  }
524
- async function processInboundQueries(inbound) {
615
+ async function processInboundQueries(inbound, responses) {
525
616
  try {
526
617
  for await (const message of inbound) {
527
- if (message.subscriptionQueryRequest) {
528
- await handleSubscriptionQueryRequest(message.subscriptionQueryRequest);
529
- continue;
530
- }
531
- if (!message.query)
532
- continue;
618
+ if (responses !== outbound)
619
+ return;
620
+ recovery.received();
621
+ if (message.instructionId)
622
+ responses.send({ ack: { instructionId: message.instructionId, success: true }, instructionId: "" });
623
+ // Every Axon query instruction consumes a provider credit, including
624
+ // acknowledgements. Ignoring a late subscription ack can exhaust a
625
+ // one-credit window between otherwise successful requests.
533
626
  permits--;
534
- const proto = message.query;
535
- const queryName = proto.query;
536
- let resultPayload;
537
- let errorCode = "";
538
- let errorMsg = "";
539
- if (subscribedNames.has(queryName)) {
540
- try {
541
- const queryMessage = {
542
- kind: "query",
543
- identifier: proto.messageIdentifier,
544
- name: qualifiedNameFromString(queryName),
545
- payload: deserializePayload(proto.payload?.data),
546
- metadata: metadataFromProto(proto.metaData),
547
- timestamp: Number(proto.timestamp),
548
- };
549
- // Through the LOCAL BUS: no unit of work is handed in, so `next`
550
- // opens one under whatever policy the caller gave it.
551
- resultPayload = await next.query(queryMessage);
552
- }
553
- catch (err) {
554
- errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
555
- errorMsg = err instanceof Error ? err.message : String(err);
627
+ if (permits <= THRESHOLD && !shutdownLatch.shuttingDown)
628
+ grantQueryPermits();
629
+ if (message.queryFlowControl?.permits > 0n) {
630
+ const identifier = message.queryFlowControl.queryReference?.requestId;
631
+ if (identifier) {
632
+ const credit = responseCredit(identifier);
633
+ if (!credit) {
634
+ responses.close();
635
+ return;
636
+ }
637
+ credit.grant();
556
638
  }
557
639
  }
558
- else {
559
- errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
560
- errorMsg = `No next handler for query "${queryName}"`;
640
+ if (message.queryCancel)
641
+ responseCredits.get(message.queryCancel.requestId)?.cancel();
642
+ const request = message.subscriptionQueryRequest;
643
+ if (request) {
644
+ if (request.unsubscribe)
645
+ handlerSubscriptions.delete(request.unsubscribe.subscriptionIdentifier);
646
+ const sub = request.subscribe;
647
+ if (sub?.subscriptionIdentifier && sub.queryRequest) {
648
+ try {
649
+ if (shutdownLatch.shuttingDown)
650
+ throw new Error("Shutdown in progress");
651
+ if (handlerSubscriptions.size >= 1024 && !handlerSubscriptions.has(sub.subscriptionIdentifier))
652
+ throw new Error("Provider subscription capacity exhausted");
653
+ const proto = sub.queryRequest;
654
+ handlerSubscriptions.set(sub.subscriptionIdentifier, {
655
+ queryName: proto.query,
656
+ payload: deserializePayload(proto.payload?.data, proto.payload?.type, proto.payload?.revision),
657
+ });
658
+ }
659
+ catch (err) {
660
+ responses.send({
661
+ subscriptionQueryResponse: {
662
+ messageIdentifier: generateIdentifier(), subscriptionIdentifier: sub.subscriptionIdentifier,
663
+ completeExceptionally: {
664
+ errorCode: AxonServerErrorCode.QUERY_EXECUTION_ERROR,
665
+ errorMessage: { message: err instanceof Error ? err.message : String(err) },
666
+ },
667
+ },
668
+ instructionId: "",
669
+ });
670
+ }
671
+ }
672
+ // Axon separates update registration from requesting the initial
673
+ // result. Running the handler on Subscribe answers the wrong phase.
674
+ const initial = request.getInitialResult;
675
+ if (initial?.subscriptionIdentifier && initial.queryRequest) {
676
+ void handleInboundQuery(initial.queryRequest, responses, initial.subscriptionIdentifier).catch((err) => {
677
+ console.error("Axon Server query bus: inbound subscription response failed", err);
678
+ });
679
+ }
680
+ continue;
561
681
  }
562
- outbound.send({
563
- queryResponse: {
564
- messageIdentifier: generateIdentifier(),
565
- requestIdentifier: proto.messageIdentifier,
566
- errorCode,
567
- errorMessage: errorCode
568
- ? {
569
- message: errorMsg,
570
- location: connection.config.componentName,
571
- details: [],
572
- errorCode,
573
- }
574
- : undefined,
575
- payload: resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
576
- metaData: {},
577
- processingInstructions: [],
578
- },
579
- instructionId: "",
580
- });
581
- outbound.send({
582
- queryComplete: {
583
- messageId: generateIdentifier(),
584
- requestId: proto.messageIdentifier,
585
- },
586
- instructionId: "",
682
+ if (!message.query)
683
+ continue;
684
+ void handleInboundQuery(message.query, responses).catch((err) => {
685
+ console.error("Axon Server query bus: inbound response failed", err);
587
686
  });
588
- if (permits <= THRESHOLD) {
589
- outbound.send({
590
- flowControl: { clientId: connection.config.clientId, permits: PERMITS },
591
- instructionId: "",
592
- });
593
- permits += PERMITS;
594
- }
595
687
  }
688
+ if (responses === outbound && !shutdownLatch.shuttingDown)
689
+ throw new Error("Inbound provider stream ended unexpectedly");
596
690
  }
597
691
  catch (err) {
598
- if (shutdownLatch.shuttingDown)
692
+ if (responses !== outbound || shutdownLatch.shuttingDown)
599
693
  return;
600
- if (String(err).includes("Connection dropped"))
694
+ if (connection.state === "reconnecting" || connection.state === "closed" || connection.state === "disconnected")
601
695
  return;
602
- console.error("Axon Server query bus: inbound stream error, attempting re-establishment via withRetry", err);
603
- await reestablishStreamWithRetry().catch((retryErr) => {
604
- console.error("Axon Server query bus: reconnect retries exhausted", retryErr);
605
- });
696
+ recovery.failed(err);
606
697
  }
607
698
  }
608
699
  const routing = {
609
- async query(unstamped, uow) {
700
+ async query(unstamped) {
610
701
  const activity = shutdownLatch.registerActivity();
702
+ let admission;
703
+ let deadline;
611
704
  try {
705
+ admission = outboundAdmission.enter(unstamped.identifier);
706
+ deadline = messagingDeadline(requestTimeoutMs);
612
707
  const queryName = qualifiedNameToString(unstamped.name);
613
708
  // Local shortcut — handle locally if a handler is co-located. The
614
- // caller's unit of work is passed straight through, so `next` makes the
615
- // nest-or-open decision on the HANDLE exactly as it does for an
616
- // in-process read: a live unit of work handed in by `ctx.query` is
617
- // reused so the consulting read shares the caller's transaction.
709
+ // co-located handler answers on a task of its own, exactly as a remote
710
+ // one would `next` mints it.
618
711
  if (shortcutQueriesToLocalHandlers && subscribedNames.has(queryName)) {
619
- return next.query(unstamped, uow);
712
+ return await next.query(unstamped);
620
713
  }
621
714
  // A transport is not a task: it has no unit of work, so it has no clock.
622
715
  // A message that reaches the wire with no instant yet gets one from system
@@ -633,16 +726,38 @@ export function axonServerQueryBus(next, conn, options = {}) {
633
726
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
634
727
  clientId: connection.config.clientId,
635
728
  componentName: connection.config.componentName,
636
- }, { metadata });
729
+ }, { metadata, signal: deadline.signal });
730
+ // NR_OF_RESULTS is one. Drain trailers before returning so transport
731
+ // failures cannot be mistaken for a successful result. The RPC deadline
732
+ // also bounds a stream that sends a response but never completes.
733
+ let received = false;
734
+ let result;
735
+ let responseError;
637
736
  for await (const response of responseStream) {
737
+ if (received)
738
+ continue;
739
+ received = true;
638
740
  if (response.errorCode && response.errorCode !== "") {
639
- throw mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error");
741
+ responseError = mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error");
742
+ }
743
+ else {
744
+ try {
745
+ result = deserializePayload(response.payload?.data, response.payload?.type, response.payload?.revision);
746
+ }
747
+ catch (error) {
748
+ responseError = error instanceof Error ? error : new Error(String(error));
749
+ }
640
750
  }
641
- return deserializePayload(response.payload?.data);
642
751
  }
752
+ if (responseError)
753
+ throw responseError;
754
+ if (received)
755
+ return result;
643
756
  throw new Error(`No response for query "${queryName}"`);
644
757
  }
645
758
  finally {
759
+ deadline?.close();
760
+ admission?.end();
646
761
  activity.end();
647
762
  }
648
763
  },
@@ -655,25 +770,33 @@ export function axonServerQueryBus(next, conn, options = {}) {
655
770
  grantQueryPermits();
656
771
  },
657
772
  subscriptionQuery(unstamped, bufferSize) {
773
+ if (shutdownLatch.shuttingDown)
774
+ throw new Error("Messaging shutdown in progress");
775
+ if (subscriptions.size >= 1024)
776
+ throw new Error("Subscription capacity 1024 exhausted");
658
777
  const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
659
778
  const queryId = message.identifier;
660
779
  if (subscriptions.has(queryId)) {
661
780
  throw new Error(`Subscription query already registered for identifier "${queryId}"`);
662
781
  }
663
- const handler = updateHandler(message, bufferSize);
664
- subscriptions.set(queryId, handler);
782
+ const handler = updateHandler(message, bufferSize, () => subscriptions.delete(queryId));
665
783
  const queryName = qualifiedNameToString(message.name);
784
+ const serialized = serializePayload(queryName, message.payload);
666
785
  const subscriptionId = generateIdentifier();
667
786
  const outboundSub = outboundStream();
787
+ const window = Math.min(1024, Math.max(256, Math.floor(bufferSize ?? 256)));
788
+ const refillBatch = Math.max(1, Math.floor(window / 4));
789
+ let consumedSinceRefill = 0;
790
+ let subscriptionClosed = false;
668
791
  outboundSub.send({
669
792
  subscribe: {
670
793
  subscriptionIdentifier: subscriptionId,
671
- numberOfPermits: BigInt(bufferSize ?? 256),
794
+ numberOfPermits: BigInt(window),
672
795
  queryRequest: {
673
796
  messageIdentifier: message.identifier,
674
797
  query: queryName,
675
798
  timestamp: BigInt(message.timestamp),
676
- payload: serializePayload(queryName, message.payload),
799
+ payload: serialized,
677
800
  metaData: metadataToProto(message.metadata),
678
801
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
679
802
  clientId: connection.config.clientId,
@@ -681,6 +804,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
681
804
  },
682
805
  },
683
806
  });
807
+ // Subscribe does not grant update credits on Axon Server; a separate
808
+ // FlowControl frame initializes the subscription stream's update window.
809
+ outboundSub.send({ flowControl: { numberOfPermits: BigInt(window) } });
684
810
  outboundSub.send({
685
811
  getInitialResult: {
686
812
  subscriptionIdentifier: subscriptionId,
@@ -689,7 +815,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
689
815
  messageIdentifier: message.identifier,
690
816
  query: queryName,
691
817
  timestamp: BigInt(message.timestamp),
692
- payload: serializePayload(queryName, message.payload),
818
+ payload: serialized,
693
819
  metaData: metadataToProto(message.metadata),
694
820
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
695
821
  clientId: connection.config.clientId,
@@ -697,7 +823,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
697
823
  },
698
824
  },
699
825
  });
700
- const responseStream = connection.queries.subscription(outboundSub.iterable, { metadata });
826
+ const subscriptionController = new AbortController();
827
+ const responseStream = connection.queries.subscription(outboundSub.iterable, { metadata, signal: subscriptionController.signal });
828
+ subscriptions.set(queryId, handler);
701
829
  let resolveInitial;
702
830
  let rejectInitial;
703
831
  const initialResult = new Promise((resolve, reject) => {
@@ -705,70 +833,114 @@ export function axonServerQueryBus(next, conn, options = {}) {
705
833
  rejectInitial = reject;
706
834
  });
707
835
  let initialSettled = false;
708
- (async () => {
836
+ let explicitlyCompleted = false;
837
+ const initialTimer = setTimeout(() => closeSubscription(new Error("Subscription initial result timed out")), requestTimeoutMs);
838
+ const removeShutdown = shutdownLatch.onShutdown(() => closeSubscription(new Error("Messaging shutdown in progress")));
839
+ // Callers may consume updates without awaiting the initial result. Keep
840
+ // the original promise rejectable without an unhandled rejection on close.
841
+ void initialResult.catch(() => { });
842
+ function closeSubscription(error) {
843
+ if (subscriptionClosed)
844
+ return;
845
+ subscriptionClosed = true;
846
+ clearTimeout(initialTimer);
847
+ removeShutdown();
848
+ if (!initialSettled) {
849
+ rejectInitial(error ?? new Error("Subscription query closed before initial result"));
850
+ initialSettled = true;
851
+ }
852
+ if (error)
853
+ handler.completeExceptionally(error);
854
+ else
855
+ handler.complete();
856
+ try {
857
+ outboundSub.send({ unsubscribe: { subscriptionIdentifier: subscriptionId } });
858
+ }
859
+ catch { /* Broken stream; local teardown still must finish. */ }
860
+ outboundSub.close();
861
+ subscriptionController.abort();
862
+ subscriptions.delete(queryId);
863
+ }
864
+ void (async () => {
709
865
  try {
710
866
  for await (const response of responseStream) {
867
+ if (subscriptionClosed)
868
+ break;
711
869
  if (response.initialResult) {
712
870
  const initial = response.initialResult;
713
871
  if (!initialSettled) {
714
- if (initial.errorCode && initial.errorCode !== "") {
715
- rejectInitial(mapErrorCode(initial.errorCode, initial.errorMessage?.message ?? "Unknown error"));
716
- }
717
- else {
718
- resolveInitial(deserializePayload(initial.payload?.data));
872
+ if (initial.errorCode) {
873
+ throw mapErrorCode(initial.errorCode, initial.errorMessage?.message ?? "Unknown error");
719
874
  }
875
+ clearTimeout(initialTimer);
876
+ resolveInitial(deserializePayload(initial.payload?.data, initial.payload?.type, initial.payload?.revision));
720
877
  initialSettled = true;
721
878
  }
722
879
  }
723
880
  else if (response.update) {
724
- const update = deserializePayload(response.update.payload?.data);
725
- handler.offer(update);
881
+ const update = deserializePayload(response.update.payload?.data, response.update.payload?.type, response.update.payload?.revision);
882
+ if (!handler.offer(update))
883
+ throw new Error("Subscription query update buffer overflow");
884
+ consumedSinceRefill++;
885
+ if (consumedSinceRefill >= refillBatch) {
886
+ outboundSub.send({
887
+ flowControl: { subscriptionIdentifier: subscriptionId, numberOfPermits: BigInt(consumedSinceRefill) },
888
+ });
889
+ consumedSinceRefill = 0;
890
+ }
726
891
  }
727
892
  else if (response.complete) {
728
- handler.complete();
893
+ explicitlyCompleted = true;
729
894
  break;
730
895
  }
731
896
  else if (response.completeExceptionally) {
732
- handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
733
- "Subscription query failed"));
734
- break;
897
+ throw new Error(response.completeExceptionally.errorMessage?.message ?? "Subscription query failed");
735
898
  }
736
899
  }
737
900
  }
738
901
  catch (err) {
739
- const error = err instanceof Error ? err : new Error(String(err));
902
+ closeSubscription(err instanceof Error ? err : new Error(String(err)));
903
+ }
904
+ finally {
905
+ // EOF and completion frames must settle BOTH faces of a subscription.
906
+ const missingInitial = !initialSettled;
740
907
  if (!initialSettled) {
741
- rejectInitial(error);
908
+ rejectInitial(new Error("Subscription stream ended before initial result"));
742
909
  initialSettled = true;
743
910
  }
744
- handler.completeExceptionally(error);
745
- }
746
- finally {
747
- subscriptions.delete(queryId);
911
+ closeSubscription(!missingInitial && !explicitlyCompleted && !subscriptionClosed ? new Error("Subscription stream ended unexpectedly") : undefined);
748
912
  }
749
913
  })();
750
914
  return {
751
915
  initialResult,
752
- updates: handler.iterable,
753
- close: () => {
754
- outboundSub.send({
755
- unsubscribe: {
756
- subscriptionIdentifier: subscriptionId,
757
- },
758
- });
759
- outboundSub.close();
760
- subscriptions.delete(queryId);
761
- handler.complete();
916
+ updates: {
917
+ [Symbol.asyncIterator]() {
918
+ const iterator = handler.iterable[Symbol.asyncIterator]();
919
+ return {
920
+ next: () => iterator.next(),
921
+ async return() {
922
+ closeSubscription();
923
+ return iterator.return ? iterator.return() : { value: undefined, done: true };
924
+ },
925
+ };
926
+ },
762
927
  },
928
+ close: () => closeSubscription(),
763
929
  };
764
930
  },
765
931
  subscribeToUpdates(unstamped, bufferSize) {
932
+ if (shutdownLatch.shuttingDown)
933
+ throw new Error("Messaging shutdown in progress");
934
+ if (subscriptions.size >= 1024)
935
+ throw new Error("Subscription capacity 1024 exhausted");
766
936
  const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
767
937
  const queryId = message.identifier;
768
938
  if (subscriptions.has(queryId)) {
769
939
  throw new Error(`Subscription query already registered for identifier "${queryId}"`);
770
940
  }
771
- const handler = updateHandler(message, bufferSize);
941
+ let removeShutdown;
942
+ const handler = updateHandler(message, bufferSize, () => { subscriptions.delete(queryId); removeShutdown?.(); });
943
+ removeShutdown = shutdownLatch.onShutdown(() => handler.completeExceptionally(new Error("Messaging shutdown in progress")));
772
944
  subscriptions.set(queryId, handler);
773
945
  return {
774
946
  [Symbol.asyncIterator]: () => handler.iterable[Symbol.asyncIterator](),
@@ -778,7 +950,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
778
950
  },
779
951
  };
780
952
  },
781
- async emitUpdate(queryName, filter, update) {
953
+ async emitUpdate(queryName, filter, update, uow) {
782
954
  runAfterCommitOrImmediately(() => {
783
955
  for (const [subId, sub] of handlerSubscriptions) {
784
956
  if (sub.queryName !== queryName)
@@ -803,9 +975,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
803
975
  instructionId: "",
804
976
  });
805
977
  }
806
- });
978
+ }, uow);
807
979
  },
808
- async completeSubscription(queryName, filter) {
980
+ async completeSubscription(queryName, filter, uow) {
809
981
  runAfterCommitOrImmediately(() => {
810
982
  for (const [subId, sub] of handlerSubscriptions) {
811
983
  if (sub.queryName !== queryName)
@@ -825,9 +997,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
825
997
  });
826
998
  handlerSubscriptions.delete(subId);
827
999
  }
828
- });
1000
+ }, uow);
829
1001
  },
830
- async completeSubscriptionExceptionally(queryName, error, filter) {
1002
+ async completeSubscriptionExceptionally(queryName, error, filter, uow) {
831
1003
  runAfterCommitOrImmediately(() => {
832
1004
  for (const [subId, sub] of handlerSubscriptions) {
833
1005
  if (sub.queryName !== queryName)
@@ -854,7 +1026,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
854
1026
  });
855
1027
  handlerSubscriptions.delete(subId);
856
1028
  }
857
- });
1029
+ }, uow);
858
1030
  },
859
1031
  };
860
1032
  return routing;