create-mercato-app 0.6.3-develop.3901.1.ddad60693a → 0.6.3
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/agentic/shared/AGENTS.md.template +1 -0
- package/dist/agentic/shared/AGENTS.md.template +1 -0
- package/package.json +2 -3
- package/template/AGENTS.md +2 -2
- package/template/scripts/dev-cache-purge.mjs +14 -5
- package/template/scripts/dev-reset.mjs +4 -3
- package/template/scripts/dev-runtime.mjs +146 -12
- package/template/scripts/dev.mjs +39 -8
- package/template/src/app/globals.css +0 -1
- package/template/src/components/ClientBootstrap.tsx +52 -26
- package/template/src/i18n/de.json +1 -0
- package/template/src/i18n/en.json +1 -0
- package/template/src/i18n/es.json +1 -0
- package/template/src/i18n/pl.json +1 -0
- package/template/src/modules/example_customers_sync/lib/runtime.ts +27 -0
- package/template/src/modules/example_customers_sync/lib/sync.ts +19 -4
|
@@ -201,6 +201,7 @@ When the user asks to **create a new application** or a **new module**, do not i
|
|
|
201
201
|
- Entity classes belong in `src/modules/<module>/data/entities.ts` and use decorators from `@mikro-orm/decorators/legacy`
|
|
202
202
|
- Standalone apps expose `yarn mercato configs cache ...` because the template enables the `configs` module from `@open-mercato/core`
|
|
203
203
|
- `yarn generate` automatically runs a best-effort structural cache purge (`yarn mercato configs cache structural --all-tenants`) after successful generation; if the cache command is unavailable, generation still succeeds
|
|
204
|
+
- Use `yarn dev:reset` only as the stale-Turbopack escape hatch; it clears `.mercato/next/dev` plus legacy `.next` caches.
|
|
204
205
|
- Detail/read-model APIs that expose `customFields` MUST return bare field keys via `normalizeCustomFieldResponse()` (for example `{ priority: 3 }`). Keep `cf_` / `cf:` prefixes for request payloads, filters, and form field IDs only.
|
|
205
206
|
- Sidebar icons MUST use `lucide-react` components — never inline SVG via `React.createElement`
|
|
206
207
|
- `page.meta.ts` MUST include `pageGroup`, `pageGroupKey`, and `pageOrder` for sidebar grouping
|
|
@@ -201,6 +201,7 @@ When the user asks to **create a new application** or a **new module**, do not i
|
|
|
201
201
|
- Entity classes belong in `src/modules/<module>/data/entities.ts` and use decorators from `@mikro-orm/decorators/legacy`
|
|
202
202
|
- Standalone apps expose `yarn mercato configs cache ...` because the template enables the `configs` module from `@open-mercato/core`
|
|
203
203
|
- `yarn generate` automatically runs a best-effort structural cache purge (`yarn mercato configs cache structural --all-tenants`) after successful generation; if the cache command is unavailable, generation still succeeds
|
|
204
|
+
- Use `yarn dev:reset` only as the stale-Turbopack escape hatch; it clears `.mercato/next/dev` plus legacy `.next` caches.
|
|
204
205
|
- Detail/read-model APIs that expose `customFields` MUST return bare field keys via `normalizeCustomFieldResponse()` (for example `{ priority: 3 }`). Keep `cf_` / `cf:` prefixes for request payloads, filters, and form field IDs only.
|
|
205
206
|
- Sidebar icons MUST use `lucide-react` components — never inline SVG via `React.createElement`
|
|
206
207
|
- `page.meta.ts` MUST include `pageGroup`, `pageGroupKey`, and `pageOrder` for sidebar grouping
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-mercato-app",
|
|
3
|
-
"version": "0.6.3
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create a new Open Mercato application",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -41,6 +41,5 @@
|
|
|
41
41
|
"scaffolding",
|
|
42
42
|
"cli"
|
|
43
43
|
],
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
"stableVersion": "0.6.2"
|
|
44
|
+
"license": "MIT"
|
|
46
45
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -99,7 +99,7 @@ yarn generate
|
|
|
99
99
|
# Manually purge structural caches when needed (Redis nav:* + Turbopack barrel mtimes)
|
|
100
100
|
yarn mercato configs cache structural --all-tenants
|
|
101
101
|
|
|
102
|
-
# Escape hatch: clear .next/
|
|
102
|
+
# Escape hatch: clear .mercato/next/dev and legacy .next caches when Turbopack still serves a stale chunk
|
|
103
103
|
yarn dev:reset
|
|
104
104
|
|
|
105
105
|
# Database operations
|
|
@@ -650,7 +650,7 @@ Notes:
|
|
|
650
650
|
|
|
651
651
|
The standalone template enables the `configs` module from `@open-mercato/core`, so `yarn mercato configs cache ...` is available here after installation. After structural changes such as enabling or disabling modules, adding or removing backend/frontend pages, or changing sidebar/navigation injections, run `yarn generate`. The generator now performs a best-effort structural cache purge automatically after successful generation; if the cache command is unavailable, generation still succeeds.
|
|
652
652
|
|
|
653
|
-
The structural cache purge invalidates two layers: Redis `nav:*` cache keys and Turbopack's module-graph fingerprints (it bumps mtimes on every file in `.mercato/generated/` without changing content). When Turbopack still serves a stale compiled chunk after a structural change — typically because its own internal cache pinned a previous compile error — run `yarn dev:reset` to clear `.next/
|
|
653
|
+
The structural cache purge invalidates two layers: Redis `nav:*` cache keys and Turbopack's module-graph fingerprints (it bumps mtimes on every file in `.mercato/generated/` without changing content). When Turbopack still serves a stale compiled chunk after a structural change — typically because its own internal cache pinned a previous compile error — run `yarn dev:reset` to clear `.mercato/next/dev` plus legacy `.next` caches and restart `yarn dev`.
|
|
654
654
|
|
|
655
655
|
Detail/read-model APIs that expose `customFields` must return bare field keys via `normalizeCustomFieldResponse()` (for example `{ priority: 3 }`). Keep `cf_` / `cf:` prefixes for request payloads, filters, and form field IDs only.
|
|
656
656
|
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
// Greenfield must not inherit
|
|
5
|
-
// configured Next.js distDir
|
|
6
|
-
// and
|
|
4
|
+
// Greenfield must not inherit stale route manifests, but wiping the whole
|
|
5
|
+
// configured Next.js distDir also discards Turbopack's reusable compiler cache
|
|
6
|
+
// and makes first /login warmup much slower. Remove manifests/locks that encode
|
|
7
|
+
// route shape while preserving `.mercato/next/dev/cache/turbopack`.
|
|
7
8
|
export const GREENFIELD_PURGE_TARGETS = Object.freeze([
|
|
8
|
-
Object.freeze(['.mercato', 'next']),
|
|
9
|
+
Object.freeze(['.mercato', 'next', 'dev', 'lock']),
|
|
10
|
+
Object.freeze(['.mercato', 'next', 'dev', 'build-manifest.json']),
|
|
11
|
+
Object.freeze(['.mercato', 'next', 'dev', 'fallback-build-manifest.json']),
|
|
12
|
+
Object.freeze(['.mercato', 'next', 'dev', 'prerender-manifest.json']),
|
|
13
|
+
Object.freeze(['.mercato', 'next', 'dev', 'routes-manifest.json']),
|
|
14
|
+
Object.freeze(['.mercato', 'next', 'dev', 'server', 'app-paths-manifest.json']),
|
|
15
|
+
Object.freeze(['.mercato', 'next', 'dev', 'server', 'middleware-build-manifest.js']),
|
|
16
|
+
Object.freeze(['.mercato', 'next', 'dev', 'server', 'middleware-manifest.json']),
|
|
17
|
+
Object.freeze(['.mercato', 'next', 'dev', 'server', 'pages-manifest.json']),
|
|
9
18
|
Object.freeze(['.next']),
|
|
10
19
|
])
|
|
11
20
|
|
|
@@ -23,7 +32,7 @@ export function purgeAppBuildCaches({
|
|
|
23
32
|
removed.push(segments.join('/'))
|
|
24
33
|
}
|
|
25
34
|
if (removed.length === 0) {
|
|
26
|
-
logger.log('🧹 [dev:greenfield] no stale Next/Turbopack
|
|
35
|
+
logger.log('🧹 [dev:greenfield] no stale Next/Turbopack manifest files to purge')
|
|
27
36
|
} else {
|
|
28
37
|
for (const relPath of removed) {
|
|
29
38
|
logger.log(`🧹 [dev:greenfield] removed ${relPath}`)
|
|
@@ -6,6 +6,7 @@ import fs from 'node:fs'
|
|
|
6
6
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
7
7
|
const appDir = path.resolve(here, '..')
|
|
8
8
|
const targets = [
|
|
9
|
+
path.join(appDir, '.mercato', 'next', 'dev'),
|
|
9
10
|
path.join(appDir, '.next', 'cache', 'turbopack'),
|
|
10
11
|
path.join(appDir, '.next', 'cache', 'webpack'),
|
|
11
12
|
]
|
|
@@ -19,9 +20,9 @@ for (const target of targets) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
if (removed === 0) {
|
|
22
|
-
console.log('🧹 [dev:reset] nothing to clean —
|
|
23
|
+
console.log('🧹 [dev:reset] nothing to clean — dev build caches already absent')
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
console.log('')
|
|
26
|
-
console.log('✅
|
|
27
|
-
console.log('
|
|
27
|
+
console.log('✅ Next.js dev cache cleared.')
|
|
28
|
+
console.log(' Start `yarn dev` again to pick up fresh module output.')
|
|
@@ -50,6 +50,12 @@ function parseEnvBooleanToken(value) {
|
|
|
50
50
|
return null
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function parsePositiveIntegerEnv(value) {
|
|
54
|
+
if (typeof value !== 'string') return null
|
|
55
|
+
const parsed = Number.parseInt(value.trim(), 10)
|
|
56
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
function resolveAutoSpawnEnabled(env, legacyName, aliasedName) {
|
|
54
60
|
const legacy = parseEnvBooleanToken(env[legacyName])
|
|
55
61
|
if (legacy !== null) return legacy
|
|
@@ -84,12 +90,20 @@ const verbose = !classic && (process.argv.includes('--verbose') || process.env.M
|
|
|
84
90
|
const rawPassthrough = classic || verbose
|
|
85
91
|
const interactiveLogToggle = !rawPassthrough && process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== 'true'
|
|
86
92
|
const splashChildStateFile = process.env.OM_DEV_SPLASH_CHILD_STATE_FILE?.trim() || null
|
|
93
|
+
const warmupReadyFile = process.env.OM_DEV_WARMUP_READY_FILE?.trim()
|
|
94
|
+
|| (splashChildStateFile ? `${splashChildStateFile}.warmup-ready` : null)
|
|
87
95
|
const splashMode = process.env.OM_DEV_SPLASH_MODE?.trim() || 'dev'
|
|
88
96
|
const setupSplashMode = splashMode === 'setup'
|
|
89
97
|
const startupSplashPhase = setupSplashMode ? 'Project setup is in progress...' : 'Installation and first compilation is in progress...'
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
98
|
+
const configuredRuntimeProgressTotal = parsePositiveIntegerEnv(process.env.OM_DEV_SPLASH_STAGE_TOTAL)
|
|
99
|
+
const configuredRuntimeProgressCurrent = parsePositiveIntegerEnv(process.env.OM_DEV_SPLASH_STAGE_CURRENT)
|
|
100
|
+
const runtimeProgressTotal = configuredRuntimeProgressTotal ?? (setupSplashMode ? 5 : 4)
|
|
101
|
+
const runtimeProgressCurrent = configuredRuntimeProgressCurrent ?? (setupSplashMode ? 4 : 0)
|
|
102
|
+
const runtimeReadyProgressCurrent = Math.max(runtimeProgressCurrent, runtimeProgressTotal)
|
|
103
|
+
const runtimeWarmupProgressCurrent = Math.max(
|
|
104
|
+
runtimeProgressCurrent,
|
|
105
|
+
Math.min(runtimeReadyProgressCurrent, Math.max(0, runtimeProgressTotal - 1)),
|
|
106
|
+
)
|
|
93
107
|
const children = new Set()
|
|
94
108
|
let shuttingDown = false
|
|
95
109
|
let logsVisible = false
|
|
@@ -108,6 +122,7 @@ const backgroundServiceModes = {
|
|
|
108
122
|
workers: resolveAutoSpawnMode(process.env, 'AUTO_SPAWN_WORKERS', 'OM_AUTO_SPAWN_WORKERS', 'OM_AUTO_SPAWN_WORKERS_LAZY'),
|
|
109
123
|
scheduler: resolveAutoSpawnMode(process.env, 'AUTO_SPAWN_SCHEDULER', 'OM_AUTO_SPAWN_SCHEDULER', 'OM_AUTO_SPAWN_SCHEDULER_LAZY'),
|
|
110
124
|
}
|
|
125
|
+
const shutdownNoticeOwnedByParent = process.env.OM_DEV_SHUTDOWN_NOTICE_OWNER === 'parent'
|
|
111
126
|
const splashState = {
|
|
112
127
|
mode: splashMode,
|
|
113
128
|
phase: startupSplashPhase,
|
|
@@ -161,11 +176,38 @@ const runtimeWarmupState = {
|
|
|
161
176
|
failed: false,
|
|
162
177
|
promise: null,
|
|
163
178
|
retryTimer: null,
|
|
179
|
+
abortController: null,
|
|
180
|
+
generation: 0,
|
|
164
181
|
retryAttempts: 0,
|
|
165
182
|
tenantId: readNonEmptyEnvValue('OM_DEV_WARMUP_TENANT_ID') ?? null,
|
|
166
183
|
tenantLookupAttempted: false,
|
|
167
184
|
}
|
|
168
185
|
|
|
186
|
+
function clearWarmupReadyFile() {
|
|
187
|
+
if (!warmupReadyFile) return
|
|
188
|
+
try {
|
|
189
|
+
fs.rmSync(warmupReadyFile, { force: true })
|
|
190
|
+
} catch {
|
|
191
|
+
// Warmup readiness is best-effort; terminal status remains authoritative.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function writeWarmupReadyFile(reason) {
|
|
196
|
+
if (!warmupReadyFile) return
|
|
197
|
+
try {
|
|
198
|
+
fs.mkdirSync(path.dirname(warmupReadyFile), { recursive: true })
|
|
199
|
+
fs.writeFileSync(warmupReadyFile, `${JSON.stringify({
|
|
200
|
+
ready: true,
|
|
201
|
+
reason,
|
|
202
|
+
at: new Date().toISOString(),
|
|
203
|
+
}, null, 2)}\n`)
|
|
204
|
+
} catch {
|
|
205
|
+
// Warmup readiness is best-effort; background services can still run without it.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
clearWarmupReadyFile()
|
|
210
|
+
|
|
169
211
|
function printCompactSummary(icon, title, lines) {
|
|
170
212
|
if (!Array.isArray(lines) || lines.length === 0) return
|
|
171
213
|
console.log(`${icon} ${title}`)
|
|
@@ -489,6 +531,8 @@ function spawnMercato(args) {
|
|
|
489
531
|
...process.env,
|
|
490
532
|
OM_CLI_QUIET: rawPassthrough ? process.env.OM_CLI_QUIET : '1',
|
|
491
533
|
DOTENV_CONFIG_QUIET: rawPassthrough ? process.env.DOTENV_CONFIG_QUIET : 'true',
|
|
534
|
+
...(!rawPassthrough ? { OM_DEV_SPLASH_RUNTIME_WRAPPER: '1' } : {}),
|
|
535
|
+
...(!rawPassthrough && warmupReadyFile ? { OM_DEV_WARMUP_READY_FILE: warmupReadyFile } : {}),
|
|
492
536
|
},
|
|
493
537
|
...resolvedSpawn.spawnOptions,
|
|
494
538
|
})
|
|
@@ -649,10 +693,18 @@ async function resolveWarmupTenantIdFromDatabase(email) {
|
|
|
649
693
|
}
|
|
650
694
|
}
|
|
651
695
|
|
|
652
|
-
async function fetchWithTimeout(url, init = {}, timeoutMs = 45000) {
|
|
696
|
+
async function fetchWithTimeout(url, init = {}, timeoutMs = 45000, externalSignal = null) {
|
|
697
|
+
if (externalSignal?.aborted) {
|
|
698
|
+
throw externalSignal.reason ?? new Error('warmup request aborted')
|
|
699
|
+
}
|
|
700
|
+
|
|
653
701
|
const controller = new AbortController()
|
|
654
702
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
655
703
|
timer.unref?.()
|
|
704
|
+
const abortFromExternalSignal = () => {
|
|
705
|
+
controller.abort(externalSignal.reason ?? new Error('warmup request aborted'))
|
|
706
|
+
}
|
|
707
|
+
externalSignal?.addEventListener?.('abort', abortFromExternalSignal, { once: true })
|
|
656
708
|
|
|
657
709
|
try {
|
|
658
710
|
return await fetch(url, {
|
|
@@ -661,6 +713,7 @@ async function fetchWithTimeout(url, init = {}, timeoutMs = 45000) {
|
|
|
661
713
|
})
|
|
662
714
|
} finally {
|
|
663
715
|
clearTimeout(timer)
|
|
716
|
+
externalSignal?.removeEventListener?.('abort', abortFromExternalSignal)
|
|
664
717
|
}
|
|
665
718
|
}
|
|
666
719
|
|
|
@@ -674,14 +727,14 @@ function isAbortLikeError(error) {
|
|
|
674
727
|
return /aborted/i.test(String(error))
|
|
675
728
|
}
|
|
676
729
|
|
|
677
|
-
async function fetchWarmupWithRetry(url, init, detailLabel, progressLabel) {
|
|
730
|
+
async function fetchWarmupWithRetry(url, init, detailLabel, progressLabel, signal = null) {
|
|
678
731
|
let lastError = null
|
|
679
732
|
|
|
680
733
|
for (let index = 0; index < warmupRequestTimeoutsMs.length; index += 1) {
|
|
681
734
|
const timeoutMs = warmupRequestTimeoutsMs[index]
|
|
682
735
|
|
|
683
736
|
try {
|
|
684
|
-
return await fetchWithTimeout(url, init, timeoutMs)
|
|
737
|
+
return await fetchWithTimeout(url, init, timeoutMs, signal)
|
|
685
738
|
} catch (error) {
|
|
686
739
|
lastError = error
|
|
687
740
|
|
|
@@ -766,6 +819,20 @@ function clearWarmupRetryTimer() {
|
|
|
766
819
|
runtimeWarmupState.retryTimer = null
|
|
767
820
|
}
|
|
768
821
|
|
|
822
|
+
function resetWarmupForRuntimeRestart(reason) {
|
|
823
|
+
clearWarmupRetryTimer()
|
|
824
|
+
runtimeWarmupState.generation += 1
|
|
825
|
+
runtimeWarmupState.readySignalSeen = false
|
|
826
|
+
runtimeWarmupState.started = false
|
|
827
|
+
runtimeWarmupState.completed = false
|
|
828
|
+
runtimeWarmupState.failed = false
|
|
829
|
+
runtimeWarmupState.promise = null
|
|
830
|
+
runtimeWarmupState.retryAttempts = 0
|
|
831
|
+
runtimeWarmupState.abortController?.abort(new Error(`warmup aborted because ${reason}`))
|
|
832
|
+
runtimeWarmupState.abortController = null
|
|
833
|
+
clearWarmupReadyFile()
|
|
834
|
+
}
|
|
835
|
+
|
|
769
836
|
function scheduleWarmupRetry(delayMs = 2000) {
|
|
770
837
|
clearWarmupRetryTimer()
|
|
771
838
|
runtimeWarmupState.retryTimer = setTimeout(() => {
|
|
@@ -782,6 +849,9 @@ async function runTargetedRouteWarmup() {
|
|
|
782
849
|
|
|
783
850
|
clearWarmupRetryTimer()
|
|
784
851
|
runtimeWarmupState.started = true
|
|
852
|
+
const generation = runtimeWarmupState.generation
|
|
853
|
+
const abortController = new AbortController()
|
|
854
|
+
runtimeWarmupState.abortController = abortController
|
|
785
855
|
const startedAt = Date.now()
|
|
786
856
|
const progressLabel = 'Precompiling login and backend'
|
|
787
857
|
const introMessage = '🔥 Precompiling /login, login POST, and /backend'
|
|
@@ -796,7 +866,9 @@ async function runTargetedRouteWarmup() {
|
|
|
796
866
|
{ method: 'GET', redirect: 'manual' },
|
|
797
867
|
'/login',
|
|
798
868
|
progressLabel,
|
|
869
|
+
abortController.signal,
|
|
799
870
|
)
|
|
871
|
+
if (generation !== runtimeWarmupState.generation) return
|
|
800
872
|
if (shouldRetryWarmupStatus(loginPageResponse.status)) {
|
|
801
873
|
throw createWarmupTransientError(`/login returned HTTP ${loginPageResponse.status}`)
|
|
802
874
|
}
|
|
@@ -825,7 +897,9 @@ async function runTargetedRouteWarmup() {
|
|
|
825
897
|
},
|
|
826
898
|
'POST /api/auth/login',
|
|
827
899
|
progressLabel,
|
|
900
|
+
abortController.signal,
|
|
828
901
|
)
|
|
902
|
+
if (generation !== runtimeWarmupState.generation) return
|
|
829
903
|
const loginPayload = await readResponsePayload(loginResponse)
|
|
830
904
|
if (!loginResponse.ok || !loginPayload || typeof loginPayload !== 'object' || loginPayload.ok !== true) {
|
|
831
905
|
const failure = extractWarmupErrorMessage(loginPayload, `HTTP ${loginResponse.status}`)
|
|
@@ -864,7 +938,9 @@ async function runTargetedRouteWarmup() {
|
|
|
864
938
|
},
|
|
865
939
|
'/backend',
|
|
866
940
|
progressLabel,
|
|
941
|
+
abortController.signal,
|
|
867
942
|
)
|
|
943
|
+
if (generation !== runtimeWarmupState.generation) return
|
|
868
944
|
if (backendResponse.status >= 300 && backendResponse.status < 400) {
|
|
869
945
|
const location = backendResponse.headers.get('location') || 'redirect'
|
|
870
946
|
if (isWarmupRetryableRedirect(location)) {
|
|
@@ -888,6 +964,7 @@ async function runTargetedRouteWarmup() {
|
|
|
888
964
|
runtimeWarmupState.completed = true
|
|
889
965
|
runtimeWarmupState.failed = false
|
|
890
966
|
runtimeWarmupState.promise = null
|
|
967
|
+
runtimeWarmupState.abortController = null
|
|
891
968
|
const completedMessage = `🚪 Login flow and backend warmed in ${formatDuration(Date.now() - startedAt)}`
|
|
892
969
|
updateSplashState({
|
|
893
970
|
phase: 'App is ready',
|
|
@@ -902,9 +979,15 @@ async function runTargetedRouteWarmup() {
|
|
|
902
979
|
progressLabel: 'App is ready',
|
|
903
980
|
activity: completedMessage,
|
|
904
981
|
})
|
|
982
|
+
writeWarmupReadyFile('warmup-complete')
|
|
905
983
|
console.log(formatStatusOutput(completedMessage, runtimeReadyProgressCurrent, 'App is ready'))
|
|
906
984
|
} catch (error) {
|
|
985
|
+
if (generation !== runtimeWarmupState.generation) {
|
|
986
|
+
return
|
|
987
|
+
}
|
|
988
|
+
|
|
907
989
|
runtimeWarmupState.promise = null
|
|
990
|
+
runtimeWarmupState.abortController = null
|
|
908
991
|
|
|
909
992
|
if (isAbortLikeError(error) || isWarmupTransientError(error)) {
|
|
910
993
|
runtimeWarmupState.started = false
|
|
@@ -978,12 +1061,14 @@ async function runTargetedRouteWarmup() {
|
|
|
978
1061
|
activity: warmupWarning,
|
|
979
1062
|
})
|
|
980
1063
|
if (isCredentialsFailure) {
|
|
1064
|
+
writeWarmupReadyFile('warmup-credentials-failed')
|
|
981
1065
|
console.log(formatStatusOutput(
|
|
982
1066
|
'⚠️ Warmup login returned 401 — credentials invalid. Set OM_INIT_SUPERADMIN_EMAIL/PASSWORD in .env or run: yarn initialize',
|
|
983
1067
|
runtimeReadyProgressCurrent,
|
|
984
1068
|
'App is ready',
|
|
985
1069
|
))
|
|
986
1070
|
} else {
|
|
1071
|
+
writeWarmupReadyFile('warmup-incomplete')
|
|
987
1072
|
console.log(formatStatusOutput(warmupWarning, runtimeReadyProgressCurrent, 'App is ready'))
|
|
988
1073
|
}
|
|
989
1074
|
}
|
|
@@ -1138,6 +1223,18 @@ function shutdown(exitCode = 0) {
|
|
|
1138
1223
|
rawModeEnabled = false
|
|
1139
1224
|
}
|
|
1140
1225
|
|
|
1226
|
+
if (!shutdownNoticeOwnedByParent) {
|
|
1227
|
+
const message = 'Shutting down services...'
|
|
1228
|
+
updateSplashState({
|
|
1229
|
+
phase: message,
|
|
1230
|
+
detail: 'Stopping app runtime, workers, and scheduler',
|
|
1231
|
+
ready: false,
|
|
1232
|
+
progressLabel: message,
|
|
1233
|
+
activity: message,
|
|
1234
|
+
})
|
|
1235
|
+
console.log(message)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1141
1238
|
const alive = Array.from(children).filter((child) => !child.killed)
|
|
1142
1239
|
if (alive.length === 0) {
|
|
1143
1240
|
process.exit(exitCode)
|
|
@@ -1582,6 +1679,37 @@ function classifyServerLine(line) {
|
|
|
1582
1679
|
}
|
|
1583
1680
|
}
|
|
1584
1681
|
|
|
1682
|
+
const runtimeRestartMatch = line.match(/^\[server\] Detected (.+?)\. Restarting app runtime\.\.\.$/)
|
|
1683
|
+
if (runtimeRestartMatch) {
|
|
1684
|
+
const reason = runtimeRestartMatch[1]
|
|
1685
|
+
resetWarmupForRuntimeRestart(reason)
|
|
1686
|
+
return {
|
|
1687
|
+
type: 'status',
|
|
1688
|
+
message: `🔄 Restarting app runtime: ${reason}`,
|
|
1689
|
+
splashPhase: 'App runtime is restarting',
|
|
1690
|
+
splashDetail: `Reason: ${reason}`,
|
|
1691
|
+
ready: false,
|
|
1692
|
+
activity: `App runtime restart: ${reason}`,
|
|
1693
|
+
progressCurrent: runtimeProgressCurrent,
|
|
1694
|
+
progressLabel: 'Restarting app runtime',
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (line === '[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...') {
|
|
1699
|
+
const reason = 'corrupted Turbopack dev cache'
|
|
1700
|
+
resetWarmupForRuntimeRestart(reason)
|
|
1701
|
+
return {
|
|
1702
|
+
type: 'status',
|
|
1703
|
+
message: `🔄 Restarting Next.js dev server: ${reason}`,
|
|
1704
|
+
splashPhase: 'App runtime is restarting',
|
|
1705
|
+
splashDetail: `Reason: ${reason}`,
|
|
1706
|
+
ready: false,
|
|
1707
|
+
activity: `Next.js restart: ${reason}`,
|
|
1708
|
+
progressCurrent: runtimeProgressCurrent,
|
|
1709
|
+
progressLabel: 'Restarting app runtime',
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1585
1713
|
const localMatch = line.match(/^- Local:\s*(.+)$/)
|
|
1586
1714
|
if (localMatch) {
|
|
1587
1715
|
return {
|
|
@@ -1592,7 +1720,7 @@ function classifyServerLine(line) {
|
|
|
1592
1720
|
readyUrl: localMatch[1],
|
|
1593
1721
|
loginUrl: `${localMatch[1].replace(/\/$/, '')}/login`,
|
|
1594
1722
|
activity: `App runtime at ${localMatch[1]}`,
|
|
1595
|
-
progressCurrent:
|
|
1723
|
+
progressCurrent: runtimeWarmupProgressCurrent,
|
|
1596
1724
|
progressLabel: 'Precompiling login page',
|
|
1597
1725
|
}
|
|
1598
1726
|
}
|
|
@@ -1607,7 +1735,7 @@ function classifyServerLine(line) {
|
|
|
1607
1735
|
ready: false,
|
|
1608
1736
|
runtimeReady: true,
|
|
1609
1737
|
activity: timing,
|
|
1610
|
-
progressCurrent:
|
|
1738
|
+
progressCurrent: runtimeWarmupProgressCurrent,
|
|
1611
1739
|
progressLabel: 'Precompiling login page',
|
|
1612
1740
|
}
|
|
1613
1741
|
}
|
|
@@ -1616,7 +1744,7 @@ function classifyServerLine(line) {
|
|
|
1616
1744
|
const target = compiledMatch[1]?.trim()
|
|
1617
1745
|
const detail = target ? ` ${target}` : ''
|
|
1618
1746
|
const timing = `⚡ Compiled${detail} in ${parseDurationToken(compiledMatch[2])}`
|
|
1619
|
-
const progressCurrent = splashState.ready ? runtimeReadyProgressCurrent :
|
|
1747
|
+
const progressCurrent = splashState.ready ? runtimeReadyProgressCurrent : runtimeWarmupProgressCurrent
|
|
1620
1748
|
return {
|
|
1621
1749
|
type: 'status',
|
|
1622
1750
|
message: timing,
|
|
@@ -1721,9 +1849,10 @@ async function runClassicRuntime() {
|
|
|
1721
1849
|
shutdown(initialGenerateExitCode)
|
|
1722
1850
|
}
|
|
1723
1851
|
|
|
1724
|
-
// Default ('in-process'): `mercato server dev`
|
|
1725
|
-
// regeneration watcher in-process
|
|
1726
|
-
//
|
|
1852
|
+
// Default ('in-process'): `mercato server dev` owns the structural
|
|
1853
|
+
// regeneration watcher in-process, so we no longer spawn the dedicated
|
|
1854
|
+
// sidecar. Saves ~190 MB of resident RSS by collapsing one Node process.
|
|
1855
|
+
// Opt back into the sidecar with OM_DEV_GENERATE_WATCH_MODE=legacy.
|
|
1727
1856
|
const watchers = []
|
|
1728
1857
|
if (generateWatchMode === 'legacy') {
|
|
1729
1858
|
watchers.push(['Generator watch (legacy sidecar)', spawnMercato(['generate', 'watch', '--skip-initial'])])
|
|
@@ -1751,6 +1880,11 @@ installLogToggle()
|
|
|
1751
1880
|
initializeRuntimeSummary()
|
|
1752
1881
|
printRuntimePackagesSummary()
|
|
1753
1882
|
|
|
1883
|
+
// Default ('in-process'): `mercato server dev` runs the structural
|
|
1884
|
+
// regeneration watcher in-process — see packages/cli/src/lib/in-process-generate-watcher.ts
|
|
1885
|
+
// — so the orchestrator no longer spawns a dedicated `mercato generate watch`
|
|
1886
|
+
// sidecar. Saves ~190 MB of resident RSS by eliminating one Node process.
|
|
1887
|
+
// Set OM_DEV_GENERATE_WATCH_MODE=legacy to opt back into the sidecar.
|
|
1754
1888
|
const sidecarWatch = generateWatchMode === 'legacy'
|
|
1755
1889
|
? startFilteredChild(['generate', 'watch', '--skip-initial'], 'Generator watch (legacy sidecar)', classifyWatchLine)
|
|
1756
1890
|
: null
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -202,6 +202,10 @@ const splashEnabled = !classic && !appOnly && splashPortConfig.enabled
|
|
|
202
202
|
const autoOpenSplash = splashEnabled && process.stdout.isTTY && process.env.CI !== 'true' && process.env.OM_DEV_AUTO_OPEN !== '0'
|
|
203
203
|
const splashBindHost = isContainerRuntime() ? '0.0.0.0' : '127.0.0.1'
|
|
204
204
|
const standaloneRuntimeScript = path.join(process.cwd(), 'scripts', 'dev-runtime.mjs')
|
|
205
|
+
const warmupReadyFilePath = path.join(
|
|
206
|
+
process.cwd(),
|
|
207
|
+
isMonorepo ? 'apps/mercato/.mercato/dev-warmup-ready.json' : '.mercato/dev-warmup-ready.json',
|
|
208
|
+
)
|
|
205
209
|
const devLogTeeDisabled = process.env.OM_DEV_LOG_TEE === '0' || process.env.OM_DEV_LOG_TEE === 'false'
|
|
206
210
|
|
|
207
211
|
let devLogSessionInstance = null
|
|
@@ -492,7 +496,7 @@ function resolveSplashLocaleConfig() {
|
|
|
492
496
|
return splashLocaleConfig
|
|
493
497
|
}
|
|
494
498
|
|
|
495
|
-
function buildSplashChildEnv() {
|
|
499
|
+
function buildSplashChildEnv(options = {}) {
|
|
496
500
|
const childEnv = devLogTeeDisabled
|
|
497
501
|
? {}
|
|
498
502
|
: {
|
|
@@ -501,18 +505,31 @@ function buildSplashChildEnv() {
|
|
|
501
505
|
}
|
|
502
506
|
|
|
503
507
|
if (!splashChildStateFile) {
|
|
504
|
-
|
|
508
|
+
const env = {
|
|
509
|
+
...childEnv,
|
|
510
|
+
OM_DEV_SHUTDOWN_NOTICE_OWNER: 'parent',
|
|
511
|
+
}
|
|
512
|
+
return Object.keys(env).length > 0 ? env : undefined
|
|
505
513
|
}
|
|
506
514
|
|
|
507
515
|
return {
|
|
508
516
|
...childEnv,
|
|
509
517
|
OM_DEV_SPLASH_CHILD_STATE_FILE: splashChildStateFile,
|
|
518
|
+
OM_DEV_WARMUP_READY_FILE: warmupReadyFilePath,
|
|
510
519
|
OM_DEV_SPLASH_MODE: splashMode,
|
|
520
|
+
OM_DEV_SHUTDOWN_NOTICE_OWNER: 'parent',
|
|
521
|
+
...(Number.isFinite(options.stageCurrent) ? { OM_DEV_SPLASH_STAGE_CURRENT: String(options.stageCurrent) } : {}),
|
|
522
|
+
...(Number.isFinite(options.stageTotal) ? { OM_DEV_SPLASH_STAGE_TOTAL: String(options.stageTotal) } : {}),
|
|
511
523
|
}
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
function applyLocalDevBackgroundServiceDefaults(childEnv) {
|
|
515
|
-
const env =
|
|
527
|
+
const env = {
|
|
528
|
+
...(childEnv ?? {}),
|
|
529
|
+
OM_DEV_WARMUP_READY_FILE: (childEnv && 'OM_DEV_WARMUP_READY_FILE' in childEnv)
|
|
530
|
+
? childEnv.OM_DEV_WARMUP_READY_FILE
|
|
531
|
+
: warmupReadyFilePath,
|
|
532
|
+
}
|
|
516
533
|
if (
|
|
517
534
|
typeof process.env.OM_AUTO_SPAWN_WORKERS_LAZY !== 'string'
|
|
518
535
|
|| process.env.OM_AUTO_SPAWN_WORKERS_LAZY.trim() === ''
|
|
@@ -528,8 +545,8 @@ function applyLocalDevBackgroundServiceDefaults(childEnv) {
|
|
|
528
545
|
return env
|
|
529
546
|
}
|
|
530
547
|
|
|
531
|
-
function buildAppDevEnv() {
|
|
532
|
-
return applyLocalDevBackgroundServiceDefaults(buildSplashChildEnv() ?? {})
|
|
548
|
+
function buildAppDevEnv(options = {}) {
|
|
549
|
+
return applyLocalDevBackgroundServiceDefaults(buildSplashChildEnv(options) ?? {})
|
|
533
550
|
}
|
|
534
551
|
|
|
535
552
|
function launchStandaloneDev(options = {}) {
|
|
@@ -566,7 +583,7 @@ function launchStandaloneDev(options = {}) {
|
|
|
566
583
|
|
|
567
584
|
const app = spawnCommand(process.execPath, runtimeArgs, {
|
|
568
585
|
stdio: 'inherit',
|
|
569
|
-
env: buildAppDevEnv(),
|
|
586
|
+
env: buildAppDevEnv({ stageCurrent, stageTotal }),
|
|
570
587
|
})
|
|
571
588
|
|
|
572
589
|
app.on('close', (code) => {
|
|
@@ -996,6 +1013,18 @@ function closeSplashServer() {
|
|
|
996
1013
|
writeSplashChildStateFileClear()
|
|
997
1014
|
}
|
|
998
1015
|
|
|
1016
|
+
function announceShutdown() {
|
|
1017
|
+
const message = 'Shutting down services...'
|
|
1018
|
+
updateSplashState({
|
|
1019
|
+
phase: message,
|
|
1020
|
+
detail: 'Stopping app runtime, watchers, workers, and scheduler',
|
|
1021
|
+
ready: false,
|
|
1022
|
+
progressLabel: message,
|
|
1023
|
+
activity: message,
|
|
1024
|
+
})
|
|
1025
|
+
console.log(message)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
999
1028
|
function openBrowser(url) {
|
|
1000
1029
|
try {
|
|
1001
1030
|
let child
|
|
@@ -1014,10 +1043,11 @@ function openBrowser(url) {
|
|
|
1014
1043
|
function shutdown(exitCode = 0) {
|
|
1015
1044
|
if (shuttingDown) return
|
|
1016
1045
|
shuttingDown = true
|
|
1017
|
-
|
|
1046
|
+
announceShutdown()
|
|
1018
1047
|
|
|
1019
1048
|
const alive = Array.from(children).filter((child) => !child.killed)
|
|
1020
1049
|
if (alive.length === 0) {
|
|
1050
|
+
closeSplashServer()
|
|
1021
1051
|
closeDevLogSession()
|
|
1022
1052
|
process.exit(exitCode)
|
|
1023
1053
|
return
|
|
@@ -1033,6 +1063,7 @@ function shutdown(exitCode = 0) {
|
|
|
1033
1063
|
killProcessTree(child, 'SIGKILL')
|
|
1034
1064
|
}
|
|
1035
1065
|
}
|
|
1066
|
+
closeSplashServer()
|
|
1036
1067
|
closeDevLogSession()
|
|
1037
1068
|
process.exit(exitCode)
|
|
1038
1069
|
}, 3000)
|
|
@@ -1615,7 +1646,7 @@ function launchMonorepoAppDev() {
|
|
|
1615
1646
|
})
|
|
1616
1647
|
const app = spawnCommand(yarnCommand, appArgs, {
|
|
1617
1648
|
stdio: 'inherit',
|
|
1618
|
-
env: buildAppDevEnv(),
|
|
1649
|
+
env: buildAppDevEnv({ stageCurrent, stageTotal }),
|
|
1619
1650
|
})
|
|
1620
1651
|
|
|
1621
1652
|
app.on('close', (code, signal) => {
|
|
@@ -1,51 +1,77 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
|
|
5
|
-
//
|
|
4
|
+
// Side-effect imports: these register types/components on import, so they
|
|
5
|
+
// MUST stay top-level to be available during the first paint.
|
|
6
6
|
import '@/.mercato/generated/translations-fields.generated'
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import '@/.mercato/generated/messages.client.generated'
|
|
8
|
+
import '@/.mercato/generated/payments.client.generated'
|
|
9
|
+
|
|
9
10
|
import { registerCoreInjectionWidgets, registerCoreInjectionTables, registerEnabledModuleIds } from '@open-mercato/core/modules/widgets/lib/injection'
|
|
10
11
|
import { registerInjectionWidgets } from '@open-mercato/ui/backend/injection/widgetRegistry'
|
|
11
|
-
import { dashboardWidgetEntries } from '@/.mercato/generated/dashboard-widgets.generated'
|
|
12
12
|
import { registerDashboardWidgets } from '@open-mercato/ui/backend/dashboard/widgetRegistry'
|
|
13
|
-
import { notificationHandlerEntries } from '@/.mercato/generated/notification-handlers.generated'
|
|
14
13
|
import { registerNotificationHandlers } from '@open-mercato/shared/lib/notifications/handler-registry'
|
|
15
|
-
// Side-effect: registers translatable fields for client-side TranslationManager
|
|
16
|
-
import '@/.mercato/generated/translations-fields.generated'
|
|
17
|
-
// Side-effect: configures message UI component and object type registries on the client.
|
|
18
|
-
import '@/.mercato/generated/messages.client.generated'
|
|
19
|
-
// Side-effect: registers provider-owned payment renderer widgets on the client.
|
|
20
|
-
import '@/.mercato/generated/payments.client.generated'
|
|
21
14
|
|
|
22
15
|
let _clientBootstrapped = false
|
|
16
|
+
let _bootstrapPromise: Promise<void> | null = null
|
|
23
17
|
|
|
24
|
-
function clientBootstrap() {
|
|
18
|
+
async function clientBootstrap(): Promise<void> {
|
|
25
19
|
if (_clientBootstrapped) return
|
|
26
|
-
|
|
20
|
+
if (_bootstrapPromise) return _bootstrapPromise
|
|
21
|
+
|
|
22
|
+
_bootstrapPromise = (async () => {
|
|
23
|
+
try {
|
|
24
|
+
// Defer generated registry barrels to a dynamic import so each barrel
|
|
25
|
+
// becomes its own lazy chunk in Turbopack. Routes that mount this
|
|
26
|
+
// provider but never use injection/dashboard/notification registries
|
|
27
|
+
// still get the chunks compiled, but no longer during initial page
|
|
28
|
+
// parse — the first paint is no longer blocked on registering every
|
|
29
|
+
// module's client widgets.
|
|
30
|
+
const [
|
|
31
|
+
injectionWidgets,
|
|
32
|
+
injectionTables,
|
|
33
|
+
enabledModuleIds,
|
|
34
|
+
dashboardWidgets,
|
|
35
|
+
notificationHandlers,
|
|
36
|
+
] = await Promise.all([
|
|
37
|
+
import('@/.mercato/generated/injection-widgets.generated'),
|
|
38
|
+
import('@/.mercato/generated/injection-tables.generated'),
|
|
39
|
+
import('@/.mercato/generated/enabled-module-ids.generated'),
|
|
40
|
+
import('@/.mercato/generated/dashboard-widgets.generated'),
|
|
41
|
+
import('@/.mercato/generated/notification-handlers.generated'),
|
|
42
|
+
])
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
registerInjectionWidgets(injectionWidgets.injectionWidgetEntries)
|
|
45
|
+
registerCoreInjectionWidgets(injectionWidgets.injectionWidgetEntries)
|
|
46
|
+
registerCoreInjectionTables(injectionTables.injectionTables)
|
|
47
|
+
registerEnabledModuleIds(enabledModuleIds.enabledModuleIds)
|
|
48
|
+
registerDashboardWidgets(dashboardWidgets.dashboardWidgetEntries)
|
|
49
|
+
registerNotificationHandlers(notificationHandlers.notificationHandlerEntries)
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
_clientBootstrapped = true
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// A lazy registry chunk failed to load (e.g. a stale chunk after a
|
|
54
|
+
// deploy). Clear the cached promise so the next render retries instead
|
|
55
|
+
// of leaving every client registry empty forever — otherwise dashboard
|
|
56
|
+
// widget cards would wait on registration indefinitely with no error.
|
|
57
|
+
_bootstrapPromise = null
|
|
58
|
+
console.error('[ClientBootstrap] Failed to register client registries; will retry on next render', err)
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
registerNotificationHandlers(notificationHandlerEntries)
|
|
62
|
+
return _bootstrapPromise
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export function ClientBootstrapProvider({ children }: { children: React.ReactNode }) {
|
|
42
66
|
React.useEffect(() => {
|
|
43
|
-
clientBootstrap()
|
|
67
|
+
void clientBootstrap()
|
|
44
68
|
}, [])
|
|
45
69
|
|
|
46
|
-
//
|
|
70
|
+
// Fire-and-forget on the very first client render so any consumer that
|
|
71
|
+
// reads registries during the same paint as this provider mounts still
|
|
72
|
+
// sees them populated by microtask flush. The promise is cached.
|
|
47
73
|
if (typeof window !== 'undefined' && !_clientBootstrapped) {
|
|
48
|
-
clientBootstrap()
|
|
74
|
+
void clientBootstrap()
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
return <>{children}</>
|
|
@@ -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
|