@ricsam/isolate-client 0.1.1 → 0.1.4

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 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
- **Features:**
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
- **Basic Usage:**
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
- memoryLimit: 128,
35
+ memoryLimitMB: 128,
31
36
  console: {
32
- log: (...args) => console.log("[isolate]", ...args),
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
- const response = await runtime.dispatchRequest(
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
- **Test Environment:**
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
- // Setup test framework in isolate
72
- await runtime.setupTestEnvironment();
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
- // Run tests
84
- const results = await runtime.runTests();
85
- console.log(`${results.passed}/${results.total} passed`);
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
- **Playwright Integration:**
226
+ ### TestEnvironmentOptions
89
227
 
90
228
  ```typescript
91
- // Setup Playwright (daemon launches browser)
92
- await runtime.setupPlaywright({
93
- browserType: "chromium",
94
- headless: true,
95
- onConsoleLog: (log) => console.log("[browser]", log),
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
- // Define browser tests
272
+ // Run automation script - no test framework needed
99
273
  await runtime.eval(`
100
- test("homepage loads", async () => {
101
- await page.goto("https://example.com");
102
- await expect(page.getByText("Example Domain")).toBeVisible();
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.runPlaywrightTests();
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 collected data
111
- const data = await runtime.getCollectedData();
112
- console.log("Console logs:", data.consoleLogs);
113
- console.log("Network requests:", data.networkRequests);
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