style: run prettier

This commit is contained in:
Jonas Gloning 2023-02-14 20:49:59 +01:00
parent fad2041a5e
commit d38066a391
No known key found for this signature in database
GPG Key ID: 684639B5E59E7614
45 changed files with 26336 additions and 26161 deletions

View File

@ -1,37 +1,37 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"env": { "env": {
"node": true, "node": true,
"es6": true, "es6": true,
"mocha": true "mocha": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018, "ecmaVersion": 2018,
"sourceType": "module" "sourceType": "module"
}, },
"rules": { "rules": {
"no-var": "error", "no-var": "error",
"no-console": "off", "no-console": "off",
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",
"@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [ "@typescript-eslint/member-delimiter-style": [
"error", "error",
{ {
"multiline": { "multiline": {
"delimiter": "semi", "delimiter": "semi",
"requireLast": true "requireLast": true
}, },
"singleline": { "singleline": {
"delimiter": "semi", "delimiter": "semi",
"requireLast": true "requireLast": true
} }
} }
], ],
"@typescript-eslint/explicit-function-return-type": "off" "@typescript-eslint/explicit-function-return-type": "off"
} }
} }

View File

@ -1,28 +1,30 @@
--- ---
name: peer template name: peer template
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ""
labels: '' labels: ""
assignees: '' assignees: ""
--- ---
### I'm having an issue: ### I'm having an issue:
- Give an expressive description of what is went wrong
- Version of `peer` you're experiencing this issue - Give an expressive description of what is went wrong
- Nodejs version? - Version of `peer` you're experiencing this issue
- Platform name and its version (Win, Mac, Linux)? - Nodejs version?
- Nice to have: a repository with code to reproduce the issue - Platform name and its version (Win, Mac, Linux)?
- If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot - Nice to have: a repository with code to reproduce the issue
- If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot
### I have a suggestion: ### I have a suggestion:
- Describe your feature / request
- How you're going to use it? Give a usage example(s) - Describe your feature / request
- How you're going to use it? Give a usage example(s)
### Documentation is missing something or incorrect (have typos, etc.): ### Documentation is missing something or incorrect (have typos, etc.):
- Give an expressive description what you have changed/added and why
- Make sure you're using correct markdown markup - Give an expressive description what you have changed/added and why
- Make sure all code blocks starts with triple ``` (*backtick*) and have a syntax tag, for more read [this docs](https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting) - Make sure you're using correct markdown markup
- Post addition/changes in issue, we will manage it - Make sure all code blocks starts with triple ``` (_backtick_) and have a syntax tag, for more read [this docs](https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting)
- Post addition/changes in issue, we will manage it
## Thank you, and do not forget to get rid of this default message ## Thank you, and do not forget to get rid of this default message

View File

@ -1,28 +1,28 @@
name: Docker build & publish name: Docker build & publish
on: on:
push: push:
branches: [ "master" ] branches: ["master"]
pull_request: pull_request:
branches: [ "master" ] branches: ["master"]
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build - name: Build
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
- name: Build & publish - name: Build & publish
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
push: true push: true
tags: peerjs/peerjs-server-test:nightly tags: peerjs/peerjs-server-test:nightly

View File

@ -2,8 +2,8 @@ name: Fly Deploy
on: on:
push: push:
branches: branches:
- master - master
env: env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
@ -13,6 +13,6 @@ jobs:
name: Deploy app name: Deploy app
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master - uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only - run: flyctl deploy --remote-only

View File

@ -5,13 +5,12 @@ name: Node.js CI
on: on:
push: push:
branches: [ "master" ] branches: ["master"]
pull_request: pull_request:
branches: [ "master" ] branches: ["master"]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -20,17 +19,17 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' cache: "npm"
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- run: npm run lint - run: npm run lint
- run: npm run coverage - run: npm run coverage
- name: Publish code coverage to CodeClimate - name: Publish code coverage to CodeClimate
uses: paambaati/codeclimate-action@v3.2.0 uses: paambaati/codeclimate-action@v3.2.0
env: env:
CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}

View File

@ -1,31 +1,31 @@
{ {
"branches": [ "branches": [
"stable", "stable",
{ {
"name": "rc", "name": "rc",
"prerelease": true "prerelease": true
} }
], ],
"plugins": [ "plugins": [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@semantic-release/changelog", "@semantic-release/changelog",
"@semantic-release/npm", "@semantic-release/npm",
"@semantic-release/git", "@semantic-release/git",
"@semantic-release/github", "@semantic-release/github",
[ [
"@codedependant/semantic-release-docker", "@codedependant/semantic-release-docker",
{ {
"dockerTags": [ "dockerTags": [
"{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", "{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}",
"{{major}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", "{{major}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}",
"{{major}}.{{minor}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", "{{major}}.{{minor}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}",
"{{version}}" "{{version}}"
], ],
"dockerImage": "peerjs-server", "dockerImage": "peerjs-server",
"dockerFile": "Dockerfile", "dockerFile": "Dockerfile",
"dockerProject": "peerjs" "dockerProject": "peerjs"
} }
] ]
] ]
} }

View File

@ -3,8 +3,8 @@
**We do not collect or store any information.** **We do not collect or store any information.**
While you are connected to a PeerJS server, your IP address, randomly-generated While you are connected to a PeerJS server, your IP address, randomly-generated
client ID, and signalling data are kept in the server's memory. With default client ID, and signalling data are kept in the server's memory. With default
settings, the server will remove this information from memory 60 seconds after settings, the server will remove this information from memory 60 seconds after
you stop communicating with the service. (See the you stop communicating with the service. (See the
[`alive_timeout`](https://github.com/peers/peerjs-server#config--cli-options) [`alive_timeout`](https://github.com/peers/peerjs-server#config--cli-options)
setting.) setting.)

150
README.md
View File

@ -4,7 +4,8 @@
[![npm version](https://badge.fury.io/js/peer.svg)](https://www.npmjs.com/package/peer) [![npm version](https://badge.fury.io/js/peer.svg)](https://www.npmjs.com/package/peer)
[![Downloads](https://img.shields.io/npm/dm/peer.svg)](https://www.npmjs.com/package/peer) [![Downloads](https://img.shields.io/npm/dm/peer.svg)](https://www.npmjs.com/package/peer)
[![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/peerjs/peerjs-server)](https://hub.docker.com/r/peerjs/peerjs-server) [![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/peerjs/peerjs-server)](https://hub.docker.com/r/peerjs/peerjs-server)
# PeerServer: A server for PeerJS #
# PeerServer: A server for PeerJS
PeerServer helps establishing connections between PeerJS clients. Data is not proxied through the server. PeerServer helps establishing connections between PeerJS clients. Data is not proxied through the server.
@ -23,20 +24,23 @@ Run your own server on Gitpod!
If you don't want to develop anything, just enter few commands below. If you don't want to develop anything, just enter few commands below.
1. Install the package globally: 1. Install the package globally:
```sh ```sh
$ npm install peer -g $ npm install peer -g
``` ```
2. Run the server: 2. Run the server:
```sh
$ peerjs --port 9000 --key peerjs --path /myapp
Started PeerServer on ::, port: 9000, path: /myapp (v. 0.3.2) ```sh
``` $ peerjs --port 9000 --key peerjs --path /myapp
Started PeerServer on ::, port: 9000, path: /myapp (v. 0.3.2)
```
3. Check it: http://127.0.0.1:9000/myapp It should returns JSON with name, description and website fields. 3. Check it: http://127.0.0.1:9000/myapp It should returns JSON with name, description and website fields.
#### Docker #### Docker
Also, you can use Docker image to run a new container: Also, you can use Docker image to run a new container:
```sh ```sh
$ docker run -p 9000:9000 -d peerjs/peerjs-server $ docker run -p 9000:9000 -d peerjs/peerjs-server
``` ```
@ -48,24 +52,28 @@ $ kubectl run peerjs-server --image=peerjs/peerjs-server --port 9000 --expose --
``` ```
### Create a custom server: ### Create a custom server:
If you have your own server, you can attach PeerServer. If you have your own server, you can attach PeerServer.
1. Install the package: 1. Install the package:
```bash
# $ cd your-project-path
# with npm ```bash
$ npm install peer # $ cd your-project-path
# with npm
$ npm install peer
# with yarn
$ yarn add peer
```
# with yarn
$ yarn add peer
```
2. Use PeerServer object to create a new server: 2. Use PeerServer object to create a new server:
```javascript
const { PeerServer } = require('peer');
const peerServer = PeerServer({ port: 9000, path: '/myapp' }); ```javascript
``` const { PeerServer } = require("peer");
const peerServer = PeerServer({ port: 9000, path: "/myapp" });
```
3. Check it: http://127.0.0.1:9000/myapp It should returns JSON with name, description and website fields. 3. Check it: http://127.0.0.1:9000/myapp It should returns JSON with name, description and website fields.
@ -73,19 +81,20 @@ If you have your own server, you can attach PeerServer.
```html ```html
<script> <script>
const peer = new Peer('someid', { const peer = new Peer("someid", {
host: 'localhost', host: "localhost",
port: 9000, port: 9000,
path: '/myapp' path: "/myapp",
}); });
</script> </script>
``` ```
## Config / CLI options ## Config / CLI options
You can provide config object to `PeerServer` function or specify options for `peerjs` CLI. You can provide config object to `PeerServer` function or specify options for `peerjs` CLI.
| CLI option | JS option | Description | Required | Default | | CLI option | JS option | Description | Required | Default |
|--------------------------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:----------:| | ------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :--------: |
| `--port, -p` | `port` | Port to listen (number) | **Yes** | | | `--port, -p` | `port` | Port to listen (number) | **Yes** | |
| `--key, -k` | `key` | Connection key (string). Client must provide it to call API methods | No | `"peerjs"` | | `--key, -k` | `key` | Connection key (string). Client must provide it to call API methods | No | `"peerjs"` |
| `--path` | `path` | Path (string). The server responds for requests to the root URL + path. **E.g.** Set the `path` to `/myapp` and run server on 9000 port via `peerjs --port 9000 --path /myapp` Then open http://127.0.0.1:9000/myapp - you should see a JSON reponse. | No | `"/"` | | `--path` | `path` | Path (string). The server responds for requests to the root URL + path. **E.g.** Set the `path` to `/myapp` and run server on 9000 port via `peerjs --port 9000 --path /myapp` Then open http://127.0.0.1:9000/myapp - you should see a JSON reponse. | No | `"/"` |
@ -98,39 +107,40 @@ You can provide config object to `PeerServer` function or specify options for `p
| `--allow_discovery` | `allow_discovery` | Allow to use GET `/peers` http API method to get an array of ids of all connected clients (boolean) | No | | | `--allow_discovery` | `allow_discovery` | Allow to use GET `/peers` http API method to get an array of ids of all connected clients (boolean) | No | |
| `--cors` | `corsOptions` | The CORS origins that can access this server | | `--cors` | `corsOptions` | The CORS origins that can access this server |
| | `generateClientId` | A function which generate random client IDs when calling `/id` API method (`() => string`) | No | `uuid/v4` | | | `generateClientId` | A function which generate random client IDs when calling `/id` API method (`() => string`) | No | `uuid/v4` |
## Using HTTPS ## Using HTTPS
Simply pass in PEM-encoded certificate and key. Simply pass in PEM-encoded certificate and key.
```javascript ```javascript
const fs = require('fs'); const fs = require("fs");
const { PeerServer } = require('peer'); const { PeerServer } = require("peer");
const peerServer = PeerServer({ const peerServer = PeerServer({
port: 9000, port: 9000,
ssl: { ssl: {
key: fs.readFileSync('/path/to/your/ssl/key/here.key'), key: fs.readFileSync("/path/to/your/ssl/key/here.key"),
cert: fs.readFileSync('/path/to/your/ssl/certificate/here.crt') cert: fs.readFileSync("/path/to/your/ssl/certificate/here.crt"),
} },
}); });
``` ```
You can also pass any other [SSL options accepted by https.createServer](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistenerfrom), such as `SNICallback: You can also pass any other [SSL options accepted by https.createServer](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistenerfrom), such as `SNICallback:
```javascript ```javascript
const fs = require('fs'); const fs = require("fs");
const { PeerServer } = require('peer'); const { PeerServer } = require("peer");
const peerServer = PeerServer({ const peerServer = PeerServer({
port: 9000, port: 9000,
ssl: { ssl: {
SNICallback: (servername, cb) => { SNICallback: (servername, cb) => {
// your code here .... // your code here ....
} },
} },
}); });
``` ```
## Running PeerServer behind a reverse proxy ## Running PeerServer behind a reverse proxy
Make sure to set the `proxied` option, otherwise IP based limiting will fail. Make sure to set the `proxied` option, otherwise IP based limiting will fail.
@ -139,29 +149,31 @@ The option is passed verbatim to the
if it is truthy. if it is truthy.
```javascript ```javascript
const { PeerServer } = require('peer'); const { PeerServer } = require("peer");
const peerServer = PeerServer({ const peerServer = PeerServer({
port: 9000, port: 9000,
path: '/myapp', path: "/myapp",
proxied: true proxied: true,
}); });
``` ```
## Custom client ID generation ## Custom client ID generation
By default, PeerServer uses `uuid/v4` npm package to generate random client IDs. By default, PeerServer uses `uuid/v4` npm package to generate random client IDs.
You can set `generateClientId` option in config to specify a custom function to generate client IDs. You can set `generateClientId` option in config to specify a custom function to generate client IDs.
```javascript ```javascript
const { PeerServer } = require('peer'); const { PeerServer } = require("peer");
const customGenerationFunction = () => (Math.random().toString(36) + '0000000000000000000').substr(2, 16); const customGenerationFunction = () =>
(Math.random().toString(36) + "0000000000000000000").substr(2, 16);
const peerServer = PeerServer({ const peerServer = PeerServer({
port: 9000, port: 9000,
path: '/myapp', path: "/myapp",
generateClientId: customGenerationFunction generateClientId: customGenerationFunction,
}); });
``` ```
@ -170,34 +182,34 @@ Open http://127.0.0.1:9000/myapp/peerjs/id to see a new random id.
## Combining with existing express app ## Combining with existing express app
```javascript ```javascript
const express = require('express'); const express = require("express");
const { ExpressPeerServer } = require('peer'); const { ExpressPeerServer } = require("peer");
const app = express(); const app = express();
app.get('/', (req, res, next) => res.send('Hello world!')); app.get("/", (req, res, next) => res.send("Hello world!"));
// ======= // =======
const server = app.listen(9000); const server = app.listen(9000);
const peerServer = ExpressPeerServer(server, { const peerServer = ExpressPeerServer(server, {
path: '/myapp' path: "/myapp",
}); });
app.use('/peerjs', peerServer); app.use("/peerjs", peerServer);
// == OR == // == OR ==
const http = require('http'); const http = require("http");
const server = http.createServer(app); const server = http.createServer(app);
const peerServer = ExpressPeerServer(server, { const peerServer = ExpressPeerServer(server, {
debug: true, debug: true,
path: '/myapp' path: "/myapp",
}); });
app.use('/peerjs', peerServer); app.use("/peerjs", peerServer);
server.listen(9000); server.listen(9000);
@ -236,18 +248,20 @@ $ npm test
We have 'ready to use' images on docker hub: We have 'ready to use' images on docker hub:
https://hub.docker.com/r/peerjs/peerjs-server https://hub.docker.com/r/peerjs/peerjs-server
To run the latest image: To run the latest image:
```sh ```sh
$ docker run -p 9000:9000 -d peerjs/peerjs-server $ docker run -p 9000:9000 -d peerjs/peerjs-server
``` ```
You can build a new image simply by calling: You can build a new image simply by calling:
```sh ```sh
$ docker build -t myimage https://github.com/peers/peerjs-server.git $ docker build -t myimage https://github.com/peers/peerjs-server.git
``` ```
To run the image execute this: To run the image execute this:
```sh ```sh
$ docker run -p 9000:9000 -d myimage $ docker run -p 9000:9000 -d myimage
``` ```
@ -289,29 +303,29 @@ resources:
3. Create `server.js` (which node will run by default for the `start` script): 3. Create `server.js` (which node will run by default for the `start` script):
```js ```js
const express = require('express'); const express = require("express");
const { ExpressPeerServer } = require('peer'); const { ExpressPeerServer } = require("peer");
const app = express(); const app = express();
app.enable('trust proxy'); app.enable("trust proxy");
const PORT = process.env.PORT || 9000; const PORT = process.env.PORT || 9000;
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`); console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.'); console.log("Press Ctrl+C to quit.");
}); });
const peerServer = ExpressPeerServer(server, { const peerServer = ExpressPeerServer(server, {
path: '/' path: "/",
}); });
app.use('/', peerServer); app.use("/", peerServer);
module.exports = app; module.exports = app;
``` ```
4. Deploy to an existing GAE project (assuming you are already logged in via 4. Deploy to an existing GAE project (assuming you are already logged in via
`gcloud`), replacing `YOUR-PROJECT-ID-HERE` with your particular project ID: `gcloud`), replacing `YOUR-PROJECT-ID-HERE` with your particular project ID:
```sh ```sh
gcloud app deploy --project=YOUR-PROJECT-ID-HERE --promote --quiet app.yaml gcloud app deploy --project=YOUR-PROJECT-ID-HERE --promote --quiet app.yaml

View File

@ -1,17 +1,17 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Client } from '../../../../src/models/client'; import { Client } from "../../../../src/models/client";
import { HeartbeatHandler } from '../../../../src/messageHandler/handlers'; import { HeartbeatHandler } from "../../../../src/messageHandler/handlers";
describe('Heartbeat handler', () => { describe("Heartbeat handler", () => {
it('should update last ping time', () => { it("should update last ping time", () => {
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
client.setLastPing(0); client.setLastPing(0);
const nowTime = new Date().getTime(); const nowTime = new Date().getTime();
HeartbeatHandler(client); HeartbeatHandler(client);
expect(client.getLastPing()).toBeGreaterThanOrEqual(nowTime-2) expect(client.getLastPing()).toBeGreaterThanOrEqual(nowTime - 2);
expect(nowTime).toBeGreaterThanOrEqual(client.getLastPing()) expect(nowTime).toBeGreaterThanOrEqual(client.getLastPing());
}); });
}); });

