@dimina-kit/devkit 0.1.2-dev.20260703051110 → 0.1.2-dev.20260706064107

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 (32) hide show
  1. package/README.md +16 -0
  2. package/dist/compile-worker-adopt.test.d.ts +2 -0
  3. package/dist/compile-worker-adopt.test.d.ts.map +1 -0
  4. package/dist/compile-worker-adopt.test.js +187 -0
  5. package/dist/compile-worker-entry-warmpool.test.d.ts +2 -0
  6. package/dist/compile-worker-entry-warmpool.test.d.ts.map +1 -0
  7. package/dist/compile-worker-entry-warmpool.test.js +110 -0
  8. package/dist/compile-worker-entry.d.ts +35 -5
  9. package/dist/compile-worker-entry.d.ts.map +1 -1
  10. package/dist/compile-worker-entry.js +45 -1
  11. package/dist/compile-worker-standby-hardening.test.d.ts +2 -0
  12. package/dist/compile-worker-standby-hardening.test.d.ts.map +1 -0
  13. package/dist/compile-worker-standby-hardening.test.js +212 -0
  14. package/dist/compile-worker-standby-integration.test.d.ts +2 -0
  15. package/dist/compile-worker-standby-integration.test.d.ts.map +1 -0
  16. package/dist/compile-worker-standby-integration.test.js +243 -0
  17. package/dist/compile-worker-standby.d.ts +60 -0
  18. package/dist/compile-worker-standby.d.ts.map +1 -0
  19. package/dist/compile-worker-standby.js +283 -0
  20. package/dist/compile-worker-standby.test.d.ts +2 -0
  21. package/dist/compile-worker-standby.test.d.ts.map +1 -0
  22. package/dist/compile-worker-standby.test.js +356 -0
  23. package/dist/compile-worker.d.ts +53 -0
  24. package/dist/compile-worker.d.ts.map +1 -1
  25. package/dist/compile-worker.js +30 -11
  26. package/dist/index.d.ts +21 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +74 -11
  29. package/dist/open-project-watcher-error-callback.test.d.ts +2 -0
  30. package/dist/open-project-watcher-error-callback.test.d.ts.map +1 -0
  31. package/dist/open-project-watcher-error-callback.test.js +191 -0
  32. package/package.json +3 -3
package/README.md CHANGED
@@ -59,6 +59,22 @@ await session.close()
59
59
  | `port` | `number` | 实际监听的端口 |
60
60
  | `close` | `() => Promise<void>` | 关闭文件监听和预览服务器 |
61
61
 
62
+ ### enableCompileWorkerStandby(热备胎,可选加速器)
63
+
64
+ 进程级开关:预先 fork 一个**项目无关**的编译子进程并预热编译器(不 chdir、不编译任何项目),之后每次 `openProject` 自动领养它——首编省掉 fork + 编译器加载(实测 ~1.6s);每次 `session.close()` 后自动补一个新备胎。**纯加速器**:备胎的任何失效(悄悄死亡、健康检查失败、崩溃熔断)都自动退化为原有的冷 fork 路径,绝不影响 openProject 的成败。
65
+
66
+ ```typescript
67
+ import { enableCompileWorkerStandby } from '@dimina-kit/devkit'
68
+
69
+ const standby = enableCompileWorkerStandby({
70
+ onEvent: ev => console.log('[standby]', ev.type, ev.pid ?? '', ev.reason ?? ''),
71
+ })
72
+ // …应用退出时:
73
+ await standby.dispose() // 杀掉备胎;之后 openProject 永远走冷路径,不再补胎
74
+ ```
75
+
76
+ 稳定性设计(面向发布到用户机器):领养前 ping/pong 健康检查(默认 1s 超时,卡死的备胎会被杀掉而不是交出去);意外死亡自动补胎,但 30 秒内连死 3 次触发熔断(`degraded`,本次会话内永久停用,防 fork 风暴);`onEvent` 暴露完整生命周期(`spawned` / `prewarmed` / `adopted` / `health-check-failed` / `died` / `degraded`)供接入诊断。未调用此 API 时行为与之前完全一致。
77
+
62
78
  ---
63
79
 
