Compare commits

...

15 Commits

Author SHA1 Message Date
Damien Arrachequesne
5ab8289c0a chore(release): 4.5.1
Diff: https://github.com/socketio/socket.io/compare/4.5.0...4.5.1
2022-05-17 23:38:07 +02:00
Damien Arrachequesne
30430f0985 fix: forward the local flag to the adapter when using fetchSockets()
Related:

- https://github.com/socketio/socket.io/issues/4359
- https://github.com/socketio/socket.io-redis-adapter/issues/454
2022-05-03 15:41:24 +02:00
h110m
9b43c9167c fix(typings): add HTTPS server to accepted types (#4351) 2022-05-03 08:15:22 +02:00
Damien Arrachequesne
8ecfcba5c1 chore(release): 4.5.0
Diff: https://github.com/socketio/socket.io/compare/4.4.1...4.5.0
2022-04-24 00:45:57 +02:00
Damien Arrachequesne
572133a58d docs(examples): update example with webpack 2022-04-22 23:10:01 +02:00
Damien Arrachequesne
6e1bb62982 chore: bump engine.io to version 6.2.0
Release notes: https://github.com/socketio/engine.io/releases/tag/6.2.0
Diff: https://github.com/socketio/engine.io/compare/6.1.3...6.2.0
2022-04-22 22:43:31 +02:00
Damien Arrachequesne
06e6838b18 docs(examples): add server bundling example with rollup
Related: https://github.com/socketio/socket.io/issues/4329
2022-04-22 22:40:15 +02:00
WD
1f03a44d1f docs(examples): update create-react-app example (#4347) 2022-04-20 22:47:34 +02:00
Damien Arrachequesne
be3d7f0f1f docs(examples): add TODO example with Postgres and Node.js cluster 2022-04-07 12:35:00 +02:00
Damien Arrachequesne
d12aab2d69 docs(examples): add example with express-session
Related: https://github.com/socketio/socket.io/issues/3933
2022-04-02 11:08:26 +02:00
Damien Arrachequesne
9f758689f6 docs(examples): pin the version of karma-jasmine-html-reporter
Related:

- https://github.com/socketio/socket.io/issues/4325
- https://github.com/dfederm/karma-jasmine-html-reporter/issues/54
2022-04-01 14:42:34 +02:00
Damien Arrachequesne
0b35dc77c0 refactor: make the protocol implementation stricter
This commit handles several edge cases that were silently ignored
before:

- receiving several CONNECT packets during a session
- receiving any packet without CONNECT packet first
2022-03-31 12:24:31 +02:00
Damien Arrachequesne
531104d332 feat: add support for catch-all listeners for outgoing packets
This is similar to `onAny()`, but for outgoing packets.

Syntax:

```js
socket.onAnyOutgoing((event, ...args) => {
  console.log(event);
});
```
2022-03-31 10:35:09 +02:00
Damien Arrachequesne
8b204570a9 feat: broadcast and expect multiple acks
Syntax:

```js
io.timeout(1000).emit("some-event", (err, responses) => {
  // ...
});
```

The adapter exposes two additional methods:

- `broadcastWithAck(packets, opts, clientCountCallback, ack)`

Similar to `broadcast(packets, opts)`, but:

* `clientCountCallback()` is called with the number of clients that
  received the packet (can be called several times in a cluster)
* `ack()` is called for each client response

- `serverCount()`

It returns the number of Socket.IO servers in the cluster (1 for the
in-memory adapter).

Those two methods will be implemented in the other adapters (Redis,
Postgres, MongoDB, ...).

Related:

- https://github.com/socketio/socket.io/issues/1811
- https://github.com/socketio/socket.io/issues/4163
- https://github.com/socketio/socket.io-redis-adapter/issues/445
2022-03-31 07:49:09 +02:00
Damien Arrachequesne
0b7d70ca42 chore: bump lockfile to v2 2022-03-30 08:15:41 +02:00
49 changed files with 6047 additions and 858 deletions

View File

@@ -1,3 +1,59 @@
## [4.5.1](https://github.com/socketio/socket.io/compare/4.5.0...4.5.1) (2022-05-17)
### Bug Fixes
* forward the local flag to the adapter when using fetchSockets() ([30430f0](https://github.com/socketio/socket.io/commit/30430f0985f8e7c49394543d4c84913b6a15df60))
* **typings:** add HTTPS server to accepted types ([#4351](https://github.com/socketio/socket.io/issues/4351)) ([9b43c91](https://github.com/socketio/socket.io/commit/9b43c9167cff817c60fa29dbda2ef7cd938aff51))
# [4.5.0](https://github.com/socketio/socket.io/compare/4.4.1...4.5.0) (2022-04-23)
### Bug Fixes
* **typings:** ensure compatibility with TypeScript 3.x ([#4259](https://github.com/socketio/socket.io/issues/4259)) ([02c87a8](https://github.com/socketio/socket.io/commit/02c87a85614e217b8e7b93753f315790ae9d99f6))
### Features
* add support for catch-all listeners for outgoing packets ([531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4))
This is similar to `onAny()`, but for outgoing packets.
Syntax:
```js
socket.onAnyOutgoing((event, ...args) => {
console.log(event);
});
```
* broadcast and expect multiple acks ([8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0))
Syntax:
```js
io.timeout(1000).emit("some-event", (err, responses) => {
// ...
});
```
* add the "maxPayload" field in the handshake details ([088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38))
So that clients in HTTP long-polling can decide how many packets they have to send to stay under the maxHttpBufferSize
value.
This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as
we only add a field in the JSON-encoded handshake data:
```
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
```
## [4.4.1](https://github.com/socketio/socket.io/compare/4.4.0...4.4.1) (2022-01-06)

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 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,13 @@
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
This repository contains several implementations of the server:
| Directory | Language | Database | Cluster? |
|----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `server/` | TypeScript | in-memory | No |
| `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Running the frontend
```

View File

@@ -37,7 +37,7 @@
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-jasmine-html-reporter": "~1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",

View File

@@ -0,0 +1,16 @@
A basic TODO project.
| Characteristic | |
|----------------|-------------------------------------------------------------------------------------------|
| Language | plain JavaScript |
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) |
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Usage
```
$ docker-compose up -d
$ npm install
$ npm start
```

View File

@@ -0,0 +1,9 @@
version: "3"
services:
postgres:
image: postgres:12
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "changeit"

View File

@@ -0,0 +1,26 @@
import { Server } from "socket.io";
import createTodoHandlers from "./todo-management/todo.handlers.js";
import { setupWorker } from "@socket.io/sticky";
import { createAdapter } from "@socket.io/postgres-adapter";
export function createApplication(httpServer, components, serverOptions = {}) {
const io = new Server(httpServer, serverOptions);
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } =
createTodoHandlers(components);
io.on("connection", (socket) => {
socket.on("todo:create", createTodo);
socket.on("todo:read", readTodo);
socket.on("todo:update", updateTodo);
socket.on("todo:delete", deleteTodo);
socket.on("todo:list", listTodo);
});
// enable sticky session in the cluster (to remove in standalone mode)
setupWorker(io);
io.adapter(createAdapter(components.connectionPool));
return io;
}

View File

@@ -0,0 +1,28 @@
import cluster from "cluster";
import { createServer } from "http";
import { setupMaster } from "@socket.io/sticky";
import { cpus } from "os";
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
const httpServer = createServer();
setupMaster(httpServer, {
loadBalancingMethod: "least-connection",
});
httpServer.listen(3000);
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
console.log(`Worker ${process.pid} started`);
import("./index.js");
}

View File

@@ -0,0 +1,51 @@
import { createServer } from "http";
import { createApplication } from "./app.js";
import { Sequelize } from "sequelize";
import pg from "pg";
import { PostgresTodoRepository } from "./todo-management/todo.repository.js";
const httpServer = createServer();
const sequelize = new Sequelize("postgres", "postgres", "changeit", {
dialect: "postgres",
});
const connectionPool = new pg.Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
createApplication(
httpServer,
{
connectionPool,
todoRepository: new PostgresTodoRepository(sequelize),
},
{
cors: {
origin: ["http://localhost:4200"],
},
}
);
const main = async () => {
// create the tables if they do not exist already
await sequelize.sync();
// create the table needed by the postgres adapter
await connectionPool.query(`
CREATE TABLE IF NOT EXISTS socket_io_attachments (
id bigserial UNIQUE,
created_at timestamptz DEFAULT NOW(),
payload bytea
);
`);
// uncomment when running in standalone mode
// httpServer.listen(3000);
};
main();

View File

@@ -0,0 +1,140 @@
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js";
import { v4 as uuid } from "uuid";
import Joi from "joi";
const idSchema = Joi.string().guid({
version: "uuidv4",
});
const todoSchema = Joi.object({
id: idSchema.alter({
create: (schema) => schema.forbidden(),
update: (schema) => schema.required(),
}),
title: Joi.string().max(256).required(),
completed: Joi.boolean().required(),
});
export default function (components) {
const { todoRepository } = components;
return {
createTodo: async function (payload, callback) {
const socket = this;
// validate the payload
const { error, value } = todoSchema.tailor("create").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
value.id = uuid();
// persist the entity
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
// acknowledge the creation
callback({
data: value.id,
});
// notify the other users
socket.broadcast.emit("todo:created", value);
},
readTodo: async function (id, callback) {
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
const todo = await todoRepository.findById(id);
callback({
data: todo,
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
updateTodo: async function (payload, callback) {
const socket = this;
const { error, value } = todoSchema.tailor("update").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:updated", value);
},
deleteTodo: async function (id, callback) {
const socket = this;
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
await todoRepository.deleteById(id);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:deleted", id);
},
listTodo: async function (callback) {
try {
callback({
data: await todoRepository.findAll(),
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
};
}

View File

@@ -0,0 +1,74 @@
import { Errors } from "../util.js";
import { Model, DataTypes } from "sequelize";
class CrudRepository {
findAll() {}
findById(id) {}
save(entity) {}
deleteById(id) {}
}
export class TodoRepository extends CrudRepository {}
class Todo extends Model {}
export class PostgresTodoRepository extends TodoRepository {
constructor(sequelize) {
super();
this.sequelize = sequelize;
Todo.init(
{
id: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
title: {
type: DataTypes.STRING,
},
completed: {
type: DataTypes.BOOLEAN,
},
},
{
sequelize,
tableName: "todos",
}
);
}
findAll() {
return this.sequelize.transaction((transaction) => {
return Todo.findAll({ transaction });
});
}
async findById(id) {
return this.sequelize.transaction(async (transaction) => {
const todo = await Todo.findByPk(id, { transaction });
if (!todo) {
throw Errors.ENTITY_NOT_FOUND;
}
return todo;
});
}
save(entity) {
return this.sequelize.transaction((transaction) => {
return Todo.upsert(entity, { transaction });
});
}
async deleteById(id) {
return this.sequelize.transaction(async (transaction) => {
const count = await Todo.destroy({ where: { id }, transaction });
if (count === 0) {
throw Errors.ENTITY_NOT_FOUND;
}
});
}
}

View File

@@ -0,0 +1,22 @@
export const Errors = {
ENTITY_NOT_FOUND: "entity not found",
INVALID_PAYLOAD: "invalid payload",
};
const errorValues = Object.values(Errors);
export function sanitizeErrorMessage(message) {
if (typeof message === "string" && errorValues.includes(message)) {
return message;
} else {
return "an unknown error has occurred";
}
}
export function mapErrorDetails(details) {
return details.map((item) => ({
message: item.message,
path: item.path,
type: item.type,
}));
}

View File

@@ -0,0 +1,30 @@
{
"name": "basic-crud-server",
"version": "0.0.1",
"description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)",
"main": "lib/cluster.js",
"type": "module",
"scripts": {
"start": "node lib/cluster.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/socketio/socket.io.git"
},
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"
},
"homepage": "https://github.com/socketio/socket.io#readme",
"dependencies": {
"@socket.io/postgres-adapter": "^0.2.0",
"@socket.io/sticky": "^1.0.1",
"joi": "^17.4.0",
"pg": "^8.7.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.18.0",
"socket.io": "^4.0.1",
"uuid": "^8.3.2"
}
}

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';
import io from 'socket.io-client';
@@ -23,7 +24,7 @@ function App() {
socket.off('disconnect');
socket.off('message');
};
});
}, []);
const sendMessage = () => {
socket.emit('hello!');
@@ -32,9 +33,21 @@ function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Connected: { '' + isConnected }</p>
<p>Last message: { lastMessage || '-' }</p>
<button onClick={ sendMessage }>Say hello!</button>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);

View File

@@ -0,0 +1,16 @@
# Example with [express-session](https://www.npmjs.com/package/express-session)
This example shows how to share a session context between [Express](http://expressjs.com/) and [Socket.IO](https://socket.io/docs/v4/):
![Video of the example](assets/demo.gif)
Please read the related guide: https://socket.io/how-to/use-with-express-session
## How to use
```
$ npm install
$ npm start
```
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example with express-session</title>
</head>
<body>
<button onclick="incrementWithFetch()">Increment with fetch()</button>
<button onclick="logout()">Logout</button>
<p>Count: <span id="httpCount">0</span></p>
<button onclick="incrementWithEmit()">
Increment with Socket.IO emit()
</button>
<p>Status: <span id="ioStatus">disconnected</span></p>
<p>Count: <span id="ioCount">0</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const httpCount = document.getElementById("httpCount");
const ioStatus = document.getElementById("ioStatus");
const ioCount = document.getElementById("ioCount");
const socket = io({
// with WebSocket only
// transports: ["websocket"],
});
async function incrementWithFetch() {
const response = await fetch("/incr", {
method: "post",
});
httpCount.innerText = await response.text();
}
function logout() {
fetch("/logout", {
method: "post",
});
}
async function incrementWithEmit() {
socket.emit("incr", (count) => {
ioCount.innerText = count;
});
}
socket.on("connect", () => {
ioStatus.innerText = "connected";
});
socket.on("disconnect", () => {
ioStatus.innerText = "disconnected";
});
</script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.get("/", (req, res) => {
res.sendFile("./index.html", { root: process.cwd() });
});
app.post("/incr", (req, res) => {
const session = req.session;
session.count = (session.count || 0) + 1;
res.status(200).end("" + session.count);
});
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(sessionId).disconnectSockets();
res.status(204).end();
});
});
const io = new Server(httpServer, {
allowRequest: (req, callback) => {
// with HTTP long-polling, we have access to the HTTP response here, but this is not
// the case with WebSocket, so we provide a dummy response object
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
// trigger the setHeader() above
fakeRes.writeHead();
// manually save the session (normally triggered by res.end())
req.session.save();
}
callback(null, true);
});
},
});
io.engine.on("initial_headers", (headers, req) => {
if (req.cookieHolder) {
headers["set-cookie"] = req.cookieHolder;
delete req.cookieHolder;
}
});
io.on("connect", (socket) => {
const req = socket.request;
socket.join(req.session.id);
socket.on("incr", (cb) => {
req.session.reload((err) => {
if (err) {
// session has expired
return socket.disconnect();
}
req.session.count = (req.session.count || 0) + 1;
req.session.save(() => {
cb(req.session.count);
});
});
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,15 @@
{
"name": "express-session-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with express-session (https://github.com/expressjs/session)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"socket.io": "~4.4.1"
}
}

View File

@@ -0,0 +1 @@
bundle.js

View File

@@ -0,0 +1,3 @@
import { Server } from "socket.io";
new Server(0);

View File

@@ -0,0 +1,19 @@
{
"name": "rollup-server-bundle",
"version": "0.0.1",
"description": "",
"main": "index.js",
"type": "module",
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "ISC",
"scripts": {
"build": "rollup --config rollup.config.js"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.3",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"rollup": "^2.70.1",
"socket.io": "^4.4.1"
}
}

View File

@@ -0,0 +1,12 @@
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
export default {
input: "index.js",
output: {
file: "bundle.js",
format: "esm",
},
plugins: [resolve(), commonjs(), json({ compact: true })],
};

View File

@@ -7,14 +7,5 @@ A sample Webpack build for the browser.
```
$ npm i
$ npm run build-all
$ npm run build
```
There are two WebPack configuration:
- the minimal configuration, just bundling the application and its dependencies. The `app.js` file in the `dist` folder is the result of that build.
- a slimmer one, where:
- the JSON polyfill needed for IE6/IE7 support has been removed.
- the `debug` calls and import have been removed (the [debug](https://github.com/visionmedia/debug) library is included in the build by default).
- the source has been uglified (dropping IE8 support), and an associated SourceMap has been generated.

View File

@@ -6,8 +6,7 @@
</head>
<body>
<!-- <script src="dist/app.js"></script> -->
<script src="dist/app.slim.js"></script>
<script src="dist/bundle.js"></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,15 @@
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnect due to ${reason}`);
});

View File

@@ -1,12 +0,0 @@
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
console.log('init');
socket.on('connect', onConnect);
function onConnect(){
console.log('connect ' + socket.id);
}

View File

@@ -2,20 +2,15 @@
"name": "webpack-build",
"version": "1.0.0",
"description": "A sample Webpack build",
"type": "module",
"scripts": {
"build": "webpack --config ./support/webpack.config.js",
"build-slim": "webpack --config ./support/webpack.config.slim.js",
"build-json-parser": "webpack --config ./support/webpack.config.json-parser.js",
"build-all": "npm run build && npm run build-slim && npm run build-json-parser"
"build": "webpack"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"dependencies": {
"socket.io-client": "^2.0.2",
"socket.io-json-parser": "^2.1.0"
},
"devDependencies": {
"strip-loader": "^0.1.2",
"webpack": "^2.6.1"
"socket.io-client": "^4.4.1",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
}
}

View File

@@ -1,2 +0,0 @@
module.exports = function () { return function () {}; };

View File

@@ -1,8 +0,0 @@
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.js'
}
};

View File

@@ -1,33 +0,0 @@
var webpack = require('webpack');
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.json-parser.js'
},
// generate sourcemap
devtool: 'source-map',
plugins: [
// replace require('debug')() with an noop function
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
// replace socket.io-parser with socket.io-json-parser
new webpack.NormalModuleReplacementPlugin(/socket\.io-parser/, 'socket.io-json-parser'),
// use uglifyJS (IE9+ support)
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
],
module: {
loaders: [
{
// strip `debug()` calls
test: /\.js$/,
loader: 'strip-loader?strip[]=debug'
}
]
}
};

View File

@@ -1,31 +0,0 @@
var webpack = require('webpack');
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.slim.js'
},
// generate sourcemap
devtool: 'source-map',
plugins: [
// replace require('debug')() with an noop function
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
// use uglifyJS (IE9+ support)
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
],
module: {
loaders: [
{
// strip `debug()` calls
test: /\.js$/,
loader: 'strip-loader?strip[]=debug'
}
]
}
};

View File

@@ -0,0 +1,7 @@
export default {
entry: "./index.js",
mode: "production",
output: {
filename: "bundle.js",
},
};

View File

@@ -129,6 +129,29 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
);
}
/**
* Adds a timeout in milliseconds for the next operation
*
* <pre><code>
*
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Emits to all clients.
*
@@ -149,14 +172,65 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
data: data,
};
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting");
const withAck = typeof data[data.length - 1] === "function";
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
});
return true;
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
const ack = data.pop() as (...args: any[]) => void;
let timedOut = false;
let responses: any[] = [];
const timer = setTimeout(() => {
timedOut = true;
ack.apply(this, [new Error("operation has timed out"), responses]);
}, this.flags.timeout);
let expectedServerCount = -1;
let actualServerCount = 0;
let expectedClientCount = 0;
const checkCompleteness = () => {
if (
!timedOut &&
expectedServerCount === actualServerCount &&
responses.length === expectedClientCount
) {
clearTimeout(timer);
ack.apply(this, [null, responses]);
}
};
this.adapter.broadcastWithAck(
packet,
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
(clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount;
actualServerCount++;
checkCompleteness();
},
(clientResponse) => {
// each client sends an acknowledgement
responses.push(clientResponse);
checkCompleteness();
}
);
this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount;
checkCompleteness();
});
return true;
@@ -188,6 +262,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
.then((sockets) => {
return sockets.map((socket) => {
@@ -215,6 +290,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
@@ -231,6 +307,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
@@ -247,6 +324,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
close
);

View File

@@ -248,6 +248,7 @@ export class Client<
try {
this.decoder.add(data);
} catch (e) {
debug("invalid packet format");
this.onerror(e);
}
}
@@ -258,22 +259,31 @@ export class Client<
* @private
*/
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
this.connect(parsed.pathname!, parsed.query);
} else {
this.connect(packet.nsp, packet.data);
}
let namespace: string;
let authPayload;
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
namespace = parsed.pathname!;
authPayload = parsed.query;
} else {
const socket = this.nsps.get(packet.nsp);
if (socket) {
process.nextTick(function () {
socket._onpacket(packet);
});
} else {
debug("no socket for namespace %s", packet.nsp);
}
namespace = packet.nsp;
authPayload = packet.data;
}
const socket = this.nsps.get(namespace);
if (!socket && packet.type === PacketType.CONNECT) {
this.connect(namespace, authPayload);
} else if (
socket &&
packet.type !== PacketType.CONNECT &&
packet.type !== PacketType.CONNECT_ERROR
) {
process.nextTick(function () {
socket._onpacket(packet);
});
} else {
debug("invalid state (packet type: %s)", packet.type);
this.close();
}
}

View File

@@ -1,4 +1,5 @@
import http = require("http");
import type { Server as HTTPSServer } from "https";
import { createReadStream } from "fs";
import { createDeflate, createGzip, createBrotliCompress } from "zlib";
import accepts = require("accepts");
@@ -28,7 +29,7 @@ import {
StrictEventEmitter,
EventNames,
} from "./typed-events";
import { patchAdapter, restoreAdapter, serveFile } from "./uws.js";
import { patchAdapter, restoreAdapter, serveFile } from "./uws";
const debug = debugModule("socket.io:server");
@@ -131,7 +132,7 @@ export class Server<
* @private
*/
_connectTimeout: number;
private httpServer: http.Server;
private httpServer: http.Server | HTTPSServer;
/**
* Server constructor.
@@ -141,13 +142,26 @@ export class Server<
* @public
*/
constructor(opts?: Partial<ServerOptions>);
constructor(srv?: http.Server | number, opts?: Partial<ServerOptions>);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
srv?: http.Server | HTTPSServer | number,
opts?: Partial<ServerOptions>
);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| number,
opts?: Partial<ServerOptions>
);
constructor(
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| number,
opts: Partial<ServerOptions> = {}
) {
super();
@@ -167,7 +181,8 @@ export class Server<
this.adapter(opts.adapter || Adapter);
this.sockets = this.of("/");
this.opts = opts;
if (srv || typeof srv == "number") this.attach(srv as http.Server | number);
if (srv || typeof srv == "number")
this.attach(srv as http.Server | HTTPSServer | number);
}
/**
@@ -300,7 +315,7 @@ export class Server<
* @public
*/
public listen(
srv: http.Server | number,
srv: http.Server | HTTPSServer | number,
opts: Partial<ServerOptions> = {}
): this {
return this.attach(srv, opts);
@@ -315,7 +330,7 @@ export class Server<
* @public
*/
public attach(
srv: http.Server | number,
srv: http.Server | HTTPSServer | number,
opts: Partial<ServerOptions> = {}
): this {
if ("function" == typeof srv) {
@@ -421,7 +436,7 @@ export class Server<
* @private
*/
private initEngine(
srv: http.Server,
srv: http.Server | HTTPSServer,
opts: EngineOptions & AttachOptions
): void {
// initialize engine
@@ -444,7 +459,7 @@ export class Server<
* @param srv http server
* @private
*/
private attachServe(srv: http.Server): void {
private attachServe(srv: http.Server | HTTPSServer): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
@@ -772,6 +787,23 @@ export class Server<
return this.sockets.local;
}
/**
* Adds a timeout in milliseconds for the next operation
*
* <pre><code>
*
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
return this.sockets.timeout(timeout);
}
/**
* Returns the matching socket instances
*

View File

@@ -379,6 +379,23 @@ export class Namespace<
return new BroadcastOperator(this.adapter).local;
}
/**
* Adds a timeout in milliseconds for the next operation
*
* <pre><code>
*
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
return new BroadcastOperator(this.adapter).timeout(timeout);
}
/**
* Returns the matching socket instances
*

View File

@@ -140,8 +140,9 @@ export class Socket<
private readonly adapter: Adapter;
private acks: Map<number, () => void> = new Map();
private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
private flags: BroadcastFlags & { timeout?: number } = {};
private flags: BroadcastFlags = {};
private _anyListeners?: Array<(...args: any[]) => void>;
private _anyOutgoingListeners?: Array<(...args: any[]) => void>;
/**
* Interface to a `Client` for a given `Namespace`.
@@ -220,6 +221,7 @@ export class Socket<
const flags = Object.assign({}, this.flags);
this.flags = {};
this.notifyOutgoingListeners(packet);
this.packet(packet, flags);
return true;
@@ -405,9 +407,6 @@ export class Socket<
case PacketType.DISCONNECT:
this.ondisconnect();
break;
case PacketType.CONNECT_ERROR:
this._onerror(new Error(packet.data));
}
}
@@ -710,8 +709,8 @@ export class Socket<
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback.
*
* @param listener
* @public
@@ -723,8 +722,8 @@ export class Socket<
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
@@ -736,7 +735,7 @@ export class Socket<
}
/**
* Removes the listener that will be fired when any event is emitted.
* Removes the listener that will be fired when any event is received.
*
* @param listener
* @public
@@ -769,6 +768,114 @@ export class Socket<
return this._anyListeners || [];
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* @param listener
*
* <pre><code>
*
* socket.onAnyOutgoing((event, ...args) => {
* console.log(event);
* });
*
* </pre></code>
*
* @public
*/
public onAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
this._anyOutgoingListeners.push(listener);
return this;
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
*
* <pre><code>
*
* socket.prependAnyOutgoing((event, ...args) => {
* console.log(event);
* });
*
* </pre></code>
*
* @public
*/
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
this._anyOutgoingListeners.unshift(listener);
return this;
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @param listener
*
* <pre><code>
*
* const handler = (event, ...args) => {
* console.log(event);
* }
*
* socket.onAnyOutgoing(handler);
*
* // then later
* socket.offAnyOutgoing(handler);
*
* </pre></code>
*
* @public
*/
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
if (!this._anyOutgoingListeners) {
return this;
}
if (listener) {
const listeners = this._anyOutgoingListeners;
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1);
return this;
}
}
} else {
this._anyOutgoingListeners = [];
}
return this;
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAnyOutgoing() {
return this._anyOutgoingListeners || [];
}
/**
* Notify the listeners for each packet sent (emit or broadcast)
*
* @param packet
*
* @private
*/
private notifyOutgoingListeners(packet: Packet) {
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
const listeners = this._anyOutgoingListeners.slice();
for (const listener of listeners) {
listener.apply(this, packet.data);
}
}
}
private newBroadcastOperator(): BroadcastOperator<EmitEvents, SocketData> {
const flags = Object.assign({}, this.flags);
this.flags = {};

3961
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "4.4.1",
"version": "4.5.1",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -49,8 +49,8 @@
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.1.2",
"socket.io-adapter": "~2.3.3",
"engine.io": "~6.2.0",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.0.4"
},
"devDependencies": {
@@ -60,7 +60,7 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.4.1",
"socket.io-client": "4.5.1",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^6.1.0",
"supertest": "^6.1.6",

View File

@@ -1,6 +1,6 @@
"use strict";
import { Server, Socket, Namespace } from "..";
import { Server, Socket, Namespace } from "../lib";
import { createServer } from "http";
import fs = require("fs");
import { join } from "path";
@@ -47,6 +47,44 @@ const getPort = (io: Server): number => {
return io.httpServer.address().port;
};
// TODO: update superagent as latest release now supports promises
const eioHandshake = (httpServer): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
const sid = JSON.parse(res.text.substring(1)).sid;
resolve(sid);
});
});
};
const eioPush = (httpServer, sid: string, body: string): Promise<void> => {
return new Promise((resolve) => {
request(httpServer)
.post("/socket.io/")
.send(body)
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end(() => {
resolve();
});
});
};
const eioPoll = (httpServer, sid): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end((err, res) => {
resolve(res.text);
});
});
};
describe("socket.io", () => {
it("should be the same version as client", () => {
const version = require("../package").version;
@@ -378,6 +416,66 @@ describe("socket.io", () => {
exec(fixture("server-close.ts"), done);
});
});
describe("protocol violations", () => {
it("should close the connection when receiving several CONNECT packets", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send a first CONNECT packet
await eioPush(httpServer, sid, "40");
// send another CONNECT packet
await eioPush(httpServer, sid, "40");
// session is cleanly closed (not discarded, see 'client.close()')
// first, we receive the Socket.IO handshake response
await eioPoll(httpServer, sid);
// then a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
it("should close the connection when receiving an EVENT packet while not connected", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send an EVENT packet
await eioPush(httpServer, sid, '42["some event"]');
// session is cleanly closed, we receive a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
it("should close the connection when receiving an invalid packet", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send a CONNECT packet
await eioPush(httpServer, sid, "40");
// send an invalid packet
await eioPush(httpServer, sid, "4abc");
// session is cleanly closed (not discarded, see 'client.close()')
// first, we receive the Socket.IO handshake response
await eioPoll(httpServer, sid);
// then a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
});
});
describe("namespaces", () => {
@@ -2082,6 +2180,100 @@ describe("socket.io", () => {
});
});
});
describe("onAnyOutgoing", () => {
it("should call listener", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv, { multiplex: false });
sio.on("connection", (socket) => {
socket.onAnyOutgoing((event, arg1) => {
expect(event).to.be("my-event");
expect(arg1).to.be("123");
success(sio, clientSocket, done);
});
socket.emit("my-event", "123");
});
});
});
it("should call listener when broadcasting", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv, { multiplex: false });
sio.on("connection", (socket) => {
socket.onAnyOutgoing((event, arg1) => {
expect(event).to.be("my-event");
expect(arg1).to.be("123");
success(sio, clientSocket, done);
});
sio.emit("my-event", "123");
});
});
});
it("should prepend listener", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(async () => {
const clientSocket = client(srv, { multiplex: false });
const socket = (await waitFor(sio, "connection")) as Socket;
let count = 0;
socket.onAnyOutgoing((event, arg1) => {
expect(count).to.be(2);
success(sio, clientSocket, done);
});
socket.prependAnyOutgoing(() => {
expect(count++).to.be(1);
});
socket.prependAnyOutgoing(() => {
expect(count++).to.be(0);
});
socket.emit("my-event", "123");
});
});
it("should remove listener", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv, { multiplex: false });
sio.on("connection", (socket) => {
const fail = () => done(new Error("fail"));
socket.onAnyOutgoing(fail);
socket.offAnyOutgoing(fail);
expect(socket.listenersAnyOutgoing.length).to.be(0);
socket.onAnyOutgoing(() => {
success(sio, clientSocket, done);
});
socket.emit("my-event", "123");
});
});
});
});
});
describe("messaging many", () => {
@@ -2519,6 +2711,119 @@ describe("socket.io", () => {
});
});
});
it("should broadcast and expect multiple acknowledgements", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(async () => {
const socket1 = client(srv, { multiplex: false });
const socket2 = client(srv, { multiplex: false });
const socket3 = client(srv, { multiplex: false });
await Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]);
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", (cb) => {
cb(3);
});
sio.timeout(2000).emit("some event", (err, responses) => {
expect(err).to.be(null);
expect(responses).to.have.length(3);
expect(responses).to.contain(1, 2, 3);
done();
});
});
});
it("should fail when a client does not acknowledge the event in the given delay", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(async () => {
const socket1 = client(srv, { multiplex: false });
const socket2 = client(srv, { multiplex: false });
const socket3 = client(srv, { multiplex: false });
await Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]);
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", (cb) => {
// timeout
});
sio.timeout(200).emit("some event", (err, responses) => {
expect(err).to.be.an(Error);
expect(responses).to.have.length(2);
expect(responses).to.contain(1, 2);
done();
});
});
});
it("should broadcast and return if the packet is sent to 0 client", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(async () => {
const socket1 = client(srv, { multiplex: false });
const socket2 = client(srv, { multiplex: false });
const socket3 = client(srv, { multiplex: false });
await Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]);
socket1.on("some event", () => {
done(new Error("should not happen"));
});
socket2.on("some event", () => {
done(new Error("should not happen"));
});
socket3.on("some event", () => {
done(new Error("should not happen"));
});
sio
.to("room123")
.timeout(200)
.emit("some event", (err, responses) => {
expect(err).to.be(null);
expect(responses).to.have.length(0);
done();
});
});
});
});
describe("middleware", () => {

View File

@@ -12,7 +12,7 @@ const i = expect.stringify;
// add support for Set/Map
const contain = expect.Assertion.prototype.contain;
expect.Assertion.prototype.contain = function (...args) {
if (typeof this.obj === "object") {
if (this.obj instanceof Set || this.obj instanceof Map) {
args.forEach((obj) => {
this.assert(
this.obj.has(obj),