@kronos-ts/axon-server 0.4.0 → 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 (84) hide show
  1. package/dist/axon-server-event-store.d.ts +0 -16
  2. package/dist/axon-server-event-store.d.ts.map +1 -1
  3. package/dist/axon-server-event-store.js +47 -14
  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 +33 -60
  10. package/dist/axon-server.d.ts.map +1 -1
  11. package/dist/axon-server.js +58 -59
  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 +23 -21
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +6 -5
  18. package/dist/connection.js.map +1 -1
  19. package/dist/context-view.d.ts +2 -2
  20. package/dist/context-view.d.ts.map +1 -1
  21. package/dist/control-plane.d.ts +5 -5
  22. package/dist/control-plane.d.ts.map +1 -1
  23. package/dist/event-processor-info.d.ts +4 -4
  24. package/dist/event-processor-info.d.ts.map +1 -1
  25. package/dist/flow-controlled-sender.d.ts +2 -2
  26. package/dist/flow-controlled-sender.d.ts.map +1 -1
  27. package/dist/generated/command.d.ts +20 -20
  28. package/dist/generated/command.d.ts.map +1 -1
  29. package/dist/generated/common.d.ts +20 -20
  30. package/dist/generated/common.d.ts.map +1 -1
  31. package/dist/generated/control.d.ts +36 -40
  32. package/dist/generated/control.d.ts.map +1 -1
  33. package/dist/generated/dcb.d.ts +88 -94
  34. package/dist/generated/dcb.d.ts.map +1 -1
  35. package/dist/generated/event.d.ts +81 -84
  36. package/dist/generated/event.d.ts.map +1 -1
  37. package/dist/generated/google/protobuf/empty.d.ts +3 -4
  38. package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
  39. package/dist/generated/query.d.ts +40 -40
  40. package/dist/generated/query.d.ts.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/message-size.d.ts +2 -2
  46. package/dist/message-size.d.ts.map +1 -1
  47. package/dist/outbound-stream.d.ts +2 -2
  48. package/dist/outbound-stream.d.ts.map +1 -1
  49. package/dist/platform-service.d.ts +4 -4
  50. package/dist/platform-service.d.ts.map +1 -1
  51. package/dist/resilience.d.ts +62 -0
  52. package/dist/resilience.d.ts.map +1 -0
  53. package/dist/resilience.js +103 -0
  54. package/dist/resilience.js.map +1 -0
  55. package/dist/shutdown-latch.d.ts +4 -4
  56. package/dist/shutdown-latch.d.ts.map +1 -1
  57. package/package.json +3 -3
  58. package/src/axon-server-event-store.ts +46 -14
  59. package/src/axon-server-snapshotting-event-store.ts +191 -0
  60. package/src/axon-server.ts +72 -77
  61. package/src/connection-manager.ts +1 -1
  62. package/src/connection.ts +16 -14
  63. package/src/context-view.ts +1 -1
  64. package/src/control-plane.ts +3 -3
  65. package/src/event-processor-info.ts +2 -2
  66. package/src/flow-controlled-sender.ts +1 -1
  67. package/src/generated/command.ts +10 -10
  68. package/src/generated/common.ts +10 -10
  69. package/src/generated/control.ts +20 -20
  70. package/src/generated/dcb.ts +47 -47
  71. package/src/generated/event.ts +42 -42
  72. package/src/generated/google/protobuf/empty.ts +2 -2
  73. package/src/generated/query.ts +20 -20
  74. package/src/index.ts +1 -1
  75. package/src/message-size.ts +1 -1
  76. package/src/outbound-stream.ts +1 -1
  77. package/src/platform-service.ts +2 -2
  78. package/src/resilience.ts +149 -0
  79. package/src/shutdown-latch.ts +2 -2
  80. package/dist/axon-server-snapshot-store.d.ts +0 -12
  81. package/dist/axon-server-snapshot-store.d.ts.map +0 -1
  82. package/dist/axon-server-snapshot-store.js +0 -82
  83. package/dist/axon-server-snapshot-store.js.map +0 -1
  84. package/src/axon-server-snapshot-store.ts +0 -110
