@fullstackhouse/open-mercato-data-sync-durable 0.1.0

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.
Files changed (175) hide show
  1. package/README.md +86 -0
  2. package/dist/engine/durable-run.js +130 -0
  3. package/dist/engine/durable-run.js.map +7 -0
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/kinds/data-sync-run.js +57 -0
  7. package/dist/kinds/data-sync-run.js.map +7 -0
  8. package/dist/mirror-version.js +5 -0
  9. package/dist/mirror-version.js.map +7 -0
  10. package/dist/mirror.manifest.json +65 -0
  11. package/dist/modules/data_sync/acl.js +8 -0
  12. package/dist/modules/data_sync/acl.js.map +7 -0
  13. package/dist/modules/data_sync/api/mappings/[id]/route.js +17 -0
  14. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +7 -0
  15. package/dist/modules/data_sync/api/mappings/route.js +15 -0
  16. package/dist/modules/data_sync/api/mappings/route.js.map +7 -0
  17. package/dist/modules/data_sync/api/options.js +13 -0
  18. package/dist/modules/data_sync/api/options.js.map +7 -0
  19. package/dist/modules/data_sync/api/run.js +59 -0
  20. package/dist/modules/data_sync/api/run.js.map +7 -0
  21. package/dist/modules/data_sync/api/runs/[id]/cancel.js +13 -0
  22. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +7 -0
  23. package/dist/modules/data_sync/api/runs/[id]/retry.js +13 -0
  24. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +7 -0
  25. package/dist/modules/data_sync/api/runs/[id]/route.js +13 -0
  26. package/dist/modules/data_sync/api/runs/[id]/route.js.map +7 -0
  27. package/dist/modules/data_sync/api/runs.js +13 -0
  28. package/dist/modules/data_sync/api/runs.js.map +7 -0
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js +17 -0
  30. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
  31. package/dist/modules/data_sync/api/schedules/route.js +15 -0
  32. package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
  33. package/dist/modules/data_sync/api/schedules/serialize.js +6 -0
  34. package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
  35. package/dist/modules/data_sync/api/validate.js +13 -0
  36. package/dist/modules/data_sync/api/validate.js.map +7 -0
  37. package/dist/modules/data_sync/backend/data-sync/page.js +7 -0
  38. package/dist/modules/data_sync/backend/data-sync/page.js.map +7 -0
  39. package/dist/modules/data_sync/backend/data-sync/page.meta.js +6 -0
  40. package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +7 -0
  41. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +7 -0
  42. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +7 -0
  43. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js +6 -0
  44. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js.map +7 -0
  45. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +9 -0
  46. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
  47. package/dist/modules/data_sync/components/RunParameterFields.js +13 -0
  48. package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
  49. package/dist/modules/data_sync/data/entities.js +9 -0
  50. package/dist/modules/data_sync/data/entities.js.map +7 -0
  51. package/dist/modules/data_sync/data/validators.js +12 -0
  52. package/dist/modules/data_sync/data/validators.js.map +7 -0
  53. package/dist/modules/data_sync/di.js +33 -0
  54. package/dist/modules/data_sync/di.js.map +7 -0
  55. package/dist/modules/data_sync/events.js +9 -0
  56. package/dist/modules/data_sync/events.js.map +7 -0
  57. package/dist/modules/data_sync/extension-points.js +8 -0
  58. package/dist/modules/data_sync/extension-points.js.map +7 -0
  59. package/dist/modules/data_sync/i18n/de.json +135 -0
  60. package/dist/modules/data_sync/i18n/en.json +135 -0
  61. package/dist/modules/data_sync/i18n/es.json +135 -0
  62. package/dist/modules/data_sync/i18n/ko.json +135 -0
  63. package/dist/modules/data_sync/i18n/pl.json +135 -0
  64. package/dist/modules/data_sync/index.js +6 -0
  65. package/dist/modules/data_sync/index.js.map +7 -0
  66. package/dist/modules/data_sync/lib/abandoned-run.js +6 -0
  67. package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
  68. package/dist/modules/data_sync/lib/adapter-registry.js +10 -0
  69. package/dist/modules/data_sync/lib/adapter-registry.js.map +7 -0
  70. package/dist/modules/data_sync/lib/adapter.js +2 -0
  71. package/dist/modules/data_sync/lib/adapter.js.map +7 -0
  72. package/dist/modules/data_sync/lib/adopt-on-delivery.js +35 -0
  73. package/dist/modules/data_sync/lib/adopt-on-delivery.js.map +7 -0
  74. package/dist/modules/data_sync/lib/batch-stream.js +6 -0
  75. package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
  76. package/dist/modules/data_sync/lib/id-mapping.js +6 -0
  77. package/dist/modules/data_sync/lib/id-mapping.js.map +7 -0
  78. package/dist/modules/data_sync/lib/queue-policy.js +11 -0
  79. package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
  80. package/dist/modules/data_sync/lib/queue.js +6 -0
  81. package/dist/modules/data_sync/lib/queue.js.map +7 -0
  82. package/dist/modules/data_sync/lib/run-parameters.js +8 -0
  83. package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
  84. package/dist/modules/data_sync/lib/start-cursor.js +8 -0
  85. package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
  86. package/dist/modules/data_sync/lib/start-run.js +51 -0
  87. package/dist/modules/data_sync/lib/start-run.js.map +7 -0
  88. package/dist/modules/data_sync/lib/sync-engine.js +6 -0
  89. package/dist/modules/data_sync/lib/sync-engine.js.map +7 -0
  90. package/dist/modules/data_sync/lib/sync-run-service.js +7 -0
  91. package/dist/modules/data_sync/lib/sync-run-service.js.map +7 -0
  92. package/dist/modules/data_sync/lib/sync-schedule-service.js +6 -0
  93. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
  94. package/dist/modules/data_sync/lib/syncRunStatus.js +9 -0
  95. package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
  96. package/dist/modules/data_sync/lib/version-guard.js +14 -0
  97. package/dist/modules/data_sync/lib/version-guard.js.map +7 -0
  98. package/dist/modules/data_sync/migrations/Migration20260304113737.js +6 -0
  99. package/dist/modules/data_sync/migrations/Migration20260304113737.js.map +7 -0
  100. package/dist/modules/data_sync/migrations/Migration20260810120000.js +6 -0
  101. package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
  102. package/dist/modules/data_sync/setup.js +8 -0
  103. package/dist/modules/data_sync/setup.js.map +7 -0
  104. package/dist/modules/data_sync/workers/sync-export.js +19 -0
  105. package/dist/modules/data_sync/workers/sync-export.js.map +7 -0
  106. package/dist/modules/data_sync/workers/sync-import.js +19 -0
  107. package/dist/modules/data_sync/workers/sync-import.js.map +7 -0
  108. package/dist/modules/data_sync/workers/sync-scheduled.js +8 -0
  109. package/dist/modules/data_sync/workers/sync-scheduled.js.map +7 -0
  110. package/generated/entities/sync_cursor/index.ts +8 -0
  111. package/generated/entities/sync_mapping/index.ts +8 -0
  112. package/generated/entities/sync_run/index.ts +22 -0
  113. package/generated/entities/sync_schedule/index.ts +16 -0
  114. package/generated/entities.ids.generated.ts +13 -0
  115. package/package.json +135 -0
  116. package/src/compat/mirror-shape.test.ts +106 -0
  117. package/src/compat/seams.test.ts +97 -0
  118. package/src/engine/durable-run.ts +231 -0
  119. package/src/index.ts +4 -0
  120. package/src/kinds/data-sync-run.ts +104 -0
  121. package/src/mirror-version.ts +3 -0
  122. package/src/mirror.manifest.json +65 -0
  123. package/src/modules/data_sync/__integration__/TC-DSD-001.spec.ts +80 -0
  124. package/src/modules/data_sync/acl.ts +5 -0
  125. package/src/modules/data_sync/api/mappings/[id]/route.ts +12 -0
  126. package/src/modules/data_sync/api/mappings/route.ts +11 -0
  127. package/src/modules/data_sync/api/options.ts +10 -0
  128. package/src/modules/data_sync/api/run.ts +103 -0
  129. package/src/modules/data_sync/api/runs/[id]/cancel.ts +10 -0
  130. package/src/modules/data_sync/api/runs/[id]/retry.ts +10 -0
  131. package/src/modules/data_sync/api/runs/[id]/route.ts +10 -0
  132. package/src/modules/data_sync/api/runs.ts +10 -0
  133. package/src/modules/data_sync/api/schedules/[id]/route.ts +12 -0
  134. package/src/modules/data_sync/api/schedules/route.ts +11 -0
  135. package/src/modules/data_sync/api/schedules/serialize.ts +4 -0
  136. package/src/modules/data_sync/api/validate.ts +10 -0
  137. package/src/modules/data_sync/backend/data-sync/page.meta.ts +4 -0
  138. package/src/modules/data_sync/backend/data-sync/page.tsx +5 -0
  139. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.meta.ts +4 -0
  140. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +5 -0
  141. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +6 -0
  142. package/src/modules/data_sync/components/RunParameterFields.tsx +5 -0
  143. package/src/modules/data_sync/data/entities.ts +4 -0
  144. package/src/modules/data_sync/data/validators.ts +4 -0
  145. package/src/modules/data_sync/di.ts +57 -0
  146. package/src/modules/data_sync/events.ts +5 -0
  147. package/src/modules/data_sync/extension-points.ts +5 -0
  148. package/src/modules/data_sync/i18n/de.json +135 -0
  149. package/src/modules/data_sync/i18n/en.json +135 -0
  150. package/src/modules/data_sync/i18n/es.json +135 -0
  151. package/src/modules/data_sync/i18n/ko.json +135 -0
  152. package/src/modules/data_sync/i18n/pl.json +135 -0
  153. package/src/modules/data_sync/index.ts +4 -0
  154. package/src/modules/data_sync/lib/adapter-registry.ts +4 -0
  155. package/src/modules/data_sync/lib/adapter.ts +3 -0
  156. package/src/modules/data_sync/lib/adopt-on-delivery.ts +69 -0
  157. package/src/modules/data_sync/lib/batch-stream.ts +4 -0
  158. package/src/modules/data_sync/lib/id-mapping.ts +4 -0
  159. package/src/modules/data_sync/lib/queue-policy.ts +4 -0
  160. package/src/modules/data_sync/lib/queue.ts +4 -0
  161. package/src/modules/data_sync/lib/run-parameters.ts +4 -0
  162. package/src/modules/data_sync/lib/start-cursor.ts +4 -0
  163. package/src/modules/data_sync/lib/start-run.ts +106 -0
  164. package/src/modules/data_sync/lib/sync-engine.ts +4 -0
  165. package/src/modules/data_sync/lib/sync-run-service.ts +4 -0
  166. package/src/modules/data_sync/lib/sync-schedule-service.ts +4 -0
  167. package/src/modules/data_sync/lib/syncRunStatus.ts +4 -0
  168. package/src/modules/data_sync/lib/version-guard.ts +27 -0
  169. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +1013 -0
  170. package/src/modules/data_sync/migrations/Migration20260304113737.ts +4 -0
  171. package/src/modules/data_sync/migrations/Migration20260810120000.ts +4 -0
  172. package/src/modules/data_sync/setup.ts +5 -0
  173. package/src/modules/data_sync/workers/sync-export.ts +20 -0
  174. package/src/modules/data_sync/workers/sync-import.ts +20 -0
  175. package/src/modules/data_sync/workers/sync-scheduled.ts +5 -0
