@ricsam/isolate-daemon 0.1.4 → 0.1.6

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.
@@ -83,18 +83,26 @@ function handleConnection(socket, state) {
83
83
  for (const isolateId of connection.isolates) {
84
84
  const instance = state.isolates.get(isolateId);
85
85
  if (instance) {
86
- try {
87
- if (instance.playwrightHandle) {
88
- instance.playwrightHandle.dispose();
89
- }
90
- instance.runtime.dispose();
91
- } catch {}
92
- state.isolates.delete(isolateId);
86
+ if (instance.namespaceId != null && !instance.isDisposed) {
87
+ softDeleteRuntime(instance, state);
88
+ } else if (!instance.isDisposed) {
89
+ try {
90
+ if (instance.playwrightHandle) {
91
+ instance.playwrightHandle.dispose();
92
+ }
93
+ instance.runtime.dispose();
94
+ } catch {}
95
+ state.isolates.delete(isolateId);
96
+ }
93
97
  }
94
98
  }
95
99
  for (const [, pending] of connection.pendingCallbacks) {
100
+ if (pending.timeoutId) {
101
+ clearTimeout(pending.timeoutId);
102
+ }
96
103
  pending.reject(new Error("Connection closed"));
97
104
  }
105
+ connection.pendingCallbacks.clear();
98
106
  state.connections.delete(socket);
99
107
  });
