@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.
- package/dist/axon-server-event-store.d.ts +0 -16
- package/dist/axon-server-event-store.d.ts.map +1 -1
- package/dist/axon-server-event-store.js +47 -14
- package/dist/axon-server-event-store.js.map +1 -1
- package/dist/axon-server-snapshotting-event-store.d.ts +30 -0
- package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -0
- package/dist/axon-server-snapshotting-event-store.js +153 -0
- package/dist/axon-server-snapshotting-event-store.js.map +1 -0
- package/dist/axon-server.d.ts +33 -60
- package/dist/axon-server.d.ts.map +1 -1
- package/dist/axon-server.js +58 -59
- package/dist/axon-server.js.map +1 -1
- package/dist/connection-manager.d.ts +2 -2
- package/dist/connection-manager.d.ts.map +1 -1
- package/dist/connection.d.ts +23 -21
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +6 -5
- package/dist/connection.js.map +1 -1
- package/dist/context-view.d.ts +2 -2
- package/dist/context-view.d.ts.map +1 -1
- package/dist/control-plane.d.ts +5 -5
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/event-processor-info.d.ts +4 -4
- package/dist/event-processor-info.d.ts.map +1 -1
- package/dist/flow-controlled-sender.d.ts +2 -2
- package/dist/flow-controlled-sender.d.ts.map +1 -1
- package/dist/generated/command.d.ts +20 -20
- package/dist/generated/command.d.ts.map +1 -1
- package/dist/generated/common.d.ts +20 -20
- package/dist/generated/common.d.ts.map +1 -1
- package/dist/generated/control.d.ts +36 -40
- package/dist/generated/control.d.ts.map +1 -1
- package/dist/generated/dcb.d.ts +88 -94
- package/dist/generated/dcb.d.ts.map +1 -1
- package/dist/generated/event.d.ts +81 -84
- package/dist/generated/event.d.ts.map +1 -1
- package/dist/generated/google/protobuf/empty.d.ts +3 -4
- package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
- package/dist/generated/query.d.ts +40 -40
- package/dist/generated/query.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/message-size.d.ts +2 -2
- package/dist/message-size.d.ts.map +1 -1
- package/dist/outbound-stream.d.ts +2 -2
- package/dist/outbound-stream.d.ts.map +1 -1
- package/dist/platform-service.d.ts +4 -4
- package/dist/platform-service.d.ts.map +1 -1
- package/dist/resilience.d.ts +62 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +103 -0
- package/dist/resilience.js.map +1 -0
- package/dist/shutdown-latch.d.ts +4 -4
- package/dist/shutdown-latch.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/axon-server-event-store.ts +46 -14
- package/src/axon-server-snapshotting-event-store.ts +191 -0
- package/src/axon-server.ts +72 -77
- package/src/connection-manager.ts +1 -1
- package/src/connection.ts +16 -14
- package/src/context-view.ts +1 -1
- package/src/control-plane.ts +3 -3
- package/src/event-processor-info.ts +2 -2
- package/src/flow-controlled-sender.ts +1 -1
- package/src/generated/command.ts +10 -10
- package/src/generated/common.ts +10 -10
- package/src/generated/control.ts +20 -20
- package/src/generated/dcb.ts +47 -47
- package/src/generated/event.ts +42 -42
- package/src/generated/google/protobuf/empty.ts +2 -2
- package/src/generated/query.ts +20 -20
- package/src/index.ts +1 -1
- package/src/message-size.ts +1 -1
- package/src/outbound-stream.ts +1 -1
- package/src/platform-service.ts +2 -2
- package/src/resilience.ts +149 -0
- package/src/shutdown-latch.ts +2 -2
- package/dist/axon-server-snapshot-store.d.ts +0 -12
- package/dist/axon-server-snapshot-store.d.ts.map +0 -1
- package/dist/axon-server-snapshot-store.js +0 -82
- package/dist/axon-server-snapshot-store.js.map +0 -1
- 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
|
+
}
|
package/src/shutdown-latch.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* This is the TypeScript equivalent of AF5's ShutdownLatch pattern.
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
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
|
|
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
|
-
}
|