@open-mercato/events 0.6.6-develop.6384.1.f06fc0b42c → 0.6.6-develop.6385.1.9a81faa5f0
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/dist/bus.js +32 -12
- package/dist/bus.js.map +2 -2
- package/package.json +3 -3
- package/src/__tests__/local.strategy.test.ts +22 -0
- package/src/bus.ts +51 -16
- package/src/types.ts +8 -1
package/dist/bus.js
CHANGED
|
@@ -4,6 +4,10 @@ import { isSingleDeliveryRequested } from "./single-delivery.js";
|
|
|
4
4
|
import { matchEventPattern } from "@open-mercato/shared/lib/events/patterns";
|
|
5
5
|
import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
6
6
|
import { isBroadcastEvent } from "@open-mercato/shared/modules/events";
|
|
7
|
+
import {
|
|
8
|
+
inferModuleIdFromResourceId,
|
|
9
|
+
withModuleResourceUsage
|
|
10
|
+
} from "@open-mercato/shared/lib/modules/resource-usage";
|
|
7
11
|
import { registerCrossProcessEventListener } from "./bridge.js";
|
|
8
12
|
import { publishCrossProcessEvent } from "./bridge.js";
|
|
9
13
|
const EVENTS_QUEUE_NAME = "events";
|
|
@@ -60,7 +64,7 @@ function registerProducerShutdownHook() {
|
|
|
60
64
|
}
|
|
61
65
|
function createEventBus(opts) {
|
|
62
66
|
const listeners = /* @__PURE__ */ new Map();
|
|
63
|
-
const
|
|
67
|
+
const persistentSubscribers = /* @__PURE__ */ new Set();
|
|
64
68
|
const queueStrategy = opts.queueStrategy ?? (process.env.QUEUE_STRATEGY === "async" ? "async" : "local");
|
|
65
69
|
let queue = null;
|
|
66
70
|
function getQueue() {
|
|
@@ -89,15 +93,24 @@ function createEventBus(opts) {
|
|
|
89
93
|
for (const [pattern, handlers] of listeners) {
|
|
90
94
|
if (!matchEventPattern(event, pattern)) continue;
|
|
91
95
|
if (!handlers || handlers.size === 0) continue;
|
|
92
|
-
for (const
|
|
93
|
-
if (skipPersistent &&
|
|
96
|
+
for (const subscriber of handlers) {
|
|
97
|
+
if (skipPersistent && persistentSubscribers.has(subscriber)) continue;
|
|
94
98
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const moduleId = subscriber.moduleId ?? inferModuleIdFromResourceId(subscriber.id);
|
|
100
|
+
await withModuleResourceUsage(
|
|
101
|
+
{
|
|
102
|
+
moduleId,
|
|
103
|
+
surface: "subscriber",
|
|
104
|
+
operation: `${event} -> ${subscriber.id ?? subscriber.event}`,
|
|
105
|
+
resourceId: subscriber.id ?? subscriber.event
|
|
106
|
+
},
|
|
107
|
+
() => subscriber.handler(payload, {
|
|
108
|
+
resolve: opts.resolve,
|
|
109
|
+
eventName: event,
|
|
110
|
+
tenantId: options?.tenantId ?? null,
|
|
111
|
+
organizationId: options?.organizationId ?? null
|
|
112
|
+
})
|
|
113
|
+
);
|
|
101
114
|
} catch (error) {
|
|
102
115
|
console.error(`[events] Handler error for "${event}" (pattern: "${pattern}"):`, error);
|
|
103
116
|
}
|
|
@@ -108,14 +121,21 @@ function createEventBus(opts) {
|
|
|
108
121
|
if (!listeners.has(event)) {
|
|
109
122
|
listeners.set(event, /* @__PURE__ */ new Set());
|
|
110
123
|
}
|
|
111
|
-
|
|
124
|
+
const subscriber = {
|
|
125
|
+
id: options?.id,
|
|
126
|
+
moduleId: options?.moduleId,
|
|
127
|
+
event,
|
|
128
|
+
handler,
|
|
129
|
+
persistent: options?.persistent
|
|
130
|
+
};
|
|
131
|
+
listeners.get(event).add(subscriber);
|
|
112
132
|
if (options?.persistent) {
|
|
113
|
-
|
|
133
|
+
persistentSubscribers.add(subscriber);
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
function registerModuleSubscribers(subs) {
|
|
117
137
|
for (const sub of subs) {
|
|
118
|
-
on(sub.event, sub.handler, { persistent: sub.persistent });
|
|
138
|
+
on(sub.event, sub.handler, { persistent: sub.persistent, moduleId: sub.moduleId, id: sub.id });
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
141
|
async function emit(event, payload, options) {
|
package/dist/bus.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/bus.ts"],
|
|
4
|
-
"sourcesContent": ["import { createQueue } from '@open-mercato/queue'\nimport type { Queue } from '@open-mercato/queue'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { isSingleDeliveryRequested } from './single-delivery'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { isBroadcastEvent } from '@open-mercato/shared/modules/events'\nexport { registerCrossProcessEventListener } from './bridge'\nimport { publishCrossProcessEvent } from './bridge'\nimport type {\n EventBus,\n CreateBusOptions,\n SubscriberHandler,\n SubscriberDescriptor,\n EventPayload,\n EmitOptions,\n} from './types'\n\n/** Queue name for persistent events */\nconst EVENTS_QUEUE_NAME = 'events'\n\n/**\n * When enabled, a persistent emit delivers each subscriber on exactly one path:\n * persistent-marked subscribers are skipped inline (the events worker dispatches\n * them via pattern match, so wildcard persistent subscribers are reached), while\n * ephemeral subscribers keep running inline. Defaults ON; the server bootstrap\n * reconciles the env against worker availability (and may rewrite it to `false`\n * for a worker-less process). Both the bus and the events worker read the same\n * (possibly reconciled) env var, so they always agree within a process.\n */\nfunction isSingleDeliveryEnabled(): boolean {\n return isSingleDeliveryRequested()\n}\n\ntype GlobalEventTap = (event: string, payload: EventPayload, options?: EmitOptions) => void | Promise<void>\nconst GLOBAL_EVENT_TAPS_KEY = '__openMercatoEventBusGlobalTaps__'\n\nfunction hasTenantScope(payload: EventPayload): boolean {\n return typeof (payload as Record<string, unknown>)?.tenantId === 'string'\n && String((payload as Record<string, unknown>).tenantId).trim().length > 0\n}\n\nfunction getGlobalEventTaps(): Set<GlobalEventTap> {\n const existing = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_TAPS_KEY]\n if (existing instanceof Set) {\n return existing as Set<GlobalEventTap>\n }\n const created = new Set<GlobalEventTap>()\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_TAPS_KEY] = created\n return created\n}\n\nexport function registerGlobalEventTap(handler: GlobalEventTap): () => void {\n const taps = getGlobalEventTaps()\n taps.add(handler)\n return () => {\n taps.delete(handler)\n }\n}\n\n/** Job data structure for queued events */\ntype EventJobData = {\n event: string\n payload: EventPayload\n options?: EmitOptions\n}\n\n// Process-wide cache of the async (BullMQ) persistent-events producer queue.\n// Each authenticated request builds a fresh DI container and event bus, so a\n// per-bus producer queue opened a new ioredis connection per write request that\n// was never closed \u2014 leaking one Redis connection per request until maxclients\n// exhaustion. Memoizing the producer on `globalThis` (keyed by Redis URL, so a\n// reconfigured URL still gets its own queue) keeps it at one connection per\n// process, mirroring the `GLOBAL_EVENT_TAPS_KEY` and `getCachedRateLimiterService`\n// patterns. The local (file-based) strategy holds no pooled connection and its\n// base dir is cwd-relative, so it stays per-bus.\nconst EVENTS_PRODUCER_QUEUE_KEY = '__openMercatoEventsProducerQueues__'\nconst EVENTS_PRODUCER_SHUTDOWN_KEY = '__openMercatoEventsProducerShutdown__'\n\nfunction isSharedProducerEnabled(): boolean {\n return parseBooleanWithDefault(process.env.OM_EVENTS_SHARED_PRODUCER, true)\n}\n\nfunction getProducerQueueRegistry(): Map<string, Queue<EventJobData>> {\n const existing = (globalThis as Record<string, unknown>)[EVENTS_PRODUCER_QUEUE_KEY]\n if (existing instanceof Map) {\n return existing as Map<string, Queue<EventJobData>>\n }\n const created = new Map<string, Queue<EventJobData>>()\n ;(globalThis as Record<string, unknown>)[EVENTS_PRODUCER_QUEUE_KEY] = created\n return created\n}\n\nfunction registerProducerShutdownHook(): void {\n if ((globalThis as Record<string, unknown>)[EVENTS_PRODUCER_SHUTDOWN_KEY]) return\n const shutdown = () => {\n const registry = getProducerQueueRegistry()\n for (const sharedQueue of registry.values()) {\n Promise.resolve(sharedQueue.close()).catch(() => {})\n }\n registry.clear()\n }\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n ;(globalThis as Record<string, unknown>)[EVENTS_PRODUCER_SHUTDOWN_KEY] = true\n}\n\n/**\n * Creates an event bus instance.\n *\n * The event bus provides:\n * - In-memory event delivery to registered handlers\n * - Optional persistence via the queue package when `persistent: true`\n *\n * @param opts - Configuration options\n * @returns An EventBus instance\n *\n * @example\n * ```typescript\n * const bus = createEventBus({\n * resolve: container.resolve.bind(container),\n * queueStrategy: 'local', // or 'async' for BullMQ\n * })\n *\n * // Register a handler\n * bus.on('user.created', async (payload, ctx) => {\n * const userService = ctx.resolve('userService')\n * await userService.sendWelcomeEmail(payload.userId)\n * })\n *\n * // Emit an event (immediate delivery)\n * await bus.emit('user.created', { userId: '123' })\n *\n * // Emit with persistence (for async worker processing)\n * await bus.emit('order.placed', { orderId: '456' }, { persistent: true })\n * ```\n */\nexport function createEventBus(opts: CreateBusOptions): EventBus {\n // In-memory listeners for immediate event delivery\n const listeners = new Map<string, Set<SubscriberHandler>>()\n // Handlers registered as persistent (worker-dispatched). Used by the\n // single-delivery path to skip them inline on a persistent emit.\n const persistentHandlers = new Set<SubscriberHandler>()\n\n // Determine queue strategy from options or environment\n const queueStrategy = opts.queueStrategy ??\n (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n // Lazy-initialized queue for persistent events\n let queue: Queue<EventJobData> | null = null\n\n /**\n * Gets or creates the queue instance for persistent events.\n *\n * The async (BullMQ) producer is memoized process-wide so the per-request\n * event bus reuses one Redis connection instead of leaking one per write\n * request. The local strategy stays per-bus (no pooled connection, cwd-relative\n * base dir). Set `OM_EVENTS_SHARED_PRODUCER=0` to fall back to per-bus producers.\n */\n function getQueue(): Queue<EventJobData> {\n if (queueStrategy !== 'async' || !isSharedProducerEnabled()) {\n if (!queue) {\n queue = queueStrategy === 'async'\n ? createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'async', {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n })\n : createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'local')\n }\n return queue\n }\n\n const redisUrl = getRedisUrlOrThrow('QUEUE')\n const registry = getProducerQueueRegistry()\n const cacheKey = `async:${redisUrl}`\n let shared = registry.get(cacheKey)\n if (!shared) {\n shared = createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'async', {\n connection: { url: redisUrl },\n })\n registry.set(cacheKey, shared)\n registerProducerShutdownHook()\n }\n return shared\n }\n\n /**\n * Delivers an event to all registered in-memory handlers.\n * Supports wildcard pattern matching for event patterns.\n */\n async function deliver(\n event: string,\n payload: EventPayload,\n options?: EmitOptions,\n skipPersistent = false,\n ): Promise<void> {\n // Check all registered patterns (including wildcards)\n for (const [pattern, handlers] of listeners) {\n if (!matchEventPattern(event, pattern)) continue\n if (!handlers || handlers.size === 0) continue\n\n for (const handler of handlers) {\n // Single-delivery: persistent subscribers are dispatched by the worker,\n // so skip them inline to avoid double execution.\n if (skipPersistent && persistentHandlers.has(handler)) continue\n try {\n // Pass eventName in context for wildcard handlers\n await Promise.resolve(handler(payload, {\n resolve: opts.resolve,\n eventName: event,\n tenantId: options?.tenantId ?? null,\n organizationId: options?.organizationId ?? null,\n }))\n } catch (error) {\n console.error(`[events] Handler error for \"${event}\" (pattern: \"${pattern}\"):`, error)\n }\n }\n }\n }\n\n /**\n * Registers a handler for an event.\n */\n function on(event: string, handler: SubscriberHandler, options?: { persistent?: boolean }): void {\n if (!listeners.has(event)) {\n listeners.set(event, new Set())\n }\n listeners.get(event)!.add(handler)\n if (options?.persistent) {\n persistentHandlers.add(handler)\n }\n }\n\n /**\n * Registers multiple module subscribers at once.\n */\n function registerModuleSubscribers(subs: SubscriberDescriptor[]): void {\n for (const sub of subs) {\n on(sub.event, sub.handler, { persistent: sub.persistent })\n }\n }\n\n /**\n * Emits an event to all registered handlers.\n *\n * If `persistent: true`, also enqueues the event for async processing.\n */\n async function emit(\n event: string,\n payload: EventPayload,\n options?: EmitOptions\n ): Promise<void> {\n const taps = getGlobalEventTaps()\n for (const tap of taps) {\n try {\n await Promise.resolve(tap(event, payload, options))\n } catch (error) {\n console.error(`[events] Global tap error for \"${event}\":`, error)\n }\n }\n\n // Deliver to in-memory handlers first. Under single-delivery, persistent\n // subscribers are skipped inline on a persistent emit because the events\n // worker will dispatch them from the queue.\n //\n // `deliverInline: false` on a persistent emit is enqueue-only: skip inline\n // delivery entirely so a heavy persistent job runs solely in the events\n // worker, off the caller's request path (the queued enqueue below still\n // happens). Without this, a persistent emit dual-dispatches by default and\n // runs the subscriber inline in the caller's request too.\n const enqueueOnly = Boolean(options?.persistent) && options?.deliverInline === false\n if (!enqueueOnly) {\n const skipPersistentInline = Boolean(options?.persistent) && isSingleDeliveryEnabled()\n await deliver(event, payload, options, skipPersistentInline)\n }\n\n if (isBroadcastEvent(event) && hasTenantScope(payload)) {\n try {\n await publishCrossProcessEvent(event, payload, options)\n } catch (error) {\n console.error(`[events] Cross-process publish error for \"${event}\":`, error)\n }\n }\n\n // If persistent, also enqueue for async processing\n if (options?.persistent) {\n const q = getQueue()\n await q.enqueue({ event, payload, options })\n }\n }\n\n /**\n * Clears all events from the persistent queue.\n */\n async function clearQueue(): Promise<{ removed: number }> {\n const q = getQueue()\n return q.clear()\n }\n\n // Backward compatibility alias\n const emitEvent = emit\n\n return {\n emit,\n emitEvent, // Alias for backward compatibility\n on,\n registerModuleSubscribers,\n clearQueue,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,mBAAmB;AAE5B,SAAS,+BAA+B;AACxC,SAAS,iCAAiC;AAC1C,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,yCAAyC;AAClD,SAAS,gCAAgC;AAWzC,MAAM,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { createQueue } from '@open-mercato/queue'\nimport type { Queue } from '@open-mercato/queue'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { isSingleDeliveryRequested } from './single-delivery'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { isBroadcastEvent } from '@open-mercato/shared/modules/events'\nimport {\n inferModuleIdFromResourceId,\n withModuleResourceUsage,\n} from '@open-mercato/shared/lib/modules/resource-usage'\nexport { registerCrossProcessEventListener } from './bridge'\nimport { publishCrossProcessEvent } from './bridge'\nimport type {\n EventBus,\n CreateBusOptions,\n SubscriberHandler,\n SubscriberDescriptor,\n EventPayload,\n EmitOptions,\n} from './types'\n\n/** Queue name for persistent events */\nconst EVENTS_QUEUE_NAME = 'events'\n\ntype RegisteredSubscriber = {\n id?: string\n moduleId?: string\n event: string\n handler: SubscriberHandler\n persistent?: boolean\n}\n\n/**\n * When enabled, a persistent emit delivers each subscriber on exactly one path:\n * persistent-marked subscribers are skipped inline (the events worker dispatches\n * them via pattern match, so wildcard persistent subscribers are reached), while\n * ephemeral subscribers keep running inline. Defaults ON; the server bootstrap\n * reconciles the env against worker availability (and may rewrite it to `false`\n * for a worker-less process). Both the bus and the events worker read the same\n * (possibly reconciled) env var, so they always agree within a process.\n */\nfunction isSingleDeliveryEnabled(): boolean {\n return isSingleDeliveryRequested()\n}\n\ntype GlobalEventTap = (event: string, payload: EventPayload, options?: EmitOptions) => void | Promise<void>\nconst GLOBAL_EVENT_TAPS_KEY = '__openMercatoEventBusGlobalTaps__'\n\nfunction hasTenantScope(payload: EventPayload): boolean {\n return typeof (payload as Record<string, unknown>)?.tenantId === 'string'\n && String((payload as Record<string, unknown>).tenantId).trim().length > 0\n}\n\nfunction getGlobalEventTaps(): Set<GlobalEventTap> {\n const existing = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_TAPS_KEY]\n if (existing instanceof Set) {\n return existing as Set<GlobalEventTap>\n }\n const created = new Set<GlobalEventTap>()\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_TAPS_KEY] = created\n return created\n}\n\nexport function registerGlobalEventTap(handler: GlobalEventTap): () => void {\n const taps = getGlobalEventTaps()\n taps.add(handler)\n return () => {\n taps.delete(handler)\n }\n}\n\n/** Job data structure for queued events */\ntype EventJobData = {\n event: string\n payload: EventPayload\n options?: EmitOptions\n}\n\n// Process-wide cache of the async (BullMQ) persistent-events producer queue.\n// Each authenticated request builds a fresh DI container and event bus, so a\n// per-bus producer queue opened a new ioredis connection per write request that\n// was never closed \u2014 leaking one Redis connection per request until maxclients\n// exhaustion. Memoizing the producer on `globalThis` (keyed by Redis URL, so a\n// reconfigured URL still gets its own queue) keeps it at one connection per\n// process, mirroring the `GLOBAL_EVENT_TAPS_KEY` and `getCachedRateLimiterService`\n// patterns. The local (file-based) strategy holds no pooled connection and its\n// base dir is cwd-relative, so it stays per-bus.\nconst EVENTS_PRODUCER_QUEUE_KEY = '__openMercatoEventsProducerQueues__'\nconst EVENTS_PRODUCER_SHUTDOWN_KEY = '__openMercatoEventsProducerShutdown__'\n\nfunction isSharedProducerEnabled(): boolean {\n return parseBooleanWithDefault(process.env.OM_EVENTS_SHARED_PRODUCER, true)\n}\n\nfunction getProducerQueueRegistry(): Map<string, Queue<EventJobData>> {\n const existing = (globalThis as Record<string, unknown>)[EVENTS_PRODUCER_QUEUE_KEY]\n if (existing instanceof Map) {\n return existing as Map<string, Queue<EventJobData>>\n }\n const created = new Map<string, Queue<EventJobData>>()\n ;(globalThis as Record<string, unknown>)[EVENTS_PRODUCER_QUEUE_KEY] = created\n return created\n}\n\nfunction registerProducerShutdownHook(): void {\n if ((globalThis as Record<string, unknown>)[EVENTS_PRODUCER_SHUTDOWN_KEY]) return\n const shutdown = () => {\n const registry = getProducerQueueRegistry()\n for (const sharedQueue of registry.values()) {\n Promise.resolve(sharedQueue.close()).catch(() => {})\n }\n registry.clear()\n }\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n ;(globalThis as Record<string, unknown>)[EVENTS_PRODUCER_SHUTDOWN_KEY] = true\n}\n\n/**\n * Creates an event bus instance.\n *\n * The event bus provides:\n * - In-memory event delivery to registered handlers\n * - Optional persistence via the queue package when `persistent: true`\n *\n * @param opts - Configuration options\n * @returns An EventBus instance\n *\n * @example\n * ```typescript\n * const bus = createEventBus({\n * resolve: container.resolve.bind(container),\n * queueStrategy: 'local', // or 'async' for BullMQ\n * })\n *\n * // Register a handler\n * bus.on('user.created', async (payload, ctx) => {\n * const userService = ctx.resolve('userService')\n * await userService.sendWelcomeEmail(payload.userId)\n * })\n *\n * // Emit an event (immediate delivery)\n * await bus.emit('user.created', { userId: '123' })\n *\n * // Emit with persistence (for async worker processing)\n * await bus.emit('order.placed', { orderId: '456' }, { persistent: true })\n * ```\n */\nexport function createEventBus(opts: CreateBusOptions): EventBus {\n // In-memory listeners for immediate event delivery\n const listeners = new Map<string, Set<RegisteredSubscriber>>()\n // Subscribers registered as persistent (worker-dispatched). Used by the\n // single-delivery path to skip them inline on a persistent emit.\n const persistentSubscribers = new Set<RegisteredSubscriber>()\n\n // Determine queue strategy from options or environment\n const queueStrategy = opts.queueStrategy ??\n (process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local')\n\n // Lazy-initialized queue for persistent events\n let queue: Queue<EventJobData> | null = null\n\n /**\n * Gets or creates the queue instance for persistent events.\n *\n * The async (BullMQ) producer is memoized process-wide so the per-request\n * event bus reuses one Redis connection instead of leaking one per write\n * request. The local strategy stays per-bus (no pooled connection, cwd-relative\n * base dir). Set `OM_EVENTS_SHARED_PRODUCER=0` to fall back to per-bus producers.\n */\n function getQueue(): Queue<EventJobData> {\n if (queueStrategy !== 'async' || !isSharedProducerEnabled()) {\n if (!queue) {\n queue = queueStrategy === 'async'\n ? createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'async', {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n })\n : createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'local')\n }\n return queue\n }\n\n const redisUrl = getRedisUrlOrThrow('QUEUE')\n const registry = getProducerQueueRegistry()\n const cacheKey = `async:${redisUrl}`\n let shared = registry.get(cacheKey)\n if (!shared) {\n shared = createQueue<EventJobData>(EVENTS_QUEUE_NAME, 'async', {\n connection: { url: redisUrl },\n })\n registry.set(cacheKey, shared)\n registerProducerShutdownHook()\n }\n return shared\n }\n\n /**\n * Delivers an event to all registered in-memory handlers.\n * Supports wildcard pattern matching for event patterns.\n */\n async function deliver(\n event: string,\n payload: EventPayload,\n options?: EmitOptions,\n skipPersistent = false,\n ): Promise<void> {\n // Check all registered patterns (including wildcards)\n for (const [pattern, handlers] of listeners) {\n if (!matchEventPattern(event, pattern)) continue\n if (!handlers || handlers.size === 0) continue\n\n for (const subscriber of handlers) {\n // Single-delivery: persistent subscribers are dispatched by the worker,\n // so skip them inline to avoid double execution.\n if (skipPersistent && persistentSubscribers.has(subscriber)) continue\n try {\n // Pass eventName in context for wildcard handlers\n const moduleId = subscriber.moduleId ?? inferModuleIdFromResourceId(subscriber.id)\n await withModuleResourceUsage(\n {\n moduleId,\n surface: 'subscriber',\n operation: `${event} -> ${subscriber.id ?? subscriber.event}`,\n resourceId: subscriber.id ?? subscriber.event,\n },\n () => subscriber.handler(payload, {\n resolve: opts.resolve,\n eventName: event,\n tenantId: options?.tenantId ?? null,\n organizationId: options?.organizationId ?? null,\n }),\n )\n } catch (error) {\n console.error(`[events] Handler error for \"${event}\" (pattern: \"${pattern}\"):`, error)\n }\n }\n }\n }\n\n /**\n * Registers a handler for an event. Pass `moduleId` whenever the subscribing\n * module is known (e.g. a module reacting to another module's event) so\n * resource-usage telemetry attributes the handler correctly instead of\n * guessing a module id from the event name.\n */\n function on(\n event: string,\n handler: SubscriberHandler,\n options?: { persistent?: boolean; moduleId?: string; id?: string },\n ): void {\n if (!listeners.has(event)) {\n listeners.set(event, new Set())\n }\n const subscriber: RegisteredSubscriber = {\n id: options?.id,\n moduleId: options?.moduleId,\n event,\n handler,\n persistent: options?.persistent,\n }\n listeners.get(event)!.add(subscriber)\n if (options?.persistent) {\n persistentSubscribers.add(subscriber)\n }\n }\n\n /**\n * Registers multiple module subscribers at once.\n */\n function registerModuleSubscribers(subs: SubscriberDescriptor[]): void {\n for (const sub of subs) {\n on(sub.event, sub.handler, { persistent: sub.persistent, moduleId: sub.moduleId, id: sub.id })\n }\n }\n\n /**\n * Emits an event to all registered handlers.\n *\n * If `persistent: true`, also enqueues the event for async processing.\n */\n async function emit(\n event: string,\n payload: EventPayload,\n options?: EmitOptions\n ): Promise<void> {\n const taps = getGlobalEventTaps()\n for (const tap of taps) {\n try {\n await Promise.resolve(tap(event, payload, options))\n } catch (error) {\n console.error(`[events] Global tap error for \"${event}\":`, error)\n }\n }\n\n // Deliver to in-memory handlers first. Under single-delivery, persistent\n // subscribers are skipped inline on a persistent emit because the events\n // worker will dispatch them from the queue.\n //\n // `deliverInline: false` on a persistent emit is enqueue-only: skip inline\n // delivery entirely so a heavy persistent job runs solely in the events\n // worker, off the caller's request path (the queued enqueue below still\n // happens). Without this, a persistent emit dual-dispatches by default and\n // runs the subscriber inline in the caller's request too.\n const enqueueOnly = Boolean(options?.persistent) && options?.deliverInline === false\n if (!enqueueOnly) {\n const skipPersistentInline = Boolean(options?.persistent) && isSingleDeliveryEnabled()\n await deliver(event, payload, options, skipPersistentInline)\n }\n\n if (isBroadcastEvent(event) && hasTenantScope(payload)) {\n try {\n await publishCrossProcessEvent(event, payload, options)\n } catch (error) {\n console.error(`[events] Cross-process publish error for \"${event}\":`, error)\n }\n }\n\n // If persistent, also enqueue for async processing\n if (options?.persistent) {\n const q = getQueue()\n await q.enqueue({ event, payload, options })\n }\n }\n\n /**\n * Clears all events from the persistent queue.\n */\n async function clearQueue(): Promise<{ removed: number }> {\n const q = getQueue()\n return q.clear()\n }\n\n // Backward compatibility alias\n const emitEvent = emit\n\n return {\n emit,\n emitEvent, // Alias for backward compatibility\n on,\n registerModuleSubscribers,\n clearQueue,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/events",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6385.1.9a81faa5f0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@open-mercato/queue": "0.6.6-develop.
|
|
37
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
36
|
+
"@open-mercato/queue": "0.6.6-develop.6385.1.9a81faa5f0",
|
|
37
|
+
"@open-mercato/shared": "0.6.6-develop.6385.1.9a81faa5f0",
|
|
38
38
|
"pg": "8.22.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
@@ -3,6 +3,10 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { createEventBus } from '@open-mercato/events/index'
|
|
6
|
+
import {
|
|
7
|
+
getModuleResourceUsageReport,
|
|
8
|
+
resetModuleResourceUsage,
|
|
9
|
+
} from '@open-mercato/shared/lib/modules/resource-usage'
|
|
6
10
|
|
|
7
11
|
function readJson(p: string) { return JSON.parse(fs.readFileSync(p, 'utf8')) }
|
|
8
12
|
|
|
@@ -14,8 +18,10 @@ describe('Event bus', () => {
|
|
|
14
18
|
process.chdir(tmp)
|
|
15
19
|
delete process.env.QUEUE_STRATEGY
|
|
16
20
|
delete process.env.EVENTS_STRATEGY
|
|
21
|
+
resetModuleResourceUsage()
|
|
17
22
|
})
|
|
18
23
|
afterEach(() => {
|
|
24
|
+
resetModuleResourceUsage()
|
|
19
25
|
process.chdir(origCwd)
|
|
20
26
|
try { fs.rmSync(tmp, { recursive: true, force: true }) } catch {}
|
|
21
27
|
})
|
|
@@ -118,6 +124,22 @@ describe('Event bus', () => {
|
|
|
118
124
|
expect(calls[1]).toEqual({ value: 2 })
|
|
119
125
|
})
|
|
120
126
|
|
|
127
|
+
test('module subscribers are attributed in resource usage telemetry', async () => {
|
|
128
|
+
const bus = createEventBus({ resolve: ((name: string) => name) as any })
|
|
129
|
+
|
|
130
|
+
bus.registerModuleSubscribers([
|
|
131
|
+
{ id: 'customers:subscribers:test', moduleId: 'customers', event: 'customers.person.created', handler: () => undefined },
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
await bus.emit('customers.person.created', { value: 1 })
|
|
135
|
+
|
|
136
|
+
const report = getModuleResourceUsageReport()
|
|
137
|
+
expect(report.modules).toHaveLength(1)
|
|
138
|
+
expect(report.modules[0].moduleId).toBe('customers')
|
|
139
|
+
expect(report.modules[0].surfaces[0].surface).toBe('subscriber')
|
|
140
|
+
expect(report.modules[0].topOperations[0].operation).toBe('customers.person.created -> customers:subscribers:test')
|
|
141
|
+
})
|
|
142
|
+
|
|
121
143
|
test('non-persistent events are not queued', async () => {
|
|
122
144
|
const queueDir = path.resolve('.mercato/queue', 'events')
|
|
123
145
|
const queuePath = path.join(queueDir, 'queue.json')
|
package/src/bus.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { isSingleDeliveryRequested } from './single-delivery'
|
|
|
5
5
|
import { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'
|
|
6
6
|
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
7
7
|
import { isBroadcastEvent } from '@open-mercato/shared/modules/events'
|
|
8
|
+
import {
|
|
9
|
+
inferModuleIdFromResourceId,
|
|
10
|
+
withModuleResourceUsage,
|
|
11
|
+
} from '@open-mercato/shared/lib/modules/resource-usage'
|
|
8
12
|
export { registerCrossProcessEventListener } from './bridge'
|
|
9
13
|
import { publishCrossProcessEvent } from './bridge'
|
|
10
14
|
import type {
|
|
@@ -19,6 +23,14 @@ import type {
|
|
|
19
23
|
/** Queue name for persistent events */
|
|
20
24
|
const EVENTS_QUEUE_NAME = 'events'
|
|
21
25
|
|
|
26
|
+
type RegisteredSubscriber = {
|
|
27
|
+
id?: string
|
|
28
|
+
moduleId?: string
|
|
29
|
+
event: string
|
|
30
|
+
handler: SubscriberHandler
|
|
31
|
+
persistent?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
/**
|
|
23
35
|
* When enabled, a persistent emit delivers each subscriber on exactly one path:
|
|
24
36
|
* persistent-marked subscribers are skipped inline (the events worker dispatches
|
|
@@ -137,10 +149,10 @@ function registerProducerShutdownHook(): void {
|
|
|
137
149
|
*/
|
|
138
150
|
export function createEventBus(opts: CreateBusOptions): EventBus {
|
|
139
151
|
// In-memory listeners for immediate event delivery
|
|
140
|
-
const listeners = new Map<string, Set<
|
|
141
|
-
//
|
|
152
|
+
const listeners = new Map<string, Set<RegisteredSubscriber>>()
|
|
153
|
+
// Subscribers registered as persistent (worker-dispatched). Used by the
|
|
142
154
|
// single-delivery path to skip them inline on a persistent emit.
|
|
143
|
-
const
|
|
155
|
+
const persistentSubscribers = new Set<RegisteredSubscriber>()
|
|
144
156
|
|
|
145
157
|
// Determine queue strategy from options or environment
|
|
146
158
|
const queueStrategy = opts.queueStrategy ??
|
|
@@ -198,18 +210,27 @@ export function createEventBus(opts: CreateBusOptions): EventBus {
|
|
|
198
210
|
if (!matchEventPattern(event, pattern)) continue
|
|
199
211
|
if (!handlers || handlers.size === 0) continue
|
|
200
212
|
|
|
201
|
-
for (const
|
|
213
|
+
for (const subscriber of handlers) {
|
|
202
214
|
// Single-delivery: persistent subscribers are dispatched by the worker,
|
|
203
215
|
// so skip them inline to avoid double execution.
|
|
204
|
-
if (skipPersistent &&
|
|
216
|
+
if (skipPersistent && persistentSubscribers.has(subscriber)) continue
|
|
205
217
|
try {
|
|
206
218
|
// Pass eventName in context for wildcard handlers
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
const moduleId = subscriber.moduleId ?? inferModuleIdFromResourceId(subscriber.id)
|
|
220
|
+
await withModuleResourceUsage(
|
|
221
|
+
{
|
|
222
|
+
moduleId,
|
|
223
|
+
surface: 'subscriber',
|
|
224
|
+
operation: `${event} -> ${subscriber.id ?? subscriber.event}`,
|
|
225
|
+
resourceId: subscriber.id ?? subscriber.event,
|
|
226
|
+
},
|
|
227
|
+
() => subscriber.handler(payload, {
|
|
228
|
+
resolve: opts.resolve,
|
|
229
|
+
eventName: event,
|
|
230
|
+
tenantId: options?.tenantId ?? null,
|
|
231
|
+
organizationId: options?.organizationId ?? null,
|
|
232
|
+
}),
|
|
233
|
+
)
|
|
213
234
|
} catch (error) {
|
|
214
235
|
console.error(`[events] Handler error for "${event}" (pattern: "${pattern}"):`, error)
|
|
215
236
|
}
|
|
@@ -218,15 +239,29 @@ export function createEventBus(opts: CreateBusOptions): EventBus {
|
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
/**
|
|
221
|
-
* Registers a handler for an event.
|
|
242
|
+
* Registers a handler for an event. Pass `moduleId` whenever the subscribing
|
|
243
|
+
* module is known (e.g. a module reacting to another module's event) so
|
|
244
|
+
* resource-usage telemetry attributes the handler correctly instead of
|
|
245
|
+
* guessing a module id from the event name.
|
|
222
246
|
*/
|
|
223
|
-
function on(
|
|
247
|
+
function on(
|
|
248
|
+
event: string,
|
|
249
|
+
handler: SubscriberHandler,
|
|
250
|
+
options?: { persistent?: boolean; moduleId?: string; id?: string },
|
|
251
|
+
): void {
|
|
224
252
|
if (!listeners.has(event)) {
|
|
225
253
|
listeners.set(event, new Set())
|
|
226
254
|
}
|
|
227
|
-
|
|
255
|
+
const subscriber: RegisteredSubscriber = {
|
|
256
|
+
id: options?.id,
|
|
257
|
+
moduleId: options?.moduleId,
|
|
258
|
+
event,
|
|
259
|
+
handler,
|
|
260
|
+
persistent: options?.persistent,
|
|
261
|
+
}
|
|
262
|
+
listeners.get(event)!.add(subscriber)
|
|
228
263
|
if (options?.persistent) {
|
|
229
|
-
|
|
264
|
+
persistentSubscribers.add(subscriber)
|
|
230
265
|
}
|
|
231
266
|
}
|
|
232
267
|
|
|
@@ -235,7 +270,7 @@ export function createEventBus(opts: CreateBusOptions): EventBus {
|
|
|
235
270
|
*/
|
|
236
271
|
function registerModuleSubscribers(subs: SubscriberDescriptor[]): void {
|
|
237
272
|
for (const sub of subs) {
|
|
238
|
-
on(sub.event, sub.handler, { persistent: sub.persistent })
|
|
273
|
+
on(sub.event, sub.handler, { persistent: sub.persistent, moduleId: sub.moduleId, id: sub.id })
|
|
239
274
|
}
|
|
240
275
|
}
|
|
241
276
|
|
package/src/types.ts
CHANGED
|
@@ -41,6 +41,8 @@ export type SubscriberHandler = (
|
|
|
41
41
|
export type SubscriberDescriptor = {
|
|
42
42
|
/** Unique identifier for this subscriber */
|
|
43
43
|
id: string
|
|
44
|
+
/** Owning module id, when the subscriber comes from module discovery */
|
|
45
|
+
moduleId?: string
|
|
44
46
|
/** Event name to subscribe to */
|
|
45
47
|
event: string
|
|
46
48
|
/**
|
|
@@ -121,8 +123,13 @@ export interface EventBus {
|
|
|
121
123
|
* @param handler - Handler function
|
|
122
124
|
* @param options - Optional registration options. `persistent: true` marks the
|
|
123
125
|
* handler as worker-dispatched so the single-delivery path skips it inline.
|
|
126
|
+
* `moduleId` attributes module-resource-usage telemetry for this handler to
|
|
127
|
+
* the given module; without it, telemetry falls back to guessing a module id
|
|
128
|
+
* from the event name, which is wrong for cross-module subscribers (e.g. a
|
|
129
|
+
* module reacting to another module's event). Pass it whenever the
|
|
130
|
+
* subscribing module is known.
|
|
124
131
|
*/
|
|
125
|
-
on(event: string, handler: SubscriberHandler, options?: { persistent?: boolean }): void
|
|
132
|
+
on(event: string, handler: SubscriberHandler, options?: { persistent?: boolean; moduleId?: string; id?: string }): void
|
|
126
133
|
|
|
127
134
|
/**
|
|
128
135
|
* Register multiple module subscribers at once.
|