@ricsam/isolate-fetch 0.1.1 → 0.1.3

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.
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Global Type Definitions for @ricsam/isolate-fetch
3
+ *
4
+ * These types define the globals injected by setupFetch() into an isolated-vm context.
5
+ * Use these types to typecheck user code that will run inside the V8 isolate.
6
+ *
7
+ * @example
8
+ * // Typecheck isolate code with serve()
9
+ * type WebSocketData = { id: number; connectedAt: number };
10
+ *
11
+ * serve({
12
+ * fetch(request, server) {
13
+ * if (request.url.includes("/ws")) {
14
+ * // server.upgrade knows data should be WebSocketData
15
+ * server.upgrade(request, { data: { id: 123, connectedAt: Date.now() } });
16
+ * return new Response(null, { status: 101 });
17
+ * }
18
+ * return new Response("Hello!");
19
+ * },
20
+ * websocket: {
21
+ * // Type hint - specifies the type of ws.data
22
+ * data: {} as WebSocketData,
23
+ * message(ws, message) {
24
+ * // ws.data is typed as WebSocketData
25
+ * console.log("User", ws.data.id, "says:", message);
26
+ * ws.send("Echo: " + message);
27
+ * }
28
+ * }
29
+ * });
30
+ */
31
+
32
+ export {};
33
+
34
+ declare global {
35
+ // ============================================
36
+ // Standard Fetch API (from lib.dom)
37
+ // ============================================
38
+
39
+ /**
40
+ * Headers class for HTTP headers manipulation.
41
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
42
+ */
43
+ const Headers: typeof globalThis.Headers;
44
+
45
+ /**
46
+ * Request class for HTTP requests.
47
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Request
48
+ */
49
+ const Request: typeof globalThis.Request;
50
+
51
+ /**
52
+ * Response class for HTTP responses.
53
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Response
54
+ */
55
+ const Response: typeof globalThis.Response;
56
+
57
+ /**
58
+ * AbortController for cancelling fetch requests.
59
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController
60
+ */
61
+ const AbortController: typeof globalThis.AbortController;
62
+
63
+ /**
64
+ * AbortSignal for listening to abort events.
65
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
66
+ */
67
+ const AbortSignal: typeof globalThis.AbortSignal;
68
+
69
+ /**
70
+ * FormData for constructing form data.
71
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData
72
+ */
73
+ const FormData: typeof globalThis.FormData;
74
+
75
+ /**
76
+ * Fetch function for making HTTP requests.
77
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
78
+ */
79
+ function fetch(
80
+ input: RequestInfo | URL,
81
+ init?: RequestInit
82
+ ): Promise<Response>;
83
+
84
+ // ============================================
85
+ // Isolate-specific: serve() API
86
+ // ============================================
87
+
88
+ /**
89
+ * Server interface for handling WebSocket upgrades within serve() handlers.
90
+ *
91
+ * @typeParam T - The type of data associated with WebSocket connections
92
+ */
93
+ interface Server<T = unknown> {
94
+ /**
95
+ * Upgrade an HTTP request to a WebSocket connection.
96
+ *
97
+ * @param request - The incoming HTTP request to upgrade
98
+ * @param options - Optional data to associate with the WebSocket connection
99
+ * @returns true if upgrade will proceed, false otherwise
100
+ *
101
+ * @example
102
+ * serve({
103
+ * fetch(request, server) {
104
+ * if (server.upgrade(request, { data: { userId: 123 } })) {
105
+ * return new Response(null, { status: 101 });
106
+ * }
107
+ * return new Response("Upgrade failed", { status: 400 });
108
+ * }
109
+ * });
110
+ */
111
+ upgrade(request: Request, options?: { data?: T }): boolean;
112
+ }
113
+
114
+ /**
115
+ * ServerWebSocket interface for WebSocket connections within serve() handlers.
116
+ *
117
+ * @typeParam T - The type of data associated with this WebSocket connection
118
+ */
119
+ interface ServerWebSocket<T = unknown> {
120
+ /**
121
+ * User data associated with this connection.
122
+ * Set via `server.upgrade(request, { data: ... })`.
123
+ */
124
+ readonly data: T;
125
+
126
+ /**
127
+ * Send a message to the client.
128
+ *
129
+ * @param message - The message to send (string, ArrayBuffer, or Uint8Array)
130
+ */
131
+ send(message: string | ArrayBuffer | Uint8Array): void;
132
+
133
+ /**
134
+ * Close the WebSocket connection.
135
+ *
136
+ * @param code - Optional close code (default: 1000)
137
+ * @param reason - Optional close reason
138
+ */
139
+ close(code?: number, reason?: string): void;
140
+
141
+ /**
142
+ * WebSocket ready state.
143
+ * - 0: CONNECTING
144
+ * - 1: OPEN
145
+ * - 2: CLOSING
146
+ * - 3: CLOSED
147
+ */
148
+ readonly readyState: number;
149
+ }
150
+
151
+ /**
152
+ * Options for the serve() function.
153
+ *
154
+ * @typeParam T - The type of data associated with WebSocket connections
155
+ */
156
+ interface ServeOptions<T = unknown> {
157
+ /**
158
+ * Handler for HTTP requests.
159
+ *
160
+ * @param request - The incoming HTTP request
161
+ * @param server - Server interface for WebSocket upgrades
162
+ * @returns Response or Promise resolving to Response
163
+ */
164
+ fetch(request: Request, server: Server<T>): Response | Promise<Response>;
165
+
166
+ /**
167
+ * WebSocket event handlers.
168
+ */
169
+ websocket?: {
170
+ /**
171
+ * Type hint for WebSocket data. The value is not used at runtime.
172
+ * Specifies the type of `ws.data` for all handlers and `server.upgrade()`.
173
+ *
174
+ * @example
175
+ * websocket: {
176
+ * data: {} as { userId: string },
177
+ * message(ws, message) {
178
+ * // ws.data.userId is typed as string
179
+ * }
180
+ * }
181
+ */
182
+ data?: T;
183
+
184
+ /**
185
+ * Called when a WebSocket connection is opened.
186
+ *
187
+ * @param ws - The WebSocket connection
188
+ */
189
+ open?(ws: ServerWebSocket<T>): void | Promise<void>;
190
+
191
+ /**
192
+ * Called when a message is received.
193
+ *
194
+ * @param ws - The WebSocket connection
195
+ * @param message - The received message (string or ArrayBuffer)
196
+ */
197
+ message?(
198
+ ws: ServerWebSocket<T>,
199
+ message: string | ArrayBuffer
200
+ ): void | Promise<void>;
201
+
202
+ /**
203
+ * Called when the connection is closed.
204
+ *
205
+ * @param ws - The WebSocket connection
206
+ * @param code - The close code
207
+ * @param reason - The close reason
208
+ */
209
+ close?(
210
+ ws: ServerWebSocket<T>,
211
+ code: number,
212
+ reason: string
213
+ ): void | Promise<void>;
214
+
215
+ /**
216
+ * Called when an error occurs.
217
+ *
218
+ * @param ws - The WebSocket connection
219
+ * @param error - The error that occurred
220
+ */
221
+ error?(ws: ServerWebSocket<T>, error: Error): void | Promise<void>;
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Register an HTTP server handler in the isolate.
227
+ *
228
+ * Only one serve() handler can be active at a time.
229
+ * Calling serve() again replaces the previous handler.
230
+ *
231
+ * @param options - Server configuration including fetch handler and optional WebSocket handlers
232
+ *
233
+ * @example
234
+ * type WsData = { connectedAt: number };
235
+ *
236
+ * serve({
237
+ * fetch(request, server) {
238
+ * const url = new URL(request.url);
239
+ *
240
+ * if (url.pathname === "/ws") {
241
+ * if (server.upgrade(request, { data: { connectedAt: Date.now() } })) {
242
+ * return new Response(null, { status: 101 });
243
+ * }
244
+ * }
245
+ *
246
+ * if (url.pathname === "/api/hello") {
247
+ * return Response.json({ message: "Hello!" });
248
+ * }
249
+ *
250
+ * return new Response("Not Found", { status: 404 });
251
+ * },
252
+ * websocket: {
253
+ * data: {} as WsData,
254
+ * open(ws) {
255
+ * console.log("Connected at:", ws.data.connectedAt);
256
+ * },
257
+ * message(ws, message) {
258
+ * ws.send("Echo: " + message);
259
+ * },
260
+ * close(ws, code, reason) {
261
+ * console.log("Closed:", code, reason);
262
+ * }
263
+ * }
264
+ * });
265
+ */
266
+ function serve<T = unknown>(options: ServeOptions<T>): void;
267
+ }
@@ -0,0 +1,61 @@
1
+ import ivm from "isolated-vm";
2
+ export interface StreamState {
3
+ /** Buffered chunks waiting to be read */
4
+ queue: Uint8Array[];
5
+ /** Total bytes in queue (for backpressure) */
6
+ queueSize: number;
7
+ /** Stream has been closed (no more data) */
8
+ closed: boolean;
9
+ /** Stream encountered an error */
10
+ errored: boolean;
11
+ /** The error value if errored */
12
+ errorValue: unknown;
13
+ /** A pull is waiting for data */
14
+ pullWaiting: boolean;
15
+ /** Resolve function for waiting pull */
16
+ pullResolve: ((chunk: Uint8Array | null) => void) | null;
17
+ /** Reject function for waiting pull */
18
+ pullReject: ((error: unknown) => void) | null;
19
+ }
20
+ export interface StreamStateRegistry {
21
+ /** Create a new stream and return its ID */
22
+ create(): number;
23
+ /** Get stream state by ID */
24
+ get(streamId: number): StreamState | undefined;
25
+ /** Push a chunk to the stream's queue */
26
+ push(streamId: number, chunk: Uint8Array): boolean;
27
+ /** Pull a chunk from the stream (returns Promise that resolves when data available) */
28
+ pull(streamId: number): Promise<{
29
+ value: Uint8Array;
30
+ done: false;
31
+ } | {
32
+ done: true;
33
+ }>;
34
+ /** Close the stream (no more data) */
35
+ close(streamId: number): void;
36
+ /** Error the stream */
37
+ error(streamId: number, errorValue: unknown): void;
38
+ /** Check if stream queue is above high-water mark */
39
+ isQueueFull(streamId: number): boolean;
40
+ /** Delete stream state (cleanup) */
41
+ delete(streamId: number): void;
42
+ /** Clear all streams (context cleanup) */
43
+ clear(): void;
44
+ }
45
+ /** Maximum bytes to buffer before backpressure kicks in */
46
+ export declare const HIGH_WATER_MARK: number;
47
+ /** Maximum number of chunks in queue */
48
+ export declare const MAX_QUEUE_CHUNKS = 16;
49
+ export declare function createStreamStateRegistry(): StreamStateRegistry;
50
+ export declare function getStreamRegistryForContext(context: ivm.Context): StreamStateRegistry;
51
+ export declare function clearStreamRegistryForContext(context: ivm.Context): void;
52
+ /**
53
+ * Start reading from a native ReadableStream and push to host queue.
54
+ * Respects backpressure by pausing when queue is full.
55
+ *
56
+ * @param nativeStream The native ReadableStream to read from
57
+ * @param streamId The stream ID in the registry
58
+ * @param registry The stream state registry
59
+ * @returns Async cleanup function to cancel the reader
60
+ */
61
+ export declare function startNativeStreamReader(nativeStream: ReadableStream<Uint8Array>, streamId: number, registry: StreamStateRegistry): () => Promise<void>;
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@ricsam/isolate-fetch",
3
- "version": "0.1.1",
4
- "type": "module",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
3
+ "version": "0.1.3",
4
+ "main": "./dist/cjs/index.cjs",
5
+ "types": "./dist/types/index.d.ts",
7
6
  "exports": {
8
7
  ".": {
9
- "import": "./src/index.ts",
10
- "types": "./src/index.ts"
8
+ "types": "./dist/types/index.d.ts",
9
+ "require": "./dist/cjs/index.cjs",
10
+ "import": "./dist/mjs/index.mjs"
11
+ },
12
+ "./isolate": {
13
+ "types": "./dist/types/isolate.d.ts"
11
14
  }
12
15
  },
13
16
  "scripts": {
@@ -19,12 +22,37 @@
19
22
  "@ricsam/isolate-core": "*",
20
23
  "isolated-vm": "^6"
21
24
  },
22
- "devDependencies": {
23
- "@ricsam/isolate-test-utils": "*",
24
- "@types/node": "^24",
25
- "typescript": "^5"
26
- },
27
25
  "peerDependencies": {
28
26
  "isolated-vm": "^6"
29
- }
30
- }
27
+ },
28
+ "author": "Richard Samuelsson",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/ricsam/isolate.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/ricsam/isolate/issues"
36
+ },
37
+ "homepage": "https://github.com/ricsam/isolate#readme",
38
+ "keywords": [
39
+ "isolated-vm",
40
+ "sandbox",
41
+ "javascript",
42
+ "runtime",
43
+ "fetch",
44
+ "filesystem",
45
+ "streams",
46
+ "v8",
47
+ "isolate"
48
+ ],
49
+ "description": "Fetch API implementation for isolated-vm V8 sandbox",
50
+ "module": "./dist/mjs/index.mjs",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "README.md"
57
+ ]
58
+ }
package/CHANGELOG.md DELETED
@@ -1,9 +0,0 @@
1
- # @ricsam/isolate-fetch
2
-
3
- ## 0.1.1
4
-
5
- ### Patch Changes
6
-
7
- - initial release
8
- - Updated dependencies
9
- - @ricsam/isolate-core@0.1.1
@@ -1,89 +0,0 @@
1
- import { test, describe, beforeEach, afterEach, it } from "node:test";
2
- import assert from "node:assert";
3
- import ivm from "isolated-vm";
4
- import {
5
- setupFetch,
6
- clearAllInstanceState,
7
- type FetchHandle,
8
- } from "./index.ts";
9
- import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
10
- import { setupConsole } from "@ricsam/isolate-console";
11
- import { clearStreamRegistryForContext } from "./stream-state.ts";
12
-
13
- describe("Debug Delayed Streaming", () => {
14
- it("delayed streaming response with setTimeout", { timeout: 10000 }, async () => {
15
- const isolate = new ivm.Isolate();
16
- const context = await isolate.createContext();
17
- clearAllInstanceState();
18
-
19
- const logs: string[] = [];
20
- await setupConsole(context, {
21
- onLog: (level, ...args) => {
22
- logs.push(`[${level}] ${args.join(' ')}`);
23
- console.log(`[ISOLATE] ${args.join(' ')}`);
24
- }
25
- });
26
-
27
- const timersHandle = await setupTimers(context);
28
- const fetchHandle = await setupFetch(context);
29
-
30
- try {
31
- context.evalSync(`
32
- console.log('Setting up serve');
33
- serve({
34
- async fetch(request) {
35
- console.log('Fetch handler called');
36
- let count = 0;
37
- const stream = new ReadableStream({
38
- async pull(controller) {
39
- console.log('pull() called, count =', count);
40
- if (count < 3) {
41
- console.log('Starting setTimeout');
42
- await new Promise(r => setTimeout(r, 10));
43
- console.log('setTimeout resolved');
44
- const data = "delayed" + count;
45
- console.log('Enqueuing:', data);
46
- controller.enqueue(new TextEncoder().encode(data));
47
- count++;
48
- console.log('Enqueued, returning');
49
- } else {
50
- console.log('Closing stream');
51
- controller.close();
52
- }
53
- }
54
- });
55
- console.log('Creating Response');
56
- const response = new Response(stream);
57
- console.log('Response created, returning');
58
- return response;
59
- }
60
- });
61
- console.log('Serve registered');
62
- `);
63
-
64
- console.log('Calling dispatchRequest');
65
- const response = await fetchHandle.dispatchRequest(
66
- new Request("http://test/"),
67
- {
68
- tick: async () => {
69
- // Advance virtual time by 50ms each tick to process timers
70
- await timersHandle.tick(50);
71
- }
72
- }
73
- );
74
-
75
- console.log('Got response, status:', response.status);
76
- console.log('Calling response.text()');
77
-
78
- const text = await response.text();
79
- console.log('Got text:', text);
80
- assert.strictEqual(text, "delayed0delayed1delayed2");
81
- } finally {
82
- fetchHandle.dispose();
83
- timersHandle.dispose();
84
- clearStreamRegistryForContext(context);
85
- context.release();
86
- isolate.dispose();
87
- }
88
- });
89
- });
@@ -1,81 +0,0 @@
1
- import { test, describe, beforeEach, afterEach, it } from "node:test";
2
- import assert from "node:assert";
3
- import ivm from "isolated-vm";
4
- import {
5
- setupFetch,
6
- clearAllInstanceState,
7
- type FetchHandle,
8
- } from "./index.ts";
9
- import { setupTimers, type TimersHandle } from "@ricsam/isolate-timers";
10
- import { setupConsole } from "@ricsam/isolate-console";
11
- import { clearStreamRegistryForContext } from "./stream-state.ts";
12
-
13
- describe("Debug Streaming", () => {
14
- it("debug pull-based stream", { timeout: 5000 }, async () => {
15
- const isolate = new ivm.Isolate();
16
- const context = await isolate.createContext();
17
- clearAllInstanceState();
18
-
19
- const logs: string[] = [];
20
- await setupConsole(context, {
21
- onLog: (level, ...args) => {
22
- logs.push(`[${level}] ${args.join(' ')}`);
23
- console.log(`[ISOLATE] ${args.join(' ')}`);
24
- }
25
- });
26
-
27
- const timersHandle = await setupTimers(context);
28
- const fetchHandle = await setupFetch(context);
29
-
30
- try {
31
- context.evalSync(`
32
- console.log('Setting up serve');
33
- serve({
34
- async fetch(request) {
35
- console.log('Fetch handler called');
36
- let count = 0;
37
- const stream = new ReadableStream({
38
- pull(controller) {
39
- console.log('pull() called, count =', count);
40
- if (count < 3) {
41
- const data = "chunk" + count;
42
- console.log('Enqueuing:', data);
43
- controller.enqueue(new TextEncoder().encode(data));
44
- count++;
45
- } else {
46
- console.log('Closing stream');
47
- controller.close();
48
- }
49
- }
50
- });
51
- console.log('Creating Response');
52
- const response = new Response(stream);
53
- console.log('Response created, returning');
54
- return response;
55
- }
56
- });
57
- console.log('Serve registered');
58
- `);
59
-
60
- console.log('Calling dispatchRequest');
61
- const response = await fetchHandle.dispatchRequest(
62
- new Request("http://test/"),
63
- { tick: () => timersHandle.tick() }
64
- );
65
-
66
- console.log('Got response, status:', response.status);
67
- console.log('Response body:', response.body);
68
- console.log('Calling response.text()');
69
-
70
- const text = await response.text();
71
- console.log('Got text:', text);
72
- assert.strictEqual(text, "chunk0chunk1chunk2");
73
- } finally {
74
- fetchHandle.dispose();
75
- timersHandle.dispose();
76
- clearStreamRegistryForContext(context);
77
- context.release();
78
- isolate.dispose();
79
- }
80
- });
81
- });