64
80
  ## 工作流程
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compile-worker-adopt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-worker-adopt.test.d.ts","sourceRoot":"","sources":["../src/compile-worker-adopt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,187 @@
1
+ import { execFileSync, fork } from 'node:child_process';
2
+ import { once } from 'node:events';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { afterEach, describe, expect, it } from 'vitest';
8
+ import { createCompileWorker } from './compile-worker.js';
9
+ /**
10
+ * Contract for warm-standby ADOPTION on the `createCompileWorker` side:
11
+ * `opts.adopt?: ChildProcess` lets a caller (devtools' warm-standby manager)
12
+ * hand over an already-forked, already-toolchain-warmed compile worker so
13
+ * the FIRST build of a newly opened project reuses it instead of paying a
14
+ * fresh fork + `@dimina-kit/compiler` import.
15
+ *
16
+ * REAL-fork, real-compile integration tests (same style as
17
+ * `compile-worker-leak.test.ts`): the adopted process is a genuine fork of
18
+ * `compile-worker-entry`, and "reused, not re-forked" is proven by a real OS
19
+ * PID scan — `ps -axo pid=,ppid=,command=` for direct children of THIS test
20
+ * process whose command line names the entry module — not by a spy.
21
+ */
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ function workerEntryPath() {
24
+ const js = path.join(__dirname, 'compile-worker-entry.js');
25
+ if (fs.existsSync(js))
26
+ return js;
27
+ return path.join(__dirname, 'compile-worker-entry.ts');
28
+ }
29
+ function workerExecArgv(entry) {
30
+ return entry.endsWith('.ts')
31
+ ? ['--experimental-strip-types', '--disable-warning=ExperimentalWarning']
32
+ : [];
33
+ }
34
+ function sleep(ms) {
35
+ return new Promise(resolve => setTimeout(resolve, ms));
36
+ }
37
+ function pidAlive(pid) {
38
+ try {
39
+ process.kill(pid, 0);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ async function waitForPidDeath(pid, timeoutMs = 8000) {
47
+ const deadline = Date.now() + timeoutMs;
48
+ while (Date.now() < deadline) {
49
+ if (!pidAlive(pid))
50
+ return true;
51
+ await sleep(100);
52
+ }
53
+ return !pidAlive(pid);
54
+ }
55
+ async function waitForPidAlive(pid, timeoutMs = 8000) {
56
+ const deadline = Date.now() + timeoutMs;
57
+ while (Date.now() < deadline) {
58
+ if (pidAlive(pid))
59
+ return true;
60
+ await sleep(100);
61
+ }
62
+ return pidAlive(pid);
63
+ }
64
+ /** Direct children of THIS test process that are compile-worker-entry forks. */
65
+ function findCompileWorkerPids() {
66
+ const out = execFileSync('ps', ['-axo', 'pid=,ppid=,command='], { encoding: 'utf8' });
67
+ const pids = [];
68
+ for (const line of out.split('\n')) {
69
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
70
+ if (!match)
71
+ continue;
72
+ const [, pid, ppid, command] = match;
73
+ if (Number(ppid) !== process.pid)
74
+ continue;
75
+ if (!command.includes('compile-worker-entry'))
76
+ continue;
77
+ pids.push(Number(pid));
78
+ }
79
+ return pids;
80
+ }
81
+ function forkAdoptCandidate() {
82
+ const entry = workerEntryPath();
83
+ // Same fork shape createCompileWorker's own spawnWorker uses: piped stdio +
84
+ // an explicit execArgv (never the parent's loaders) so the adopted process
85
+ // is indistinguishable from one createCompileWorker would have forked itself.
86
+ return fork(entry, [], { execArgv: workerExecArgv(entry), silent: true });
87
+ }
88
+ const doomedPids = [];
89
+ const cleanupRoots = [];
90
+ const activeWorkers = [];
91
+ afterEach(async () => {
92
+ for (const worker of activeWorkers.splice(0)) {
93
+ try {
94
+ await worker.close();
95
+ }
96
+ catch {
97
+ // best-effort teardown
98
+ }
99
+ }
100
+ for (const pid of doomedPids.splice(0)) {
101
+ if (pidAlive(pid)) {
102
+ try {
103
+ process.kill(pid, 'SIGKILL');
104
+ }
105
+ catch {
106
+ // already gone
107
+ }
108
+ }
109
+ }
110
+ for (const root of cleanupRoots.splice(0)) {
111
+ fs.rmSync(root, { recursive: true, force: true });
112
+ }
113
+ });
114
+ function makeFixture() {
115
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'devkit-adopt-'));
116
+ cleanupRoots.push(root);
117
+ const write = (rel, content) => {
118
+ const target = path.join(root, rel);
119
+ fs.mkdirSync(path.dirname(target), { recursive: true });
120
+ fs.writeFileSync(target, content);
121
+ };
122
+ write('project.config.json', JSON.stringify({ appid: 'fixture_app_001', projectname: 'fixture-app' }));
123
+ write('app.json', JSON.stringify({ pages: ['pages/index/index'] }));
124
+ write('app.js', 'App({})\n');
125
+ write('app.wxss', 'page { font-size: 14px; }\n');
126
+ write('pages/index/index.json', '{}\n');
127
+ write('pages/index/index.js', 'Page({ data: { msg: "hi" } })\n');
128
+ write('pages/index/index.wxml', '<view>{{msg}}</view>\n');
129
+ write('pages/index/index.wxss', '.x { color: red; }\n');
130
+ return root;
131
+ }
132
+ describe('createCompileWorker — opts.adopt (warm-standby hand-off)', () => {
133
+ it('the FIRST build reuses the adopted process (same PID, no extra fork), pipes its stdout to onLog, and close() kills that same PID', async () => {
134
+ const adoptee = forkAdoptCandidate();
135
+ await once(adoptee, 'spawn');
136
+ doomedPids.push(adoptee.pid);
137
+ expect(pidAlive(adoptee.pid)).toBe(true);
138
+ const logEntries = [];
139
+ const worker = createCompileWorker({
140
+ onLog: entry => logEntries.push(entry),
141
+ adopt: adoptee,
142
+ });
143
+ activeWorkers.push(worker);
144
+ const root = makeFixture();
145
+ const appInfo = await worker.build({
146
+ projectPath: root,
147
+ outputDir: path.join(root, '.out'),
148
+ options: {},
149
+ });
150
+ expect(appInfo?.appId).toBe('fixture_app_001');
151
+ const liveWorkerPids = findCompileWorkerPids();
152
+ expect(liveWorkerPids, 'the first build must reuse the ADOPTED process, not fork a fresh one — exactly one compile-worker-entry child must exist, and it must be the adoptee').toEqual([adoptee.pid]);
153
+ expect(logEntries.length, 'the adopted process\'s stdout/stderr must flow to onLog just like a self-forked worker\'s').toBeGreaterThan(0);
154
+ await worker.close();
155
+ expect(await waitForPidDeath(adoptee.pid, 8000), 'close() on a worker using an adopted process must leave that PID actually dead — same guarantee as a self-forked worker').toBe(true);
156
+ }, 90_000);
157
+ it('an adopted process that dies BEFORE the first build falls back to a fresh fork, and the build still succeeds', async () => {
158
+ const adoptee = forkAdoptCandidate();
159
+ await once(adoptee, 'spawn');
160
+ const deadPid = adoptee.pid;
161
+ adoptee.kill('SIGKILL');
162
+ expect(await waitForPidDeath(deadPid, 8000)).toBe(true);
163
+ const worker = createCompileWorker({
164
+ adopt: adoptee,
165
+ });
166
+ activeWorkers.push(worker);
167
+ const root = makeFixture();
168
+ const appInfo = await worker.build({
169
+ projectPath: root,
170
+ outputDir: path.join(root, '.out'),
171
+ options: {},
172
+ });
173
+ expect(appInfo?.appId, 'a dead adoptee must not fail the build — createCompileWorker must transparently fork a FRESH worker instead').toBe('fixture_app_001');
174
+ const liveWorkerPids = findCompileWorkerPids();
175
+ expect(liveWorkerPids).toHaveLength(1);
176
+ expect(liveWorkerPids[0], 'the fallback fork must be a genuinely NEW process, not the dead adoptee').not.toBe(deadPid);
177
+ doomedPids.push(liveWorkerPids[0]);
178
+ await worker.close();
179
+ }, 90_000);
180
+ it('a fresh (never-crashed) adopted process is genuinely alive at hand-off time — precondition sanity check', async () => {
181
+ const adoptee = forkAdoptCandidate();
182
+ await once(adoptee, 'spawn');
183
+ doomedPids.push(adoptee.pid);
184
+ expect(await waitForPidAlive(adoptee.pid, 3000)).toBe(true);
185
+ adoptee.kill('SIGKILL');
186
+ });
187
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compile-worker-entry-warmpool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-worker-entry-warmpool.test.d.ts","sourceRoot":"","sources":["../src/compile-worker-entry-warmpool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ async function getCreateHandler() {
3
+ const mod = await import('./compile-worker-entry.js').catch(() => null);
4
+ expect(mod, 'src/compile-worker-entry must exist').not.toBeNull();
5
+ const fn = mod.createCompileWorkerHandler;
6
+ expect(typeof fn, 'compile-worker-entry must export createCompileWorkerHandler(deps)').toBe('function');
7
+ return fn;
8
+ }
9
+ const FIXTURE_APP = {
10
+ appId: 'fixture_app_001',
11
+ name: 'fixture-app',
12
+ path: '/tmp/fixture-project',
13
+ };
14
+ const BUILD_MSG = {
15
+ cmd: 'build',
16
+ projectPath: '/tmp/fixture-project',
17
+ outputDir: '/tmp/fixture-out',
18
+ options: { sourcemap: true },
19
+ };
20
+ function makeDeps(build, warmPool) {
21
+ const buildSpy = vi.fn((...args) => build(...args));
22
+ const loadCompilerSpy = vi.fn(() => buildSpy);
23
+ const deps = {
24
+ loadCompiler: loadCompilerSpy,
25
+ chdir: vi.fn((_dir) => { }),
26
+ send: vi.fn((_msg) => { }),
27
+ ...(warmPool ? { warmPool: vi.fn(warmPool) } : {}),
28
+ };
29
+ return { deps, buildSpy, loadCompilerSpy };
30
+ }
31
+ describe('compile-worker-entry — ping/prewarm (warm-standby protocol extension)', () => {
32
+ it('ping replies pong synchronously and NEVER triggers loadCompiler', async () => {
33
+ const createHandler = await getCreateHandler();
34
+ const { deps, loadCompilerSpy } = makeDeps(async () => FIXTURE_APP);
35
+ const handler = createHandler(deps);
36
+ await handler({ cmd: 'ping', id: 'p1' });
37
+ expect(loadCompilerSpy, 'a ping health check must never load the compiler — a spare must be pingable while cold-idle').not.toHaveBeenCalled();
38
+ expect(deps.send).toHaveBeenCalledTimes(1);
39
+ expect(deps.send).toHaveBeenCalledWith({ type: 'pong', id: 'p1' });
40
+ });
41
+ it('prewarm loads the compiler, calls warmPool if provided, and replies prewarm-result ok:true', async () => {
42
+ const createHandler = await getCreateHandler();
43
+ const warmPool = vi.fn(async () => { });
44
+ const { deps, loadCompilerSpy } = makeDeps(async () => FIXTURE_APP, warmPool);
45
+ const handler = createHandler(deps);
46
+ await handler({ cmd: 'prewarm', id: 'w1' });
47
+ expect(loadCompilerSpy).toHaveBeenCalledTimes(1);
48
+ expect(deps.warmPool, 'when warmPool is provided, prewarm must await it (after loadCompiler)').toHaveBeenCalledTimes(1);
49
+ expect(deps.send).toHaveBeenCalledWith({ type: 'prewarm-result', id: 'w1', ok: true });
50
+ });
51
+ it('a build AFTER a successful prewarm reuses the SAME cached compiler — loadCompiler is called exactly once total', async () => {
52
+ const createHandler = await getCreateHandler();
53
+ const { deps, buildSpy, loadCompilerSpy } = makeDeps(async () => FIXTURE_APP);
54
+ const handler = createHandler(deps);
55
+ await handler({ cmd: 'prewarm', id: 'w1' });
56
+ await handler(BUILD_MSG);
57
+ expect(loadCompilerSpy, 'prewarm and the first build must share ONE compiler load/cache — a prewarmed spare that reloads on its first real build wastes the whole point of prewarming').toHaveBeenCalledTimes(1);
58
+ expect(buildSpy).toHaveBeenCalledTimes(1);
59
+ expect(deps.send).toHaveBeenCalledTimes(2);
60
+ expect(deps.send).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'result', appInfo: FIXTURE_APP }));
61
+ });
62
+ it('prewarm works without a warmPool dependency (it is OPTIONAL) — no crash, ok:true', async () => {
63
+ const createHandler = await getCreateHandler();
64
+ const { deps } = makeDeps(async () => FIXTURE_APP);
65
+ const handler = createHandler(deps);
66
+ await expect(handler({ cmd: 'prewarm', id: 'w2' })).resolves.toBeUndefined();
67
+ expect(deps.send).toHaveBeenCalledWith({ type: 'prewarm-result', id: 'w2', ok: true });
68
+ });
69
+ it('prewarm replies ok:false with the error message when loadCompiler throws — the handler itself still resolves', async () => {
70
+ const createHandler = await getCreateHandler();
71
+ const failingLoad = vi.fn(() => {
72
+ throw new Error('boom — toolchain load failed');
73
+ });
74
+ const deps = {
75
+ loadCompiler: failingLoad,
76
+ chdir: vi.fn(),
77
+ send: vi.fn(),
78
+ };
79
+ const handler = createHandler(deps);
80
+ await expect(handler({ cmd: 'prewarm', id: 'w3' })).resolves.toBeUndefined();
81
+ expect(deps.send).toHaveBeenCalledWith(expect.objectContaining({
82
+ type: 'prewarm-result',
83
+ id: 'w3',
84
+ ok: false,
85
+ error: expect.stringContaining('boom'),
86
+ }));
87
+ });
88
+ it('prewarm replies ok:false with the error message when warmPool rejects — the handler itself still resolves', async () => {
89
+ const createHandler = await getCreateHandler();
90
+ const warmPool = vi.fn(async () => {
91
+ throw new Error('boom — pool warm-up failed');
92
+ });
93
+ const { deps } = makeDeps(async () => FIXTURE_APP, warmPool);
94
+ const handler = createHandler(deps);
95
+ await expect(handler({ cmd: 'prewarm', id: 'w4' })).resolves.toBeUndefined();
96
+ expect(deps.send).toHaveBeenCalledWith(expect.objectContaining({
97
+ type: 'prewarm-result',
98
+ id: 'w4',
99
+ ok: false,
100
+ error: expect.stringContaining('boom'),
101
+ }));
102
+ });
103
+ it('prewarm NEVER calls chdir — the whole safety of a project-agnostic spare rests on this', async () => {
104
+ const createHandler = await getCreateHandler();
105
+ const { deps } = makeDeps(async () => FIXTURE_APP, async () => { });
106
+ const handler = createHandler(deps);
107
+ await handler({ cmd: 'prewarm', id: 'w5' });
108
+ expect(deps.chdir, 'prewarm must be project-agnostic: chdir-ing during prewarm would tie the spare to whatever project happened to be current at fork time').not.toHaveBeenCalled();
109
+ });
110
+ });
@@ -20,25 +20,55 @@ export interface WorkerResultMessage {
20
20
  message: string;
21
21
  };
