@ricsam/isolate-client 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.
- package/README.md +89 -0
- package/dist/cjs/connection.cjs +146 -49
- package/dist/cjs/connection.cjs.map +3 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/connection.mjs +145 -48
- package/dist/mjs/connection.mjs.map +3 -3
- package/dist/mjs/index.mjs.map +1 -1
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ npm add @ricsam/isolate-client
|
|
|
18
18
|
- Module loader for custom ES module resolution
|
|
19
19
|
- Custom functions callable from isolate code
|
|
20
20
|
- Test environment and Playwright support
|
|
21
|
+
- **Namespace-based runtime caching** for performance optimization
|
|
21
22
|
|
|
22
23
|
## Basic Usage
|
|
23
24
|
|
|
@@ -69,6 +70,79 @@ await runtime.dispose();
|
|
|
69
70
|
await client.close();
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
## Namespace-Based Runtime Caching
|
|
74
|
+
|
|
75
|
+
For performance-critical applications, use **namespaces** to cache and reuse runtimes. Namespaced runtimes preserve their V8 isolate, context, and compiled module cache across dispose/create cycles:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { connect } from "@ricsam/isolate-client";
|
|
79
|
+
|
|
80
|
+
const client = await connect({ socket: "/tmp/isolate.sock" });
|
|
81
|
+
|
|
82
|
+
// Create a namespace for a tenant/user/session
|
|
83
|
+
const namespace = client.createNamespace("tenant-123");
|
|
84
|
+
|
|
85
|
+
// Create a runtime in this namespace
|
|
86
|
+
const runtime = await namespace.createRuntime({
|
|
87
|
+
memoryLimitMB: 128,
|
|
88
|
+
moduleLoader: async (name) => loadModule(name),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(runtime.reused); // false - first time
|
|
92
|
+
|
|
93
|
+
// Import heavy modules (gets compiled and cached)
|
|
94
|
+
await runtime.eval(`
|
|
95
|
+
import { heavyLibrary } from "@/heavy-module";
|
|
96
|
+
console.log("Module loaded!");
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
// Dispose returns runtime to pool (soft-delete)
|
|
100
|
+
await runtime.dispose();
|
|
101
|
+
|
|
102
|
+
// Later: reuse the same namespace (same or different connection!)
|
|
103
|
+
const client2 = await connect({ socket: "/tmp/isolate.sock" });
|
|
104
|
+
const namespace2 = client2.createNamespace("tenant-123");
|
|
105
|
+
const runtime2 = await namespace2.createRuntime({ /* options */ });
|
|
106
|
+
|
|
107
|
+
console.log(runtime2.reused); // true - reused from pool!
|
|
108
|
+
// Module cache preserved - no recompilation needed
|
|
109
|
+
await runtime2.eval(`
|
|
110
|
+
import { heavyLibrary } from "@/heavy-module"; // instant!
|
|
111
|
+
`);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Namespace Interface
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface Namespace {
|
|
118
|
+
/** The namespace ID */
|
|
119
|
+
readonly id: string;
|
|
120
|
+
/** Create a runtime in this namespace (cacheable on dispose) */
|
|
121
|
+
createRuntime(options?: RuntimeOptions): Promise<RemoteRuntime>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### What's Preserved vs Reset
|
|
126
|
+
|
|
127
|
+
**Preserved on reuse (performance benefit):**
|
|
128
|
+
- V8 Isolate instance
|
|
129
|
+
- V8 Context
|
|
130
|
+
- Compiled ES module cache
|
|
131
|
+
- Global state and imported modules
|
|
132
|
+
|
|
133
|
+
**Reset on reuse:**
|
|
134
|
+
- Owner connection (new owner)
|
|
135
|
+
- Callbacks (re-registered from new client)
|
|
136
|
+
- Timers (cleared)
|
|
137
|
+
- Console state (counters, timers, groups reset)
|
|
138
|
+
|
|
139
|
+
### Behavior Notes
|
|
140
|
+
|
|
141
|
+
- Non-namespaced runtimes (`client.createRuntime()`) work as before - true disposal
|
|
142
|
+
- Namespaced runtimes are cached on dispose and evicted via LRU when `maxIsolates` limit is reached
|
|
143
|
+
- Cross-client reuse is allowed - any connection can reuse a namespace by ID
|
|
144
|
+
- A namespace can only have one active runtime at a time; creating a second runtime with the same namespace ID while one is active will fail
|
|
145
|
+
|
|
72
146
|
## Module Loader
|
|
73
147
|
|
|
74
148
|
Register a custom module loader to handle dynamic `import()` calls:
|
|
@@ -340,7 +414,11 @@ await browser.close();
|
|
|
340
414
|
```typescript
|
|
341
415
|
interface RemoteRuntime {
|
|
342
416
|
readonly id: string;
|
|
417
|
+
/** True if runtime was reused from namespace pool */
|
|
418
|
+
readonly reused?: boolean;
|
|
419
|
+
|
|
343
420
|
eval(code: string, filename?: string): Promise<void>;
|
|
421
|
+
/** Dispose runtime (soft-delete if namespaced, hard delete otherwise) */
|
|
344
422
|
dispose(): Promise<void>;
|
|
345
423
|
|
|
346
424
|
// Module handles
|
|
@@ -351,6 +429,17 @@ interface RemoteRuntime {
|
|
|
351
429
|
readonly playwright: RemotePlaywrightHandle;
|
|
352
430
|
}
|
|
353
431
|
|
|
432
|
+
interface DaemonConnection {
|
|
433
|
+
/** Create a new runtime in the daemon */
|
|
434
|
+
createRuntime(options?: RuntimeOptions): Promise<RemoteRuntime>;
|
|
435
|
+
/** Create a namespace for runtime pooling/reuse */
|
|
436
|
+
createNamespace(id: string): Namespace;
|
|
437
|
+
/** Close the connection */
|
|
438
|
+
close(): Promise<void>;
|
|
439
|
+
/** Check if connected */
|
|
440
|
+
isConnected(): boolean;
|
|
441
|
+
}
|
|
442
|
+
|
|
354
443
|
interface RemoteFetchHandle {
|
|
355
444
|
dispatchRequest(request: Request, options?: DispatchOptions): Promise<Response>;
|
|
356
445
|
hasServeHandler(): Promise<boolean>;
|
package/dist/cjs/connection.cjs
CHANGED
|
@@ -35,7 +35,7 @@ __export(exports_connection, {
|
|
|
35
35
|
module.exports = __toCommonJS(exports_connection);
|
|
36
36
|
var import_node_net = require("net");
|
|
37
37
|
var import_isolate_protocol = require("@ricsam/isolate-protocol");
|
|
38
|
-
var
|
|
38
|
+
var import_client = require("@ricsam/isolate-playwright/client");
|
|
39
39
|
var DEFAULT_TIMEOUT = 30000;
|
|
40
40
|
var isolateWsCallbacks = new Map;
|
|
41
41
|
async function connect(options = {}) {
|
|
@@ -49,7 +49,8 @@ async function connect(options = {}) {
|
|
|
49
49
|
nextStreamId: 1,
|
|
50
50
|
connected: true,
|
|
51
51
|
streamResponses: new Map,
|
|
52
|
-
uploadStreams: new Map
|
|
52
|
+
uploadStreams: new Map,
|
|
53
|
+
moduleSourceCache: new Map
|
|
53
54
|
};
|
|
54
55
|
const parser = import_isolate_protocol.createFrameParser();
|
|
55
56
|
socket.on("data", (data) => {
|
|
@@ -64,15 +65,38 @@ async function connect(options = {}) {
|
|
|
64
65
|
socket.on("close", () => {
|
|
65
66
|
state.connected = false;
|
|
66
67
|
for (const [, pending] of state.pendingRequests) {
|
|
68
|
+
if (pending.timeoutId) {
|
|
69
|
+
clearTimeout(pending.timeoutId);
|
|
70
|
+
}
|
|
67
71
|
pending.reject(new Error("Connection closed"));
|
|
68
72
|
}
|
|
69
73
|
state.pendingRequests.clear();
|
|
74
|
+
for (const [, receiver] of state.streamResponses) {
|
|
75
|
+
receiver.state = "errored";
|
|
76
|
+
receiver.error = new Error("Connection closed");
|
|
77
|
+
const resolvers = receiver.pullResolvers.splice(0);
|
|
78
|
+
for (const resolver of resolvers) {
|
|
79
|
+
resolver();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
state.streamResponses.clear();
|
|
83
|
+
for (const [, session] of state.uploadStreams) {
|
|
84
|
+
session.state = "closed";
|
|
85
|
+
if (session.creditResolver) {
|
|
86
|
+
session.creditResolver();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
state.uploadStreams.clear();
|
|
70
90
|
});
|
|
71
91
|
socket.on("error", (err) => {
|
|
72
92
|
console.error("Socket error:", err);
|
|
73
93
|
});
|
|
74
94
|
return {
|
|
75
95
|
createRuntime: (runtimeOptions) => createRuntime(state, runtimeOptions),
|
|
96
|
+
createNamespace: (id) => ({
|
|
97
|
+
id,
|
|
98
|
+
createRuntime: (runtimeOptions) => createRuntime(state, runtimeOptions, id)
|
|
99
|
+
}),
|
|
76
100
|
close: async () => {
|
|
77
101
|
state.connected = false;
|
|
78
102
|
socket.destroy();
|
|
@@ -173,10 +197,76 @@ function handleMessage(message, state) {
|
|
|
173
197
|
streamId: msg.streamId,
|
|
174
198
|
requestId: msg.requestId,
|
|
175
199
|
metadata: msg.metadata,
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
controller: null,
|
|
201
|
+
state: "active",
|
|
202
|
+
pendingChunks: [],
|
|
203
|
+
pullResolvers: [],
|
|
204
|
+
controllerFinalized: false
|
|
178
205
|
};
|
|
206
|
+
const readableStream = new ReadableStream({
|
|
207
|
+
start(controller) {
|
|
208
|
+
receiver.controller = controller;
|
|
209
|
+
},
|
|
210
|
+
pull(_controller) {
|
|
211
|
+
if (receiver.controllerFinalized) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
while (receiver.pendingChunks.length > 0) {
|
|
215
|
+
const chunk = receiver.pendingChunks.shift();
|
|
216
|
+
receiver.controller.enqueue(chunk);
|
|
217
|
+
}
|
|
218
|
+
if (receiver.state === "closed") {
|
|
219
|
+
if (!receiver.controllerFinalized) {
|
|
220
|
+
receiver.controllerFinalized = true;
|
|
221
|
+
receiver.controller.close();
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve();
|
|
224
|
+
}
|
|
225
|
+
if (receiver.state === "errored") {
|
|
226
|
+
if (!receiver.controllerFinalized && receiver.error) {
|
|
227
|
+
receiver.controllerFinalized = true;
|
|
228
|
+
receiver.controller.error(receiver.error);
|
|
229
|
+
}
|
|
230
|
+
return Promise.resolve();
|
|
231
|
+
}
|
|
232
|
+
sendMessage(state.socket, {
|
|
233
|
+
type: import_isolate_protocol.MessageType.STREAM_PULL,
|
|
234
|
+
streamId: msg.streamId,
|
|
235
|
+
maxBytes: import_isolate_protocol.STREAM_DEFAULT_CREDIT
|
|
236
|
+
});
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
receiver.pullResolvers.push(resolve);
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
cancel(_reason) {
|
|
242
|
+
receiver.state = "closed";
|
|
243
|
+
receiver.controllerFinalized = true;
|
|
244
|
+
const resolvers = receiver.pullResolvers.splice(0);
|
|
245
|
+
for (const resolver of resolvers) {
|
|
246
|
+
resolver();
|
|
247
|
+
}
|
|
248
|
+
sendMessage(state.socket, {
|
|
249
|
+
type: import_isolate_protocol.MessageType.STREAM_ERROR,
|
|
250
|
+
streamId: msg.streamId,
|
|
251
|
+
error: "Stream cancelled by consumer"
|
|
252
|
+
});
|
|
253
|
+
state.streamResponses.delete(msg.streamId);
|
|
254
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
179
257
|
state.streamResponses.set(msg.streamId, receiver);
|
|
258
|
+
const pending = state.pendingRequests.get(msg.requestId);
|
|
259
|
+
if (pending) {
|
|
260
|
+
state.pendingRequests.delete(msg.requestId);
|
|
261
|
+
if (pending.timeoutId)
|
|
262
|
+
clearTimeout(pending.timeoutId);
|
|
263
|
+
const response = new Response(readableStream, {
|
|
264
|
+
status: msg.metadata?.status ?? 200,
|
|
265
|
+
statusText: msg.metadata?.statusText ?? "OK",
|
|
266
|
+
headers: msg.metadata?.headers
|
|
267
|
+
});
|
|
268
|
+
pending.resolve({ response, __streaming: true });
|
|
269
|
+
}
|
|
180
270
|
sendMessage(state.socket, {
|
|
181
271
|
type: import_isolate_protocol.MessageType.STREAM_PULL,
|
|
182
272
|
streamId: msg.streamId,
|
|
@@ -187,14 +277,14 @@ function handleMessage(message, state) {
|
|
|
187
277
|
case import_isolate_protocol.MessageType.RESPONSE_STREAM_CHUNK: {
|
|
188
278
|
const msg = message;
|
|
189
279
|
const receiver = state.streamResponses.get(msg.streamId);
|
|
190
|
-
if (receiver) {
|
|
191
|
-
receiver.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
280
|
+
if (receiver && receiver.state === "active") {
|
|
281
|
+
if (receiver.pullResolvers.length > 0) {
|
|
282
|
+
receiver.controller.enqueue(msg.chunk);
|
|
283
|
+
const resolver = receiver.pullResolvers.shift();
|
|
284
|
+
resolver();
|
|
285
|
+
} else {
|
|
286
|
+
receiver.pendingChunks.push(msg.chunk);
|
|
287
|
+
}
|
|
198
288
|
}
|
|
199
289
|
break;
|
|
200
290
|
}
|
|
@@ -202,20 +292,18 @@ function handleMessage(message, state) {
|
|
|
202
292
|
const msg = message;
|
|
203
293
|
const receiver = state.streamResponses.get(msg.streamId);
|
|
204
294
|
if (receiver) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
});
|
|
295
|
+
receiver.state = "closed";
|
|
296
|
+
while (receiver.pendingChunks.length > 0) {
|
|
297
|
+
const chunk = receiver.pendingChunks.shift();
|
|
298
|
+
receiver.controller.enqueue(chunk);
|
|
299
|
+
}
|
|
300
|
+
if (!receiver.controllerFinalized) {
|
|
301
|
+
receiver.controllerFinalized = true;
|
|
302
|
+
receiver.controller.close();
|
|
303
|
+
}
|
|
304
|
+
const resolvers = receiver.pullResolvers.splice(0);
|
|
305
|
+
for (const resolver of resolvers) {
|
|
306
|
+
resolver();
|
|
219
307
|
}
|
|
220
308
|
state.streamResponses.delete(msg.streamId);
|
|
221
309
|
}
|
|
@@ -242,12 +330,15 @@ function handleMessage(message, state) {
|
|
|
242
330
|
}
|
|
243
331
|
const receiver = state.streamResponses.get(msg.streamId);
|
|
244
332
|
if (receiver) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
333
|
+
receiver.state = "errored";
|
|
334
|
+
receiver.error = new Error(msg.error);
|
|
335
|
+
while (receiver.pendingChunks.length > 0) {
|
|
336
|
+
const chunk = receiver.pendingChunks.shift();
|
|
337
|
+
receiver.controller.enqueue(chunk);
|
|
338
|
+
}
|
|
339
|
+
const resolvers = receiver.pullResolvers.splice(0);
|
|
340
|
+
for (const resolver of resolvers) {
|
|
341
|
+
resolver();
|
|
251
342
|
}
|
|
252
343
|
state.streamResponses.delete(msg.streamId);
|
|
253
344
|
}
|
|
@@ -257,16 +348,6 @@ function handleMessage(message, state) {
|
|
|
257
348
|
console.warn(`Unexpected message type: ${message.type}`);
|
|
258
349
|
}
|
|
259
350
|
}
|
|
260
|
-
function concatUint8Arrays(arrays) {
|
|
261
|
-
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
262
|
-
const result = new Uint8Array(totalLength);
|
|
263
|
-
let offset = 0;
|
|
264
|
-
for (const arr of arrays) {
|
|
265
|
-
result.set(arr, offset);
|
|
266
|
-
offset += arr.length;
|
|
267
|
-
}
|
|
268
|
-
return result;
|
|
269
|
-
}
|
|
270
351
|
async function handleCallbackInvoke(invoke, state) {
|
|
271
352
|
const callback = state.callbacks.get(invoke.callbackId);
|
|
272
353
|
const response = {
|
|
@@ -316,7 +397,7 @@ function sendRequest(state, message, timeout = DEFAULT_TIMEOUT) {
|
|
|
316
397
|
sendMessage(state.socket, message);
|
|
317
398
|
});
|
|
318
399
|
}
|
|
319
|
-
async function createRuntime(state, options = {}) {
|
|
400
|
+
async function createRuntime(state, options = {}, namespaceId) {
|
|
320
401
|
const callbacks = {};
|
|
321
402
|
if (options.console) {
|
|
322
403
|
callbacks.console = registerConsoleCallbacks(state, options.console);
|
|
@@ -335,7 +416,7 @@ async function createRuntime(state, options = {}) {
|
|
|
335
416
|
}
|
|
336
417
|
let playwrightHandler;
|
|
337
418
|
if (options.playwright) {
|
|
338
|
-
playwrightHandler =
|
|
419
|
+
playwrightHandler = import_client.createPlaywrightHandler(options.playwright.page, {
|
|
339
420
|
timeout: options.playwright.timeout,
|
|
340
421
|
baseUrl: options.playwright.baseUrl
|
|
341
422
|
});
|
|
@@ -439,11 +520,13 @@ async function createRuntime(state, options = {}) {
|
|
|
439
520
|
memoryLimitMB: options.memoryLimitMB,
|
|
440
521
|
cwd: options.cwd,
|
|
441
522
|
callbacks,
|
|
442
|
-
testEnvironment: testEnvironmentOption
|
|
523
|
+
testEnvironment: testEnvironmentOption,
|
|
524
|
+
namespaceId
|
|
443
525
|
}
|
|
444
526
|
};
|
|
445
527
|
const result = await sendRequest(state, request);
|
|
446
528
|
const isolateId = result.isolateId;
|
|
529
|
+
const reused = result.reused ?? false;
|
|
447
530
|
const wsCommandCallbacks = new Set;
|
|
448
531
|
isolateWsCallbacks.set(isolateId, wsCommandCallbacks);
|
|
449
532
|
const fetchHandle = {
|
|
@@ -458,15 +541,21 @@ async function createRuntime(state, options = {}) {
|
|
|
458
541
|
request: serializableRequest,
|
|
459
542
|
options: opts
|
|
460
543
|
};
|
|
544
|
+
const handleResponse = (res) => {
|
|
545
|
+
if (res.__streaming && res.response instanceof Response) {
|
|
546
|
+
return res.response;
|
|
547
|
+
}
|
|
548
|
+
return deserializeResponse(res.response);
|
|
549
|
+
};
|
|
461
550
|
if (serialized.bodyStreamId !== undefined && bodyStream) {
|
|
462
551
|
const streamId = serialized.bodyStreamId;
|
|
463
552
|
const responsePromise = sendRequest(state, request2, opts?.timeout ?? DEFAULT_TIMEOUT);
|
|
464
553
|
await sendBodyStream(state, streamId, bodyStream);
|
|
465
554
|
const res = await responsePromise;
|
|
466
|
-
return
|
|
555
|
+
return handleResponse(res);
|
|
467
556
|
} else {
|
|
468
557
|
const res = await sendRequest(state, request2, opts?.timeout ?? DEFAULT_TIMEOUT);
|
|
469
|
-
return
|
|
558
|
+
return handleResponse(res);
|
|
470
559
|
}
|
|
471
560
|
},
|
|
472
561
|
async getUpgradeRequest() {
|
|
@@ -681,6 +770,7 @@ async function createRuntime(state, options = {}) {
|
|
|
681
770
|
return {
|
|
682
771
|
id: isolateId,
|
|
683
772
|
isolateId,
|
|
773
|
+
reused,
|
|
684
774
|
fetch: fetchHandle,
|
|
685
775
|
timers: timersHandle,
|
|
686
776
|
console: consoleHandle,
|
|
@@ -814,7 +904,14 @@ function registerFsCallbacks(state, callbacks) {
|
|
|
814
904
|
function registerModuleLoaderCallback(state, callback) {
|
|
815
905
|
const callbackId = state.nextCallbackId++;
|
|
816
906
|
state.callbacks.set(callbackId, async (moduleName) => {
|
|
817
|
-
|
|
907
|
+
const specifier = moduleName;
|
|
908
|
+
const cached = state.moduleSourceCache.get(specifier);
|
|
909
|
+
if (cached !== undefined) {
|
|
910
|
+
return cached;
|
|
911
|
+
}
|
|
912
|
+
const source = await callback(specifier);
|
|
913
|
+
state.moduleSourceCache.set(specifier, source);
|
|
914
|
+
return source;
|
|
818
915
|
});
|
|
819
916
|
return { callbackId, name: "moduleLoader", type: "async" };
|
|
820
917
|
}
|
|
@@ -1141,4 +1238,4 @@ async function sendBodyStream(state, streamId, body) {
|
|
|
1141
1238
|
}
|
|
1142
1239
|
})
|
|
1143
1240
|
|
|
1144
|
-
//# debugId=
|
|
1241
|
+
//# debugId=6FA96113BD7C15F464756E2164756E21
|