@ricsam/isolate-fetch 0.1.3 → 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 CHANGED
@@ -1,6 +1,14 @@
1
1
  # @ricsam/isolate-fetch
2
2
 
3
- Fetch API and HTTP server handler.
3
+ Fetch API and HTTP server handler for isolated-vm V8 sandbox.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @ricsam/isolate-fetch
9
+ ```
10
+
11
+ ## Usage
4
12
 
5
13
  ```typescript
6
14
  import { setupFetch } from "@ricsam/isolate-fetch";
@@ -14,12 +22,13 @@ const handle = await setupFetch(context, {
14
22
  });
15
23
  ```
16
24
 
17
- **Injected Globals:**
25
+ ## Injected Globals
26
+
18
27
  - `fetch`, `Request`, `Response`, `Headers`
19
28
  - `FormData`, `AbortController`, `AbortSignal`
20
29
  - `serve` (HTTP server handler)
21
30
 
22
- **Usage in Isolate:**
31
+ ## Usage in Isolate
23
32
 
24
33
  ```javascript
25
34
  // Outbound fetch
@@ -53,30 +62,262 @@ formData.append("name", "John");
53
62
  formData.append("file", new File(["content"], "file.txt"));
54
63
  ```
55
64
 
56
- #### HTTP Server
65
+ ## HTTP Server (`serve`)
66
+
67
+ The `serve()` function registers a request handler in the isolate that can receive HTTP requests dispatched from the host. It uses a Bun-compatible API.
68
+
69
+ ### Basic Usage
70
+
71
+ ```javascript
72
+ // In isolate code
73
+ serve({
74
+ fetch(request, server) {
75
+ const url = new URL(request.url);
76
+ return Response.json({ path: url.pathname, method: request.method });
77
+ }
78
+ });
79
+ ```
80
+
81
+ ### The `fetch` Handler
82
+
83
+ The `fetch` handler receives two arguments:
57
84
 
58
- Register a server handler in the isolate and dispatch requests from the host:
85
+ - `request` - A standard `Request` object
86
+ - `server` - A server object with WebSocket upgrade capability
87
+
88
+ ```javascript
89
+ serve({
90
+ fetch(request, server) {
91
+ // Access request properties
92
+ const url = new URL(request.url);
93
+ const method = request.method;
94
+ const headers = request.headers;
95
+ const body = await request.json(); // for POST/PUT
96
+
97
+ // Return a Response
98
+ return new Response("Hello World");
99
+ }
100
+ });
101
+ ```
102
+
103
+ ### The `server` Object
104
+
105
+ The `server` argument provides WebSocket upgrade functionality:
106
+
107
+ ```javascript
108
+ serve({
109
+ fetch(request, server) {
110
+ // Check for WebSocket upgrade request
111
+ if (request.headers.get("Upgrade") === "websocket") {
112
+ // Upgrade the connection, optionally passing data
113
+ server.upgrade(request, { data: { userId: "123" } });
114
+ return new Response(null, { status: 101 });
115
+ }
116
+
117
+ return new Response("Not a WebSocket request", { status: 400 });
118
+ }
119
+ });
120
+ ```
121
+
122
+ ## WebSocket Support
123
+
124
+ The `serve()` function supports WebSocket connections through a `websocket` handler object.
125
+
126
+ ### WebSocket Handlers
127
+
128
+ ```javascript
129
+ serve({
130
+ fetch(request, server) {
131
+ if (request.headers.get("Upgrade") === "websocket") {
132
+ server.upgrade(request, { data: { userId: "123" } });
133
+ return new Response(null, { status: 101 });
134
+ }
135
+ return new Response("OK");
136
+ },
137
+ websocket: {
138
+ open(ws) {
139
+ // Called when a WebSocket connection is opened
140
+ console.log("Connected:", ws.data.userId);
141
+ ws.send("Welcome!");
142
+ },
143
+ message(ws, message) {
144
+ // Called when a message is received from the client
145
+ console.log("Received:", message);
146
+ ws.send("Echo: " + message);
147
+ },
148
+ close(ws, code, reason) {
149
+ // Called when the connection is closed
150
+ console.log("Closed:", code, reason);
151
+ },
152
+ error(ws, error) {
153
+ // Called when an error occurs
154
+ console.error("Error:", error);
155
+ }
156
+ }
157
+ });
158
+ ```
159
+
160
+ ### The `ws` Object
161
+
162
+ Each WebSocket handler receives a `ws` object with the following properties and methods:
163
+
164
+ | Property/Method | Description |
165
+ |-----------------|-------------|
166
+ | `ws.data` | Custom data passed during `server.upgrade()` |
167
+ | `ws.send(message)` | Send a message to the client (string or ArrayBuffer) |
168
+ | `ws.close(code?, reason?)` | Close the connection with optional code and reason |
169
+ | `ws.readyState` | Current state: 1 (OPEN), 2 (CLOSING), 3 (CLOSED) |
170
+
171
+ ### Optional Handlers
172
+
173
+ All WebSocket handlers are optional. You can define only the handlers you need:
174
+
175
+ ```javascript
176
+ // Only handle messages - no open/close/error handlers needed
177
+ serve({
178
+ fetch(request, server) { /* ... */ },
179
+ websocket: {
180
+ message(ws, message) {
181
+ ws.send("Echo: " + message);
182
+ }
183
+ }
184
+ });
185
+
186
+ // Only handle open and close
187
+ serve({
188
+ fetch(request, server) { /* ... */ },
189
+ websocket: {
190
+ open(ws) {
191
+ console.log("Connected");
192
+ },
193
+ close(ws, code, reason) {
194
+ console.log("Disconnected");
195
+ }
196
+ }
197
+ });
198
+ ```
199
+
200
+ ## Host-Side API
201
+
202
+ The host dispatches requests and WebSocket events to the isolate.
203
+
204
+ ### Dispatching HTTP Requests
205
+
206
+ ```typescript
207
+ // From host code
208
+ const response = await handle.dispatchRequest(
209
+ new Request("http://localhost/api/users", {
210
+ method: "POST",
211
+ body: JSON.stringify({ name: "Alice" }),
212
+ })
213
+ );
214
+
215
+ console.log(await response.json());
216
+ ```
217
+
218
+ ### WebSocket Flow
219
+
220
+ The host manages WebSocket connections and dispatches events to the isolate:
221
+
222
+ ```typescript
223
+ // 1. Dispatch the upgrade request
224
+ await handle.dispatchRequest(
225
+ new Request("http://localhost/ws", {
226
+ headers: { "Upgrade": "websocket" }
227
+ })
228
+ );
229
+
230
+ // 2. Check if isolate requested an upgrade
231
+ const upgradeRequest = handle.getUpgradeRequest();
232
+ if (upgradeRequest?.requested) {
233
+ const connectionId = upgradeRequest.connectionId;
234
+
235
+ // 3. Register callback for commands FROM the isolate
236
+ handle.onWebSocketCommand((cmd) => {
237
+ if (cmd.type === "message") {
238
+ // Isolate called ws.send() - forward to real WebSocket
239
+ realWebSocket.send(cmd.data);
240
+ } else if (cmd.type === "close") {
241
+ // Isolate called ws.close()
242
+ realWebSocket.close(cmd.code, cmd.reason);
243
+ }
244
+ });
245
+
246
+ // 4. Notify isolate the connection is open (triggers websocket.open)
247
+ handle.dispatchWebSocketOpen(connectionId);
248
+
249
+ // 5. Forward messages TO the isolate (triggers websocket.message)
250
+ realWebSocket.onmessage = (event) => {
251
+ handle.dispatchWebSocketMessage(connectionId, event.data);
252
+ };
253
+
254
+ // 6. Forward close events TO the isolate (triggers websocket.close)
255
+ realWebSocket.onclose = (event) => {
256
+ handle.dispatchWebSocketClose(connectionId, event.code, event.reason);
257
+ };
258
+ }
259
+ ```
260
+
261
+ ### Host API Reference
262
+
263
+ | Method | Description |
264
+ |--------|-------------|
265
+ | `dispatchRequest(request)` | Dispatch HTTP request to isolate's `serve()` handler |
266
+ | `hasServeHandler()` | Check if `serve()` has been called in isolate |
267
+ | `hasActiveConnections()` | Check if there are active WebSocket connections |
268
+ | `getUpgradeRequest()` | Get pending WebSocket upgrade request info |
269
+ | `dispatchWebSocketOpen(id)` | Notify isolate that WebSocket connection opened |
270
+ | `dispatchWebSocketMessage(id, data)` | Send message to isolate's `websocket.message` handler |
271
+ | `dispatchWebSocketClose(id, code, reason)` | Notify isolate that connection closed |
272
+ | `dispatchWebSocketError(id, error)` | Notify isolate of WebSocket error |
273
+ | `onWebSocketCommand(callback)` | Register callback for `ws.send()`/`ws.close()` from isolate |
274
+
275
+ ### WebSocket Command Types
276
+
277
+ Commands received via `onWebSocketCommand`:
59
278
 
60
279
  ```typescript
