@auto-engineer/message-store 0.8.6

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,483 @@
1
+ import { nanoid } from 'nanoid';
2
+ import type {
3
+ Message,
4
+ PositionalMessage,
5
+ MessageFilter,
6
+ StreamInfo,
7
+ SessionInfo,
8
+ MessageStoreStats,
9
+ MessageType,
10
+ } from '../interfaces/types';
11
+ import type { ILocalMessageStore } from '../interfaces/IMessageStore';
12
+ import createDebug from 'debug';
13
+
14
+ const debug = createDebug('message-store:memory');
15
+
16
+ interface StreamData {
17
+ messages: PositionalMessage[];
18
+ revision: bigint;
19
+ createdAt: Date;
20
+ lastUpdated: Date;
21
+ }
22
+
23
+ export class MemoryMessageStore implements ILocalMessageStore {
24
+ private streams: Map<string, StreamData> = new Map();
25
+ private globalPosition = BigInt(0);
26
+ private sessions: Map<string, SessionInfo> = new Map();
27
+ private currentSessionId: string | null = null;
28
+
29
+ constructor() {
30
+ debug('MemoryMessageStore initialized.');
31
+ }
32
+
33
+ async createSession(): Promise<string> {
34
+ const sessionId = `session-${Date.now()}-${nanoid(8)}`;
35
+ const sessionInfo: SessionInfo = {
36
+ sessionId,
37
+ startedAt: new Date(),
38
+ messageCount: 0,
39
+ commandCount: 0,
40
+ eventCount: 0,
41
+ lastActivity: new Date(),
42
+ };
43
+
44
+ this.sessions.set(sessionId, sessionInfo);
45
+ this.currentSessionId = sessionId;
46
+
47
+ // Create a dedicated stream for this session
48
+ const sessionStreamId = `session-${sessionId}`;
49
+ this.streams.set(sessionStreamId, {
50
+ messages: [],
51
+ revision: BigInt(-1),
52
+ createdAt: new Date(),
53
+ lastUpdated: new Date(),
54
+ });
55
+
56
+ debug('Created new session: %s', sessionId);
57
+ return sessionId;
58
+ }
59
+
60
+ async endSession(sessionId: string): Promise<void> {
61
+ const session = this.sessions.get(sessionId);
62
+ if (session) {
63
+ session.lastActivity = new Date();
64
+ }
65
+
66
+ if (this.currentSessionId === sessionId) {
67
+ this.currentSessionId = null;
68
+ }
69
+
70
+ debug('Ended session: %s', sessionId);
71
+ }
72
+
73
+ async saveMessage(
74
+ streamId: string,
75
+ message: Message,
76
+ expectedRevision?: bigint,
77
+ messageType?: MessageType,
78
+ ): Promise<void> {
79
+ return this.saveMessages(streamId, [message], expectedRevision, messageType);
80
+ }
81
+
82
+ // eslint-disable-next-line complexity
83
+ async saveMessages(
84
+ streamId: string,
85
+ messages: Message[],
86
+ expectedRevision?: bigint,
87
+ messageType?: MessageType,
88
+ ): Promise<void> {
89
+ debug('Saving %d messages to stream: %s', messages.length, streamId);
90
+
91
+ // Get or create stream
92
+ let streamData = this.streams.get(streamId);
93
+ if (!streamData) {
94
+ if (expectedRevision !== undefined && expectedRevision !== BigInt(-1)) {
95
+ throw new Error(`Stream ${streamId} does not exist, but expected revision ${expectedRevision} was provided`);
96
+ }
97
+
98
+ streamData = {
99
+ messages: [],
100
+ revision: BigInt(-1),
101
+ createdAt: new Date(),
102
+ lastUpdated: new Date(),
103
+ };
104
+ this.streams.set(streamId, streamData);
105
+ }
106
+
107
+ // Check expected revision
108
+ if (expectedRevision !== undefined && streamData.revision !== expectedRevision) {
109
+ throw new Error(`Expected revision ${expectedRevision} but stream is at revision ${streamData.revision}`);
110
+ }
111
+
112
+ const now = new Date();
113
+ const sessionId = this.currentSessionId ?? 'no-session';
114
+
115
+ // Convert and store messages
116
+ for (const message of messages) {
117
+ this.globalPosition++;
118
+ streamData.revision++;
119
+
120
+ // Determine message type from context or stream name
121
+ const detectedMessageType: MessageType =
122
+ messageType ||
123
+ (streamId.includes('command')
124
+ ? 'command'
125
+ : streamId.includes('event')
126
+ ? 'event'
127
+ : // Try to infer from the message structure - commands typically have requestId
128
+ message.requestId !== undefined && message.requestId !== null && message.requestId !== ''
129
+ ? 'command'
130
+ : 'event');
131
+
132
+ const positionalMessage: PositionalMessage = {
133
+ streamId,
134
+ message: {
135
+ ...message,
136
+ timestamp: message.timestamp || now,
137
+ },
138
+ messageType: detectedMessageType,
139
+ revision: streamData.revision,
140
+ position: this.globalPosition,
141
+ timestamp: now,
142
+ sessionId,
143
+ };
144
+
145
+ streamData.messages.push(positionalMessage);
146
+ streamData.lastUpdated = now;
147
+
148
+ // Update session stats
149
+ const session = this.sessions.get(sessionId);
150
+ if (session) {
151
+ session.messageCount++;
152
+ session.lastActivity = now;
153
+ if (detectedMessageType === 'command') {
154
+ session.commandCount++;
155
+ } else {
156
+ session.eventCount++;
157
+ }
158
+ }
159
+
160
+ // Also store in session stream
161
+ if (sessionId !== 'no-session') {
162
+ const sessionStreamId = `session-${sessionId}`;
163
+ const sessionStream = this.streams.get(sessionStreamId);
164
+ if (sessionStream) {
165
+ sessionStream.revision++;
166
+ const sessionMessage: PositionalMessage = {
167
+ ...positionalMessage,
168
+ streamId: sessionStreamId,
169
+ revision: sessionStream.revision,
170
+ };
171
+ sessionStream.messages.push(sessionMessage);
172
+ sessionStream.lastUpdated = now;
173
+ }
174
+ }
175
+ }
176
+
177
+ debug(
178
+ 'Saved %d messages to stream %s, new revision: %s',
179
+ messages.length,
180
+ streamId,
181
+ streamData.revision.toString(),
182
+ );
183
+ }
184
+
185
+ async getMessages(streamId: string, fromRevision?: bigint, count?: number): Promise<PositionalMessage[]> {
186
+ const streamData = this.streams.get(streamId);
187
+ if (!streamData) {
188
+ return [];
189
+ }
190
+
191
+ let messages = streamData.messages;
192
+
193
+ if (fromRevision !== undefined && fromRevision !== null) {
194
+ messages = messages.filter((m) => m.revision >= fromRevision);
195
+ }
196
+
197
+ if (count !== undefined && count !== null && !isNaN(count) && count > 0) {
198
+ messages = messages.slice(-count); // Get the most recent N messages
199
+ }
200
+
201
+ return [...messages];
202
+ }
203
+
204
+ async getAllMessages(filter?: MessageFilter, count?: number): Promise<PositionalMessage[]> {
205
+ const allMessages: PositionalMessage[] = [];
206
+
207
+ for (const streamData of this.streams.values()) {
208
+ allMessages.push(...streamData.messages);
209
+ }
210
+
211
+ // Sort by position
212
+ allMessages.sort((a, b) => Number(a.position) - Number(b.position));
213
+
214
+ let filtered = this.applyFilter(allMessages, filter);
215
+
216
+ if (count !== undefined && count !== null && !isNaN(count) && count > 0) {
217
+ filtered = filtered.slice(-count); // Get the most recent N messages
218
+ }
219
+
220
+ return filtered;
221
+ }
222
+
223
+ async getAllCommands(filter?: Omit<MessageFilter, 'messageType'>, count?: number): Promise<PositionalMessage[]> {
224
+ return this.getAllMessages({ ...filter, messageType: 'command' }, count);
225
+ }
226
+
227
+ async getAllEvents(filter?: Omit<MessageFilter, 'messageType'>, count?: number): Promise<PositionalMessage[]> {
228
+ return this.getAllMessages({ ...filter, messageType: 'event' }, count);
229
+ }
230
+
231
+ private applyFilter(messages: PositionalMessage[], filter?: MessageFilter): PositionalMessage[] {
232
+ if (filter === undefined || filter === null) {
233
+ return messages;
234
+ }
235
+
236
+ return messages.filter((message) => this.messageMatchesFilter(message, filter));
237
+ }
238
+
239
+ private messageMatchesFilter(message: PositionalMessage, filter: MessageFilter): boolean {
240
+ return (
241
+ this.passesTypeFilter(message, filter) &&
242
+ this.passesIdentifierFilters(message, filter) &&
243
+ this.passesPositionFilters(message, filter) &&
244
+ this.passesTimestampFilters(message, filter) &&
245
+ this.passesJsonFilter(message, filter)
246
+ );
247
+ }
248
+
249
+ private passesTypeFilter(message: PositionalMessage, filter: MessageFilter): boolean {
250
+ // Message type filter
251
+ if (filter.messageType !== undefined && filter.messageType !== null && message.messageType !== filter.messageType) {
252
+ return false;
253
+ }
254
+
255
+ // Message names filter
256
+ if (
257
+ filter.messageNames !== undefined &&
258
+ filter.messageNames !== null &&
259
+ filter.messageNames.length > 0 &&
260
+ !filter.messageNames.includes(message.message.type)
261
+ ) {
262
+ return false;
263
+ }
264
+
265
+ return true;
266
+ }
267
+
268
+ // eslint-disable-next-line complexity
269
+ private passesIdentifierFilters(message: PositionalMessage, filter: MessageFilter): boolean {
270
+ // Stream ID filter
271
+ if (
272
+ filter.streamId !== undefined &&
273
+ filter.streamId !== null &&
274
+ filter.streamId !== '' &&
275
+ message.streamId !== filter.streamId
276
+ ) {
277
+ return false;
278
+ }
279
+
280
+ // Session ID filter
281
+ if (
282
+ filter.sessionId !== undefined &&
283
+ filter.sessionId !== null &&
284
+ filter.sessionId !== '' &&
285
+ message.sessionId !== filter.sessionId
286
+ ) {
287
+ return false;
288
+ }
289
+
290
+ // Correlation ID filter
291
+ if (
292
+ filter.correlationId !== undefined &&
293
+ filter.correlationId !== null &&
294
+ filter.correlationId !== '' &&
295
+ message.message.correlationId !== filter.correlationId
296
+ ) {
297
+ return false;
298
+ }
299
+
300
+ // Request ID filter
301
+ if (
302
+ filter.requestId !== undefined &&
303
+ filter.requestId !== null &&
304
+ filter.requestId !== '' &&
305
+ message.message.requestId !== filter.requestId
306
+ ) {
307
+ return false;
308
+ }
309
+
310
+ return true;
311
+ }
312
+
313
+ private passesPositionFilters(message: PositionalMessage, filter: MessageFilter): boolean {
314
+ // Position filters
315
+ if (filter.fromPosition !== undefined && filter.fromPosition !== null && message.position < filter.fromPosition) {
316
+ return false;
317
+ }
318
+ if (filter.toPosition !== undefined && filter.toPosition !== null && message.position > filter.toPosition) {
319
+ return false;
320
+ }
321
+
322
+ return true;
323
+ }
324
+
325
+ private passesTimestampFilters(message: PositionalMessage, filter: MessageFilter): boolean {
326
+ // Timestamp filters
327
+ if (
328
+ filter.fromTimestamp !== undefined &&
329
+ filter.fromTimestamp !== null &&
330
+ message.timestamp < filter.fromTimestamp
331
+ ) {
332
+ return false;
333
+ }
334
+ if (filter.toTimestamp !== undefined && filter.toTimestamp !== null && message.timestamp > filter.toTimestamp) {
335
+ return false;
336
+ }
337
+
338
+ return true;
339
+ }
340
+
341
+ private passesJsonFilter(message: PositionalMessage, filter: MessageFilter): boolean {
342
+ // JSON filter (simple property matching for now)
343
+ if (filter.jsonFilter !== undefined && filter.jsonFilter !== null) {
344
+ return this.matchesJsonFilter(message.message.data, filter.jsonFilter);
345
+ }
346
+
347
+ return true;
348
+ }
349
+
350
+ private matchesJsonFilter(data: unknown, jsonFilter: Record<string, unknown>): boolean {
351
+ if (data === undefined || data === null || typeof data !== 'object') {
352
+ return false;
353
+ }
354
+
355
+ const dataObj = data as Record<string, unknown>;
356
+
357
+ for (const [key, expectedValue] of Object.entries(jsonFilter)) {
358
+ if (dataObj[key] !== expectedValue) {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ return true;
364
+ }
365
+
366
+ async getStreamInfo(streamId: string): Promise<StreamInfo | null> {
367
+ const streamData = this.streams.get(streamId);
368
+ if (!streamData) {
369
+ return null;
370
+ }
371
+
372
+ const messages = streamData.messages;
373
+ const firstMessage = messages[0];
374
+ const lastMessage = messages[messages.length - 1];
375
+
376
+ return {
377
+ streamId,
378
+ revision: streamData.revision,
379
+ messageCount: messages.length,
380
+ firstPosition: firstMessage?.position ?? BigInt(0),
381
+ lastPosition: lastMessage?.position ?? BigInt(0),
382
+ createdAt: streamData.createdAt,
383
+ lastUpdated: streamData.lastUpdated,
384
+ };
385
+ }
386
+
387
+ async getStreams(): Promise<string[]> {
388
+ return Array.from(this.streams.keys());
389
+ }
390
+
391
+ async getSessions(): Promise<SessionInfo[]> {
392
+ return Array.from(this.sessions.values());
393
+ }
394
+
395
+ async getSessionInfo(sessionId: string): Promise<SessionInfo | null> {
396
+ return this.sessions.get(sessionId) || null;
397
+ }
398
+
399
+ async getSessionMessages(
400
+ sessionId: string,
401
+ filter?: Omit<MessageFilter, 'sessionId'>,
402
+ count?: number,
403
+ ): Promise<PositionalMessage[]> {
404
+ const sessionStreamId = `session-${sessionId}`;
405
+ const sessionMessages = await this.getMessages(sessionStreamId, undefined, count);
406
+
407
+ if (filter) {
408
+ return this.applyFilter(sessionMessages, { ...filter, sessionId });
409
+ }
410
+
411
+ return sessionMessages;
412
+ }
413
+
414
+ async getStats(): Promise<MessageStoreStats> {
415
+ let totalMessages = 0;
416
+ let totalCommands = 0;
417
+ let totalEvents = 0;
418
+ let oldestTimestamp: Date | undefined;
419
+ let newestTimestamp: Date | undefined;
420
+
421
+ for (const streamData of this.streams.values()) {
422
+ for (const message of streamData.messages) {
423
+ totalMessages++;
424
+
425
+ if (message.messageType === 'command') {
426
+ totalCommands++;
427
+ } else {
428
+ totalEvents++;
429
+ }
430
+
431
+ if (!oldestTimestamp || message.timestamp < oldestTimestamp) {
432
+ oldestTimestamp = message.timestamp;
433
+ }
434
+ if (!newestTimestamp || message.timestamp > newestTimestamp) {
435
+ newestTimestamp = message.timestamp;
436
+ }
437
+ }
438
+ }
439
+
440
+ return {
441
+ totalMessages,
442
+ totalCommands,
443
+ totalEvents,
444
+ totalStreams: this.streams.size,
445
+ totalSessions: this.sessions.size,
446
+ memoryUsage: this.estimateMemoryUsage(),
447
+ oldestMessage: oldestTimestamp,
448
+ newestMessage: newestTimestamp,
449
+ };
450
+ }
451
+
452
+ private estimateMemoryUsage(): number {
453
+ // Rough estimation of memory usage in bytes
454
+ let size = 0;
455
+
456
+ for (const streamData of this.streams.values()) {
457
+ for (const message of streamData.messages) {
458
+ // Convert BigInt fields to strings for JSON serialization
459
+ const serializedMessage = {
460
+ ...message,
461
+ revision: message.revision.toString(),
462
+ position: message.position.toString(),
463
+ };
464
+ size += JSON.stringify(serializedMessage).length * 2; // Rough estimate for UTF-16
465
+ }
466
+ }
467
+
468
+ return size;
469
+ }
470
+
471
+ async reset(): Promise<void> {
472
+ debug('Resetting message store');
473
+ this.streams.clear();
474
+ this.sessions.clear();
475
+ this.globalPosition = BigInt(0);
476
+ this.currentSessionId = null;
477
+ }
478
+
479
+ async dispose(): Promise<void> {
480
+ debug('Disposing message store');
481
+ await this.reset();
482
+ }
483
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"],
9
+ "references": [
10
+ {
11
+ "path": "../message-bus"
12
+ }
13
+ ]
14
+ }