100
108
  socket.on("error", (err) => {
@@ -220,10 +228,147 @@ async function handleMessage(message, connection, state) {
220
228
  sendError(connection.socket, message.requestId ?? 0, import_isolate_protocol.ErrorCode.UNKNOWN_MESSAGE_TYPE, `Unknown message type: ${message.type}`);
221
229
  }
222
230
  }
231
+ function softDeleteRuntime(instance, state) {
232
+ instance.isDisposed = true;
233
+ instance.disposedAt = Date.now();
234
+ instance.ownerConnection = null;
235
+ instance.callbacks.clear();
236
+ instance.runtime.timers.clearAll();
237
+ instance.runtime.console.reset();
238
+ instance.pendingCallbacks.length = 0;
239
+ instance.returnedCallbacks?.clear();
240
+ instance.returnedPromises?.clear();
241
+ instance.returnedIterators?.clear();
242
+ }
243
+ function reuseNamespacedRuntime(instance, connection, message, state) {
244
+ instance.ownerConnection = connection.socket;
245
+ instance.isDisposed = false;
246
+ instance.disposedAt = undefined;
247
+ instance.lastActivity = Date.now();
248
+ connection.isolates.add(instance.isolateId);
249
+ const callbacks = message.options.callbacks;
250
+ if (instance.callbackContext) {
251
+ instance.callbackContext.connection = connection;
252
+ instance.callbackContext.consoleOnEntry = callbacks?.console?.onEntry?.callbackId;
253
+ instance.callbackContext.fetch = callbacks?.fetch?.callbackId;
254
+ instance.callbackContext.moduleLoader = callbacks?.moduleLoader?.callbackId;
255
+ instance.callbackContext.fs = {
256
+ readFile: callbacks?.fs?.readFile?.callbackId,
257
+ writeFile: callbacks?.fs?.writeFile?.callbackId,
258
+ stat: callbacks?.fs?.stat?.callbackId,
259
+ readdir: callbacks?.fs?.readdir?.callbackId,
260
+ unlink: callbacks?.fs?.unlink?.callbackId,
261
+ mkdir: callbacks?.fs?.mkdir?.callbackId,
262
+ rmdir: callbacks?.fs?.rmdir?.callbackId
263
+ };
264
+ instance.callbackContext.custom.clear();
265
+ if (callbacks?.custom) {
266
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
267
+ if (reg) {
268
+ instance.callbackContext.custom.set(name, reg.callbackId);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ instance.callbacks.clear();
274
+ if (callbacks?.console?.onEntry) {
275
+ instance.callbacks.set(callbacks.console.onEntry.callbackId, {
276
+ ...callbacks.console.onEntry,
277
+ name: "onEntry"
278
+ });
279
+ }
280
+ if (callbacks?.fetch) {
281
+ instance.callbacks.set(callbacks.fetch.callbackId, callbacks.fetch);
282
+ }
283
+ if (callbacks?.fs) {
284
+ for (const [name, reg] of Object.entries(callbacks.fs)) {
285
+ if (reg) {
286
+ instance.callbacks.set(reg.callbackId, { ...reg, name });
287
+ }
288
+ }
289
+ }
290
+ if (callbacks?.moduleLoader) {
291
+ instance.moduleLoaderCallbackId = callbacks.moduleLoader.callbackId;
292
+ instance.callbacks.set(callbacks.moduleLoader.callbackId, callbacks.moduleLoader);
293
+ }
294
+ if (callbacks?.custom) {
295
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
296
+ if (reg) {
297
+ instance.callbacks.set(reg.callbackId, { ...reg, name });
298
+ }
299
+ }
300
+ }
301
+ instance.returnedCallbacks = new Map;
302
+ instance.returnedPromises = new Map;
303
+ instance.returnedIterators = new Map;
304
+ instance.nextLocalCallbackId = 1e6;
305
+ if (callbacks?.custom) {
306
+ const newCallbackIdMap = {};
307
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
308
+ if (reg) {
309
+ newCallbackIdMap[name] = reg.callbackId;
310
+ }
311
+ }
312
+ try {
313
+ instance.runtime.context.global.setSync("__customFnCallbackIds", new import_isolated_vm.default.ExternalCopy(newCallbackIdMap).copyInto());
314
+ } catch {}
315
+ }
316
+ }
317
+ function evictOldestDisposedRuntime(state) {
318
+ let oldest = null;
319
+ let oldestTime = Infinity;
320
+ for (const [, instance] of state.isolates) {
321
+ if (instance.isDisposed && instance.disposedAt !== undefined) {
322
+ if (instance.disposedAt < oldestTime) {
323
+ oldestTime = instance.disposedAt;
324
+ oldest = instance;
325
+ }
326
+ }
327
+ }
328
+ if (oldest) {
329
+ try {
330
+ if (oldest.playwrightHandle) {
331
+ oldest.playwrightHandle.dispose();
332
+ }
333
+ oldest.runtime.dispose();
334
+ } catch {}
335
+ state.isolates.delete(oldest.isolateId);
336
+ if (oldest.namespaceId != null) {
337
+ state.namespacedRuntimes.delete(oldest.namespaceId);
338
+ }
339
+ return true;
340
+ }
341
+ return false;
342
+ }
223
343
  async function handleCreateRuntime(message, connection, state) {
344
+ const namespaceId = message.options.namespaceId;
345
+ if (namespaceId != null) {
346
+ const existing = state.namespacedRuntimes.get(namespaceId);
347
+ if (existing) {
348
+ if (!existing.isDisposed) {
349
+ if (existing.ownerConnection === connection.socket) {
350
+ sendOk(connection.socket, message.requestId, {
351
+ isolateId: existing.isolateId,
352
+ reused: true
353
+ });
354
+ return;
355
+ }
356
+ sendError(connection.socket, message.requestId, import_isolate_protocol.ErrorCode.SCRIPT_ERROR, `Namespace "${namespaceId}" already has an active runtime`);
357
+ return;
358
+ }
359
+ reuseNamespacedRuntime(existing, connection, message, state);
360
+ sendOk(connection.socket, message.requestId, {
361
+ isolateId: existing.isolateId,
362
+ reused: true
363
+ });
364
+ return;
365
+ }
366
+ }
224
367
  if (state.isolates.size >= state.options.maxIsolates) {
225
- sendError(connection.socket, message.requestId, import_isolate_protocol.ErrorCode.ISOLATE_MEMORY_LIMIT, `Maximum isolates (${state.options.maxIsolates}) reached`);
226
- return;
368
+ if (!evictOldestDisposedRuntime(state)) {
369
+ sendError(connection.socket, message.requestId, import_isolate_protocol.ErrorCode.ISOLATE_MEMORY_LIMIT, `Maximum isolates (${state.options.maxIsolates}) reached`);
370
+ return;
371
+ }
227
372
  }
228
373
  try {
229
374
  const isolateId = import_node_crypto.randomUUID();
@@ -233,32 +378,61 @@ async function handleCreateRuntime(message, connection, state) {
233
378
  const moduleLoaderCallback = message.options.callbacks?.moduleLoader;
234
379
  const customCallbacks = message.options.callbacks?.custom;
235
380
  const pendingCallbacks = [];
381
+ const callbackContext = {
382
+ connection,
383
+ consoleOnEntry: consoleCallbacks?.onEntry?.callbackId,
384
+ fetch: fetchCallback?.callbackId,
385
+ moduleLoader: moduleLoaderCallback?.callbackId,
386
+ fs: {
387
+ readFile: fsCallbacks?.readFile?.callbackId,
388
+ writeFile: fsCallbacks?.writeFile?.callbackId,
389
+ stat: fsCallbacks?.stat?.callbackId,
390
+ readdir: fsCallbacks?.readdir?.callbackId,
391
+ unlink: fsCallbacks?.unlink?.callbackId,
392
+ mkdir: fsCallbacks?.mkdir?.callbackId,
393
+ rmdir: fsCallbacks?.rmdir?.callbackId
394
+ },
395
+ custom: new Map(customCallbacks ? Object.entries(customCallbacks).map(([name, reg]) => [name, reg.callbackId]) : [])
396
+ };
236
397
  const runtime = await import_isolate_runtime.createInternalRuntime({
237
398
  memoryLimitMB: message.options.memoryLimitMB ?? state.options.defaultMemoryLimitMB,
238
399
  cwd: message.options.cwd,
239
- console: consoleCallbacks?.onEntry ? {
400
+ console: {
240
401
  onEntry: (entry) => {
241
- const promise = invokeClientCallback(connection, consoleCallbacks.onEntry.callbackId, [entry]).catch(() => {});
402
+ const conn = callbackContext.connection;
403
+ const callbackId = callbackContext.consoleOnEntry;
404
+ if (!conn || callbackId === undefined)
405
+ return;
406
+ const promise = invokeClientCallback(conn, callbackId, [entry]).catch(() => {});
242
407
  pendingCallbacks.push(promise);
243
408
  }
244
- } : undefined,
245
- fetch: fetchCallback ? {
409
+ },
410
+ fetch: {
246
411
  onFetch: async (request) => {
412
+ const conn = callbackContext.connection;
413
+ const callbackId = callbackContext.fetch;
414
+ if (!conn || callbackId === undefined) {
415
+ throw new Error("Fetch callback not available");
416
+ }
247
417
  const serialized = await serializeRequest(request);
248
- const result = await invokeClientCallback(connection, fetchCallback.callbackId, [serialized]);
418
+ const result = await invokeClientCallback(conn, callbackId, [serialized]);
249
419
  return deserializeResponse(result);
250
420
  }
251
- } : undefined,
252
- fs: fsCallbacks ? {
421
+ },
422
+ fs: {
253
423
  getDirectory: async (path) => {
424
+ const conn = callbackContext.connection;
425
+ if (!conn) {
426
+ throw new Error("FS callbacks not available");
427
+ }
254
428
  return import_callback_fs_handler.createCallbackFileSystemHandler({
255
- connection,
256
- callbacks: fsCallbacks,
429
+ connection: conn,
430
+ callbackContext,
257
431
  invokeClientCallback,
258
432
  basePath: path
259
433
  });
260
434
  }
261
- } : undefined
435
+ }
262
436
  });
263
437
  const instance = {
264
438
  isolateId,
@@ -271,7 +445,10 @@ async function handleCreateRuntime(message, connection, state) {
271
445
  returnedCallbacks: new Map,
272
446
  returnedPromises: new Map,
273
447
  returnedIterators: new Map,
274
- nextLocalCallbackId: 1e6
448
+ nextLocalCallbackId: 1e6,
449
+ namespaceId,
450
+ isDisposed: false,
451
+ callbackContext
275
452
  };
276
453
  if (moduleLoaderCallback) {
277
454
  instance.moduleLoaderCallbackId = moduleLoaderCallback.callbackId;
@@ -356,6 +533,9 @@ async function handleCreateRuntime(message, connection, state) {
356
533
  state.isolates.set(isolateId, instance);
357
534
  connection.isolates.add(isolateId);
358
535
  state.stats.totalIsolatesCreated++;
536
+ if (namespaceId != null) {
537
+ state.namespacedRuntimes.set(namespaceId, instance);
538
+ }
359
539
  instance.runtime.fetch.onWebSocketCommand((cmd) => {
360
540
  let data;
361
541
  if (cmd.data instanceof ArrayBuffer) {
@@ -376,7 +556,7 @@ async function handleCreateRuntime(message, connection, state) {
376
556
  };
377
557
  sendMessage(connection.socket, wsCommandMsg);
378
558
  });
379
- sendOk(connection.socket, message.requestId, { isolateId });
559
+ sendOk(connection.socket, message.requestId, { isolateId, reused: false });
380
560
  } catch (err) {
381
561
  const error = err;
382
562
  sendError(connection.socket, message.requestId, import_isolate_protocol.ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
@@ -393,12 +573,16 @@ async function handleDisposeRuntime(message, connection, state) {
393
573
  return;
394
574
  }
395
575
  try {
396
- if (instance.playwrightHandle) {
397
- instance.playwrightHandle.dispose();
398
- }
399
- instance.runtime.dispose();
400
- state.isolates.delete(message.isolateId);
401
576
  connection.isolates.delete(message.isolateId);
577
+ if (instance.namespaceId != null) {
578
+ softDeleteRuntime(instance, state);
579
+ } else {
580
+ if (instance.playwrightHandle) {
581
+ instance.playwrightHandle.dispose();
582
+ }
583
+ instance.runtime.dispose();
584
+ state.isolates.delete(message.isolateId);
585
+ }
402
586
  sendOk(connection.socket, message.requestId);
403
587
  } catch (err) {
404
588
  const error = err;
@@ -455,22 +639,21 @@ async function handleDispatchRequest(message, connection, state) {
455
639
  body: requestBody
456
640
  });
457
641
  const response = await instance.runtime.fetch.dispatchRequest(request);
458
- const contentLength = response.headers.get("content-length");
459
- const knownSize = contentLength ? parseInt(contentLength, 10) : null;
460
- if (knownSize !== null && knownSize > import_isolate_protocol.STREAM_THRESHOLD) {
642
+ if (response.body) {
461
643
  await sendStreamedResponse(connection, message.requestId, response);
462
644
  } else {
463
- const clonedResponse = response.clone();
464
- try {
465
- const serialized = await serializeResponse(response);
466
- if (serialized.body && serialized.body.length > import_isolate_protocol.STREAM_THRESHOLD) {
467
- await sendStreamedResponse(connection, message.requestId, clonedResponse);
468
- } else {
469
- sendOk(connection.socket, message.requestId, { response: serialized });
645
+ const headers = [];
646
+ response.headers.forEach((value, key) => {
647
+ headers.push([key, value]);
648
+ });
649
+ sendOk(connection.socket, message.requestId, {
650
+ response: {
651
+ status: response.status,
652
+ statusText: response.statusText,
653
+ headers,
654
+ body: null
470
655
  }
471
- } catch {
472
- await sendStreamedResponse(connection, message.requestId, clonedResponse);
473
- }
656
+ });
474
657
  }
475
658
  } catch (err) {
476
659
  const error = err;
@@ -1045,7 +1228,8 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1045
1228
  }
1046
1229
  result = await callback(...args);
1047
1230
  } else {
1048
- result = await invokeClientCallback(connection, callbackId, args);
1231
+ const conn = instance.callbackContext?.connection || connection;
1232
+ result = await invokeClientCallback(conn, callbackId, args);
1049
1233
  }
1050
1234
  const ctx = createMarshalContext();
1051
1235
  const marshalledResult = await import_isolate_protocol.marshalValue({ ok: true, value: result }, ctx);
@@ -1061,6 +1245,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1061
1245
  });
1062
1246
  global.setSync("__customFn_invoke", invokeCallbackRef);
1063
1247
  context.evalSync(ISOLATE_MARSHAL_CODE);
1248
+ const callbackIdMap = {};
1249
+ for (const [name, registration] of Object.entries(customCallbacks)) {
1250
+ callbackIdMap[name] = registration.callbackId;
1251
+ }
1252
+ global.setSync("__customFnCallbackIds", new import_isolated_vm.default.ExternalCopy(callbackIdMap).copyInto());
1064
1253
  for (const [name, registration] of Object.entries(customCallbacks)) {
1065
1254
  if (name.includes(":")) {
1066
1255
  continue;
@@ -1068,10 +1257,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1068
1257
  if (registration.type === "sync") {
1069
1258
  context.evalSync(`
1070
1259
  globalThis.${name} = function(...args) {
1260
+ const callbackId = globalThis.__customFnCallbackIds["${name}"];
1071
1261
  const argsJson = JSON.stringify(__marshalForHost(args));
1072
1262
  const resultJson = __customFn_invoke.applySyncPromise(
1073
1263
  undefined,
1074
- [${registration.callbackId}, argsJson]
1264
+ [callbackId, argsJson]
1075
1265
  );
1076
1266
  const result = JSON.parse(resultJson);
1077
1267
  if (result.ok) {
@@ -1094,10 +1284,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1094
1284
  context.evalSync(`
1095
1285
  globalThis.${name} = function(...args) {
1096
1286
  // Start the iterator and get the iteratorId
1287
+ const startCallbackId = globalThis.__customFnCallbackIds["${name}:start"];
1097
1288
  const argsJson = JSON.stringify(__marshalForHost(args));
1098
1289
  const startResultJson = __customFn_invoke.applySyncPromise(
1099
1290
  undefined,
1100
- [${startReg.callbackId}, argsJson]
1291
+ [startCallbackId, argsJson]
1101
1292
  );
1102
1293
  const startResult = JSON.parse(startResultJson);
1103
1294
  if (!startResult.ok) {
@@ -1110,10 +1301,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1110
1301
  return {
1111
1302
  [Symbol.asyncIterator]() { return this; },
1112
1303
  async next() {
1304
+ const nextCallbackId = globalThis.__customFnCallbackIds["${name}:next"];
1113
1305
  const argsJson = JSON.stringify(__marshalForHost([iteratorId]));
1114
1306
  const resultJson = __customFn_invoke.applySyncPromise(
1115
1307
  undefined,
1116
- [${nextReg.callbackId}, argsJson]
1308
+ [nextCallbackId, argsJson]
1117
1309
  );
1118
1310
  const result = JSON.parse(resultJson);
1119
1311
  if (!result.ok) {
@@ -1125,19 +1317,21 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1125
1317
  return { done: val.done, value: val.value };
1126
1318
  },
1127
1319
  async return(v) {
1320
+ const returnCallbackId = globalThis.__customFnCallbackIds["${name}:return"];
1128
1321
  const argsJson = JSON.stringify(__marshalForHost([iteratorId, v]));
1129
1322
  const resultJson = __customFn_invoke.applySyncPromise(
1130
1323
  undefined,
1131
- [${returnReg.callbackId}, argsJson]
1324
+ [returnCallbackId, argsJson]
1132
1325
  );
1133
1326
  const result = JSON.parse(resultJson);
1134
1327
  return { done: true, value: result.ok ? __unmarshalFromHost(result.value) : undefined };
1135
1328
  },
1136
1329
  async throw(e) {
1330
+ const throwCallbackId = globalThis.__customFnCallbackIds["${name}:throw"];
1137
1331
  const argsJson = JSON.stringify(__marshalForHost([iteratorId, { message: e?.message, name: e?.name }]));
1138
1332
  const resultJson = __customFn_invoke.applySyncPromise(
1139
1333
  undefined,
1140
- [${throwReg.callbackId}, argsJson]
1334
+ [throwCallbackId, argsJson]
1141
1335
  );
1142
1336
  const result = JSON.parse(resultJson);
1143
1337
  if (!result.ok) {
@@ -1154,10 +1348,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1154
1348
  } else if (registration.type === "async") {
1155
1349
  context.evalSync(`
1156
1350
  globalThis.${name} = async function(...args) {
1351
+ const callbackId = globalThis.__customFnCallbackIds["${name}"];
1157
1352
  const argsJson = JSON.stringify(__marshalForHost(args));
1158
1353
  const resultJson = __customFn_invoke.applySyncPromise(
1159
1354
  undefined,
1160
- [${registration.callbackId}, argsJson]
1355
+ [callbackId, argsJson]
1161
1356
  );
1162
1357
  const result = JSON.parse(resultJson);
1163
1358
  if (result.ok) {
@@ -1206,22 +1401,6 @@ async function serializeRequest(request) {
1206
1401
  body
1207
1402
  };
1208
1403
  }
1209
- async function serializeResponse(response) {
1210
- const headers = [];
1211
- response.headers.forEach((value, key) => {
1212
- headers.push([key, value]);
1213
- });
1214
- let body = null;
1215
- if (response.body) {
1216
- body = new Uint8Array(await response.arrayBuffer());
1217
- }
1218
- return {
1219
- status: response.status,
1220
- statusText: response.statusText,
1221
- headers,
1222
- body
1223
- };
1224
- }
1225
1404
  function deserializeResponse(data) {
1226
1405
  return new Response(data.body, {
1227
1406
  status: data.status,
@@ -1382,14 +1561,21 @@ async function handleRunTests(message, connection, state) {
1382
1561
  instance.lastActivity = Date.now();
1383
1562
  try {
1384
1563
  const timeout = message.timeout ?? 30000;
1564
+ let timeoutId;
1385
1565
  const timeoutPromise = new Promise((_, reject) => {
1386
- setTimeout(() => reject(new Error("Test timeout")), timeout);
1566
+ timeoutId = setTimeout(() => reject(new Error("Test timeout")), timeout);
1387
1567
  });
1388
- const results = await Promise.race([
1389
- import_isolate_test_environment.runTests(instance.runtime.context),
1390
- timeoutPromise
1391
- ]);
1392
- sendOk(connection.socket, message.requestId, results);
1568
+ try {
1569
+ const results = await Promise.race([
1570
+ import_isolate_test_environment.runTests(instance.runtime.context),
1571
+ timeoutPromise
1572
+ ]);
1573
+ sendOk(connection.socket, message.requestId, results);
1574
+ } finally {
1575
+ if (timeoutId) {
1576
+ clearTimeout(timeoutId);
1577
+ }
1578
+ }
1393
1579
  } catch (err) {
1394
1580
  const error = err;
1395
1581
  sendError(connection.socket, message.requestId, import_isolate_protocol.ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
@@ -1502,4 +1688,4 @@ async function handleClearCollectedData(message, connection, state) {
1502
1688
  }
1503
1689
  })
1504
1690
 
1505
- //# debugId=A7BAC1A3990C35CA64756E2164756E21
1691
+ //# debugId=4B3FAD2EB54DBE6A64756E2164756E21