@ricsam/isolate-daemon 0.1.0 → 0.1.4

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.
@@ -1,24 +1,31 @@
1
1
  // @bun
2
2
  // packages/isolate-daemon/src/connection.ts
3
3
  import { randomUUID } from "crypto";
4
+ import ivm from "isolated-vm";
4
5
  import {
5
6
  createFrameParser,
6
7
  buildFrame,
7
8
  MessageType,
8
- ErrorCode
9
+ ErrorCode,
10
+ STREAM_THRESHOLD,
11
+ STREAM_CHUNK_SIZE,
12
+ STREAM_DEFAULT_CREDIT,
13
+ marshalValue,
14
+ unmarshalValue
9
15
  } from "@ricsam/isolate-protocol";
10
16
  import { createCallbackFileSystemHandler } from "./callback-fs-handler.mjs";
11
17
  import {
12
18
  setupTestEnvironment,
13
- runTests as runTestsInContext
19
+ runTests as runTestsInContext,
20
+ hasTests as hasTestsInContext,
21
+ getTestCount as getTestCountInContext
14
22
  } from "@ricsam/isolate-test-environment";
15
23
  import {
16
- setupPlaywright,
17
- runPlaywrightTests,
18
- resetPlaywrightTests
24
+ setupPlaywright
19
25
  } from "@ricsam/isolate-playwright";
