veryfront 0.1.74 → 0.1.75

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 (86) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +2 -0
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +64 -1
  4. package/esm/deno.d.ts +2 -0
  5. package/esm/deno.js +3 -1
  6. package/esm/src/data/data-fetcher.d.ts +11 -1
  7. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  8. package/esm/src/data/data-fetcher.js +5 -2
  9. package/esm/src/data/index.d.ts +1 -1
  10. package/esm/src/data/index.d.ts.map +1 -1
  11. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  12. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  13. package/esm/src/data/server-data-fetcher.js +49 -3
  14. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  15. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  16. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  17. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  18. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  19. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  20. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  21. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  22. package/esm/src/routing/api/handler.d.ts.map +1 -1
  23. package/esm/src/routing/api/handler.js +6 -2
  24. package/esm/src/routing/api/route-executor.d.ts +8 -2
  25. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  26. package/esm/src/routing/api/route-executor.js +131 -3
  27. package/esm/src/security/deno-permissions.d.ts +6 -0
  28. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  29. package/esm/src/security/deno-permissions.js +10 -0
  30. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  31. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  32. package/esm/src/security/sandbox/project-worker.js +318 -0
  33. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  34. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  35. package/esm/src/security/sandbox/worker-permissions.js +60 -0
  36. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  37. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  38. package/esm/src/security/sandbox/worker-pool.js +356 -0
  39. package/esm/src/security/sandbox/worker-types.d.ts +165 -0
  40. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  41. package/esm/src/security/sandbox/worker-types.js +17 -0
  42. package/esm/src/server/project-env/storage.d.ts +6 -0
  43. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  44. package/esm/src/server/project-env/storage.js +8 -0
  45. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  46. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  47. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  48. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  49. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  50. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  51. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  52. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  53. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  54. package/esm/src/utils/index.d.ts +10 -1
  55. package/esm/src/utils/index.d.ts.map +1 -1
  56. package/esm/src/utils/index.js +9 -1
  57. package/esm/src/utils/logger/index.d.ts +1 -1
  58. package/esm/src/utils/logger/index.d.ts.map +1 -1
  59. package/esm/src/utils/logger/index.js +1 -1
  60. package/esm/src/utils/logger/logger.d.ts +14 -0
  61. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  62. package/esm/src/utils/logger/logger.js +17 -0
  63. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  64. package/package.json +4 -1
  65. package/src/cli/commands/knowledge/command.ts +76 -1
  66. package/src/deno.js +3 -1
  67. package/src/src/data/data-fetcher.ts +18 -2
  68. package/src/src/data/index.ts +1 -1
  69. package/src/src/data/server-data-fetcher.ts +78 -3
  70. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  71. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  72. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  73. package/src/src/routing/api/handler.ts +16 -3
  74. package/src/src/routing/api/route-executor.ts +222 -1
  75. package/src/src/security/deno-permissions.ts +11 -0
  76. package/src/src/security/sandbox/project-worker.ts +416 -0
  77. package/src/src/security/sandbox/worker-permissions.ts +74 -0
  78. package/src/src/security/sandbox/worker-pool.ts +451 -0
  79. package/src/src/security/sandbox/worker-types.ts +209 -0
  80. package/src/src/server/project-env/storage.ts +9 -0
  81. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  82. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  83. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  84. package/src/src/utils/index.ts +11 -0
  85. package/src/src/utils/logger/index.ts +1 -0
  86. package/src/src/utils/logger/logger.ts +34 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Worker Permission Builder
