@open-mercato/cli 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/in-process-generate-watcher-mode.js +13 -0
- package/dist/lib/in-process-generate-watcher-mode.js.map +7 -0
- package/dist/lib/in-process-generate-watcher.js +112 -0
- package/dist/lib/in-process-generate-watcher.js.map +7 -0
- package/dist/lib/testing/integration.js +5 -1
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +99 -75
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +9 -0
- package/src/lib/__tests__/in-process-generate-watcher-mode.test.ts +20 -0
- package/src/lib/__tests__/in-process-generate-watcher.test.ts +191 -0
- package/src/lib/in-process-generate-watcher-mode.ts +21 -0
- package/src/lib/in-process-generate-watcher.ts +175 -0
- package/src/lib/testing/integration.ts +5 -1
- package/src/mercato.ts +126 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3894.1.352abf4240",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@mikro-orm/decorators": "^7.1.1",
|
|
60
60
|
"@mikro-orm/migrations": "^7.1.1",
|
|
61
61
|
"@mikro-orm/postgresql": "^7.1.1",
|
|
62
|
-
"@open-mercato/queue": "0.6.3-develop.
|
|
63
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
62
|
+
"@open-mercato/queue": "0.6.3-develop.3894.1.352abf4240",
|
|
63
|
+
"@open-mercato/shared": "0.6.3-develop.3894.1.352abf4240",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
65
|
"pg": "8.21.0",
|
|
66
66
|
"semver": "^7.8.1",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
73
|
+
"@open-mercato/shared": "0.6.3-develop.3894.1.352abf4240"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.3-develop.3894.1.352abf4240",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.4.2",
|
|
79
79
|
"ts-jest": "^29.4.11"
|
|
@@ -632,6 +632,7 @@ describe('server dev managed process exits', () => {
|
|
|
632
632
|
const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
|
|
633
633
|
const originalLazy = process.env.OM_AUTO_SPAWN_WORKERS_LAZY
|
|
634
634
|
const originalLazyScheduler = process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
|
|
635
|
+
const originalGenerateWatchMode = process.env.OM_DEV_GENERATE_WATCH_MODE
|
|
635
636
|
|
|
636
637
|
beforeEach(() => {
|
|
637
638
|
jest.restoreAllMocks()
|
|
@@ -640,6 +641,12 @@ describe('server dev managed process exits', () => {
|
|
|
640
641
|
process.env.AUTO_SPAWN_WORKERS = 'true'
|
|
641
642
|
delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
|
|
642
643
|
delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
|
|
644
|
+
// These tests stub the resolver to an empty object; the in-process
|
|
645
|
+
// generate watcher's default checksum function would error on the
|
|
646
|
+
// missing methods. Force the legacy out-of-process mode so the
|
|
647
|
+
// managed-exit assertions only exercise the worker/scheduler/Next
|
|
648
|
+
// spawn surface.
|
|
649
|
+
process.env.OM_DEV_GENERATE_WATCH_MODE = 'legacy'
|
|
643
650
|
})
|
|
644
651
|
|
|
645
652
|
afterEach(() => {
|
|
@@ -651,6 +658,8 @@ describe('server dev managed process exits', () => {
|
|
|
651
658
|
jest.dontMock('../lib/queue-worker-supervisor')
|
|
652
659
|
jest.dontMock('../lib/scheduler-supervisor')
|
|
653
660
|
jest.resetModules()
|
|
661
|
+
if (originalGenerateWatchMode === undefined) delete process.env.OM_DEV_GENERATE_WATCH_MODE
|
|
662
|
+
else process.env.OM_DEV_GENERATE_WATCH_MODE = originalGenerateWatchMode
|
|
654
663
|
delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
|
|
655
664
|
delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
|
|
656
665
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolveGenerateWatcherMode } from '../in-process-generate-watcher-mode'
|
|
2
|
+
|
|
3
|
+
describe('resolveGenerateWatcherMode', () => {
|
|
4
|
+
it('defaults to in-process when the env var is unset', () => {
|
|
5
|
+
expect(resolveGenerateWatcherMode({})).toBe('in-process')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('defaults to in-process for empty / whitespace / unknown values', () => {
|
|
9
|
+
expect(resolveGenerateWatcherMode({ OM_DEV_GENERATE_WATCH_MODE: '' })).toBe('in-process')
|
|
10
|
+
expect(resolveGenerateWatcherMode({ OM_DEV_GENERATE_WATCH_MODE: ' ' })).toBe('in-process')
|
|
11
|
+
expect(resolveGenerateWatcherMode({ OM_DEV_GENERATE_WATCH_MODE: 'maybe' })).toBe('in-process')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it.each(['legacy', 'LEGACY', 'sidecar', 'out-of-process', ' legacy '])(
|
|
15
|
+
'returns legacy for opt-out alias %p',
|
|
16
|
+
(value) => {
|
|
17
|
+
expect(resolveGenerateWatcherMode({ OM_DEV_GENERATE_WATCH_MODE: value })).toBe('legacy')
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { startInProcessGenerateWatcher } from '../in-process-generate-watcher'
|
|
2
|
+
|
|
3
|
+
const silentLogger = { log: jest.fn(), error: jest.fn() }
|
|
4
|
+
|
|
5
|
+
async function flushAsync(times = 5): Promise<void> {
|
|
6
|
+
for (let i = 0; i < times; i++) {
|
|
7
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8
|
+
await Promise.resolve()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('startInProcessGenerateWatcher', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.useFakeTimers()
|
|
15
|
+
silentLogger.log.mockReset()
|
|
16
|
+
silentLogger.error.mockReset()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.useRealTimers()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('runs the generator once on startup when skipInitial is false', async () => {
|
|
24
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
25
|
+
const computeStructureChecksum = jest.fn(async () => 'checksum-a')
|
|
26
|
+
|
|
27
|
+
const handle = startInProcessGenerateWatcher({
|
|
28
|
+
pollMs: 1000,
|
|
29
|
+
skipInitial: false,
|
|
30
|
+
quiet: true,
|
|
31
|
+
logger: silentLogger,
|
|
32
|
+
computeStructureChecksum,
|
|
33
|
+
runGenerators,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await flushAsync(8)
|
|
37
|
+
|
|
38
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
39
|
+
expect(runGenerators.mock.calls[0][0]).toBe('initial')
|
|
40
|
+
|
|
41
|
+
await handle.close()
|
|
42
|
+
await handle.done
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('skips the initial run when skipInitial is true', async () => {
|
|
46
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
47
|
+
const computeStructureChecksum = jest.fn(async () => 'checksum-b')
|
|
48
|
+
|
|
49
|
+
const handle = startInProcessGenerateWatcher({
|
|
50
|
+
pollMs: 1000,
|
|
51
|
+
skipInitial: true,
|
|
52
|
+
quiet: true,
|
|
53
|
+
logger: silentLogger,
|
|
54
|
+
computeStructureChecksum,
|
|
55
|
+
runGenerators,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await flushAsync(8)
|
|
59
|
+
|
|
60
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
61
|
+
expect(computeStructureChecksum).toHaveBeenCalled()
|
|
62
|
+
|
|
63
|
+
await handle.close()
|
|
64
|
+
await handle.done
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('re-runs the generator when the checksum changes', async () => {
|
|
68
|
+
let currentChecksum = 'stable'
|
|
69
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
70
|
+
const computeStructureChecksum = jest.fn(async () => currentChecksum)
|
|
71
|
+
|
|
72
|
+
const handle = startInProcessGenerateWatcher({
|
|
73
|
+
pollMs: 1000,
|
|
74
|
+
skipInitial: true,
|
|
75
|
+
quiet: true,
|
|
76
|
+
logger: silentLogger,
|
|
77
|
+
computeStructureChecksum,
|
|
78
|
+
runGenerators,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await flushAsync(8)
|
|
82
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
83
|
+
|
|
84
|
+
// First tick: same checksum, no regeneration.
|
|
85
|
+
jest.advanceTimersByTime(1000)
|
|
86
|
+
await flushAsync(8)
|
|
87
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
88
|
+
|
|
89
|
+
// Second tick: checksum changed, generator must run with 'structure change'.
|
|
90
|
+
currentChecksum = 'changed'
|
|
91
|
+
jest.advanceTimersByTime(1000)
|
|
92
|
+
await flushAsync(8)
|
|
93
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
94
|
+
expect(runGenerators.mock.calls[0][0]).toBe('structure change')
|
|
95
|
+
|
|
96
|
+
await handle.close()
|
|
97
|
+
await handle.done
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not start a new poll while a regeneration is in flight', async () => {
|
|
101
|
+
let release: (() => void) | null = null
|
|
102
|
+
const runGenerators = jest.fn(async (_reason: string) => {
|
|
103
|
+
await new Promise<void>((resolve) => {
|
|
104
|
+
release = resolve
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
let currentChecksum = 'a'
|
|
108
|
+
const computeStructureChecksum = jest.fn(async () => currentChecksum)
|
|
109
|
+
|
|
110
|
+
const handle = startInProcessGenerateWatcher({
|
|
111
|
+
pollMs: 1000,
|
|
112
|
+
skipInitial: true,
|
|
113
|
+
quiet: true,
|
|
114
|
+
logger: silentLogger,
|
|
115
|
+
computeStructureChecksum,
|
|
116
|
+
runGenerators,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await flushAsync(8)
|
|
120
|
+
// Establish baseline checksum, no run yet.
|
|
121
|
+
expect(runGenerators).not.toHaveBeenCalled()
|
|
122
|
+
|
|
123
|
+
// Tick #1: change checksum so a run starts and blocks.
|
|
124
|
+
currentChecksum = 'b'
|
|
125
|
+
jest.advanceTimersByTime(1000)
|
|
126
|
+
await flushAsync(8)
|
|
127
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
128
|
+
|
|
129
|
+
// Tick #2 and #3 must NOT spawn parallel runs while #1 is in flight —
|
|
130
|
+
// the helper serializes via the running/pending pair plus the
|
|
131
|
+
// single-shot finally-scheduled poll cycle.
|
|
132
|
+
currentChecksum = 'c'
|
|
133
|
+
jest.advanceTimersByTime(1000)
|
|
134
|
+
await flushAsync(8)
|
|
135
|
+
jest.advanceTimersByTime(1000)
|
|
136
|
+
await flushAsync(8)
|
|
137
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
138
|
+
|
|
139
|
+
release?.()
|
|
140
|
+
await flushAsync(20)
|
|
141
|
+
|
|
142
|
+
await handle.close()
|
|
143
|
+
await handle.done
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('logs but does not crash when the generator throws', async () => {
|
|
147
|
+
const runGenerators = jest.fn(async () => {
|
|
148
|
+
throw new Error('boom')
|
|
149
|
+
})
|
|
150
|
+
const computeStructureChecksum = jest.fn(async () => 'k')
|
|
151
|
+
|
|
152
|
+
const handle = startInProcessGenerateWatcher({
|
|
153
|
+
pollMs: 1000,
|
|
154
|
+
skipInitial: false,
|
|
155
|
+
quiet: true,
|
|
156
|
+
logger: silentLogger,
|
|
157
|
+
computeStructureChecksum,
|
|
158
|
+
runGenerators,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await flushAsync(8)
|
|
162
|
+
expect(runGenerators).toHaveBeenCalledTimes(1)
|
|
163
|
+
expect(silentLogger.error).toHaveBeenCalled()
|
|
164
|
+
const message = String(silentLogger.error.mock.calls[0][0] ?? '')
|
|
165
|
+
expect(message).toMatch(/Generation failed/)
|
|
166
|
+
expect(message).toMatch(/boom/)
|
|
167
|
+
|
|
168
|
+
await handle.close()
|
|
169
|
+
await handle.done
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('close() is idempotent and resolves done exactly once', async () => {
|
|
173
|
+
const runGenerators = jest.fn(async () => undefined)
|
|
174
|
+
const computeStructureChecksum = jest.fn(async () => 'k')
|
|
175
|
+
|
|
176
|
+
const handle = startInProcessGenerateWatcher({
|
|
177
|
+
pollMs: 1000,
|
|
178
|
+
skipInitial: true,
|
|
179
|
+
quiet: true,
|
|
180
|
+
logger: silentLogger,
|
|
181
|
+
computeStructureChecksum,
|
|
182
|
+
runGenerators,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await flushAsync(8)
|
|
186
|
+
await handle.close()
|
|
187
|
+
await handle.close()
|
|
188
|
+
await handle.done
|
|
189
|
+
expect(true).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the watcher-mode env flag that governs whether the dev runtime
|
|
3
|
+
* runs the generate watcher in-process (default) or as a legacy out-of-process
|
|
4
|
+
* `mercato generate watch --skip-initial` sidecar.
|
|
5
|
+
*
|
|
6
|
+
* `OM_DEV_GENERATE_WATCH_MODE=legacy` opts back into the pre-consolidation
|
|
7
|
+
* behavior (one extra long-running Node process). Any other value, missing
|
|
8
|
+
* value, or unparseable value resolves to `in-process`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type GenerateWatcherMode = 'in-process' | 'legacy'
|
|
12
|
+
|
|
13
|
+
export function resolveGenerateWatcherMode(env: NodeJS.ProcessEnv): GenerateWatcherMode {
|
|
14
|
+
const raw = env.OM_DEV_GENERATE_WATCH_MODE
|
|
15
|
+
if (typeof raw !== 'string') return 'in-process'
|
|
16
|
+
const normalized = raw.trim().toLowerCase()
|
|
17
|
+
if (normalized === 'legacy' || normalized === 'sidecar' || normalized === 'out-of-process') {
|
|
18
|
+
return 'legacy'
|
|
19
|
+
}
|
|
20
|
+
return 'in-process'
|
|
21
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process generate watcher.
|
|
3
|
+
*
|
|
4
|
+
* Polls a structural-fingerprint function on a configurable interval and
|
|
5
|
+
* invokes the supplied generator callback whenever the fingerprint changes.
|
|
6
|
+
* The polling loop is identical to the legacy standalone watcher; the only
|
|
7
|
+
* difference is that it now runs inside whatever process calls it
|
|
8
|
+
* (typically `mercato server dev`), so the dev runtime no longer needs a
|
|
9
|
+
* sidecar `mercato generate watch` Node process.
|
|
10
|
+
*
|
|
11
|
+
* Contract preserved 1:1 with the prior standalone watcher:
|
|
12
|
+
* - Default poll interval 1000 ms (minimum 250 ms).
|
|
13
|
+
* - One initial generator run unless `skipInitial` is true.
|
|
14
|
+
* - Concurrent regeneration requests are coalesced (running + pending).
|
|
15
|
+
* - Generator errors are logged but never crash the watcher.
|
|
16
|
+
* - The polling timer uses `.unref()` so it never blocks process exit.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type GenerateWatcherLogger = Pick<Console, 'log' | 'error'>
|
|
20
|
+
|
|
21
|
+
export type GenerateWatcherOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Function that returns the current structural fingerprint of the module
|
|
24
|
+
* tree. The watcher re-runs `runGenerators` whenever this value changes.
|
|
25
|
+
*/
|
|
26
|
+
computeStructureChecksum: () => Promise<string> | string
|
|
27
|
+
/**
|
|
28
|
+
* Function that performs the actual regeneration work. Called once on
|
|
29
|
+
* startup (unless `skipInitial`) and again whenever the checksum changes.
|
|
30
|
+
* The `reason` argument is suitable for logging (`'initial'`,
|
|
31
|
+
* `'structure change'`, `'queued change'`).
|
|
32
|
+
*/
|
|
33
|
+
runGenerators: (reason: string) => Promise<void>
|
|
34
|
+
/** Poll interval in milliseconds. Defaults to 1000. Clamped to >= 250. */
|
|
35
|
+
pollMs?: number
|
|
36
|
+
/** Skip the initial regeneration on startup. Defaults to false. */
|
|
37
|
+
skipInitial?: boolean
|
|
38
|
+
/** Suppress informational logs. Errors are always logged. */
|
|
39
|
+
quiet?: boolean
|
|
40
|
+
/** Logger override. Defaults to `console`. */
|
|
41
|
+
logger?: GenerateWatcherLogger
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type GenerateWatcherHandle = {
|
|
45
|
+
/** Resolves when the watcher loop has stopped (after `close()`). */
|
|
46
|
+
readonly done: Promise<void>
|
|
47
|
+
/** Stop the polling loop and resolve `done`. Idempotent. */
|
|
48
|
+
close(): Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MIN_POLL_MS = 250
|
|
52
|
+
const DEFAULT_POLL_MS = 1000
|
|
53
|
+
|
|
54
|
+
function resolvePollMs(value: number | undefined): number {
|
|
55
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_POLL_MS
|
|
56
|
+
const numeric = Math.floor(value)
|
|
57
|
+
return numeric >= MIN_POLL_MS ? numeric : DEFAULT_POLL_MS
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start an in-process generate watcher. The returned handle exposes a
|
|
62
|
+
* `close()` to stop polling and a `done` promise that resolves once the
|
|
63
|
+
* watcher loop has finished.
|
|
64
|
+
*/
|
|
65
|
+
export function startInProcessGenerateWatcher(
|
|
66
|
+
options: GenerateWatcherOptions,
|
|
67
|
+
): GenerateWatcherHandle {
|
|
68
|
+
const logger = options.logger ?? console
|
|
69
|
+
const quiet = options.quiet === true
|
|
70
|
+
const pollMs = resolvePollMs(options.pollMs)
|
|
71
|
+
const skipInitial = options.skipInitial === true
|
|
72
|
+
const { computeStructureChecksum, runGenerators } = options
|
|
73
|
+
|
|
74
|
+
let stopping = false
|
|
75
|
+
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
|
76
|
+
let running = false
|
|
77
|
+
let pendingReason: string | null = null
|
|
78
|
+
let previousChecksum = ''
|
|
79
|
+
let doneResolve: (() => void) | null = null
|
|
80
|
+
const done = new Promise<void>((resolve) => {
|
|
81
|
+
doneResolve = resolve
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
async function runOnce(reason: string): Promise<void> {
|
|
85
|
+
if (running) {
|
|
86
|
+
pendingReason = reason
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
running = true
|
|
90
|
+
try {
|
|
91
|
+
if (!quiet) {
|
|
92
|
+
logger.log(`[generate:watch] Regenerating (${reason})...`)
|
|
93
|
+
}
|
|
94
|
+
await runGenerators(reason)
|
|
95
|
+
if (!quiet) {
|
|
96
|
+
logger.log('[generate:watch] Generators completed.')
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
100
|
+
logger.error(`[generate:watch] Generation failed: ${message}`)
|
|
101
|
+
} finally {
|
|
102
|
+
running = false
|
|
103
|
+
if (pendingReason && !stopping) {
|
|
104
|
+
const queued = pendingReason
|
|
105
|
+
pendingReason = null
|
|
106
|
+
await runOnce(queued)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function scheduleNext(): void {
|
|
112
|
+
if (stopping) return
|
|
113
|
+
pollTimer = setTimeout(() => {
|
|
114
|
+
void (async () => {
|
|
115
|
+
if (stopping) return
|
|
116
|
+
try {
|
|
117
|
+
const nextChecksum = await computeStructureChecksum()
|
|
118
|
+
if (nextChecksum !== previousChecksum) {
|
|
119
|
+
previousChecksum = nextChecksum
|
|
120
|
+
await runOnce('structure change')
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
124
|
+
logger.error(`[generate:watch] Poll cycle failed: ${message}`)
|
|
125
|
+
} finally {
|
|
126
|
+
if (!stopping) scheduleNext()
|
|
127
|
+
}
|
|
128
|
+
})()
|
|
129
|
+
}, pollMs)
|
|
130
|
+
pollTimer.unref?.()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void (async () => {
|
|
134
|
+
try {
|
|
135
|
+
if (!skipInitial) {
|
|
136
|
+
await runOnce('initial')
|
|
137
|
+
}
|
|
138
|
+
previousChecksum = await computeStructureChecksum()
|
|
139
|
+
if (!quiet) {
|
|
140
|
+
if (skipInitial) {
|
|
141
|
+
logger.log('[generate:watch] Skipping initial regeneration and watching the current generated state.')
|
|
142
|
+
}
|
|
143
|
+
logger.log(`[generate:watch] Watching structural module files every ${pollMs}ms (in-process).`)
|
|
144
|
+
}
|
|
145
|
+
scheduleNext()
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
148
|
+
logger.error(`[generate:watch] Initial setup failed: ${message}`)
|
|
149
|
+
// Even if the initial bootstrap fails, schedule polling so a later
|
|
150
|
+
// checksum recovery still picks up the next change on disk.
|
|
151
|
+
scheduleNext()
|
|
152
|
+
}
|
|
153
|
+
})()
|
|
154
|
+
|
|
155
|
+
async function close(): Promise<void> {
|
|
156
|
+
if (stopping) {
|
|
157
|
+
await done
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
stopping = true
|
|
161
|
+
if (pollTimer) {
|
|
162
|
+
clearTimeout(pollTimer)
|
|
163
|
+
pollTimer = null
|
|
164
|
+
}
|
|
165
|
+
if (doneResolve) {
|
|
166
|
+
doneResolve()
|
|
167
|
+
doneResolve = null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
done,
|
|
173
|
+
close,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -1649,6 +1649,10 @@ function buildReusableEnvironment(
|
|
|
1649
1649
|
OM_ENABLE_ENTERPRISE_MODULES_SECURITY: process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY ?? enterpriseModulesFlag,
|
|
1650
1650
|
OM_TEST_MODE: '1',
|
|
1651
1651
|
OM_TEST_AUTH_RATE_LIMIT_MODE: 'opt-in',
|
|
1652
|
+
// Tests assert on access_logs immediately after CRUD reads; keep the
|
|
1653
|
+
// blocking write path on inside the integration runtime so tests do
|
|
1654
|
+
// not have to call flushPendingCrudAccessLogs() explicitly.
|
|
1655
|
+
OM_CRUD_ACCESS_LOG_BLOCKING: process.env.OM_CRUD_ACCESS_LOG_BLOCKING ?? '1',
|
|
1652
1656
|
OM_WEBHOOKS_ALLOW_PRIVATE_URLS: process.env.OM_WEBHOOKS_ALLOW_PRIVATE_URLS ?? '1',
|
|
1653
1657
|
ENABLE_CRUD_API_CACHE: 'true',
|
|
1654
1658
|
MOCK_GATEWAY_WEBHOOK_SECRET: 'open-mercato-mock-dev-webhook-secret',
|
|
@@ -2936,7 +2940,7 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2936
2940
|
NEXT_PUBLIC_UMES_DEVTOOLS: 'true',
|
|
2937
2941
|
CI: 'true',
|
|
2938
2942
|
TENANT_DATA_ENCRYPTION_FALLBACK_KEY: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? 'om-ephemeral-integration-fallback-key',
|
|
2939
|
-
AUTO_SPAWN_WORKERS: '
|
|
2943
|
+
AUTO_SPAWN_WORKERS: process.env.AUTO_SPAWN_WORKERS ?? 'true',
|
|
2940
2944
|
AUTO_SPAWN_SCHEDULER: 'false',
|
|
2941
2945
|
OM_CLI_QUIET: '1',
|
|
2942
2946
|
MERCATO_QUIET: '1',
|