Compare commits

...

26 Commits
3.0.5 ... 3.1.2

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

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

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

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

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

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

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

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

Reference: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#hash
2021-01-28 10:52:26 +01:00
JPSO
10aafbbc16 ci: add Node.js 15 (#3765) 2021-01-20 22:34:51 +01:00
Hamdi Bayhan
f34cfca26d docs: fix broken link (#3759) 2021-01-17 22:10:37 +01:00
PrashoonB
d412e876b8 docs: add installation with yarn (#3757) 2021-01-15 22:20:04 +01:00
Damien Arrachequesne
f05a4a6f82 chore(release): 3.1.0
Diff: https://github.com/socketio/socket.io/compare/3.0.5...3.1.0
2021-01-15 02:21:54 +01:00
Damien Arrachequesne
2c883f5d4e chore: bump socket.io-adapter version
Diff: https://github.com/socketio/socket.io-adapter/compare/2.0.3...2.1.0

Includes:

- add room events ([155fa63](155fa6333a))
- make rooms and sids public ([313c5a9](313c5a9fb6))
2021-01-15 01:41:37 +01:00
Jakob Ackermann
161091dd4c feat: confirm a weak but matching ETag (#3485)
When handling compression at the proxy server level, the client receives a weak ETag.
Weak ETags are prefixed with `W/`, e.g. `W/"2.2.0"`.
Upon cache validation we should take care of these too.

Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
2021-01-15 01:04:55 +01:00
汪心禾 Wang, Xinhe
d52532b7be docs: add other client implementations (#3593)
Reference: https://socket.io/docs/#Other-client-implementations
2021-01-15 00:37:40 +01:00
Sergio Lenza
6b1d7901db docs(examples): Improve the chat example with more ES6 features (#3240) 2021-01-15 00:36:17 +01:00
EloniX-X
b55892ae80 docs: add run on repl.it badge to README (#3617) 2021-01-15 00:30:17 +01:00
Pablo Tejada
233650c222 feat(esm): export the Namespace and Socket class (#3699) 2021-01-15 00:28:20 +01:00
Damien Arrachequesne
9925746c8e feat: add support for Socket.IO v2 clients
In order to ease the migration to Socket.IO v3, the Socket.IO server
can now communicate with v2 clients.

```js
const io = require("socket.io")({
  allowEIO3: true
});
```

This feature is disabled by default.
2021-01-14 23:38:24 +01:00
Rohan Chougule
de8dffd252 refactor: strict type check in if expressions (#3744) 2021-01-08 14:58:37 +01:00
62 changed files with 2208 additions and 1769 deletions

View File

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

2
.replit Normal file
View File

@@ -0,0 +1,2 @@
language = "nodejs"
run = "npm start"

View File

@@ -1,3 +1,37 @@
## [3.1.2](https://github.com/socketio/socket.io/compare/3.1.1...3.1.2) (2021-02-26)
### Bug Fixes
* ignore packets received after disconnection ([494c64e](https://github.com/socketio/socket.io/commit/494c64e44f645cbd24c645f1186d203789e84af0))
## [3.1.1](https://github.com/socketio/socket.io/compare/3.1.0...3.1.1) (2021-02-03)
### Bug Fixes
* properly parse the CONNECT packet in v2 compatibility mode ([6f4bd7f](https://github.com/socketio/socket.io/commit/6f4bd7f8e7c41a075a8014565330a77c38b03a8d))
* **typings:** add return types and general-case overload signatures ([#3776](https://github.com/socketio/socket.io/issues/3776)) ([9e8f288](https://github.com/socketio/socket.io/commit/9e8f288ca9f14f91064b8d3cce5946f7d23d407c))
* **typings:** update the types of "query", "auth" and "headers" ([4f2e9a7](https://github.com/socketio/socket.io/commit/4f2e9a716d9835b550c8fd9a9b429ebf069c2895))
# [3.1.0](https://github.com/socketio/socket.io/compare/3.0.5...3.1.0) (2021-01-15)
### Features
* confirm a weak but matching ETag ([#3485](https://github.com/socketio/socket.io/issues/3485)) ([161091d](https://github.com/socketio/socket.io/commit/161091dd4c9e1b1610ac3d45d964195e63d92b94))
* **esm:** export the Namespace and Socket class ([#3699](https://github.com/socketio/socket.io/issues/3699)) ([233650c](https://github.com/socketio/socket.io/commit/233650c22209708b5fccc4349c38d2fa1b465d8f))
* add support for Socket.IO v2 clients ([9925746](https://github.com/socketio/socket.io/commit/9925746c8ee3a6522bd640b5d586c83f04f2f1ba))
* add room events ([155fa63](https://github.com/socketio/socket.io-adapter/commit/155fa6333a504036e99a33667dc0397f6aede25e))
### Bug Fixes
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)

View File

@@ -1,6 +1,5 @@
# socket.io
[![Run on Repl.it](https://repl.it/badge/github/socketio/socket.io)](https://repl.it/github/socketio/socket.io)
[![Backers on Open Collective](https://opencollective.com/socketio/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/socketio/sponsors/badge.svg)](#sponsors)
[![Build Status](https://github.com/socketio/socket.io/workflows/CI/badge.svg)](https://github.com/socketio/socket.io/actions)
[![Dependency Status](https://david-dm.org/socketio/socket.io.svg)](https://david-dm.org/socketio/socket.io)
@@ -22,6 +21,8 @@ Some implementations in other languages are also available:
- [C++](https://github.com/socketio/socket.io-client-cpp)
- [Swift](https://github.com/socketio/socket.io-client-swift)
- [Dart](https://github.com/rikulo/socket.io-client-dart)
- [Python](https://github.com/miguelgrinberg/python-socketio)
- [.Net](https://github.com/Quobject/SocketIoClientDotNet)
Its main features are:
@@ -35,7 +36,7 @@ For this purpose, it relies on [Engine.IO](https://github.com/socketio/engine.io
#### Auto-reconnection support
Unless instructed otherwise a disconnected client will try to reconnect forever, until the server is available again. Please see the available reconnection options [here](https://github.com/socketio/socket.io-client/blob/master/docs/API.md#new-managerurl-options).
Unless instructed otherwise a disconnected client will try to reconnect forever, until the server is available again. Please see the available reconnection options [here](https://socket.io/docs/v3/client-api/#new-Manager-url-options).
#### Disconnection detection
@@ -84,7 +85,11 @@ This is a useful feature to send notifications to a group of users, or to a give
## Installation
```bash
// with npm
npm install socket.io
// with yarn
yarn add socket.io
```
## How to use

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,36 @@
$(function() {
var FADE_TIME = 150; // ms
var TYPING_TIMER_LENGTH = 400; // ms
var COLORS = [
const FADE_TIME = 150; // ms
const TYPING_TIMER_LENGTH = 400; // ms
const COLORS = [
'#e21400', '#91580f', '#f8a700', '#f78b00',
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
];
// Initialize variables
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
const $window = $(window);
const $usernameInput = $('.usernameInput'); // Input for username
const $messages = $('.messages'); // Messages area
const $inputMessage = $('.inputMessage'); // Input message input box
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
const $loginPage = $('.login.page'); // The login page
const $chatPage = $('.chat.page'); // The chatroom page
const socket = io();
// Prompt for setting a username
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();
var socket = io();
let username;
let connected = false;
let typing = false;
let lastTypingTime;
let $currentInput = $usernameInput.focus();
const addParticipantsMessage = (data) => {
var message = '';
let message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
message += `there's 1 participant`;
} else {
message += "there are " + data.numUsers + " participants";
message += `there are ${data.numUsers} participants`;
}
log(message);
}
@@ -53,45 +53,41 @@ $(function() {
// Sends a chat message
const sendMessage = () => {
var message = $inputMessage.val();
let message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
// if there is a non-empty message and a socket connection
if (message && connected) {
$inputMessage.val('');
addChatMessage({
username: username,
message: message
});
addChatMessage({ username, message });
// tell server to execute 'new message' and send along one parameter
socket.emit('new message', message);
}
}
// Log a message
const log = (message, options) => {
var $el = $('<li>').addClass('log').text(message);
const log = (message, options) => {
const $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
}
// Adds the visual chat message to the message list
const addChatMessage = (data, options) => {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
const $typingMessages = getTypingMessages(data);
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
var $usernameDiv = $('<span class="username"/>')
const $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
const $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);
var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
const typingClass = data.typing ? 'typing' : '';
const $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.append($usernameDiv, $messageBodyDiv);
@@ -119,8 +115,7 @@ $(function() {
// options.prepend - If the element should prepend
// all other messages (default = false)
const addMessageElement = (el, options) => {
var $el = $(el);
const $el = $(el);
// Setup default options
if (!options) {
options = {};
@@ -141,6 +136,7 @@ $(function() {
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight;
}
@@ -159,8 +155,8 @@ $(function() {
lastTypingTime = (new Date()).getTime();
setTimeout(() => {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
const typingTimer = (new Date()).getTime();
const timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
@@ -179,12 +175,12 @@ $(function() {
// Gets the color of a username through our hash function
const getUsernameColor = (username) => {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
let hash = 7;
for (let i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
var index = Math.abs(hash % COLORS.length);
const index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
@@ -229,7 +225,7 @@ $(function() {
socket.on('login', (data) => {
connected = true;
// Display the welcome message
var message = "Welcome to Socket.IO Chat ";
const message = 'Welcome to Socket.IO Chat ';
log(message, {
prepend: true
});
@@ -243,13 +239,13 @@ $(function() {
// Whenever the server emits 'user joined', log it in the chat body
socket.on('user joined', (data) => {
log(data.username + ' joined');
log(`${data.username} joined`);
addParticipantsMessage(data);
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', (data) => {
log(data.username + ' left');
log(`${data.username} left`);
addParticipantsMessage(data);
removeChatTyping(data);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
import debugModule = require("debug");
import url = require("url");
import type { IncomingMessage } from "http";
import type { Namespace, Server } from "./index";
import type { Socket } from "./socket";
@@ -77,7 +78,7 @@ export class Client {
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}) {
private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
@@ -112,7 +113,7 @@ export class Client {
*
* @private
*/
private doConnect(name: string, auth: object) {
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name);
const socket = nsp._add(this, auth, () => {
@@ -131,7 +132,7 @@ export class Client {
*
* @private
*/
_disconnect() {
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect();
}
@@ -144,7 +145,7 @@ export class Client {
*
* @private
*/
_remove(socket: Socket) {
_remove(socket: Socket): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id);
@@ -159,8 +160,8 @@ export class Client {
*
* @private
*/
private close() {
if ("open" == this.conn.readyState) {
private close(): void {
if ("open" === this.conn.readyState) {
debug("forcing transport close");
this.conn.close();
this.onclose("forced server close");
@@ -174,19 +175,20 @@ export class Client {
* @param {Object} opts
* @private
*/
_packet(packet, opts?) {
_packet(packet: Packet, opts?: any): void {
opts = opts || {};
const self = this;
// this writes to the actual connection
function writeToEngine(encodedPackets) {
function writeToEngine(encodedPackets: any) {
// TODO clarify this.
if (opts.volatile && !self.conn.transport.writable) return;
for (let i = 0; i < encodedPackets.length; i++) {
self.conn.write(encodedPackets[i], { compress: opts.compress });
}
}
if ("open" == this.conn.readyState) {
if ("open" === this.conn.readyState) {
debug("writing packet %j", packet);
if (!opts.preEncoded) {
// not broadcasting, need to encode
@@ -205,7 +207,7 @@ export class Client {
*
* @private
*/
private ondata(data) {
private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data);
@@ -219,9 +221,14 @@ export class Client {
*
* @private
*/
private ondecoded(packet: Packet) {
if (PacketType.CONNECT == packet.type) {
this.connect(packet.nsp, packet.data);
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
this.connect(parsed.pathname!, parsed.query);
} else {
this.connect(packet.nsp, packet.data);
}
} else {
const socket = this.nsps.get(packet.nsp);
if (socket) {
@@ -240,7 +247,7 @@ export class Client {
* @param {Object} err object
* @private
*/
private onerror(err) {
private onerror(err): void {
for (const socket of this.sockets.values()) {
socket._onerror(err);
}
@@ -253,7 +260,7 @@ export class Client {
* @param reason
* @private
*/
private onclose(reason: string) {
private onclose(reason: string): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
@@ -272,7 +279,7 @@ export class Client {
* Cleans up event listeners.
* @private
*/
private destroy() {
private destroy(): void {
this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose);

View File

@@ -25,7 +25,7 @@ const dotMapRegex = /\.map/;
type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
query: object,
auth: { [key: string]: any },
fn: (err: Error | null, success: boolean) => void
) => void;
@@ -100,6 +100,11 @@ interface EngineOptions {
* the options that will be forwarded to the cors module
*/
cors: CorsOptions;
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3: boolean;
}
interface AttachOptions {
@@ -186,6 +191,10 @@ export class Server extends EventEmitter {
*/
constructor(opts?: Partial<ServerOptions>);
constructor(srv?: http.Server | number, opts?: Partial<ServerOptions>);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
opts?: Partial<ServerOptions>
);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
opts: Partial<ServerOptions> = {}
@@ -217,9 +226,10 @@ export class Server extends EventEmitter {
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): Server;
public serveClient(v: boolean): this;
public serveClient(): boolean;
public serveClient(v?: boolean): Server | boolean {
public serveClient(v?: boolean): this | boolean;
public serveClient(v?: boolean): this | boolean {
if (!arguments.length) return this._serveClient;
this._serveClient = v!;
return this;
@@ -229,16 +239,16 @@ export class Server extends EventEmitter {
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param name - name of incoming namespace
* @param {Object} auth - the auth parameters
* @param auth - the auth parameters
* @param fn - callback
*
* @private
*/
_checkNamespace(
name: string,
auth: object,
auth: { [key: string]: any },
fn: (nsp: Namespace | false) => void
) {
): void {
if (this.parentNsps.size === 0) return fn(false);
const keysIterator = this.parentNsps.keys();
@@ -267,9 +277,10 @@ export class Server extends EventEmitter {
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): Server;
public path(v: string): this;
public path(): string;
public path(v?: string): Server | string {
public path(v?: string): this | string;
public path(v?: string): this | string {
if (!arguments.length) return this._path;
this._path = v!.replace(/\/$/, "");
@@ -288,9 +299,10 @@ export class Server extends EventEmitter {
* @param v
* @public
*/
public connectTimeout(v: number): Server;
public connectTimeout(v: number): this;
public connectTimeout(): number;
public connectTimeout(v?: number): Server | number {
public connectTimeout(v?: number): this | number;
public connectTimeout(v?: number): this | number {
if (v === undefined) return this._connectTimeout;
this._connectTimeout = v;
return this;
@@ -304,8 +316,9 @@ export class Server extends EventEmitter {
* @public
*/
public adapter(): typeof Adapter | undefined;
public adapter(v: typeof Adapter): Server;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | Server {
public adapter(v: typeof Adapter): this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this {
if (!arguments.length) return this._adapter;
this._adapter = v;
for (const nsp of this._nsps.values()) {
@@ -325,7 +338,7 @@ export class Server extends EventEmitter {
public listen(
srv: http.Server | number,
opts: Partial<ServerOptions> = {}
): Server {
): this {
return this.attach(srv, opts);
}
@@ -340,7 +353,7 @@ export class Server extends EventEmitter {
public attach(
srv: http.Server | number,
opts: Partial<ServerOptions> = {}
): Server {
): this {
if ("function" == typeof srv) {
const msg =
"You are trying to attach socket.io to an express " +
@@ -380,7 +393,10 @@ export class Server extends EventEmitter {
* @param opts - options passed to engine.io
* @private
*/
private initEngine(srv: http.Server, opts: Partial<EngineAttachOptions>) {
private initEngine(
srv: http.Server,
opts: Partial<EngineAttachOptions>
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts);
this.eio = engine.attach(srv, opts);
@@ -401,7 +417,7 @@ export class Server extends EventEmitter {
* @param srv http server
* @private
*/
private attachServe(srv: http.Server) {
private attachServe(srv: http.Server): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
@@ -424,7 +440,7 @@ export class Server extends EventEmitter {
* @param res
* @private
*/
private serve(req: http.IncomingMessage, res: http.ServerResponse) {
private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
const filename = req.url!.replace(this._path, "");
const isMap = dotMapRegex.test(filename);
const type = isMap ? "map" : "source";
@@ -432,10 +448,11 @@ export class Server extends EventEmitter {
// Per the standard, ETags must be quoted:
// https://tools.ietf.org/html/rfc7232#section-2.3
const expectedEtag = '"' + clientVersion + '"';
const weakEtag = "W/" + expectedEtag;
const etag = req.headers["if-none-match"];
if (etag) {
if (expectedEtag == etag) {
if (expectedEtag === etag || weakEtag === etag) {
debug("serve client %s 304", type);
res.writeHead(304);
res.end();
@@ -468,7 +485,7 @@ export class Server extends EventEmitter {
filename: string,
req: http.IncomingMessage,
res: http.ServerResponse
) {
): void {
const readStream = createReadStream(
path.join(__dirname, "../client-dist/", filename)
);
@@ -507,7 +524,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public bind(engine): Server {
public bind(engine): this {
this.engine = engine;
this.engine.on("connection", this.onconnection.bind(this));
return this;
@@ -520,9 +537,13 @@ export class Server extends EventEmitter {
* @return self
* @private
*/
private onconnection(conn): Server {
private onconnection(conn): this {
debug("incoming connection with id %s", conn.id);
new Client(this, conn);
const client = new Client(this, conn);
if (conn.protocol === 3) {
// @ts-ignore
client.connect("/");
}
return this;
}
@@ -530,13 +551,13 @@ export class Server extends EventEmitter {
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @param [fn] optional, nsp `connection` ev handler
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket) => void
) {
): Namespace {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this);
debug("initializing parent namespace %s", parentNsp.name);
@@ -595,7 +616,7 @@ export class Server extends EventEmitter {
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Server {
): this {
this.sockets.use(fn);
return this;
}
@@ -607,7 +628,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public to(name: Room): Server {
public to(name: Room): this {
this.sockets.to(name);
return this;
}
@@ -619,7 +640,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public in(name: Room): Server {
public in(name: Room): this {
this.sockets.in(name);
return this;
}
@@ -630,7 +651,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Server {
public send(...args: readonly any[]): this {
this.sockets.emit("message", ...args);
return this;
}
@@ -641,7 +662,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Server {
public write(...args: readonly any[]): this {
this.sockets.emit("message", ...args);
return this;
}
@@ -662,7 +683,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public compress(compress: boolean): Server {
public compress(compress: boolean): this {
this.sockets.compress(compress);
return this;
}
@@ -675,7 +696,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get volatile(): Server {
public get volatile(): this {
this.sockets.volatile;
return this;
}
@@ -686,7 +707,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get local(): Server {
public get local(): this {
this.sockets.local;
return this;
}

View File

@@ -67,7 +67,7 @@ export class Namespace extends EventEmitter {
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Namespace {
): this {
this._fns.push(fn);
return this;
}
@@ -106,7 +106,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public to(name: Room): Namespace {
public to(name: Room): this {
this._rooms.add(name);
return this;
}
@@ -118,7 +118,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public in(name: Room): Namespace {
public in(name: Room): this {
this._rooms.add(name);
return this;
}
@@ -135,11 +135,16 @@ export class Namespace extends EventEmitter {
this.run(socket, (err) => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err)
return socket._error({
message: err.message,
data: err.data,
});
if (err) {
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message);
} else {
return socket._error({
message: err.message,
data: err.data,
});
}
}
// track socket
this.sockets.set(socket.id, socket);
@@ -217,7 +222,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Namespace {
public send(...args: readonly any[]): this {
this.emit("message", ...args);
return this;
}
@@ -228,7 +233,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Namespace {
public write(...args: readonly any[]): this {
this.emit("message", ...args);
return this;
}
@@ -257,7 +262,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public compress(compress: boolean): Namespace {
public compress(compress: boolean): this {
this._flags.compress = compress;
return this;
}
@@ -270,7 +275,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public get volatile(): Namespace {
public get volatile(): this {
this._flags.volatile = true;
return this;
}
@@ -281,7 +286,7 @@ export class Namespace extends EventEmitter {
* @return self
* @public
*/
public get local(): Namespace {
public get local(): this {
this._flags.local = true;
return this;
}

View File

@@ -1,11 +1,11 @@
import { EventEmitter } from "events";
import { PacketType } from "socket.io-parser";
import { Packet, PacketType } from "socket.io-parser";
import url = require("url");
import debugModule from "debug";
import type { Server } from "./index";
import type { Client } from "./client";
import type { Namespace } from "./namespace";
import type { IncomingMessage } from "http";
import type { IncomingMessage, IncomingHttpHeaders } from "http";
import type {
Adapter,
BroadcastFlags,
@@ -13,6 +13,7 @@ import type {
SocketId,
} from "socket.io-adapter";
import base64id from "base64id";
import type { ParsedUrlQuery } from "querystring";
const debug = debugModule("socket.io:socket");
@@ -33,7 +34,7 @@ export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: object;
headers: IncomingHttpHeaders;
/**
* The date of creation (as string)
@@ -68,12 +69,12 @@ export interface Handshake {
/**
* The query object
*/
query: object;
query: ParsedUrlQuery;
/**
* The auth object
*/
auth: object;
auth: { [key: string]: any };
}
export class Socket extends EventEmitter {
@@ -105,7 +106,12 @@ export class Socket extends EventEmitter {
super();
this.server = nsp.server;
this.adapter = this.nsp.adapter;
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
if (client.conn.protocol === 3) {
// @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id;
} else {
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
}
this.connected = true;
this.disconnected = false;
this.handshake = this.buildHandshake(auth);
@@ -185,7 +191,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public to(name: Room): Socket {
public to(name: Room): this {
this._rooms.add(name);
return this;
}
@@ -197,7 +203,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public in(name: Room): Socket {
public in(name: Room): this {
this._rooms.add(name);
return this;
}
@@ -208,7 +214,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): Socket {
public send(...args: readonly any[]): this {
this.emit("message", ...args);
return this;
}
@@ -219,7 +225,7 @@ export class Socket extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): Socket {
public write(...args: readonly any[]): this {
this.emit("message", ...args);
return this;
}
@@ -231,10 +237,13 @@ export class Socket extends EventEmitter {
* @param {Object} opts - options
* @private
*/
private packet(packet, opts: any = {}) {
private packet(
packet: Omit<Packet, "nsp"> & Partial<Pick<Packet, "nsp">>,
opts: any = {}
): void {
packet.nsp = this.nsp.name;
opts.compress = false !== opts.compress;
this.client._packet(packet, opts);
this.client._packet(packet as Packet, opts);
}
/**
@@ -286,7 +295,11 @@ export class Socket extends EventEmitter {
_onconnect(): void {
debug("socket connected - writing packet");
this.join(this.id);
this.packet({ type: PacketType.CONNECT, data: { sid: this.id } });
if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT });
} else {
this.packet({ type: PacketType.CONNECT, data: { sid: this.id } });
}
}
/**
@@ -295,7 +308,7 @@ export class Socket extends EventEmitter {
* @param {Object} packet
* @private
*/
_onpacket(packet) {
_onpacket(packet: Packet): void {
debug("got packet %j", packet);
switch (packet.type) {
case PacketType.EVENT:
@@ -326,10 +339,10 @@ export class Socket extends EventEmitter {
/**
* Called upon event packet.
*
* @param {Object} packet - packet object
* @param {Packet} packet - packet object
* @private
*/
private onevent(packet): void {
private onevent(packet: Packet): void {
const args = packet.data || [];
debug("emitting event %j", args);
@@ -353,7 +366,7 @@ export class Socket extends EventEmitter {
* @param {Number} id - packet id
* @private
*/
private ack(id: number) {
private ack(id: number): () => void {
const self = this;
let sent = false;
return function () {
@@ -377,12 +390,12 @@ export class Socket extends EventEmitter {
*
* @private
*/
private onack(packet): void {
const ack = this.acks.get(packet.id);
private onack(packet: Packet): void {
const ack = this.acks.get(packet.id!);
if ("function" == typeof ack) {
debug("calling ack %s with %j", packet.id, packet.data);
ack.apply(this, packet.data);
this.acks.delete(packet.id);
this.acks.delete(packet.id!);
} else {
debug("bad ack %s", packet.id);
}
@@ -420,7 +433,7 @@ export class Socket extends EventEmitter {
*
* @private
*/
_onclose(reason: string): Socket | undefined {
_onclose(reason: string): this | undefined {
if (!this.connected) return this;
debug("closing socket - reason %s", reason);
super.emit("disconnecting", reason);
@@ -440,7 +453,7 @@ export class Socket extends EventEmitter {
*
* @private
*/
_error(err) {
_error(err): void {
this.packet({ type: PacketType.CONNECT_ERROR, data: err });
}
@@ -452,7 +465,7 @@ export class Socket extends EventEmitter {
*
* @public
*/
public disconnect(close = false): Socket {
public disconnect(close = false): this {
if (!this.connected) return this;
if (close) {
this.client._disconnect();
@@ -470,7 +483,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public compress(compress: boolean): Socket {
public compress(compress: boolean): this {
this.flags.compress = compress;
return this;
}
@@ -483,7 +496,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get volatile(): Socket {
public get volatile(): this {
this.flags.volatile = true;
return this;
}
@@ -495,7 +508,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get broadcast(): Socket {
public get broadcast(): this {
this.flags.broadcast = true;
return this;
}
@@ -506,7 +519,7 @@ export class Socket extends EventEmitter {
* @return {Socket} self
* @public
*/
public get local(): Socket {
public get local(): this {
this.flags.local = true;
return this;
}
@@ -517,14 +530,18 @@ export class Socket extends EventEmitter {
* @param {Array} event - event that will get emitted
* @private
*/
private dispatch(event): void {
private dispatch(event: [eventName: string, ...args: any[]]): void {
debug("dispatching an event %j", event);
this.run(event, (err) => {
process.nextTick(() => {
if (err) {
return this._onerror(err);
}
super.emit.apply(this, event);
if (this.connected) {
super.emit.apply(this, event);
} else {
debug("ignore packet received after disconnection");
}
});
});
}
@@ -538,7 +555,7 @@ export class Socket extends EventEmitter {
*/
public use(
fn: (event: Array<any>, next: (err: Error) => void) => void
): Socket {
): this {
this.fns.push(fn);
return this;
}
@@ -550,11 +567,14 @@ export class Socket extends EventEmitter {
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(event: Array<any>, fn: (err: Error | null) => void) {
private run(
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
const fns = this.fns.slice(0);
if (!fns.length) return fn(null);
function run(i) {
function run(i: number) {
fns[i](event, function (err) {
// upon error, short-circuit
if (err) return fn(err);
@@ -602,7 +622,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): Socket {
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
this._anyListeners.push(listener);
return this;
@@ -615,7 +635,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): Socket {
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
this._anyListeners.unshift(listener);
return this;
@@ -627,7 +647,7 @@ export class Socket extends EventEmitter {
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): Socket {
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
return this;
}

179
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "3.0.5",
"version": "3.1.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -462,6 +462,12 @@
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
"dev": true
},
"aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -535,6 +541,12 @@
"sprintf-js": "~1.0.2"
}
},
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -584,6 +596,12 @@
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -722,11 +740,23 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
"dev": true
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -830,9 +860,9 @@
"dev": true
},
"engine.io": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.6.tgz",
"integrity": "sha512-rf7HAVZpcRrcKEKddgIzYUnwg0g5HE1RvJaTLwkcfJmce4g+po8aMuE6vxzp6JwlK8FEq/vi0KWN6tA585DjaA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.0.tgz",
"integrity": "sha512-vW7EAtn0HDQ4MtT5QbmCHF17TaYLONv2/JwdYsq9USPRZVM4zG7WB3k0Nc321z8EuSOlhGokrYlYx4176QhD0A==",
"requires": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
@@ -844,9 +874,9 @@
}
},
"engine.io-client": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.0.6.tgz",
"integrity": "sha512-5lPh8rrhxIruo5ZlgFt31KM626o5OCXrCHBweieWWuVicDtnYdz/iR93k6N9k0Xs61WrYxZKIWXzeSaJF6fpNA==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.2.tgz",
"integrity": "sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ==",
"dev": true,
"requires": {
"base64-arraybuffer": "0.1.4",
@@ -1314,6 +1344,15 @@
"function-bind": "^1.1.1"
}
},
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"dev": true,
"requires": {
"isarray": "2.0.1"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
@@ -1376,6 +1415,12 @@
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -1440,6 +1485,12 @@
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
"dev": true
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2202,23 +2253,115 @@
}
},
"socket.io-adapter": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz",
"integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz",
"integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg=="
},
"socket.io-client": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.0.5.tgz",
"integrity": "sha512-NNnv3UH5h+aICeVDAdSHll3vSujp1OnzvDtuVz1ukUXliffr1+LTGc1W+qZAm3H7McapGsJhTI5nsBoY1r21dQ==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.2.tgz",
"integrity": "sha512-fXhF8plHrd7U14A7K0JPOmZzpmGkLpIS6623DzrBZqYzI/yvlP4fA3LnxwthEVgiHmn2uJ4KjdnQD8A03PuBWQ==",
"dev": true,
"requires": {
"@types/component-emitter": "^1.2.10",
"backo2": "~1.0.2",
"component-emitter": "~1.3.0",
"debug": "~4.3.1",
"engine.io-client": "~4.0.6",
"engine.io-client": "~4.1.0",
"parseuri": "0.0.6",
"socket.io-parser": "~4.0.3"
"socket.io-parser": "~4.0.4"
},
"dependencies": {
"socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"dev": true,
"requires": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
}
}
}
},
"socket.io-client-v2": {
"version": "npm:socket.io-client@2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz",
"integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==",
"dev": true,
"requires": {
"backo2": "1.0.2",
"component-bind": "1.0.0",
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"engine.io-client": "~3.5.0",
"has-binary2": "~1.0.2",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"engine.io-client": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz",
"integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
}
},
"engine.io-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
"dev": true,
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.4",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"socket.io-parser": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"isarray": "2.0.1"
}
}
}
},
"socket.io-parser": {
@@ -2408,6 +2551,12 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
"dev": true
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "3.0.5",
"version": "3.1.2",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -45,12 +45,12 @@
"dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": "^14.14.10",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~4.0.6",
"socket.io-adapter": "~2.0.3",
"engine.io": "~4.1.0",
"socket.io-adapter": "~2.1.0",
"socket.io-parser": "~4.0.3"
},
"devDependencies": {
@@ -63,7 +63,8 @@
"nyc": "^15.1.0",
"prettier": "^2.2.0",
"rimraf": "^3.0.2",
"socket.io-client": "3.0.5",
"socket.io-client": "3.1.2",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^6.1.0",
"supertest": "^6.0.1",
"ts-node": "^9.0.0",

View File

@@ -8,11 +8,11 @@ import { exec } from "child_process";
import request from "supertest";
import expect from "expect.js";
import type { AddressInfo } from "net";
import * as io_v2 from "socket.io-client-v2";
const ioc = require("socket.io-client");
import "./support/util";
import exp = require("constants");
// Creates a socket.io client for the given server
function client(srv, nsp?: string | object, opts?: object) {
@@ -26,6 +26,18 @@ function client(srv, nsp?: string | object, opts?: object) {
return ioc(url, opts);
}
const success = (sio, clientSocket, done) => {
sio.close();
clientSocket.close();
done();
};
const waitFor = (emitter, event) => {
return new Promise((resolve) => {
emitter.once(event, resolve);
});
};
describe("socket.io", () => {
it("should be the same version as client", () => {
const version = require("../package").version;
@@ -116,6 +128,19 @@ describe("socket.io", () => {
});
});
it("should handle 304", (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/socket.io.js")
.set("If-None-Match", 'W/"' + clientVersion + '"')
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.be(304);
done();
});
});
it("should not serve static files", (done) => {
const srv = createServer();
new Server(srv, { serveClient: false });
@@ -1762,6 +1787,33 @@ describe("socket.io", () => {
});
});
it("should ignore a packet received after disconnection", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv);
const success = () => {
clientSocket.close();
sio.close();
done();
};
sio.on("connection", (socket) => {
socket.on("test", () => {
done(new Error("should not happen"));
});
socket.on("disconnect", success);
});
clientSocket.on("connect", () => {
clientSocket.emit("test", Buffer.alloc(10));
clientSocket.disconnect();
});
});
});
describe("onAny", () => {
it("should call listener", (done) => {
const srv = createServer();
@@ -2430,4 +2482,74 @@ describe("socket.io", () => {
});
});
});
describe("v2 compatibility", () => {
it("should connect if `allowEIO3` is true", (done) => {
const srv = createServer();
const sio = new Server(srv, {
allowEIO3: true,
});
srv.listen(async () => {
const port = (srv.address() as AddressInfo).port;
const clientSocket = io_v2.connect(`http://localhost:${port}`, {
multiplex: false,
});
const [socket]: Array<any> = await Promise.all([
waitFor(sio, "connection"),
waitFor(clientSocket, "connect"),
]);
expect(socket.id).to.eql(clientSocket.id);
success(sio, clientSocket, done);
});
});
it("should be able to connect to a namespace with a query", (done) => {
const srv = createServer();
const sio = new Server(srv, {
allowEIO3: true,
});
srv.listen(async () => {
const port = (srv.address() as AddressInfo).port;
const clientSocket = io_v2.connect(
`http://localhost:${port}/the-namespace`,
{
multiplex: false,
}
);
clientSocket.query = { test: "123" };
const [socket]: Array<any> = await Promise.all([
waitFor(sio.of("/the-namespace"), "connection"),
waitFor(clientSocket, "connect"),
]);
expect(socket.handshake.auth).to.eql({ test: "123" });
success(sio, clientSocket, done);
});
});
it("should not connect if `allowEIO3` is false (default)", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const port = (srv.address() as AddressInfo).port;
const clientSocket = io_v2.connect(`http://localhost:${port}`, {
multiplex: false,
});
clientSocket.on("connect", () => {
done(new Error("should not happen"));
});
clientSocket.on("connect_error", () => {
success(sio, clientSocket, done);
});
});
});
});
});

View File

@@ -1,3 +1,3 @@
import io from "./dist/index.js";
export const Server = io.Server;
export const {Server, Namespace, Socket} = io;