@sparkleideas/claims 3.5.2-patch.1

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.
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @sparkleideas/claims - Event Store Implementation
3
+ * Event sourcing storage for claims (ADR-007, ADR-016)
4
+ *
5
+ * @module v3/claims/infrastructure/event-store
6
+ */
7
+
8
+ import {
9
+ ClaimId,
10
+ IssueId,
11
+ } from '../domain/types.js';
12
+ import {
13
+ ClaimDomainEvent,
14
+ AllClaimEvents,
15
+ AllExtendedClaimEvents,
16
+ ClaimEventType,
17
+ ExtendedClaimEventType,
18
+ } from '../domain/events.js';
19
+ import { IClaimEventStore } from '../domain/repositories.js';
20
+
21
+ // =============================================================================
22
+ // Event Store Types
23
+ // =============================================================================
24
+
25
+ export interface EventFilter {
26
+ aggregateId?: string;
27
+ eventTypes?: (ClaimEventType | ExtendedClaimEventType)[];
28
+ fromTimestamp?: number;
29
+ toTimestamp?: number;
30
+ fromVersion?: number;
31
+ toVersion?: number;
32
+ limit?: number;
33
+ offset?: number;
34
+ }
35
+
36
+ export interface EventSubscription {
37
+ id: string;
38
+ eventTypes: (ClaimEventType | ExtendedClaimEventType)[];
39
+ handler: (event: AllExtendedClaimEvents) => void | Promise<void>;
40
+ }
41
+
42
+ // =============================================================================
43
+ // In-Memory Event Store Implementation
44
+ // =============================================================================
45
+
46
+ /**
47
+ * In-memory implementation of the event store
48
+ * Suitable for development and testing
49
+ */
50
+ export class InMemoryClaimEventStore implements IClaimEventStore {
51
+ private events: AllExtendedClaimEvents[] = [];
52
+ private aggregateVersions: Map<string, number> = new Map();
53
+ private subscriptions: EventSubscription[] = [];
54
+ private nextSubscriptionId = 0;
55
+
56
+ async initialize(): Promise<void> {
57
+ // No initialization needed for in-memory store
58
+ }
59
+
60
+ async shutdown(): Promise<void> {
61
+ this.events = [];
62
+ this.aggregateVersions.clear();
63
+ this.subscriptions = [];
64
+ }
65
+
66
+ // ==========================================================================
67
+ // Write Operations
68
+ // ==========================================================================
69
+
70
+ async append(event: ClaimDomainEvent): Promise<void> {
71
+ // Assign version
72
+ const currentVersion = this.aggregateVersions.get(event.aggregateId) ?? 0;
73
+ const newVersion = currentVersion + 1;
74
+ (event as any).version = newVersion;
75
+
76
+ // Store event
77
+ this.events.push(event as AllExtendedClaimEvents);
78
+ this.aggregateVersions.set(event.aggregateId, newVersion);
79
+
80
+ // Notify subscribers
81
+ await this.notifySubscribers(event as AllExtendedClaimEvents);
82
+ }
83
+
84
+ async appendBatch(events: ClaimDomainEvent[]): Promise<void> {
85
+ for (const event of events) {
86
+ await this.append(event);
87
+ }
88
+ }
89
+
90
+ // ==========================================================================
91
+ // Read Operations
92
+ // ==========================================================================
93
+
94
+ async getEvents(
95
+ claimId: ClaimId,
96
+ fromVersion?: number
97
+ ): Promise<ClaimDomainEvent[]> {
98
+ return this.events.filter(
99
+ (e) =>
100
+ e.aggregateId === claimId &&
101
+ (fromVersion === undefined || e.version >= fromVersion)
102
+ ) as ClaimDomainEvent[];
103
+ }
104
+
105
+ async getEventsByType(type: string): Promise<ClaimDomainEvent[]> {
106
+ return this.events.filter((e) => e.type === type) as ClaimDomainEvent[];
107
+ }
108
+
109
+ async getEventsByIssueId(issueId: IssueId): Promise<ClaimDomainEvent[]> {
110
+ return this.events.filter(
111
+ (e) => (e.payload as any)?.issueId === issueId
112
+ ) as ClaimDomainEvent[];
113
+ }
114
+
115
+ async query(filter: EventFilter): Promise<AllExtendedClaimEvents[]> {
116
+ let results = [...this.events];
117
+
118
+ if (filter.aggregateId) {
119
+ results = results.filter((e) => e.aggregateId === filter.aggregateId);
120
+ }
121
+
122
+ if (filter.eventTypes && filter.eventTypes.length > 0) {
123
+ results = results.filter((e) => filter.eventTypes!.includes(e.type as any));
124
+ }
125
+
126
+ if (filter.fromTimestamp !== undefined) {
127
+ results = results.filter((e) => e.timestamp >= filter.fromTimestamp!);
128
+ }
129
+
130
+ if (filter.toTimestamp !== undefined) {
131
+ results = results.filter((e) => e.timestamp <= filter.toTimestamp!);
132
+ }
133
+
134
+ if (filter.fromVersion !== undefined) {
135
+ results = results.filter((e) => e.version >= filter.fromVersion!);
136
+ }
137
+
138
+ if (filter.toVersion !== undefined) {
139
+ results = results.filter((e) => e.version <= filter.toVersion!);
140
+ }
141
+
142
+ // Apply pagination
143
+ if (filter.offset) {
144
+ results = results.slice(filter.offset);
145
+ }
146
+
147
+ if (filter.limit) {
148
+ results = results.slice(0, filter.limit);
149
+ }
150
+
151
+ return results;
152
+ }
153
+
154
+ // ==========================================================================
155
+ // Subscription Operations
156
+ // ==========================================================================
157
+
158
+ subscribe(
159
+ eventTypes: (ClaimEventType | ExtendedClaimEventType)[],
160
+ handler: (event: AllExtendedClaimEvents) => void | Promise<void>
161
+ ): () => void {
162
+ const subscription: EventSubscription = {
163
+ id: `sub-${++this.nextSubscriptionId}`,
164
+ eventTypes,
165
+ handler,
166
+ };
167
+
168
+ this.subscriptions.push(subscription);
169
+
170
+ // Return unsubscribe function
171
+ return () => {
172
+ const index = this.subscriptions.findIndex((s) => s.id === subscription.id);
173
+ if (index !== -1) {
174
+ this.subscriptions.splice(index, 1);
175
+ }
176
+ };
177
+ }
178
+
179
+ subscribeAll(
180
+ handler: (event: AllExtendedClaimEvents) => void | Promise<void>
181
+ ): () => void {
182
+ return this.subscribe([], handler);
183
+ }
184
+
185
+ private async notifySubscribers(event: AllExtendedClaimEvents): Promise<void> {
186
+ for (const subscription of this.subscriptions) {
187
+ // If no event types specified, handler receives all events
188
+ if (
189
+ subscription.eventTypes.length === 0 ||
190
+ subscription.eventTypes.includes(event.type as any)
191
+ ) {
192
+ try {
193
+ await subscription.handler(event);
194
+ } catch (error) {
195
+ console.error(
196
+ `Event handler error for subscription ${subscription.id}:`,
197
+ error
198
+ );
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ // ==========================================================================
205
+ // Aggregate Operations
206
+ // ==========================================================================
207
+
208
+ async getAggregateVersion(aggregateId: string): Promise<number> {
209
+ return this.aggregateVersions.get(aggregateId) ?? 0;
210
+ }
211
+
212
+ async getAggregateState<T>(
213
+ aggregateId: string,
214
+ reducer: (state: T, event: AllExtendedClaimEvents) => T,
215
+ initialState: T
216
+ ): Promise<T> {
217
+ const events = await this.getEvents(aggregateId as ClaimId);
218
+ return events.reduce(
219
+ (state, event) => reducer(state, event as AllExtendedClaimEvents),
220
+ initialState
221
+ );
222
+ }
223
+
224
+ // ==========================================================================
225
+ // Snapshot Operations
226
+ // ==========================================================================
227
+
228
+ private snapshots: Map<string, { state: unknown; version: number }> = new Map();
229
+
230
+ async saveSnapshot<T>(aggregateId: string, state: T, version: number): Promise<void> {
231
+ this.snapshots.set(aggregateId, { state, version });
232
+ }
233
+
234
+ async getSnapshot<T>(aggregateId: string): Promise<{ state: T; version: number } | null> {
235
+ const snapshot = this.snapshots.get(aggregateId);
236
+ if (!snapshot) return null;
237
+ return snapshot as { state: T; version: number };
238
+ }
239
+
240
+ async getStateFromSnapshot<T>(
241
+ aggregateId: string,
242
+ reducer: (state: T, event: AllExtendedClaimEvents) => T,
243
+ initialState: T
244
+ ): Promise<T> {
245
+ // Try to get snapshot
246
+ const snapshot = await this.getSnapshot<T>(aggregateId);
247
+
248
+ let state: T;
249
+ let fromVersion: number;
250
+
251
+ if (snapshot) {
252
+ state = snapshot.state;
253
+ fromVersion = snapshot.version + 1;
254
+ } else {
255
+ state = initialState;
256
+ fromVersion = 1;
257
+ }
258
+
259
+ // Apply events after snapshot
260
+ const events = await this.getEvents(aggregateId as ClaimId, fromVersion);
261
+ return events.reduce(
262
+ (s, event) => reducer(s, event as AllExtendedClaimEvents),
263
+ state
264
+ );
265
+ }
266
+
267
+ // ==========================================================================
268
+ // Statistics
269
+ // ==========================================================================
270
+
271
+ async getEventCount(): Promise<number> {
272
+ return this.events.length;
273
+ }
274
+
275
+ async getEventCountByType(): Promise<Record<string, number>> {
276
+ const counts: Record<string, number> = {};
277
+ for (const event of this.events) {
278
+ counts[event.type] = (counts[event.type] ?? 0) + 1;
279
+ }
280
+ return counts;
281
+ }
282
+
283
+ async getAggregateCount(): Promise<number> {
284
+ return this.aggregateVersions.size;
285
+ }
286
+ }
287
+
288
+ // =============================================================================
289
+ // Factory Function
290
+ // =============================================================================
291
+
292
+ /**
293
+ * Create a new event store
294
+ */
295
+ export function createClaimEventStore(): InMemoryClaimEventStore {
296
+ return new InMemoryClaimEventStore();
297
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @sparkleideas/claims - Infrastructure Layer
3
+ *
4
+ * Exports persistence implementations for the claims module.
5
+ *
6
+ * @module v3/claims/infrastructure
7
+ */
8
+
9
+ // Claim Repository
10
+ export {
11
+ InMemoryClaimRepository,
12
+ createClaimRepository,
13
+ } from './claim-repository.js';
14
+
15
+ // Event Store
16
+ export {
17
+ InMemoryClaimEventStore,
18
+ createClaimEventStore,
19
+ type EventFilter,
20
+ type EventSubscription,
21
+ } from './event-store.js';