@yuva-devlab/events 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/dist/index.cjs +297 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +425 -0
- package/dist/index.d.ts +425 -0
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yuva Devlab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var regex = require('@yuva-devlab/regex');
|
|
5
|
+
var logger = require('@yuva-devlab/logger');
|
|
6
|
+
|
|
7
|
+
// src/contracts/envelope.ts
|
|
8
|
+
var UuidSchema = zod.z.string().regex(regex.UUID_REGEX, "Must be a valid RFC UUID format");
|
|
9
|
+
var DomainEventEnvelopeSchema = zod.z.object({
|
|
10
|
+
/** Unique event occurrence UUID for deduplication */
|
|
11
|
+
eventId: UuidSchema,
|
|
12
|
+
/** Qualified dot-notated or uppercase event type (e.g. AGENT.EXECUTION.COMPLETED) */
|
|
13
|
+
eventType: zod.z.string().min(1, "Event type must not be empty"),
|
|
14
|
+
/** Optional execution or correlation identifier */
|
|
15
|
+
executionId: zod.z.string().optional(),
|
|
16
|
+
/** Optional tenant partition identifier */
|
|
17
|
+
tenantId: zod.z.string().optional(),
|
|
18
|
+
/** Distributed trace identifier for end-to-end telemetry */
|
|
19
|
+
traceId: zod.z.string().optional(),
|
|
20
|
+
/** ISO-8601 string or Date object representing event generation time */
|
|
21
|
+
timestamp: zod.z.string().datetime().default(() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
22
|
+
/** Domain-specific event payload matching the eventType */
|
|
23
|
+
payload: zod.z.record(zod.z.string(), zod.z.unknown())
|
|
24
|
+
}).describe(
|
|
25
|
+
"Universal envelope for domain events transported via outbox, memory, or stream"
|
|
26
|
+
);
|
|
27
|
+
function createEventEnvelope(options) {
|
|
28
|
+
const envelope = {
|
|
29
|
+
eventId: options.eventId ?? crypto.randomUUID(),
|
|
30
|
+
eventType: options.eventType,
|
|
31
|
+
payload: options.payload,
|
|
32
|
+
executionId: options.executionId,
|
|
33
|
+
tenantId: options.tenantId,
|
|
34
|
+
traceId: options.traceId,
|
|
35
|
+
timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
36
|
+
};
|
|
37
|
+
DomainEventEnvelopeSchema.parse(envelope);
|
|
38
|
+
return envelope;
|
|
39
|
+
}
|
|
40
|
+
var InMemoryEventBus = class {
|
|
41
|
+
/** Map of event pattern to set of subscribed callback handlers */
|
|
42
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
43
|
+
/**
|
|
44
|
+
* Emits a single domain event envelope to all matching listeners.
|
|
45
|
+
*
|
|
46
|
+
* @param event - The validated event envelope.
|
|
47
|
+
*/
|
|
48
|
+
async publish(event) {
|
|
49
|
+
const matchingHandlers = this.getMatchingHandlers(event.eventType);
|
|
50
|
+
if (matchingHandlers.length === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await Promise.allSettled(
|
|
54
|
+
matchingHandlers.map(async (handler) => {
|
|
55
|
+
try {
|
|
56
|
+
await handler(event);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.logger.error("Event handler threw an unhandled exception", {
|
|
59
|
+
eventType: event.eventType,
|
|
60
|
+
eventId: event.eventId,
|
|
61
|
+
error: error instanceof Error ? error.message : String(error)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Emits an array of domain events sequentially or concurrently.
|
|
69
|
+
*
|
|
70
|
+
* @param events - List of event envelopes to publish.
|
|
71
|
+
*/
|
|
72
|
+
async publishBatch(events) {
|
|
73
|
+
for (const event of events) {
|
|
74
|
+
await this.publish(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Registers a listener for an exact event type or wildcard pattern (*, domain.*).
|
|
79
|
+
*
|
|
80
|
+
* @param eventPattern - Topic pattern to subscribe to.
|
|
81
|
+
* @param handler - Asynchronous callback handler.
|
|
82
|
+
* @returns Unsubscribe handle.
|
|
83
|
+
*/
|
|
84
|
+
subscribe(eventPattern, handler) {
|
|
85
|
+
if (!this.subscribers.has(eventPattern)) {
|
|
86
|
+
this.subscribers.set(eventPattern, /* @__PURE__ */ new Set());
|
|
87
|
+
}
|
|
88
|
+
const handlerSet = this.subscribers.get(eventPattern);
|
|
89
|
+
handlerSet.add(handler);
|
|
90
|
+
return {
|
|
91
|
+
unsubscribe: () => {
|
|
92
|
+
handlerSet.delete(handler);
|
|
93
|
+
if (handlerSet.size === 0) {
|
|
94
|
+
this.subscribers.delete(eventPattern);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clears all active subscribers from memory.
|
|
101
|
+
*/
|
|
102
|
+
clear() {
|
|
103
|
+
this.subscribers.clear();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolves all handlers matching the given concrete event type.
|
|
107
|
+
*
|
|
108
|
+
* @param concreteType - The incoming eventType string.
|
|
109
|
+
* @returns Array of unique matching handlers.
|
|
110
|
+
*/
|
|
111
|
+
getMatchingHandlers(concreteType) {
|
|
112
|
+
const matching = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const [pattern, handlers] of this.subscribers.entries()) {
|
|
114
|
+
if (this.matchesPattern(pattern, concreteType)) {
|
|
115
|
+
for (const handler of handlers) {
|
|
116
|
+
matching.add(handler);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Array.from(matching);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Evaluates if a concrete event type matches a subscription pattern.
|
|
124
|
+
* Supports global wildcard "*" and prefix wildcards "prefix.*".
|
|
125
|
+
*
|
|
126
|
+
* @param pattern - Registered pattern.
|
|
127
|
+
* @param target - Concrete event type.
|
|
128
|
+
* @returns True if matched.
|
|
129
|
+
*/
|
|
130
|
+
matchesPattern(pattern, target) {
|
|
131
|
+
if (pattern === "*" || pattern === target) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (pattern.endsWith(".*")) {
|
|
135
|
+
const prefix = pattern.slice(0, -2);
|
|
136
|
+
return target.startsWith(prefix + ".");
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var OutboxStatus = {
|
|
142
|
+
PENDING: "PENDING",
|
|
143
|
+
PROCESSING: "PROCESSING",
|
|
144
|
+
PUBLISHED: "PUBLISHED",
|
|
145
|
+
FAILED: "FAILED"
|
|
146
|
+
};
|
|
147
|
+
var OutboxRecordSchema = zod.z.object({
|
|
148
|
+
id: zod.z.string().uuid("Outbox record ID must be an RFC UUID"),
|
|
149
|
+
envelope: DomainEventEnvelopeSchema,
|
|
150
|
+
status: zod.z.enum([
|
|
151
|
+
OutboxStatus.PENDING,
|
|
152
|
+
OutboxStatus.PROCESSING,
|
|
153
|
+
OutboxStatus.PUBLISHED,
|
|
154
|
+
OutboxStatus.FAILED
|
|
155
|
+
]),
|
|
156
|
+
attempts: zod.z.number().int().nonnegative().default(0),
|
|
157
|
+
maxAttempts: zod.z.number().int().positive().default(5),
|
|
158
|
+
lastError: zod.z.string().optional(),
|
|
159
|
+
createdAt: zod.z.string().datetime(),
|
|
160
|
+
updatedAt: zod.z.string().datetime(),
|
|
161
|
+
publishedAt: zod.z.string().datetime().optional()
|
|
162
|
+
});
|
|
163
|
+
var OutboxRelay = class {
|
|
164
|
+
constructor(store, publisher, options = {}) {
|
|
165
|
+
this.store = store;
|
|
166
|
+
this.publisher = publisher;
|
|
167
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
168
|
+
this.batchSize = options.batchSize ?? 50;
|
|
169
|
+
}
|
|
170
|
+
isRunning = false;
|
|
171
|
+
timer = null;
|
|
172
|
+
pollIntervalMs;
|
|
173
|
+
batchSize;
|
|
174
|
+
/**
|
|
175
|
+
* Starts the background relay polling loop.
|
|
176
|
+
*/
|
|
177
|
+
start() {
|
|
178
|
+
if (this.isRunning) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.isRunning = true;
|
|
182
|
+
this.scheduleNextTick();
|
|
183
|
+
logger.logger.info("OutboxRelay started", {
|
|
184
|
+
pollIntervalMs: this.pollIntervalMs,
|
|
185
|
+
batchSize: this.batchSize
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Stops the background relay polling loop.
|
|
190
|
+
*/
|
|
191
|
+
stop() {
|
|
192
|
+
this.isRunning = false;
|
|
193
|
+
if (this.timer) {
|
|
194
|
+
clearTimeout(this.timer);
|
|
195
|
+
this.timer = null;
|
|
196
|
+
}
|
|
197
|
+
logger.logger.info("OutboxRelay stopped");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Executes a single polling tick, draining pending outbox records.
|
|
201
|
+
*
|
|
202
|
+
* @returns Number of events successfully processed during this tick.
|
|
203
|
+
*/
|
|
204
|
+
async drainOnce() {
|
|
205
|
+
try {
|
|
206
|
+
const records = await this.store.fetchPending(this.batchSize);
|
|
207
|
+
if (records.length === 0) {
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
let processedCount = 0;
|
|
211
|
+
for (const record of records) {
|
|
212
|
+
try {
|
|
213
|
+
await this.publisher.publish(record.envelope);
|
|
214
|
+
await this.store.markPublished(record.id);
|
|
215
|
+
processedCount++;
|
|
216
|
+
} catch (publishError) {
|
|
217
|
+
const errMsg = publishError instanceof Error ? publishError.message : String(publishError);
|
|
218
|
+
logger.logger.error("OutboxRelay failed to publish event", {
|
|
219
|
+
recordId: record.id,
|
|
220
|
+
error: errMsg
|
|
221
|
+
});
|
|
222
|
+
await this.store.markFailed(record.id, errMsg);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return processedCount;
|
|
226
|
+
} catch (storeError) {
|
|
227
|
+
logger.logger.error("OutboxRelay error fetching pending records", {
|
|
228
|
+
error: storeError instanceof Error ? storeError.message : String(storeError)
|
|
229
|
+
});
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Schedules the next execution tick with timer guard.
|
|
235
|
+
*/
|
|
236
|
+
scheduleNextTick() {
|
|
237
|
+
if (!this.isRunning) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.timer = setTimeout(async () => {
|
|
241
|
+
await this.drainOnce();
|
|
242
|
+
this.scheduleNextTick();
|
|
243
|
+
}, this.pollIntervalMs);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/idempotency/in-memory-idempotency-store.ts
|
|
248
|
+
var InMemoryIdempotencyStore = class {
|
|
249
|
+
records = /* @__PURE__ */ new Map();
|
|
250
|
+
async acquire(key, ttlSeconds = 60) {
|
|
251
|
+
this.cleanExpired();
|
|
252
|
+
const existing = this.records.get(key);
|
|
253
|
+
if (existing) {
|
|
254
|
+
if (existing.state === "COMPLETED") {
|
|
255
|
+
return {
|
|
256
|
+
state: "ALREADY_COMPLETED",
|
|
257
|
+
completedAt: existing.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { state: "IN_FLIGHT" };
|
|
261
|
+
}
|
|
262
|
+
this.records.set(key, {
|
|
263
|
+
state: "IN_FLIGHT",
|
|
264
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
265
|
+
});
|
|
266
|
+
return { state: "ACQUIRED" };
|
|
267
|
+
}
|
|
268
|
+
async markCompleted(key, retentionSeconds = 86400) {
|
|
269
|
+
this.records.set(key, {
|
|
270
|
+
state: "COMPLETED",
|
|
271
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
272
|
+
expiresAt: Date.now() + retentionSeconds * 1e3
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async release(key) {
|
|
276
|
+
this.records.delete(key);
|
|
277
|
+
}
|
|
278
|
+
cleanExpired() {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
for (const [key, entry] of this.records.entries()) {
|
|
281
|
+
if (entry.expiresAt <= now) {
|
|
282
|
+
this.records.delete(key);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
exports.DomainEventEnvelopeSchema = DomainEventEnvelopeSchema;
|
|
289
|
+
exports.InMemoryEventBus = InMemoryEventBus;
|
|
290
|
+
exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
|
|
291
|
+
exports.OutboxRecordSchema = OutboxRecordSchema;
|
|
292
|
+
exports.OutboxRelay = OutboxRelay;
|
|
293
|
+
exports.OutboxStatus = OutboxStatus;
|
|
294
|
+
exports.UuidSchema = UuidSchema;
|
|
295
|
+
exports.createEventEnvelope = createEventEnvelope;
|
|
296
|
+
//# sourceMappingURL=index.cjs.map
|
|
297
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contracts/envelope.ts","../src/bus/in-memory-event-bus.ts","../src/outbox/outbox-record.ts","../src/outbox/outbox-relay.ts","../src/idempotency/in-memory-idempotency-store.ts"],"names":["z","UUID_REGEX","logger"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @file packages/events/src/contracts/envelope.ts\n * @description Standard transactional outbox and event bus envelope schema for DevLab.\n *\n * Guarantees cross-service traceability, event ordering, and idempotency across all services.\n *\n * @module @yuva-devlab/events\n */\n\nimport { z } from \"zod\";\nimport { UUID_REGEX } from \"@yuva-devlab/regex\";\n\n/**\n * Standard Zod validator for RFC UUID strings.\n */\nexport const UuidSchema = z\n .string()\n .regex(UUID_REGEX, \"Must be a valid RFC UUID format\");\n\n/**\n * Zod schema defining the canonical metadata envelope wrapping all domain events.\n */\nexport const DomainEventEnvelopeSchema = z\n .object({\n /** Unique event occurrence UUID for deduplication */\n eventId: UuidSchema,\n /** Qualified dot-notated or uppercase event type (e.g. AGENT.EXECUTION.COMPLETED) */\n eventType: z.string().min(1, \"Event type must not be empty\"),\n /** Optional execution or correlation identifier */\n executionId: z.string().optional(),\n /** Optional tenant partition identifier */\n tenantId: z.string().optional(),\n /** Distributed trace identifier for end-to-end telemetry */\n traceId: z.string().optional(),\n /** ISO-8601 string or Date object representing event generation time */\n timestamp: z\n .string()\n .datetime()\n .default(() => new Date().toISOString()),\n /** Domain-specific event payload matching the eventType */\n payload: z.record(z.string(), z.unknown()),\n })\n .describe(\n \"Universal envelope for domain events transported via outbox, memory, or stream\",\n );\n\n/**\n * Inferred type of the canonical domain event envelope.\n */\nexport type DomainEventEnvelope<\n TPayload extends Record<string, unknown> = Record<string, unknown>,\n> = Omit<z.infer<typeof DomainEventEnvelopeSchema>, \"payload\"> & {\n payload: TPayload;\n};\n\n/**\n * Factory options for generating a new DomainEventEnvelope.\n */\nexport interface CreateEventEnvelopeOptions<\n TPayload extends Record<string, unknown>,\n> {\n eventId?: string;\n eventType: string;\n payload: TPayload;\n executionId?: string;\n tenantId?: string;\n traceId?: string;\n timestamp?: string;\n}\n\n/**\n * Generates a valid domain event envelope with defaults.\n *\n * @param options - Factory configuration options.\n * @returns Fully constructed DomainEventEnvelope.\n */\nexport function createEventEnvelope<TPayload extends Record<string, unknown>>(\n options: CreateEventEnvelopeOptions<TPayload>,\n): DomainEventEnvelope<TPayload> {\n const envelope: DomainEventEnvelope<TPayload> = {\n eventId: options.eventId ?? crypto.randomUUID(),\n eventType: options.eventType,\n payload: options.payload,\n executionId: options.executionId,\n tenantId: options.tenantId,\n traceId: options.traceId,\n timestamp: options.timestamp ?? new Date().toISOString(),\n };\n\n // Validate envelope constraints against schema\n DomainEventEnvelopeSchema.parse(envelope);\n return envelope;\n}\n","/**\n * @file packages/events/src/bus/in-memory-event-bus.ts\n * @description Fast, non-blocking in-memory event bus implementation supporting wildcards.\n * @module @yuva-devlab/events\n */\n\nimport { logger } from \"@yuva-devlab/logger\";\nimport type {\n DomainEventEnvelope,\n EventHandler,\n EventSubscription,\n IEventBus,\n} from \"../contracts\";\n\n/**\n * In-memory event bus with pattern matching, asynchronous execution, and fault isolation.\n */\nexport class InMemoryEventBus implements IEventBus {\n /** Map of event pattern to set of subscribed callback handlers */\n private readonly subscribers = new Map<string, Set<EventHandler>>();\n\n /**\n * Emits a single domain event envelope to all matching listeners.\n *\n * @param event - The validated event envelope.\n */\n public async publish(event: DomainEventEnvelope): Promise<void> {\n const matchingHandlers = this.getMatchingHandlers(event.eventType);\n\n // If no handlers are subscribed, return early\n if (matchingHandlers.length === 0) {\n return;\n }\n\n // Execute all handlers concurrently with isolated failure boundaries\n await Promise.allSettled(\n matchingHandlers.map(async (handler) => {\n try {\n await handler(event);\n } catch (error) {\n logger.error(\"Event handler threw an unhandled exception\", {\n eventType: event.eventType,\n eventId: event.eventId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }),\n );\n }\n\n /**\n * Emits an array of domain events sequentially or concurrently.\n *\n * @param events - List of event envelopes to publish.\n */\n public async publishBatch(events: DomainEventEnvelope[]): Promise<void> {\n for (const event of events) {\n await this.publish(event);\n }\n }\n\n /**\n * Registers a listener for an exact event type or wildcard pattern (*, domain.*).\n *\n * @param eventPattern - Topic pattern to subscribe to.\n * @param handler - Asynchronous callback handler.\n * @returns Unsubscribe handle.\n */\n public subscribe(\n eventPattern: string,\n handler: EventHandler,\n ): EventSubscription {\n if (!this.subscribers.has(eventPattern)) {\n this.subscribers.set(eventPattern, new Set());\n }\n\n const handlerSet = this.subscribers.get(eventPattern)!;\n handlerSet.add(handler);\n\n return {\n unsubscribe: () => {\n handlerSet.delete(handler);\n // Clean up empty key\n if (handlerSet.size === 0) {\n this.subscribers.delete(eventPattern);\n }\n },\n };\n }\n\n /**\n * Clears all active subscribers from memory.\n */\n public clear(): void {\n this.subscribers.clear();\n }\n\n /**\n * Resolves all handlers matching the given concrete event type.\n *\n * @param concreteType - The incoming eventType string.\n * @returns Array of unique matching handlers.\n */\n private getMatchingHandlers(concreteType: string): EventHandler[] {\n const matching = new Set<EventHandler>();\n\n for (const [pattern, handlers] of this.subscribers.entries()) {\n if (this.matchesPattern(pattern, concreteType)) {\n for (const handler of handlers) {\n matching.add(handler);\n }\n }\n }\n\n return Array.from(matching);\n }\n\n /**\n * Evaluates if a concrete event type matches a subscription pattern.\n * Supports global wildcard \"*\" and prefix wildcards \"prefix.*\".\n *\n * @param pattern - Registered pattern.\n * @param target - Concrete event type.\n * @returns True if matched.\n */\n private matchesPattern(pattern: string, target: string): boolean {\n // Exact match or universal catch-all\n if (pattern === \"*\" || pattern === target) {\n return true;\n }\n\n // Prefix wildcard match (e.g. \"AGENT.*\")\n if (pattern.endsWith(\".*\")) {\n const prefix = pattern.slice(0, -2);\n return target.startsWith(prefix + \".\");\n }\n\n return false;\n }\n}\n","/**\n * @file packages/events/src/outbox/outbox-record.ts\n * @description Outbox table record model and validation schema for the Transactional Outbox pattern.\n * @module @yuva-devlab/events\n */\n\nimport { z } from \"zod\";\nimport { DomainEventEnvelopeSchema } from \"../contracts/envelope\";\n\n/**\n * Lifecycle status of an outbox record awaiting dispatch.\n */\nexport const OutboxStatus = {\n PENDING: \"PENDING\",\n PROCESSING: \"PROCESSING\",\n PUBLISHED: \"PUBLISHED\",\n FAILED: \"FAILED\",\n} as const;\n\nexport type OutboxStatus = (typeof OutboxStatus)[keyof typeof OutboxStatus];\n\n/**\n * Zod schema validating a persisted outbox record.\n */\nexport const OutboxRecordSchema = z.object({\n id: z.string().uuid(\"Outbox record ID must be an RFC UUID\"),\n envelope: DomainEventEnvelopeSchema,\n status: z.enum([\n OutboxStatus.PENDING,\n OutboxStatus.PROCESSING,\n OutboxStatus.PUBLISHED,\n OutboxStatus.FAILED,\n ]),\n attempts: z.number().int().nonnegative().default(0),\n maxAttempts: z.number().int().positive().default(5),\n lastError: z.string().optional(),\n createdAt: z.string().datetime(),\n updatedAt: z.string().datetime(),\n publishedAt: z.string().datetime().optional(),\n});\n\nexport type OutboxRecord = z.infer<typeof OutboxRecordSchema>;\n","/**\n * @file packages/events/src/outbox/outbox-relay.ts\n * @description Background polling relay engine delivering transactional outbox events to the event bus.\n * @module @yuva-devlab/events\n */\n\nimport { logger } from \"@yuva-devlab/logger\";\nimport type { IEventPublisher } from \"../contracts/event-bus.interface\";\nimport type { IOutboxStore } from \"./outbox-store.interface\";\n\n/**\n * Configuration options for the background outbox dispatch relay.\n */\nexport interface OutboxRelayOptions {\n /** Polling interval in milliseconds (default: 1000ms) */\n pollIntervalMs?: number;\n /** Max records to drain in each tick (default: 50) */\n batchSize?: number;\n}\n\n/**\n * Background poller ensuring guaranteed at-least-once delivery of outbox records.\n */\nexport class OutboxRelay {\n private isRunning = false;\n private timer: NodeJS.Timeout | null = null;\n private readonly pollIntervalMs: number;\n private readonly batchSize: number;\n\n constructor(\n private readonly store: IOutboxStore,\n private readonly publisher: IEventPublisher,\n options: OutboxRelayOptions = {},\n ) {\n this.pollIntervalMs = options.pollIntervalMs ?? 1000;\n this.batchSize = options.batchSize ?? 50;\n }\n\n /**\n * Starts the background relay polling loop.\n */\n public start(): void {\n if (this.isRunning) {\n return;\n }\n this.isRunning = true;\n this.scheduleNextTick();\n logger.info(\"OutboxRelay started\", {\n pollIntervalMs: this.pollIntervalMs,\n batchSize: this.batchSize,\n });\n }\n\n /**\n * Stops the background relay polling loop.\n */\n public stop(): void {\n this.isRunning = false;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n logger.info(\"OutboxRelay stopped\");\n }\n\n /**\n * Executes a single polling tick, draining pending outbox records.\n *\n * @returns Number of events successfully processed during this tick.\n */\n public async drainOnce(): Promise<number> {\n try {\n const records = await this.store.fetchPending(this.batchSize);\n if (records.length === 0) {\n return 0;\n }\n\n let processedCount = 0;\n for (const record of records) {\n try {\n await this.publisher.publish(record.envelope);\n await this.store.markPublished(record.id);\n processedCount++;\n } catch (publishError) {\n const errMsg =\n publishError instanceof Error\n ? publishError.message\n : String(publishError);\n logger.error(\"OutboxRelay failed to publish event\", {\n recordId: record.id,\n error: errMsg,\n });\n await this.store.markFailed(record.id, errMsg);\n }\n }\n\n return processedCount;\n } catch (storeError) {\n logger.error(\"OutboxRelay error fetching pending records\", {\n error:\n storeError instanceof Error ? storeError.message : String(storeError),\n });\n return 0;\n }\n }\n\n /**\n * Schedules the next execution tick with timer guard.\n */\n private scheduleNextTick(): void {\n if (!this.isRunning) {\n return;\n }\n this.timer = setTimeout(async () => {\n await this.drainOnce();\n this.scheduleNextTick();\n }, this.pollIntervalMs);\n }\n}\n","/**\n * @file packages/events/src/idempotency/in-memory-idempotency-store.ts\n * @description In-memory TTL idempotency store implementation for testing and single-node services.\n * @module @yuva-devlab/events\n */\n\nimport type {\n IIdempotencyStore,\n IdempotencyAcquireResult,\n} from \"./idempotency-store.interface\";\n\ninterface RecordEntry {\n state: \"IN_FLIGHT\" | \"COMPLETED\";\n expiresAt: number;\n completedAt?: string;\n}\n\n/**\n * Single-process in-memory idempotency store with active expiration.\n */\nexport class InMemoryIdempotencyStore implements IIdempotencyStore {\n private readonly records = new Map<string, RecordEntry>();\n\n public async acquire(\n key: string,\n ttlSeconds = 60,\n ): Promise<IdempotencyAcquireResult> {\n this.cleanExpired();\n const existing = this.records.get(key);\n\n if (existing) {\n if (existing.state === \"COMPLETED\") {\n return {\n state: \"ALREADY_COMPLETED\",\n completedAt: existing.completedAt ?? new Date().toISOString(),\n };\n }\n return { state: \"IN_FLIGHT\" };\n }\n\n // Acquire new in-flight lock\n this.records.set(key, {\n state: \"IN_FLIGHT\",\n expiresAt: Date.now() + ttlSeconds * 1000,\n });\n\n return { state: \"ACQUIRED\" };\n }\n\n public async markCompleted(\n key: string,\n retentionSeconds = 86400,\n ): Promise<void> {\n this.records.set(key, {\n state: \"COMPLETED\",\n completedAt: new Date().toISOString(),\n expiresAt: Date.now() + retentionSeconds * 1000,\n });\n }\n\n public async release(key: string): Promise<void> {\n this.records.delete(key);\n }\n\n private cleanExpired(): void {\n const now = Date.now();\n for (const [key, entry] of this.records.entries()) {\n if (entry.expiresAt <= now) {\n this.records.delete(key);\n }\n }\n }\n}\n"]}
|