22
22
  }
23
+ /** Reply to `{ cmd: 'ping' }` — the warm-standby manager's health probe. */
24
+ export interface WorkerPongMessage {
25
+ type: 'pong';
26
+ id: string;
27
+ }
28
+ /** Reply to `{ cmd: 'prewarm' }` — ok:false carries the load/warm failure. */
29
+ export interface WorkerPrewarmResultMessage {
30
+ type: 'prewarm-result';
31
+ id: string;
32
+ ok: boolean;
33
+ error?: string;
34
+ }
35
+ export type WorkerOutboundMessage = WorkerResultMessage | WorkerPongMessage | WorkerPrewarmResultMessage;
23
36
  export interface CompileWorkerHandlerDeps {
24
37
  /**
25
- * Lazy compiler load — deferred to the first build. May be sync (a plain
26
- * `require`) or async (a dynamic `import()` of the ESM `@dimina-kit/compiler`
27
- * pool); the handler awaits it either way. Loaded ONCE and cached.
38
+ * Lazy compiler load — deferred to the first build (or an explicit prewarm).
39
+ * May be sync (a plain `require`) or async (a dynamic `import()` of the ESM
40
+ * `@dimina-kit/compiler` pool); the handler awaits it either way. Loaded
41
+ * ONCE and cached — a build after a prewarm reuses the prewarmed load.
28
42
  */
29
43
  loadCompiler: () => WorkerBuildFn | Promise<WorkerBuildFn>;
30
44
  /** `process.chdir` — mutates the CHILD process cwd only. */
31
45
  chdir: (dir: string) => void;
32
46
  /** `process.send` — replies to the parent. */
33
- send: (msg: WorkerResultMessage) => void;
47
+ send: (msg: WorkerOutboundMessage) => void;
48
+ /**
49
+ * Optional deep warm-up run by `{ cmd: 'prewarm' }` after the compiler
50
+ * loads (e.g. spinning up the resident pool's stage workers so the first
51
+ * real build starts on warm threads). Project-agnostic by contract.
52
+ */
53
+ warmPool?: () => Promise<void>;
34
54
  }
