mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
143 Commits
engine.io-
...
socket.io-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
968277cef8 | ||
|
|
2bf16bd214 | ||
|
|
ad616070b8 | ||
|
|
dd71792455 | ||
|
|
bb0b480d2a | ||
|
|
161be91975 | ||
|
|
fd9d4cab5e | ||
|
|
0a99ac44a2 | ||
|
|
4338f47336 | ||
|
|
9199156758 | ||
|
|
594841617d | ||
|
|
84e7253e57 | ||
|
|
30ec4a136a | ||
|
|
e08293bc37 | ||
|
|
b837949479 | ||
|
|
118ef41b94 | ||
|
|
d19928e8d8 | ||
|
|
cdae01983a | ||
|
|
39bb72039d | ||
|
|
98741e15e9 | ||
|
|
8af70195bb | ||
|
|
d88f3f4578 | ||
|
|
f5ee981ee8 | ||
|
|
76e3a72bba | ||
|
|
a7b1938d06 | ||
|
|
54743633ff | ||
|
|
7617707ed8 | ||
|
|
599001d213 | ||
|
|
1c3e4711c1 | ||
|
|
693080cac7 | ||
|
|
5080c73e1e | ||
|
|
47ff1cd04c | ||
|
|
0ae76360f9 | ||
|
|
27fd420e75 | ||
|
|
0c431243e2 | ||
|
|
4fc25d80ec | ||
|
|
1dd729b1a1 | ||
|
|
6877512f57 | ||
|
|
cf6816afcf | ||
|
|
625fd66d73 | ||
|
|
f3e1f5ebdf | ||
|
|
e97549259e | ||
|
|
1da9cddeab | ||
|
|
6f9b198bc8 | ||
|
|
ac3df9a747 | ||
|
|
21fd54ece6 | ||
|
|
96d907b9b5 | ||
|
|
32257b6cb8 | ||
|
|
c7144920e3 | ||
|
|
42480e9a7f | ||
|
|
0a8f91047c | ||
|
|
a66ed68506 | ||
|
|
3be6481d9d | ||
|
|
be13cca94c | ||
|
|
e95f6abf93 | ||
|
|
72d61dab82 | ||
|
|
5a31aaf917 | ||
|
|
62e4da125e | ||
|
|
bfa6eab195 | ||
|
|
7fcddcb3bb | ||
|
|
7427109658 | ||
|
|
91e1c8b358 | ||
|
|
8d5528aa2a | ||
|
|
71387e5294 | ||
|
|
aead83560d | ||
|
|
029e010901 | ||
|
|
4ca6ddb3a2 | ||
|
|
ca9e994815 | ||
|
|
4865f2e62e | ||
|
|
d4b3ddedff | ||
|
|
3b68658201 | ||
|
|
175a2c58c1 | ||
|
|
9b80ab42d6 | ||
|
|
a5d2368512 | ||
|
|
88efd446f1 | ||
|
|
d0fc720420 | ||
|
|
4a0555c671 | ||
|
|
2b60df18a8 | ||
|
|
d4cb375856 | ||
|
|
c251ae7ba7 | ||
|
|
8a2f5a3da0 | ||
|
|
b04fa64365 | ||
|
|
7085f0e3e4 | ||
|
|
4f66708210 | ||
|
|
1a95db2145 | ||
|
|
282ae922a4 | ||
|
|
93010ca3c4 | ||
|
|
132d05fc0b | ||
|
|
d5095fe98c | ||
|
|
da613810fd | ||
|
|
19c48a44e6 | ||
|
|
9b3c9abeca | ||
|
|
043b55c418 | ||
|
|
32c761f02f | ||
|
|
1f54ee08c6 | ||
|
|
923a12e2de | ||
|
|
13c6d2e89d | ||
|
|
8adcfbfde5 | ||
|
|
7a23dde6ef | ||
|
|
60c757f718 | ||
|
|
04c8dd979c | ||
|
|
2194264820 | ||
|
|
09f573cad8 | ||
|
|
fcbecd4f46 | ||
|
|
fd99f2e15f | ||
|
|
02d59a0e99 | ||
|
|
7160eb7eb0 | ||
|
|
a1ccba3a77 | ||
|
|
e347a3c24e | ||
|
|
b5ccfd4838 | ||
|
|
5d9a2d5544 | ||
|
|
d5b22f5a76 | ||
|
|
582655f679 | ||
|
|
b79d80aa59 | ||
|
|
7fd75e6aac | ||
|
|
b7577556e3 | ||
|
|
6e9bff4fcf | ||
|
|
8b0a40fd4a | ||
|
|
1f09a3e979 | ||
|
|
0af50758f6 | ||
|
|
b9b16132c2 | ||
|
|
be1e4cb24d | ||
|
|
7521ac227b | ||
|
|
b00124b65a | ||
|
|
b7da542890 | ||
|
|
0692bed462 | ||
|
|
56a53bceb9 | ||
|
|
683720a67d | ||
|
|
a529eb08d6 | ||
|
|
cddb78e5fa | ||
|
|
15fd56e78d | ||
|
|
e86ef45f87 | ||
|
|
fe840e2eb3 | ||
|
|
a5a1c29082 | ||
|
|
66b4079953 | ||
|
|
be0a0e3217 | ||
|
|
43f9ee8d23 | ||
|
|
055b7840d8 | ||
|
|
6397c1bdfd | ||
|
|
ff370cfc46 | ||
|
|
1f8a6c4ecb | ||
|
|
eb01ff5803 | ||
|
|
f2e3d162ab |
2
.github/workflows/build-examples.yml
vendored
2
.github/workflows/build-examples.yml
vendored
@@ -19,6 +19,8 @@ jobs:
|
||||
- custom-parsers
|
||||
- typescript-example/cjs
|
||||
- typescript-example/esm
|
||||
- typescript-client-example/cjs
|
||||
- typescript-client-example/esm
|
||||
- webpack-build
|
||||
- webpack-build-server
|
||||
- basic-crud-application/angular-client
|
||||
|
||||
4
.github/workflows/ci-browser.yml
vendored
4
.github/workflows/ci-browser.yml
vendored
@@ -2,6 +2,8 @@ name: CI (browser)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'packages/engine.io-parser/**'
|
||||
- 'packages/engine.io-client/**'
|
||||
@@ -14,7 +16,7 @@ permissions:
|
||||
jobs:
|
||||
test-browser:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@@ -2,6 +2,8 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
@@ -18,28 +20,59 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version:
|
||||
- 18
|
||||
- 20
|
||||
- 22
|
||||
- 24
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
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
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# in order to test our compliance with TypeScript v4.2 (older versions are not tested)
|
||||
- name: Install TypeScript 4.2
|
||||
run: npm i typescript@4.2
|
||||
if: ${{ matrix.node-version == '16' }}
|
||||
|
||||
- name: Compile each package
|
||||
run: npm run compile --workspaces --if-present
|
||||
|
||||
- name: Run tests
|
||||
run: npm test --workspaces
|
||||
|
||||
- name: Run tests with uws (engine.io)
|
||||
run: npm run test:uws --workspace=engine.io
|
||||
if: ${{ matrix.node-version == '18' }}
|
||||
|
||||
- name: Run tests with fetch instead of XHR (engine.io-client)
|
||||
run: npm run test:node-fetch --workspace=engine.io-client
|
||||
if: ${{ matrix.node-version == '18' }}
|
||||
|
||||
- name: Run tests with Node.js native WebSocket (engine.io-client)
|
||||
run: npm run test:node-builtin-ws --workspace=engine.io-client
|
||||
if: ${{ matrix.node-version == '22' }}
|
||||
|
||||
16
.github/workflows/publish.yml
vendored
16
.github/workflows/publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# reference: https://docs.npmjs.com/generating-provenance-statements
|
||||
# reference: https://docs.npmjs.com/trusted-publishers#for-github-actions
|
||||
|
||||
name: Publish
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
# expected format: <package>@<version> (example: socket.io@1.2.3)
|
||||
- '*@*'
|
||||
- '**@*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -19,15 +19,17 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Compile each package
|
||||
run: npm run compile --workspaces --if-present
|
||||
|
||||
- name: Publish package
|
||||
run: npm publish --workspace=${GITHUB_REF_NAME%@*} --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish --workspace=${GITHUB_REF_NAME%@*} --access public
|
||||
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -2,13 +2,17 @@
|
||||
|
||||
Here are the detailed changelogs for each package in this monorepo:
|
||||
|
||||
| Package | Changelog |
|
||||
|--------------------------------|---------------------------------------------------------|
|
||||
| `engine.io` | [link](packages/engine.io/CHANGELOG.md) |
|
||||
| `engine.io-client` | [link](packages/engine.io-client/CHANGELOG.md) |
|
||||
| `engine.io-parser` | [link](packages/engine.io-parser/CHANGELOG.md) |
|
||||
| `socket.io` | [link](packages/socket.io/CHANGELOG.md) |
|
||||
| `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) |
|
||||
| `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) |
|
||||
| `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) |
|
||||
| `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) |
|
||||
| Package | Changelog |
|
||||
|------------------------------------|----------------------------------------------------------------|
|
||||
| `engine.io` | [link](packages/engine.io/CHANGELOG.md) |
|
||||
| `engine.io-client` | [link](packages/engine.io-client/CHANGELOG.md) |
|
||||
| `engine.io-parser` | [link](packages/engine.io-parser/CHANGELOG.md) |
|
||||
| `socket.io` | [link](packages/socket.io/CHANGELOG.md) |
|
||||
| `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) |
|
||||
| `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) |
|
||||
| `@socket.io/cluster-adapter` | [link](packages/socket.io-cluster-adapter/CHANGELOG.md) |
|
||||
| `@socket.io/cluster-engine` | [link](packages/socket.io-cluster-engine/CHANGELOG.md) |
|
||||
| `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) |
|
||||
| `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) |
|
||||
| `@socket.io/postgres-emitter` | [link](packages/socket.io-postgres-emitter/CHANGELOG.md) |
|
||||
| `@socket.io/redis-streams-emitter` | [link](/packages/socket.io-redis-streams-emitter/CHANGELOG.md) |
|
||||
|
||||
@@ -78,6 +78,7 @@ This repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) which co
|
||||
| `socket.io` | The server-side implementation of the bidirectional channel, built on top on the `engine.io` package. |
|
||||
| `socket.io-adapter` | An extensible component responsible for broadcasting a packet to all connected clients, used by the `socket.io` package. |
|
||||
| `socket.io-client` | The client-side implementation of the bidirectional channel, built on top on the `engine.io-client` package. |
|
||||
| `@socket.io/cluster-engine` | A cluster-friendly engine to share load between multiple Node.js processes (without sticky sessions) |
|
||||
| `@socket.io/component-emitter` | An `EventEmitter` implementation, similar to the one provided by [Node.js](https://nodejs.org/api/events.html) but for all platforms. |
|
||||
| `socket.io-parser` | The parser responsible for encoding and decoding Socket.IO packets, used by both the `socket.io` and `socket.io-client` packages. |
|
||||
|
||||
@@ -149,3 +150,18 @@ For a specific workspace:
|
||||
```bash
|
||||
npm test --workspace=socket.io
|
||||
```
|
||||
|
||||
### Generate the changelog
|
||||
|
||||
Install the [`conventional-changelog-cli`](https://www.npmjs.com/package/conventional-changelog-cli) package:
|
||||
|
||||
```bash
|
||||
npm i -g conventional-changelog-cli
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
cd packages/engine.io-client
|
||||
conventional-changelog -p angular --tag-prefix "engine.io-client@" --commit-path .
|
||||
```
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"prettier": "^2.8.4",
|
||||
"rollup": "^3.20.2",
|
||||
"socket.io": "^4.6.1",
|
||||
"ws": "^8.13.0"
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "rollup -c",
|
||||
|
||||
19
examples/cluster-engine-node-cluster/README.md
Normal file
19
examples/cluster-engine-node-cluster/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Example with `@socket.io/cluster-engine` and Node.js cluster
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# run the server
|
||||
$ node server.js
|
||||
|
||||
# run the client
|
||||
$ node client.js
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
The `server.js` script will create one Socket.IO server per core, each listening on the same port (`3000`).
|
||||
|
||||
With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same worker.
|
||||
|
||||
The `NodeClusterEngine` is a custom engine which takes care of the synchronization between the servers by using [the IPC channel](https://nodejs.org/api/cluster.html#workersendmessage-sendhandle-options-callback) and removes the need for sticky sessions when scaling horizontally.
|
||||
26
examples/cluster-engine-node-cluster/client.js
Normal file
26
examples/cluster-engine-node-cluster/client.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const CLIENTS_COUNT = 3;
|
||||
|
||||
for (let i = 0; i < CLIENTS_COUNT; i++) {
|
||||
const socket = io("ws://localhost:3000/", {
|
||||
// transports: ["polling"],
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connected as ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnected due to ${reason}`);
|
||||
});
|
||||
|
||||
socket.on("hello", (socketId, workerId) => {
|
||||
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
socket.emit("hello");
|
||||
}, 2000);
|
||||
}
|
||||
12
examples/cluster-engine-node-cluster/package.json
Normal file
12
examples/cluster-engine-node-cluster/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "cluster-engine-node-cluster",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@socket.io/cluster-adapter": "^0.2.2",
|
||||
"@socket.io/cluster-engine": "^0.1.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
}
|
||||
63
examples/cluster-engine-node-cluster/server.js
Normal file
63
examples/cluster-engine-node-cluster/server.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import cluster from "node:cluster";
|
||||
import process from "node:process";
|
||||
import { availableParallelism } from "node:os";
|
||||
import {
|
||||
setupPrimary as setupPrimaryEngine,
|
||||
NodeClusterEngine,
|
||||
} from "@socket.io/cluster-engine";
|
||||
import {
|
||||
setupPrimary as setupPrimaryAdapter,
|
||||
createAdapter,
|
||||
} from "@socket.io/cluster-adapter";
|
||||
import { createServer } from "node:http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
if (cluster.isPrimary) {
|
||||
console.log(`Primary ${process.pid} is running`);
|
||||
|
||||
const numCPUs = availableParallelism();
|
||||
|
||||
// fork workers
|
||||
for (let i = 0; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
setupPrimaryEngine();
|
||||
setupPrimaryAdapter();
|
||||
|
||||
// needed for packets containing Buffer objects (you can ignore it if you only send plaintext objects)
|
||||
cluster.setupPrimary({
|
||||
serialization: "advanced",
|
||||
});
|
||||
|
||||
cluster.on("exit", (worker, code, signal) => {
|
||||
console.log(`worker ${worker.process.pid} died`);
|
||||
});
|
||||
} else {
|
||||
const httpServer = createServer((req, res) => {
|
||||
res.writeHead(404).end();
|
||||
});
|
||||
|
||||
const engine = new NodeClusterEngine();
|
||||
|
||||
engine.attach(httpServer, {
|
||||
path: "/socket.io/",
|
||||
});
|
||||
|
||||
const io = new Server({
|
||||
adapter: createAdapter(),
|
||||
});
|
||||
|
||||
io.bind(engine);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("hello", () => {
|
||||
socket.broadcast.emit("hello", socket.id, process.pid);
|
||||
});
|
||||
});
|
||||
|
||||
// workers will share the same port
|
||||
httpServer.listen(3000);
|
||||
|
||||
console.log(`Worker ${process.pid} started`);
|
||||
}
|
||||
22
examples/cluster-engine-redis/README.md
Normal file
22
examples/cluster-engine-redis/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Example with `@socket.io/cluster-engine` and Redis
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# start the redis server
|
||||
$ docker compose up -d
|
||||
|
||||
# run the server
|
||||
$ node server.js
|
||||
|
||||
# run the client
|
||||
$ node client.js
|
||||
```
|
||||
|
||||
## Explanation
|
||||
|
||||
The `server.js` script will create 3 Socket.IO servers, each listening on a distinct port (`3001`, `3002` and `3003`), and a proxy server listening on port `3000` which randomly redirects to one of those servers.
|
||||
|
||||
With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same server.
|
||||
|
||||
The `RedisEngine` is a custom engine which takes care of the synchronization between the servers by using [Redis pub/sub](https://redis.io/docs/latest/develop/interact/pubsub/) and removes the need for sticky sessions when scaling horizontally.
|
||||
26
examples/cluster-engine-redis/client.js
Normal file
26
examples/cluster-engine-redis/client.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const CLIENTS_COUNT = 3;
|
||||
|
||||
for (let i = 0; i < CLIENTS_COUNT; i++) {
|
||||
const socket = io("ws://localhost:3000/", {
|
||||
// transports: ["polling"],
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connected as ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnected due to ${reason}`);
|
||||
});
|
||||
|
||||
socket.on("hello", (socketId, workerId) => {
|
||||
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
socket.emit("hello");
|
||||
}, 2000);
|
||||
}
|
||||
5
examples/cluster-engine-redis/compose.yaml
Normal file
5
examples/cluster-engine-redis/compose.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
14
examples/cluster-engine-redis/package.json
Normal file
14
examples/cluster-engine-redis/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "cluster-engine-redis",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@socket.io/cluster-engine": "^0.1.0",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"redis": "^4.6.15",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
}
|
||||
65
examples/cluster-engine-redis/server.js
Normal file
65
examples/cluster-engine-redis/server.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { RedisEngine } from "@socket.io/cluster-engine";
|
||||
import { createServer } from "node:http";
|
||||
import { createClient } from "redis";
|
||||
import { Server } from "socket.io";
|
||||
import { createAdapter } from "@socket.io/redis-adapter";
|
||||
import proxyModule from "http-proxy";
|
||||
|
||||
const { createProxyServer } = proxyModule;
|
||||
|
||||
async function initServer(port) {
|
||||
const httpServer = createServer((req, res) => {
|
||||
res.writeHead(404).end();
|
||||
});
|
||||
|
||||
const pubClient = createClient();
|
||||
const subClient = pubClient.duplicate();
|
||||
|
||||
await Promise.all([pubClient.connect(), subClient.connect()]);
|
||||
|
||||
const engine = new RedisEngine(pubClient, subClient);
|
||||
|
||||
engine.attach(httpServer, {
|
||||
path: "/socket.io/",
|
||||
});
|
||||
|
||||
const io = new Server({
|
||||
adapter: createAdapter(pubClient, subClient),
|
||||
});
|
||||
|
||||
io.bind(engine);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("hello", () => {
|
||||
socket.broadcast.emit("hello", socket.id, port);
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port);
|
||||
}
|
||||
|
||||
function initProxy() {
|
||||
const proxy = createProxyServer();
|
||||
|
||||
function randomTarget() {
|
||||
return [
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
"http://localhost:3003",
|
||||
][Math.floor(Math.random() * 3)];
|
||||
}
|
||||
|
||||
const httpServer = createServer((req, res) => {
|
||||
proxy.web(req, res, { target: randomTarget() });
|
||||
});
|
||||
|
||||
httpServer.on("upgrade", function (req, socket, head) {
|
||||
proxy.ws(req, socket, head, { target: randomTarget() });
|
||||
});
|
||||
|
||||
httpServer.listen(3000);
|
||||
}
|
||||
|
||||
await Promise.all([initServer(3001), initServer(3002), initServer(3003)]);
|
||||
|
||||
initProxy();
|
||||
25
examples/nestjs-example/.eslintrc.js
Normal file
25
examples/nestjs-example/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
56
examples/nestjs-example/.gitignore
vendored
Normal file
56
examples/nestjs-example/.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
4
examples/nestjs-example/.prettierrc
Normal file
4
examples/nestjs-example/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
73
examples/nestjs-example/README.md
Normal file
73
examples/nestjs-example/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
||||
8
examples/nestjs-example/nest-cli.json
Normal file
8
examples/nestjs-example/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
72
examples/nestjs-example/package.json
Normal file
72
examples/nestjs-example/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "nestjs-example",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.3.10",
|
||||
"@nestjs/websockets": "^10.3.10",
|
||||
"hbs": "^4.2.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
22
examples/nestjs-example/src/app.controller.spec.ts
Normal file
22
examples/nestjs-example/src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
13
examples/nestjs-example/src/app.controller.ts
Normal file
13
examples/nestjs-example/src/app.controller.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get, Render } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@Render('index')
|
||||
root() {
|
||||
return { message: 'Hello world2!' };
|
||||
}
|
||||
}
|
||||
11
examples/nestjs-example/src/app.module.ts
Normal file
11
examples/nestjs-example/src/app.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { EventsModule } from './events/events.module';
|
||||
|
||||
@Module({
|
||||
imports: [EventsModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
examples/nestjs-example/src/app.service.ts
Normal file
8
examples/nestjs-example/src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
18
examples/nestjs-example/src/events/events.gateway.ts
Normal file
18
examples/nestjs-example/src/events/events.gateway.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
MessageBody,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server } from 'socket.io';
|
||||
|
||||
@WebSocketGateway({})
|
||||
export class EventsGateway {
|
||||
@WebSocketServer()
|
||||
io: Server;
|
||||
|
||||
@SubscribeMessage('hello')
|
||||
handleEvent(@MessageBody() data: string): string {
|
||||
return data.split('').reverse().join('');
|
||||
}
|
||||
}
|
||||
7
examples/nestjs-example/src/events/events.module.ts
Normal file
7
examples/nestjs-example/src/events/events.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EventsGateway } from './events.gateway';
|
||||
|
||||
@Module({
|
||||
providers: [EventsGateway],
|
||||
})
|
||||
export class EventsModule {}
|
||||
15
examples/nestjs-example/src/main.ts
Normal file
15
examples/nestjs-example/src/main.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'node:path';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||
app.setViewEngine('hbs');
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
24
examples/nestjs-example/test/app.e2e-spec.ts
Normal file
24
examples/nestjs-example/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
9
examples/nestjs-example/test/jest-e2e.json
Normal file
9
examples/nestjs-example/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
examples/nestjs-example/tsconfig.build.json
Normal file
4
examples/nestjs-example/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
21
examples/nestjs-example/tsconfig.json
Normal file
21
examples/nestjs-example/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
47
examples/nestjs-example/views/index.hbs
Normal file
47
examples/nestjs-example/views/index.hbs
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>App</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Status: <span id="status"></span></p>
|
||||
<p>Transport: <span id="transport"></span></p>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const statusSpan = document.getElementById("status");
|
||||
const transportSpan = document.getElementById("transport");
|
||||
const socket = io({
|
||||
// transports: ["polling"],
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
statusSpan.innerText = "Disconnected";
|
||||
transportSpan.innerText = "N/A";
|
||||
|
||||
socket.on("connect", () => {
|
||||
statusSpan.innerText = "Connected";
|
||||
transportSpan.innerText = socket.io.engine.transport.name;
|
||||
socket.io.engine.on("upgrade", (transport) => {
|
||||
transportSpan.innerText = transport.name;
|
||||
});
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
socket.emit("hello", "world", (val) => {
|
||||
console.log(`got ${val}`);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.log(`connect_error due to ${err.message}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
statusSpan.innerText = "Disconnected";
|
||||
transportSpan.innerText = "N/A";
|
||||
console.log(`disconnect due to ${reason}`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,17 +20,18 @@ export default defineNitroPlugin((nitroApp: NitroApp) => {
|
||||
},
|
||||
websocket: {
|
||||
open(peer) {
|
||||
const nodeContext = peer.ctx.node;
|
||||
const req = nodeContext.req;
|
||||
// crossws >= 0.3.0
|
||||
// @ts-expect-error private method and property
|
||||
engine.prepare(peer._internal.nodeReq);
|
||||
// @ts-expect-error private method and property
|
||||
engine.onWebSocket(peer._internal.nodeReq, peer._internal.nodeReq.socket, peer.websocket);
|
||||
|
||||
// @ts-expect-error private method
|
||||
engine.prepare(req);
|
||||
|
||||
const rawSocket = nodeContext.req.socket;
|
||||
const websocket = nodeContext.ws;
|
||||
|
||||
// @ts-expect-error private method
|
||||
engine.onWebSocket(req, rawSocket, websocket);
|
||||
// crossws < 0.3.0
|
||||
// const context = peer.ctx.node;
|
||||
// // @ts-expect-error private method
|
||||
// engine.prepare(context.req);
|
||||
// // @ts-expect-error private method
|
||||
// engine.onWebSocket(context.req, context.req.socket, context.ws);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
25
examples/postgres-adapter-example/README.md
Normal file
25
examples/postgres-adapter-example/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Example with `@socket.io/postgres-adapter`
|
||||
|
||||
**Table of contents**
|
||||
|
||||
<!-- TOC -->
|
||||
* [How to use](#how-to-use)
|
||||
* [Documentation](#documentation)
|
||||
<!-- TOC -->
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# start the postgres server
|
||||
$ docker compose up -d
|
||||
|
||||
# run the cluster
|
||||
$ node cluster.js
|
||||
|
||||
# run the client
|
||||
$ node client.js
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found here: https://socket.io/docs/v4/postgres-adapter/
|
||||
31
examples/postgres-adapter-example/client.js
Normal file
31
examples/postgres-adapter-example/client.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const CLIENTS_COUNT = 3;
|
||||
const PORTS = [3000, 3001, 3002];
|
||||
|
||||
for (let i = 0; i < CLIENTS_COUNT; i++) {
|
||||
const socket = io(`ws://localhost:${PORTS[i % 3]}`, {
|
||||
// transports: ["polling"],
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connected as ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("connect_error", () => {
|
||||
console.log(`connect_error`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnected due to ${reason}`);
|
||||
});
|
||||
|
||||
socket.on("hello", (socketId, pid) => {
|
||||
console.log(`received "hello" from ${socketId} (process: ${pid})`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
socket.emit("hello");
|
||||
}, 2000);
|
||||
}
|
||||
13
examples/postgres-adapter-example/cluster.js
Normal file
13
examples/postgres-adapter-example/cluster.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import cluster from 'node:cluster';
|
||||
|
||||
const SERVERS_COUNT = 3;
|
||||
|
||||
cluster.setupPrimary({
|
||||
exec: 'server.js',
|
||||
});
|
||||
|
||||
for (let i = 0; i < SERVERS_COUNT; i++) {
|
||||
cluster.fork({
|
||||
PORT: 3000 + i
|
||||
});
|
||||
}
|
||||
7
examples/postgres-adapter-example/compose.yaml
Normal file
7
examples/postgres-adapter-example/compose.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
12
examples/postgres-adapter-example/package.json
Normal file
12
examples/postgres-adapter-example/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "postgres-adapter-example",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@socket.io/postgres-adapter": "^0.4.0",
|
||||
"pg": "^8.12.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
}
|
||||
40
examples/postgres-adapter-example/server.js
Normal file
40
examples/postgres-adapter-example/server.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Server } from "socket.io";
|
||||
import { createAdapter } from "@socket.io/postgres-adapter";
|
||||
import pg from "pg";
|
||||
import process from "node:process";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
const pool = new pg.Pool({
|
||||
user: "postgres",
|
||||
host: "localhost",
|
||||
database: "postgres",
|
||||
password: "changeit",
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS socket_io_attachments (
|
||||
id bigserial UNIQUE,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
payload bytea
|
||||
);
|
||||
`);
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Postgres error", err);
|
||||
});
|
||||
|
||||
const io = new Server({
|
||||
adapter: createAdapter(pool)
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("hello", () => {
|
||||
// send to anyone except the sender
|
||||
socket.broadcast.emit("hello", socket.id, process.pid);
|
||||
});
|
||||
});
|
||||
|
||||
io.listen(PORT);
|
||||
console.log(`server listening on port ${PORT}`);
|
||||
30
examples/typescript-client-example/cjs/client.ts
Normal file
30
examples/typescript-client-example/cjs/client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
|
||||
interface ServerToClientEvents {
|
||||
hello: (val: string) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
ping: (cb: () => void) => void;
|
||||
}
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("ws://localhost:8080/");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("hello", (val) => {
|
||||
console.log(`got ${val}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log(`disconnect`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
const start = Date.now();
|
||||
socket.emit("ping", () => {
|
||||
console.log(`pong (latency: ${Date.now() - start} ms)`);
|
||||
});
|
||||
}, 1000);
|
||||
17
examples/typescript-client-example/cjs/package.json
Normal file
17
examples/typescript-client-example/cjs/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "typescript-client-example-cjs",
|
||||
"version": "0.0.1",
|
||||
"description": "An example with TypeScript",
|
||||
"type": "commonjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "ts-node client.ts"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
9
examples/typescript-client-example/cjs/tsconfig.json
Normal file
9
examples/typescript-client-example/cjs/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "es2022",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
30
examples/typescript-client-example/esm/client.ts
Normal file
30
examples/typescript-client-example/esm/client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
|
||||
interface ServerToClientEvents {
|
||||
hello: (val: string) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
ping: (cb: () => void) => void;
|
||||
}
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io("ws://localhost:8080/");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("hello", (val) => {
|
||||
console.log(`got ${val}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log(`disconnect`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
const start = Date.now();
|
||||
socket.emit("ping", () => {
|
||||
console.log(`pong (latency: ${Date.now() - start} ms)`);
|
||||
});
|
||||
}, 1000);
|
||||
17
examples/typescript-client-example/esm/package.json
Normal file
17
examples/typescript-client-example/esm/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "typescript-client-example-esm",
|
||||
"version": "0.0.1",
|
||||
"description": "An example with TypeScript",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm client.ts"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
9
examples/typescript-client-example/esm/tsconfig.json
Normal file
9
examples/typescript-client-example/esm/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "es2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
5447
package-lock.json
generated
5447
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -1,35 +1,44 @@
|
||||
{
|
||||
"name": "socket.io",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/socket.io-component-emitter",
|
||||
"packages/engine.io-parser",
|
||||
"packages/engine.io",
|
||||
"packages/socket.io-cluster-engine",
|
||||
"packages/engine.io-client",
|
||||
"packages/socket.io-adapter",
|
||||
"packages/socket.io-cluster-adapter",
|
||||
"packages/socket.io-parser",
|
||||
"packages/socket.io-client",
|
||||
"packages/socket.io"
|
||||
"packages/socket.io",
|
||||
"packages/socket.io-postgres-emitter",
|
||||
"packages/socket.io-redis-streams-emitter"
|
||||
],
|
||||
"overrides": {
|
||||
"@types/estree": "0.0.52",
|
||||
"@types/lodash": "4.14.189",
|
||||
"ws": "8.17.1"
|
||||
"ws": "8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/plugin-transform-object-assign": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"@babel/register": "^7.24.6",
|
||||
"@fails-components/webtransport": "^0.1.7",
|
||||
"@fails-components/webtransport": "^1.1.4",
|
||||
"@fails-components/webtransport-transport-http3-quiche": "^1.1.4",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@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",
|
||||
"@socket.io/redis-streams-adapter": "~0.2.2",
|
||||
"@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",
|
||||
@@ -40,6 +49,7 @@
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"benchmark": "^2.1.4",
|
||||
"blob": "^0.1.0",
|
||||
"cookie": "~0.7.2",
|
||||
"eiows": "^7.1.0",
|
||||
"engine.io-client-v3": "npm:engine.io-client@^3.5.2",
|
||||
"expect.js": "^0.3.1",
|
||||
@@ -47,10 +57,13 @@
|
||||
"express-session": "^1.18.0",
|
||||
"has-cors": "^1.1.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"mocha": "^10.6.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"nyc": "^17.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"pg": "^8.6.0",
|
||||
"prettier": "^3.3.2",
|
||||
"redis": "^4.6.15",
|
||||
"rimraf": "^6.0.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
@@ -63,8 +76,9 @@
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsd": "^0.31.1",
|
||||
"tsx": "~4.20.6",
|
||||
"typescript": "^5.5.3",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.56.0",
|
||||
"wdio-geckodriver-service": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# History
|
||||
# Changelog
|
||||
|
||||
| Version | Release date | Bundle size (UMD min+gzip) |
|
||||
|-------------------------------------------------------------------------------------------------------------|----------------|----------------------------|
|
||||
| [6.6.4](#664-2025-12-23) | December 2025 | `8.7 KB` |
|
||||
| [6.6.3](#663-2025-01-23) | January 2025 | `8.7 KB` |
|
||||
| [6.6.2](#662-2024-10-23) | October 2024 | `8.7 KB` |
|
||||
| [6.6.1](#661-2024-09-21) | September 2024 | `8.7 KB` |
|
||||
| [6.6.0](#660-2024-06-21) | June 2024 | `8.6 KB` |
|
||||
| [6.5.4](#654-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io-client/tree/6.5.x) branch) | June 2024 | `8.8 KB` |
|
||||
| [3.5.4](#354-2024-06-18) (from the [3.5.x](https://github.com/socketio/engine.io-client/tree/3.5.x) branch) | June 2024 | `-` |
|
||||
@@ -36,7 +40,74 @@
|
||||
| [4.1.1](#411-2021-02-02) | February 2021 | `9.1 KB` |
|
||||
| [4.1.0](#410-2021-01-14) | January 2021 | `9.1 KB` |
|
||||
|
||||
# Release notes
|
||||
|
||||
## [6.6.4](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.3...engine.io-client@6.6.4) (2025-12-23)
|
||||
|
||||
This release contains a bump of:
|
||||
|
||||
- `ws` from `~8.17.1` to `~8.18.3`
|
||||
- `debug` from `~4.3.1` to `~4.4.1`
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handle port option ([#5241](https://github.com/socketio/socket.io/issues/5241)) ([1da9cdd](https://github.com/socketio/socket.io/commit/1da9cddeab0bf5ce41890d156d73af8194cef656))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.18.3`](https://github.com/websockets/ws/releases/tag/8.18.3) ([diff](https://github.com/websockets/ws/compare/8.17.1...8.18.3))
|
||||
|
||||
|
||||
|
||||
## [6.6.3](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.2...engine.io-client@6.6.3) (2025-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correctly consume the `ws` package ([#5220](https://github.com/socketio/socket.io/issues/5220)) ([7fcddcb](https://github.com/socketio/socket.io/commit/7fcddcb3bbd236b46aa8fee6f4ce6c45afb7b03a))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.2](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.1...engine.io-client@6.6.2) (2024-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** remove ws type from .d.ts file ([175a2c5](https://github.com/socketio/socket.io/commit/175a2c58c1bc37eb9b87f87df47e1f9388b01d55))
|
||||
* prevent infinite loop with Node.js built-in WebSocket ([4865f2e](https://github.com/socketio/socket.io/commit/4865f2e62eff9cf59f602e753d9f84159a3139af))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.1](https://github.com/socketio/socket.io/compare/engine.io-client@6.6.0...engine.io-client@6.6.1) (2024-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* move 'offline' event listener at the top ([8a2f5a3](https://github.com/socketio/socket.io/commit/8a2f5a3da0addb386e7a0f4970e1a9696b82797e))
|
||||
* only remove the event listener if it exists ([9b3c9ab](https://github.com/socketio/socket.io/commit/9b3c9abecab028822357beb6e2b502f548e312eb)), closes [/github.com/socketio/socket.io/issues/5088#issuecomment-2217202350](https://github.com//github.com/socketio/socket.io/issues/5088/issues/issuecomment-2217202350)
|
||||
* do not send a packet on an expired connection ([#5134](https://github.com/socketio/socket.io/issues/5134)) ([8adcfbf](https://github.com/socketio/socket.io/commit/8adcfbfde50679095ec2abe376650cf2b6814325))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* do not reset the heartbeat timer on each packet ([7a23dde](https://github.com/socketio/socket.io/commit/7a23dde6efff3079edeeda951fe0ee25516da833))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.0](https://github.com/socketio/engine.io-client/compare/6.5.3...6.6.0) (2024-06-21)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
899
packages/engine.io-client/dist/engine.io.js
vendored
899
packages/engine.io-client/dist/engine.io.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -55,7 +55,7 @@ export function parse(setCookieString: string): Cookie {
|
||||
case "Max-Age":
|
||||
const expiration = new Date();
|
||||
expiration.setUTCSeconds(
|
||||
expiration.getUTCSeconds() + parseInt(value, 10)
|
||||
expiration.getUTCSeconds() + parseInt(value, 10),
|
||||
);
|
||||
cookie.expires = expiration;
|
||||
break;
|
||||
|
||||
@@ -10,11 +10,34 @@ import {
|
||||
CookieJar,
|
||||
createCookieJar,
|
||||
defaultBinaryType,
|
||||
nextTick,
|
||||
} from "./globals.node.js";
|
||||
import debugModule from "debug"; // debug()
|
||||
|
||||
const debug = debugModule("engine.io-client:socket"); // debug()
|
||||
|
||||
const withEventListeners =
|
||||
typeof addEventListener === "function" &&
|
||||
typeof removeEventListener === "function";
|
||||
|
||||
const OFFLINE_EVENT_LISTENERS = [];
|
||||
|
||||
if (withEventListeners) {
|
||||
// within a ServiceWorker, any event handler for the 'offline' event must be added on the initial evaluation of the
|
||||
// script, so we create one single event listener here which will forward the event to the socket instances
|
||||
addEventListener(
|
||||
"offline",
|
||||
() => {
|
||||
debug(
|
||||
"closing %d connection(s) because the network was lost",
|
||||
OFFLINE_EVENT_LISTENERS.length,
|
||||
);
|
||||
OFFLINE_EVENT_LISTENERS.forEach((listener) => listener());
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
export interface SocketOptions {
|
||||
/**
|
||||
* The host that we're connecting to. Set from the URI passed when connecting
|
||||
@@ -85,7 +108,9 @@ export interface SocketOptions {
|
||||
*
|
||||
* @default ['polling','websocket', 'webtransport']
|
||||
*/
|
||||
transports?: string[] | TransportCtor[];
|
||||
transports?:
|
||||
| ("polling" | "websocket" | "webtransport" | string)[]
|
||||
| TransportCtor[];
|
||||
|
||||
/**
|
||||
* Whether all the transports should be tested, instead of just the first one.
|
||||
@@ -318,6 +343,11 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
private _pingTimeout: number = -1;
|
||||
private _maxPayload?: number = -1;
|
||||
private _pingTimeoutTimer: NodeJS.Timer;
|
||||
/**
|
||||
* The expiration timestamp of the {@link _pingTimeoutTimer} object is tracked, in case the timer is throttled and the
|
||||
* callback is not fired on time. This can happen for example when a laptop is suspended or when a phone is locked.
|
||||
*/
|
||||
private _pingTimeoutTime = Infinity;
|
||||
private clearTimeoutFn: typeof clearTimeout;
|
||||
private readonly _beforeunloadEventListener: () => void;
|
||||
private readonly _offlineEventListener: () => void;
|
||||
@@ -379,8 +409,8 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
(typeof location !== "undefined" && location.port
|
||||
? location.port
|
||||
: this.secure
|
||||
? "443"
|
||||
: "80");
|
||||
? "443"
|
||||
: "80");
|
||||
|
||||
this.transports = [];
|
||||
this._transportsByName = {};
|
||||
@@ -406,7 +436,7 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
transportOptions: {},
|
||||
closeOnBeforeunload: false,
|
||||
},
|
||||
opts
|
||||
opts,
|
||||
);
|
||||
|
||||
this.opts.path =
|
||||
@@ -417,7 +447,7 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
this.opts.query = decode(this.opts.query);
|
||||
}
|
||||
|
||||
if (typeof addEventListener === "function") {
|
||||
if (withEventListeners) {
|
||||
if (this.opts.closeOnBeforeunload) {
|
||||
// Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
|
||||
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
|
||||
@@ -432,16 +462,17 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
addEventListener(
|
||||
"beforeunload",
|
||||
this._beforeunloadEventListener,
|
||||
false
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (this.hostname !== "localhost") {
|
||||
debug("adding listener for the 'offline' event");
|
||||
this._offlineEventListener = () => {
|
||||
this._onClose("transport close", {
|
||||
description: "network connection lost",
|
||||
});
|
||||
};
|
||||
addEventListener("offline", this._offlineEventListener, false);
|
||||
OFFLINE_EVENT_LISTENERS.push(this._offlineEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +513,7 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
secure: this.secure,
|
||||
port: this.port,
|
||||
},
|
||||
this.opts.transportOptions[name]
|
||||
this.opts.transportOptions[name],
|
||||
);
|
||||
|
||||
debug("options: %j", opts);
|
||||
@@ -572,7 +603,6 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
|
||||
// Socket is live - any packet counts
|
||||
this.emitReserved("heartbeat");
|
||||
this._resetPingTimeout();
|
||||
|
||||
switch (packet.type) {
|
||||
case "open":
|
||||
@@ -583,6 +613,7 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
this._sendPacket("pong");
|
||||
this.emitReserved("ping");
|
||||
this.emitReserved("pong");
|
||||
this._resetPingTimeout();
|
||||
break;
|
||||
|
||||
case "error":
|
||||
@@ -628,9 +659,11 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
*/
|
||||
private _resetPingTimeout() {
|
||||
this.clearTimeoutFn(this._pingTimeoutTimer);
|
||||
const delay = this._pingInterval + this._pingTimeout;
|
||||
this._pingTimeoutTime = Date.now() + delay;
|
||||
this._pingTimeoutTimer = this.setTimeoutFn(() => {
|
||||
this._onClose("ping timeout");
|
||||
}, this._pingInterval + this._pingTimeout);
|
||||
}, delay);
|
||||
if (this.opts.autoUnref) {
|
||||
this._pingTimeoutTimer.unref();
|
||||
}
|
||||
@@ -708,6 +741,31 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
return this.writeBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the heartbeat timer has expired but the socket has not yet been notified.
|
||||
*
|
||||
* Note: this method is private for now because it does not really fit the WebSocket API, but if we put it in the
|
||||
* `write()` method then the message would not be buffered by the Socket.IO client.
|
||||
*
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
/* private */ _hasPingExpired() {
|
||||
if (!this._pingTimeoutTime) return true;
|
||||
|
||||
const hasExpired = Date.now() > this._pingTimeoutTime;
|
||||
if (hasExpired) {
|
||||
debug("throttled timer detected, scheduling connection close");
|
||||
this._pingTimeoutTime = 0;
|
||||
|
||||
nextTick(() => {
|
||||
this._onClose("ping timeout");
|
||||
}, this.setTimeoutFn);
|
||||
}
|
||||
|
||||
return hasExpired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message.
|
||||
*
|
||||
@@ -747,7 +805,7 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
type: PacketType,
|
||||
data?: RawData,
|
||||
options?: WriteOptions,
|
||||
fn?: () => void
|
||||
fn?: () => void,
|
||||
) {
|
||||
if ("function" === typeof data) {
|
||||
fn = data;
|
||||
@@ -868,13 +926,21 @@ export class SocketWithoutUpgrade extends Emitter<
|
||||
// ignore further transport communication
|
||||
this.transport.removeAllListeners();
|
||||
|
||||
if (typeof removeEventListener === "function") {
|
||||
removeEventListener(
|
||||
"beforeunload",
|
||||
this._beforeunloadEventListener,
|
||||
false
|
||||
);
|
||||
removeEventListener("offline", this._offlineEventListener, false);
|
||||
if (withEventListeners) {
|
||||
if (this._beforeunloadEventListener) {
|
||||
removeEventListener(
|
||||
"beforeunload",
|
||||
this._beforeunloadEventListener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (this._offlineEventListener) {
|
||||
const i = OFFLINE_EVENT_LISTENERS.indexOf(this._offlineEventListener);
|
||||
if (i !== -1) {
|
||||
debug("removing listener for the 'offline' event");
|
||||
OFFLINE_EVENT_LISTENERS.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set ready state
|
||||
|
||||
@@ -14,7 +14,7 @@ export class TransportError extends Error {
|
||||
constructor(
|
||||
reason: string,
|
||||
readonly description: any,
|
||||
readonly context: any
|
||||
readonly context: any,
|
||||
) {
|
||||
super(reason);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export abstract class Transport extends Emitter<
|
||||
protected onError(reason: string, description: any, context?: any) {
|
||||
super.emitReserved(
|
||||
"error",
|
||||
new TransportError(reason, description, context)
|
||||
new TransportError(reason, description, context),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export abstract class Transport extends Emitter<
|
||||
private _port() {
|
||||
if (
|
||||
this.opts.port &&
|
||||
((this.opts.secure && Number(this.opts.port !== 443)) ||
|
||||
((this.opts.secure && Number(this.opts.port) !== 443) ||
|
||||
(!this.opts.secure && Number(this.opts.port) !== 80))
|
||||
) {
|
||||
return ":" + this.opts.port;
|
||||
|
||||
@@ -15,12 +15,12 @@ export class XHR extends BaseXHR {
|
||||
Object.assign(
|
||||
opts,
|
||||
{ xd: this.xd, cookieJar: this.socket?._cookieJar },
|
||||
this.opts
|
||||
this.opts,
|
||||
);
|
||||
return new Request(
|
||||
(opts) => new XMLHttpRequest(opts),
|
||||
this.uri(),
|
||||
opts as RequestOptions
|
||||
opts as RequestOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export class Request extends Emitter<
|
||||
constructor(
|
||||
private readonly createRequest: (opts: RequestOptions) => XMLHttpRequest,
|
||||
uri: string,
|
||||
opts: RequestOptions
|
||||
opts: RequestOptions,
|
||||
) {
|
||||
super();
|
||||
installTimerFunctions(this, opts);
|
||||
@@ -151,7 +151,7 @@ export class Request extends Emitter<
|
||||
"ca",
|
||||
"ciphers",
|
||||
"rejectUnauthorized",
|
||||
"autoUnref"
|
||||
"autoUnref",
|
||||
);
|
||||
opts.xdomain = !!this._opts.xd;
|
||||
|
||||
@@ -197,7 +197,7 @@ export class Request extends Emitter<
|
||||
if (xhr.readyState === 3) {
|
||||
this._opts.cookieJar?.parseCookies(
|
||||
// @ts-ignore
|
||||
xhr.getResponseHeader("set-cookie")
|
||||
xhr.getResponseHeader("set-cookie"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ function newRequest(opts) {
|
||||
if (!xdomain) {
|
||||
try {
|
||||
return new globalThis[["Active"].concat("Object").join("X")](
|
||||
"Microsoft.XMLHTTP"
|
||||
"Microsoft.XMLHTTP",
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { WebSocket } from "ws";
|
||||
import * as ws from "ws";
|
||||
import type { Packet, RawData } from "engine.io-parser";
|
||||
import { BaseWS } from "./websocket.js";
|
||||
|
||||
@@ -14,8 +14,8 @@ export class WS extends BaseWS {
|
||||
createSocket(
|
||||
uri: string,
|
||||
protocols: string | string[] | undefined,
|
||||
opts: Record<string, any>
|
||||
) {
|
||||
opts: Record<string, any>,
|
||||
): any {
|
||||
if (this.socket?._cookieJar) {
|
||||
opts.headers = opts.headers || {};
|
||||
|
||||
@@ -27,7 +27,7 @@ export class WS extends BaseWS {
|
||||
opts.headers.cookie.push(`${name}=${cookie.value}`);
|
||||
}
|
||||
}
|
||||
return new WebSocket(uri, protocols, opts);
|
||||
return new ws.WebSocket(uri, protocols, opts);
|
||||
}
|
||||
|
||||
doWrite(packet: Packet, data: RawData) {
|
||||
|
||||
@@ -43,7 +43,7 @@ export abstract class BaseWS extends Transport {
|
||||
"origin",
|
||||
"maxPayload",
|
||||
"family",
|
||||
"checkServerIdentity"
|
||||
"checkServerIdentity",
|
||||
);
|
||||
|
||||
if (this.opts.extraHeaders) {
|
||||
@@ -64,7 +64,7 @@ export abstract class BaseWS extends Transport {
|
||||
abstract createSocket(
|
||||
uri: string,
|
||||
protocols: string | string[] | undefined,
|
||||
opts: Record<string, any>
|
||||
opts: Record<string, any>,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -123,6 +123,7 @@ export abstract class BaseWS extends Transport {
|
||||
|
||||
override doClose() {
|
||||
if (typeof this.ws !== "undefined") {
|
||||
this.ws.onerror = () => {};
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
@@ -166,7 +167,7 @@ export class WS extends BaseWS {
|
||||
createSocket(
|
||||
uri: string,
|
||||
protocols: string | string[] | undefined,
|
||||
opts: Record<string, any>
|
||||
opts: Record<string, any>,
|
||||
) {
|
||||
return !isReactNative
|
||||
? protocols
|
||||
|
||||
@@ -30,7 +30,7 @@ export class WT extends Transport {
|
||||
// @ts-ignore
|
||||
this._transport = new WebTransport(
|
||||
this.createUri("https"),
|
||||
this.opts.transportOptions[this.name]
|
||||
this.opts.transportOptions[this.name],
|
||||
);
|
||||
} catch (err) {
|
||||
return this.emitReserved("error", err);
|
||||
@@ -51,7 +51,7 @@ export class WT extends Transport {
|
||||
this._transport.createBidirectionalStream().then((stream) => {
|
||||
const decoderStream = createPacketDecoderStream(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
this.socket.binaryType
|
||||
this.socket.binaryType,
|
||||
);
|
||||
const reader = stream.readable.pipeThrough(decoderStream).getReader();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "engine.io-client",
|
||||
"description": "Client for the realtime Engine",
|
||||
"license": "MIT",
|
||||
"version": "6.6.0",
|
||||
"version": "6.6.4",
|
||||
"main": "./build/cjs/index.js",
|
||||
"module": "./build/esm/index.js",
|
||||
"exports": {
|
||||
@@ -53,16 +53,17 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
"ws": "~8.18.3",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
|
||||
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
|
||||
"test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
|
||||
"test:node-fetch": "USE_FETCH=1 npm run test:node",
|
||||
"test:node-builtin-ws": "USE_BUILTIN_WS=1 npm run test:node",
|
||||
"test:browser": "zuul test/index.js",
|
||||
"build": "rimraf ./dist && rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
|
||||
"bundle-size": "node support/bundle-size.js",
|
||||
@@ -82,7 +83,7 @@
|
||||
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io-client#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/socketio/socket.io.git"
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
|
||||
@@ -17,6 +17,19 @@ describe("connection", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should connect to localhost (ws)", (done) => {
|
||||
const socket = new Socket({
|
||||
transports: ["websocket"],
|
||||
});
|
||||
socket.on("open", () => {
|
||||
socket.on("message", (data) => {
|
||||
expect(data).to.equal("hi");
|
||||
socket.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should receive multibyte utf-8 strings with polling", (done) => {
|
||||
const socket = new Socket();
|
||||
socket.on("open", () => {
|
||||
@@ -34,12 +47,12 @@ describe("connection", function () {
|
||||
const socket = new Socket();
|
||||
socket.on("open", () => {
|
||||
socket.send(
|
||||
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
|
||||
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF",
|
||||
);
|
||||
socket.on("message", (data) => {
|
||||
if ("hi" === data) return;
|
||||
expect(data).to.be(
|
||||
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
|
||||
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF",
|
||||
);
|
||||
socket.close();
|
||||
done();
|
||||
@@ -59,7 +72,7 @@ describe("connection", function () {
|
||||
setTimeout(() => {
|
||||
expect(noPacket).to.be(true);
|
||||
done();
|
||||
}, 1200);
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,7 +190,7 @@ describe("connection", function () {
|
||||
setTimeout(() => {
|
||||
expect(noPacket).to.be(true);
|
||||
done();
|
||||
}, 1200);
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ const socket = new Socket("http://localhost:3000", {
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("process should not exit");
|
||||
}, 500);
|
||||
}, 50);
|
||||
|
||||
@@ -10,4 +10,4 @@ socket.on("open", () => {
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("process should exit now");
|
||||
}, 500);
|
||||
}, 50);
|
||||
|
||||
@@ -10,4 +10,4 @@ socket.on("open", () => {
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("process should exit now");
|
||||
}, 500);
|
||||
}, 50);
|
||||
|
||||
@@ -9,4 +9,4 @@ socket.on("open", () => {
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("process should exit now");
|
||||
}, 500);
|
||||
}, 50);
|
||||
|
||||
@@ -7,8 +7,6 @@ if (env.browser) {
|
||||
require("./node");
|
||||
}
|
||||
|
||||
const Blob = require("blob");
|
||||
|
||||
require("./engine.io-client");
|
||||
require("./socket");
|
||||
require("./transport");
|
||||
@@ -23,6 +21,6 @@ if (typeof ArrayBuffer !== "undefined") {
|
||||
}
|
||||
|
||||
// Blob is available in Node.js since v18, but not yet supported by the `engine.io-parser` package
|
||||
if (Blob && env.browser) {
|
||||
if (typeof Blob === "function" && env.browser) {
|
||||
require("./blob");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("node.js", () => {
|
||||
isComplete = true;
|
||||
process.kill();
|
||||
done();
|
||||
}, 1000);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("Socket", function () {
|
||||
|
||||
socket.on("error", (err) => {
|
||||
expect(err.message).to.eql(
|
||||
useFetch ? "fetch read error" : "xhr poll error"
|
||||
useFetch ? "fetch read error" : "xhr poll error",
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -237,7 +237,7 @@ describe("Socket", function () {
|
||||
// err.context is a XMLHttpRequest object
|
||||
expect(err.context.readyState).to.eql(4);
|
||||
expect(err.context.responseText).to.eql(
|
||||
'{"code":1,"message":"Session ID unknown"}'
|
||||
'{"code":1,"message":"Session ID unknown"}',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -270,4 +270,30 @@ describe("Socket", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("throttled timer", () => {
|
||||
it("checks the state of the timer", (done) => {
|
||||
const socket = new Socket();
|
||||
|
||||
expect(socket._hasPingExpired()).to.be(false);
|
||||
|
||||
socket.on("open", () => {
|
||||
expect(socket._hasPingExpired()).to.be(false);
|
||||
|
||||
// simulate a throttled timer
|
||||
socket._pingTimeoutTime = Date.now() - 1;
|
||||
|
||||
expect(socket._hasPingExpired()).to.be(true);
|
||||
|
||||
// subsequent calls should not trigger more 'close' events
|
||||
expect(socket._hasPingExpired()).to.be(true);
|
||||
expect(socket._hasPingExpired()).to.be(true);
|
||||
});
|
||||
|
||||
socket.on("close", (reason) => {
|
||||
expect(reason).to.eql("ping timeout");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,3 +35,11 @@ if (exports.useFetch) {
|
||||
const { transports, Fetch } = require("../..");
|
||||
transports.polling = Fetch;
|
||||
}
|
||||
|
||||
exports.useBuiltinWs = process.env.USE_BUILTIN_WS !== undefined;
|
||||
|
||||
if (exports.useBuiltinWs) {
|
||||
console.warn("testing with built-in WebSocket object");
|
||||
const { transports, WebSocket } = require("../..");
|
||||
transports.websocket = WebSocket;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ exports.mochaHooks = {
|
||||
maxHttpBufferSize: 100,
|
||||
allowRequest: (req, fn) => {
|
||||
const denyRequest = new URL(`http://${req.url}`).searchParams.has(
|
||||
"deny"
|
||||
"deny",
|
||||
);
|
||||
fn(null, !denyRequest);
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("Transport", () => {
|
||||
timestampRequests: false,
|
||||
});
|
||||
expect(polling.uri()).to.contain(
|
||||
"http://localhost:3000/engine.io?sid=test"
|
||||
"http://localhost:3000/engine.io?sid=test",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -116,6 +116,32 @@ describe("Transport", () => {
|
||||
expect(polling.uri()).to.contain("https://localhost/engine.io?sid=test");
|
||||
});
|
||||
|
||||
it("should generate an https uri w/o a port (string)", () => {
|
||||
const polling = new eio.transports.polling({
|
||||
path: "/engine.io",
|
||||
hostname: "localhost",
|
||||
secure: true,
|
||||
query: { sid: "test" },
|
||||
port: "443",
|
||||
timestampRequests: false,
|
||||
});
|
||||
expect(polling.uri()).to.contain("https://localhost/engine.io?sid=test");
|
||||
});
|
||||
|
||||
it("should generate an https uri with a port", () => {
|
||||
const polling = new eio.transports.polling({
|
||||
path: "/engine.io",
|
||||
hostname: "localhost",
|
||||
secure: true,
|
||||
query: { sid: "test" },
|
||||
port: 8443,
|
||||
timestampRequests: false,
|
||||
});
|
||||
expect(polling.uri()).to.contain(
|
||||
"https://localhost:8443/engine.io?sid=test",
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate a timestamped uri", () => {
|
||||
const polling = new eio.transports.polling({
|
||||
path: "/engine.io",
|
||||
@@ -124,7 +150,7 @@ describe("Transport", () => {
|
||||
timestampRequests: true,
|
||||
});
|
||||
expect(polling.uri()).to.match(
|
||||
/http:\/\/localhost\/engine\.io\?(j=[0-9]+&)?(t=[0-9A-Za-z-_]+)/
|
||||
/http:\/\/localhost\/engine\.io\?(j=[0-9]+&)?(t=[0-9A-Za-z-_]+)/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -180,7 +206,7 @@ describe("Transport", () => {
|
||||
timestampRequests: true,
|
||||
});
|
||||
expect(ws.uri()).to.match(
|
||||
/ws:\/\/localhost\/engine\.io\?woot=[0-9A-Za-z-_]+/
|
||||
/ws:\/\/localhost\/engine\.io\?woot=[0-9A-Za-z-_]+/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -210,7 +236,10 @@ describe("Transport", () => {
|
||||
// these are server only
|
||||
if (!env.browser) {
|
||||
describe("options", () => {
|
||||
it("should accept an `agent` option for WebSockets", (done) => {
|
||||
it("should accept an `agent` option for WebSockets", function (done) {
|
||||
if (env.useBuiltinWs) {
|
||||
return this.skip();
|
||||
}
|
||||
const polling = new eio.transports.websocket({
|
||||
path: "/engine.io",
|
||||
hostname: "localhost",
|
||||
@@ -269,7 +298,10 @@ describe("Transport", () => {
|
||||
});
|
||||
|
||||
describe("perMessageDeflate", () => {
|
||||
it("should set threshold", (done) => {
|
||||
it("should set threshold", function (done) {
|
||||
if (env.useBuiltinWs) {
|
||||
return this.skip();
|
||||
}
|
||||
const socket = new eio.Socket({
|
||||
transports: ["websocket"],
|
||||
perMessageDeflate: { threshold: 0 },
|
||||
@@ -289,7 +321,10 @@ describe("Transport", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not compress when the byte size is below threshold", (done) => {
|
||||
it("should not compress when the byte size is below threshold", function (done) {
|
||||
if (env.useBuiltinWs) {
|
||||
return this.skip();
|
||||
}
|
||||
const socket = new eio.Socket({ transports: ["websocket"] });
|
||||
socket.on("open", () => {
|
||||
const ws = socket.transport.ws;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Http3Server, WebTransport } from "@fails-components/webtransport";
|
||||
import { Http3EventLoop } from "@fails-components/webtransport/lib/event-loop.js";
|
||||
import expect from "expect.js";
|
||||
import { Server } from "engine.io";
|
||||
import { Socket } from "../build/esm-debug/index.js";
|
||||
@@ -16,8 +15,8 @@ async function setup(opts, cb) {
|
||||
const certificate = await generateWebTransportCertificate(
|
||||
[{ shortName: "CN", value: "localhost" }],
|
||||
{
|
||||
days: 14, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
|
||||
}
|
||||
days: 13, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
|
||||
},
|
||||
);
|
||||
|
||||
const engine = new Server(opts);
|
||||
@@ -48,7 +47,30 @@ async function setup(opts, cb) {
|
||||
})();
|
||||
|
||||
h3Server.startServer();
|
||||
h3Server.onServerListening = () => cb({ engine, h3Server, certificate });
|
||||
|
||||
await h3Server.ready;
|
||||
|
||||
cb({ engine, h3Server, certificate });
|
||||
}
|
||||
|
||||
function createHttpServer(port) {
|
||||
const httpServer = createServer();
|
||||
|
||||
let retryCount = 0;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
httpServer.listen(port, () => resolve(httpServer));
|
||||
|
||||
httpServer.on("error", (e) => {
|
||||
if (e.code === "EADDRINUSE" && ++retryCount <= 3) {
|
||||
console.warn("port already in use, retrying...");
|
||||
setTimeout(() => {
|
||||
httpServer.listen(port, () => resolve(httpServer));
|
||||
}, 100);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function success(engine, h3server, done) {
|
||||
@@ -73,16 +95,12 @@ function createSocket(port, certificate, opts) {
|
||||
},
|
||||
},
|
||||
},
|
||||
opts
|
||||
)
|
||||
opts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe("WebTransport", () => {
|
||||
after(() => {
|
||||
Http3EventLoop.globalLoop.shutdownEventLoop(); // manually shutdown the event loop, instead of waiting 20s
|
||||
});
|
||||
|
||||
it("should allow to connect with WebTransport directly", (done) => {
|
||||
setup({}, ({ engine, h3Server, certificate }) => {
|
||||
const socket = createSocket(h3Server.port, certificate, {
|
||||
@@ -100,10 +118,9 @@ describe("WebTransport", () => {
|
||||
{
|
||||
transports: ["polling", "webtransport"],
|
||||
},
|
||||
({ engine, h3Server, certificate }) => {
|
||||
const httpServer = createServer();
|
||||
async ({ engine, h3Server, certificate }) => {
|
||||
const httpServer = await createHttpServer(h3Server.port);
|
||||
engine.attach(httpServer);
|
||||
httpServer.listen(h3Server.port);
|
||||
|
||||
const socket = createSocket(h3Server.port, certificate, {
|
||||
transports: ["polling", "webtransport"],
|
||||
@@ -113,7 +130,7 @@ describe("WebTransport", () => {
|
||||
httpServer.close();
|
||||
success(engine, h3Server, done);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -122,10 +139,9 @@ describe("WebTransport", () => {
|
||||
{
|
||||
transports: ["polling", "websocket", "webtransport"],
|
||||
},
|
||||
({ engine, h3Server, certificate }) => {
|
||||
const httpServer = createServer();
|
||||
async ({ engine, h3Server, certificate }) => {
|
||||
const httpServer = await createHttpServer(h3Server.port);
|
||||
engine.attach(httpServer);
|
||||
httpServer.listen(h3Server.port);
|
||||
|
||||
const socket = createSocket(h3Server.port, certificate, {
|
||||
transports: ["polling", "websocket", "webtransport"],
|
||||
@@ -137,7 +153,7 @@ describe("WebTransport", () => {
|
||||
httpServer.close();
|
||||
success(engine, h3Server, done);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,7 +176,7 @@ describe("WebTransport", () => {
|
||||
success(engine, h3Server, done);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const browsers = require('socket.io-browsers');
|
||||
|
||||
const zuulConfig = module.exports = {
|
||||
ui: 'mocha-bdd',
|
||||
|
||||
// test on localhost by default
|
||||
local: true,
|
||||
open: true,
|
||||
|
||||
concurrency: 2, // ngrok only accepts two tunnels by default
|
||||
// if browser does not sends output in 120s since last output:
|
||||
// stop testing, something is wrong
|
||||
browser_output_timeout: 120 * 1000,
|
||||
browser_open_timeout: 60 * 4 * 1000,
|
||||
// we want to be notified something is wrong asap, so no retry
|
||||
browser_retries: 1,
|
||||
|
||||
server: './test/support/server.js',
|
||||
builder: 'zuul-builder-webpack',
|
||||
webpack: require('./support/webpack.config.js')
|
||||
};
|
||||
|
||||
if (process.env.CI === 'true') {
|
||||
zuulConfig.local = false;
|
||||
zuulConfig.tunnel = {
|
||||
type: 'ngrok',
|
||||
bind_tls: true
|
||||
};
|
||||
}
|
||||
|
||||
zuulConfig.browsers = [
|
||||
{
|
||||
name: 'firefox',
|
||||
version: 'latest'
|
||||
}, {
|
||||
name: 'internet explorer',
|
||||
version: '9..11'
|
||||
}, {
|
||||
name: 'safari',
|
||||
version: '14'
|
||||
}, {
|
||||
name: 'iphone',
|
||||
version: '14'
|
||||
}, {
|
||||
name: 'android',
|
||||
version: '5.1..6.0'
|
||||
}, {
|
||||
name: 'ipad',
|
||||
version: '14'
|
||||
}, {
|
||||
name: 'MicrosoftEdge',
|
||||
version: 'latest'
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const mapBinary = (data: RawData, binaryType?: BinaryType) => {
|
||||
);
|
||||
} else {
|
||||
// from WebTransport (Uint8Array)
|
||||
return data.buffer;
|
||||
return data.slice().buffer;
|
||||
}
|
||||
case "nodebuffer":
|
||||
default:
|
||||
|
||||
@@ -10,13 +10,10 @@
|
||||
"require": "./build/cjs/index.js"
|
||||
},
|
||||
"types": "build/esm/index.d.ts",
|
||||
"devDependencies": {
|
||||
"prettier": "^3.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
|
||||
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
|
||||
"test:node": "nyc mocha -r ts-node/register test/index.ts",
|
||||
"test:node": "nyc mocha --import=tsx test/index.ts",
|
||||
"test:browser": "zuul test/index.ts --no-coverage",
|
||||
"format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.ts'",
|
||||
"format:fix": "prettier --write 'lib/**/*.ts' 'test/**/*.ts'",
|
||||
@@ -25,7 +22,7 @@
|
||||
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io-parser#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/socketio/socket.io.git"
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# History
|
||||
# Changelog
|
||||
|
||||
| Version | Release date |
|
||||
|------------------------------------------------------------------------------------------------------|----------------|
|
||||
| [6.6.5](#665-2025-12-22) | December 2025 |
|
||||
| [6.6.4](#664-2025-01-28) | January 2025 |
|
||||
| [6.6.3](#663-2025-01-23) | January 2025 |
|
||||
| [6.6.2](#662-2024-10-09) | October 2024 |
|
||||
| [6.6.1](#661-2024-09-21) | September 2024 |
|
||||
| [6.6.0](#660-2024-06-21) | June 2024 |
|
||||
| [6.5.5](#655-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io/tree/6.5.x) branch) | June 2024 |
|
||||
| [3.6.2](#362-2024-06-18) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | June 2024 |
|
||||
@@ -43,7 +48,68 @@
|
||||
| [3.4.1](#341-2020-04-17) | April 2020 |
|
||||
|
||||
|
||||
# Release notes
|
||||
## [6.6.5](https://github.com/socketio/socket.io/compare/engine.io@6.6.4...engine.io@6.6.5) (2025-12-22)
|
||||
|
||||
The `url.parse()` function is now deprecated and has been replaced by `new URL()` (see [e08293b](https://github.com/socketio/socket.io/commit/e08293bc3735de5b824b347383e86e0b8ab9fbd5)).
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.18.3`](https://github.com/websockets/ws/releases/tag/8.18.3) ([diff](https://github.com/websockets/ws/compare/8.17.1...8.18.3))
|
||||
|
||||
|
||||
|
||||
## [6.6.4](https://github.com/socketio/socket.io/compare/engine.io@6.6.3...engine.io@6.6.4) (2025-01-28)
|
||||
|
||||
The bump of the `cookie` dependency was reverted, as it drops support for older Node.js versions (< 14).
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.3](https://github.com/socketio/socket.io/compare/engine.io@6.6.2...engine.io@6.6.3) (2025-01-23)
|
||||
|
||||
This release contains a bump of the `cookie` dependency.
|
||||
|
||||
Release notes: https://github.com/jshttp/cookie/releases/tag/v1.0.0
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.2](https://github.com/socketio/socket.io/compare/engine.io@6.6.1...engine.io@6.6.2) (2024-10-09)
|
||||
|
||||
This release contains a bump of the `cookie` dependency.
|
||||
|
||||
See also: https://github.com/advisories/GHSA-pxg6-pf52-xh8x
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.1](https://github.com/socketio/socket.io/compare/engine.io@6.6.0...engine.io@6.6.1) (2024-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* discard all pending packets when the server is closed ([923a12e](https://github.com/socketio/socket.io/commit/923a12e2de83ecaa75746a575e71a4739815d5c5))
|
||||
* **uws:** prevent the client from upgrading twice ([d5095fe](https://github.com/socketio/socket.io/commit/d5095fe98c3976673c19f433c0114d06dbd8de1b))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
|
||||
|
||||
|
||||
|
||||
## [6.6.0](https://github.com/socketio/engine.io/compare/6.5.4...6.6.0) (2024-06-21)
|
||||
|
||||
|
||||
117
packages/engine.io/lib/contrib/types.cookie.ts
Normal file
117
packages/engine.io/lib/contrib/types.cookie.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// imported from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b83cf9ef8b044e69f05b2a00aa7c6cb767a9acd2/types/cookie/index.d.ts (now deleted)
|
||||
/**
|
||||
* Basic HTTP cookie parser and serializer for HTTP servers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Additional serialization options
|
||||
*/
|
||||
export interface CookieSerializeOptions {
|
||||
/**
|
||||
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
|
||||
* domain is set, and most clients will consider the cookie to apply to only
|
||||
* the current domain.
|
||||
*/
|
||||
domain?: string | undefined;
|
||||
|
||||
/**
|
||||
* Specifies a function that will be used to encode a cookie's value. Since
|
||||
* value of a cookie has a limited character set (and must be a simple
|
||||
* string), this function can be used to encode a value into a string suited
|
||||
* for a cookie's value.
|
||||
*
|
||||
* The default function is the global `encodeURIComponent`, which will
|
||||
* encode a JavaScript string into UTF-8 byte sequences and then URL-encode
|
||||
* any that fall outside of the cookie range.
|
||||
*/
|
||||
encode?(value: string): string;
|
||||
|
||||
/**
|
||||
* Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default,
|
||||
* no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete
|
||||
* it on a condition like exiting a web browser application.
|
||||
*
|
||||
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
|
||||
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
|
||||
* possible not all clients by obey this, so if both are set, they should
|
||||
* point to the same date and time.
|
||||
*/
|
||||
expires?: Date | undefined;
|
||||
/**
|
||||
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}.
|
||||
* When truthy, the `HttpOnly` attribute is set, otherwise it is not. By
|
||||
* default, the `HttpOnly` attribute is not set.
|
||||
*
|
||||
* *Note* be careful when setting this to true, as compliant clients will
|
||||
* not allow client-side JavaScript to see the cookie in `document.cookie`.
|
||||
*/
|
||||
httpOnly?: boolean | undefined;
|
||||
/**
|
||||
* Specifies the number (in seconds) to be the value for the `Max-Age`
|
||||
* `Set-Cookie` attribute. The given number will be converted to an integer
|
||||
* by rounding down. By default, no maximum age is set.
|
||||
*
|
||||
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
|
||||
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
|
||||
* possible not all clients by obey this, so if both are set, they should
|
||||
* point to the same date and time.
|
||||
*/
|
||||
maxAge?: number | undefined;
|
||||
/**
|
||||
* Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies)
|
||||
* attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the
|
||||
* `Partitioned` attribute is not set.
|
||||
*
|
||||
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
* This also means many clients may ignore this attribute until they understand it.
|
||||
*
|
||||
* More information about can be found in [the proposal](https://github.com/privacycg/CHIPS)
|
||||
*/
|
||||
partitioned?: boolean | undefined;
|
||||
/**
|
||||
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}.
|
||||
* By default, the path is considered the "default path".
|
||||
*/
|
||||
path?: string | undefined;
|
||||
/**
|
||||
* Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
|
||||
*
|
||||
* - `'low'` will set the `Priority` attribute to `Low`.
|
||||
* - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
|
||||
* - `'high'` will set the `Priority` attribute to `High`.
|
||||
*
|
||||
* More information about the different priority levels can be found in
|
||||
* [the specification][rfc-west-cookie-priority-00-4.1].
|
||||
*
|
||||
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
* This also means many clients may ignore this attribute until they understand it.
|
||||
*/
|
||||
priority?: "low" | "medium" | "high" | undefined;
|
||||
/**
|
||||
* Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}.
|
||||
*
|
||||
* - `true` will set the `SameSite` attribute to `Strict` for strict same
|
||||
* site enforcement.
|
||||
* - `false` will not set the `SameSite` attribute.
|
||||
* - `'lax'` will set the `SameSite` attribute to Lax for lax same site
|
||||
* enforcement.
|
||||
* - `'strict'` will set the `SameSite` attribute to Strict for strict same
|
||||
* site enforcement.
|
||||
* - `'none'` will set the SameSite attribute to None for an explicit
|
||||
* cross-site cookie.
|
||||
*
|
||||
* More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}.
|
||||
*
|
||||
* *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it.
|
||||
*/
|
||||
sameSite?: true | false | "lax" | "strict" | "none" | undefined;
|
||||
/**
|
||||
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the
|
||||
* `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
*
|
||||
* *Note* be careful when setting this to `true`, as compliant clients will
|
||||
* not send the cookie back to the server in the future if the browser does
|
||||
* not have an HTTPS connection.
|
||||
*/
|
||||
secure?: boolean | undefined;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import * as qs from "querystring";
|
||||
import { parse } from "url";
|
||||
import * as base64id from "base64id";
|
||||
import transports from "./transports";
|
||||
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,
|
||||
ServerResponse,
|
||||
} from "http";
|
||||
import type { CookieSerializeOptions } from "cookie";
|
||||
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 {
|
||||
/**
|
||||
@@ -78,7 +86,7 @@ export interface ServerOptions {
|
||||
*/
|
||||
allowRequest?: (
|
||||
req: IncomingMessage,
|
||||
fn: (err: string | null | undefined, success: boolean) => void
|
||||
fn: (err: string | null | undefined, success: boolean) => void,
|
||||
) => void;
|
||||
/**
|
||||
* The low-level transports that are enabled. WebTransport is disabled by default and must be manually enabled:
|
||||
@@ -90,7 +98,7 @@ export interface ServerOptions {
|
||||
*
|
||||
* @default ["polling", "websocket"]
|
||||
*/
|
||||
transports?: Transport[];
|
||||
transports?: TransportName[];
|
||||
/**
|
||||
* whether to allow transport upgrades
|
||||
* @default true
|
||||
@@ -100,7 +108,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
|
||||
@@ -146,10 +154,10 @@ export interface ServerOptions {
|
||||
type Middleware = (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: (err?: any) => void
|
||||
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") {
|
||||
@@ -192,7 +200,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
cors: false,
|
||||
allowEIO3: false,
|
||||
},
|
||||
opts
|
||||
opts,
|
||||
);
|
||||
|
||||
if (opts.cookie) {
|
||||
@@ -204,7 +212,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
httpOnly: opts.cookie.path !== false,
|
||||
sameSite: "lax",
|
||||
},
|
||||
opts.cookie
|
||||
opts.cookie,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,14 +225,14 @@ export abstract class BaseServer extends EventEmitter {
|
||||
{
|
||||
threshold: 1024,
|
||||
},
|
||||
opts.perMessageDeflate
|
||||
opts.perMessageDeflate,
|
||||
);
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected abstract init();
|
||||
protected abstract init(): void;
|
||||
|
||||
/**
|
||||
* Compute the pathname of the requests that are handled by the server
|
||||
@@ -244,10 +252,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 +265,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);
|
||||
@@ -361,7 +368,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
protected _applyMiddlewares(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
callback: (err?: any) => void
|
||||
callback: (err?: any) => void,
|
||||
) {
|
||||
if (this.middlewares.length === 0) {
|
||||
debug("no middleware to apply, skipping");
|
||||
@@ -408,7 +415,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 +429,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) {
|
||||
@@ -519,7 +526,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
public async onWebTransportSession(session: any) {
|
||||
const timeout = setTimeout(() => {
|
||||
debug(
|
||||
"the client failed to establish a bidirectional stream in the given period"
|
||||
"the client failed to establish a bidirectional stream in the given period",
|
||||
);
|
||||
session.close();
|
||||
}, this.opts.upgradeTimeout);
|
||||
@@ -535,7 +542,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
const stream = result.value;
|
||||
const transformStream = createPacketDecoderStream(
|
||||
this.opts.maxHttpBufferSize,
|
||||
"nodebuffer"
|
||||
"nodebuffer",
|
||||
);
|
||||
const reader = stream.readable.pipeThrough(transformStream).getReader();
|
||||
|
||||
@@ -600,7 +607,10 @@ export abstract class BaseServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract createTransport(transportName, req);
|
||||
protected abstract createTransport(
|
||||
transportName: TransportName,
|
||||
req: EngineRequest,
|
||||
);
|
||||
|
||||
/**
|
||||
* Protocol errors mappings.
|
||||
@@ -613,7 +623,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 +632,7 @@ export abstract class BaseServer extends EventEmitter {
|
||||
3: "Bad request",
|
||||
4: "Forbidden",
|
||||
5: "Unsupported protocol version",
|
||||
};
|
||||
} as const;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -632,7 +642,10 @@ export abstract class BaseServer extends EventEmitter {
|
||||
* @see https://nodejs.org/api/http.html#class-httpserverresponse
|
||||
*/
|
||||
class WebSocketResponse {
|
||||
constructor(readonly req, readonly socket: Duplex) {
|
||||
constructor(
|
||||
readonly req,
|
||||
readonly socket: Duplex,
|
||||
) {
|
||||
// temporarily store the response headers on the req object (see the "headers" event)
|
||||
req[kResponseHeaders] = {};
|
||||
}
|
||||
@@ -659,9 +672,12 @@ class WebSocketResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Engine.IO server based on Node.js built-in HTTP server and the `ws` package for WebSocket connections.
|
||||
*/
|
||||
export class Server extends BaseServer {
|
||||
public httpServer?: HttpServer;
|
||||
private ws: any;
|
||||
private ws: WsServer;
|
||||
|
||||
/**
|
||||
* Initialize websocket server
|
||||
@@ -681,7 +697,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] || {};
|
||||
@@ -718,13 +734,16 @@ export class Server extends BaseServer {
|
||||
private prepare(req: EngineRequest) {
|
||||
// try to leverage pre-existing `req._query` (e.g: from connect)
|
||||
if (!req._query) {
|
||||
req._query = (
|
||||
~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
|
||||
) as Record<string, string>;
|
||||
const url = new URL(req.url, "https://socket.io");
|
||||
req._query = Object.fromEntries(url.searchParams.entries());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -739,7 +758,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,
|
||||
@@ -757,7 +776,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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -776,12 +799,12 @@ export class Server extends BaseServer {
|
||||
public handleUpgrade(
|
||||
req: EngineRequest,
|
||||
socket: Duplex,
|
||||
upgradeHead: Buffer
|
||||
upgradeHead: Buffer,
|
||||
) {
|
||||
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,
|
||||
@@ -817,11 +840,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 (
|
||||
@@ -856,14 +884,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() {
|
||||
@@ -941,7 +977,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
|
||||
@@ -953,7 +993,7 @@ function abortRequest(res, errorCode, errorContext) {
|
||||
JSON.stringify({
|
||||
code: errorCode,
|
||||
message,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -968,7 +1008,7 @@ function abortRequest(res, errorCode, errorContext) {
|
||||
function abortUpgrade(
|
||||
socket,
|
||||
errorCode,
|
||||
errorContext: { message?: string } = {}
|
||||
errorContext: { message?: string } = {},
|
||||
) {
|
||||
socket.on("error", () => {
|
||||
debug("ignoring error from closed connection");
|
||||
@@ -984,7 +1024,7 @@ function abortUpgrade(
|
||||
length +
|
||||
"\r\n" +
|
||||
"\r\n" +
|
||||
message
|
||||
message,
|
||||
);
|
||||
}
|
||||
socket.destroy();
|
||||
@@ -1024,7 +1064,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)]) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -80,7 +81,7 @@ export class Socket extends EventEmitter {
|
||||
server: BaseServer,
|
||||
transport: Transport,
|
||||
req: EngineRequest,
|
||||
protocol: number
|
||||
protocol: number,
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
@@ -125,7 +126,7 @@ export class Socket extends EventEmitter {
|
||||
pingInterval: this.server.opts.pingInterval,
|
||||
pingTimeout: this.server.opts.pingTimeout,
|
||||
maxPayload: this.server.opts.maxHttpBufferSize,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.server.opts.initialPacket) {
|
||||
@@ -212,7 +213,7 @@ export class Socket extends EventEmitter {
|
||||
this.pingIntervalTimer = setTimeout(() => {
|
||||
debug(
|
||||
"writing ping packet - expecting pong within %sms",
|
||||
this.server.opts.pingTimeout
|
||||
this.server.opts.pingTimeout,
|
||||
);
|
||||
this.sendPacket("ping");
|
||||
this.resetPingTimeout();
|
||||
@@ -233,7 +234,7 @@ export class Socket extends EventEmitter {
|
||||
},
|
||||
this.protocol === 3
|
||||
? this.server.opts.pingInterval + this.server.opts.pingTimeout
|
||||
: this.server.opts.pingTimeout
|
||||
: this.server.opts.pingTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ export class Socket extends EventEmitter {
|
||||
debug(
|
||||
'might upgrade socket transport from "%s" to "%s"',
|
||||
this.transport.name,
|
||||
transport.name
|
||||
transport.name,
|
||||
);
|
||||
|
||||
this.upgrading = true;
|
||||
@@ -468,7 +469,7 @@ export class Socket extends EventEmitter {
|
||||
type: PacketType,
|
||||
data?: RawData,
|
||||
options: SendOptions = {},
|
||||
callback?: SendCallback
|
||||
callback?: SendCallback,
|
||||
) {
|
||||
if ("function" === typeof options) {
|
||||
callback = options;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -554,6 +557,13 @@ export class Socket extends EventEmitter {
|
||||
* @return {Socket} for chaining
|
||||
*/
|
||||
public close(discard?: boolean) {
|
||||
if (
|
||||
discard &&
|
||||
(this.readyState === "open" || this.readyState === "closing")
|
||||
) {
|
||||
return this.closeTransport(discard);
|
||||
}
|
||||
|
||||
if ("open" !== this.readyState) return;
|
||||
|
||||
this.readyState = "closing";
|
||||
@@ -561,7 +571,7 @@ export class Socket extends EventEmitter {
|
||||
if (this.writeBuffer.length) {
|
||||
debug(
|
||||
"there are %d remaining packets in the buffer, waiting for the 'drain' event",
|
||||
this.writeBuffer.length
|
||||
this.writeBuffer.length,
|
||||
);
|
||||
this.once("drain", () => {
|
||||
debug("all packets have been sent, closing the transport");
|
||||
@@ -570,7 +580,7 @@ export class Socket extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
debug("the buffer is empty, closing the transport right away", discard);
|
||||
debug("the buffer is empty, closing the transport right away");
|
||||
this.closeTransport(discard);
|
||||
}
|
||||
|
||||
@@ -581,7 +591,7 @@ export class Socket extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
private closeTransport(discard: boolean) {
|
||||
debug("closing the transport (discard? %s)", discard);
|
||||
debug("closing the transport (discard? %s)", !!discard);
|
||||
if (discard) this.transport.discard();
|
||||
this.transport.close(this.onClose.bind(this, "forced close"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -69,11 +74,16 @@ export abstract class Transport extends EventEmitter {
|
||||
"readyState updated from %s to %s (%s)",
|
||||
this._readyState,
|
||||
state,
|
||||
this.name
|
||||
this.name,
|
||||
);
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 +52,16 @@ 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
|
||||
isLast ? this._onSentLast : this._onSent,
|
||||
);
|
||||
} else {
|
||||
this.parser.encodePacket(
|
||||
packet,
|
||||
this.supportsBinary,
|
||||
isLast ? this._doSendLast : this._doSend
|
||||
isLast ? this._doSendLast : this._doSend,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ const debug = debugModule("engine:webtransport");
|
||||
export class WebTransport extends Transport {
|
||||
private readonly writer;
|
||||
|
||||
constructor(private readonly session, stream, reader) {
|
||||
constructor(
|
||||
private readonly session,
|
||||
stream,
|
||||
reader,
|
||||
) {
|
||||
super({ _query: { EIO: "4" } });
|
||||
|
||||
const transformStream = createPacketEncoderStream();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -23,6 +24,10 @@ export interface uOptions {
|
||||
maxBackpressure?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An Engine.IO server based on the `uWebSockets.js` package.
|
||||
*/
|
||||
// TODO export it into its own package
|
||||
export class uServer extends BaseServer {
|
||||
protected init() {}
|
||||
protected cleanup() {}
|
||||
@@ -32,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();
|
||||
|
||||
@@ -44,6 +49,7 @@ export class uServer extends BaseServer {
|
||||
req.headers[key] = value;
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
req.connection = {
|
||||
remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(),
|
||||
};
|
||||
@@ -53,7 +59,7 @@ export class uServer extends BaseServer {
|
||||
});
|
||||
}
|
||||
|
||||
protected createTransport(transportName, req) {
|
||||
protected createTransport(transportName: string, req: EngineRequest) {
|
||||
return new transports[transportName](req);
|
||||
}
|
||||
|
||||
@@ -64,7 +70,7 @@ export class uServer extends BaseServer {
|
||||
*/
|
||||
public attach(
|
||||
app /* : TemplatedApp */,
|
||||
options: AttachOptions & uOptions = {}
|
||||
options: AttachOptions & uOptions = {},
|
||||
) {
|
||||
const path = this._computePath(options);
|
||||
(app as TemplatedApp)
|
||||
@@ -84,7 +90,7 @@ export class uServer extends BaseServer {
|
||||
},
|
||||
message: (ws, message, isBinary) => {
|
||||
ws.getUserData().transport.onData(
|
||||
isBinary ? message : Buffer.from(message).toString()
|
||||
isBinary ? message : Buffer.from(message).toString(),
|
||||
);
|
||||
},
|
||||
close: (ws, code, message) => {
|
||||
@@ -96,7 +102,7 @@ export class uServer extends BaseServer {
|
||||
override _applyMiddlewares(
|
||||
req: any,
|
||||
res: any,
|
||||
callback: (err?: any) => void
|
||||
callback: (err?: any) => void,
|
||||
): void {
|
||||
if (this.middlewares.length === 0) {
|
||||
return callback();
|
||||
@@ -116,10 +122,10 @@ export class uServer extends BaseServer {
|
||||
|
||||
private handleRequest(
|
||||
res: HttpResponse,
|
||||
req: HttpRequest & { res: any; _query: any }
|
||||
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;
|
||||
|
||||
@@ -142,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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -158,11 +172,11 @@ export class uServer extends BaseServer {
|
||||
private handleUpgrade(
|
||||
res: HttpResponse,
|
||||
req: HttpRequest & { res: any; _query: any },
|
||||
context
|
||||
context,
|
||||
) {
|
||||
debug("on upgrade");
|
||||
|
||||
this.prepare(req, res);
|
||||
this.prepare(req as unknown as HttpRequest & EngineRequest, res);
|
||||
|
||||
req.res = res;
|
||||
|
||||
@@ -185,24 +199,27 @@ export class uServer extends BaseServer {
|
||||
const client = this.clients[id];
|
||||
if (!client) {
|
||||
debug("upgrade attempt for closed client");
|
||||
res.close();
|
||||
return res.close();
|
||||
} else if (client.upgrading) {
|
||||
debug("transport has already been trying to upgrade");
|
||||
res.close();
|
||||
return res.close();
|
||||
} else if (client.upgraded) {
|
||||
debug("transport had already been upgraded");
|
||||
res.close();
|
||||
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)
|
||||
this.abortRequest(res, errorCode, errorContext),
|
||||
);
|
||||
if (!transport) {
|
||||
return;
|
||||
@@ -219,7 +236,7 @@ export class uServer extends BaseServer {
|
||||
req.getHeader("sec-websocket-key"),
|
||||
req.getHeader("sec-websocket-protocol"),
|
||||
req.getHeader("sec-websocket-extensions"),
|
||||
context
|
||||
context,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -227,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
|
||||
@@ -252,7 +269,7 @@ export class uServer extends BaseServer {
|
||||
JSON.stringify({
|
||||
code: errorCode,
|
||||
message,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "engine.io",
|
||||
"version": "6.6.0",
|
||||
"version": "6.6.5",
|
||||
"description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server",
|
||||
"type": "commonjs",
|
||||
"main": "./build/engine.io.js",
|
||||
@@ -31,16 +31,15 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
"ws": "~8.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "rimraf ./build && tsc",
|
||||
@@ -56,7 +55,7 @@
|
||||
"homepage": "https://github.com/socketio/socket.io/tree/main/packages/engine.io#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/socketio/socket.io.git"
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
if (process.env.EIO_CLIENT === "3") {
|
||||
// we need the WebSocket object provided by the "ws" library to test the SSL certs and HTTP headers so we hide the now built-in WebSocket constructor
|
||||
// ref: https://nodejs.org/api/globals.html#class-websocket
|
||||
global.WebSocket = null;
|
||||
}
|
||||
|
||||
const { listen, uServer } = require("..");
|
||||
const { Socket } =
|
||||
process.env.EIO_CLIENT === "3"
|
||||
? require("engine.io-client-v3")
|
||||
: require("engine.io-client");
|
||||
|
||||
switch (process.env.EIO_WS_ENGINE) {
|
||||
case "uws":
|
||||
console.log(
|
||||
"[WARN] testing with uWebSockets.js instead of Node.js built-in HTTP server",
|
||||
);
|
||||
break;
|
||||
case "eiows":
|
||||
console.log("[WARN] testing with eiows instead of ws");
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen shortcut that fires a callback on an ephemeral port.
|
||||
*/
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("engine", () => {
|
||||
"Upgrade: IRC/6.9",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
].join("\r\n"),
|
||||
);
|
||||
|
||||
const check = setTimeout(() => {
|
||||
@@ -122,7 +122,7 @@ describe("engine", () => {
|
||||
"Upgrade: IRC/6.9",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
].join("\r\n"),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -154,7 +154,7 @@ describe("engine", () => {
|
||||
"Upgrade: IRC/6.9",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
].join("\r\n"),
|
||||
);
|
||||
|
||||
// send from client to server
|
||||
@@ -198,7 +198,7 @@ describe("engine", () => {
|
||||
"Upgrade: IRC/6.9",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
].join("\r\n"),
|
||||
);
|
||||
|
||||
// test that socket is still open by writing after the timeout period
|
||||
@@ -245,7 +245,7 @@ describe("engine", () => {
|
||||
server.once("close", done);
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
48
packages/engine.io/test/fixtures/ca.crt
vendored
48
packages/engine.io/test/fixtures/ca.crt
vendored
@@ -1,33 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFtTCCA52gAwIBAgIJAJKBPV3nMXjsMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTE4MTczODAwWhcNMjUxMTE1MTczODAwWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
||||
CgKCAgEAmNWKTumE1f+ptArhPTOcaUReoqBlri/ujIzm1N8Qr0hghS6B8eXGngAS
|
||||
YM7ziTlLZmLKgZg7TYOs+qK+xNNjSMbkA4Tions7vX3FYAokfh1iSiQigAw3TAwg
|
||||
brUaA0phucJBjvWI2mDuwzTLhQp1wmGrliAJhXag2ZQt817m6wrsFWuwiviMIHlq
|
||||
mQhC+vwd2SvW4xGf5zxjzCM8m7pOiJCLjxXwvNphiTR3tb807W00mi5cMFwhmAUT
|
||||
uSiVkVERubIYEKNSW2ynxzGFfb+GF/ddUxCKsnMDfM+SpMrsTBv9BzJzXU7Hc9jP
|
||||
NPFtrZiVo9aKn8csTSvKifmfiNwl2YGuWlW++1+ew6Q9rqEqvKHnKU+Cuwt3y37U
|
||||
ryqrBS47cz1xxFb3fCn+a72ytcHjI9lMqIQ0+IZ0/4cf0TK80ECEQ0CyrCk0E9Qz
|
||||
eMEzIALRa/pI8uTXdoAtQIlOsfALWeni+QphZ1BVjwZRmr+F1Px2/R30+gAcZHKc
|
||||
D+0Bm6owvpBWDe1s0DrkwtY3fyZ+OPS5/3eQtyhy9/3vnz9WBw0BGZyN2nzs5HsB
|
||||
RB5qDBRx+NQz1QYp/Ba3WeVmZURe2NMnS4uEypkWahW1XNQ+g+JJhK1p01s0+v/B
|
||||
f4DodYEcsw/3fRU0AKdsAkabQ68VIJAYyfQyinpNR9sHDKZ6Dx8CAwEAAaOBpzCB
|
||||
pDAdBgNVHQ4EFgQUdwTc4idMFJo0xYmoLTJQeD7A59kwdQYDVR0jBG4wbIAUdwTc
|
||||
4idMFJo0xYmoLTJQeD7A59mhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
|
||||
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCS
|
||||
gT1d5zF47DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQBRLzphOdJO
|
||||
ekU+iwFLGXJiLfUQi3EORaA3mBtozvyRRUyxu0FqWdGGINcyq7xaZxkQ0P+eBMDv
|
||||
V3F7FvidhaVe6TR7VJB2gyaWeLd1BSOi5gGBk5kuuqV3VbusNzY+WhJip98HEK+y
|
||||
XP+Lt/LXJpPwOORF9TiR6IBFQn3fBVhPjsqQRzT468QuZ5DCGQ5UW+wWl43I8OxS
|
||||
PDiUmHTlwLnLqqVFgSE+VnX4vSUZD8kDf0kkxssg1r56IzneSlRBegSVXIuRCbRf
|
||||
QmWaxz+D6ffM1eNiE3nQxsgJy3dPL1Lfsaidgz39yAC099pjLyVH23cfmFmT/l5b
|
||||
OdhRE5D75rL8eXAiI2/voz1M+v7XznHjZEhcVUlFsBXsk3zHb2vQQZRNPLnybTb8
|
||||
biFpReSIWdpno+F5IrT7z0L8JI3LU0leEFV+Rf525Q+78Rffohxd51fUj0vLKoy9
|
||||
un0IEkOcaJhHTPc2dMXb2qGcV4MaUEUsERrnanGZmdNd1aD3YAG3C+nJ8gxrEGHO
|
||||
veD6Xbyf1K8e7H2sqhGCm8eyHgCFGQiul6yQ41ZwjKgoSCJvOJaYUFE18k0S9k/I
|
||||
rWYvYWRYbDj4GYx+6LUTfyo30KK5jl4KAil92LrGlIfWK4IjUJyPlOJkb6gXkj0l
|
||||
lfbUHTmnKthFwJS0958xBq7UM7+RzyFIOg==
|
||||
MIIDBTCCAe2gAwIBAgIUPV8LcHEkiA3LPuNyt8kuDxRqjlQwDQYJKoZIhvcNAQEL
|
||||
BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTEyMTUwNzQ1MDdaFw0yNjEyMTUw
|
||||
NzQ1MDdaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQChKwRqFDrGgqMHjDUx5he6MDQ+BxlMqqpm0qu5pOCCPzSvYDFo
|
||||
iU4n3lK6uCbemv9Gdk/U99ev8LAGg2SkZAZXBpDrdZepkA84ehUHu5u7PHlXNodE
|
||||
KL7DLKsvoaYYiQ5rLyBYieDOYqtYJxtfLOeh+tmnNa+G4chpYzkll7OCeEQhbocQ
|
||||
QLdP8novscoSibp6bPmoVsat8RRru0CK9ND1v+FvJ2R7Lz2isBIr+p9ZrkYkIXa3
|
||||
OqN3wacZ+doYfAC/a4SK64Jgv+Lz6wuzsc7XVjBGEEaa1P1zd2rh70wNm8Lgmwr/
|
||||
Oq1Lv7Lg3plXY7e/7V915p96/bxbIiVHiu1JAgMBAAGjUzBRMB0GA1UdDgQWBBRN
|
||||
SIfG8SbjQTQCuymcNnQh4R5oxDAfBgNVHSMEGDAWgBRNSIfG8SbjQTQCuymcNnQh
|
||||
4R5oxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQADHV/KfXlN
|
||||
ZBmjWWqQRCmJCUsFkn29C0RQQMILbxYkNl08DuSQw68+PsA2YeB4aUikmW5sRHYF
|
||||
TgLXxgZ+aaGH4WtedumD4RGodcFbxI2WLIilKD4nH0FmB4I9bkULgMyyOZ0g+Vc7
|
||||
ekynqzYtzQBgK+HLWtIWZRTM/BR5IAzt/SdAVoQwL3EHzlc57Q9rRqYeBiyse3lC
|
||||
8Ojb9ZLhwv/hihWNd+mFKeWzLAGJIB439AUzKcg96YDKB3Nwosa16g0xslwxvAL/
|
||||
cKJQ3mh7pIOX3iv9YV/uifRvdCiI/e2p00YjZiS3ggdravSGjLLypw6HFH3hFnw7
|
||||
2ZzA88NgFQXi
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user