@open-mercato/events 0.6.6-develop.6365.1.641d695f07 → 0.6.6-develop.6366.1.fba69a7362

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.
@@ -53,6 +53,7 @@ async function handle(job, ctx) {
53
53
  if (!subscribers || subscribers.length === 0) return;
54
54
  const handlerCtx = {
55
55
  resolve: ctx.resolve,
56
+ eventName: event,
56
57
  tenantId: options?.tenantId ?? null,
57
58
  organizationId: options?.organizationId ?? null
58
59
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/events/workers/events.worker.ts"],
4
- "sourcesContent": ["import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport { getCliModules } from '@open-mercato/shared/modules/registry'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { isSingleDeliveryRequested } from '../../../single-delivery'\n\nexport const EVENTS_QUEUE_NAME = 'events'\n\nconst DEFAULT_CONCURRENCY = 1\nconst envConcurrency = process.env.WORKERS_EVENTS_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: EVENTS_QUEUE_NAME,\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype EventJobPayload = {\n event: string\n payload: unknown\n options?: {\n tenantId?: string | null\n organizationId?: string | null\n }\n}\n\ntype HandlerContext = {\n resolve: <T = unknown>(name: string) => T\n tenantId?: string | null\n organizationId?: string | null\n}\n\ntype SubscriberEntry = {\n id: string\n event: string\n persistent?: boolean\n handler: (payload: unknown, ctx: unknown) => Promise<void> | void\n}\n\n/**\n * Mirror of the event bus single-delivery flag. When enabled, the worker owns\n * dispatch of every persistent subscriber and matches by pattern so wildcard\n * (`event: '*'`) persistent subscribers are finally reached. Defaults ON;\n * reconciled by the server bootstrap against worker availability and read from\n * the same env var as the bus so the two always agree within a process.\n */\nfunction isSingleDeliveryEnabled(): boolean {\n return isSingleDeliveryRequested()\n}\n\n/**\n * Resolves the subscribers to run for a queued event.\n * - Legacy (flag off): exact-match lookup of every subscriber for the event.\n * - Single-delivery (flag on): persistent subscribers whose pattern matches the\n * event, including wildcards, so they run exactly once here instead of inline.\n */\nfunction resolveSubscribers(\n listeners: Map<string, SubscriberEntry[]>,\n event: string,\n): SubscriberEntry[] {\n if (!isSingleDeliveryEnabled()) {\n return listeners.get(event) ?? []\n }\n const matched: SubscriberEntry[] = []\n for (const [pattern, subs] of listeners) {\n if (!matchEventPattern(event, pattern)) continue\n for (const sub of subs) {\n if (sub.persistent) matched.push(sub)\n }\n }\n return matched\n}\n\n// Cached listener map - built once on first use\nlet cachedListenerMap: Map<string, SubscriberEntry[]> | null = null\n\n/**\n * Clear the cached listener map (for testing purposes).\n */\nexport function clearListenerCache(): void {\n cachedListenerMap = null\n}\n\n// Build listener map from module subscribers\nfunction buildListenerMap(): Map<string, SubscriberEntry[]> {\n const listeners = new Map<string, SubscriberEntry[]>()\n for (const mod of getCliModules()) {\n const subs = (mod as { subscribers?: SubscriberEntry[] }).subscribers\n if (!subs) continue\n for (const sub of subs) {\n if (!listeners.has(sub.event)) listeners.set(sub.event, [])\n listeners.get(sub.event)!.push(sub)\n }\n }\n return listeners\n}\n\n// Get cached listener map, building on first access\nfunction getListenerMap(): Map<string, SubscriberEntry[]> {\n if (!cachedListenerMap) {\n cachedListenerMap = buildListenerMap()\n }\n return cachedListenerMap\n}\n\n/**\n * Events worker handler.\n * Dispatches queued events to registered module subscribers.\n * Each subscriber is isolated - failures in one don't affect others.\n */\nexport default async function handle(\n job: QueuedJob<EventJobPayload>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n const { event, payload, options } = job.payload\n const listeners = getListenerMap()\n const subscribers = resolveSubscribers(listeners, event)\n\n if (!subscribers || subscribers.length === 0) return\n\n const handlerCtx = {\n resolve: ctx.resolve,\n tenantId: options?.tenantId ?? null,\n organizationId: options?.organizationId ?? null,\n }\n\n const results = await Promise.allSettled(\n subscribers.map((sub) => Promise.resolve(sub.handler(payload, handlerCtx)))\n )\n\n const errors: Array<{ subscriberId: string; error: unknown }> = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const sub = subscribers[i]\n console.error(`[events] Subscriber \"${sub.id}\" failed for event \"${event}\":`, result.reason)\n errors.push({ subscriberId: sub.id, error: result.reason })\n }\n }\n\n if (errors.length > 0) {\n const failedIds = errors.map((e) => e.subscriberId).join(', ')\n throw new Error(\n `${errors.length}/${subscribers.length} subscriber(s) failed for event \"${event}\": ${failedIds}`\n )\n }\n}\n"],
5
- "mappings": "AACA,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,iCAAiC;AAEnC,MAAM,oBAAoB;AAEjC,MAAM,sBAAsB;AAC5B,MAAM,iBAAiB,QAAQ,IAAI;AAE5B,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,aAAa,iBAAiB,SAAS,gBAAgB,EAAE,IAAI;AAC/D;AA+BA,SAAS,0BAAmC;AAC1C,SAAO,0BAA0B;AACnC;AAQA,SAAS,mBACP,WACA,OACmB;AACnB,MAAI,CAAC,wBAAwB,GAAG;AAC9B,WAAO,UAAU,IAAI,KAAK,KAAK,CAAC;AAAA,EAClC;AACA,QAAM,UAA6B,CAAC;AACpC,aAAW,CAAC,SAAS,IAAI,KAAK,WAAW;AACvC,QAAI,CAAC,kBAAkB,OAAO,OAAO,EAAG;AACxC,eAAW,OAAO,MAAM;AACtB,UAAI,IAAI,WAAY,SAAQ,KAAK,GAAG;AAAA,IACtC;AAAA,EACF;AACA,SAAO;AACT;AAGA,IAAI,oBAA2D;AAKxD,SAAS,qBAA2B;AACzC,sBAAoB;AACtB;AAGA,SAAS,mBAAmD;AAC1D,QAAM,YAAY,oBAAI,IAA+B;AACrD,aAAW,OAAO,cAAc,GAAG;AACjC,UAAM,OAAQ,IAA4C;AAC1D,QAAI,CAAC,KAAM;AACX,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,UAAU,IAAI,IAAI,KAAK,EAAG,WAAU,IAAI,IAAI,OAAO,CAAC,CAAC;AAC1D,gBAAU,IAAI,IAAI,KAAK,EAAG,KAAK,GAAG;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,iBAAiD;AACxD,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,iBAAiB;AAAA,EACvC;AACA,SAAO;AACT;AAOA,eAAO,OACL,KACA,KACe;AACf,QAAM,EAAE,OAAO,SAAS,QAAQ,IAAI,IAAI;AACxC,QAAM,YAAY,eAAe;AACjC,QAAM,cAAc,mBAAmB,WAAW,KAAK;AAEvD,MAAI,CAAC,eAAe,YAAY,WAAW,EAAG;AAE9C,QAAM,aAAa;AAAA,IACjB,SAAS,IAAI;AAAA,IACb,UAAU,SAAS,YAAY;AAAA,IAC/B,gBAAgB,SAAS,kBAAkB;AAAA,EAC7C;AAEA,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,YAAY,IAAI,CAAC,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,SAAS,UAAU,CAAC,CAAC;AAAA,EAC5E;AAEA,QAAM,SAA0D,CAAC;AACjE,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,SAAS,QAAQ,CAAC;AACxB,QAAI,OAAO,WAAW,YAAY;AAChC,YAAM,MAAM,YAAY,CAAC;AACzB,cAAQ,MAAM,wBAAwB,IAAI,EAAE,uBAAuB,KAAK,MAAM,OAAO,MAAM;AAC3F,aAAO,KAAK,EAAE,cAAc,IAAI,IAAI,OAAO,OAAO,OAAO,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,YAAY,OAAO,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,IAAI;AAC7D,UAAM,IAAI;AAAA,MACR,GAAG,OAAO,MAAM,IAAI,YAAY,MAAM,oCAAoC,KAAK,MAAM,SAAS;AAAA,IAChG;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport { getCliModules } from '@open-mercato/shared/modules/registry'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { isSingleDeliveryRequested } from '../../../single-delivery'\n\nexport const EVENTS_QUEUE_NAME = 'events'\n\nconst DEFAULT_CONCURRENCY = 1\nconst envConcurrency = process.env.WORKERS_EVENTS_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: EVENTS_QUEUE_NAME,\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype EventJobPayload = {\n event: string\n payload: unknown\n options?: {\n tenantId?: string | null\n organizationId?: string | null\n }\n}\n\ntype HandlerContext = {\n resolve: <T = unknown>(name: string) => T\n eventName?: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\ntype SubscriberEntry = {\n id: string\n event: string\n persistent?: boolean\n handler: (payload: unknown, ctx: unknown) => Promise<void> | void\n}\n\n/**\n * Mirror of the event bus single-delivery flag. When enabled, the worker owns\n * dispatch of every persistent subscriber and matches by pattern so wildcard\n * (`event: '*'`) persistent subscribers are finally reached. Defaults ON;\n * reconciled by the server bootstrap against worker availability and read from\n * the same env var as the bus so the two always agree within a process.\n */\nfunction isSingleDeliveryEnabled(): boolean {\n return isSingleDeliveryRequested()\n}\n\n/**\n * Resolves the subscribers to run for a queued event.\n * - Legacy (flag off): exact-match lookup of every subscriber for the event.\n * - Single-delivery (flag on): persistent subscribers whose pattern matches the\n * event, including wildcards, so they run exactly once here instead of inline.\n */\nfunction resolveSubscribers(\n listeners: Map<string, SubscriberEntry[]>,\n event: string,\n): SubscriberEntry[] {\n if (!isSingleDeliveryEnabled()) {\n return listeners.get(event) ?? []\n }\n const matched: SubscriberEntry[] = []\n for (const [pattern, subs] of listeners) {\n if (!matchEventPattern(event, pattern)) continue\n for (const sub of subs) {\n if (sub.persistent) matched.push(sub)\n }\n }\n return matched\n}\n\n// Cached listener map - built once on first use\nlet cachedListenerMap: Map<string, SubscriberEntry[]> | null = null\n\n/**\n * Clear the cached listener map (for testing purposes).\n */\nexport function clearListenerCache(): void {\n cachedListenerMap = null\n}\n\n// Build listener map from module subscribers\nfunction buildListenerMap(): Map<string, SubscriberEntry[]> {\n const listeners = new Map<string, SubscriberEntry[]>()\n for (const mod of getCliModules()) {\n const subs = (mod as { subscribers?: SubscriberEntry[] }).subscribers\n if (!subs) continue\n for (const sub of subs) {\n if (!listeners.has(sub.event)) listeners.set(sub.event, [])\n listeners.get(sub.event)!.push(sub)\n }\n }\n return listeners\n}\n\n// Get cached listener map, building on first access\nfunction getListenerMap(): Map<string, SubscriberEntry[]> {\n if (!cachedListenerMap) {\n cachedListenerMap = buildListenerMap()\n }\n return cachedListenerMap\n}\n\n/**\n * Events worker handler.\n * Dispatches queued events to registered module subscribers.\n * Each subscriber is isolated - failures in one don't affect others.\n */\nexport default async function handle(\n job: QueuedJob<EventJobPayload>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n const { event, payload, options } = job.payload\n const listeners = getListenerMap()\n const subscribers = resolveSubscribers(listeners, event)\n\n if (!subscribers || subscribers.length === 0) return\n\n const handlerCtx = {\n resolve: ctx.resolve,\n eventName: event,\n tenantId: options?.tenantId ?? null,\n organizationId: options?.organizationId ?? null,\n }\n\n const results = await Promise.allSettled(\n subscribers.map((sub) => Promise.resolve(sub.handler(payload, handlerCtx)))\n )\n\n const errors: Array<{ subscriberId: string; error: unknown }> = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const sub = subscribers[i]\n console.error(`[events] Subscriber \"${sub.id}\" failed for event \"${event}\":`, result.reason)\n errors.push({ subscriberId: sub.id, error: result.reason })\n }\n }\n\n if (errors.length > 0) {\n const failedIds = errors.map((e) => e.subscriberId).join(', ')\n throw new Error(\n `${errors.length}/${subscribers.length} subscriber(s) failed for event \"${event}\": ${failedIds}`\n )\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,iCAAiC;AAEnC,MAAM,oBAAoB;AAEjC,MAAM,sBAAsB;AAC5B,MAAM,iBAAiB,QAAQ,IAAI;AAE5B,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,aAAa,iBAAiB,SAAS,gBAAgB,EAAE,IAAI;AAC/D;AAgCA,SAAS,0BAAmC;AAC1C,SAAO,0BAA0B;AACnC;AAQA,SAAS,mBACP,WACA,OACmB;AACnB,MAAI,CAAC,wBAAwB,GAAG;AAC9B,WAAO,UAAU,IAAI,KAAK,KAAK,CAAC;AAAA,EAClC;AACA,QAAM,UAA6B,CAAC;AACpC,aAAW,CAAC,SAAS,IAAI,KAAK,WAAW;AACvC,QAAI,CAAC,kBAAkB,OAAO,OAAO,EAAG;AACxC,eAAW,OAAO,MAAM;AACtB,UAAI,IAAI,WAAY,SAAQ,KAAK,GAAG;AAAA,IACtC;AAAA,EACF;AACA,SAAO;AACT;AAGA,IAAI,oBAA2D;AAKxD,SAAS,qBAA2B;AACzC,sBAAoB;AACtB;AAGA,SAAS,mBAAmD;AAC1D,QAAM,YAAY,oBAAI,IAA+B;AACrD,aAAW,OAAO,cAAc,GAAG;AACjC,UAAM,OAAQ,IAA4C;AAC1D,QAAI,CAAC,KAAM;AACX,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,UAAU,IAAI,IAAI,KAAK,EAAG,WAAU,IAAI,IAAI,OAAO,CAAC,CAAC;AAC1D,gBAAU,IAAI,IAAI,KAAK,EAAG,KAAK,GAAG;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,iBAAiD;AACxD,MAAI,CAAC,mBAAmB;AACtB,wBAAoB,iBAAiB;AAAA,EACvC;AACA,SAAO;AACT;AAOA,eAAO,OACL,KACA,KACe;AACf,QAAM,EAAE,OAAO,SAAS,QAAQ,IAAI,IAAI;AACxC,QAAM,YAAY,eAAe;AACjC,QAAM,cAAc,mBAAmB,WAAW,KAAK;AAEvD,MAAI,CAAC,eAAe,YAAY,WAAW,EAAG;AAE9C,QAAM,aAAa;AAAA,IACjB,SAAS,IAAI;AAAA,IACb,WAAW;AAAA,IACX,UAAU,SAAS,YAAY;AAAA,IAC/B,gBAAgB,SAAS,kBAAkB;AAAA,EAC7C;AAEA,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,YAAY,IAAI,CAAC,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,SAAS,UAAU,CAAC,CAAC;AAAA,EAC5E;AAEA,QAAM,SAA0D,CAAC;AACjE,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,SAAS,QAAQ,CAAC;AACxB,QAAI,OAAO,WAAW,YAAY;AAChC,YAAM,MAAM,YAAY,CAAC;AACzB,cAAQ,MAAM,wBAAwB,IAAI,EAAE,uBAAuB,KAAK,MAAM,OAAO,MAAM;AAC3F,aAAO,KAAK,EAAE,cAAc,IAAI,IAAI,OAAO,OAAO,OAAO,CAAC;AAAA,IAC5D;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,YAAY,OAAO,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,IAAI;AAC7D,UAAM,IAAI;AAAA,MACR,GAAG,OAAO,MAAM,IAAI,YAAY,MAAM,oCAAoC,KAAK,MAAM,SAAS;AAAA,IAChG;AAAA,EACF;AACF;",
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.6365.1.641d695f07",
3
+ "version": "0.6.6-develop.6366.1.fba69a7362",
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.6365.1.641d695f07",
37
- "@open-mercato/shared": "0.6.6-develop.6365.1.641d695f07",
36
+ "@open-mercato/queue": "0.6.6-develop.6366.1.fba69a7362",
37
+ "@open-mercato/shared": "0.6.6-develop.6366.1.fba69a7362",
38
38
  "pg": "8.22.0"
39
39
  },
40
40
  "devDependencies": {
@@ -453,9 +453,17 @@ describe('Events Worker', () => {
453
453
  else process.env.OM_EVENTS_SINGLE_DELIVERY = origFlag
454
454
  })
455
455
 
456
- const createMockJob = (event: string, payload: unknown): QueuedJob<{ event: string; payload: unknown }> => ({
456
+ const createMockJob = (
457
+ event: string,
458
+ payload: unknown,
459
+ options?: { tenantId?: string | null; organizationId?: string | null },
460
+ ): QueuedJob<{
461
+ event: string
462
+ payload: unknown
463
+ options?: { tenantId?: string | null; organizationId?: string | null }
464
+ }> => ({
457
465
  id: 'test-job-id',
458
- payload: { event, payload },
466
+ payload: { event, payload, options },
459
467
  createdAt: new Date().toISOString(),
460
468
  })
461
469
 
@@ -518,6 +526,48 @@ describe('Events Worker', () => {
518
526
  expect(calls.sort()).toEqual(['p', 'w'])
519
527
  })
520
528
 
529
+ it('default (unset): forwards eventName and trusted scope to persistent wildcard subscribers', async () => {
530
+ delete process.env.OM_EVENTS_SINGLE_DELIVERY
531
+ clearListenerCache()
532
+ const contexts: Array<{
533
+ eventName?: string
534
+ tenantId?: string | null
535
+ organizationId?: string | null
536
+ }> = []
537
+ registerCliModules([{
538
+ id: 'm',
539
+ subscribers: [
540
+ {
541
+ id: 'workflow:event-trigger',
542
+ event: '*',
543
+ persistent: true,
544
+ handler: (_payload, ctx) => {
545
+ contexts.push({
546
+ eventName: ctx.eventName,
547
+ tenantId: ctx.tenantId,
548
+ organizationId: ctx.organizationId,
549
+ })
550
+ },
551
+ },
552
+ ],
553
+ }])
554
+
555
+ await handle(
556
+ createMockJob(
557
+ 'customers.deal.created',
558
+ { id: 'deal-1', tenantId: 'payload-tenant', organizationId: 'payload-org' },
559
+ { tenantId: 'trusted-tenant', organizationId: 'trusted-org' },
560
+ ),
561
+ createMockContext(),
562
+ )
563
+
564
+ expect(contexts).toEqual([{
565
+ eventName: 'customers.deal.created',
566
+ tenantId: 'trusted-tenant',
567
+ organizationId: 'trusted-org',
568
+ }])
569
+ })
570
+
521
571
  it('flag explicitly OFF (legacy opt-out): preserves exact-match dispatch and never reaches wildcards', async () => {
522
572
  process.env.OM_EVENTS_SINGLE_DELIVERY = 'false'
523
573
  clearListenerCache()
@@ -24,6 +24,7 @@ type EventJobPayload = {
24
24
 
25
25
  type HandlerContext = {
26
26
  resolve: <T = unknown>(name: string) => T
27
+ eventName?: string
27
28
  tenantId?: string | null
28
29
  organizationId?: string | null
29
30
  }
@@ -118,6 +119,7 @@ export default async function handle(
118
119
 
119
120
  const handlerCtx = {
120
121
  resolve: ctx.resolve,
122
+ eventName: event,
121
123
  tenantId: options?.tenantId ?? null,
122
124
  organizationId: options?.organizationId ?? null,
123
125
  }