35
55
  /**
36
- * Build the fork-side IPC message handler. Non-build messages are ignored
56
+ * Build the fork-side IPC message handler. Unknown messages are ignored
37
57
  * (no compiler load, no reply). A build command chdirs into the project,
38
58
  * runs the compiler exactly as the old in-process call sites did, and ALWAYS
39
59
  * replies — `@dimina/compiler` swallows compile errors internally (resolves
40
60
  * undefined), which is normalized to `appInfo: null`; a genuine throw still
41
61
  * replies with `error.message` so the parent never hangs on a lost build.
62
+ *
63
+ * Two warm-standby commands ride the same channel:
64
+ * - `{ cmd: 'ping', id }` → `{ type: 'pong', id }`. Pure liveness; never
65
+ * loads the compiler (a health check on a cold-idle spare must stay cheap).
66
+ * - `{ cmd: 'prewarm', id }` → loads the compiler into the SAME cache the
67
+ * build path uses, then awaits `deps.warmPool?.()`, and always replies
68
+ * `{ type: 'prewarm-result', id, ok, error? }` (a failed prewarm reports —
69
+ * it never throws out of the handler). Deliberately NO chdir: a prewarmed
70
+ * spare stays project-agnostic, which is what makes adopting it into ANY
71
+ * next-opened project safe.
42
72
  */
