@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.
Files changed (106) hide show
  1. package/dist/axon-server-event-store.d.ts +16 -0
  2. package/dist/axon-server-event-store.d.ts.map +1 -0
  3. package/dist/axon-server-event-store.js +282 -0
  4. package/dist/axon-server-event-store.js.map +1 -0
  5. package/dist/axon-server-snapshot-store.d.ts +12 -0
  6. package/dist/axon-server-snapshot-store.d.ts.map +1 -0
  7. package/dist/axon-server-snapshot-store.js +88 -0
  8. package/dist/axon-server-snapshot-store.js.map +1 -0
  9. package/dist/axon-server.d.ts +115 -0
  10. package/dist/axon-server.d.ts.map +1 -0
  11. package/dist/axon-server.js +986 -0
  12. package/dist/axon-server.js.map +1 -0
  13. package/dist/connection-manager.d.ts +49 -0
  14. package/dist/connection-manager.d.ts.map +1 -0
  15. package/dist/connection-manager.js +37 -0
  16. package/dist/connection-manager.js.map +1 -0
  17. package/dist/connection.d.ts +129 -0
  18. package/dist/connection.d.ts.map +1 -0
  19. package/dist/connection.js +130 -0
  20. package/dist/connection.js.map +1 -0
  21. package/dist/errors.d.ts +96 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +189 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/event-processor-info.d.ts +35 -0
  26. package/dist/event-processor-info.d.ts.map +1 -0
  27. package/dist/event-processor-info.js +28 -0
  28. package/dist/event-processor-info.js.map +1 -0
  29. package/dist/flow-controlled-sender.d.ts +30 -0
  30. package/dist/flow-controlled-sender.d.ts.map +1 -0
  31. package/dist/flow-controlled-sender.js +60 -0
  32. package/dist/flow-controlled-sender.js.map +1 -0
  33. package/dist/generated/command.d.ts +158 -0
  34. package/dist/generated/command.d.ts.map +1 -0
  35. package/dist/generated/command.js +970 -0
  36. package/dist/generated/command.js.map +1 -0
  37. package/dist/generated/common.d.ts +130 -0
  38. package/dist/generated/common.d.ts.map +1 -0
  39. package/dist/generated/common.js +908 -0
  40. package/dist/generated/common.js.map +1 -0
  41. package/dist/generated/control.d.ts +293 -0
  42. package/dist/generated/control.d.ts.map +1 -0
  43. package/dist/generated/control.js +1938 -0
  44. package/dist/generated/control.js.map +1 -0
  45. package/dist/generated/dcb.d.ts +650 -0
  46. package/dist/generated/dcb.d.ts.map +1 -0
  47. package/dist/generated/dcb.js +2943 -0
  48. package/dist/generated/dcb.js.map +1 -0
  49. package/dist/generated/event.d.ts +667 -0
  50. package/dist/generated/event.d.ts.map +1 -0
  51. package/dist/generated/event.js +3185 -0
  52. package/dist/generated/event.js.map +1 -0
  53. package/dist/generated/google/protobuf/empty.d.ts +30 -0
  54. package/dist/generated/google/protobuf/empty.d.ts.map +1 -0
  55. package/dist/generated/google/protobuf/empty.js +46 -0
  56. package/dist/generated/google/protobuf/empty.js.map +1 -0
  57. package/dist/generated/query.d.ts +300 -0
  58. package/dist/generated/query.d.ts.map +1 -0
  59. package/dist/generated/query.js +2183 -0
  60. package/dist/generated/query.js.map +1 -0
  61. package/dist/index.d.ts +12 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +12 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/message-size.d.ts +38 -0
  66. package/dist/message-size.d.ts.map +1 -0
  67. package/dist/message-size.js +57 -0
  68. package/dist/message-size.js.map +1 -0
  69. package/dist/metadata-conversion.d.ts +11 -0
  70. package/dist/metadata-conversion.d.ts.map +1 -0
  71. package/dist/metadata-conversion.js +51 -0
  72. package/dist/metadata-conversion.js.map +1 -0
  73. package/dist/outbound-stream.d.ts +15 -0
  74. package/dist/outbound-stream.d.ts.map +1 -0
  75. package/dist/outbound-stream.js +39 -0
  76. package/dist/outbound-stream.js.map +1 -0
  77. package/dist/platform-service.d.ts +119 -0
  78. package/dist/platform-service.d.ts.map +1 -0
  79. package/dist/platform-service.js +250 -0
  80. package/dist/platform-service.js.map +1 -0
  81. package/dist/shutdown-latch.d.ts +38 -0
  82. package/dist/shutdown-latch.d.ts.map +1 -0
  83. package/dist/shutdown-latch.js +51 -0
  84. package/dist/shutdown-latch.js.map +1 -0
  85. package/package.json +69 -0
  86. package/src/axon-server-event-store.ts +358 -0
  87. package/src/axon-server-snapshot-store.ts +118 -0
  88. package/src/axon-server.ts +1202 -0
  89. package/src/connection-manager.ts +88 -0
  90. package/src/connection.ts +272 -0
  91. package/src/errors.ts +223 -0
  92. package/src/event-processor-info.ts +62 -0
  93. package/src/flow-controlled-sender.ts +91 -0
  94. package/src/generated/command.ts +1231 -0
  95. package/src/generated/common.ts +1097 -0
  96. package/src/generated/control.ts +2419 -0
  97. package/src/generated/dcb.ts +3826 -0
  98. package/src/generated/event.ts +4076 -0
  99. package/src/generated/google/protobuf/empty.ts +84 -0
  100. package/src/generated/query.ts +2723 -0
  101. package/src/index.ts +75 -0
  102. package/src/message-size.ts +75 -0
  103. package/src/metadata-conversion.ts +46 -0
  104. package/src/outbound-stream.ts +52 -0
  105. package/src/platform-service.ts +361 -0
  106. 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
+ }