@kronos-ts/axon-server 0.5.0 → 0.6.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 (63) 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 +444 -270
  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/shutdown-latch.d.ts +1 -0
  43. package/dist/shutdown-latch.d.ts.map +1 -1
  44. package/dist/shutdown-latch.js +20 -1
  45. package/dist/shutdown-latch.js.map +1 -1
  46. package/dist/stream-recovery.d.ts +9 -0
  47. package/dist/stream-recovery.d.ts.map +1 -0
  48. package/dist/stream-recovery.js +70 -0
  49. package/dist/stream-recovery.js.map +1 -0
  50. package/package.json +2 -2
  51. package/src/axon-server-event-store.ts +77 -79
  52. package/src/axon-server-snapshotting-event-store.ts +10 -7
  53. package/src/axon-server.ts +414 -311
  54. package/src/bounded-read.ts +43 -0
  55. package/src/connection.ts +77 -45
  56. package/src/control-plane.ts +19 -63
  57. package/src/event-processor-info.ts +22 -43
  58. package/src/flow-controlled-sender.ts +33 -14
  59. package/src/index.ts +0 -1
  60. package/src/outbound-stream.ts +57 -27
  61. package/src/platform-service.ts +75 -11
  62. package/src/shutdown-latch.ts +14 -1
  63. 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,175 +508,202 @@ 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
700
  async query(unstamped, uow) {
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
709
  // caller's unit of work is passed straight through, so `next` makes the
@@ -616,7 +711,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
616
711
  // in-process read: a live unit of work handed in by `ctx.query` is
617
712
  // reused so the consulting read shares the caller's transaction.
618
713
  if (shortcutQueriesToLocalHandlers && subscribedNames.has(queryName)) {
619
- return next.query(unstamped, uow);
714
+ return await next.query(unstamped, uow);
620
715
  }
621
716
  // A transport is not a task: it has no unit of work, so it has no clock.
622
717
  // A message that reaches the wire with no instant yet gets one from system
@@ -633,16 +728,38 @@ export function axonServerQueryBus(next, conn, options = {}) {
633
728
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
634
729
  clientId: connection.config.clientId,
635
730
  componentName: connection.config.componentName,
636
- }, { metadata });
731
+ }, { metadata, signal: deadline.signal });
732
+ // NR_OF_RESULTS is one. Drain trailers before returning so transport
733
+ // failures cannot be mistaken for a successful result. The RPC deadline
734
+ // also bounds a stream that sends a response but never completes.
735
+ let received = false;
736
+ let result;
737
+ let responseError;
637
738
  for await (const response of responseStream) {
739
+ if (received)
740
+ continue;
741
+ received = true;
638
742
  if (response.errorCode && response.errorCode !== "") {
639
- throw mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error");
743
+ responseError = mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error");
744
+ }
745
+ else {
746
+ try {
747
+ result = deserializePayload(response.payload?.data, response.payload?.type, response.payload?.revision);
748
+ }
749
+ catch (error) {
750
+ responseError = error instanceof Error ? error : new Error(String(error));
751
+ }
640
752
  }
641
- return deserializePayload(response.payload?.data);
642
753
  }
754
+ if (responseError)
755
+ throw responseError;
756
+ if (received)
757
+ return result;
643
758
  throw new Error(`No response for query "${queryName}"`);
644
759
  }
645
760
  finally {
761
+ deadline?.close();
762
+ admission?.end();
646
763
  activity.end();
647
764
  }
648
765
  },
@@ -655,25 +772,33 @@ export function axonServerQueryBus(next, conn, options = {}) {
655
772
  grantQueryPermits();
656
773
  },
