Compare commits

...

24 Commits
3.1.0 ... 4.0.0

Author SHA1 Message Date
Damien Arrachequesne
5eaeffc8e2 chore(release): 4.0.0
Diff: https://github.com/socketio/socket.io/compare/3.1.2...4.0.0
2021-03-10 12:43:53 +01:00
Damien Arrachequesne
1b6d6de4ed chore: include Engine.IO v5
Release notes: https://github.com/socketio/engine.io/releases/tag/5.0.0
2021-03-10 11:14:33 +01:00
Maxime Kjaer
0107510ba8 feat: add support for typed events (#3822)
Syntax:

```ts
interface ClientToServerEvents {
  "my-event": (a: number, b: string, c: number[]) => void;
}

interface ServerToClientEvents {
  hello: (message: string) => void;
}

const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer);

io.emit("hello", "world");

io.on("connection", (socket) => {
  socket.on("my-event", (a, b, c) => {
    // ...
  });

  socket.emit("hello", "again");
});
```

The events are not typed by default (inferred as any), so this change
is backward compatible.

Note: we could also have reused the method here ([1]) to add types to
the EventEmitter, instead of creating a StrictEventEmitter class.

Related: https://github.com/socketio/socket.io/issues/3742

[1]: https://github.com/binier/tiny-typed-emitter
2021-03-10 00:18:13 +01:00
Damien Arrachequesne
b25495c069 feat: add some utility methods
This commit adds the following methods:

- fetchSockets: returns the matching socket instances

Syntax:

```js
// return all Socket instances
const sockets = await io.fetchSockets();

// return all Socket instances of the "admin" namespace in the "room1" room
const sockets = await io.of("/admin").in("room1").fetchSockets();
```

- socketsJoin: makes the matching socket instances join the specified rooms

Syntax:

```js
// make all Socket instances join the "room1" room
io.socketsJoin("room1");

// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
```

- socketsLeave: makes the matching socket instances leave the specified rooms

Syntax:

```js
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");

// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
```

- disconnectSockets: makes the matching socket instances disconnect

Syntax:

```js
// make all Socket instances disconnect
io.disconnectSockets();

// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
```

Those methods share the same semantics as broadcasting. They will also
work with multiple Socket.IO servers when using the Redis adapter. In
that case, the fetchSockets() method will return a list of RemoteSocket
instances, which expose a subset of the methods and attributes of the
Socket class (the "request" attribute cannot be mocked, for example).

Related:

- https://github.com/socketio/socket.io/issues/3042
- https://github.com/socketio/socket.io/issues/3418
- https://github.com/socketio/socket.io/issues/3570
- https://github.com/socketio/socket.io-redis/issues/283
2021-03-02 11:17:21 +01:00
Damien Arrachequesne
085d1de9df feat: allow to pass an array to io.to(...)
In some cases it is necessary to pass an array of rooms instead of a single room.

New syntax:

```
io.to(["room1", "room2"]).except(["room3"]).emit(...);

socket.to(["room1", "room2"]).except(["room3"]).emit(...);
```

Related: https://github.com/socketio/socket.io/issues/3048
2021-03-01 23:20:46 +01:00
Damien Arrachequesne
ac9e8ca6c7 fix: make io.to(...) immutable
Previously, broadcasting to a given room (by calling `io.to()`) would
mutate the io instance, which could lead to surprising behaviors, like:

```js
io.to("room1");
io.to("room2").emit(...); // also sent to room1

// or with async/await
io.to("room3").emit("details", await fetchDetails()); // random behavior: maybe in room3, maybe to all clients
```

Calling `io.to()` (or any other broadcast modifier) will now return an
immutable instance.

Related:

- https://github.com/socketio/socket.io/issues/3431
- https://github.com/socketio/socket.io/issues/3444
2021-03-01 23:17:08 +01:00
Sebastiaan Marynissen
7de2e87e88 feat: allow to exclude specific rooms when broadcasting (#3789)
New syntax:

```
io.except("room1").emit(...);
io.to("room1").except("room2").emit(...);

socket.broadcast.except("room1").emit(...);
socket.to("room1").except("room2").emit(...);
```

Related:

- https://github.com/socketio/socket.io/issues/3629
- https://github.com/socketio/socket.io/issues/3657
2021-03-01 09:30:58 +01:00
Damien Arrachequesne
225ade062a chore(release): 3.1.2
Diff: https://github.com/socketio/socket.io/compare/3.1.1...3.1.2
2021-02-26 01:18:42 +01:00
Damien Arrachequesne
494c64e44f fix: ignore packet received after disconnection
Related: https://github.com/socketio/socket.io/issues/3095
2021-02-26 00:58:30 +01:00
Damien Arrachequesne
67a61e39e6 chore: loosen the version requirement of @types/node
Related: https://github.com/socketio/socket.io/issues/3793
2021-02-26 00:58:01 +01:00
Damien Arrachequesne
7467216e02 docs(examples): 4th and final part of the "private messaging" example
See also: https://socket.io/get-started/private-messaging-part-4/
2021-02-17 00:24:23 +01:00
Damien Arrachequesne
7247b4051f docs(examples): 3rd part of the "private messaging" example
See also: https://socket.io/get-started/private-messaging-part-3/
2021-02-17 00:22:55 +01:00
Damien Arrachequesne
992c9380c3 docs(examples): 2nd part of the "private messaging" example
See also: https://socket.io/get-started/private-messaging-part-2/
2021-02-15 01:09:12 +01:00
Damien Arrachequesne
8b404f424b docs(examples): 1st part of the "private messaging" example 2021-02-10 00:33:39 +01:00
Damien Arrachequesne
12221f296d chore(release): 3.1.1
Diff: https://github.com/socketio/socket.io/compare/3.1.0...3.1.1
2021-02-03 22:57:21 +01:00
Damien Arrachequesne
6f4bd7f8e7 fix: properly parse the CONNECT packet in v2 compatibility mode
In Socket.IO v2, the Socket query option was appended to the namespace
in the CONNECT packet:

{
  type: 0,
  nsp: "/my-namespace?abc=123"
}

Note: the "query" option on the client-side (v2) will be found in the
"auth" attribute on the server-side:

```
// client-side
const socket = io("/nsp1", {
  query: {
    abc: 123
  }
});
socket.query = { abc: 456 };

// server-side
const io = require("socket.io")(httpServer, {
  allowEIO3: true // enable compatibility mode
});

io.of("/nsp1").on("connection", (socket) => {
  console.log(socket.handshake.auth); // { abc: 456 } (the Socket query)
  console.log(socket.handshake.query.abc); // 123 (the Manager query)
});

More information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/#Add-a-clear-distinction-between-the-Manager-query-option-and-the-Socket-query-option

Related: https://github.com/socketio/socket.io/issues/3791
2021-02-03 22:54:07 +01:00
Damien Arrachequesne
4f2e9a716d fix(typings): update the types of "query", "auth" and "headers"
Related: https://github.com/socketio/socket.io/issues/3770
2021-02-03 22:53:38 +01:00
david-fong
9e8f288ca9 fix(typings): add return types and general-case overload signatures (#3776)
See also: https://stackoverflow.com/questions/52760509/typescript-returntype-of-overloaded-function/52760599#52760599
2021-02-02 11:50:08 +01:00
Damien Arrachequesne
86eb4227b2 docs(examples): add example with traefik
Reference: https://doc.traefik.io/traefik/v2.0/
2021-01-31 23:47:23 +01:00
Damien Arrachequesne
cf873fd831 docs(examples): update cluster examples to Socket.IO v3 2021-01-28 11:21:38 +01:00
Damien Arrachequesne
0d10e6131b docs(examples): update the nginx cluster example
Related: https://github.com/socketio/socket.io/discussions/3778

Reference: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#hash
2021-01-28 10:52:26 +01:00
JPSO
10aafbbc16 ci: add Node.js 15 (#3765) 2021-01-20 22:34:51 +01:00
Hamdi Bayhan
f34cfca26d docs: fix broken link (#3759) 2021-01-17 22:10:37 +01:00
PrashoonB
d412e876b8 docs: add installation with yarn (#3757) 2021-01-15 22:20:04 +01:00
64 changed files with 4847 additions and 601 deletions

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [10.x, 12.x, 14.x, 15.x]
steps:
- uses: actions/checkout@v2

View File

@@ -1,3 +1,37 @@
# [4.0.0](https://github.com/socketio/socket.io/compare/3.1.2...4.0.0) (2021-03-10)
### Bug Fixes
* make io.to(...) immutable ([ac9e8ca](https://github.com/socketio/socket.io/commit/ac9e8ca6c71e00d4af45ee03f590fe56f3951186))
### Features
* add some utility methods ([b25495c](https://github.com/socketio/socket.io/commit/b25495c069031674da08e19aed68922c7c7a0e28))
* add support for typed events ([#3822](https://github.com/socketio/socket.io/issues/3822)) ([0107510](https://github.com/socketio/socket.io/commit/0107510ba8a0f148c78029d8be8919b350feb633))
* allow to exclude specific rooms when broadcasting ([#3789](https://github.com/socketio/socket.io/issues/3789)) ([7de2e87](https://github.com/socketio/socket.io/commit/7de2e87e888d849eb2dfc5e362af4c9e86044701))
* allow to pass an array to io.to(...) ([085d1de](https://github.com/socketio/socket.io/commit/085d1de9df909651de8b313cc6f9f253374b702e))
## [3.1.2](https://github.com/socketio/socket.io/compare/3.1.1...3.1.2) (2021-02-26)
### Bug Fixes
* ignore packets received after disconnection ([494c64e](https://github.com/socketio/socket.io/commit/494c64e44f645cbd24c645f1186d203789e84af0))
## [3.1.1](https://github.com/socketio/socket.io/compare/3.1.0...3.1.1) (2021-02-03)
### Bug Fixes
* properly parse the CONNECT packet in v2 compatibility mode ([6f4bd7f](https://github.com/socketio/socket.io/commit/6f4bd7f8e7c41a075a8014565330a77c38b03a8d))
* **typings:** add return types and general-case overload signatures ([#3776](https://github.com/socketio/socket.io/issues/3776)) ([9e8f288](https://github.com/socketio/socket.io/commit/9e8f288ca9f14f91064b8d3cce5946f7d23d407c))
* **typings:** update the types of "query", "auth" and "headers" ([4f2e9a7](https://github.com/socketio/socket.io/commit/4f2e9a716d9835b550c8fd9a9b429ebf069c2895))
# [3.1.0](https://github.com/socketio/socket.io/compare/3.0.5...3.1.0) (2021-01-15)

View File

@@ -36,7 +36,7 @@ For this purpose, it relies on [Engine.IO](https://github.com/socketio/engine.io
#### Auto-reconnection support
Unless instructed otherwise a disconnected client will try to reconnect forever, until the server is available again. Please see the available reconnection options [here](https://github.com/socketio/socket.io-client/blob/master/docs/API.md#new-managerurl-options).
Unless instructed otherwise a disconnected client will try to reconnect forever, until the server is available again. Please see the available reconnection options [here](https://socket.io/docs/v3/client-api/#new-Manager-url-options).
#### Disconnection detection
@@ -85,7 +85,11 @@ This is a useful feature to send notifications to a group of users, or to a give
## Installation
```bash
// with npm
npm install socket.io
// with yarn
yarn add socket.io
```
## How to use

View File

@@ -1,5 +1,5 @@
/*!
* Socket.IO v3.1.0
* Socket.IO v4.0.0
* (c) 2014-2021 Guillermo Rauch
* Released under the MIT License.
*/
@@ -12,7 +12,7 @@
exports["io"] = factory();
else
root["io"] = factory();
})(window, function() {
})(self, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
@@ -151,7 +151,7 @@ function lookup(uri, opts) {
}
opts = opts || {};
var parsed = url_1.url(uri);
var parsed = url_1.url(uri, opts.path);
var source = parsed.source;
var id = parsed.id;
var path = parsed.path;
@@ -172,7 +172,7 @@ function lookup(uri, opts) {
}
if (parsed.query && !opts.query) {
opts.query = parsed.query;
opts.query = parsed.queryKey;
}
return io.socket(parsed.path, opts);
@@ -236,10 +236,6 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
@@ -263,18 +259,18 @@ var eio = __webpack_require__(/*! engine.io-client */ "./node_modules/engine.io-
var socket_1 = __webpack_require__(/*! ./socket */ "./build/socket.js");
var Emitter = __webpack_require__(/*! component-emitter */ "./node_modules/component-emitter/index.js");
var parser = __webpack_require__(/*! socket.io-parser */ "./node_modules/socket.io-parser/dist/index.js");
var on_1 = __webpack_require__(/*! ./on */ "./build/on.js");
var Backoff = __webpack_require__(/*! backo2 */ "./node_modules/backo2/index.js");
var typed_events_1 = __webpack_require__(/*! ./typed-events */ "./build/typed-events.js");
var debug = __webpack_require__(/*! debug */ "./node_modules/debug/src/browser.js")("socket.io-client:manager");
var Manager = /*#__PURE__*/function (_Emitter) {
_inherits(Manager, _Emitter);
var Manager = /*#__PURE__*/function (_typed_events_1$Stric) {
_inherits(Manager, _typed_events_1$Stric);
var _super = _createSuper(Manager);
@@ -425,7 +421,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
self.cleanup();
self._readyState = "closed";
_get(_getPrototypeOf(Manager.prototype), "emit", _this2).call(_this2, "error", err);
_this2.emitReserved("error", err);
if (fn) {
fn(err);
@@ -450,6 +446,11 @@ var Manager = /*#__PURE__*/function (_Emitter) {
socket.close();
socket.emit("error", new Error("timeout"));
}, timeout);
if (this.opts.autoUnref) {
timer.unref();
}
this.subs.push(function subDestroy() {
clearTimeout(timer);
});
@@ -485,9 +486,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
this.cleanup(); // mark as open
this._readyState = "open";
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "open"); // add new subs
this.emitReserved("open"); // add new subs
var socket = this.engine;
this.subs.push(on_1.on(socket, "ping", this.onping.bind(this)), on_1.on(socket, "data", this.ondata.bind(this)), on_1.on(socket, "error", this.onerror.bind(this)), on_1.on(socket, "close", this.onclose.bind(this)), on_1.on(this.decoder, "decoded", this.ondecoded.bind(this)));
@@ -501,7 +500,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
}, {
key: "onping",
value: function onping() {
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "ping");
this.emitReserved("ping");
}
/**
* Called with data.
@@ -523,7 +522,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
}, {
key: "ondecoded",
value: function ondecoded(packet) {
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "packet", packet);
this.emitReserved("packet", packet);
}
/**
* Called upon socket error.
@@ -535,8 +534,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
key: "onerror",
value: function onerror(err) {
debug("error", err);
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "error", err);
this.emitReserved("error", err);
}
/**
* Creates a new socket for the given `nsp`.
@@ -592,7 +590,6 @@ var Manager = /*#__PURE__*/function (_Emitter) {
key: "_packet",
value: function _packet(packet) {
debug("writing packet %j", packet);
if (packet.query && packet.type === 0) packet.nsp += "?" + packet.query;
var encodedPackets = this.encoder.encode(packet);
for (var i = 0; i < encodedPackets.length; i++) {
@@ -662,8 +659,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
this.cleanup();
this.backoff.reset();
this._readyState = "closed";
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "close", reason);
this.emitReserved("close", reason);
if (this._reconnection && !this.skipReconnect) {
this.reconnect();
@@ -686,9 +682,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
if (this.backoff.attempts >= this._reconnectionAttempts) {
debug("reconnect failed");
this.backoff.reset();
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "reconnect_failed");
this.emitReserved("reconnect_failed");
this._reconnecting = false;
} else {
var delay = this.backoff.duration();
@@ -698,7 +692,7 @@ var Manager = /*#__PURE__*/function (_Emitter) {
if (self.skipReconnect) return;
debug("attempting reconnect");
_get(_getPrototypeOf(Manager.prototype), "emit", _this3).call(_this3, "reconnect_attempt", self.backoff.attempts); // check again for the case socket closed in above events
_this3.emitReserved("reconnect_attempt", self.backoff.attempts); // check again for the case socket closed in above events
if (self.skipReconnect) return;
@@ -708,13 +702,18 @@ var Manager = /*#__PURE__*/function (_Emitter) {
self._reconnecting = false;
self.reconnect();
_get(_getPrototypeOf(Manager.prototype), "emit", _this3).call(_this3, "reconnect_error", err);
_this3.emitReserved("reconnect_error", err);
} else {
debug("reconnect success");
self.onreconnect();
}
});
}, delay);
if (this.opts.autoUnref) {
timer.unref();
}
this.subs.push(function subDestroy() {
clearTimeout(timer);
});
@@ -732,13 +731,12 @@ var Manager = /*#__PURE__*/function (_Emitter) {
var attempt = this.backoff.attempts;
this._reconnecting = false;
this.backoff.reset();
_get(_getPrototypeOf(Manager.prototype), "emit", this).call(this, "reconnect", attempt);
this.emitReserved("reconnect", attempt);
}
}]);
return Manager;
}(Emitter);
}(typed_events_1.StrictEventEmitter);
exports.Manager = Manager;
@@ -819,10 +817,10 @@ exports.Socket = void 0;
var socket_io_parser_1 = __webpack_require__(/*! socket.io-parser */ "./node_modules/socket.io-parser/dist/index.js");
var Emitter = __webpack_require__(/*! component-emitter */ "./node_modules/component-emitter/index.js");
var on_1 = __webpack_require__(/*! ./on */ "./build/on.js");
var typed_events_1 = __webpack_require__(/*! ./typed-events */ "./build/typed-events.js");
var debug = __webpack_require__(/*! debug */ "./node_modules/debug/src/browser.js")("socket.io-client:socket");
/**
* Internal events.
@@ -840,8 +838,8 @@ var RESERVED_EVENTS = Object.freeze({
removeListener: 1
});
var Socket = /*#__PURE__*/function (_Emitter) {
_inherits(Socket, _Emitter);
var Socket = /*#__PURE__*/function (_typed_events_1$Stric) {
_inherits(Socket, _typed_events_1$Stric);
var _super = _createSuper(Socket);
@@ -943,7 +941,6 @@ var Socket = /*#__PURE__*/function (_Emitter) {
* Override `emit`.
* If the event is in `events`, it's emitted normally.
*
* @param ev - event name
* @return self
* @public
*/
@@ -1039,7 +1036,7 @@ var Socket = /*#__PURE__*/function (_Emitter) {
key: "onerror",
value: function onerror(err) {
if (!this.connected) {
_get(_getPrototypeOf(Socket.prototype), "emit", this).call(this, "connect_error", err);
this.emitReserved("connect_error", err);
}
}
/**
@@ -1056,8 +1053,7 @@ var Socket = /*#__PURE__*/function (_Emitter) {
this.connected = false;
this.disconnected = true;
delete this.id;
_get(_getPrototypeOf(Socket.prototype), "emit", this).call(this, "disconnect", reason);
this.emitReserved("disconnect", reason);
}
/**
* Called with socket packet.
@@ -1078,7 +1074,7 @@ var Socket = /*#__PURE__*/function (_Emitter) {
var id = packet.data.sid;
this.onconnect(id);
} else {
_get(_getPrototypeOf(Socket.prototype), "emit", this).call(this, "connect_error", new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));
this.emitReserved("connect_error", new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));
}
break;
@@ -1107,9 +1103,7 @@ var Socket = /*#__PURE__*/function (_Emitter) {
var err = new Error(packet.data.message); // @ts-ignore
err.data = packet.data.data;
_get(_getPrototypeOf(Socket.prototype), "emit", this).call(this, "connect_error", err);
this.emitReserved("connect_error", err);
break;
}
}
@@ -1221,9 +1215,7 @@ var Socket = /*#__PURE__*/function (_Emitter) {
this.id = id;
this.connected = true;
this.disconnected = false;
_get(_getPrototypeOf(Socket.prototype), "emit", this).call(this, "connect");
this.emitReserved("connect");
this.emitBuffered();
}
/**
@@ -1430,12 +1422,172 @@ var Socket = /*#__PURE__*/function (_Emitter) {
}]);
return Socket;
}(Emitter);
}(typed_events_1.StrictEventEmitter);
exports.Socket = Socket;
/***/ }),
/***/ "./build/typed-events.js":
/*!*******************************!*\
!*** ./build/typed-events.js ***!
\*******************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.StrictEventEmitter = void 0;
var Emitter = __webpack_require__(/*! component-emitter */ "./node_modules/component-emitter/index.js");
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
var StrictEventEmitter = /*#__PURE__*/function (_Emitter) {
_inherits(StrictEventEmitter, _Emitter);
var _super = _createSuper(StrictEventEmitter);
function StrictEventEmitter() {
_classCallCheck(this, StrictEventEmitter);
return _super.apply(this, arguments);
}
_createClass(StrictEventEmitter, [{
key: "on",
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
value: function on(ev, listener) {
_get(_getPrototypeOf(StrictEventEmitter.prototype), "on", this).call(this, ev, listener);
return this;
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
}, {
key: "once",
value: function once(ev, listener) {
_get(_getPrototypeOf(StrictEventEmitter.prototype), "once", this).call(this, ev, listener);
return this;
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
}, {
key: "emit",
value: function emit(ev) {
var _get2;
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
(_get2 = _get(_getPrototypeOf(StrictEventEmitter.prototype), "emit", this)).call.apply(_get2, [this, ev].concat(args));
return this;
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
}, {
key: "emitReserved",
value: function emitReserved(ev) {
var _get3;
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
(_get3 = _get(_getPrototypeOf(StrictEventEmitter.prototype), "emit", this)).call.apply(_get3, [this, ev].concat(args));
return this;
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
}, {
key: "listeners",
value: function listeners(event) {
return _get(_getPrototypeOf(StrictEventEmitter.prototype), "listeners", this).call(this, event);
}
}]);
return StrictEventEmitter;
}(Emitter);
exports.StrictEventEmitter = StrictEventEmitter;
/***/ }),
/***/ "./build/url.js":
/*!**********************!*\
!*** ./build/url.js ***!
@@ -1458,13 +1610,16 @@ var debug = __webpack_require__(/*! debug */ "./node_modules/debug/src/browser.j
* URL parser.
*
* @param uri - url
* @param path - the request path of the connection
* @param loc - An object meant to mimic window.location.
* Defaults to window.location.
* @public
*/
function url(uri, loc) {
function url(uri) {
var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
var loc = arguments.length > 2 ? arguments[2] : undefined;
var obj = uri; // default to window.location
loc = loc || typeof location !== "undefined" && location;
@@ -1507,7 +1662,7 @@ function url(uri, loc) {
var ipv6 = obj.host.indexOf(":") !== -1;
var host = ipv6 ? "[" + obj.host + "]" : obj.host; // define unique id
obj.id = obj.protocol + "://" + host + ":" + obj.port; // define href
obj.id = obj.protocol + "://" + host + ":" + obj.port + path; // define href
obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
return obj;
@@ -1797,7 +1952,7 @@ Emitter.prototype.hasListeners = function (event) {
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(process) {/* eslint-env browser */
/* eslint-env browser */
/**
* This is the web browser implementation of `debug()`.
@@ -1978,7 +2133,6 @@ formatters.j = function (v) {
return '[UnexpectedJSONParseError]: ' + error.message;
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../process/browser.js */ "./node_modules/process/browser.js")))
/***/ }),
@@ -2432,6 +2586,25 @@ var Socket = /*#__PURE__*/function (_Emitter) {
_this.pingTimeoutTimer = null;
if (typeof addEventListener === "function") {
addEventListener("beforeunload", function () {
if (_this.transport) {
// silently close the transport
_this.transport.removeAllListeners();
_this.transport.close();
}
}, false);
if (_this.hostname !== "localhost") {
_this.offlineEventListener = function () {
_this.onClose("transport close");
};
addEventListener("offline", _this.offlineEventListener, false);
}
}
_this.open();
return _this;
@@ -2753,6 +2926,10 @@ var Socket = /*#__PURE__*/function (_Emitter) {
this.pingTimeoutTimer = setTimeout(function () {
_this2.onClose("ping timeout");
}, this.pingInterval + this.pingTimeout);
if (this.opts.autoUnref) {
this.pingTimeoutTimer.unref();
}
}
/**
* Called on `drain` event
@@ -2937,7 +3114,12 @@ var Socket = /*#__PURE__*/function (_Emitter) {
this.transport.close(); // ignore further transport communication
this.transport.removeAllListeners(); // set ready state
this.transport.removeAllListeners();
if (typeof removeEventListener === "function") {
removeEventListener("offline", this.offlineEventListener, false);
} // set ready state
this.readyState = "closed"; // clear session id
@@ -3186,7 +3368,7 @@ module.exports = Transport;
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
var XMLHttpRequest = __webpack_require__(/*! xmlhttprequest-ssl */ "./node_modules/engine.io-client/lib/xmlhttprequest.js");
var XMLHttpRequest = __webpack_require__(/*! ../../contrib/xmlhttprequest-ssl/XMLHttpRequest */ "./node_modules/engine.io-client/lib/xmlhttprequest.js");
var XHR = __webpack_require__(/*! ./polling-xhr */ "./node_modules/engine.io-client/lib/transports/polling-xhr.js");
@@ -3279,11 +3461,6 @@ var rEscapedNewline = /\\n/g;
*/
var callbacks;
/**
* Noop.
*/
function empty() {}
var JSONPPolling = /*#__PURE__*/function (_Polling) {
_inherits(JSONPPolling, _Polling);
@@ -3319,14 +3496,7 @@ var JSONPPolling = /*#__PURE__*/function (_Polling) {
self.onData(msg);
}); // append to query string
_this.query.j = _this.index; // prevent spurious errors from being emitted when the window is unloaded
if (typeof addEventListener === "function") {
addEventListener("beforeunload", function () {
if (self.script) self.script.onerror = empty;
}, false);
}
_this.query.j = _this.index;
return _this;
}
/**
@@ -3344,6 +3514,9 @@ var JSONPPolling = /*#__PURE__*/function (_Polling) {
*/
value: function doClose() {
if (this.script) {
// prevent spurious errors from being emitted when the window is unloaded
this.script.onerror = function () {};
this.script.parentNode.removeChild(this.script);
this.script = null;
}
@@ -3528,7 +3701,7 @@ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Re
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
/* global attachEvent */
var XMLHttpRequest = __webpack_require__(/*! xmlhttprequest-ssl */ "./node_modules/engine.io-client/lib/xmlhttprequest.js");
var XMLHttpRequest = __webpack_require__(/*! ../../contrib/xmlhttprequest-ssl/XMLHttpRequest */ "./node_modules/engine.io-client/lib/xmlhttprequest.js");
var Polling = __webpack_require__(/*! ./polling */ "./node_modules/engine.io-client/lib/transports/polling.js");
@@ -3695,7 +3868,7 @@ var Request = /*#__PURE__*/function (_Emitter) {
_createClass(Request, [{
key: "create",
value: function create() {
var opts = pick(this.opts, "agent", "enablesXDR", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized");
var opts = pick(this.opts, "agent", "enablesXDR", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "autoUnref");
opts.xdomain = !!this.opts.xd;
opts.xscheme = !!this.opts.xs;
var xhr = this.xhr = new XMLHttpRequest(opts);
@@ -4318,22 +4491,24 @@ var WS = /*#__PURE__*/function (_Transport) {
}, {
key: "addEventListeners",
value: function addEventListeners() {
var self = this;
var _this2 = this;
this.ws.onopen = function () {
self.onOpen();
if (_this2.opts.autoUnref) {
_this2.ws._socket.unref();
}
_this2.onOpen();
};
this.ws.onclose = function () {
self.onClose();
};
this.ws.onclose = this.onClose.bind(this);
this.ws.onmessage = function (ev) {
self.onData(ev.data);
return _this2.onData(ev.data);
};
this.ws.onerror = function (e) {
self.onError("websocket error", e);
return _this2.onError("websocket error", e);
};
}
/**
@@ -4425,6 +4600,7 @@ var WS = /*#__PURE__*/function (_Transport) {
value: function doClose() {
if (typeof this.ws !== "undefined") {
this.ws.close();
this.ws = null;
}
}
/**
@@ -5188,224 +5364,6 @@ function queryKey(uri, query) {
/***/ }),
/***/ "./node_modules/process/browser.js":
/*!*****************************************!*\
!*** ./node_modules/process/browser.js ***!
\*****************************************/
/*! no static exports found */
/***/ (function(module, exports) {
// shim for using process in browser
var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it
// don't break things. But we need to wrap it in a try catch in case it is
// wrapped in strict mode code which doesn't define any globals. It's inside a
// function because try/catches deoptimize in certain engines.
var cachedSetTimeout;
var cachedClearTimeout;
function defaultSetTimout() {
throw new Error('setTimeout has not been defined');
}
function defaultClearTimeout() {
throw new Error('clearTimeout has not been defined');
}
(function () {
try {
if (typeof setTimeout === 'function') {
cachedSetTimeout = setTimeout;
} else {
cachedSetTimeout = defaultSetTimout;
}
} catch (e) {
cachedSetTimeout = defaultSetTimout;
}
try {
if (typeof clearTimeout === 'function') {
cachedClearTimeout = clearTimeout;
} else {
cachedClearTimeout = defaultClearTimeout;
}
} catch (e) {
cachedClearTimeout = defaultClearTimeout;
}
})();
function runTimeout(fun) {
if (cachedSetTimeout === setTimeout) {
//normal enviroments in sane situations
return setTimeout(fun, 0);
} // if setTimeout wasn't available but was latter defined
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
cachedSetTimeout = setTimeout;
return setTimeout(fun, 0);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedSetTimeout(fun, 0);
} catch (e) {
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedSetTimeout.call(null, fun, 0);
} catch (e) {
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
return cachedSetTimeout.call(this, fun, 0);
}
}
}
function runClearTimeout(marker) {
if (cachedClearTimeout === clearTimeout) {
//normal enviroments in sane situations
return clearTimeout(marker);
} // if clearTimeout wasn't available but was latter defined
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
cachedClearTimeout = clearTimeout;
return clearTimeout(marker);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedClearTimeout(marker);
} catch (e) {
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedClearTimeout.call(null, marker);
} catch (e) {
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
return cachedClearTimeout.call(this, marker);
}
}
}
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
function cleanUpNextTick() {
if (!draining || !currentQueue) {
return;
}
draining = false;
if (currentQueue.length) {
queue = currentQueue.concat(queue);
} else {
queueIndex = -1;
}
if (queue.length) {
drainQueue();
}
}
function drainQueue() {
if (draining) {
return;
}
var timeout = runTimeout(cleanUpNextTick);
draining = true;
var len = queue.length;
while (len) {
currentQueue = queue;
queue = [];
while (++queueIndex < len) {
if (currentQueue) {
currentQueue[queueIndex].run();
}
}
queueIndex = -1;
len = queue.length;
}
currentQueue = null;
draining = false;
runClearTimeout(timeout);
}
process.nextTick = function (fun) {
var args = new Array(arguments.length - 1);
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
}
queue.push(new Item(fun, args));
if (queue.length === 1 && !draining) {
runTimeout(drainQueue);
}
}; // v8 likes predictible objects
function Item(fun, array) {
this.fun = fun;
this.array = array;
}
Item.prototype.run = function () {
this.fun.apply(null, this.array);
};
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.version = ''; // empty string to avoid regexp issues
process.versions = {};
function noop() {}
process.on = noop;
process.addListener = noop;
process.once = noop;
process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.prependListener = noop;
process.prependOnceListener = noop;
process.listeners = function (name) {
return [];
};
process.binding = function (name) {
throw new Error('process.binding is not supported');
};
process.cwd = function () {
return '/';
};
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
process.umask = function () {
return 0;
};
/***/ }),
/***/ "./node_modules/socket.io-parser/dist/binary.js":
/*!******************************************************!*\
!*** ./node_modules/socket.io-parser/dist/binary.js ***!

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

@@ -1,4 +1,4 @@
FROM mhart/alpine-node:6
FROM node:14-alpine
# Create app directory
RUN mkdir -p /usr/src/app
@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install
RUN npm install --prod
# Bundle app source
COPY . /usr/src/app

View File

@@ -8,8 +8,8 @@
"license": "BSD",
"dependencies": {
"express": "4.13.4",
"socket.io": "^1.7.2",
"socket.io-redis": "^3.0.0"
"socket.io": "^3.1.0",
"socket.io-redis": "^6.0.1"
},
"scripts": {
"start": "node index.js"

View File

@@ -3,6 +3,8 @@ Listen 80
ServerName localhost
LoadModule mpm_event_module modules/mod_mpm_event.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authz_host_module modules/mod_authz_host.so

View File

@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install
RUN npm install --prod
# Bundle app source
COPY . /usr/src/app

View File

@@ -8,8 +8,8 @@
"license": "BSD",
"dependencies": {
"express": "4.13.4",
"socket.io": "^1.7.2",
"socket.io-redis": "^3.0.0"
"socket.io": "^3.1.0",
"socket.io-redis": "^6.0.1"
},
"scripts": {
"start": "node index.js"

View File

@@ -22,6 +22,16 @@ Each node connects to the redis backend, which will enable to broadcast to every
$ docker-compose stop server-george
```
A `client` container is included in the `docker-compose.yml` file, in order to test the routing.
You can create additional `client` containers with:
```
$ docker-compose up -d --scale=client=10 client
# and then
$ docker-compose logs client
```
## Features
- Multiple users can join a chat room by each entering a unique username

View File

@@ -0,0 +1,15 @@
FROM node:14-alpine
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install --prod
# Bundle app source
COPY . /usr/src/app
EXPOSE 3000
CMD [ "npm", "start" ]

View File

@@ -0,0 +1,13 @@
const socket = require('socket.io-client')('ws://nginx');
socket.on('connect', () => {
console.log('connected');
});
socket.on('my-name-is', (serverName) => {
console.log(`connected to ${serverName}`);
});
socket.on('disconnect', (reason) => {
console.log(`disconnected due to ${reason}`);
});

View File

@@ -0,0 +1,15 @@
{
"name": "socket.io-chat",
"version": "0.0.0",
"description": "A simple chat client using socket.io",
"main": "index.js",
"author": "Grant Timmerman",
"private": true,
"license": "MIT",
"dependencies": {
"socket.io-client": "^3.1.0"
},
"scripts": {
"start": "node index.js"
}
}

View File

@@ -45,6 +45,11 @@ server-ringo:
environment:
- NAME=Ringo
client:
build: ./client
links:
- nginx
redis:
image: redis:alpine
expose:

View File

@@ -24,8 +24,12 @@ http {
}
upstream nodes {
# enable sticky session
ip_hash;
# enable sticky session with either "hash" (uses the complete IP address)
hash $remote_addr consistent;
# or "ip_hash" (uses the first three octets of the client IPv4 address, or the entire IPv6 address)
# ip_hash;
# or "sticky" (needs commercial subscription)
# sticky cookie srv_id expires=1h domain=.example.com path=/;
server server-john:3000;
server server-paul:3000;

View File

@@ -1,4 +1,4 @@
FROM mhart/alpine-node:6
FROM node:14-alpine
# Create app directory
RUN mkdir -p /usr/src/app
@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install
RUN npm install --prod
# Bundle app source
COPY . /usr/src/app

View File

@@ -8,8 +8,8 @@
"license": "MIT",
"dependencies": {
"express": "4.13.4",
"socket.io": "^1.7.2",
"socket.io-redis": "^3.0.0"
"socket.io": "^3.1.0",
"socket.io-redis": "^6.0.1"
},
"scripts": {
"start": "node index.js"

View File

@@ -0,0 +1,22 @@
# Socket.IO Chat with traefik & [redis](https://redis.io/)
A simple chat demo for Socket.IO
## How to use
Install [Docker Compose](https://docs.docker.com/compose/install/), then:
```
$ docker-compose up -d
```
And then point your browser to `http://localhost:3000`.
You can then scale the server to multiple instances:
```
$ docker-compose up -d --scale=server=7
```
The session stickiness, which is [required](https://socket.io/docs/v3/using-multiple-nodes/) when using multiple Socket.IO server instances, is achieved with a cookie. More information [here](https://doc.traefik.io/traefik/v2.0/routing/services/#sticky-sessions).

View File

@@ -0,0 +1,27 @@
version: "3"
services:
traefik:
image: traefik:2.4
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml
- /var/run/docker.sock:/var/run/docker.sock
links:
- server
ports:
- "3000:80"
- "8080:8080"
server:
build: ./server
links:
- redis
labels:
- "traefik.http.routers.chat.rule=PathPrefix(`/`)"
- traefik.http.services.chat.loadBalancer.sticky.cookie.name=server_id
- traefik.http.services.chat.loadBalancer.sticky.cookie.httpOnly=true
redis:
image: redis:6-alpine
labels:
- traefik.enable=false

View File

@@ -0,0 +1,15 @@
FROM node:14-alpine
# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install --prod
# Bundle app source
COPY . /usr/src/app
EXPOSE 3000
CMD [ "npm", "start" ]

View File

@@ -0,0 +1,83 @@
// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var redis = require('socket.io-redis');
var port = process.env.PORT || 3000;
var crypto = require('crypto');
var serverName = crypto.randomBytes(3).toString('hex');
io.adapter(redis({ host: 'redis', port: 6379 }));
server.listen(port, function () {
console.log('Server listening at port %d', port);
console.log('Hello, I\'m %s, how can I help?', serverName);
});
// Routing
app.use(express.static(__dirname + '/public'));
// Chatroom
var numUsers = 0;
io.on('connection', function (socket) {
socket.emit('my-name-is', serverName);
var addedUser = false;
// when the client emits 'new message', this listens and executes
socket.on('new message', function (data) {
// we tell the client to execute 'new message'
socket.broadcast.emit('new message', {
username: socket.username,
message: data
});
});
// when the client emits 'add user', this listens and executes
socket.on('add user', function (username) {
if (addedUser) return;
// we store the username in the socket session for this client
socket.username = username;
++numUsers;
addedUser = true;
socket.emit('login', {
numUsers: numUsers
});
// echo globally (all clients) that a person has connected
socket.broadcast.emit('user joined', {
username: socket.username,
numUsers: numUsers
});
});
// when the client emits 'typing', we broadcast it to others
socket.on('typing', function () {
socket.broadcast.emit('typing', {
username: socket.username
});
});
// when the client emits 'stop typing', we broadcast it to others
socket.on('stop typing', function () {
socket.broadcast.emit('stop typing', {
username: socket.username
});
});
// when the user disconnects.. perform this
socket.on('disconnect', function () {
if (addedUser) {
--numUsers;
// echo globally that this client has left
socket.broadcast.emit('user left', {
username: socket.username,
numUsers: numUsers
});
}
});
});

View File

@@ -0,0 +1,17 @@
{
"name": "socket.io-chat",
"version": "0.0.0",
"description": "A simple chat client using socket.io",
"main": "index.js",
"author": "Grant Timmerman",
"private": true,
"license": "MIT",
"dependencies": {
"express": "4.13.4",
"socket.io": "^3.1.0",
"socket.io-redis": "^6.0.1"
},
"scripts": {
"start": "node index.js"
}
}

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,286 @@
$(function() {
var FADE_TIME = 150; // ms
var TYPING_TIMER_LENGTH = 400; // ms
var COLORS = [
'#e21400', '#91580f', '#f8a700', '#f78b00',
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
];
// Initialize variables
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
// Prompt for setting a username
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();
var socket = io();
function addParticipantsMessage (data) {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
} else {
message += "there are " + data.numUsers + " participants";
}
log(message);
}
// Sets the client's username
function setUsername () {
username = cleanInput($usernameInput.val().trim());
// If the username is valid
if (username) {
$loginPage.fadeOut();
$chatPage.show();
$loginPage.off('click');
$currentInput = $inputMessage.focus();
// Tell the server your username
socket.emit('add user', username);
}
}
// Sends a chat message
function sendMessage () {
var message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
// if there is a non-empty message and a socket connection
if (message && connected) {
$inputMessage.val('');
addChatMessage({
username: username,
message: message
});
// tell server to execute 'new message' and send along one parameter
socket.emit('new message', message);
}
}
// Log a message
function log (message, options) {
var $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
}
// Adds the visual chat message to the message list
function addChatMessage (data, options) {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
var $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);
var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.append($usernameDiv, $messageBodyDiv);
addMessageElement($messageDiv, options);
}
// Adds the visual chat typing message
function addChatTyping (data) {
data.typing = true;
data.message = 'is typing';
addChatMessage(data);
}
// Removes the visual chat typing message
function removeChatTyping (data) {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
}
// Adds a message element to the messages and scrolls to the bottom
// el - The element to add as a message
// options.fade - If the element should fade-in (default = true)
// options.prepend - If the element should prepend
// all other messages (default = false)
function addMessageElement (el, options) {
var $el = $(el);
// Setup default options
if (!options) {
options = {};
}
if (typeof options.fade === 'undefined') {
options.fade = true;
}
if (typeof options.prepend === 'undefined') {
options.prepend = false;
}
// Apply options
if (options.fade) {
$el.hide().fadeIn(FADE_TIME);
}
if (options.prepend) {
$messages.prepend($el);
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight;
}
// Prevents input from having injected markup
function cleanInput (input) {
return $('<div/>').text(input).text();
}
// Updates the typing event
function updateTyping () {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
setTimeout(function () {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
// Gets the 'X is typing' messages of a user
function getTypingMessages (data) {
return $('.typing.message').filter(function (i) {
return $(this).data('username') === data.username;
});
}
// Gets the color of a username through our hash function
function getUsernameColor (username) {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
var index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
// Keyboard events
$window.keydown(function (event) {
// Auto-focus the current input when a key is typed
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
}
// When the client hits ENTER on their keyboard
if (event.which === 13) {
if (username) {
sendMessage();
socket.emit('stop typing');
typing = false;
} else {
setUsername();
}
}
});
$inputMessage.on('input', function() {
updateTyping();
});
// Click events
// Focus input when clicking anywhere on login page
$loginPage.click(function () {
$currentInput.focus();
});
// Focus input when clicking on the message input's border
$inputMessage.click(function () {
$inputMessage.focus();
});
// Socket events
// Whenever the server emits 'login', log the login message
socket.on('login', function (data) {
connected = true;
// Display the welcome message
var message = "Welcome to Socket.IO Chat ";
log(message, {
prepend: true
});
addParticipantsMessage(data);
});
// Whenever the server emits 'new message', update the chat body
socket.on('new message', function (data) {
addChatMessage(data);
});
// Whenever the server emits 'user joined', log it in the chat body
socket.on('user joined', function (data) {
log(data.username + ' joined');
addParticipantsMessage(data);
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', function (data) {
log(data.username + ' left');
addParticipantsMessage(data);
removeChatTyping(data);
});
// Whenever the server emits 'typing', show the typing message
socket.on('typing', function (data) {
addChatTyping(data);
});
// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function (data) {
removeChatTyping(data);
});
socket.on('disconnect', function () {
log('you have been disconnected');
});
socket.on('connect', function () {
if (username) {
log('you have been reconnected');
socket.emit('add user', username);
}
});
socket.io.on('reconnect_error', function () {
log('attempt to reconnect has failed');
});
socket.on('my-name-is', function (serverName) {
log('host is now ' + serverName);
})
});

View File

@@ -0,0 +1,149 @@
/* Fix user-agent */
* {
box-sizing: border-box;
}
html {
font-weight: 300;
-webkit-font-smoothing: antialiased;
}
html, input {
font-family:
"HelveticaNeue-Light",
"Helvetica Neue Light",
"Helvetica Neue",
Helvetica,
Arial,
"Lucida Grande",
sans-serif;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
ul {
list-style: none;
word-wrap: break-word;
}
/* Pages */
.pages {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
.page {
height: 100%;
position: absolute;
width: 100%;
}
/* Login Page */
.login.page {
background-color: #000;
}
.login.page .form {
height: 100px;
margin-top: -100px;
position: absolute;
text-align: center;
top: 50%;
width: 100%;
}
.login.page .form .usernameInput {
background-color: transparent;
border: none;
border-bottom: 2px solid #fff;
outline: none;
padding-bottom: 15px;
text-align: center;
width: 400px;
}
.login.page .title {
font-size: 200%;
}
.login.page .usernameInput {
font-size: 200%;
letter-spacing: 3px;
}
.login.page .title, .login.page .usernameInput {
color: #fff;
font-weight: 100;
}
/* Chat page */
.chat.page {
display: none;
}
/* Font */
.messages {
font-size: 150%;
}
.inputMessage {
font-size: 100%;
}
.log {
color: gray;
font-size: 70%;
margin: 5px;
text-align: center;
}
/* Messages */
.chatArea {
height: 100%;
padding-bottom: 60px;
}
.messages {
height: 100%;
margin: 0;
overflow-y: scroll;
padding: 10px 20px 10px 20px;
}
.message.typing .messageBody {
color: gray;
}
.username {
font-weight: 700;
overflow: hidden;
padding-right: 15px;
text-align: right;
}
/* Input */
.inputMessage {
border: 10px solid #000;
bottom: 0;
height: 60px;
left: 0;
outline: none;
padding-left: 10px;
position: absolute;
right: 0;
width: 100%;
}

View File

@@ -0,0 +1,10 @@
api:
insecure: true
entryPoints:
web:
address: ":80"
providers:
docker: {}

24
examples/private-messaging/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

View File

@@ -0,0 +1,23 @@
# Private messaging with Socket.IO
Please read the related guide:
- [Part I](https://socket.io/get-started/private-messaging-part-1/): initial implementation
- [Part II](https://socket.io/get-started/private-messaging-part-2/): persistent user ID
- [Part III](https://socket.io/get-started/private-messaging-part-3/): persistent messages
- [Part IV](https://socket.io/get-started/private-messaging-part-4/): scaling up
## Running the frontend
```
npm install
npm run serve
```
### Running the server
```
cd server
npm install
npm start
```

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -0,0 +1,43 @@
{
"name": "private-messaging",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"socket.io-client": "^3.1.1",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Private messaging with Socket.IO</title>
</head>
<body>
<noscript>
<strong>We're sorry but this application doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,31 @@
const cluster = require("cluster");
const http = require("http");
const { setupMaster } = require("@socket.io/sticky");
const WORKERS_COUNT = 4;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
for (let i = 0; i < WORKERS_COUNT; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
const httpServer = http.createServer();
setupMaster(httpServer, {
loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection"
});
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () =>
console.log(`server listening at http://localhost:${PORT}`)
);
} else {
console.log(`Worker ${process.pid} started`);
require("./index");
}

View File

@@ -0,0 +1,7 @@
version: "3"
services:
redis:
image: redis:5
ports:
- "6379:6379"

View File

@@ -0,0 +1,125 @@
const httpServer = require("http").createServer();
const Redis = require("ioredis");
const redisClient = new Redis();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:8080",
},
adapter: require("socket.io-redis")({
pubClient: redisClient,
subClient: redisClient.duplicate(),
}),
});
const { setupWorker } = require("@socket.io/sticky");
const crypto = require("crypto");
const randomId = () => crypto.randomBytes(8).toString("hex");
const { RedisSessionStore } = require("./sessionStore");
const sessionStore = new RedisSessionStore(redisClient);
const { RedisMessageStore } = require("./messageStore");
const messageStore = new RedisMessageStore(redisClient);
io.use(async (socket, next) => {
const sessionID = socket.handshake.auth.sessionID;
if (sessionID) {
const session = await sessionStore.findSession(sessionID);
if (session) {
socket.sessionID = sessionID;
socket.userID = session.userID;
socket.username = session.username;
return next();
}
}
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
socket.sessionID = randomId();
socket.userID = randomId();
socket.username = username;
next();
});
io.on("connection", async (socket) => {
// persist session
sessionStore.saveSession(socket.sessionID, {
userID: socket.userID,
username: socket.username,
connected: true,
});
// emit session details
socket.emit("session", {
sessionID: socket.sessionID,
userID: socket.userID,
});
// join the "userID" room
socket.join(socket.userID);
// fetch existing users
const users = [];
const [messages, sessions] = await Promise.all([
messageStore.findMessagesForUser(socket.userID),
sessionStore.findAllSessions(),
]);
const messagesPerUser = new Map();
messages.forEach((message) => {
const { from, to } = message;
const otherUser = socket.userID === from ? to : from;
if (messagesPerUser.has(otherUser)) {
messagesPerUser.get(otherUser).push(message);
} else {
messagesPerUser.set(otherUser, [message]);
}
});
sessions.forEach((session) => {
users.push({
userID: session.userID,
username: session.username,
connected: session.connected,
messages: messagesPerUser.get(session.userID) || [],
});
});
socket.emit("users", users);
// notify existing users
socket.broadcast.emit("user connected", {
userID: socket.userID,
username: socket.username,
connected: true,
messages: [],
});
// forward the private message to the right recipient (and to other tabs of the sender)
socket.on("private message", ({ content, to }) => {
const message = {
content,
from: socket.userID,
to,
};
socket.to(to).to(socket.userID).emit("private message", message);
messageStore.saveMessage(message);
});
// notify users upon disconnection
socket.on("disconnect", async () => {
const matchingSockets = await io.in(socket.userID).allSockets();
const isDisconnected = matchingSockets.size === 0;
if (isDisconnected) {
// notify other users
socket.broadcast.emit("user disconnected", socket.userID);
// update the connection status of the session
sessionStore.saveSession(socket.sessionID, {
userID: socket.userID,
username: socket.username,
connected: false,
});
}
});
});
setupWorker(io);

View File

@@ -0,0 +1,54 @@
/* abstract */ class MessageStore {
saveMessage(message) {}
findMessagesForUser(userID) {}
}
class InMemoryMessageStore extends MessageStore {
constructor() {
super();
this.messages = [];
}
saveMessage(message) {
this.messages.push(message);
}
findMessagesForUser(userID) {
return this.messages.filter(
({ from, to }) => from === userID || to === userID
);
}
}
const CONVERSATION_TTL = 24 * 60 * 60;
class RedisMessageStore extends MessageStore {
constructor(redisClient) {
super();
this.redisClient = redisClient;
}
saveMessage(message) {
const value = JSON.stringify(message);
this.redisClient
.multi()
.rpush(`messages:${message.from}`, value)
.rpush(`messages:${message.to}`, value)
.expire(`messages:${message.from}`, CONVERSATION_TTL)
.expire(`messages:${message.to}`, CONVERSATION_TTL)
.exec();
}
findMessagesForUser(userID) {
return this.redisClient
.lrange(`messages:${userID}`, 0, -1)
.then((results) => {
return results.map((result) => JSON.parse(result));
});
}
}
module.exports = {
InMemoryMessageStore,
RedisMessageStore,
};

View File

@@ -0,0 +1,17 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node cluster.js"
},
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "MIT",
"dependencies": {
"@socket.io/sticky": "^1.0.0",
"ioredis": "^4.22.0",
"socket.io": "^3.1.1",
"socket.io-redis": "^6.0.1"
}
}

View File

@@ -0,0 +1,89 @@
/* abstract */ class SessionStore {
findSession(id) {}
saveSession(id, session) {}
findAllSessions() {}
}
class InMemorySessionStore extends SessionStore {
constructor() {
super();
this.sessions = new Map();
}
findSession(id) {
return this.sessions.get(id);
}
saveSession(id, session) {
this.sessions.set(id, session);
}
findAllSessions() {
return [...this.sessions.values()];
}
}
const SESSION_TTL = 24 * 60 * 60;
const mapSession = ([userID, username, connected]) =>
userID ? { userID, username, connected: connected === "true" } : undefined;
class RedisSessionStore extends SessionStore {
constructor(redisClient) {
super();
this.redisClient = redisClient;
}
findSession(id) {
return this.redisClient
.hmget(`session:${id}`, "userID", "username", "connected")
.then(mapSession);
}
saveSession(id, { userID, username, connected }) {
this.redisClient
.multi()
.hset(
`session:${id}`,
"userID",
userID,
"username",
username,
"connected",
connected
)
.expire(`session:${id}`, SESSION_TTL)
.exec();
}
async findAllSessions() {
const keys = new Set();
let nextIndex = 0;
do {
const [nextIndexAsStr, results] = await this.redisClient.scan(
nextIndex,
"MATCH",
"session:*",
"COUNT",
"100"
);
nextIndex = parseInt(nextIndexAsStr, 10);
results.forEach((s) => keys.add(s));
} while (nextIndex !== 0);
const commands = [];
keys.forEach((key) => {
commands.push(["hmget", key, "userID", "username", "connected"]);
});
return this.redisClient
.multi(commands)
.exec()
.then((results) => {
return results
.map(([err, session]) => (err ? undefined : mapSession(session)))
.filter((v) => !!v);
});
}
}
module.exports = {
InMemorySessionStore,
RedisSessionStore,
};

View File

@@ -0,0 +1,78 @@
<template>
<div id="app">
<select-username
v-if="!usernameAlreadySelected"
@input="onUsernameSelection"
/>
<chat v-else />
</div>
</template>
<script>
import SelectUsername from "./components/SelectUsername";
import Chat from "./components/Chat";
import socket from "./socket";
export default {
name: "App",
components: {
Chat,
SelectUsername,
},
data() {
return {
usernameAlreadySelected: false,
};
},
methods: {
onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
},
},
created() {
const sessionID = localStorage.getItem("sessionID");
if (sessionID) {
this.usernameAlreadySelected = true;
socket.auth = { sessionID };
socket.connect();
}
socket.on("session", ({ sessionID, userID }) => {
// attach the session ID to the next reconnection attempts
socket.auth = { sessionID };
// store it in the localStorage
localStorage.setItem("sessionID", sessionID);
// save the ID of the user
socket.userID = userID;
});
socket.on("connect_error", (err) => {
if (err.message === "invalid username") {
this.usernameAlreadySelected = false;
}
});
},
destroyed() {
socket.off("connect_error");
},
};
</script>
<style>
body {
margin: 0;
}
@font-face {
font-family: Lato;
src: url("/fonts/Lato-Regular.ttf");
}
#app {
font-family: Lato, Arial, sans-serif;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div>
<div class="left-panel">
<user
v-for="user in users"
:key="user.userID"
:user="user"
:selected="selectedUser === user"
@select="onSelectUser(user)"
/>
</div>
<message-panel
v-if="selectedUser"
:user="selectedUser"
@input="onMessage"
class="right-panel"
/>
</div>
</template>
<script>
import socket from "../socket";
import User from "./User";
import MessagePanel from "./MessagePanel";
export default {
name: "Chat",
components: { User, MessagePanel },
data() {
return {
selectedUser: null,
users: [],
};
},
methods: {
onMessage(content) {
if (this.selectedUser) {
socket.emit("private message", {
content,
to: this.selectedUser.userID,
});
this.selectedUser.messages.push({
content,
fromSelf: true,
});
}
},
onSelectUser(user) {
this.selectedUser = user;
user.hasNewMessages = false;
},
},
created() {
socket.on("connect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = true;
}
});
});
socket.on("disconnect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = false;
}
});
});
const initReactiveProperties = (user) => {
user.hasNewMessages = false;
};
socket.on("users", (users) => {
users.forEach((user) => {
user.messages.forEach((message) => {
message.fromSelf = message.from === socket.userID;
});
for (let i = 0; i < this.users.length; i++) {
const existingUser = this.users[i];
if (existingUser.userID === user.userID) {
existingUser.connected = user.connected;
existingUser.messages = user.messages;
return;
}
}
user.self = user.userID === socket.userID;
initReactiveProperties(user);
this.users.push(user);
});
// put the current user first, and sort by username
this.users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
});
socket.on("user connected", (user) => {
for (let i = 0; i < this.users.length; i++) {
const existingUser = this.users[i];
if (existingUser.userID === user.userID) {
existingUser.connected = true;
return;
}
}
initReactiveProperties(user);
this.users.push(user);
});
socket.on("user disconnected", (id) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
if (user.userID === id) {
user.connected = false;
break;
}
}
});
socket.on("private message", ({ content, from, to }) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
const fromSelf = socket.userID === from;
if (user.userID === (fromSelf ? to : from)) {
user.messages.push({
content,
fromSelf,
});
if (user !== this.selectedUser) {
user.hasNewMessages = true;
}
break;
}
}
});
},
destroyed() {
socket.off("connect");
socket.off("disconnect");
socket.off("users");
socket.off("user connected");
socket.off("user disconnected");
socket.off("private message");
},
};
</script>
<style scoped>
.left-panel {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
overflow-x: hidden;
background-color: #3f0e40;
color: white;
}
.right-panel {
margin-left: 260px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<div class="header">
<status-icon :connected="user.connected" />{{ user.username }}
</div>
<ul class="messages">
<li
v-for="(message, index) in user.messages"
:key="index"
class="message"
>
<div v-if="displaySender(message, index)" class="sender">
{{ message.fromSelf ? "(yourself)" : user.username }}
</div>
{{ message.content }}
</li>
</ul>
<form @submit.prevent="onSubmit" class="form">
<textarea v-model="input" placeholder="Your message..." class="input" />
<button :disabled="!isValid" class="send-button">Send</button>
</form>
</div>
</template>
<script>
import StatusIcon from "./StatusIcon";
export default {
name: "MessagePanel",
components: {
StatusIcon,
},
props: {
user: Object,
},
data() {
return {
input: "",
};
},
methods: {
onSubmit() {
this.$emit("input", this.input);
this.input = "";
},
displaySender(message, index) {
return (
index === 0 ||
this.user.messages[index - 1].fromSelf !==
this.user.messages[index].fromSelf
);
},
},
computed: {
isValid() {
return this.input.length > 0;
},
},
};
</script>
<style scoped>
.header {
line-height: 40px;
padding: 10px 20px;
border-bottom: 1px solid #dddddd;
}
.messages {
margin: 0;
padding: 20px;
}
.message {
list-style: none;
}
.sender {
font-weight: bold;
margin-top: 5px;
}
.form {
padding: 10px;
}
.input {
width: 80%;
resize: none;
padding: 10px;
line-height: 1.5;
border-radius: 5px;
border: 1px solid #000;
}
.send-button {
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div class="select-username">
<form @submit.prevent="onSubmit">
<input v-model="username" placeholder="Your username..." />
<button :disabled="!isValid">Send</button>
</form>
</div>
</template>
<script>
export default {
name: "SelectUsername",
data() {
return {
username: "",
};
},
computed: {
isValid() {
return this.username.length > 2;
},
},
methods: {
onSubmit() {
this.$emit("input", this.username);
},
},
};
</script>
<style scoped>
.select-username {
width: 300px;
margin: 200px auto 0;
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<i class="icon" :class="{ connected: connected }"></i>
</template>
<script>
export default {
name: "StatusIcon",
props: {
connected: Boolean,
},
};
</script>
<style scoped>
.icon {
height: 8px;
width: 8px;
border-radius: 50%;
display: inline-block;
background-color: #e38968;
margin-right: 6px;
}
.icon.connected {
background-color: #86bb71;
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="user" @click="onClick" :class="{ selected: selected }">
<div class="description">
<div class="name">
{{ user.username }} {{ user.self ? " (yourself)" : "" }}
</div>
<div class="status">
<status-icon :connected="user.connected" />{{ status }}
</div>
</div>
<div v-if="user.hasNewMessages" class="new-messages">!</div>
</div>
</template>
<script>
import StatusIcon from "./StatusIcon";
export default {
name: "User",
components: { StatusIcon },
props: {
user: Object,
selected: Boolean,
},
methods: {
onClick() {
this.$emit("select");
},
},
computed: {
status() {
return this.user.connected ? "online" : "offline";
},
},
};
</script>
<style scoped>
.selected {
background-color: #1164a3;
}
.user {
padding: 10px;
}
.description {
display: inline-block;
}
.status {
color: #92959e;
}
.new-messages {
color: white;
background-color: red;
width: 20px;
border-radius: 5px;
text-align: center;
float: right;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,8 @@
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
new Vue({
render: (h) => h(App),
}).$mount("#app");

View File

@@ -0,0 +1,10 @@
import { io } from "socket.io-client";
const URL = "http://localhost:3000";
const socket = io(URL, { autoConnect: false });
socket.onAny((event, ...args) => {
console.log(event, args);
});
export default socket;

315
lib/broadcast-operator.ts Normal file
View File

@@ -0,0 +1,315 @@
import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { PacketType } from "socket.io-parser";
import type { Adapter } from "socket.io-adapter";
import type {
EventParams,
EventNames,
EventsMap,
TypedEventBroadcaster,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents> {
constructor(
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {}
) {}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room);
}
return new BroadcastOperator(
this.adapter,
rooms,
this.exceptRooms,
this.flags
);
}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.to(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room);
}
return new BroadcastOperator(
this.adapter,
this.rooms,
exceptRooms,
this.flags
);
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return a new BroadcastOperator instance
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
}
// set up packet object
const data = [ev, ...args];
const packet = {
type: PacketType.EVENT,
data: data,
};
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting");
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
});
return true;
}
/**
* Gets a list of clients.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
);
}
return this.adapter.sockets(this.rooms);
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return (socket as unknown) as RemoteSocket<EmitEvents>;
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails);
}
});
});
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
);
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
);
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
close
);
}
}
/**
* Format of the data when the Socket instance exists on another Socket.IO server
*/
interface SocketDetails {
id: SocketId;
handshake: Handshake;
rooms: Room[];
data: any;
}
/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents> {
public readonly id: SocketId;
public readonly handshake: Handshake;
public readonly rooms: Set<Room>;
public readonly data: any;
private readonly operator: BroadcastOperator<EmitEvents>;
constructor(adapter: Adapter, details: SocketDetails) {
this.id = details.id;
this.handshake = details.handshake;
this.rooms = new Set(details.rooms);
this.data = details.data;
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
}
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
return this.operator.emit(ev, ...args);
}
/**
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room);
}
/**
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room);
}
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close);
return this;
}
}

View File

@@ -1,21 +1,26 @@
import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
import debugModule = require("debug");
import url = require("url");
import type { IncomingMessage } from "http";
import type { Namespace, Server } from "./index";
import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket";
import type { SocketId } from "socket.io-adapter";
const debug = debugModule("socket.io:client");
export class Client {
export class Client<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap
> {
public readonly conn;
private readonly id: string;
private readonly server: Server;
private readonly server: Server<ListenEvents, EmitEvents>;
private readonly encoder: Encoder;
private readonly decoder: Decoder;
private sockets: Map<SocketId, Socket> = new Map();
private nsps: Map<string, Socket> = new Map();
private sockets: Map<SocketId, Socket<ListenEvents, EmitEvents>> = new Map();
private nsps: Map<string, Socket<ListenEvents, EmitEvents>> = new Map();
private connectTimeout?: NodeJS.Timeout;
/**
@@ -25,7 +30,7 @@ export class Client {
* @param conn
* @package
*/
constructor(server: Server, conn: Socket) {
constructor(server: Server<ListenEvents, EmitEvents>, conn: any) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
@@ -77,7 +82,7 @@ export class Client {
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}) {
private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
@@ -86,7 +91,7 @@ export class Client {
this.server._checkNamespace(
name,
auth,
(dynamicNspName: Namespace | false) => {
(dynamicNspName: Namespace<ListenEvents, EmitEvents> | false) => {
if (dynamicNspName) {
debug("dynamic namespace %s was created", dynamicNspName);
this.doConnect(name, auth);
@@ -112,7 +117,7 @@ export class Client {
*
* @private
*/
private doConnect(name: string, auth: object) {
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name);
const socket = nsp._add(this, auth, () => {
@@ -131,7 +136,7 @@ export class Client {
*
* @private
*/
_disconnect() {
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect();
}
@@ -144,7 +149,7 @@ export class Client {
*
* @private
*/
_remove(socket: Socket) {
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id);
@@ -159,7 +164,7 @@ export class Client {
*
* @private
*/
private close() {
private close(): void {
if ("open" === this.conn.readyState) {
debug("forcing transport close");
this.conn.close();
@@ -174,12 +179,13 @@ export class Client {
* @param {Object} opts
* @private
*/
_packet(packet, opts?) {
_packet(packet: Packet, opts?: any): void {
opts = opts || {};
const self = this;
// this writes to the actual connection
function writeToEngine(encodedPackets) {
function writeToEngine(encodedPackets: any) {
// TODO clarify this.
if (opts.volatile && !self.conn.transport.writable) return;
for (let i = 0; i < encodedPackets.length; i++) {
self.conn.write(encodedPackets[i], { compress: opts.compress });
@@ -205,7 +211,7 @@ export class Client {
*
* @private
*/
private ondata(data) {
private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data);
@@ -219,9 +225,14 @@ export class Client {
*
* @private
*/
private ondecoded(packet: Packet) {
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
this.connect(packet.nsp, packet.data);
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);
}
} else {
const socket = this.nsps.get(packet.nsp);
if (socket) {
@@ -240,7 +251,7 @@ export class Client {
* @param {Object} err object
* @private
*/
private onerror(err) {
private onerror(err): void {
for (const socket of this.sockets.values()) {
socket._onerror(err);
}
@@ -253,7 +264,7 @@ export class Client {
* @param reason
* @private
*/
private onclose(reason: string) {
private onclose(reason: string): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
@@ -272,7 +283,7 @@ export class Client {
* Cleans up event listeners.
* @private
*/
private destroy() {
private destroy(): void {
this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose);

View File

@@ -7,7 +7,11 @@ import path = require("path");
import engine = require("engine.io");
import { Client } from "./client";
import { EventEmitter } from "events";
import { ExtendedError, Namespace } from "./namespace";
import {
ExtendedError,
Namespace,
NamespaceReservedEventsMap,
} from "./namespace";
import { ParentNamespace } from "./parent-namespace";
import { Adapter, Room, SocketId } from "socket.io-adapter";
import * as parser from "socket.io-parser";
@@ -16,6 +20,13 @@ import debugModule from "debug";
import { Socket } from "./socket";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
import {
EventsMap,
DefaultEventsMap,
EventParams,
StrictEventEmitter,
} from "./typed-events";
const debug = debugModule("socket.io:server");
@@ -25,14 +36,14 @@ const dotMapRegex = /\.map/;
type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
query: object,
auth: { [key: string]: any },
fn: (err: Error | null, success: boolean) => void
) => void;
interface EngineOptions {
/**
* how many ms without a pong packet to consider the connection closed
* @default 5000
* @default 20000
*/
pingTimeout: number;
/**
@@ -82,10 +93,12 @@ interface EngineOptions {
httpCompression: boolean | object;
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs). Default value is ws.
* An alternative c++ addon is also available by installing uws module.
* conform to the ws interface (see ws module api docs).
* An alternative c++ addon is also available by installing eiows module.
*
* @default `require("ws").Server`
*/
wsEngine: string;
wsEngine: Function;
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
@@ -155,8 +168,15 @@ interface ServerOptions extends EngineAttachOptions {
connectTimeout: number;
}
export class Server extends EventEmitter {
public readonly sockets: Namespace;
export class Server<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<
{},
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
> {
public readonly sockets: Namespace<ListenEvents, EmitEvents>;
/** @private */
readonly _parser: typeof parser;
@@ -166,8 +186,11 @@ export class Server extends EventEmitter {
/**
* @private
*/
_nsps: Map<string, Namespace> = new Map();
private parentNsps: Map<ParentNspNameMatchFn, ParentNamespace> = new Map();
_nsps: Map<string, Namespace<ListenEvents, EmitEvents>> = new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents>
> = new Map();
private _adapter?: typeof Adapter;
private _serveClient: boolean;
private opts: Partial<EngineOptions>;
@@ -191,6 +214,10 @@ export class Server extends EventEmitter {
*/
constructor(opts?: Partial<ServerOptions>);
constructor(srv?: http.Server | number, opts?: Partial<ServerOptions>);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
opts?: Partial<ServerOptions>
);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
opts: Partial<ServerOptions> = {}
@@ -222,9 +249,10 @@ export class Server extends EventEmitter {
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): Server;
public serveClient(v: boolean): this;
public serveClient(): boolean;
public serveClient(v?: boolean): Server | boolean {
public serveClient(v?: boolean): this | boolean;
public serveClient(v?: boolean): this | boolean {
if (!arguments.length) return this._serveClient;
this._serveClient = v!;
return this;
@@ -234,16 +262,16 @@ export class Server extends EventEmitter {
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param name - name of incoming namespace
* @param {Object} auth - the auth parameters
* @param auth - the auth parameters
* @param fn - callback
*
* @private
*/
_checkNamespace(
name: string,
auth: object,
fn: (nsp: Namespace | false) => void
) {
auth: { [key: string]: any },
fn: (nsp: Namespace<ListenEvents, EmitEvents> | false) => void
): void {
if (this.parentNsps.size === 0) return fn(false);
const keysIterator = this.parentNsps.keys();
@@ -272,9 +300,10 @@ export class Server extends EventEmitter {
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): Server;
public path(v: string): this;
public path(): string;
public path(v?: string): Server | string {
public path(v?: string): this | string;
public path(v?: string): this | string {
if (!arguments.length) return this._path;
this._path = v!.replace(/\/$/, "");
@@ -293,9 +322,10 @@ export class Server extends EventEmitter {
* @param v
* @public
*/
public connectTimeout(v: number): Server;
public connectTimeout(v: number): this;
public connectTimeout(): number;
public connectTimeout(v?: number): Server | number {
public connectTimeout(v?: number): this | number;
public connectTimeout(v?: number): this | number {
if (v === undefined) return this._connectTimeout;
this._connectTimeout = v;
return this;
@@ -309,8 +339,9 @@ export class Server extends EventEmitter {
* @public
*/
public adapter(): typeof Adapter | undefined;
public adapter(v: typeof Adapter): Server;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | Server {
public adapter(v: typeof Adapter): this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this {
if (!arguments.length) return this._adapter;
this._adapter = v;
for (const nsp of this._nsps.values()) {
@@ -330,7 +361,7 @@ export class Server extends EventEmitter {
public listen(
srv: http.Server | number,
opts: Partial<ServerOptions> = {}
): Server {
): this {
return this.attach(srv, opts);
}
@@ -345,7 +376,7 @@ export class Server extends EventEmitter {
public attach(
srv: http.Server | number,
opts: Partial<ServerOptions> = {}
): Server {
): this {
if ("function" == typeof srv) {
const msg =
"You are trying to attach socket.io to an express " +
@@ -385,7 +416,10 @@ export class Server extends EventEmitter {
* @param opts - options passed to engine.io
* @private
*/
private initEngine(srv: http.Server, opts: Partial<EngineAttachOptions>) {
private initEngine(
srv: http.Server,
opts: Partial<EngineAttachOptions>
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts);
this.eio = engine.attach(srv, opts);
@@ -406,7 +440,7 @@ export class Server extends EventEmitter {
* @param srv http server
* @private
*/
private attachServe(srv: http.Server) {
private attachServe(srv: http.Server): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
@@ -429,7 +463,7 @@ export class Server extends EventEmitter {
* @param res
* @private
*/
private serve(req: http.IncomingMessage, res: http.ServerResponse) {
private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
const filename = req.url!.replace(this._path, "");
const isMap = dotMapRegex.test(filename);
const type = isMap ? "map" : "source";
@@ -474,7 +508,7 @@ export class Server extends EventEmitter {
filename: string,
req: http.IncomingMessage,
res: http.ServerResponse
) {
): void {
const readStream = createReadStream(
path.join(__dirname, "../client-dist/", filename)
);
@@ -513,7 +547,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public bind(engine): Server {
public bind(engine): this {
this.engine = engine;
this.engine.on("connection", this.onconnection.bind(this));
return this;
@@ -526,7 +560,7 @@ export class Server extends EventEmitter {
* @return self
* @private
*/
private onconnection(conn): Server {
private onconnection(conn): this {
debug("incoming connection with id %s", conn.id);
const client = new Client(this, conn);
if (conn.protocol === 3) {
@@ -540,13 +574,13 @@ export class Server extends EventEmitter {
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @param [fn] optional, nsp `connection` ev handler
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket) => void
) {
fn?: (socket: Socket<ListenEvents, EmitEvents>) => void
): Namespace<ListenEvents, EmitEvents> {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this);
debug("initializing parent namespace %s", parentNsp.name);
@@ -604,8 +638,11 @@ export class Server extends EventEmitter {
* @public
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Server {
fn: (
socket: Socket<ListenEvents, EmitEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this.sockets.use(fn);
return this;
}
@@ -613,24 +650,34 @@ export class Server extends EventEmitter {
/**
* Targets a room when emitting.
*
* @param name
* @param room
* @return self
* @public
*/
public to(name: Room): Server {
this.sockets.to(name);
return this;
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.to(room);
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.in(room);
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
*/
public in(name: Room): Server {
this.sockets.in(name);
public except(name: Room | Room[]): Server<ListenEvents, EmitEvents> {
this.sockets.except(name);
return this;
}
@@ -640,7 +687,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Server {
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
return this;
}
@@ -651,7 +698,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Server {
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
return this;
}
@@ -672,9 +719,8 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public compress(compress: boolean): Server {
this.sockets.compress(compress);
return this;
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return this.sockets.compress(compress);
}
/**
@@ -685,9 +731,8 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get volatile(): Server {
this.sockets.volatile;
return this;
public get volatile(): BroadcastOperator<EmitEvents> {
return this.sockets.volatile;
}
/**
@@ -696,9 +741,47 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get local(): Server {
this.sockets.local;
return this;
public get local(): BroadcastOperator<EmitEvents> {
return this.sockets.local;
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.sockets.fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return this.sockets.socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return this.sockets.socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return this.sockets.disconnectSockets(close);
}
}
@@ -721,4 +804,4 @@ emitterMethods.forEach(function (fn) {
module.exports = (srv?, opts?) => new Server(srv, opts);
module.exports.Server = Server;
export { Socket, ServerOptions, Namespace };
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };

View File

@@ -1,10 +1,16 @@
import { Socket, RESERVED_EVENTS } from "./socket";
import { Socket } from "./socket";
import type { Server } from "./index";
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events";
import type { Client } from "./client";
import { EventEmitter } from "events";
import { PacketType } from "socket.io-parser";
import debugModule from "debug";
import type { Adapter, Room, SocketId } from "socket.io-adapter";
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
const debug = debugModule("socket.io:namespace");
@@ -12,26 +18,41 @@ export interface ExtendedError extends Error {
data?: any;
}
export class Namespace extends EventEmitter {
export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap
> {
connect: (socket: Socket<ListenEvents, EmitEvents>) => void;
connection: (socket: Socket<ListenEvents, EmitEvents>) => void;
}
export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<
{},
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
> {
public readonly name: string;
public readonly sockets: Map<SocketId, Socket> = new Map();
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents>
> = new Map();
public adapter: Adapter;
/** @private */
readonly server: Server;
readonly server: Server<ListenEvents, EmitEvents>;
/** @private */
_fns: Array<
(socket: Socket, next: (err?: ExtendedError) => void) => void
(
socket: Socket<ListenEvents, EmitEvents>,
next: (err?: ExtendedError) => void
) => void
> = [];
/** @private */
_rooms: Set<Room> = new Set();
/** @private */
_flags: any = {};
/** @private */
_ids: number = 0;
@@ -41,7 +62,7 @@ export class Namespace extends EventEmitter {
* @param server instance
* @param name
*/
constructor(server: Server, name: string) {
constructor(server: Server<ListenEvents, EmitEvents>, name: string) {
super();
this.server = server;
this.name = name;
@@ -66,8 +87,11 @@ export class Namespace extends EventEmitter {
* @public
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Namespace {
fn: (
socket: Socket<ListenEvents, EmitEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this._fns.push(fn);
return this;
}
@@ -79,7 +103,10 @@ export class Namespace extends EventEmitter {
* @param fn - last fn call in the middleware
* @private
*/
private run(socket: Socket, fn: (err: ExtendedError | null) => void) {
private run(
socket: Socket<ListenEvents, EmitEvents>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0);
if (!fns.length) return fn(null);
@@ -102,25 +129,34 @@ export class Namespace extends EventEmitter {
/**
* Targets a room when emitting.
*
* @param name
* @param room
* @return self
* @public
*/
public to(name: Room): Namespace {
this._rooms.add(name);
return this;
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).to(room);
}
/**
* Targets a room when emitting.
*
* @param name
* @param room
* @return self
* @public
*/
public in(name: Room): Namespace {
this._rooms.add(name);
return this;
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).in(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).except(room);
}
/**
@@ -129,7 +165,11 @@ export class Namespace extends EventEmitter {
* @return {Socket}
* @private
*/
_add(client: Client, query, fn?: () => void): Socket {
_add(
client: Client<ListenEvents, EmitEvents>,
query,
fn?: () => void
): Socket<ListenEvents, EmitEvents> {
debug("adding socket to nsp %s", this.name);
const socket = new Socket(this, client, query);
this.run(socket, (err) => {
@@ -157,8 +197,8 @@ export class Namespace extends EventEmitter {
if (fn) fn();
// fire user-set events
super.emit("connect", socket);
super.emit("connection", socket);
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
} else {
debug("next called after client was closed - ignoring socket");
}
@@ -172,7 +212,7 @@ export class Namespace extends EventEmitter {
*
* @private
*/
_remove(socket: Socket): void {
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id);
} else {
@@ -186,34 +226,11 @@ export class Namespace extends EventEmitter {
* @return Always true
* @public
*/
public emit(ev: string | Symbol, ...args: any[]): true {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
}
// set up packet object
args.unshift(ev);
const packet = {
type: PacketType.EVENT,
data: args,
};
if ("function" == typeof args[args.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting");
}
const rooms = new Set(this._rooms);
const flags = Object.assign({}, this._flags);
// reset flags
this._rooms.clear();
this._flags = {};
this.adapter.broadcast(packet, {
rooms: rooms,
flags: flags,
});
return true;
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args);
}
/**
@@ -222,7 +239,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Namespace {
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
return this;
}
@@ -233,7 +250,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Namespace {
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
return this;
}
@@ -245,14 +262,7 @@ export class Namespace extends EventEmitter {
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
);
}
const rooms = new Set(this._rooms);
this._rooms.clear();
return this.adapter.sockets(rooms);
return new BroadcastOperator(this.adapter).allSockets();
}
/**
@@ -262,9 +272,8 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public compress(compress: boolean): Namespace {
this._flags.compress = compress;
return this;
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).compress(compress);
}
/**
@@ -275,9 +284,8 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public get volatile(): Namespace {
this._flags.volatile = true;
return this;
public get volatile(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).volatile;
}
/**
@@ -286,8 +294,46 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public get local(): Namespace {
this._flags.local = true;
return this;
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local;
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close);
}
}

View File

@@ -1,11 +1,21 @@
import { Namespace } from "./namespace";
import type { Server } from "./index";
import type {
EventParams,
EventNames,
EventsMap,
DefaultEventsMap,
} from "./typed-events";
import type { BroadcastOptions } from "socket.io-adapter";
export class ParentNamespace extends Namespace {
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends Namespace<ListenEvents, EmitEvents> {
private static count: number = 0;
private children: Set<Namespace> = new Set();
private children: Set<Namespace<ListenEvents, EmitEvents>> = new Set();
constructor(server: Server) {
constructor(server: Server<ListenEvents, EmitEvents>) {
super(server, "/_" + ParentNamespace.count++);
}
@@ -13,29 +23,34 @@ export class ParentNamespace extends Namespace {
* @private
*/
_initAdapter(): void {
/* no-op */
const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts);
});
};
// @ts-ignore FIXME is there a way to declare an inner class in TypeScript?
this.adapter = { broadcast };
}
public emit(ev: string | Symbol, ...args: [...any]): true {
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): true {
this.children.forEach((nsp) => {
nsp._rooms = this._rooms;
nsp._flags = this._flags;
nsp.emit(ev, ...args);
});
this._rooms.clear();
this._flags = {};
return true;
}
createChild(name: string): Namespace {
createChild(name: string): Namespace<ListenEvents, EmitEvents> {
const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) =>
namespace.on("connect", listener as (...args: any[]) => void)
namespace.on("connect", listener)
);
this.listeners("connection").forEach((listener) =>
namespace.on("connection", listener as (...args: any[]) => void)
namespace.on("connection", listener)
);
this.children.add(namespace);
this.server._nsps.set(name, namespace);

View File

@@ -1,11 +1,17 @@
import { EventEmitter } from "events";
import { PacketType } from "socket.io-parser";
import { Packet, PacketType } from "socket.io-parser";
import url = require("url");
import debugModule from "debug";
import type { Server } from "./index";
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events";
import type { Client } from "./client";
import type { Namespace } from "./namespace";
import type { IncomingMessage } from "http";
import type { Namespace, NamespaceReservedEventsMap } from "./namespace";
import type { IncomingMessage, IncomingHttpHeaders } from "http";
import type {
Adapter,
BroadcastFlags,
@@ -13,18 +19,44 @@ import type {
SocketId,
} from "socket.io-adapter";
import base64id from "base64id";
import type { ParsedUrlQuery } from "querystring";
import { BroadcastOperator } from "./broadcast-operator";
const debug = debugModule("socket.io:socket");
export const RESERVED_EVENTS = new Set(<const>[
type ClientReservedEvents = "connect_error";
export interface SocketReservedEventsMap {
disconnect: (reason: string) => void;
disconnecting: (reason: string) => void;
error: (err: Error) => void;
}
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
export interface EventEmitterReservedEventsMap {
newListener: (
eventName: string | Symbol,
listener: (...args: any[]) => void
) => void;
removeListener: (
eventName: string | Symbol,
listener: (...args: any[]) => void
) => void;
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never>
| keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap
>(<const>[
"connect",
"connect_error",
"disconnect",
"disconnecting",
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
"newListener",
"removeListener",
]) as ReadonlySet<string | Symbol>;
]);
/**
* The handshake details
@@ -33,7 +65,7 @@ export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: object;
headers: IncomingHttpHeaders;
/**
* The date of creation (as string)
@@ -68,29 +100,39 @@ export interface Handshake {
/**
* The query object
*/
query: object;
query: ParsedUrlQuery;
/**
* The auth object
*/
auth: object;
auth: { [key: string]: any };
}
export class Socket extends EventEmitter {
export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<
ListenEvents,
EmitEvents,
SocketReservedEventsMap
> {
public readonly id: SocketId;
public readonly handshake: Handshake;
/**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
*/
public data: any = {};
public connected: boolean;
public disconnected: boolean;
private readonly server: Server;
private readonly server: Server<ListenEvents, EmitEvents>;
private readonly adapter: Adapter;
private acks: Map<number, () => void> = new Map();
private fns: Array<
(event: Array<any>, next: (err: Error) => void) => void
(event: Array<any>, next: (err?: Error) => void) => void
> = [];
private flags: BroadcastFlags = {};
private _rooms: Set<Room> = new Set();
private _anyListeners?: Array<(...args: any[]) => void>;
/**
@@ -101,7 +143,11 @@ export class Socket extends EventEmitter {
* @param {Object} auth
* @package
*/
constructor(readonly nsp: Namespace, readonly client: Client, auth: object) {
constructor(
readonly nsp: Namespace<ListenEvents, EmitEvents>,
readonly client: Client<ListenEvents, EmitEvents>,
auth: object
) {
super();
this.server = nsp.server;
this.adapter = this.nsp.adapter;
@@ -142,69 +188,65 @@ export class Socket extends EventEmitter {
* @return Always returns `true`.
* @public
*/
public emit(ev: string, ...args: any[]): boolean {
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
}
args.unshift(ev);
const data: any[] = [ev, ...args];
const packet: any = {
type: PacketType.EVENT,
data: args,
data: data,
};
// access last argument to see if it's an ACK callback
if (typeof args[args.length - 1] === "function") {
if (this._rooms.size || this.flags.broadcast) {
throw new Error("Callbacks are not supported when broadcasting");
}
if (typeof data[data.length - 1] === "function") {
debug("emitting packet with ack id %d", this.nsp._ids);
this.acks.set(this.nsp._ids, args.pop());
this.acks.set(this.nsp._ids, data.pop());
packet.id = this.nsp._ids++;
}
const rooms = new Set(this._rooms);
const flags = Object.assign({}, this.flags);
// reset flags
this._rooms.clear();
this.flags = {};
if (rooms.size || flags.broadcast) {
this.adapter.broadcast(packet, {
except: new Set([this.id]),
rooms: rooms,
flags: flags,
});
} else {
// dispatch packet
this.packet(packet, flags);
}
this.packet(packet, flags);
return true;
}
/**
* Targets a room when broadcasting.
*
* @param name
* @param room
* @return self
* @public
*/
public to(name: Room): Socket {
this._rooms.add(name);
return this;
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().to(room);
}
/**
* Targets a room when broadcasting.
*
* @param name
* @param room
* @return self
* @public
*/
public in(name: Room): Socket {
this._rooms.add(name);
return this;
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().in(room);
}
/**
* Excludes a room when broadcasting.
*
* @param room
* @return self
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().except(room);
}
/**
@@ -213,7 +255,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Socket {
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
return this;
}
@@ -224,7 +266,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Socket {
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
return this;
}
@@ -236,10 +278,13 @@ export class Socket extends EventEmitter {
* @param {Object} opts - options
* @private
*/
private packet(packet, opts: any = {}) {
private packet(
packet: Omit<Packet, "nsp"> & Partial<Pick<Packet, "nsp">>,
opts: any = {}
): void {
packet.nsp = this.nsp.name;
opts.compress = false !== opts.compress;
this.client._packet(packet, opts);
this.client._packet(packet as Packet, opts);
}
/**
@@ -304,7 +349,7 @@ export class Socket extends EventEmitter {
* @param {Object} packet
* @private
*/
_onpacket(packet) {
_onpacket(packet: Packet): void {
debug("got packet %j", packet);
switch (packet.type) {
case PacketType.EVENT:
@@ -335,10 +380,10 @@ export class Socket extends EventEmitter {
/**
* Called upon event packet.
*
* @param {Object} packet - packet object
* @param {Packet} packet - packet object
* @private
*/
private onevent(packet): void {
private onevent(packet: Packet): void {
const args = packet.data || [];
debug("emitting event %j", args);
@@ -362,7 +407,7 @@ export class Socket extends EventEmitter {
* @param {Number} id - packet id
* @private
*/
private ack(id: number) {
private ack(id: number): () => void {
const self = this;
let sent = false;
return function () {
@@ -386,12 +431,12 @@ export class Socket extends EventEmitter {
*
* @private
*/
private onack(packet): void {
const ack = this.acks.get(packet.id);
private onack(packet: Packet): void {
const ack = this.acks.get(packet.id!);
if ("function" == typeof ack) {
debug("calling ack %s with %j", packet.id, packet.data);
ack.apply(this, packet.data);
this.acks.delete(packet.id);
this.acks.delete(packet.id!);
} else {
debug("bad ack %s", packet.id);
}
@@ -412,9 +457,9 @@ export class Socket extends EventEmitter {
*
* @private
*/
_onerror(err): void {
_onerror(err: Error): void {
if (this.listeners("error").length) {
super.emit("error", err);
this.emitReserved("error", err);
} else {
console.error("Missing error handler on `socket`.");
console.error(err.stack);
@@ -429,16 +474,16 @@ export class Socket extends EventEmitter {
*
* @private
*/
_onclose(reason: string): Socket | undefined {
_onclose(reason: string): this | undefined {
if (!this.connected) return this;
debug("closing socket - reason %s", reason);
super.emit("disconnecting", reason);
this.emitReserved("disconnecting", reason);
this.leaveAll();
this.nsp._remove(this);
this.client._remove(this);
this.connected = false;
this.disconnected = true;
super.emit("disconnect", reason);
this.emitReserved("disconnect", reason);
return;
}
@@ -449,7 +494,7 @@ export class Socket extends EventEmitter {
*
* @private
*/
_error(err) {
_error(err): void {
this.packet({ type: PacketType.CONNECT_ERROR, data: err });
}
@@ -461,7 +506,7 @@ export class Socket extends EventEmitter {
*
* @public
*/
public disconnect(close = false): Socket {
public disconnect(close = false): this {
if (!this.connected) return this;
if (close) {
this.client._disconnect();
@@ -479,7 +524,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public compress(compress: boolean): Socket {
public compress(compress: boolean): this {
this.flags.compress = compress;
return this;
}
@@ -492,7 +537,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get volatile(): Socket {
public get volatile(): this {
this.flags.volatile = true;
return this;
}
@@ -504,9 +549,8 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get broadcast(): Socket {
this.flags.broadcast = true;
return this;
public get broadcast(): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator();
}
/**
@@ -515,9 +559,8 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get local(): Socket {
this.flags.local = true;
return this;
public get local(): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().local;
}
/**
@@ -526,14 +569,18 @@ export class Socket extends EventEmitter {
* @param {Array} event - event that will get emitted
* @private
*/
private dispatch(event): void {
private dispatch(event: [eventName: string, ...args: any[]]): void {
debug("dispatching an event %j", event);
this.run(event, (err) => {
process.nextTick(() => {
if (err) {
return this._onerror(err);
}
super.emit.apply(this, event);
if (this.connected) {
super.emitUntyped.apply(this, event);
} else {
debug("ignore packet received after disconnection");
}
});
});
}
@@ -546,8 +593,8 @@ export class Socket extends EventEmitter {
* @public
*/
public use(
fn: (event: Array<any>, next: (err: Error) => void) => void
): Socket {
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
this.fns.push(fn);
return this;
}
@@ -559,11 +606,14 @@ export class Socket extends EventEmitter {
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(event: Array<any>, fn: (err: Error | null) => void) {
private run(
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
const fns = this.fns.slice(0);
if (!fns.length) return fn(null);
function run(i) {
function run(i: number) {
fns[i](event, function (err) {
// upon error, short-circuit
if (err) return fn(err);
@@ -611,7 +661,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): Socket {
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
this._anyListeners.push(listener);
return this;
@@ -624,7 +674,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): Socket {
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
this._anyListeners.unshift(listener);
return this;
@@ -636,7 +686,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): Socket {
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
return this;
}
@@ -663,4 +713,15 @@ export class Socket extends EventEmitter {
public listenersAny() {
return this._anyListeners || [];
}
private newBroadcastOperator(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags);
this.flags = {};
return new BroadcastOperator(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),
flags
);
}
}

167
lib/typed-events.ts Normal file
View File

@@ -0,0 +1,167 @@
import { EventEmitter } from "events";
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any;
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>;
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never;
/**
* Interface for classes that aren't `EventEmitter`s, but still expose a
* strictly typed `emit` method.
*/
export interface TypedEventBroadcaster<EmitEvents extends EventsMap> {
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean;
}
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export abstract class StrictEventEmitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
>
extends EventEmitter
implements TypedEventBroadcaster<EmitEvents> {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.on(ev, listener);
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.once(ev, listener);
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return super.emit(ev, ...args);
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): boolean {
return super.emit(ev, ...args);
}
/**
* Emits an event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can get around the strict typing. This is useful for
* calling `emit.apply`, which can be called as `emitUntyped.apply`.
*
* @param ev Event name
* @param args Arguments to emit along with the event
*/
protected emitUntyped(ev: string, ...args: any[]): boolean {
return super.emit(ev, ...args);
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
return super.listeners(event) as ReservedOrUserListener<
ReservedEvents,
ListenEvents,
Ev
>[];
}
}

1356
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "3.1.0",
"version": "4.0.0",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -37,7 +37,9 @@
},
"scripts": {
"compile": "rimraf ./dist && tsc",
"test": "npm run format:check && npm run compile && nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/socket.io.ts",
"test": "npm run format:check && npm run compile && npm run test:types && npm run test:unit",
"test:types": "tsd",
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/socket.io.ts",
"format:check": "prettier --check \"lib/**/*.ts\" \"test/**/*.ts\"",
"format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\"",
"prepack": "npm run compile"
@@ -45,12 +47,12 @@
"dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": "^14.14.10",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~4.1.0",
"socket.io-adapter": "~2.1.0",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
},
"devDependencies": {
@@ -63,11 +65,12 @@
"nyc": "^15.1.0",
"prettier": "^2.2.0",
"rimraf": "^3.0.2",
"socket.io-client": "3.1.0",
"socket.io-client": "4.0.0",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^6.1.0",
"supertest": "^6.0.1",
"ts-node": "^9.0.0",
"tsd": "^0.14.0",
"typescript": "^4.1.2"
},
"contributors": [
@@ -90,5 +93,8 @@
],
"engines": {
"node": ">=10.0.0"
},
"tsd": {
"directory": "test"
}
}

200
test/socket.io.test-d.ts Normal file
View File

@@ -0,0 +1,200 @@
"use strict";
import { Server, Socket } from "..";
import type { DefaultEventsMap } from "../lib/typed-events";
import { createServer } from "http";
import { expectError, expectType } from "tsd";
// This file is run by tsd, not mocha.
describe("server", () => {
describe("no event map", () => {
describe("on", () => {
it("infers correct types for listener parameters of reserved events", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(s);
s.on("disconnect", (reason) => {
expectType<string>(reason);
});
s.on("disconnecting", (reason) => {
expectType<string>(reason);
});
});
sio.on("connect", (s) => {
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(s);
});
done();
});
});
it("infers 'any' for listener parameters of other events", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
sio.on("connection", (s) => {
s.on("random", (a, b, c) => {
expectType<any>(a);
expectType<any>(b);
expectType<any>(c);
done();
});
s.emit("random", 1, "2", [3]);
});
});
});
});
describe("emit", () => {
it("accepts any parameters", () => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
sio.on("connection", (s) => {
s.emit("random", 1, "2", [3]);
s.emit("no parameters");
});
});
});
});
});
describe("single event map", () => {
interface BidirectionalEvents {
random: (a: number, b: string, c: number[]) => void;
}
describe("on", () => {
it("infers correct types for listener parameters", (done) => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
expectType<Server<BidirectionalEvents, BidirectionalEvents>>(sio);
srv.listen(() => {
sio.on("connection", (s) => {
expectType<Socket<BidirectionalEvents, BidirectionalEvents>>(s);
s.on("random", (a, b, c) => {
expectType<number>(a);
expectType<string>(b);
expectType<number[]>(c);
done();
});
});
});
});
it("does not accept arguments of wrong types", (done) => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
expectError(sio.on("random", (a, b, c) => {}));
srv.listen(() => {
expectError(sio.on("wrong name", (s) => {}));
sio.on("connection", (s) => {
s.on("random", (a, b, c) => {});
expectError(s.on("random"));
expectError(s.on("random", (a, b, c, d) => {}));
expectError(s.on(2, 3));
});
});
});
});
describe("emit", () => {
it("accepts arguments of the correct types", () => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
s.emit("random", 1, "2", [3]);
});
});
});
it("does not accept arguments of the wrong types", () => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectError(s.emit("noParameter", 2));
expectError(s.emit("oneParameter"));
expectError(s.emit("random"));
expectError(s.emit("oneParameter", 2, 3));
expectError(s.emit("random", (a, b, c) => {}));
expectError(s.emit("wrong name", () => {}));
expectError(s.emit("complicated name with spaces", 2));
});
});
});
});
});
describe("listen and emit event maps", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
}
interface ServerToClientEvents {
helloFromServer: (message: string, x: number) => void;
}
describe("on", () => {
it("infers correct types for listener parameters", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
expectType<Server<ClientToServerEvents, ServerToClientEvents>>(sio);
srv.listen(() => {
sio.on("connection", (s) => {
expectType<Socket<ClientToServerEvents, ServerToClientEvents>>(s);
s.on("helloFromClient", (message) => {
expectType<string>(message);
done();
});
});
});
});
it("does not accept emit events", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectError(
s.on("helloFromServer", (message, number) => {
done();
})
);
});
});
});
});
describe("emit", () => {
it("accepts arguments of the correct types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
s.emit("helloFromServer", "hi", 10);
done();
});
});
});
it("does not accept arguments of wrong types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.on("connection", (s) => {
expectError(s.emit("helloFromClient", "hi"));
expectError(s.emit("helloFromServer", "hi", 10, "10"));
expectError(s.emit("helloFromServer", "hi", "10"));
expectError(s.emit("helloFromServer", 0, 0));
expectError(s.emit("wrong name", 10));
expectError(s.emit("wrong name"));
done();
});
});
});
});
});
});

View File

@@ -9,13 +9,14 @@ import request from "supertest";
import expect from "expect.js";
import type { AddressInfo } from "net";
import * as io_v2 from "socket.io-client-v2";
const ioc = require("socket.io-client");
import type { SocketId } from "socket.io-adapter";
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import "./support/util";
import "./utility-methods";
// Creates a socket.io client for the given server
function client(srv, nsp?: string | object, opts?: object) {
function client(srv, nsp?: string | object, opts?: object): ClientSocket {
if ("object" == typeof nsp) {
opts = nsp;
nsp = undefined;
@@ -227,7 +228,7 @@ describe("socket.io", () => {
});
request
.options("http://localhost:54013/socket.io/default/")
.query({ transport: "polling" })
.query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54023")
.end((err, res) => {
expect(res.status).to.be(204);
@@ -255,7 +256,7 @@ describe("socket.io", () => {
});
request
.get("http://localhost:54014/socket.io/default/")
.query({ transport: "polling" })
.query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54024")
.end((err, res) => {
expect(res.status).to.be(200);
@@ -275,7 +276,7 @@ describe("socket.io", () => {
request
.get("http://localhost:54022/socket.io/default/")
.query({ transport: "polling" })
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
expect(res.status).to.be(200);
done();
@@ -289,7 +290,7 @@ describe("socket.io", () => {
request
.get("http://localhost:54023/socket.io/default/")
.set("origin", "http://foo.example")
.query({ transport: "polling" })
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
expect(res.status).to.be(403);
done();
@@ -332,7 +333,9 @@ describe("socket.io", () => {
const net = require("net");
const server = net.createServer();
const clientSocket = ioc("ws://0.0.0.0:" + PORT, { reconnection: false });
const clientSocket = ioc("ws://0.0.0.0:" + PORT, {
reconnection: false,
});
clientSocket.on("disconnect", () => {
expect(Object.keys(sio._nsps["/"].sockets).length).to.equal(0);
@@ -388,10 +391,22 @@ describe("socket.io", () => {
expect(sio.write).to.be.a("function");
expect(sio.allSockets).to.be.a("function");
expect(sio.compress).to.be.a("function");
expect(sio.volatile).to.be(sio);
expect(sio.local).to.be(sio);
expect(sio.sockets._flags).to.eql({ volatile: true, local: true });
delete sio.sockets._flags;
});
it("should return an immutable broadcast operator", () => {
const sio = new Server();
const operator = sio.local.to(["room1", "room2"]).except("room3");
operator.compress(true).emit("hello");
operator.volatile.emit("hello");
operator.to("room4").emit("hello");
operator.except("room5").emit("hello");
sio.to("room6").emit("hello");
// @ts-ignore
expect(operator.rooms).to.contain("room1", "room2");
// @ts-ignore
expect(operator.exceptRooms).to.contain("room3");
// @ts-ignore
expect(operator.flags).to.eql({ local: true });
});
it("should automatically connect", (done) => {
@@ -592,7 +607,7 @@ describe("socket.io", () => {
const srv = createServer();
const sio = new Server(srv);
const chatSids: string[] = [];
let otherSid = null;
let otherSid: SocketId | null = null;
srv.listen(() => {
const c1 = client(srv, "/chat");
const c2 = client(srv, "/chat", { forceNew: true });
@@ -619,9 +634,9 @@ describe("socket.io", () => {
it("should find all clients in a namespace room", (done) => {
const srv = createServer();
const sio = new Server(srv);
let chatFooSid = null;
let chatBarSid = null;
let otherSid = null;
let chatFooSid: SocketId | null = null;
let chatBarSid: SocketId | null = null;
let otherSid: SocketId | null = null;
srv.listen(() => {
const c1 = client(srv, "/chat");
const c2 = client(srv, "/chat", { forceNew: true });
@@ -658,9 +673,9 @@ describe("socket.io", () => {
it("should find all clients across namespace rooms", (done) => {
const srv = createServer();
const sio = new Server(srv);
let chatFooSid = null;
let chatBarSid = null;
let otherSid = null;
let chatFooSid: SocketId | null = null;
let chatBarSid: SocketId | null = null;
let otherSid: SocketId | null = null;
srv.listen(() => {
const c1 = client(srv, "/chat");
const c2 = client(srv, "/chat", { forceNew: true });
@@ -823,6 +838,57 @@ describe("socket.io", () => {
});
});
it("should exclude a specific socket when emitting", (done) => {
const srv = createServer();
const sio = new Server(srv);
const nsp = sio.of("/nsp");
srv.listen(() => {
const socket1 = client(srv, "/nsp");
const socket2 = client(srv, "/nsp");
socket2.on("a", () => {
done(new Error("not"));
});
socket1.on("a", () => {
done();
});
socket2.on("connect", () => {
nsp.except(socket2.id).emit("a");
});
});
});
it("should exclude a specific room when emitting", (done) => {
const srv = createServer();
const sio = new Server(srv);
const nsp = sio.of("/nsp");
srv.listen(() => {
const socket1 = client(srv, "/nsp");
const socket2 = client(srv, "/nsp");
socket1.on("a", () => {
done();
});
socket2.on("a", () => {
done(new Error("not"));
});
nsp.on("connection", (socket) => {
socket.on("broadcast", () => {
socket.join("room1");
nsp.except("room1").emit("a");
});
});
socket2.emit("broadcast");
});
});
describe("dynamic namespaces", () => {
it("should allow connections to dynamic namespaces with a regex", (done) => {
const srv = createServer();
@@ -889,7 +955,7 @@ describe("socket.io", () => {
srv.listen(() => {
const clientSocket = client(srv, { reconnection: false });
clientSocket.on("connect", function init() {
clientSocket.removeListener("connect", init);
clientSocket.off("connect", init);
clientSocket.io.engine.close();
clientSocket.connect();
@@ -978,7 +1044,7 @@ describe("socket.io", () => {
done();
});
});
s.client.ondata(null);
(s as any).client.ondata(null);
});
});
});
@@ -1787,6 +1853,33 @@ describe("socket.io", () => {
});
});
it("should ignore a packet received after disconnection", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv);
const success = () => {
clientSocket.close();
sio.close();
done();
};
sio.on("connection", (socket) => {
socket.on("test", () => {
done(new Error("should not happen"));
});
socket.on("disconnect", success);
});
clientSocket.on("connect", () => {
clientSocket.emit("test", Buffer.alloc(10));
clientSocket.disconnect();
});
});
});
describe("onAny", () => {
it("should call listener", (done) => {
const srv = createServer();
@@ -2139,7 +2232,7 @@ describe("socket.io", () => {
expect(s.rooms).to.contain(s.id, "a", "b", "c");
s.leave("b");
expect(s.rooms).to.contain(s.id, "a", "c");
s.leaveAll();
(s as any).leaveAll();
expect(s.rooms.size).to.eql(0);
done();
});
@@ -2175,7 +2268,7 @@ describe("socket.io", () => {
expect(s.rooms).to.contain(s.id, "a", "b");
s.leave("unknown");
expect(s.rooms).to.contain(s.id, "a", "b");
s.leaveAll();
(s as any).leaveAll();
expect(s.rooms.size).to.eql(0);
done();
});
@@ -2195,6 +2288,106 @@ describe("socket.io", () => {
});
});
});
it("should exclude specific sockets when broadcasting", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const socket1 = client(srv, { multiplex: false });
const socket2 = client(srv, { multiplex: false });
const socket3 = client(srv, { multiplex: false });
socket2.on("a", () => {
done(new Error("not"));
});
socket3.on("a", () => {
done(new Error("not"));
});
socket1.on("a", () => {
done();
});
sio.on("connection", (socket) => {
socket.on("exclude", (id) => {
socket.broadcast.except(id).emit("a");
});
});
socket2.on("connect", () => {
socket3.emit("exclude", socket2.id);
});
});
});
it("should exclude a specific room when broadcasting", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const socket1 = client(srv, { multiplex: false });
const socket2 = client(srv, { multiplex: false });
const socket3 = client(srv, { multiplex: false });
socket2.on("a", () => {
done(new Error("not"));
});
socket3.on("a", () => {
done(new Error("not"));
});
socket1.on("a", () => {
done();
});
sio.on("connection", (socket) => {
socket.on("join", (room, cb) => {
socket.join(room);
cb();
});
socket.on("broadcast", () => {
socket.broadcast.except("room1").emit("a");
});
});
socket2.emit("join", "room1", () => {
socket3.emit("broadcast");
});
});
});
it("should return an immutable broadcast operator", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv, { multiplex: false });
sio.on("connection", (socket: Socket) => {
const operator = socket.local
.compress(false)
.to(["room1", "room2"])
.except("room3");
operator.compress(true).emit("hello");
operator.volatile.emit("hello");
operator.to("room4").emit("hello");
operator.except("room5").emit("hello");
socket.emit("hello");
socket.to("room6").emit("hello");
// @ts-ignore
expect(operator.rooms).to.contain("room1", "room2");
// @ts-ignore
expect(operator.rooms).to.not.contain("room4", "room5", "room6");
// @ts-ignore
expect(operator.exceptRooms).to.contain("room3");
// @ts-ignore
expect(operator.flags).to.eql({ local: true, compress: false });
clientSocket.close();
sio.close();
done();
});
});
});
});
describe("middleware", () => {
@@ -2262,6 +2455,7 @@ describe("socket.io", () => {
socket.on("connect_error", (err) => {
expect(err).to.be.an(Error);
expect(err.message).to.eql("Authentication error");
// @ts-ignore
expect(err.data).to.eql({ a: "b", c: 3 });
done();
});
@@ -2278,7 +2472,7 @@ describe("socket.io", () => {
srv.listen(() => {
const socket = client(srv);
sio.on("connection", (socket) => {
expect(socket.name).to.be("guillermo");
expect((socket as any).name).to.be("guillermo");
done();
});
});
@@ -2479,6 +2673,32 @@ describe("socket.io", () => {
});
});
it("should be able to connect to a namespace with a query", (done) => {
const srv = createServer();
const sio = new Server(srv, {
allowEIO3: true,
});
srv.listen(async () => {
const port = (srv.address() as AddressInfo).port;
const clientSocket = io_v2.connect(
`http://localhost:${port}/the-namespace`,
{
multiplex: false,
}
);
clientSocket.query = { test: "123" };
const [socket]: Array<any> = await Promise.all([
waitFor(sio.of("/the-namespace"), "connection"),
waitFor(clientSocket, "connect"),
]);
expect(socket.handshake.auth).to.eql({ test: "123" });
success(sio, clientSocket, done);
});
});
it("should not connect if `allowEIO3` is false (default)", (done) => {
const srv = createServer();
const sio = new Server(srv);

176
test/utility-methods.ts Normal file
View File

@@ -0,0 +1,176 @@
import { createServer } from "http";
import { Server, Socket } from "..";
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import { Adapter, BroadcastOptions } from "socket.io-adapter";
import expect from "expect.js";
import type { AddressInfo } from "net";
import "./support/util";
const SOCKETS_COUNT = 3;
const createPartialDone = (
count: number,
done: () => void,
callback?: () => void
) => {
let i = 0;
return () => {
i++;
if (i === count) {
done();
if (callback) {
callback();
}
}
};
};
class DummyAdapter extends Adapter {
fetchSockets(opts: BroadcastOptions): Promise<any[]> {
return Promise.resolve([
{
id: "42",
handshake: {
headers: {
accept: "*/*",
},
query: {
transport: "polling",
EIO: "4",
},
},
rooms: ["42", "room1"],
data: {
username: "john",
},
},
]);
}
}
describe("socket.io", () => {
let io: Server, clientSockets: ClientSocket[], serverSockets: Socket[];
beforeEach((done) => {
const srv = createServer();
io = new Server(srv);
srv.listen(() => {
const port = (srv.address() as AddressInfo).port;
clientSockets = [];
for (let i = 0; i < SOCKETS_COUNT; i++) {
clientSockets.push(ioc(`http://localhost:${port}`));
}
serverSockets = [];
io.on("connection", (socket: Socket) => {
serverSockets.push(socket);
if (serverSockets.length === SOCKETS_COUNT) {
done();
}
});
});
});
afterEach(() => {
io.close();
clientSockets.forEach((socket) => socket.disconnect());
});
describe("utility methods", () => {
describe("fetchSockets", () => {
it("returns all socket instances", async () => {
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(3);
});
it("returns all socket instances in the given room", async () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
const sockets = await io.in("room1").fetchSockets();
expect(sockets.length).to.eql(2);
});
it("works with a custom adapter", async () => {
io.adapter(DummyAdapter);
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(1);
const remoteSocket = sockets[0];
expect(remoteSocket.id).to.eql("42");
expect(remoteSocket.rooms).to.contain("42", "room1");
expect(remoteSocket.data).to.eql({ username: "john" });
});
});
describe("socketsJoin", () => {
it("makes all socket instances join the given room", () => {
io.socketsJoin("room1");
serverSockets.forEach((socket) => {
expect(socket.rooms).to.contain("room1");
});
});
it("makes all socket instances in a room join the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room1").socketsJoin("room3");
expect(serverSockets[0].rooms).to.contain("room3");
expect(serverSockets[1].rooms).to.contain("room3");
expect(serverSockets[2].rooms).to.not.contain("room3");
});
});
describe("socketsLeave", () => {
it("makes all socket instances leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.not.contain("room1");
});
it("makes all socket instances in a room leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.contain("room1");
});
});
describe("disconnectSockets", () => {
it("makes all socket instances disconnect", (done) => {
io.disconnectSockets(true);
const partialDone = createPartialDone(3, done);
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", partialDone);
clientSockets[2].on("disconnect", partialDone);
});
it("makes all socket instances in a room disconnect", (done) => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").disconnectSockets(true);
const partialDone = createPartialDone(2, done, () => {
clientSockets[1].off("disconnect");
});
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", () => {
done(new Error("should not happen"));
});
clientSockets[2].on("disconnect", partialDone);
});
});
});
});