View File

@ -7,111 +7,111 @@ import { MessageType } from "../../../../src/enums";
import type WebSocket from "ws"; import type WebSocket from "ws";
const createFakeSocket = (): WebSocket => { const createFakeSocket = (): WebSocket => {
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
const sock = { const sock = {
send: (): void => {}, send: (): void => {},
close: (): void => {}, close: (): void => {},
on: (): void => {}, on: (): void => {},
}; };
/* eslint-enable @typescript-eslint/no-empty-function */ /* eslint-enable @typescript-eslint/no-empty-function */
return sock as unknown as WebSocket; return sock as unknown as WebSocket;
}; };
describe("Transmission handler", () => { describe("Transmission handler", () => {
it("should save message in queue when destination client not connected", () => { it("should save message in queue when destination client not connected", () => {
const realm = new Realm(); const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm }); const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" }); const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2"; const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId()); realm.setClient(clientFrom, clientFrom.getId());
handleTransmission(clientFrom, { handleTransmission(clientFrom, {
type: MessageType.OFFER, type: MessageType.OFFER,
src: clientFrom.getId(), src: clientFrom.getId(),
dst: idTo, dst: idTo,
}); });
expect(realm.getMessageQueueById(idTo)?.getMessages().length).toBe(1); expect(realm.getMessageQueueById(idTo)?.getMessages().length).toBe(1);
}); });
it("should not save LEAVE and EXPIRE messages in queue when destination client not connected", () => { it("should not save LEAVE and EXPIRE messages in queue when destination client not connected", () => {
const realm = new Realm(); const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm }); const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" }); const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2"; const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId()); realm.setClient(clientFrom, clientFrom.getId());
handleTransmission(clientFrom, { handleTransmission(clientFrom, {
type: MessageType.LEAVE, type: MessageType.LEAVE,
src: clientFrom.getId(), src: clientFrom.getId(),
dst: idTo, dst: idTo,
}); });
handleTransmission(clientFrom, { handleTransmission(clientFrom, {
type: MessageType.EXPIRE, type: MessageType.EXPIRE,
src: clientFrom.getId(), src: clientFrom.getId(),
dst: idTo, dst: idTo,
}); });
expect(realm.getMessageQueueById(idTo)).toBeUndefined(); expect(realm.getMessageQueueById(idTo)).toBeUndefined();
}); });
it("should send message to destination client when destination client connected", () => { it("should send message to destination client when destination client connected", () => {
const realm = new Realm(); const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm }); const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" }); const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" }); const clientTo = new Client({ id: "id2", token: "" });
const socketTo = createFakeSocket(); const socketTo = createFakeSocket();
clientTo.setSocket(socketTo); clientTo.setSocket(socketTo);
realm.setClient(clientTo, clientTo.getId()); realm.setClient(clientTo, clientTo.getId());
let sent = false; let sent = false;
socketTo.send = (): void => { socketTo.send = (): void => {
sent = true; sent = true;
}; };
handleTransmission(clientFrom, { handleTransmission(clientFrom, {
type: MessageType.OFFER, type: MessageType.OFFER,
src: clientFrom.getId(), src: clientFrom.getId(),
dst: clientTo.getId(), dst: clientTo.getId(),
}); });
expect(sent).toBe(true); expect(sent).toBe(true);
}); });
it("should send LEAVE message to source client when sending to destination client failed", () => { it("should send LEAVE message to source client when sending to destination client failed", () => {
const realm = new Realm(); const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm }); const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" }); const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" }); const clientTo = new Client({ id: "id2", token: "" });
const socketFrom = createFakeSocket(); const socketFrom = createFakeSocket();
const socketTo = createFakeSocket(); const socketTo = createFakeSocket();
clientFrom.setSocket(socketFrom); clientFrom.setSocket(socketFrom);
clientTo.setSocket(socketTo); clientTo.setSocket(socketTo);
realm.setClient(clientFrom, clientFrom.getId()); realm.setClient(clientFrom, clientFrom.getId());
realm.setClient(clientTo, clientTo.getId()); realm.setClient(clientTo, clientTo.getId());
let sent = false; let sent = false;
socketFrom.send = (data: string): void => { socketFrom.send = (data: string): void => {
if (JSON.parse(data)?.type === MessageType.LEAVE) { if (JSON.parse(data)?.type === MessageType.LEAVE) {
sent = true; sent = true;
} }
}; };
socketTo.send = (): void => { socketTo.send = (): void => {
throw Error(); throw Error();
}; };
handleTransmission(clientFrom, { handleTransmission(clientFrom, {
type: MessageType.OFFER, type: MessageType.OFFER,
src: clientFrom.getId(), src: clientFrom.getId(),
dst: clientTo.getId(), dst: clientTo.getId(),
}); });
expect(sent).toBe(true); expect(sent).toBe(true);
}); });
}); });

View File

@ -1,24 +1,28 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { HandlersRegistry } from '../../src/messageHandler/handlersRegistry'; import { HandlersRegistry } from "../../src/messageHandler/handlersRegistry";
import type { Handler } from '../../src/messageHandler/handler'; import type { Handler } from "../../src/messageHandler/handler";
import { MessageType } from '../../src/enums'; import { MessageType } from "../../src/enums";
describe('HandlersRegistry', () => { describe("HandlersRegistry", () => {
it('should execute handler for message type', () => { it("should execute handler for message type", () => {
const handlersRegistry = new HandlersRegistry(); const handlersRegistry = new HandlersRegistry();
let handled = false; let handled = false;
const handler: Handler = (): boolean => { const handler: Handler = (): boolean => {
handled = true; handled = true;
return true; return true;
}; };
handlersRegistry.registerHandler(MessageType.OPEN, handler); handlersRegistry.registerHandler(MessageType.OPEN, handler);
handlersRegistry.handle(undefined, { type: MessageType.OPEN, src: 'src', dst: 'dst' }); handlersRegistry.handle(undefined, {
type: MessageType.OPEN,
src: "src",
dst: "dst",
});
expect(handled).toBe(true); expect(handled).toBe(true);
}); });
}); });

View File

@ -1,63 +1,63 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { MessageQueue } from '../../src/models/messageQueue'; import { MessageQueue } from "../../src/models/messageQueue";
import { MessageType } from '../../src/enums'; import { MessageType } from "../../src/enums";
import type { IMessage } from '../../src/models/message'; import type { IMessage } from "../../src/models/message";
import { wait } from '../utils'; import { wait } from "../utils";
describe('MessageQueue', () => { describe("MessageQueue", () => {
const createTestMessage = (): IMessage => { const createTestMessage = (): IMessage => {
return { return {
type: MessageType.OPEN, type: MessageType.OPEN,
src: 'src', src: "src",
dst: 'dst' dst: "dst",
}; };
}; };
describe('#addMessage', () => { describe("#addMessage", () => {
it('should add message to queue', () => { it("should add message to queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
queue.addMessage(createTestMessage()); queue.addMessage(createTestMessage());
expect(queue.getMessages().length).toBe(1); expect(queue.getMessages().length).toBe(1);
}); });
}); });
describe('#readMessage', () => { describe("#readMessage", () => {
it('should return undefined for empty queue', () => { it("should return undefined for empty queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
expect(queue.readMessage()).toBeUndefined(); expect(queue.readMessage()).toBeUndefined();
}); });
it('should return message if any exists in queue', () => { it("should return message if any exists in queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const message = createTestMessage(); const message = createTestMessage();
queue.addMessage(message); queue.addMessage(message);
expect(queue.readMessage()).toEqual(message); expect(queue.readMessage()).toEqual(message);
expect(queue.readMessage()).toBeUndefined(); expect(queue.readMessage()).toBeUndefined();
}); });
}); });
describe('#getLastReadAt', () => { describe("#getLastReadAt", () => {
it('should not be changed if no messages when read', () => { it("should not be changed if no messages when read", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt(); const lastReadAt = queue.getLastReadAt();
queue.readMessage(); queue.readMessage();
expect(queue.getLastReadAt()).toBe(lastReadAt); expect(queue.getLastReadAt()).toBe(lastReadAt);
}); });
it('should be changed when read message', async () => { it("should be changed when read message", async () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt(); const lastReadAt = queue.getLastReadAt();
queue.addMessage(createTestMessage()); queue.addMessage(createTestMessage());
await wait(10); await wait(10);
expect(queue.getLastReadAt()).toBe(lastReadAt); expect(queue.getLastReadAt()).toBe(lastReadAt);
queue.readMessage(); queue.readMessage();
expect(queue.getLastReadAt()).toBeGreaterThanOrEqual(lastReadAt + 10); expect(queue.getLastReadAt()).toBeGreaterThanOrEqual(lastReadAt + 10);
}); });
}); });
}); });