43
73
  export declare function createCompileWorkerHandler(deps: CompileWorkerHandlerDeps): (msg: unknown) => Promise<void>;
44
74
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"compile-worker-entry.d.ts","sourceRoot":"","sources":["../src/compile-worker-entry.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,kBAAkB;IAClC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAC5E;AAED,MAAM,MAAM,aAAa,GAAG,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,OAAO,EACpB,OAAO,EAAE,kBAAkB,KACvB,OAAO,CAAC,aAAa,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AAE9C,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACxC;;;;OAIG;IACH,YAAY,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAC1D,4DAA4D;IAC5D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,8CAA8C;IAC9C,IAAI,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACxC;AAiBD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACzC,IAAI,EAAE,wBAAwB,GAC5B,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAwBjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACvC,IAAI,EAAE;IACL,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAA;IACxF,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B,EACD,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,IAAI,CASN"}
1
+ {"version":3,"file":"compile-worker-entry.d.ts","sourceRoot":"","sources":["../src/compile-worker-entry.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,kBAAkB;IAClC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAC5E;AAED,MAAM,MAAM,aAAa,GAAG,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,OAAO,EACpB,OAAO,EAAE,kBAAkB,KACvB,OAAO,CAAC,aAAa,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AAE9C,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3B;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACV;AAED,8EAA8E;AAC9E,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,gBAAgB,CAAA;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,qBAAqB,GAC9B,mBAAmB,GAAG,iBAAiB,GAAG,0BAA0B,CAAA;AAEvE,MAAM,WAAW,wBAAwB;IACxC;;;;;OAKG;IACH,YAAY,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAC1D,4DAA4D;IAC5D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,8CAA8C;IAC9C,IAAI,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACzC,IAAI,EAAE,wBAAwB,GAC5B,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CA4CjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACvC,IAAI,EAAE;IACL,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAA;IACxF,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B,EACD,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,IAAI,CASN"}
@@ -19,17 +19,54 @@ function isBuildMessage(msg) {
19
19
  && msg !== null
20
20
  && msg.cmd === 'build');
21
21
  }
