@signaltree/events 7.3.5 → 7.4.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 (54) hide show
  1. package/dist/angular/handlers.cjs +38 -0
  2. package/dist/angular/handlers.js +35 -0
  3. package/dist/angular/index.cjs +15 -0
  4. package/dist/angular/index.js +3 -0
  5. package/dist/angular/optimistic-updates.cjs +161 -0
  6. package/dist/angular/optimistic-updates.js +159 -0
  7. package/dist/angular/websocket.service.cjs +357 -0
  8. package/{angular.esm.js → dist/angular/websocket.service.js} +1 -191
  9. package/dist/core/error-classification.cjs +282 -0
  10. package/dist/core/error-classification.js +276 -0
  11. package/dist/core/factory.cjs +148 -0
  12. package/{factory.esm.js → dist/core/factory.js} +2 -37
  13. package/dist/core/idempotency.cjs +252 -0
  14. package/dist/core/idempotency.js +247 -0
  15. package/dist/core/registry.cjs +183 -0
  16. package/dist/core/registry.js +180 -0
  17. package/dist/core/types.cjs +41 -0
  18. package/dist/core/types.js +38 -0
  19. package/dist/core/validation.cjs +185 -0
  20. package/{index.esm.js → dist/core/validation.js} +1 -4
  21. package/dist/index.cjs +43 -0
  22. package/dist/index.js +7 -0
  23. package/dist/nestjs/base.subscriber.cjs +287 -0
  24. package/dist/nestjs/base.subscriber.js +287 -0
  25. package/dist/nestjs/decorators.cjs +35 -0
  26. package/dist/nestjs/decorators.js +32 -0
  27. package/dist/nestjs/dlq.service.cjs +249 -0
  28. package/dist/nestjs/dlq.service.js +249 -0
  29. package/dist/nestjs/event-bus.module.cjs +152 -0
  30. package/dist/nestjs/event-bus.module.js +152 -0
  31. package/dist/nestjs/event-bus.service.cjs +243 -0
  32. package/dist/nestjs/event-bus.service.js +243 -0
  33. package/dist/nestjs/index.cjs +33 -0
  34. package/dist/nestjs/index.js +6 -0
  35. package/dist/nestjs/tokens.cjs +14 -0
  36. package/dist/nestjs/tokens.js +9 -0
  37. package/dist/testing/assertions.cjs +172 -0
  38. package/dist/testing/assertions.js +169 -0
  39. package/dist/testing/factories.cjs +122 -0
  40. package/dist/testing/factories.js +119 -0
  41. package/dist/testing/helpers.cjs +233 -0
  42. package/dist/testing/helpers.js +227 -0
  43. package/dist/testing/index.cjs +20 -0
  44. package/dist/testing/index.js +4 -0
  45. package/dist/testing/mock-event-bus.cjs +237 -0
  46. package/dist/testing/mock-event-bus.js +234 -0
  47. package/package.json +22 -22
  48. package/angular.d.ts +0 -1
  49. package/idempotency.esm.js +0 -701
  50. package/index.d.ts +0 -1
  51. package/nestjs.d.ts +0 -1
  52. package/nestjs.esm.js +0 -944
  53. package/testing.d.ts +0 -1
  54. package/testing.esm.js +0 -743
@@ -1,39 +1,4 @@
1
- /**
2
- * Core event types - framework-agnostic definitions
3
- */
4
- /**
5
- * Priority configuration with SLA targets
6
- */
7
- const EVENT_PRIORITIES = {
8
- critical: {
9
- sla: 100,
10
- weight: 10
11
- },
12
- // < 100ms
13
- high: {
14
- sla: 500,
15
- weight: 7
16
- },
17
- // < 500ms
18
- normal: {
19
- sla: 2000,
20
- weight: 5
21
- },
22
- // < 2s
23
- low: {
24
- sla: 30000,
25
- weight: 3
26
- },
27
- // < 30s
28
- bulk: {
29
- sla: 300000,
30
- weight: 1
31
- } // < 5min
32
- };
33
- const DEFAULT_EVENT_VERSION = {
34
- major: 1,
35
- minor: 0
36
- };
1
+ import { DEFAULT_EVENT_VERSION } from './types.js';
37
2
 
