@ricsam/isolate-client 0.1.1 → 0.1.5
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 +313 -39
- package/dist/cjs/connection.cjs +785 -104
- 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 +790 -105
- 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 +207 -82
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Client library for connecting to the isolate daemon. Works with **any JavaScript runtime** (Node.js, Bun, Deno) since it only requires standard socket APIs.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npm add @ricsam/isolate-client
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Features
|
|
12
|
+
|
|
10
13
|
- Connect via Unix socket or TCP
|
|
11
14
|
- Create and manage remote runtimes
|
|
12
15
|
- Execute code in isolated V8 contexts
|
|
13
16
|
- Dispatch HTTP requests to isolate handlers
|
|
14
17
|
- Bidirectional callbacks (console, fetch, fs)
|
|
18
|
+
- Module loader for custom ES module resolution
|
|
19
|
+
- Custom functions callable from isolate code
|
|
15
20
|
- Test environment and Playwright support
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
## Basic Usage
|
|
18
23
|
|
|
19
24
|
```typescript
|
|
20
25
|
import { connect } from "@ricsam/isolate-client";
|
|
@@ -27,23 +32,14 @@ const client = await connect({
|
|
|
27
32
|
|
|
28
33
|
// Create a runtime with callbacks
|
|
29
34
|
const runtime = await client.createRuntime({
|
|
30
|
-
|
|
35
|
+
memoryLimitMB: 128,
|
|
31
36
|
console: {
|
|
32
|
-
|
|
33
|
-
error: (...args) => console.error("[isolate]", ...args),
|
|
37
|
+
onEntry: (entry) => console.log("[isolate]", entry),
|
|
34
38
|
},
|
|
35
39
|
fetch: async (request) => fetch(request),
|
|
36
|
-
fs: {
|
|
37
|
-
readFile: async (path) => Bun.file(path).arrayBuffer(),
|
|
38
|
-
writeFile: async (path, data) => Bun.write(path, data),
|
|
39
|
-
stat: async (path) => {
|
|
40
|
-
const stat = await Bun.file(path).stat();
|
|
41
|
-
return { isFile: true, isDirectory: false, size: stat.size };
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
40
|
});
|
|
45
41
|
|
|
46
|
-
// Execute code
|
|
42
|
+
// Execute code (always ES module mode)
|
|
47
43
|
await runtime.eval(`console.log("Hello from isolate!")`);
|
|
48
44
|
|
|
49
45
|
// Set up HTTP handler and dispatch requests
|
|
@@ -55,60 +51,338 @@ await runtime.eval(`
|
|
|
55
51
|
});
|
|
56
52
|
`);
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
// Dispatch requests via fetch handle
|
|
55
|
+
const response = await runtime.fetch.dispatchRequest(
|
|
59
56
|
new Request("http://localhost/api")
|
|
60
57
|
);
|
|
61
58
|
console.log(await response.json()); // { message: "Hello!" }
|
|
62
59
|
|
|
60
|
+
// Timers fire automatically with real time
|
|
61
|
+
// Clear all pending timers if needed
|
|
62
|
+
await runtime.timers.clearAll();
|
|
63
|
+
|
|
64
|
+
// Console state access
|
|
65
|
+
const counters = await runtime.console.getCounters();
|
|
66
|
+
|
|
63
67
|
// Cleanup
|
|
64
68
|
await runtime.dispose();
|
|
65
69
|
await client.close();
|
|
66
70
|
```
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
## Module Loader
|
|
73
|
+
|
|
74
|
+
Register a custom module loader to handle dynamic `import()` calls:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const runtime = await client.createRuntime({
|
|
78
|
+
moduleLoader: async (moduleName: string) => {
|
|
79
|
+
if (moduleName === "@/db") {
|
|
80
|
+
return `
|
|
81
|
+
export async function getUser(id) {
|
|
82
|
+
const response = await fetch("/api/users/" + id);
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
if (moduleName === "@/config") {
|
|
88
|
+
return `export const API_KEY = "sk-xxx";`;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Unknown module: ${moduleName}`);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await runtime.eval(`
|
|
95
|
+
import { getUser } from "@/db";
|
|
96
|
+
import { API_KEY } from "@/config";
|
|
97
|
+
|
|
98
|
+
const user = await getUser("123");
|
|
99
|
+
console.log("User:", user, "API Key:", API_KEY);
|
|
100
|
+
`);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Custom Functions
|
|
104
|
+
|
|
105
|
+
Register custom functions callable from isolate code. Each function must specify its `type`:
|
|
106
|
+
|
|
107
|
+
- `'sync'` - Synchronous function, returns value directly
|
|
108
|
+
- `'async'` - Asynchronous function, returns a Promise
|
|
109
|
+
- `'asyncIterator'` - Async generator, yields values via `for await...of`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import bcrypt from "bcrypt";
|
|
113
|
+
|
|
114
|
+
const runtime = await client.createRuntime({
|
|
115
|
+
customFunctions: {
|
|
116
|
+
// Async function
|
|
117
|
+
hashPassword: {
|
|
118
|
+
fn: async (password: string) => {
|
|
119
|
+
return bcrypt.hash(password, 10);
|
|
120
|
+
},
|
|
121
|
+
type: 'async',
|
|
122
|
+
},
|
|
123
|
+
// Sync function
|
|
124
|
+
getConfig: {
|
|
125
|
+
fn: () => ({ environment: "production" }),
|
|
126
|
+
type: 'sync',
|
|
127
|
+
},
|
|
128
|
+
// Async iterator (generator)
|
|
129
|
+
streamData: {
|
|
130
|
+
fn: async function* (count: number) {
|
|
131
|
+
for (let i = 0; i < count; i++) {
|
|
132
|
+
yield { chunk: i, timestamp: Date.now() };
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
type: 'asyncIterator',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await runtime.eval(`
|
|
141
|
+
const hash = await hashPassword("secret123");
|
|
142
|
+
const config = getConfig(); // sync function, no await needed
|
|
143
|
+
console.log(hash, config.environment);
|
|
144
|
+
|
|
145
|
+
// Consume async iterator
|
|
146
|
+
for await (const data of streamData(5)) {
|
|
147
|
+
console.log(data.chunk); // 0, 1, 2, 3, 4
|
|
148
|
+
}
|
|
149
|
+
`);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Supported Data Types
|
|
153
|
+
|
|
154
|
+
Custom function arguments and return values support:
|
|
155
|
+
|
|
156
|
+
- **Primitives**: `string`, `number`, `boolean`, `null`, `undefined`, `bigint`
|
|
157
|
+
- **Web APIs**: `Request`, `Response`, `File`, `Blob`, `FormData`, `Headers`, `URL`
|
|
158
|
+
- **Binary**: `Uint8Array`, `ArrayBuffer`
|
|
159
|
+
- **Containers**: Arrays, plain objects (nested)
|
|
160
|
+
- **Advanced**: `Date`, `RegExp`, `Promise` (nested), `AsyncIterator` (nested), `Function` (returned)
|
|
161
|
+
|
|
162
|
+
**Unsupported**: Custom class instances, `Symbol`, circular references
|
|
163
|
+
|
|
164
|
+
See the [full documentation](#custom-functions) for advanced usage examples including nested promises and returned functions.
|
|
165
|
+
|
|
166
|
+
## File System Callbacks
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const runtime = await client.createRuntime({
|
|
170
|
+
fs: {
|
|
171
|
+
readFile: async (path) => Bun.file(path).arrayBuffer(),
|
|
172
|
+
writeFile: async (path, data) => Bun.write(path, data),
|
|
173
|
+
stat: async (path) => {
|
|
174
|
+
const stat = await Bun.file(path).stat();
|
|
175
|
+
return { isFile: true, isDirectory: false, size: stat.size };
|
|
176
|
+
},
|
|
177
|
+
readdir: async (path) => {
|
|
178
|
+
const entries = [];
|
|
179
|
+
for await (const entry of new Bun.Glob("*").scan({ cwd: path })) {
|
|
180
|
+
entries.push(entry);
|
|
181
|
+
}
|
|
182
|
+
return entries;
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Test Environment
|
|
189
|
+
|
|
190
|
+
Enable test environment to run tests inside the sandbox:
|
|
69
191
|
|
|
70
192
|
```typescript
|
|
71
|
-
|
|
72
|
-
|
|
193
|
+
const runtime = await client.createRuntime({
|
|
194
|
+
testEnvironment: {
|
|
195
|
+
onEvent: (event) => {
|
|
196
|
+
// Receive lifecycle events during test execution
|
|
197
|
+
if (event.type === "testEnd") {
|
|
198
|
+
const icon = event.test.status === "pass" ? "✓" : "✗";
|
|
199
|
+
console.log(`${icon} ${event.test.fullName}`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
73
204
|
|
|
74
|
-
// Define tests
|
|
75
205
|
await runtime.eval(`
|
|
76
206
|
describe("math", () => {
|
|
77
207
|
it("adds numbers", () => {
|
|
78
208
|
expect(1 + 1).toBe(2);
|
|
79
209
|
});
|
|
210
|
+
it.todo("subtract numbers");
|
|
80
211
|
});
|
|
81
212
|
`);
|
|
82
213
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
console.log(
|
|
214
|
+
// Check if tests exist before running
|
|
215
|
+
if (await runtime.testEnvironment.hasTests()) {
|
|
216
|
+
console.log(`Found ${await runtime.testEnvironment.getTestCount()} tests`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const results = await runtime.testEnvironment.runTests();
|
|
220
|
+
console.log(`${results.passed}/${results.total} passed, ${results.todo} todo`);
|
|
221
|
+
|
|
222
|
+
// Reset for new tests
|
|
223
|
+
await runtime.testEnvironment.reset();
|
|
86
224
|
```
|
|
87
225
|
|
|
88
|
-
|
|
226
|
+
### TestEnvironmentOptions
|
|
89
227
|
|
|
90
228
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
229
|
+
interface TestEnvironmentOptions {
|
|
230
|
+
onEvent?: (event: TestEvent) => void;
|
|
231
|
+
testTimeout?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type TestEvent =
|
|
235
|
+
| { type: "runStart"; testCount: number; suiteCount: number }
|
|
236
|
+
| { type: "suiteStart"; suite: SuiteInfo }
|
|
237
|
+
| { type: "suiteEnd"; suite: SuiteResult }
|
|
238
|
+
| { type: "testStart"; test: TestInfo }
|
|
239
|
+
| { type: "testEnd"; test: TestResult }
|
|
240
|
+
| { type: "runEnd"; results: RunResults };
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Playwright Integration
|
|
244
|
+
|
|
245
|
+
Run browser automation with untrusted code. **The client owns the browser** - you provide the Playwright page object:
|
|
246
|
+
|
|
247
|
+
### Script Mode (No Tests)
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { chromium } from "playwright";
|
|
251
|
+
|
|
252
|
+
const browser = await chromium.launch({ headless: true });
|
|
253
|
+
const page = await browser.newPage();
|
|
254
|
+
|
|
255
|
+
const runtime = await client.createRuntime({
|
|
256
|
+
playwright: {
|
|
257
|
+
page,
|
|
258
|
+
baseUrl: "https://example.com",
|
|
259
|
+
onEvent: (event) => {
|
|
260
|
+
// Unified event handler for all playwright events
|
|
261
|
+
if (event.type === "browserConsoleLog") {
|
|
262
|
+
console.log(`[browser:${event.level}]`, ...event.args);
|
|
263
|
+
} else if (event.type === "networkRequest") {
|
|
264
|
+
console.log(`[request] ${event.method} ${event.url}`);
|
|
265
|
+
} else if (event.type === "networkResponse") {
|
|
266
|
+
console.log(`[response] ${event.status} ${event.url}`);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
},
|
|
96
270
|
});
|
|
97
271
|
|
|
98
|
-
//
|
|
272
|
+
// Run automation script - no test framework needed
|
|
99
273
|
await runtime.eval(`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
274
|
+
await page.goto("/");
|
|
275
|
+
const title = await page.title();
|
|
276
|
+
console.log("Page title:", title);
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
// Get collected data
|
|
280
|
+
const data = await runtime.playwright.getCollectedData();
|
|
281
|
+
console.log("Network requests:", data.networkRequests);
|
|
282
|
+
|
|
283
|
+
await runtime.dispose();
|
|
284
|
+
await browser.close();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Test Mode (With Test Framework)
|
|
288
|
+
|
|
289
|
+
Combine `testEnvironment` and `playwright` for browser testing. Playwright extends `expect` with locator matchers:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { chromium } from "playwright";
|
|
293
|
+
|
|
294
|
+
const browser = await chromium.launch({ headless: true });
|
|
295
|
+
const page = await browser.newPage();
|
|
296
|
+
|
|
297
|
+
const runtime = await client.createRuntime({
|
|
298
|
+
// Unified console handler for both sandbox and browser logs
|
|
299
|
+
console: {
|
|
300
|
+
onEntry: (entry) => {
|
|
301
|
+
if (entry.type === "output") {
|
|
302
|
+
console.log(`[sandbox:${entry.level}]`, ...entry.args);
|
|
303
|
+
} else if (entry.type === "browserOutput") {
|
|
304
|
+
console.log(`[browser:${entry.level}]`, ...entry.args);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
testEnvironment: true, // Provides describe, it, expect
|
|
309
|
+
playwright: {
|
|
310
|
+
page,
|
|
311
|
+
baseUrl: "https://example.com",
|
|
312
|
+
console: true, // Routes browser logs through the console handler above
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await runtime.eval(`
|
|
317
|
+
describe("homepage", () => {
|
|
318
|
+
it("loads correctly", async () => {
|
|
319
|
+
await page.goto("/");
|
|
320
|
+
await expect(page.getByText("Example Domain")).toBeVisible(); // Locator matcher
|
|
321
|
+
expect(await page.title()).toBe("Example Domain"); // Primitive matcher
|
|
322
|
+
});
|
|
103
323
|
});
|
|
104
324
|
`);
|
|
105
325
|
|
|
106
|
-
// Run tests
|
|
107
|
-
const results = await runtime.
|
|
326
|
+
// Run tests via test-environment
|
|
327
|
+
const results = await runtime.testEnvironment.runTests();
|
|
108
328
|
console.log(`${results.passed}/${results.total} passed`);
|
|
109
329
|
|
|
110
|
-
// Get
|
|
111
|
-
const data = await runtime.getCollectedData();
|
|
112
|
-
console.log("
|
|
113
|
-
|
|
114
|
-
|
|
330
|
+
// Get browser data
|
|
331
|
+
const data = await runtime.playwright.getCollectedData();
|
|
332
|
+
console.log("Browser logs:", data.browserConsoleLogs);
|
|
333
|
+
|
|
334
|
+
await runtime.dispose();
|
|
335
|
+
await browser.close();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Runtime Interface
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
interface RemoteRuntime {
|
|
342
|
+
readonly id: string;
|
|
343
|
+
eval(code: string, filename?: string): Promise<void>;
|
|
344
|
+
dispose(): Promise<void>;
|
|
345
|
+
|
|
346
|
+
// Module handles
|
|
347
|
+
readonly fetch: RemoteFetchHandle;
|
|
348
|
+
readonly timers: RemoteTimersHandle;
|
|
349
|
+
readonly console: RemoteConsoleHandle;
|
|
350
|
+
readonly testEnvironment: RemoteTestEnvironmentHandle;
|
|
351
|
+
readonly playwright: RemotePlaywrightHandle;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface RemoteFetchHandle {
|
|
355
|
+
dispatchRequest(request: Request, options?: DispatchOptions): Promise<Response>;
|
|
356
|
+
hasServeHandler(): Promise<boolean>;
|
|
357
|
+
hasActiveConnections(): Promise<boolean>;
|
|
358
|
+
getUpgradeRequest(): Promise<UpgradeRequest | null>;
|
|
359
|
+
// WebSocket methods...
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
interface RemoteTimersHandle {
|
|
363
|
+
clearAll(): Promise<void>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
interface RemoteConsoleHandle {
|
|
367
|
+
reset(): Promise<void>;
|
|
368
|
+
getTimers(): Promise<Map<string, number>>;
|
|
369
|
+
getCounters(): Promise<Map<string, number>>;
|
|
370
|
+
getGroupDepth(): Promise<number>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface RemoteTestEnvironmentHandle {
|
|
374
|
+
runTests(timeout?: number): Promise<RunResults>;
|
|
375
|
+
hasTests(): Promise<boolean>;
|
|
376
|
+
getTestCount(): Promise<number>;
|
|
377
|
+
reset(): Promise<void>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface RemotePlaywrightHandle {
|
|
381
|
+
getCollectedData(): Promise<CollectedData>;
|
|
382
|
+
clearCollectedData(): Promise<void>;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT
|