@kronos-ts/kronosdb 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 (96) hide show
  1. package/dist/connection.d.ts +86 -0
  2. package/dist/connection.d.ts.map +1 -0
  3. package/dist/connection.js +133 -0
  4. package/dist/connection.js.map +1 -0
  5. package/dist/errors.d.ts +72 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +149 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/event-processor-info.d.ts +32 -0
  10. package/dist/event-processor-info.d.ts.map +1 -0
  11. package/dist/event-processor-info.js +24 -0
  12. package/dist/event-processor-info.js.map +1 -0
  13. package/dist/flow-controlled-sender.d.ts +12 -0
  14. package/dist/flow-controlled-sender.d.ts.map +1 -0
  15. package/dist/flow-controlled-sender.js +53 -0
  16. package/dist/flow-controlled-sender.js.map +1 -0
  17. package/dist/generated/command.d.ts +169 -0
  18. package/dist/generated/command.d.ts.map +1 -0
  19. package/dist/generated/command.js +964 -0
  20. package/dist/generated/command.js.map +1 -0
  21. package/dist/generated/common.d.ts +76 -0
  22. package/dist/generated/common.d.ts.map +1 -0
  23. package/dist/generated/common.js +648 -0
  24. package/dist/generated/common.js.map +1 -0
  25. package/dist/generated/eventstore.d.ts +337 -0
  26. package/dist/generated/eventstore.d.ts.map +1 -0
  27. package/dist/generated/eventstore.js +1757 -0
  28. package/dist/generated/eventstore.js.map +1 -0
  29. package/dist/generated/platform.d.ts +242 -0
  30. package/dist/generated/platform.d.ts.map +1 -0
  31. package/dist/generated/platform.js +1525 -0
  32. package/dist/generated/platform.js.map +1 -0
  33. package/dist/generated/query.d.ts +265 -0
  34. package/dist/generated/query.d.ts.map +1 -0
  35. package/dist/generated/query.js +2114 -0
  36. package/dist/generated/query.js.map +1 -0
  37. package/dist/generated/snapshot.d.ts +180 -0
  38. package/dist/generated/snapshot.d.ts.map +1 -0
  39. package/dist/generated/snapshot.js +861 -0
  40. package/dist/generated/snapshot.js.map +1 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +13 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/kronosdb-event-store.d.ts +17 -0
  46. package/dist/kronosdb-event-store.d.ts.map +1 -0
  47. package/dist/kronosdb-event-store.js +328 -0
  48. package/dist/kronosdb-event-store.js.map +1 -0
  49. package/dist/kronosdb-snapshot-store.d.ts +10 -0
  50. package/dist/kronosdb-snapshot-store.d.ts.map +1 -0
  51. package/dist/kronosdb-snapshot-store.js +79 -0
  52. package/dist/kronosdb-snapshot-store.js.map +1 -0
  53. package/dist/kronosdb.d.ts +53 -0
  54. package/dist/kronosdb.d.ts.map +1 -0
  55. package/dist/kronosdb.js +852 -0
  56. package/dist/kronosdb.js.map +1 -0
  57. package/dist/metadata-conversion.d.ts +37 -0
  58. package/dist/metadata-conversion.d.ts.map +1 -0
  59. package/dist/metadata-conversion.js +75 -0
  60. package/dist/metadata-conversion.js.map +1 -0
  61. package/dist/outbound-stream.d.ts +15 -0
  62. package/dist/outbound-stream.d.ts.map +1 -0
  63. package/dist/outbound-stream.js +39 -0
  64. package/dist/outbound-stream.js.map +1 -0
  65. package/dist/platform-service.d.ts +87 -0
  66. package/dist/platform-service.d.ts.map +1 -0
  67. package/dist/platform-service.js +218 -0
  68. package/dist/platform-service.js.map +1 -0
  69. package/dist/service-definitions.d.ts +187 -0
  70. package/dist/service-definitions.d.ts.map +1 -0
  71. package/dist/service-definitions.js +18 -0
  72. package/dist/service-definitions.js.map +1 -0
  73. package/dist/shutdown-latch.d.ts +18 -0
  74. package/dist/shutdown-latch.d.ts.map +1 -0
  75. package/dist/shutdown-latch.js +51 -0
  76. package/dist/shutdown-latch.js.map +1 -0
  77. package/package.json +69 -0
  78. package/src/connection.ts +235 -0
  79. package/src/errors.ts +173 -0
  80. package/src/event-processor-info.ts +53 -0
  81. package/src/flow-controlled-sender.ts +73 -0
  82. package/src/generated/command.ts +1226 -0
  83. package/src/generated/common.ts +770 -0
  84. package/src/generated/eventstore.ts +2241 -0
  85. package/src/generated/platform.ts +1914 -0
  86. package/src/generated/query.ts +2571 -0
  87. package/src/generated/snapshot.ts +1110 -0
  88. package/src/index.ts +87 -0
  89. package/src/kronosdb-event-store.ts +401 -0
  90. package/src/kronosdb-snapshot-store.ts +104 -0
  91. package/src/kronosdb.ts +1000 -0
  92. package/src/metadata-conversion.ts +85 -0
  93. package/src/outbound-stream.ts +52 -0
  94. package/src/platform-service.ts +297 -0
  95. package/src/service-definitions.ts +25 -0
  96. package/src/shutdown-latch.ts +74 -0
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ export {
2
+ type KronosDbConnectionConfig,
3
+ type KronosDbConnection,
4
+ type ConnectionState,
5
+ connectToKronosDb,
6
+ createKronosMetadata,
7
+ } from "./connection.js"
8
+
9
+ export {
10
+ createKronosDbEventStore,
11
+ } from "./kronosdb-event-store.js"
12
+
13
+ export {
14
+ createKronosDbSnapshotStore,
15
+ } from "./kronosdb-snapshot-store.js"
16
+
17
+ export {
18
+ kronosDb,
19
+ type KronosDbExtensionConfig,
20
+ type FlowControlConfig,
21
+ type ProcessingInstructions,
22
+ } from "./kronosdb.js"
23
+
24
+ export {
25
+ type PlatformConnection,
26
+ type PlatformInstruction,
27
+ type InstructionHandler,
28
+ type PlatformServiceOptions,
29
+ createPlatformConnection,
30
+ } from "./platform-service.js"
31
+
32
+ export {
33
+ type ProcessorStatus,
34
+ type SegmentStatus,
35
+ type ProcessorStatusSupplier,
36
+ toEventProcessorInfo,
37
+ } from "./event-processor-info.js"
38
+
39
+ export {
40
+ type FlowControlledSender,
41
+ createFlowControlledSender,
42
+ } from "./flow-controlled-sender.js"
43
+
44
+ export {
45
+ type ShutdownLatch,
46
+ type ActivityHandle,
47
+ ShutdownInProgressError,
48
+ createShutdownLatch,
49
+ } from "./shutdown-latch.js"
50
+
51
+ export {
52
+ KronosDbErrorCode,
53
+ type KronosDbErrorCodeValue,
54
+ KronosDbError,
55
+ NoHandlerForCommandError,
56
+ NoHandlerForQueryError,
57
+ CommandExecutionError,
58
+ QueryExecutionError,
59
+ CommandDispatchError,
60
+ QueryDispatchError,
61
+ ConcurrencyError,
62
+ ConnectionFailedError,
63
+ AuthenticationError,
64
+ mapErrorCode,
65
+ isTransientError,
66
+ } from "./errors.js"
67
+
68
+ export {
69
+ type OutboundStream,
70
+ createOutboundStream,
71
+ } from "./outbound-stream.js"
72
+
73
+ export {
74
+ metadataToProto,
75
+ metadataFromProto,
76
+ metadataToStringMap,
77
+ metadataFromStringMap,
78
+ } from "./metadata-conversion.js"
79
+
80
+ export {
81
+ kronosDbServiceDefinitions,
82
+ PlatformServiceDefinition,
83
+ CommandServiceDefinition,
84
+ QueryServiceDefinition,
85
+ EventStoreDefinition,
86
+ SnapshotStoreDefinition,
87
+ } from "./service-definitions.js"
@@ -0,0 +1,401 @@
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 { KronosDbConnection } from "./connection.js"
27
+ import { createKronosMetadata } from "./connection.js"
28
+ import { metadataFromStringMap, metadataToStringMap } from "./metadata-conversion.js"
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Tag conversion — framework Tag (string k/v) ↔ proto Tag (binary k/v)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const textEncoder = new TextEncoder()
35
+ const textDecoder = new TextDecoder()
36
+
37
+ function tagToProto(tag: Tag): { key: Uint8Array; value: Uint8Array } {
38
+ return {
39
+ key: textEncoder.encode(tag.key),
40
+ value: textEncoder.encode(tag.value),
41
+ }
42
+ }
43
+
44
+ function tagFromProto(tag: { key: Uint8Array; value: Uint8Array }): Tag {
45
+ return {
46
+ key: textDecoder.decode(tag.key),
47
+ value: textDecoder.decode(tag.value),
48
+ }
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Criteria conversion — framework EventCriteria → proto Criterion[]
53
+ //
54
+ // KronosDB Criterion has: names (string[]) + tags (Tag[])
55
+ // Semantics: event matches if (names empty OR name in names) AND all tags present
56
+ // Multiple criteria are OR'd together.
57
+ // ---------------------------------------------------------------------------
58
+
59
+ function criteriaToCriterions(criteria: EventCriteria): any[] {
60
+ switch (criteria.kind) {
61
+ case "tags":
62
+ return [{
63
+ names: [],
64
+ tags: criteria.tags.map(tagToProto),
65
+ }]
66
+
67
+ case "type-restricted": {
68
+ const innerTags = criteria.inner.kind === "tags"
69
+ ? criteria.inner.tags.map(tagToProto)
70
+ : []
71
+ return [{
72
+ names: [...criteria.types],
73
+ tags: innerTags,
74
+ }]
75
+ }
76
+
77
+ case "either":
78
+ return criteria.criteria.flatMap(criteriaToCriterions)
79
+
80
+ case "any-tag":
81
+ return [{ names: [], tags: [] }]
82
+ }
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Event conversion — framework EventMessage ↔ proto Event/TaggedEvent
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function createEventConverters(serializer: Serializer) {
90
+ return {
91
+ eventToProto(event: EventMessage): any {
92
+ const name = qualifiedNameToString(event.name)
93
+ const serialized = serializer.serialize(event.payload, name, event.version)
94
+ return {
95
+ event: {
96
+ identifier: event.identifier,
97
+ timestamp: BigInt(event.timestamp),
98
+ name,
99
+ version: event.version,
100
+ payload: serialized.data,
101
+ metadata: metadataToStringMap(event.metadata),
102
+ },
103
+ tags: event.tags.map(tagToProto),
104
+ }
105
+ },
106
+
107
+ eventFromProto(protoEvent: any, tags?: any[]): EventMessage {
108
+ const payload = protoEvent.payload && protoEvent.payload.length > 0
109
+ ? serializer.deserialize({ data: protoEvent.payload, type: protoEvent.name, revision: protoEvent.version })
110
+ : {}
111
+
112
+ return {
113
+ identifier: protoEvent.identifier,
114
+ name: qualifiedNameFromString(protoEvent.name),
115
+ version: protoEvent.version,
116
+ payload,
117
+ metadata: metadataFromStringMap(protoEvent.metadata ?? {}),
118
+ timestamp: Number(protoEvent.timestamp),
119
+ tags: (tags ?? []).map(tagFromProto),
120
+ }
121
+ },
122
+ }
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // KronosDB Event Store — implements EventStore interface via gRPC
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Creates an EventStore implementation backed by KronosDB's gRPC event store.
131
+ *
132
+ * Maps the framework's EventStore interface to KronosDB's EventStore service,
133
+ * handling conversion between framework types and proto messages.
134
+ *
135
+ * Key differences from Axon Server:
136
+ * - Event metadata is `map<string, string>` (not MetadataValue)
137
+ * - Source returns `SequencedEvent` (no tags on read — only on append)
138
+ * - Stream returns `SequencedEvent` directly
139
+ * - Criterion uses flat `names` + `tags` (not TagsAndNamesCriterion wrapper)
140
+ */
141
+ export function createKronosDbEventStore(connection: KronosDbConnection, serializer: Serializer): EventStore {
142
+ const { eventToProto, eventFromProto } = createEventConverters(serializer)
143
+
144
+ function getMetadata() {
145
+ return createKronosMetadata(connection.config)
146
+ }
147
+
148
+ // Push-based subscriber registry (EventBus.subscribe contract). KronosDB's
149
+ // own distribution is the server-side stream RPC (see open()); these
150
+ // in-process subscribers are notified best-effort on every local append.
151
+ const subscribers = new Set<(events: ReadonlyArray<EventMessage>) => Promise<void>>()
152
+ async function notifySubscribers(events: ReadonlyArray<EventMessage>): Promise<void> {
153
+ for (const sub of subscribers) {
154
+ try {
155
+ await sub(events)
156
+ } catch {
157
+ /* ignore subscriber errors */
158
+ }
159
+ }
160
+ }
161
+
162
+ return {
163
+ async source(condition: SourcingCondition): Promise<SourcingResult> {
164
+ const criterions = criteriaToCriterions(condition.criteria)
165
+
166
+ // KronosDB requires at least one criterion for tag-index matching.
167
+ // An empty criterion (no names, no tags) matches all events.
168
+ const effectiveCriterions = criterions.length === 0
169
+ ? [{ names: [], tags: [] }]
170
+ : criterions
171
+
172
+ const request = {
173
+ fromSequence: condition.start ?? 0n,
174
+ criteria: effectiveCriterions,
175
+ }
176
+
177
+ const events: EventMessage[] = []
178
+ let marker: ConsistencyMarker = noMarker()
179
+
180
+ const stream = connection.eventStore.source(request, { metadata: getMetadata() })
181
+ for await (const response of stream) {
182
+ // SourceResponse uses oneof: event (SequencedEvent) or consistency_marker (int64)
183
+ if (response.event) {
184
+ const seqEvent = response.event
185
+ if (seqEvent.event) {
186
+ // KronosDB doesn't return tags on source — we need to fetch them
187
+ // For now, pass empty tags; tags are only relevant for append conditions
188
+ events.push(eventFromProto(seqEvent.event))
189
+ }
190
+ }
191
+ if (response.consistencyMarker !== undefined && response.consistencyMarker !== 0n) {
192
+ marker = markerAt(response.consistencyMarker)
193
+ }
194
+ }
195
+
196
+ return { events, marker }
197
+ },
198
+
199
+ async appendEvents(
200
+ newEvents: ReadonlyArray<EventMessage>,
201
+ condition?: AppendCondition,
202
+ ): Promise<AppendTransaction> {
203
+ const taggedEvents = newEvents.map(eventToProto)
204
+ const request = {
205
+ condition: condition ? {
206
+ consistencyMarker: condition.marker.position,
207
+ criteria: criteriaToCriterions(condition.criteria),
208
+ } : undefined,
209
+ events: taggedEvents,
210
+ }
211
+
212
+ let responseMarker: bigint | undefined
213
+
214
+ return {
215
+ async commit() {
216
+ async function* requestStream() {
217
+ yield request
218
+ }
219
+ const response = await connection.eventStore.append(requestStream(), { metadata: getMetadata() })
220
+ responseMarker = response.consistencyMarker
221
+ await notifySubscribers(newEvents)
222
+ },
223
+ async afterCommit() {
224
+ return markerAt(responseMarker ?? 0n)
225
+ },
226
+ rollback() {
227
+ // If commit() was never called, nothing was sent
228
+ },
229
+ }
230
+ },
231
+
232
+ async append(
233
+ newEvents: ReadonlyArray<EventMessage>,
234
+ condition?: AppendCondition,
235
+ ): Promise<ConsistencyMarker> {
236
+ const tx = await this.appendEvents(newEvents, condition)
237
+ await tx.commit()
238
+ return tx.afterCommit()
239
+ },
240
+
241
+ open(condition: StreamingCondition): MessageStream<SequencedEvent> {
242
+ const criterions = condition.criteria ? criteriaToCriterions(condition.criteria) : []
243
+
244
+ // KronosDB requires at least one criterion for tag-index matching.
245
+ // An empty criterion (no names, no tags) matches all events.
246
+ const effectiveCriterions = criterions.length === 0
247
+ ? [{ names: [], tags: [] }]
248
+ : criterions
249
+
250
+ const PERMIT_BATCH = 500
251
+ const REFILL_THRESHOLD = 0.25
252
+
253
+ // Controllable async iterable for sending StreamControl messages.
254
+ let sendControl: ((msg: any) => void) | null = null
255
+ let controlDone = false
256
+ const controlQueue: any[] = []
257
+ let controlResolve: (() => void) | null = null
258
+
259
+ async function* controlStream() {
260
+ // First message: subscribe with initial permits.
261
+ yield {
262
+ subscribe: {
263
+ fromSequence: condition.position,
264
+ criteria: effectiveCriterions,
265
+ initialPermits: BigInt(PERMIT_BATCH),
266
+ blacklistedNames: [],
267
+ },
268
+ }
269
+
270
+ // Subsequent messages: permit grants.
271
+ while (!controlDone) {
272
+ while (controlQueue.length > 0) {
273
+ yield controlQueue.shift()!
274
+ }
275
+ // Wait for more messages to send.
276
+ await new Promise<void>((resolve) => {
277
+ controlResolve = resolve
278
+ })
279
+ }
280
+ }
281
+
282
+ function grantPermits(count: number) {
283
+ controlQueue.push({
284
+ permits: { permits: BigInt(count) },
285
+ })
286
+ controlResolve?.()
287
+ }
288
+
289
+ const grpcStream = connection.eventStore.stream(controlStream(), { metadata: getMetadata() })
290
+
291
+ const buffer: SequencedEvent[] = []
292
+ let availableCallback: (() => void) | null = null
293
+ let completed = false
294
+ let streamError: Error | undefined
295
+ let reading = false
296
+ let remainingPermits = PERMIT_BATCH
297
+
298
+ async function startReading() {
299
+ if (reading) return
300
+ reading = true
301
+ try {
302
+ for await (const response of grpcStream) {
303
+ if (completed) break
304
+ const seqEvent = response.event
305
+ // StreamResponse is a oneof { event, heartbeat } since kronosdb v0.2.0.
306
+ // For heartbeat frames, response.event is undefined and this guard skips
307
+ // them transparently — no explicit heartbeat branch needed (RESEARCH.md
308
+ // KDB-03, Pitfall 3). Server emits one heartbeat every ~15 seconds.
309
+ if (seqEvent?.event) {
310
+ buffer.push({
311
+ sequence: seqEvent.sequence,
312
+ event: eventFromProto(seqEvent.event),
313
+ })
314
+ availableCallback?.()
315
+ }
316
+ }
317
+ completed = true
318
+ availableCallback?.()
319
+ } catch (err) {
320
+ streamError = err instanceof Error ? err : new Error(String(err))
321
+ completed = true
322
+ availableCallback?.()
323
+ }
324
+ }
325
+
326
+ startReading()
327
+
328
+ function onConsumed() {
329
+ remainingPermits--
330
+ const threshold = Math.floor(PERMIT_BATCH * REFILL_THRESHOLD)
331
+ if (remainingPermits <= threshold && !completed) {
332
+ const grant = PERMIT_BATCH - remainingPermits
333
+ remainingPermits += grant
334
+ grantPermits(grant)
335
+ }
336
+ }
337
+
338
+ return createMessageStream<SequencedEvent>({
339
+ next() {
340
+ const item = buffer.shift()
341
+ if (item) onConsumed()
342
+ return item
343
+ },
344
+
345
+ peek() {
346
+ return buffer[0]
347
+ },
348
+
349
+ hasNextAvailable() {
350
+ return buffer.length > 0
351
+ },
352
+
353
+ isCompleted() {
354
+ return completed && buffer.length === 0
355
+ },
356
+
357
+ error() {
358
+ return streamError
359
+ },
360
+
361
+ setCallback(callback: () => void) {
362
+ availableCallback = callback
363
+ },
364
+
365
+ close() {
366
+ completed = true
367
+ controlDone = true
368
+ controlResolve?.()
369
+ availableCallback = null
370
+ },
371
+ })
372
+ },
373
+
374
+ async getHeadPosition(): Promise<bigint> {
375
+ const response = await connection.eventStore.getHead({}, { metadata: getMetadata() })
376
+ return response.sequence
377
+ },
378
+
379
+ async firstToken(): Promise<TrackingToken> {
380
+ return FIRST_TOKEN
381
+ },
382
+
383
+ async latestToken(): Promise<TrackingToken> {
384
+ const response = await connection.eventStore.getHead({}, { metadata: getMetadata() })
385
+ return globalSequenceToken(response.sequence)
386
+ },
387
+
388
+ // EventBus contract — publish = append without condition, then notify
389
+ // in-process subscribers.
390
+ async publish(events: ReadonlyArray<EventMessage>): Promise<void> {
391
+ await this.append(events)
392
+ },
393
+
394
+ subscribe(handler: (events: ReadonlyArray<EventMessage>) => Promise<void>): () => void {
395
+ subscribers.add(handler)
396
+ return () => {
397
+ subscribers.delete(handler)
398
+ }
399
+ },
400
+ }
401
+ }
@@ -0,0 +1,104 @@
1
+ import type { SnapshotStore, Snapshot } from "@kronos-ts/eventsourcing"
2
+ import type { Serializer } from "@kronos-ts/common"
3
+ import type { KronosDbConnection } from "./connection.js"
4
+ import { createKronosMetadata } from "./connection.js"
5
+
6
+ const encoder = new TextEncoder()
7
+
8
+ /**
9
+ * Encode a snapshot key as binary.
10
+ * Format: stateName + NUL + id.
11
+ */
12
+ function encodeKey(stateName: string, id: unknown): Uint8Array {
13
+ const idStr = typeof id === "object" && id !== null ? JSON.stringify(id) : String(id)
14
+ return encoder.encode(`${stateName}\0${idStr}`)
15
+ }
16
+
17
+ function createSnapshotConverters(serializer: Serializer) {
18
+ return {
19
+ snapshotToProto(snapshot: Snapshot): any {
20
+ const serialized = serializer.serialize(snapshot.payload, "snapshot", "")
21
+ return {
22
+ name: "",
23
+ version: "",
24
+ payload: serialized.data,
25
+ timestamp: BigInt(snapshot.timestamp),
26
+ metadata: snapshot.metadata ?? {},
27
+ }
28
+ },
29
+
30
+ snapshotFromProto(proto: any, position: bigint): Snapshot {
31
+ const payload = proto.payload && proto.payload.length > 0
32
+ ? serializer.deserialize({ data: proto.payload, type: "snapshot", revision: "" })
33
+ : {}
34
+
35
+ return {
36
+ position,
37
+ payload,
38
+ timestamp: Number(proto.timestamp),
39
+ metadata: proto.metadata ?? {},
40
+ }
41
+ },
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Creates a SnapshotStore backed by KronosDB's gRPC snapshot service.
47
+ *
48
+ * Uses NUL-separated keys (stateName\0id).
49
+ */
50
+ export function createKronosDbSnapshotStore(
51
+ connection: KronosDbConnection,
52
+ serializer: Serializer,
53
+ ): SnapshotStore {
54
+ const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
55
+
56
+ function getMetadata() {
57
+ return createKronosMetadata(connection.config)
58
+ }
59
+
60
+ return {
61
+ async store(stateName: string, id: unknown, snapshot: Snapshot): Promise<void> {
62
+ await connection.snapshotStore.add(
63
+ {
64
+ key: encodeKey(stateName, id),
65
+ sequence: snapshot.position,
66
+ prune: true,
67
+ snapshot: snapshotToProto(snapshot),
68
+ },
69
+ { metadata: getMetadata() },
70
+ )
71
+ },
72
+
73
+ async load(stateName: string, id: unknown): Promise<Snapshot | undefined> {
74
+ try {
75
+ const response = await connection.snapshotStore.getLast(
76
+ { key: encodeKey(stateName, id) },
77
+ { metadata: getMetadata() },
78
+ )
79
+
80
+ if (!response.snapshot) {
81
+ return undefined
82
+ }
83
+
84
+ return snapshotFromProto(response.snapshot, response.sequence)
85
+ } catch (err) {
86
+ // KronosDB returns empty response when no snapshot exists
87
+ if (String(err).includes("No snapshot found") || String(err).includes("NOT_FOUND")) {
88
+ return undefined
89
+ }
90
+ throw err
91
+ }
92
+ },
93
+
94
+ async deleteSnapshots(stateName: string, id: unknown): Promise<void> {
95
+ await connection.snapshotStore.delete(
96
+ {
97
+ key: encodeKey(stateName, id),
98
+ toSequence: BigInt(Number.MAX_SAFE_INTEGER),
99
+ },
100
+ { metadata: getMetadata() },
101
+ )
102
+ },
103
+ }
104
+ }