22
+ function isCommandWithId(msg, cmd) {
23
+ return (typeof msg === 'object'
24
+ && msg !== null
25
+ && msg.cmd === cmd
26
+ && typeof msg.id === 'string');
27
+ }
22
28
  /**
23
- * Build the fork-side IPC message handler. Non-build messages are ignored
29
+ * Build the fork-side IPC message handler. Unknown messages are ignored
24
30
  * (no compiler load, no reply). A build command chdirs into the project,
25
31
  * runs the compiler exactly as the old in-process call sites did, and ALWAYS
26
32
  * replies — `@dimina/compiler` swallows compile errors internally (resolves
27
33
  * undefined), which is normalized to `appInfo: null`; a genuine throw still
28
34
  * replies with `error.message` so the parent never hangs on a lost build.
35
+ *
36
+ * Two warm-standby commands ride the same channel:
37
+ * - `{ cmd: 'ping', id }` → `{ type: 'pong', id }`. Pure liveness; never
38
+ * loads the compiler (a health check on a cold-idle spare must stay cheap).
39
+ * - `{ cmd: 'prewarm', id }` → loads the compiler into the SAME cache the
40
+ * build path uses, then awaits `deps.warmPool?.()`, and always replies
41
+ * `{ type: 'prewarm-result', id, ok, error? }` (a failed prewarm reports —
42
+ * it never throws out of the handler). Deliberately NO chdir: a prewarmed
43
+ * spare stays project-agnostic, which is what makes adopting it into ANY
44
+ * next-opened project safe.
29
45
  */
