@ricsam/isolate-fetch 0.0.1 → 0.1.1
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/CHANGELOG.md +9 -0
- package/package.json +27 -7
- package/src/debug-delayed.test.ts +89 -0
- package/src/debug-streaming.test.ts +81 -0
- package/src/download-streaming-simple.test.ts +167 -0
- package/src/download-streaming.test.ts +286 -0
- package/src/form-data.test.ts +824 -0
- package/src/formdata.test.ts +212 -0
- package/src/headers.test.ts +582 -0
- package/src/host-backed-stream.test.ts +363 -0
- package/src/index.test.ts +274 -0
- package/src/index.ts +2325 -0
- package/src/integration.test.ts +665 -0
- package/src/request.test.ts +482 -0
- package/src/response.test.ts +520 -0
- package/src/serve.test.ts +425 -0
- package/src/stream-state.test.ts +338 -0
- package/src/stream-state.ts +337 -0
- package/src/upload-streaming.test.ts +373 -0
- package/src/websocket.test.ts +627 -0
- package/tsconfig.json +8 -0
- package/README.md +0 -45
|
@@ -0,0 +1,627 @@
|
|
|
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, type FetchHandle, type WebSocketCommand } from "./index.ts";
|
|
5
|
+
|
|
6
|
+
describe("WebSocket", () => {
|
|
7
|
+
let isolate: ivm.Isolate;
|
|
8
|
+
let context: ivm.Context;
|
|
9
|
+
let fetchHandle: FetchHandle;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
isolate = new ivm.Isolate();
|
|
13
|
+
context = await isolate.createContext();
|
|
14
|
+
clearAllInstanceState();
|
|
15
|
+
fetchHandle = await setupFetch(context);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fetchHandle.dispose();
|
|
20
|
+
context.release();
|
|
21
|
+
isolate.dispose();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("server.upgrade() returns true and sets pendingUpgrade with connectionId", async () => {
|
|
25
|
+
context.evalSync(`
|
|
26
|
+
serve({
|
|
27
|
+
fetch(request, server) {
|
|
28
|
+
const upgraded = server.upgrade(request, { data: { userId: "123" } });
|
|
29
|
+
return new Response(upgraded ? "upgrading" : "failed", { status: upgraded ? 101 : 400 });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
const response = await fetchHandle.dispatchRequest(
|
|
35
|
+
new Request("http://localhost/ws")
|
|
36
|
+
);
|
|
37
|
+
// Note: Status 101 is not valid for native Response, so we expose it via _originalStatus
|
|
38
|
+
// @ts-expect-error - accessing custom property
|
|
39
|
+
assert.strictEqual(response._originalStatus, 101);
|
|
40
|
+
|
|
41
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
42
|
+
assert.strictEqual(upgrade?.requested, true);
|
|
43
|
+
assert.strictEqual(typeof upgrade?.connectionId, "string");
|
|
44
|
+
assert.ok(upgrade?.connectionId);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("server.upgrade() without data option", async () => {
|
|
48
|
+
context.evalSync(`
|
|
49
|
+
serve({
|
|
50
|
+
fetch(request, server) {
|
|
51
|
+
const upgraded = server.upgrade(request);
|
|
52
|
+
return new Response(null, { status: upgraded ? 101 : 400 });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
const response = await fetchHandle.dispatchRequest(
|
|
58
|
+
new Request("http://localhost/ws")
|
|
59
|
+
);
|
|
60
|
+
// @ts-expect-error - accessing custom property
|
|
61
|
+
assert.strictEqual(response._originalStatus, 101);
|
|
62
|
+
|
|
63
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
64
|
+
assert.strictEqual(upgrade?.requested, true);
|
|
65
|
+
assert.strictEqual(typeof upgrade?.connectionId, "string");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("dispatchWebSocketOpen calls websocket.open handler", async () => {
|
|
69
|
+
context.evalSync(`
|
|
70
|
+
globalThis.openedConnections = [];
|
|
71
|
+
serve({
|
|
72
|
+
fetch(request, server) {
|
|
73
|
+
server.upgrade(request, { data: { test: true } });
|
|
74
|
+
return new Response(null, { status: 101 });
|
|
75
|
+
},
|
|
76
|
+
websocket: {
|
|
77
|
+
open(ws) {
|
|
78
|
+
globalThis.openedConnections.push(ws.data);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
85
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
86
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
87
|
+
|
|
88
|
+
const result = context.evalSync(`JSON.stringify(globalThis.openedConnections)`);
|
|
89
|
+
const connections = JSON.parse(result as string);
|
|
90
|
+
assert.strictEqual(connections.length, 1);
|
|
91
|
+
assert.deepStrictEqual(connections[0], { test: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("dispatchWebSocketMessage delivers messages to handler", async () => {
|
|
95
|
+
context.evalSync(`
|
|
96
|
+
globalThis.receivedMessages = [];
|
|
97
|
+
serve({
|
|
98
|
+
fetch(request, server) {
|
|
99
|
+
server.upgrade(request);
|
|
100
|
+
return new Response(null, { status: 101 });
|
|
101
|
+
},
|
|
102
|
+
websocket: {
|
|
103
|
+
open(ws) {
|
|
104
|
+
// Need open handler for connection to be established
|
|
105
|
+
},
|
|
106
|
+
message(ws, message) {
|
|
107
|
+
globalThis.receivedMessages.push(message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
`);
|
|
112
|
+
|
|
113
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
114
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
115
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
116
|
+
fetchHandle.dispatchWebSocketMessage(upgrade!.connectionId, "Hello WebSocket!");
|
|
117
|
+
|
|
118
|
+
const result = context.evalSync(`JSON.stringify(globalThis.receivedMessages)`);
|
|
119
|
+
const messages = JSON.parse(result as string);
|
|
120
|
+
assert.deepStrictEqual(messages, ["Hello WebSocket!"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("dispatchWebSocketClose notifies handler with code and reason", async () => {
|
|
124
|
+
context.evalSync(`
|
|
125
|
+
globalThis.closeInfo = null;
|
|
126
|
+
serve({
|
|
127
|
+
fetch(request, server) {
|
|
128
|
+
server.upgrade(request);
|
|
129
|
+
return new Response(null, { status: 101 });
|
|
130
|
+
},
|
|
131
|
+
websocket: {
|
|
132
|
+
open(ws) {
|
|
133
|
+
// Need open handler for connection to be established
|
|
134
|
+
},
|
|
135
|
+
close(ws, code, reason) {
|
|
136
|
+
globalThis.closeInfo = { code, reason };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
143
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
144
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
145
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal closure");
|
|
146
|
+
|
|
147
|
+
const result = context.evalSync(`JSON.stringify(globalThis.closeInfo)`);
|
|
148
|
+
const closeInfo = JSON.parse(result as string);
|
|
149
|
+
assert.deepStrictEqual(closeInfo, { code: 1000, reason: "Normal closure" });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("dispatchWebSocketError delivers error to handler", async () => {
|
|
153
|
+
context.evalSync(`
|
|
154
|
+
globalThis.errorInfo = null;
|
|
155
|
+
serve({
|
|
156
|
+
fetch(request, server) {
|
|
157
|
+
server.upgrade(request);
|
|
158
|
+
return new Response(null, { status: 101 });
|
|
159
|
+
},
|
|
160
|
+
websocket: {
|
|
161
|
+
open(ws) {
|
|
162
|
+
// Need open handler for connection to be established
|
|
163
|
+
},
|
|
164
|
+
error(ws, error) {
|
|
165
|
+
globalThis.errorInfo = { name: error.name, message: error.message };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
`);
|
|
170
|
+
|
|
171
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
172
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
173
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
174
|
+
fetchHandle.dispatchWebSocketError(upgrade!.connectionId, new Error("Connection lost"));
|
|
175
|
+
|
|
176
|
+
const result = context.evalSync(`JSON.stringify(globalThis.errorInfo)`);
|
|
177
|
+
const errorInfo = JSON.parse(result as string);
|
|
178
|
+
assert.deepStrictEqual(errorInfo, { name: "Error", message: "Connection lost" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("ws.send() triggers onWebSocketCommand callback", async () => {
|
|
182
|
+
context.evalSync(`
|
|
183
|
+
serve({
|
|
184
|
+
fetch(request, server) {
|
|
185
|
+
server.upgrade(request);
|
|
186
|
+
return new Response(null, { status: 101 });
|
|
187
|
+
},
|
|
188
|
+
websocket: {
|
|
189
|
+
open(ws) {
|
|
190
|
+
ws.send("Welcome!");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
const commands: WebSocketCommand[] = [];
|
|
197
|
+
fetchHandle.onWebSocketCommand((cmd) => {
|
|
198
|
+
commands.push(cmd);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
202
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
203
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
204
|
+
|
|
205
|
+
assert.strictEqual(commands.length, 1);
|
|
206
|
+
assert.strictEqual(commands[0].type, "message");
|
|
207
|
+
assert.strictEqual(commands[0].connectionId, upgrade!.connectionId);
|
|
208
|
+
assert.strictEqual(commands[0].data, "Welcome!");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("ws.close() triggers onWebSocketCommand with close message", async () => {
|
|
212
|
+
context.evalSync(`
|
|
213
|
+
serve({
|
|
214
|
+
fetch(request, server) {
|
|
215
|
+
server.upgrade(request);
|
|
216
|
+
return new Response(null, { status: 101 });
|
|
217
|
+
},
|
|
218
|
+
websocket: {
|
|
219
|
+
open(ws) {
|
|
220
|
+
ws.close(1000, "Goodbye");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
const commands: WebSocketCommand[] = [];
|
|
227
|
+
fetchHandle.onWebSocketCommand((cmd) => {
|
|
228
|
+
commands.push(cmd);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
232
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
233
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
234
|
+
|
|
235
|
+
assert.strictEqual(commands.length, 1);
|
|
236
|
+
assert.strictEqual(commands[0].type, "close");
|
|
237
|
+
assert.strictEqual(commands[0].connectionId, upgrade!.connectionId);
|
|
238
|
+
assert.strictEqual(commands[0].code, 1000);
|
|
239
|
+
assert.strictEqual(commands[0].reason, "Goodbye");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("WebSocket echo server roundtrip", async () => {
|
|
243
|
+
context.evalSync(`
|
|
244
|
+
serve({
|
|
245
|
+
fetch(request, server) {
|
|
246
|
+
server.upgrade(request);
|
|
247
|
+
return new Response(null, { status: 101 });
|
|
248
|
+
},
|
|
249
|
+
websocket: {
|
|
250
|
+
open(ws) {
|
|
251
|
+
// Need open handler for connection to be established
|
|
252
|
+
},
|
|
253
|
+
message(ws, message) {
|
|
254
|
+
ws.send("Echo: " + message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
`);
|
|
259
|
+
|
|
260
|
+
const messages: string[] = [];
|
|
261
|
+
fetchHandle.onWebSocketCommand((cmd) => {
|
|
262
|
+
if (cmd.type === "message") {
|
|
263
|
+
messages.push(cmd.data as string);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
268
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
269
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
270
|
+
fetchHandle.dispatchWebSocketMessage(upgrade!.connectionId, "Hello");
|
|
271
|
+
|
|
272
|
+
assert.deepStrictEqual(messages, ["Echo: Hello"]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("message to unknown connection is ignored", async () => {
|
|
276
|
+
context.evalSync(`
|
|
277
|
+
globalThis.receivedMessages = [];
|
|
278
|
+
serve({
|
|
279
|
+
fetch(request, server) {
|
|
280
|
+
server.upgrade(request);
|
|
281
|
+
return new Response(null, { status: 101 });
|
|
282
|
+
},
|
|
283
|
+
websocket: {
|
|
284
|
+
open(ws) {},
|
|
285
|
+
message(ws, message) {
|
|
286
|
+
globalThis.receivedMessages.push(message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
293
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
294
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
295
|
+
|
|
296
|
+
// Send to unknown connection - should not throw
|
|
297
|
+
fetchHandle.dispatchWebSocketMessage("unknown-conn", "Hello");
|
|
298
|
+
|
|
299
|
+
const result = context.evalSync(`JSON.stringify(globalThis.receivedMessages)`);
|
|
300
|
+
const messages = JSON.parse(result as string);
|
|
301
|
+
assert.deepStrictEqual(messages, []);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("ws.readyState is 1 (OPEN) when connected", async () => {
|
|
305
|
+
context.evalSync(`
|
|
306
|
+
globalThis.readyState = null;
|
|
307
|
+
serve({
|
|
308
|
+
fetch(request, server) {
|
|
309
|
+
server.upgrade(request);
|
|
310
|
+
return new Response(null, { status: 101 });
|
|
311
|
+
},
|
|
312
|
+
websocket: {
|
|
313
|
+
open(ws) {
|
|
314
|
+
globalThis.readyState = ws.readyState;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
`);
|
|
319
|
+
|
|
320
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
321
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
322
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
323
|
+
|
|
324
|
+
const result = context.evalSync(`globalThis.readyState`);
|
|
325
|
+
assert.strictEqual(result, 1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("multiple connections are tracked independently", async () => {
|
|
329
|
+
context.evalSync(`
|
|
330
|
+
globalThis.messages = {};
|
|
331
|
+
serve({
|
|
332
|
+
fetch(request, server) {
|
|
333
|
+
const match = request.url.match(/id=([^&]+)/);
|
|
334
|
+
const id = match ? match[1] : "unknown";
|
|
335
|
+
server.upgrade(request, { data: { id } });
|
|
336
|
+
return new Response(null, { status: 101 });
|
|
337
|
+
},
|
|
338
|
+
websocket: {
|
|
339
|
+
open(ws) {
|
|
340
|
+
// Need open handler for connection to be established
|
|
341
|
+
},
|
|
342
|
+
message(ws, message) {
|
|
343
|
+
if (!globalThis.messages[ws.data.id]) {
|
|
344
|
+
globalThis.messages[ws.data.id] = [];
|
|
345
|
+
}
|
|
346
|
+
globalThis.messages[ws.data.id].push(message);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
`);
|
|
351
|
+
|
|
352
|
+
// Open two connections
|
|
353
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?id=conn1"));
|
|
354
|
+
const upgrade1 = fetchHandle.getUpgradeRequest();
|
|
355
|
+
fetchHandle.dispatchWebSocketOpen(upgrade1!.connectionId);
|
|
356
|
+
|
|
357
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?id=conn2"));
|
|
358
|
+
const upgrade2 = fetchHandle.getUpgradeRequest();
|
|
359
|
+
fetchHandle.dispatchWebSocketOpen(upgrade2!.connectionId);
|
|
360
|
+
|
|
361
|
+
// Send messages to each
|
|
362
|
+
fetchHandle.dispatchWebSocketMessage(upgrade1!.connectionId, "Message to conn1");
|
|
363
|
+
fetchHandle.dispatchWebSocketMessage(upgrade2!.connectionId, "Message to conn2");
|
|
364
|
+
|
|
365
|
+
const result = context.evalSync(`JSON.stringify(globalThis.messages)`);
|
|
366
|
+
const messages = JSON.parse(result as string);
|
|
367
|
+
assert.deepStrictEqual(messages, {
|
|
368
|
+
conn1: ["Message to conn1"],
|
|
369
|
+
conn2: ["Message to conn2"],
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("connection is removed after close", async () => {
|
|
374
|
+
context.evalSync(`
|
|
375
|
+
globalThis.messageCount = 0;
|
|
376
|
+
serve({
|
|
377
|
+
fetch(request, server) {
|
|
378
|
+
server.upgrade(request);
|
|
379
|
+
return new Response(null, { status: 101 });
|
|
380
|
+
},
|
|
381
|
+
websocket: {
|
|
382
|
+
open(ws) {
|
|
383
|
+
// Need open handler for connection to be established
|
|
384
|
+
},
|
|
385
|
+
message(ws, message) {
|
|
386
|
+
globalThis.messageCount++;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
`);
|
|
391
|
+
|
|
392
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
393
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
394
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
395
|
+
fetchHandle.dispatchWebSocketMessage(upgrade!.connectionId, "Before close");
|
|
396
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal");
|
|
397
|
+
|
|
398
|
+
// Message after close should be ignored
|
|
399
|
+
fetchHandle.dispatchWebSocketMessage(upgrade!.connectionId, "After close");
|
|
400
|
+
|
|
401
|
+
const result = context.evalSync(`globalThis.messageCount`);
|
|
402
|
+
assert.strictEqual(result, 1);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("hasActiveConnections returns correct state", async () => {
|
|
406
|
+
assert.strictEqual(fetchHandle.hasActiveConnections(), false);
|
|
407
|
+
|
|
408
|
+
context.evalSync(`
|
|
409
|
+
serve({
|
|
410
|
+
fetch(request, server) {
|
|
411
|
+
server.upgrade(request);
|
|
412
|
+
return new Response(null, { status: 101 });
|
|
413
|
+
},
|
|
414
|
+
websocket: {
|
|
415
|
+
open(ws) {}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
`);
|
|
419
|
+
|
|
420
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
421
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
422
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
423
|
+
|
|
424
|
+
assert.strictEqual(fetchHandle.hasActiveConnections(), true);
|
|
425
|
+
|
|
426
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal");
|
|
427
|
+
|
|
428
|
+
assert.strictEqual(fetchHandle.hasActiveConnections(), false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("close handler data access", () => {
|
|
432
|
+
test("close handler can access ws.data set during upgrade", async () => {
|
|
433
|
+
context.evalSync(`
|
|
434
|
+
globalThis.closeHandlerData = null;
|
|
435
|
+
serve({
|
|
436
|
+
fetch(request, server) {
|
|
437
|
+
server.upgrade(request, { data: { userId: "user123", sessionId: "sess456" } });
|
|
438
|
+
return new Response(null, { status: 101 });
|
|
439
|
+
},
|
|
440
|
+
websocket: {
|
|
441
|
+
open(ws) {
|
|
442
|
+
// Connection established
|
|
443
|
+
},
|
|
444
|
+
close(ws, code, reason) {
|
|
445
|
+
globalThis.closeHandlerData = ws.data;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
`);
|
|
450
|
+
|
|
451
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
452
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
453
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
454
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal");
|
|
455
|
+
|
|
456
|
+
const result = context.evalSync(`JSON.stringify(globalThis.closeHandlerData)`);
|
|
457
|
+
const data = JSON.parse(result as string);
|
|
458
|
+
assert.deepStrictEqual(data, { userId: "user123", sessionId: "sess456" });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("close handler can access ws.data modified during message handling", async () => {
|
|
462
|
+
context.evalSync(`
|
|
463
|
+
globalThis.closeHandlerData = null;
|
|
464
|
+
serve({
|
|
465
|
+
fetch(request, server) {
|
|
466
|
+
server.upgrade(request, { data: {} });
|
|
467
|
+
return new Response(null, { status: 101 });
|
|
468
|
+
},
|
|
469
|
+
websocket: {
|
|
470
|
+
open(ws) {
|
|
471
|
+
// Initial data is empty
|
|
472
|
+
},
|
|
473
|
+
message(ws, message) {
|
|
474
|
+
if (message === "join:Alice") {
|
|
475
|
+
ws.data.username = "Alice";
|
|
476
|
+
ws.data.joinedAt = Date.now();
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
close(ws, code, reason) {
|
|
480
|
+
globalThis.closeHandlerData = ws.data;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
`);
|
|
485
|
+
|
|
486
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
487
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
488
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
489
|
+
fetchHandle.dispatchWebSocketMessage(upgrade!.connectionId, "join:Alice");
|
|
490
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal");
|
|
491
|
+
|
|
492
|
+
const result = context.evalSync(`JSON.stringify(globalThis.closeHandlerData)`);
|
|
493
|
+
const data = JSON.parse(result as string);
|
|
494
|
+
assert.strictEqual(data.username, "Alice");
|
|
495
|
+
assert.strictEqual(typeof data.joinedAt, "number");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("close handler requires open handler for connection tracking", async () => {
|
|
499
|
+
context.evalSync(`
|
|
500
|
+
globalThis.closeCalled = false;
|
|
501
|
+
serve({
|
|
502
|
+
fetch(request, server) {
|
|
503
|
+
server.upgrade(request, { data: { test: true } });
|
|
504
|
+
return new Response(null, { status: 101 });
|
|
505
|
+
},
|
|
506
|
+
websocket: {
|
|
507
|
+
open(ws) {
|
|
508
|
+
// Must define open handler for connection tracking
|
|
509
|
+
},
|
|
510
|
+
close(ws, code, reason) {
|
|
511
|
+
globalThis.closeCalled = true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
`);
|
|
516
|
+
|
|
517
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws"));
|
|
518
|
+
const upgrade = fetchHandle.getUpgradeRequest();
|
|
519
|
+
fetchHandle.dispatchWebSocketOpen(upgrade!.connectionId);
|
|
520
|
+
fetchHandle.dispatchWebSocketClose(upgrade!.connectionId, 1000, "Normal");
|
|
521
|
+
|
|
522
|
+
const result = context.evalSync(`globalThis.closeCalled`);
|
|
523
|
+
assert.strictEqual(result, true);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("multiple connections close handlers receive correct data", async () => {
|
|
527
|
+
context.evalSync(`
|
|
528
|
+
globalThis.closedUsers = [];
|
|
529
|
+
serve({
|
|
530
|
+
fetch(request, server) {
|
|
531
|
+
const match = request.url.match(/user=([^&]+)/);
|
|
532
|
+
const username = match ? match[1] : "unknown";
|
|
533
|
+
server.upgrade(request, { data: { username } });
|
|
534
|
+
return new Response(null, { status: 101 });
|
|
535
|
+
},
|
|
536
|
+
websocket: {
|
|
537
|
+
open(ws) {
|
|
538
|
+
// Connection established
|
|
539
|
+
},
|
|
540
|
+
close(ws, code, reason) {
|
|
541
|
+
globalThis.closedUsers.push(ws.data.username);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
`);
|
|
546
|
+
|
|
547
|
+
// Open three connections
|
|
548
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Alice"));
|
|
549
|
+
const upgrade1 = fetchHandle.getUpgradeRequest();
|
|
550
|
+
fetchHandle.dispatchWebSocketOpen(upgrade1!.connectionId);
|
|
551
|
+
|
|
552
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Bob"));
|
|
553
|
+
const upgrade2 = fetchHandle.getUpgradeRequest();
|
|
554
|
+
fetchHandle.dispatchWebSocketOpen(upgrade2!.connectionId);
|
|
555
|
+
|
|
556
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Charlie"));
|
|
557
|
+
const upgrade3 = fetchHandle.getUpgradeRequest();
|
|
558
|
+
fetchHandle.dispatchWebSocketOpen(upgrade3!.connectionId);
|
|
559
|
+
|
|
560
|
+
// Close them in different order
|
|
561
|
+
fetchHandle.dispatchWebSocketClose(upgrade2!.connectionId, 1000, "Normal");
|
|
562
|
+
fetchHandle.dispatchWebSocketClose(upgrade1!.connectionId, 1000, "Normal");
|
|
563
|
+
fetchHandle.dispatchWebSocketClose(upgrade3!.connectionId, 1000, "Normal");
|
|
564
|
+
|
|
565
|
+
const result = context.evalSync(`JSON.stringify(globalThis.closedUsers)`);
|
|
566
|
+
const closedUsers = JSON.parse(result as string);
|
|
567
|
+
assert.deepStrictEqual(closedUsers, ["Bob", "Alice", "Charlie"]);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("close handler can broadcast to other connections", async () => {
|
|
571
|
+
context.evalSync(`
|
|
572
|
+
globalThis.connections = new Map();
|
|
573
|
+
globalThis.broadcastMessages = [];
|
|
574
|
+
serve({
|
|
575
|
+
fetch(request, server) {
|
|
576
|
+
const match = request.url.match(/user=([^&]+)/);
|
|
577
|
+
const username = match ? match[1] : "unknown";
|
|
578
|
+
server.upgrade(request, { data: { username } });
|
|
579
|
+
return new Response(null, { status: 101 });
|
|
580
|
+
},
|
|
581
|
+
websocket: {
|
|
582
|
+
open(ws) {
|
|
583
|
+
globalThis.connections.set(ws.data.username, ws);
|
|
584
|
+
},
|
|
585
|
+
close(ws, code, reason) {
|
|
586
|
+
const username = ws.data.username;
|
|
587
|
+
globalThis.connections.delete(username);
|
|
588
|
+
for (const [name, otherWs] of globalThis.connections) {
|
|
589
|
+
otherWs.send("userLeft:" + username);
|
|
590
|
+
globalThis.broadcastMessages.push({ to: name, about: username });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
`);
|
|
596
|
+
|
|
597
|
+
const commands: WebSocketCommand[] = [];
|
|
598
|
+
fetchHandle.onWebSocketCommand((cmd) => {
|
|
599
|
+
commands.push(cmd);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Open three connections
|
|
603
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Alice"));
|
|
604
|
+
const upgrade1 = fetchHandle.getUpgradeRequest();
|
|
605
|
+
fetchHandle.dispatchWebSocketOpen(upgrade1!.connectionId);
|
|
606
|
+
|
|
607
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Bob"));
|
|
608
|
+
const upgrade2 = fetchHandle.getUpgradeRequest();
|
|
609
|
+
fetchHandle.dispatchWebSocketOpen(upgrade2!.connectionId);
|
|
610
|
+
|
|
611
|
+
await fetchHandle.dispatchRequest(new Request("http://localhost/ws?user=Charlie"));
|
|
612
|
+
const upgrade3 = fetchHandle.getUpgradeRequest();
|
|
613
|
+
fetchHandle.dispatchWebSocketOpen(upgrade3!.connectionId);
|
|
614
|
+
|
|
615
|
+
// Bob disconnects - should broadcast to Alice and Charlie
|
|
616
|
+
fetchHandle.dispatchWebSocketClose(upgrade2!.connectionId, 1000, "Normal");
|
|
617
|
+
|
|
618
|
+
const result = context.evalSync(`JSON.stringify(globalThis.broadcastMessages)`);
|
|
619
|
+
const broadcasts = JSON.parse(result as string);
|
|
620
|
+
|
|
621
|
+
// Should have 2 broadcasts (to Alice and Charlie)
|
|
622
|
+
assert.strictEqual(broadcasts.length, 2);
|
|
623
|
+
assert.deepStrictEqual(broadcasts.map((b: { about: string }) => b.about), ["Bob", "Bob"]);
|
|
624
|
+
assert.deepStrictEqual(broadcasts.map((b: { to: string }) => b.to).sort(), ["Alice", "Charlie"]);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|
package/tsconfig.json
ADDED
package/README.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# @ricsam/isolate-fetch
|
|
2
|
-
|
|
3
|
-
## ⚠️ IMPORTANT NOTICE ⚠️
|
|
4
|
-
|
|
5
|
-
**This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
|
|
6
|
-
|
|
7
|
-
This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This package exists to:
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@ricsam/isolate-fetch`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
15
|
-
|
|
16
|
-
## What is OIDC Trusted Publishing?
|
|
17
|
-
|
|
18
|
-
OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
|
|
19
|
-
|
|
20
|
-
## Setup Instructions
|
|
21
|
-
|
|
22
|
-
To properly configure OIDC trusted publishing for this package:
|
|
23
|
-
|
|
24
|
-
1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
28
|
-
|
|
29
|
-
## DO NOT USE THIS PACKAGE
|
|
30
|
-
|
|
31
|
-
This package is a placeholder for OIDC configuration only. It:
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
36
|
-
|
|
37
|
-
## More Information
|
|
38
|
-
|
|
39
|
-
For more details about npm's trusted publishing feature, see:
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
**Maintained for OIDC setup purposes only**
|