657
774
  subscriptionQuery(unstamped, bufferSize) {
775
+ if (shutdownLatch.shuttingDown)
776
+ throw new Error("Messaging shutdown in progress");
777
+ if (subscriptions.size >= 1024)
778
+ throw new Error("Subscription capacity 1024 exhausted");
658
779
  const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
659
780
  const queryId = message.identifier;
660
781
  if (subscriptions.has(queryId)) {
661
782
  throw new Error(`Subscription query already registered for identifier "${queryId}"`);
662
783
  }
663
- const handler = updateHandler(message, bufferSize);
664
- subscriptions.set(queryId, handler);
784
+ const handler = updateHandler(message, bufferSize, () => subscriptions.delete(queryId));
665
785
  const queryName = qualifiedNameToString(message.name);
786
+ const serialized = serializePayload(queryName, message.payload);
666
787
  const subscriptionId = generateIdentifier();
667
788
  const outboundSub = outboundStream();
789
+ const window = Math.min(1024, Math.max(256, Math.floor(bufferSize ?? 256)));
790
+ const refillBatch = Math.max(1, Math.floor(window / 4));
791
+ let consumedSinceRefill = 0;
792
+ let subscriptionClosed = false;
668
793
  outboundSub.send({
669
794
  subscribe: {
670
795
  subscriptionIdentifier: subscriptionId,
671
- numberOfPermits: BigInt(bufferSize ?? 256),
796
+ numberOfPermits: BigInt(window),
672
797
  queryRequest: {
673
798
  messageIdentifier: message.identifier,
674
799
  query: queryName,
675
800
  timestamp: BigInt(message.timestamp),
676
- payload: serializePayload(queryName, message.payload),
801
+ payload: serialized,
677
802
  metaData: metadataToProto(message.metadata),
678
803
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
679
804
  clientId: connection.config.clientId,
@@ -681,6 +806,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
681
806
  },
682
807
  },
683
808
  });
809
+ // Subscribe does not grant update credits on Axon Server; a separate
810
+ // FlowControl frame initializes the subscription stream's update window.
811
+ outboundSub.send({ flowControl: { numberOfPermits: BigInt(window) } });
684
812
  outboundSub.send({
685
813
  getInitialResult: {
686
814
  subscriptionIdentifier: subscriptionId,
@@ -689,7 +817,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
689
817
  messageIdentifier: message.identifier,
690
818
  query: queryName,
691
819
  timestamp: BigInt(message.timestamp),
692
- payload: serializePayload(queryName, message.payload),
820
+ payload: serialized,
693
821
  metaData: metadataToProto(message.metadata),
694
822
  processingInstructions: defaultQueryInstructions(queryTimeoutMs),
695
823
  clientId: connection.config.clientId,
@@ -697,7 +825,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
697
825
  },
698
826
  },
699
827
  });
700
- const responseStream = connection.queries.subscription(outboundSub.iterable, { metadata });
828
+ const subscriptionController = new AbortController();
829
+ const responseStream = connection.queries.subscription(outboundSub.iterable, { metadata, signal: subscriptionController.signal });
830
+ subscriptions.set(queryId, handler);
701
831
  let resolveInitial;
702
832
  let rejectInitial;
703
833
  const initialResult = new Promise((resolve, reject) => {
@@ -705,70 +835,114 @@ export function axonServerQueryBus(next, conn, options = {}) {
705
835
  rejectInitial = reject;
706
836
  });
707
837
  let initialSettled = false;
708
- (async () => {
838
+ let explicitlyCompleted = false;
839
+ const initialTimer = setTimeout(() => closeSubscription(new Error("Subscription initial result timed out")), requestTimeoutMs);
840
+ const removeShutdown = shutdownLatch.onShutdown(() => closeSubscription(new Error("Messaging shutdown in progress")));
841
+ // Callers may consume updates without awaiting the initial result. Keep
842
+ // the original promise rejectable without an unhandled rejection on close.
843
+ void initialResult.catch(() => { });
844
+ function closeSubscription(error) {
845
+ if (subscriptionClosed)
846
+ return;
847
+ subscriptionClosed = true;
848
+ clearTimeout(initialTimer);
849
+ removeShutdown();
850
+ if (!initialSettled) {
851
+ rejectInitial(error ?? new Error("Subscription query closed before initial result"));
852
+ initialSettled = true;
853
+ }
854
+ if (error)
855
+ handler.completeExceptionally(error);
856
+ else
857
+ handler.complete();
858
+ try {
859
+ outboundSub.send({ unsubscribe: { subscriptionIdentifier: subscriptionId } });
860
+ }
861
+ catch { /* Broken stream; local teardown still must finish. */ }
862
+ outboundSub.close();
863
+ subscriptionController.abort();
864
+ subscriptions.delete(queryId);
865
+ }
866
+ void (async () => {
709
867
  try {
710
868
  for await (const response of responseStream) {
869
+ if (subscriptionClosed)
870
+ break;
711
871
  if (response.initialResult) {
712
872
  const initial = response.initialResult;
713
873
  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));
874
+ if (initial.errorCode) {
875
+ throw mapErrorCode(initial.errorCode, initial.errorMessage?.message ?? "Unknown error");
719
876
  }
877
+ clearTimeout(initialTimer);
878
+ resolveInitial(deserializePayload(initial.payload?.data, initial.payload?.type, initial.payload?.revision));
720
879
  initialSettled = true;
721
880
  }
722
881
  }
723
882
  else if (response.update) {
724
- const update = deserializePayload(response.update.payload?.data);
725
- handler.offer(update);
883
+ const update = deserializePayload(response.update.payload?.data, response.update.payload?.type, response.update.payload?.revision);
884
+ if (!handler.offer(update))
885
+ throw new Error("Subscription query update buffer overflow");
886
+ consumedSinceRefill++;
887
+ if (consumedSinceRefill >= refillBatch) {
888
+ outboundSub.send({
889
+ flowControl: { subscriptionIdentifier: subscriptionId, numberOfPermits: BigInt(consumedSinceRefill) },
890
+ });
891
+ consumedSinceRefill = 0;
892
+ }
726
893
  }
727
894
  else if (response.complete) {
728
- handler.complete();
895
+ explicitlyCompleted = true;
729
896
  break;
730
897
  }
731
898
  else if (response.completeExceptionally) {
732
- handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
733
- "Subscription query failed"));
734
- break;
899
+ throw new Error(response.completeExceptionally.errorMessage?.message ?? "Subscription query failed");
735
900
  }
736
901
  }
737
902
  }
