@ricsam/isolate-fetch 0.1.1 → 0.1.2

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.
@@ -1,363 +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 { clearStreamRegistryForContext } from "./stream-state.ts";
11
-
12
- describe("HostBackedReadableStream", () => {
13
- let isolate: ivm.Isolate;
14
- let context: ivm.Context;
15
- let fetchHandle: FetchHandle;
16
- let timersHandle: TimersHandle;
17
-
18
- beforeEach(async () => {
19
- isolate = new ivm.Isolate();
20
- context = await isolate.createContext();
21
- clearAllInstanceState();
22
- timersHandle = await setupTimers(context);
23
- fetchHandle = await setupFetch(context);
24
- });
25
-
26
- afterEach(async () => {
27
- fetchHandle.dispose();
28
- timersHandle.dispose();
29
- clearStreamRegistryForContext(context);
30
- context.release();
31
- isolate.dispose();
32
- });
33
-
34
- describe("creation", () => {
35
- it("creates a stream with a valid stream ID", () => {
36
- const result = context.evalSync(`
37
- const stream = new HostBackedReadableStream();
38
- const streamId = stream._getStreamId();
39
- JSON.stringify({
40
- hasStreamId: typeof streamId === "number",
41
- streamIdPositive: streamId > 0
42
- })
43
- `);
44
- const data = JSON.parse(result as string) as {
45
- hasStreamId: boolean;
46
- streamIdPositive: boolean;
47
- };
48
- assert.strictEqual(data.hasStreamId, true);
49
- assert.strictEqual(data.streamIdPositive, true);
50
- });
51
-
52
- it("each stream gets a unique ID", () => {
53
- const result = context.evalSync(`
54
- const stream1 = new HostBackedReadableStream();
55
- const stream2 = new HostBackedReadableStream();
56
- const stream3 = new HostBackedReadableStream();
57
- JSON.stringify({
58
- id1: stream1._getStreamId(),
59
- id2: stream2._getStreamId(),
60
- id3: stream3._getStreamId()
61
- })
62
- `);
63
- const data = JSON.parse(result as string) as {
64
- id1: number;
65
- id2: number;
66
- id3: number;
67
- };
68
- assert.notStrictEqual(data.id1, data.id2);
69
- assert.notStrictEqual(data.id2, data.id3);
70
- assert.notStrictEqual(data.id1, data.id3);
71
- });
72
- });
73
-
74
- describe("getReader", () => {
75
- it("returns a reader object", () => {
76
- const result = context.evalSync(`
77
- const stream = new HostBackedReadableStream();
78
- const reader = stream.getReader();
79
- JSON.stringify({
80
- hasReader: reader != null,
81
- hasReadMethod: typeof reader.read === "function",
82
- hasCancelMethod: typeof reader.cancel === "function",
83
- hasReleaseLockMethod: typeof reader.releaseLock === "function"
84
- })
85
- `);
86
- const data = JSON.parse(result as string) as {
87
- hasReader: boolean;
88
- hasReadMethod: boolean;
89
- hasCancelMethod: boolean;
90
- hasReleaseLockMethod: boolean;
91
- };
92
- assert.strictEqual(data.hasReader, true);
93
- assert.strictEqual(data.hasReadMethod, true);
94
- assert.strictEqual(data.hasCancelMethod, true);
95
- assert.strictEqual(data.hasReleaseLockMethod, true);
96
- });
97
-
98
- it("has locked property (always false in current implementation)", () => {
99
- const result = context.evalSync(`
100
- const stream = new HostBackedReadableStream();
101
- const before = stream.locked;
102
- const reader = stream.getReader();
103
- const after = stream.locked;
104
- JSON.stringify({ before, after, hasLocked: "locked" in stream })
105
- `);
106
- const data = JSON.parse(result as string) as {
107
- before: boolean;
108
- after: boolean;
109
- hasLocked: boolean;
110
- };
111
- assert.strictEqual(data.hasLocked, true);
112
- // Note: Current implementation always returns false for locked
113
- assert.strictEqual(data.before, false);
114
- assert.strictEqual(data.after, false);
115
- });
116
-
117
- it("allows getting multiple readers (current implementation)", () => {
118
- // Note: This differs from WHATWG spec but matches current implementation
119
- const result = context.evalSync(`
120
- const stream = new HostBackedReadableStream();
121
- const reader1 = stream.getReader();
122
- const reader2 = stream.getReader();
123
- JSON.stringify({
124
- hasReader1: reader1 != null,
125
- hasReader2: reader2 != null,
126
- areDifferent: reader1 !== reader2
127
- })
128
- `);
129
- const data = JSON.parse(result as string) as {
130
- hasReader1: boolean;
131
- hasReader2: boolean;
132
- areDifferent: boolean;
133
- };
134
- assert.strictEqual(data.hasReader1, true);
135
- assert.strictEqual(data.hasReader2, true);
136
- assert.strictEqual(data.areDifferent, true);
137
- });
138
- });
139
-
140
- describe("read", () => {
141
- it("returns chunks pushed to stream", async () => {
142
- const result = await context.eval(
143
- `
144
- (async () => {
145
- const stream = new HostBackedReadableStream();
146
- const streamId = stream._getStreamId();
147
-
148
- // Push data to the stream
149
- __Stream_push(streamId, Array.from(new TextEncoder().encode("hello")));
150
- __Stream_close(streamId);
151
-
152
- const reader = stream.getReader();
153
- const { value, done } = await reader.read();
154
-
155
- return JSON.stringify({
156
- text: new TextDecoder().decode(value),
157
- done
158
- });
159
- })()
160
- `,
161
- { promise: true }
162
- );
163
- const data = JSON.parse(result as string) as { text: string; done: boolean };
164
- assert.strictEqual(data.text, "hello");
165
- assert.strictEqual(data.done, false);
166
- });
167
-
168
- it("returns done when stream closes", async () => {
169
- const result = await context.eval(
170
- `
171
- (async () => {
172
- const stream = new HostBackedReadableStream();
173
- const streamId = stream._getStreamId();
174
-
175
- __Stream_close(streamId);
176
-
177
- const reader = stream.getReader();
178
- const { done } = await reader.read();
179
-
180
- return JSON.stringify({ done });
181
- })()
182
- `,
183
- { promise: true }
184
- );
185
- const data = JSON.parse(result as string) as { done: boolean };
186
- assert.strictEqual(data.done, true);
187
- });
188
-
189
- it("returns all queued chunks before done", async () => {
190
- const result = await context.eval(
191
- `
192
- (async () => {
193
- const stream = new HostBackedReadableStream();
194
- const streamId = stream._getStreamId();
195
-
196
- // Push multiple chunks
197
- __Stream_push(streamId, Array.from(new TextEncoder().encode("a")));
198
- __Stream_push(streamId, Array.from(new TextEncoder().encode("b")));
199
- __Stream_push(streamId, Array.from(new TextEncoder().encode("c")));
200
- __Stream_close(streamId);
201
-
202
- const reader = stream.getReader();
203
- const chunks = [];
204
- while (true) {
205
- const { value, done } = await reader.read();
206
- if (done) break;
207
- chunks.push(new TextDecoder().decode(value));
208
- }
209
-
210
- return JSON.stringify({ chunks });
211
- })()
212
- `,
213
- { promise: true }
214
- );
215
- const data = JSON.parse(result as string) as { chunks: string[] };
216
- assert.deepStrictEqual(data.chunks, ["a", "b", "c"]);
217
- });
218
-
219
- it("throws on errored stream", async () => {
220
- await assert.rejects(
221
- context.eval(
222
- `
223
- (async () => {
224
- const stream = new HostBackedReadableStream();
225
- const streamId = stream._getStreamId();
226
-
227
- __Stream_error(streamId, "Stream failed");
228
-
229
- const reader = stream.getReader();
230
- await reader.read();
231
- })()
232
- `,
233
- { promise: true }
234
- )
235
- );
236
- });
237
-
238
- });
239
-
240
- describe("cancel", () => {
241
- it("cancel returns a promise", async () => {
242
- const result = await context.eval(
243
- `
244
- (async () => {
245
- const stream = new HostBackedReadableStream();
246
- const reader = stream.getReader();
247
- const cancelResult = await reader.cancel();
248
- return JSON.stringify({ cancelResult: cancelResult === undefined });
249
- })()
250
- `,
251
- { promise: true }
252
- );
253
- const data = JSON.parse(result as string) as { cancelResult: boolean };
254
- assert.strictEqual(data.cancelResult, true);
255
- });
256
- });
257
-
258
- describe("releaseLock", () => {
259
- it("releaseLock prevents further reads from that reader", async () => {
260
- await assert.rejects(
261
- context.eval(
262
- `
263
- (async () => {
264
- const stream = new HostBackedReadableStream();
265
- const streamId = stream._getStreamId();
266
- __Stream_push(streamId, Array.from(new TextEncoder().encode("test")));
267
- __Stream_close(streamId);
268
-
269
- const reader = stream.getReader();
270
- reader.releaseLock();
271
-
272
- // Should throw after releaseLock
273
- await reader.read();
274
- })()
275
- `,
276
- { promise: true }
277
- ),
278
- { message: "Reader has been released" }
279
- );
280
- });
281
-
282
- it("can get new reader after releaseLock", async () => {
283
- const result = await context.eval(
284
- `
285
- (async () => {
286
- const stream = new HostBackedReadableStream();
287
- const streamId = stream._getStreamId();
288
- __Stream_push(streamId, Array.from(new TextEncoder().encode("test")));
289
- __Stream_close(streamId);
290
-
291
- const reader1 = stream.getReader();
292
- reader1.releaseLock();
293
-
294
- const reader2 = stream.getReader();
295
- const { value, done } = await reader2.read();
296
-
297
- return JSON.stringify({
298
- text: new TextDecoder().decode(value),
299
- done,
300
- areDifferentReaders: reader1 !== reader2
301
- });
302
- })()
303
- `,
304
- { promise: true }
305
- );
306
- const data = JSON.parse(result as string) as {
307
- text: string;
308
- done: boolean;
309
- areDifferentReaders: boolean;
310
- };
311
- assert.strictEqual(data.text, "test");
312
- assert.strictEqual(data.done, false);
313
- assert.strictEqual(data.areDifferentReaders, true);
314
- });
315
- });
316
-
317
- describe("integration with Request", () => {
318
- it("Request.body is a HostBackedReadableStream", () => {
319
- const result = context.evalSync(`
320
- const request = new Request("http://test/", {
321
- method: "POST",
322
- body: "test body"
323
- });
324
- JSON.stringify({
325
- isHostBackedStream: request.body instanceof HostBackedReadableStream,
326
- hasGetReader: typeof request.body.getReader === "function"
327
- })
328
- `);
329
- const data = JSON.parse(result as string) as {
330
- isHostBackedStream: boolean;
331
- hasGetReader: boolean;
332
- };
333
- assert.strictEqual(data.isHostBackedStream, true);
334
- assert.strictEqual(data.hasGetReader, true);
335
- });
336
-
337
- it("can read Request.body via stream reader", async () => {
338
- const result = await context.eval(
339
- `
340
- (async () => {
341
- const request = new Request("http://test/", {
342
- method: "POST",
343
- body: "hello world"
344
- });
345
-
346
- const reader = request.body.getReader();
347
- const chunks = [];
348
- while (true) {
349
- const { value, done } = await reader.read();
350
- if (done) break;
351
- chunks.push(new TextDecoder().decode(value));
352
- }
353
-
354
- return JSON.stringify({ text: chunks.join("") });
355
- })()
356
- `,
357
- { promise: true }
358
- );
359
- const data = JSON.parse(result as string) as { text: string };
360
- assert.strictEqual(data.text, "hello world");
361
- });
362
- });
363
- });
package/src/index.test.ts DELETED
@@ -1,274 +0,0 @@
1
- import { test, describe, beforeEach, afterEach } from "node:test";
2
- import assert from "node:assert";
3
- import ivm from "isolated-vm";
4
- import { setupFetch, clearAllInstanceState } from "./index.ts";
5
-
6
- /**
7
- * Integration tests for @ricsam/isolate-fetch
8
- *
9
- * Note: Comprehensive tests for Headers, Request, Response, and FormData
10
- * are in their respective test files (headers.test.ts, request.test.ts, etc.)
11
- * These tests focus on integration scenarios like fetch function and AbortController.
12
- */
13
- describe("@ricsam/isolate-fetch Integration", () => {
14
- let isolate: ivm.Isolate;
15
- let context: ivm.Context;
16
-
17
- beforeEach(async () => {
18
- isolate = new ivm.Isolate();
19
- context = await isolate.createContext();
20
- clearAllInstanceState();
21
- });
22
-
23
- afterEach(() => {
24
- context.release();
25
- isolate.dispose();
26
- });
27
-
28
- describe("AbortController", () => {
29
- test("creates AbortController", async () => {
30
- await setupFetch(context);
31
- const result = context.evalSync(`
32
- const controller = new AbortController();
33
- typeof controller.signal;
34
- `);
35
- assert.strictEqual(result, "object");
36
- });
37
-
38
- test("signal starts not aborted", async () => {
39
- await setupFetch(context);
40
- const result = context.evalSync(`
41
- const controller = new AbortController();
42
- controller.signal.aborted;
43
- `);
44
- assert.strictEqual(result, false);
45
- });
46
-
47
- test("abort() sets signal.aborted to true", async () => {
48
- await setupFetch(context);
49
- const result = context.evalSync(`
50
- const controller = new AbortController();
51
- controller.abort();
52
- controller.signal.aborted;
53
- `);
54
- assert.strictEqual(result, true);
55
- });
56
-
57
- test("abort() with reason", async () => {
58
- await setupFetch(context);
59
- const result = context.evalSync(`
60
- const controller = new AbortController();
61
- controller.abort("custom reason");
62
- JSON.stringify({
63
- aborted: controller.signal.aborted,
64
- reason: controller.signal.reason
65
- });
66
- `);
67
- const data = JSON.parse(result as string);
68
- assert.strictEqual(data.aborted, true);
69
- assert.strictEqual(data.reason, "custom reason");
70
- });
71
- });
72
-
73
- describe("fetch function", () => {
74
- test("calls onFetch handler", async () => {
75
- let requestReceived: Request | null = null;
76
-
77
- await setupFetch(context, {
78
- onFetch: async (request) => {
79
- requestReceived = request;
80
- return new Response("OK");
81
- },
82
- });
83
-
84
- await context.eval(
85
- `
86
- (async () => {
87
- await fetch('https://example.com/api');
88
- })();
89
- `,
90
- { promise: true }
91
- );
92
-
93
- assert.notStrictEqual(requestReceived, null);
94
- assert.strictEqual(requestReceived!.url, "https://example.com/api");
95
- });
96
-
97
- test("returns Response from handler", async () => {
98
- await setupFetch(context, {
99
- onFetch: async () => {
100
- return new Response(JSON.stringify({ success: true }), {
101
- status: 200,
102
- headers: { "Content-Type": "application/json" },
103
- });
104
- },
105
- });
106
-
107
- const result = await context.eval(
108
- `
109
- (async () => {
110
- const response = await fetch('https://example.com/api');
111
- const data = await response.json();
112
- return JSON.stringify({
113
- status: response.status,
114
- data: data
115
- });
116
- })();
117
- `,
118
- { promise: true }
119
- );
120
-
121
- const data = JSON.parse(result as string);
122
- assert.strictEqual(data.status, 200);
123
- assert.deepStrictEqual(data.data, { success: true });
124
- });
125
-
126
- test("passes request method and headers to handler", async () => {
127
- let receivedMethod = "";
128
- let receivedHeaders: Headers | null = null;
129
-
130
- await setupFetch(context, {
131
- onFetch: async (request) => {
132
- receivedMethod = request.method;
133
- receivedHeaders = request.headers;
134
- return new Response("OK");
135
- },
136
- });
137
-
138
- await context.eval(
139
- `
140
- (async () => {
141
- await fetch('https://example.com/api', {
142
- method: 'POST',
143
- headers: { 'Content-Type': 'application/json', 'X-Custom': 'value' }
144
- });
145
- })();
146
- `,
147
- { promise: true }
148
- );
149
-
150
- assert.strictEqual(receivedMethod, "POST");
151
- assert.ok(receivedHeaders !== null);
152
- assert.strictEqual(receivedHeaders!.get("content-type"), "application/json");
153
- assert.strictEqual(receivedHeaders!.get("x-custom"), "value");
154
- });
155
-
156
- test("supports abort signal", async () => {
157
- await setupFetch(context, {
158
- onFetch: async () => {
159
- return new Response("OK");
160
- },
161
- });
162
-
163
- const result = await context.eval(
164
- `
165
- (async () => {
166
- const controller = new AbortController();
167
- controller.abort();
168
- try {
169
- await fetch('https://example.com', { signal: controller.signal });
170
- return 'no error';
171
- } catch (e) {
172
- return e.name;
173
- }
174
- })();
175
- `,
176
- { promise: true }
177
- );
178
-
179
- assert.strictEqual(result, "AbortError");
180
- });
181
-
182
- test("abort signal works with default handler", async () => {
183
- await setupFetch(context);
184
-
185
- const result = await context.eval(
186
- `
187
- (async () => {
188
- const controller = new AbortController();
189
- controller.abort();
190
- try {
191
- await fetch('https://example.com', { signal: controller.signal });
192
- return 'no error';
193
- } catch (e) {
194
- return e.name;
195
- }
196
- })();
197
- `,
198
- { promise: true }
199
- );
200
-
201
- assert.strictEqual(result, "AbortError");
202
- });
203
-
204
- test("fetch with Request object", async () => {
205
- let receivedUrl = "";
206
- let receivedMethod = "";
207
-
208
- await setupFetch(context, {
209
- onFetch: async (request) => {
210
- receivedUrl = request.url;
211
- receivedMethod = request.method;
212
- return new Response("OK");
213
- },
214
- });
215
-
216
- await context.eval(
217
- `
218
- (async () => {
219
- const request = new Request('https://example.com/api', { method: 'PUT' });
220
- await fetch(request);
221
- })();
222
- `,
223
- { promise: true }
224
- );
225
-
226
- assert.strictEqual(receivedUrl, "https://example.com/api");
227
- assert.strictEqual(receivedMethod, "PUT");
228
- });
229
-
230
- test("fetch response body can be read as text", async () => {
231
- await setupFetch(context, {
232
- onFetch: async () => {
233
- return new Response("Hello from handler");
234
- },
235
- });
236
-
237
- const result = await context.eval(
238
- `
239
- (async () => {
240
- const response = await fetch('https://example.com');
241
- return await response.text();
242
- })();
243
- `,
244
- { promise: true }
245
- );
246
-
247
- assert.strictEqual(result, "Hello from handler");
248
- });
249
-
250
- test("fetch response body can be read as JSON", async () => {
251
- await setupFetch(context, {
252
- onFetch: async () => {
253
- return new Response(JSON.stringify({ message: "hello", count: 42 }), {
254
- headers: { "Content-Type": "application/json" },
255
- });
256
- },
257
- });
258
-
259
- const result = await context.eval(
260
- `
261
- (async () => {
262
- const response = await fetch('https://example.com');
263
- const data = await response.json();
264
- return JSON.stringify(data);
265
- })();
266
- `,
267
- { promise: true }
268
- );
269
-
270
- const data = JSON.parse(result as string);
271
- assert.deepStrictEqual(data, { message: "hello", count: 42 });
272
- });
273
- });
274
- });