@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,338 +0,0 @@
1
- import { test, describe } from "node:test";
2
- import assert from "node:assert";
3
- import {
4
- createStreamStateRegistry,
5
- HIGH_WATER_MARK,
6
- MAX_QUEUE_CHUNKS,
7
- } from "./stream-state.ts";
8
-
9
- describe("StreamStateRegistry", () => {
10
- test("create returns unique IDs", () => {
11
- const registry = createStreamStateRegistry();
12
- const id1 = registry.create();
13
- const id2 = registry.create();
14
- expect(id1).not.toBe(id2);
15
- });
16
-
17
- test("create starts from 1", () => {
18
- const registry = createStreamStateRegistry();
19
- const id = registry.create();
20
- assert.strictEqual(id, 1);
21
- });
22
-
23
- test("get returns stream state after create", () => {
24
- const registry = createStreamStateRegistry();
25
- const streamId = registry.create();
26
- const state = registry.get(streamId);
27
-
28
- assert.notStrictEqual(state, undefined);
29
- assert.deepStrictEqual(state?.queue, []);
30
- assert.strictEqual(state?.queueSize, 0);
31
- assert.strictEqual(state?.closed, false);
32
- assert.strictEqual(state?.errored, false);
33
- assert.strictEqual(state?.pullWaiting, false);
34
- });
35
-
36
- test("get returns undefined for non-existent stream", () => {
37
- const registry = createStreamStateRegistry();
38
- const state = registry.get(999);
39
- assert.strictEqual(state, undefined);
40
- });
41
-
42
- test("push and pull work synchronously when data available", async () => {
43
- const registry = createStreamStateRegistry();
44
- const streamId = registry.create();
45
-
46
- registry.push(streamId, new Uint8Array([1, 2, 3]));
47
- const result = await registry.pull(streamId);
48
-
49
- assert.strictEqual(result.done, false);
50
- if (!result.done) {
51
- assert.deepStrictEqual(result.value, new Uint8Array([1, 2, 3]));
52
- }
53
- });
54
-
55
- test("push returns true on success", () => {
56
- const registry = createStreamStateRegistry();
57
- const streamId = registry.create();
58
-
59
- const result = registry.push(streamId, new Uint8Array([1, 2, 3]));
60
- assert.strictEqual(result, true);
61
- });
62
-
63
- test("push returns false for non-existent stream", () => {
64
- const registry = createStreamStateRegistry();
65
- const result = registry.push(999, new Uint8Array([1, 2, 3]));
66
- assert.strictEqual(result, false);
67
- });
68
-
69
- test("push returns false for closed stream", () => {
70
- const registry = createStreamStateRegistry();
71
- const streamId = registry.create();
72
-
73
- registry.close(streamId);
74
- const result = registry.push(streamId, new Uint8Array([1, 2, 3]));
75
- assert.strictEqual(result, false);
76
- });
77
-
78
- test("push returns false for errored stream", () => {
79
- const registry = createStreamStateRegistry();
80
- const streamId = registry.create();
81
-
82
- registry.error(streamId, new Error("test error"));
83
- const result = registry.push(streamId, new Uint8Array([1, 2, 3]));
84
- assert.strictEqual(result, false);
85
- });
86
-
87
- test("pull waits for data when queue empty", async () => {
88
- const registry = createStreamStateRegistry();
89
- const streamId = registry.create();
90
-
91
- // Start pull (will wait)
92
- const pullPromise = registry.pull(streamId);
93
-
94
- // Push after delay
95
- setTimeout(() => {
96
- registry.push(streamId, new Uint8Array([4, 5, 6]));
97
- }, 10);
98
-
99
- const result = await pullPromise;
100
- assert.strictEqual(result.done, false);
101
- if (!result.done) {
102
- assert.deepStrictEqual(result.value, new Uint8Array([4, 5, 6]));
103
- }
104
- });
105
-
106
- test("push delivers directly to waiting pull", async () => {
107
- const registry = createStreamStateRegistry();
108
- const streamId = registry.create();
109
-
110
- // Start pull (will wait)
111
- const pullPromise = registry.pull(streamId);
112
-
113
- // Verify pull is waiting
114
- const state = registry.get(streamId);
115
- assert.strictEqual(state?.pullWaiting, true);
116
-
117
- // Push should deliver directly
118
- registry.push(streamId, new Uint8Array([7, 8, 9]));
119
-
120
- // Queue should still be empty (delivered directly)
121
- assert.strictEqual(state?.queue.length, 0);
122
- assert.strictEqual(state?.pullWaiting, false);
123
-
124
- const result = await pullPromise;
125
- assert.strictEqual(result.done, false);
126
- if (!result.done) {
127
- assert.deepStrictEqual(result.value, new Uint8Array([7, 8, 9]));
128
- }
129
- });
130
-
131
- test("multiple chunks can be queued and pulled in order", async () => {
132
- const registry = createStreamStateRegistry();
133
- const streamId = registry.create();
134
-
135
- registry.push(streamId, new Uint8Array([1]));
136
- registry.push(streamId, new Uint8Array([2]));
137
- registry.push(streamId, new Uint8Array([3]));
138
-
139
- const result1 = await registry.pull(streamId);
140
- const result2 = await registry.pull(streamId);
141
- const result3 = await registry.pull(streamId);
142
-
143
- assert.strictEqual(result1.done, false);
144
- assert.strictEqual(result2.done, false);
145
- assert.strictEqual(result3.done, false);
146
-
147
- if (!result1.done && !result2.done && !result3.done) {
148
- assert.deepStrictEqual(result1.value, new Uint8Array([1]));
149
- assert.deepStrictEqual(result2.value, new Uint8Array([2]));
150
- assert.deepStrictEqual(result3.value, new Uint8Array([3]));
151
- }
152
- });
153
-
154
- test("close resolves waiting pull with done", async () => {
155
- const registry = createStreamStateRegistry();
156
- const streamId = registry.create();
157
-
158
- const pullPromise = registry.pull(streamId);
159
- registry.close(streamId);
160
-
161
- const result = await pullPromise;
162
- assert.strictEqual(result.done, true);
163
- });
164
-
165
- test("pull returns done after close when queue empty", async () => {
166
- const registry = createStreamStateRegistry();
167
- const streamId = registry.create();
168
-
169
- registry.close(streamId);
170
-
171
- const result = await registry.pull(streamId);
172
- assert.strictEqual(result.done, true);
173
- });
174
-
175
- test("pull returns queued data before done after close", async () => {
176
- const registry = createStreamStateRegistry();
177
- const streamId = registry.create();
178
-
179
- registry.push(streamId, new Uint8Array([1, 2, 3]));
180
- registry.close(streamId);
181
-
182
- // First pull gets the data
183
- const result1 = await registry.pull(streamId);
184
- assert.strictEqual(result1.done, false);
185
- if (!result1.done) {
186
- assert.deepStrictEqual(result1.value, new Uint8Array([1, 2, 3]));
187
- }
188
-
189
- // Second pull gets done
190
- const result2 = await registry.pull(streamId);
191
- assert.strictEqual(result2.done, true);
192
- });
193
-
194
- test("error rejects waiting pull", async () => {
195
- const registry = createStreamStateRegistry();
196
- const streamId = registry.create();
197
-
198
- const pullPromise = registry.pull(streamId);
199
- registry.error(streamId, new Error("test error"));
200
-
201
- await assert.rejects(pullPromise, { message: "test error" });
202
- });
203
-
204
- test("pull throws for errored stream", async () => {
205
- const registry = createStreamStateRegistry();
206
- const streamId = registry.create();
207
-
208
- registry.error(streamId, new Error("test error"));
209
-
210
- await assert.rejects(registry.pull(streamId), { message: "test error" });
211
- });
212
-
213
- test("pull returns done for non-existent stream", async () => {
214
- const registry = createStreamStateRegistry();
215
- const result = await registry.pull(999);
216
- assert.strictEqual(result.done, true);
217
- });
218
-
219
- test("isQueueFull returns false initially", () => {
220
- const registry = createStreamStateRegistry();
221
- const streamId = registry.create();
222
-
223
- assert.strictEqual(registry.isQueueFull(streamId), false);
224
- });
225
-
226
- test("isQueueFull returns true for non-existent stream", () => {
227
- const registry = createStreamStateRegistry();
228
- assert.strictEqual(registry.isQueueFull(999), true);
229
- });
230
-
231
- test("isQueueFull respects high water mark", () => {
232
- const registry = createStreamStateRegistry();
233
- const streamId = registry.create();
234
-
235
- assert.strictEqual(registry.isQueueFull(streamId), false);
236
-
237
- // Push chunk at high water mark
238
- registry.push(streamId, new Uint8Array(HIGH_WATER_MARK));
239
- assert.strictEqual(registry.isQueueFull(streamId), true);
240
- });
241
-
242
- test("isQueueFull respects max queue chunks", () => {
243
- const registry = createStreamStateRegistry();
244
- const streamId = registry.create();
245
-
246
- // Push small chunks up to MAX_QUEUE_CHUNKS
247
- for (let i = 0; i < MAX_QUEUE_CHUNKS; i++) {
248
- registry.push(streamId, new Uint8Array([i]));
249
- }
250
-
251
- assert.strictEqual(registry.isQueueFull(streamId), true);
252
- });
253
-
254
- test("queueSize tracks total bytes correctly", () => {
255
- const registry = createStreamStateRegistry();
256
- const streamId = registry.create();
257
-
258
- registry.push(streamId, new Uint8Array(100));
259
- registry.push(streamId, new Uint8Array(200));
260
-
261
- const state = registry.get(streamId);
262
- assert.strictEqual(state?.queueSize, 300);
263
- });
264
-
265
- test("queueSize decreases on pull", async () => {
266
- const registry = createStreamStateRegistry();
267
- const streamId = registry.create();
268
-
269
- registry.push(streamId, new Uint8Array(100));
270
- registry.push(streamId, new Uint8Array(200));
271
-
272
- await registry.pull(streamId);
273
-
274
- const state = registry.get(streamId);
275
- assert.strictEqual(state?.queueSize, 200);
276
- });
277
-
278
- test("delete removes stream state", () => {
279
- const registry = createStreamStateRegistry();
280
- const streamId = registry.create();
281
-
282
- registry.delete(streamId);
283
-
284
- const state = registry.get(streamId);
285
- assert.strictEqual(state, undefined);
286
- });
287
-
288
- test("delete rejects waiting pull", async () => {
289
- const registry = createStreamStateRegistry();
290
- const streamId = registry.create();
291
-
292
- const pullPromise = registry.pull(streamId);
293
- registry.delete(streamId);
294
-
295
- await assert.rejects(pullPromise, { message: "Stream deleted" });
296
- });
297
-
298
- test("clear removes all streams", () => {
299
- const registry = createStreamStateRegistry();
300
- const id1 = registry.create();
301
- const id2 = registry.create();
302
- const id3 = registry.create();
303
-
304
- registry.clear();
305
-
306
- assert.strictEqual(registry.get(id1), undefined);
307
- assert.strictEqual(registry.get(id2), undefined);
308
- assert.strictEqual(registry.get(id3), undefined);
309
- });
310
-
311
- test("clear rejects all waiting pulls", async () => {
312
- const registry = createStreamStateRegistry();
313
- const id1 = registry.create();
314
- const id2 = registry.create();
315
-
316
- const pull1 = registry.pull(id1);
317
- const pull2 = registry.pull(id2);
318
-
319
- registry.clear();
320
-
321
- await assert.rejects(pull1, { message: "Stream deleted" });
322
- await assert.rejects(pull2, { message: "Stream deleted" });
323
- });
324
- });
325
-
326
- // Test that we correctly use assert.notStrictEqual to fix the first test
327
- function expect(value: unknown) {
328
- return {
329
- not: {
330
- toBe(other: unknown) {
331
- assert.notStrictEqual(value, other);
332
- },
333
- },
334
- toBe(other: unknown) {
335
- assert.strictEqual(value, other);
336
- },
337
- };
338
- }
@@ -1,337 +0,0 @@
1
- import ivm from "isolated-vm";
2
-
3
- // ============================================================================
4
- // Types
5
- // ============================================================================
6
-
7
- export interface StreamState {
8
- /** Buffered chunks waiting to be read */
9
- queue: Uint8Array[];
10
-
11
- /** Total bytes in queue (for backpressure) */
12
- queueSize: number;
13
-
14
- /** Stream has been closed (no more data) */
15
- closed: boolean;
16
-
17
- /** Stream encountered an error */
18
- errored: boolean;
19
-
20
- /** The error value if errored */
21
- errorValue: unknown;
22
-
23
- /** A pull is waiting for data */
24
- pullWaiting: boolean;
25
-
26
- /** Resolve function for waiting pull */
27
- pullResolve: ((chunk: Uint8Array | null) => void) | null;
28
-
29
- /** Reject function for waiting pull */
30
- pullReject: ((error: unknown) => void) | null;
31
- }
32
-
33
- export interface StreamStateRegistry {
34
- /** Create a new stream and return its ID */
35
- create(): number;
36
-
37
- /** Get stream state by ID */
38
- get(streamId: number): StreamState | undefined;
39
-
40
- /** Push a chunk to the stream's queue */
41
- push(streamId: number, chunk: Uint8Array): boolean;
42
-
43
- /** Pull a chunk from the stream (returns Promise that resolves when data available) */
44
- pull(
45
- streamId: number
46
- ): Promise<{ value: Uint8Array; done: false } | { done: true }>;
47
-
48
- /** Close the stream (no more data) */
49
- close(streamId: number): void;
50
-
51
- /** Error the stream */
52
- error(streamId: number, errorValue: unknown): void;
53
-
54
- /** Check if stream queue is above high-water mark */
55
- isQueueFull(streamId: number): boolean;
56
-
57
- /** Delete stream state (cleanup) */
58
- delete(streamId: number): void;
59
-
60
- /** Clear all streams (context cleanup) */
61
- clear(): void;
62
- }
63
-
64
- // ============================================================================
65
- // Constants
66
- // ============================================================================
67
-
68
- /** Maximum bytes to buffer before backpressure kicks in */
69
- export const HIGH_WATER_MARK = 64 * 1024; // 64KB
70
-
71
- /** Maximum number of chunks in queue */
72
- export const MAX_QUEUE_CHUNKS = 16;
73
-
74
- // ============================================================================
75
- // Implementation
76
- // ============================================================================
77
-
78
- export function createStreamStateRegistry(): StreamStateRegistry {
79
- const streams = new Map<number, StreamState>();
80
- let nextStreamId = 1;
81
-
82
- return {
83
- create(): number {
84
- const streamId = nextStreamId++;
85
- streams.set(streamId, {
86
- queue: [],
87
- queueSize: 0,
88
- closed: false,
89
- errored: false,
90
- errorValue: undefined,
91
- pullWaiting: false,
92
- pullResolve: null,
93
- pullReject: null,
94
- });
95
- return streamId;
96
- },
97
-
98
- get(streamId: number): StreamState | undefined {
99
- return streams.get(streamId);
100
- },
101
-
102
- push(streamId: number, chunk: Uint8Array): boolean {
103
- const state = streams.get(streamId);
104
- if (!state) return false;
105
- if (state.closed || state.errored) return false;
106
-
107
- // If a pull is waiting, deliver directly
108
- if (state.pullWaiting && state.pullResolve) {
109
- state.pullWaiting = false;
110
- const resolve = state.pullResolve;
111
- state.pullResolve = null;
112
- state.pullReject = null;
113
- resolve(chunk);
114
- return true;
115
- }
116
-
117
- // Otherwise queue the chunk
118
- state.queue.push(chunk);
119
- state.queueSize += chunk.length;
120
- return true;
121
- },
122
-
123
- async pull(
124
- streamId: number
125
- ): Promise<{ value: Uint8Array; done: false } | { done: true }> {
126
- const state = streams.get(streamId);
127
- if (!state) {
128
- return { done: true };
129
- }
130
-
131
- // If queue has data, return it first (even if stream is errored)
132
- if (state.queue.length > 0) {
133
- const chunk = state.queue.shift()!;
134
- state.queueSize -= chunk.length;
135
- return { value: chunk, done: false };
136
- }
137
-
138
- // If errored (and queue is empty), throw
139
- if (state.errored) {
140
- throw state.errorValue;
141
- }
142
-
143
- // If closed and queue empty, we're done
144
- if (state.closed) {
145
- return { done: true };
146
- }
147
-
148
- // Wait for data
149
- return new Promise((resolve, reject) => {
150
- state.pullWaiting = true;
151
- state.pullResolve = (chunk) => {
152
- if (chunk === null) {
153
- resolve({ done: true });
154
- } else {
155
- resolve({ value: chunk, done: false });
156
- }
157
- };
158
- state.pullReject = reject;
159
- });
160
- },
161
-
162
- close(streamId: number): void {
163
- const state = streams.get(streamId);
164
- if (!state) return;
165
-
166
- state.closed = true;
167
-
168
- // If a pull is waiting, resolve with done
169
- if (state.pullWaiting && state.pullResolve) {
170
- state.pullWaiting = false;
171
- const resolve = state.pullResolve;
172
- state.pullResolve = null;
173
- state.pullReject = null;
174
- resolve(null);
175
- }
176
- },
177
-
178
- error(streamId: number, errorValue: unknown): void {
179
- const state = streams.get(streamId);
180
- if (!state) return;
181
-
182
- state.errored = true;
183
- state.errorValue = errorValue;
184
-
185
- // If a pull is waiting, reject it
186
- if (state.pullWaiting && state.pullReject) {
187
- state.pullWaiting = false;
188
- const reject = state.pullReject;
189
- state.pullResolve = null;
190
- state.pullReject = null;
191
- reject(errorValue);
192
- }
193
- },
194
-
195
- isQueueFull(streamId: number): boolean {
196
- const state = streams.get(streamId);
197
- if (!state) return true;
198
- return (
199
- state.queueSize >= HIGH_WATER_MARK ||
200
- state.queue.length >= MAX_QUEUE_CHUNKS
201
- );
202
- },
203
-
204
- delete(streamId: number): void {
205
- const state = streams.get(streamId);
206
- if (state && state.pullWaiting && state.pullReject) {
207
- state.pullReject(new Error("Stream deleted"));
208
- }
209
- streams.delete(streamId);
210
- },
211
-
212
- clear(): void {
213
- for (const [streamId] of streams) {
214
- this.delete(streamId);
215
- }
216
- },
217
- };
218
- }
219
-
220
- // ============================================================================
221
- // Context-Scoped Registry
222
- // ============================================================================
223
-
224
- const contextRegistries = new WeakMap<ivm.Context, StreamStateRegistry>();
225
-
226
- export function getStreamRegistryForContext(
227
- context: ivm.Context
228
- ): StreamStateRegistry {
229
- let registry = contextRegistries.get(context);
230
- if (!registry) {
231
- registry = createStreamStateRegistry();
232
- contextRegistries.set(context, registry);
233
- }
234
- return registry;
235
- }
236
-
237
- export function clearStreamRegistryForContext(context: ivm.Context): void {
238
- const registry = contextRegistries.get(context);
239
- if (registry) {
240
- registry.clear();
241
- contextRegistries.delete(context);
242
- }
243
- }
244
-
245
- // ============================================================================
246
- // Native Stream Reader
247
- // ============================================================================
248
-
249
- /**
250
- * Start reading from a native ReadableStream and push to host queue.
251
- * Respects backpressure by pausing when queue is full.
252
- *
253
- * @param nativeStream The native ReadableStream to read from
254
- * @param streamId The stream ID in the registry
255
- * @param registry The stream state registry
256
- * @returns Async cleanup function to cancel the reader
257
- */
258
- export function startNativeStreamReader(
259
- nativeStream: ReadableStream<Uint8Array>,
260
- streamId: number,
261
- registry: StreamStateRegistry
262
- ): () => Promise<void> {
263
- let cancelled = false;
264
- let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
265
- let readLoopPromise: Promise<void> | null = null;
266
-
267
- const CHUNK_SIZE = 64 * 1024; // 64KB max chunk size
268
-
269
- async function readLoop() {
270
- try {
271
- reader = nativeStream.getReader();
272
-
273
- while (!cancelled) {
274
- // Respect backpressure - wait if queue is full
275
- while (registry.isQueueFull(streamId) && !cancelled) {
276
- await new Promise((resolve) => setTimeout(resolve, 1));
277
- }
278
- if (cancelled) break;
279
-
280
- const { done, value } = await reader.read();
281
-
282
- if (done) {
283
- registry.close(streamId);
284
- break;
285
- }
286
-
287
- if (value) {
288
- // Split large chunks to maintain granularity
289
- if (value.length > CHUNK_SIZE) {
290
- for (let offset = 0; offset < value.length; offset += CHUNK_SIZE) {
291
- const chunk = value.slice(
292
- offset,
293
- Math.min(offset + CHUNK_SIZE, value.length)
294
- );
295
- registry.push(streamId, chunk);
296
- }
297
- } else {
298
- registry.push(streamId, value);
299
- }
300
- }
301
- }
302
- } catch (error) {
303
- registry.error(streamId, error);
304
- } finally {
305
- if (reader) {
306
- try {
307
- reader.releaseLock();
308
- } catch {
309
- // Ignore release errors
310
- }
311
- }
312
- }
313
- }
314
-
315
- // Start the read loop and save the promise
316
- readLoopPromise = readLoop();
317
-
318
- // Return async cleanup function
319
- return async () => {
320
- cancelled = true;
321
- if (reader) {
322
- try {
323
- await reader.cancel();
324
- } catch {
325
- // Ignore cancel errors
326
- }
327
- }
328
- // Wait for read loop to finish
329
- if (readLoopPromise) {
330
- try {
331
- await readLoopPromise;
332
- } catch {
333
- // Ignore read loop errors during cleanup
334
- }
335
- }
336
- };
337
- }