@@ -0,0 +1,104 @@
1
+ // The job kinds a `data_sync` run becomes.
2
+ //
3
+ // A slice is one call into core's own engine, with the run service and the progress service
4
+ // decorated for its duration. The engine does all the work it always did; what changes is where
5
+ // its cursor commits go (through the lease fence), when it stops (at a batch boundary when the
6
+ // slice budget is spent), and who writes the terminal state (the durable transition, in the
7
+ // same transaction as the job's own).
8
+
9
+ import type { KindDefinition, SliceContext, SliceOutcome, SqlExecutor } from '@fullstackhouse/open-mercato-durable-work'
10
+
11
+ import {
12
+ mirrorRunStatus,
13
+ outcomeOf,
14
+ recordSlice,
15
+ reopenRun,
16
+ type ProgressServiceLike,
17
+ type SyncRunServiceLike,
18
+ type SyncScope,
19
+ } from '../engine/durable-run'
20
+
21
+ export const IMPORT_KIND = 'data_sync.import'
22
+ export const EXPORT_KIND = 'data_sync.export'
23
+ export const DATA_SYNC_QUEUE = 'durable-work.data-sync'
24
+
25
+ export type SyncRunInput = {
26
+ runId: string
27
+ batchSize: number
28
+ direction: 'import' | 'export'
29
+ }
30
+
31
+ /** What a slice needs from the host, resolved per slice from the container. */
32
+ export type SyncEngineLike = {
33
+ runImport(runId: string, batchSize: number, scope: SyncScope): Promise<void>
34
+ runExport(runId: string, batchSize: number, scope: SyncScope): Promise<void>
35
+ }
36
+
37
+ export type DataSyncKindDeps = {
38
+ /** Resolved per slice, because each slice runs on its own EntityManager. */
39
+ resolve(): Promise<{
40
+ engine: (services: { runService: SyncRunServiceLike; progressService: ProgressServiceLike }) => SyncEngineLike
41
+ runService: SyncRunServiceLike
42
+ progressService: ProgressServiceLike
43
+ }>
44
+ }
45
+
46
+ async function runSlice(ctx: SliceContext<SyncRunInput>, deps: DataSyncKindDeps): Promise<SliceOutcome> {
47
+ const input = ctx.job.input as SyncRunInput
48
+ const scope: SyncScope = { tenantId: ctx.scope.tenantId, organizationId: ctx.scope.organizationId }
49
+ const { engine, runService, progressService } = await deps.resolve()
50
+
51
+ const recorder = recordSlice(ctx, runService, progressService)
52
+ const decorated = engine({ runService: recorder.runService, progressService: recorder.progressService })
53
+
54
+ if (input.direction === 'export') await decorated.runExport(input.runId, input.batchSize, scope)
55
+ else await decorated.runImport(input.runId, input.batchSize, scope)
56
+
57
+ const after = await runService.getRun(input.runId, scope)
58
+ return outcomeOf(recorder, input.runId, after?.status)
59
+ }
60
+
61
+ /**
62
+ * Builds both kinds.
63
+ *
64
+ * `orphanPolicy: 'redrive'` is deliberate and is the one place the adopter asserts something
65
+ * about the work rather than the mechanism: a sync run keeps a committed cursor, so re-running
66
+ * it after a worker died resumes rather than repeats. That is what makes automatic recovery
67
+ * safe here when the mechanism's default is to park and wait for a human.
68
+ */
69
+ export function dataSyncKinds(deps: DataSyncKindDeps): KindDefinition<SyncRunInput, never>[] {
70
+ const shared = {
71
+ queue: DATA_SYNC_QUEUE,
72
+ requiredFeatures: ['data_sync.run'],
73
+ orphanPolicy: 'redrive' as const,
74
+ // Long enough that a slow adapter page does not look like a dead worker, short enough that
75
+ // a dead worker is noticed in about a minute.
76
+ lease: { ttlMs: 60_000, sliceBudgetMs: 300_000, pendingTtlMs: 900_000 },
77
+
78
+ async onTransition(job: { id: string; input: unknown; status: string; errorMessage: string | null }, _scope: unknown, tx: SqlExecutor) {
79
+ const { runId } = job.input as SyncRunInput
80
+ const status = job.status === 'completed' ? 'completed' : job.status === 'cancelled' ? 'cancelled' : 'failed'
81
+ return mirrorRunStatus(tx, runId, status, job.errorMessage)
82
+ },
83
+
84
+ async onRedrive(job: { input: unknown }, _scope: unknown, tx: SqlExecutor) {
85
+ return reopenRun(tx, (job.input as SyncRunInput).runId)
86
+ },
87
+ }
88
+
89
+ return [
90
+ { ...shared, kind: IMPORT_KIND, step: (ctx) => runSlice(ctx, deps) } as KindDefinition<SyncRunInput, never>,
91
+ { ...shared, kind: EXPORT_KIND, step: (ctx) => runSlice(ctx, deps) } as KindDefinition<SyncRunInput, never>,
92
+ ]
93
+ }
94
+
95
+ /** The single-runner key: one live run per integration, entity and direction. Two imports of
96
+ * the same entity would interleave over one cursor. */
97
+ export function syncLockKey(integrationId: string, entityType: string, direction: string): string {
98
+ return `data_sync:${integrationId}:${entityType}:${direction}`
99
+ }
100
+
101
+ /** Makes starting a run twice for the same run id return the first job rather than a second. */
102
+ export function syncIdempotencyKey(runId: string): string {
103
+ return `data_sync.run:${runId}`
104
+ }
@@ -0,0 +1,3 @@
1
+ // GENERATED by scripts/gen-mirror.mjs. Do not edit.
2
+ /** The @open-mercato/core version this package's mirror and decorators were built against. */
3
+ export const MIRRORED_CORE_VERSION = "0.7.0"
@@ -0,0 +1,65 @@
1
+ {
2
+ "coreVersion": "0.7.0",
3
+ "moduleId": "data_sync",
4
+ "generated": {
5
+ "acl.ts": "3b30f591eae78964",
6
+ "api/mappings/[id]/route.ts": "7b3fb917db7bd831",
7
+ "api/mappings/route.ts": "5551bc5ff61dfebb",
8
+ "api/options.ts": "bff527b4061b6c2b",
9
+ "api/runs.ts": "a2024a96d896c814",
10
+ "api/runs/[id]/cancel.ts": "1c99c66183d994ae",
11
+ "api/runs/[id]/retry.ts": "cf6ea67efcebdeb9",
12
+ "api/runs/[id]/route.ts": "2d0e39daef513f92",
13
+ "api/schedules/[id]/route.ts": "af2b16bd45fd02b5",
14
+ "api/schedules/route.ts": "2dbf0d2f04cdc100",
15
+ "api/schedules/serialize.ts": "44664c58abafd26f",
16
+ "api/validate.ts": "9fd095700abd39cb",
17
+ "backend/data-sync/page.meta.ts": "0dd9b6b406fa2699",
18
+ "backend/data-sync/page.tsx": "e68b00a4242abbbb",
19
+ "backend/data-sync/runs/[id]/page.meta.ts": "a1946eb5885a2109",
20
+ "backend/data-sync/runs/[id]/page.tsx": "963778ff29f7d96f",
21
+ "components/IntegrationScheduleTab.tsx": "185732c76a2f953a",
22
+ "components/RunParameterFields.tsx": "3f49886ef0a55193",
23
+ "data/entities.ts": "16476c912a6c7d40",
24
+ "data/validators.ts": "6c238076541373b0",
25
+ "events.ts": "8eea801045ff9789",
26
+ "extension-points.ts": "903cae9d931e0fb3",
27
+ "i18n/de.json": "943e5875db1ab98b",
28
+ "i18n/en.json": "ae6c5edcba6bb7f7",
29
+ "i18n/es.json": "de135edcf1917269",
30
+ "i18n/ko.json": "cb75626e56a0d267",
31
+ "i18n/pl.json": "6096d5c85f98ba8a",
32
+ "index.ts": "c15d4c24db598205",
33
+ "lib/adapter-registry.ts": "9ddb1b6cbfa911d7",
34
+ "lib/adapter.ts": "3902953e7bdc6021",
35
+ "lib/batch-stream.ts": "db921759b23aef76",
36
+ "lib/id-mapping.ts": "08628d924244bfc8",
37
+ "lib/queue-policy.ts": "7c75ee82eca0ca70",
38
+ "lib/queue.ts": "0fb590d7695c3837",
39
+ "lib/run-parameters.ts": "145b0a8697d34c82",
40
+ "lib/start-cursor.ts": "2620c39153035fb5",
41
+ "lib/sync-engine.ts": "bf3dbfdb1676190c",
42
+ "lib/sync-run-service.ts": "f55c4ec8c547cbd9",
43
+ "lib/sync-schedule-service.ts": "fe2e6bafc37ed319",
44
+ "lib/syncRunStatus.ts": "c30a457245c9cd11",
45
+ "migrations/.snapshot-open-mercato.json": "662107af1e52e5d3",
46
+ "migrations/Migration20260304113737.ts": "e3f0edec96d4a4d8",
47
+ "migrations/Migration20260810120000.ts": "29a2154fcf03c5ce",
48
+ "setup.ts": "f33921e5cb6aafa3",
49
+ "workers/sync-scheduled.ts": "bd1c585595bc7148"
50
+ },
51
+ "replaced": {
52
+ "api/run.ts": "0eda5b6869a0b95d",
53
+ "di.ts": "d351534bd20b2b29",
54
+ "lib/start-run.ts": "74081590458b202b",
55
+ "workers/sync-export.ts": "a23cad4d03dd42da",
56
+ "workers/sync-import.ts": "21e5356d2d394b6d"
57
+ },
58
+ "entityDescriptor": [
59
+ "entities.ids.generated.ts",
60
+ "entities/sync_run/index.ts",
61
+ "entities/sync_cursor/index.ts",
62
+ "entities/sync_mapping/index.ts",
63
+ "entities/sync_schedule/index.ts"
64
+ ]
65
+ }
@@ -0,0 +1,80 @@
1
+ // The `data_sync` drop-in, against a running app.
2
+ //
3
+ // The point of these is that nothing in them is durable-work specific: they exercise core's own
4
+ // `data_sync` API and assert core's own behaviour. If this package is doing its job, a host
5
+ // cannot tell the difference — except that a run now leaves a durable job behind it.
6
+
7
+ import { expect, test } from '@playwright/test'
8
+ import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'
9
+
10
+ type Options = { items: Array<{ integrationId: string; canStartRun?: boolean; isEnabled?: boolean }> }
11
+ type StartedRun = { id: string; progressJobId: string }
12
+ type Job = {
13
+ id: string
14
+ kind: string
15
+ status: string
16
+ lockKey: string | null
17
+ idempotencyKey: string | null
18
+ subject: { type: string; id: string } | null
19
+ redrivable: boolean
20
+ }
21
+
22
+ const startRun = async (request: Parameters<typeof apiRequest>[0], token: string, parameters: Record<string, unknown>) =>
23
+ apiRequest(request, 'POST', '/api/data_sync/run', {
24
+ token,
25
+ data: { integrationId: 'example_sync', entityType: 'example.record', direction: 'import', parameters },
26
+ })
27
+
28
+ // Serial: these share one lock key, so a run left live by one test is the reason the next
29
+ // would fail. Ordering them is honest — a parallel run of the same integration is exactly what
30
+ // the single-runner guarantee forbids.
31
+ test.describe.serial('data_sync served from the durable package', () => {
32
+ test('keeps core\'s API surface: the runs list answers as it always did', async ({ request }) => {
33
+ const token = await getAuthToken(request, 'admin')
34
+ const response = await apiRequest(request, 'GET', '/api/data_sync/runs', { token })
35
+ expect(response.status()).toBe(200)
36
+ expect(await response.json()).toMatchObject({ items: expect.any(Array) })
37
+ })
38
+
39
+ test('keeps core\'s adapter registry: the scripted integration is startable', async ({ request }) => {
40
+ // `example_sync` registers through core's own adapter registry from an `@app` module. That
41
+ // it is visible here is the evidence that swapping the package did not break the registry
42
+ // every other provider shares.
43
+ const token = await getAuthToken(request, 'admin')
44
+ const response = await apiRequest(request, 'GET', '/api/data_sync/options', { token })
45
+ expect(response.status()).toBe(200)
46
+
47
+ const body = (await response.json()) as Options
48
+ const example = body.items.find((item) => item.integrationId === 'example_sync')
49
+ expect(example, 'example_sync should be registered and enabled').toBeTruthy()
50
+ expect(example?.canStartRun).toBe(true)
51
+ })
52
+
53
+ test('a started run becomes a durable job with its lock, idempotency key and subject', async ({ request }) => {
54
+ const token = await getAuthToken(request, 'admin')
55
+ const started = await startRun(request, token, { batches: 3, itemsPerBatch: 1 })
56
+ expect(started.status()).toBe(201)
57
+ // Reported by the route so a failure here says *why* rather than just "no job found".
58
+ expect(started.headers()['x-durable-work'], started.headers()['x-durable-work-reason'] ?? 'no reason reported').toBe('created')
59
+ const run = (await started.json()) as StartedRun
60
+ expect(run.progressJobId, 'a progress job is required: core consults it for the per-batch stop').toBeTruthy()
61
+
62
+ const jobs = await apiRequest(request, 'GET', `/api/durable_work/jobs?kind=data_sync.import&pageSize=50`, { token })
63
+ const body = (await jobs.json()) as { items: Job[] }
64
+ const job = body.items.find((item) => item.subject?.id === run.id)
65
+
66
+ expect(job, 'the run should have produced a durable job').toBeTruthy()
67
+ expect(job!.lockKey).toBe('data_sync:example_sync:example.record:import')
68
+ expect(job!.idempotencyKey).toBe(`data_sync.run:${run.id}`)
69
+ expect(job!.subject).toEqual({ type: 'data_sync.run', id: run.id })
70
+ })
71
+
72
+ test('refuses a second live run for the same integration, entity and direction', async ({ request }) => {
73
+ // The single-runner guarantee. Two imports of one entity would interleave over a single
74
+ // cursor, and the rows one of them skipped would never be noticed.
75
+ const token = await getAuthToken(request, 'admin')
76
+ await startRun(request, token, { batches: 3 })
77
+ const second = await startRun(request, token, { batches: 3 })
78
+ expect(second.status()).toBe(409)
79
+ })
80
+ })
@@ -0,0 +1,5 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (acl.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/acl'
4
+ export { default } from '@open-mercato/core/modules/data_sync/acl'
5
+ export { features } from '@open-mercato/core/modules/data_sync/acl'
@@ -0,0 +1,12 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/mappings/[id]/route.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'
4
+ export { GET, PUT, DELETE } from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/mappings/[id]/route'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
10
+ PUT: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
11
+ DELETE: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
12
+ }
@@ -0,0 +1,11 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/mappings/route.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/mappings/route'
4
+ export { GET, POST } from '@open-mercato/core/modules/data_sync/api/mappings/route'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/mappings/route'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
10
+ POST: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
11
+ }
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/options.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/options'
4
+ export { GET } from '@open-mercato/core/modules/data_sync/api/options'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/options'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },
10
+ }
@@ -0,0 +1,103 @@
1
+ // Core's run route, with the durable job created alongside the run.
2
+ //
3
+ // Wrapped rather than copied. Core's route is 180 lines of validation, ACL guards, cursor
4
+ // resolution and parameter normalisation, and duplicating that is precisely the liability
5
+ // ADR 0004 removed for the engine — the same argument applies here.
6
+ //
7
+ // Why the wrapper is needed at all: replacing `lib/start-run.ts` redirects only callers that
8
+ // import *our* deep path. Core's own route imports its own sibling, so a run started through
9
+ // it never reaches our start path. This is where that is fixed.
10
+ //
11
+ // The durable job is created after core's transaction rather than inside it, which is weaker
12
+ // than the transactional start our own path gives. It is not a hole: core also enqueues onto
13
+ // its queue, our adopt-on-delivery worker is registered under core's worker id, and both paths
14
+ // carry the same idempotency key — so a crash between the two ends with the delivery adopting
15
+ // the run instead. What the wrapper buys is that the job normally exists immediately, rather
16
+ // than whenever the queue gets to it.
17
+
18
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
19
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
20
+ import { createLogger } from '@open-mercato/shared/lib/logger'
21
+ import { POST as corePost, openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/run'
22
+ import type { DurableWorkService } from '@fullstackhouse/open-mercato-durable-work'
23
+
24
+ import { EXPORT_KIND, IMPORT_KIND, syncIdempotencyKey, syncLockKey } from '../../../kinds/data-sync-run'
25
+
26
+ const logger = createLogger('data_sync').child({ component: 'durable-run-route' })
27
+
28
+ // Copied from core, because the generator reads it by AST and cannot follow a re-export.
29
+ export const metadata = {
30
+ POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },
31
+ }
32
+
33
+ export const openApi = coreOpenApi
34
+
35
+ type StartedRun = { id: string; progressJobId: string | null }
36
+ type RunRow = { id: string; integrationId: string; entityType: string; direction: string; progressJobId?: string | null }
37
+
38
+ /**
39
+ * Says what happened to the durable job, without changing core's response body.
40
+ *
41
+ * created — the job exists now
42
+ * deferred — it does not, and the queue delivery will adopt the run instead
43
+ *
44
+ * A header rather than a log line because the difference matters to whoever called: `created`
45
+ * means the run is already under a lease, `deferred` means it is not yet. Buried in a log,
46
+ * that distinction is unavailable to exactly the people who need it, including a test.
47
+ */
48
+ const DURABLE_HEADER = 'x-durable-work'
49
+
50
+ export async function POST(req: Request) {
51
+ const response = await corePost(req)
52
+ if (response.status !== 201) return response
53
+
54
+ // Cloned: the caller still gets core's response untouched, whatever happens below.
55
+ const started = (await response.clone().json()) as StartedRun
56
+
57
+ try {
58
+ const auth = await getAuthFromRequest(req)
59
+ if (!auth?.tenantId) {
60
+ response.headers.set(DURABLE_HEADER, 'deferred')
61
+ return response
62
+ }
63
+
64
+ const container = await createRequestContainer()
65
+ const durable = container.resolve('durableWorkService') as DurableWorkService
66
+ const runService = container.resolve('dataSyncRunService') as {
67
+ getRun(id: string, scope: unknown): Promise<RunRow | null>
68
+ }
69
+ const scope = { tenantId: auth.tenantId, organizationId: auth.orgId ?? null }
70
+
71
+ const run = await runService.getRun(started.id, { ...scope, organizationId: auth.orgId })
72
+ if (!run) {
73
+ response.headers.set(DURABLE_HEADER, 'deferred')
74
+ return response
75
+ }
76
+
77
+ await durable.startAndEnqueue(
78
+ {
79
+ kind: run.direction === 'export' ? EXPORT_KIND : IMPORT_KIND,
80
+ input: { runId: run.id, batchSize: 100, direction: run.direction === 'export' ? 'export' : 'import' },
81
+ lockKey: syncLockKey(run.integrationId, run.entityType, run.direction),
82
+ idempotencyKey: syncIdempotencyKey(run.id),
83
+ subject: { type: 'data_sync.run', id: run.id },
84
+ progressJobId: run.progressJobId ?? started.progressJobId ?? null,
85
+ createdBy: auth.sub ?? null,
86
+ },
87
+ scope,
88
+ )
89
+ response.headers.set(DURABLE_HEADER, 'created')
90
+ } catch (error) {
91
+ // Never fails the request. The run exists and core has already enqueued it, so the
92
+ // adopt-on-delivery worker will make it durable; turning that into a 500 would refuse a
93
+ // run that is going to happen anyway.
94
+ logger.warn('Could not create the durable job for a started run; the queue delivery will adopt it', {
95
+ runId: started.id,
96
+ err: error,
97
+ })
98
+ response.headers.set(DURABLE_HEADER, 'deferred')
99
+ response.headers.set(`${DURABLE_HEADER}-reason`, error instanceof Error ? error.message.slice(0, 200) : 'unknown')
100
+ }
101
+
102
+ return response
103
+ }
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/runs/[id]/cancel.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'
4
+ export { POST } from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/runs/[id]/cancel'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },
10
+ }
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/runs/[id]/retry.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/runs/[id]/retry'
4
+ export { POST } from '@open-mercato/core/modules/data_sync/api/runs/[id]/retry'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/runs/[id]/retry'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },
10
+ }
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/runs/[id]/route.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/runs/[id]/route'
4
+ export { GET } from '@open-mercato/core/modules/data_sync/api/runs/[id]/route'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/runs/[id]/route'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },
10
+ }
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/runs.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/runs'
4
+ export { GET } from '@open-mercato/core/modules/data_sync/api/runs'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/runs'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },
10
+ }
@@ -0,0 +1,12 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/schedules/[id]/route.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/schedules/[id]/route'
4
+ export { GET, PUT, DELETE } from '@open-mercato/core/modules/data_sync/api/schedules/[id]/route'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/schedules/[id]/route'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
10
+ PUT: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
11
+ DELETE: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
12
+ }
@@ -0,0 +1,11 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/schedules/route.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/schedules/route'
4
+ export { GET, POST } from '@open-mercato/core/modules/data_sync/api/schedules/route'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/schedules/route'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
10
+ POST: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
11
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/schedules/serialize.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/schedules/serialize'
4
+ export { serializeSchedule } from '@open-mercato/core/modules/data_sync/api/schedules/serialize'
@@ -0,0 +1,10 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (api/validate.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/api/validate'
4
+ export { POST } from '@open-mercato/core/modules/data_sync/api/validate'
5
+ import { openApi as coreOpenApi } from '@open-mercato/core/modules/data_sync/api/validate'
6
+ export const openApi = coreOpenApi
7
+ // Copied from core: the generator reads this by AST and cannot follow a re-export.
8
+ export const metadata = {
9
+ POST: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
10
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (backend/data-sync/page.meta.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/backend/data-sync/page.meta'
4
+ export { metadata } from '@open-mercato/core/modules/data_sync/backend/data-sync/page.meta'
@@ -0,0 +1,5 @@
1
+ 'use client'
2
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (backend/data-sync/page.tsx). Do not edit.
3
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
4
+ export * from '@open-mercato/core/modules/data_sync/backend/data-sync/page'
5
+ export { default } from '@open-mercato/core/modules/data_sync/backend/data-sync/page'
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (backend/data-sync/runs/[id]/page.meta.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/backend/data-sync/runs/[id]/page.meta'
4
+ export { metadata } from '@open-mercato/core/modules/data_sync/backend/data-sync/runs/[id]/page.meta'
@@ -0,0 +1,5 @@
1
+ 'use client'
2
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (backend/data-sync/runs/[id]/page.tsx). Do not edit.
3
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
4
+ export * from '@open-mercato/core/modules/data_sync/backend/data-sync/runs/[id]/page'
5
+ export { default } from '@open-mercato/core/modules/data_sync/backend/data-sync/runs/[id]/page'
@@ -0,0 +1,6 @@
1
+ 'use client'
2
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (components/IntegrationScheduleTab.tsx). Do not edit.
3
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
4
+ export * from '@open-mercato/core/modules/data_sync/components/IntegrationScheduleTab'
5
+ export { default } from '@open-mercato/core/modules/data_sync/components/IntegrationScheduleTab'
6
+ export { IntegrationScheduleTab } from '@open-mercato/core/modules/data_sync/components/IntegrationScheduleTab'
@@ -0,0 +1,5 @@
1
+ 'use client'
2
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (components/RunParameterFields.tsx). Do not edit.
3
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
4
+ export * from '@open-mercato/core/modules/data_sync/components/RunParameterFields'
5
+ export { RunParameterFields, buildDefaultRunParameterValues, buildRetryFailureMessage, buildRunFailureMessage, buildRunParametersPayload, hasRequiredRunParameterWithoutDefault, resolveRunParameterText } from '@open-mercato/core/modules/data_sync/components/RunParameterFields'
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (data/entities.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/data/entities'
4
+ export { SyncCursor, SyncMapping, SyncRun, SyncSchedule } from '@open-mercato/core/modules/data_sync/data/entities'
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (data/validators.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/data/validators'
4
+ export { createSyncScheduleSchema, listSyncRunsQuerySchema, listSyncSchedulesQuerySchema, retrySyncSchema, runSyncSchema, updateSyncScheduleSchema, validateConnectionSchema } from '@open-mercato/core/modules/data_sync/data/validators'
@@ -0,0 +1,57 @@
1
+ // Core's DI, plus the two job kinds a run becomes.
2
+ //
3
+ // Core's registrations are reused as they are — the engine, the run service, the mapping and
4
+ // schedule services are all core's, unchanged. The decoration that makes a run durable happens
5
+ // per slice, inside the kind, not here (see docs/adr/0004): a container-level decoration would
6
+ // apply to every caller of the engine, including ones not running under a lease.
7
+
8
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
9
+ import type { EntityManager } from '@mikro-orm/postgresql'
10
+ import { register as registerCore } from '@open-mercato/core/modules/data_sync/di'
11
+ import { createSyncEngine } from '@open-mercato/core/modules/data_sync/lib/sync-engine'
12
+ import { registry } from '@fullstackhouse/open-mercato-durable-work'
13
+
14
+ import { dataSyncKinds } from '../../kinds/data-sync-run'
15
+
16
+ /**
17
+ * Built once, at module scope, not once per container.
18
+ *
19
+ * `register` runs on every container build — which in a web process means every request — and
20
+ * the registry refuses two different handlers under one kind id, because silently accepting
21
+ * the second would make "which code is running this job?" depend on request ordering. Kinds
22
+ * created per call would be a different handler every time, so the second request would throw.
23
+ *
24
+ * Each slice resolves its own container instead, which it needs anyway: slices run in the
25
+ * worker process, outside any request, and each needs its own EntityManager rather than one
26
+ * captured from whichever request happened to build the registry first.
27
+ */
28
+ const KINDS = dataSyncKinds({
29
+ resolve: async () => {
30
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
31
+ const container = await createRequestContainer()
32
+ const em = container.resolve<EntityManager>('em')
33
+ return {
34
+ runService: container.resolve('dataSyncRunService'),
35
+ progressService: container.resolve('progressService'),
36
+ engine: ({ runService, progressService }) =>
37
+ createSyncEngine({
38
+ em,
39
+ syncRunService: runService as never,
40
+ integrationCredentialsService: container.resolve('integrationCredentialsService'),
41
+ integrationLogService: container.resolve('integrationLogService'),
42
+ integrationStateService: container.resolve('integrationStateService'),
43
+ progressService: progressService as never,
44
+ }),
45
+ }
46
+ },
47
+ })
48
+
49
+ export function register(container: AppContainer) {
50
+ registerCore(container)
51
+ // Idempotent: the same handler objects every time, so re-registering is a no-op. The web
52
+ // process needs them to re-drive and cancel, the worker to run slices, and the reconciler to
53
+ // know whether a job has a handler at all.
54
+ for (const kind of KINDS) registry.register(kind)
55
+ }
56
+
57
+ export default register
@@ -0,0 +1,5 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (events.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/events'
4
+ export { default } from '@open-mercato/core/modules/data_sync/events'
5
+ export { emitDataSyncEvent, eventsConfig } from '@open-mercato/core/modules/data_sync/events'
@@ -0,0 +1,5 @@
1
+ // GENERATED by scripts/gen-mirror.mjs from @open-mercato/core@0.7.0 (extension-points.ts). Do not edit.
2
+ // This package mirrors core's data_sync module file-for-file; see docs/adr/0006-drop-in-data-sync.md.
3
+ export * from '@open-mercato/core/modules/data_sync/extension-points'
4
+ export { default } from '@open-mercato/core/modules/data_sync/extension-points'
5
+ export { extensionPoints } from '@open-mercato/core/modules/data_sync/extension-points'