mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-08 22:48:20 -05:00
Merge remote-tracking branch 'socket.io-adapter/main' into monorepo
Source: https://github.com/socketio/socket.io-adapter
This commit is contained in:
223
packages/socket.io-adapter/CHANGELOG.md
Normal file
223
packages/socket.io-adapter/CHANGELOG.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# History
|
||||
|
||||
- [2.5.5](#255-2024-06-18) (Jun 2024)
|
||||
- [2.5.4](#254-2024-02-22) (Feb 2024)
|
||||
- [2.5.3](#253-2024-02-21) (Feb 2024)
|
||||
- [2.5.2](#252-2023-01-12) (Jan 2023)
|
||||
- [2.5.1](#251-2023-01-06) (Jan 2023)
|
||||
- [2.5.0](#250-2023-01-06) (Jan 2023)
|
||||
- [2.4.0](#240-2022-03-30) (Mar 2022)
|
||||
- [2.3.3](#233-2021-11-16) (Nov 2021)
|
||||
- [2.3.2](#232-2021-08-28) (Aug 2021)
|
||||
- [2.3.1](#231-2021-05-19) (May 2021)
|
||||
- [2.3.0](#230-2021-05-10) (May 2021)
|
||||
- [2.2.0](#220-2021-02-27) (Feb 2021)
|
||||
- [2.1.0](#210-2021-01-15) (Jan 2021)
|
||||
- [2.0.3](#203-2020-11-05) (Nov 2020)
|
||||
- [2.0.2](#202-2020-09-28) (Sep 2020)
|
||||
- [2.0.1](#201-2020-09-28) (Sep 2020)
|
||||
- [**2.0.0**](#200-2020-09-25) (Sep 2020)
|
||||
|
||||
|
||||
|
||||
# Release notes
|
||||
|
||||
## [2.5.5](https://github.com/socketio/socket.io-adapter/compare/2.5.4...2.5.5) (2024-06-18)
|
||||
|
||||
This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c).
|
||||
|
||||
Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q
|
||||
|
||||
|
||||
|
||||
## [2.5.4](https://github.com/socketio/socket.io-adapter/compare/2.5.3...2.5.4) (2024-02-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure the order of the commands ([a13f35f](https://github.com/socketio/socket.io-adapter/commit/a13f35f0e6b85bbba07f99ee2440e914f1429d83))
|
||||
* **types:** ensure compatibility with TypeScript < 4.5 ([ca397f3](https://github.com/socketio/socket.io-adapter/commit/ca397f3afe06ed9390db52b70a506a9721e091d8))
|
||||
|
||||
|
||||
|
||||
## [2.5.3](https://github.com/socketio/socket.io-adapter/compare/2.5.2...2.5.3) (2024-02-21)
|
||||
|
||||
Two abstract classes were imported from the [Redis adapter repository](https://github.com/socketio/socket.io-redis-adapter/blob/bd32763043a2eb79a21dffd8820f20e598348adf/lib/cluster-adapter.ts):
|
||||
|
||||
- the `ClusterAdapter` class, which manages the messages sent between the server instances of the cluster
|
||||
- the `ClusterAdapterWithHeartbeat` class, which extends the `ClusterAdapter` and adds a heartbeat mechanism in order to check the healthiness of the other instances
|
||||
|
||||
Other adapters can then just extend those classes and only have to implement the pub/sub mechanism (and not the internal chit-chat protocol):
|
||||
|
||||
```js
|
||||
class MyAdapter extends ClusterAdapterWithHeartbeat {
|
||||
constructor(nsp, pubSub, opts) {
|
||||
super(nsp, opts);
|
||||
this.pubSub = pubSub;
|
||||
pubSub.subscribe("main-channel", (message) => this.onMessage(message));
|
||||
pubSub.subscribe("specific-channel#" + this.uid, (response) => this.onResponse(response));
|
||||
}
|
||||
|
||||
doPublish(message) {
|
||||
return this.pubSub.publish("main-channel", message);
|
||||
}
|
||||
|
||||
doPublishResponse(requesterUid, response) {
|
||||
return this.pubSub.publish("specific-channel#" + requesterUid, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Besides, the number of "timeout reached: only x responses received out of y" errors (which can happen when a server instance leaves the cluster) should be greatly reduced by [this commit](https://github.com/socketio/socket.io-adapter/commit/0e23ff0cc671e3186510f7cfb8a4c1147457296f).
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cluster:** fix count in fetchSockets() method ([80af4e9](https://github.com/socketio/socket.io-adapter/commit/80af4e939c9caf89b0234ba1e676a3887c8d0ce6))
|
||||
* **cluster:** notify the other nodes when closing ([0e23ff0](https://github.com/socketio/socket.io-adapter/commit/0e23ff0cc671e3186510f7cfb8a4c1147457296f))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **cluster:** use timer.refresh() ([d99a71b](https://github.com/socketio/socket.io-adapter/commit/d99a71b5588f53f0b181eee989ab2ac939f965db))
|
||||
|
||||
|
||||
|
||||
## [2.5.2](https://github.com/socketio/socket.io-adapter/compare/2.5.1...2.5.2) (2023-01-12)
|
||||
|
||||
The `ws` dependency was moved from `peerDependencies` to `dependencies`, in order to prevent issues like [this](https://github.com/socketio/socket.io-redis-adapter/issues/478).
|
||||
|
||||
|
||||
|
||||
## [2.5.1](https://github.com/socketio/socket.io-adapter/compare/2.5.0...2.5.1) (2023-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly precompute the WebSocket frames ([99b0f18](https://github.com/socketio/socket.io-adapter/commit/99b0f188194b58a213682d564607913a447279e3))
|
||||
|
||||
|
||||
|
||||
## [2.5.0](https://github.com/socketio/socket.io-adapter/compare/2.4.0...2.5.0) (2023-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement connection state recovery ([f529412](https://github.com/socketio/socket.io-adapter/commit/f5294126a8feec1906bca439443c3864415415fb))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* precompute the WebSocket frames when broadcasting ([5f7b47d](https://github.com/socketio/socket.io-adapter/commit/5f7b47d40f9daabe4e3c321eda620bbadfe5ce96))
|
||||
|
||||
|
||||
|
||||
## [2.4.0](https://github.com/socketio/socket.io-adapter/compare/2.3.3...2.4.0) (2022-03-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* broadcast and expect multiple acks ([a7f1c90](https://github.com/socketio/socket.io-adapter/commit/a7f1c90a322241ffaca96ddc42f204d79bc514b5))
|
||||
* notify listeners for each outgoing packet ([38ee887](https://github.com/socketio/socket.io-adapter/commit/38ee887fefa8288f3a3468292c17fe7d5ca57ffc))
|
||||
|
||||
|
||||
|
||||
## [2.3.3](https://github.com/socketio/socket.io-adapter/compare/2.3.2...2.3.3) (2021-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix broadcasting volatile packets with binary attachments ([88eee59](https://github.com/socketio/socket.io-adapter/commit/88eee5948aba94f999405239025f29c754a002e2))
|
||||
|
||||
|
||||
|
||||
## [2.3.2](https://github.com/socketio/socket.io-adapter/compare/2.3.1...2.3.2) (2021-08-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix race condition when leaving rooms ([#74](https://github.com/socketio/socket.io-adapter/issues/74)) ([912e13a](https://github.com/socketio/socket.io-adapter/commit/912e13ad30bd584e2ece747be96a1ba0669dd874))
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/socketio/socket.io-adapter/compare/2.3.0...2.3.1) (2021-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* restore compatibility with binary parsers ([a33e42b](https://github.com/socketio/socket.io-adapter/commit/a33e42bb7b935ccdd3688b4c305714b791ade0db))
|
||||
|
||||
|
||||
## [2.3.0](https://github.com/socketio/socket.io-adapter/compare/2.2.0...2.3.0) (2021-05-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a serverSideEmit empty function ([c4cbd4b](https://github.com/socketio/socket.io-adapter/commit/c4cbd4ba2d8997f9ab8e06cfb631c8f9a43d16f1))
|
||||
* add support for the "wsPreEncoded" writing option ([5579d40](https://github.com/socketio/socket.io-adapter/commit/5579d40c24d15f69e44246f788fb93beb367f994))
|
||||
|
||||
|
||||
## [2.2.0](https://github.com/socketio/socket.io-adapter/compare/2.1.0...2.2.0) (2021-02-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add some utility methods ([1c9827e](https://github.com/socketio/socket.io-adapter/commit/1c9827ec1136e24094295907efaf4d4e6c2fef2f))
|
||||
* allow excluding all sockets in a room ([#66](https://github.com/socketio/socket.io-adapter/issues/66)) ([985bb41](https://github.com/socketio/socket.io-adapter/commit/985bb41fa2c04f17f1cf3a17c14ab9acde8947f7))
|
||||
|
||||
|
||||
## [2.1.0](https://github.com/socketio/socket.io-adapter/compare/2.0.3...2.1.0) (2021-01-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add room events ([155fa63](https://github.com/socketio/socket.io-adapter/commit/155fa6333a504036e99a33667dc0397f6aede25e))
|
||||
* make rooms and sids public ([313c5a9](https://github.com/socketio/socket.io-adapter/commit/313c5a9fb60d913cd3a866001d67516399d8ee2f))
|
||||
|
||||
|
||||
## [2.0.3](https://github.com/socketio/socket.io-adapter/compare/1.1.2...2.0.3) (2020-11-05)
|
||||
|
||||
### Features
|
||||
|
||||
* add init() and close() methods ([2e023bf](https://github.com/socketio/socket.io-adapter/commit/2e023bf2b651e543a34147fab19497fbdb8bdb72))
|
||||
* use ES6 Sets and Maps ([53ed3f4](https://github.com/socketio/socket.io-adapter/commit/53ed3f4099c073546c66d911a95171adcefc524c))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Encoder#encode() is now synchronous ([c043650](https://github.com/socketio/socket.io-adapter/commit/c043650f1c6e58b20364383103314ddc733e4615))
|
||||
|
||||
|
||||
|
||||
## [2.0.3-rc2](https://github.com/socketio/socket.io-adapter/compare/2.0.3-rc1...2.0.3-rc2) (2020-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add init() and close() methods ([2e023bf](https://github.com/socketio/socket.io-adapter/commit/2e023bf2b651e543a34147fab19497fbdb8bdb72))
|
||||
|
||||
|
||||
|
||||
## [2.0.3-rc1](https://github.com/socketio/socket.io-adapter/compare/2.0.2...2.0.3-rc1) (2020-10-15)
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/socketio/socket.io-adapter/compare/2.0.1...2.0.2) (2020-09-28)
|
||||
|
||||
The dist/ directory was not up-to-date when publishing the previous version...
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/socketio/socket.io-adapter/compare/2.0.0...2.0.1) (2020-09-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Encoder#encode() is now synchronous ([c043650](https://github.com/socketio/socket.io-adapter/commit/c043650f1c6e58b20364383103314ddc733e4615))
|
||||
|
||||
|
||||
|
||||
## [2.0.0](https://github.com/socketio/socket.io-adapter/compare/1.1.2...2.0.0) (2020-09-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* use ES6 Sets and Maps ([53ed3f4](https://github.com/socketio/socket.io-adapter/commit/53ed3f4099c073546c66d911a95171adcefc524c))
|
||||
20
packages/socket.io-adapter/LICENSE
Normal file
20
packages/socket.io-adapter/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Guillermo Rauch <guillermo@learnboost.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the 'Software'), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
23
packages/socket.io-adapter/Readme.md
Normal file
23
packages/socket.io-adapter/Readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
# socket.io-adapter
|
||||
|
||||
Default socket.io in-memory adapter class.
|
||||
|
||||
Compatibility table:
|
||||
|
||||
| Adapter version | Socket.IO server version |
|
||||
|-----------------| ------------------------ |
|
||||
| 1.x.x | 1.x.x / 2.x.x |
|
||||
| 2.x.x | 3.x.x |
|
||||
|
||||
## How to use
|
||||
|
||||
This module is not intended for end-user usage, but can be used as an
|
||||
interface to inherit from other adapters you might want to build.
|
||||
|
||||
As an example of an adapter that builds on top of this, please take a look
|
||||
at [socket.io-redis](https://github.com/learnboost/socket.io-redis).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
1010
packages/socket.io-adapter/lib/cluster-adapter.ts
Normal file
1010
packages/socket.io-adapter/lib/cluster-adapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
65
packages/socket.io-adapter/lib/contrib/yeast.ts
Normal file
65
packages/socket.io-adapter/lib/contrib/yeast.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// imported from https://github.com/unshiftio/yeast
|
||||
"use strict";
|
||||
|
||||
const alphabet =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(
|
||||
""
|
||||
),
|
||||
length = 64,
|
||||
map = {};
|
||||
let seed = 0,
|
||||
i = 0,
|
||||
prev;
|
||||
|
||||
/**
|
||||
* Return a string representing the specified number.
|
||||
*
|
||||
* @param {Number} num The number to convert.
|
||||
* @returns {String} The string representation of the number.
|
||||
* @api public
|
||||
*/
|
||||
export function encode(num) {
|
||||
let encoded = "";
|
||||
|
||||
do {
|
||||
encoded = alphabet[num % length] + encoded;
|
||||
num = Math.floor(num / length);
|
||||
} while (num > 0);
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the integer value specified by the given string.
|
||||
*
|
||||
* @param {String} str The string to convert.
|
||||
* @returns {Number} The integer value represented by the string.
|
||||
* @api public
|
||||
*/
|
||||
export function decode(str) {
|
||||
let decoded = 0;
|
||||
|
||||
for (i = 0; i < str.length; i++) {
|
||||
decoded = decoded * length + map[str.charAt(i)];
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yeast: A tiny growing id generator.
|
||||
*
|
||||
* @returns {String} A unique id.
|
||||
* @api public
|
||||
*/
|
||||
export function yeast() {
|
||||
const now = encode(+new Date());
|
||||
|
||||
if (now !== prev) return (seed = 0), (prev = now);
|
||||
return now + "." + encode(seed++);
|
||||
}
|
||||
|
||||
//
|
||||
// Map each character to its index.
|
||||
//
|
||||
for (; i < length; i++) map[alphabet[i]] = i;
|
||||
507
packages/socket.io-adapter/lib/in-memory-adapter.ts
Normal file
507
packages/socket.io-adapter/lib/in-memory-adapter.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { yeast } from "./contrib/yeast";
|
||||
import WebSocket = require("ws");
|
||||
|
||||
const canPreComputeFrame = typeof WebSocket?.Sender?.frame === "function";
|
||||
|
||||
/**
|
||||
* A public ID, sent by the server at the beginning of the Socket.IO session and which can be used for private messaging
|
||||
*/
|
||||
export type SocketId = string;
|
||||
/**
|
||||
* A private ID, sent by the server at the beginning of the Socket.IO session and used for connection state recovery
|
||||
* upon reconnection
|
||||
*/
|
||||
export type PrivateSessionId = string;
|
||||
|
||||
// we could extend the Room type to "string | number", but that would be a breaking change
|
||||
// related: https://github.com/socketio/socket.io-redis-adapter/issues/418
|
||||
export type Room = string;
|
||||
|
||||
export interface BroadcastFlags {
|
||||
volatile?: boolean;
|
||||
compress?: boolean;
|
||||
local?: boolean;
|
||||
broadcast?: boolean;
|
||||
binary?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface BroadcastOptions {
|
||||
rooms: Set<Room>;
|
||||
except?: Set<Room>;
|
||||
flags?: BroadcastFlags;
|
||||
}
|
||||
|
||||
interface SessionToPersist {
|
||||
sid: SocketId;
|
||||
pid: PrivateSessionId;
|
||||
rooms: Room[];
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export type Session = SessionToPersist & { missedPackets: unknown[][] };
|
||||
|
||||
export class Adapter extends EventEmitter {
|
||||
public rooms: Map<Room, Set<SocketId>> = new Map();
|
||||
public sids: Map<SocketId, Set<Room>> = new Map();
|
||||
private readonly encoder;
|
||||
|
||||
/**
|
||||
* In-memory adapter constructor.
|
||||
*
|
||||
* @param {Namespace} nsp
|
||||
*/
|
||||
constructor(readonly nsp: any) {
|
||||
super();
|
||||
this.encoder = nsp.server.encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be overridden
|
||||
*/
|
||||
public init(): Promise<void> | void {}
|
||||
|
||||
/**
|
||||
* To be overridden
|
||||
*/
|
||||
public close(): Promise<void> | void {}
|
||||
|
||||
/**
|
||||
* Returns the number of Socket.IO servers in the cluster
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
public serverCount(): Promise<number> {
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a socket to a list of room.
|
||||
*
|
||||
* @param {SocketId} id the socket id
|
||||
* @param {Set<Room>} rooms a set of rooms
|
||||
* @public
|
||||
*/
|
||||
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
|
||||
if (!this.sids.has(id)) {
|
||||
this.sids.set(id, new Set());
|
||||
}
|
||||
|
||||
for (const room of rooms) {
|
||||
this.sids.get(id).add(room);
|
||||
|
||||
if (!this.rooms.has(room)) {
|
||||
this.rooms.set(room, new Set());
|
||||
this.emit("create-room", room);
|
||||
}
|
||||
if (!this.rooms.get(room).has(id)) {
|
||||
this.rooms.get(room).add(id);
|
||||
this.emit("join-room", room, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a socket from a room.
|
||||
*
|
||||
* @param {SocketId} id the socket id
|
||||
* @param {Room} room the room name
|
||||
*/
|
||||
public del(id: SocketId, room: Room): Promise<void> | void {
|
||||
if (this.sids.has(id)) {
|
||||
this.sids.get(id).delete(room);
|
||||
}
|
||||
|
||||
this._del(room, id);
|
||||
}
|
||||
|
||||
private _del(room: Room, id: SocketId) {
|
||||
const _room = this.rooms.get(room);
|
||||
if (_room != null) {
|
||||
const deleted = _room.delete(id);
|
||||
if (deleted) {
|
||||
this.emit("leave-room", room, id);
|
||||
}
|
||||
if (_room.size === 0 && this.rooms.delete(room)) {
|
||||
this.emit("delete-room", room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a socket from all rooms it's joined.
|
||||
*
|
||||
* @param {SocketId} id the socket id
|
||||
*/
|
||||
public delAll(id: SocketId): void {
|
||||
if (!this.sids.has(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const room of this.sids.get(id)) {
|
||||
this._del(room, id);
|
||||
}
|
||||
|
||||
this.sids.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a packet.
|
||||
*
|
||||
* Options:
|
||||
* - `flags` {Object} flags for this packet
|
||||
* - `except` {Array} sids that should be excluded
|
||||
* - `rooms` {Array} list of rooms to broadcast to
|
||||
*
|
||||
* @param {Object} packet the packet object
|
||||
* @param {Object} opts the options
|
||||
* @public
|
||||
*/
|
||||
public broadcast(packet: any, opts: BroadcastOptions): void {
|
||||
const flags = opts.flags || {};
|
||||
const packetOpts = {
|
||||
preEncoded: true,
|
||||
volatile: flags.volatile,
|
||||
compress: flags.compress,
|
||||
};
|
||||
|
||||
packet.nsp = this.nsp.name;
|
||||
const encodedPackets = this._encode(packet, packetOpts);
|
||||
|
||||
this.apply(opts, (socket) => {
|
||||
if (typeof socket.notifyOutgoingListeners === "function") {
|
||||
socket.notifyOutgoingListeners(packet);
|
||||
}
|
||||
|
||||
socket.client.writeToEngine(encodedPackets, packetOpts);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a packet and expects multiple acknowledgements.
|
||||
*
|
||||
* Options:
|
||||
* - `flags` {Object} flags for this packet
|
||||
* - `except` {Array} sids that should be excluded
|
||||
* - `rooms` {Array} list of rooms to broadcast to
|
||||
*
|
||||
* @param {Object} packet the packet object
|
||||
* @param {Object} opts the options
|
||||
* @param clientCountCallback - the number of clients that received the packet
|
||||
* @param ack - the callback that will be called for each client response
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
public broadcastWithAck(
|
||||
packet: any,
|
||||
opts: BroadcastOptions,
|
||||
clientCountCallback: (clientCount: number) => void,
|
||||
ack: (...args: any[]) => void
|
||||
) {
|
||||
const flags = opts.flags || {};
|
||||
const packetOpts = {
|
||||
preEncoded: true,
|
||||
volatile: flags.volatile,
|
||||
compress: flags.compress,
|
||||
};
|
||||
|
||||
packet.nsp = this.nsp.name;
|
||||
// we can use the same id for each packet, since the _ids counter is common (no duplicate)
|
||||
packet.id = this.nsp._ids++;
|
||||
|
||||
const encodedPackets = this._encode(packet, packetOpts);
|
||||
|
||||
let clientCount = 0;
|
||||
|
||||
this.apply(opts, (socket) => {
|
||||
// track the total number of acknowledgements that are expected
|
||||
clientCount++;
|
||||
// call the ack callback for each client response
|
||||
socket.acks.set(packet.id, ack);
|
||||
|
||||
if (typeof socket.notifyOutgoingListeners === "function") {
|
||||
socket.notifyOutgoingListeners(packet);
|
||||
}
|
||||
|
||||
socket.client.writeToEngine(encodedPackets, packetOpts);
|
||||
});
|
||||
|
||||
clientCountCallback(clientCount);
|
||||
}
|
||||
|
||||
private _encode(packet: unknown, packetOpts: Record<string, unknown>) {
|
||||
const encodedPackets = this.encoder.encode(packet);
|
||||
|
||||
if (
|
||||
canPreComputeFrame &&
|
||||
encodedPackets.length === 1 &&
|
||||
typeof encodedPackets[0] === "string"
|
||||
) {
|
||||
// "4" being the "message" packet type in the Engine.IO protocol
|
||||
const data = Buffer.from("4" + encodedPackets[0]);
|
||||
// see https://github.com/websockets/ws/issues/617#issuecomment-283002469
|
||||
packetOpts.wsPreEncodedFrame = WebSocket.Sender.frame(data, {
|
||||
readOnly: false,
|
||||
mask: false,
|
||||
rsv1: false,
|
||||
opcode: 1,
|
||||
fin: true,
|
||||
});
|
||||
}
|
||||
|
||||
return encodedPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of sockets by sid.
|
||||
*
|
||||
* @param {Set<Room>} rooms the explicit set of rooms to check.
|
||||
*/
|
||||
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
|
||||
const sids = new Set<SocketId>();
|
||||
|
||||
this.apply({ rooms }, (socket) => {
|
||||
sids.add(socket.id);
|
||||
});
|
||||
|
||||
return Promise.resolve(sids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of rooms a given socket has joined.
|
||||
*
|
||||
* @param {SocketId} id the socket id
|
||||
*/
|
||||
public socketRooms(id: SocketId): Set<Room> | undefined {
|
||||
return this.sids.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matching socket instances
|
||||
*
|
||||
* @param opts - the filters to apply
|
||||
*/
|
||||
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
|
||||
const sockets = [];
|
||||
|
||||
this.apply(opts, (socket) => {
|
||||
sockets.push(socket);
|
||||
});
|
||||
|
||||
return Promise.resolve(sockets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances join the specified rooms
|
||||
*
|
||||
* @param opts - the filters to apply
|
||||
* @param rooms - the rooms to join
|
||||
*/
|
||||
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
|
||||
this.apply(opts, (socket) => {
|
||||
socket.join(rooms);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances leave the specified rooms
|
||||
*
|
||||
* @param opts - the filters to apply
|
||||
* @param rooms - the rooms to leave
|
||||
*/
|
||||
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
|
||||
this.apply(opts, (socket) => {
|
||||
rooms.forEach((room) => socket.leave(room));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances disconnect
|
||||
*
|
||||
* @param opts - the filters to apply
|
||||
* @param close - whether to close the underlying connection
|
||||
*/
|
||||
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
|
||||
this.apply(opts, (socket) => {
|
||||
socket.disconnect(close);
|
||||
});
|
||||
}
|
||||
|
||||
private apply(opts: BroadcastOptions, callback: (socket) => void): void {
|
||||
const rooms = opts.rooms;
|
||||
const except = this.computeExceptSids(opts.except);
|
||||
|
||||
if (rooms.size) {
|
||||
const ids = new Set();
|
||||
for (const room of rooms) {
|
||||
if (!this.rooms.has(room)) continue;
|
||||
|
||||
for (const id of this.rooms.get(room)) {
|
||||
if (ids.has(id) || except.has(id)) continue;
|
||||
const socket = this.nsp.sockets.get(id);
|
||||
if (socket) {
|
||||
callback(socket);
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [id] of this.sids) {
|
||||
if (except.has(id)) continue;
|
||||
const socket = this.nsp.sockets.get(id);
|
||||
if (socket) callback(socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computeExceptSids(exceptRooms?: Set<Room>) {
|
||||
const exceptSids = new Set();
|
||||
if (exceptRooms && exceptRooms.size > 0) {
|
||||
for (const room of exceptRooms) {
|
||||
if (this.rooms.has(room)) {
|
||||
this.rooms.get(room).forEach((sid) => exceptSids.add(sid));
|
||||
}
|
||||
}
|
||||
}
|
||||
return exceptSids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to the other Socket.IO servers in the cluster
|
||||
* @param packet - an array of arguments, which may include an acknowledgement callback at the end
|
||||
*/
|
||||
public serverSideEmit(packet: any[]): void {
|
||||
console.warn(
|
||||
"this adapter does not support the serverSideEmit() functionality"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the client session in order to restore it upon reconnection.
|
||||
*/
|
||||
public persistSession(session: SessionToPersist) {}
|
||||
|
||||
/**
|
||||
* Restore the session and find the packets that were missed by the client.
|
||||
* @param pid
|
||||
* @param offset
|
||||
*/
|
||||
public restoreSession(
|
||||
pid: PrivateSessionId,
|
||||
offset: string
|
||||
): Promise<Session> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface PersistedPacket {
|
||||
id: string;
|
||||
emittedAt: number;
|
||||
data: unknown[];
|
||||
opts: BroadcastOptions;
|
||||
}
|
||||
|
||||
type SessionWithTimestamp = SessionToPersist & { disconnectedAt: number };
|
||||
|
||||
export class SessionAwareAdapter extends Adapter {
|
||||
private readonly maxDisconnectionDuration: number;
|
||||
|
||||
private sessions: Map<PrivateSessionId, SessionWithTimestamp> = new Map();
|
||||
private packets: PersistedPacket[] = [];
|
||||
|
||||
constructor(readonly nsp: any) {
|
||||
super(nsp);
|
||||
this.maxDisconnectionDuration =
|
||||
nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const threshold = Date.now() - this.maxDisconnectionDuration;
|
||||
this.sessions.forEach((session, sessionId) => {
|
||||
const hasExpired = session.disconnectedAt < threshold;
|
||||
if (hasExpired) {
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
});
|
||||
for (let i = this.packets.length - 1; i >= 0; i--) {
|
||||
const hasExpired = this.packets[i].emittedAt < threshold;
|
||||
if (hasExpired) {
|
||||
this.packets.splice(0, i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 60 * 1000);
|
||||
// prevents the timer from keeping the process alive
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
override persistSession(session: SessionToPersist) {
|
||||
(session as SessionWithTimestamp).disconnectedAt = Date.now();
|
||||
this.sessions.set(session.pid, session as SessionWithTimestamp);
|
||||
}
|
||||
|
||||
override restoreSession(
|
||||
pid: PrivateSessionId,
|
||||
offset: string
|
||||
): Promise<Session> {
|
||||
const session = this.sessions.get(pid);
|
||||
if (!session) {
|
||||
// the session may have expired
|
||||
return null;
|
||||
}
|
||||
const hasExpired =
|
||||
session.disconnectedAt + this.maxDisconnectionDuration < Date.now();
|
||||
if (hasExpired) {
|
||||
// the session has expired
|
||||
this.sessions.delete(pid);
|
||||
return null;
|
||||
}
|
||||
const index = this.packets.findIndex((packet) => packet.id === offset);
|
||||
if (index === -1) {
|
||||
// the offset may be too old
|
||||
return null;
|
||||
}
|
||||
const missedPackets = [];
|
||||
for (let i = index + 1; i < this.packets.length; i++) {
|
||||
const packet = this.packets[i];
|
||||
if (shouldIncludePacket(session.rooms, packet.opts)) {
|
||||
missedPackets.push(packet.data);
|
||||
}
|
||||
}
|
||||
return Promise.resolve({
|
||||
...session,
|
||||
missedPackets,
|
||||
});
|
||||
}
|
||||
|
||||
override broadcast(packet: any, opts: BroadcastOptions) {
|
||||
const isEventPacket = packet.type === 2;
|
||||
// packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
|
||||
// restored on another server upon reconnection
|
||||
const withoutAcknowledgement = packet.id === undefined;
|
||||
const notVolatile = opts.flags?.volatile === undefined;
|
||||
if (isEventPacket && withoutAcknowledgement && notVolatile) {
|
||||
const id = yeast();
|
||||
// the offset is stored at the end of the data array, so the client knows the ID of the last packet it has
|
||||
// processed (and the format is backward-compatible)
|
||||
packet.data.push(id);
|
||||
this.packets.push({
|
||||
id,
|
||||
opts,
|
||||
data: packet.data,
|
||||
emittedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
super.broadcast(packet, opts);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldIncludePacket(
|
||||
sessionRooms: Room[],
|
||||
opts: BroadcastOptions
|
||||
): boolean {
|
||||
const included =
|
||||
opts.rooms.size === 0 || sessionRooms.some((room) => opts.rooms.has(room));
|
||||
const notExcluded = sessionRooms.every((room) => !opts.except.has(room));
|
||||
return included && notExcluded;
|
||||
}
|
||||
21
packages/socket.io-adapter/lib/index.ts
Normal file
21
packages/socket.io-adapter/lib/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export {
|
||||
SocketId,
|
||||
PrivateSessionId,
|
||||
Room,
|
||||
BroadcastFlags,
|
||||
BroadcastOptions,
|
||||
Session,
|
||||
Adapter,
|
||||
SessionAwareAdapter,
|
||||
} from "./in-memory-adapter";
|
||||
|
||||
export {
|
||||
ClusterAdapter,
|
||||
ClusterAdapterWithHeartbeat,
|
||||
ClusterAdapterOptions,
|
||||
ClusterMessage,
|
||||
ClusterResponse,
|
||||
MessageType,
|
||||
ServerId,
|
||||
Offset,
|
||||
} from "./cluster-adapter";
|
||||
5077
packages/socket.io-adapter/package-lock.json
generated
Normal file
5077
packages/socket.io-adapter/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
packages/socket.io-adapter/package.json
Normal file
39
packages/socket.io-adapter/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "socket.io-adapter",
|
||||
"version": "2.5.5",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/socketio/socket.io-adapter.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"description": "default socket.io in-memory adapter",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/expect.js": "^0.3.32",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^14.11.2",
|
||||
"expect.js": "^0.3.1",
|
||||
"mocha": "^10.2.0",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.8.1",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run format:check && tsc && nyc mocha --require ts-node/register test/*.ts",
|
||||
"format:check": "prettier --parser typescript --check 'lib/**/*.ts' 'test/**/*.ts'",
|
||||
"format:fix": "prettier --parser typescript --write 'lib/**/*.ts' 'test/**/*.ts'",
|
||||
"prepack": "tsc"
|
||||
}
|
||||
}
|
||||
440
packages/socket.io-adapter/test/cluster-adapter.ts
Normal file
440
packages/socket.io-adapter/test/cluster-adapter.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
import { createServer } from "http";
|
||||
import { Server, Socket as ServerSocket } from "socket.io";
|
||||
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
|
||||
import expect = require("expect.js");
|
||||
import type { AddressInfo } from "net";
|
||||
import { times, shouldNotHappen, sleep } from "./util";
|
||||
import {
|
||||
ClusterAdapterWithHeartbeat,
|
||||
type ClusterMessage,
|
||||
type ClusterResponse,
|
||||
} from "../lib";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
const NODES_COUNT = 3;
|
||||
|
||||
class EventEmitterAdapter extends ClusterAdapterWithHeartbeat {
|
||||
private offset = 1;
|
||||
|
||||
constructor(nsp, readonly eventBus) {
|
||||
super(nsp, {});
|
||||
this.eventBus.on("message", (message) => {
|
||||
this.onMessage(message as ClusterMessage);
|
||||
});
|
||||
}
|
||||
|
||||
protected doPublish(message: ClusterMessage): Promise<string> {
|
||||
this.eventBus.emit("message", message);
|
||||
return Promise.resolve(String(++this.offset));
|
||||
}
|
||||
|
||||
protected doPublishResponse(
|
||||
requesterUid: string,
|
||||
response: ClusterResponse
|
||||
): Promise<void> {
|
||||
this.eventBus.emit("message", response);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe("cluster adapter", () => {
|
||||
let servers: Server[],
|
||||
serverSockets: ServerSocket[],
|
||||
clientSockets: ClientSocket[];
|
||||
|
||||
beforeEach((done) => {
|
||||
servers = [];
|
||||
serverSockets = [];
|
||||
clientSockets = [];
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
for (let i = 1; i <= NODES_COUNT; i++) {
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
// @ts-ignore
|
||||
io.adapter(function (nsp) {
|
||||
return new EventEmitterAdapter(nsp, eventBus);
|
||||
});
|
||||
httpServer.listen(() => {
|
||||
const port = (httpServer.address() as AddressInfo).port;
|
||||
const clientSocket = ioc(`http://localhost:${port}`);
|
||||
|
||||
io.on("connection", async (socket) => {
|
||||
clientSockets.push(clientSocket);
|
||||
serverSockets.push(socket);
|
||||
servers.push(io);
|
||||
if (servers.length === NODES_COUNT) {
|
||||
// ensure all nodes know each other
|
||||
servers[0].emit("ping");
|
||||
servers[1].emit("ping");
|
||||
servers[2].emit("ping");
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
servers.forEach((server) => {
|
||||
// @ts-ignore
|
||||
server.httpServer.close();
|
||||
server.of("/").adapter.close();
|
||||
});
|
||||
clientSockets.forEach((socket) => {
|
||||
socket.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
describe("broadcast", function () {
|
||||
it("broadcasts to all clients", (done) => {
|
||||
const partialDone = times(3, done);
|
||||
|
||||
clientSockets.forEach((clientSocket) => {
|
||||
clientSocket.on("test", (arg1, arg2, arg3) => {
|
||||
expect(arg1).to.eql(1);
|
||||
expect(arg2).to.eql("2");
|
||||
expect(Buffer.isBuffer(arg3)).to.be(true);
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
servers[0].emit("test", 1, "2", Buffer.from([3, 4]));
|
||||
});
|
||||
|
||||
it("broadcasts to all clients in a namespace", (done) => {
|
||||
const partialDone = times(3, () => {
|
||||
servers.forEach((server) => server.of("/custom").adapter.close());
|
||||
done();
|
||||
});
|
||||
|
||||
servers.forEach((server) => server.of("/custom"));
|
||||
|
||||
const onConnect = times(3, async () => {
|
||||
servers[0].of("/custom").emit("test");
|
||||
});
|
||||
|
||||
clientSockets.forEach((clientSocket) => {
|
||||
const socket = clientSocket.io.socket("/custom");
|
||||
socket.on("connect", onConnect);
|
||||
socket.on("test", () => {
|
||||
socket.disconnect();
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts to all clients in a room", (done) => {
|
||||
serverSockets[1].join("room1");
|
||||
|
||||
clientSockets[0].on("test", shouldNotHappen(done));
|
||||
clientSockets[1].on("test", () => done());
|
||||
clientSockets[2].on("test", shouldNotHappen(done));
|
||||
|
||||
servers[0].to("room1").emit("test");
|
||||
});
|
||||
|
||||
it("broadcasts to all clients except in room", (done) => {
|
||||
const partialDone = times(2, done);
|
||||
serverSockets[1].join("room1");
|
||||
|
||||
clientSockets[0].on("test", () => partialDone());
|
||||
clientSockets[1].on("test", shouldNotHappen(done));
|
||||
clientSockets[2].on("test", () => partialDone());
|
||||
|
||||
servers[0].of("/").except("room1").emit("test");
|
||||
});
|
||||
|
||||
it("broadcasts to local clients only", (done) => {
|
||||
clientSockets[0].on("test", () => done());
|
||||
clientSockets[1].on("test", shouldNotHappen(done));
|
||||
clientSockets[2].on("test", shouldNotHappen(done));
|
||||
|
||||
servers[0].local.emit("test");
|
||||
});
|
||||
|
||||
it("broadcasts with multiple acknowledgements", (done) => {
|
||||
clientSockets[0].on("test", (cb) => cb(1));
|
||||
clientSockets[1].on("test", (cb) => cb(2));
|
||||
clientSockets[2].on("test", (cb) => cb(3));
|
||||
|
||||
servers[0].timeout(50).emit("test", (err: Error, responses: any[]) => {
|
||||
expect(err).to.be(null);
|
||||
expect(responses).to.contain(1);
|
||||
expect(responses).to.contain(2);
|
||||
expect(responses).to.contain(3);
|
||||
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
expect(servers[0].of("/").adapter.ackRequests.size).to.eql(0);
|
||||
|
||||
done();
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts with multiple acknowledgements (binary content)", (done) => {
|
||||
clientSockets[0].on("test", (cb) => cb(Buffer.from([1])));
|
||||
clientSockets[1].on("test", (cb) => cb(Buffer.from([2])));
|
||||
clientSockets[2].on("test", (cb) => cb(Buffer.from([3])));
|
||||
|
||||
servers[0].timeout(500).emit("test", (err: Error, responses: any[]) => {
|
||||
expect(err).to.be(null);
|
||||
responses.forEach((response) => {
|
||||
expect(Buffer.isBuffer(response)).to.be(true);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts with multiple acknowledgements (no client)", (done) => {
|
||||
servers[0]
|
||||
.to("abc")
|
||||
.timeout(500)
|
||||
.emit("test", (err: Error, responses: any[]) => {
|
||||
expect(err).to.be(null);
|
||||
expect(responses).to.eql([]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts with multiple acknowledgements (timeout)", (done) => {
|
||||
clientSockets[0].on("test", (cb) => cb(1));
|
||||
clientSockets[1].on("test", (cb) => cb(2));
|
||||
clientSockets[2].on("test", (_cb) => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
servers[0].timeout(50).emit("test", (err: Error, responses: any[]) => {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(responses).to.contain(1);
|
||||
expect(responses).to.contain(2);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts with a single acknowledgement (local)", async () => {
|
||||
clientSockets[0].on("test", () => expect().fail());
|
||||
clientSockets[1].on("test", (cb) => cb(2));
|
||||
clientSockets[2].on("test", () => expect().fail());
|
||||
|
||||
const response = await serverSockets[1].emitWithAck("test");
|
||||
expect(response).to.eql(2);
|
||||
});
|
||||
|
||||
it("broadcasts with a single acknowledgement (remote)", async () => {
|
||||
clientSockets[0].on("test", () => expect().fail());
|
||||
clientSockets[1].on("test", (cb) => cb(2));
|
||||
clientSockets[2].on("test", () => expect().fail());
|
||||
|
||||
const sockets = await servers[0].in(serverSockets[1].id).fetchSockets();
|
||||
expect(sockets.length).to.eql(1);
|
||||
|
||||
const response = await sockets[0].timeout(500).emitWithAck("test");
|
||||
expect(response).to.eql(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("socketsJoin", () => {
|
||||
it("makes all socket instances join the specified room", async () => {
|
||||
servers[0].socketsJoin("room1");
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(serverSockets[0].rooms.has("room1")).to.be(true);
|
||||
expect(serverSockets[1].rooms.has("room1")).to.be(true);
|
||||
expect(serverSockets[2].rooms.has("room1")).to.be(true);
|
||||
});
|
||||
|
||||
it("makes the matching socket instances join the specified room", async () => {
|
||||
serverSockets[0].join("room1");
|
||||
serverSockets[2].join("room1");
|
||||
|
||||
servers[0].in("room1").socketsJoin("room2");
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(serverSockets[0].rooms.has("room2")).to.be(true);
|
||||
expect(serverSockets[1].rooms.has("room2")).to.be(false);
|
||||
expect(serverSockets[2].rooms.has("room2")).to.be(true);
|
||||
});
|
||||
|
||||
it("makes the given socket instance join the specified room", async () => {
|
||||
servers[0].in(serverSockets[1].id).socketsJoin("room3");
|
||||
|
||||
expect(serverSockets[0].rooms.has("room3")).to.be(false);
|
||||
expect(serverSockets[1].rooms.has("room3")).to.be(true);
|
||||
expect(serverSockets[2].rooms.has("room3")).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("socketsLeave", () => {
|
||||
it("makes all socket instances leave the specified room", async () => {
|
||||
serverSockets[0].join("room1");
|
||||
serverSockets[2].join("room1");
|
||||
|
||||
servers[0].socketsLeave("room1");
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(serverSockets[0].rooms.has("room1")).to.be(false);
|
||||
expect(serverSockets[1].rooms.has("room1")).to.be(false);
|
||||
expect(serverSockets[2].rooms.has("room1")).to.be(false);
|
||||
});
|
||||
|
||||
it("makes the matching socket instances leave the specified room", async () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join(["room1", "room2"]);
|
||||
serverSockets[2].join(["room2"]);
|
||||
|
||||
servers[0].in("room1").socketsLeave("room2");
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(serverSockets[0].rooms.has("room2")).to.be(false);
|
||||
expect(serverSockets[1].rooms.has("room2")).to.be(false);
|
||||
expect(serverSockets[2].rooms.has("room2")).to.be(true);
|
||||
});
|
||||
|
||||
it("makes the given socket instance leave the specified room", async () => {
|
||||
serverSockets[0].join("room3");
|
||||
serverSockets[1].join("room3");
|
||||
serverSockets[2].join("room3");
|
||||
|
||||
servers[0].in(serverSockets[1].id).socketsLeave("room3");
|
||||
|
||||
expect(serverSockets[0].rooms.has("room3")).to.be(true);
|
||||
expect(serverSockets[1].rooms.has("room3")).to.be(false);
|
||||
expect(serverSockets[2].rooms.has("room3")).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("disconnectSockets", () => {
|
||||
it("makes all socket instances disconnect", (done) => {
|
||||
const partialDone = times(3, done);
|
||||
|
||||
clientSockets.forEach((clientSocket) => {
|
||||
clientSocket.on("disconnect", (reason) => {
|
||||
expect(reason).to.eql("io server disconnect");
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
servers[0].disconnectSockets(true);
|
||||
});
|
||||
|
||||
it("sends a packet before all socket instances disconnect", (done) => {
|
||||
const partialDone = times(3, done);
|
||||
|
||||
clientSockets.forEach((clientSocket) => {
|
||||
clientSocket.on("disconnect", shouldNotHappen(done));
|
||||
|
||||
clientSocket.on("bye", () => {
|
||||
clientSocket.off("disconnect");
|
||||
clientSocket.on("disconnect", partialDone);
|
||||
});
|
||||
});
|
||||
|
||||
servers[0].emit("bye");
|
||||
servers[0].disconnectSockets(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchSockets", () => {
|
||||
it("returns all socket instances", async () => {
|
||||
const sockets = await servers[0].fetchSockets();
|
||||
|
||||
expect(sockets).to.be.an(Array);
|
||||
expect(sockets).to.have.length(3);
|
||||
// @ts-ignore
|
||||
expect(servers[0].of("/").adapter.requests.size).to.eql(0); // clean up
|
||||
});
|
||||
|
||||
it("returns a single socket instance", async () => {
|
||||
serverSockets[1].data = "test" as any;
|
||||
|
||||
const [remoteSocket] = await servers[0]
|
||||
.in(serverSockets[1].id)
|
||||
.fetchSockets();
|
||||
|
||||
expect(remoteSocket.handshake).to.eql(serverSockets[1].handshake);
|
||||
expect(remoteSocket.data).to.eql("test");
|
||||
expect(remoteSocket.rooms.size).to.eql(1);
|
||||
});
|
||||
|
||||
it("returns only local socket instances", async () => {
|
||||
const sockets = await servers[0].local.fetchSockets();
|
||||
|
||||
expect(sockets).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serverSideEmit", () => {
|
||||
it("sends an event to other server instances", (done) => {
|
||||
const partialDone = times(2, done);
|
||||
|
||||
servers[0].on("hello", shouldNotHappen(done));
|
||||
|
||||
servers[1].on("hello", (arg1, arg2, arg3) => {
|
||||
expect(arg1).to.eql("world");
|
||||
expect(arg2).to.eql(1);
|
||||
expect(arg3).to.eql("2");
|
||||
partialDone();
|
||||
});
|
||||
|
||||
servers[2].of("/").on("hello", () => partialDone());
|
||||
|
||||
servers[0].serverSideEmit("hello", "world", 1, "2");
|
||||
});
|
||||
|
||||
it("sends an event and receives a response from the other server instances", (done) => {
|
||||
servers[0].on("hello", shouldNotHappen(done));
|
||||
servers[1].on("hello", (cb) => cb(2));
|
||||
servers[2].on("hello", (cb) => cb("3"));
|
||||
|
||||
servers[0].serverSideEmit("hello", (err: Error, response: any) => {
|
||||
expect(err).to.be(null);
|
||||
expect(response).to.be.an(Array);
|
||||
expect(response).to.contain(2);
|
||||
expect(response).to.contain("3");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("sends an event but timeout if one server does not respond", function (done) {
|
||||
this.timeout(6000);
|
||||
|
||||
servers[0].on("hello", shouldNotHappen(done));
|
||||
servers[1].on("hello", (cb) => cb(2));
|
||||
servers[2].on("hello", () => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
servers[0].serverSideEmit("hello", (err: Error, response: any) => {
|
||||
expect(err.message).to.be("timeout reached: missing 1 responses");
|
||||
expect(response).to.be.an(Array);
|
||||
expect(response).to.contain(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("succeeds even if an instance leaves the cluster", (done) => {
|
||||
servers[0].on("hello", shouldNotHappen(done));
|
||||
servers[1].on("hello", (cb) => cb(2));
|
||||
servers[2].on("hello", () => {
|
||||
servers[2].of("/").adapter.close();
|
||||
});
|
||||
|
||||
servers[0].serverSideEmit("hello", (err: Error, response: any) => {
|
||||
expect(err).to.be(null);
|
||||
expect(response).to.be.an(Array);
|
||||
expect(response).to.contain(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
555
packages/socket.io-adapter/test/index.ts
Normal file
555
packages/socket.io-adapter/test/index.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import { Adapter, SessionAwareAdapter } from "../lib";
|
||||
import expect = require("expect.js");
|
||||
|
||||
describe("socket.io-adapter", () => {
|
||||
it("should add/remove sockets", () => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.addAll("s2", new Set(["r2", "r3"]));
|
||||
|
||||
expect(adapter.rooms.has("r1")).to.be(true);
|
||||
expect(adapter.rooms.has("r2")).to.be(true);
|
||||
expect(adapter.rooms.has("r3")).to.be(true);
|
||||
expect(adapter.rooms.has("r4")).to.be(false);
|
||||
|
||||
expect(adapter.sids.has("s1")).to.be(true);
|
||||
expect(adapter.sids.has("s2")).to.be(true);
|
||||
expect(adapter.sids.has("s3")).to.be(false);
|
||||
|
||||
adapter.del("s1", "r1");
|
||||
expect(adapter.rooms.has("r1")).to.be(false);
|
||||
|
||||
adapter.delAll("s2");
|
||||
expect(adapter.rooms.has("r2")).to.be(true);
|
||||
expect(adapter.rooms.has("r3")).to.be(false);
|
||||
|
||||
expect(adapter.sids.has("s2")).to.be(false);
|
||||
});
|
||||
|
||||
it("should return a list of sockets", async () => {
|
||||
const adapter = new Adapter({
|
||||
server: { encoder: null },
|
||||
sockets: new Map([
|
||||
["s1", { id: "s1" }],
|
||||
["s2", { id: "s2" }],
|
||||
["s3", { id: "s3" }],
|
||||
]),
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.addAll("s2", new Set(["r2", "r3"]));
|
||||
adapter.addAll("s3", new Set(["r3"]));
|
||||
|
||||
const sockets = await adapter.sockets(new Set());
|
||||
expect(sockets).to.be.a(Set);
|
||||
expect(sockets.size).to.be(3);
|
||||
expect((await adapter.sockets(new Set(["r2"]))).size).to.be(2);
|
||||
expect((await adapter.sockets(new Set(["r4"]))).size).to.be(0);
|
||||
});
|
||||
|
||||
it("should return a list of rooms", () => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.addAll("s2", new Set(["r2", "r3"]));
|
||||
adapter.addAll("s3", new Set(["r3"]));
|
||||
|
||||
const rooms = adapter.socketRooms("s2");
|
||||
expect(rooms).to.be.a(Set);
|
||||
expect(rooms.size).to.be(2);
|
||||
expect(adapter.socketRooms("s4")).to.be(undefined);
|
||||
});
|
||||
|
||||
it("should exclude sockets in specific rooms when broadcasting", () => {
|
||||
let ids = [];
|
||||
function socket(id) {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
id,
|
||||
client: {
|
||||
writeToEngine(payload, opts) {
|
||||
expect(payload).to.eql(["123"]);
|
||||
expect(opts.preEncoded).to.eql(true);
|
||||
ids.push(id);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const nsp = {
|
||||
server: {
|
||||
encoder: {
|
||||
encode() {
|
||||
return ["123"];
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
sockets: new Map([socket("s1"), socket("s2"), socket("s3")]),
|
||||
};
|
||||
const adapter = new Adapter(nsp);
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.addAll("s2", new Set());
|
||||
adapter.addAll("s3", new Set(["r1"]));
|
||||
|
||||
adapter.broadcast([], {
|
||||
rooms: new Set(),
|
||||
except: new Set(["r1"]),
|
||||
});
|
||||
expect(ids).to.eql(["s2"]);
|
||||
});
|
||||
|
||||
it("should exclude sockets in specific rooms when broadcasting to rooms", () => {
|
||||
let ids = [];
|
||||
function socket(id) {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
id,
|
||||
client: {
|
||||
writeToEngine(payload, opts) {
|
||||
expect(payload).to.be.a(Array);
|
||||
expect(payload[0]).to.be.a(Buffer);
|
||||
expect(opts.preEncoded).to.eql(true);
|
||||
expect(opts.wsPreEncoded).to.be(undefined);
|
||||
ids.push(id);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const nsp = {
|
||||
server: {
|
||||
encoder: {
|
||||
encode() {
|
||||
return [Buffer.from([1, 2, 3])];
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
sockets: new Map([socket("s1"), socket("s2"), socket("s3")]),
|
||||
};
|
||||
const adapter = new Adapter(nsp);
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.addAll("s2", new Set(["r2"]));
|
||||
adapter.addAll("s3", new Set(["r1"]));
|
||||
|
||||
adapter.broadcast([], {
|
||||
rooms: new Set(["r1"]),
|
||||
except: new Set(["r2"]),
|
||||
});
|
||||
expect(ids).to.eql(["s3"]);
|
||||
});
|
||||
|
||||
it("should precompute the WebSocket frames when broadcasting", () => {
|
||||
function socket(id) {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
id,
|
||||
client: {
|
||||
writeToEngine(payload, opts) {
|
||||
expect(payload).to.eql(["123"]);
|
||||
expect(opts.preEncoded).to.eql(true);
|
||||
expect(opts.wsPreEncodedFrame.length).to.eql(2);
|
||||
expect(opts.wsPreEncodedFrame[0]).to.eql(Buffer.from([129, 4]));
|
||||
expect(opts.wsPreEncodedFrame[1]).to.eql(
|
||||
Buffer.from([52, 49, 50, 51])
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const nsp = {
|
||||
server: {
|
||||
encoder: {
|
||||
encode() {
|
||||
return ["123"];
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
sockets: new Map([socket("s1"), socket("s2"), socket("s3")]),
|
||||
};
|
||||
const adapter = new Adapter(nsp);
|
||||
adapter.addAll("s1", new Set());
|
||||
adapter.addAll("s2", new Set());
|
||||
adapter.addAll("s3", new Set());
|
||||
|
||||
adapter.broadcast([], {
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
describe("utility methods", () => {
|
||||
let adapter;
|
||||
|
||||
before(() => {
|
||||
adapter = new Adapter({
|
||||
server: { encoder: null },
|
||||
sockets: new Map([
|
||||
["s1", { id: "s1" }],
|
||||
["s2", { id: "s2" }],
|
||||
["s3", { id: "s3" }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchSockets", () => {
|
||||
it("returns the matching socket instances", async () => {
|
||||
adapter.addAll("s1", new Set(["s1"]));
|
||||
adapter.addAll("s2", new Set(["s2"]));
|
||||
adapter.addAll("s3", new Set(["s3"]));
|
||||
const matchingSockets = await adapter.fetchSockets({
|
||||
rooms: new Set(),
|
||||
});
|
||||
expect(matchingSockets).to.be.an(Array);
|
||||
expect(matchingSockets.length).to.be(3);
|
||||
});
|
||||
|
||||
it("returns the matching socket instances within room", async () => {
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.addAll("s2", new Set(["r1"]));
|
||||
adapter.addAll("s3", new Set(["r2"]));
|
||||
const matchingSockets = await adapter.fetchSockets({
|
||||
rooms: new Set(["r1"]),
|
||||
except: new Set(["r2"]),
|
||||
});
|
||||
expect(matchingSockets).to.be.an(Array);
|
||||
expect(matchingSockets.length).to.be(1);
|
||||
expect(matchingSockets[0].id).to.be("s2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("events", () => {
|
||||
it("should emit a 'create-room' event", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("create-room", (room) => {
|
||||
expect(room).to.eql("r1");
|
||||
done();
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
});
|
||||
|
||||
it("should not emit a 'create-room' event if the room already exists", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.on("create-room", (room) => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
adapter.addAll("s2", new Set(["r1"]));
|
||||
done();
|
||||
});
|
||||
|
||||
it("should emit a 'join-room' event", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("join-room", (room, sid) => {
|
||||
expect(room).to.eql("r1");
|
||||
expect(sid).to.eql("s1");
|
||||
done();
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
});
|
||||
|
||||
it("should not emit a 'join-room' event if the sid is already in the room", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.addAll("s1", new Set(["r1", "r2"]));
|
||||
adapter.on("join-room", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
done();
|
||||
});
|
||||
|
||||
it("should emit a 'leave-room' event with del method", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("leave-room", (room, sid) => {
|
||||
expect(room).to.eql("r1");
|
||||
expect(sid).to.eql("s1");
|
||||
done();
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.del("s1", "r1");
|
||||
});
|
||||
|
||||
it("should not throw when calling del twice", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("leave-room", (room, sid) => {
|
||||
adapter.del("s1", "r1");
|
||||
process.nextTick(done);
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.del("s1", "r1");
|
||||
});
|
||||
|
||||
it("should emit a 'leave-room' event with delAll method", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("leave-room", (room, sid) => {
|
||||
expect(room).to.eql("r1");
|
||||
expect(sid).to.eql("s1");
|
||||
done();
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.delAll("s1");
|
||||
});
|
||||
|
||||
it("should emit a 'delete-room' event", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("delete-room", (room) => {
|
||||
expect(room).to.eql("r1");
|
||||
done();
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.delAll("s1");
|
||||
});
|
||||
|
||||
it("should not emit a 'delete-room' event if there is another sid in the room", (done) => {
|
||||
const adapter = new Adapter({ server: { encoder: null } });
|
||||
adapter.on("delete-room", (room) => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
adapter.addAll("s1", new Set(["r1"]));
|
||||
adapter.addAll("s2", new Set(["r1", "r2"]));
|
||||
adapter.delAll("s1");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("connection state recovery", () => {
|
||||
it("should persist and restore session", async () => {
|
||||
const adapter = new SessionAwareAdapter({
|
||||
server: {
|
||||
encoder: {
|
||||
encode(packet) {
|
||||
return packet;
|
||||
},
|
||||
},
|
||||
opts: {
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
adapter.persistSession({
|
||||
sid: "abc",
|
||||
pid: "def",
|
||||
data: "ghi",
|
||||
rooms: ["r1", "r2"],
|
||||
});
|
||||
|
||||
const packetData = ["hello"];
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: packetData,
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
const offset = packetData[1];
|
||||
const session = await adapter.restoreSession("def", offset);
|
||||
|
||||
expect(session).to.not.be(null);
|
||||
expect(session.sid).to.eql("abc");
|
||||
expect(session.pid).to.eql("def");
|
||||
expect(session.missedPackets).to.eql([]);
|
||||
});
|
||||
|
||||
it("should restore missed packets", async () => {
|
||||
const adapter = new SessionAwareAdapter({
|
||||
server: {
|
||||
encoder: {
|
||||
encode(packet) {
|
||||
return packet;
|
||||
},
|
||||
},
|
||||
opts: {
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
adapter.persistSession({
|
||||
sid: "abc",
|
||||
pid: "def",
|
||||
data: "ghi",
|
||||
rooms: ["r1", "r2"],
|
||||
});
|
||||
|
||||
const packetData = ["hello"];
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: packetData,
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["all"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["room"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(["r1"]),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["except"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(["r2"]),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["no except"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(["r3"]),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["with ack"],
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 3,
|
||||
data: ["ack type"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
}
|
||||
);
|
||||
|
||||
adapter.broadcast(
|
||||
{
|
||||
nsp: "/",
|
||||
type: 2,
|
||||
data: ["volatile"],
|
||||
},
|
||||
{
|
||||
rooms: new Set(),
|
||||
except: new Set(),
|
||||
flags: {
|
||||
volatile: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const offset = packetData[1];
|
||||
const session = await adapter.restoreSession("def", offset);
|
||||
|
||||
expect(session).to.not.be(null);
|
||||
expect(session.sid).to.eql("abc");
|
||||
expect(session.pid).to.eql("def");
|
||||
|
||||
expect(session.missedPackets.length).to.eql(3);
|
||||
expect(session.missedPackets[0].length).to.eql(2);
|
||||
expect(session.missedPackets[0][0]).to.eql("all");
|
||||
expect(session.missedPackets[1][0]).to.eql("room");
|
||||
expect(session.missedPackets[2][0]).to.eql("no except");
|
||||
});
|
||||
|
||||
it("should fail to restore an unknown session", async () => {
|
||||
const adapter = new SessionAwareAdapter({
|
||||
server: {
|
||||
encoder: {
|
||||
encode(packet) {
|
||||
return packet;
|
||||
},
|
||||
},
|
||||
opts: {
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const session = await adapter.restoreSession("abc", "def");
|
||||
|
||||
expect(session).to.be(null);
|
||||
});
|
||||
|
||||
it("should fail to restore a known session with an unknown offset", async () => {
|
||||
const adapter = new SessionAwareAdapter({
|
||||
server: {
|
||||
encoder: {
|
||||
encode(packet) {
|
||||
return packet;
|
||||
},
|
||||
},
|
||||
opts: {
|
||||
connectionStateRecovery: {
|
||||
maxDisconnectionDuration: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
adapter.persistSession({
|
||||
sid: "abc",
|
||||
pid: "def",
|
||||
data: "ghi",
|
||||
rooms: ["r1", "r2"],
|
||||
});
|
||||
|
||||
const session = await adapter.restoreSession("abc", "def");
|
||||
|
||||
expect(session).to.be(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
19
packages/socket.io-adapter/test/util.ts
Normal file
19
packages/socket.io-adapter/test/util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function times(count: number, fn: () => void) {
|
||||
let i = 0;
|
||||
return () => {
|
||||
i++;
|
||||
if (i === count) {
|
||||
fn();
|
||||
} else if (i > count) {
|
||||
throw new Error(`too many calls: ${i} instead of ${count}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldNotHappen(done) {
|
||||
return () => done(new Error("should not happen"));
|
||||
}
|
||||
|
||||
export function sleep() {
|
||||
return new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
12
packages/socket.io-adapter/tsconfig.json
Normal file
12
packages/socket.io-adapter/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowJs": false,
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"./lib/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user