View File

@ -1,51 +1,51 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Realm } from '../../src/models/realm'; import { Realm } from "../../src/models/realm";
import { Client } from '../../src/models/client'; import { Client } from "../../src/models/client";
describe('Realm', () => { describe("Realm", () => {
describe('#generateClientId', () => { describe("#generateClientId", () => {
it('should generate a 36-character UUID, or return function value', () => { it("should generate a 36-character UUID, or return function value", () => {
const realm = new Realm(); const realm = new Realm();
expect(realm.generateClientId().length).toBe(36); expect(realm.generateClientId().length).toBe(36);
expect(realm.generateClientId(() => 'abcd')).toBe('abcd'); expect(realm.generateClientId(() => "abcd")).toBe("abcd");
}); });
}); });
describe('#setClient', () => { describe("#setClient", () => {
it('should add client to realm', () => { it("should add client to realm", () => {
const realm = new Realm(); const realm = new Realm();
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
expect(realm.getClientsIds()).toEqual(['id']); expect(realm.getClientsIds()).toEqual(["id"]);
}); });
}); });
describe('#removeClientById', () => { describe("#removeClientById", () => {
it('should remove client from realm', () => { it("should remove client from realm", () => {
const realm = new Realm(); const realm = new Realm();
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
realm.removeClientById('id'); realm.removeClientById("id");
expect(realm.getClientById('id')).toBeUndefined(); expect(realm.getClientById("id")).toBeUndefined();
}); });
}); });
describe('#getClientsIds', () => { describe("#getClientsIds", () => {
it('should reflects on add/remove childs', () => { it("should reflects on add/remove childs", () => {
const realm = new Realm(); const realm = new Realm();
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
expect(realm.getClientsIds()).toEqual(['id']); expect(realm.getClientsIds()).toEqual(["id"]);
expect(realm.getClientById('id')).toBe(client); expect(realm.getClientById("id")).toBe(client);
realm.removeClientById('id'); realm.removeClientById("id");
expect(realm.getClientsIds()).toEqual([]); expect(realm.getClientsIds()).toEqual([]);
}); });
}); });
}); });

View File

@ -1,91 +1,91 @@
import {describe, expect, it} from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import http from 'http'; import http from "http";
import expectedJson from '../app.json'; import expectedJson from "../app.json";
import fetch from "node-fetch"; import fetch from "node-fetch";
import * as crypto from "crypto"; import * as crypto from "crypto";
import {startServer} from "./utils"; import { startServer } from "./utils";
const PORT = '9000'; const PORT = "9000";
async function makeRequest() { async function makeRequest() {
return new Promise<object>((resolve, reject) => { return new Promise<object>((resolve, reject) => {
http.get(`http://localhost:${PORT}/`, resp => { http
let data = ''; .get(`http://localhost:${PORT}/`, (resp) => {
let data = "";
resp.on('data', chunk => { resp.on("data", (chunk) => {
data += chunk; data += chunk;
}); });
resp.on('end', () => { resp.on("end", () => {
resolve(JSON.parse(data)); resolve(JSON.parse(data));
}); });
})
}).on("error", err => { .on("error", (err) => {
console.log("Error: " + err.message); console.log("Error: " + err.message);
reject(err); reject(err);
}); });
}); });
} }
describe('Check bin/peerjs', () => { describe("Check bin/peerjs", () => {
it('should return content of app.json file', async () => { it("should return content of app.json file", async () => {
expect.assertions(1); expect.assertions(1);
const ls = await startServer() const ls = await startServer();
try { try {
const resp = await makeRequest(); const resp = await makeRequest();
expect(resp).toEqual(expectedJson); expect(resp).toEqual(expectedJson);
} finally { } finally {
ls.kill(); ls.kill();
} }
});
}); it("should reflect the origin header in CORS by default", async () => {
expect.assertions(1);
it('should reflect the origin header in CORS by default', async () => { const ls = await startServer();
expect.assertions(1); const origin = crypto.randomUUID();
try {
const res = await fetch(`http://localhost:${PORT}/peerjs/id`, {
headers: {
Origin: origin,
},
});
expect(res.headers.get("access-control-allow-origin")).toBe(origin);
} finally {
ls.kill();
}
});
it("should respect the CORS parameters", async () => {
expect.assertions(3);
const ls = await startServer() const origin1 = crypto.randomUUID();
const origin = crypto.randomUUID(); const origin2 = crypto.randomUUID();
try { const origin3 = crypto.randomUUID();
const res = await fetch(`http://localhost:${PORT}/peerjs/id`, { const ls = await startServer(["--cors", origin1, "--cors", origin2]);
headers: { try {
Origin: origin const res1 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
} headers: {
}) Origin: origin1,
expect(res.headers.get("access-control-allow-origin")).toBe(origin) },
} finally { });
ls.kill() expect(res1.headers.get("access-control-allow-origin")).toBe(origin1);
} const res2 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
}); headers: {
it('should respect the CORS parameters', async () => { Origin: origin2,
expect.assertions(3); },
});
const origin1 = crypto.randomUUID(); expect(res2.headers.get("access-control-allow-origin")).toBe(origin2);
const origin2 = crypto.randomUUID(); const res3 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
const origin3 = crypto.randomUUID(); headers: {
const ls = await startServer(["--cors", origin1, "--cors", origin2]) Origin: origin3,
try { },
const res1 = await fetch(`http://localhost:${PORT}/peerjs/id`, { });
headers: { expect(res3.headers.get("access-control-allow-origin")).toBe(null);
Origin: origin1 } finally {
} ls.kill();
}) }
expect(res1.headers.get("access-control-allow-origin")).toBe(origin1) });
const res2 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
headers: {
Origin: origin2
}
})
expect(res2.headers.get("access-control-allow-origin")).toBe(origin2)
const res3 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
headers: {
Origin: origin3
}
})
expect(res3.headers.get("access-control-allow-origin")).toBe(null)
} finally {
ls.kill()
}
});
}); });

View File

@ -1,45 +1,53 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Client } from '../../../src/models/client'; import { Client } from "../../../src/models/client";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import { CheckBrokenConnections } from '../../../src/services/checkBrokenConnections'; import { CheckBrokenConnections } from "../../../src/services/checkBrokenConnections";
import { wait } from '../../utils'; import { wait } from "../../utils";
describe('CheckBrokenConnections', () => { describe("CheckBrokenConnections", () => {
it('should remove client after 2 checks', async () => { it("should remove client after 2 checks", async () => {
const realm = new Realm(); const realm = new Realm();
const doubleCheckTime = 55;//~ equals to checkBrokenConnections.checkInterval * 2 const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 }); const checkBrokenConnections = new CheckBrokenConnections({
const client = new Client({ id: 'id', token: '' }); realm,
realm.setClient(client, 'id'); config: { alive_timeout: doubleCheckTime },
checkInterval: 30,
});
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
checkBrokenConnections.start(); checkBrokenConnections.start();
await wait(checkBrokenConnections.checkInterval * 2 + 30); await wait(checkBrokenConnections.checkInterval * 2 + 30);
expect(realm.getClientById('id')).toBeUndefined(); expect(realm.getClientById("id")).toBeUndefined();
checkBrokenConnections.stop(); checkBrokenConnections.stop();
}); });
it('should remove client after 1 ping', async () => { it("should remove client after 1 ping", async () => {
const realm = new Realm(); const realm = new Realm();
const doubleCheckTime = 55;//~ equals to checkBrokenConnections.checkInterval * 2 const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 }); const checkBrokenConnections = new CheckBrokenConnections({
const client = new Client({ id: 'id', token: '' }); realm,
realm.setClient(client, 'id'); config: { alive_timeout: doubleCheckTime },
checkInterval: 30,
});
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
checkBrokenConnections.start(); checkBrokenConnections.start();
//set ping after first check //set ping after first check
await wait(checkBrokenConnections.checkInterval); await wait(checkBrokenConnections.checkInterval);
client.setLastPing(new Date().getTime()); client.setLastPing(new Date().getTime());
await wait(checkBrokenConnections.checkInterval * 2 + 10); await wait(checkBrokenConnections.checkInterval * 2 + 10);
expect(realm.getClientById('id')).toBeUndefined(); expect(realm.getClientById("id")).toBeUndefined();
checkBrokenConnections.stop(); checkBrokenConnections.stop();
}); });
}); });

View File

@ -1,80 +1,96 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Client } from '../../../src/models/client'; import { Client } from "../../../src/models/client";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import type { IMessage } from '../../../src/models/message'; import type { IMessage } from "../../../src/models/message";
import { MessagesExpire } from '../../../src/services/messagesExpire'; import { MessagesExpire } from "../../../src/services/messagesExpire";
import { MessageHandler } from '../../../src/messageHandler'; import { MessageHandler } from "../../../src/messageHandler";
import { MessageType } from '../../../src/enums'; import { MessageType } from "../../../src/enums";
import { wait } from '../../utils'; import { wait } from "../../utils";
describe('MessagesExpire', () => { describe("MessagesExpire", () => {
const createTestMessage = (dst: string): IMessage => { const createTestMessage = (dst: string): IMessage => {
return { return {
type: MessageType.OPEN, type: MessageType.OPEN,
src: 'src', src: "src",
dst, dst,
}; };
}; };
it('should remove client if no read from queue', async () => { it("should remove client if no read from queue", async () => {
const realm = new Realm(); const realm = new Realm();
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const checkInterval = 10; const checkInterval = 10;
const expireTimeout = 50; const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout }; const config = {
cleanup_out_msgs: checkInterval,
expire_timeout: expireTimeout,
};
const messagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire = new MessagesExpire({
realm,
config,
messageHandler,
});
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
realm.addMessageToQueue(client.getId(), createTestMessage('dst')); realm.addMessageToQueue(client.getId(), createTestMessage("dst"));
messagesExpire.startMessagesExpiration(); messagesExpire.startMessagesExpiration();
await wait(checkInterval * 2); await wait(checkInterval * 2);
expect(realm.getMessageQueueById(client.getId())?.getMessages().length).toBe(1); expect(
realm.getMessageQueueById(client.getId())?.getMessages().length,
).toBe(1);
await wait(expireTimeout); await wait(expireTimeout);
expect(realm.getMessageQueueById(client.getId())).toBeUndefined(); expect(realm.getMessageQueueById(client.getId())).toBeUndefined();
messagesExpire.stopMessagesExpiration(); messagesExpire.stopMessagesExpiration();
}); });
it('should fire EXPIRE message', async () => { it("should fire EXPIRE message", async () => {
const realm = new Realm(); const realm = new Realm();
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const checkInterval = 10; const checkInterval = 10;
const expireTimeout = 50; const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout }; const config = {
cleanup_out_msgs: checkInterval,
expire_timeout: expireTimeout,
};
const messagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire = new MessagesExpire({
realm,
config,
messageHandler,
});
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
realm.addMessageToQueue(client.getId(), createTestMessage('dst1')); realm.addMessageToQueue(client.getId(), createTestMessage("dst1"));
realm.addMessageToQueue(client.getId(), createTestMessage('dst2')); realm.addMessageToQueue(client.getId(), createTestMessage("dst2"));
let handledCount = 0; let handledCount = 0;
messageHandler.handle = (client, message): boolean => { messageHandler.handle = (client, message): boolean => {
expect(client).toBeUndefined(); expect(client).toBeUndefined();
expect(message.type).toBe(MessageType.EXPIRE); expect(message.type).toBe(MessageType.EXPIRE);
handledCount++; handledCount++;
return true; return true;
}; };
messagesExpire.startMessagesExpiration(); messagesExpire.startMessagesExpiration();
await wait(checkInterval * 2); await wait(checkInterval * 2);
await wait(expireTimeout); await wait(expireTimeout);
expect(handledCount).toBe(2); expect(handledCount).toBe(2);
messagesExpire.stopMessagesExpiration(); messagesExpire.stopMessagesExpiration();
}); });
}); });

View File

