@jskit-ai/realtime 0.1.194 → 0.1.196

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/realtime",
3
- "version": "0.1.194",
3
+ "version": "0.1.196",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -20,8 +20,8 @@
20
20
  "socket.io-client": "^4.8.3"
21
21
  },
22
22
  "peerDependencies": {
23
- "@jskit-ai/kernel": "0.1.197",
24
- "@jskit-ai/shell-web": "0.1.201",
23
+ "@jskit-ai/kernel": "0.1.199",
24
+ "@jskit-ai/shell-web": "0.1.203",
25
25
  "vue": "^3.5.13"
26
26
  },
27
27
  "description": "Thin, generic realtime runtime wrappers for socket.io server and client.",
@@ -32,6 +32,8 @@ deployment needs a Redis backplane.
32
32
  - Listeners are registered through the public provider seams.
33
33
  - The status indicator is an explicit app placement, not a source mutation.
34
34
  - Event payloads do not become an undocumented second API.
35
+ - Socket.IO owns `/socket.io`; other WebSocket routes keep their own upgrade
36
+ and access-check lifecycle without Socket.IO's one-second cleanup timeout.
35
37
 
36
38
  ## Framework APIs
37
39
 
@@ -2,7 +2,7 @@
2
2
  "private": true,
3
3
  "type": "module",
4
4
  "dependencies": {
5
- "@jskit-ai/realtime": "0.1.194",
6
- "@jskit-ai/shell-web": "0.1.201"
5
+ "@jskit-ai/realtime": "0.1.196",
6
+ "@jskit-ai/shell-web": "0.1.203"
7
7
  }
8
8
  }
@@ -33,6 +33,8 @@ function createSocketIoServer({
33
33
  });
34
34
  const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
35
35
  const normalizedOptions = {
36
+ // Application WebSocket routes may still be authorizing their upgrades.
37
+ destroyUpgrade: false,
36
38
  ...source,
37
39
  path: SOCKET_IO_PATH
38
40
  };
@@ -1,5 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
+ import { once } from "node:events";
3
+ import { createServer, get } from "node:http";
2
4
  import test from "node:test";
5
+ import { io as connectSocketIo } from "socket.io-client";
3
6
 
4
7
  import {
5
8
  REDIS_URL_ENV_KEY,
@@ -40,6 +43,7 @@ test("createSocketIoServer uses provided http server and fixed socket path", ()
40
43
  assert.equal(calls.length, 1);
41
44
  assert.equal(calls[0].server, httpServer);
42
45
  assert.deepEqual(calls[0].options, {
46
+ destroyUpgrade: false,
43
47
  path: "/socket.io",
44
48
  cors: {
45
49
  origin: "*"
@@ -47,6 +51,51 @@ test("createSocketIoServer uses provided http server and fixed socket path", ()
47
51
  });
48
52
  });
49
53
 
54
+ test("realtime leaves slow application upgrades and access denials to their route owner", { timeout: 5_000 }, async (t) => {
55
+ const server = createServer();
56
+ const sockets = new Set();
57
+ const timers = new Set();
58
+ server.on("connection", (socket) => {
59
+ sockets.add(socket);
60
+ socket.once("close", () => sockets.delete(socket));
61
+ });
62
+ server.on("upgrade", (request, socket) => {
63
+ if (request.url.startsWith("/socket.io/")) return;
64
+ const timer = setTimeout(() => {
65
+ timers.delete(timer);
66
+ socket.end(request.url === "/allowed"
67
+ ? "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n"
68
+ : "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
69
+ }, 1_200);
70
+ timers.add(timer);
71
+ });
72
+ const realtime = createSocketIoServer({ httpServer: server });
73
+ let client;
74
+ t.after(async () => {
75
+ client?.disconnect();
76
+ for (const timer of timers) clearTimeout(timer);
77
+ for (const socket of sockets) socket.destroy();
78
+ await closeSocketIoServer(realtime);
79
+ });
80
+ server.listen(0, "127.0.0.1");
81
+ await once(server, "listening");
82
+ const origin = `http://127.0.0.1:${server.address().port}`;
83
+ for (const [pathname, status] of [["/allowed", 101], ["/denied", 403]]) {
84
+ const response = await new Promise((resolve, reject) => {
85
+ const request = get(`${origin}${pathname}`, {
86
+ headers: { Connection: "Upgrade", Upgrade: "websocket" }
87
+ });
88
+ request.on("error", reject);
89
+ request.on("response", (reply) => { reply.resume(); resolve(reply.statusCode); });
90
+ request.on("upgrade", (reply, socket) => { socket.destroy(); resolve(reply.statusCode); });
91
+ });
92
+ assert.equal(response, status);
93
+ }
94
+ client = connectSocketIo(origin, { transports: ["websocket"], reconnection: false });
95
+ await once(client, "connect");
96
+ assert.equal(client.connected, true, "Socket.IO still owns its own upgrade route");
97
+ });
98
+
50
99
  test("createSocketIoServer falls back to fastify.server", () => {
51
100
  const fastifyServer = {
52
101
  id: "fastify-server"