30
46
  export function createCompileWorkerHandler(deps) {
31
47
  let cachedBuild = null;
32
48
  return async (msg) => {
49
+ if (isCommandWithId(msg, 'ping')) {
50
+ deps.send({ type: 'pong', id: msg.id });
51
+ return;
52
+ }
53
+ if (isCommandWithId(msg, 'prewarm')) {
54
+ try {
55
+ if (!cachedBuild)
56
+ cachedBuild = await deps.loadCompiler();
57
+ await deps.warmPool?.();
58
+ deps.send({ type: 'prewarm-result', id: msg.id, ok: true });
59
+ }
60
+ catch (err) {
61
+ deps.send({
62
+ type: 'prewarm-result',
63
+ id: msg.id,
64
+ ok: false,
65
+ error: err instanceof Error ? err.message : String(err),
66
+ });
67
+ }
68
+ return;
69
+ }
33
70
  if (!isBuildMessage(msg))
34
71
  return;
35
72
  try {
@@ -107,6 +144,13 @@ if (isForkedWorkerEntry) {
107
144
  // workers WARM across rebuilds). ESM package → dynamic import (works on Node 20+);
108
145
  // its default export is the pooled build fn.
109
146
  loadCompiler: async () => (await import('@dimina-kit/compiler/pool-node')).default,
147
+ // Deep prewarm for the warm-standby spare: spin up the resident pool's stage
148
+ // workers ahead of the first build. Optional-chained so an older compiler
149
+ // package without the export degrades to load-only prewarm.
150
+ warmPool: async () => {
151
+ const pool = await import('@dimina-kit/compiler/pool-node');
152
+ await pool.warmDefaultPool?.();
153
+ },
110
154
  chdir: dir => process.chdir(dir),
111
155
  send: (msg) => {
112
156
  process.send?.(msg);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compile-worker-standby-hardening.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-worker-standby-hardening.test.d.ts","sourceRoot":"","sources":["../src/compile-worker-standby-hardening.test.ts"],"names":[],"mappings":""}