zod-compiler 1.26.4 → 1.28.0

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 (43) hide show
  1. package/README.md +77 -1
  2. package/dist/jit.d.ts +2 -0
  3. package/dist/jit.d.ts.map +1 -1
  4. package/dist/jit.js +9 -6
  5. package/dist/jit.js.map +1 -1
  6. package/dist/register/config.d.ts +20 -0
  7. package/dist/register/config.d.ts.map +1 -0
  8. package/dist/register/config.js +1 -0
  9. package/dist/register/index.d.ts +1 -0
  10. package/dist/register/index.js +68 -0
  11. package/dist/register/index.js.map +1 -0
  12. package/dist/register/transform.d.ts +16 -0
  13. package/dist/register/transform.d.ts.map +1 -0
  14. package/dist/register/transform.js +148 -0
  15. package/dist/register/transform.js.map +1 -0
  16. package/dist/swc.d.ts +5 -1
  17. package/dist/swc.d.ts.map +1 -1
  18. package/dist/swc.js.map +1 -1
  19. package/dist/turbopack.d.ts +6 -1
  20. package/dist/turbopack.d.ts.map +1 -1
  21. package/dist/turbopack.js.map +1 -1
  22. package/dist/unplugin/disk-cache.d.ts.map +1 -1
  23. package/dist/unplugin/disk-cache.js +1 -0
  24. package/dist/unplugin/disk-cache.js.map +1 -1
  25. package/dist/unplugin/index.d.ts.map +1 -1
  26. package/dist/unplugin/index.js +56 -9
  27. package/dist/unplugin/index.js.map +1 -1
  28. package/dist/unplugin/pool-worker.d.ts +1 -0
  29. package/dist/unplugin/pool-worker.js +116 -0
  30. package/dist/unplugin/pool-worker.js.map +1 -0
  31. package/dist/unplugin/pool.d.ts +162 -0
  32. package/dist/unplugin/pool.d.ts.map +1 -0
  33. package/dist/unplugin/pool.js +445 -0
  34. package/dist/unplugin/pool.js.map +1 -0
  35. package/dist/unplugin/transform.d.ts +2 -1
  36. package/dist/unplugin/transform.d.ts.map +1 -1
  37. package/dist/unplugin/transform.js +1 -1
  38. package/dist/unplugin/transform.js.map +1 -1
  39. package/dist/unplugin/types.d.ts +36 -1
  40. package/dist/unplugin/types.d.ts.map +1 -1
  41. package/dist/unplugin/types.js.map +1 -1
  42. package/package.json +12 -1
  43. package/schema.json +80 -0
