@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 +278 -13
- package/dist/cjs/index.cjs +11 -12
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +11 -12
- 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
|
@@ -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,
|
|
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=
|
|
1799
|
+
//# debugId=721F87E22A9905C564756E2164756E21
|