Skip to content

Commit e0f490c

Browse files
committed
Merge branch 'main' into check-heartbeat
2 parents 3512f7b + 582655f commit e0f490c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3039
-39
lines changed

Diff for: .github/workflows/ci.yml

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ jobs:
2121
- 18
2222
- 20
2323

24+
services:
25+
redis:
26+
image: redis:7
27+
options: >-
28+
--health-cmd "redis-cli ping"
29+
--health-interval 10s
30+
--health-timeout 5s
31+
--health-retries 5
32+
ports:
33+
- 6379:6379
34+
2435
steps:
2536
- name: Checkout repository
2637
uses: actions/checkout@v4

Diff for: .github/workflows/publish.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
push:
77
tags:
88
# expected format: <package>@<version> (example: [email protected])
9-
- '*@*'
9+
- '**@*'
1010

1111
jobs:
1212
publish:
@@ -28,6 +28,9 @@ jobs:
2828
- name: Install dependencies
2929
run: npm ci
3030

31+
- name: Compile each package
32+
run: npm run compile --workspaces --if-present
33+
3134
- name: Publish package
3235
run: npm publish --workspace=${GITHUB_REF_NAME%@*} --provenance --access public
3336
env:

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ Here are the detailed changelogs for each package in this monorepo:
1010
| `socket.io` | [link](packages/socket.io/CHANGELOG.md) |
1111
| `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) |
1212
| `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) |
13+
| `@socket.io/cluster-engine` | [link](packages/socket.io-cluster-engine/CHANGELOG.md) |
1314
| `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) |
1415
| `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) |