61
- // In isolate
280
+ interface WebSocketCommand {
281
+ type: "message" | "close";
282
+ connectionId: string;
283
+ data?: string | ArrayBuffer; // For "message" type
284
+ code?: number; // For "close" type
285
+ reason?: string; // For "close" type
286
+ }
287
+ ```
288
+
289
+ ## Complete Example
290
+
291
+ ```typescript
292
+ // Host code
293
+ import { setupFetch } from "@ricsam/isolate-fetch";
294
+
295
+ const handle = await setupFetch(context, {
296
+ onFetch: async (request) => fetch(request),
297
+ });
298
+
299
+ // Set up serve handler in isolate
62
300
  await context.eval(`
63
301
  serve({
64
302
  fetch(request, server) {
65
303
  const url = new URL(request.url);
66
304
 
67
- if (url.pathname === "/ws") {
68
- if (server.upgrade(request, { data: { userId: "123" } })) {
69
- return new Response(null, { status: 101 });
70
- }
305
+ // WebSocket upgrade
306
+ if (url.pathname === "/ws" && request.headers.get("Upgrade") === "websocket") {
307
+ server.upgrade(request, { data: { path: url.pathname } });
308
+ return new Response(null, { status: 101 });
71
309
  }
72
310
 
311
+ // Regular HTTP
73
312
  return Response.json({ path: url.pathname });
74
313
  },
75
314
  websocket: {
76
315
  open(ws) {
77
- console.log("Connected:", ws.data.userId);
316
+ console.log("WebSocket connected to:", ws.data.path);
317
+ ws.send("Connected!");
78
318
  },
79
319
  message(ws, message) {
320
+ console.log("Received:", message);
80
321
  ws.send("Echo: " + message);
81
322
  },
82
323
  close(ws, code, reason) {
@@ -86,8 +327,32 @@ await context.eval(`
86
327
  });
