@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.
- package/README.md +261 -0
- package/package.json +84 -0
- package/src/api/cli-commands.ts +1459 -0
- package/src/api/cli-types.ts +154 -0
- package/src/api/index.ts +24 -0
- package/src/api/mcp-tools.ts +1977 -0
- package/src/application/claim-service.ts +753 -0
- package/src/application/index.ts +46 -0
- package/src/application/load-balancer.ts +840 -0
- package/src/application/work-stealing-service.ts +807 -0
- package/src/domain/events.ts +779 -0
- package/src/domain/index.ts +214 -0
- package/src/domain/repositories.ts +239 -0
- package/src/domain/rules.ts +526 -0
- package/src/domain/types.ts +826 -0
- package/src/index.ts +79 -0
- package/src/infrastructure/claim-repository.ts +358 -0
- package/src/infrastructure/event-store.ts +297 -0
- package/src/infrastructure/index.ts +21 -0
|
@@ -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';
|