3
+ *
4
+ * Builds scoped Deno Worker permissions for per-project isolation.
5
+ * Each project worker gets the minimum required permissions.
6
+ *
7
+ * @module security/sandbox/worker-permissions
8
+ */
9
+
10
+ /**
11
+ * Deno Worker permission object.
12
+ * See: https://docs.deno.com/runtime/fundamentals/permissions/
13
+ */
14
+ import * as dntShim from "../../../_dnt.shims.js";
15
+
16
+ export interface WorkerPermissions {
17
+ read: string[] | boolean;
18
+ write: boolean;
19
+ net: boolean;
20
+ env: boolean;
21
+ run: boolean;
22
+ ffi: boolean;
23
+ sys: boolean;
24
+ }
25
+
26
+ /**
27
+ * Build scoped permissions for a project worker.
28
+ *
29
+ * - read: restricted to the project temp dir (transformed modules) and cache dirs
30
+ * - write: denied (workers produce output via postMessage, not filesystem)
31
+ * - net: allowed (data fetchers and API routes may call external APIs)
32
+ * - env: allowed (user code reads API keys and config from environment)
33
+ * - run: denied (no subprocess spawning from user code)
34
+ * - ffi: denied (no native code from user code)
35
+ * - sys: denied (no system info access from user code)
36
+ */
37
+ export function buildWorkerPermissions(
38
+ readPaths: string[],
39
+ ): WorkerPermissions {
40
+ // In compiled binaries, user modules import from the VFS temp dir which
41
+ // is outside the project directory. Rather than trying to enumerate all
42
+ // read paths, grant full read access — the security boundary is enforced
43
+ // by denying write/run/ffi/sys, not by restricting reads.
44
+ // Check for compiled binary by testing if execPath is NOT "deno"/"deno.exe"
45
+ try {
46
+ const exec = typeof dntShim.Deno !== "undefined" ? dntShim.Deno.execPath?.() : undefined;
47
+ if (exec) {
48
+ const name = exec.split(/[/\\]/).pop()?.toLowerCase() ?? "";
49
+ if (name !== "deno" && name !== "deno.exe") {
50
+ return {
51
+ read: true,
52
+ write: false,
53
+ net: true,
54
+ env: true,
55
+ run: false,
56
+ ffi: false,
57
+ sys: false,
58
+ };
59
+ }
60
+ }
61
+ } catch {
62
+ // execPath may not be available
63
+ }
64
+
65
+ return {
66
+ read: readPaths.length > 0 ? readPaths : false,
67
+ write: false,
68
+ net: true,
69
+ env: true,
70
+ run: false,
71
+ ffi: false,
72
+ sys: false,
73
+ };
74
+ }
@@ -0,0 +1,451 @@
1
+ /**
2
+ * Worker Pool Manager
3
+ *
4
+ * Manages a pool of per-project Deno Workers for tenant-isolated code execution.
5
+ * Uses LRU eviction when the pool exceeds its capacity, idle timeout for
6
+ * cleanup, and health checks for reliability.
7
+ *
8
+ * @module security/sandbox/worker-pool
9
+ */
10
+ import * as dntShim from "../../../_dnt.shims.js";
11
+
12
+
13
+ import { serverLogger } from "../../utils/index.js";
14
+ import { getEnvBoolean, getEnvNumber, unrefTimer } from "../../platform/compat/process.js";
15
+ import { withSpan } from "../../observability/tracing/otlp-setup.js";
16
+ import { SECURITY_VIOLATION } from "../../errors/index.js";
17
+ import { ProjectWorker } from "./project-worker.js";
18
+ import { buildWorkerPermissions } from "./worker-permissions.js";
19
+ import type { WorkerPoolConfig, WorkerRequest, WorkerResponse } from "./worker-types.js";
20
+ import { DEFAULT_WORKER_POOL_CONFIG } from "./worker-types.js";
21
+
22
+ const logger = serverLogger.component("worker-pool");
23
+
24
+ interface PoolEntry {
25
+ worker: ProjectWorker;
26
+ lastAccessedAt: number;
27
+ createdAt: number;
28
+ }
29
+
30
+ export class WorkerPool {
31
+ private pool = new Map<string, PoolEntry>();
32
+ private recycling = new Set<string>();
33
+ private config: WorkerPoolConfig;
34
+
35
+ private cleanupInterval: ReturnType<typeof dntShim.setInterval> | undefined;
36
+ private healthCheckInterval: ReturnType<typeof dntShim.setInterval> | undefined;
37
+
38
+ constructor(config: Partial<WorkerPoolConfig> = {}) {
39
+ this.config = { ...DEFAULT_WORKER_POOL_CONFIG, ...config };
40
+ this.startCleanup();
41
+ this.startHealthChecks();
42
+ }
43
+
44
+ /**
45
+ * Get or create a worker for the given project.
46
+ */
47
+ getOrCreateWorker(projectId: string, readPaths: string[]): ProjectWorker {
48
+ const existing = this.pool.get(projectId);
49
+ if (
50
+ existing && existing.worker.status !== "crashed" && existing.worker.status !== "terminated"
51
+ ) {
52
+ existing.lastAccessedAt = Date.now();
53
+ return existing.worker;
54
+ }
55
+
56
+ // If an existing entry is crashed/terminated, clean it up
57
+ if (existing) {
58
+ existing.worker.terminate();
59
+ this.pool.delete(projectId);
60
+ }
61
+
62
+ // Evict LRU if at capacity
63
+ this.evictIfNeeded();
64
+
65
+ const permissions = buildWorkerPermissions(readPaths);
66
+ const worker = new ProjectWorker({
67
+ projectId,
68
+ permissions,
69
+ requestTimeoutMs: this.config.requestTimeoutMs,
70
+ });
71
+
72
+ worker.start();
73
+
74
+ const now = Date.now();
75
+ this.pool.set(projectId, {
76
+ worker,
77
+ lastAccessedAt: now,
78
+ createdAt: now,
79
+ });
80
+
81
+ logger.debug("Worker created", {
82
+ projectId,
83
+ poolSize: this.pool.size,
84
+ });
85
+
86
+ return worker;
87
+ }
88
+
89
+ /**
90
+ * Execute a request in a project worker. Convenience method that
91
+ * combines getOrCreateWorker + execute.
92
+ */
93
+ execute(
94
+ projectId: string,
95
+ readPaths: string[],
96
+ request: WorkerRequest,
97
+ ): Promise<WorkerResponse> {
98
+ // Validate modulePath is within allowed read paths (defense-in-depth)
99
+ if ("modulePath" in request && request.modulePath) {
100
+ const modulePath = request.modulePath;
101
+ const isAllowed = readPaths.some((p) => modulePath.startsWith(p));
102
+ if (!isAllowed) {
103
+ return Promise.reject(
104
+ SECURITY_VIOLATION.create({
105
+ detail:
106
+ `Module path "${modulePath}" is outside allowed read paths for project "${projectId}"`,
107
+ }),
108
+ );
109
+ }
110
+ }
111
+
112
+ return withSpan(
113
+ "workerPool.execute",
114
+ async () => {
115
+ const worker = this.getOrCreateWorker(projectId, readPaths);
116
+
117
+ // Check if worker should be recycled (request count or age)
118
+ const entry = this.pool.get(projectId);
119
+ const shouldRecycle = worker.requestCount >= this.config.maxRequestsPerWorker ||
120
+ (entry && Date.now() - entry.createdAt > this.config.maxWorkerAgeMs);
121
+
122
+ if (shouldRecycle && !this.recycling.has(projectId)) {
123
+ this.recycling.add(projectId);
124
+ try {
125
+ logger.debug("Recycling worker", {
126
+ projectId,
127
+ requestCount: worker.requestCount,
128
+ ageMs: entry ? Date.now() - entry.createdAt : 0,
129
+ reason: worker.requestCount >= this.config.maxRequestsPerWorker
130
+ ? "request_count"
131
+ : "age",
132
+ });
133
+ this.evictWorker(projectId);
134
+ const fresh = this.getOrCreateWorker(projectId, readPaths);
135
+ return fresh.execute(request);
136
+ } finally {
137
+ this.recycling.delete(projectId);
138
+ }
139
+ }
140
+
141
+ return worker.execute(request);
142
+ },
143
+ { "workerPool.projectId": projectId },
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Evict a specific project's worker.
149
+ */
150
+ evictWorker(projectId: string): void {
151
+ const entry = this.pool.get(projectId);
152
+ if (!entry) return;
153
+
154
+ entry.worker.terminate();
155
+ this.pool.delete(projectId);
156
+
157
+ logger.debug("Worker evicted", { projectId, poolSize: this.pool.size });
158
+ }
159
+
160
+ /**
161
+ * Get pool statistics for monitoring.
162
+ */
163
+ getStats(): {
164
+ poolSize: number;
165
+ maxPoolSize: number;
166
+ memoryBudgetMb: number;
167
+ workers: Record<string, {
168
+ status: string;
169
+ requestCount: number;
170
+ hasPending: boolean;
171
+ idleMs: number;
172
+ ageMs: number;
173
+ }>;
174
+ } {
175
+ const workers: Record<string, {
176
+ status: string;
177
+ requestCount: number;
178
+ hasPending: boolean;
179
+ idleMs: number;
180
+ ageMs: number;
181
+ }> = {};
182
+ const now = Date.now();
183
+
184
+ for (const [id, entry] of this.pool) {
185
+ workers[id] = {
186
+ status: entry.worker.status,
187
+ requestCount: entry.worker.requestCount,
188
+ hasPending: entry.worker.hasPendingRequests,
189
+ idleMs: now - entry.lastAccessedAt,
190
+ ageMs: now - entry.createdAt,
191
+ };
192
+ }
193
+
194
+ return {
195
+ poolSize: this.pool.size,
196
+ maxPoolSize: this.config.maxPoolSize,
197
+ memoryBudgetMb: this.config.memoryBudgetMb,
198
+ workers,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Get aggregate metrics suitable for Prometheus exposition.
204
+ */
205
+ getMetrics(): {
206
+ /** Current number of active workers */
207
+ workerPoolSize: number;
208
+ /** Number of workers at capacity (max pool size) */
209
+ workerPoolCapacity: number;
210
+ /** Total requests processed across all workers */
211
+ totalRequestsProcessed: number;
212
+ /** Number of workers with pending requests (busy) */
213
+ busyWorkers: number;
214
+ /** Number of crashed workers (cleaned up at next health check) */
215
+ crashedWorkers: number;
216
+ } {
217
+ let totalRequests = 0;
218
+ let busy = 0;
219
+ let crashed = 0;
220
+
221
+ for (const [, entry] of this.pool) {
222
+ totalRequests += entry.worker.requestCount;
223
+ if (entry.worker.hasPendingRequests) busy++;
224
+ if (entry.worker.status === "crashed") crashed++;
225
+ }
226
+
227
+ return {
228
+ workerPoolSize: this.pool.size,
229
+ workerPoolCapacity: this.config.maxPoolSize,
230
+ totalRequestsProcessed: totalRequests,
231
+ busyWorkers: busy,
232
+ crashedWorkers: crashed,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Shutdown the pool. Terminates all workers and stops timers.
238
+ */
239
+ shutdown(): void {
240
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
241
+ if (this.healthCheckInterval) clearInterval(this.healthCheckInterval);
242
+
243
+ for (const [, entry] of this.pool) {
244
+ entry.worker.terminate();
245
+ }
246
+
247
+ this.pool.clear();
248
+ logger.debug("Worker pool shut down");
249
+ }
250
+
251
+ // -----------------------------------------------------------------------
252
+ // Private — Cleanup & Eviction
253
+ // -----------------------------------------------------------------------
254
+
255
+ private startCleanup(): void {
256
+ // Run idle eviction every 30 seconds
257
+ this.cleanupInterval = dntShim.setInterval(() => {
258
+ this.evictIdleWorkers();
259
+ }, 30_000);
260
+
261
+ unrefTimer(this.cleanupInterval);
262
+ }
263
+
264
+ private startHealthChecks(): void {
265
+ this.healthCheckInterval = dntShim.setInterval(() => {
266
+ void this.checkHealth();
267
+ }, this.config.healthCheckIntervalMs);
268
+
269
+ unrefTimer(this.healthCheckInterval);
270
+ }
271
+
272
+ private evictIdleWorkers(): void {
273
+ const now = Date.now();
274
+
275
+ for (const [projectId, entry] of this.pool) {
276
+ const idleTime = now - entry.lastAccessedAt;
277
+
278
+ if (idleTime > this.config.idleTimeoutMs && !entry.worker.hasPendingRequests) {
279
+ entry.worker.terminate();
280
+ this.pool.delete(projectId);
281
+
282
+ logger.debug("Evicted idle worker", {
283
+ projectId,
284
+ idleMs: idleTime,
285
+ poolSize: this.pool.size,
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ private evictIfNeeded(): void {
292
+ if (this.pool.size < this.config.maxPoolSize) return;
293
+
294
+ // Find the LRU entry that has no pending requests
295
+ let lruId: string | null = null;
296
+ let lruTime = Infinity;
297
+
298
+ for (const [projectId, entry] of this.pool) {
299
+ if (!entry.worker.hasPendingRequests && entry.lastAccessedAt < lruTime) {
300
+ lruTime = entry.lastAccessedAt;
301
+ lruId = projectId;
302
+ }
303
+ }
304
+
305
+ if (lruId) {
306
+ this.evictWorker(lruId);
307
+ } else {
308
+ // All workers have pending requests — force evict the oldest
309
+ for (const [projectId, entry] of this.pool) {
310
+ if (entry.lastAccessedAt < lruTime) {
311
+ lruTime = entry.lastAccessedAt;
312
+ lruId = projectId;
313
+ }
314
+ }
315
+ if (lruId) this.evictWorker(lruId);
316
+ }
317
+ }
318
+
319
+ private async checkHealth(): Promise<void> {
320
+ for (const [projectId, entry] of this.pool) {
321
+ if (entry.worker.status === "crashed" || entry.worker.status === "terminated") {
322
+ this.pool.delete(projectId);
323
+ continue;
324
+ }
325
+
326
+ const healthy = await entry.worker.isHealthy();
327
+ if (!healthy) {
328
+ logger.warn("Worker failed health check", { projectId });
329
+ entry.worker.terminate();
330
+ this.pool.delete(projectId);
331
+ }
332
+ }
333
+
334
+ // Evict oldest workers when under memory pressure
335
+ this.evictUnderMemoryPressure();
336
+ }
337
+
338
+ /**
339
+ * Evict workers when the process is under memory pressure.
340
+ * Uses the global heap stats — if heap usage is above a threshold,
341
+ * evict idle workers starting with the oldest to free memory.
342
+ */
343
+ private evictUnderMemoryPressure(): void {
344
+ // Lazy import to avoid circular deps — this is only called during health checks
345
+ try {
346
+ // deno-lint-ignore no-explicit-any
347
+ const { getHeapStats } = (dntShim.dntGlobalThis as any).__veryfront_heap_stats ?? {};
348
+ if (!getHeapStats) return;
349
+
350
+ const { heapUsedPercent } = getHeapStats();
351
+ if (heapUsedPercent < 70) return; // Only act above 70%
352
+
353
+ // Sort workers by last access time (oldest first)
354
+ const entries = [...this.pool.entries()]
355
+ .filter(([, e]) => !e.worker.hasPendingRequests)
356
+ .sort(([, a], [, b]) => a.lastAccessedAt - b.lastAccessedAt);
357
+
358
+ // Evict up to 25% of idle workers
359
+ const toEvict = Math.max(1, Math.ceil(entries.length * 0.25));
360
+ for (let i = 0; i < toEvict && i < entries.length; i++) {
361
+ const projectId = entries[i]![0];
362
+ this.evictWorker(projectId);
363
+ logger.debug("Evicted worker due to memory pressure", {
364
+ projectId,
365
+ heapUsedPercent,
366
+ poolSize: this.pool.size,
367
+ });
368
+ }
369
+ } catch {
370
+ // getHeapStats may not be available in all environments
371
+ }
372
+ }
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Singleton & Feature Flag
377
+ // ---------------------------------------------------------------------------
378
+
379
+ // Cache feature flag results to avoid env lookups on every request
380
+ let _flagsResolved = false;
381
+ let _apiIsolation = false;
382
+ let _dataIsolation = false;
383
+ let _ssrIsolation = false;
384
+
385
+ function resolveFlags(): void {
386
+ if (_flagsResolved) return;
387
+ const master = getEnvBoolean("WORKER_ISOLATION_ENABLED", false);
388
+ _apiIsolation = master && getEnvBoolean("WORKER_ISOLATION_API", false);
389
+ _dataIsolation = master && getEnvBoolean("WORKER_ISOLATION_DATA", false);
390
+ _ssrIsolation = master && getEnvBoolean("WORKER_ISOLATION_SSR", false);
391
+ _flagsResolved = true;
392
+ }
393
+
394
+ /**
395
+ * Whether worker isolation is enabled for API routes.
396
+ * Controlled by WORKER_ISOLATION_API=1 (or WORKER_ISOLATION_ENABLED=1 as master switch).
397
+ */
398
+ export function isWorkerIsolationEnabled(): boolean {
399
+ resolveFlags();
400
+ return _apiIsolation;
401
+ }
402
+
403
+ /**
404
+ * Whether worker isolation is enabled for data fetchers (getServerData).
405
+ * Controlled by WORKER_ISOLATION_DATA=1 (requires WORKER_ISOLATION_ENABLED=1).
406
+ */
407
+ export function isDataIsolationEnabled(): boolean {
408
+ resolveFlags();
409
+ return _dataIsolation;
410
+ }
411
+
412
+ /**
413
+ * Whether worker isolation is enabled for SSR rendering.
414
+ * Controlled by WORKER_ISOLATION_SSR=1 (requires WORKER_ISOLATION_ENABLED=1).
415
+ */
416
+ export function isSSRIsolationEnabled(): boolean {
417
+ resolveFlags();
418
+ return _ssrIsolation;
419
+ }
420
+
421
+ /** Lazy singleton — created on first use when isolation is enabled */
422
+ let _pool: WorkerPool | null = null;
423
+
424
+ export function getWorkerPool(): WorkerPool {
425
+ if (!_pool) {
426
+ _pool = new WorkerPool({
427
+ maxPoolSize: getEnvNumber("WORKER_MAX_POOL_SIZE") ?? DEFAULT_WORKER_POOL_CONFIG.maxPoolSize,
428
+ idleTimeoutMs: getEnvNumber("WORKER_IDLE_TIMEOUT_MS") ??
429
+ DEFAULT_WORKER_POOL_CONFIG.idleTimeoutMs,
430
+ requestTimeoutMs: getEnvNumber("WORKER_REQUEST_TIMEOUT_MS") ??
431
+ DEFAULT_WORKER_POOL_CONFIG.requestTimeoutMs,
432
+ maxRequestsPerWorker: getEnvNumber("WORKER_MAX_REQUESTS_PER_WORKER") ??
433
+ DEFAULT_WORKER_POOL_CONFIG.maxRequestsPerWorker,
434
+ maxWorkerAgeMs: getEnvNumber("WORKER_MAX_AGE_MS") ??
435
+ DEFAULT_WORKER_POOL_CONFIG.maxWorkerAgeMs,
436
+ memoryBudgetMb: getEnvNumber("WORKER_MEMORY_BUDGET_MB") ??
437
+ DEFAULT_WORKER_POOL_CONFIG.memoryBudgetMb,
438
+ });
439
+ }
440
+ return _pool;
441
+ }
442
+
443
+ /** Reset the singleton and cached flags — for testing only */
444
+ export function __resetPoolForTests(): void {
445
+ _pool?.shutdown();
446
+ _pool = null;
447
+ _flagsResolved = false;
448
+ _apiIsolation = false;
449
+ _dataIsolation = false;
450
+ _ssrIsolation = false;
451
+ }