@open-mercato/webhooks 0.6.4-develop.4210.1.d412061cfe → 0.6.4-develop.4236.1.9fa6806b34

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.
@@ -15,6 +15,14 @@ function shouldSkipOutboundDispatch(eventId) {
15
15
  const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId);
16
16
  return declaredEvent?.excludeFromTriggers === true;
17
17
  }
18
+ function forkOutboundEntityManager(em) {
19
+ const fork = em.fork;
20
+ if (typeof fork !== "function") return em;
21
+ return fork.call(em, { clear: true, useContext: false });
22
+ }
23
+ function integrationScopeKey(tenantId, organizationId) {
24
+ return `${tenantId}:${organizationId}`;
25
+ }
18
26
  async function handler(payload, ctx) {
19
27
  const eventId = ctx.eventId ?? ctx.eventName ?? payload.eventId ?? payload.type;
20
28
  if (!eventId) return;
@@ -44,45 +52,63 @@ async function handler(payload, ctx) {
44
52
  (webhook) => matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents)
45
53
  );
46
54
  if (!matchingWebhooks.length) return;
47
- for (const webhook of matchingWebhooks) {
48
- const integrationEnabled = await isWebhookIntegrationEnabled(em, {
49
- tenantId: webhook.tenantId,
50
- organizationId: webhook.organizationId
51
- });
52
- if (!integrationEnabled) continue;
53
- let createdDeliveryId = null;
54
- try {
55
- const delivery = await createWebhookDelivery({
56
- em,
57
- webhook,
58
- eventId,
59
- payload
55
+ const integrationEnabledByScope = /* @__PURE__ */ new Map();
56
+ const resolveIntegrationEnabled = (webhook) => {
57
+ const key = integrationScopeKey(webhook.tenantId, webhook.organizationId);
58
+ let pending = integrationEnabledByScope.get(key);
59
+ if (!pending) {
60
+ pending = isWebhookIntegrationEnabled(em, {
61
+ tenantId: webhook.tenantId,
62
+ organizationId: webhook.organizationId
60
63
  });
61
- createdDeliveryId = delivery.id;
62
- await enqueueWebhookDelivery({
63
- deliveryId: delivery.id,
64
- tenantId: delivery.tenantId,
65
- organizationId: delivery.organizationId
66
- });
67
- } catch (error) {
68
- if (createdDeliveryId) {
69
- const failedDelivery = await findOneWithDecryption(em, WebhookDeliveryEntity, { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId }, void 0, { tenantId: webhook.tenantId, organizationId: webhook.organizationId });
70
- if (failedDelivery) {
71
- failedDelivery.status = "failed";
72
- failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : "Queue enqueue failed";
73
- failedDelivery.nextRetryAt = null;
74
- await em.flush();
64
+ integrationEnabledByScope.set(key, pending);
65
+ }
66
+ return pending;
67
+ };
68
+ await Promise.all(
69
+ matchingWebhooks.map(async (webhook) => {
70
+ if (!await resolveIntegrationEnabled(webhook)) return;
71
+ const webhookEm = forkOutboundEntityManager(em);
72
+ let createdDeliveryId = null;
73
+ try {
74
+ const delivery = await createWebhookDelivery({
75
+ em: webhookEm,
76
+ webhook,
77
+ eventId,
78
+ payload
79
+ });
80
+ createdDeliveryId = delivery.id;
81
+ await enqueueWebhookDelivery({
82
+ deliveryId: delivery.id,
83
+ tenantId: delivery.tenantId,
84
+ organizationId: delivery.organizationId
85
+ });
86
+ } catch (error) {
87
+ if (createdDeliveryId) {
88
+ const failedDelivery = await findOneWithDecryption(
89
+ webhookEm,
90
+ WebhookDeliveryEntity,
91
+ { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId },
92
+ void 0,
93
+ { tenantId: webhook.tenantId, organizationId: webhook.organizationId }
94
+ );
95
+ if (failedDelivery) {
96
+ failedDelivery.status = "failed";
97
+ failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : "Queue enqueue failed";
98
+ failedDelivery.nextRetryAt = null;
99
+ await webhookEm.flush();
100
+ }
75
101
  }
102
+ console.error("[webhooks] Failed to enqueue outbound delivery", {
103
+ webhookId: webhook.id,
104
+ eventId,
105
+ tenantId,
106
+ organizationId: organizationId ?? webhook.organizationId,
107
+ error: error instanceof Error ? error.message : String(error)
108
+ });
76
109
  }
77
- console.error("[webhooks] Failed to enqueue outbound delivery", {
78
- webhookId: webhook.id,
79
- eventId,
80
- tenantId,
81
- organizationId: organizationId ?? webhook.organizationId,
82
- error: error instanceof Error ? error.message : String(error)
83
- });
84
- }
85
- }
110
+ })
111
+ );
86
112
  }
87
113
  export {
88
114
  handler as default,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/subscribers/outbound-dispatch.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nfunction shouldSkipOutboundDispatch(eventId: string): boolean {\n if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true\n\n const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)\n return declaredEvent?.excludeFromTriggers === true\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n if (shouldSkipOutboundDispatch(eventId)) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ?? '' },\n )\n\n if (!webhooks.length) return\n\n const matchingWebhooks = webhooks.filter((webhook) =>\n matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents),\n )\n\n if (!matchingWebhooks.length) return\n\n for (const webhook of matchingWebhooks) {\n const integrationEnabled = await isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n\n if (!integrationEnabled) continue\n\n let createdDeliveryId: string | null = null\n try {\n const delivery = await createWebhookDelivery({\n em,\n webhook,\n eventId,\n payload,\n })\n createdDeliveryId = delivery.id\n\n await enqueueWebhookDelivery({\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n })\n } catch (error) {\n if (createdDeliveryId) {\n const failedDelivery = await findOneWithDecryption(em, WebhookDeliveryEntity, { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId }, undefined, { tenantId: webhook.tenantId, organizationId: webhook.organizationId })\n if (failedDelivery) {\n failedDelivery.status = 'failed'\n failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'\n failedDelivery.nextRetryAt = null\n await em.flush()\n }\n }\n console.error('[webhooks] Failed to enqueue outbound delivery', {\n webhookId: webhook.id,\n eventId,\n tenantId,\n organizationId: organizationId ?? webhook.organizationId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SubscriberContext } from '@open-mercato/events/types'\nimport { WebhookDeliveryEntity, WebhookEntity } from '../data/entities'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { matchAnyWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { getDeclaredEvents } from '@open-mercato/shared/modules/events'\nimport { createWebhookDelivery } from '../lib/delivery'\nimport { enqueueWebhookDelivery } from '../lib/queue'\nimport { isWebhookIntegrationEnabled } from '../lib/integration-state'\n\nexport const metadata = {\n event: '*',\n persistent: true,\n id: 'webhooks:outbound-dispatch',\n}\n\nfunction shouldSkipOutboundDispatch(eventId: string): boolean {\n if (eventId.startsWith('webhooks.') || eventId.startsWith('application.')) return true\n\n const declaredEvent = getDeclaredEvents().find((event) => event.id === eventId)\n return declaredEvent?.excludeFromTriggers === true\n}\n\nfunction forkOutboundEntityManager(em: EntityManager): EntityManager {\n const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork\n if (typeof fork !== 'function') return em\n return fork.call(em, { clear: true, useContext: false })\n}\n\nfunction integrationScopeKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\nexport default async function handler(\n payload: Record<string, unknown>,\n ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },\n) {\n const eventId = ctx.eventId ?? ctx.eventName ?? (payload.eventId as string) ?? (payload.type as string)\n if (!eventId) return\n if (shouldSkipOutboundDispatch(eventId)) return\n\n const tenantId = payload.tenantId as string | undefined\n const organizationId = payload.organizationId as string | undefined\n if (!tenantId) return\n\n if (eventId.startsWith('webhooks.')) return\n if (eventId.startsWith('query_index.')) return\n\n\n const resolve = ('resolve' in ctx && typeof ctx.resolve === 'function')\n ? ctx.resolve\n : ('container' in ctx && ctx.container && typeof ctx.container.resolve === 'function')\n ? ctx.container.resolve.bind(ctx.container)\n : null\n\n if (!resolve) return\n\n const em = (resolve('em') as EntityManager).fork()\n\n const webhooks = await findWithDecryption(\n em,\n WebhookEntity,\n {\n isActive: true,\n deletedAt: null,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n {},\n { tenantId, organizationId: organizationId ?? '' },\n )\n\n if (!webhooks.length) return\n\n const matchingWebhooks = webhooks.filter((webhook) =>\n matchAnyWebhookEventPattern(eventId, webhook.subscribedEvents),\n )\n\n if (!matchingWebhooks.length) return\n\n const integrationEnabledByScope = new Map<string, Promise<boolean>>()\n\n const resolveIntegrationEnabled = (webhook: WebhookEntity): Promise<boolean> => {\n const key = integrationScopeKey(webhook.tenantId, webhook.organizationId)\n let pending = integrationEnabledByScope.get(key)\n if (!pending) {\n pending = isWebhookIntegrationEnabled(em, {\n tenantId: webhook.tenantId,\n organizationId: webhook.organizationId,\n })\n integrationEnabledByScope.set(key, pending)\n }\n return pending\n }\n\n await Promise.all(\n matchingWebhooks.map(async (webhook) => {\n if (!(await resolveIntegrationEnabled(webhook))) return\n\n const webhookEm = forkOutboundEntityManager(em)\n let createdDeliveryId: string | null = null\n try {\n const delivery = await createWebhookDelivery({\n em: webhookEm,\n webhook,\n eventId,\n payload,\n })\n createdDeliveryId = delivery.id\n\n await enqueueWebhookDelivery({\n deliveryId: delivery.id,\n tenantId: delivery.tenantId,\n organizationId: delivery.organizationId,\n })\n } catch (error) {\n if (createdDeliveryId) {\n const failedDelivery = await findOneWithDecryption(\n webhookEm,\n WebhookDeliveryEntity,\n { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId },\n undefined,\n { tenantId: webhook.tenantId, organizationId: webhook.organizationId },\n )\n if (failedDelivery) {\n failedDelivery.status = 'failed'\n failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'\n failedDelivery.nextRetryAt = null\n await webhookEm.flush()\n }\n }\n console.error('[webhooks] Failed to enqueue outbound delivery', {\n webhookId: webhook.id,\n eventId,\n tenantId,\n organizationId: organizationId ?? webhook.organizationId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }),\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/webhooks",
3
- "version": "0.6.4-develop.4210.1.d412061cfe",
3
+ "version": "0.6.4-develop.4236.1.9fa6806b34",
4
4
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -69,19 +69,19 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@open-mercato/core": "0.6.4-develop.4210.1.d412061cfe",
73
- "@open-mercato/queue": "0.6.4-develop.4210.1.d412061cfe",
74
- "@open-mercato/ui": "0.6.4-develop.4210.1.d412061cfe",
72
+ "@open-mercato/core": "0.6.4-develop.4236.1.9fa6806b34",
73
+ "@open-mercato/queue": "0.6.4-develop.4236.1.9fa6806b34",
74
+ "@open-mercato/ui": "0.6.4-develop.4236.1.9fa6806b34",
75
75
  "svix": "^1.92.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@mikro-orm/postgresql": "^7.0.14",
79
- "@open-mercato/shared": "0.6.4-develop.4210.1.d412061cfe",
79
+ "@open-mercato/shared": "0.6.4-develop.4236.1.9fa6806b34",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@open-mercato/shared": "0.6.4-develop.4210.1.d412061cfe",
84
+ "@open-mercato/shared": "0.6.4-develop.4236.1.9fa6806b34",
85
85
  "@types/jest": "^30.0.0",
86
86
  "@types/react": "^19.2.15",
87
87
  "@types/react-dom": "^19.2.3",
@@ -26,6 +26,22 @@ import { createWebhookDelivery } from '../../lib/delivery'
26
26
  import { enqueueWebhookDelivery } from '../../lib/queue'
27
27
  import { isWebhookIntegrationEnabled } from '../../lib/integration-state'
28
28
 
29
+ function createDispatchEntityManagers() {
30
+ const webhookEm = {
31
+ flush: jest.fn(async () => undefined),
32
+ } as unknown as EntityManager
33
+ const handlerEm = {
34
+ fork: jest.fn(() => webhookEm),
35
+ flush: jest.fn(async () => undefined),
36
+ } as unknown as EntityManager
37
+ const rootEm = {
38
+ fork: jest.fn(() => handlerEm),
39
+ flush: jest.fn(async () => undefined),
40
+ } as unknown as EntityManager
41
+
42
+ return { rootEm, handlerEm, webhookEm }
43
+ }
44
+
29
45
  describe('webhooks outbound dispatch subscriber', () => {
30
46
  afterEach(() => {
31
47
  jest.clearAllMocks()
@@ -36,13 +52,7 @@ describe('webhooks outbound dispatch subscriber', () => {
36
52
  })
37
53
 
38
54
  it('uses the event bus eventName for wildcard subscribers and schedules matching webhook deliveries', async () => {
39
- const em = {
40
- fork: jest.fn(function fork() {
41
- return em
42
- }),
43
- findOne: jest.fn(),
44
- flush: jest.fn(async () => undefined),
45
- } as unknown as EntityManager
55
+ const { rootEm, handlerEm, webhookEm } = createDispatchEntityManagers()
46
56
 
47
57
  ;(findWithDecryption as jest.Mock).mockResolvedValue([
48
58
  {
@@ -69,13 +79,15 @@ describe('webhooks outbound dispatch subscriber', () => {
69
79
  {
70
80
  eventName: 'catalog.product.deleted',
71
81
  resolve: <T,>(name: string): T => {
72
- if (name === 'em') return em as T
82
+ if (name === 'em') return rootEm as T
73
83
  throw new Error(`Unexpected dependency: ${name}`)
74
84
  },
75
85
  },
76
86
  )
77
87
 
88
+ expect(handlerEm.fork).toHaveBeenCalled()
78
89
  expect(createWebhookDelivery).toHaveBeenCalledWith(expect.objectContaining({
90
+ em: webhookEm,
79
91
  eventId: 'catalog.product.deleted',
80
92
  payload: expect.objectContaining({
81
93
  id: 'product-1',
@@ -90,53 +102,120 @@ describe('webhooks outbound dispatch subscriber', () => {
90
102
  })
91
103
  })
92
104
 
93
- it('scopes the failed-delivery lookup to webhook tenantId and organizationId when enqueue throws', async () => {
94
- const em = {
95
- fork: jest.fn(function fork() {
96
- return em
97
- }),
98
- flush: jest.fn(async () => undefined),
99
- } as unknown as EntityManager
105
+ it('checks integration state once per organization when multiple webhooks match', async () => {
106
+ const { rootEm, handlerEm } = createDispatchEntityManagers()
100
107
 
101
108
  ;(findWithDecryption as jest.Mock).mockResolvedValue([
102
109
  {
103
110
  id: 'webhook-1',
104
111
  organizationId: 'org-1',
105
112
  tenantId: 'tenant-1',
106
- subscribedEvents: ['catalog.product.created'],
113
+ subscribedEvents: ['catalog.product.deleted'],
114
+ },
115
+ {
116
+ id: 'webhook-2',
117
+ organizationId: 'org-1',
118
+ tenantId: 'tenant-1',
119
+ subscribedEvents: ['catalog.product.deleted'],
107
120
  },
108
121
  ])
109
122
  ;(isWebhookIntegrationEnabled as jest.Mock).mockResolvedValue(true)
110
- ;(createWebhookDelivery as jest.Mock).mockResolvedValue({
111
- id: 'delivery-1',
112
- tenantId: 'tenant-1',
113
- organizationId: 'org-1',
114
- })
115
- ;(enqueueWebhookDelivery as jest.Mock).mockRejectedValue(new Error('Queue unavailable'))
116
- ;(findOneWithDecryption as jest.Mock).mockResolvedValue({
117
- id: 'delivery-1',
118
- status: 'pending',
119
- nextRetryAt: null,
120
- })
123
+ ;(createWebhookDelivery as jest.Mock)
124
+ .mockResolvedValueOnce({
125
+ id: 'delivery-1',
126
+ tenantId: 'tenant-1',
127
+ organizationId: 'org-1',
128
+ })
129
+ .mockResolvedValueOnce({
130
+ id: 'delivery-2',
131
+ tenantId: 'tenant-1',
132
+ organizationId: 'org-1',
133
+ })
134
+ ;(enqueueWebhookDelivery as jest.Mock).mockResolvedValue('job-1')
121
135
 
122
136
  await handler(
123
- { id: 'product-2', tenantId: 'tenant-1', organizationId: 'org-1' },
124
137
  {
125
- eventName: 'catalog.product.created',
138
+ id: 'product-1',
139
+ tenantId: 'tenant-1',
140
+ organizationId: 'org-1',
141
+ },
142
+ {
143
+ eventName: 'catalog.product.deleted',
126
144
  resolve: <T,>(name: string): T => {
127
- if (name === 'em') return em as T
145
+ if (name === 'em') return rootEm as T
128
146
  throw new Error(`Unexpected dependency: ${name}`)
129
147
  },
130
148
  },
131
149
  )
132
150
 
133
- expect(findOneWithDecryption).toHaveBeenCalledWith(
134
- em,
135
- expect.anything(),
136
- expect.objectContaining({ id: 'delivery-1', tenantId: 'tenant-1', organizationId: 'org-1' }),
137
- undefined,
138
- expect.objectContaining({ tenantId: 'tenant-1', organizationId: 'org-1' }),
139
- )
151
+ expect(isWebhookIntegrationEnabled).toHaveBeenCalledTimes(1)
152
+ expect(isWebhookIntegrationEnabled).toHaveBeenCalledWith(handlerEm, {
153
+ tenantId: 'tenant-1',
154
+ organizationId: 'org-1',
155
+ })
156
+ expect(createWebhookDelivery).toHaveBeenCalledTimes(2)
157
+ expect(enqueueWebhookDelivery).toHaveBeenCalledTimes(2)
158
+ })
159
+
160
+ it('scopes the failed-delivery lookup to webhook tenantId and organizationId when enqueue throws', async () => {
161
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
162
+
163
+ try {
164
+ const { rootEm, webhookEm } = createDispatchEntityManagers()
165
+
166
+ ;(findWithDecryption as jest.Mock).mockResolvedValue([
167
+ {
168
+ id: 'webhook-1',
169
+ organizationId: 'org-1',
170
+ tenantId: 'tenant-1',
171
+ subscribedEvents: ['catalog.product.created'],
172
+ },
173
+ ])
174
+ ;(isWebhookIntegrationEnabled as jest.Mock).mockResolvedValue(true)
175
+ ;(createWebhookDelivery as jest.Mock).mockResolvedValue({
176
+ id: 'delivery-1',
177
+ tenantId: 'tenant-1',
178
+ organizationId: 'org-1',
179
+ })
180
+ ;(enqueueWebhookDelivery as jest.Mock).mockRejectedValue(new Error('Queue unavailable'))
181
+ ;(findOneWithDecryption as jest.Mock).mockResolvedValue({
182
+ id: 'delivery-1',
183
+ status: 'pending',
184
+ nextRetryAt: null,
185
+ })
186
+
187
+ await handler(
188
+ { id: 'product-2', tenantId: 'tenant-1', organizationId: 'org-1' },
189
+ {
190
+ eventName: 'catalog.product.created',
191
+ resolve: <T,>(name: string): T => {
192
+ if (name === 'em') return rootEm as T
193
+ throw new Error(`Unexpected dependency: ${name}`)
194
+ },
195
+ },
196
+ )
197
+
198
+ expect(findOneWithDecryption).toHaveBeenCalledWith(
199
+ webhookEm,
200
+ expect.anything(),
201
+ expect.objectContaining({ id: 'delivery-1', tenantId: 'tenant-1', organizationId: 'org-1' }),
202
+ undefined,
203
+ expect.objectContaining({ tenantId: 'tenant-1', organizationId: 'org-1' }),
204
+ )
205
+ expect(webhookEm.flush).toHaveBeenCalled()
206
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
207
+ '[webhooks] Failed to enqueue outbound delivery',
208
+ expect.objectContaining({
209
+ webhookId: 'webhook-1',
210
+ eventId: 'catalog.product.created',
211
+ tenantId: 'tenant-1',
212
+ organizationId: 'org-1',
213
+ error: 'Queue unavailable',
214
+ }),
215
+ )
216
+ } finally {
217
+ consoleErrorSpy.mockRestore()
218
+ }
140
219
  })
141
220
 
142
221
  it('does not crash when the event bus provides only ctx.resolve', async () => {
@@ -21,6 +21,16 @@ function shouldSkipOutboundDispatch(eventId: string): boolean {
21
21
  return declaredEvent?.excludeFromTriggers === true
22
22
  }
23
23
 
24
+ function forkOutboundEntityManager(em: EntityManager): EntityManager {
25
+ const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork
26
+ if (typeof fork !== 'function') return em
27
+ return fork.call(em, { clear: true, useContext: false })
28
+ }
29
+
30
+ function integrationScopeKey(tenantId: string, organizationId: string): string {
31
+ return `${tenantId}:${organizationId}`
32
+ }
33
+
24
34
  export default async function handler(
25
35
  payload: Record<string, unknown>,
26
36
  ctx: (SubscriberContext & { eventId?: string }) | { container?: { resolve: <T = unknown>(name: string) => T }; eventId?: string; eventName?: string; resolve?: <T = unknown>(name: string) => T },
@@ -68,46 +78,65 @@ export default async function handler(
68
78
 
69
79
  if (!matchingWebhooks.length) return
70
80
 
71
- for (const webhook of matchingWebhooks) {
72
- const integrationEnabled = await isWebhookIntegrationEnabled(em, {
73
- tenantId: webhook.tenantId,
74
- organizationId: webhook.organizationId,
75
- })
76
-
77
- if (!integrationEnabled) continue
78
-
79
- let createdDeliveryId: string | null = null
80
- try {
81
- const delivery = await createWebhookDelivery({
82
- em,
83
- webhook,
84
- eventId,
85
- payload,
86
- })
87
- createdDeliveryId = delivery.id
81
+ const integrationEnabledByScope = new Map<string, Promise<boolean>>()
88
82
 
89
- await enqueueWebhookDelivery({
90
- deliveryId: delivery.id,
91
- tenantId: delivery.tenantId,
92
- organizationId: delivery.organizationId,
93
- })
94
- } catch (error) {
95
- if (createdDeliveryId) {
96
- const failedDelivery = await findOneWithDecryption(em, WebhookDeliveryEntity, { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId }, undefined, { tenantId: webhook.tenantId, organizationId: webhook.organizationId })
97
- if (failedDelivery) {
98
- failedDelivery.status = 'failed'
99
- failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'
100
- failedDelivery.nextRetryAt = null
101
- await em.flush()
102
- }
103
- }
104
- console.error('[webhooks] Failed to enqueue outbound delivery', {
105
- webhookId: webhook.id,
106
- eventId,
107
- tenantId,
108
- organizationId: organizationId ?? webhook.organizationId,
109
- error: error instanceof Error ? error.message : String(error),
83
+ const resolveIntegrationEnabled = (webhook: WebhookEntity): Promise<boolean> => {
84
+ const key = integrationScopeKey(webhook.tenantId, webhook.organizationId)
85
+ let pending = integrationEnabledByScope.get(key)
86
+ if (!pending) {
87
+ pending = isWebhookIntegrationEnabled(em, {
88
+ tenantId: webhook.tenantId,
89
+ organizationId: webhook.organizationId,
110
90
  })
91
+ integrationEnabledByScope.set(key, pending)
111
92
  }
93
+ return pending
112
94
  }
95
+
96
+ await Promise.all(
97
+ matchingWebhooks.map(async (webhook) => {
98
+ if (!(await resolveIntegrationEnabled(webhook))) return
99
+
100
+ const webhookEm = forkOutboundEntityManager(em)
101
+ let createdDeliveryId: string | null = null
102
+ try {
103
+ const delivery = await createWebhookDelivery({
104
+ em: webhookEm,
105
+ webhook,
106
+ eventId,
107
+ payload,
108
+ })
109
+ createdDeliveryId = delivery.id
110
+
111
+ await enqueueWebhookDelivery({
112
+ deliveryId: delivery.id,
113
+ tenantId: delivery.tenantId,
114
+ organizationId: delivery.organizationId,
115
+ })
116
+ } catch (error) {
117
+ if (createdDeliveryId) {
118
+ const failedDelivery = await findOneWithDecryption(
119
+ webhookEm,
120
+ WebhookDeliveryEntity,
121
+ { id: createdDeliveryId, tenantId: webhook.tenantId, organizationId: webhook.organizationId },
122
+ undefined,
123
+ { tenantId: webhook.tenantId, organizationId: webhook.organizationId },
124
+ )
125
+ if (failedDelivery) {
126
+ failedDelivery.status = 'failed'
127
+ failedDelivery.errorMessage = error instanceof Error ? `Queue enqueue failed: ${error.message}` : 'Queue enqueue failed'
128
+ failedDelivery.nextRetryAt = null
129
+ await webhookEm.flush()
130
+ }
131
+ }
132
+ console.error('[webhooks] Failed to enqueue outbound delivery', {
133
+ webhookId: webhook.id,
134
+ eventId,
135
+ tenantId,
136
+ organizationId: organizationId ?? webhook.organizationId,
137
+ error: error instanceof Error ? error.message : String(error),
138
+ })
139
+ }
140
+ }),
141
+ )
113
142
  }