create-mercato-app 0.6.4-develop.3921.1.8a42ddf4c8 → 0.6.4-develop.3926.1.c83e4d3395

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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.6.4-develop.3921.1.8a42ddf4c8",
3
+ "version": "0.6.4-develop.3926.1.c83e4d3395",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -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
- container: ContainerLike,
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(container, scope.tenantId)
469
+ const flags = await resolveExampleCustomersSyncFlags(rawContainer, scope.tenantId)
469
470
  if (!flags.enabled) return
470
471
 
471
- const em = (container.resolve('em') as EntityManager).fork()
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