@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.
Files changed (33) 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 +12 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +53 -4
  29. package/fe/dimina-fe-container/assets/index.js +8 -8
  30. package/fe/dimina-fe-container/assets/pageFrame.js +3 -3
  31. package/fe/dimina-fe-container/assets/service.js +2 -2
  32. package/fe/dimina-fe-container/dimina-version.json +2 -2
  33. package/package.json +3 -3
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Parent-side orchestration of the forked compile worker.
3
+ *
4
+ * One long-lived worker per `openProject` session: the first compile and
5
+ * every watcher rebuild are `{ cmd: 'build' }` IPC messages to the SAME
6
+ * child, so the compiler's module/worker caches stay warm. The parent never
7
+ * calls `process.chdir` — the worker chdirs in its own process (the root
8
+ * motive of this architecture).
9
+ *
10
+ * Crash handling: an unexpected worker death ('exit', 'close' or a lone
11
+ * 'error' event — Node does NOT guarantee an 'exit' after 'error') rejects
12
+ * the in-flight build (so the rebuild scheduler settles instead of hanging)
13
+ * and the NEXT build lazily re-forks a fresh worker. The death handler is
14
+ * idempotent and generation-guarded: a dead child's late 'exit' can never
15
+ * settle a build that belongs to a fresh worker. `close()` kills, rejects
16
+ * the in-flight build itself, and resolves on the child's first death event
17
+ * ('exit'/'close', or a lone 'error') — it never re-forks
18
+ * (refill-on-graceful-close would wedge process teardown).
19
+ */
20
+ import { type ChildProcess } from 'node:child_process';
1
21
  import type { WorkerAppInfo, WorkerBuildOptions } from './compile-worker-entry.js';
