@dimina-kit/devkit 0.1.2-dev.20260703101348 → 0.1.2
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/README.md +16 -0
- package/dist/compile-worker-adopt.test.d.ts +2 -0
- package/dist/compile-worker-adopt.test.d.ts.map +1 -0
- package/dist/compile-worker-adopt.test.js +187 -0
- package/dist/compile-worker-entry-warmpool.test.d.ts +2 -0
- package/dist/compile-worker-entry-warmpool.test.d.ts.map +1 -0
- package/dist/compile-worker-entry-warmpool.test.js +110 -0
- package/dist/compile-worker-entry.d.ts +35 -5
- package/dist/compile-worker-entry.d.ts.map +1 -1
- package/dist/compile-worker-entry.js +45 -1
- package/dist/compile-worker-standby-hardening.test.d.ts +2 -0
- package/dist/compile-worker-standby-hardening.test.d.ts.map +1 -0
- package/dist/compile-worker-standby-hardening.test.js +212 -0
- package/dist/compile-worker-standby-integration.test.d.ts +2 -0
- package/dist/compile-worker-standby-integration.test.d.ts.map +1 -0
- package/dist/compile-worker-standby-integration.test.js +243 -0
- package/dist/compile-worker-standby.d.ts +60 -0
- package/dist/compile-worker-standby.d.ts.map +1 -0
- package/dist/compile-worker-standby.js +283 -0
- package/dist/compile-worker-standby.test.d.ts +2 -0
- package/dist/compile-worker-standby.test.d.ts.map +1 -0
- package/dist/compile-worker-standby.test.js +356 -0
- package/dist/compile-worker.d.ts +53 -0
- package/dist/compile-worker.d.ts.map +1 -1
- package/dist/compile-worker.js +30 -11
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -4
- package/fe/dimina-fe-container/assets/index.js +8 -8
- package/fe/dimina-fe-container/assets/pageFrame.js +3 -3
- package/fe/dimina-fe-container/assets/service.js +2 -2
- package/fe/dimina-fe-container/dimina-version.json +2 -2
- 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 @@
|
|
|
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 @@
|
|
|
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
|
|
26
|
-
* `require`) or async (a dynamic `import()` of the ESM
|
|
27
|
-
* pool); the handler awaits it either way. Loaded
|
|
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:
|
|
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.
|
|
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
|
|
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.
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"compile-worker-standby-hardening.test.d.ts","sourceRoot":"","sources":["../src/compile-worker-standby-hardening.test.ts"],"names":[],"mappings":""}
|