@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.
@@ -7,7 +7,6 @@ import {
7
7
  buildFrame,
8
8
  MessageType,
9
9
  ErrorCode,
10
- STREAM_THRESHOLD,
11
10
  STREAM_CHUNK_SIZE,
12
11
  STREAM_DEFAULT_CREDIT,
13
12
  marshalValue,
@@ -56,18 +55,26 @@ function handleConnection(socket, state) {
56
55
  for (const isolateId of connection.isolates) {
57
56
  const instance = state.isolates.get(isolateId);
58
57
  if (instance) {
59
- try {
60
- if (instance.playwrightHandle) {
61
- instance.playwrightHandle.dispose();
62
- }
63
- instance.runtime.dispose();
64
- } catch {}
65
- state.isolates.delete(isolateId);
58
+ if (instance.namespaceId != null && !instance.isDisposed) {
59
+ softDeleteRuntime(instance, state);
60
+ } else if (!instance.isDisposed) {
61
+ try {
62
+ if (instance.playwrightHandle) {
63
+ instance.playwrightHandle.dispose();
64
+ }
65
+ instance.runtime.dispose();
66
+ } catch {}
67
+ state.isolates.delete(isolateId);
68
+ }
66
69
  }
67
70
  }
68
71
  for (const [, pending] of connection.pendingCallbacks) {
72
+ if (pending.timeoutId) {
73
+ clearTimeout(pending.timeoutId);
74
+ }
69
75
  pending.reject(new Error("Connection closed"));
70
76
  }
77
+ connection.pendingCallbacks.clear();
71
78
  state.connections.delete(socket);
72
79
  });
