@kronos-ts/axon-server 0.1.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 +16 -0
- package/dist/axon-server-event-store.d.ts.map +1 -0
- package/dist/axon-server-event-store.js +282 -0
- package/dist/axon-server-event-store.js.map +1 -0
- package/dist/axon-server-snapshot-store.d.ts +12 -0
- package/dist/axon-server-snapshot-store.d.ts.map +1 -0
- package/dist/axon-server-snapshot-store.js +88 -0
- package/dist/axon-server-snapshot-store.js.map +1 -0
- package/dist/axon-server.d.ts +115 -0
- package/dist/axon-server.d.ts.map +1 -0
- package/dist/axon-server.js +986 -0
- package/dist/axon-server.js.map +1 -0
- package/dist/connection-manager.d.ts +49 -0
- package/dist/connection-manager.d.ts.map +1 -0
- package/dist/connection-manager.js +37 -0
- package/dist/connection-manager.js.map +1 -0
- package/dist/connection.d.ts +129 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +130 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +189 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-processor-info.d.ts +35 -0
- package/dist/event-processor-info.d.ts.map +1 -0
- package/dist/event-processor-info.js +28 -0
- package/dist/event-processor-info.js.map +1 -0
- package/dist/flow-controlled-sender.d.ts +30 -0
- package/dist/flow-controlled-sender.d.ts.map +1 -0
- package/dist/flow-controlled-sender.js +60 -0
- package/dist/flow-controlled-sender.js.map +1 -0
- package/dist/generated/command.d.ts +158 -0
- package/dist/generated/command.d.ts.map +1 -0
- package/dist/generated/command.js +970 -0
- package/dist/generated/command.js.map +1 -0
- package/dist/generated/common.d.ts +130 -0
- package/dist/generated/common.d.ts.map +1 -0
- package/dist/generated/common.js +908 -0
- package/dist/generated/common.js.map +1 -0
- package/dist/generated/control.d.ts +293 -0
- package/dist/generated/control.d.ts.map +1 -0
- package/dist/generated/control.js +1938 -0
- package/dist/generated/control.js.map +1 -0
- package/dist/generated/dcb.d.ts +650 -0
- package/dist/generated/dcb.d.ts.map +1 -0
- package/dist/generated/dcb.js +2943 -0
- package/dist/generated/dcb.js.map +1 -0
- package/dist/generated/event.d.ts +667 -0
- package/dist/generated/event.d.ts.map +1 -0
- package/dist/generated/event.js +3185 -0
- package/dist/generated/event.js.map +1 -0
- package/dist/generated/google/protobuf/empty.d.ts +30 -0
- package/dist/generated/google/protobuf/empty.d.ts.map +1 -0
- package/dist/generated/google/protobuf/empty.js +46 -0
- package/dist/generated/google/protobuf/empty.js.map +1 -0
- package/dist/generated/query.d.ts +300 -0
- package/dist/generated/query.d.ts.map +1 -0
- package/dist/generated/query.js +2183 -0
- package/dist/generated/query.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/message-size.d.ts +38 -0
- package/dist/message-size.d.ts.map +1 -0
- package/dist/message-size.js +57 -0
- package/dist/message-size.js.map +1 -0
- package/dist/metadata-conversion.d.ts +11 -0
- package/dist/metadata-conversion.d.ts.map +1 -0
- package/dist/metadata-conversion.js +51 -0
- package/dist/metadata-conversion.js.map +1 -0
- package/dist/outbound-stream.d.ts +15 -0
- package/dist/outbound-stream.d.ts.map +1 -0
- package/dist/outbound-stream.js +39 -0
- package/dist/outbound-stream.js.map +1 -0
- package/dist/platform-service.d.ts +119 -0
- package/dist/platform-service.d.ts.map +1 -0
- package/dist/platform-service.js +250 -0
- package/dist/platform-service.js.map +1 -0
- package/dist/shutdown-latch.d.ts +38 -0
- package/dist/shutdown-latch.d.ts.map +1 -0
- package/dist/shutdown-latch.js +51 -0
- package/dist/shutdown-latch.js.map +1 -0
- package/package.json +69 -0
- package/src/axon-server-event-store.ts +358 -0
- package/src/axon-server-snapshot-store.ts +118 -0
- package/src/axon-server.ts +1202 -0
- package/src/connection-manager.ts +88 -0
- package/src/connection.ts +272 -0
- package/src/errors.ts +223 -0
- package/src/event-processor-info.ts +62 -0
- package/src/flow-controlled-sender.ts +91 -0
- package/src/generated/command.ts +1231 -0
- package/src/generated/common.ts +1097 -0
- package/src/generated/control.ts +2419 -0
- package/src/generated/dcb.ts +3826 -0
- package/src/generated/event.ts +4076 -0
- package/src/generated/google/protobuf/empty.ts +84 -0
- package/src/generated/query.ts +2723 -0
- package/src/index.ts +75 -0
- package/src/message-size.ts +75 -0
- package/src/metadata-conversion.ts +46 -0
- package/src/outbound-stream.ts +52 -0
- package/src/platform-service.ts +361 -0
- package/src/shutdown-latch.ts +97 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
qualifiedNameToString,
|
|
3
|
+
qualifiedNameFromString,
|
|
4
|
+
type Tag,
|
|
5
|
+
type Serializer,
|
|
6
|
+
} from "@kronos-ts/common"
|
|
7
|
+
import type {
|
|
8
|
+
EventCriteria,
|
|
9
|
+
EventMessage,
|
|
10
|
+
MessageStream,
|
|
11
|
+
SequencedEvent,
|
|
12
|
+
StreamingCondition,
|
|
13
|
+
} from "@kronos-ts/messaging"
|
|
14
|
+
import { createMessageStream } from "@kronos-ts/messaging"
|
|
15
|
+
import type {
|
|
16
|
+
EventStore,
|
|
17
|
+
SourcingResult,
|
|
18
|
+
SourcingCondition,
|
|
19
|
+
AppendCondition,
|
|
20
|
+
ConsistencyMarker,
|
|
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"
|
|
27
|
+
import type {
|
|
28
|
+
Criterion,
|
|
29
|
+
TagsAndNamesCriterion,
|
|
30
|
+
Tag as ProtoTag,
|
|
31
|
+
TaggedEvent,
|
|
32
|
+
Event as ProtoEvent,
|
|
33
|
+
SourceEventsResponse,
|
|
34
|
+
} from "./generated/dcb.js"
|
|
35
|
+
import { Metadata } from "nice-grpc"
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Criteria conversion — framework EventCriteria → proto Criterion[]
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
function tagToProto(tag: Tag): ProtoTag {
|
|
42
|
+
const encoder = new TextEncoder()
|
|
43
|
+
return {
|
|
44
|
+
key: encoder.encode(tag.key),
|
|
45
|
+
value: encoder.encode(tag.value),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tagFromProto(tag: ProtoTag): Tag {
|
|
50
|
+
const decoder = new TextDecoder()
|
|
51
|
+
return {
|
|
52
|
+
key: decoder.decode(tag.key),
|
|
53
|
+
value: decoder.decode(tag.value),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function criteriaToCriterions(criteria: EventCriteria): Criterion[] {
|
|
58
|
+
switch (criteria.kind) {
|
|
59
|
+
case "tags":
|
|
60
|
+
return [{
|
|
61
|
+
tagsAndNames: {
|
|
62
|
+
name: [],
|
|
63
|
+
tag: criteria.tags.map(tagToProto),
|
|
64
|
+
},
|
|
65
|
+
}]
|
|
66
|
+
|
|
67
|
+
case "type-restricted":
|
|
68
|
+
// 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,
|
|
76
|
+
},
|
|
77
|
+
}]
|
|
78
|
+
|
|
79
|
+
case "either":
|
|
80
|
+
// Flatten all sub-criteria into a list of criterions (OR semantics)
|
|
81
|
+
return criteria.criteria.flatMap(criteriaToCriterions)
|
|
82
|
+
|
|
83
|
+
case "any-tag":
|
|
84
|
+
// Match any tagged event — empty criterion
|
|
85
|
+
return [{ tagsAndNames: { name: [], tag: [] } }]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Event conversion — framework EventMessage ↔ proto Event/TaggedEvent
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
function createEventConverters(serializer: Serializer) {
|
|
94
|
+
return {
|
|
95
|
+
eventToProto(event: EventMessage): TaggedEvent {
|
|
96
|
+
const name = qualifiedNameToString(event.name)
|
|
97
|
+
const serialized = serializer.serialize(event.payload, name, event.version)
|
|
98
|
+
return {
|
|
99
|
+
event: {
|
|
100
|
+
identifier: event.identifier,
|
|
101
|
+
timestamp: BigInt(event.timestamp),
|
|
102
|
+
name,
|
|
103
|
+
version: event.version,
|
|
104
|
+
payload: serialized.data,
|
|
105
|
+
metadata: Object.fromEntries(
|
|
106
|
+
Object.entries(event.metadata).map(([k, v]) => [k, String(v)]),
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
tag: event.tags.map(tagToProto),
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
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
|
+
: {}
|
|
117
|
+
|
|
118
|
+
const metadata: Record<string, unknown> = {}
|
|
119
|
+
for (const [k, v] of Object.entries(protoEvent.metadata)) {
|
|
120
|
+
metadata[k] = v
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
identifier: protoEvent.identifier,
|
|
125
|
+
name: qualifiedNameFromString(protoEvent.name),
|
|
126
|
+
version: protoEvent.version,
|
|
127
|
+
payload,
|
|
128
|
+
metadata,
|
|
129
|
+
timestamp: Number(protoEvent.timestamp),
|
|
130
|
+
tags: tags.map(tagFromProto),
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// AxonServerDcbEventStore — implements our EventStore interface via gRPC
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates an EventStore implementation backed by Axon Server's DCB event store.
|
|
142
|
+
*
|
|
143
|
+
* This bridges the framework's EventStore interface to the gRPC
|
|
144
|
+
* DcbEventStore service, handling conversion between framework types
|
|
145
|
+
* and proto messages.
|
|
146
|
+
*
|
|
147
|
+
* The {@link open} method returns a persistent {@link MessageStream} backed by
|
|
148
|
+
* a single gRPC Stream RPC call that stays open indefinitely, aligned with
|
|
149
|
+
* Java's infinite {@code ResultStream}.
|
|
150
|
+
*/
|
|
151
|
+
export function createAxonServerEventStore(connection: AxonServerConnection, serializer: Serializer): EventStore {
|
|
152
|
+
const { eventToProto, eventFromProto } = createEventConverters(serializer)
|
|
153
|
+
|
|
154
|
+
function createAxonMetadata(): Metadata {
|
|
155
|
+
const axonMetadata = new Metadata()
|
|
156
|
+
axonMetadata.set("AxonIQ-Context", connection.config.context)
|
|
157
|
+
if (connection.config.token) {
|
|
158
|
+
axonMetadata.set("AxonIQ-Access-Token", connection.config.token)
|
|
159
|
+
}
|
|
160
|
+
return axonMetadata
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Push-based subscriber registry (EventBus.subscribe contract). Axon Server's
|
|
164
|
+
// own distribution is the server-side Stream RPC (see open()); these in-process
|
|
165
|
+
// subscribers are notified best-effort on every successful local append.
|
|
166
|
+
const subscribers = new Set<(events: ReadonlyArray<EventMessage>) => Promise<void>>()
|
|
167
|
+
async function notifySubscribers(events: ReadonlyArray<EventMessage>): Promise<void> {
|
|
168
|
+
for (const sub of subscribers) {
|
|
169
|
+
try {
|
|
170
|
+
await sub(events)
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore subscriber errors */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
async source(condition: SourcingCondition): Promise<SourcingResult> {
|
|
179
|
+
const criterions = criteriaToCriterions(condition.criteria)
|
|
180
|
+
|
|
181
|
+
const request = {
|
|
182
|
+
fromSequence: condition.start ?? 0n,
|
|
183
|
+
criterion: criterions,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const events: EventMessage[] = []
|
|
187
|
+
let marker: ConsistencyMarker = noMarker()
|
|
188
|
+
|
|
189
|
+
const stream = connection.eventStore.source(request, { metadata: createAxonMetadata() })
|
|
190
|
+
for await (const response of stream) {
|
|
191
|
+
if (response.event) {
|
|
192
|
+
const taggedEvent = response.event
|
|
193
|
+
const protoEvent = taggedEvent.event
|
|
194
|
+
if (protoEvent) {
|
|
195
|
+
// DCB source/stream responses carry no tags — the server indexes
|
|
196
|
+
// them write-side but does not echo them back (SequencedEvent has
|
|
197
|
+
// only sequence + event). Reconstructed events get empty tags.
|
|
198
|
+
events.push(eventFromProto(protoEvent, []))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (response.consistencyMarker !== undefined) {
|
|
202
|
+
marker = markerAt(response.consistencyMarker)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { events, marker }
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async appendEvents(
|
|
210
|
+
newEvents: ReadonlyArray<EventMessage>,
|
|
211
|
+
condition?: AppendCondition,
|
|
212
|
+
): Promise<AppendTransaction> {
|
|
213
|
+
const taggedEvents = newEvents.map(eventToProto)
|
|
214
|
+
const request = {
|
|
215
|
+
condition: condition ? {
|
|
216
|
+
consistencyMarker: condition.marker.position,
|
|
217
|
+
criterion: criteriaToCriterions(condition.criteria),
|
|
218
|
+
} : undefined,
|
|
219
|
+
event: taggedEvents,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Axon Server's Append RPC is atomic — commit happens on the server
|
|
223
|
+
// We send the request eagerly and wrap the response in a transaction
|
|
224
|
+
let responseMarker: bigint | undefined
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
async commit() {
|
|
228
|
+
async function* requestStream() {
|
|
229
|
+
yield request
|
|
230
|
+
}
|
|
231
|
+
const response = await connection.eventStore.append(requestStream(), { metadata: createAxonMetadata() })
|
|
232
|
+
responseMarker = response.consistencyMarker
|
|
233
|
+
await notifySubscribers(newEvents)
|
|
234
|
+
},
|
|
235
|
+
async afterCommit() {
|
|
236
|
+
return markerAt(responseMarker ?? 0n)
|
|
237
|
+
},
|
|
238
|
+
rollback() {
|
|
239
|
+
// Axon Server: if commit() was never called, nothing was sent
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
async append(
|
|
245
|
+
newEvents: ReadonlyArray<EventMessage>,
|
|
246
|
+
condition?: AppendCondition,
|
|
247
|
+
): Promise<ConsistencyMarker> {
|
|
248
|
+
const tx = await this.appendEvents(newEvents, condition)
|
|
249
|
+
await tx.commit()
|
|
250
|
+
return tx.afterCommit()
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
open(condition: StreamingCondition): MessageStream<SequencedEvent> {
|
|
254
|
+
const criterions = condition.criteria ? criteriaToCriterions(condition.criteria) : []
|
|
255
|
+
|
|
256
|
+
const request = {
|
|
257
|
+
fromSequence: condition.position,
|
|
258
|
+
criterion: criterions,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const grpcStream = connection.eventStore.stream(request, { metadata: createAxonMetadata() })
|
|
262
|
+
|
|
263
|
+
// Internal buffer for events pulled from the gRPC stream
|
|
264
|
+
const buffer: SequencedEvent[] = []
|
|
265
|
+
let availableCallback: (() => void) | null = null
|
|
266
|
+
let completed = false
|
|
267
|
+
let streamError: Error | undefined
|
|
268
|
+
let reading = false
|
|
269
|
+
|
|
270
|
+
// Background reader: pulls from gRPC stream into buffer
|
|
271
|
+
async function startReading() {
|
|
272
|
+
if (reading) return
|
|
273
|
+
reading = true
|
|
274
|
+
try {
|
|
275
|
+
for await (const response of grpcStream) {
|
|
276
|
+
if (completed) break
|
|
277
|
+
const taggedEvent = response.event
|
|
278
|
+
if (taggedEvent?.event) {
|
|
279
|
+
buffer.push({
|
|
280
|
+
sequence: taggedEvent.sequence,
|
|
281
|
+
event: eventFromProto(taggedEvent.event, []),
|
|
282
|
+
})
|
|
283
|
+
availableCallback?.()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Stream ended (shouldn't happen for infinite stream)
|
|
287
|
+
completed = true
|
|
288
|
+
availableCallback?.()
|
|
289
|
+
} catch (err) {
|
|
290
|
+
streamError = err instanceof Error ? err : new Error(String(err))
|
|
291
|
+
completed = true
|
|
292
|
+
availableCallback?.()
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
startReading()
|
|
297
|
+
|
|
298
|
+
return createMessageStream<SequencedEvent>({
|
|
299
|
+
next() {
|
|
300
|
+
return buffer.shift()
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
peek() {
|
|
304
|
+
return buffer[0]
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
hasNextAvailable() {
|
|
308
|
+
return buffer.length > 0
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
isCompleted() {
|
|
312
|
+
return completed && buffer.length === 0
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
error() {
|
|
316
|
+
return streamError
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
setCallback(callback: () => void) {
|
|
320
|
+
availableCallback = callback
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
close() {
|
|
324
|
+
completed = true
|
|
325
|
+
availableCallback = null
|
|
326
|
+
// gRPC stream will be cancelled when the async iterator is abandoned
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async getHeadPosition(): Promise<bigint> {
|
|
332
|
+
const response = await connection.eventStore.getHead({}, { metadata: createAxonMetadata() })
|
|
333
|
+
return response.sequence
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
async firstToken(): Promise<TrackingToken> {
|
|
337
|
+
return FIRST_TOKEN
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
async latestToken(): Promise<TrackingToken> {
|
|
341
|
+
const response = await connection.eventStore.getHead({}, { metadata: createAxonMetadata() })
|
|
342
|
+
return globalSequenceToken(response.sequence)
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// EventBus contract — publish = append without condition, then notify
|
|
346
|
+
// in-process subscribers.
|
|
347
|
+
async publish(events: ReadonlyArray<EventMessage>): Promise<void> {
|
|
348
|
+
await this.append(events)
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
subscribe(handler: (events: ReadonlyArray<EventMessage>) => Promise<void>): () => void {
|
|
352
|
+
subscribers.add(handler)
|
|
353
|
+
return () => {
|
|
354
|
+
subscribers.delete(handler)
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { SnapshotStore, Snapshot } from "@kronos-ts/eventsourcing"
|
|
2
|
+
import type { Serializer } from "@kronos-ts/common"
|
|
3
|
+
import type { AxonServerConnection } from "./connection.js"
|
|
4
|
+
import type {
|
|
5
|
+
Snapshot as ProtoSnapshot,
|
|
6
|
+
} from "./generated/dcb.js"
|
|
7
|
+
import { Metadata } from "nice-grpc"
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Conversion — framework Snapshot ↔ proto Snapshot
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const encoder = new TextEncoder()
|
|
14
|
+
|
|
15
|
+
function createSnapshotConverters(serializer: Serializer) {
|
|
16
|
+
return {
|
|
17
|
+
snapshotToProto(snapshot: Snapshot): ProtoSnapshot {
|
|
18
|
+
const serialized = serializer.serialize(snapshot.payload, "snapshot", "")
|
|
19
|
+
return {
|
|
20
|
+
name: "",
|
|
21
|
+
version: "",
|
|
22
|
+
payload: serialized.data,
|
|
23
|
+
timestamp: BigInt(snapshot.timestamp),
|
|
24
|
+
metadata: snapshot.metadata,
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
snapshotFromProto(proto: ProtoSnapshot, position: bigint): Snapshot {
|
|
29
|
+
const payload = proto.payload.length > 0
|
|
30
|
+
? serializer.deserialize({ data: proto.payload, type: "snapshot", revision: "" })
|
|
31
|
+
: {}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
position,
|
|
35
|
+
payload,
|
|
36
|
+
timestamp: Number(proto.timestamp),
|
|
37
|
+
metadata: proto.metadata ?? {},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function encodeKey(stateName: string, id: unknown): Uint8Array {
|
|
44
|
+
const idStr = typeof id === "object" && id !== null ? JSON.stringify(id) : String(id)
|
|
45
|
+
return encoder.encode(`${stateName}:${idStr}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Axon Server snapshot store
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a SnapshotStore backed by Axon Server's gRPC snapshot service.
|
|
54
|
+
*
|
|
55
|
+
* Uses the `DcbSnapshotStore` gRPC service to store and retrieve
|
|
56
|
+
* state snapshots. Payload serialization uses the configured
|
|
57
|
+
* Serializer (defaults to JSON).
|
|
58
|
+
*/
|
|
59
|
+
export function createAxonServerSnapshotStore(
|
|
60
|
+
connection: AxonServerConnection,
|
|
61
|
+
serializer: Serializer,
|
|
62
|
+
): SnapshotStore {
|
|
63
|
+
const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
|
|
64
|
+
|
|
65
|
+
function createAxonMetadata(): Metadata {
|
|
66
|
+
const axonMetadata = new Metadata()
|
|
67
|
+
axonMetadata.set("AxonIQ-Context", connection.config.context)
|
|
68
|
+
if (connection.config.token) {
|
|
69
|
+
axonMetadata.set("AxonIQ-Access-Token", connection.config.token)
|
|
70
|
+
}
|
|
71
|
+
return axonMetadata
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
async store(stateName: string, id: unknown, snapshot: Snapshot): Promise<void> {
|
|
76
|
+
await connection.snapshotStore.add(
|
|
77
|
+
{
|
|
78
|
+
key: encodeKey(stateName, id),
|
|
79
|
+
sequence: snapshot.position,
|
|
80
|
+
prune: true,
|
|
81
|
+
snapshot: snapshotToProto(snapshot),
|
|
82
|
+
},
|
|
83
|
+
{ metadata: createAxonMetadata() },
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async load(stateName: string, id: unknown): Promise<Snapshot | undefined> {
|
|
88
|
+
try {
|
|
89
|
+
const response = await connection.snapshotStore.getLast(
|
|
90
|
+
{ key: encodeKey(stateName, id) },
|
|
91
|
+
{ metadata: createAxonMetadata() },
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (!response.snapshot) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return snapshotFromProto(response.snapshot, response.sequence)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Axon Server throws when no snapshot exists — treat as "not found"
|
|
101
|
+
if (String(err).includes("No snapshot found")) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async deleteSnapshots(stateName: string, id: unknown): Promise<void> {
|
|
109
|
+
await connection.snapshotStore.delete(
|
|
110
|
+
{
|
|
111
|
+
key: encodeKey(stateName, id),
|
|
112
|
+
toSequence: BigInt(Number.MAX_SAFE_INTEGER),
|
|
113
|
+
},
|
|
114
|
+
{ metadata: createAxonMetadata() },
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|