@@ -0,0 +1,149 @@
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
+
20
+ /** Classified retry sites used by transport extensions (D-97). */
21
+ export type RetryEvent =
22
+ | "initial-connect"
23
+ | "reconnect"
24
+ | "per-operation"
25
+ | "health-check"
26
+
27
+ export type ResilienceConfig = {
28
+ /** Initial backoff delay (ms). Default: 100. */
29
+ initialDelayMs: number
30
+ /** Cap for backoff (ms). Default: 30_000. */
31
+ maxDelayMs: number
32
+ /** Max retry attempts before giving up. Per-event defaults applied if not provided. */
33
+ maxAttempts: number
34
+ /** Base for exponential growth. Default: 2. */
35
+ multiplier: number
36
+ /** Health-check threshold (ms) before warn-then-continue. Default: 5000. */
37
+ healthCheckThresholdMs: number
38
+ /** Optional logger. Defaults to console.warn. */
39
+ log?: (msg: string) => void
40
+ /** Per-attempt classification: returns false to short-circuit (terminal error). */
41
+ isRetryable?: (err: unknown) => boolean
42
+ }
43
+
44
+ const DEFAULTS: Omit<ResilienceConfig, "log" | "isRetryable"> = {
45
+ initialDelayMs: 100,
46
+ maxDelayMs: 30_000,
47
+ maxAttempts: 10,
48
+ multiplier: 2,
49
+ healthCheckThresholdMs: 5_000,
50
+ }
51
+
52
+ const ATTEMPTS_BY_EVENT: Record<RetryEvent, number> = {
53
+ "initial-connect": 10,
54
+ "reconnect": 30,
55
+ "per-operation": 5,
56
+ "health-check": 3,
57
+ }
58
+
59
+ function resolveConfig(
60
+ event: RetryEvent,
61
+ partial?: Partial<ResilienceConfig>,
62
+ ): ResilienceConfig {
63
+ return {
64
+ ...DEFAULTS,
65
+ maxAttempts: ATTEMPTS_BY_EVENT[event],
66
+ ...partial,
67
+ }
68
+ }
69
+
70
+ function fullJitterDelay(attempt: number, cfg: ResilienceConfig): number {
71
+ const exponential = Math.min(
72
+ cfg.initialDelayMs * Math.pow(cfg.multiplier, attempt),
73
+ cfg.maxDelayMs,
74
+ )
75
+ return Math.random() * exponential
76
+ }
77
+
78
+ /**
79
+ * Run `fn` with exponential-backoff full-jitter retries.
80
+ *
81
+ * Resolves with the first successful result, or rejects with the last error
82
+ * after `maxAttempts` exhausted. If `isRetryable(err)` returns false on any
83
+ * attempt, the error is rethrown immediately (terminal error short-circuit).
84
+ *
85
+ * D-101: opts may carry caller-supplied resilience overrides — this is the
86
+ * per-extension knob path (`kronosDbConnection({ resilience: { ... } })`).
87
+ */
88
+ export async function withRetry<T>(
89
+ fn: () => Promise<T>,
90
+ opts: { event: RetryEvent } & Partial<ResilienceConfig>,
91
+ ): Promise<T> {
92
+ const cfg = resolveConfig(opts.event, opts)
93
+ let lastErr: unknown
94
+ for (let attempt = 0; attempt < cfg.maxAttempts; attempt++) {
95
+ try {
96
+ return await fn()
97
+ } catch (err) {
98
+ lastErr = err
99
+ if (cfg.isRetryable && !cfg.isRetryable(err)) throw err
100
+ if (attempt === cfg.maxAttempts - 1) break
101
+ const delay = fullJitterDelay(attempt, cfg)
102
+ cfg.log?.(
103
+ `[kronos:resilience] ${opts.event} attempt ${attempt + 1}/${cfg.maxAttempts} ` +
104
+ `failed; retrying in ${Math.round(delay)}ms — ${
105
+ (err as Error)?.message ?? String(err)
106
+ }`,
107
+ )
108
+ await new Promise((r) => setTimeout(r, delay))
109
+ }
110
+ }
111
+ throw lastErr
112
+ }
113
+
114
+ /**
115
+ * Run a health-check probe with warn-then-continue semantics (D-100).
116
+ *
117
+ * Resolves WITHOUT throwing on success OR after `thresholdMs` elapses with
118
+ * the probe still pending — production loops should NEVER stall on a flaky
119
+ * health probe. Failures (rejection or threshold breach) emit a single warn
120
+ * via `log` and otherwise resolve normally.
121
+ */
122
+ export async function healthCheck(
123
+ fn: () => Promise<void>,
124
+ opts: { thresholdMs?: number; log?: (msg: string) => void } = {},
125
+ ): Promise<void> {
126
+ const threshold = opts.thresholdMs ?? DEFAULTS.healthCheckThresholdMs
127
+ const log = opts.log ?? ((m: string) => console.warn(m))
128
+ let timer: ReturnType<typeof setTimeout> | undefined
129
+ try {
130
+ await Promise.race([
131
+ fn(),
132
+ new Promise<never>((_, reject) => {
133
+ timer = setTimeout(
134
+ () => reject(new Error("health-check threshold exceeded")),
135
+ threshold,
136
+ )
137
+ }),
138
+ ])
139
+ } catch (err) {
140
+ log(
141
+ `[kronos:resilience] health-check soft-failed (${
142
+ (err as Error)?.message ?? String(err)
143
+ }); continuing`,
144
+ )
145
+ // warn-then-continue: do NOT rethrow.
146
+ } finally {
147
+ if (timer) clearTimeout(timer)
148
+ }
149
+ }
@@ -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.
@@ -31,7 +31,7 @@ export interface ShutdownLatch {
31
31
  readonly activeCount: number
32
32
  }