73
80
  socket.on("error", (err) => {
@@ -193,10 +200,147 @@ async function handleMessage(message, connection, state) {
193
200
  sendError(connection.socket, message.requestId ?? 0, ErrorCode.UNKNOWN_MESSAGE_TYPE, `Unknown message type: ${message.type}`);
194
201
  }
195
202
  }
203
+ function softDeleteRuntime(instance, state) {
204
+ instance.isDisposed = true;
205
+ instance.disposedAt = Date.now();
206
+ instance.ownerConnection = null;
207
+ instance.callbacks.clear();
208
+ instance.runtime.timers.clearAll();
209
+ instance.runtime.console.reset();
210
+ instance.pendingCallbacks.length = 0;
211
+ instance.returnedCallbacks?.clear();
212
+ instance.returnedPromises?.clear();
213
+ instance.returnedIterators?.clear();
214
+ }
215
+ function reuseNamespacedRuntime(instance, connection, message, state) {
216
+ instance.ownerConnection = connection.socket;
217
+ instance.isDisposed = false;
218
+ instance.disposedAt = undefined;
219
+ instance.lastActivity = Date.now();
220
+ connection.isolates.add(instance.isolateId);
221
+ const callbacks = message.options.callbacks;
222
+ if (instance.callbackContext) {
223
+ instance.callbackContext.connection = connection;
224
+ instance.callbackContext.consoleOnEntry = callbacks?.console?.onEntry?.callbackId;
225
+ instance.callbackContext.fetch = callbacks?.fetch?.callbackId;
226
+ instance.callbackContext.moduleLoader = callbacks?.moduleLoader?.callbackId;
227
+ instance.callbackContext.fs = {
228
+ readFile: callbacks?.fs?.readFile?.callbackId,
229
+ writeFile: callbacks?.fs?.writeFile?.callbackId,
230
+ stat: callbacks?.fs?.stat?.callbackId,
231
+ readdir: callbacks?.fs?.readdir?.callbackId,
232
+ unlink: callbacks?.fs?.unlink?.callbackId,
233
+ mkdir: callbacks?.fs?.mkdir?.callbackId,
234
+ rmdir: callbacks?.fs?.rmdir?.callbackId
235
+ };
236
+ instance.callbackContext.custom.clear();
237
+ if (callbacks?.custom) {
238
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
239
+ if (reg) {
240
+ instance.callbackContext.custom.set(name, reg.callbackId);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ instance.callbacks.clear();
246
+ if (callbacks?.console?.onEntry) {
247
+ instance.callbacks.set(callbacks.console.onEntry.callbackId, {
248
+ ...callbacks.console.onEntry,
249
+ name: "onEntry"
250
+ });
251
+ }
252
+ if (callbacks?.fetch) {
253
+ instance.callbacks.set(callbacks.fetch.callbackId, callbacks.fetch);
254
+ }
255
+ if (callbacks?.fs) {
256
+ for (const [name, reg] of Object.entries(callbacks.fs)) {
257
+ if (reg) {
258
+ instance.callbacks.set(reg.callbackId, { ...reg, name });
259
+ }
260
+ }
261
+ }
262
+ if (callbacks?.moduleLoader) {
263
+ instance.moduleLoaderCallbackId = callbacks.moduleLoader.callbackId;
264
+ instance.callbacks.set(callbacks.moduleLoader.callbackId, callbacks.moduleLoader);
265
+ }
266
+ if (callbacks?.custom) {
267
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
268
+ if (reg) {
269
+ instance.callbacks.set(reg.callbackId, { ...reg, name });
270
+ }
271
+ }
272
+ }
273
+ instance.returnedCallbacks = new Map;
274
+ instance.returnedPromises = new Map;
275
+ instance.returnedIterators = new Map;
276
+ instance.nextLocalCallbackId = 1e6;
277
+ if (callbacks?.custom) {
278
+ const newCallbackIdMap = {};
279
+ for (const [name, reg] of Object.entries(callbacks.custom)) {
280
+ if (reg) {
281
+ newCallbackIdMap[name] = reg.callbackId;
282
+ }
283
+ }
284
+ try {
285
+ instance.runtime.context.global.setSync("__customFnCallbackIds", new ivm.ExternalCopy(newCallbackIdMap).copyInto());
286
+ } catch {}
287
+ }
288
+ }
289
+ function evictOldestDisposedRuntime(state) {
290
+ let oldest = null;
291
+ let oldestTime = Infinity;
292
+ for (const [, instance] of state.isolates) {
293
+ if (instance.isDisposed && instance.disposedAt !== undefined) {
294
+ if (instance.disposedAt < oldestTime) {
295
+ oldestTime = instance.disposedAt;
296
+ oldest = instance;
297
+ }
298
+ }
299
+ }
300
+ if (oldest) {
301
+ try {
302
+ if (oldest.playwrightHandle) {
303
+ oldest.playwrightHandle.dispose();
304
+ }
305
+ oldest.runtime.dispose();
306
+ } catch {}
307
+ state.isolates.delete(oldest.isolateId);
308
+ if (oldest.namespaceId != null) {
309
+ state.namespacedRuntimes.delete(oldest.namespaceId);
310
+ }
311
+ return true;
312
+ }
313
+ return false;
314
+ }
196
315
  async function handleCreateRuntime(message, connection, state) {
316
+ const namespaceId = message.options.namespaceId;
317
+ if (namespaceId != null) {
318
+ const existing = state.namespacedRuntimes.get(namespaceId);
319
+ if (existing) {
320
+ if (!existing.isDisposed) {
321
+ if (existing.ownerConnection === connection.socket) {
322
+ sendOk(connection.socket, message.requestId, {
323
+ isolateId: existing.isolateId,
324
+ reused: true
325
+ });
326
+ return;
327
+ }
328
+ sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, `Namespace "${namespaceId}" already has an active runtime`);
329
+ return;
330
+ }
331
+ reuseNamespacedRuntime(existing, connection, message, state);
332
+ sendOk(connection.socket, message.requestId, {
333
+ isolateId: existing.isolateId,
334
+ reused: true
335
+ });
336
+ return;
337
+ }
338
+ }
197
339
  if (state.isolates.size >= state.options.maxIsolates) {
198
- sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_MEMORY_LIMIT, `Maximum isolates (${state.options.maxIsolates}) reached`);
199
- return;
340
+ if (!evictOldestDisposedRuntime(state)) {
341
+ sendError(connection.socket, message.requestId, ErrorCode.ISOLATE_MEMORY_LIMIT, `Maximum isolates (${state.options.maxIsolates}) reached`);
342
+ return;
343
+ }
200
344
  }
201
345
  try {
202
346
  const isolateId = randomUUID();
@@ -206,32 +350,61 @@ async function handleCreateRuntime(message, connection, state) {
206
350
  const moduleLoaderCallback = message.options.callbacks?.moduleLoader;
207
351
  const customCallbacks = message.options.callbacks?.custom;
208
352
  const pendingCallbacks = [];
353
+ const callbackContext = {
354
+ connection,
355
+ consoleOnEntry: consoleCallbacks?.onEntry?.callbackId,
356
+ fetch: fetchCallback?.callbackId,
357
+ moduleLoader: moduleLoaderCallback?.callbackId,
358
+ fs: {
359
+ readFile: fsCallbacks?.readFile?.callbackId,
360
+ writeFile: fsCallbacks?.writeFile?.callbackId,
361
+ stat: fsCallbacks?.stat?.callbackId,
362
+ readdir: fsCallbacks?.readdir?.callbackId,
363
+ unlink: fsCallbacks?.unlink?.callbackId,
364
+ mkdir: fsCallbacks?.mkdir?.callbackId,
365
+ rmdir: fsCallbacks?.rmdir?.callbackId
366
+ },
367
+ custom: new Map(customCallbacks ? Object.entries(customCallbacks).map(([name, reg]) => [name, reg.callbackId]) : [])
368
+ };
209
369
  const runtime = await createInternalRuntime({
210
370
  memoryLimitMB: message.options.memoryLimitMB ?? state.options.defaultMemoryLimitMB,
211
371
  cwd: message.options.cwd,
212
- console: consoleCallbacks?.onEntry ? {
372
+ console: {
213
373
  onEntry: (entry) => {
214
- const promise = invokeClientCallback(connection, consoleCallbacks.onEntry.callbackId, [entry]).catch(() => {});
374
+ const conn = callbackContext.connection;
375
+ const callbackId = callbackContext.consoleOnEntry;
376
+ if (!conn || callbackId === undefined)
377
+ return;
378
+ const promise = invokeClientCallback(conn, callbackId, [entry]).catch(() => {});
215
379
  pendingCallbacks.push(promise);
216
380
  }
217
- } : undefined,
218
- fetch: fetchCallback ? {
381
+ },
382
+ fetch: {
219
383
  onFetch: async (request) => {
384
+ const conn = callbackContext.connection;
385
+ const callbackId = callbackContext.fetch;
386
+ if (!conn || callbackId === undefined) {
387
+ throw new Error("Fetch callback not available");
388
+ }
220
389
  const serialized = await serializeRequest(request);
221
- const result = await invokeClientCallback(connection, fetchCallback.callbackId, [serialized]);
390
+ const result = await invokeClientCallback(conn, callbackId, [serialized]);
222
391
  return deserializeResponse(result);
223
392
  }
224
- } : undefined,
225
- fs: fsCallbacks ? {
393
+ },
394
+ fs: {
226
395
  getDirectory: async (path) => {
396
+ const conn = callbackContext.connection;
397
+ if (!conn) {
398
+ throw new Error("FS callbacks not available");
399
+ }
227
400
  return createCallbackFileSystemHandler({
228
- connection,
229
- callbacks: fsCallbacks,
401
+ connection: conn,
402
+ callbackContext,
230
403
  invokeClientCallback,
231
404
  basePath: path
232
405
  });
233
406
  }
234
- } : undefined
407
+ }
235
408
  });
236
409
  const instance = {
237
410
  isolateId,
@@ -244,7 +417,10 @@ async function handleCreateRuntime(message, connection, state) {
244
417
  returnedCallbacks: new Map,
245
418
  returnedPromises: new Map,
246
419
  returnedIterators: new Map,
247
- nextLocalCallbackId: 1e6
420
+ nextLocalCallbackId: 1e6,
421
+ namespaceId,
422
+ isDisposed: false,
423
+ callbackContext
248
424
  };
249
425
  if (moduleLoaderCallback) {
250
426
  instance.moduleLoaderCallbackId = moduleLoaderCallback.callbackId;
@@ -329,6 +505,9 @@ async function handleCreateRuntime(message, connection, state) {
329
505
  state.isolates.set(isolateId, instance);
330
506
  connection.isolates.add(isolateId);
331
507
  state.stats.totalIsolatesCreated++;
508
+ if (namespaceId != null) {
509
+ state.namespacedRuntimes.set(namespaceId, instance);
510
+ }
332
511
  instance.runtime.fetch.onWebSocketCommand((cmd) => {
333
512
  let data;
334
513
  if (cmd.data instanceof ArrayBuffer) {
@@ -349,7 +528,7 @@ async function handleCreateRuntime(message, connection, state) {
349
528
  };
350
529
  sendMessage(connection.socket, wsCommandMsg);
351
530
  });
352
- sendOk(connection.socket, message.requestId, { isolateId });
531
+ sendOk(connection.socket, message.requestId, { isolateId, reused: false });
353
532
  } catch (err) {
354
533
  const error = err;
355
534
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
@@ -366,12 +545,16 @@ async function handleDisposeRuntime(message, connection, state) {
366
545
  return;
367
546
  }
368
547
  try {
369
- if (instance.playwrightHandle) {
370
- instance.playwrightHandle.dispose();
371
- }
372
- instance.runtime.dispose();
373
- state.isolates.delete(message.isolateId);
374
548
  connection.isolates.delete(message.isolateId);
549
+ if (instance.namespaceId != null) {
550
+ softDeleteRuntime(instance, state);
551
+ } else {
552
+ if (instance.playwrightHandle) {
553
+ instance.playwrightHandle.dispose();
554
+ }
555
+ instance.runtime.dispose();
556
+ state.isolates.delete(message.isolateId);
557
+ }
375
558
  sendOk(connection.socket, message.requestId);
376
559
  } catch (err) {
377
560
  const error = err;
@@ -428,22 +611,21 @@ async function handleDispatchRequest(message, connection, state) {
428
611
  body: requestBody
429
612
  });
430
613
  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) {
614
+ if (response.body) {
434
615
  await sendStreamedResponse(connection, message.requestId, response);
435
616
  } 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 });
617
+ const headers = [];
618
+ response.headers.forEach((value, key) => {
619
+ headers.push([key, value]);
620
+ });
621
+ sendOk(connection.socket, message.requestId, {
622
+ response: {
623
+ status: response.status,
624
+ statusText: response.statusText,
625
+ headers,
626
+ body: null
443
627
  }
444
- } catch {
445
- await sendStreamedResponse(connection, message.requestId, clonedResponse);
446
- }
628
+ });
447
629
  }
448
630
  } catch (err) {
449
631
  const error = err;
@@ -1018,7 +1200,8 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1018
1200
  }
1019
1201
  result = await callback(...args);
1020
1202
  } else {
1021
- result = await invokeClientCallback(connection, callbackId, args);
1203
+ const conn = instance.callbackContext?.connection || connection;
1204
+ result = await invokeClientCallback(conn, callbackId, args);
1022
1205
  }
1023
1206
  const ctx = createMarshalContext();
1024
1207
  const marshalledResult = await marshalValue({ ok: true, value: result }, ctx);
@@ -1034,6 +1217,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1034
1217
  });
