@ricsam/isolate-fetch 0.1.4 → 0.1.7
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 +278 -13
- package/dist/cjs/index.cjs +26 -17
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +26 -17
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +0 -2
- package/package.json +1 -1
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
|
-
|
|
25
|
+
## Injected Globals
|
|
26
|
+
|
|
18
27
|
- `fetch`, `Request`, `Response`, `Headers`
|
|
19
28
|
- `FormData`, `AbortController`, `AbortSignal`
|
|
20
29
|
- `serve` (HTTP server handler)
|
|
21
30
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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("
|
|
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
|
-
//
|
|
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
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1253,6 +1253,12 @@ function setupRequest(context, stateMap) {
|
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
1255
|
get body() {
|
|
1256
|
+
// Per WHATWG Fetch spec: GET/HEAD requests cannot have a body
|
|
1257
|
+
const method = __Request_get_method(this.#instanceId);
|
|
1258
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1256
1262
|
// Return cached body if available
|
|
1257
1263
|
if (this.#cachedBody !== null) {
|
|
1258
1264
|
return this.#cachedBody;
|
|
@@ -1264,12 +1270,15 @@ function setupRequest(context, stateMap) {
|
|
|
1264
1270
|
return this.#cachedBody;
|
|
1265
1271
|
}
|
|
1266
1272
|
|
|
1267
|
-
//
|
|
1268
|
-
const newStreamId = __Stream_create();
|
|
1273
|
+
// Check if there's any buffered body data
|
|
1269
1274
|
const buffer = __Request_arrayBuffer(this.#instanceId);
|
|
1270
|
-
if (buffer.byteLength
|
|
1271
|
-
|
|
1275
|
+
if (buffer.byteLength === 0) {
|
|
1276
|
+
return null; // Return null per WHATWG Fetch spec for empty body
|
|
1272
1277
|
}
|
|
1278
|
+
|
|
1279
|
+
// Create stream from non-empty buffered body
|
|
1280
|
+
const newStreamId = __Stream_create();
|
|
1281
|
+
__Stream_push(newStreamId, Array.from(new Uint8Array(buffer)));
|
|
1273
1282
|
__Stream_close(newStreamId);
|
|
1274
1283
|
|
|
1275
1284
|
this.#cachedBody = HostBackedReadableStream._fromStreamId(newStreamId);
|
|
@@ -1568,8 +1577,7 @@ async function setupFetch(context, options) {
|
|
|
1568
1577
|
serveState.activeConnections.clear();
|
|
1569
1578
|
serveState.pendingUpgrade = null;
|
|
1570
1579
|
},
|
|
1571
|
-
async dispatchRequest(request,
|
|
1572
|
-
const tick = dispatchOptions?.tick;
|
|
1580
|
+
async dispatchRequest(request, _dispatchOptions) {
|
|
1573
1581
|
if (serveState.pendingUpgrade) {
|
|
1574
1582
|
const oldConnectionId = serveState.pendingUpgrade.connectionId;
|
|
1575
1583
|
context.evalSync(`globalThis.__upgradeRegistry__.delete("${oldConnectionId}")`);
|
|
@@ -1581,7 +1589,8 @@ async function setupFetch(context, options) {
|
|
|
1581
1589
|
}
|
|
1582
1590
|
let requestStreamId = null;
|
|
1583
1591
|
let streamCleanup = null;
|
|
1584
|
-
|
|
1592
|
+
const canHaveBody = !["GET", "HEAD"].includes(request.method.toUpperCase());
|
|
1593
|
+
if (canHaveBody && request.body) {
|
|
1585
1594
|
requestStreamId = streamRegistry.create();
|
|
1586
1595
|
streamCleanup = import_stream_state.startNativeStreamReader(request.body, requestStreamId, streamRegistry);
|
|
1587
1596
|
}
|
|
@@ -1623,9 +1632,6 @@ async function setupFetch(context, options) {
|
|
|
1623
1632
|
if (streamDone)
|
|
1624
1633
|
return;
|
|
1625
1634
|
while (!streamDone) {
|
|
1626
|
-
if (tick) {
|
|
1627
|
-
await tick();
|
|
1628
|
-
}
|
|
1629
1635
|
const state = streamRegistry.get(responseStreamId);
|
|
1630
1636
|
if (!state) {
|
|
1631
1637
|
controller.close();
|
|
@@ -1692,19 +1698,22 @@ async function setupFetch(context, options) {
|
|
|
1692
1698
|
return result;
|
|
1693
1699
|
},
|
|
1694
1700
|
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
1701
|
serveState.activeConnections.set(connectionId, { connectionId });
|
|
1702
|
+
const hasOpenHandler = context.evalSync(`!!globalThis.__serveOptions__?.websocket?.open`);
|
|
1701
1703
|
context.evalSync(`
|
|
1702
1704
|
(function() {
|
|
1703
1705
|
const ws = new __ServerWebSocket__("${connectionId}");
|
|
1704
1706
|
globalThis.__activeWs_${connectionId}__ = ws;
|
|
1705
|
-
__serveOptions__.websocket.open(ws);
|
|
1706
1707
|
})()
|
|
1707
1708
|
`);
|
|
1709
|
+
if (hasOpenHandler) {
|
|
1710
|
+
context.evalSync(`
|
|
1711
|
+
(function() {
|
|
1712
|
+
const ws = globalThis.__activeWs_${connectionId}__;
|
|
1713
|
+
__serveOptions__.websocket.open(ws);
|
|
1714
|
+
})()
|
|
1715
|
+
`);
|
|
1716
|
+
}
|
|
1708
1717
|
if (serveState.pendingUpgrade?.connectionId === connectionId) {
|
|
1709
1718
|
serveState.pendingUpgrade = null;
|
|
1710
1719
|
}
|
|
@@ -1797,4 +1806,4 @@ async function setupFetch(context, options) {
|
|
|
1797
1806
|
}
|
|
1798
1807
|
})
|
|
1799
1808
|
|
|
1800
|
-
//# debugId=
|
|
1809
|
+
//# debugId=1924AA5BC28D2CB964756E2164756E21
|