@ -1,196 +1,244 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Server, WebSocket } from 'mock-socket'; import { Server, WebSocket } from "mock-socket";
import type {Server as HttpServer} from 'node:http'; import type { Server as HttpServer } from "node:http";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import { WebSocketServer } from '../../../src/services/webSocketServer'; import { WebSocketServer } from "../../../src/services/webSocketServer";
import { Errors, MessageType } from '../../../src/enums'; import { Errors, MessageType } from "../../../src/enums";
import { wait } from '../../utils'; import { wait } from "../../utils";
type Destroyable<T> = T & { destroy?: () => Promise<void>; }; type Destroyable<T> = T & { destroy?: () => Promise<void> };
const checkOpen = async (c: WebSocket): Promise<boolean> => { const checkOpen = async (c: WebSocket): Promise<boolean> => {
return new Promise(resolve => { return new Promise((resolve) => {
c.onmessage = (event: object & { data?: string; }): void => { c.onmessage = (event: object & { data?: string }): void => {
const message = JSON.parse(event.data as string); const message = JSON.parse(event.data as string);
resolve(message.type === MessageType.OPEN); resolve(message.type === MessageType.OPEN);
}; };
}); });
}; };
const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Errors; }[]): Promise<boolean> => { const checkSequence = async (
return new Promise(resolve => { c: WebSocket,
const restMessages = [...msgs]; msgs: { type: MessageType; error?: Errors }[],
): Promise<boolean> => {
return new Promise((resolve) => {
const restMessages = [...msgs];
const finish = (success = false): void => { const finish = (success = false): void => {
resolve(success); resolve(success);
}; };
c.onmessage = (event: object & { data?: string; }): void => { c.onmessage = (event: object & { data?: string }): void => {
const [mes] = restMessages; const [mes] = restMessages;
if (!mes) { if (!mes) {
return finish(); return finish();
} }
restMessages.shift(); restMessages.shift();
const message = JSON.parse(event.data as string); const message = JSON.parse(event.data as string);
if (message.type !== mes.type) { if (message.type !== mes.type) {
return finish(); return finish();
} }
const isOk = !mes.error || message.payload?.msg === mes.error; const isOk = !mes.error || message.payload?.msg === mes.error;
if (!isOk) { if (!isOk) {
return finish(); return finish();
} }
if (restMessages.length === 0) { if (restMessages.length === 0) {
finish(true); finish(true);
} }
}; };
}); });
}; };
const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path: string; key: string; concurrent_limit: number; }; url: string; }): Destroyable<WebSocketServer> => { const createTestServer = ({
const server = new Server(url) as Server & HttpServer; realm,
const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({ server, realm, config }); config,
url,
}: {
realm: Realm;
config: { path: string; key: string; concurrent_limit: number };
url: string;
}): Destroyable<WebSocketServer> => {
const server = new Server(url) as Server & HttpServer;
const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({
server,
realm,
config,
});
server.on('connection', (socket: WebSocket & { on?: (eventName: string, callback: () => void) => void; }) => { server.on(
const s = webSocketServer.socketServer; "connection",
s.emit('connection', socket, { url: socket.url }); (
socket: WebSocket & {
on?: (eventName: string, callback: () => void) => void;
},
) => {
const s = webSocketServer.socketServer;
s.emit("connection", socket, { url: socket.url });
socket.onclose = (): void => { socket.onclose = (): void => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; const userId = socket.url
.split("?")[1]
?.split("&")
.find((p) => p.startsWith("id"))
?.split("=")[1];
if (!userId) return; if (!userId) return;
const client = realm.getClientById(userId); const client = realm.getClientById(userId);
const clientSocket = client?.getSocket(); const clientSocket = client?.getSocket();
if (!clientSocket) return; if (!clientSocket) return;
(clientSocket as unknown as WebSocket).listeners['server::close']?.forEach((s: () => void) => s()); (clientSocket as unknown as WebSocket).listeners[
}; "server::close"
]?.forEach((s: () => void) => s());
};
socket.onmessage = (event: object & { data?: string; }): void => { socket.onmessage = (event: object & { data?: string }): void => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; const userId = socket.url
.split("?")[1]
?.split("&")
.find((p) => p.startsWith("id"))
?.split("=")[1];
if (!userId) return; if (!userId) return;
const client = realm.getClientById(userId); const client = realm.getClientById(userId);
const clientSocket = client?.getSocket(); const clientSocket = client?.getSocket();
if (!clientSocket) return; if (!clientSocket) return;
(clientSocket as unknown as WebSocket).listeners['server::message']?.forEach((s: (data: object) => void) => s(event)); (clientSocket as unknown as WebSocket).listeners[
}; "server::message"
}); ]?.forEach((s: (data: object) => void) => s(event));
};
},
);
webSocketServer.destroy = async (): Promise<void> => { webSocketServer.destroy = async (): Promise<void> => {
server.close(); server.close();
}; };
return webSocketServer; return webSocketServer;
}; };
describe('WebSocketServer', () => { describe("WebSocketServer", () => {
it("should return valid path", () => {
const realm = new Realm();
const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const config2 = { ...config, path: "path" };
const server = new Server("path1") as Server & HttpServer;
const server2 = new Server("path2") as Server & HttpServer;
it('should return valid path', () => { const webSocketServer = new WebSocketServer({ server, realm, config });
const realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
const config2 = { ...config, path: 'path' };
const server = new Server('path1') as Server & HttpServer;
const server2 = new Server('path2') as Server & HttpServer;
const webSocketServer = new WebSocketServer({ server, realm, config }); expect(webSocketServer.path).toBe("/peerjs");
expect(webSocketServer.path).toBe('/peerjs'); const webSocketServer2 = new WebSocketServer({
server: server2,
realm,
config: config2,
});
const webSocketServer2 = new WebSocketServer({ server: server2, realm, config: config2 }); expect(webSocketServer2.path).toBe("path/peerjs");
expect(webSocketServer2.path).toBe('path/peerjs'); server.stop();
server2.stop();
});
server.stop(); it(`should check client's params`, async () => {
server2.stop(); const realm = new Realm();
}); const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const fakeURL = "ws://localhost:8080/peerjs";
it(`should check client's params`, async () => { const getError = async (
const realm = new Realm(); url: string,
const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; validError: Errors = Errors.INVALID_WS_PARAMETERS,
const fakeURL = 'ws://localhost:8080/peerjs'; ): Promise<boolean> => {
const webSocketServer = createTestServer({ url, realm, config });
const getError = async (url: string, validError: Errors = Errors.INVALID_WS_PARAMETERS): Promise<boolean> => { const ws = new WebSocket(url);
const webSocketServer = createTestServer({ url, realm, config });
const ws = new WebSocket(url); const errorSent = await checkSequence(ws, [
{ type: MessageType.ERROR, error: validError },
]);
const errorSent = await checkSequence(ws, [{ type: MessageType.ERROR, error: validError }]); ws.close();
ws.close(); await webSocketServer.destroy?.();
await webSocketServer.destroy?.(); return errorSent;
};
return errorSent; expect(await getError(fakeURL)).toBe(true);
}; expect(await getError(`${fakeURL}?key=${config.key}`)).toBe(true);
expect(await getError(`${fakeURL}?key=${config.key}&id=1`)).toBe(true);
expect(
await getError(
`${fakeURL}?key=notValidKey&id=userId&token=userToken`,
Errors.INVALID_KEY,
),
).toBe(true);
});
expect(await getError(fakeURL)).toBe(true); it(`should check concurrent limit`, async () => {
expect(await getError(`${fakeURL}?key=${config.key}`)).toBe(true); const realm = new Realm();
expect(await getError(`${fakeURL}?key=${config.key}&id=1`)).toBe(true); const config = { path: "/", key: "testKey", concurrent_limit: 1 };
expect(await getError(`${fakeURL}?key=notValidKey&id=userId&token=userToken`, Errors.INVALID_KEY)).toBe(true); const fakeURL = "ws://localhost:8080/peerjs";
});
it(`should check concurrent limit`, async () => { const createClient = (id: string): Destroyable<WebSocket> => {
const realm = new Realm(); // id in the path ensures that all mock servers listen on different urls
const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const url = `${fakeURL}${id}?key=${config.key}&id=${id}&token=${id}`;
const fakeURL = 'ws://localhost:8080/peerjs'; const webSocketServer = createTestServer({ url, realm, config });
const ws: Destroyable<WebSocket> = new WebSocket(url);
const createClient = (id: string): Destroyable<WebSocket> => { ws.destroy = async (): Promise<void> => {
// id in the path ensures that all mock servers listen on different urls ws.close();
const url = `${fakeURL}${id}?key=${config.key}&id=${id}&token=${id}`;
const webSocketServer = createTestServer({ url, realm, config });
const ws: Destroyable<WebSocket> = new WebSocket(url);
ws.destroy = async (): Promise<void> => { wait(10);
ws.close();
wait(10); webSocketServer.destroy?.();
webSocketServer.destroy?.(); wait(10);
wait(10); ws.destroy = undefined;
};
ws.destroy = undefined; return ws;
}; };
return ws; const c1 = createClient("1");
};
expect(await checkOpen(c1)).toBe(true);
const c1 = createClient('1'); const c2 = createClient("2");
expect(await checkOpen(c1)).toBe(true); expect(
await checkSequence(c2, [
{ type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED },
]),
).toBe(true);
const c2 = createClient('2'); await c1.destroy?.();
await c2.destroy?.();
expect(await checkSequence(c2, [ await wait(10);
{ type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED }
])).toBe(true);
await c1.destroy?.(); expect(realm.getClientsIds().length).toBe(0);
await c2.destroy?.();
await wait(10); const c3 = createClient("3");
expect(realm.getClientsIds().length).toBe(0); expect(await checkOpen(c3)).toBe(true);
const c3 = createClient('3'); await c3.destroy?.();
});
expect(await checkOpen(c3)).toBe(true);
await c3.destroy?.();
});
}); });

View File

@ -1,15 +1,21 @@
import {ChildProcessWithoutNullStreams, spawn} from "child_process"; import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import path from "path"; import path from "path";
export const wait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms)); export const wait = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
export const startServer = (params: string[] = []) => { export const startServer = (params: string[] = []) => {
return new Promise<ChildProcessWithoutNullStreams>((resolve, reject)=> { return new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
const ls = spawn('node', [path.join(__dirname, '../', 'dist/bin/peerjs.js'), '--port', "9000", ...params]); const ls = spawn("node", [
ls.stdout.once("data", ()=> resolve(ls)) path.join(__dirname, "../", "dist/bin/peerjs.js"),
ls.stderr.once("data", ()=>{ "--port",
ls.kill() "9000",
reject() ...params,
}) ]);
}) ls.stdout.once("data", () => resolve(ls));
} ls.stderr.once("data", () => {
ls.kill();
reject();
});
});
};

View File

@ -1,5 +1,5 @@
{ {
"name": "PeerJS Server", "name": "PeerJS Server",
"description": "A server side element to broker connections between PeerJS clients.", "description": "A server side element to broker connections between PeerJS clients.",
"website": "https://peerjs.com/" "website": "https://peerjs.com/"
} }

View File

@ -1,148 +1,149 @@
#!/usr/bin/env node #!/usr/bin/env node
import path from "node:path"; import path from "node:path";
import {version} from "../package.json"; import { version } from "../package.json";
import fs from "node:fs"; import fs from "node:fs";
const optimistUsageLength = 98; const optimistUsageLength = 98;
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from 'yargs/helpers' import { hideBin } from "yargs/helpers";
import { PeerServer} from "../src"; import { PeerServer } from "../src";
import type { AddressInfo } from "node:net"; import type { AddressInfo } from "node:net";
import type {CorsOptions} from "cors"; import type { CorsOptions } from "cors";
const y = yargs(hideBin(process.argv)); const y = yargs(hideBin(process.argv));
const portEnvIsSet = !!process.env["PORT"] const portEnvIsSet = !!process.env["PORT"];
const opts = y const opts = y
.usage("Usage: $0") .usage("Usage: $0")
.wrap(Math.min(optimistUsageLength, y.terminalWidth())) .wrap(Math.min(optimistUsageLength, y.terminalWidth()))
.options({ .options({
expire_timeout: { expire_timeout: {
demandOption: false, demandOption: false,
alias: "t", alias: "t",
describe: "timeout (milliseconds)", describe: "timeout (milliseconds)",
default: 5000, default: 5000,
}, },
concurrent_limit: { concurrent_limit: {
demandOption: false, demandOption: false,
alias: "c", alias: "c",
describe: "concurrent limit", describe: "concurrent limit",
default: 5000, default: 5000,
}, },
alive_timeout: { alive_timeout: {
demandOption: false, demandOption: false,
describe: "broken connection check timeout (milliseconds)", describe: "broken connection check timeout (milliseconds)",
default: 60000, default: 60000,
}, },
key: { key: {
demandOption: false, demandOption: false,
alias: "k", alias: "k",
describe: "connection key", describe: "connection key",
default: "peerjs", default: "peerjs",
}, },
sslkey: { sslkey: {
type: "string", type: "string",
demandOption: false, demandOption: false,
describe: "path to SSL key", describe: "path to SSL key",
}, },
sslcert: { sslcert: {
type: "string", type: "string",
demandOption: false, demandOption: false,
describe: "path to SSL certificate", describe: "path to SSL certificate",
}, },
host: { host: {
type: "string", type: "string",
demandOption: false, demandOption: false,
alias: "H", alias: "H",
describe: "host", describe: "host",
}, },
port: { port: {
type: "number", type: "number",
demandOption: !portEnvIsSet, demandOption: !portEnvIsSet,
alias: "p", alias: "p",
describe: "port", describe: "port",
}, },
path: { path: {
type: "string", type: "string",
demandOption: false, demandOption: false,
describe: "custom path", describe: "custom path",
default: process.env["PEERSERVER_PATH"] || "/", default: process.env["PEERSERVER_PATH"] || "/",
}, },
allow_discovery: { allow_discovery: {
type: "boolean", type: "boolean",
demandOption: false, demandOption: false,
describe: "allow discovery of peers", describe: "allow discovery of peers",
}, },
proxied: { proxied: {
type: "boolean", type: "boolean",
demandOption: false, demandOption: false,
describe: "Set true if PeerServer stays behind a reverse proxy", describe: "Set true if PeerServer stays behind a reverse proxy",
default: false, default: false,
}, },
cors: { cors: {
type: "string", type: "string",
array: true, array: true,
describe: "Set the list of CORS origins", describe: "Set the list of CORS origins",
}, },
}) })
.boolean("allow_discovery").parseSync(); .boolean("allow_discovery")
.parseSync();
if(!opts.port){ if (!opts.port) {
opts.port= parseInt(process.env["PORT"] as string) opts.port = parseInt(process.env["PORT"] as string);
} }
if(opts.cors){ if (opts.cors) {
opts["corsOptions"] = { opts["corsOptions"] = {
origin: opts.cors origin: opts.cors,
} satisfies CorsOptions; } satisfies CorsOptions;
} }
process.on("uncaughtException", function (e) { process.on("uncaughtException", function (e) {
console.error("Error: " + e); console.error("Error: " + e);
}); });
if (opts.sslkey || opts.sslcert) { if (opts.sslkey || opts.sslcert) {
if (opts.sslkey && opts.sslcert) { if (opts.sslkey && opts.sslcert) {
opts["ssl"] = { opts["ssl"] = {
key: fs.readFileSync(path.resolve(opts.sslkey)), key: fs.readFileSync(path.resolve(opts.sslkey)),
cert: fs.readFileSync(path.resolve(opts.sslcert)), cert: fs.readFileSync(path.resolve(opts.sslcert)),
}; };
} else { } else {
console.error( console.error(
"Warning: PeerServer will not run because either " + "Warning: PeerServer will not run because either " +
"the key or the certificate has not been provided." "the key or the certificate has not been provided.",
); );
process.exit(1); process.exit(1);
} }
} }
const userPath = opts.path; const userPath = opts.path;
const server = PeerServer(opts, (server) => { const server = PeerServer(opts, (server) => {
const { address: host, port } = server.address() as AddressInfo; const { address: host, port } = server.address() as AddressInfo;
console.log( console.log(
"Started PeerServer on %s, port: %s, path: %s (v. %s)", "Started PeerServer on %s, port: %s, path: %s (v. %s)",
host, host,
port, port,
userPath || "/", userPath || "/",
version version,
); );
const shutdownApp = () => { const shutdownApp = () => {
server.close(() => { server.close(() => {
console.log("Http server closed."); console.log("Http server closed.");
process.exit(0); process.exit(0);
}); });
}; };
process.on("SIGINT", shutdownApp); process.on("SIGINT", shutdownApp);
process.on("SIGTERM", shutdownApp); process.on("SIGTERM", shutdownApp);
}); });
server.on("connection", (client) => { server.on("connection", (client) => {
console.log(`Client connected: ${client.getId()}`); console.log(`Client connected: ${client.getId()}`);
}); });
server.on("disconnect", (client) => { server.on("disconnect", (client) => {
console.log(`Client disconnected: ${client.getId()}`); console.log(`Client disconnected: ${client.getId()}`);
}); });

