create-mercato-app 0.6.4-develop.3921.1.8a42ddf4c8 → 0.6.4-develop.3929.1.fcf7afece2
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/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { DefaultDataEngine, type DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
1
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
2
4
|
|
|
3
5
|
export type ExampleCustomersSyncScope = {
|
|
@@ -8,6 +10,31 @@ export type ExampleCustomersSyncScope = {
|
|
|
8
10
|
export const EXAMPLE_CUSTOMERS_SYNC_OUTBOUND_ORIGIN = 'example_customers_sync:outbound'
|
|
9
11
|
export const EXAMPLE_CUSTOMERS_SYNC_INBOUND_ORIGIN = 'example_customers_sync:inbound'
|
|
10
12
|
|
|
13
|
+
type AnyContainer = { resolve: <T = unknown>(name: string) => T }
|
|
14
|
+
|
|
15
|
+
// Each sync invocation gets its own forked EM + fresh DataEngine so that
|
|
16
|
+
// concurrent or sequential jobs cannot pollute the shared identity map.
|
|
17
|
+
// Without this, two outbound jobs targeting the same interaction.id would
|
|
18
|
+
// both add an INSERT for the same Todo into the shared UoW, and the second
|
|
19
|
+
// flush (inside setRecordCustomFields) would fail with todos_pkey duplicate.
|
|
20
|
+
export function createScopedSyncContainer(container: AnyContainer): AnyContainer {
|
|
21
|
+
const baseEm = container.resolve('em') as EntityManager
|
|
22
|
+
const scopedEm = baseEm.fork({ clear: true })
|
|
23
|
+
let scopedDataEngine: DataEngine | null = null
|
|
24
|
+
return {
|
|
25
|
+
resolve<T = unknown>(name: string): T {
|
|
26
|
+
if (name === 'em') return scopedEm as unknown as T
|
|
27
|
+
if (name === 'dataEngine') {
|
|
28
|
+
if (!scopedDataEngine) {
|
|
29
|
+
scopedDataEngine = new DefaultDataEngine(scopedEm, container as never)
|
|
30
|
+
}
|
|
31
|
+
return scopedDataEngine as unknown as T
|
|
32
|
+
}
|
|
33
|
+
return container.resolve<T>(name)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
export function buildExampleCustomersSyncCommandContext(
|
|
12
39
|
container: { resolve: <T = unknown>(name: string) => T },
|
|
13
40
|
scope: ExampleCustomersSyncScope,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './mappings'
|
|
31
31
|
import {
|
|
32
32
|
buildExampleCustomersSyncCommandContext,
|
|
33
|
+
createScopedSyncContainer,
|
|
33
34
|
EXAMPLE_CUSTOMERS_SYNC_INBOUND_ORIGIN,
|
|
34
35
|
EXAMPLE_CUSTOMERS_SYNC_OUTBOUND_ORIGIN,
|
|
35
36
|
type ExampleCustomersSyncScope,
|
|
@@ -461,14 +462,24 @@ async function ensureLegacyExampleMapping(
|
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
export async function syncCustomerInteractionToExampleTodo(
|
|
464
|
-
|
|
465
|
+
rawContainer: ContainerLike,
|
|
465
466
|
payload: ExampleCustomersSyncOutboundJobPayload,
|
|
466
467
|
): Promise<void> {
|
|
467
468
|
const scope = { tenantId: payload.tenantId, organizationId: payload.organizationId }
|
|
468
|
-
const flags = await resolveExampleCustomersSyncFlags(
|
|
469
|
+
const flags = await resolveExampleCustomersSyncFlags(rawContainer, scope.tenantId)
|
|
469
470
|
if (!flags.enabled) return
|
|
470
471
|
|
|
471
|
-
|
|
472
|
+
// Worker's own fork — used for reads (findMappingByInteractionId,
|
|
473
|
+
// loadExampleTodoSnapshot, ensureLegacyExampleMapping) and for the
|
|
474
|
+
// error-path mapping write (markMappingError). This preserves the
|
|
475
|
+
// pre-fix behavior of the local em so the catch path keeps working.
|
|
476
|
+
const em = (rawContainer.resolve('em') as EntityManager).fork()
|
|
477
|
+
// Separate scoped container — used ONLY for command-bus calls so each
|
|
478
|
+
// sync invocation gets its own DataEngine.em. This isolates the
|
|
479
|
+
// identity-map pollution that surfaced as a todos_pkey duplicate inside
|
|
480
|
+
// setRecordCustomFields when multiple jobs touched the same interaction.id
|
|
481
|
+
// through the shared request-container DataEngine.
|
|
482
|
+
const container = createScopedSyncContainer(rawContainer)
|
|
472
483
|
let mapping = await findMappingByInteractionId(em, scope, payload.interactionId)
|
|
473
484
|
|
|
474
485
|
try {
|
|
@@ -600,6 +611,10 @@ export async function syncExampleTodoToCanonicalInteraction(
|
|
|
600
611
|
const flags = await resolveExampleCustomersSyncFlags(container, scope.tenantId)
|
|
601
612
|
if (!flags.enabled || !flags.bidirectional) return
|
|
602
613
|
|
|
614
|
+
// Inbound sync never hit the outbound duplicate-key bug (it creates/updates
|
|
615
|
+
// canonical interactions, not same-id Todos), so it keeps the original
|
|
616
|
+
// shared-container behavior — scoping it was speculative and regressed the
|
|
617
|
+
// inbound field-clear propagation in TC-CRM-028.
|
|
603
618
|
const em = (container.resolve('em') as EntityManager).fork()
|
|
604
619
|
let mapping = await findMappingByTodoId(em, scope, payload.todoId)
|
|
605
620
|
let todo: ExampleTodoSnapshot | null = null
|
|
@@ -900,8 +915,8 @@ export async function reconcileLegacyExampleTodoLinks(
|
|
|
900
915
|
): Promise<ExampleCustomersSyncReconcileResult> {
|
|
901
916
|
const scope = { tenantId: input.tenantId, organizationId: input.organizationId }
|
|
902
917
|
const limit = Math.min(Math.max(input.limit ?? 100, 1), 500)
|
|
903
|
-
const { rows, nextCursor } = await loadLegacyExampleTodoLinks(container, scope, limit, input.cursor)
|
|
904
918
|
const em = (container.resolve('em') as EntityManager).fork()
|
|
919
|
+
const { rows, nextCursor } = await loadLegacyExampleTodoLinks(container, scope, limit, input.cursor)
|
|
905
920
|
const items: ExampleCustomersSyncReconcileItem[] = []
|
|
906
921
|
let mapped = 0
|
|
907
922
|
let createdInteractions = 0
|