@@ -0,0 +1,162 @@
1
+ import { CodegenMode } from "../core/codegen/context.js";
2
+ import { HoistOptions } from "./hoist.js";
3
+ import { BuildStats, TransformOptions } from "./types.js";
4
+ import { TransformSourceMap } from "./transform.js";
5
+ //#region src/unplugin/pool.d.ts
6
+ /** The subset of TransformOptions that survives structured cloning. */
7
+ interface PoolTransformOptions {
8
+ mode: CodegenMode;
9
+ runtimeId?: string | undefined;
10
+ zodCompat?: boolean | undefined;
11
+ compact?: boolean | undefined;
12
+ verbose?: boolean | undefined;
13
+ autoDiscover?: boolean | undefined;
14
+ hoist?: boolean | HoistOptions | undefined;
15
+ }
16
+ /** Main → worker. */
17
+ type PoolRequest = {
18
+ type: "transform";
19
+ seq: number;
20
+ code: string;
21
+ id: string;
22
+ options: PoolTransformOptions;
23
+ } | {
24
+ type: "invalidate";
25
+ seq: number;
26
+ };
27
+ /**
28
+ * What a worker observed while transforming one file: the output plus the
29
+ * signals the main thread's disk-cache bookkeeping needs, which arrive as
30
+ * callbacks in the in-process path and cannot cross a thread boundary.
31
+ */
32
+ interface PoolTransformResult {
33
+ output: {
34
+ code: string;
35
+ map: TransformSourceMap | null;
36
+ } | null;
37
+ discoveryRan: boolean;
38
+ substantialWork: boolean;
39
+ uncacheable: boolean;
40
+ stats: BuildStats | null;
41
+ /**
42
+ * First-party modules this worker executed since its last report, or `null`
43
+ * when the worker cannot track them (Bun/Deno native import).
44
+ */
45
+ newModulePaths: string[] | null;
46
+ }
47
+ /** Worker → main. */
48
+ type PoolResponse = {
49
+ type: "result";
50
+ seq: number;
51
+ result: PoolTransformResult;
52
+ } | {
53
+ type: "error";
54
+ seq: number;
55
+ name: string;
56
+ message: string;
57
+ stack?: string | undefined;
58
+ } | {
59
+ type: "invalidated";
60
+ seq: number;
61
+ };
62
+ /**
63
+ * Raised when the pool itself failed — a worker that would not spawn, or one
64
+ * that died mid-task (OOM is the realistic case; each worker holds a full
65
+ * module graph). Distinct from a transform's own error, which is rethrown with
66
+ * its original identity, because the caller's response differs: an
67
+ * infrastructure failure is worth retrying on the bundler thread, a schema
68
+ * that cannot compile is not.
69
+ */
70
+ declare class PoolUnavailableError extends Error {
71
+ constructor(message: string, options?: {
72
+ cause?: unknown;
73
+ });
74
+ }
75
+ /**
76
+ * Resolve `parallel` to a worker count. `false`/`undefined` disable the pool.
77
+ *
78
+ * `true` leaves one core for the bundler itself — it is still parsing,
79
+ * resolving and generating chunks while transforms run — and then caps at
80
+ * {@link MAX_AUTO_WORKERS}.
81
+ */
82
+ declare function resolvePoolSize(parallel: boolean | number | undefined): number;
83
+ /** Strip the callbacks from TransformOptions; what remains is cloneable. */
84
+ declare function poolTransformOptions(options: TransformOptions): PoolTransformOptions;
85
+ /** Terminate every live transform pool. */
86
+ declare function disposeAllPools(): Promise<void>;
87
+ /**
88
+ * A lazily grown pool of transform workers.
89
+ *
90
+ * Not a general-purpose executor: it knows the transform protocol, and it
91
+ * knows that a broadcast invalidation has to be ordered ahead of every
92
+ * subsequent transform on every worker (per-worker message delivery is FIFO,
93
+ * so posting the broadcast before any later task is sufficient — and the only
94
+ * ordering guarantee available, since a worker mid-transform cannot be
95
+ * interrupted).
96
+ */
97
+ declare class TransformPool {
98
+ private readonly size;
99
+ private readonly entry;
100
+ private readonly workers;
101
+ private readonly queue;
102
+ /** In-flight requests by seq, so a worker exit can reject exactly its own. */
103
+ private readonly inflight;
104
+ private readonly executedModules;
105
+ /** A worker reported it cannot track executed modules — the union is unusable. */
106
+ private moduleTrackingUnavailable;
107
+ private seq;
108
+ private disposed;
109
+ /** Worker deaths since the last successful result; trips MAX_CONSECUTIVE_FAILURES. */
110
+ private consecutiveFailures;
111
+ /** Set once the breaker trips: every later run() rejects without spawning. */
112
+ private brokenPool;
113
+ private constructor();
114
+ /**
115
+ * Build a pool of `size` workers, or return null when workers are
116
+ * unavailable in this installation (the entry could not be located). Callers
117
+ * treat null as "run in-process".
118
+ */
119
+ static create(size: number): TransformPool | null;
120
+ /** Transform one file on a worker. Rejects with the transform's own error, or PoolUnavailableError. */
121
+ run(code: string, id: string, options: PoolTransformOptions): Promise<PoolTransformResult>;
122
+ /**
123
+ * Drop every worker's executed-module cache, so the next discovery re-runs
124
+ * changed schema graphs. Mirrors the loader's invalidateModuleCache() for
125
+ * the in-process path; called from watchChange.
126
+ *
127
+ * Fire-and-forget: the broadcast is posted to every live worker before any
128
+ * later transform can be, and per-worker FIFO delivery does the rest. A
129
+ * transform ALREADY executing in a worker keeps its pre-change module cache
130
+ * — the same window the in-process path has, where invalidation lands
131
+ * between awaits — and the main thread's own result cache is cleared
132
+ * alongside, so the file is recomputed either way.
133
+ */
134
+ invalidate(): void;
135
+ /**
136
+ * Union of the first-party modules every worker has executed, or null when
137
+ * any worker cannot track them. Synchronous, because DiskCache.flushDeferred
138
+ * runs from `buildEnd` and from a process 'exit' hook, where only
139
+ * synchronous work is allowed — which is why workers push deltas rather than
140
+ * answering a query.
141
+ */
142
+ firstPartyModulePaths(): string[] | null;
143
+ /** Terminate every worker. Pending and queued tasks reject. */
144
+ dispose(): Promise<void>;
145
+ /** Hand a task to an idle worker, grow the pool, or queue it. */
146
+ private dispatch;
147
+ private send;
148
+ private spawn;
149
+ private onMessage;
150
+ /**
151
+ * A worker died — OOM while holding a large module graph is the realistic
152
+ * cause. Its task fails with PoolUnavailableError so the caller can retry
153
+ * in-process, and the worker is dropped from the pool; the next dispatch
154
+ * spawns a replacement.
155
+ */
156
+ private onWorkerGone;
157
+ private drain;
158
+ private recordModulePaths;
159
+ }
160
+ //#endregion
161
+ export { PoolRequest, PoolResponse, PoolTransformOptions, PoolTransformResult, PoolUnavailableError, TransformPool, disposeAllPools, poolTransformOptions, resolvePoolSize };
162
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.d.ts","names":[],"sources":["../../src/unplugin/pool.ts"],"mappings":";;;;;;UA8GiB;EACf,MAAM;EACN;EACA;EACA;EACA;EACA;EACA,kBAAkB;;;KAIR;EACN;EAAmB;EAAa;EAAc;EAAY,SAAS;;EACnE;EAAoB;;;;;;;UAOT;EACf;IAAU;IAAc,KAAK;;EAC7B;EACA;EACA;EACA,OAAO;;;;;EAKP;;;KAIU;EACN;EAAgB;EAAa,QAAQ;;EACrC;EAAe;EAAa;EAAc;EAAiB;;EAC3D;EAAqB;;;;;;;;;;cAUd,6BAA6B;EAC5B,YAAA,iBAAiB;IAAY;;;;;;;;;;iBAa3B,gBAAgB;;iBAUhB,qBAAqB,SAAS,mBAAmB;;iBA4F3C,mBAAmB;;;;;;;;;;;cA0B5B;mBACM;mBACA;mBACA;mBACA;;mBAEA;mBACA;;UAET;UACA;UACA;;UAEA;;UAEA;UAED;;;;;;SAUA,OAAO,eAAe;;EAU7B,IAAI,cAAc,YAAY,SAAS,uBAAuB,QAAQ;;;;;;;;;;;;;EA6BtE;;;;;;;;EAgBA;;EAUM,WAAW;;UAYT;UA0BA;UAUA;UAwBA;;;;;;;UAmCA;UAwBA;UA2BA"}
@@ -0,0 +1,445 @@
1
+ import * as fs$1 from "node:fs";
2
+ import { availableParallelism } from "node:os";
3
+ import { Worker } from "node:worker_threads";
4
+ //#region src/unplugin/pool.ts
5
+ /**
6
+ * Worker-thread pool for the unplugin transform.
7
+ *
8
+ * Discovery — executing a schema file and its whole first-party import graph
9
+ * so Zod's constructors run and produce the objects extract walks — is the
10
+ * single most expensive phase of a cold build, and today it runs strictly one
11
+ * file at a time: `loadQueue` in ../loader.ts serializes every jiti import so
12
+ * concurrent transforms cannot double-execute a shared dependency, and the
13
+ * bundler itself is one process. Nothing about the work requires that. Each
14
+ * file's transform reads only `(code, id, options)` and returns
15
+ * `{ code, map }` plus a few flags — the shape of a task, not of a stage.
16
+ *
17
+ * So the pool moves whole transforms off the bundler thread. Each worker owns
18
+ * its own loader, its own jiti instance and its own copy of zod; nothing is
19
+ * shared, which is exactly why the serialization inside a worker is harmless.
20
+ * Measured on 120 files of deeply nested schemas: 3,633 ms sequential →
21
+ * 1,508 ms at four workers. The pool is spawned LAZILY — a project with three
22
+ * schema files starts three workers, not one per core.
23
+ *
24
+ * ## When it does NOT pay
25
+ *
26
+ * Independence is the whole bet, and it is a bet about the user's import
27
+ * graph, not about their machine. In-process, the loader executes a shared
28
+ * dependency ONCE for the whole build; N workers execute it N times. So the
29
+ * win tracks how disjoint the schema files' graphs are, and it can invert: on
30
+ * a fixture of the same size whose files form a 120-deep chain, four workers
31
+ * measured 1,045 ms against 945 ms in-process — a small loss — and twelve
32
+ * measured 3,332 ms, because each worker was re-executing most of the project
33
+ * to compile its share of it. The same files with the chain removed went
34
+ * 636 ms → 341 ms.
35
+ *
36
+ * That, plus N copies of the graph in memory, is why this is opt-in rather
37
+ * than the default — see ZodCompilerPluginOptions.
38
+ *
39
+ * ## What still runs on the bundler thread
40
+ *
41
+ * Everything that touches shared state: the disk cache (one directory, one GC,
42
+ * atomic writes), the static dependency crawl (pure over the filesystem, and
43
+ * memoized across files — sharding it would multiply the memo instead of the
44
+ * throughput), and the in-memory result cache. Workers do the executing and
45
+ * the generating; the main thread does the bookkeeping.
46
+ *
47
+ * ## Executed-module reporting
48
+ *
49
+ * The disk cache needs `getFirstPartyModulePaths()` — the superset of modules
50
+ * the loader has executed — to persist entries whose static dep crawl came
51
+ * back incomplete (the common outcome on large graphs: one non-literal dynamic
52
+ * import anywhere poisons the closure). With discovery in workers, the main
53
+ * thread's loader executes nothing, so that superset would be empty and every
54
+ * deferred entry would be silently dropped — no stale results, but a large
55
+ * chunk of the cache quietly stops working.
56
+ *
57
+ * Workers therefore report the paths they have executed back with every
58
+ * result, as a DELTA against what they have already reported (the full list
59
+ * grows into the thousands and would be re-sent per file). The pool unions
60
+ * those deltas into {@link TransformPool.firstPartyModulePaths}, which the
61
+ * DiskCache reads synchronously at flush time exactly as it reads the loader's
62
+ * own. A worker that cannot track modules at all — Bun/Deno, where native
63
+ * import has no evictable cache — reports `null`, and the union degrades to
64
+ * `null` so the cache declines to persist rather than guessing.
65
+ */
66
+ /**
67
+ * Upper bound on pool size for `parallel: true`.
68
+ *
69
+ * Four, and not "one per core", because throughput peaks there and then goes
70
+ * BACKWARDS. Measured on 120 files of deeply nested schemas (960 schemas), on
71
+ * a machine with 12 performance cores:
72
+ *
73
+ * in-process 3,633 ms · n=2 2,263 · n=4 1,508 · n=6 1,568 · n=8 1,786 · n=12 2,119
74
+ *
75
+ * Past four the bottleneck stops being CPU. Every worker re-executes whatever
76
+ * of the project graph its files reach, holds its own copy of it, and ships
77
+ * megabytes of generated source back through structured cloning that the ONE
78
+ * receiving thread has to deserialize. More workers multiply the first two
79
+ * costs and contend on the third.
80
+ *
81
+ * The same effect makes the cap protective rather than merely tidy: on a
82
+ * fixture whose files form a 120-deep import chain — where the in-process
83
+ * loader executes the shared graph once and every worker re-executes most of
84
+ * it — n=4 costs 1,045 ms against 945 ms in-process, while n=12 costs 3,332 ms.
85
+ * Overshooting turns a small loss into a rout. An explicit `parallel: <n>`
86
+ * overrides this for anyone who has measured their own graph.
87
+ */
88
+ const MAX_AUTO_WORKERS = 4;
89
+ /** Hard ceiling on an explicit `parallel: <n>`, guarding a typo'd 400. */
90
+ const MAX_WORKERS = 32;
91
+ /**
92
+ * Consecutive worker deaths before the pool gives up for the rest of the build.
93
+ *
94
+ * Falling back per file is the right response to ONE unlucky worker (an OOM
95
+ * under a particularly large graph). It is the wrong response to a pool that
96
+ * cannot work at all — a partial install, a Node build without worker support
97
+ * — where every file would spawn a worker, wait for it to die, warn, and then
98
+ * do the transform in-process anyway. Tripping the breaker converts that into
99
+ * a normal serial build after the first few files.
100
+ */
101
+ const MAX_CONSECUTIVE_FAILURES = 3;
102
+ /**
103
+ * Raised when the pool itself failed — a worker that would not spawn, or one
104
+ * that died mid-task (OOM is the realistic case; each worker holds a full
105
+ * module graph). Distinct from a transform's own error, which is rethrown with
106
+ * its original identity, because the caller's response differs: an
107
+ * infrastructure failure is worth retrying on the bundler thread, a schema
108
+ * that cannot compile is not.
109
+ */
110
+ var PoolUnavailableError = class extends Error {
111
+ constructor(message, options) {
112
+ super(message, options);
113
+ this.name = "PoolUnavailableError";
114
+ }
115
+ };
116
+ /**
117
+ * Resolve `parallel` to a worker count. `false`/`undefined` disable the pool.
118
+ *
119
+ * `true` leaves one core for the bundler itself — it is still parsing,
120
+ * resolving and generating chunks while transforms run — and then caps at
121
+ * {@link MAX_AUTO_WORKERS}.
122
+ */
123
+ function resolvePoolSize(parallel) {
124
+ if (parallel === void 0 || parallel === false) return 0;
125
+ if (parallel === true) return Math.min(Math.max(availableParallelism() - 1, 1), MAX_AUTO_WORKERS);
126
+ if (!Number.isFinite(parallel)) return 0;
127
+ return Math.min(Math.max(Math.floor(parallel), 1), MAX_WORKERS);
128
+ }
129
+ /** Strip the callbacks from TransformOptions; what remains is cloneable. */
130
+ function poolTransformOptions(options) {
131
+ return {
132
+ mode: options.mode,
133
+ runtimeId: options.runtimeId,
134
+ zodCompat: options.zodCompat,
135
+ compact: options.compact,
136
+ verbose: options.verbose,
137
+ autoDiscover: options.autoDiscover,
138
+ hoist: options.hoist
139
+ };
140
+ }
141
+ /**
142
+ * Locate the worker entry next to this module.
143
+ *
144
+ * Published, that is `dist/unplugin/pool-worker.js` and a plain dynamic import
145
+ * loads it. Running from source — this repository's own test suite resolves
146
+ * `#src/**` through Vite — only the `.ts` sibling exists, and a worker starts
147
+ * outside Vite's pipeline, in bare Node. Node's native type stripping is not
148
+ * enough there: it erases types but does not remap the `./transform.js`
149
+ * specifiers TypeScript requires onto the `.ts` files that actually exist. So
150
+ * the bootstrap hands `.ts` entries to jiti, which resolves them the same way
151
+ * the loader already resolves user schema files.
152
+ *
153
+ * Returns null when neither exists — the plugin's own code has been bundled
154
+ * into a single file by a consumer, so `import.meta.url` no longer points at a
155
+ * directory containing it. The caller falls back to in-process transforms.
156
+ */
157
+ function resolveWorkerEntry() {
158
+ for (const [specifier, isTypeScript] of [["./pool-worker.js", false], ["./pool-worker.ts", true]]) {
159
+ const url = new URL(specifier, import.meta.url);
160
+ if (url.protocol !== "file:") continue;
161
+ try {
162
+ if (fs$1.statSync(url, { throwIfNoEntry: false })?.isFile()) return {
163
+ href: url.href,
164
+ isTypeScript
165
+ };
166
+ } catch {}
167
+ }
168
+ return null;
169
+ }
170
+ /**
171
+ * CommonJS bootstrap evaluated inside each worker (`eval: true`). A string
172
+ * rather than a file because it has to work identically from `dist/` and from
173
+ * `src/`, where no `.js` sibling exists to point a Worker at.
174
+ */
175
+ const WORKER_BOOTSTRAP = `
176
+ const { workerData } = require("node:worker_threads");
177
+ (async () => {
178
+ if (workerData.isTypeScript) {
179
+ const { createJiti } = await import(workerData.jitiSpecifier);
180
+ await createJiti(workerData.entry).import(workerData.entry);
181
+ } else {
182
+ await import(workerData.entry);
183
+ }
184
+ })().catch((error) => {
185
+ // Reaching the parent as an 'error' event is the whole point: a worker that
186
+ // cannot load its entry must fail loudly enough for the pool to fall back,
187
+ // not sit idle holding a task.
188
+ setTimeout(() => { throw error; });
189
+ });
190
+ `;
191
+ /** Resolve jiti's URL for the bootstrap; bare specifier if resolution is unavailable. */
192
+ function jitiSpecifier() {
193
+ try {
194
+ return import.meta.resolve("jiti");
195
+ } catch {
196
+ return "jiti";
197
+ }
198
+ }
199
+ /**
200
+ * Live pools, so a host that outlives its bundler can shut them all down.
201
+ *
202
+ * Nothing in the plugin lifecycle disposes a pool on its own: workers are kept
203
+ * warm ACROSS watch rebuilds on purpose — invalidation drops first-party
204
+ * modules but leaves node_modules (zod itself, the expensive bulk) executed,
205
+ * and discarding that per rebuild would re-pay boot plus a cold zod on every
206
+ * keystroke. Idle workers are unref'd, so they never hold a process open and a
207
+ * one-shot build exits normally. This registry exists for hosts that need
208
+ * determinism instead — chiefly this package's own test suite, which builds
209
+ * many plugin instances in one process.
210
+ */
211
+ const livePools = /* @__PURE__ */ new Set();
212
+ /** Terminate every live transform pool. */
213
+ async function disposeAllPools() {
214
+ await Promise.all([...livePools].map((pool) => pool.dispose()));
215
+ }
216
+ /**
217
+ * A lazily grown pool of transform workers.
218
+ *
219
+ * Not a general-purpose executor: it knows the transform protocol, and it
220
+ * knows that a broadcast invalidation has to be ordered ahead of every
221
+ * subsequent transform on every worker (per-worker message delivery is FIFO,
222
+ * so posting the broadcast before any later task is sufficient — and the only
223
+ * ordering guarantee available, since a worker mid-transform cannot be
224
+ * interrupted).
225
+ */
226
+ var TransformPool = class TransformPool {
227
+ size;
228
+ entry;
229
+ workers = [];
230
+ queue = [];
231
+ /** In-flight requests by seq, so a worker exit can reject exactly its own. */
232
+ inflight = /* @__PURE__ */ new Map();
233
+ executedModules = /* @__PURE__ */ new Set();
234
+ /** A worker reported it cannot track executed modules — the union is unusable. */
235
+ moduleTrackingUnavailable = false;
236
+ seq = 0;
237
+ disposed = false;
238
+ /** Worker deaths since the last successful result; trips MAX_CONSECUTIVE_FAILURES. */
239
+ consecutiveFailures = 0;
240
+ /** Set once the breaker trips: every later run() rejects without spawning. */
241
+ brokenPool = false;
242
+ constructor(size, entry) {
243
+ this.size = size;
244
+ this.entry = entry;
245
+ }
246
+ /**
247
+ * Build a pool of `size` workers, or return null when workers are
248
+ * unavailable in this installation (the entry could not be located). Callers
249
+ * treat null as "run in-process".
250
+ */
251
+ static create(size) {
252
+ if (size <= 0) return null;
253
+ const entry = resolveWorkerEntry();
254
+ if (entry === null) return null;
255
+ const pool = new TransformPool(size, entry);
256
+ livePools.add(pool);
257
+ return pool;
258
+ }
259
+ /** Transform one file on a worker. Rejects with the transform's own error, or PoolUnavailableError. */
260
+ run(code, id, options) {
261
+ if (this.disposed) return Promise.reject(new PoolUnavailableError("Transform pool is disposed"));
262
+ if (this.brokenPool) return Promise.reject(new PoolUnavailableError(`Transform workers failed ${MAX_CONSECUTIVE_FAILURES} times in a row; this build is running in-process`));
263
+ return new Promise((resolve, reject) => {
264
+ const request = {
265
+ type: "transform",
266
+ seq: this.seq++,
267
+ code,
268
+ id,
269
+ options
270
+ };
271
+ this.dispatch({
272
+ request,
273
+ resolve,
274
+ reject
275
+ });
276
+ });
277
+ }
278
+ /**
279
+ * Drop every worker's executed-module cache, so the next discovery re-runs
280
+ * changed schema graphs. Mirrors the loader's invalidateModuleCache() for
281
+ * the in-process path; called from watchChange.
282
+ *
283
+ * Fire-and-forget: the broadcast is posted to every live worker before any
284
+ * later transform can be, and per-worker FIFO delivery does the rest. A
285
+ * transform ALREADY executing in a worker keeps its pre-change module cache
286
+ * — the same window the in-process path has, where invalidation lands
287
+ * between awaits — and the main thread's own result cache is cleared
288
+ * alongside, so the file is recomputed either way.
289
+ */
290
+ invalidate() {
291
+ if (this.disposed) return;
292
+ this.executedModules.clear();
293
+ this.moduleTrackingUnavailable = false;
294
+ for (const entry of this.workers) entry.worker.postMessage({
295
+ type: "invalidate",
296
+ seq: this.seq++
297
+ });
298
+ }
299
+ /**
300
+ * Union of the first-party modules every worker has executed, or null when
301
+ * any worker cannot track them. Synchronous, because DiskCache.flushDeferred
302
+ * runs from `buildEnd` and from a process 'exit' hook, where only
303
+ * synchronous work is allowed — which is why workers push deltas rather than
304
+ * answering a query.
305
+ */
306
+ firstPartyModulePaths() {
307
+ if (this.moduleTrackingUnavailable) return null;
308
+ return [...this.executedModules];
309
+ }
310
+ /** Terminate every worker. Pending and queued tasks reject. */
311
+ async dispose() {
312
+ if (this.disposed) return;
313
+ this.disposed = true;
314
+ livePools.delete(this);
315
+ const error = new PoolUnavailableError("Transform pool was disposed");
316
+ for (const task of this.queue.splice(0)) task.reject(error);
317
+ for (const task of this.inflight.values()) task.reject(error);
318
+ this.inflight.clear();
319
+ await Promise.all(this.workers.splice(0).map((entry) => entry.worker.terminate()));
320
+ }
321
+ /** Hand a task to an idle worker, grow the pool, or queue it. */
322
+ dispatch(task) {
323
+ const idle = this.workers.find((entry) => entry.busy === null);
324
+ if (idle !== void 0) {
325
+ this.send(idle, task);
326
+ return;
327
+ }
328
+ if (this.workers.length < this.size) {
329
+ let created;
330
+ try {
331
+ created = this.spawn();
332
+ } catch (error) {
333
+ task.reject(new PoolUnavailableError(`Could not start a zod-compiler transform worker: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
334
+ return;
335
+ }
336
+ this.workers.push(created);
337
+ this.send(created, task);
338
+ return;
339
+ }
340
+ this.queue.push(task);
341
+ }
342
+ send(entry, task) {
343
+ entry.busy = task.request.seq;
344
+ this.inflight.set(task.request.seq, task);
345
+ entry.worker.ref();
346
+ entry.worker.postMessage(task.request);
347
+ }
348
+ spawn() {
349
+ const worker = new Worker(WORKER_BOOTSTRAP, {
350
+ eval: true,
351
+ workerData: {
352
+ entry: this.entry.href,
353
+ isTypeScript: this.entry.isTypeScript,
354
+ jitiSpecifier: jitiSpecifier()
355
+ }
356
+ });
357
+ const entry = {
358
+ worker,
359
+ busy: null
360
+ };
361
+ worker.unref();
362
+ worker.on("message", (message) => {
363
+ this.onMessage(entry, message);
364
+ });
365
+ worker.on("error", (error) => {
366
+ this.onWorkerGone(entry, error);
367
+ });
368
+ worker.on("exit", (code) => {
369
+ if (this.disposed) return;
370
+ this.onWorkerGone(entry, /* @__PURE__ */ new Error(`worker exited with code ${code}`));
371
+ });
372
+ return entry;
373
+ }
374
+ onMessage(entry, message) {
375
+ if (message.type === "invalidated") return;
376
+ const task = this.inflight.get(message.seq);
377
+ this.inflight.delete(message.seq);
378
+ if (entry.busy === message.seq) {
379
+ entry.busy = null;
380
+ entry.worker.unref();
381
+ }
382
+ if (task !== void 0) if (message.type === "result") {
383
+ this.consecutiveFailures = 0;
384
+ this.recordModulePaths(message.result.newModulePaths);
385
+ task.resolve(message.result);
386
+ } else {
387
+ const error = new Error(message.message);
388
+ error.name = message.name;
389
+ if (message.stack !== void 0) error.stack = message.stack;
390
+ task.reject(error);
391
+ }
392
+ this.drain();
393
+ }
394
+ /**
395
+ * A worker died — OOM while holding a large module graph is the realistic
396
+ * cause. Its task fails with PoolUnavailableError so the caller can retry
397
+ * in-process, and the worker is dropped from the pool; the next dispatch
398
+ * spawns a replacement.
399
+ */
400
+ onWorkerGone(entry, cause) {
401
+ const index = this.workers.indexOf(entry);
402
+ if (index === -1) return;
403
+ this.workers.splice(index, 1);
404
+ if (++this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) this.brokenPool = true;
405
+ const seq = entry.busy;
406
+ entry.busy = null;
407
+ if (seq !== null) {
408
+ const task = this.inflight.get(seq);
409
+ this.inflight.delete(seq);
410
+ task?.reject(new PoolUnavailableError(`zod-compiler transform worker failed: ${cause.message}`, { cause }));
411
+ }
412
+ this.drain();
413
+ }
414
+ drain() {
415
+ if (this.brokenPool) {
416
+ const error = new PoolUnavailableError(`Transform workers failed ${MAX_CONSECUTIVE_FAILURES} times in a row; this build is running in-process`);
417
+ for (const task of this.queue.splice(0)) task.reject(error);
418
+ return;
419
+ }
420
+ while (this.queue.length > 0) {
421
+ const idle = this.workers.find((e) => e.busy === null);
422
+ if (idle === void 0) {
423
+ if (this.workers.length >= this.size) return;
424
+ const task = this.queue.shift();
425
+ if (task === void 0) return;
426
+ this.dispatch(task);
427
+ continue;
428
+ }
429
+ const task = this.queue.shift();
430
+ if (task === void 0) return;
431
+ this.send(idle, task);
432
+ }
433
+ }
434
+ recordModulePaths(paths) {
435
+ if (paths === null) {
436
+ this.moduleTrackingUnavailable = true;
437
+ return;
438
+ }
439
+ for (const p of paths) this.executedModules.add(p);
440
+ }
441
+ };
442
+ //#endregion
443
+ export { PoolUnavailableError, TransformPool, disposeAllPools, poolTransformOptions, resolvePoolSize };
444
+
445
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","names":["fs"],"sources":["../../src/unplugin/pool.ts"],"sourcesContent":["/**\n * Worker-thread pool for the unplugin transform.\n *\n * Discovery — executing a schema file and its whole first-party import graph\n * so Zod's constructors run and produce the objects extract walks — is the\n * single most expensive phase of a cold build, and today it runs strictly one\n * file at a time: `loadQueue` in ../loader.ts serializes every jiti import so\n * concurrent transforms cannot double-execute a shared dependency, and the\n * bundler itself is one process. Nothing about the work requires that. Each\n * file's transform reads only `(code, id, options)` and returns\n * `{ code, map }` plus a few flags — the shape of a task, not of a stage.\n *\n * So the pool moves whole transforms off the bundler thread. Each worker owns\n * its own loader, its own jiti instance and its own copy of zod; nothing is\n * shared, which is exactly why the serialization inside a worker is harmless.\n * Measured on 120 files of deeply nested schemas: 3,633 ms sequential →\n * 1,508 ms at four workers. The pool is spawned LAZILY — a project with three\n * schema files starts three workers, not one per core.\n *\n * ## When it does NOT pay\n *\n * Independence is the whole bet, and it is a bet about the user's import\n * graph, not about their machine. In-process, the loader executes a shared\n * dependency ONCE for the whole build; N workers execute it N times. So the\n * win tracks how disjoint the schema files' graphs are, and it can invert: on\n * a fixture of the same size whose files form a 120-deep chain, four workers\n * measured 1,045 ms against 945 ms in-process — a small loss — and twelve\n * measured 3,332 ms, because each worker was re-executing most of the project\n * to compile its share of it. The same files with the chain removed went\n * 636 ms → 341 ms.\n *\n * That, plus N copies of the graph in memory, is why this is opt-in rather\n * than the default — see ZodCompilerPluginOptions.\n *\n * ## What still runs on the bundler thread\n *\n * Everything that touches shared state: the disk cache (one directory, one GC,\n * atomic writes), the static dependency crawl (pure over the filesystem, and\n * memoized across files — sharding it would multiply the memo instead of the\n * throughput), and the in-memory result cache. Workers do the executing and\n * the generating; the main thread does the bookkeeping.\n *\n * ## Executed-module reporting\n *\n * The disk cache needs `getFirstPartyModulePaths()` — the superset of modules\n * the loader has executed — to persist entries whose static dep crawl came\n * back incomplete (the common outcome on large graphs: one non-literal dynamic\n * import anywhere poisons the closure). With discovery in workers, the main\n * thread's loader executes nothing, so that superset would be empty and every\n * deferred entry would be silently dropped — no stale results, but a large\n * chunk of the cache quietly stops working.\n *\n * Workers therefore report the paths they have executed back with every\n * result, as a DELTA against what they have already reported (the full list\n * grows into the thousands and would be re-sent per file). The pool unions\n * those deltas into {@link TransformPool.firstPartyModulePaths}, which the\n * DiskCache reads synchronously at flush time exactly as it reads the loader's\n * own. A worker that cannot track modules at all — Bun/Deno, where native\n * import has no evictable cache — reports `null`, and the union degrades to\n * `null` so the cache declines to persist rather than guessing.\n */\n\nimport * as fs from \"node:fs\";\nimport { availableParallelism } from \"node:os\";\nimport { Worker } from \"node:worker_threads\";\nimport type { CodegenMode } from \"../core/codegen/context.js\";\nimport type { HoistOptions } from \"./hoist.js\";\nimport type { TransformSourceMap } from \"./transform.js\";\nimport type { BuildStats, TransformOptions } from \"./types.js\";\n\n/**\n * Upper bound on pool size for `parallel: true`.\n *\n * Four, and not \"one per core\", because throughput peaks there and then goes\n * BACKWARDS. Measured on 120 files of deeply nested schemas (960 schemas), on\n * a machine with 12 performance cores:\n *\n * in-process 3,633 ms · n=2 2,263 · n=4 1,508 · n=6 1,568 · n=8 1,786 · n=12 2,119\n *\n * Past four the bottleneck stops being CPU. Every worker re-executes whatever\n * of the project graph its files reach, holds its own copy of it, and ships\n * megabytes of generated source back through structured cloning that the ONE\n * receiving thread has to deserialize. More workers multiply the first two\n * costs and contend on the third.\n *\n * The same effect makes the cap protective rather than merely tidy: on a\n * fixture whose files form a 120-deep import chain — where the in-process\n * loader executes the shared graph once and every worker re-executes most of\n * it — n=4 costs 1,045 ms against 945 ms in-process, while n=12 costs 3,332 ms.\n * Overshooting turns a small loss into a rout. An explicit `parallel: <n>`\n * overrides this for anyone who has measured their own graph.\n */\nconst MAX_AUTO_WORKERS = 4;\n\n/** Hard ceiling on an explicit `parallel: <n>`, guarding a typo'd 400. */\nconst MAX_WORKERS = 32;\n\n/**\n * Consecutive worker deaths before the pool gives up for the rest of the build.\n *\n * Falling back per file is the right response to ONE unlucky worker (an OOM\n * under a particularly large graph). It is the wrong response to a pool that\n * cannot work at all — a partial install, a Node build without worker support\n * — where every file would spawn a worker, wait for it to die, warn, and then\n * do the transform in-process anyway. Tripping the breaker converts that into\n * a normal serial build after the first few files.\n */\nconst MAX_CONSECUTIVE_FAILURES = 3;\n\n/** The subset of TransformOptions that survives structured cloning. */\nexport interface PoolTransformOptions {\n mode: CodegenMode;\n runtimeId?: string | undefined;\n zodCompat?: boolean | undefined;\n compact?: boolean | undefined;\n verbose?: boolean | undefined;\n autoDiscover?: boolean | undefined;\n hoist?: boolean | HoistOptions | undefined;\n}\n\n/** Main → worker. */\nexport type PoolRequest =\n | { type: \"transform\"; seq: number; code: string; id: string; options: PoolTransformOptions }\n | { type: \"invalidate\"; seq: number };\n\n/**\n * What a worker observed while transforming one file: the output plus the\n * signals the main thread's disk-cache bookkeeping needs, which arrive as\n * callbacks in the in-process path and cannot cross a thread boundary.\n */\nexport interface PoolTransformResult {\n output: { code: string; map: TransformSourceMap | null } | null;\n discoveryRan: boolean;\n substantialWork: boolean;\n uncacheable: boolean;\n stats: BuildStats | null;\n /**\n * First-party modules this worker executed since its last report, or `null`\n * when the worker cannot track them (Bun/Deno native import).\n */\n newModulePaths: string[] | null;\n}\n\n/** Worker → main. */\nexport type PoolResponse =\n | { type: \"result\"; seq: number; result: PoolTransformResult }\n | { type: \"error\"; seq: number; name: string; message: string; stack?: string | undefined }\n | { type: \"invalidated\"; seq: number };\n\n/**\n * Raised when the pool itself failed — a worker that would not spawn, or one\n * that died mid-task (OOM is the realistic case; each worker holds a full\n * module graph). Distinct from a transform's own error, which is rethrown with\n * its original identity, because the caller's response differs: an\n * infrastructure failure is worth retrying on the bundler thread, a schema\n * that cannot compile is not.\n */\nexport class PoolUnavailableError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"PoolUnavailableError\";\n }\n}\n\n/**\n * Resolve `parallel` to a worker count. `false`/`undefined` disable the pool.\n *\n * `true` leaves one core for the bundler itself — it is still parsing,\n * resolving and generating chunks while transforms run — and then caps at\n * {@link MAX_AUTO_WORKERS}.\n */\nexport function resolvePoolSize(parallel: boolean | number | undefined): number {\n if (parallel === undefined || parallel === false) return 0;\n if (parallel === true) {\n return Math.min(Math.max(availableParallelism() - 1, 1), MAX_AUTO_WORKERS);\n }\n if (!Number.isFinite(parallel)) return 0;\n return Math.min(Math.max(Math.floor(parallel), 1), MAX_WORKERS);\n}\n\n/** Strip the callbacks from TransformOptions; what remains is cloneable. */\nexport function poolTransformOptions(options: TransformOptions): PoolTransformOptions {\n return {\n mode: options.mode,\n runtimeId: options.runtimeId,\n zodCompat: options.zodCompat,\n compact: options.compact,\n verbose: options.verbose,\n autoDiscover: options.autoDiscover,\n hoist: options.hoist,\n };\n}\n\n/**\n * Locate the worker entry next to this module.\n *\n * Published, that is `dist/unplugin/pool-worker.js` and a plain dynamic import\n * loads it. Running from source — this repository's own test suite resolves\n * `#src/**` through Vite — only the `.ts` sibling exists, and a worker starts\n * outside Vite's pipeline, in bare Node. Node's native type stripping is not\n * enough there: it erases types but does not remap the `./transform.js`\n * specifiers TypeScript requires onto the `.ts` files that actually exist. So\n * the bootstrap hands `.ts` entries to jiti, which resolves them the same way\n * the loader already resolves user schema files.\n *\n * Returns null when neither exists — the plugin's own code has been bundled\n * into a single file by a consumer, so `import.meta.url` no longer points at a\n * directory containing it. The caller falls back to in-process transforms.\n */\nfunction resolveWorkerEntry(): { href: string; isTypeScript: boolean } | null {\n for (const [specifier, isTypeScript] of [\n [\"./pool-worker.js\", false],\n [\"./pool-worker.ts\", true],\n ] as const) {\n const url = new URL(specifier, import.meta.url);\n if (url.protocol !== \"file:\") continue;\n try {\n if (fs.statSync(url, { throwIfNoEntry: false })?.isFile()) {\n return { href: url.href, isTypeScript };\n }\n } catch {\n // Unreadable path — try the next candidate.\n }\n }\n return null;\n}\n\n/**\n * CommonJS bootstrap evaluated inside each worker (`eval: true`). A string\n * rather than a file because it has to work identically from `dist/` and from\n * `src/`, where no `.js` sibling exists to point a Worker at.\n */\nconst WORKER_BOOTSTRAP = `\nconst { workerData } = require(\"node:worker_threads\");\n(async () => {\n if (workerData.isTypeScript) {\n const { createJiti } = await import(workerData.jitiSpecifier);\n await createJiti(workerData.entry).import(workerData.entry);\n } else {\n await import(workerData.entry);\n }\n})().catch((error) => {\n // Reaching the parent as an 'error' event is the whole point: a worker that\n // cannot load its entry must fail loudly enough for the pool to fall back,\n // not sit idle holding a task.\n setTimeout(() => { throw error; });\n});\n`;\n\n/** Resolve jiti's URL for the bootstrap; bare specifier if resolution is unavailable. */\nfunction jitiSpecifier(): string {\n try {\n return import.meta.resolve(\"jiti\");\n } catch {\n return \"jiti\";\n }\n}\n\n/**\n * Live pools, so a host that outlives its bundler can shut them all down.\n *\n * Nothing in the plugin lifecycle disposes a pool on its own: workers are kept\n * warm ACROSS watch rebuilds on purpose — invalidation drops first-party\n * modules but leaves node_modules (zod itself, the expensive bulk) executed,\n * and discarding that per rebuild would re-pay boot plus a cold zod on every\n * keystroke. Idle workers are unref'd, so they never hold a process open and a\n * one-shot build exits normally. This registry exists for hosts that need\n * determinism instead — chiefly this package's own test suite, which builds\n * many plugin instances in one process.\n */\nconst livePools = new Set<TransformPool>();\n\n/** Terminate every live transform pool. */\nexport async function disposeAllPools(): Promise<void> {\n await Promise.all([...livePools].map((pool) => pool.dispose()));\n}\n\ninterface PoolWorker {\n worker: Worker;\n /** Seq of the request this worker is handling, or null when idle. */\n busy: number | null;\n}\n\ninterface PendingTask {\n request: PoolRequest;\n resolve: (result: PoolTransformResult) => void;\n reject: (error: Error) => void;\n}\n\n/**\n * A lazily grown pool of transform workers.\n *\n * Not a general-purpose executor: it knows the transform protocol, and it\n * knows that a broadcast invalidation has to be ordered ahead of every\n * subsequent transform on every worker (per-worker message delivery is FIFO,\n * so posting the broadcast before any later task is sufficient — and the only\n * ordering guarantee available, since a worker mid-transform cannot be\n * interrupted).\n */\nexport class TransformPool {\n private readonly size: number;\n private readonly entry: { href: string; isTypeScript: boolean };\n private readonly workers: PoolWorker[] = [];\n private readonly queue: PendingTask[] = [];\n /** In-flight requests by seq, so a worker exit can reject exactly its own. */\n private readonly inflight = new Map<number, PendingTask>();\n private readonly executedModules = new Set<string>();\n /** A worker reported it cannot track executed modules — the union is unusable. */\n private moduleTrackingUnavailable = false;\n private seq = 0;\n private disposed = false;\n /** Worker deaths since the last successful result; trips MAX_CONSECUTIVE_FAILURES. */\n private consecutiveFailures = 0;\n /** Set once the breaker trips: every later run() rejects without spawning. */\n private brokenPool = false;\n\n private constructor(size: number, entry: { href: string; isTypeScript: boolean }) {\n this.size = size;\n this.entry = entry;\n }\n\n /**\n * Build a pool of `size` workers, or return null when workers are\n * unavailable in this installation (the entry could not be located). Callers\n * treat null as \"run in-process\".\n */\n static create(size: number): TransformPool | null {\n if (size <= 0) return null;\n const entry = resolveWorkerEntry();\n if (entry === null) return null;\n const pool = new TransformPool(size, entry);\n livePools.add(pool);\n return pool;\n }\n\n /** Transform one file on a worker. Rejects with the transform's own error, or PoolUnavailableError. */\n run(code: string, id: string, options: PoolTransformOptions): Promise<PoolTransformResult> {\n if (this.disposed) {\n return Promise.reject(new PoolUnavailableError(\"Transform pool is disposed\"));\n }\n if (this.brokenPool) {\n return Promise.reject(\n new PoolUnavailableError(\n `Transform workers failed ${MAX_CONSECUTIVE_FAILURES} times in a row; this build is running in-process`,\n ),\n );\n }\n return new Promise<PoolTransformResult>((resolve, reject) => {\n const request: PoolRequest = { type: \"transform\", seq: this.seq++, code, id, options };\n this.dispatch({ request, resolve, reject });\n });\n }\n\n /**\n * Drop every worker's executed-module cache, so the next discovery re-runs\n * changed schema graphs. Mirrors the loader's invalidateModuleCache() for\n * the in-process path; called from watchChange.\n *\n * Fire-and-forget: the broadcast is posted to every live worker before any\n * later transform can be, and per-worker FIFO delivery does the rest. A\n * transform ALREADY executing in a worker keeps its pre-change module cache\n * — the same window the in-process path has, where invalidation lands\n * between awaits — and the main thread's own result cache is cleared\n * alongside, so the file is recomputed either way.\n */\n invalidate(): void {\n if (this.disposed) return;\n this.executedModules.clear();\n this.moduleTrackingUnavailable = false;\n for (const entry of this.workers) {\n entry.worker.postMessage({ type: \"invalidate\", seq: this.seq++ } satisfies PoolRequest);\n }\n }\n\n /**\n * Union of the first-party modules every worker has executed, or null when\n * any worker cannot track them. Synchronous, because DiskCache.flushDeferred\n * runs from `buildEnd` and from a process 'exit' hook, where only\n * synchronous work is allowed — which is why workers push deltas rather than\n * answering a query.\n */\n firstPartyModulePaths(): string[] | null {\n if (this.moduleTrackingUnavailable) return null;\n // An empty array is a real answer — no worker has executed anything yet\n // (every file hit the disk cache). It is the CALLER's job not to pair an\n // empty superset with an entry that ran discovery; DiskCache.flushDeferred\n // refuses that combination outright.\n return [...this.executedModules];\n }\n\n /** Terminate every worker. Pending and queued tasks reject. */\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n livePools.delete(this);\n const error = new PoolUnavailableError(\"Transform pool was disposed\");\n for (const task of this.queue.splice(0)) task.reject(error);\n for (const task of this.inflight.values()) task.reject(error);\n this.inflight.clear();\n await Promise.all(this.workers.splice(0).map((entry) => entry.worker.terminate()));\n }\n\n /** Hand a task to an idle worker, grow the pool, or queue it. */\n private dispatch(task: PendingTask): void {\n const idle = this.workers.find((entry) => entry.busy === null);\n if (idle !== undefined) {\n this.send(idle, task);\n return;\n }\n if (this.workers.length < this.size) {\n let created: PoolWorker;\n try {\n created = this.spawn();\n } catch (error) {\n task.reject(\n new PoolUnavailableError(\n `Could not start a zod-compiler transform worker: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n ),\n );\n return;\n }\n this.workers.push(created);\n this.send(created, task);\n return;\n }\n this.queue.push(task);\n }\n\n private send(entry: PoolWorker, task: PendingTask): void {\n entry.busy = task.request.seq;\n this.inflight.set(task.request.seq, task);\n // A worker is unref'd while idle so it never holds the process open, and\n // ref'd while it owns a task so the process cannot exit out from under the\n // promise the caller is awaiting.\n entry.worker.ref();\n entry.worker.postMessage(task.request);\n }\n\n private spawn(): PoolWorker {\n const worker = new Worker(WORKER_BOOTSTRAP, {\n eval: true,\n workerData: {\n entry: this.entry.href,\n isTypeScript: this.entry.isTypeScript,\n jitiSpecifier: jitiSpecifier(),\n },\n });\n const entry: PoolWorker = { worker, busy: null };\n worker.unref();\n worker.on(\"message\", (message: PoolResponse) => {\n this.onMessage(entry, message);\n });\n worker.on(\"error\", (error: Error) => {\n this.onWorkerGone(entry, error);\n });\n worker.on(\"exit\", (code) => {\n if (this.disposed) return;\n this.onWorkerGone(entry, new Error(`worker exited with code ${code}`));\n });\n return entry;\n }\n\n private onMessage(entry: PoolWorker, message: PoolResponse): void {\n if (message.type === \"invalidated\") return;\n const task = this.inflight.get(message.seq);\n this.inflight.delete(message.seq);\n if (entry.busy === message.seq) {\n entry.busy = null;\n entry.worker.unref();\n }\n if (task !== undefined) {\n if (message.type === \"result\") {\n // A worker that answered is a working worker: an earlier death was\n // this file's bad luck, not a pool that cannot run.\n this.consecutiveFailures = 0;\n this.recordModulePaths(message.result.newModulePaths);\n task.resolve(message.result);\n } else {\n // Rebuild the transform's own error on this side of the boundary. The\n // message is what surfaces in the bundler's output, so it has to\n // survive verbatim — this is the path a genuine \"Failed to load\n // schemas from …\" travels.\n const error = new Error(message.message);\n error.name = message.name;\n if (message.stack !== undefined) error.stack = message.stack;\n task.reject(error);\n }\n }\n this.drain();\n }\n\n /**\n * A worker died — OOM while holding a large module graph is the realistic\n * cause. Its task fails with PoolUnavailableError so the caller can retry\n * in-process, and the worker is dropped from the pool; the next dispatch\n * spawns a replacement.\n */\n private onWorkerGone(entry: PoolWorker, cause: Error): void {\n const index = this.workers.indexOf(entry);\n // Node emits 'error' and then 'exit' for the same death; only the first\n // one still finds the worker in the pool, so only it counts as a failure.\n if (index === -1) return;\n this.workers.splice(index, 1);\n if (++this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) this.brokenPool = true;\n const seq = entry.busy;\n entry.busy = null;\n if (seq !== null) {\n const task = this.inflight.get(seq);\n this.inflight.delete(seq);\n task?.reject(\n new PoolUnavailableError(`zod-compiler transform worker failed: ${cause.message}`, {\n cause,\n }),\n );\n }\n // Executed-module reports from a dead worker describe a module cache that\n // no longer exists, but the union is a SUPERSET — keeping stale paths can\n // only over-invalidate cache entries, never validate a stale one.\n this.drain();\n }\n\n private drain(): void {\n if (this.brokenPool) {\n // The breaker tripped while these waited. Spawning for them would repeat\n // the failure per file; hand them back so the caller runs them itself.\n const error = new PoolUnavailableError(\n `Transform workers failed ${MAX_CONSECUTIVE_FAILURES} times in a row; this build is running in-process`,\n );\n for (const task of this.queue.splice(0)) task.reject(error);\n return;\n }\n while (this.queue.length > 0) {\n const idle = this.workers.find((e) => e.busy === null);\n if (idle === undefined) {\n // No idle worker; grow if the pool is under size (a death may have\n // freed a slot), otherwise wait for the next completion.\n if (this.workers.length >= this.size) return;\n const task = this.queue.shift();\n if (task === undefined) return;\n this.dispatch(task);\n continue;\n }\n const task = this.queue.shift();\n if (task === undefined) return;\n this.send(idle, task);\n }\n }\n\n private recordModulePaths(paths: string[] | null): void {\n if (paths === null) {\n this.moduleTrackingUnavailable = true;\n return;\n }\n for (const p of paths) this.executedModules.add(p);\n }\n}\n"],"mappings":"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"}