View File

@ -1,13 +1,13 @@
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
const config = { const config = {
testEnvironment: "node", testEnvironment: "node",
transform: { transform: {
"^.+\\.(t|j)sx?$": "@swc/jest", "^.+\\.(t|j)sx?$": "@swc/jest",
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
// "node_modules" // "node_modules"
], ],
collectCoverageFrom: ["./src/**"] collectCoverageFrom: ["./src/**"],
}; };
export default config; export default config;

49072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,106 +1,106 @@
{ {
"name": "peer", "name": "peer",
"version": "0.0.0-development", "version": "0.0.0-development",
"keywords": [ "keywords": [
"peerjs", "peerjs",
"webrtc", "webrtc",
"p2p", "p2p",
"rtc" "rtc"
], ],
"description": "PeerJS server component", "description": "PeerJS server component",
"homepage": "https://peerjs.com", "homepage": "https://peerjs.com",
"bugs": { "bugs": {
"url": "https://github.com/peers/peerjs-server/issues" "url": "https://github.com/peers/peerjs-server/issues"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/peers/peerjs-server" "url": "https://github.com/peers/peerjs-server"
}, },
"license": "MIT", "license": "MIT",
"contributors": [], "contributors": [],
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"import": { "import": {
"types": "./dist/peer.d.ts", "types": "./dist/peer.d.ts",
"default": "./dist/module.mjs" "default": "./dist/module.mjs"
}, },
"require": { "require": {
"types": "./dist/peer.d.ts", "types": "./dist/peer.d.ts",
"default": "./dist/index.cjs" "default": "./dist/index.cjs"
} }
} }
}, },
"main": "dist/index.cjs", "main": "dist/index.cjs",
"module": "dist/module.mjs", "module": "dist/module.mjs",
"source": "src/index.ts", "source": "src/index.ts",
"binary": "dist/bin/peerjs.js", "binary": "dist/bin/peerjs.js",
"types": "dist/peer.d.ts", "types": "dist/peer.d.ts",
"bin": { "bin": {
"peerjs": "dist/bin/peerjs.js" "peerjs": "dist/bin/peerjs.js"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/peer" "url": "https://opencollective.com/peer"
}, },
"collective": { "collective": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/peer" "url": "https://opencollective.com/peer"
}, },
"files": [ "files": [
"dist/" "dist/"
], ],
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },
"targets": { "targets": {
"binary": { "binary": {
"source": "bin/peerjs.ts" "source": "bin/peerjs.ts"
}, },
"main": {}, "main": {},
"module": {} "module": {}
}, },
"scripts": { "scripts": {
"format": "prettier --write .", "format": "prettier --write .",
"build": "parcel build", "build": "parcel build",
"lint": "eslint --ext .js,.ts . && npm run check", "lint": "eslint --ext .js,.ts . && npm run check",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test": "npm run lint && jest", "test": "npm run lint && jest",
"coverage": "jest --coverage", "coverage": "jest --coverage",
"start": "node dist/bin/peerjs.js --port ${PORT:=9000}", "start": "node dist/bin/peerjs.js --port ${PORT:=9000}",
"dev": "nodemon --watch src -e ts --exec 'npm run build && npm run start'", "dev": "nodemon --watch src -e ts --exec 'npm run build && npm run start'",
"semantic-release": "semantic-release" "semantic-release": "semantic-release"
}, },
"dependencies": { "dependencies": {
"@types/express": "^4.17.3", "@types/express": "^4.17.3",
"@types/ws": "^7.2.3 || ^8.0.0", "@types/ws": "^7.2.3 || ^8.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"node-fetch": "^3.3.0", "node-fetch": "^3.3.0",
"ws": "^7.2.3 || ^8.0.0", "ws": "^7.2.3 || ^8.0.0",
"yargs": "^17.6.2" "yargs": "^17.6.2"
}, },
"devDependencies": { "devDependencies": {
"@codedependant/semantic-release-docker": "^4.3.0", "@codedependant/semantic-release-docker": "^4.3.0",
"@parcel/packager-ts": "^2.8.2", "@parcel/packager-ts": "^2.8.2",
"@parcel/transformer-typescript-types": "^2.8.2", "@parcel/transformer-typescript-types": "^2.8.2",
"@semantic-release/changelog": "^6.0.1", "@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@swc/core": "^1.3.35", "@swc/core": "^1.3.35",
"@swc/jest": "^0.2.24", "@swc/jest": "^0.2.24",
"@tsconfig/node16-strictest-esm": "^1.0.3", "@tsconfig/node16-strictest-esm": "^1.0.3",
"@types/cors": "^2.8.6", "@types/cors": "^2.8.6",
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",
"@types/node": "^14.18.33", "@types/node": "^14.18.33",
"@types/yargs": "^17.0.19", "@types/yargs": "^17.0.19",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0", "eslint": "^8.0.0",
"jest": "^29.4.2", "jest": "^29.4.2",
"mock-socket": "^9.1.5", "mock-socket": "^9.1.5",
"parcel": "^2.8.2", "parcel": "^2.8.2",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"semantic-release": "^20.0.0", "semantic-release": "^20.0.0",
"typescript": "^4.1.2" "typescript": "^4.1.2"
} }
} }

View File

@ -1,29 +1,29 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":assignAndReview(jonasgloning)"], "extends": ["config:base", ":assignAndReview(jonasgloning)"],
"labels": ["dependencies"], "labels": ["dependencies"],
"assignees": ["jonasgloning"], "assignees": ["jonasgloning"],
"major": { "major": {
"dependencyDashboardApproval": true "dependencyDashboardApproval": true
}, },
"packageRules": [ "packageRules": [
{ {
"matchDepTypes": ["devDependencies"], "matchDepTypes": ["devDependencies"],
"addLabels": ["dev-dependencies"], "addLabels": ["dev-dependencies"],
"automerge": true, "automerge": true,
"automergeType": "branch" "automergeType": "branch"
}, },
{ {
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/", "matchCurrentVersion": "!/^0/",
"automerge": true, "automerge": true,
"automergeType": "pr", "automergeType": "pr",
"platformAutomerge": true "platformAutomerge": true
} }
], ],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
"automerge": true, "automerge": true,
"automergeType": "branch" "automergeType": "branch"
} }
} }

View File

