@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.
- package/README.md +93 -0
- package/dist/cjs/index.cjs +1800 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/cjs/stream-state.cjs +230 -0
- package/dist/cjs/stream-state.cjs.map +10 -0
- package/{src/index.ts → dist/mjs/index.mjs} +357 -923
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/mjs/stream-state.mjs +199 -0
- package/dist/mjs/stream-state.mjs.map +10 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/isolate.d.ts +267 -0
- package/dist/types/stream-state.d.ts +61 -0
- package/package.json +41 -13
- package/CHANGELOG.md +0 -9
- package/src/debug-delayed.test.ts +0 -89
- package/src/debug-streaming.test.ts +0 -81
- package/src/download-streaming-simple.test.ts +0 -167
- package/src/download-streaming.test.ts +0 -286
- package/src/form-data.test.ts +0 -824
- package/src/formdata.test.ts +0 -212
- package/src/headers.test.ts +0 -582
- package/src/host-backed-stream.test.ts +0 -363
- package/src/index.test.ts +0 -274
- package/src/integration.test.ts +0 -665
- package/src/request.test.ts +0 -482
- package/src/response.test.ts +0 -520
- package/src/serve.test.ts +0 -425
- package/src/stream-state.test.ts +0 -338
- package/src/stream-state.ts +0 -337
- package/src/upload-streaming.test.ts +0 -373
- package/src/websocket.test.ts +0 -627
- package/tsconfig.json +0 -8
package/src/stream-state.test.ts
DELETED
|
@@ -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
|
-
}
|
package/src/stream-state.ts
DELETED
|
@@ -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
|
-
}
|