@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
@@ -5,8 +5,10 @@ export class ShutdownInProgressError extends Error {
5
5
  }
6
6
  }
7
7
  export function shutdownLatch() {
8
+ const callbacks = new Set();
8
9
  let activeCount = 0;
9
10
  let shuttingDown = false;
11
+ let drainPromise;
10
12
  let drainResolve = null;
11
13
  function checkDrained() {
12
14
  if (shuttingDown && activeCount === 0 && drainResolve) {
@@ -15,6 +17,13 @@ export function shutdownLatch() {
15
17
  }
16
18
  }
17
19
  return {
20
+ onShutdown(callback) {
21
+ if (shuttingDown)
22
+ callback();
23
+ else
24
+ callbacks.add(callback);
25
+ return () => { callbacks.delete(callback); };
26
+ },
18
27
  registerActivity() {
19
28
  if (shuttingDown) {
20
29
  throw new ShutdownInProgressError();
@@ -33,12 +42,22 @@ export function shutdownLatch() {
33
42
  },
34
43
  initiateShutdown() {
35
44
  shuttingDown = true;
45
+ for (const callback of callbacks) {
46
+ try {
47
+ callback();
48
+ }
49
+ catch (error) {
50
+ console.error("Messaging shutdown callback failed", error);
51
+ }
52
+ }
53
+ callbacks.clear();
36
54
  if (activeCount === 0) {
37
55
  return Promise.resolve();
38
56
  }
39
- return new Promise((resolve) => {
57
+ drainPromise ??= new Promise((resolve) => {
40
58
  drainResolve = resolve;
41
59
  });
60
+ return drainPromise;
42
61
  },
43
62
  get shuttingDown() {
44
63
  return shuttingDown;
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown-latch.js","sourceRoot":"","sources":["../src/shutdown-latch.ts"],"names":[],"mappings":"AAsCA,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAO,GAAW,sBAAsB;QAClD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;IACvC,CAAC;CACF;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,IAAI,YAAY,GAAG,KAAK,CAAA;IACxB,IAAI,YAAY,GAAwB,IAAI,CAAA;IAE5C,SAAS,YAAY;QACnB,IAAI,YAAY,IAAI,WAAW,KAAK,CAAC,IAAI,YAAY,EAAE,CAAC;YACtD,YAAY,EAAE,CAAA;YACd,YAAY,GAAG,IAAI,CAAA;QACrB,CAAC;IACH,CAAC;IAED,OAAO;QACL,gBAAgB;YACd,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,uBAAuB,EAAE,CAAA;YACrC,CAAC;YAED,WAAW,EAAE,CAAA;YACb,IAAI,KAAK,GAAG,KAAK,CAAA;YAEjB,OAAO;gBACL,GAAG;oBACD,IAAI,KAAK;wBAAE,OAAM;oBACjB,KAAK,GAAG,IAAI,CAAA;oBACZ,WAAW,EAAE,CAAA;oBACb,YAAY,EAAE,CAAA;gBAChB,CAAC;aACF,CAAA;QACH,CAAC;QAED,gBAAgB;YACd,YAAY,GAAG,IAAI,CAAA;YAEnB,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;gBACtB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;YAC1B,CAAC;YAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC7B,YAAY,GAAG,OAAO,CAAA;YACxB,CAAC,CAAC,CAAA;QACJ,CAAC;QAED,IAAI,YAAY;YACd,OAAO,YAAY,CAAA;QACrB,CAAC;QAED,IAAI,WAAW;YACb,OAAO,WAAW,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"shutdown-latch.js","sourceRoot":"","sources":["../src/shutdown-latch.ts"],"names":[],"mappings":"AAuCA,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAO,GAAW,sBAAsB;QAClD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;IACvC,CAAC;CACF;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;IACvC,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,IAAI,YAAY,GAAG,KAAK,CAAA;IACxB,IAAI,YAAuC,CAAA;IAC3C,IAAI,YAAY,GAAwB,IAAI,CAAA;IAE5C,SAAS,YAAY;QACnB,IAAI,YAAY,IAAI,WAAW,KAAK,CAAC,IAAI,YAAY,EAAE,CAAC;YACtD,YAAY,EAAE,CAAA;YACd,YAAY,GAAG,IAAI,CAAA;QACrB,CAAC;IACH,CAAC;IAED,OAAO;QACL,UAAU,CAAC,QAAQ;YACjB,IAAI,YAAY;gBAAE,QAAQ,EAAE,CAAA;;gBACvB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAC5B,OAAO,GAAG,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA,CAAC,CAAC,CAAA;QAC7C,CAAC;QACD,gBAAgB;YACd,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,uBAAuB,EAAE,CAAA;YACrC,CAAC;YAED,WAAW,EAAE,CAAA;YACb,IAAI,KAAK,GAAG,KAAK,CAAA;YAEjB,OAAO;gBACL,GAAG;oBACD,IAAI,KAAK;wBAAE,OAAM;oBACjB,KAAK,GAAG,IAAI,CAAA;oBACZ,WAAW,EAAE,CAAA;oBACb,YAAY,EAAE,CAAA;gBAChB,CAAC;aACF,CAAA;QACH,CAAC;QAED,gBAAgB;YACd,YAAY,GAAG,IAAI,CAAA;YACnB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBACjC,IAAI,CAAC;oBAAC,QAAQ,EAAE,CAAA;gBAAC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAAC,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAA;gBAAC,CAAC;YACjG,CAAC;YACD,SAAS,CAAC,KAAK,EAAE,CAAA;YAEjB,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;gBACtB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;YAC1B,CAAC;YAED,YAAY,KAAK,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBACvC,YAAY,GAAG,OAAO,CAAA;YACxB,CAAC,CAAC,CAAA;YACF,OAAO,YAAY,CAAA;QACrB,CAAC;QAED,IAAI,YAAY;YACd,OAAO,YAAY,CAAA;QACrB,CAAC;QAED,IAAI,WAAW;YACb,OAAO,WAAW,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { ResilienceConfig } from "./resilience.js";
2
+ /** Retry failures of the async stream, not just synchronous stream creation. */
3
+ export declare function streamRecovery(reopen: () => void, canReconnect: () => boolean, config?: Partial<ResilienceConfig>): {
4
+ failed: (error: unknown) => void;
5
+ received(): void;
6
+ restart(): void;
7
+ stop(): void;
8
+ };
9
+ //# sourceMappingURL=stream-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-recovery.d.ts","sourceRoot":"","sources":["../src/stream-recovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,gFAAgF;AAChF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,IAAI,EAClB,YAAY,EAAE,MAAM,OAAO,EAC3B,MAAM,GAAE,OAAO,CAAC,gBAAgB,CAAM;oBAcf,OAAO;;;;EAmD/B"}
@@ -0,0 +1,70 @@
1
+ /** Retry failures of the async stream, not just synchronous stream creation. */
2
+ export function streamRecovery(reopen, canReconnect, config = {}) {
3
+ const log = (message) => {
4
+ try {
5
+ ;
6
+ (config.log ?? console.warn)(message);
7
+ }
8
+ catch {
9
+ /* Diagnostics cannot prevent recovery. */
10
+ }
11
+ };
12
+ let timer;
13
+ let stopped = false;
14
+ let attempts = 0;
15
+ let openedAt = Date.now();
16
+ function failed(error) {
17
+ if (stopped || timer || !canReconnect())
18
+ return;
19
+ if (config.isRetryable?.(error) === false || attempts >= (config.maxAttempts ?? 30)) {
20
+ log(`Provider stream recovery exhausted: ${String(error)}`);
21
+ return;
22
+ }
23
+ // Keep a positive floor even when jitter is zero. An immediately-ended
24
+ // async generator must never create a microtask reconnect loop.
25
+ const cap = Math.min((config.initialDelayMs ?? 100) * (config.multiplier ?? 2) ** attempts++, config.maxDelayMs ?? 30000);
26
+ const delay = Math.max(1, cap * (0.5 + Math.random() * 0.5));
27
+ log(`Provider stream failed; reconnecting in ${Math.round(delay)}ms: ${String(error)}`);
28
+ timer = setTimeout(() => {
29
+ timer = undefined;
30
+ if (stopped || !canReconnect())
31
+ return;
32
+ openedAt = Date.now();
33
+ try {
34
+ reopen();
35
+ }
36
+ catch (error) {
37
+ failed(error);
38
+ }
39
+ }, delay);
40
+ timer.unref?.();
41
+ }
42
+ return {
43
+ failed,
44
+ received() {
45
+ // Subscription acknowledgements alone do not prove a stable connection.
46
+ if (Date.now() - openedAt >= 30000)
47
+ attempts = 0;
48
+ },
49
+ restart() {
50
+ if (stopped || !canReconnect())
51
+ return;
52
+ clearTimeout(timer);
53
+ timer = undefined;
54
+ attempts = 0;
55
+ openedAt = Date.now();
56
+ try {
57
+ reopen();
58
+ }
59
+ catch (error) {
60
+ failed(error);
61
+ }
62
+ },
63
+ stop() {
64
+ stopped = true;
65
+ clearTimeout(timer);
66
+ timer = undefined;
67
+ },
68
+ };
69
+ }
70
+ //# sourceMappingURL=stream-recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-recovery.js","sourceRoot":"","sources":["../src/stream-recovery.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,MAAM,UAAU,cAAc,CAC5B,MAAkB,EAClB,YAA2B,EAC3B,MAAM,GAA8B,EAAE;IAEtC,MAAM,GAAG,GAAG,CAAC,OAAe,EAAE,EAAE;QAC9B,IAAI,CAAC;YACH,CAAC;YAAA,CAAC,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;IACH,CAAC,CAAA;IACD,IAAI,KAAgD,CAAA;IACpD,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,IAAI,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAEzB,SAAS,MAAM,CAAC,KAAc;QAC5B,IAAI,OAAO,IAAI,KAAK,IAAI,CAAC,YAAY,EAAE;YAAE,OAAM;QAC/C,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC;YACpF,GAAG,CAAC,uCAAuC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC3D,OAAM;QACR,CAAC;QACD,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAClB,CAAC,MAAM,CAAC,cAAc,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC,IAAI,QAAQ,EAAE,EACvE,MAAM,CAAC,UAAU,IAAI,KAAK,CAC3B,CAAA;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;QAC5D,GAAG,CAAC,2CAA2C,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACvF,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,GAAG,SAAS,CAAA;YACjB,IAAI,OAAO,IAAI,CAAC,YAAY,EAAE;gBAAE,OAAM;YACtC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACrB,IAAI,CAAC;gBACH,MAAM,EAAE,CAAA;YACV,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,CAAA;YACf,CAAC;QACH,CAAC,EAAE,KAAK,CAAC,CAAA;QACT,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;IACjB,CAAC;IAED,OAAO;QACL,MAAM;QACN,QAAQ;YACN,wEAAwE;YACxE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,IAAI,KAAK;gBAAE,QAAQ,GAAG,CAAC,CAAA;QAClD,CAAC;QACD,OAAO;YACL,IAAI,OAAO,IAAI,CAAC,YAAY,EAAE;gBAAE,OAAM;YACtC,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,KAAK,GAAG,SAAS,CAAA;YACjB,QAAQ,GAAG,CAAC,CAAA;YACZ,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACrB,IAAI,CAAC;gBACH,MAAM,EAAE,CAAA;YACV,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,CAAA;YACf,CAAC;QACH,CAAC;QACD,IAAI;YACF,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,KAAK,GAAG,SAAS,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kronos-ts/axon-server",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Axon Server extension for Kronos — event store and command bus over Axon Server.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@kronos-ts/core": "0.3.0",
52
+ "@kronos-ts/core": "0.4.0",
53
53
  "@bufbuild/protobuf": "^2.3",
54
54
  "@grpc/grpc-js": "^1.12",
55
55
  "nice-grpc": "^2.1",
@@ -11,7 +11,7 @@ import type {
11
11
  SequencedEvent,
12
12
  StreamingCondition,
13
13
  } from "@kronos-ts/core"
14
- import { messageStream, compileQuery } from "@kronos-ts/core"
14
+ import { compileQuery } from "@kronos-ts/core"
15
15
  import type {
16
16
  EventStore,
17
17
  SourcingResult,
@@ -25,6 +25,7 @@ import { globalSequenceToken, FIRST_TOKEN } from "@kronos-ts/core"
25
25
  import { markerAt, noMarker } from "@kronos-ts/core"
26
26
  import type { AxonServerStoreSource } from "./connection.js"
27
27
  import { contextView } from "./context-view.js"
28
+ import { boundedRead } from "./bounded-read.js"
28
29
  import type {
29
30
  Criterion,
30
31
  TagsAndNamesCriterion,
@@ -178,69 +179,59 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
178
179
  const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
179
180
  const { eventToProto, eventFromProto } = createEventConverters(serializer)
180
181
 
181
- // Push-based subscriber registry (EventBus.subscribe contract). Axon Server's
182
- // own distribution is the server-side Stream RPC (see open()); these in-process
183
- // subscribers are notified best-effort on every successful local append.
184
- const subscribers = new Set<(events: ReadonlyArray<EventMessage>) => Promise<void>>()
185
- async function notifySubscribers(events: ReadonlyArray<EventMessage>): Promise<void> {
186
- for (const sub of subscribers) {
187
- try {
188
- await sub(events)
189
- } catch {
190
- /* ignore subscriber errors */
191
- }
192
- }
193
- }
194
-
195
- return {
196
- async source(condition: SourcingCondition): Promise<SourcingResult> {
197
- const criterions = criteriaToCriterions(compileQuery(condition.query))
198
- const start = condition.start ?? 0n
199
-
200
- const request = {
201
- fromSequence: start,
202
- criterion: criterions,
203
- }
182
+ async function sourceOnce(condition: SourcingCondition, signal: AbortSignal): Promise<SourcingResult> {
183
+ const criterions = criteriaToCriterions(compileQuery(condition.query))
184
+ const start = condition.start ?? 0n
204
185
 
205
- const events: EventMessage[] = []
206
- let marker: ConsistencyMarker = noMarker()
186
+ const request = {
187
+ fromSequence: start,
188
+ criterion: criterions,
189
+ }
207
190
 
208
- try {
209
- const stream = connection.eventStore.source(request, { metadata: createAxonMetadata() })
210
- for await (const response of stream) {
211
- if (response.event) {
212
- const taggedEvent = response.event
213
- const protoEvent = taggedEvent.event
214
- if (protoEvent) {
215
- // DCB source/stream responses carry no tags — the server indexes
216
- // them write-side but does not echo them back (SequencedEvent has
217
- // only sequence + event). Reconstructed events get empty tags.
218
- events.push(eventFromProto(protoEvent, []))
219
- }
220
- }
221
- if (response.consistencyMarker !== undefined) {
222
- marker = markerAt(response.consistencyMarker)
191
+ const events: EventMessage[] = []
192
+ let marker: ConsistencyMarker = noMarker()
193
+
194
+ try {
195
+ const stream = connection.eventStore.source(request, { metadata: createAxonMetadata(), signal })
196
+ for await (const response of stream) {
197
+ if (response.event) {
198
+ const taggedEvent = response.event
199
+ const protoEvent = taggedEvent.event
200
+ if (protoEvent) {
201
+ // DCB source/stream responses carry no tags — the server indexes
202
+ // them write-side but does not echo them back (SequencedEvent has
203
+ // only sequence + event). Reconstructed events get empty tags.
204
+ events.push(eventFromProto(protoEvent, []))
223
205
  }
224
206
  }
225
- } catch (err) {
226
- // READING PAST THE HEAD IS AN EMPTY ANSWER, NOT AN ERROR — and Axon
227
- // Server disagrees, so this is where the two vocabularies are
228
- // reconciled. `Source` fails OUT_OF_RANGE with "Start sequence cannot
229
- // be larger than end sequence" whenever `fromSequence` is beyond the
230
- // global head, which is the ORDINARY STEADY STATE of a snapshotted
231
- // load: an entry written at the head means the very next read resumes
232
- // at head + 1 and legitimately finds nothing.
233
- //
234
- // The marker is `start - 1`, and it is exact rather than conservative:
235
- // the caller has already accounted for everything up to there (that is
236
- // what asking to start later MEANS), and nothing can exist after it or
237
- // the server would not have refused. So an append conditioned on this
238
- // read is checked against precisely the range that was read.
239
- if (!isStartPastHead(err)) throw err
240
- return { events: [], marker: start > 0n ? markerAt(start - 1n) : noMarker() }
207
+ if (response.consistencyMarker !== undefined) {
208
+ marker = markerAt(response.consistencyMarker)
209
+ }
241
210
  }
211
+ } catch (err) {
212
+ // READING PAST THE HEAD IS AN EMPTY ANSWER, NOT AN ERROR — and Axon
213
+ // Server disagrees, so this is where the two vocabularies are
214
+ // reconciled. `Source` fails OUT_OF_RANGE with "Start sequence cannot
215
+ // be larger than end sequence" whenever `fromSequence` is beyond the
216
+ // global head, which is the ORDINARY STEADY STATE of a snapshotted
217
+ // load: an entry written at the head means the very next read resumes
218
+ // at head + 1 and legitimately finds nothing.
219
+ //
220
+ // The marker is `start - 1`, and it is exact rather than conservative:
221
+ // the caller has already accounted for everything up to there (that is
222
+ // what asking to start later MEANS), and nothing can exist after it or
223
+ // the server would not have refused. So an append conditioned on this
224
+ // read is checked against precisely the range that was read.
225
+ if (!isStartPastHead(err)) throw err
226
+ return { events: [], marker: start > 0n ? markerAt(start - 1n) : noMarker() }
227
+ }
228
+
229
+ return { events, marker }
230
+ }
242
231
 
243
- return { events, marker }
232
+ return {
233
+ source(condition: SourcingCondition): Promise<SourcingResult> {
234
+ return boundedRead(connection.config.readTimeoutMs, (signal) => sourceOnce(condition, signal))
244
235
  },
245
236
 
246
237
  async appendEvents(
@@ -271,7 +262,6 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
271
262
  metadata: createAxonMetadata(),
272
263
  })
273
264
  responseMarker = response.consistencyMarker
274
- await notifySubscribers(newEvents)
275
265
  },
276
266
  async afterCommit() {
277
267
  return markerAt(responseMarker ?? 0n)
@@ -299,7 +289,20 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
299
289
  criterion: criterions,
300
290
  }
301
291
 
302
- const grpcStream = connection.eventStore.stream(request, { metadata: createAxonMetadata() })
292
+ // `close()` must reach the server. This is a server-streaming call: the
293
+ // client sends nothing after the request, so the only way to end it from
294
+ // this side is to cancel it. Abandoning the async iterator does not do
295
+ // that — the `for await` below stays parked on the call until the next
296
+ // event arrives, which at the head of a quiet stream is never, and every
297
+ // closed stream leaves a live call behind on the server. With enough of
298
+ // them open, Axon Server stops answering later `source` reads (a 2-CPU
299
+ // container gets there after a handful): handlers never reply, and
300
+ // commands die at the server's own timeout.
301
+ const cancel = new AbortController()
302
+ const grpcStream = connection.eventStore.stream(request, {
303
+ metadata: createAxonMetadata(),
304
+ signal: cancel.signal,
305
+ })
303
306
 
304
307
  // Internal buffer for events pulled from the gRPC stream
305
308
  const buffer: SequencedEvent[] = []
@@ -328,7 +331,11 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
328
331
  completed = true
329
332
  availableCallback?.()
330
333
  } catch (err) {
331
- streamError = err instanceof Error ? err : new Error(String(err))
334
+ // Our own cancellation surfaces here as an AbortError; that is the
335
+ // stream closing as asked, not a failure to report.
336
+ if (!completed) {
337
+ streamError = err instanceof Error ? err : new Error(String(err))
338
+ }
332
339
  completed = true
333
340
  availableCallback?.()
334
341
  }
@@ -336,7 +343,7 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
336
343
 
337
344
  startReading()
338
345
 
339
- return messageStream<SequencedEvent>({
346
+ return {
340
347
  next() {
341
348
  return buffer.shift()
342
349
  },
@@ -364,13 +371,15 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
364
371
  close() {
365
372
  completed = true
366
373
  availableCallback = null
367
- // gRPC stream will be cancelled when the async iterator is abandoned
374
+ cancel.abort()
368
375
  },
369
- })
376
+ }
370
377
  },
371
378
 
372
379
  async getHeadPosition(): Promise<bigint> {
373
- const response = await connection.eventStore.getHead({}, { metadata: createAxonMetadata() })
380
+ const response = await boundedRead(connection.config.readTimeoutMs, (signal) =>
381
+ connection.eventStore.getHead({}, { metadata: createAxonMetadata(), signal }),
382
+ )
374
383
  return response.sequence
375
384
  },
376
385
 
@@ -379,21 +388,10 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
379
388
  },
380
389
 
381
390
  async latestToken(): Promise<TrackingToken> {
382
- const response = await connection.eventStore.getHead({}, { metadata: createAxonMetadata() })
391
+ const response = await boundedRead(connection.config.readTimeoutMs, (signal) =>
392
+ connection.eventStore.getHead({}, { metadata: createAxonMetadata(), signal }),
393
+ )
383
394
  return globalSequenceToken(response.sequence)
384
395
  },
385
-
386
- // EventBus contract — publish = append without condition, then notify
387
- // in-process subscribers.
388
- async publish(events: ReadonlyArray<EventMessage>): Promise<void> {
389
- await this.append(events)
390
- },
391
-
392
- subscribe(handler: (events: ReadonlyArray<EventMessage>) => Promise<void>): () => void {
393
- subscribers.add(handler)
394
- return () => {
395
- subscribers.delete(handler)
396
- }
397
- },
398
396
  }
399
397
  }
@@ -25,7 +25,7 @@
25
25
  import type {
26
26
  EventStore,
27
27
  Snapshot,
28
- SnapshotCapability,
28
+ SnapshotStoreCapability,
29
29
  SourcingCondition,
30
30
  SourcingResult,
31
31
  } from "@kronos-ts/core"
@@ -33,6 +33,7 @@ import type { Serializer } from "@kronos-ts/core"
33
33
  import { withoutSnapshotKey } from "@kronos-ts/core"
34
34
  import type { AxonServerStoreSource } from "./connection.js"
35
35
  import { contextView } from "./context-view.js"
36
+ import { boundedRead } from "./bounded-read.js"
36
37
  import type { Snapshot as ProtoSnapshot } from "./generated/dcb.js"
37
38
 
38
39
  // ---------------------------------------------------------------------------
@@ -92,7 +93,7 @@ function encodeKey(key: string): Uint8Array {
92
93
  * )
93
94
  * ```
94
95
  *
95
- * ADDITIVE, NOT COLLAPSING. It returns `E & SnapshotCapability` — the store you
96
+ * ADDITIVE, NOT COLLAPSING. It returns `E & SnapshotStoreCapability` — the store you
96
97
  * passed in, plus the write — so capabilities stack in either order and nothing
97
98
  * the inner store carried is laundered on the way through.
98
99
  *
@@ -111,16 +112,18 @@ export function axonServerSnapshottingEventStore<E extends EventStore>(
111
112
  next: E,
112
113
  conn: AxonServerStoreSource,
113
114
  context: string,
114
- ): E & SnapshotCapability {
115
+ ): E & SnapshotStoreCapability {
115
116
  const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
116
117
  const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
117
118
 
118
119
  /** The cached fold filed under `key`, or nothing — the first of the two calls. */
119
120
  async function loadSnapshot(key: string): Promise<Snapshot | undefined> {
120
121
  try {
121
- const response = await connection.snapshotStore.getLast(
122
- { key: encodeKey(key) },
123
- { metadata: createAxonMetadata() },
122
+ const response = await boundedRead(connection.config.readTimeoutMs, (signal) =>
123
+ connection.snapshotStore.getLast(
124
+ { key: encodeKey(key) },
125
+ { metadata: createAxonMetadata(), signal },
126
+ ),
124
127
  )
125
128
 
126
129
  if (!response.snapshot) {
@@ -187,5 +190,5 @@ export function axonServerSnapshottingEventStore<E extends EventStore>(
187
190
  const result = await next.source({ ...plain, start })
188
191
  return { ...result, snapshot }
189
192
  },
190
- } as E & SnapshotCapability
193
+ } as E & SnapshotStoreCapability
191
194
  }