@open-mercato/webhooks 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js +113 -4
- package/dist/modules/webhooks/api/inbound/[endpointId]/route.js.map +3 -3
- package/dist/modules/webhooks/data/entities.js +101 -1
- package/dist/modules/webhooks/data/entities.js.map +2 -2
- package/dist/modules/webhooks/encryption.js +7 -0
- package/dist/modules/webhooks/encryption.js.map +2 -2
- package/dist/modules/webhooks/events.js +2 -0
- package/dist/modules/webhooks/events.js.map +2 -2
- package/dist/modules/webhooks/generators.js +57 -0
- package/dist/modules/webhooks/generators.js.map +7 -0
- package/dist/modules/webhooks/lib/inbound-dispatch.js +105 -0
- package/dist/modules/webhooks/lib/inbound-dispatch.js.map +7 -0
- package/dist/modules/webhooks/lib/inbound-registry.js +76 -0
- package/dist/modules/webhooks/lib/inbound-registry.js.map +7 -0
- package/dist/modules/webhooks/lib/module-webhook-registry.js +12 -0
- package/dist/modules/webhooks/lib/module-webhook-registry.js.map +7 -0
- package/dist/modules/webhooks/lib/queue.js +47 -0
- package/dist/modules/webhooks/lib/queue.js.map +2 -2
- package/dist/modules/webhooks/migrations/Migration20260617141327_webhooks.js +16 -0
- package/dist/modules/webhooks/migrations/Migration20260617141327_webhooks.js.map +7 -0
- package/dist/modules/webhooks/workers/inbound-dispatch.js +29 -0
- package/dist/modules/webhooks/workers/inbound-dispatch.js.map +7 -0
- package/generated/entities/inbound_endpoint_config_entity/index.ts +8 -0
- package/generated/entities/webhook_ingestion_entity/index.ts +16 -0
- package/generated/entities.ids.generated.ts +3 -1
- package/generated/entity-fields-registry.ts +28 -0
- package/package.json +6 -6
- package/src/modules/webhooks/api/inbound/[endpointId]/__tests__/route.unify.test.ts +108 -0
- package/src/modules/webhooks/api/inbound/[endpointId]/route.ts +136 -4
- package/src/modules/webhooks/data/entities.ts +84 -0
- package/src/modules/webhooks/encryption.ts +7 -0
- package/src/modules/webhooks/events.ts +2 -0
- package/src/modules/webhooks/generators.ts +57 -0
- package/src/modules/webhooks/lib/__tests__/inbound-dispatch.test.ts +186 -0
- package/src/modules/webhooks/lib/__tests__/inbound-registry.test.ts +102 -0
- package/src/modules/webhooks/lib/__tests__/module-webhook-registry.test.ts +53 -0
- package/src/modules/webhooks/lib/inbound-dispatch.ts +147 -0
- package/src/modules/webhooks/lib/inbound-registry.ts +92 -0
- package/src/modules/webhooks/lib/module-webhook-registry.ts +33 -0
- package/src/modules/webhooks/lib/queue.ts +60 -0
- package/src/modules/webhooks/migrations/.snapshot-open-mercato.json +907 -338
- package/src/modules/webhooks/migrations/Migration20260617141327_webhooks.ts +16 -0
- package/src/modules/webhooks/workers/inbound-dispatch.ts +31 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { WebhookHandler } from '@open-mercato/shared/lib/webhooks'
|
|
2
|
+
import {
|
|
3
|
+
registerWebhookHandlerEntries,
|
|
4
|
+
registerWebhookSourceEntries,
|
|
5
|
+
} from '../module-webhook-registry'
|
|
6
|
+
import {
|
|
7
|
+
clearWebhookHandlers,
|
|
8
|
+
clearWebhookSources,
|
|
9
|
+
listWebhookHandlers,
|
|
10
|
+
listWebhookSources,
|
|
11
|
+
} from '../inbound-registry'
|
|
12
|
+
|
|
13
|
+
const noopHandler: WebhookHandler = async () => undefined
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clearWebhookSources()
|
|
17
|
+
clearWebhookHandlers()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('flattens module source entries into the source registry', () => {
|
|
21
|
+
registerWebhookSourceEntries([
|
|
22
|
+
{
|
|
23
|
+
moduleId: 'gateway_stripe',
|
|
24
|
+
sources: [
|
|
25
|
+
{ key: 'stripe', label: 'Stripe', verifier: async () => true, eventTypeExtractor: () => '' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ moduleId: 'inbox_ops', sources: [
|
|
29
|
+
{ key: 'resend', label: 'Resend', verifier: async () => true, eventTypeExtractor: () => '' },
|
|
30
|
+
] },
|
|
31
|
+
])
|
|
32
|
+
expect(listWebhookSources().map((s) => s.key).sort()).toEqual(['resend', 'stripe'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('flattens module handler entries into the handler registry', () => {
|
|
36
|
+
registerWebhookHandlerEntries([
|
|
37
|
+
{
|
|
38
|
+
moduleId: 'gateway_stripe',
|
|
39
|
+
handlers: [
|
|
40
|
+
{ meta: { source: 'stripe', event: 'payment_intent.succeeded', id: 'a' }, handler: async () => ({ default: noopHandler }) },
|
|
41
|
+
{ meta: { source: 'stripe', event: 'charge.refunded', id: 'b' }, handler: async () => ({ default: noopHandler }) },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
])
|
|
45
|
+
expect(listWebhookHandlers().map((e) => e.meta.id).sort()).toEqual(['a', 'b'])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('tolerates entries with no sources/handlers', () => {
|
|
49
|
+
registerWebhookSourceEntries([{ moduleId: 'empty', sources: [] }])
|
|
50
|
+
registerWebhookHandlerEntries([{ moduleId: 'empty', handlers: [] }])
|
|
51
|
+
expect(listWebhookSources()).toEqual([])
|
|
52
|
+
expect(listWebhookHandlers()).toEqual([])
|
|
53
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import type {
|
|
4
|
+
WebhookHandlerContext,
|
|
5
|
+
WebhookHandlerPayload,
|
|
6
|
+
WebhookHandlerResult,
|
|
7
|
+
} from '@open-mercato/shared/lib/webhooks'
|
|
8
|
+
import { WebhookIngestionEntity } from '../data/entities'
|
|
9
|
+
import { emitWebhooksEvent } from '../events'
|
|
10
|
+
import { resolveWebhookHandlers } from './inbound-registry'
|
|
11
|
+
|
|
12
|
+
const MAX_HANDLER_RESULTS = 50
|
|
13
|
+
const MAX_ERROR_MESSAGE_LENGTH = 1024
|
|
14
|
+
|
|
15
|
+
export type InboundDispatchJob = {
|
|
16
|
+
ingestionId: string
|
|
17
|
+
sourceKey: string
|
|
18
|
+
eventType: string
|
|
19
|
+
tenantId: string
|
|
20
|
+
organizationId: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncateError(message: string): string {
|
|
24
|
+
return message.length > MAX_ERROR_MESSAGE_LENGTH
|
|
25
|
+
? `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}…`
|
|
26
|
+
: message
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function moduleOfHandler(handlerId: string): string {
|
|
30
|
+
const separatorIndex = handlerId.indexOf(':')
|
|
31
|
+
return separatorIndex > 0 ? handlerId.slice(0, separatorIndex) : handlerId
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Process a queued inbound-dispatch job: load the ingestion, run every matching
|
|
36
|
+
* handler independently (a failing handler never blocks the others), record
|
|
37
|
+
* per-handler results, and emit lifecycle events. The payload and headers are
|
|
38
|
+
* read from the (decrypted) ingestion row rather than carried on the job, so the
|
|
39
|
+
* queue never stores the raw webhook body. Idempotent — a job whose ingestion is
|
|
40
|
+
* already `processed` returns early, and a retry after a partial failure re-runs
|
|
41
|
+
* only the handlers that have not already succeeded.
|
|
42
|
+
*/
|
|
43
|
+
export async function processInboundDispatchJob(
|
|
44
|
+
em: EntityManager,
|
|
45
|
+
job: InboundDispatchJob,
|
|
46
|
+
ctx: WebhookHandlerContext,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
const startedAtMs = Date.now()
|
|
49
|
+
const ingestion = await findOneWithDecryption(
|
|
50
|
+
em,
|
|
51
|
+
WebhookIngestionEntity,
|
|
52
|
+
{
|
|
53
|
+
id: job.ingestionId,
|
|
54
|
+
tenantId: job.tenantId,
|
|
55
|
+
organizationId: job.organizationId,
|
|
56
|
+
},
|
|
57
|
+
{},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (!ingestion || ingestion.status === 'processed') return
|
|
61
|
+
|
|
62
|
+
ingestion.status = 'processing'
|
|
63
|
+
await em.flush()
|
|
64
|
+
|
|
65
|
+
const handlers = resolveWebhookHandlers(job.sourceKey, job.eventType)
|
|
66
|
+
const payload: WebhookHandlerPayload = {
|
|
67
|
+
data: ingestion.payload,
|
|
68
|
+
eventType: job.eventType,
|
|
69
|
+
sourceKey: job.sourceKey,
|
|
70
|
+
headers: ingestion.headers ?? {},
|
|
71
|
+
ingestionId: job.ingestionId,
|
|
72
|
+
tenantId: job.tenantId,
|
|
73
|
+
organizationId: job.organizationId,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const previousResults = Array.isArray(ingestion.handlerResults) ? ingestion.handlerResults : []
|
|
77
|
+
const alreadySucceeded = new Map(
|
|
78
|
+
previousResults
|
|
79
|
+
.filter((result) => result?.status === 'success')
|
|
80
|
+
.map((result) => [result.handlerId, result]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const results: WebhookHandlerResult[] = []
|
|
84
|
+
let failedCount = 0
|
|
85
|
+
|
|
86
|
+
for (const entry of handlers) {
|
|
87
|
+
const previousSuccess = alreadySucceeded.get(entry.meta.id)
|
|
88
|
+
if (previousSuccess) {
|
|
89
|
+
results.push(previousSuccess)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handlerStartedMs = Date.now()
|
|
94
|
+
const handlerStartedAt = new Date(handlerStartedMs).toISOString()
|
|
95
|
+
try {
|
|
96
|
+
const mod = await entry.handler()
|
|
97
|
+
await mod.default(payload, ctx)
|
|
98
|
+
results.push({
|
|
99
|
+
handlerId: entry.meta.id,
|
|
100
|
+
module: moduleOfHandler(entry.meta.id),
|
|
101
|
+
status: 'success',
|
|
102
|
+
durationMs: Date.now() - handlerStartedMs,
|
|
103
|
+
startedAt: handlerStartedAt,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
failedCount += 1
|
|
107
|
+
const message = truncateError(error instanceof Error ? error.message : String(error))
|
|
108
|
+
results.push({
|
|
109
|
+
handlerId: entry.meta.id,
|
|
110
|
+
module: moduleOfHandler(entry.meta.id),
|
|
111
|
+
status: 'failed',
|
|
112
|
+
errorMessage: message,
|
|
113
|
+
durationMs: Date.now() - handlerStartedMs,
|
|
114
|
+
startedAt: handlerStartedAt,
|
|
115
|
+
})
|
|
116
|
+
await emitWebhooksEvent('webhooks.inbound.handler_failed', {
|
|
117
|
+
ingestionId: job.ingestionId,
|
|
118
|
+
sourceKey: job.sourceKey,
|
|
119
|
+
eventType: job.eventType,
|
|
120
|
+
handlerId: entry.meta.id,
|
|
121
|
+
errorMessage: message,
|
|
122
|
+
tenantId: job.tenantId,
|
|
123
|
+
organizationId: job.organizationId,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ingestion.handlerCount = handlers.length
|
|
129
|
+
ingestion.handlerResults = results.slice(0, MAX_HANDLER_RESULTS)
|
|
130
|
+
ingestion.status = failedCount > 0 ? 'failed' : 'processed'
|
|
131
|
+
ingestion.processedAt = new Date()
|
|
132
|
+
ingestion.durationMs = Date.now() - startedAtMs
|
|
133
|
+
ingestion.errorMessage = failedCount > 0
|
|
134
|
+
? `${failedCount}/${handlers.length} handlers failed`
|
|
135
|
+
: null
|
|
136
|
+
await em.flush()
|
|
137
|
+
|
|
138
|
+
await emitWebhooksEvent('webhooks.inbound.processed', {
|
|
139
|
+
ingestionId: job.ingestionId,
|
|
140
|
+
sourceKey: job.sourceKey,
|
|
141
|
+
eventType: job.eventType,
|
|
142
|
+
handlerCount: handlers.length,
|
|
143
|
+
failedCount,
|
|
144
|
+
tenantId: job.tenantId,
|
|
145
|
+
organizationId: job.organizationId,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { matchWebhookEventPattern } from '@open-mercato/shared/lib/events/patterns'
|
|
2
|
+
import type {
|
|
3
|
+
WebhookHandlerRegistryEntry,
|
|
4
|
+
WebhookSourceConfig,
|
|
5
|
+
} from '@open-mercato/shared/lib/webhooks'
|
|
6
|
+
|
|
7
|
+
const WEBHOOK_SOURCES_KEY = '__openMercatoWebhookSources__'
|
|
8
|
+
const WEBHOOK_HANDLERS_KEY = '__openMercatoWebhookHandlers__'
|
|
9
|
+
|
|
10
|
+
type GlobalState = typeof globalThis & {
|
|
11
|
+
[WEBHOOK_SOURCES_KEY]?: Map<string, WebhookSourceConfig>
|
|
12
|
+
[WEBHOOK_HANDLERS_KEY]?: WebhookHandlerRegistryEntry[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSourceRegistry(): Map<string, WebhookSourceConfig> {
|
|
16
|
+
const globalState = globalThis as GlobalState
|
|
17
|
+
if (!globalState[WEBHOOK_SOURCES_KEY]) {
|
|
18
|
+
globalState[WEBHOOK_SOURCES_KEY] = new Map<string, WebhookSourceConfig>()
|
|
19
|
+
}
|
|
20
|
+
return globalState[WEBHOOK_SOURCES_KEY]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getHandlerRegistry(): WebhookHandlerRegistryEntry[] {
|
|
24
|
+
const globalState = globalThis as GlobalState
|
|
25
|
+
if (!globalState[WEBHOOK_HANDLERS_KEY]) {
|
|
26
|
+
globalState[WEBHOOK_HANDLERS_KEY] = []
|
|
27
|
+
}
|
|
28
|
+
return globalState[WEBHOOK_HANDLERS_KEY]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerWebhookSource(config: WebhookSourceConfig): () => void {
|
|
32
|
+
const registry = getSourceRegistry()
|
|
33
|
+
registry.set(config.key, config)
|
|
34
|
+
return () => {
|
|
35
|
+
if (registry.get(config.key) === config) registry.delete(config.key)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setWebhookSources(configs: WebhookSourceConfig[]): void {
|
|
40
|
+
const registry = getSourceRegistry()
|
|
41
|
+
registry.clear()
|
|
42
|
+
for (const config of configs) registry.set(config.key, config)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getWebhookSource(sourceKey: string): WebhookSourceConfig | undefined {
|
|
46
|
+
return getSourceRegistry().get(sourceKey)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listWebhookSources(): WebhookSourceConfig[] {
|
|
50
|
+
return Array.from(getSourceRegistry().values())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearWebhookSources(): void {
|
|
54
|
+
getSourceRegistry().clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerWebhookHandler(entry: WebhookHandlerRegistryEntry): () => void {
|
|
58
|
+
const registry = getHandlerRegistry()
|
|
59
|
+
registry.push(entry)
|
|
60
|
+
return () => {
|
|
61
|
+
const index = registry.indexOf(entry)
|
|
62
|
+
if (index >= 0) registry.splice(index, 1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setWebhookHandlers(entries: WebhookHandlerRegistryEntry[]): void {
|
|
67
|
+
const registry = getHandlerRegistry()
|
|
68
|
+
registry.length = 0
|
|
69
|
+
registry.push(...entries)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function listWebhookHandlers(): WebhookHandlerRegistryEntry[] {
|
|
73
|
+
return [...getHandlerRegistry()]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function clearWebhookHandlers(): void {
|
|
77
|
+
getHandlerRegistry().length = 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the handlers that match an inbound webhook's source key and event type.
|
|
82
|
+
* Event matching reuses the outbound prefix-wildcard semantics (`*`, `payment_intent.*`).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveWebhookHandlers(
|
|
85
|
+
sourceKey: string,
|
|
86
|
+
eventType: string,
|
|
87
|
+
): WebhookHandlerRegistryEntry[] {
|
|
88
|
+
return getHandlerRegistry().filter((entry) => {
|
|
89
|
+
if (entry.meta.source !== sourceKey) return false
|
|
90
|
+
return matchWebhookEventPattern(eventType, entry.meta.event)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WebhookHandlerRegistryEntry,
|
|
3
|
+
WebhookSourceConfig,
|
|
4
|
+
} from '@open-mercato/shared/lib/webhooks'
|
|
5
|
+
import { setWebhookHandlers, setWebhookSources } from './inbound-registry'
|
|
6
|
+
|
|
7
|
+
export type WebhookSourceModuleEntry = {
|
|
8
|
+
moduleId: string
|
|
9
|
+
sources: WebhookSourceConfig[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WebhookHandlerModuleEntry = {
|
|
13
|
+
moduleId: string
|
|
14
|
+
handlers: WebhookHandlerRegistryEntry[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bootstrap-time registration for module-declared webhook sources.
|
|
19
|
+
* Driven by the `webhooks.sources` generator plugin via
|
|
20
|
+
* `bootstrap-registrations.generated.ts` — modules contribute a
|
|
21
|
+
* `webhook-sources.ts` and are wired here without bootstrap.ts edits.
|
|
22
|
+
*/
|
|
23
|
+
export function registerWebhookSourceEntries(entries: WebhookSourceModuleEntry[]): void {
|
|
24
|
+
setWebhookSources(entries.flatMap((entry) => entry.sources ?? []))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bootstrap-time registration for module-declared webhook handlers.
|
|
29
|
+
* Driven by the `webhooks.handlers` generator plugin.
|
|
30
|
+
*/
|
|
31
|
+
export function registerWebhookHandlerEntries(entries: WebhookHandlerModuleEntry[]): void {
|
|
32
|
+
setWebhookHandlers(entries.flatMap((entry) => entry.handlers ?? []))
|
|
33
|
+
}
|
|
@@ -2,13 +2,17 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
2
2
|
import { createModuleQueue, type Queue } from '@open-mercato/queue'
|
|
3
3
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
4
|
import type { WebhookDeliveryJob } from './delivery'
|
|
5
|
+
import type { InboundDispatchJob } from './inbound-dispatch'
|
|
5
6
|
|
|
6
7
|
const logger = createLogger('webhooks')
|
|
7
8
|
|
|
8
9
|
const queues = new Map<string, Queue<WebhookDeliveryJob>>()
|
|
10
|
+
const inboundQueues = new Map<string, Queue<InboundDispatchJob>>()
|
|
9
11
|
const LOCAL_WORKER_PROMISE_KEY = '__openMercatoWebhookLocalWorkerPromise__'
|
|
12
|
+
const LOCAL_INBOUND_WORKER_PROMISE_KEY = '__openMercatoWebhookLocalInboundWorkerPromise__'
|
|
10
13
|
|
|
11
14
|
export const WEBHOOK_DELIVERIES_QUEUE = 'webhook-deliveries'
|
|
15
|
+
export const WEBHOOK_INBOUND_DISPATCH_QUEUE = 'webhook-inbound-dispatch'
|
|
12
16
|
|
|
13
17
|
export function getWebhookQueue(queueName: string = WEBHOOK_DELIVERIES_QUEUE): Queue<WebhookDeliveryJob> {
|
|
14
18
|
const existing = queues.get(queueName)
|
|
@@ -61,3 +65,59 @@ export async function enqueueWebhookDelivery(job: WebhookDeliveryJob, delayMs?:
|
|
|
61
65
|
await ensureLocalWebhookQueueWorkerStarted()
|
|
62
66
|
return jobId
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
export function getInboundDispatchQueue(
|
|
70
|
+
queueName: string = WEBHOOK_INBOUND_DISPATCH_QUEUE,
|
|
71
|
+
): Queue<InboundDispatchJob> {
|
|
72
|
+
const existing = inboundQueues.get(queueName)
|
|
73
|
+
if (existing) return existing
|
|
74
|
+
|
|
75
|
+
const concurrency = Math.max(1, Number.parseInt(process.env.WEBHOOK_INBOUND_QUEUE_CONCURRENCY ?? '5', 10) || 5)
|
|
76
|
+
const created = createModuleQueue<InboundDispatchJob>(queueName, { concurrency })
|
|
77
|
+
|
|
78
|
+
inboundQueues.set(queueName, created)
|
|
79
|
+
return created
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function ensureLocalInboundQueueWorkerStarted(): Promise<void> {
|
|
83
|
+
if (process.env.QUEUE_STRATEGY === 'async') return
|
|
84
|
+
|
|
85
|
+
const globalStore = globalThis as typeof globalThis & {
|
|
86
|
+
[LOCAL_INBOUND_WORKER_PROMISE_KEY]?: Promise<void>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]) {
|
|
90
|
+
await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY] = (async () => {
|
|
95
|
+
const queue = getInboundDispatchQueue()
|
|
96
|
+
|
|
97
|
+
await queue.process(async (job) => {
|
|
98
|
+
const [{ createRequestContainer }, { processInboundDispatchJob }] = await Promise.all([
|
|
99
|
+
import('@open-mercato/shared/lib/di/container'),
|
|
100
|
+
import('./inbound-dispatch'),
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
const container = await createRequestContainer()
|
|
104
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
105
|
+
await processInboundDispatchJob(em, job.payload, {
|
|
106
|
+
resolve: <T,>(name: string) => container.resolve(name) as T,
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})().catch((error) => {
|
|
110
|
+
delete globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]
|
|
111
|
+
logger.error('Failed to start local inbound dispatch worker', { err: error })
|
|
112
|
+
throw error
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await globalStore[LOCAL_INBOUND_WORKER_PROMISE_KEY]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function enqueueInboundDispatch(job: InboundDispatchJob): Promise<string> {
|
|
119
|
+
const queue = getInboundDispatchQueue()
|
|
120
|
+
const jobId = await queue.enqueue(job)
|
|
121
|
+
await ensureLocalInboundQueueWorkerStarted()
|
|
122
|
+
return jobId
|
|
123
|
+
}
|