1035
1218
  global.setSync("__customFn_invoke", invokeCallbackRef);
1036
1219
  context.evalSync(ISOLATE_MARSHAL_CODE);
1220
+ const callbackIdMap = {};
1221
+ for (const [name, registration] of Object.entries(customCallbacks)) {
1222
+ callbackIdMap[name] = registration.callbackId;
1223
+ }
1224
+ global.setSync("__customFnCallbackIds", new ivm.ExternalCopy(callbackIdMap).copyInto());
1037
1225
  for (const [name, registration] of Object.entries(customCallbacks)) {
1038
1226
  if (name.includes(":")) {
1039
1227
  continue;
@@ -1041,10 +1229,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1041
1229
  if (registration.type === "sync") {
1042
1230
  context.evalSync(`
1043
1231
  globalThis.${name} = function(...args) {
1232
+ const callbackId = globalThis.__customFnCallbackIds["${name}"];
1044
1233
  const argsJson = JSON.stringify(__marshalForHost(args));
1045
1234
  const resultJson = __customFn_invoke.applySyncPromise(
1046
1235
  undefined,
1047
- [${registration.callbackId}, argsJson]
1236
+ [callbackId, argsJson]
1048
1237
  );
1049
1238
  const result = JSON.parse(resultJson);
1050
1239
  if (result.ok) {
@@ -1067,10 +1256,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1067
1256
  context.evalSync(`
1068
1257
  globalThis.${name} = function(...args) {
1069
1258
  // Start the iterator and get the iteratorId
1259
+ const startCallbackId = globalThis.__customFnCallbackIds["${name}:start"];
1070
1260
  const argsJson = JSON.stringify(__marshalForHost(args));
1071
1261
  const startResultJson = __customFn_invoke.applySyncPromise(
1072
1262
  undefined,
1073
- [${startReg.callbackId}, argsJson]
1263
+ [startCallbackId, argsJson]
1074
1264
  );
1075
1265
  const startResult = JSON.parse(startResultJson);
1076
1266
  if (!startResult.ok) {
@@ -1083,10 +1273,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1083
1273
  return {
1084
1274
  [Symbol.asyncIterator]() { return this; },
1085
1275
  async next() {
1276
+ const nextCallbackId = globalThis.__customFnCallbackIds["${name}:next"];
1086
1277
  const argsJson = JSON.stringify(__marshalForHost([iteratorId]));
1087
1278
  const resultJson = __customFn_invoke.applySyncPromise(
1088
1279
  undefined,
1089
- [${nextReg.callbackId}, argsJson]
1280
+ [nextCallbackId, argsJson]
1090
1281
  );
1091
1282
  const result = JSON.parse(resultJson);
1092
1283
  if (!result.ok) {
@@ -1098,19 +1289,21 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1098
1289
  return { done: val.done, value: val.value };
1099
1290
  },
1100
1291
  async return(v) {
1292
+ const returnCallbackId = globalThis.__customFnCallbackIds["${name}:return"];
1101
1293
  const argsJson = JSON.stringify(__marshalForHost([iteratorId, v]));
1102
1294
  const resultJson = __customFn_invoke.applySyncPromise(
1103
1295
  undefined,
1104
- [${returnReg.callbackId}, argsJson]
1296
+ [returnCallbackId, argsJson]
1105
1297
  );
1106
1298
  const result = JSON.parse(resultJson);
1107
1299
  return { done: true, value: result.ok ? __unmarshalFromHost(result.value) : undefined };
1108
1300
  },
1109
1301
  async throw(e) {
1302
+ const throwCallbackId = globalThis.__customFnCallbackIds["${name}:throw"];
1110
1303
  const argsJson = JSON.stringify(__marshalForHost([iteratorId, { message: e?.message, name: e?.name }]));
1111
1304
  const resultJson = __customFn_invoke.applySyncPromise(
1112
1305
  undefined,
1113
- [${throwReg.callbackId}, argsJson]
1306
+ [throwCallbackId, argsJson]
1114
1307
  );
1115
1308
  const result = JSON.parse(resultJson);
1116
1309
  if (!result.ok) {
@@ -1127,10 +1320,11 @@ async function setupCustomFunctions(context, customCallbacks, connection, instan
1127
1320
  } else if (registration.type === "async") {
1128
1321
  context.evalSync(`
1129
1322
  globalThis.${name} = async function(...args) {
1323
+ const callbackId = globalThis.__customFnCallbackIds["${name}"];
1130
1324
  const argsJson = JSON.stringify(__marshalForHost(args));
1131
1325
  const resultJson = __customFn_invoke.applySyncPromise(
1132
1326
  undefined,
1133
- [${registration.callbackId}, argsJson]
1327
+ [callbackId, argsJson]
1134
1328
  );
1135
1329
  const result = JSON.parse(resultJson);
1136
1330
  if (result.ok) {
@@ -1179,22 +1373,6 @@ async function serializeRequest(request) {
1179
1373
  body
1180
1374
  };
1181
1375
  }
1182
- async function serializeResponse(response) {
1183
- const headers = [];
1184
- response.headers.forEach((value, key) => {
1185
- headers.push([key, value]);
1186
- });
1187
- let body = null;
1188
- if (response.body) {
1189
- body = new Uint8Array(await response.arrayBuffer());
1190
- }
1191
- return {
1192
- status: response.status,
1193
- statusText: response.statusText,
1194
- headers,
1195
- body
1196
- };
1197
- }
1198
1376
  function deserializeResponse(data) {
1199
1377
  return new Response(data.body, {
1200
1378
  status: data.status,
@@ -1355,14 +1533,21 @@ async function handleRunTests(message, connection, state) {
1355
1533
  instance.lastActivity = Date.now();
1356
1534
  try {
1357
1535
  const timeout = message.timeout ?? 30000;
1536
+ let timeoutId;
1358
1537
  const timeoutPromise = new Promise((_, reject) => {
1359
- setTimeout(() => reject(new Error("Test timeout")), timeout);
1538
+ timeoutId = setTimeout(() => reject(new Error("Test timeout")), timeout);
1360
1539
  });
1361
- const results = await Promise.race([
1362
- runTestsInContext(instance.runtime.context),
1363
- timeoutPromise
1364
- ]);
1365
- sendOk(connection.socket, message.requestId, results);
1540
+ try {
1541
+ const results = await Promise.race([
1542
+ runTestsInContext(instance.runtime.context),
1543
+ timeoutPromise
1544
+ ]);
1545
+ sendOk(connection.socket, message.requestId, results);
1546
+ } finally {
1547
+ if (timeoutId) {
1548
+ clearTimeout(timeoutId);
1549
+ }
1550
+ }
1366
1551
  } catch (err) {
1367
1552
  const error = err;
1368
1553
  sendError(connection.socket, message.requestId, ErrorCode.SCRIPT_ERROR, error.message, { name: error.name, stack: error.stack });
@@ -1477,4 +1662,4 @@ export {
1477
1662
  handleConnection
1478
1663
  };
1479
1664
 
1480
- //# debugId=8FEF67C0571D870E64756E2164756E21
1665
+ //# debugId=EC4C1FF56C0445D064756E2164756E21