@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.
- package/dist/connection.d.ts +86 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +133 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +149 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-processor-info.d.ts +32 -0
- package/dist/event-processor-info.d.ts.map +1 -0
- package/dist/event-processor-info.js +24 -0
- package/dist/event-processor-info.js.map +1 -0
- package/dist/flow-controlled-sender.d.ts +12 -0
- package/dist/flow-controlled-sender.d.ts.map +1 -0
- package/dist/flow-controlled-sender.js +53 -0
- package/dist/flow-controlled-sender.js.map +1 -0
- package/dist/generated/command.d.ts +169 -0
- package/dist/generated/command.d.ts.map +1 -0
- package/dist/generated/command.js +964 -0
- package/dist/generated/command.js.map +1 -0
- package/dist/generated/common.d.ts +76 -0
- package/dist/generated/common.d.ts.map +1 -0
- package/dist/generated/common.js +648 -0
- package/dist/generated/common.js.map +1 -0
- package/dist/generated/eventstore.d.ts +337 -0
- package/dist/generated/eventstore.d.ts.map +1 -0
- package/dist/generated/eventstore.js +1757 -0
- package/dist/generated/eventstore.js.map +1 -0
- package/dist/generated/platform.d.ts +242 -0
- package/dist/generated/platform.d.ts.map +1 -0
- package/dist/generated/platform.js +1525 -0
- package/dist/generated/platform.js.map +1 -0
- package/dist/generated/query.d.ts +265 -0
- package/dist/generated/query.d.ts.map +1 -0
- package/dist/generated/query.js +2114 -0
- package/dist/generated/query.js.map +1 -0
- package/dist/generated/snapshot.d.ts +180 -0
- package/dist/generated/snapshot.d.ts.map +1 -0
- package/dist/generated/snapshot.js +861 -0
- package/dist/generated/snapshot.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/kronosdb-event-store.d.ts +17 -0
- package/dist/kronosdb-event-store.d.ts.map +1 -0
- package/dist/kronosdb-event-store.js +328 -0
- package/dist/kronosdb-event-store.js.map +1 -0
- package/dist/kronosdb-snapshot-store.d.ts +10 -0
- package/dist/kronosdb-snapshot-store.d.ts.map +1 -0
- package/dist/kronosdb-snapshot-store.js +79 -0
- package/dist/kronosdb-snapshot-store.js.map +1 -0
- package/dist/kronosdb.d.ts +53 -0
- package/dist/kronosdb.d.ts.map +1 -0
- package/dist/kronosdb.js +852 -0
- package/dist/kronosdb.js.map +1 -0
- package/dist/metadata-conversion.d.ts +37 -0
- package/dist/metadata-conversion.d.ts.map +1 -0
- package/dist/metadata-conversion.js +75 -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 +87 -0
- package/dist/platform-service.d.ts.map +1 -0
- package/dist/platform-service.js +218 -0
- package/dist/platform-service.js.map +1 -0
- package/dist/service-definitions.d.ts +187 -0
- package/dist/service-definitions.d.ts.map +1 -0
- package/dist/service-definitions.js +18 -0
- package/dist/service-definitions.js.map +1 -0
- package/dist/shutdown-latch.d.ts +18 -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/connection.ts +235 -0
- package/src/errors.ts +173 -0
- package/src/event-processor-info.ts +53 -0
- package/src/flow-controlled-sender.ts +73 -0
- package/src/generated/command.ts +1226 -0
- package/src/generated/common.ts +770 -0
- package/src/generated/eventstore.ts +2241 -0
- package/src/generated/platform.ts +1914 -0
- package/src/generated/query.ts +2571 -0
- package/src/generated/snapshot.ts +1110 -0
- package/src/index.ts +87 -0
- package/src/kronosdb-event-store.ts +401 -0
- package/src/kronosdb-snapshot-store.ts +104 -0
- package/src/kronosdb.ts +1000 -0
- package/src/metadata-conversion.ts +85 -0
- package/src/outbound-stream.ts +52 -0
- package/src/platform-service.ts +297 -0
- package/src/service-definitions.ts +25 -0
- 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
|
+
}
|