Merge remote-tracking branch 'socket.io-adapter/main' into monorepo

Source: https://github.com/socketio/socket.io-adapter
This commit is contained in:
Damien Arrachequesne
2024-07-08 11:09:23 +02:00
13 changed files with 8011 additions and 0 deletions

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

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

View 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

File diff suppressed because it is too large Load Diff

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

View 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;
}

View 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";

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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();
});
});
});
});

View 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);
});
});
});

View 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));
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": false,
"target": "es2017",
"module": "commonjs",
"declaration": true
},
"include": [
"./lib/**/*"
]
}