ai-evaluate 2.1.7 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -4
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +18 -16
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/miniflare-pool.d.ts +109 -0
- package/dist/miniflare-pool.d.ts.map +1 -0
- package/dist/miniflare-pool.js +308 -0
- package/dist/miniflare-pool.js.map +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +42 -10
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +66 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +169 -0
- package/dist/shared.js.map +1 -0
- package/dist/type-guards.d.ts +21 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +216 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +26 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-template/code-transforms.d.ts +9 -0
- package/dist/worker-template/code-transforms.d.ts.map +1 -0
- package/dist/worker-template/code-transforms.js +28 -0
- package/dist/worker-template/code-transforms.js.map +1 -0
- package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
- package/dist/worker-template/core.d.ts.map +1 -0
- package/dist/worker-template/core.js +502 -0
- package/dist/worker-template/core.js.map +1 -0
- package/dist/worker-template/helpers.d.ts +14 -0
- package/dist/worker-template/helpers.d.ts.map +1 -0
- package/dist/worker-template/helpers.js +79 -0
- package/dist/worker-template/helpers.js.map +1 -0
- package/dist/worker-template/index.d.ts +14 -0
- package/dist/worker-template/index.d.ts.map +1 -0
- package/dist/worker-template/index.js +19 -0
- package/dist/worker-template/index.js.map +1 -0
- package/dist/worker-template/sdk-generator.d.ts +17 -0
- package/dist/worker-template/sdk-generator.d.ts.map +1 -0
- package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
- package/dist/worker-template/sdk-generator.js.map +1 -0
- package/dist/worker-template/test-generator.d.ts +16 -0
- package/dist/worker-template/test-generator.d.ts.map +1 -0
- package/dist/worker-template/test-generator.js +357 -0
- package/dist/worker-template/test-generator.js.map +1 -0
- package/dist/worker-template.d.ts +2 -2
- package/dist/worker-template.d.ts.map +1 -1
- package/dist/worker-template.js +22 -10
- package/dist/worker-template.js.map +1 -1
- package/example/src/index.ts +12 -5
- package/package.json +1 -3
- package/src/evaluate.ts +21 -17
- package/src/index.ts +3 -1
- package/src/miniflare-pool.ts +395 -0
- package/src/node.ts +56 -11
- package/src/shared.ts +186 -0
- package/src/type-guards.ts +323 -0
- package/src/types.ts +18 -2
- package/src/validation.ts +120 -0
- package/src/worker-template/code-transforms.ts +32 -0
- package/src/worker-template/core.ts +557 -0
- package/src/worker-template/helpers.ts +90 -0
- package/src/worker-template/index.ts +23 -0
- package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1613
- package/src/worker-template/test-generator.ts +358 -0
- package/test/miniflare-pool.test.ts +246 -0
- package/test/node.test.ts +467 -0
- package/test/security.test.ts +1009 -0
- package/test/shared.test.ts +105 -0
- package/test/type-guards.test.ts +303 -0
- package/test/validation.test.ts +240 -0
- package/test/worker-template.test.ts +21 -19
- package/src/evaluate.js +0 -187
- package/src/index.js +0 -10
- package/src/node.d.ts +0 -17
- package/src/node.d.ts.map +0 -1
- package/src/node.js +0 -168
- package/src/node.js.map +0 -1
- package/src/types.d.ts +0 -172
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -4
- package/src/types.js.map +0 -1
- package/src/worker-template.d.ts.map +0 -1
- package/src/worker-template.js.map +0 -1
package/src/evaluate.ts
CHANGED
|
@@ -10,13 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { EvaluateOptions, EvaluateResult, WorkerLoader, SandboxEnv } from './types.js'
|
|
13
|
-
import { generateWorkerCode } from './worker-template.js'
|
|
13
|
+
import { generateWorkerCode } from './worker-template/index.js'
|
|
14
14
|
import { CAPNWEB_SOURCE } from './capnweb-bundle.js'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Compatibility date for dynamic workers (2026)
|
|
18
|
-
*/
|
|
19
|
-
const COMPATIBILITY_DATE = '2026-01-01'
|
|
15
|
+
import { COMPATIBILITY_DATE, normalizeImport, extractPackageName } from './shared.js'
|
|
20
16
|
|
|
21
17
|
/**
|
|
22
18
|
* Generate a minimal worker for simple script execution
|
|
@@ -37,10 +33,8 @@ function generateSimpleWorkerCode(options: {
|
|
|
37
33
|
|
|
38
34
|
// Make imports available as globals
|
|
39
35
|
const importGlobals = imports
|
|
40
|
-
.map((
|
|
41
|
-
|
|
42
|
-
const match = url.match(/esm\.sh\/([^@/]+)/)
|
|
43
|
-
const pkgName = match ? match[1].replace(/-/g, '_') : `pkg${i}`
|
|
36
|
+
.map((specifier, i) => {
|
|
37
|
+
const pkgName = extractPackageName(specifier, i)
|
|
44
38
|
const varName = pkgName === 'lodash' ? '_' : pkgName
|
|
45
39
|
return `globalThis.${varName} = __import${i}__.default || __import${i}__;
|
|
46
40
|
globalThis.pkg = __import${i}__.default || __import${i}__;`
|
|
@@ -157,17 +151,24 @@ export async function evaluate(
|
|
|
157
151
|
}
|
|
158
152
|
|
|
159
153
|
/**
|
|
160
|
-
* Pre-fetch external modules from URLs
|
|
154
|
+
* Pre-fetch external modules from URLs or package names
|
|
161
155
|
* Returns a map of module name to source code
|
|
162
156
|
*
|
|
157
|
+
* Supports:
|
|
158
|
+
* - Full URLs: https://esm.sh/lodash@4.17.21
|
|
159
|
+
* - Bare package names: lodash, lodash@4.17.21, @scope/pkg
|
|
160
|
+
*
|
|
163
161
|
* Handles esm.sh's redirect-style modules by following the internal import paths.
|
|
164
162
|
*/
|
|
165
163
|
async function prefetchModules(imports: string[]): Promise<Record<string, string>> {
|
|
166
164
|
const modules: Record<string, string> = {}
|
|
167
165
|
|
|
168
166
|
await Promise.all(
|
|
169
|
-
imports.map(async (
|
|
167
|
+
imports.map(async (specifier, i) => {
|
|
170
168
|
try {
|
|
169
|
+
// Normalize bare package names to esm.sh URLs
|
|
170
|
+
const url = normalizeImport(specifier)
|
|
171
|
+
|
|
171
172
|
// For esm.sh URLs, try to get the bundled version directly
|
|
172
173
|
let fetchUrl = url
|
|
173
174
|
if (url.includes('esm.sh/') && !url.includes('.mjs') && !url.includes('.js')) {
|
|
@@ -197,7 +198,9 @@ async function prefetchModules(imports: string[]): Promise<Record<string, string
|
|
|
197
198
|
modules[moduleName] = source
|
|
198
199
|
} catch (error) {
|
|
199
200
|
throw new Error(
|
|
200
|
-
`Failed to fetch import ${
|
|
201
|
+
`Failed to fetch import ${specifier}: ${
|
|
202
|
+
error instanceof Error ? error.message : String(error)
|
|
203
|
+
}`
|
|
201
204
|
)
|
|
202
205
|
}
|
|
203
206
|
})
|
|
@@ -244,8 +247,8 @@ async function evaluateSimple(
|
|
|
244
247
|
...externalModules,
|
|
245
248
|
},
|
|
246
249
|
compatibilityDate: COMPATIBILITY_DATE,
|
|
247
|
-
// Block network
|
|
248
|
-
globalOutbound: options.fetch === null ? null : undefined,
|
|
250
|
+
// Block network if fetch is false or null
|
|
251
|
+
globalOutbound: options.fetch === false || options.fetch === null ? null : undefined,
|
|
249
252
|
}))
|
|
250
253
|
|
|
251
254
|
// Get the entrypoint and call fetch
|
|
@@ -274,6 +277,7 @@ async function evaluateWithWorkerLoader(
|
|
|
274
277
|
script: options.script,
|
|
275
278
|
sdk: options.sdk,
|
|
276
279
|
imports: options.imports,
|
|
280
|
+
fetch: options.fetch,
|
|
277
281
|
})
|
|
278
282
|
const id = `sandbox-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
279
283
|
|
|
@@ -285,8 +289,8 @@ async function evaluateWithWorkerLoader(
|
|
|
285
289
|
'capnweb.js': CAPNWEB_SOURCE,
|
|
286
290
|
},
|
|
287
291
|
compatibilityDate: COMPATIBILITY_DATE,
|
|
288
|
-
// Block network
|
|
289
|
-
globalOutbound: options.fetch === null ? null : undefined,
|
|
292
|
+
// Block network if fetch is false or null
|
|
293
|
+
globalOutbound: options.fetch === false || options.fetch === null ? null : undefined,
|
|
290
294
|
bindings: {
|
|
291
295
|
TEST: testService,
|
|
292
296
|
},
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export { evaluate, createEvaluator } from './evaluate.js'
|
|
12
|
+
export { normalizeImport, normalizeImports } from './shared.js'
|
|
12
13
|
|
|
13
14
|
export type {
|
|
14
15
|
EvaluateOptions,
|
|
@@ -17,5 +18,6 @@ export type {
|
|
|
17
18
|
TestResults,
|
|
18
19
|
TestResult,
|
|
19
20
|
SandboxEnv,
|
|
20
|
-
SDKConfig
|
|
21
|
+
SDKConfig,
|
|
22
|
+
FetchConfig,
|
|
21
23
|
} from './types.js'
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Miniflare instance pool for improved performance
|
|
3
|
+
*
|
|
4
|
+
* Reuses Miniflare instances between evaluations instead of creating/disposing
|
|
5
|
+
* for each evaluation, providing 4-5x performance improvement.
|
|
6
|
+
*
|
|
7
|
+
* Uses Miniflare's setOptions() to update the worker script between uses,
|
|
8
|
+
* avoiding the expensive instance creation/teardown cycle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Miniflare as MiniflareType,
|
|
13
|
+
MiniflareOptions as MiniflareOptionsType,
|
|
14
|
+
} from 'miniflare'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pool configuration options
|
|
18
|
+
*/
|
|
19
|
+
export interface PoolConfig {
|
|
20
|
+
/** Number of instances to maintain in the pool (default: 3) */
|
|
21
|
+
size?: number
|
|
22
|
+
/** Milliseconds before disposing idle instance (default: 30000) */
|
|
23
|
+
maxIdleTime?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Outbound service handler type for network control
|
|
28
|
+
* - () => never: Block all network access (throw error)
|
|
29
|
+
* - (request: Request) => Response | Promise<Response>: Custom handler (allowlist, proxy, etc.)
|
|
30
|
+
*/
|
|
31
|
+
export type OutboundServiceHandler =
|
|
32
|
+
| (() => never)
|
|
33
|
+
| ((request: Request) => Response | Promise<Response>)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for updating a pooled instance's worker
|
|
37
|
+
*/
|
|
38
|
+
export interface WorkerOptions {
|
|
39
|
+
script: string
|
|
40
|
+
compatibilityDate?: string
|
|
41
|
+
outboundService?: OutboundServiceHandler
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A pooled Miniflare instance with metadata
|
|
46
|
+
*/
|
|
47
|
+
interface PooledInstance {
|
|
48
|
+
instance: MiniflareType
|
|
49
|
+
inUse: boolean
|
|
50
|
+
lastUsed: number
|
|
51
|
+
createdAt: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Type for the Miniflare constructor
|
|
55
|
+
type MiniflareConstructor = new (config: MiniflareOptionsType) => MiniflareType
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Global pool state (singleton per process)
|
|
59
|
+
*/
|
|
60
|
+
let pool: PooledInstance[] = []
|
|
61
|
+
let poolConfig: Required<PoolConfig> = {
|
|
62
|
+
size: 3,
|
|
63
|
+
maxIdleTime: 30000,
|
|
64
|
+
}
|
|
65
|
+
let idleCleanupInterval: NodeJS.Timeout | null = null
|
|
66
|
+
let MiniflareClass: MiniflareConstructor | null = null
|
|
67
|
+
let isShuttingDown = false
|
|
68
|
+
|
|
69
|
+
// Default worker script for warm instances
|
|
70
|
+
const WARM_WORKER_SCRIPT = `
|
|
71
|
+
export default {
|
|
72
|
+
async fetch(request, env) {
|
|
73
|
+
return new Response('ready', { status: 200 });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
`
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Configure the Miniflare pool
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { configurePool } from 'ai-evaluate/node'
|
|
84
|
+
*
|
|
85
|
+
* configurePool({
|
|
86
|
+
* size: 5, // Keep 5 warm instances
|
|
87
|
+
* maxIdleTime: 60000 // Dispose after 60s idle
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function configurePool(config: PoolConfig): void {
|
|
92
|
+
poolConfig = {
|
|
93
|
+
size: config.size ?? poolConfig.size,
|
|
94
|
+
maxIdleTime: config.maxIdleTime ?? poolConfig.maxIdleTime,
|
|
95
|
+
}
|
|
96
|
+
startIdleCleanup()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the current pool configuration
|
|
101
|
+
*/
|
|
102
|
+
export function getPoolConfig(): Required<PoolConfig> {
|
|
103
|
+
return { ...poolConfig }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get pool statistics for monitoring
|
|
108
|
+
*/
|
|
109
|
+
export function getPoolStats(): {
|
|
110
|
+
size: number
|
|
111
|
+
available: number
|
|
112
|
+
inUse: number
|
|
113
|
+
config: Required<PoolConfig>
|
|
114
|
+
} {
|
|
115
|
+
const available = pool.filter((p) => !p.inUse).length
|
|
116
|
+
return {
|
|
117
|
+
size: pool.length,
|
|
118
|
+
available,
|
|
119
|
+
inUse: pool.length - available,
|
|
120
|
+
config: { ...poolConfig },
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Initialize the Miniflare class (lazy load)
|
|
126
|
+
*/
|
|
127
|
+
async function getMiniflareClass(): Promise<MiniflareConstructor> {
|
|
128
|
+
if (!MiniflareClass) {
|
|
129
|
+
const { Miniflare } = await import('miniflare')
|
|
130
|
+
MiniflareClass = Miniflare as MiniflareConstructor
|
|
131
|
+
}
|
|
132
|
+
return MiniflareClass
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a new Miniflare instance with a warm worker
|
|
137
|
+
*/
|
|
138
|
+
async function createInstance(): Promise<MiniflareType> {
|
|
139
|
+
const Miniflare = await getMiniflareClass()
|
|
140
|
+
return new Miniflare({
|
|
141
|
+
modules: true,
|
|
142
|
+
script: WARM_WORKER_SCRIPT,
|
|
143
|
+
compatibilityDate: '2026-01-01',
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Start the idle cleanup interval
|
|
149
|
+
*/
|
|
150
|
+
function startIdleCleanup(): void {
|
|
151
|
+
if (idleCleanupInterval) {
|
|
152
|
+
clearInterval(idleCleanupInterval)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
idleCleanupInterval = setInterval(async () => {
|
|
156
|
+
if (isShuttingDown) return
|
|
157
|
+
|
|
158
|
+
const now = Date.now()
|
|
159
|
+
const toDispose: PooledInstance[] = []
|
|
160
|
+
|
|
161
|
+
// Find idle instances beyond the idle timeout
|
|
162
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
163
|
+
const item = pool[i]
|
|
164
|
+
if (!item.inUse && now - item.lastUsed > poolConfig.maxIdleTime) {
|
|
165
|
+
// Keep at least one warm instance
|
|
166
|
+
if (pool.filter((p) => !p.inUse && !toDispose.includes(p)).length > 1) {
|
|
167
|
+
toDispose.push(item)
|
|
168
|
+
pool.splice(i, 1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dispose old instances
|
|
174
|
+
for (const item of toDispose) {
|
|
175
|
+
try {
|
|
176
|
+
await item.instance.dispose()
|
|
177
|
+
} catch {
|
|
178
|
+
// Ignore disposal errors
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, 5000) // Check every 5 seconds
|
|
182
|
+
|
|
183
|
+
// Don't keep the process alive just for cleanup
|
|
184
|
+
if (idleCleanupInterval.unref) {
|
|
185
|
+
idleCleanupInterval.unref()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Acquire a Miniflare instance from the pool and configure it with a worker
|
|
191
|
+
*
|
|
192
|
+
* If a free instance is available, it will be reconfigured and returned.
|
|
193
|
+
* Otherwise, a new instance will be created (up to pool size limit).
|
|
194
|
+
* If pool is exhausted, creates a temporary instance.
|
|
195
|
+
*
|
|
196
|
+
* @param workerOptions - Configuration for the worker to run
|
|
197
|
+
* @returns Object with the configured instance and a release function
|
|
198
|
+
*/
|
|
199
|
+
export async function acquireInstance(workerOptions: WorkerOptions): Promise<{
|
|
200
|
+
instance: MiniflareType
|
|
201
|
+
release: () => Promise<void>
|
|
202
|
+
isPooled: boolean
|
|
203
|
+
}> {
|
|
204
|
+
if (isShuttingDown) {
|
|
205
|
+
throw new Error('Pool is shutting down')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Start idle cleanup if not started
|
|
209
|
+
if (!idleCleanupInterval) {
|
|
210
|
+
startIdleCleanup()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { script, compatibilityDate = '2026-01-01', outboundService } = workerOptions
|
|
214
|
+
|
|
215
|
+
// Build the options for setOptions
|
|
216
|
+
const updateOptions: MiniflareOptionsType = {
|
|
217
|
+
modules: true,
|
|
218
|
+
script,
|
|
219
|
+
compatibilityDate,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Only add outboundService if it's defined (for blocking network)
|
|
223
|
+
if (outboundService !== undefined) {
|
|
224
|
+
updateOptions.outboundService = outboundService
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Try to find an available instance
|
|
228
|
+
const available = pool.find((p) => !p.inUse)
|
|
229
|
+
if (available) {
|
|
230
|
+
available.inUse = true
|
|
231
|
+
// Reconfigure the instance with the new worker script
|
|
232
|
+
await available.instance.setOptions(updateOptions)
|
|
233
|
+
return {
|
|
234
|
+
instance: available.instance,
|
|
235
|
+
release: async () => {
|
|
236
|
+
available.inUse = false
|
|
237
|
+
available.lastUsed = Date.now()
|
|
238
|
+
},
|
|
239
|
+
isPooled: true,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Create new instance if pool not full
|
|
244
|
+
if (pool.length < poolConfig.size) {
|
|
245
|
+
const Miniflare = await getMiniflareClass()
|
|
246
|
+
const instance = new Miniflare(updateOptions)
|
|
247
|
+
const pooled: PooledInstance = {
|
|
248
|
+
instance,
|
|
249
|
+
inUse: true,
|
|
250
|
+
lastUsed: Date.now(),
|
|
251
|
+
createdAt: Date.now(),
|
|
252
|
+
}
|
|
253
|
+
pool.push(pooled)
|
|
254
|
+
return {
|
|
255
|
+
instance,
|
|
256
|
+
release: async () => {
|
|
257
|
+
pooled.inUse = false
|
|
258
|
+
pooled.lastUsed = Date.now()
|
|
259
|
+
},
|
|
260
|
+
isPooled: true,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Pool exhausted - create temporary instance
|
|
265
|
+
const Miniflare = await getMiniflareClass()
|
|
266
|
+
const tempInstance = new Miniflare(updateOptions)
|
|
267
|
+
return {
|
|
268
|
+
instance: tempInstance,
|
|
269
|
+
release: async () => {
|
|
270
|
+
// Dispose temporary instance immediately
|
|
271
|
+
await tempInstance.dispose()
|
|
272
|
+
},
|
|
273
|
+
isPooled: false,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Pre-warm the pool with instances
|
|
279
|
+
*
|
|
280
|
+
* Call this at application startup to avoid cold start latency.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* import { warmPool } from 'ai-evaluate/node'
|
|
285
|
+
*
|
|
286
|
+
* // Pre-warm 3 instances at startup
|
|
287
|
+
* await warmPool(3)
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
export async function warmPool(count?: number): Promise<void> {
|
|
291
|
+
const targetCount = count ?? poolConfig.size
|
|
292
|
+
const toCreate = Math.max(0, targetCount - pool.length)
|
|
293
|
+
|
|
294
|
+
const promises: Promise<void>[] = []
|
|
295
|
+
for (let i = 0; i < toCreate; i++) {
|
|
296
|
+
promises.push(
|
|
297
|
+
(async () => {
|
|
298
|
+
const instance = await createInstance()
|
|
299
|
+
pool.push({
|
|
300
|
+
instance,
|
|
301
|
+
inUse: false,
|
|
302
|
+
lastUsed: Date.now(),
|
|
303
|
+
createdAt: Date.now(),
|
|
304
|
+
})
|
|
305
|
+
})()
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await Promise.all(promises)
|
|
310
|
+
|
|
311
|
+
// Start idle cleanup if not already started
|
|
312
|
+
if (!idleCleanupInterval) {
|
|
313
|
+
startIdleCleanup()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Dispose all instances and clean up the pool
|
|
319
|
+
*
|
|
320
|
+
* Call this before process exit to ensure clean shutdown.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* import { disposePool } from 'ai-evaluate/node'
|
|
325
|
+
*
|
|
326
|
+
* process.on('beforeExit', async () => {
|
|
327
|
+
* await disposePool()
|
|
328
|
+
* })
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
export async function disposePool(): Promise<void> {
|
|
332
|
+
isShuttingDown = true
|
|
333
|
+
|
|
334
|
+
if (idleCleanupInterval) {
|
|
335
|
+
clearInterval(idleCleanupInterval)
|
|
336
|
+
idleCleanupInterval = null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const instances = [...pool]
|
|
340
|
+
pool = []
|
|
341
|
+
|
|
342
|
+
await Promise.all(
|
|
343
|
+
instances.map(async (item) => {
|
|
344
|
+
try {
|
|
345
|
+
await item.instance.dispose()
|
|
346
|
+
} catch {
|
|
347
|
+
// Ignore disposal errors
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
isShuttingDown = false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Reset the pool (for testing purposes)
|
|
357
|
+
*/
|
|
358
|
+
export async function resetPool(): Promise<void> {
|
|
359
|
+
await disposePool()
|
|
360
|
+
poolConfig = {
|
|
361
|
+
size: 3,
|
|
362
|
+
maxIdleTime: 30000,
|
|
363
|
+
}
|
|
364
|
+
MiniflareClass = null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Register cleanup on process exit
|
|
368
|
+
if (typeof process !== 'undefined') {
|
|
369
|
+
const cleanup = () => {
|
|
370
|
+
isShuttingDown = true
|
|
371
|
+
if (idleCleanupInterval) {
|
|
372
|
+
clearInterval(idleCleanupInterval)
|
|
373
|
+
}
|
|
374
|
+
// Synchronous disposal attempt - best effort
|
|
375
|
+
for (const item of pool) {
|
|
376
|
+
try {
|
|
377
|
+
// Fire and forget - we're exiting anyway
|
|
378
|
+
item.instance.dispose().catch(() => {})
|
|
379
|
+
} catch {
|
|
380
|
+
// Ignore errors during shutdown
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
pool = []
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
process.on('exit', cleanup)
|
|
387
|
+
process.on('SIGINT', () => {
|
|
388
|
+
cleanup()
|
|
389
|
+
process.exit(0)
|
|
390
|
+
})
|
|
391
|
+
process.on('SIGTERM', () => {
|
|
392
|
+
cleanup()
|
|
393
|
+
process.exit(0)
|
|
394
|
+
})
|
|
395
|
+
}
|
package/src/node.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* For Workers-only builds, import from 'ai-evaluate' instead.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
9
|
-
|
|
8
|
+
import type {
|
|
9
|
+
EvaluateOptions,
|
|
10
|
+
EvaluateResult,
|
|
11
|
+
WorkerLoader,
|
|
12
|
+
SandboxEnv,
|
|
13
|
+
FetchConfig,
|
|
14
|
+
} from './types.js'
|
|
15
|
+
import { generateWorkerCode, generateDevWorkerCode } from './worker-template/index.js'
|
|
16
|
+
import { isDomainAllowed, normalizeImports } from './shared.js'
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* Check if code contains JSX syntax that needs transformation
|
|
@@ -60,6 +67,7 @@ export async function evaluate(
|
|
|
60
67
|
module: transformedModule,
|
|
61
68
|
tests: transformedTests,
|
|
62
69
|
script: transformedScript,
|
|
70
|
+
imports: normalizeImports(options.imports),
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
// Use worker_loaders if available (Cloudflare Workers)
|
|
@@ -122,6 +130,25 @@ async function evaluateWithWorkerLoader(
|
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Determine if network access should be blocked based on fetch options
|
|
135
|
+
* fetch: false | null -> block
|
|
136
|
+
*/
|
|
137
|
+
function shouldBlockNetwork(options: EvaluateOptions): boolean {
|
|
138
|
+
return options.fetch === false || options.fetch === null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get allowlist domains if fetch is an array
|
|
143
|
+
* fetch: string[] -> allowlist
|
|
144
|
+
*/
|
|
145
|
+
function getAllowlistDomains(options: EvaluateOptions): string[] | null {
|
|
146
|
+
if (Array.isArray(options.fetch)) {
|
|
147
|
+
return options.fetch
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
/**
|
|
126
153
|
* Evaluate using Miniflare (for Node.js/development)
|
|
127
154
|
*/
|
|
@@ -140,20 +167,38 @@ async function evaluateWithMiniflare(
|
|
|
140
167
|
fetch: options.fetch, // Pass fetch option to worker template
|
|
141
168
|
})
|
|
142
169
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
const
|
|
170
|
+
// Determine outbound service configuration based on fetch option
|
|
171
|
+
const blockNetwork = shouldBlockNetwork(options)
|
|
172
|
+
const allowlistDomains = getAllowlistDomains(options)
|
|
173
|
+
|
|
174
|
+
// Build outboundService based on mode:
|
|
175
|
+
// - block: throw error for all requests
|
|
176
|
+
// - allowlist: check domain against allowlist
|
|
177
|
+
// - allow (default): no outboundService (allow all)
|
|
178
|
+
type OutboundServiceFn = (() => never) | ((request: Request) => Response | Promise<Response>)
|
|
179
|
+
let outboundService: OutboundServiceFn | undefined
|
|
180
|
+
if (blockNetwork) {
|
|
181
|
+
outboundService = () => {
|
|
182
|
+
throw new Error('Network access blocked: fetch is disabled in this sandbox')
|
|
183
|
+
}
|
|
184
|
+
} else if (allowlistDomains) {
|
|
185
|
+
outboundService = (request: Request) => {
|
|
186
|
+
const url = request.url
|
|
187
|
+
if (!isDomainAllowed(url, allowlistDomains)) {
|
|
188
|
+
const hostname = new URL(url).hostname
|
|
189
|
+
throw new Error(`Network access blocked: domain not in allowlist. Attempted: ${hostname}`)
|
|
190
|
+
}
|
|
191
|
+
// Allow the request by returning a fetched response
|
|
192
|
+
return fetch(request)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
146
195
|
|
|
147
196
|
const mf = new Miniflare({
|
|
148
197
|
modules: true,
|
|
149
198
|
script: workerCode,
|
|
150
199
|
compatibilityDate: '2026-01-01',
|
|
151
|
-
//
|
|
152
|
-
...(
|
|
153
|
-
outboundService: () => {
|
|
154
|
-
throw new Error('Network access blocked: fetch is disabled in this sandbox')
|
|
155
|
-
},
|
|
156
|
-
}),
|
|
200
|
+
// Configure outbound service based on fetch mode
|
|
201
|
+
...(outboundService && { outboundService }),
|
|
157
202
|
})
|
|
158
203
|
|
|
159
204
|
try {
|