33
33
 
34
- export interface ActivityHandle {
34
+ export type ActivityHandle = {
35
35
  /** Mark this activity as complete. */
36
36
  end(): void
37
37
  }
@@ -1,12 +0,0 @@
1
- import type { SnapshotStore } from "@kronos-ts/core";
2
- import type { AxonServerStoreSource } from "./connection.js";
3
- /**
4
- * A SnapshotStore over Axon Server's gRPC snapshot service, in one context.
5
- *
6
- * Uses the `DcbSnapshotStore` gRPC service to store and retrieve state
7
- * snapshots. `context` is a per-call header, so this shares the one channel
8
- * `conn` holds with every other context — see `contextView`. Payloads go
9
- * through the connection's serializer.
10
- */
11
- export declare function axonServerSnapshotStore(conn: AxonServerStoreSource, context: string): SnapshotStore;
12
- //# sourceMappingURL=axon-server-snapshot-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"axon-server-snapshot-store.d.ts","sourceRoot":"","sources":["../src/axon-server-snapshot-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,iBAAiB,CAAA;AAE9D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAgD5D;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,MAAM,GACd,aAAa,CAgDf"}
@@ -1,82 +0,0 @@
1
- import { contextView } from "./context-view.js";
2
- // ---------------------------------------------------------------------------
3
- // Conversion — framework Snapshot ↔ proto Snapshot
4
- // ---------------------------------------------------------------------------
5
- const encoder = new TextEncoder();
6
- function createSnapshotConverters(serializer) {
7
- return {
8
- snapshotToProto(snapshot) {
9
- const serialized = serializer.serialize(snapshot.payload, "snapshot", "");
10
- return {
11
- name: "",
12
- version: "",
13
- payload: serialized.data,
14
- timestamp: BigInt(snapshot.timestamp),
15
- metadata: snapshot.metadata,
16
- };
17
- },
18
- snapshotFromProto(proto, position) {
19
- const payload = proto.payload.length > 0
20
- ? serializer.deserialize({ data: proto.payload, type: "snapshot", revision: "" })
21
- : {};
22
- return {
23
- position,
24
- payload,
25
- timestamp: Number(proto.timestamp),
26
- metadata: proto.metadata ?? {},
27
- };
28
- },
29
- };
30
- }
31
- function encodeKey(stateName, id) {
32
- const idStr = typeof id === "object" && id !== null ? JSON.stringify(id) : String(id);
33
- return encoder.encode(`${stateName}:${idStr}`);
34
- }
35
- // ---------------------------------------------------------------------------
36
- // Axon Server snapshot store
37
- // ---------------------------------------------------------------------------
38
- /**
39
- * A SnapshotStore over Axon Server's gRPC snapshot service, in one context.
40
- *
41
- * Uses the `DcbSnapshotStore` gRPC service to store and retrieve state
42
- * snapshots. `context` is a per-call header, so this shares the one channel
43
- * `conn` holds with every other context — see `contextView`. Payloads go
44
- * through the connection's serializer.
45
- */
46
- export function axonServerSnapshotStore(conn, context) {
47
- const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context);
48
- const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer);
49
- return {
50
- async store(stateName, id, snapshot) {
51
- await connection.snapshotStore.add({
52
- key: encodeKey(stateName, id),
53
- sequence: snapshot.position,
54
- prune: true,
55
- snapshot: snapshotToProto(snapshot),
56
- }, { metadata: createAxonMetadata() });
57
- },
58
- async load(stateName, id) {
59
- try {
60
- const response = await connection.snapshotStore.getLast({ key: encodeKey(stateName, id) }, { metadata: createAxonMetadata() });
61
- if (!response.snapshot) {
62
- return undefined;
63
- }
64
- return snapshotFromProto(response.snapshot, response.sequence);
65
- }
66
- catch (err) {
67
- // Axon Server throws when no snapshot exists — treat as "not found"
68
- if (String(err).includes("No snapshot found")) {
69
- return undefined;
70
- }
71
- throw err;
72
- }
73
- },
74
- async deleteSnapshots(stateName, id) {
75
- await connection.snapshotStore.delete({
76
- key: encodeKey(stateName, id),
77
- toSequence: BigInt(Number.MAX_SAFE_INTEGER),
78
- }, { metadata: createAxonMetadata() });
79
- },
80
- };
81
- }
82
- //# sourceMappingURL=axon-server-snapshot-store.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"axon-server-snapshot-store.js","sourceRoot":"","sources":["../src/axon-server-snapshot-store.ts"],"names":[],"mappings":"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"}
@@ -1,110 +0,0 @@
1
- import type { SnapshotStore, Snapshot } from "@kronos-ts/core"
2
- import type { Serializer } from "@kronos-ts/core"
3
- import type { AxonServerStoreSource } from "./connection.js"
4
- import { contextView } from "./context-view.js"
5
- import type { Snapshot as ProtoSnapshot } from "./generated/dcb.js"
6
-
7
- // ---------------------------------------------------------------------------
8
- // Conversion — framework Snapshot ↔ proto Snapshot
9
- // ---------------------------------------------------------------------------
10
-
11
- const encoder = new TextEncoder()
12
-
13
- function createSnapshotConverters(serializer: Serializer) {
14
- return {
15
- snapshotToProto(snapshot: Snapshot): ProtoSnapshot {
16
- const serialized = serializer.serialize(snapshot.payload, "snapshot", "")
17
- return {
18
- name: "",
19
- version: "",
20
- payload: serialized.data,
21
- timestamp: BigInt(snapshot.timestamp),
22
- metadata: snapshot.metadata,
23
- }
24
- },
25
-
26
- snapshotFromProto(proto: ProtoSnapshot, position: bigint): Snapshot {
27
- const payload =
28
- proto.payload.length > 0
29
- ? serializer.deserialize({ data: proto.payload, type: "snapshot", revision: "" })
30
- : {}
31
-
32
- return {
33
- position,
34
- payload,
35
- timestamp: Number(proto.timestamp),
36
- metadata: proto.metadata ?? {},
37
- }
38
- },
39
- }
40
- }
41
-
42
- function encodeKey(stateName: string, id: unknown): Uint8Array {
43
- const idStr = typeof id === "object" && id !== null ? JSON.stringify(id) : String(id)
44
- return encoder.encode(`${stateName}:${idStr}`)
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Axon Server snapshot store
49
- // ---------------------------------------------------------------------------
50
-
51
- /**
52
- * A SnapshotStore over Axon Server's gRPC snapshot service, in one context.
53
- *
54
- * Uses the `DcbSnapshotStore` gRPC service to store and retrieve state
55
- * snapshots. `context` is a per-call header, so this shares the one channel
56
- * `conn` holds with every other context — see `contextView`. Payloads go
57
- * through the connection's serializer.
58
- */
59
- export function axonServerSnapshotStore(
60
- conn: AxonServerStoreSource,
61
- context: string,
62
- ): SnapshotStore {
63
- const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
64
- const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
65
-
66
- return {
67
- async store(stateName: string, id: unknown, snapshot: Snapshot): Promise<void> {
68
- await connection.snapshotStore.add(
69
- {
70
- key: encodeKey(stateName, id),
71
- sequence: snapshot.position,
72
- prune: true,
73
- snapshot: snapshotToProto(snapshot),
74
- },
75
- { metadata: createAxonMetadata() },
76
- )
77
- },
78
-
79
- async load(stateName: string, id: unknown): Promise<Snapshot | undefined> {
80
- try {
81
- const response = await connection.snapshotStore.getLast(
82
- { key: encodeKey(stateName, id) },
83
- { metadata: createAxonMetadata() },
84
- )
85
-
86
- if (!response.snapshot) {
87
- return undefined
88
- }
89
-
90
- return snapshotFromProto(response.snapshot, response.sequence)
91
- } catch (err) {
92
- // Axon Server throws when no snapshot exists — treat as "not found"
93
- if (String(err).includes("No snapshot found")) {
94
- return undefined
95
- }
96
- throw err
97
- }
98
- },
99
-
100
- async deleteSnapshots(stateName: string, id: unknown): Promise<void> {
101
- await connection.snapshotStore.delete(
102
- {
103
- key: encodeKey(stateName, id),
104
- toSequence: BigInt(Number.MAX_SAFE_INTEGER),
105
- },
106
- { metadata: createAxonMetadata() },
107
- )
108
- },
109
- }
110
- }