38
3
  /**
39
4
  * Generate a UUID v7 (time-sortable)
@@ -175,4 +140,4 @@ function createEventFactory(config) {
175
140
  };
176
141
  }
177
142
 
178
- export { DEFAULT_EVENT_VERSION as D, EVENT_PRIORITIES as E, createEventFactory as a, generateCorrelationId as b, createEvent as c, generateEventId as g };
143
+ export { createEvent, createEventFactory, generateCorrelationId, generateEventId };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * In-memory idempotency store
5
+ *
6
+ * Best for:
7
+ * - Development and testing
8
+ * - Single-instance deployments
9
+ * - Short-lived processes
10
+ *
11
+ * NOT recommended for:
12
+ * - Production multi-instance deployments
13
+ * - Long-running processes with many events
14
+ */
15
+ class InMemoryIdempotencyStore {
16
+ records = new Map();
17
+ cleanupTimer;
18
+ defaultTtlMs;
19
+ defaultLockTtlMs;
20
+ maxRecords;
21
+ constructor(config = {}) {
22
+ this.defaultTtlMs = config.defaultTtlMs ?? 24 * 60 * 60 * 1000; // 24 hours
23
+ this.defaultLockTtlMs = config.defaultLockTtlMs ?? 30 * 1000; // 30 seconds
24
+ this.maxRecords = config.maxRecords ?? 100000;
25
+ if (config.cleanupIntervalMs !== 0) {
26
+ const interval = config.cleanupIntervalMs ?? 60 * 1000;
27
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
28
+ }
29
+ }
30
+ makeKey(eventId, consumer) {
31
+ return `${consumer}:${eventId}`;
32
+ }
33
+ async check(event, consumer, options = {}) {
34
+ const key = this.makeKey(event.id, consumer);
35
+ const record = this.records.get(key);
36
+ const now = Date.now();
37
+ // Check for existing record
38
+ if (record && record.expiresAt > now) {
39
+ // If processing and lock expired, treat as stale and allow reprocessing
40
+ if (record.status === 'processing') {
41
+ const lockExpired = record.startedAt.getTime() + this.defaultLockTtlMs < now;
42
+ if (lockExpired) {
43
+ // Lock expired, allow new processing attempt
44
+ if (options.acquireLock) {
45
+ const updated = {
46
+ ...record,
47
+ startedAt: new Date(),
48
+ attempts: record.attempts + 1
49
+ };
50
+ this.records.set(key, updated);
51
+ return {
52
+ isDuplicate: false,
53
+ lockAcquired: true
54
+ };
55
+ }
56
+ return {
57
+ isDuplicate: false
58
+ };
59
+ }
60
+ // Still processing within lock period - treat as duplicate
61
+ return {
62
+ isDuplicate: true,
63
+ processedAt: record.startedAt
64
+ };
65
+ }
66
+ // Completed or failed record exists
67
+ return {
68
+ isDuplicate: true,
69
+ processedAt: record.completedAt ?? record.startedAt,
70
+ result: record.result
71
+ };
72
+ }
73
+ // No existing record or expired
74
+ if (options.acquireLock !== false) {
75
+ const ttlMs = options.lockTtlMs ?? this.defaultTtlMs;
76
+ const newRecord = {
77
+ eventId: event.id,
78
+ eventType: event.type,
79
+ startedAt: new Date(),
80
+ status: 'processing',
81
+ consumer,
82
+ attempts: 1,
83
+ expiresAt: now + ttlMs
84
+ };
85
+ this.records.set(key, newRecord);
86
+ this.enforceMaxRecords();
87
+ return {
88
+ isDuplicate: false,
89
+ lockAcquired: true
90
+ };
91
+ }
92
+ return {
93
+ isDuplicate: false
94
+ };
95
+ }
96
+ async markProcessing(event, consumer, ttlMs) {
97
+ const result = await this.check(event, consumer, {
98
+ acquireLock: true,
99
+ lockTtlMs: ttlMs ?? this.defaultLockTtlMs
100
+ });
101
+ return result.lockAcquired ?? false;
102
+ }
103
+ async markCompleted(event, consumer, result, ttlMs) {
104
+ const key = this.makeKey(event.id, consumer);
105
+ const record = this.records.get(key);
106
+ const now = Date.now();
107
+ const updated = {
108
+ eventId: event.id,
109
+ eventType: event.type,
110
+ startedAt: record?.startedAt ?? new Date(),
111
+ completedAt: new Date(),
112
+ status: 'completed',
113
+ result,
114
+ consumer,
115
+ attempts: record?.attempts ?? 1,
116
+ expiresAt: now + (ttlMs ?? this.defaultTtlMs)
117
+ };
118
+ this.records.set(key, updated);
119
+ }
120
+ async markFailed(event, consumer, error) {
121
+ const key = this.makeKey(event.id, consumer);
122
+ const record = this.records.get(key);
123
+ const now = Date.now();
124
+ const errorStr = error instanceof Error ? error.message : String(error);
125
+ const updated = {
126
+ eventId: event.id,
127
+ eventType: event.type,
128
+ startedAt: record?.startedAt ?? new Date(),
129
+ completedAt: new Date(),
130
+ status: 'failed',
131
+ error: errorStr,
132
+ consumer,
133
+ attempts: record?.attempts ?? 1,
134
+ expiresAt: now + this.defaultTtlMs
135
+ };
136
+ this.records.set(key, updated);
137
+ }
138
+ async releaseLock(event, consumer) {
139
+ const key = this.makeKey(event.id, consumer);
140
+ this.records.delete(key);
141
+ }
142
+ async getRecord(eventId, consumer) {
143
+ const key = this.makeKey(eventId, consumer);
144
+ const record = this.records.get(key);
145
+ if (!record || record.expiresAt < Date.now()) {
146
+ return null;
147
+ }
148
+ // Return without internal expiresAt field
149
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
150
+ const {
151
+ expiresAt: _,
152
+ ...publicRecord
153
+ } = record;
154
+ return publicRecord;
155
+ }
156
+ async cleanup() {
157
+ const now = Date.now();
158
+ let cleaned = 0;
159
+ for (const [key, record] of Array.from(this.records.entries())) {
160
+ if (record.expiresAt < now) {
161
+ this.records.delete(key);
162
+ cleaned++;
163
+ }
164
+ }
165
+ return cleaned;
166
+ }
167
+ /**
168
+ * Enforce max records limit using LRU-like eviction
169
+ */
170
+ enforceMaxRecords() {
171
+ if (this.records.size <= this.maxRecords) return;
172
+ // Sort by expires time (oldest first)
173
+ const entries = Array.from(this.records.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
174
+ // Remove oldest 10%
175
+ const toRemove = Math.ceil(this.maxRecords * 0.1);
176
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
177
+ this.records.delete(entries[i][0]);
178
+ }
179
+ }
180
+ /**
181
+ * Stop cleanup timer (for graceful shutdown)
182
+ */
183
+ dispose() {
184
+ if (this.cleanupTimer) {
185
+ clearInterval(this.cleanupTimer);
186
+ this.cleanupTimer = undefined;
187
+ }
188
+ }
189
+ /**
190
+ * Clear all records (for testing)
191
+ */
192
+ clear() {
193
+ this.records.clear();
194
+ }
195
+ /**
196
+ * Get stats (for monitoring)
197
+ */
198
+ getStats() {
199
+ return {
200
+ size: this.records.size,
201
+ maxRecords: this.maxRecords
202
+ };
203
+ }
204
+ }
205
+ /**
206
+ * Create an in-memory idempotency store
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const store = createInMemoryIdempotencyStore({
211
+ * defaultTtlMs: 60 * 60 * 1000, // 1 hour
212
+ * maxRecords: 50000,
213
+ * });
214
+ *
215
+ * // In your subscriber
216
+ * const result = await store.check(event, 'my-subscriber');
217
+ * if (result.isDuplicate) {
218
+ * return result.result; // Return cached result
219
+ * }
220
+ *
221
+ * try {
222
+ * const processResult = await processEvent(event);
223
+ * await store.markCompleted(event, 'my-subscriber', processResult);
224
+ * return processResult;
225
+ * } catch (error) {
226
+ * await store.markFailed(event, 'my-subscriber', error);
227
+ * throw error;
228
+ * }
229
+ * ```
230
+ */
231
+ function createInMemoryIdempotencyStore(config) {
232
+ return new InMemoryIdempotencyStore(config);
233
+ }
234
+ /**
235
+ * Generate an idempotency key from event
236
+ * Useful for custom implementations
237
+ */
238
+ function generateIdempotencyKey(event, consumer) {
239
+ return `idempotency:${consumer}:${event.id}`;
240
+ }
241
+ /**
242
+ * Generate an idempotency key from correlation ID
243
+ * Useful for request-level idempotency
244
+ */
245
+ function generateCorrelationKey(correlationId, operation) {
246
+ return `idempotency:correlation:${operation}:${correlationId}`;
247
+ }
248
+
249
+ exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
250
+ exports.createInMemoryIdempotencyStore = createInMemoryIdempotencyStore;
251
+ exports.generateCorrelationKey = generateCorrelationKey;
252
+ exports.generateIdempotencyKey = generateIdempotencyKey;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * In-memory idempotency store
3
+ *
4
+ * Best for:
5
+ * - Development and testing
6
+ * - Single-instance deployments
7
+ * - Short-lived processes
8
+ *
9
+ * NOT recommended for:
10
+ * - Production multi-instance deployments
11
+ * - Long-running processes with many events
12
+ */
13
+ class InMemoryIdempotencyStore {
14
+ records = new Map();
15
+ cleanupTimer;
16
+ defaultTtlMs;
17
+ defaultLockTtlMs;
18
+ maxRecords;
19
+ constructor(config = {}) {
20
+ this.defaultTtlMs = config.defaultTtlMs ?? 24 * 60 * 60 * 1000; // 24 hours
21
+ this.defaultLockTtlMs = config.defaultLockTtlMs ?? 30 * 1000; // 30 seconds
22
+ this.maxRecords = config.maxRecords ?? 100000;
23
+ if (config.cleanupIntervalMs !== 0) {
24
+ const interval = config.cleanupIntervalMs ?? 60 * 1000;
25
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
26
+ }
27
+ }
28
+ makeKey(eventId, consumer) {
29
+ return `${consumer}:${eventId}`;
30
+ }
31
+ async check(event, consumer, options = {}) {
32
+ const key = this.makeKey(event.id, consumer);
33
+ const record = this.records.get(key);
34
+ const now = Date.now();
35
+ // Check for existing record
36
+ if (record && record.expiresAt > now) {
37
+ // If processing and lock expired, treat as stale and allow reprocessing
38
+ if (record.status === 'processing') {
39
+ const lockExpired = record.startedAt.getTime() + this.defaultLockTtlMs < now;
40
+ if (lockExpired) {
41
+ // Lock expired, allow new processing attempt
42
+ if (options.acquireLock) {
43
+ const updated = {
44
+ ...record,
45
+ startedAt: new Date(),
46
+ attempts: record.attempts + 1
47
+ };
48
+ this.records.set(key, updated);
49
+ return {
50
+ isDuplicate: false,
51
+ lockAcquired: true
52
+ };
53
+ }
54
+ return {
55
+ isDuplicate: false
56
+ };
57
+ }
58
+ // Still processing within lock period - treat as duplicate
59
+ return {
60
+ isDuplicate: true,
61
+ processedAt: record.startedAt
62
+ };
63
+ }
64
+ // Completed or failed record exists
65
+ return {
66
+ isDuplicate: true,
67
+ processedAt: record.completedAt ?? record.startedAt,
68
+ result: record.result
69
+ };
70
+ }
71
+ // No existing record or expired
72
+ if (options.acquireLock !== false) {
73
+ const ttlMs = options.lockTtlMs ?? this.defaultTtlMs;
74
+ const newRecord = {
75
+ eventId: event.id,
76
+ eventType: event.type,
77
+ startedAt: new Date(),
78
+ status: 'processing',
79
+ consumer,
80
+ attempts: 1,
81
+ expiresAt: now + ttlMs
82
+ };
83
+ this.records.set(key, newRecord);
84
+ this.enforceMaxRecords();
85
+ return {
86
+ isDuplicate: false,
87
+ lockAcquired: true
88
+ };
89
+ }
90
+ return {
91
+ isDuplicate: false
92
+ };
93
+ }
94
+ async markProcessing(event, consumer, ttlMs) {
95
+ const result = await this.check(event, consumer, {
96
+ acquireLock: true,
97
+ lockTtlMs: ttlMs ?? this.defaultLockTtlMs
98
+ });
99
+ return result.lockAcquired ?? false;
100
+ }
101
+ async markCompleted(event, consumer, result, ttlMs) {
102
+ const key = this.makeKey(event.id, consumer);
103
+ const record = this.records.get(key);
104
+ const now = Date.now();
105
+ const updated = {
106
+ eventId: event.id,
107
+ eventType: event.type,
108
+ startedAt: record?.startedAt ?? new Date(),
109
+ completedAt: new Date(),
110
+ status: 'completed',
111
+ result,
112
+ consumer,
113
+ attempts: record?.attempts ?? 1,
114
+ expiresAt: now + (ttlMs ?? this.defaultTtlMs)
115
+ };
116
+ this.records.set(key, updated);
117
+ }
118
+ async markFailed(event, consumer, error) {
119
+ const key = this.makeKey(event.id, consumer);
120
+ const record = this.records.get(key);
121
+ const now = Date.now();
122
+ const errorStr = error instanceof Error ? error.message : String(error);
123
+ const updated = {
124
+ eventId: event.id,
125
+ eventType: event.type,
126
+ startedAt: record?.startedAt ?? new Date(),
127
+ completedAt: new Date(),
128
+ status: 'failed',
129
+ error: errorStr,
130
+ consumer,
131
+ attempts: record?.attempts ?? 1,
132
+ expiresAt: now + this.defaultTtlMs
133
+ };
134
+ this.records.set(key, updated);
135
+ }
136
+ async releaseLock(event, consumer) {
137
+ const key = this.makeKey(event.id, consumer);
138
+ this.records.delete(key);
139
+ }
140
+ async getRecord(eventId, consumer) {
141
+ const key = this.makeKey(eventId, consumer);
142
+ const record = this.records.get(key);
143
+ if (!record || record.expiresAt < Date.now()) {
144
+ return null;
145
+ }
146
+ // Return without internal expiresAt field
147
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
148
+ const {
149
+ expiresAt: _,
150
+ ...publicRecord
151
+ } = record;
152
+ return publicRecord;
153
+ }
154
+ async cleanup() {
155
+ const now = Date.now();
156
+ let cleaned = 0;
157
+ for (const [key, record] of Array.from(this.records.entries())) {
158
+ if (record.expiresAt < now) {
159
+ this.records.delete(key);
160
+ cleaned++;
161
+ }
162
+ }
163
+ return cleaned;
164
+ }
165
+ /**
166
+ * Enforce max records limit using LRU-like eviction
167
+ */
168
+ enforceMaxRecords() {
169
+ if (this.records.size <= this.maxRecords) return;
170
+ // Sort by expires time (oldest first)
171
+ const entries = Array.from(this.records.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
172
+ // Remove oldest 10%
173
+ const toRemove = Math.ceil(this.maxRecords * 0.1);
174
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
175
+ this.records.delete(entries[i][0]);
176
+ }
177
+ }
178
+ /**
179
+ * Stop cleanup timer (for graceful shutdown)
180
+ */
181
+ dispose() {
182
+ if (this.cleanupTimer) {
183
+ clearInterval(this.cleanupTimer);
184
+ this.cleanupTimer = undefined;
185
+ }
186
+ }
187
+ /**
188
+ * Clear all records (for testing)
189
+ */
190
+ clear() {
191
+ this.records.clear();
192
+ }
193
+ /**
194
+ * Get stats (for monitoring)
195
+ */
196
+ getStats() {
197
+ return {
198
+ size: this.records.size,
199
+ maxRecords: this.maxRecords
200
+ };
201
+ }
202
+ }
203
+ /**
204
+ * Create an in-memory idempotency store
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const store = createInMemoryIdempotencyStore({
209
+ * defaultTtlMs: 60 * 60 * 1000, // 1 hour
210
+ * maxRecords: 50000,
211
+ * });
212
+ *
213
+ * // In your subscriber
214
+ * const result = await store.check(event, 'my-subscriber');
215
+ * if (result.isDuplicate) {
216
+ * return result.result; // Return cached result
217
+ * }
218
+ *
219
+ * try {
220
+ * const processResult = await processEvent(event);
221
+ * await store.markCompleted(event, 'my-subscriber', processResult);
222
+ * return processResult;
223
+ * } catch (error) {
224
+ * await store.markFailed(event, 'my-subscriber', error);
225
+ * throw error;
226
+ * }
227
+ * ```
228
+ */
229
+ function createInMemoryIdempotencyStore(config) {
230
+ return new InMemoryIdempotencyStore(config);
231
+ }
232
+ /**
233
+ * Generate an idempotency key from event
234
+ * Useful for custom implementations
235
+ */
236
+ function generateIdempotencyKey(event, consumer) {
237
+ return `idempotency:${consumer}:${event.id}`;
238
+ }
239
+ /**
240
+ * Generate an idempotency key from correlation ID
241
+ * Useful for request-level idempotency
242
+ */
243
+ function generateCorrelationKey(correlationId, operation) {
244
+ return `idempotency:correlation:${operation}:${correlationId}`;
245
+ }
246
+
247
+ export { InMemoryIdempotencyStore, createInMemoryIdempotencyStore, generateCorrelationKey, generateIdempotencyKey };