mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 07:58:13 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eaeffc8e2 | ||
|
|
1b6d6de4ed | ||
|
|
0107510ba8 | ||
|
|
b25495c069 | ||
|
|
085d1de9df | ||
|
|
ac9e8ca6c7 | ||
|
|
7de2e87e88 | ||
|
|
225ade062a | ||
|
|
494c64e44f | ||
|
|
67a61e39e6 | ||
|
|
7467216e02 | ||
|
|
7247b4051f | ||
|
|
992c9380c3 | ||
|
|
8b404f424b | ||
|
|
12221f296d | ||
|
|
6f4bd7f8e7 | ||
|
|
4f2e9a716d | ||
|
|
9e8f288ca9 | ||
|
|
86eb4227b2 | ||
|
|
cf873fd831 | ||
|
|
0d10e6131b | ||
|
|
10aafbbc16 | ||
|
|
f34cfca26d | ||
|
|
d412e876b8 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
4
client-dist/socket.io.min.js
vendored
4
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
client-dist/socket.io.msgpack.min.js
vendored
4
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
15
examples/cluster-nginx/client/Dockerfile
Normal file
15
examples/cluster-nginx/client/Dockerfile
Normal 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" ]
|
||||
13
examples/cluster-nginx/client/index.js
Normal file
13
examples/cluster-nginx/client/index.js
Normal 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}`);
|
||||
});
|
||||
15
examples/cluster-nginx/client/package.json
Normal file
15
examples/cluster-nginx/client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ server-ringo:
|
||||
environment:
|
||||
- NAME=Ringo
|
||||
|
||||
client:
|
||||
build: ./client
|
||||
links:
|
||||
- nginx
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
expose:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
22
examples/cluster-traefik/README.md
Normal file
22
examples/cluster-traefik/README.md
Normal 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).
|
||||
27
examples/cluster-traefik/docker-compose.yml
Normal file
27
examples/cluster-traefik/docker-compose.yml
Normal 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
|
||||
15
examples/cluster-traefik/server/Dockerfile
Normal file
15
examples/cluster-traefik/server/Dockerfile
Normal 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" ]
|
||||
83
examples/cluster-traefik/server/index.js
Normal file
83
examples/cluster-traefik/server/index.js
Normal 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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
17
examples/cluster-traefik/server/package.json
Normal file
17
examples/cluster-traefik/server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
28
examples/cluster-traefik/server/public/index.html
Normal file
28
examples/cluster-traefik/server/public/index.html
Normal 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>
|
||||
286
examples/cluster-traefik/server/public/main.js
Normal file
286
examples/cluster-traefik/server/public/main.js
Normal 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);
|
||||
})
|
||||
|
||||
});
|
||||
149
examples/cluster-traefik/server/public/style.css
Normal file
149
examples/cluster-traefik/server/public/style.css
Normal 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%;
|
||||
}
|
||||
10
examples/cluster-traefik/traefik.yml
Normal file
10
examples/cluster-traefik/traefik.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
api:
|
||||
insecure: true
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
|
||||
providers:
|
||||
docker: {}
|
||||
24
examples/private-messaging/.gitignore
vendored
Normal file
24
examples/private-messaging/.gitignore
vendored
Normal 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
|
||||
23
examples/private-messaging/README.md
Normal file
23
examples/private-messaging/README.md
Normal 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
|
||||
```
|
||||
5
examples/private-messaging/babel.config.js
Normal file
5
examples/private-messaging/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
43
examples/private-messaging/package.json
Normal file
43
examples/private-messaging/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
BIN
examples/private-messaging/public/favicon.ico
Normal file
BIN
examples/private-messaging/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
examples/private-messaging/public/fonts/Lato-Regular.ttf
Normal file
BIN
examples/private-messaging/public/fonts/Lato-Regular.ttf
Normal file
Binary file not shown.
17
examples/private-messaging/public/index.html
Normal file
17
examples/private-messaging/public/index.html
Normal 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>
|
||||
31
examples/private-messaging/server/cluster.js
Normal file
31
examples/private-messaging/server/cluster.js
Normal 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");
|
||||
}
|
||||
7
examples/private-messaging/server/docker-compose.yml
Normal file
7
examples/private-messaging/server/docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:5
|
||||
ports:
|
||||
- "6379:6379"
|
||||
125
examples/private-messaging/server/index.js
Normal file
125
examples/private-messaging/server/index.js
Normal 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);
|
||||
54
examples/private-messaging/server/messageStore.js
Normal file
54
examples/private-messaging/server/messageStore.js
Normal 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,
|
||||
};
|
||||
17
examples/private-messaging/server/package.json
Normal file
17
examples/private-messaging/server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
89
examples/private-messaging/server/sessionStore.js
Normal file
89
examples/private-messaging/server/sessionStore.js
Normal 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,
|
||||
};
|
||||
78
examples/private-messaging/src/App.vue
Normal file
78
examples/private-messaging/src/App.vue
Normal 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>
|
||||
165
examples/private-messaging/src/components/Chat.vue
Normal file
165
examples/private-messaging/src/components/Chat.vue
Normal 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>
|
||||
101
examples/private-messaging/src/components/MessagePanel.vue
Normal file
101
examples/private-messaging/src/components/MessagePanel.vue
Normal 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>
|
||||
36
examples/private-messaging/src/components/SelectUsername.vue
Normal file
36
examples/private-messaging/src/components/SelectUsername.vue
Normal 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>
|
||||
27
examples/private-messaging/src/components/StatusIcon.vue
Normal file
27
examples/private-messaging/src/components/StatusIcon.vue
Normal 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>
|
||||
63
examples/private-messaging/src/components/User.vue
Normal file
63
examples/private-messaging/src/components/User.vue
Normal 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>
|
||||
8
examples/private-messaging/src/main.js
Normal file
8
examples/private-messaging/src/main.js
Normal 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");
|
||||
10
examples/private-messaging/src/socket.js
Normal file
10
examples/private-messaging/src/socket.js
Normal 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
315
lib/broadcast-operator.ts
Normal 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 they’re 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
189
lib/index.ts
189
lib/index.ts
@@ -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 };
|
||||
|
||||
198
lib/namespace.ts
198
lib/namespace.ts
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
223
lib/socket.ts
223
lib/socket.ts
@@ -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
167
lib/typed-events.ts
Normal 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
1356
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -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
200
test/socket.io.test-d.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
176
test/utility-methods.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user