87
328
  `, { promise: true });
88
329
 
89
- // From host - dispatch HTTP request
330
+ // Dispatch HTTP request
90
331
  const response = await handle.dispatchRequest(
91
332
  new Request("http://localhost/api/users")
92
333
  );
93
- ```
334
+ console.log(await response.json()); // { path: "/api/users" }
335
+
336
+ // Handle WebSocket connection
337
+ await handle.dispatchRequest(
338
+ new Request("http://localhost/ws", { headers: { "Upgrade": "websocket" } })
339
+ );
340
+
341
+ const upgrade = handle.getUpgradeRequest();
342
+ if (upgrade?.requested) {
343
+ // Listen for commands from isolate
344
+ handle.onWebSocketCommand((cmd) => {
345
+ console.log("Command from isolate:", cmd);
346
+ });
347
+
348
+ // Open connection (triggers websocket.open)
349
+ handle.dispatchWebSocketOpen(upgrade.connectionId);
350
+
351
+ // Send message (triggers websocket.message)
352
+ handle.dispatchWebSocketMessage(upgrade.connectionId, "Hello!");
353
+ }
354
+ ```
355
+
356
+ ## License
357
+
358
+ MIT
@@ -1568,8 +1568,7 @@ async function setupFetch(context, options) {
1568
1568
  serveState.activeConnections.clear();
1569
1569
  serveState.pendingUpgrade = null;
1570
1570
  },
1571
- async dispatchRequest(request, dispatchOptions) {
1572
- const tick = dispatchOptions?.tick;
1571
+ async dispatchRequest(request, _dispatchOptions) {
1573
1572
  if (serveState.pendingUpgrade) {
1574
1573
  const oldConnectionId = serveState.pendingUpgrade.connectionId;
1575
1574
  context.evalSync(`globalThis.__upgradeRegistry__.delete("${oldConnectionId}")`);
@@ -1623,9 +1622,6 @@ async function setupFetch(context, options) {
1623
1622
  if (streamDone)
1624
1623
  return;
1625
1624
  while (!streamDone) {
1626
- if (tick) {
1627
- await tick();
1628
- }
1629
1625
  const state = streamRegistry.get(responseStreamId);
1630
1626
  if (!state) {
1631
1627
  controller.close();
@@ -1692,19 +1688,22 @@ async function setupFetch(context, options) {
1692
1688
  return result;
1693
1689
  },
1694
1690
  dispatchWebSocketOpen(connectionId) {
1695
- const hasOpenHandler = context.evalSync(`!!globalThis.__serveOptions__?.websocket?.open`);
1696
- if (!hasOpenHandler) {
1697
- context.evalSync(`globalThis.__upgradeRegistry__.delete("${connectionId}")`);
1698
- return;
1699
- }
1700
1691
  serveState.activeConnections.set(connectionId, { connectionId });
1692
+ const hasOpenHandler = context.evalSync(`!!globalThis.__serveOptions__?.websocket?.open`);
1701
1693
  context.evalSync(`
1702
1694
  (function() {
1703
1695
  const ws = new __ServerWebSocket__("${connectionId}");
1704
1696
  globalThis.__activeWs_${connectionId}__ = ws;
1705
- __serveOptions__.websocket.open(ws);
1706
1697
  })()
1707
1698
  `);
1699
+ if (hasOpenHandler) {
1700
+ context.evalSync(`
1701
+ (function() {
1702
+ const ws = globalThis.__activeWs_${connectionId}__;
1703
+ __serveOptions__.websocket.open(ws);
1704
+ })()
1705
+ `);
1706
+ }
1708
1707
  if (serveState.pendingUpgrade?.connectionId === connectionId) {
1709
1708
  serveState.pendingUpgrade = null;
1710
1709
  }
@@ -1797,4 +1796,4 @@ async function setupFetch(context, options) {
1797
1796
  }
1798
1797
  })
1799
1798
 
1800
- //# debugId=D6B21F115A1DCB5264756E2164756E21
1799
+ //# debugId=721F87E22A9905C564756E2164756E21