738
903
  catch (err) {
739
- const error = err instanceof Error ? err : new Error(String(err));
904
+ closeSubscription(err instanceof Error ? err : new Error(String(err)));
905
+ }
906
+ finally {
907
+ // EOF and completion frames must settle BOTH faces of a subscription.
908
+ const missingInitial = !initialSettled;
740
909
  if (!initialSettled) {
741
- rejectInitial(error);
910
+ rejectInitial(new Error("Subscription stream ended before initial result"));
742
911
  initialSettled = true;
743
912
  }
744
- handler.completeExceptionally(error);
745
- }
746
- finally {
747
- subscriptions.delete(queryId);
913
+ closeSubscription(!missingInitial && !explicitlyCompleted && !subscriptionClosed ? new Error("Subscription stream ended unexpectedly") : undefined);
748
914
  }
749
915
  })();
750
916
  return {
751
917
  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();
918
+ updates: {
919
+ [Symbol.asyncIterator]() {
920
+ const iterator = handler.iterable[Symbol.asyncIterator]();
921
+ return {
922
+ next: () => iterator.next(),
923
+ async return() {
924
+ closeSubscription();
925
+ return iterator.return ? iterator.return() : { value: undefined, done: true };
926
+ },
927
+ };
928
+ },
762
929
  },
930
+ close: () => closeSubscription(),
763
931
  };
764
932
  },
765
933
  subscribeToUpdates(unstamped, bufferSize) {
934
+ if (shutdownLatch.shuttingDown)
935
+ throw new Error("Messaging shutdown in progress");
936
+ if (subscriptions.size >= 1024)
937
+ throw new Error("Subscription capacity 1024 exhausted");
766
938
  const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
767
939
  const queryId = message.identifier;
768
940
  if (subscriptions.has(queryId)) {
769
941
  throw new Error(`Subscription query already registered for identifier "${queryId}"`);
770
942
  }
771
- const handler = updateHandler(message, bufferSize);
943
+ let removeShutdown;
944
+ const handler = updateHandler(message, bufferSize, () => { subscriptions.delete(queryId); removeShutdown?.(); });
945
+ removeShutdown = shutdownLatch.onShutdown(() => handler.completeExceptionally(new Error("Messaging shutdown in progress")));
772
946
  subscriptions.set(queryId, handler);
773
947
  return {
774
948
  [Symbol.asyncIterator]: () => handler.iterable[Symbol.asyncIterator](),
@@ -778,7 +952,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
778
952
  },
779
953
  };
780
954
  },
781
- async emitUpdate(queryName, filter, update) {
955
+ async emitUpdate(queryName, filter, update, uow) {
782
956
  runAfterCommitOrImmediately(() => {
783
957
  for (const [subId, sub] of handlerSubscriptions) {
784
958
  if (sub.queryName !== queryName)
@@ -803,9 +977,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
803
977
  instructionId: "",
804
978
  });
805
979
  }
806
- });
980
+ }, uow);
807
981
  },
808
- async completeSubscription(queryName, filter) {
982
+ async completeSubscription(queryName, filter, uow) {
809
983
  runAfterCommitOrImmediately(() => {
810
984
  for (const [subId, sub] of handlerSubscriptions) {
811
985
  if (sub.queryName !== queryName)
@@ -825,9 +999,9 @@ export function axonServerQueryBus(next, conn, options = {}) {
825
999
  });
826
1000
  handlerSubscriptions.delete(subId);
827
1001
  }
828
- });
1002
+ }, uow);
829
1003
  },
830
- async completeSubscriptionExceptionally(queryName, error, filter) {
1004
+ async completeSubscriptionExceptionally(queryName, error, filter, uow) {
831
1005
  runAfterCommitOrImmediately(() => {
832
1006
  for (const [subId, sub] of handlerSubscriptions) {
833
1007
  if (sub.queryName !== queryName)
@@ -854,7 +1028,7 @@ export function axonServerQueryBus(next, conn, options = {}) {
854
1028
  });
855
1029
  handlerSubscriptions.delete(subId);
856
1030
  }
857
- });
1031
+ }, uow);
858
1032
  },
859
1033
  };
860
1034
  return routing;