@ricsam/isolate-client 0.1.5 → 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 +78 -22
- 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 +78 -22
- 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
|
@@ -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();
|
|
@@ -175,23 +199,35 @@ function handleMessage(message, state) {
|
|
|
175
199
|
metadata: msg.metadata,
|
|
176
200
|
controller: null,
|
|
177
201
|
state: "active",
|
|
178
|
-
pendingChunks: []
|
|
202
|
+
pendingChunks: [],
|
|
203
|
+
pullResolvers: [],
|
|
204
|
+
controllerFinalized: false
|
|
179
205
|
};
|
|
180
206
|
const readableStream = new ReadableStream({
|
|
181
207
|
start(controller) {
|
|
182
208
|
receiver.controller = controller;
|
|
183
209
|
},
|
|
184
210
|
pull(_controller) {
|
|
211
|
+
if (receiver.controllerFinalized) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
185
214
|
while (receiver.pendingChunks.length > 0) {
|
|
186
215
|
const chunk = receiver.pendingChunks.shift();
|
|
187
216
|
receiver.controller.enqueue(chunk);
|
|
188
217
|
}
|
|
189
218
|
if (receiver.state === "closed") {
|
|
190
|
-
receiver.
|
|
191
|
-
|
|
219
|
+
if (!receiver.controllerFinalized) {
|
|
220
|
+
receiver.controllerFinalized = true;
|
|
221
|
+
receiver.controller.close();
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve();
|
|
192
224
|
}
|
|
193
225
|
if (receiver.state === "errored") {
|
|
194
|
-
|
|
226
|
+
if (!receiver.controllerFinalized && receiver.error) {
|
|
227
|
+
receiver.controllerFinalized = true;
|
|
228
|
+
receiver.controller.error(receiver.error);
|
|
229
|
+
}
|
|
230
|
+
return Promise.resolve();
|
|
195
231
|
}
|
|
196
232
|
sendMessage(state.socket, {
|
|
197
233
|
type: import_isolate_protocol.MessageType.STREAM_PULL,
|
|
@@ -199,17 +235,23 @@ function handleMessage(message, state) {
|
|
|
199
235
|
maxBytes: import_isolate_protocol.STREAM_DEFAULT_CREDIT
|
|
200
236
|
});
|
|
201
237
|
return new Promise((resolve) => {
|
|
202
|
-
receiver.
|
|
238
|
+
receiver.pullResolvers.push(resolve);
|
|
203
239
|
});
|
|
204
240
|
},
|
|
205
241
|
cancel(_reason) {
|
|
206
|
-
receiver.state = "
|
|
242
|
+
receiver.state = "closed";
|
|
243
|
+
receiver.controllerFinalized = true;
|
|
244
|
+
const resolvers = receiver.pullResolvers.splice(0);
|
|
245
|
+
for (const resolver of resolvers) {
|
|
246
|
+
resolver();
|
|
247
|
+
}
|
|
207
248
|
sendMessage(state.socket, {
|
|
208
249
|
type: import_isolate_protocol.MessageType.STREAM_ERROR,
|
|
209
250
|
streamId: msg.streamId,
|
|
210
251
|
error: "Stream cancelled by consumer"
|
|
211
252
|
});
|
|
212
253
|
state.streamResponses.delete(msg.streamId);
|
|
254
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
213
255
|
}
|
|
214
256
|
});
|
|
215
257
|
state.streamResponses.set(msg.streamId, receiver);
|
|
@@ -236,10 +278,9 @@ function handleMessage(message, state) {
|
|
|
236
278
|
const msg = message;
|
|
237
279
|
const receiver = state.streamResponses.get(msg.streamId);
|
|
238
280
|
if (receiver && receiver.state === "active") {
|
|
239
|
-
if (receiver.
|
|
281
|
+
if (receiver.pullResolvers.length > 0) {
|
|
240
282
|
receiver.controller.enqueue(msg.chunk);
|
|
241
|
-
const resolver = receiver.
|
|
242
|
-
receiver.pullResolver = undefined;
|
|
283
|
+
const resolver = receiver.pullResolvers.shift();
|
|
243
284
|
resolver();
|
|
244
285
|
} else {
|
|
245
286
|
receiver.pendingChunks.push(msg.chunk);
|
|
@@ -256,10 +297,12 @@ function handleMessage(message, state) {
|
|
|
256
297
|
const chunk = receiver.pendingChunks.shift();
|
|
257
298
|
receiver.controller.enqueue(chunk);
|
|
258
299
|
}
|
|
259
|
-
receiver.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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) {
|
|
263
306
|
resolver();
|
|
264
307
|
}
|
|
265
308
|
state.streamResponses.delete(msg.streamId);
|
|
@@ -288,10 +331,13 @@ function handleMessage(message, state) {
|
|
|
288
331
|
const receiver = state.streamResponses.get(msg.streamId);
|
|
289
332
|
if (receiver) {
|
|
290
333
|
receiver.state = "errored";
|
|
291
|
-
receiver.
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
receiver.
|
|
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) {
|
|
295
341
|
resolver();
|
|
296
342
|
}
|
|
297
343
|
state.streamResponses.delete(msg.streamId);
|
|
@@ -351,7 +397,7 @@ function sendRequest(state, message, timeout = DEFAULT_TIMEOUT) {
|
|
|
351
397
|
sendMessage(state.socket, message);
|
|
352
398
|
});
|
|
353
399
|
}
|
|
354
|
-
async function createRuntime(state, options = {}) {
|
|
400
|
+
async function createRuntime(state, options = {}, namespaceId) {
|
|
355
401
|
const callbacks = {};
|
|
356
402
|
if (options.console) {
|
|
357
403
|
callbacks.console = registerConsoleCallbacks(state, options.console);
|
|
@@ -474,11 +520,13 @@ async function createRuntime(state, options = {}) {
|
|
|
474
520
|
memoryLimitMB: options.memoryLimitMB,
|
|
475
521
|
cwd: options.cwd,
|
|
476
522
|
callbacks,
|
|
477
|
-
testEnvironment: testEnvironmentOption
|
|
523
|
+
testEnvironment: testEnvironmentOption,
|
|
524
|
+
namespaceId
|
|
478
525
|
}
|
|
479
526
|
};
|
|
480
527
|
const result = await sendRequest(state, request);
|
|
481
528
|
const isolateId = result.isolateId;
|
|
529
|
+
const reused = result.reused ?? false;
|
|
482
530
|
const wsCommandCallbacks = new Set;
|
|
483
531
|
isolateWsCallbacks.set(isolateId, wsCommandCallbacks);
|
|
484
532
|
const fetchHandle = {
|
|
@@ -722,6 +770,7 @@ async function createRuntime(state, options = {}) {
|
|
|
722
770
|
return {
|
|
723
771
|
id: isolateId,
|
|
724
772
|
isolateId,
|
|
773
|
+
reused,
|
|
725
774
|
fetch: fetchHandle,
|
|
726
775
|
timers: timersHandle,
|
|
727
776
|
console: consoleHandle,
|
|
@@ -855,7 +904,14 @@ function registerFsCallbacks(state, callbacks) {
|
|
|
855
904
|
function registerModuleLoaderCallback(state, callback) {
|
|
856
905
|
const callbackId = state.nextCallbackId++;
|
|
857
906
|
state.callbacks.set(callbackId, async (moduleName) => {
|
|
858
|
-
|
|
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;
|
|
859
915
|
});
|
|
860
916
|
return { callbackId, name: "moduleLoader", type: "async" };
|
|
861
917
|
}
|
|
@@ -1182,4 +1238,4 @@ async function sendBodyStream(state, streamId, body) {
|
|
|
1182
1238
|
}
|
|
1183
1239
|
})
|
|
1184
1240
|
|
|
1185
|
-
//# debugId=
|
|
1241
|
+
//# debugId=6FA96113BD7C15F464756E2164756E21
|