2
22
  export interface CompileLogEntry {
3
23
  stream: 'stdout' | 'stderr';
@@ -6,6 +26,16 @@ export interface CompileLogEntry {
6
26
  export interface CompileWorkerOptions {
7
27
  /** Filtered dmcc log lines (already through `filterDmccLogLine`). */
8
28
  onLog?: (entry: CompileLogEntry) => void;
29
+ /**
30
+ * An already-forked compile-worker-entry process to reuse for the first
31
+ * build (a warm-standby hand-off) instead of paying a fresh fork + compiler
32
+ * import. Must be an IPC fork with piped stdio — the same shape spawnWorker
33
+ * produces. Pure accelerator: an adoptee that is already dead (or dies
34
+ * before the first build) is simply skipped and the next build forks fresh.
35
+ * Once adopted, the process is owned by this worker exactly like a
36
+ * self-forked one — close() kills it, its death rejects in-flight builds.
37
+ */
38
+ adopt?: ChildProcess;
9
39
  }
10
40
  export interface BuildRequest {
11
41
  projectPath: string;
@@ -29,5 +59,28 @@ export interface CompileWorker {
29
59
  */
30
60
  close: () => Promise<void>;
31
61
  }
62
+ /**
63
+ * Resolve the fork target. In the published build this is the compiled
64
+ * `dist/compile-worker-entry.js` sibling; under vitest/dev the module runs
65
+ * from `src/`, where only the `.ts` source exists — the entry is a
66
+ * zero-dependency, erasable-syntax-only shell by design, so Node can run it
67
+ * with type stripping (default since 22.18; behind `--experimental-strip-types`
68
+ * on 22.6–22.17, which `workerExecArgv` passes for a `.ts` entry).
69
+ *
70
+ * Exported (with `workerExecArgv`) as the single authority on the fork shape:
71
+ * the warm-standby manager forks the SAME entry with the SAME execArgv, so a
72
+ * spare is indistinguishable from a worker this module forks itself.
73
+ */
74
+ export declare function resolveWorkerEntry(): string;
75
+ /**
76
+ * execArgv for the fork. A compiled `.js` entry (the published build, engines
77
+ * `node >=20`) needs nothing. A `.ts` entry (vitest/dev only) needs Node type
78
+ * stripping: `--experimental-strip-types` exists since 22.6 — on older Node
79
+ * the child would die at startup with an opaque ERR_UNKNOWN_FILE_EXTENSION /
80
+ * bad-option crash, so fail HERE with an actionable message instead. The
81
+ * ExperimentalWarning is silenced because the child's stderr is the dmcc log
82
+ * transport. On ≥22.18 (stripping on by default) the flag is still accepted.
83
+ */
84
+ export declare function workerExecArgv(entry: string): string[];
32
85
  export declare function createCompileWorker(opts?: CompileWorkerOptions): CompileWorker;
33
86
  //# sourceMappingURL=compile-worker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compile-worker.d.ts","sourceRoot":"","sources":["../src/compile-worker.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAuB,MAAM,2BAA2B,CAAA;AAIvG,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,kBAAkB,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B;;;;;OAKG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC3D;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAsCD,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB,GAAG,aAAa,CA6KlF"}
1
+ {"version":3,"file":"compile-worker.d.ts","sourceRoot":"","sources":["../src/compile-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAM5D,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAuB,MAAM,2BAA2B,CAAA;AAIvG,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,kBAAkB,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B;;;;;OAKG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC3D;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAWtD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB,GAAG,aAAa,CA8LlF"}
@@ -30,8 +30,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
30
  * zero-dependency, erasable-syntax-only shell by design, so Node can run it
31
31
  * with type stripping (default since 22.18; behind `--experimental-strip-types`
32
32
  * on 22.6–22.17, which `workerExecArgv` passes for a `.ts` entry).
33
+ *
34
+ * Exported (with `workerExecArgv`) as the single authority on the fork shape:
35
+ * the warm-standby manager forks the SAME entry with the SAME execArgv, so a
36
+ * spare is indistinguishable from a worker this module forks itself.
33
37
  */
34
- function resolveWorkerEntry() {
38
+ export function resolveWorkerEntry() {
35
39
  const js = path.join(__dirname, 'compile-worker-entry.js');
36
40
  if (fs.existsSync(js))
37
41
  return js;
@@ -46,7 +50,7 @@ function resolveWorkerEntry() {
46
50
  * ExperimentalWarning is silenced because the child's stderr is the dmcc log
47
51
  * transport. On ≥22.18 (stripping on by default) the flag is still accepted.
48
52
  */
49
- function workerExecArgv(entry) {
53
+ export function workerExecArgv(entry) {
50
54
  if (!entry.endsWith('.ts'))
51
55
  return [];
52
56
  const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
@@ -112,15 +116,10 @@ export function createCompileWorker(opts = {}) {
112
116
  stream.on('end', flush);
113
117
  stream.on('close', flush);
114
118
  }
115
- function spawnWorker() {
116
- const entry = resolveWorkerEntry();
117
- const worker = fork(entry, [], {
118
- // Pipe stdout/stderr back to the parent — the dmcc log transport.
119
- silent: true,
120
- // Explicit execArgv, NOT the parent's (vitest/electron loaders would
121
- // leak into a plain Node child).
122
- execArgv: workerExecArgv(entry),
123
- });
119
+ // Attach the log pipes, result routing and death handling to a worker
120
+ // process the SAME wiring whether the process was self-forked or adopted
121
+ // from a warm standby (an inline copy for one of the two would drift).
122
+ function wireWorker(worker) {
124
123
  attachLineReader(worker.stdout, 'stdout');
125
124
  attachLineReader(worker.stderr, 'stderr');
126
125
  // Shared, idempotent death handler for 'exit' / 'close' / 'error': the
@@ -177,6 +176,26 @@ export function createCompileWorker(opts = {}) {
177
176
  worker.on('error', (err) => settleDeath(`compile worker errored: ${err.message}`));
178
177
  return worker;
179
178
  }
179
+ function spawnWorker() {
180
+ const entry = resolveWorkerEntry();
181
+ return wireWorker(fork(entry, [], {
182
+ // Pipe stdout/stderr back to the parent — the dmcc log transport.
183
+ silent: true,
184
+ // Explicit execArgv, NOT the parent's (vitest/electron loaders would
185
+ // leak into a plain Node child).
186
+ execArgv: workerExecArgv(entry),
187
+ }));
188
+ }
189
+ // Adopt the warm-standby hand-off, if any, at creation time — wiring
190
+ // immediately (not at the first build) so a spare that dies in the gap is
191
+ // handled by the normal death path (child cleared → next build re-forks)
192
+ // instead of being discovered as a broken pipe mid-build. An adoptee that
193
+ // is ALREADY dead is skipped outright: its death events fired before any
194
+ // listener could attach, so wiring it would leave a permanently wedged
195
+ // child slot.
196
+ if (opts.adopt && opts.adopt.exitCode === null && opts.adopt.signalCode === null && opts.adopt.connected) {
197
+ child = wireWorker(opts.adopt);
198
+ }
180
199
  function runBuild(req) {
181
200
  if (closed) {
182
201
  return Promise.reject(new Error('compile worker is closed'));
package/dist/index.d.ts CHANGED
@@ -1,9 +1,21 @@
1
1
  import type { CompileLogEntry } from './compile-worker.js';
2
+ import type { CompileWorkerStandby, CompileWorkerStandbyOptions } from './compile-worker-standby.js';
2
3
  export { createRebuildScheduler } from './rebuild-scheduler.js';
3
4
  export type { RebuildScheduler } from './rebuild-scheduler.js';
4
5
  export { filterDmccLogLine } from './compile-log.js';
5
6
  export { createCompileWorker } from './compile-worker.js';
6
7
  export type { CompileLogEntry, CompileWorker } from './compile-worker.js';
8
+ export { createCompileWorkerStandby } from './compile-worker-standby.js';
9
+ export type { CompileWorkerStandby, CompileWorkerStandbyOptions, StandbyEvent, StandbyState, } from './compile-worker-standby.js';
10
+ /**
11
+ * Turn on the warm-standby accelerator and start warming the first spare
12
+ * immediately. Idempotent while the returned manager is live — repeat calls
13
+ * return the SAME manager (their opts are ignored). After `dispose()` (host
14
+ * shutdown, or a test tearing down) a new call builds a fresh manager. The
15
+ * caller owns disposal; a disposed manager makes every openProject fall back
16
+ * to cold forks and never spawns again.
17
+ */
18
+ export declare function enableCompileWorkerStandby(opts?: CompileWorkerStandbyOptions): CompileWorkerStandby;
7
19
  export interface AppInfo {
8
20
  appId: string;
9
21
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAgB,eAAe,EAAiB,MAAM,qBAAqB,CAAA;AAEvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAyCzE,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,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;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACvC;AAuFD,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwInF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACpC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GACrC;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CA2DtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAgB,eAAe,EAAiB,MAAM,qBAAqB,CAAA;AAEvF,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAA;AAEpG,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAA;AACxE,YAAY,EACX,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,EACZ,YAAY,GACZ,MAAM,6BAA6B,CAAA;AAYpC;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACzC,IAAI,CAAC,EAAE,2BAA2B,GAChC,oBAAoB,CAKtB;AAqDD,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,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;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACvC;AAuFD,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAoJnF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACpC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GACrC;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CA2DtD"}
package/dist/index.js CHANGED
@@ -8,9 +8,46 @@ import { createRequire } from 'node:module';
8
8
  import chokidar from 'chokidar';
9
9
  import { createRebuildScheduler } from './rebuild-scheduler.js';
10
10
  import { createCompileWorker } from './compile-worker.js';
11
+ import { createCompileWorkerStandby } from './compile-worker-standby.js';
11
12
  export { createRebuildScheduler } from './rebuild-scheduler.js';
12
13
  export { filterDmccLogLine } from './compile-log.js';
13
14
  export { createCompileWorker } from './compile-worker.js';
15
+ export { createCompileWorkerStandby } from './compile-worker-standby.js';
16
+ // ── warm standby (opt-in) ──────────────────────────────────────────────────
17
+ //
18
+ // One module-level spare compile worker shared by every `openProject` in this
19
+ // process. While no project is open the spare sits forked + prewarmed
20
+ // (project-agnostic — it never chdirs, see compile-worker-entry); openProject
21
+ // adopts it for the first compile and each session close refills it. Purely
22
+ // additive: hosts that never call `enableCompileWorkerStandby` get exactly the
23
+ // cold-fork behavior they had before.
24
+ let standbyManager = null;
25
+ /**
26
+ * Turn on the warm-standby accelerator and start warming the first spare
27
+ * immediately. Idempotent while the returned manager is live — repeat calls
28
+ * return the SAME manager (their opts are ignored). After `dispose()` (host
29
+ * shutdown, or a test tearing down) a new call builds a fresh manager. The
30
+ * caller owns disposal; a disposed manager makes every openProject fall back
31
+ * to cold forks and never spawns again.
32
+ */
33
+ export function enableCompileWorkerStandby(opts) {
34
+ if (standbyManager && standbyManager.state !== 'disposed')
35
+ return standbyManager;
36
+ standbyManager = createCompileWorkerStandby(opts);
37
+ standbyManager.ensure();
38
+ return standbyManager;
39
+ }
40
+ /**
41
+ * Refill the spare after a compile worker has been consumed and fully closed.
42
+ * Fire-and-forget BY DESIGN: the fork happens asynchronously inside the
43
+ * manager, so a session close never waits on (and can never be wedged by)
44
+ * standby work — the refill-on-graceful-close deadlock class stays
45
+ * structurally impossible. No-ops when the standby is off, already warming,
46
+ * or disposed.
47
+ */
48
+ function refillStandby() {
49
+ standbyManager?.ensure();
50
+ }
14
51
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
52
  const require = createRequire(import.meta.url);
16
53
  // esbuild's native binary is shipped inside node_modules, but when packaged
@@ -125,8 +162,12 @@ export async function openProject(opts) {
125
162
  // Compilation runs in a long-lived forked worker — the worker chdirs in
126
163
  // its OWN process, so this (host) process never mutates its cwd, and dmcc
127
164
  // terminal output arrives on the worker's piped streams (delivered to
128
- // `onLog` line-by-line after `filterDmccLogLine`).
129
- const compileWorker = createCompileWorker({ onLog });
165
+ // `onLog` line-by-line after `filterDmccLogLine`). When the warm standby is
166
+ // enabled and holds a healthy spare, adopt it: the first compile then runs
167
+ // on an already-forked, toolchain-warm process. adopt() degrades to null on
168
+ // every failure path, which lands in the normal cold-fork behavior.
169
+ const adopted = standbyManager ? await standbyManager.adopt() : null;
170
+ const compileWorker = createCompileWorker({ onLog, ...(adopted ? { adopt: adopted } : {}) });
130
171
  const buildRequest = {
131
172
  projectPath,
132
173
  outputDir: resolvedOutputDir,
@@ -138,6 +179,9 @@ export async function openProject(opts) {
138
179
  }
139
180
  catch (err) {
140
181
  await compileWorker.close();
182
+ // A failed open consumed the spare too — refill (fire-and-forget) so the
183
+ // NEXT open attempt still starts warm.
184
+ refillStandby();
141
185
  throw err;
142
186
  }
143
187
  // When the compiler is racing (e.g. opening a project that was just
@@ -207,6 +251,7 @@ export async function openProject(opts) {
207
251
  }
208
252
  catch (err) {
209
253
  await cleanupFailedOpen(watcher, compileWorker, server);
254
+ refillStandby();
210
255
  throw err;
211
256
  }
212
257
  // `server` is always assigned when the try block completes without throwing
@@ -220,9 +265,13 @@ export async function openProject(opts) {
220
265
  close: async () => {
221
266
  await watcher?.close();
222
267
  // Kill the long-lived compile worker and WAIT for the child to be
223
- // actually dead — only kill, never re-fork on a graceful close (a
224
- // refill here would wedge process teardown).
268
+ // actually dead — THIS worker is never re-forked on a graceful close
269
+ // (an awaited refill here would wedge process teardown).
225
270
  await compileWorker.close();
271
+ // The standby refill is different: fire-and-forget into the manager
272
+ // (nothing here waits on it), sequenced after the old worker's death
273
+ // so spare + dying worker never hold memory simultaneously.
274
+ refillStandby();
226
275
  liveServer.closeAllConnections?.();
227
276
  await new Promise(resolve => liveServer.close(() => resolve()));
228
277
  },