@ -8,8 +8,9 @@ So, the base path should be like `http://127.0.0.1:9000/` or `http://127.0.0.1:9
Endpoints: Endpoints:
* GET `/` - return a JSON to test the server. - GET `/` - return a JSON to test the server.
This group of methods uses `:key` option from config: This group of methods uses `:key` option from config:
* GET `/:key/id` - return a new user id. required `:key` from config.
* GET `/:key/peers` - return an array of all connected users. required `:key` from config. **IMPORTANT:** You should set `allow_discovery` to `true` in config to enable this method. It disabled by default. - GET `/:key/id` - return a new user id. required `:key` from config.
- GET `/:key/peers` - return an array of all connected users. required `:key` from config. **IMPORTANT:** You should set `allow_discovery` to `true` in config to enable this method. It disabled by default.

View File

@ -1,24 +1,28 @@
import cors, {CorsOptions} from "cors"; import cors, { CorsOptions } from "cors";
import express from "express"; import express from "express";
import publicContent from "../../app.json"; import publicContent from "../../app.json";
import PublicApi from "./v1/public"; import PublicApi from "./v1/public";
import type {IConfig} from "../config"; import type { IConfig } from "../config";
import type {IRealm} from "../models/realm"; import type { IRealm } from "../models/realm";
export const Api = ({ config, realm, corsOptions }: { export const Api = ({
config: IConfig; config,
realm: IRealm; realm,
corsOptions: CorsOptions; corsOptions,
}: {
config: IConfig;
realm: IRealm;
corsOptions: CorsOptions;
}): express.Router => { }): express.Router => {
const app = express.Router(); const app = express.Router();
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.get("/", (_, res) => { app.get("/", (_, res) => {
res.send(publicContent); res.send(publicContent);
}); });
app.use("/:key", PublicApi({ config, realm })); app.use("/:key", PublicApi({ config, realm }));
return app; return app;
}; };

View File

@ -1,28 +1,32 @@
import express from "express"; import express from "express";
import type {IConfig} from "../../../config"; import type { IConfig } from "../../../config";
import type {IRealm} from "../../../models/realm"; import type { IRealm } from "../../../models/realm";
export default ({ config, realm }: { export default ({
config: IConfig; realm: IRealm; config,
realm,
}: {
config: IConfig;
realm: IRealm;
}): express.Router => { }): express.Router => {
const app = express.Router(); const app = express.Router();
// Retrieve guaranteed random ID. // Retrieve guaranteed random ID.
app.get("/id", (_, res: express.Response) => { app.get("/id", (_, res: express.Response) => {
res.contentType("html"); res.contentType("html");
res.send(realm.generateClientId(config.generateClientId)); res.send(realm.generateClientId(config.generateClientId));
}); });
// Get a list of all peers for a key, enabled by the `allowDiscovery` flag. // Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
app.get("/peers", (_, res: express.Response) => { app.get("/peers", (_, res: express.Response) => {
if (config.allow_discovery) { if (config.allow_discovery) {
const clientsIds = realm.getClientsIds(); const clientsIds = realm.getClientsIds();
return res.send(clientsIds); return res.send(clientsIds);
} }
return res.sendStatus(401); return res.sendStatus(401);
}); });
return app; return app;
}; };

View File

@ -1,38 +1,38 @@
import type {WebSocketServer, ServerOptions} from 'ws'; import type { WebSocketServer, ServerOptions } from "ws";
import type {CorsOptions} from "cors"; import type { CorsOptions } from "cors";
export interface IConfig { export interface IConfig {
readonly host: string; readonly host: string;
readonly port: number; readonly port: number;
readonly expire_timeout: number; readonly expire_timeout: number;
readonly alive_timeout: number; readonly alive_timeout: number;
readonly key: string; readonly key: string;
readonly path: string; readonly path: string;
readonly concurrent_limit: number; readonly concurrent_limit: number;
readonly allow_discovery: boolean; readonly allow_discovery: boolean;
readonly proxied: boolean | string; readonly proxied: boolean | string;
readonly cleanup_out_msgs: number; readonly cleanup_out_msgs: number;
readonly ssl?: { readonly ssl?: {
key: string; key: string;
cert: string; cert: string;
}; };
readonly generateClientId?: () => string; readonly generateClientId?: () => string;
readonly createWebSocketServer?: (options: ServerOptions) => WebSocketServer; readonly createWebSocketServer?: (options: ServerOptions) => WebSocketServer;
readonly corsOptions : CorsOptions; readonly corsOptions: CorsOptions;
} }
const defaultConfig: IConfig = { const defaultConfig: IConfig = {
host: "::", host: "::",
port: 9000, port: 9000,
expire_timeout: 5000, expire_timeout: 5000,
alive_timeout: 60000, alive_timeout: 60000,
key: "peerjs", key: "peerjs",
path: "/", path: "/",
concurrent_limit: 5000, concurrent_limit: 5000,
allow_discovery: false, allow_discovery: false,
proxied: false, proxied: false,
cleanup_out_msgs: 1000, cleanup_out_msgs: 1000,
corsOptions: {origin: true}, corsOptions: { origin: true },
}; };
export default defaultConfig; export default defaultConfig;

View File

@ -1,18 +1,18 @@
export enum Errors { export enum Errors {
INVALID_KEY = "Invalid key provided", INVALID_KEY = "Invalid key provided",
INVALID_TOKEN = "Invalid token provided", INVALID_TOKEN = "Invalid token provided",
INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server", INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server",
CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit" CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit",
} }
export enum MessageType { export enum MessageType {
OPEN = "OPEN", OPEN = "OPEN",
LEAVE = "LEAVE", LEAVE = "LEAVE",
CANDIDATE = "CANDIDATE", CANDIDATE = "CANDIDATE",
OFFER = "OFFER", OFFER = "OFFER",
ANSWER = "ANSWER", ANSWER = "ANSWER",
EXPIRE = "EXPIRE", EXPIRE = "EXPIRE",
HEARTBEAT = "HEARTBEAT", HEARTBEAT = "HEARTBEAT",
ID_TAKEN = "ID-TAKEN", ID_TAKEN = "ID-TAKEN",
ERROR = "ERROR" ERROR = "ERROR",
} }

View File

@ -1,72 +1,79 @@
import express, {type Express} from "express"; import express, { type Express } from "express";
import http from "node:http"; import http from "node:http";
import https from "node:https"; import https from "node:https";
import type {IConfig} from "./config"; import type { IConfig } from "./config";
import defaultConfig from "./config"; import defaultConfig from "./config";
import type {PeerServerEvents} from "./instance"; import type { PeerServerEvents } from "./instance";
import {createInstance} from "./instance"; import { createInstance } from "./instance";
import type { IClient } from "./models/client"; import type { IClient } from "./models/client";
import type { IMessage } from "./models/message"; import type { IMessage } from "./models/message";
export type {MessageType} from "./enums" export type { MessageType } from "./enums";
export type {IConfig, PeerServerEvents, IClient, IMessage} export type { IConfig, PeerServerEvents, IClient, IMessage };
function ExpressPeerServer(server: https.Server | http.Server, options?: Partial<IConfig>) { function ExpressPeerServer(
const app = express(); server: https.Server | http.Server,
options?: Partial<IConfig>,
) {
const app = express();
const newOptions: IConfig = { const newOptions: IConfig = {
...defaultConfig, ...defaultConfig,
...options ...options,
}; };
if (newOptions.proxied) { if (newOptions.proxied) {
app.set("trust proxy", newOptions.proxied === "false" ? false : !!newOptions.proxied); app.set(
} "trust proxy",
newOptions.proxied === "false" ? false : !!newOptions.proxied,
);
}
app.on("mount", () => { app.on("mount", () => {
if (!server) { if (!server) {
throw new Error("Server is not passed to constructor - " + throw new Error(
"can't start PeerServer"); "Server is not passed to constructor - " + "can't start PeerServer",
} );
}
createInstance({ app, server, options: newOptions }); createInstance({ app, server, options: newOptions });
}); });
return app as Express & PeerServerEvents return app as Express & PeerServerEvents;
} }
function PeerServer(options: Partial<IConfig> = {}, callback?: (server: https.Server | http.Server) => void) { function PeerServer(
const app = express(); options: Partial<IConfig> = {},
callback?: (server: https.Server | http.Server) => void,
) {
const app = express();
let newOptions: IConfig = { let newOptions: IConfig = {
...defaultConfig, ...defaultConfig,
...options ...options,
}; };
const port = newOptions.port; const port = newOptions.port;
const host = newOptions.host; const host = newOptions.host;
let server: https.Server | http.Server; let server: https.Server | http.Server;
const { ssl, ...restOptions } = newOptions; const { ssl, ...restOptions } = newOptions;
if (ssl && Object.keys(ssl).length) { if (ssl && Object.keys(ssl).length) {
server = https.createServer(ssl, app); server = https.createServer(ssl, app);
newOptions = restOptions; newOptions = restOptions;
} else { } else {
server = http.createServer(app); server = http.createServer(app);
} }
const peerjs = ExpressPeerServer(server, newOptions); const peerjs = ExpressPeerServer(server, newOptions);
app.use(peerjs); app.use(peerjs);
server.listen(port, host, () => callback?.(server)); server.listen(port, host, () => callback?.(server));
return peerjs; return peerjs;
} }
export { export { ExpressPeerServer, PeerServer };
ExpressPeerServer,
PeerServer
};

View File

@ -1,85 +1,99 @@
import type express from "express"; import type express from "express";
import type {Server as HttpServer} from "node:http"; import type { Server as HttpServer } from "node:http";
import type {Server as HttpsServer} from "node:https"; import type { Server as HttpsServer } from "node:https";
import path from "node:path"; import path from "node:path";
import type {IRealm} from "./models/realm"; import type { IRealm } from "./models/realm";
import {Realm} from "./models/realm"; import { Realm } from "./models/realm";
import {CheckBrokenConnections} from "./services/checkBrokenConnections"; import { CheckBrokenConnections } from "./services/checkBrokenConnections";
import type {IMessagesExpire} from "./services/messagesExpire"; import type { IMessagesExpire } from "./services/messagesExpire";
import {MessagesExpire} from "./services/messagesExpire"; import { MessagesExpire } from "./services/messagesExpire";
import type {IWebSocketServer} from "./services/webSocketServer"; import type { IWebSocketServer } from "./services/webSocketServer";
import {WebSocketServer} from "./services/webSocketServer"; import { WebSocketServer } from "./services/webSocketServer";
import {MessageHandler} from "./messageHandler"; import { MessageHandler } from "./messageHandler";
import {Api} from "./api"; import { Api } from "./api";
import type {IClient} from "./models/client"; import type { IClient } from "./models/client";
import type {IMessage} from "./models/message"; import type { IMessage } from "./models/message";
import type {IConfig} from "./config"; import type { IConfig } from "./config";
export interface PeerServerEvents { export interface PeerServerEvents {
on(event: 'connection', listener: (client: IClient) => void): this; on(event: "connection", listener: (client: IClient) => void): this;
on(event: "message", listener: (client: IClient, message: IMessage) => void): this; on(
event: "message",
listener: (client: IClient, message: IMessage) => void,
): this;
on(event: "disconnect", listener: (client: IClient) => void): this; on(event: "disconnect", listener: (client: IClient) => void): this;
on(event: "error", listener: (client: Error) => void): this; on(event: "error", listener: (client: Error) => void): this;
} }
export const createInstance = ({ app, server, options }: { export const createInstance = ({
app: express.Application; app,
server: HttpServer | HttpsServer; server,
options: IConfig; options,
}: {
app: express.Application;
server: HttpServer | HttpsServer;
options: IConfig;
}): void => { }): void => {
const config = options; const config = options;
const realm: IRealm = new Realm(); const realm: IRealm = new Realm();
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const api = Api({ config, realm , corsOptions: options.corsOptions }); const api = Api({ config, realm, corsOptions: options.corsOptions });
const messagesExpire: IMessagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire: IMessagesExpire = new MessagesExpire({
const checkBrokenConnections = new CheckBrokenConnections({ realm,
realm, config,
config, messageHandler,
onClose: client => { });
app.emit("disconnect", client); const checkBrokenConnections = new CheckBrokenConnections({
} realm,
}); config,
onClose: (client) => {
app.emit("disconnect", client);
},
});
app.use(options.path, api); app.use(options.path, api);
//use mountpath for WS server //use mountpath for WS server
const customConfig = { ...config, path: path.posix.join(app.path(), options.path, '/') }; const customConfig = {
...config,
path: path.posix.join(app.path(), options.path, "/"),
};
const wss: IWebSocketServer = new WebSocketServer({ const wss: IWebSocketServer = new WebSocketServer({
server, server,
realm, realm,
config: customConfig config: customConfig,
}); });
wss.on("connection", (client: IClient) => { wss.on("connection", (client: IClient) => {
const messageQueue = realm.getMessageQueueById(client.getId()); const messageQueue = realm.getMessageQueueById(client.getId());
if (messageQueue) { if (messageQueue) {
let message: IMessage | undefined; let message: IMessage | undefined;
while ((message = messageQueue.readMessage())) { while ((message = messageQueue.readMessage())) {
messageHandler.handle(client, message); messageHandler.handle(client, message);
} }
realm.clearMessageQueue(client.getId()); realm.clearMessageQueue(client.getId());
} }
app.emit("connection", client); app.emit("connection", client);
}); });
wss.on("message", (client: IClient, message: IMessage) => { wss.on("message", (client: IClient, message: IMessage) => {
app.emit("message", client, message); app.emit("message", client, message);
messageHandler.handle(client, message); messageHandler.handle(client, message);
}); });
wss.on("close", (client: IClient) => { wss.on("close", (client: IClient) => {
app.emit("disconnect", client); app.emit("disconnect", client);
}); });
wss.on("error", (error: Error) => { wss.on("error", (error: Error) => {
app.emit("error", error); app.emit("error", error);
}); });
messagesExpire.startMessagesExpiration(); messagesExpire.startMessagesExpiration();
checkBrokenConnections.start(); checkBrokenConnections.start();
}; };

View File

@ -1,4 +1,7 @@
import type {IClient} from "../models/client"; import type { IClient } from "../models/client";
import type {IMessage} from "../models/message"; import type { IMessage } from "../models/message";
export type Handler = (client: IClient | undefined, message: IMessage) => boolean; export type Handler = (
client: IClient | undefined,
message: IMessage,
) => boolean;

View File

@ -1,10 +1,10 @@
import type {IClient} from "../../../models/client"; import type { IClient } from "../../../models/client";
export const HeartbeatHandler = (client: IClient | undefined): boolean => { export const HeartbeatHandler = (client: IClient | undefined): boolean => {
if (client) { if (client) {
const nowTime = new Date().getTime(); const nowTime = new Date().getTime();
client.setLastPing(nowTime); client.setLastPing(nowTime);
} }
return true; return true;
}; };

View File

@ -1,61 +1,65 @@
import {MessageType} from "../../../enums"; import { MessageType } from "../../../enums";
import type {IClient} from "../../../models/client"; import type { IClient } from "../../../models/client";
import type {IMessage} from "../../../models/message"; import type { IMessage } from "../../../models/message";
import type {IRealm} from "../../../models/realm"; import type { IRealm } from "../../../models/realm";
export const TransmissionHandler = ({ realm }: { realm: IRealm; }): (client: IClient | undefined, message: IMessage) => boolean => { export const TransmissionHandler = ({
const handle = (client: IClient | undefined, message: IMessage) => { realm,
const type = message.type; }: {
const srcId = message.src; realm: IRealm;
const dstId = message.dst; }): ((client: IClient | undefined, message: IMessage) => boolean) => {
const handle = (client: IClient | undefined, message: IMessage) => {
const type = message.type;
const srcId = message.src;
const dstId = message.dst;
const destinationClient = realm.getClientById(dstId); const destinationClient = realm.getClientById(dstId);
// User is connected! // User is connected!
if (destinationClient) { if (destinationClient) {
const socket = destinationClient.getSocket(); const socket = destinationClient.getSocket();
try { try {
if (socket) { if (socket) {
const data = JSON.stringify(message); const data = JSON.stringify(message);
socket.send(data); socket.send(data);
} else { } else {
// Neither socket no res available. Peer dead? // Neither socket no res available. Peer dead?
throw new Error("Peer dead"); throw new Error("Peer dead");
} }
} catch (e) { } catch (e) {
// This happens when a peer disconnects without closing connections and // This happens when a peer disconnects without closing connections and
// the associated WebSocket has not closed. // the associated WebSocket has not closed.
// Tell other side to stop trying. // Tell other side to stop trying.
if (socket) { if (socket) {
socket.close(); socket.close();
} else { } else {
realm.removeClientById(destinationClient.getId()); realm.removeClientById(destinationClient.getId());
} }
handle(client, { handle(client, {
type: MessageType.LEAVE, type: MessageType.LEAVE,
src: dstId, src: dstId,
dst: srcId dst: srcId,
}); });
} }
} else { } else {
// Wait for this client to connect/reconnect (XHR) for important // Wait for this client to connect/reconnect (XHR) for important
// messages. // messages.
const ignoredTypes = [MessageType.LEAVE, MessageType.EXPIRE]; const ignoredTypes = [MessageType.LEAVE, MessageType.EXPIRE];
if (!ignoredTypes.includes(type) && dstId) { if (!ignoredTypes.includes(type) && dstId) {
realm.addMessageToQueue(dstId, message); realm.addMessageToQueue(dstId, message);
} else if (type === MessageType.LEAVE && !dstId) { } else if (type === MessageType.LEAVE && !dstId) {
realm.removeClientById(srcId); realm.removeClientById(srcId);
} else { } else {
// Unavailable destination specified with message LEAVE or EXPIRE // Unavailable destination specified with message LEAVE or EXPIRE
// Ignore // Ignore
} }
} }
return true; return true;
}; };
return handle; return handle;
}; };

View File

@ -1,29 +1,29 @@
import type {MessageType} from "../enums"; import type { MessageType } from "../enums";
import type {IClient} from "../models/client"; import type { IClient } from "../models/client";
import type {IMessage} from "../models/message"; import type { IMessage } from "../models/message";
import type {Handler} from "./handler"; import type { Handler } from "./handler";
export interface IHandlersRegistry { export interface IHandlersRegistry {
registerHandler(messageType: MessageType, handler: Handler): void; registerHandler(messageType: MessageType, handler: Handler): void;
handle(client: IClient | undefined, message: IMessage): boolean; handle(client: IClient | undefined, message: IMessage): boolean;
} }
export class HandlersRegistry implements IHandlersRegistry { export class HandlersRegistry implements IHandlersRegistry {
private readonly handlers: Map<MessageType, Handler> = new Map(); private readonly handlers: Map<MessageType, Handler> = new Map();
public registerHandler(messageType: MessageType, handler: Handler): void { public registerHandler(messageType: MessageType, handler: Handler): void {
if (this.handlers.has(messageType)) return; if (this.handlers.has(messageType)) return;
this.handlers.set(messageType, handler); this.handlers.set(messageType, handler);
} }
public handle(client: IClient | undefined, message: IMessage): boolean { public handle(client: IClient | undefined, message: IMessage): boolean {
const { type } = message; const { type } = message;
const handler = this.handlers.get(type); const handler = this.handlers.get(type);
if (!handler) return false; if (!handler) return false;
return handler(client, message); return handler(client, message);
} }
} }

View File

@ -1,41 +1,66 @@
import {MessageType} from "../enums"; import { MessageType } from "../enums";
import {HeartbeatHandler, TransmissionHandler} from "./handlers"; import { HeartbeatHandler, TransmissionHandler } from "./handlers";
import type {IHandlersRegistry} from "./handlersRegistry"; import type { IHandlersRegistry } from "./handlersRegistry";
import {HandlersRegistry} from "./handlersRegistry"; import { HandlersRegistry } from "./handlersRegistry";
import type {IClient} from "../models/client"; import type { IClient } from "../models/client";
import type {IMessage} from "../models/message"; import type { IMessage } from "../models/message";
import type {IRealm} from "../models/realm"; import type { IRealm } from "../models/realm";
import type {Handler} from "./handler"; import type { Handler } from "./handler";
export interface IMessageHandler { export interface IMessageHandler {
handle(client: IClient | undefined, message: IMessage): boolean; handle(client: IClient | undefined, message: IMessage): boolean;
} }
export class MessageHandler implements IMessageHandler { export class MessageHandler implements IMessageHandler {
constructor(realm: IRealm, private readonly handlersRegistry: IHandlersRegistry = new HandlersRegistry()) { constructor(
const transmissionHandler: Handler = TransmissionHandler({ realm }); realm: IRealm,
const heartbeatHandler: Handler = HeartbeatHandler; private readonly handlersRegistry: IHandlersRegistry = new HandlersRegistry(),
) {
const transmissionHandler: Handler = TransmissionHandler({ realm });
const heartbeatHandler: Handler = HeartbeatHandler;
const handleTransmission: Handler = (client: IClient | undefined, { type, src, dst, payload }: IMessage): boolean => { const handleTransmission: Handler = (
return transmissionHandler(client, { client: IClient | undefined,
type, { type, src, dst, payload }: IMessage,
src, ): boolean => {
dst, return transmissionHandler(client, {
payload, type,
}); src,
}; dst,
payload,
});
};
const handleHeartbeat = (client: IClient | undefined, message: IMessage) => heartbeatHandler(client, message); const handleHeartbeat = (client: IClient | undefined, message: IMessage) =>
heartbeatHandler(client, message);
this.handlersRegistry.registerHandler(MessageType.HEARTBEAT, handleHeartbeat); this.handlersRegistry.registerHandler(
this.handlersRegistry.registerHandler(MessageType.OFFER, handleTransmission); MessageType.HEARTBEAT,
this.handlersRegistry.registerHandler(MessageType.ANSWER, handleTransmission); handleHeartbeat,
this.handlersRegistry.registerHandler(MessageType.CANDIDATE, handleTransmission); );
this.handlersRegistry.registerHandler(MessageType.LEAVE, handleTransmission); this.handlersRegistry.registerHandler(
this.handlersRegistry.registerHandler(MessageType.EXPIRE, handleTransmission); MessageType.OFFER,
} handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.ANSWER,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.CANDIDATE,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.LEAVE,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.EXPIRE,
handleTransmission,
);
}
public handle(client: IClient | undefined, message: IMessage): boolean { public handle(client: IClient | undefined, message: IMessage): boolean {
return this.handlersRegistry.handle(client, message); return this.handlersRegistry.handle(client, message);
} }
} }

View File

@ -1,57 +1,57 @@
import type WebSocket from "ws"; import type WebSocket from "ws";
export interface IClient { export interface IClient {
getId(): string; getId(): string;
getToken(): string; getToken(): string;
getSocket(): WebSocket | null; getSocket(): WebSocket | null;
setSocket(socket: WebSocket | null): void; setSocket(socket: WebSocket | null): void;
getLastPing(): number; getLastPing(): number;
setLastPing(lastPing: number): void; setLastPing(lastPing: number): void;
send<T>(data: T): void; send<T>(data: T): void;
} }
export class Client implements IClient { export class Client implements IClient {
private readonly id: string; private readonly id: string;
private readonly token: string; private readonly token: string;
private socket: WebSocket | null = null; private socket: WebSocket | null = null;
private lastPing: number = new Date().getTime(); private lastPing: number = new Date().getTime();
constructor({ id, token }: { id: string; token: string; }) { constructor({ id, token }: { id: string; token: string }) {
this.id = id; this.id = id;
this.token = token; this.token = token;
} }
public getId(): string { public getId(): string {
return this.id; return this.id;
} }
public getToken(): string { public getToken(): string {
return this.token; return this.token;
} }
public getSocket(): WebSocket | null { public getSocket(): WebSocket | null {
return this.socket; return this.socket;
} }
public setSocket(socket: WebSocket | null): void { public setSocket(socket: WebSocket | null): void {
this.socket = socket; this.socket = socket;
} }
public getLastPing(): number { public getLastPing(): number {
return this.lastPing; return this.lastPing;
} }
public setLastPing(lastPing: number): void { public setLastPing(lastPing: number): void {
this.lastPing = lastPing; this.lastPing = lastPing;
} }
public send<T>(data: T): void { public send<T>(data: T): void {
this.socket?.send(JSON.stringify(data)); this.socket?.send(JSON.stringify(data));
} }
} }

View File

@ -1,8 +1,8 @@
import type {MessageType} from "../enums"; import type { MessageType } from "../enums";
export interface IMessage { export interface IMessage {
readonly type: MessageType; readonly type: MessageType;
readonly src: string; readonly src: string;
readonly dst: string; readonly dst: string;
readonly payload?: string |undefined; readonly payload?: string | undefined;
} }

View File

@ -1,37 +1,37 @@
import type {IMessage} from "./message"; import type { IMessage } from "./message";
export interface IMessageQueue { export interface IMessageQueue {
getLastReadAt(): number; getLastReadAt(): number;
addMessage(message: IMessage): void; addMessage(message: IMessage): void;
readMessage(): IMessage | undefined; readMessage(): IMessage | undefined;
getMessages(): IMessage[]; getMessages(): IMessage[];
} }
export class MessageQueue implements IMessageQueue { export class MessageQueue implements IMessageQueue {
private lastReadAt: number = new Date().getTime(); private lastReadAt: number = new Date().getTime();
private readonly messages: IMessage[] = []; private readonly messages: IMessage[] = [];
public getLastReadAt(): number { public getLastReadAt(): number {
return this.lastReadAt; return this.lastReadAt;
} }
public addMessage(message: IMessage): void { public addMessage(message: IMessage): void {
this.messages.push(message); this.messages.push(message);
} }
public readMessage(): IMessage | undefined { public readMessage(): IMessage | undefined {
if (this.messages.length > 0) { if (this.messages.length > 0) {
this.lastReadAt = new Date().getTime(); this.lastReadAt = new Date().getTime();
return this.messages.shift(); return this.messages.shift();
} }
return undefined; return undefined;
} }
public getMessages(): IMessage[] { public getMessages(): IMessage[] {
return this.messages; return this.messages;
} }
} }

View File

@ -1,84 +1,84 @@
import type {IMessageQueue} from "./messageQueue"; import type { IMessageQueue } from "./messageQueue";
import {MessageQueue} from "./messageQueue"; import { MessageQueue } from "./messageQueue";
import {randomUUID} from "node:crypto"; import { randomUUID } from "node:crypto";
import type {IClient} from "./client"; import type { IClient } from "./client";
import type {IMessage} from "./message"; import type { IMessage } from "./message";
export interface IRealm { export interface IRealm {
getClientsIds(): string[]; getClientsIds(): string[];
getClientById(clientId: string): IClient | undefined; getClientById(clientId: string): IClient | undefined;
getClientsIdsWithQueue(): string[]; getClientsIdsWithQueue(): string[];
setClient(client: IClient, id: string): void; setClient(client: IClient, id: string): void;
removeClientById(id: string): boolean; removeClientById(id: string): boolean;
getMessageQueueById(id: string): IMessageQueue | undefined; getMessageQueueById(id: string): IMessageQueue | undefined;
addMessageToQueue(id: string, message: IMessage): void; addMessageToQueue(id: string, message: IMessage): void;
clearMessageQueue(id: string): void; clearMessageQueue(id: string): void;
generateClientId(generateClientId?: () => string): string; generateClientId(generateClientId?: () => string): string;
} }
export class Realm implements IRealm { export class Realm implements IRealm {
private readonly clients: Map<string, IClient> = new Map(); private readonly clients: Map<string, IClient> = new Map();
private readonly messageQueues: Map<string, IMessageQueue> = new Map(); private readonly messageQueues: Map<string, IMessageQueue> = new Map();
public getClientsIds(): string[] { public getClientsIds(): string[] {
return [...this.clients.keys()]; return [...this.clients.keys()];
} }
public getClientById(clientId: string): IClient | undefined { public getClientById(clientId: string): IClient | undefined {
return this.clients.get(clientId); return this.clients.get(clientId);
} }
public getClientsIdsWithQueue(): string[] { public getClientsIdsWithQueue(): string[] {
return [...this.messageQueues.keys()]; return [...this.messageQueues.keys()];
} }
public setClient(client: IClient, id: string): void { public setClient(client: IClient, id: string): void {
this.clients.set(id, client); this.clients.set(id, client);
} }
public removeClientById(id: string): boolean { public removeClientById(id: string): boolean {
const client = this.getClientById(id); const client = this.getClientById(id);
if (!client) return false; if (!client) return false;
this.clients.delete(id); this.clients.delete(id);
return true; return true;
} }
public getMessageQueueById(id: string): IMessageQueue | undefined { public getMessageQueueById(id: string): IMessageQueue | undefined {
return this.messageQueues.get(id); return this.messageQueues.get(id);
} }
public addMessageToQueue(id: string, message: IMessage): void { public addMessageToQueue(id: string, message: IMessage): void {
if (!this.getMessageQueueById(id)) { if (!this.getMessageQueueById(id)) {
this.messageQueues.set(id, new MessageQueue()); this.messageQueues.set(id, new MessageQueue());
} }
this.getMessageQueueById(id)?.addMessage(message); this.getMessageQueueById(id)?.addMessage(message);
} }
public clearMessageQueue(id: string): void { public clearMessageQueue(id: string): void {
this.messageQueues.delete(id); this.messageQueues.delete(id);
} }
public generateClientId(generateClientId?: () => string): string { public generateClientId(generateClientId?: () => string): string {
const generateId = generateClientId ? generateClientId : randomUUID; const generateId = generateClientId ? generateClientId : randomUUID;
let clientId = generateId(); let clientId = generateId();
while (this.getClientById(clientId)) { while (this.getClientById(clientId)) {
clientId = generateId(); clientId = generateId();
} }
return clientId; return clientId;
} }
} }

View File

@ -1,77 +1,81 @@
import type {IConfig} from "../../config"; import type { IConfig } from "../../config";
import type {IClient} from "../../models/client"; import type { IClient } from "../../models/client";
import type {IRealm} from "../../models/realm"; import type { IRealm } from "../../models/realm";
const DEFAULT_CHECK_INTERVAL = 300; const DEFAULT_CHECK_INTERVAL = 300;
type CustomConfig = Pick<IConfig, 'alive_timeout'>; type CustomConfig = Pick<IConfig, "alive_timeout">;
export class CheckBrokenConnections { export class CheckBrokenConnections {
public readonly checkInterval: number;
private timeoutId: NodeJS.Timeout | null = null;
private readonly realm: IRealm;
private readonly config: CustomConfig;
private readonly onClose?: (client: IClient) => void;
public readonly checkInterval: number; constructor({
private timeoutId: NodeJS.Timeout | null = null; realm,
private readonly realm: IRealm; config,
private readonly config: CustomConfig; checkInterval = DEFAULT_CHECK_INTERVAL,
private readonly onClose?: (client: IClient) => void; onClose,
}: {
realm: IRealm;
config: CustomConfig;
checkInterval?: number;
onClose?: (client: IClient) => void;
}) {
this.realm = realm;
this.config = config;
this.onClose = onClose;
this.checkInterval = checkInterval;
}
constructor({ realm, config, checkInterval = DEFAULT_CHECK_INTERVAL, onClose }: { public start(): void {
realm: IRealm; if (this.timeoutId) {
config: CustomConfig; clearTimeout(this.timeoutId);
checkInterval?: number; }
onClose?: (client: IClient) => void;
}) {
this.realm = realm;
this.config = config;
this.onClose = onClose;
this.checkInterval = checkInterval;
}
public start(): void { this.timeoutId = setTimeout(() => {
if (this.timeoutId) { this.checkConnections();
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => { this.timeoutId = null;
this.checkConnections();
this.timeoutId = null; this.start();
}, this.checkInterval);
}
this.start(); public stop(): void {
}, this.checkInterval); if (this.timeoutId) {
} clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
public stop(): void { private checkConnections(): void {
if (this.timeoutId) { const clientsIds = this.realm.getClientsIds();
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
private checkConnections(): void { const now = new Date().getTime();
const clientsIds = this.realm.getClientsIds(); const { alive_timeout: aliveTimeout } = this.config;
const now = new Date().getTime(); for (const clientId of clientsIds) {
const { alive_timeout: aliveTimeout } = this.config; const client = this.realm.getClientById(clientId);
for (const clientId of clientsIds) { if (!client) continue;
const client = this.realm.getClientById(clientId);
if (!client) continue; const timeSinceLastPing = now - client.getLastPing();
const timeSinceLastPing = now - client.getLastPing(); if (timeSinceLastPing < aliveTimeout) continue;
if (timeSinceLastPing < aliveTimeout) continue; try {
client.getSocket()?.close();
} finally {
this.realm.clearMessageQueue(clientId);
this.realm.removeClientById(clientId);
try { client.setSocket(null);
client.getSocket()?.close();
} finally {
this.realm.clearMessageQueue(clientId);
this.realm.removeClientById(clientId);
client.setSocket(null); this.onClose?.(client);
}
this.onClose?.(client); }
} }
}
}
} }

View File

@ -1,88 +1,92 @@
import {MessageType} from "../../enums"; import { MessageType } from "../../enums";
import type {IConfig} from "../../config"; import type { IConfig } from "../../config";
import type {IMessageHandler} from "../../messageHandler"; import type { IMessageHandler } from "../../messageHandler";
import type {IRealm} from "../../models/realm"; import type { IRealm } from "../../models/realm";
export interface IMessagesExpire { export interface IMessagesExpire {
startMessagesExpiration(): void; startMessagesExpiration(): void;
stopMessagesExpiration(): void; stopMessagesExpiration(): void;
} }
type CustomConfig = Pick<IConfig, 'cleanup_out_msgs' | 'expire_timeout'>; type CustomConfig = Pick<IConfig, "cleanup_out_msgs" | "expire_timeout">;
export class MessagesExpire implements IMessagesExpire { export class MessagesExpire implements IMessagesExpire {
private readonly realm: IRealm; private readonly realm: IRealm;
private readonly config: CustomConfig; private readonly config: CustomConfig;
private readonly messageHandler: IMessageHandler; private readonly messageHandler: IMessageHandler;
private timeoutId: NodeJS.Timeout | null = null; private timeoutId: NodeJS.Timeout | null = null;
constructor({ realm, config, messageHandler }: { constructor({
realm: IRealm; realm,
config: CustomConfig; config,
messageHandler: IMessageHandler; messageHandler,
}) { }: {
this.realm = realm; realm: IRealm;
this.config = config; config: CustomConfig;
this.messageHandler = messageHandler; messageHandler: IMessageHandler;
} }) {
this.realm = realm;
this.config = config;
this.messageHandler = messageHandler;
}
public startMessagesExpiration(): void { public startMessagesExpiration(): void {
if (this.timeoutId) { if (this.timeoutId) {
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId);
} }
// Clean up outstanding messages // Clean up outstanding messages
this.timeoutId = setTimeout(() => { this.timeoutId = setTimeout(() => {
this.pruneOutstanding(); this.pruneOutstanding();
this.timeoutId = null; this.timeoutId = null;
this.startMessagesExpiration(); this.startMessagesExpiration();
}, this.config.cleanup_out_msgs); }, this.config.cleanup_out_msgs);
} }
public stopMessagesExpiration(): void { public stopMessagesExpiration(): void {
if (this.timeoutId) { if (this.timeoutId) {
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId);
this.timeoutId = null; this.timeoutId = null;
} }
} }
private pruneOutstanding(): void { private pruneOutstanding(): void {
const destinationClientsIds = this.realm.getClientsIdsWithQueue(); const destinationClientsIds = this.realm.getClientsIdsWithQueue();
const now = new Date().getTime(); const now = new Date().getTime();
const maxDiff = this.config.expire_timeout; const maxDiff = this.config.expire_timeout;
const seen: Record<string, boolean> = {}; const seen: Record<string, boolean> = {};
for (const destinationClientId of destinationClientsIds) { for (const destinationClientId of destinationClientsIds) {
const messageQueue = this.realm.getMessageQueueById(destinationClientId); const messageQueue = this.realm.getMessageQueueById(destinationClientId);
if (!messageQueue) continue; if (!messageQueue) continue;
const lastReadDiff = now - messageQueue.getLastReadAt(); const lastReadDiff = now - messageQueue.getLastReadAt();
if (lastReadDiff < maxDiff) continue; if (lastReadDiff < maxDiff) continue;
const messages = messageQueue.getMessages(); const messages = messageQueue.getMessages();
for (const message of messages) { for (const message of messages) {
const seenKey = `${message.src}_${message.dst}`; const seenKey = `${message.src}_${message.dst}`;
if (!seen[seenKey]) { if (!seen[seenKey]) {
this.messageHandler.handle(undefined, { this.messageHandler.handle(undefined, {
type: MessageType.EXPIRE, type: MessageType.EXPIRE,
src: message.dst, src: message.dst,
dst: message.src, dst: message.src,
}); });
seen[seenKey] = true; seen[seenKey] = true;
} }
} }
this.realm.clearMessageQueue(destinationClientId); this.realm.clearMessageQueue(destinationClientId);
} }
} }
} }

View File

@ -1,158 +1,173 @@
import {EventEmitter} from "node:events"; import { EventEmitter } from "node:events";
import type {IncomingMessage} from "node:http"; import type { IncomingMessage } from "node:http";
import url from "node:url"; import url from "node:url";
import type WebSocket from "ws"; import type WebSocket from "ws";
import {Errors, MessageType} from "../../enums"; import { Errors, MessageType } from "../../enums";
import type {IClient} from "../../models/client"; import type { IClient } from "../../models/client";
import {Client} from "../../models/client"; import { Client } from "../../models/client";
import type {IConfig} from "../../config"; import type { IConfig } from "../../config";
import type {IRealm} from "../../models/realm"; import type { IRealm } from "../../models/realm";
import {WebSocketServer as Server} from "ws"; import { WebSocketServer as Server } from "ws";
import type {Server as HttpServer} from "node:http"; import type { Server as HttpServer } from "node:http";
import type {Server as HttpsServer} from "node:https"; import type { Server as HttpsServer } from "node:https";
export interface IWebSocketServer extends EventEmitter { export interface IWebSocketServer extends EventEmitter {
readonly path: string; readonly path: string;
} }
interface IAuthParams { interface IAuthParams {
id?: string; id?: string;
token?: string; token?: string;
key?: string; key?: string;
} }
type CustomConfig = Pick<IConfig, 'path' | 'key' | 'concurrent_limit' | 'createWebSocketServer'>; type CustomConfig = Pick<
IConfig,
"path" | "key" | "concurrent_limit" | "createWebSocketServer"
>;
const WS_PATH = 'peerjs'; const WS_PATH = "peerjs";
export class WebSocketServer extends EventEmitter implements IWebSocketServer { export class WebSocketServer extends EventEmitter implements IWebSocketServer {
public readonly path: string;
private readonly realm: IRealm;
private readonly config: CustomConfig;
public readonly socketServer: Server;
public readonly path: string; constructor({
private readonly realm: IRealm; server,
private readonly config: CustomConfig; realm,
public readonly socketServer: Server; config,
}: {
server: HttpServer | HttpsServer;
realm: IRealm;
config: CustomConfig;
}) {
super();
constructor({ server, realm, config }: { server: HttpServer | HttpsServer; realm: IRealm; config: CustomConfig; }) { this.setMaxListeners(0);
super();
this.setMaxListeners(0); this.realm = realm;
this.config = config;
this.realm = realm; const path = this.config.path;
this.config = config; this.path = `${path}${path.endsWith("/") ? "" : "/"}${WS_PATH}`;
const path = this.config.path; const options: WebSocket.ServerOptions = {
this.path = `${path}${path.endsWith('/') ? "" : "/"}${WS_PATH}`; path: this.path,
server,
};
const options: WebSocket.ServerOptions = { this.socketServer = config.createWebSocketServer
path: this.path, ? config.createWebSocketServer(options)
server, : new Server(options);
};
this.socketServer = ( this.socketServer.on("connection", (socket, req) =>
config.createWebSocketServer ? this._onSocketConnection(socket, req),
config.createWebSocketServer(options) : );
new Server(options) this.socketServer.on("error", (error: Error) => this._onSocketError(error));
); }
this.socketServer.on("connection", (socket, req) => this._onSocketConnection(socket, req)); private _onSocketConnection(socket: WebSocket, req: IncomingMessage): void {
this.socketServer.on("error", (error: Error) => this._onSocketError(error)); // An unhandled socket error might crash the server. Handle it first.
} socket.on("error", (error) => this._onSocketError(error));
private _onSocketConnection(socket: WebSocket, req: IncomingMessage): void { const { query = {} } = url.parse(req.url ?? "", true);
// An unhandled socket error might crash the server. Handle it first.
socket.on("error", error => this._onSocketError(error))
const { query = {} } = url.parse(req.url ?? '', true); const { id, token, key }: IAuthParams = query;
const { id, token, key }: IAuthParams = query; if (!id || !token || !key) {
return this._sendErrorAndClose(socket, Errors.INVALID_WS_PARAMETERS);
}
if (!id || !token || !key) { if (key !== this.config.key) {
return this._sendErrorAndClose(socket, Errors.INVALID_WS_PARAMETERS); return this._sendErrorAndClose(socket, Errors.INVALID_KEY);
} }
if (key !== this.config.key) { const client = this.realm.getClientById(id);
return this._sendErrorAndClose(socket, Errors.INVALID_KEY);
}
const client = this.realm.getClientById(id); if (client) {
if (token !== client.getToken()) {
// ID-taken, invalid token
socket.send(
JSON.stringify({
type: MessageType.ID_TAKEN,
payload: { msg: "ID is taken" },
}),
);
if (client) { return socket.close();
if (token !== client.getToken()) { }
// ID-taken, invalid token
socket.send(JSON.stringify({
type: MessageType.ID_TAKEN,
payload: { msg: "ID is taken" }
}));
return socket.close(); return this._configureWS(socket, client);
} }
return this._configureWS(socket, client); this._registerClient({ socket, id, token });
} }
this._registerClient({ socket, id, token }); private _onSocketError(error: Error): void {
} // handle error
this.emit("error", error);
}
private _onSocketError(error: Error): void { private _registerClient({
// handle error socket,
this.emit("error", error); id,
} token,
}: {
socket: WebSocket;
id: string;
token: string;
}): void {
// Check concurrent limit
const clientsCount = this.realm.getClientsIds().length;
private _registerClient({ socket, id, token }: if (clientsCount >= this.config.concurrent_limit) {
{ return this._sendErrorAndClose(socket, Errors.CONNECTION_LIMIT_EXCEED);
socket: WebSocket; }
id: string;
token: string;
}): void {
// Check concurrent limit
const clientsCount = this.realm.getClientsIds().length;
if (clientsCount >= this.config.concurrent_limit) { const newClient: IClient = new Client({ id, token });
return this._sendErrorAndClose(socket, Errors.CONNECTION_LIMIT_EXCEED); this.realm.setClient(newClient, id);
} socket.send(JSON.stringify({ type: MessageType.OPEN }));
const newClient: IClient = new Client({ id, token }); this._configureWS(socket, newClient);
this.realm.setClient(newClient, id); }
socket.send(JSON.stringify({ type: MessageType.OPEN }));
this._configureWS(socket, newClient); private _configureWS(socket: WebSocket, client: IClient): void {
} client.setSocket(socket);
private _configureWS(socket: WebSocket, client: IClient): void { // Cleanup after a socket closes.
client.setSocket(socket); socket.on("close", () => {
if (client.getSocket() === socket) {
this.realm.removeClientById(client.getId());
this.emit("close", client);
}
});
// Cleanup after a socket closes. // Handle messages from peers.
socket.on("close", () => { socket.on("message", (data) => {
if (client.getSocket() === socket) { try {
this.realm.removeClientById(client.getId()); const message = JSON.parse(data.toString());
this.emit("close", client);
}
});
// Handle messages from peers. message.src = client.getId();
socket.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
message.src = client.getId(); this.emit("message", client, message);
} catch (e) {
this.emit("error", e);
}
});
this.emit("message", client, message); this.emit("connection", client);
} catch (e) { }
this.emit("error", e);
}
});
this.emit("connection", client); private _sendErrorAndClose(socket: WebSocket, msg: Errors): void {
} socket.send(
JSON.stringify({
type: MessageType.ERROR,
payload: { msg },
}),
);
private _sendErrorAndClose(socket: WebSocket, msg: Errors): void { socket.close();
socket.send( }
JSON.stringify({
type: MessageType.ERROR,
payload: { msg }
})
);
socket.close();
}
} }

View File

@ -1,19 +1,11 @@
{ {
"extends": "@tsconfig/node16-strictest-esm/tsconfig.json", "extends": "@tsconfig/node16-strictest-esm/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": ["esnext"],
"esnext" "noEmit": true,
], "resolveJsonModule": true,
"noEmit": true, "exactOptionalPropertyTypes": false
"resolveJsonModule": true, },
"exactOptionalPropertyTypes": false "include": ["./src/**/*", "__test__/**/*"],
}, "exclude": ["test", "bin"]
"include": [
"./src/**/*",
"__test__/**/*"
],
"exclude": [
"test",
"bin"
]
} }