@kronos-ts/axon-server 0.3.2 → 0.5.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 (102) hide show
  1. package/dist/axon-server-event-store.d.ts +3 -15
  2. package/dist/axon-server-event-store.d.ts.map +1 -1
  3. package/dist/axon-server-event-store.js +85 -44
  4. package/dist/axon-server-event-store.js.map +1 -1
  5. package/dist/axon-server-snapshotting-event-store.d.ts +30 -0
  6. package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -0
  7. package/dist/axon-server-snapshotting-event-store.js +153 -0
  8. package/dist/axon-server-snapshotting-event-store.js.map +1 -0
  9. package/dist/axon-server.d.ts +92 -208
  10. package/dist/axon-server.d.ts.map +1 -1
  11. package/dist/axon-server.js +221 -323
  12. package/dist/axon-server.js.map +1 -1
  13. package/dist/connection-manager.d.ts +2 -2
  14. package/dist/connection-manager.d.ts.map +1 -1
  15. package/dist/connection.d.ts +134 -4
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +135 -10
  18. package/dist/connection.js.map +1 -1
  19. package/dist/context-view.d.ts +30 -0
  20. package/dist/context-view.d.ts.map +1 -0
  21. package/dist/context-view.js +19 -0
  22. package/dist/context-view.js.map +1 -0
  23. package/dist/control-plane.d.ts +18 -17
  24. package/dist/control-plane.d.ts.map +1 -1
  25. package/dist/control-plane.js +8 -6
  26. package/dist/control-plane.js.map +1 -1
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js.map +1 -1
  29. package/dist/event-processor-info.d.ts +4 -4
  30. package/dist/event-processor-info.d.ts.map +1 -1
  31. package/dist/flow-controlled-sender.d.ts +2 -2
  32. package/dist/flow-controlled-sender.d.ts.map +1 -1
  33. package/dist/flow-controlled-sender.js.map +1 -1
  34. package/dist/generated/command.d.ts +20 -20
  35. package/dist/generated/command.d.ts.map +1 -1
  36. package/dist/generated/command.js.map +1 -1
  37. package/dist/generated/common.d.ts +20 -20
  38. package/dist/generated/common.d.ts.map +1 -1
  39. package/dist/generated/common.js.map +1 -1
  40. package/dist/generated/control.d.ts +36 -40
  41. package/dist/generated/control.d.ts.map +1 -1
  42. package/dist/generated/control.js.map +1 -1
  43. package/dist/generated/dcb.d.ts +88 -94
  44. package/dist/generated/dcb.d.ts.map +1 -1
  45. package/dist/generated/dcb.js.map +1 -1
  46. package/dist/generated/event.d.ts +81 -84
  47. package/dist/generated/event.d.ts.map +1 -1
  48. package/dist/generated/event.js.map +1 -1
  49. package/dist/generated/google/protobuf/empty.d.ts +3 -4
  50. package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
  51. package/dist/generated/google/protobuf/empty.js.map +1 -1
  52. package/dist/generated/query.d.ts +40 -40
  53. package/dist/generated/query.d.ts.map +1 -1
  54. package/dist/generated/query.js.map +1 -1
  55. package/dist/index.d.ts +4 -6
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -5
  58. package/dist/index.js.map +1 -1
  59. package/dist/message-size.d.ts +2 -2
  60. package/dist/message-size.d.ts.map +1 -1
  61. package/dist/metadata-conversion.d.ts +1 -1
  62. package/dist/metadata-conversion.d.ts.map +1 -1
  63. package/dist/outbound-stream.d.ts +2 -2
  64. package/dist/outbound-stream.d.ts.map +1 -1
  65. package/dist/platform-service.d.ts +4 -4
  66. package/dist/platform-service.d.ts.map +1 -1
  67. package/dist/resilience.d.ts +62 -0
  68. package/dist/resilience.d.ts.map +1 -0
  69. package/dist/resilience.js +103 -0
  70. package/dist/resilience.js.map +1 -0
  71. package/dist/shutdown-latch.d.ts +4 -4
  72. package/dist/shutdown-latch.d.ts.map +1 -1
  73. package/dist/shutdown-latch.js.map +1 -1
  74. package/package.json +5 -9
  75. package/src/axon-server-event-store.ts +97 -57
  76. package/src/axon-server-snapshotting-event-store.ts +191 -0
  77. package/src/axon-server.ts +365 -524
  78. package/src/connection-manager.ts +1 -1
  79. package/src/connection.ts +274 -20
  80. package/src/context-view.ts +46 -0
  81. package/src/control-plane.ts +19 -19
  82. package/src/event-processor-info.ts +2 -2
  83. package/src/flow-controlled-sender.ts +1 -1
  84. package/src/generated/command.ts +10 -10
  85. package/src/generated/common.ts +10 -10
  86. package/src/generated/control.ts +20 -20
  87. package/src/generated/dcb.ts +47 -47
  88. package/src/generated/event.ts +42 -42
  89. package/src/generated/google/protobuf/empty.ts +2 -2
  90. package/src/generated/query.ts +20 -20
  91. package/src/index.ts +12 -18
  92. package/src/message-size.ts +1 -1
  93. package/src/metadata-conversion.ts +1 -1
  94. package/src/outbound-stream.ts +1 -1
  95. package/src/platform-service.ts +2 -2
  96. package/src/resilience.ts +149 -0
  97. package/src/shutdown-latch.ts +2 -2
  98. package/dist/axon-server-snapshot-store.d.ts +0 -12
  99. package/dist/axon-server-snapshot-store.d.ts.map +0 -1
  100. package/dist/axon-server-snapshot-store.js +0 -88
  101. package/dist/axon-server-snapshot-store.js.map +0 -1
  102. package/src/axon-server-snapshot-store.ts +0 -118
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Connection resilience — PACKAGE-PRIVATE.
3
+ *
4
+ * Exponential backoff with full jitter, per-event attempt caps, terminal-error
5
+ * short-circuit, and a warn-then-continue health probe. Nothing here is
6
+ * exported from this package's barrel.
7
+ *
8
+ * It used to live in `@kronos-ts/core`, and it does not belong there. Core is
9
+ * only what cannot be a helper, and this is a helper by every test: it touches
10
+ * no core shape, no message, no unit of work — it is `setTimeout` and a loop
11
+ * over a function anybody could have written. Its only consumers were the
12
+ * transports and the pool, core's own `src/` never called it, and a helper
13
+ * lives with its users. Same reasoning that moved each persistence package's
14
+ * transaction glue out of core, applied one folder up.
15
+ *
16
+ * Each consuming package keeps its own copy, carrying only what it uses, so
17
+ * each can also state its own retry policy without negotiating with the others.
18
+ */
19
+ const DEFAULTS = {
20
+ initialDelayMs: 100,
21
+ maxDelayMs: 30_000,
22
+ maxAttempts: 10,
23
+ multiplier: 2,
24
+ healthCheckThresholdMs: 5_000,
25
+ };
26
+ const ATTEMPTS_BY_EVENT = {
27
+ "initial-connect": 10,
28
+ "reconnect": 30,
29
+ "per-operation": 5,
30
+ "health-check": 3,
31
+ };
32
+ function resolveConfig(event, partial) {
33
+ return {
34
+ ...DEFAULTS,
35
+ maxAttempts: ATTEMPTS_BY_EVENT[event],
36
+ ...partial,
37
+ };
38
+ }
39
+ function fullJitterDelay(attempt, cfg) {
40
+ const exponential = Math.min(cfg.initialDelayMs * Math.pow(cfg.multiplier, attempt), cfg.maxDelayMs);
41
+ return Math.random() * exponential;
42
+ }
43
+ /**
44
+ * Run `fn` with exponential-backoff full-jitter retries.
45
+ *
46
+ * Resolves with the first successful result, or rejects with the last error
47
+ * after `maxAttempts` exhausted. If `isRetryable(err)` returns false on any
48
+ * attempt, the error is rethrown immediately (terminal error short-circuit).
49
+ *
50
+ * D-101: opts may carry caller-supplied resilience overrides — this is the
51
+ * per-extension knob path (`kronosDbConnection({ resilience: { ... } })`).
52
+ */
53
+ export async function withRetry(fn, opts) {
54
+ const cfg = resolveConfig(opts.event, opts);
55
+ let lastErr;
56
+ for (let attempt = 0; attempt < cfg.maxAttempts; attempt++) {
57
+ try {
58
+ return await fn();
59
+ }
60
+ catch (err) {
61
+ lastErr = err;
62
+ if (cfg.isRetryable && !cfg.isRetryable(err))
63
+ throw err;
64
+ if (attempt === cfg.maxAttempts - 1)
65
+ break;
66
+ const delay = fullJitterDelay(attempt, cfg);
67
+ cfg.log?.(`[kronos:resilience] ${opts.event} attempt ${attempt + 1}/${cfg.maxAttempts} ` +
68
+ `failed; retrying in ${Math.round(delay)}ms — ${err?.message ?? String(err)}`);
69
+ await new Promise((r) => setTimeout(r, delay));
70
+ }
71
+ }
72
+ throw lastErr;
73
+ }
74
+ /**
75
+ * Run a health-check probe with warn-then-continue semantics (D-100).
76
+ *
77
+ * Resolves WITHOUT throwing on success OR after `thresholdMs` elapses with
78
+ * the probe still pending — production loops should NEVER stall on a flaky
79
+ * health probe. Failures (rejection or threshold breach) emit a single warn
80
+ * via `log` and otherwise resolve normally.
81
+ */
82
+ export async function healthCheck(fn, opts = {}) {
83
+ const threshold = opts.thresholdMs ?? DEFAULTS.healthCheckThresholdMs;
84
+ const log = opts.log ?? ((m) => console.warn(m));
85
+ let timer;
86
+ try {
87
+ await Promise.race([
88
+ fn(),
89
+ new Promise((_, reject) => {
90
+ timer = setTimeout(() => reject(new Error("health-check threshold exceeded")), threshold);
91
+ }),
92
+ ]);
93
+ }
94
+ catch (err) {
95
+ log(`[kronos:resilience] health-check soft-failed (${err?.message ?? String(err)}); continuing`);
96
+ // warn-then-continue: do NOT rethrow.
97
+ }
98
+ finally {
99
+ if (timer)
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+ //# sourceMappingURL=resilience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience.js","sourceRoot":"","sources":["../src/resilience.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * This is the TypeScript equivalent of AF5's ShutdownLatch pattern.
12
12
  */
13
- export interface ShutdownLatch {
13
+ export type ShutdownLatch = {
14
14
  /**
15
15
  * Register an in-flight activity. Throws if shutdown is in progress.
16
16
  * Call `end()` on the returned handle when the activity completes.
@@ -26,11 +26,11 @@ export interface ShutdownLatch {
26
26
  readonly shuttingDown: boolean;
27
27
  /** Number of currently in-flight activities. */
28
28
  readonly activeCount: number;
29
- }
30
- export interface ActivityHandle {
29
+ };
30
+ export type ActivityHandle = {
31
31
  /** Mark this activity as complete. */
32
32
  end(): void;
33
- }
33
+ };
34
34
  export declare class ShutdownInProgressError extends Error {
35
35
  constructor(message?: string);
36
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"shutdown-latch.d.ts","sourceRoot":"","sources":["../src/shutdown-latch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,gBAAgB,IAAI,cAAc,CAAA;IAElC;;;;OAIG;IACH,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAE9B,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,GAAG,IAAI,IAAI,CAAA;CACZ;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,GAAE,MAA+B;CAIrD;AAED,wBAAgB,aAAa,IAAI,aAAa,CAmD7C"}
1
+ {"version":3,"file":"shutdown-latch.d.ts","sourceRoot":"","sources":["../src/shutdown-latch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,gBAAgB,IAAI,cAAc,CAAA;IAElC;;;;OAIG;IACH,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAE9B,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,sCAAsC;IACtC,GAAG,IAAI,IAAI,CAAA;CACZ,CAAA;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAO,GAAE,MAA+B,EAGnD;CACF;AAED,wBAAgB,aAAa,IAAI,aAAa,CAmD7C"}
@@ -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,UAAkB,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":"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"}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@kronos-ts/axon-server",
3
- "version": "0.3.2",
3
+ "version": "0.5.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",
7
7
  "author": "Theo Emanuelsson",
8
- "homepage": "https://github.com/KronosDB/kronos-ts/tree/main/packages/extensions/axon-server#readme",
8
+ "homepage": "https://github.com/KronosDB/kronos-ts/tree/main/packages/axon-server#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/KronosDB/kronos-ts.git",
12
- "directory": "packages/extensions/axon-server"
12
+ "directory": "packages/axon-server"
13
13
  },
14
14
  "bugs": {
15
15
  "url": "https://github.com/KronosDB/kronos-ts/issues"
@@ -33,7 +33,7 @@
33
33
  "!src/**/*.bench.ts"
34
34
  ],
35
35
  "scripts": {
36
- "generate-proto": "grpc_tools_node_protoc --ts_proto_out=./src/generated --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,esModuleInterop=true --proto_path=./proto ./proto/*.proto",
36
+ "generate-proto": "grpc_tools_node_protoc --ts_proto_out=./src/generated --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,esModuleInterop=true --proto_path=./proto ./proto/*.proto && bun ../../scripts/interface-to-type/convert.ts ./src/generated",
37
37
  "build": "tsc -p tsconfig.json",
38
38
  "clean": "rm -rf dist *.tsbuildinfo"
39
39
  },
@@ -49,11 +49,7 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@kronos-ts/common": "0.1.2",
53
- "@kronos-ts/app": "0.6.2",
54
- "@kronos-ts/eventsourcing": "0.4.2",
55
- "@kronos-ts/messaging": "0.11.0",
56
- "@kronos-ts/modelling": "0.3.2",
52
+ "@kronos-ts/core": "0.3.0",
57
53
  "@bufbuild/protobuf": "^2.3",
58
54
  "@grpc/grpc-js": "^1.12",
59
55
  "nice-grpc": "^2.1",
@@ -3,15 +3,15 @@ import {
3
3
  qualifiedNameFromString,
4
4
  type Tag,
5
5
  type Serializer,
6
- } from "@kronos-ts/common"
6
+ } from "@kronos-ts/core"
7
7
  import type {
8
8
  EventCriteria,
9
9
  EventMessage,
10
10
  MessageStream,
11
11
  SequencedEvent,
12
12
  StreamingCondition,
13
- } from "@kronos-ts/messaging"
14
- import { messageStream } from "@kronos-ts/messaging"
13
+ } from "@kronos-ts/core"
14
+ import { messageStream, compileQuery } from "@kronos-ts/core"
15
15
  import type {
16
16
  EventStore,
17
17
  SourcingResult,
@@ -19,11 +19,12 @@ import type {
19
19
  AppendCondition,
20
20
  ConsistencyMarker,
21
21
  AppendTransaction,
22
- } from "@kronos-ts/eventsourcing"
23
- import type { TrackingToken } from "@kronos-ts/messaging"
24
- import { globalSequenceToken, FIRST_TOKEN } from "@kronos-ts/messaging"
25
- import { markerAt, noMarker } from "@kronos-ts/eventsourcing"
26
- import type { AxonServerConnection } from "./connection.js"
22
+ } from "@kronos-ts/core"
23
+ import type { TrackingToken } from "@kronos-ts/core"
24
+ import { globalSequenceToken, FIRST_TOKEN } from "@kronos-ts/core"
25
+ import { markerAt, noMarker } from "@kronos-ts/core"
26
+ import type { AxonServerStoreSource } from "./connection.js"
27
+ import { contextView } from "./context-view.js"
27
28
  import type {
28
29
  Criterion,
29
30
  TagsAndNamesCriterion,
@@ -32,7 +33,6 @@ import type {
32
33
  Event as ProtoEvent,
33
34
  SourceEventsResponse,
34
35
  } from "./generated/dcb.js"
35
- import { Metadata } from "nice-grpc"
36
36
 
37
37
  // ---------------------------------------------------------------------------
38
38
  // Criteria conversion — framework EventCriteria → proto Criterion[]
@@ -57,24 +57,26 @@ function tagFromProto(tag: ProtoTag): Tag {
57
57
  function criteriaToCriterions(criteria: EventCriteria): Criterion[] {
58
58
  switch (criteria.kind) {
59
59
  case "tags":
60
- return [{
61
- tagsAndNames: {
62
- name: [],
63
- tag: criteria.tags.map(tagToProto),
60
+ return [
61
+ {
62
+ tagsAndNames: {
63
+ name: [],
64
+ tag: criteria.tags.map(tagToProto),
65
+ },
64
66
  },
65
- }]
67
+ ]
66
68
 
67
69
  case "type-restricted":
68
70
  // Inner must be tags or any-tag
69
- const innerTags = criteria.inner.kind === "tags"
70
- ? criteria.inner.tags.map(tagToProto)
71
- : []
72
- return [{
73
- tagsAndNames: {
74
- name: [...criteria.types],
75
- tag: innerTags,
71
+ const innerTags = criteria.inner.kind === "tags" ? criteria.inner.tags.map(tagToProto) : []
72
+ return [
73
+ {
74
+ tagsAndNames: {
75
+ name: [...criteria.types],
76
+ tag: innerTags,
77
+ },
76
78
  },
77
- }]
79
+ ]
78
80
 
79
81
  case "either":
80
82
  // Flatten all sub-criteria into a list of criterions (OR semantics)
@@ -111,9 +113,14 @@ function createEventConverters(serializer: Serializer) {
111
113
  },
112
114
 
113
115
  eventFromProto(protoEvent: ProtoEvent, tags: ProtoTag[]): EventMessage {
114
- const payload = protoEvent.payload.length > 0
115
- ? serializer.deserialize({ data: protoEvent.payload, type: protoEvent.name, revision: protoEvent.version })
116
- : {}
116
+ const payload =
117
+ protoEvent.payload.length > 0
118
+ ? serializer.deserialize({
119
+ data: protoEvent.payload,
120
+ type: protoEvent.name,
121
+ revision: protoEvent.version,
122
+ })
123
+ : {}
117
124
 
118
125
  const metadata: Record<string, unknown> = {}
119
126
  for (const [k, v] of Object.entries(protoEvent.metadata)) {
@@ -139,27 +146,37 @@ function createEventConverters(serializer: Serializer) {
139
146
  // ---------------------------------------------------------------------------
140
147
 
141
148
  /**
142
- * Creates an EventStore implementation backed by Axon Server's DCB event store.
149
+ * An EventStore over Axon Server's DCB event store, in one context.
143
150
  *
144
151
  * This bridges the framework's EventStore interface to the gRPC
145
152
  * DcbEventStore service, handling conversion between framework types
146
153
  * and proto messages.
147
154
  *
155
+ * `context` is Axon Server's tenancy boundary and is a per-call header, so
156
+ * several contexts share the one channel `conn` holds — see `contextView`. The
157
+ * serializer is the connection's: one codec for everything this client puts on
158
+ * the wire.
159
+ *
148
160
  * The {@link open} method returns a persistent {@link MessageStream} backed by
149
161
  * a single gRPC Stream RPC call that stays open indefinitely, aligned with
150
162
  * Java's infinite {@code ResultStream}.
151
163
  */
152
- export function axonServerEventStore(connection: AxonServerConnection, serializer: Serializer): EventStore {
153
- const { eventToProto, eventFromProto } = createEventConverters(serializer)
164
+ /**
165
+ * Axon Server's way of saying "there is nothing at or after that position".
166
+ *
167
+ * Matched on the gRPC status code first — `OUT_OF_RANGE` is 11 — with the
168
+ * server's own message as a second, narrower gate, so an unrelated future
169
+ * OUT_OF_RANGE is not swallowed along with it.
170
+ */
171
+ function isStartPastHead(err: unknown): boolean {
172
+ const code = (err as { code?: unknown }).code
173
+ const details = String((err as { details?: unknown }).details ?? "")
174
+ return code === 11 && details.includes("Start sequence cannot be larger than end sequence")
175
+ }
154
176
 
155
- function createAxonMetadata(): Metadata {
156
- const axonMetadata = new Metadata()
157
- axonMetadata.set("AxonIQ-Context", connection.config.context)
158
- if (connection.config.token) {
159
- axonMetadata.set("AxonIQ-Access-Token", connection.config.token)
160
- }
161
- return axonMetadata
162
- }
177
+ export function axonServerEventStore(conn: AxonServerStoreSource, context: string): EventStore {
178
+ const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
179
+ const { eventToProto, eventFromProto } = createEventConverters(serializer)
163
180
 
164
181
  // Push-based subscriber registry (EventBus.subscribe contract). Axon Server's
165
182
  // own distribution is the server-side Stream RPC (see open()); these in-process
@@ -177,31 +194,50 @@ export function axonServerEventStore(connection: AxonServerConnection, serialize
177
194
 
178
195
  return {
179
196
  async source(condition: SourcingCondition): Promise<SourcingResult> {
180
- const criterions = criteriaToCriterions(condition.criteria)
197
+ const criterions = criteriaToCriterions(compileQuery(condition.query))
198
+ const start = condition.start ?? 0n
181
199
 
182
200
  const request = {
183
- fromSequence: condition.start ?? 0n,
201
+ fromSequence: start,
184
202
  criterion: criterions,
185
203
  }
186
204
 
187
205
  const events: EventMessage[] = []
188
206
  let marker: ConsistencyMarker = noMarker()
189
207
 
190
- const stream = connection.eventStore.source(request, { metadata: createAxonMetadata() })
191
- for await (const response of stream) {
192
- if (response.event) {
193
- const taggedEvent = response.event
194
- const protoEvent = taggedEvent.event
195
- if (protoEvent) {
196
- // DCB source/stream responses carry no tags — the server indexes
197
- // them write-side but does not echo them back (SequencedEvent has
198
- // only sequence + event). Reconstructed events get empty tags.
199
- events.push(eventFromProto(protoEvent, []))
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)
200
223
  }
201
224
  }
202
- if (response.consistencyMarker !== undefined) {
203
- marker = markerAt(response.consistencyMarker)
204
- }
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() }
205
241
  }
206
242
 
207
243
  return { events, marker }
@@ -213,10 +249,12 @@ export function axonServerEventStore(connection: AxonServerConnection, serialize
213
249
  ): Promise<AppendTransaction> {
214
250
  const taggedEvents = newEvents.map(eventToProto)
215
251
  const request = {
216
- condition: condition ? {
217
- consistencyMarker: condition.marker.position,
218
- criterion: criteriaToCriterions(condition.criteria),
219
- } : undefined,
252
+ condition: condition
253
+ ? {
254
+ consistencyMarker: condition.marker.position,
255
+ criterion: criteriaToCriterions(compileQuery(condition.query)),
256
+ }
257
+ : undefined,
220
258
  event: taggedEvents,
221
259
  }
222
260
 
@@ -229,7 +267,9 @@ export function axonServerEventStore(connection: AxonServerConnection, serialize
229
267
  async function* requestStream() {
230
268
  yield request
231
269
  }
232
- const response = await connection.eventStore.append(requestStream(), { metadata: createAxonMetadata() })
270
+ const response = await connection.eventStore.append(requestStream(), {
271
+ metadata: createAxonMetadata(),
272
+ })
233
273
  responseMarker = response.consistencyMarker
234
274
  await notifySubscribers(newEvents)
235
275
  },
@@ -252,7 +292,7 @@ export function axonServerEventStore(connection: AxonServerConnection, serialize
252
292
  },
253
293
 
254
294
  open(condition: StreamingCondition): MessageStream<SequencedEvent> {
255
- const criterions = condition.criteria ? criteriaToCriterions(condition.criteria) : []
295
+ const criterions = condition.query ? criteriaToCriterions(compileQuery(condition.query)) : []
256
296
 
257
297
  const request = {
258
298
  fromSequence: condition.position,
@@ -0,0 +1,191 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AXON SERVER SNAPSHOTS — the capability tier, served by `DcbSnapshotStore`.
3
+ //
4
+ // THE PROBE, AND WHAT IT SETTLED. Axon Server's public API defines both a
5
+ // snapshot STORE service (`DcbSnapshotStore`: Add / Delete / List / GetLast)
6
+ // and, on newer API drops, a fused `SnapshottedDcbEventStore.Source` whose
7
+ // stream leads with the snapshot. Only the first of those is actually served:
8
+ //
9
+ // axoniq/axonserver:2025.2.5 DcbSnapshotStore/GetLast → answers
10
+ // SnapshottedDcbEventStore/Source → UNIMPLEMENTED
11
+ // axoniq/axonserver:2026.0.4 same, both ways
12
+ //
13
+ // So there is no server-side fusion to call here — there is nothing on the
14
+ // other end of the wire. The READ is therefore fused CLIENT-SIDE: `GetLast` for
15
+ // the entry, then a source after its position, assembled into the one
16
+ // `SourcingResult` a fold expects. Two round trips, the same answers, correct
17
+ // today.
18
+ //
19
+ // When a server version does serve `SnapshottedDcbEventStore.Source`, the fused
20
+ // call lands INSIDE this function — `source` stops making two calls and starts
21
+ // making one — and no host changes a line, because the capability was never a
22
+ // promise about round trips.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ import type {
26
+ EventStore,
27
+ Snapshot,
28
+ SnapshotCapability,
29
+ SourcingCondition,
30
+ SourcingResult,
31
+ } from "@kronos-ts/core"
32
+ import type { Serializer } from "@kronos-ts/core"
33
+ import { withoutSnapshotKey } from "@kronos-ts/core"
34
+ import type { AxonServerStoreSource } from "./connection.js"
35
+ import { contextView } from "./context-view.js"
36
+ import type { Snapshot as ProtoSnapshot } from "./generated/dcb.js"
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Conversion — framework Snapshot ↔ proto Snapshot
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const encoder = new TextEncoder()
43
+
44
+ /**
45
+ * The proto message carries `name`, `version`, `timestamp` and a metadata map
46
+ * the narrowed seam has no equivalent for. `name` carries the cache key,
47
+ * `timestamp` is filled at write time for operators, and `version` stays EMPTY
48
+ * — snapshots have no versions, and inventing one to fill a proto field would
49
+ * be exactly the bookkeeping this design removed. The metadata map stays empty
50
+ * too: a cache entry carries no cargo.
51
+ */
52
+ function createSnapshotConverters(serializer: Serializer) {
53
+ return {
54
+ snapshotToProto(name: string, snapshot: Snapshot): ProtoSnapshot {
55
+ const serialized = serializer.serialize(snapshot.state, name, "")
56
+ return {
57
+ name,
58
+ version: "",
59
+ payload: serialized.data,
60
+ timestamp: BigInt(Date.now()),
61
+ metadata: {},
62
+ }
63
+ },
64
+
65
+ snapshotFromProto(name: string, proto: ProtoSnapshot, position: bigint): Snapshot {
66
+ const state =
67
+ proto.payload.length > 0
68
+ ? serializer.deserialize({ data: proto.payload, type: name, revision: "" })
69
+ : {}
70
+
71
+ return { state, position }
72
+ },
73
+ }
74
+ }
75
+
76
+ function encodeKey(key: string): Uint8Array {
77
+ return encoder.encode(key)
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Axon Server snapshot store
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Add the snapshotting capability to an Axon Server-backed log.
86
+ *
87
+ * ```ts
88
+ * const eventStore = axonServerSnapshottingEventStore(
89
+ * axonServerEventStore(axon, "default"),
90
+ * axon,
91
+ * "default",
92
+ * )
93
+ * ```
94
+ *
95
+ * ADDITIVE, NOT COLLAPSING. It returns `E & SnapshotCapability` — the store you
96
+ * passed in, plus the write — so capabilities stack in either order and nothing
97
+ * the inner store carried is laundered on the way through.
98
+ *
99
+ * `context` is a per-call header, so this shares the one channel `conn` holds
100
+ * with every other context — see `contextView`. Payloads go through the
101
+ * connection's serializer.
102
+ *
103
+ * LATEST-ONLY, over a service that is not. `DcbSnapshotStore` keeps a sequence
104
+ * of snapshots per key; the capability keeps one. `add` therefore sets
105
+ * `prune: true`, so writing an entry retires the ones before it, and `getLast`
106
+ * is the only read. That is the narrowing doing its job: the framework programs
107
+ * against the cache it needs, not against everything the backend happens to
108
+ * offer.
109
+ */
110
+ export function axonServerSnapshottingEventStore<E extends EventStore>(
111
+ next: E,
112
+ conn: AxonServerStoreSource,
113
+ context: string,
114
+ ): E & SnapshotCapability {
115
+ const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
116
+ const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
117
+
118
+ /** The cached fold filed under `key`, or nothing — the first of the two calls. */
119
+ async function loadSnapshot(key: string): Promise<Snapshot | undefined> {
120
+ try {
121
+ const response = await connection.snapshotStore.getLast(
122
+ { key: encodeKey(key) },
123
+ { metadata: createAxonMetadata() },
124
+ )
125
+
126
+ if (!response.snapshot) {
127
+ return undefined
128
+ }
129
+
130
+ return snapshotFromProto(key, response.snapshot, response.sequence)
131
+ } catch (err) {
132
+ // Axon Server throws rather than answering empty when no snapshot
133
+ // exists — and a cache miss is not an error to anybody upstream.
134
+ if (
135
+ String(err).includes("No snapshot found") ||
136
+ String(err).includes("not found")
137
+ ) {
138
+ return undefined
139
+ }
140
+ throw err
141
+ }
142
+ }
143
+
144
+ return {
145
+ ...next,
146
+
147
+ async storeSnapshot(key: string, snapshot: Snapshot): Promise<void> {
148
+ await connection.snapshotStore.add(
149
+ {
150
+ key: encodeKey(key),
151
+ sequence: snapshot.position,
152
+ prune: true,
153
+ snapshot: snapshotToProto(key, snapshot),
154
+ },
155
+ { metadata: createAxonMetadata() },
156
+ )
157
+ },
158
+
159
+ /**
160
+ * THE CLIENT-SIDE FUSION. Two calls, assembled into the one result a fold
161
+ * expects — and the cache is NEVER LOAD-BEARING, so a miss or an outright
162
+ * throw from the snapshot service both fall through to a full read.
163
+ */
164
+ async source(condition: SourcingCondition): Promise<SourcingResult> {
165
+ const key = condition.snapshot
166
+ if (key === undefined) return next.source(condition)
167
+
168
+ // The strategy is CONSUMED here; the store below gets a plain condition.
169
+ const plain = withoutSnapshotKey(condition)
170
+
171
+ let snapshot: Snapshot | undefined
172
+ try {
173
+ snapshot = await loadSnapshot(key.key)
174
+ } catch {
175
+ // A cache you cannot reach is a cache miss. Loads stay correct; they
176
+ // just cost what they always cost.
177
+ return next.source(plain)
178
+ }
179
+ if (snapshot === undefined) return next.source(plain)
180
+
181
+ // Resume AFTER the position the snapshot already folded; a condition that
182
+ // independently asked to start later keeps its own floor.
183
+ const resumeFrom = snapshot.position + 1n
184
+ const start =
185
+ plain.start !== undefined && plain.start > resumeFrom ? plain.start : resumeFrom
186
+
187
+ const result = await next.source({ ...plain, start })
188
+ return { ...result, snapshot }
189
+ },
190
+ } as E & SnapshotCapability
191
+ }