Compare commits

...

14 Commits

Author SHA1 Message Date
Damien Arrachequesne
ac3df9a747 chore(release): @socket.io/postgres-emitter@0.1.1 2025-09-05 07:27:48 +02:00
Damien Arrachequesne
21fd54ece6 refactor(postgres-emitter): update compose file 2025-09-05 07:20:29 +02:00
Damien Arrachequesne
96d907b9b5 docs(postgres-emitter): add dark version of the explanation diagram 2025-09-05 07:19:07 +02:00
Damien Arrachequesne
32257b6cb8 fix(postgres-emitter): use parameterized query to send the NOTIFY command
Related:

- https://github.com/socketio/socket.io-postgres-emitter/issues/1
- https://github.com/socketio/socket.io-postgres-adapter/pull/1
2025-09-05 07:18:36 +02:00
Damien Arrachequesne
c7144920e3 Merge remote-tracking branch 'socket.io-postgres-emitter/main' into monorepo 2025-09-04 09:30:26 +02:00
Damien Arrachequesne
42480e9a7f chore: prepare migration to monorepo 2025-09-04 09:23:32 +02:00
Lou Klepner
0a8f91047c docs: fix adapter link (#2) 2025-09-04 09:22:40 +02:00
Damien Arrachequesne
a66ed68506 docs(protocol): add test with cancelled request
Related: 8f1ea3d58f
2025-09-03 09:02:44 +02:00
Damien Arrachequesne
3be6481d9d ci: pin Node.js 22 version
Related: https://github.com/nodejs/node/issues/59364
2025-08-09 09:31:12 +02:00
Wang Guan
be13cca94c refactor: improve type annotations and comments (#5364) 2025-08-09 08:43:34 +02:00
Damien Arrachequesne
e95f6abf93 docs: fix message handler latency in test suites
Related: https://github.com/socketio/socket.io-protocol/issues/32
2025-03-28 21:29:20 +01:00
Damien Arrachequesne
1f8a6c4ecb docs: add link to related packages 2021-06-14 08:21:28 +02:00
Damien Arrachequesne
eb01ff5803 chore(release): 0.1.0 2021-06-14 08:02:20 +02:00
Damien Arrachequesne
f2e3d162ab Initial commit 2021-06-14 07:59:12 +02:00
34 changed files with 3063 additions and 194 deletions

View File

@@ -22,7 +22,7 @@ jobs:
node-version:
- 18
- 20
- 22
- 22.17.1 # experimental TS type striping in 22.18.0 breaks the build
services:
redis:
@@ -35,6 +35,18 @@ jobs:
ports:
- 6379:6379
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: changeit
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout repository
uses: actions/checkout@v4

View File

@@ -17,16 +17,35 @@ function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function createWebSocket(url) {
const socket = new WebSocket(url);
socket._eventBuffer = {};
socket._pendingPromises = {};
for (const eventType of ["open", "close", "message"]) {
socket._eventBuffer[eventType] = [];
socket._pendingPromises[eventType] = [];
socket.addEventListener(eventType, (event) => {
if (socket._pendingPromises[eventType].length) {
socket._pendingPromises[eventType].shift()(event);
} else {
socket._eventBuffer[eventType].push(event);
}
});
}
return socket;
}
function waitFor(socket, eventType) {
return new Promise((resolve) => {
socket.addEventListener(
eventType,
(event) => {
resolve(event);
},
{ once: true }
);
});
if (socket._eventBuffer[eventType].length) {
return Promise.resolve(socket._eventBuffer[eventType].shift());
} else {
return new Promise((resolve) => {
socket._pendingPromises[eventType].push(resolve);
});
}
}
async function initLongPollingSession() {
@@ -110,7 +129,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("successfully opens a session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -137,7 +156,7 @@ describe("Engine.IO protocol", () => {
});
it("fails with an invalid 'EIO' query parameter", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?transport=websocket`
);
@@ -145,9 +164,9 @@ describe("Engine.IO protocol", () => {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=abc&transport=websocket`
);
@@ -155,19 +174,19 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
it("fails with an invalid 'transport' query parameter", async () => {
const socket = new WebSocket(`${WS_URL}/engine.io/?EIO=4`);
const socket = createWebSocket(`${WS_URL}/engine.io/?EIO=4`);
if (isNodejs) {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=abc`
);
@@ -175,7 +194,7 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
});
});
@@ -313,11 +332,30 @@ describe("Engine.IO protocol", () => {
expect(pollResponse.status).to.eql(400);
});
it("closes the session upon cancelled polling request", async () => {
const sid = await initLongPollingSession();
const controller = new AbortController();
fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, {
signal: controller.signal,
}).catch(() => {});
await sleep(5);
controller.abort();
const pollResponse = await fetch(
`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`,
);
expect(pollResponse.status).to.eql(400);
});
});
describe("WebSocket", () => {
it("sends and receives a plain text packet", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -335,7 +373,7 @@ describe("Engine.IO protocol", () => {
});
it("sends and receives a binary packet", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
socket.binaryType = "arraybuffer";
@@ -352,7 +390,7 @@ describe("Engine.IO protocol", () => {
});
it("closes the session upon invalid packet format", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -412,7 +450,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("sends ping/pong packets", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -430,7 +468,7 @@ describe("Engine.IO protocol", () => {
});
it("closes the session upon ping timeout", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -468,7 +506,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("forcefully closes the session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket`
);
@@ -485,7 +523,7 @@ describe("Engine.IO protocol", () => {
it("successfully upgrades from HTTP long-polling to WebSocket", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -521,12 +559,13 @@ describe("Engine.IO protocol", () => {
it("ignores HTTP requests with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const pollResponse = await fetch(
@@ -545,15 +584,16 @@ describe("Engine.IO protocol", () => {
it("ignores WebSocket connection with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}`
);

View File

@@ -17,16 +17,35 @@ function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function createWebSocket(url) {
const socket = new WebSocket(url);
socket._eventBuffer = {};
socket._pendingPromises = {};
for (const eventType of ["open", "close", "message"]) {
socket._eventBuffer[eventType] = [];
socket._pendingPromises[eventType] = [];
socket.addEventListener(eventType, (event) => {
if (socket._pendingPromises[eventType].length) {
socket._pendingPromises[eventType].shift()(event);
} else {
socket._eventBuffer[eventType].push(event);
}
});
}
return socket;
}
function waitFor(socket, eventType) {
return new Promise((resolve) => {
socket.addEventListener(
eventType,
(event) => {
resolve(event);
},
{ once: true }
);
});
if (socket._eventBuffer[eventType].length) {
return Promise.resolve(socket._eventBuffer[eventType].shift());
} else {
return new Promise((resolve) => {
socket._pendingPromises[eventType].push(resolve);
});
}
}
function waitForPackets(socket, count) {
@@ -55,7 +74,7 @@ async function initLongPollingSession() {
}
async function initSocketIOConnection() {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
socket.binaryType = "arraybuffer";
@@ -145,7 +164,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should successfully open a session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -172,7 +191,7 @@ describe("Engine.IO protocol", () => {
});
it("should fail with an invalid 'EIO' query parameter", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?transport=websocket`
);
@@ -180,9 +199,9 @@ describe("Engine.IO protocol", () => {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=abc&transport=websocket`
);
@@ -190,19 +209,19 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
it("should fail with an invalid 'transport' query parameter", async () => {
const socket = new WebSocket(`${WS_URL}/socket.io/?EIO=4`);
const socket = createWebSocket(`${WS_URL}/socket.io/?EIO=4`);
if (isNodejs) {
socket.on("error", () => {});
}
waitFor(socket, "close");
await waitFor(socket, "close");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=abc`
);
@@ -210,7 +229,7 @@ describe("Engine.IO protocol", () => {
socket2.on("error", () => {});
}
waitFor(socket2, "close");
await waitFor(socket2, "close");
});
});
});
@@ -260,7 +279,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should send ping/pong packets", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -278,7 +297,7 @@ describe("Engine.IO protocol", () => {
});
it("should close the session upon ping timeout", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -316,7 +335,7 @@ describe("Engine.IO protocol", () => {
describe("WebSocket", () => {
it("should forcefully close the session", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -333,7 +352,7 @@ describe("Engine.IO protocol", () => {
it("should successfully upgrade from HTTP long-polling to WebSocket", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -353,12 +372,13 @@ describe("Engine.IO protocol", () => {
it("should ignore HTTP requests with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const pollResponse = await fetch(
@@ -371,15 +391,16 @@ describe("Engine.IO protocol", () => {
it("should ignore WebSocket connection with same sid after upgrade", async () => {
const sid = await initLongPollingSession();
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
await waitFor(socket, "open");
socket.send("2probe");
await waitFor(socket, "message"); // "3probe"
socket.send("5");
const socket2 = new WebSocket(
const socket2 = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}`
);
@@ -391,7 +412,7 @@ describe("Engine.IO protocol", () => {
describe("Socket.IO protocol", () => {
describe("connect", () => {
it("should allow connection to the main namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -414,7 +435,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to the main namespace with a payload", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -437,7 +458,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to a custom namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -460,7 +481,7 @@ describe("Socket.IO protocol", () => {
});
it("should allow connection to a custom namespace with a payload", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -483,7 +504,7 @@ describe("Socket.IO protocol", () => {
});
it("should disallow connection to an unknown namespace", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -497,7 +518,7 @@ describe("Socket.IO protocol", () => {
});
it("should disallow connection with an invalid handshake", async () => {
const socket = new WebSocket(
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
@@ -508,10 +529,9 @@ describe("Socket.IO protocol", () => {
await waitFor(socket, "close");
});
it("should close the connection if no handshake is received", async () => {
const socket = new WebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
const socket = createWebSocket(
`${WS_URL}/socket.io/?EIO=4&transport=websocket`
);
await waitFor(socket, "close");

250
package-lock.json generated
View File

@@ -14,7 +14,8 @@
"packages/socket.io-adapter",
"packages/socket.io-parser",
"packages/socket.io-client",
"packages/socket.io"
"packages/socket.io",
"packages/socket.io-postgres-emitter"
],
"devDependencies": {
"@babel/core": "^7.24.7",
@@ -28,10 +29,12 @@
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@sinonjs/fake-timers": "^11.2.2",
"@socket.io/postgres-adapter": "^0.1.0",
"@types/debug": "^4.1.12",
"@types/expect.js": "^0.3.32",
"@types/mocha": "^10.0.7",
"@types/node": "18.15.3",
"@types/pg": "^8.6.0",
"@types/sinonjs__fake-timers": "^8.1.5",
"@wdio/cli": "^8.39.1",
"@wdio/local-runner": "^8.39.1",
@@ -54,6 +57,7 @@
"mocha": "^10.6.0",
"node-forge": "^1.3.1",
"nyc": "^17.0.0",
"pg": "^8.6.0",
"prettier": "^3.3.2",
"redis": "^4.6.15",
"rimraf": "^6.0.0",
@@ -2074,15 +2078,6 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@@ -2750,6 +2745,42 @@
"resolved": "packages/socket.io-component-emitter",
"link": true
},
"node_modules/@socket.io/postgres-adapter": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@socket.io/postgres-adapter/-/postgres-adapter-0.1.1.tgz",
"integrity": "sha512-xUWeLC1Tz771XIEoJ0iuPTRUbEg8aQdyb1CSHXAT2y+WxvtGy7XxDN5kwkGSumyfxb+Qo8RSX8ZfTmbdx+gXXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "~2.7.0",
"debug": "~4.3.1",
"socket.io-adapter": "~2.3.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@socket.io/postgres-adapter/node_modules/@msgpack/msgpack": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.7.2.tgz",
"integrity": "sha512-rYEi46+gIzufyYUAoHDnRzkWGxajpD9vVXFQ3g1vbjrBm6P7MBmm+s/fqPa46sxa+8FOUdEuRQKaugo5a4JWpw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 10"
}
},
"node_modules/@socket.io/postgres-adapter/node_modules/socket.io-adapter": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@socket.io/postgres-emitter": {
"resolved": "packages/socket.io-postgres-emitter",
"link": true
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@@ -2948,6 +2979,18 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
"node_modules/@types/pg": {
"version": "8.15.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz",
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -10927,15 +10970,6 @@
"node": ">=8"
}
},
"node_modules/nyc/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/nyc/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -11441,6 +11475,103 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@@ -11571,6 +11702,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
@@ -12443,6 +12617,16 @@
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
@@ -15507,7 +15691,7 @@
}
},
"packages/engine.io": {
"version": "6.6.3",
"version": "6.6.4",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
@@ -15676,6 +15860,32 @@
"optional": true
}
}
},
"packages/socket.io-postgres-emitter": {
"name": "@socket.io/postgres-emitter",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^2.7.0",
"debug": "~4.3.1"
}
},
"packages/socket.io-postgres-emitter/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
}
}
}

View File

@@ -10,7 +10,8 @@
"packages/socket.io-adapter",
"packages/socket.io-parser",
"packages/socket.io-client",
"packages/socket.io"
"packages/socket.io",
"packages/socket.io-postgres-emitter"
],
"overrides": {
"@types/estree": "0.0.52",
@@ -29,10 +30,12 @@
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@sinonjs/fake-timers": "^11.2.2",
"@socket.io/postgres-adapter": "^0.1.0",
"@types/debug": "^4.1.12",
"@types/expect.js": "^0.3.32",
"@types/mocha": "^10.0.7",
"@types/node": "18.15.3",
"@types/pg": "^8.6.0",
"@types/sinonjs__fake-timers": "^8.1.5",
"@wdio/cli": "^8.39.1",
"@wdio/local-runner": "^8.39.1",
@@ -55,6 +58,7 @@
"mocha": "^10.6.0",
"node-forge": "^1.3.1",
"nyc": "^17.0.0",
"pg": "^8.6.0",
"prettier": "^3.3.2",
"redis": "^4.6.15",
"rimraf": "^6.0.0",

View File

@@ -32,7 +32,11 @@ export type RawData = any;
export interface Packet {
type: PacketType;
options?: { compress: boolean };
options?: {
compress: boolean;
wsPreEncoded?: string; // deprecated in favor of `wsPreEncodedFrame`
wsPreEncodedFrame?: any; // computed in the socket.io-adapter package (should be typed as Buffer)
};
data?: RawData;
}

View File

@@ -1,27 +1,36 @@
import { createServer } from "http";
import { createServer, Server as HttpServer } from "http";
import { Server, AttachOptions, ServerOptions } from "./server";
import transports from "./transports/index";
import * as parser from "engine.io-parser";
export { Server, transports, listen, attach, parser };
export type { AttachOptions, ServerOptions, BaseServer } from "./server";
export type {
AttachOptions,
ServerOptions,
BaseServer,
ErrorCallback,
} from "./server";
export { uServer } from "./userver";
export { Socket } from "./socket";
export { Transport } from "./transport";
export const protocol = parser.protocol;
/**
* Creates an http.Server exclusively used for WS upgrades.
* Creates an http.Server exclusively used for WS upgrades, and starts listening.
*
* @param {Number} port
* @param {Function} callback
* @param {Object} options
* @return {Server} websocket.io server
* @param port
* @param options
* @param listenCallback - callback for http.Server.listen()
* @return engine.io server
*/
function listen(port, options: AttachOptions & ServerOptions, fn) {
function listen(
port: number,
options?: AttachOptions & ServerOptions,
listenCallback?: () => void,
): Server {
if ("function" === typeof options) {
fn = options;
listenCallback = options;
options = {};
}
@@ -34,7 +43,7 @@ function listen(port, options: AttachOptions & ServerOptions, fn) {
const engine = attach(server, options);
engine.httpServer = server;
server.listen(port, fn);
server.listen(port, listenCallback);
return engine;
}
@@ -42,12 +51,15 @@ function listen(port, options: AttachOptions & ServerOptions, fn) {
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @return {Server} engine server
* @param server
* @param options
* @return engine.io server
*/
function attach(server, options: AttachOptions & ServerOptions) {
function attach(
server: HttpServer,
options: AttachOptions & ServerOptions,
): Server {
const engine = new Server(options);
engine.attach(server, options);
return engine;

View File

@@ -59,8 +59,7 @@ const EMPTY_BUFFER = Buffer.concat([]);
*
* @api private
*/
export function encodePacket (packet, supportsBinary, utf8encode, callback) {
export function encodePacket (packet: any, supportsBinary?: any, utf8encode?: any, callback?: any) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary;
supportsBinary = null;
@@ -86,7 +85,7 @@ export function encodePacket (packet, supportsBinary, utf8encode, callback) {
}
return callback('' + encoded);
};
}
/**
* Encode Buffer data
@@ -120,16 +119,16 @@ export function encodeBase64Packet (packet, callback){
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return {Object} with `type` and `data` (if any)
* @return {import('engine.io-parser').Packet} with `type` and `data` (if any)
* @api private
*/
export function decodePacket (data, binaryType, utf8decode) {
export function decodePacket (data: any, binaryType?: any, utf8decode?: any): any {
if (data === undefined) {
return err;
}
var type;
let type: string | number;
// String data
if (typeof data === 'string') {
@@ -147,6 +146,7 @@ export function decodePacket (data, binaryType, utf8decode) {
}
}
// @ts-expect-error
if (Number(type) != type || !packetslist[type]) {
return err;
}
@@ -274,7 +274,7 @@ function map(ary, each, done) {
* @api public
*/
export function decodePayload (data, binaryType, callback) {
export function decodePayload (data: any, binaryType?: any, callback?: any) {
if (typeof data !== 'string') {
return decodePayloadAsBinary(data, binaryType, callback);
}

View File

@@ -6,7 +6,12 @@ import { EventEmitter } from "events";
import { Socket } from "./socket";
import debugModule from "debug";
import { serialize } from "cookie";
import { Server as DEFAULT_WS_ENGINE } from "ws";
import {
Server as DEFAULT_WS_ENGINE,
type Server as WsServer,
type PerMessageDeflateOptions,
type WebSocket as WsWebSocket,
} from "ws";
import type {
IncomingMessage,
Server as HttpServer,
@@ -16,14 +21,19 @@ import type { CorsOptions, CorsOptionsDelegate } from "cors";
import type { Duplex } from "stream";
import { WebTransport } from "./transports/webtransport";
import { createPacketDecoderStream } from "engine.io-parser";
import type { EngineRequest } from "./transport";
import type { EngineRequest, Transport } from "./transport";
import type { CookieSerializeOptions } from "./contrib/types.cookie";
const debug = debugModule("engine");
const kResponseHeaders = Symbol("responseHeaders");
type Transport = "polling" | "websocket" | "webtransport";
type TransportName = "polling" | "websocket" | "webtransport";
export type ErrorCallback = (
errorCode?: (typeof Server.errors)[keyof typeof Server.errors],
errorContext?: Record<string, unknown> & { name?: string; message?: string },
) => void;
export interface AttachOptions {
/**
@@ -90,7 +100,7 @@ export interface ServerOptions {
*
* @default ["polling", "websocket"]
*/
transports?: Transport[];
transports?: TransportName[];
/**
* whether to allow transport upgrades
* @default true
@@ -100,7 +110,7 @@ export interface ServerOptions {
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate?: boolean | object;
perMessageDeflate?: boolean | PerMessageDeflateOptions;
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
@@ -149,7 +159,7 @@ type Middleware = (
next: (err?: any) => void,
) => void;
function parseSessionId(data: string) {
function parseSessionId(data: string): string | undefined {
try {
const parsed = JSON.parse(data);
if (typeof parsed.sid === "string") {
@@ -224,7 +234,7 @@ export abstract class BaseServer extends EventEmitter {
this.init();
}
protected abstract init();
protected abstract init(): void;
/**
* Compute the pathname of the requests that are handled by the server
@@ -244,10 +254,8 @@ export abstract class BaseServer extends EventEmitter {
/**
* Returns a list of available transports for upgrade given a certain transport.
*
* @return {Array}
*/
public upgrades(transport: string) {
public upgrades(transport: TransportName): string[] {
if (!this.opts.allowUpgrades) return [];
return transports[transport].upgradesTo || [];
}
@@ -259,17 +267,18 @@ export abstract class BaseServer extends EventEmitter {
* @param upgrade - whether it's an upgrade request
* @param fn
* @protected
* @return whether the request is valid
*/
protected verify(
req: any,
req: EngineRequest,
upgrade: boolean,
fn: (errorCode?: number, errorContext?: any) => void,
) {
fn: ErrorCallback,
): void | boolean {
// transport check
const transport = req._query.transport;
// WebTransport does not go through the verify() method, see the onWebTransportSession() method
if (
!~this.opts.transports.indexOf(transport) ||
!~this.opts.transports.indexOf(transport as TransportName) ||
transport === "webtransport"
) {
debug('unknown transport "%s"', transport);
@@ -408,7 +417,7 @@ export abstract class BaseServer extends EventEmitter {
*
* @param {IncomingMessage} req - the request object
*/
public generateId(req: IncomingMessage) {
public generateId(req: IncomingMessage): string | PromiseLike<string> {
return base64id.generateId();
}
@@ -422,9 +431,9 @@ export abstract class BaseServer extends EventEmitter {
* @protected
*/
protected async handshake(
transportName: string,
req: any,
closeConnection: (errorCode?: number, errorContext?: any) => void,
transportName: TransportName,
req: EngineRequest,
closeConnection: ErrorCallback,
) {
const protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
@@ -600,7 +609,10 @@ export abstract class BaseServer extends EventEmitter {
}
}
protected abstract createTransport(transportName, req);
protected abstract createTransport(
transportName: TransportName,
req: EngineRequest,
);
/**
* Protocol errors mappings.
@@ -613,7 +625,7 @@ export abstract class BaseServer extends EventEmitter {
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5,
};
} as const;
static errorMessages = {
0: "Transport unknown",
@@ -622,7 +634,7 @@ export abstract class BaseServer extends EventEmitter {
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version",
};
} as const;
}
/**
@@ -667,7 +679,7 @@ class WebSocketResponse {
*/
export class Server extends BaseServer {
public httpServer?: HttpServer;
private ws: any;
private ws: WsServer;
/**
* Initialize websocket server
@@ -687,7 +699,7 @@ export class Server extends BaseServer {
});
if (typeof this.ws.on === "function") {
this.ws.on("headers", (headersArray, req) => {
this.ws.on("headers", (headersArray, req: EngineRequest) => {
// note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// we could also try to parse the array and then sync the values, but that will be error-prone
const additionalHeaders = req[kResponseHeaders] || {};
@@ -730,7 +742,11 @@ export class Server extends BaseServer {
}
}
protected createTransport(transportName: string, req: IncomingMessage) {
protected createTransport(
transportName: TransportName,
req: IncomingMessage,
): Transport {
// @ts-expect-error 'polling' is a plain function used as constructor
return new transports[transportName](req);
}
@@ -745,7 +761,7 @@ export class Server extends BaseServer {
this.prepare(req);
req.res = res;
const callback = (errorCode, errorContext) => {
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
@@ -763,7 +779,11 @@ export class Server extends BaseServer {
} else {
const closeConnection = (errorCode, errorContext) =>
abortRequest(res, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport as TransportName,
req,
closeConnection,
);
}
};
@@ -787,7 +807,7 @@ export class Server extends BaseServer {
this.prepare(req);
const res = new WebSocketResponse(req, socket);
const callback = (errorCode, errorContext) => {
const callback: ErrorCallback = (errorCode, errorContext) => {
if (errorCode !== undefined) {
this.emit("connection_error", {
req,
@@ -823,11 +843,16 @@ export class Server extends BaseServer {
/**
* Called upon a ws.io connection.
*
* @param {ws.Socket} websocket
* @param req
* @param socket
* @param websocket
* @private
*/
private onWebSocket(req, socket, websocket) {
private onWebSocket(
req: EngineRequest,
socket: Duplex,
websocket: WsWebSocket,
) {
websocket.on("error", onUpgradeError);
if (
@@ -862,14 +887,22 @@ export class Server extends BaseServer {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError);
const transport = this.createTransport(req._query.transport, req);
const transport = this.createTransport(
req._query.transport as TransportName,
req,
);
// @ts-expect-error this option is only for WebSocket impl
transport.perMessageDeflate = this.opts.perMessageDeflate;
client._maybeUpgrade(transport);
}
} else {
const closeConnection = (errorCode, errorContext) =>
abortUpgrade(socket, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport as TransportName,
req,
closeConnection,
);
}
function onUpgradeError() {
@@ -947,7 +980,11 @@ export class Server extends BaseServer {
* @private
*/
function abortRequest(res, errorCode, errorContext) {
function abortRequest(
res: ServerResponse,
errorCode: number,
errorContext?: { message?: string },
) {
const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400;
const message =
errorContext && errorContext.message
@@ -1030,7 +1067,7 @@ const validHdrChars = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255
]
function checkInvalidHeaderChar(val) {
function checkInvalidHeaderChar(val?: string) {
val += "";
if (val.length < 1) return false;
if (!validHdrChars[val.charCodeAt(0)]) {

View File

@@ -5,6 +5,7 @@ import type { EngineRequest, Transport } from "./transport";
import type { BaseServer } from "./server";
import { setTimeout, clearTimeout } from "timers";
import type { Packet, PacketType, RawData } from "engine.io-parser";
import type transports from "./transports";
const debug = debugModule("engine:socket");
@@ -537,9 +538,11 @@ export class Socket extends EventEmitter {
*/
private getAvailableUpgrades() {
const availableUpgrades = [];
const allUpgrades = this.server.upgrades(this.transport.name);
const allUpgrades = this.server.upgrades(
this.transport.name as keyof typeof transports,
);
for (let i = 0; i < allUpgrades.length; ++i) {
const upg = allUpgrades[i];
const upg = allUpgrades[i] as keyof typeof transports;
if (this.server.opts.transports.indexOf(upg) !== -1) {
availableUpgrades.push(upg);
}

View File

@@ -4,6 +4,7 @@ import * as parser_v3 from "./parser-v3/index";
import debugModule from "debug";
import type { IncomingMessage, ServerResponse } from "http";
import { Packet, RawData } from "engine.io-parser";
import type { WebSocket } from "ws";
const debug = debugModule("engine:transport");
@@ -15,7 +16,11 @@ export type EngineRequest = IncomingMessage & {
_query: Record<string, string>;
res?: ServerResponse;
cleanup?: Function;
websocket?: any;
websocket?: WebSocket & {
_socket?: {
remoteAddress: string;
};
};
};
export abstract class Transport extends EventEmitter {
@@ -37,7 +42,7 @@ export abstract class Transport extends EventEmitter {
*
* @see https://github.com/socketio/engine.io-protocol
*/
public protocol: number;
public protocol: 3 | 4;
/**
* The current state of the transport.
@@ -53,7 +58,7 @@ export abstract class Transport extends EventEmitter {
* The parser to use (depends on the revision of the {@link Transport#protocol}.
* @protected
*/
protected parser: any;
protected parser: typeof parser_v4 | typeof parser_v3;
/**
* Whether the transport supports binary payloads (else it will be base64-encoded)
* @protected
@@ -74,6 +79,11 @@ export abstract class Transport extends EventEmitter {
this._readyState = state;
}
/**
* The list of transports this transport can be upgraded to.
*/
static upgradesTo: string[] = [];
/**
* Transport constructor.
*
@@ -148,7 +158,7 @@ export abstract class Transport extends EventEmitter {
/**
* Called with the encoded packet data.
*
* @param {String} data
* @param data
* @protected
*/
protected onData(data: RawData) {

View File

@@ -3,6 +3,8 @@ import { createGzip, createDeflate } from "zlib";
import * as accepts from "accepts";
import debugModule from "debug";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
import type * as parser_v4 from "engine.io-parser";
import type * as parser_v3 from "../parser-v3/index";
const debug = debugModule("engine:polling");
@@ -228,9 +230,9 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.decodePayload(data, callback);
(this.parser as typeof parser_v3).decodePayload(data, callback);
} else {
this.parser.decodePayload(data).forEach(callback);
(this.parser as typeof parser_v4).decodePayload(data).forEach(callback);
}
}
@@ -263,7 +265,7 @@ export class Polling extends Transport {
this.shouldClose = null;
}
const doWrite = (data) => {
const doWrite = (data: string) => {
const compress = packets.some((packet) => {
return packet.options && packet.options.compress;
});
@@ -271,9 +273,13 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.encodePayload(packets, this.supportsBinary, doWrite);
(this.parser as typeof parser_v3).encodePayload(
packets,
this.supportsBinary,
doWrite,
);
} else {
this.parser.encodePayload(packets, doWrite);
(this.parser as typeof parser_v4).encodePayload(packets, doWrite);
}
}

View File

@@ -2,9 +2,10 @@ import { Polling as XHR } from "./polling";
import { JSONP } from "./polling-jsonp";
import { WebSocket } from "./websocket";
import { WebTransport } from "./webtransport";
import type { EngineRequest } from "../transport";
export default {
polling: polling,
polling,
websocket: WebSocket,
webtransport: WebTransport,
};
@@ -12,8 +13,7 @@ export default {
/**
* Polling polymorphic constructor.
*/
function polling(req) {
function polling(req: EngineRequest) {
if ("string" === typeof req._query.j) {
return new JSONP(req);
} else {

View File

@@ -4,6 +4,8 @@ import * as accepts from "accepts";
import debugModule from "debug";
import type { IncomingMessage, ServerResponse } from "http";
import type { Packet, RawData } from "engine.io-parser";
import type * as parser_v4 from "engine.io-parser";
import type * as parser_v3 from "../parser-v3/index";
const debug = debugModule("engine:polling");
@@ -196,9 +198,9 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.decodePayload(data, callback);
(this.parser as typeof parser_v3).decodePayload(data, callback);
} else {
this.parser.decodePayload(data).forEach(callback);
(this.parser as typeof parser_v4).decodePayload(data).forEach(callback);
}
}
@@ -225,7 +227,7 @@ export class Polling extends Transport {
this.shouldClose = null;
}
const doWrite = (data) => {
const doWrite = (data: string) => {
const compress = packets.some((packet) => {
return packet.options && packet.options.compress;
});
@@ -233,9 +235,13 @@ export class Polling extends Transport {
};
if (this.protocol === 3) {
this.parser.encodePayload(packets, this.supportsBinary, doWrite);
(this.parser as typeof parser_v3).encodePayload(
packets,
this.supportsBinary,
doWrite,
);
} else {
this.parser.encodePayload(packets, doWrite);
(this.parser as typeof parser_v4).encodePayload(packets, doWrite);
}
}

View File

@@ -1,12 +1,13 @@
import { EngineRequest, Transport } from "../transport";
import debugModule from "debug";
import type { Packet, RawData } from "engine.io-parser";
import type { PerMessageDeflateOptions, WebSocket as WsWebSocket } from "ws";
const debug = debugModule("engine:ws");
export class WebSocket extends Transport {
protected perMessageDeflate: any;
private socket: any;
perMessageDeflate?: boolean | PerMessageDeflateOptions;
private socket: WsWebSocket;
/**
* WebSocket transport
@@ -51,8 +52,8 @@ export class WebSocket extends Transport {
if (this._canSendPreEncodedFrame(packet)) {
// the WebSocket frame was computed with WebSocket.Sender.frame()
// see https://github.com/websockets/ws/issues/617#issuecomment-283002469
// @ts-expect-error use of untyped member
this.socket._sender.sendFrame(
// @ts-ignore
packet.options.wsPreEncodedFrame,
isLast ? this._onSentLast : this._onSent,
);
@@ -74,8 +75,8 @@ export class WebSocket extends Transport {
private _canSendPreEncodedFrame(packet: Packet) {
return (
!this.perMessageDeflate &&
// @ts-expect-error use of untyped member
typeof this.socket?._sender?.sendFrame === "function" &&
// @ts-ignore
packet.options?.wsPreEncodedFrame !== undefined
);
}

View File

@@ -2,6 +2,7 @@ import debugModule from "debug";
import { AttachOptions, BaseServer, Server } from "./server";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import transports from "./transports-uws";
import { EngineRequest } from "./transport";
const debug = debugModule("engine:uws");
@@ -36,7 +37,7 @@ export class uServer extends BaseServer {
*
* @private
*/
private prepare(req, res: HttpResponse) {
private prepare(req: HttpRequest & EngineRequest, res: HttpResponse) {
req.method = req.getMethod().toUpperCase();
req.url = req.getUrl();
@@ -48,6 +49,7 @@ export class uServer extends BaseServer {
req.headers[key] = value;
});
// @ts-expect-error
req.connection = {
remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(),
};
@@ -57,7 +59,7 @@ export class uServer extends BaseServer {
});
}
protected createTransport(transportName, req) {
protected createTransport(transportName: string, req: EngineRequest) {
return new transports[transportName](req);
}
@@ -123,7 +125,7 @@ export class uServer extends BaseServer {
req: HttpRequest & { res: any; _query: any },
) {
debug('handling "%s" http request "%s"', req.getMethod(), req.getUrl());
this.prepare(req, res);
this.prepare(req as unknown as HttpRequest & EngineRequest, res);
req.res = res;
@@ -146,7 +148,11 @@ export class uServer extends BaseServer {
} else {
const closeConnection = (errorCode, errorContext) =>
this.abortRequest(res, errorCode, errorContext);
this.handshake(req._query.transport, req, closeConnection);
this.handshake(
req._query.transport,
req as unknown as EngineRequest,
closeConnection,
);
}
};
@@ -154,7 +160,11 @@ export class uServer extends BaseServer {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, false, callback);
this.verify(
req as unknown as HttpRequest & EngineRequest,
false,
callback,
);
}
});
}
@@ -166,7 +176,7 @@ export class uServer extends BaseServer {
) {
debug("on upgrade");
this.prepare(req, res);
this.prepare(req as unknown as HttpRequest & EngineRequest, res);
req.res = res;
@@ -198,13 +208,16 @@ export class uServer extends BaseServer {
return res.close();
} else {
debug("upgrading existing transport");
transport = this.createTransport(req._query.transport, req);
transport = this.createTransport(
req._query.transport,
req as unknown as EngineRequest,
);
client._maybeUpgrade(transport);
}
} else {
transport = await this.handshake(
req._query.transport,
req,
req as unknown as EngineRequest,
(errorCode, errorContext) =>
this.abortRequest(res, errorCode, errorContext),
);
@@ -231,15 +244,15 @@ export class uServer extends BaseServer {
if (err) {
callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" });
} else {
this.verify(req, true, callback);
this.verify(req as unknown as EngineRequest, true, callback);
}
});
}
private abortRequest(
res: HttpResponse | ResponseWrapper,
errorCode,
errorContext,
errorCode: number,
errorContext?: { message?: string },
) {
const statusCode =
errorCode === Server.errors.FORBIDDEN

View File

@@ -1,6 +1,6 @@
import { EventEmitter } from "events";
import { yeast } from "./contrib/yeast";
import WebSocket = require("ws");
import * as WebSocket from "ws";
// @ts-expect-error
const canPreComputeFrame = typeof WebSocket?.Sender?.frame === "function";
@@ -51,11 +51,11 @@ export class Adapter extends EventEmitter {
/**
* In-memory adapter constructor.
*
* @param {Namespace} nsp
* @param nsp
*/
constructor(readonly nsp: any) {
super();
this.encoder = nsp.server.encoder;
this.encoder = nsp.server.encoder; // nsp is a Namespace object
}
/**

View File

@@ -1,4 +1,10 @@
import { Server, type ServerOptions, Socket, type Transport } from "engine.io";
import {
Server,
type ServerOptions,
type ErrorCallback,
Socket,
type Transport,
} from "engine.io";
import { randomBytes } from "node:crypto";
import { setTimeout, clearTimeout } from "node:timers";
import { type IncomingMessage } from "node:http";
@@ -392,9 +398,9 @@ export abstract class ClusterEngine extends Server {
override verify(
req: IncomingMessage & { _query: Record<string, string> },
upgrade: boolean,
fn: (errorCode?: number, context?: any) => void,
fn: ErrorCallback,
): void {
super.verify(req, upgrade, (errorCode: number, errorContext: any) => {
super.verify(req, upgrade, (errorCode, errorContext) => {
if (errorCode !== Server.errors.UNKNOWN_SID) {
return fn(errorCode, errorContext);
}
@@ -412,7 +418,7 @@ export abstract class ClusterEngine extends Server {
req[kSenderId] = senderId;
fn();
} else {
const transport = this.createTransport(transportName, req);
const transport = this.createTransport(transportName as any, req);
this._hookTransport(sid, transport, lockType, senderId);
transport.onRequest(req);
}

View File

@@ -0,0 +1,19 @@
# History
| Version | Release date |
|--------------------------|----------------|
| [0.1.1](#011-2025-09-05) | September 2025 |
| [0.1.0](#010-2021-06-14) | June 2021 |
# Release notes
## 0.1.1 (2025-09-05)
### Bug Fixes
* use parameterized query to send the NOTIFY command ([32257b6](https://github.com/socketio/socket.io/commit/32257b6cb89f9dac15e69e1d6ee76365ff262170))
## 0.1.0 (2021-06-14)
Initial release!

View File

@@ -0,0 +1,7 @@
Copyright (c) 2021 Damien Arrachequesne (@darrachequesne)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,152 @@
# Socket.IO Postgres emitter
The `@socket.io/postgres-emitter` package allows you to easily communicate with a group of Socket.IO servers from another Node.js process (server-side).
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/emitter_dark.png">
<img alt="Diagram of Socket.IO packets forwarded through PostgreSQL" src="./assets/emitter.png">
</picture>
It must be used in conjunction with [`@socket.io/postgres-adapter`](https://github.com/socketio/socket.io-postgres-adapter/).
Supported features:
- [broadcasting](https://socket.io/docs/v4/broadcasting-events/)
- [utility methods](https://socket.io/docs/v4/server-instance/#Utility-methods)
- [`socketsJoin`](https://socket.io/docs/v4/server-instance/#socketsJoin)
- [`socketsLeave`](https://socket.io/docs/v4/server-instance/#socketsLeave)
- [`disconnectSockets`](https://socket.io/docs/v4/server-instance/#disconnectSockets)
- [`serverSideEmit`](https://socket.io/docs/v4/server-instance/#serverSideEmit)
Related packages:
- Postgres adapter: https://github.com/socketio/socket.io-postgres-adapter/
- Redis adapter: https://github.com/socketio/socket.io-redis-adapter/
- Redis emitter: https://github.com/socketio/socket.io-redis-emitter/
- MongoDB adapter: https://github.com/socketio/socket.io-mongo-adapter/
- MongoDB emitter: https://github.com/socketio/socket.io-mongo-emitter/
**Table of contents**
- [Installation](#installation)
- [Usage](#usage)
- [API](#api)
- [Known errors](#known-errors)
- [License](#license)
## Installation
```
npm install @socket.io/postgres-emitter pg
```
For TypeScript users, you might also need `@types/pg`.
## Usage
```js
const { Emitter } = require("@socket.io/postgres-emitter");
const { Pool } = require("pg");
const pool = new Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
const io = new Emitter(pool);
setInterval(() => {
io.emit("ping", new Date());
}, 1000);
```
## API
### `Emitter(pool[, nsp][, opts])`
```js
const io = new Emitter(pool);
```
The `pool` argument is a [Pool object](https://node-postgres.com/api/pool) from the `pg` package.
### `Emitter#to(room:string):BroadcastOperator`
### `Emitter#in(room:string):BroadcastOperator`
Specifies a specific `room` that you want to emit to.
```js
io.to("room1").emit("hello");
```
### `Emitter#except(room:string):BroadcastOperator`
Specifies a specific `room` that you want to exclude from broadcasting.
```js
io.except("room2").emit("hello");
```
### `Emitter#of(namespace:string):Emitter`
Specifies a specific namespace that you want to emit to.
```js
const customNamespace = io.of("/custom");
customNamespace.emit("hello");
```
### `Emitter#socketsJoin(rooms:string|string[])`
Makes the matching socket instances join the specified rooms:
```js
// make all Socket instances join the "room1" room
io.socketsJoin("room1");
// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
```
### `Emitter#socketsLeave(rooms:string|string[])`
Makes the matching socket instances leave the specified rooms:
```js
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");
// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
```
### `Emitter#disconnectSockets(close:boolean)`
Makes the matching socket instances disconnect:
```js
// make all Socket instances disconnect
io.disconnectSockets();
// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
// this also works with a single socket ID
io.of("/admin").in(theSocketId).disconnectSockets();
```
### `Emitter#serverSideEmit(ev:string[,...args:any[]])`
Emits an event that will be received by each Socket.IO server of the cluster.
```js
io.serverSideEmit("ping");
```
## License
[MIT](LICENSE)

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,7 @@
services:
postgres:
image: postgres:14
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "changeit"

View File

@@ -0,0 +1,536 @@
import debugModule from "debug";
import type {
DefaultEventsMap,
EventNames,
EventParams,
EventsMap,
TypedEventBroadcaster,
} from "./typed-events";
import { encode } from "@msgpack/msgpack";
const debug = debugModule("socket.io-postgres-emitter");
const EMITTER_UID = "emitter";
const hasBinary = (obj: any, toJSON?: boolean): boolean => {
if (!obj || typeof obj !== "object") {
return false;
}
if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
return true;
}
if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) {
return true;
}
}
return false;
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true;
}
}
if (obj.toJSON && typeof obj.toJSON === "function" && !toJSON) {
return hasBinary(obj.toJSON(), true);
}
return false;
};
/**
* Event types, for messages between nodes
*/
enum EventType {
INITIAL_HEARTBEAT = 1,
HEARTBEAT,
BROADCAST,
SOCKETS_JOIN,
SOCKETS_LEAVE,
DISCONNECT_SOCKETS,
FETCH_SOCKETS,
FETCH_SOCKETS_RESPONSE,
SERVER_SIDE_EMIT,
SERVER_SIDE_EMIT_RESPONSE,
}
interface BroadcastFlags {
volatile?: boolean;
compress?: boolean;
}
export interface PostgresEmitterOptions {
/**
* The prefix of the notification channel
* @default "socket.io"
*/
channelPrefix: string;
/**
* The name of the table for payloads over the 8000 bytes limit or containing binary data
*/
tableName: string;
/**
* The threshold for the payload size in bytes (see https://www.postgresql.org/docs/current/sql-notify.html)
* @default 8000
*/
payloadThreshold: number;
}
export class Emitter<
EmitEvents extends EventsMap = DefaultEventsMap,
ServerSideEvents extends EventsMap = DefaultEventsMap,
> {
public readonly channel: string;
public readonly tableName: string;
public payloadThreshold: number;
constructor(
readonly pool: any,
readonly nsp: string = "/",
opts: Partial<PostgresEmitterOptions> = {},
) {
const channelPrefix = opts.channelPrefix || "socket.io";
this.channel = `${channelPrefix}#${nsp}`;
this.tableName = opts.tableName || "socket_io_attachments";
this.payloadThreshold = opts.payloadThreshold || 8000;
}
/**
* Return a new emitter for the given namespace.
*
* @param nsp - namespace
* @public
*/
public of(nsp: string): Emitter {
return new Emitter(this.pool, (nsp[0] !== "/" ? "/" : "") + nsp);
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
return new BroadcastOperator<EmitEvents, ServerSideEvents>(this).emit(
ev,
...args,
);
}
/**
* Targets a room when emitting.
*
* @param room
* @return BroadcastOperator
* @public
*/
public to(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
return new BroadcastOperator(this).to(room);
}
/**
* Targets a room when emitting.
*
* @param room
* @return BroadcastOperator
* @public
*/
public in(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
return new BroadcastOperator(this).in(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return BroadcastOperator
* @public
*/
public except(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
return new BroadcastOperator(this).except(room);
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return BroadcastOperator
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents, ServerSideEvents> {
return new BroadcastOperator(this).volatile;
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return BroadcastOperator
* @public
*/
public compress(
compress: boolean,
): BroadcastOperator<EmitEvents, ServerSideEvents> {
return new BroadcastOperator(this).compress(compress);
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param rooms
* @public
*/
public socketsJoin(rooms: string | string[]): void {
return new BroadcastOperator(this).socketsJoin(rooms);
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param rooms
* @public
*/
public socketsLeave(rooms: string | string[]): void {
return new BroadcastOperator(this).socketsLeave(rooms);
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this).disconnectSockets(close);
}
/**
* Send a packet to the Socket.IO servers in the cluster
*
* @param ev - the event name
* @param args - any number of serializable arguments
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): void {
return new BroadcastOperator<EmitEvents, ServerSideEvents>(
this,
).serverSideEmit(ev, ...args);
}
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set(<const>[
"connect",
"connect_error",
"disconnect",
"disconnecting",
"newListener",
"removeListener",
]);
export class BroadcastOperator<
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
> implements TypedEventBroadcaster<EmitEvents>
{
constructor(
private readonly emitter: Emitter,
private readonly rooms: Set<string> = new Set<string>(),
private readonly exceptRooms: Set<string> = new Set<string>(),
private readonly flags: BroadcastFlags = {},
) {}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public to(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room);
}
return new BroadcastOperator(
this.emitter,
rooms,
this.exceptRooms,
this.flags,
);
}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public in(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
return this.to(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public except(
room: string | string[],
): BroadcastOperator<EmitEvents, ServerSideEvents> {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room);
}
return new BroadcastOperator(
this.emitter,
this.rooms,
exceptRooms,
this.flags,
);
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(
compress: boolean,
): BroadcastOperator<EmitEvents, ServerSideEvents> {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator(
this.emitter,
this.rooms,
this.exceptRooms,
flags,
);
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents, ServerSideEvents> {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator(
this.emitter,
this.rooms,
this.exceptRooms,
flags,
);
}
private async publish(document: any) {
document.uid = EMITTER_UID;
try {
if (
[
EventType.BROADCAST,
EventType.SERVER_SIDE_EMIT,
EventType.SERVER_SIDE_EMIT_RESPONSE,
].includes(document.type) &&
hasBinary(document)
) {
return await this.publishWithAttachment(document);
}
const payload = JSON.stringify(document);
if (Buffer.byteLength(payload) > this.emitter.payloadThreshold) {
return await this.publishWithAttachment(document);
}
debug(
"sending event of type %s to channel %s",
document.type,
this.emitter.channel,
);
await this.emitter.pool.query("SELECT pg_notify($1, $2)", [
this.emitter.channel,
payload,
]);
} catch (err) {
// @ts-ignore
this.emit("error", err);
}
}
private async publishWithAttachment(document: any) {
const payload = encode(document);
debug(
"sending event of type %s with attachment to channel %s",
document.type,
this.emitter.channel,
);
const result = await this.emitter.pool.query(
`INSERT INTO ${this.emitter.tableName} (payload) VALUES ($1) RETURNING id;`,
[payload],
);
const attachmentId = result.rows[0].id;
const headerPayload = JSON.stringify({
uid: document.uid,
type: document.type,
attachmentId,
});
await this.emitter.pool.query("SELECT pg_notify($1, $2)", [
this.emitter.channel,
headerPayload,
]);
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`);
}
// set up packet object
const data = [ev, ...args];
const packet = {
type: 2, // EVENT
data: data,
nsp: this.emitter.nsp,
};
const opts = {
rooms: [...this.rooms],
flags: this.flags,
except: [...this.exceptRooms],
};
this.publish({
type: EventType.BROADCAST,
data: {
packet,
opts,
},
});
return true;
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param rooms
* @public
*/
public socketsJoin(rooms: string | string[]): void {
this.publish({
type: EventType.SOCKETS_JOIN,
data: {
opts: {
rooms: [...this.rooms],
except: [...this.exceptRooms],
},
rooms: Array.isArray(rooms) ? rooms : [rooms],
},
});
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param rooms
* @public
*/
public socketsLeave(rooms: string | string[]): void {
this.publish({
type: EventType.SOCKETS_LEAVE,
data: {
opts: {
rooms: [...this.rooms],
except: [...this.exceptRooms],
},
rooms: Array.isArray(rooms) ? rooms : [rooms],
},
});
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.publish({
type: EventType.DISCONNECT_SOCKETS,
data: {
opts: {
rooms: [...this.rooms],
except: [...this.exceptRooms],
},
close,
},
});
}
/**
* Send a packet to the Socket.IO servers in the cluster
*
* @param ev - the event name
* @param args - any number of serializable arguments
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): void {
const withAck = args.length && typeof args[args.length - 1] === "function";
if (withAck) {
throw new Error("Acknowledgements are not supported");
}
this.publish({
type: EventType.SERVER_SIDE_EMIT,
data: {
packet: [ev, ...args],
},
});
}
}

View File

@@ -0,0 +1,37 @@
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any;
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>,
> = Parameters<Map[Ev]>;
/**
* Interface for classes that aren't `EventEmitter`s, but still expose a
* strictly typed `emit` method.
*/
export interface TypedEventBroadcaster<EmitEvents extends EventsMap> {
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean;
}

View File

@@ -0,0 +1,33 @@
{
"name": "@socket.io/postgres-emitter",
"version": "0.1.1",
"description": "The Socket.IO Postgres emitter, allowing to communicate with a group of Socket.IO servers from another Node.js process",
"license": "MIT",
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/socket.io-postgres-emitter#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/socketio/socket.io.git"
},
"files": [
"dist/"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"compile": "rimraf ./dist && tsc",
"test": "npm run format:check && npm run compile && nyc mocha --require ts-node/register --timeout 5000 test/index.ts",
"format:check": "prettier --parser typescript --check 'lib/**/*.ts' 'test/**/*.ts'",
"format:fix": "prettier --parser typescript --write 'lib/**/*.ts' 'test/**/*.ts'",
"prepack": "npm run compile"
},
"dependencies": {
"@msgpack/msgpack": "^2.7.0",
"debug": "~4.3.1"
},
"keywords": [
"socket.io",
"postgres",
"postgresql",
"emitter"
]
}

View File

@@ -0,0 +1,278 @@
import { createServer } from "http";
import { Server, Socket as ServerSocket } from "socket.io";
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import expect = require("expect.js");
import { createAdapter } from "@socket.io/postgres-adapter";
import type { AddressInfo } from "net";
import { Pool } from "pg";
import { times, sleep } from "./util";
import { Emitter } from "..";
const NODES_COUNT = 3;
describe("@socket.io/postgres-emitter", () => {
let servers: Server[],
serverSockets: ServerSocket[],
clientSockets: ClientSocket[],
pool: Pool,
emitter: Emitter;
beforeEach((done) => {
servers = [];
serverSockets = [];
clientSockets = [];
pool = new Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
pool.query(
`
CREATE TABLE IF NOT EXISTS socket_io_attachments (
id bigserial UNIQUE,
created_at timestamptz DEFAULT NOW(),
payload bytea
);
`,
() => {},
);
emitter = new Emitter(pool);
for (let i = 1; i <= NODES_COUNT; i++) {
const httpServer = createServer();
const io = new Server(httpServer);
// @ts-ignore
io.adapter(createAdapter(pool));
httpServer.listen(() => {
const port = (httpServer.address() as AddressInfo).port;
const clientSocket = ioc(`http://localhost:${port}`);
io.on("connection", async (socket) => {
clientSockets.push(clientSocket);
serverSockets.push(socket);
servers.push(io);
if (servers.length === NODES_COUNT) {
// ensure all nodes know each other
servers[0].emit("ping");
servers[1].emit("ping");
servers[2].emit("ping");
await sleep(200);
done();
}
});
});
}
});
afterEach((done) => {
servers.forEach((server) => {
// @ts-ignore
server.httpServer.close();
server.of("/").adapter.close();
});
clientSockets.forEach((socket) => {
socket.disconnect();
});
pool.end(done);
});
describe("broadcast", function () {
it("broadcasts to all clients", (done) => {
const partialDone = times(3, done);
clientSockets.forEach((clientSocket) => {
clientSocket.on("test", (arg1, arg2, arg3) => {
expect(arg1).to.eql(1);
expect(arg2).to.eql("2");
expect(Buffer.isBuffer(arg3)).to.be(true);
partialDone();
});
});
emitter.emit("test", 1, "2", Buffer.from([3, 4]));
});
it("broadcasts to all clients in a namespace", (done) => {
const partialDone = times(3, () => {
servers.forEach((server) => server.of("/custom").adapter.close());
done();
});
servers.forEach((server) => server.of("/custom"));
const onConnect = times(3, async () => {
await sleep(200);
emitter.of("/custom").emit("test");
});
clientSockets.forEach((clientSocket) => {
const socket = clientSocket.io.socket("/custom");
socket.on("connect", onConnect);
socket.on("test", () => {
socket.disconnect();
partialDone();
});
});
});
it("broadcasts to all clients in a room", (done) => {
serverSockets[1].join("room1");
clientSockets[0].on("test", () => {
done(new Error("should not happen"));
});
clientSockets[1].on("test", () => {
done();
});
clientSockets[2].on("test", () => {
done(new Error("should not happen"));
});
emitter.to("room1").emit("test");
});
it("broadcasts to all clients except in room", (done) => {
const partialDone = times(2, done);
serverSockets[1].join("room1");
clientSockets[0].on("test", () => {
partialDone();
});
clientSockets[1].on("test", () => {
done(new Error("should not happen"));
});
clientSockets[2].on("test", () => {
partialDone();
});
emitter.of("/").except("room1").emit("test");
});
});
describe("socketsJoin", () => {
it("makes all socket instances join the specified room", async () => {
emitter.socketsJoin("room1");
await sleep(200);
expect(serverSockets[0].rooms.has("room1")).to.be(true);
expect(serverSockets[1].rooms.has("room1")).to.be(true);
expect(serverSockets[2].rooms.has("room1")).to.be(true);
});
it("makes the matching socket instances join the specified room", async () => {
serverSockets[0].join("room1");
serverSockets[2].join("room1");
emitter.in("room1").socketsJoin("room2");
await sleep(200);
expect(serverSockets[0].rooms.has("room2")).to.be(true);
expect(serverSockets[1].rooms.has("room2")).to.be(false);
expect(serverSockets[2].rooms.has("room2")).to.be(true);
});
it("makes the given socket instance join the specified room", async () => {
emitter.in(serverSockets[1].id).socketsJoin("room3");
await sleep(200);
expect(serverSockets[0].rooms.has("room3")).to.be(false);
expect(serverSockets[1].rooms.has("room3")).to.be(true);
expect(serverSockets[2].rooms.has("room3")).to.be(false);
});
});
describe("socketsLeave", () => {
it("makes all socket instances leave the specified room", async () => {
serverSockets[0].join("room1");
serverSockets[2].join("room1");
emitter.socketsLeave("room1");
await sleep(200);
expect(serverSockets[0].rooms.has("room1")).to.be(false);
expect(serverSockets[1].rooms.has("room1")).to.be(false);
expect(serverSockets[2].rooms.has("room1")).to.be(false);
});
it("makes the matching socket instances leave the specified room", async () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join(["room1", "room2"]);
serverSockets[2].join(["room2"]);
emitter.in("room1").socketsLeave("room2");
await sleep(200);
expect(serverSockets[0].rooms.has("room2")).to.be(false);
expect(serverSockets[1].rooms.has("room2")).to.be(false);
expect(serverSockets[2].rooms.has("room2")).to.be(true);
});
it("makes the given socket instance leave the specified room", async () => {
serverSockets[0].join("room3");
serverSockets[1].join("room3");
serverSockets[2].join("room3");
emitter.in(serverSockets[1].id).socketsLeave("room3");
await sleep(200);
expect(serverSockets[0].rooms.has("room3")).to.be(true);
expect(serverSockets[1].rooms.has("room3")).to.be(false);
expect(serverSockets[2].rooms.has("room3")).to.be(true);
});
});
describe("disconnectSockets", () => {
it("makes all socket instances disconnect", (done) => {
const partialDone = times(3, done);
clientSockets.forEach((clientSocket) => {
clientSocket.on("disconnect", (reason) => {
expect(reason).to.eql("io server disconnect");
partialDone();
});
});
emitter.disconnectSockets();
});
});
describe("serverSideEmit", () => {
it("sends an event to other server instances", (done) => {
const partialDone = times(3, done);
emitter.serverSideEmit("hello", "world", 1, "2");
servers[0].on("hello", (arg1, arg2, arg3) => {
expect(arg1).to.eql("world");
expect(arg2).to.eql(1);
expect(arg3).to.eql("2");
partialDone();
});
servers[1].on("hello", (arg1, arg2, arg3) => {
partialDone();
});
servers[2].of("/").on("hello", () => {
partialDone();
});
});
});
});

View File

@@ -0,0 +1,13 @@
export function times(count: number, fn: () => void) {
let i = 0;
return () => {
i++;
if (i === count) {
fn();
}
};
}
export function sleep(duration: number) {
return new Promise((resolve) => setTimeout(resolve, duration));
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": false,
"target": "es2017",
"module": "commonjs",
"declaration": true
},
"include": [
"./lib/**/*"
]
}

View File

@@ -1,6 +1,6 @@
import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
import debugModule = require("debug");
import url = require("url");
import debugModule from "debug";
import url from "url";
import type { IncomingMessage } from "http";
import type { Server } from "./index";
import type { Namespace } from "./namespace";
@@ -61,12 +61,13 @@ export class Client<
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any,
conn: RawSocket,
) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
this.decoder = new server._parser.Decoder();
// @ts-expect-error use of private
this.id = conn.id;
this.setup();
}
@@ -216,13 +217,13 @@ export class Client<
* @param {Object} opts
* @private
*/
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
_packet(packet: Packet | Packet[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") {
debug("ignoring packet write %j", packet);
return;
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
? (packet as Packet[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet);
this.writeToEngine(encodedPackets, opts);
}
@@ -250,7 +251,7 @@ export class Client<
*
* @private
*/
private ondata(data): void {
private ondata(data: unknown): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data);

View File

@@ -1,4 +1,4 @@
import http = require("http");
import http from "http";
import type { Server as HTTPSServer } from "https";
import type { Http2SecureServer, Http2Server } from "http2";
import { createReadStream } from "fs";
@@ -7,7 +7,11 @@ import accepts = require("accepts");
import { pipeline } from "stream";
import path = require("path");
import { attach, Server as Engine, uServer } from "engine.io";
import type { ServerOptions as EngineOptions, AttachOptions } from "engine.io";
import type {
ServerOptions as EngineOptions,
AttachOptions,
Socket as RawSocket,
} from "engine.io";
import { Client } from "./client";
import { EventEmitter } from "events";
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace";
@@ -506,6 +510,11 @@ export class Server<
return this;
}
/**
* Attaches socket.io to a uWebSockets.js app.
* @param app
* @param opts
*/
public attachApp(app /*: TemplatedApp */, opts: Partial<ServerOptions> = {}) {
// merge the options passed to the Socket.IO server
Object.assign(opts, this.opts);
@@ -582,7 +591,7 @@ export class Server<
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts);
this.eio = attach(srv, opts);
this.eio = attach(srv as http.Server, opts);
// attach static file serving
if (this._serveClient) this.attachServe(srv);
@@ -723,11 +732,12 @@ export class Server<
* @return self
* @private
*/
private onconnection(conn): this {
private onconnection(conn: RawSocket): this {
// @ts-expect-error use of private
debug("incoming connection with id %s", conn.id);
const client = new Client(this, conn);
if (conn.protocol === 3) {
// @ts-ignore
// @ts-expect-error use of private
client.connect("/");
}
return this;

View File

@@ -163,7 +163,7 @@ export class Socket<
) {
super();
this.server = nsp.server;
this.adapter = this.nsp.adapter;
this.adapter = nsp.adapter;
if (previousSession) {
this.id = previousSession.sid;
this.pid = previousSession.pid;