Diff for: CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ This repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) which co
7878
| `socket.io` | The server-side implementation of the bidirectional channel, built on top on the `engine.io` package. |
7979
| `socket.io-adapter` | An extensible component responsible for broadcasting a packet to all connected clients, used by the `socket.io` package. |
8080
| `socket.io-client` | The client-side implementation of the bidirectional channel, built on top on the `engine.io-client` package. |
81+
| `@socket.io/cluster-engine` | A cluster-friendly engine to share load between multiple Node.js processes (without sticky sessions) |
8182
| `@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. |
8283
| `socket.io-parser` | The parser responsible for encoding and decoding Socket.IO packets, used by both the `socket.io` and `socket.io-client` packages. |
8384

Diff for: examples/cluster-engine-node-cluster/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Example with `@socket.io/cluster-engine` and Node.js cluster
2+
3+
## How to use
4+
5+
```bash
6+
# run the server
7+
$ node server.js
8+
9+
# run the client
10+
$ node client.js
11+
```
12+
13+
## Explanation
14+
15+
The `server.js` script will create one Socket.IO server per core, each listening on the same port (`3000`).
16+
17+
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.
18+
19+
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.

Diff for: examples/cluster-engine-node-cluster/client.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { io } from "socket.io-client";
2+
3+
const CLIENTS_COUNT = 3;
4+
5+
for (let i = 0; i < CLIENTS_COUNT; i++) {
6+
const socket = io("ws://localhost:3000/", {
7+
// transports: ["polling"],
8+
// transports: ["websocket"],
9+
});
10+
11+
socket.on("connect", () => {
12+
console.log(`connected as ${socket.id}`);
13+
});
14+
15+
socket.on("disconnect", (reason) => {
16+
console.log(`disconnected due to ${reason}`);
17+
});
18+
19+
socket.on("hello", (socketId, workerId) => {
20+
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
21+
});
22+
23+
setInterval(() => {
24+
socket.emit("hello");
25+
}, 2000);
26+
}

Diff for: examples/cluster-engine-node-cluster/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"private": true,
3+
"name": "cluster-engine-node-cluster",
4+
"version": "0.0.1",
5+
"type": "module",
6+
"dependencies": {
7+
"@socket.io/cluster-adapter": "^0.2.2",
8+
"@socket.io/cluster-engine": "^0.1.0",
9+
"socket.io": "^4.7.5",
10+
"socket.io-client": "^4.7.5"
11+
}
12+
}

Diff for: examples/cluster-engine-node-cluster/server.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import cluster from "node:cluster";
2+
import process from "node:process";
3+
import { availableParallelism } from "node:os";
4+
import {
5+
setupPrimary as setupPrimaryEngine,
6+
NodeClusterEngine,
7+
} from "@socket.io/cluster-engine";
8+
import {
9+
setupPrimary as setupPrimaryAdapter,
10+
createAdapter,
11+
} from "@socket.io/cluster-adapter";
12+
import { createServer } from "node:http";
13+
import { Server } from "socket.io";
14+
15+
if (cluster.isPrimary) {
16+
console.log(`Primary ${process.pid} is running`);
17+
18+
const numCPUs = availableParallelism();
19+
20+
// fork workers
21+
for (let i = 0; i < numCPUs; i++) {
22+
cluster.fork();
23+
}
24+
25+
setupPrimaryEngine();
26+
setupPrimaryAdapter();
27+
28+
// needed for packets containing Buffer objects (you can ignore it if you only send plaintext objects)
29+
cluster.setupPrimary({
30+
serialization: "advanced",
31+
});
32+
33+
cluster.on("exit", (worker, code, signal) => {
34+
console.log(`worker ${worker.process.pid} died`);
35+
});
36+
} else {
37+
const httpServer = createServer((req, res) => {
38+
res.writeHead(404).end();
39+
});
40+
41+
const engine = new NodeClusterEngine();
42+
43+
engine.attach(httpServer, {
44+
path: "/socket.io/",
45+
});
46+
47+
const io = new Server({
48+
adapter: createAdapter(),
49+
});
50+
51+
io.bind(engine);
52+
53+
io.on("connection", (socket) => {
54+
socket.on("hello", () => {
55+
socket.broadcast.emit("hello", socket.id, process.pid);
56+
});
57+
});
58+
59+
// workers will share the same port
60+
httpServer.listen(3000);
61+
62+
console.log(`Worker ${process.pid} started`);
63+
}

Diff for: examples/cluster-engine-redis/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Example with `@socket.io/cluster-engine` and Redis
2+
3+
## How to use
4+
5+
```bash
6+
# start the redis server
7+
$ docker compose up -d
8+
9+
# run the server
10+
$ node server.js
11+
12+
# run the client
13+
$ node client.js
14+
```
15+
16+
## Explanation
17+
18+
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.
19+
20+
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.
21+
22+
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.

Diff for: examples/cluster-engine-redis/client.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { io } from "socket.io-client";
2+
3+
const CLIENTS_COUNT = 3;
4+
5+
for (let i = 0; i < CLIENTS_COUNT; i++) {
6+
const socket = io("ws://localhost:3000/", {
7+
// transports: ["polling"],
8+
// transports: ["websocket"],
9+
});
10+
11+
socket.on("connect", () => {
12+
console.log(`connected as ${socket.id}`);
13+
});
14+
15+
socket.on("disconnect", (reason) => {
16+
console.log(`disconnected due to ${reason}`);
17+
});
18+
19+
socket.on("hello", (socketId, workerId) => {
20+
console.log(`received "hello" from ${socketId} (worker: ${workerId})`);
21+
});
22+
23+
setInterval(() => {
24+
socket.emit("hello");
25+
}, 2000);
26+
}

Diff for: examples/cluster-engine-redis/compose.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
redis:
3+
image: redis:7
4+
ports:
5+
- "6379:6379"

Diff for: examples/cluster-engine-redis/package.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"private": true,
3+
"name": "cluster-engine-redis",
4+
"version": "0.0.1",
5+
"type": "module",
6+
"dependencies": {
7+
"@socket.io/cluster-engine": "^0.1.0",
8+
"@socket.io/redis-adapter": "^8.3.0",
9+
"http-proxy": "^1.18.1",
10+
"redis": "^4.6.15",
11+
"socket.io": "^4.7.5",
12+
"socket.io-client": "^4.7.5"
13+
}
14+
}

Diff for: examples/cluster-engine-redis/server.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { RedisEngine } from "@socket.io/cluster-engine";
2+
import { createServer } from "node:http";
3+
import { createClient } from "redis";
4+
import { Server } from "socket.io";
5+
import { createAdapter } from "@socket.io/redis-adapter";
6+
import proxyModule from "http-proxy";
7+
8+
const { createProxyServer } = proxyModule;
9+
10+
async function initServer(port) {
11+
const httpServer = createServer((req, res) => {
12+
res.writeHead(404).end();
13+
});
14+
15+
const pubClient = createClient();
16+
const subClient = pubClient.duplicate();
17+
18+
await Promise.all([pubClient.connect(), subClient.connect()]);
19+
20+
const engine = new RedisEngine(pubClient, subClient);
21+
22+
engine.attach(httpServer, {
23+
path: "/socket.io/",
24+
});
25+
26+
const io = new Server({
27+
adapter: createAdapter(pubClient, subClient),
28+
});
29+
30+
io.bind(engine);
31+
32+
io.on("connection", (socket) => {
33+
socket.on("hello", () => {
34+
socket.broadcast.emit("hello", socket.id, port);
35+
});
36+
});
37+
38+
httpServer.listen(port);
39+
}
40+
41+
function initProxy() {
42+
const proxy = createProxyServer();
43+
44+
function randomTarget() {
45+
return [
46+
"http://localhost:3001",
47+
"http://localhost:3002",
48+
"http://localhost:3003",
49+
][Math.floor(Math.random() * 3)];
50+
}
51+
52+
const httpServer = createServer((req, res) => {
53+
proxy.web(req, res, { target: randomTarget() });
54+
});
55+
56+
httpServer.on("upgrade", function (req, socket, head) {
57+
proxy.ws(req, socket, head, { target: randomTarget() });
58+
});
59+
60+
httpServer.listen(3000);
61+
}
62+
63+
await Promise.all([initServer(3001), initServer(3002), initServer(3003)]);
64+
65+
initProxy();

Diff for: examples/nestjs-example/.eslintrc.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
parser: '@typescript-eslint/parser',
3+
parserOptions: {
4+
project: 'tsconfig.json',
5+
tsconfigRootDir: __dirname,
6+
sourceType: 'module',
7+
},
8+
plugins: ['@typescript-eslint/eslint-plugin'],
9+
extends: [
10+
'plugin:@typescript-eslint/recommended',
11+
'plugin:prettier/recommended',
12+
],
13+
root: true,
14+
env: {
15+
node: true,
16+
jest: true,
17+
},
18+
ignorePatterns: ['.eslintrc.js'],
19+
rules: {
20+
'@typescript-eslint/interface-name-prefix': 'off',
21+
'@typescript-eslint/explicit-function-return-type': 'off',
22+
'@typescript-eslint/explicit-module-boundary-types': 'off',
23+
'@typescript-eslint/no-explicit-any': 'off',
24+
},
25+
};

Diff for: examples/nestjs-example/.gitignore

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# compiled output
2+
/dist
3+
/node_modules
4+
/build
5+
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
pnpm-debug.log*
11+
yarn-debug.log*
12+
yarn-error.log*
13+
lerna-debug.log*
14+
15+
# OS
16+
.DS_Store
17+
18+
# Tests
19+
/coverage
20+
/.nyc_output
21+
22+
# IDEs and editors
23+
/.idea
24+
.project
25+
.classpath
26+
.c9/
27+
*.launch
28+
.settings/
29+
*.sublime-workspace
30+
31+
# IDE - VSCode
32+
.vscode/*
33+
!.vscode/settings.json
34+
!.vscode/tasks.json
35+
!.vscode/launch.json
36+
!.vscode/extensions.json
37+
38+
# dotenv environment variable files
39+
.env
40+
.env.development.local
41+
.env.test.local
42+
.env.production.local
43+
.env.local
44+
45+
# temp directory
46+
.temp
47+
.tmp
48+
49+
# Runtime data
50+
pids
51+
*.pid
52+
*.seed
53+
*.pid.lock
54+
55+
# Diagnostic reports (https://nodejs.org/api/report.html)
56+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

0 commit comments

Comments
 (0)