20
- import { chromium, firefox, webkit } from "playwright";
21
- import { createRuntime } from "@ricsam/isolate-runtime";
26
+ import {
27
+ createInternalRuntime
28
+ } from "@ricsam/isolate-runtime";
22
29
  function handleConnection(socket, state) {
23
30
  const connection = {
24
31
  socket,
@@ -27,7 +34,9 @@ function handleConnection(socket, state) {
27
34
  pendingCallbacks: new Map,
28
35
  nextRequestId: 1,
29
36
  nextCallbackId: 1,
30
- nextStreamId: 1
37
+ nextStreamId: 1,
38
+ activeStreams: new Map,
39
+ streamReceivers: new Map
31
40
  };
32
41
  state.connections.set(socket, connection);
33
42
  const parser = createFrameParser();
@@ -51,12 +60,6 @@ function handleConnection(socket, state) {
51
60
  if (instance.playwrightHandle) {
52
61
  instance.playwrightHandle.dispose();
53
62
  }
54
- if (instance.browserContext) {
55
- instance.browserContext.close().catch(() => {});
56
- }
57
- if (instance.browser) {
58
- instance.browser.close().catch(() => {});
59
- }
60
63
  instance.runtime.dispose();
61
64
  } catch {}
62
65
  state.isolates.delete(isolateId);
@@ -108,20 +111,56 @@ async function handleMessage(message, connection, state) {
108
111
  case MessageType.DISPATCH_REQUEST:
109
112
  await handleDispatchRequest(message, connection, state);
110
113
  break;
111
- case MessageType.TICK:
112
- await handleTick(message, connection, state);
113
- break;
114
114
  case MessageType.CALLBACK_RESPONSE:
115
115
  handleCallbackResponse(message, connection);
116
116
  break;
117
- case MessageType.SETUP_TEST_ENV:
118
- await handleSetupTestEnv(message, connection, state);
117
+ case MessageType.WS_OPEN:
118
+ await handleWsOpen(message, connection, state);
119
+ break;
120
+ case MessageType.WS_MESSAGE:
121
+ await handleWsMessage(message, connection, state);
122
+ break;
123
+ case MessageType.WS_CLOSE:
124
+ await handleWsClose(message, connection, state);
125
+ break;
126
+ case MessageType.FETCH_GET_UPGRADE_REQUEST:
127
+ await handleFetchGetUpgradeRequest(message, connection, state);
128
+ break;
129
+ case MessageType.FETCH_HAS_SERVE_HANDLER:
130
+ await handleFetchHasServeHandler(message, connection, state);
131
+ break;
132
+ case MessageType.FETCH_HAS_ACTIVE_CONNECTIONS:
133
+ await handleFetchHasActiveConnections(message, connection, state);
134
+ break;
135
+ case MessageType.FETCH_WS_ERROR:
136
+ await handleFetchWsError(message, connection, state);
137
+ break;
138
+ case MessageType.TIMERS_CLEAR_ALL:
139
+ await handleTimersClearAll(message, connection, state);
140
+ break;
141
+ case MessageType.CONSOLE_RESET:
142
+ await handleConsoleReset(message, connection, state);
143
+ break;
144
+ case MessageType.CONSOLE_GET_TIMERS:
145
+ await handleConsoleGetTimers(message, connection, state);
146
+ break;
147
+ case MessageType.CONSOLE_GET_COUNTERS:
148
+ await handleConsoleGetCounters(message, connection, state);
149
+ break;
150
+ case MessageType.CONSOLE_GET_GROUP_DEPTH:
151
+ await handleConsoleGetGroupDepth(message, connection, state);
119
152
  break;
120
153
  case MessageType.RUN_TESTS:
121
154
  await handleRunTests(message, connection, state);
122
155
  break;
123
- case MessageType.SETUP_PLAYWRIGHT:
124
- await handleSetupPlaywright(message, connection, state);
156
+ case MessageType.RESET_TEST_ENV:
157
+ await handleResetTestEnv(message, connection, state);
158
+ break;
159
+ case MessageType.HAS_TESTS:
160
+ await handleHasTests(message, connection, state);
161
+ break;
162
+ case MessageType.GET_TEST_COUNT:
163
+ await handleGetTestCount(message, connection, state);
125
164
  break;
126
165
  case MessageType.RUN_PLAYWRIGHT_TESTS:
127
166
  await handleRunPlaywrightTests(message, connection, state);
@@ -132,9 +171,24 @@ async function handleMessage(message, connection, state) {
132
171
  case MessageType.GET_COLLECTED_DATA:
133
172
  await handleGetCollectedData(message, connection, state);
134
173
  break;
174
+ case MessageType.CLEAR_COLLECTED_DATA:
175
+ await handleClearCollectedData(message, connection, state);
176
+ break;
135
177
  case MessageType.PING:
136
178
  sendMessage(connection.socket, { type: MessageType.PONG });
137
179
  break;
180
+ case MessageType.STREAM_PUSH:
181
+ handleStreamPush(message, connection);
182
+ break;
183
+ case MessageType.STREAM_PULL:
184
+ handleStreamPull(message, connection);
185
+ break;
186
+ case MessageType.STREAM_CLOSE:
187
+ handleStreamClose(message, connection);
188
+ break;
189
+ case MessageType.STREAM_ERROR:
190
+ handleStreamError(message, connection);
191
+ break;
138
192
  default:
139
193
  sendError(connection.socket, message.requestId ?? 0, ErrorCode.UNKNOWN_MESSAGE_TYPE, `Unknown message type: ${message.type}`);
140
194
  }
@@ -149,16 +203,16 @@ async function handleCreateRuntime(message, connection, state) {
149
203
  const consoleCallbacks = message.options.callbacks?.console;
150
204
  const fetchCallback = message.options.callbacks?.fetch;
151
205
  const fsCallbacks = message.options.callbacks?.fs;
152
- const runtime = await createRuntime({
153
- memoryLimit: message.options.memoryLimit ?? state.options.defaultMemoryLimit,
154
- console: consoleCallbacks ? {
155
- onLog: async (level, ...args) => {
156
- const levelCallback = consoleCallbacks[level];
157
- if (levelCallback) {
158
- await invokeClientCallback(connection, levelCallback.callbackId, [level, ...args]);
159
- } else if (consoleCallbacks.log) {
160
- await invokeClientCallback(connection, consoleCallbacks.log.callbackId, [level, ...args]);
161
- }
206
+ const moduleLoaderCallback = message.options.callbacks?.moduleLoader;
207
+ const customCallbacks = message.options.callbacks?.custom;
208
+ const pendingCallbacks = [];
209
+ const runtime = await createInternalRuntime({
210
+ memoryLimitMB: message.options.memoryLimitMB ?? state.options.defaultMemoryLimitMB,
211
+ cwd: message.options.cwd,
212
+ console: consoleCallbacks?.onEntry ? {
213
+ onEntry: (entry) => {
214
+ const promise = invokeClientCallback(connection, consoleCallbacks.onEntry.callbackId, [entry]).catch(() => {});
215
+ pendingCallbacks.push(promise);
162
216
  }
163
217
  } : undefined,
164
218
  fetch: fetchCallback ? {
@@ -185,14 +239,25 @@ async function handleCreateRuntime(message, connection, state) {
185
239
  ownerConnection: connection.socket,
186
240
  callbacks: new Map,
187
241
  createdAt: Date.now(),
188
- lastActivity: Date.now()
242
+ lastActivity: Date.now(),
243
+ pendingCallbacks,
244
+ returnedCallbacks: new Map,
245
+ returnedPromises: new Map,
246
+ returnedIterators: new Map,
247
+ nextLocalCallbackId: 1e6
189
248
  };
190
- if (consoleCallbacks) {
191
- for (const [name, reg] of Object.entries(consoleCallbacks)) {
192
- if (reg) {
193
- instance.callbacks.set(reg.callbackId, { ...reg, name });
194
- }
195
- }
249
+ if (moduleLoaderCallback) {
250
+ instance.moduleLoaderCallbackId = moduleLoaderCallback.callbackId;
251
+ instance.moduleCache = new Map;
252
+ }
253
+ if (customCallbacks) {
254
+ await setupCustomFunctions(runtime.context, customCallbacks, connection, instance);
255
+ }
256
+ if (consoleCallbacks?.onEntry) {
257
+ instance.callbacks.set(consoleCallbacks.onEntry.callbackId, {
258
+ ...consoleCallbacks.onEntry,
259
+ name: "onEntry"
260
+ });
196
261
  }
197
262
  if (fetchCallback) {
198
263
  instance.callbacks.set(fetchCallback.callbackId, fetchCallback);
@@ -204,9 +269,86 @@ async function handleCreateRuntime(message, connection, state) {
204
269
  }
205
270
  }
206
271
  }
272
+ if (moduleLoaderCallback) {
273
+ instance.callbacks.set(moduleLoaderCallback.callbackId, moduleLoaderCallback);
274
+ }
275
+ if (customCallbacks) {
276
+ for (const [name, reg] of Object.entries(customCallbacks)) {
277
+ if (reg) {
278
+ instance.callbacks.set(reg.callbackId, { ...reg, name });
279
+ }
280
+ }
281
+ }
282
+ if (message.options.testEnvironment) {
283
+ const testEnvOption = message.options.testEnvironment;
284
+ const testEnvOptions = typeof testEnvOption === "object" ? testEnvOption : undefined;
285
+ const onEventCallback = testEnvOptions?.callbacks?.onEvent;
286
+ await setupTestEnvironment(runtime.context, {
287
+ onEvent: onEventCallback ? (event) => {
288
+ const promise = invokeClientCallback(connection, onEventCallback.callbackId, [JSON.stringify(event)]).catch(() => {});
289
+ pendingCallbacks.push(promise);
290
+ } : undefined,
291
+ testTimeout: testEnvOptions?.testTimeout
292
+ });
293
+ instance.testEnvironmentEnabled = true;
294
+ if (onEventCallback) {
295
+ instance.callbacks.set(onEventCallback.callbackId, {
296
+ ...onEventCallback,
297
+ name: "testEnvironment.onEvent"
298
+ });
299
+ }
300
+ }
301
+ const playwrightCallbacks = message.options.callbacks?.playwright;
302
+ if (playwrightCallbacks) {
303
+ const handler = async (op) => {
304
+ try {
305
+ const resultJson = await invokeClientCallback(connection, playwrightCallbacks.handlerCallbackId, [JSON.stringify(op)]);
306
+ return JSON.parse(resultJson);
307
+ } catch (err) {
308
+ const error = err;
309
+ return { ok: false, error: { name: error.name, message: error.message } };
310
+ }
311
+ };
312
+ instance.playwrightHandle = await setupPlaywright(runtime.context, {
313
+ handler,
314
+ console: playwrightCallbacks.console,
315
+ onEvent: (event) => {
316
+ if (event.type === "browserConsoleLog" && playwrightCallbacks.onBrowserConsoleLogCallbackId) {
317
+ const promise = invokeClientCallback(connection, playwrightCallbacks.onBrowserConsoleLogCallbackId, [{ level: event.level, args: event.args, timestamp: event.timestamp }]).catch(() => {});
318
+ pendingCallbacks.push(promise);
319
+ } else if (event.type === "networkRequest" && playwrightCallbacks.onNetworkRequestCallbackId) {
320
+ const promise = invokeClientCallback(connection, playwrightCallbacks.onNetworkRequestCallbackId, [event]).catch(() => {});
321
+ pendingCallbacks.push(promise);
322
+ } else if (event.type === "networkResponse" && playwrightCallbacks.onNetworkResponseCallbackId) {
323
+ const promise = invokeClientCallback(connection, playwrightCallbacks.onNetworkResponseCallbackId, [event]).catch(() => {});
324
+ pendingCallbacks.push(promise);
325
+ }
326
+ }
327
+ });
328
+ }
207
329
  state.isolates.set(isolateId, instance);
208
330
  connection.isolates.add(isolateId);
209
331
  state.stats.totalIsolatesCreated++;
332
+ instance.runtime.fetch.onWebSocketCommand((cmd) => {
333
+ let data;
334
+ if (cmd.data instanceof ArrayBuffer) {
335
+ data = new Uint8Array(cmd.data);
336
+ } else {
337
+ data = cmd.data;
338
+ }
339
+ const wsCommandMsg = {
340
+ type: MessageType.WS_COMMAND,
341
+ isolateId,
342
+ command: {
343
+ type: cmd.type,
344
+ connectionId: cmd.connectionId,
345
+ data,
346
+ code: cmd.code,
347
+ reason: cmd.reason
348
+ }
349
+ };
350
+ sendMessage(connection.socket, wsCommandMsg);
351
+ });
210
352
  sendOk(connection.socket, message.requestId, { isolateId });
211
353
  } catch (err) {
212
354
  const error = err;
@@ -227,12 +369,6 @@ async function handleDisposeRuntime(message, connection, state) {
227
369
  if (instance.playwrightHandle) {
228
370
  instance.playwrightHandle.dispose();
229
371
  }
230
- if (instance.browserContext) {
231
- await instance.browserContext.close();
232
- }
233
- if (instance.browser) {
234
- await instance.browser.close();
235
- }
236
372
  instance.runtime.dispose();
237
373
  state.isolates.delete(message.isolateId);
238
374
  connection.isolates.delete(message.isolateId);
@@ -250,13 +386,26 @@ async function handleEval(message, connection, state) {
250
386
  }
251
387
  instance.lastActivity = Date.now();
252
388
  try {
253
- const result = instance.runtime.context.evalSync(message.code, {
254
- filename: message.filename
389
+ const mod = await instance.runtime.isolate.compileModule(message.code, {
390
+ filename: message.filename ?? "<eval>"
255
391
  });
256
- sendOk(connection.socket, message.requestId, { value: result });
392
+ if (instance.moduleLoaderCallbackId) {
393
+ const resolver = createModuleResolver(instance, connection);
394
+ await mod.instantiate(instance.runtime.context, resolver);
395
+ } else {
396
+ await mod.instantiate(instance.runtime.context, (specifier) => {
397
+ throw new Error(`No module loader registered. Cannot import: ${specifier}`);
398
+ });
399
+ }
400
+ const timeout = message.maxExecutionMs;
401
+ await mod.evaluate(timeout ? { timeout } : undefined);
402
+ await Promise.all(instance.pendingCallbacks);
403
+ instance.pendingCallbacks.length = 0;
404
+ sendOk(connection.socket, message.requestId, { value: undefined });
257
405
  } catch (err) {
258
406
  const error = err;
259
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
407
+ const isTimeoutError = error.message?.includes("Script execution timed out");
408
+ sendError(connection.socket, message.requestId, isTimeoutError ? ErrorCode.ISOLATE_TIMEOUT : ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
260
409
  }
261
410
  }
262
411
  async function handleDispatchRequest(message, connection, state) {
@@ -267,24 +416,135 @@ async function handleDispatchRequest(message, connection, state) {
267
416
  }
268
417
  instance.lastActivity = Date.now();
269
418
  try {
419
+ let requestBody = null;
420
+ if (message.request.bodyStreamId !== undefined) {
421
+ requestBody = await receiveStreamedBody(connection, message.request.bodyStreamId);
422
+ } else if (message.request.body) {
423
+ requestBody = message.request.body;
424
+ }
270
425
  const request = new Request(message.request.url, {
271
426
  method: message.request.method,
272
427
  headers: message.request.headers,
273
- body: message.request.body
428
+ body: requestBody
274
429
  });
275
- const response = await instance.runtime.fetch.dispatchRequest(request, {
276
- tick: async () => {
277
- await instance.runtime.tick();
430
+ const response = await instance.runtime.fetch.dispatchRequest(request);
431
+ const contentLength = response.headers.get("content-length");
432
+ const knownSize = contentLength ? parseInt(contentLength, 10) : null;
433
+ if (knownSize !== null && knownSize > STREAM_THRESHOLD) {
434
+ await sendStreamedResponse(connection, message.requestId, response);
435
+ } else {
436
+ const clonedResponse = response.clone();
437
+ try {
438
+ const serialized = await serializeResponse(response);
439
+ if (serialized.body && serialized.body.length > STREAM_THRESHOLD) {
440
+ await sendStreamedResponse(connection, message.requestId, clonedResponse);
441
+ } else {
442
+ sendOk(connection.socket, message.requestId, { response: serialized });
443
+ }
444
+ } catch {
445
+ await sendStreamedResponse(connection, message.requestId, clonedResponse);
278
446
  }
447
+ }
448
+ } catch (err) {
449
+ const error = err;
450
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
451
+ }
452
+ }
453
+ function receiveStreamedBody(connection, streamId) {
454
+ return new Promise((resolve, reject) => {
455
+ const receiver = {
456
+ streamId,
457
+ requestId: 0,
458
+ chunks: [],
459
+ totalBytes: 0,
460
+ resolve,
461
+ reject
462
+ };
463
+ connection.streamReceivers.set(streamId, receiver);
464
+ sendMessage(connection.socket, {
465
+ type: MessageType.STREAM_PULL,
466
+ streamId,
467
+ maxBytes: STREAM_DEFAULT_CREDIT
279
468
  });
280
- const serialized = await serializeResponse(response);
281
- sendOk(connection.socket, message.requestId, { response: serialized });
469
+ });
470
+ }
471
+ async function handleWsOpen(message, connection, state) {
472
+ const instance = state.isolates.get(message.isolateId);
473
+ if (!instance) {
474
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
475
+ return;
476
+ }
477
+ instance.lastActivity = Date.now();
478
+ try {
479
+ instance.runtime.fetch.dispatchWebSocketOpen(message.connectionId);
480
+ sendOk(connection.socket, message.requestId);
481
+ } catch (err) {
482
+ const error = err;
483
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
484
+ }
485
+ }
486
+ async function handleWsMessage(message, connection, state) {
487
+ const instance = state.isolates.get(message.isolateId);
488
+ if (!instance) {
489
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
490
+ return;
491
+ }
492
+ instance.lastActivity = Date.now();
493
+ try {
494
+ const data = message.data instanceof Uint8Array ? message.data.buffer.slice(message.data.byteOffset, message.data.byteOffset + message.data.byteLength) : message.data;
495
+ instance.runtime.fetch.dispatchWebSocketMessage(message.connectionId, data);
496
+ sendOk(connection.socket, message.requestId);
497
+ } catch (err) {
498
+ const error = err;
499
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
500
+ }
501
+ }
502
+ async function handleWsClose(message, connection, state) {
503
+ const instance = state.isolates.get(message.isolateId);
504
+ if (!instance) {
505
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
506
+ return;
507
+ }
508
+ instance.lastActivity = Date.now();
509
+ try {
510
+ instance.runtime.fetch.dispatchWebSocketClose(message.connectionId, message.code, message.reason);
511
+ sendOk(connection.socket, message.requestId);
512
+ } catch (err) {
513
+ const error = err;
514
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
515
+ }
516
+ }
517
+ async function handleFetchGetUpgradeRequest(message, connection, state) {
518
+ const instance = state.isolates.get(message.isolateId);
519
+ if (!instance) {
520
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
521
+ return;
522
+ }
523
+ instance.lastActivity = Date.now();
524
+ try {
525
+ const upgradeRequest = instance.runtime.fetch.getUpgradeRequest();
526
+ sendOk(connection.socket, message.requestId, upgradeRequest);
527
+ } catch (err) {
528
+ const error = err;
529
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
530
+ }
531
+ }
532
+ async function handleFetchHasServeHandler(message, connection, state) {
533
+ const instance = state.isolates.get(message.isolateId);
534
+ if (!instance) {
535
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
536
+ return;
537
+ }
538
+ instance.lastActivity = Date.now();
539
+ try {
540
+ const hasHandler = instance.runtime.fetch.hasServeHandler();
541
+ sendOk(connection.socket, message.requestId, hasHandler);
282
542
  } catch (err) {
283
543
  const error = err;
284
544
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
285
545
  }
286
546
  }
287
- async function handleTick(message, connection, state) {
547
+ async function handleFetchHasActiveConnections(message, connection, state) {
288
548
  const instance = state.isolates.get(message.isolateId);
289
549
  if (!instance) {
290
550
  sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
@@ -292,13 +552,103 @@ async function handleTick(message, connection, state) {
292
552
  }
293
553
  instance.lastActivity = Date.now();
294
554
  try {
295
- instance.runtime.tick(message.ms);
555
+ const hasConnections = instance.runtime.fetch.hasActiveConnections();
556
+ sendOk(connection.socket, message.requestId, hasConnections);
557
+ } catch (err) {
558
+ const error = err;
559
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
560
+ }
561
+ }
562
+ async function handleFetchWsError(message, connection, state) {
563
+ const instance = state.isolates.get(message.isolateId);
564
+ if (!instance) {
565
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
566
+ return;
567
+ }
568
+ instance.lastActivity = Date.now();
569
+ try {
570
+ instance.runtime.fetch.dispatchWebSocketError(message.connectionId, new Error(message.error));
296
571
  sendOk(connection.socket, message.requestId);
297
572
  } catch (err) {
298
573
  const error = err;
299
574
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
300
575
  }
301
576
  }
577
+ async function handleTimersClearAll(message, connection, state) {
578
+ const instance = state.isolates.get(message.isolateId);
579
+ if (!instance) {
580
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
581
+ return;
582
+ }
583
+ instance.lastActivity = Date.now();
584
+ try {
585
+ instance.runtime.timers.clearAll();
586
+ sendOk(connection.socket, message.requestId);
587
+ } catch (err) {
588
+ const error = err;
589
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
590
+ }
591
+ }
592
+ async function handleConsoleReset(message, connection, state) {
593
+ const instance = state.isolates.get(message.isolateId);
594
+ if (!instance) {
595
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
596
+ return;
597
+ }
598
+ instance.lastActivity = Date.now();
599
+ try {
600
+ instance.runtime.console.reset();
601
+ sendOk(connection.socket, message.requestId);
602
+ } catch (err) {
603
+ const error = err;
604
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
605
+ }
606
+ }
607
+ async function handleConsoleGetTimers(message, connection, state) {
608
+ const instance = state.isolates.get(message.isolateId);
609
+ if (!instance) {
610
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
611
+ return;
612
+ }
613
+ instance.lastActivity = Date.now();
614
+ try {
615
+ const timers = instance.runtime.console.getTimers();
616
+ sendOk(connection.socket, message.requestId, Object.fromEntries(timers));
617
+ } catch (err) {
618
+ const error = err;
619
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
620
+ }
621
+ }
622
+ async function handleConsoleGetCounters(message, connection, state) {
623
+ const instance = state.isolates.get(message.isolateId);
624
+ if (!instance) {
625
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
626
+ return;
627
+ }
628
+ instance.lastActivity = Date.now();
629
+ try {
630
+ const counters = instance.runtime.console.getCounters();
631
+ sendOk(connection.socket, message.requestId, Object.fromEntries(counters));
632
+ } catch (err) {
633
+ const error = err;
634
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
635
+ }
636
+ }
637
+ async function handleConsoleGetGroupDepth(message, connection, state) {
638
+ const instance = state.isolates.get(message.isolateId);
639
+ if (!instance) {
640
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
641
+ return;
642
+ }
643
+ instance.lastActivity = Date.now();
644
+ try {
645
+ const depth = instance.runtime.console.getGroupDepth();
646
+ sendOk(connection.socket, message.requestId, depth);
647
+ } catch (err) {
648
+ const error = err;
649
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
650
+ }
651
+ }
302
652
  function handleCallbackResponse(message, connection) {
303
653
  const pending = connection.pendingCallbacks.get(message.requestId);
304
654
  if (!pending) {
@@ -342,6 +692,477 @@ async function invokeClientCallback(connection, callbackId, args, timeout = 1e4)
342
692
  sendMessage(connection.socket, invoke);
343
693
  });
344
694
  }
695
+ var ISOLATE_MARSHAL_CODE = `
696
+ (function() {
697
+ // Marshal a value (JavaScript \u2192 Ref)
698
+ function marshalForHost(value, depth = 0) {
699
+ if (depth > 100) throw new Error('Maximum marshalling depth exceeded');
700
+
701
+ if (value === null) return null;
702
+ if (value === undefined) return { __type: 'UndefinedRef' };
703
+
704
+ const type = typeof value;
705
+ if (type === 'string' || type === 'number' || type === 'boolean') return value;
706
+ if (type === 'bigint') return { __type: 'BigIntRef', value: value.toString() };
707
+ if (type === 'function') throw new Error('Cannot marshal functions from isolate');
708
+ if (type === 'symbol') throw new Error('Cannot marshal Symbol values');
709
+
710
+ if (type === 'object') {
711
+ if (value instanceof Date) {
712
+ return { __type: 'DateRef', timestamp: value.getTime() };
713
+ }
714
+ if (value instanceof RegExp) {
715
+ return { __type: 'RegExpRef', source: value.source, flags: value.flags };
716
+ }
717
+ if (value instanceof URL) {
718
+ return { __type: 'URLRef', href: value.href };
719
+ }
720
+ if (typeof Headers !== 'undefined' && value instanceof Headers) {
721
+ const pairs = [];
722
+ value.forEach((v, k) => pairs.push([k, v]));
723
+ return { __type: 'HeadersRef', pairs };
724
+ }
725
+ if (value instanceof Uint8Array) {
726
+ return { __type: 'Uint8ArrayRef', data: Array.from(value) };
727
+ }
728
+ if (value instanceof ArrayBuffer) {
729
+ return { __type: 'Uint8ArrayRef', data: Array.from(new Uint8Array(value)) };
730
+ }
731
+ if (typeof Request !== 'undefined' && value instanceof Request) {
732
+ throw new Error('Cannot marshal Request from isolate. Use fetch callback instead.');
733
+ }
734
+ if (typeof Response !== 'undefined' && value instanceof Response) {
735
+ throw new Error('Cannot marshal Response from isolate. Return plain objects instead.');
736
+ }
737
+ if (typeof File !== 'undefined' && value instanceof File) {
738
+ throw new Error('Cannot marshal File from isolate.');
739
+ }
740
+ if (typeof Blob !== 'undefined' && value instanceof Blob) {
741
+ throw new Error('Cannot marshal Blob from isolate.');
742
+ }
743
+ if (typeof FormData !== 'undefined' && value instanceof FormData) {
744
+ throw new Error('Cannot marshal FormData from isolate.');
745
+ }
746
+ if (Array.isArray(value)) {
747
+ return value.map(v => marshalForHost(v, depth + 1));
748
+ }
749
+ // Plain object
750
+ const result = {};
751
+ for (const key of Object.keys(value)) {
752
+ result[key] = marshalForHost(value[key], depth + 1);
753
+ }
754
+ return result;
755
+ }
756
+ return value;
757
+ }
758
+
759
+ // Unmarshal a value (Ref \u2192 JavaScript)
760
+ function unmarshalFromHost(value, depth = 0) {
761
+ if (depth > 100) throw new Error('Maximum unmarshalling depth exceeded');
762
+
763
+ if (value === null) return null;
764
+ if (typeof value !== 'object') return value;
765
+
766
+ if (value.__type) {
767
+ switch (value.__type) {
768
+ case 'UndefinedRef': return undefined;
769
+ case 'DateRef': return new Date(value.timestamp);
770
+ case 'RegExpRef': return new RegExp(value.source, value.flags);
771
+ case 'BigIntRef': return BigInt(value.value);
772
+ case 'URLRef': return new URL(value.href);
773
+ case 'HeadersRef': return new Headers(value.pairs);
774
+ case 'Uint8ArrayRef': return new Uint8Array(value.data);
775
+ case 'RequestRef': {
776
+ const init = {
777
+ method: value.method,
778
+ headers: value.headers,
779
+ body: value.body ? new Uint8Array(value.body) : null,
780
+ };
781
+ if (value.mode) init.mode = value.mode;
782
+ if (value.credentials) init.credentials = value.credentials;
783
+ if (value.cache) init.cache = value.cache;
784
+ if (value.redirect) init.redirect = value.redirect;
785
+ if (value.referrer) init.referrer = value.referrer;
786
+ if (value.referrerPolicy) init.referrerPolicy = value.referrerPolicy;
787
+ if (value.integrity) init.integrity = value.integrity;
788
+ return new Request(value.url, init);
789
+ }
790
+ case 'ResponseRef': {
791
+ return new Response(value.body ? new Uint8Array(value.body) : null, {
792
+ status: value.status,
793
+ statusText: value.statusText,
794
+ headers: value.headers,
795
+ });
796
+ }
797
+ case 'FileRef': {
798
+ if (!value.name) {
799
+ return new Blob([new Uint8Array(value.data)], { type: value.type });
800
+ }
801
+ return new File([new Uint8Array(value.data)], value.name, {
802
+ type: value.type,
803
+ lastModified: value.lastModified,
804
+ });
805
+ }
806
+ case 'FormDataRef': {
807
+ const fd = new FormData();
808
+ for (const [key, entry] of value.entries) {
809
+ if (typeof entry === 'string') {
810
+ fd.append(key, entry);
811
+ } else {
812
+ const file = unmarshalFromHost(entry, depth + 1);
813
+ fd.append(key, file);
814
+ }
815
+ }
816
+ return fd;
817
+ }
818
+ case 'CallbackRef': {
819
+ // Create a proxy function that invokes the callback
820
+ const callbackId = value.callbackId;
821
+ return function(...args) {
822
+ const argsJson = JSON.stringify(marshalForHost(args));
823
+ const resultJson = __customFn_invoke.applySyncPromise(undefined, [callbackId, argsJson]);
824
+ const result = JSON.parse(resultJson);
825
+ if (result.ok) {
826
+ return unmarshalFromHost(result.value);
827
+ } else {
828
+ const error = new Error(result.error.message);
829
+ error.name = result.error.name;
830
+ throw error;
831
+ }
832
+ };
833
+ }
834
+ case 'PromiseRef': {
835
+ // Create a proxy Promise that resolves via callback
836
+ const promiseId = value.promiseId;
837
+ return new Promise((resolve, reject) => {
838
+ try {
839
+ const argsJson = JSON.stringify([promiseId]);
840
+ const resultJson = __customFn_invoke.applySyncPromise(undefined, [value.__resolveCallbackId, argsJson]);
841
+ const result = JSON.parse(resultJson);
842
+ if (result.ok) {
843
+ resolve(unmarshalFromHost(result.value));
844
+ } else {
845
+ reject(new Error(result.error.message));
846
+ }
847
+ } catch (e) {
848
+ reject(e);
849
+ }
850
+ });
851
+ }
852
+ case 'AsyncIteratorRef': {
853
+ const iteratorId = value.iteratorId;
854
+ const nextCallbackId = value.__nextCallbackId;
855
+ const returnCallbackId = value.__returnCallbackId;
856
+ return {
857
+ [Symbol.asyncIterator]() { return this; },
858
+ async next() {
859
+ const argsJson = JSON.stringify([iteratorId]);
860
+ const resultJson = __customFn_invoke.applySyncPromise(undefined, [nextCallbackId, argsJson]);
861
+ const result = JSON.parse(resultJson);
862
+ if (!result.ok) {
863
+ const error = new Error(result.error.message);
864
+ error.name = result.error.name;
865
+ throw error;
866
+ }
867
+ return {
868
+ done: result.value.done,
869
+ value: unmarshalFromHost(result.value.value)
870
+ };
871
+ },
872
+ async return(v) {
873
+ const argsJson = JSON.stringify([iteratorId, marshalForHost(v)]);
874
+ const resultJson = __customFn_invoke.applySyncPromise(undefined, [returnCallbackId, argsJson]);
875
+ const result = JSON.parse(resultJson);
876
+ return { done: true, value: result.ok ? unmarshalFromHost(result.value) : undefined };
877
+ }
878
+ };
879
+ }
880
+ default:
881
+ // Unknown ref type, return as-is
882
+ break;
883
+ }
884
+ }
885
+
886
+ if (Array.isArray(value)) {
887
+ return value.map(v => unmarshalFromHost(v, depth + 1));
888
+ }
889
+
890
+ // Plain object - recursively unmarshal
891
+ const result = {};
892
+ for (const key of Object.keys(value)) {
893
+ result[key] = unmarshalFromHost(value[key], depth + 1);
894
+ }
895
+ return result;
896
+ }
897
+
898
+ globalThis.__marshalForHost = marshalForHost;
899
+ globalThis.__unmarshalFromHost = unmarshalFromHost;
900
+ })();
901
+ `;
902
+ var LOCAL_CALLBACK_THRESHOLD = 1e6;
903
+ function isPromiseRef(value) {
904
+ return typeof value === "object" && value !== null && value.__type === "PromiseRef";
905
+ }
906
+ function isAsyncIteratorRef(value) {
907
+ return typeof value === "object" && value !== null && value.__type === "AsyncIteratorRef";
908
+ }
909
+ function isLocalCallbackId(callbackId) {
910
+ return callbackId >= LOCAL_CALLBACK_THRESHOLD;
911
+ }
912
+ async function setupCustomFunctions(context, customCallbacks, connection, instance) {
913
+ const global = context.global;
914
+ function createMarshalContext() {
915
+ return {
916
+ registerCallback: (fn) => {
917
+ const callbackId = instance.nextLocalCallbackId++;
918
+ instance.returnedCallbacks.set(callbackId, fn);
919
+ return callbackId;
920
+ },
921
+ registerPromise: (promise) => {
922
+ const promiseId = instance.nextLocalCallbackId++;
923
+ instance.returnedPromises.set(promiseId, promise);
924
+ return promiseId;
925
+ },
926
+ registerIterator: (iterator) => {
927
+ const iteratorId = instance.nextLocalCallbackId++;
928
+ instance.returnedIterators.set(iteratorId, iterator);
929
+ return iteratorId;
930
+ }
931
+ };
932
+ }
933
+ function addCallbackIdsToRefs(value) {
934
+ if (value === null || typeof value !== "object") {
935
+ return value;
936
+ }
937
+ if (isPromiseRef(value)) {
938
+ if ("__resolveCallbackId" in value) {
939
+ return value;
940
+ }
941
+ const resolveCallbackId = instance.nextLocalCallbackId++;
942
+ instance.returnedCallbacks.set(resolveCallbackId, async (promiseId) => {
943
+ const promise = instance.returnedPromises.get(promiseId);
944
+ if (!promise) {
945
+ throw new Error(`Promise ${promiseId} not found`);
946
+ }
947
+ const result2 = await promise;
948
+ instance.returnedPromises.delete(promiseId);
949
+ const ctx = createMarshalContext();
950
+ const marshalled = await marshalValue(result2, ctx);
951
+ return addCallbackIdsToRefs(marshalled);
952
+ });
953
+ return {
954
+ ...value,
955
+ __resolveCallbackId: resolveCallbackId
956
+ };
957
+ }
958
+ if (isAsyncIteratorRef(value)) {
959
+ if ("__nextCallbackId" in value) {
960
+ return value;
961
+ }
962
+ const nextCallbackId = instance.nextLocalCallbackId++;
963
+ instance.returnedCallbacks.set(nextCallbackId, async (iteratorId) => {
964
+ const iterator = instance.returnedIterators.get(iteratorId);
965
+ if (!iterator) {
966
+ throw new Error(`Iterator ${iteratorId} not found`);
967
+ }
968
+ const result2 = await iterator.next();
969
+ if (result2.done) {
970
+ instance.returnedIterators.delete(iteratorId);
971
+ }
972
+ const ctx = createMarshalContext();
973
+ const marshalledValue = await marshalValue(result2.value, ctx);
974
+ return {
975
+ done: result2.done,
976
+ value: addCallbackIdsToRefs(marshalledValue)
977
+ };
978
+ });
979
+ const returnCallbackId = instance.nextLocalCallbackId++;
980
+ instance.returnedCallbacks.set(returnCallbackId, async (iteratorId, returnValue) => {
981
+ const iterator = instance.returnedIterators.get(iteratorId);
982
+ instance.returnedIterators.delete(iteratorId);
983
+ if (!iterator || !iterator.return) {
984
+ return { done: true, value: undefined };
985
+ }
986
+ const result2 = await iterator.return(returnValue);
987
+ const ctx = createMarshalContext();
988
+ const marshalledValue = await marshalValue(result2.value, ctx);
989
+ return {
990
+ done: true,
991
+ value: addCallbackIdsToRefs(marshalledValue)
992
+ };
993
+ });
994
+ return {
995
+ ...value,
996
+ __nextCallbackId: nextCallbackId,
997
+ __returnCallbackId: returnCallbackId
998
+ };
999
+ }
1000
+ if (Array.isArray(value)) {
1001
+ return value.map((item) => addCallbackIdsToRefs(item));
1002
+ }
1003
+ const result = {};
1004
+ for (const key of Object.keys(value)) {
1005
+ result[key] = addCallbackIdsToRefs(value[key]);
1006
+ }
1007
+ return result;
1008
+ }
1009
+ const invokeCallbackRef = new ivm.Reference(async (callbackId, argsJson) => {
1010
+ const marshalledArgs = JSON.parse(argsJson);
1011
+ const args = unmarshalValue(marshalledArgs);
1012
+ try {
1013
+ let result;
1014
+ if (isLocalCallbackId(callbackId)) {
1015
+ const callback = instance.returnedCallbacks.get(callbackId);
1016
+ if (!callback) {
1017
+ throw new Error(`Local callback ${callbackId} not found`);
1018
+ }
1019
+ result = await callback(...args);
1020
+ } else {
1021
+ result = await invokeClientCallback(connection, callbackId, args);
1022
+ }
1023
+ const ctx = createMarshalContext();
1024
+ const marshalledResult = await marshalValue({ ok: true, value: result }, ctx);
1025
+ const processedResult = addCallbackIdsToRefs(marshalledResult);
1026
+ return JSON.stringify(processedResult);
1027
+ } catch (error) {
1028
+ const err = error;
1029
+ return JSON.stringify({
1030
+ ok: false,
1031
+ error: { message: err.message, name: err.name }
1032
+ });
1033
+ }
1034
+ });
1035
+ global.setSync("__customFn_invoke", invokeCallbackRef);
1036
+ context.evalSync(ISOLATE_MARSHAL_CODE);
1037
+ for (const [name, registration] of Object.entries(customCallbacks)) {
1038
+ if (name.includes(":")) {
1039
+ continue;
1040
+ }
1041
+ if (registration.type === "sync") {
1042
+ context.evalSync(`
1043
+ globalThis.${name} = function(...args) {
1044
+ const argsJson = JSON.stringify(__marshalForHost(args));
1045
+ const resultJson = __customFn_invoke.applySyncPromise(
1046
+ undefined,
1047
+ [${registration.callbackId}, argsJson]
1048
+ );
1049
+ const result = JSON.parse(resultJson);
1050
+ if (result.ok) {
1051
+ return __unmarshalFromHost(result.value);
1052
+ } else {
1053
+ const error = new Error(result.error.message);
1054
+ error.name = result.error.name;
1055
+ throw error;
1056
+ }
1057
+ };
1058
+ `);
1059
+ } else if (registration.type === "asyncIterator") {
1060
+ const startReg = customCallbacks[`${name}:start`];
1061
+ const nextReg = customCallbacks[`${name}:next`];
1062
+ const returnReg = customCallbacks[`${name}:return`];
1063
+ const throwReg = customCallbacks[`${name}:throw`];
1064
+ if (!startReg || !nextReg || !returnReg || !throwReg) {
1065
+ throw new Error(`Missing companion callbacks for asyncIterator function "${name}"`);
1066
+ }
1067
+ context.evalSync(`
1068
+ globalThis.${name} = function(...args) {
1069
+ // Start the iterator and get the iteratorId
1070
+ const argsJson = JSON.stringify(__marshalForHost(args));
1071
+ const startResultJson = __customFn_invoke.applySyncPromise(
1072
+ undefined,
1073
+ [${startReg.callbackId}, argsJson]
1074
+ );
1075
+ const startResult = JSON.parse(startResultJson);
1076
+ if (!startResult.ok) {
1077
+ const error = new Error(startResult.error.message);
1078
+ error.name = startResult.error.name;
1079
+ throw error;
1080
+ }
1081
+ const iteratorId = __unmarshalFromHost(startResult.value).iteratorId;
1082
+
1083
+ return {
1084
+ [Symbol.asyncIterator]() { return this; },
1085
+ async next() {
1086
+ const argsJson = JSON.stringify(__marshalForHost([iteratorId]));
1087
+ const resultJson = __customFn_invoke.applySyncPromise(
1088
+ undefined,
1089
+ [${nextReg.callbackId}, argsJson]
1090
+ );
1091
+ const result = JSON.parse(resultJson);
1092
+ if (!result.ok) {
1093
+ const error = new Error(result.error.message);
1094
+ error.name = result.error.name;
1095
+ throw error;
1096
+ }
1097
+ const val = __unmarshalFromHost(result.value);
1098
+ return { done: val.done, value: val.value };
1099
+ },
1100
+ async return(v) {
1101
+ const argsJson = JSON.stringify(__marshalForHost([iteratorId, v]));
1102
+ const resultJson = __customFn_invoke.applySyncPromise(
1103
+ undefined,
1104
+ [${returnReg.callbackId}, argsJson]
1105
+ );
1106
+ const result = JSON.parse(resultJson);
1107
+ return { done: true, value: result.ok ? __unmarshalFromHost(result.value) : undefined };
1108
+ },
1109
+ async throw(e) {
1110
+ const argsJson = JSON.stringify(__marshalForHost([iteratorId, { message: e?.message, name: e?.name }]));
1111
+ const resultJson = __customFn_invoke.applySyncPromise(
1112
+ undefined,
1113
+ [${throwReg.callbackId}, argsJson]
1114
+ );
1115
+ const result = JSON.parse(resultJson);
1116
+ if (!result.ok) {
1117
+ const error = new Error(result.error.message);
1118
+ error.name = result.error.name;
1119
+ throw error;
1120
+ }
1121
+ const val = __unmarshalFromHost(result.value);
1122
+ return { done: val.done, value: val.value };
1123
+ }
1124
+ };
1125
+ };
1126
+ `);
1127
+ } else if (registration.type === "async") {
1128
+ context.evalSync(`
1129
+ globalThis.${name} = async function(...args) {
1130
+ const argsJson = JSON.stringify(__marshalForHost(args));
1131
+ const resultJson = __customFn_invoke.applySyncPromise(
1132
+ undefined,
1133
+ [${registration.callbackId}, argsJson]
1134
+ );
1135
+ const result = JSON.parse(resultJson);
1136
+ if (result.ok) {
1137
+ return __unmarshalFromHost(result.value);
1138
+ } else {
1139
+ const error = new Error(result.error.message);
1140
+ error.name = result.error.name;
1141
+ throw error;
1142
+ }
1143
+ };
1144
+ `);
1145
+ }
1146
+ }
1147
+ }
1148
+ function createModuleResolver(instance, connection) {
1149
+ return async (specifier, _referrer) => {
1150
+ const cached = instance.moduleCache?.get(specifier);
1151
+ if (cached)
1152
+ return cached;
1153
+ if (!instance.moduleLoaderCallbackId) {
1154
+ throw new Error(`Module not found: ${specifier}`);
1155
+ }
1156
+ const code = await invokeClientCallback(connection, instance.moduleLoaderCallbackId, [specifier]);
1157
+ const mod = await instance.runtime.isolate.compileModule(code, {
1158
+ filename: specifier
1159
+ });
1160
+ const resolver = createModuleResolver(instance, connection);
1161
+ await mod.instantiate(instance.runtime.context, resolver);
1162
+ instance.moduleCache?.set(specifier, mod);
1163
+ return mod;
1164
+ };
1165
+ }
345
1166
  async function serializeRequest(request) {
346
1167
  const headers = [];
347
1168
  request.headers.forEach((value, key) => {
@@ -381,19 +1202,144 @@ function deserializeResponse(data) {
381
1202
  headers: data.headers
382
1203
  });
383
1204
  }
384
- async function handleSetupTestEnv(message, connection, state) {
385
- const instance = state.isolates.get(message.isolateId);
386
- if (!instance) {
387
- sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
1205
+ function handleStreamPush(message, connection) {
1206
+ const receiver = connection.streamReceivers.get(message.streamId);
1207
+ if (!receiver) {
1208
+ sendMessage(connection.socket, {
1209
+ type: MessageType.STREAM_ERROR,
1210
+ streamId: message.streamId,
1211
+ error: "Stream not found"
1212
+ });
388
1213
  return;
389
1214
  }
390
- instance.lastActivity = Date.now();
1215
+ receiver.chunks.push(message.chunk);
1216
+ receiver.totalBytes += message.chunk.length;
1217
+ sendMessage(connection.socket, {
1218
+ type: MessageType.STREAM_PULL,
1219
+ streamId: message.streamId,
1220
+ maxBytes: STREAM_DEFAULT_CREDIT
1221
+ });
1222
+ }
1223
+ function handleStreamPull(message, connection) {
1224
+ const session = connection.activeStreams.get(message.streamId);
1225
+ if (!session) {
1226
+ return;
1227
+ }
1228
+ session.credit += message.maxBytes;
1229
+ if (session.creditResolver) {
1230
+ session.creditResolver();
1231
+ session.creditResolver = undefined;
1232
+ }
1233
+ }
1234
+ function handleStreamClose(message, connection) {
1235
+ const receiver = connection.streamReceivers.get(message.streamId);
1236
+ if (!receiver) {
1237
+ return;
1238
+ }
1239
+ const totalLength = receiver.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1240
+ const body = new Uint8Array(totalLength);
1241
+ let offset = 0;
1242
+ for (const chunk of receiver.chunks) {
1243
+ body.set(chunk, offset);
1244
+ offset += chunk.length;
1245
+ }
1246
+ receiver.resolve(body);
1247
+ connection.streamReceivers.delete(message.streamId);
1248
+ }
1249
+ function handleStreamError(message, connection) {
1250
+ const receiver = connection.streamReceivers.get(message.streamId);
1251
+ if (receiver) {
1252
+ receiver.reject(new Error(message.error));
1253
+ connection.streamReceivers.delete(message.streamId);
1254
+ }
1255
+ const session = connection.activeStreams.get(message.streamId);
1256
+ if (session) {
1257
+ session.state = "closed";
1258
+ connection.activeStreams.delete(message.streamId);
1259
+ }
1260
+ }
1261
+ function waitForCredit(session) {
1262
+ return new Promise((resolve) => {
1263
+ session.creditResolver = resolve;
1264
+ });
1265
+ }
1266
+ async function sendStreamedResponse(connection, requestId, response) {
1267
+ const streamId = connection.nextStreamId++;
1268
+ const headers = [];
1269
+ response.headers.forEach((value, key) => {
1270
+ headers.push([key, value]);
1271
+ });
1272
+ const startMsg = {
1273
+ type: MessageType.RESPONSE_STREAM_START,
1274
+ requestId,
1275
+ streamId,
1276
+ metadata: {
1277
+ status: response.status,
1278
+ statusText: response.statusText,
1279
+ headers
1280
+ }
1281
+ };
1282
+ sendMessage(connection.socket, startMsg);
1283
+ if (!response.body) {
1284
+ const endMsg = {
1285
+ type: MessageType.RESPONSE_STREAM_END,
1286
+ requestId,
1287
+ streamId
1288
+ };
1289
+ sendMessage(connection.socket, endMsg);
1290
+ return;
1291
+ }
1292
+ const session = {
1293
+ streamId,
1294
+ direction: "download",
1295
+ requestId,
1296
+ state: "active",
1297
+ bytesTransferred: 0,
1298
+ credit: STREAM_DEFAULT_CREDIT
1299
+ };
1300
+ connection.activeStreams.set(streamId, session);
1301
+ const reader = response.body.getReader();
391
1302
  try {
392
- await setupTestEnvironment(instance.runtime.context);
393
- sendOk(connection.socket, message.requestId);
1303
+ while (true) {
1304
+ while (session.credit < STREAM_CHUNK_SIZE && session.state === "active") {
1305
+ await waitForCredit(session);
1306
+ }
1307
+ if (session.state !== "active") {
1308
+ throw new Error("Stream cancelled");
1309
+ }
1310
+ const { done, value } = await reader.read();
1311
+ if (done) {
1312
+ const endMsg = {
1313
+ type: MessageType.RESPONSE_STREAM_END,
1314
+ requestId,
1315
+ streamId
1316
+ };
1317
+ sendMessage(connection.socket, endMsg);
1318
+ break;
1319
+ }
1320
+ for (let offset = 0;offset < value.length; offset += STREAM_CHUNK_SIZE) {
1321
+ const chunk = value.slice(offset, offset + STREAM_CHUNK_SIZE);
1322
+ const chunkMsg = {
1323
+ type: MessageType.RESPONSE_STREAM_CHUNK,
1324
+ requestId,
1325
+ streamId,
1326
+ chunk
1327
+ };
1328
+ sendMessage(connection.socket, chunkMsg);
1329
+ session.credit -= chunk.length;
1330
+ session.bytesTransferred += chunk.length;
1331
+ }
1332
+ }
394
1333
  } catch (err) {
395
- const error = err;
396
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
1334
+ const errorMsg = {
1335
+ type: MessageType.STREAM_ERROR,
1336
+ streamId,
1337
+ error: err.message
1338
+ };
1339
+ sendMessage(connection.socket, errorMsg);
1340
+ } finally {
1341
+ reader.releaseLock();
1342
+ connection.activeStreams.delete(streamId);
397
1343
  }
398
1344
  }
399
1345
  async function handleRunTests(message, connection, state) {
@@ -402,6 +1348,10 @@ async function handleRunTests(message, connection, state) {
402
1348
  sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
403
1349
  return;
404
1350
  }
1351
+ if (!instance.testEnvironmentEnabled) {
1352
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Test environment not enabled. Set testEnvironment: true in createRuntime options.");
1353
+ return;
1354
+ }
405
1355
  instance.lastActivity = Date.now();
406
1356
  try {
407
1357
  const timeout = message.timeout ?? 30000;
@@ -418,119 +1368,69 @@ async function handleRunTests(message, connection, state) {
418
1368
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
419
1369
  }
420
1370
  }
421
- async function handleSetupPlaywright(message, connection, state) {
1371
+ async function handleResetTestEnv(message, connection, state) {
422
1372
  const instance = state.isolates.get(message.isolateId);
423
1373
  if (!instance) {
424
1374
  sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
425
1375
  return;
426
1376
  }
427
- if (instance.browser) {
428
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright already set up for this isolate");
1377
+ if (!instance.testEnvironmentEnabled) {
1378
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Test environment not enabled. Set testEnvironment: true in createRuntime options.");
429
1379
  return;
430
1380
  }
431
1381
  instance.lastActivity = Date.now();
432
1382
  try {
433
- const browserType = message.options.browserType ?? "chromium";
434
- const headless = message.options.headless ?? true;
435
- let browser;
436
- switch (browserType) {
437
- case "firefox":
438
- browser = await firefox.launch({ headless });
439
- break;
440
- case "webkit":
441
- browser = await webkit.launch({ headless });
442
- break;
443
- default:
444
- browser = await chromium.launch({ headless });
445
- }
446
- const browserContext = await browser.newContext();
447
- const page = await browserContext.newPage();
448
- const playwrightHandle = await setupPlaywright(instance.runtime.context, {
449
- page,
450
- baseUrl: message.options.baseURL,
451
- onConsoleLog: (level, ...args) => {
452
- const event = {
453
- type: MessageType.PLAYWRIGHT_EVENT,
454
- isolateId: message.isolateId,
455
- eventType: "consoleLog",
456
- payload: { level, args }
457
- };
458
- sendMessage(connection.socket, event);
459
- },
460
- onNetworkRequest: (info) => {
461
- const event = {
462
- type: MessageType.PLAYWRIGHT_EVENT,
463
- isolateId: message.isolateId,
464
- eventType: "networkRequest",
465
- payload: info
466
- };
467
- sendMessage(connection.socket, event);
468
- },
469
- onNetworkResponse: (info) => {
470
- const event = {
471
- type: MessageType.PLAYWRIGHT_EVENT,
472
- isolateId: message.isolateId,
473
- eventType: "networkResponse",
474
- payload: info
475
- };
476
- sendMessage(connection.socket, event);
477
- }
478
- });
479
- instance.browser = browser;
480
- instance.browserContext = browserContext;
481
- instance.page = page;
482
- instance.playwrightHandle = playwrightHandle;
1383
+ await instance.runtime.context.eval("__resetTestEnvironment()", { promise: true });
483
1384
  sendOk(connection.socket, message.requestId);
484
1385
  } catch (err) {
485
1386
  const error = err;
486
1387
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
487
1388
  }
488
1389
  }
489
- async function handleRunPlaywrightTests(message, connection, state) {
1390
+ async function handleHasTests(message, connection, state) {
490
1391
  const instance = state.isolates.get(message.isolateId);
491
1392
  if (!instance) {
492
1393
  sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
493
1394
  return;
494
1395
  }
495
- if (!instance.playwrightHandle) {
496
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright not set up for this isolate");
1396
+ if (!instance.testEnvironmentEnabled) {
1397
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Test environment not enabled. Set testEnvironment: true in createRuntime options.");
497
1398
  return;
498
1399
  }
499
1400
  instance.lastActivity = Date.now();
500
1401
  try {
501
- const timeout = message.timeout ?? 30000;
502
- const timeoutPromise = new Promise((_, reject) => {
503
- setTimeout(() => reject(new Error("Playwright test timeout")), timeout);
504
- });
505
- const results = await Promise.race([
506
- runPlaywrightTests(instance.runtime.context),
507
- timeoutPromise
508
- ]);
509
- sendOk(connection.socket, message.requestId, results);
1402
+ const result = hasTestsInContext(instance.runtime.context);
1403
+ sendOk(connection.socket, message.requestId, result);
510
1404
  } catch (err) {
511
1405
  const error = err;
512
1406
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
513
1407
  }
514
1408
  }
515
- async function handleResetPlaywrightTests(message, connection, state) {
1409
+ async function handleGetTestCount(message, connection, state) {
516
1410
  const instance = state.isolates.get(message.isolateId);
517
1411
  if (!instance) {
518
1412
  sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
519
1413
  return;
520
1414
  }
521
- if (!instance.playwrightHandle) {
522
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright not set up for this isolate");
1415
+ if (!instance.testEnvironmentEnabled) {
1416
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Test environment not enabled. Set testEnvironment: true in createRuntime options.");
523
1417
  return;
524
1418
  }
525
1419
  instance.lastActivity = Date.now();
526
1420
  try {
527
- await resetPlaywrightTests(instance.runtime.context);
528
- sendOk(connection.socket, message.requestId);
1421
+ const result = getTestCountInContext(instance.runtime.context);
1422
+ sendOk(connection.socket, message.requestId, result);
529
1423
  } catch (err) {
530
1424
  const error = err;
531
1425
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
532
1426
  }
533
1427
  }
1428
+ async function handleRunPlaywrightTests(message, connection, _state) {
1429
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "playwright.runTests() has been removed. Use testEnvironment.runTests() instead.");
1430
+ }
1431
+ async function handleResetPlaywrightTests(message, connection, _state) {
1432
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "playwright.reset() has been removed. Use testEnvironment.reset() instead.");
1433
+ }
534
1434
  async function handleGetCollectedData(message, connection, state) {
535
1435
  const instance = state.isolates.get(message.isolateId);
536
1436
  if (!instance) {
@@ -538,13 +1438,13 @@ async function handleGetCollectedData(message, connection, state) {
538
1438
  return;
539
1439
  }
540
1440
  if (!instance.playwrightHandle) {
541
- sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright not set up for this isolate");
1441
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright not configured. Provide playwright.page in createRuntime options.");
542
1442
  return;
543
1443
  }
544
1444
  instance.lastActivity = Date.now();
545
1445
  try {
546
1446
  const data = {
547
- consoleLogs: instance.playwrightHandle.getConsoleLogs(),
1447
+ browserConsoleLogs: instance.playwrightHandle.getBrowserConsoleLogs(),
548
1448
  networkRequests: instance.playwrightHandle.getNetworkRequests(),
549
1449
  networkResponses: instance.playwrightHandle.getNetworkResponses()
550
1450
  };
@@ -554,8 +1454,27 @@ async function handleGetCollectedData(message, connection, state) {
554
1454
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
555
1455
  }
556
1456
  }
1457
+ async function handleClearCollectedData(message, connection, state) {
1458
+ const instance = state.isolates.get(message.isolateId);
1459
+ if (!instance) {
1460
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_NOT_FOUND, `Isolate not found: ${message.isolateId}`);
1461
+ return;
1462
+ }
1463
+ if (!instance.playwrightHandle) {
1464
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, "Playwright not configured. Provide playwright.page in createRuntime options.");
1465
+ return;
1466
+ }
1467
+ instance.lastActivity = Date.now();
1468
+ try {
1469
+ instance.playwrightHandle.clearCollected();
1470
+ sendOk(connection.socket, message.requestId);
1471
+ } catch (err) {
1472
+ const error = err;
1473
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
1474
+ }
1475
+ }
557
1476
  export {
558
1477
  handleConnection
559
1478
  };
560
1479
 
561
- //# debugId=4F884F5F7FCD4D4F64756E2164756E21
1480
+ //# debugId=8FEF67C0571D870E64756E2164756E21