Skip to content

Commit

Permalink
Merge branch 'main' into check-heartbeat
Browse files Browse the repository at this point in the history
  • Loading branch information
tjenkinson committed Aug 9, 2024
2 parents 3512f7b + 582655f commit e0f490c
Show file tree
Hide file tree
Showing 57 changed files with 3,039 additions and 39 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ jobs:
- 18
- 20

services:
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
push:
tags:
# expected format: <package>@<version> (example: socket.io@1.2.3)
- '*@*'
- '**@*'

jobs:
publish:
Expand All @@ -28,6 +28,9 @@ jobs:
- 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:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ Here are the detailed changelogs for each package in this monorepo:
| `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-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) |
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down
19 changes: 19 additions & 0 deletions examples/cluster-engine-node-cluster/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions examples/cluster-engine-node-cluster/client.js
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions examples/cluster-engine-node-cluster/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions examples/cluster-engine-node-cluster/server.js
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions examples/cluster-engine-redis/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions examples/cluster-engine-redis/client.js
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions examples/cluster-engine-redis/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
redis:
image: redis:7
ports:
- "6379:6379"
14 changes: 14 additions & 0 deletions examples/cluster-engine-redis/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 65 additions & 0 deletions examples/cluster-engine-redis/server.js
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions examples/nestjs-example/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions examples/nestjs-example/.gitignore
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit e0f490c

Please sign in to comment.