mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab8289c0a | ||
|
|
30430f0985 | ||
|
|
9b43c9167c | ||
|
|
8ecfcba5c1 | ||
|
|
572133a58d | ||
|
|
6e1bb62982 | ||
|
|
06e6838b18 | ||
|
|
1f03a44d1f | ||
|
|
be3d7f0f1f | ||
|
|
d12aab2d69 | ||
|
|
9f758689f6 | ||
|
|
0b35dc77c0 | ||
|
|
531104d332 | ||
|
|
8b204570a9 | ||
|
|
0b7d70ca42 |
56
CHANGELOG.md
56
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
4
client-dist/socket.io.esm.min.js
vendored
4
client-dist/socket.io.esm.min.js
vendored
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
4
client-dist/socket.io.min.js
vendored
4
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
client-dist/socket.io.msgpack.min.js
vendored
4
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
16
examples/express-session-example/README.md
Normal file
16
examples/express-session-example/README.md
Normal 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/):
|
||||
|
||||

|
||||
|
||||
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.
|
||||
BIN
examples/express-session-example/assets/demo.gif
Normal file
BIN
examples/express-session-example/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
57
examples/express-session-example/index.html
Normal file
57
examples/express-session-example/index.html
Normal 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>
|
||||
91
examples/express-session-example/index.js
Normal file
91
examples/express-session-example/index.js
Normal 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}`);
|
||||
});
|
||||
15
examples/express-session-example/package.json
Normal file
15
examples/express-session-example/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
examples/rollup-server-bundle/.gitignore
vendored
Normal file
1
examples/rollup-server-bundle/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bundle.js
|
||||
3
examples/rollup-server-bundle/index.js
Normal file
3
examples/rollup-server-bundle/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Server } from "socket.io";
|
||||
|
||||
new Server(0);
|
||||
19
examples/rollup-server-bundle/package.json
Normal file
19
examples/rollup-server-bundle/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
12
examples/rollup-server-bundle/rollup.config.js
Normal file
12
examples/rollup-server-bundle/rollup.config.js
Normal 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 })],
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
examples/webpack-build/index.js
Normal file
15
examples/webpack-build/index.js
Normal 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}`);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
module.exports = function () { return function () {}; };
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'app.js'
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
7
examples/webpack-build/webpack.config.js
Normal file
7
examples/webpack-build/webpack.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
entry: "./index.js",
|
||||
mode: "production",
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
lib/index.ts
52
lib/index.ts
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
125
lib/socket.ts
125
lib/socket.ts
@@ -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
3961
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user