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

View File

@ -1 +1 @@
blank_issues_enabled: false
blank_issues_enabled: false

View File

@ -1,28 +1,30 @@
---
name: peer template
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
title: ""
labels: ""
assignees: ""
---
### I'm having an issue:
- Give an expressive description of what is went wrong
- Version of `peer` you're experiencing this issue
- Nodejs version?
- Platform name and its version (Win, Mac, Linux)?
- 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
- Give an expressive description of what is went wrong
- Version of `peer` you're experiencing this issue
- Nodejs version?
- Platform name and its version (Win, Mac, Linux)?
- 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:
- 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.):
- Give an expressive description what you have changed/added and why
- Make sure you're using correct markdown markup
- 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
- Give an expressive description what you have changed/added and why
- Make sure you're using correct markdown markup
- 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

View File

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

View File

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

View File

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

View File

@ -1,31 +1,31 @@
{
"branches": [
"stable",
{
"name": "rc",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git",
"@semantic-release/github",
[
"@codedependant/semantic-release-docker",
{
"dockerTags": [
"{{#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}}",
"{{version}}"
],
"dockerImage": "peerjs-server",
"dockerFile": "Dockerfile",
"dockerProject": "peerjs"
}
]
]
"branches": [
"stable",
{
"name": "rc",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git",
"@semantic-release/github",
[
"@codedependant/semantic-release-docker",
{
"dockerTags": [
"{{#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}}",
"{{version}}"
],
"dockerImage": "peerjs-server",
"dockerFile": "Dockerfile",
"dockerProject": "peerjs"
}
]
]
}

View File

@ -3,8 +3,8 @@
**We do not collect or store any information.**
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
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)
setting.)

158
README.md
View File

@ -4,7 +4,8 @@
[![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)
[![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.
@ -23,20 +24,23 @@ Run your own server on Gitpod!
If you don't want to develop anything, just enter few commands below.
1. Install the package globally:
```sh
$ npm install peer -g
```
```sh
$ npm install peer -g
```
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.
#### Docker
Also, you can use Docker image to run a new container:
```sh
$ 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:
If you have your own server, you can attach PeerServer.
1. Install the package:
```bash
# $ cd your-project-path
# with npm
$ npm install peer
# with yarn
$ yarn add peer
```
2. Use PeerServer object to create a new server:
```javascript
const { PeerServer } = require('peer');
const peerServer = PeerServer({ port: 9000, path: '/myapp' });
```
```bash
# $ cd your-project-path
# with npm
$ npm install peer
# with yarn
$ yarn add peer
```
2. Use PeerServer object to create a new server:
```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.
@ -73,19 +81,20 @@ If you have your own server, you can attach PeerServer.
```html
<script>
const peer = new Peer('someid', {
host: 'localhost',
port: 9000,
path: '/myapp'
});
const peer = new Peer("someid", {
host: "localhost",
port: 9000,
path: "/myapp",
});
</script>
```
## Config / CLI options
You can provide config object to `PeerServer` function or specify options for `peerjs` CLI.
| CLI option | JS option | Description | Required | Default |
|--------------------------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:----------:|
| ------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :--------: |
| `--port, -p` | `port` | Port to listen (number) | **Yes** | |
| `--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 | `"/"` |
@ -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 | |
| `--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` |
## Using HTTPS
Simply pass in PEM-encoded certificate and key.
```javascript
const fs = require('fs');
const { PeerServer } = require('peer');
const fs = require("fs");
const { PeerServer } = require("peer");
const peerServer = PeerServer({
port: 9000,
ssl: {
key: fs.readFileSync('/path/to/your/ssl/key/here.key'),
cert: fs.readFileSync('/path/to/your/ssl/certificate/here.crt')
}
port: 9000,
ssl: {
key: fs.readFileSync("/path/to/your/ssl/key/here.key"),
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:
```javascript
const fs = require('fs');
const { PeerServer } = require('peer');
const fs = require("fs");
const { PeerServer } = require("peer");
const peerServer = PeerServer({
port: 9000,
ssl: {
SNICallback: (servername, cb) => {
// your code here ....
}
}
port: 9000,
ssl: {
SNICallback: (servername, cb) => {
// your code here ....
},
},
});
```
## Running PeerServer behind a reverse proxy
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.
```javascript
const { PeerServer } = require('peer');
const { PeerServer } = require("peer");
const peerServer = PeerServer({
port: 9000,
path: '/myapp',
proxied: true
port: 9000,
path: "/myapp",
proxied: true,
});
```
## Custom client ID generation
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.
```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({
port: 9000,
path: '/myapp',
generateClientId: customGenerationFunction
port: 9000,
path: "/myapp",
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
```javascript
const express = require('express');
const { ExpressPeerServer } = require('peer');
const express = require("express");
const { ExpressPeerServer } = require("peer");
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 peerServer = ExpressPeerServer(server, {
path: '/myapp'
path: "/myapp",
});
app.use('/peerjs', peerServer);
app.use("/peerjs", peerServer);
// == OR ==
const http = require('http');
const http = require("http");
const server = http.createServer(app);
const peerServer = ExpressPeerServer(server, {
debug: true,
path: '/myapp'
debug: true,
path: "/myapp",
});
app.use('/peerjs', peerServer);
app.use("/peerjs", peerServer);
server.listen(9000);
@ -236,18 +248,20 @@ $ npm test
We have 'ready to use' images on docker hub:
https://hub.docker.com/r/peerjs/peerjs-server
To run the latest image:
To run the latest image:
```sh
$ docker run -p 9000:9000 -d peerjs/peerjs-server
```
You can build a new image simply by calling:
```sh
$ docker build -t myimage https://github.com/peers/peerjs-server.git
```
To run the image execute this:
To run the image execute this:
```sh
$ 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):
```js
const express = require('express');
const { ExpressPeerServer } = require('peer');
const express = require("express");
const { ExpressPeerServer } = require("peer");
const app = express();
app.enable('trust proxy');
app.enable("trust proxy");
const PORT = process.env.PORT || 9000;
const server = app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
console.log(`App listening on port ${PORT}`);
console.log("Press Ctrl+C to quit.");
});
const peerServer = ExpressPeerServer(server, {
path: '/'
path: "/",
});
app.use('/', peerServer);
app.use("/", peerServer);
module.exports = app;
```
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
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 { Client } from '../../../../src/models/client';
import { HeartbeatHandler } from '../../../../src/messageHandler/handlers';
import { Client } from "../../../../src/models/client";
import { HeartbeatHandler } from "../../../../src/messageHandler/handlers";
describe('Heartbeat handler', () => {
it('should update last ping time', () => {
const client = new Client({ id: 'id', token: '' });
client.setLastPing(0);
describe("Heartbeat handler", () => {
it("should update last ping time", () => {
const client = new Client({ id: "id", token: "" });
client.setLastPing(0);
const nowTime = new Date().getTime();
const nowTime = new Date().getTime();
HeartbeatHandler(client);
expect(client.getLastPing()).toBeGreaterThanOrEqual(nowTime-2)
expect(nowTime).toBeGreaterThanOrEqual(client.getLastPing())
});
HeartbeatHandler(client);
expect(client.getLastPing()).toBeGreaterThanOrEqual(nowTime - 2);
expect(nowTime).toBeGreaterThanOrEqual(client.getLastPing());
});
});

View File

@ -7,111 +7,111 @@ import { MessageType } from "../../../../src/enums";
import type WebSocket from "ws";
const createFakeSocket = (): WebSocket => {
/* eslint-disable @typescript-eslint/no-empty-function */
const sock = {
send: (): void => {},
close: (): void => {},
on: (): void => {},
};
/* eslint-enable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-empty-function */
const sock = {
send: (): void => {},
close: (): void => {},
on: (): void => {},
};
/* eslint-enable @typescript-eslint/no-empty-function */
return sock as unknown as WebSocket;
return sock as unknown as WebSocket;
};
describe("Transmission handler", () => {
it("should save message in queue when destination client not connected", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
it("should save message in queue when destination client not connected", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId());
const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId());
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.getId(),
dst: idTo,
});
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.getId(),
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", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
it("should not save LEAVE and EXPIRE messages in queue when destination client not connected", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId());
const clientFrom = new Client({ id: "id1", token: "" });
const idTo = "id2";
realm.setClient(clientFrom, clientFrom.getId());
handleTransmission(clientFrom, {
type: MessageType.LEAVE,
src: clientFrom.getId(),
dst: idTo,
});
handleTransmission(clientFrom, {
type: MessageType.EXPIRE,
src: clientFrom.getId(),
dst: idTo,
});
handleTransmission(clientFrom, {
type: MessageType.LEAVE,
src: clientFrom.getId(),
dst: idTo,
});
handleTransmission(clientFrom, {
type: MessageType.EXPIRE,
src: clientFrom.getId(),
dst: idTo,
});
expect(realm.getMessageQueueById(idTo)).toBeUndefined();
});
expect(realm.getMessageQueueById(idTo)).toBeUndefined();
});
it("should send message to destination client when destination client connected", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
it("should send message to destination client when destination client connected", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" });
const socketTo = createFakeSocket();
clientTo.setSocket(socketTo);
realm.setClient(clientTo, clientTo.getId());
const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" });
const socketTo = createFakeSocket();
clientTo.setSocket(socketTo);
realm.setClient(clientTo, clientTo.getId());
let sent = false;
socketTo.send = (): void => {
sent = true;
};
let sent = false;
socketTo.send = (): void => {
sent = true;
};
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.getId(),
dst: clientTo.getId(),
});
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.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", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
it("should send LEAVE message to source client when sending to destination client failed", () => {
const realm = new Realm();
const handleTransmission = TransmissionHandler({ realm });
const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" });
const socketFrom = createFakeSocket();
const socketTo = createFakeSocket();
clientFrom.setSocket(socketFrom);
clientTo.setSocket(socketTo);
realm.setClient(clientFrom, clientFrom.getId());
realm.setClient(clientTo, clientTo.getId());
const clientFrom = new Client({ id: "id1", token: "" });
const clientTo = new Client({ id: "id2", token: "" });
const socketFrom = createFakeSocket();
const socketTo = createFakeSocket();
clientFrom.setSocket(socketFrom);
clientTo.setSocket(socketTo);
realm.setClient(clientFrom, clientFrom.getId());
realm.setClient(clientTo, clientTo.getId());
let sent = false;
socketFrom.send = (data: string): void => {
if (JSON.parse(data)?.type === MessageType.LEAVE) {
sent = true;
}
};
let sent = false;
socketFrom.send = (data: string): void => {
if (JSON.parse(data)?.type === MessageType.LEAVE) {
sent = true;
}
};
socketTo.send = (): void => {
throw Error();
};
socketTo.send = (): void => {
throw Error();
};
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.getId(),
dst: clientTo.getId(),
});
handleTransmission(clientFrom, {
type: MessageType.OFFER,
src: clientFrom.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 { HandlersRegistry } from '../../src/messageHandler/handlersRegistry';
import type { Handler } from '../../src/messageHandler/handler';
import { MessageType } from '../../src/enums';
import { HandlersRegistry } from "../../src/messageHandler/handlersRegistry";
import type { Handler } from "../../src/messageHandler/handler";
import { MessageType } from "../../src/enums";
describe('HandlersRegistry', () => {
it('should execute handler for message type', () => {
const handlersRegistry = new HandlersRegistry();
describe("HandlersRegistry", () => {
it("should execute handler for message type", () => {
const handlersRegistry = new HandlersRegistry();
let handled = false;
let handled = false;
const handler: Handler = (): boolean => {
handled = true;
return true;
};
const handler: Handler = (): boolean => {
handled = 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 { MessageQueue } from '../../src/models/messageQueue';
import { MessageType } from '../../src/enums';
import type { IMessage } from '../../src/models/message';
import { wait } from '../utils';
import { MessageQueue } from "../../src/models/messageQueue";
import { MessageType } from "../../src/enums";
import type { IMessage } from "../../src/models/message";
import { wait } from "../utils";
describe('MessageQueue', () => {
const createTestMessage = (): IMessage => {
return {
type: MessageType.OPEN,
src: 'src',
dst: 'dst'
};
};
describe("MessageQueue", () => {
const createTestMessage = (): IMessage => {
return {
type: MessageType.OPEN,
src: "src",
dst: "dst",
};
};
describe('#addMessage', () => {
it('should add message to queue', () => {
const queue = new MessageQueue();
queue.addMessage(createTestMessage());
expect(queue.getMessages().length).toBe(1);
});
});
describe("#addMessage", () => {
it("should add message to queue", () => {
const queue = new MessageQueue();
queue.addMessage(createTestMessage());
expect(queue.getMessages().length).toBe(1);
});
});
describe('#readMessage', () => {
it('should return undefined for empty queue', () => {
const queue = new MessageQueue();
expect(queue.readMessage()).toBeUndefined();
});
describe("#readMessage", () => {
it("should return undefined for empty queue", () => {
const queue = new MessageQueue();
expect(queue.readMessage()).toBeUndefined();
});
it('should return message if any exists in queue', () => {
const queue = new MessageQueue();
const message = createTestMessage();
queue.addMessage(message);
it("should return message if any exists in queue", () => {
const queue = new MessageQueue();
const message = createTestMessage();
queue.addMessage(message);
expect(queue.readMessage()).toEqual(message);
expect(queue.readMessage()).toBeUndefined();
});
});
expect(queue.readMessage()).toEqual(message);
expect(queue.readMessage()).toBeUndefined();
});
});
describe('#getLastReadAt', () => {
it('should not be changed if no messages when read', () => {
const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt();
queue.readMessage();
expect(queue.getLastReadAt()).toBe(lastReadAt);
});
describe("#getLastReadAt", () => {
it("should not be changed if no messages when read", () => {
const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt();
queue.readMessage();
expect(queue.getLastReadAt()).toBe(lastReadAt);
});
it('should be changed when read message', async () => {
const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt();
queue.addMessage(createTestMessage());
it("should be changed when read message", async () => {
const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt();
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 { Realm } from '../../src/models/realm';
import { Client } from '../../src/models/client';
import { Realm } from "../../src/models/realm";
import { Client } from "../../src/models/client";
describe('Realm', () => {
describe('#generateClientId', () => {
it('should generate a 36-character UUID, or return function value', () => {
const realm = new Realm();
expect(realm.generateClientId().length).toBe(36);
expect(realm.generateClientId(() => 'abcd')).toBe('abcd');
});
});
describe("Realm", () => {
describe("#generateClientId", () => {
it("should generate a 36-character UUID, or return function value", () => {
const realm = new Realm();
expect(realm.generateClientId().length).toBe(36);
expect(realm.generateClientId(() => "abcd")).toBe("abcd");
});
});
describe('#setClient', () => {
it('should add client to realm', () => {
const realm = new Realm();
const client = new Client({ id: 'id', token: '' });
describe("#setClient", () => {
it("should add client to realm", () => {
const realm = new Realm();
const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id');
expect(realm.getClientsIds()).toEqual(['id']);
});
});
realm.setClient(client, "id");
expect(realm.getClientsIds()).toEqual(["id"]);
});
});
describe('#removeClientById', () => {
it('should remove client from realm', () => {
const realm = new Realm();
const client = new Client({ id: 'id', token: '' });
describe("#removeClientById", () => {
it("should remove client from realm", () => {
const realm = new Realm();
const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id');
realm.removeClientById('id');
realm.setClient(client, "id");
realm.removeClientById("id");
expect(realm.getClientById('id')).toBeUndefined();
});
});
expect(realm.getClientById("id")).toBeUndefined();
});
});
describe('#getClientsIds', () => {
it('should reflects on add/remove childs', () => {
const realm = new Realm();
const client = new Client({ id: 'id', token: '' });
describe("#getClientsIds", () => {
it("should reflects on add/remove childs", () => {
const realm = new Realm();
const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id');
expect(realm.getClientsIds()).toEqual(['id']);
realm.setClient(client, "id");
expect(realm.getClientsIds()).toEqual(["id"]);
expect(realm.getClientById('id')).toBe(client);
expect(realm.getClientById("id")).toBe(client);
realm.removeClientById('id');
expect(realm.getClientsIds()).toEqual([]);
});
});
realm.removeClientById("id");
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 expectedJson from '../app.json';
import http from "http";
import expectedJson from "../app.json";
import fetch from "node-fetch";
import * as crypto from "crypto";
import {startServer} from "./utils";
import { startServer } from "./utils";
const PORT = '9000';
const PORT = "9000";
async function makeRequest() {
return new Promise<object>((resolve, reject) => {
http.get(`http://localhost:${PORT}/`, resp => {
let data = '';
return new Promise<object>((resolve, reject) => {
http
.get(`http://localhost:${PORT}/`, (resp) => {
let data = "";
resp.on('data', chunk => {
data += chunk;
});
resp.on("data", (chunk) => {
data += chunk;
});
resp.on('end', () => {
resolve(JSON.parse(data));
});
}).on("error", err => {
console.log("Error: " + err.message);
reject(err);
});
});
resp.on("end", () => {
resolve(JSON.parse(data));
});
})
.on("error", (err) => {
console.log("Error: " + err.message);
reject(err);
});
});
}
describe('Check bin/peerjs', () => {
it('should return content of app.json file', async () => {
expect.assertions(1);
describe("Check bin/peerjs", () => {
it("should return content of app.json file", async () => {
expect.assertions(1);
const ls = await startServer()
try {
const resp = await makeRequest();
expect(resp).toEqual(expectedJson);
} finally {
ls.kill();
}
const ls = await startServer();
try {
const resp = await makeRequest();
expect(resp).toEqual(expectedJson);
} finally {
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 () => {
expect.assertions(1);
const ls = await startServer();
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 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 origin1 = crypto.randomUUID();
const origin2 = crypto.randomUUID();
const origin3 = crypto.randomUUID();
const ls = await startServer(["--cors", origin1, "--cors", origin2])
try {
const res1 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
headers: {
Origin: origin1
}
})
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()
}
});
const origin1 = crypto.randomUUID();
const origin2 = crypto.randomUUID();
const origin3 = crypto.randomUUID();
const ls = await startServer(["--cors", origin1, "--cors", origin2]);
try {
const res1 = await fetch(`http://localhost:${PORT}/peerjs/id`, {
headers: {
Origin: origin1,
},
});
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 { Client } from '../../../src/models/client';
import { Realm } from '../../../src/models/realm';
import { CheckBrokenConnections } from '../../../src/services/checkBrokenConnections';
import { wait } from '../../utils';
import { Client } from "../../../src/models/client";
import { Realm } from "../../../src/models/realm";
import { CheckBrokenConnections } from "../../../src/services/checkBrokenConnections";
import { wait } from "../../utils";
describe('CheckBrokenConnections', () => {
it('should remove client after 2 checks', async () => {
const realm = new Realm();
const doubleCheckTime = 55;//~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 });
const client = new Client({ id: 'id', token: '' });
realm.setClient(client, 'id');
describe("CheckBrokenConnections", () => {
it("should remove client after 2 checks", async () => {
const realm = new Realm();
const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({
realm,
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 () => {
const realm = new Realm();
const doubleCheckTime = 55;//~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 });
const client = new Client({ id: 'id', token: '' });
realm.setClient(client, 'id');
it("should remove client after 1 ping", async () => {
const realm = new Realm();
const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({
realm,
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
await wait(checkBrokenConnections.checkInterval);
//set ping after first check
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 { Client } from '../../../src/models/client';
import { Realm } from '../../../src/models/realm';
import type { IMessage } from '../../../src/models/message';
import { MessagesExpire } from '../../../src/services/messagesExpire';
import { MessageHandler } from '../../../src/messageHandler';
import { MessageType } from '../../../src/enums';
import { wait } from '../../utils';
import { Client } from "../../../src/models/client";
import { Realm } from "../../../src/models/realm";
import type { IMessage } from "../../../src/models/message";
import { MessagesExpire } from "../../../src/services/messagesExpire";
import { MessageHandler } from "../../../src/messageHandler";
import { MessageType } from "../../../src/enums";
import { wait } from "../../utils";
describe('MessagesExpire', () => {
const createTestMessage = (dst: string): IMessage => {
return {
type: MessageType.OPEN,
src: 'src',
dst,
};
};
describe("MessagesExpire", () => {
const createTestMessage = (dst: string): IMessage => {
return {
type: MessageType.OPEN,
src: "src",
dst,
};
};
it('should remove client if no read from queue', async () => {
const realm = new Realm();
const messageHandler = new MessageHandler(realm);
const checkInterval = 10;
const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout };
it("should remove client if no read from queue", async () => {
const realm = new Realm();
const messageHandler = new MessageHandler(realm);
const checkInterval = 10;
const expireTimeout = 50;
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: '' });
realm.setClient(client, 'id');
realm.addMessageToQueue(client.getId(), createTestMessage('dst'));
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
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 () => {
const realm = new Realm();
const messageHandler = new MessageHandler(realm);
const checkInterval = 10;
const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout };
it("should fire EXPIRE message", async () => {
const realm = new Realm();
const messageHandler = new MessageHandler(realm);
const checkInterval = 10;
const expireTimeout = 50;
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: '' });
realm.setClient(client, 'id');
realm.addMessageToQueue(client.getId(), createTestMessage('dst1'));
realm.addMessageToQueue(client.getId(), createTestMessage('dst2'));
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
realm.addMessageToQueue(client.getId(), createTestMessage("dst1"));
realm.addMessageToQueue(client.getId(), createTestMessage("dst2"));
let handledCount = 0;
let handledCount = 0;
messageHandler.handle = (client, message): boolean => {
expect(client).toBeUndefined();
expect(message.type).toBe(MessageType.EXPIRE);
messageHandler.handle = (client, message): boolean => {
expect(client).toBeUndefined();
expect(message.type).toBe(MessageType.EXPIRE);
handledCount++;
handledCount++;
return true;
};
return true;
};
messagesExpire.startMessagesExpiration();
messagesExpire.startMessagesExpiration();
await wait(checkInterval * 2);
await wait(expireTimeout);
await wait(checkInterval * 2);
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 { Server, WebSocket } from 'mock-socket';
import type {Server as HttpServer} from 'node:http';
import { Realm } from '../../../src/models/realm';
import { WebSocketServer } from '../../../src/services/webSocketServer';
import { Errors, MessageType } from '../../../src/enums';
import { wait } from '../../utils';
import { Server, WebSocket } from "mock-socket";
import type { Server as HttpServer } from "node:http";
import { Realm } from "../../../src/models/realm";
import { WebSocketServer } from "../../../src/services/webSocketServer";
import { Errors, MessageType } from "../../../src/enums";
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> => {
return new Promise(resolve => {
c.onmessage = (event: object & { data?: string; }): void => {
const message = JSON.parse(event.data as string);
resolve(message.type === MessageType.OPEN);
};
});
return new Promise((resolve) => {
c.onmessage = (event: object & { data?: string }): void => {
const message = JSON.parse(event.data as string);
resolve(message.type === MessageType.OPEN);
};
});
};
const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Errors; }[]): Promise<boolean> => {
return new Promise(resolve => {
const restMessages = [...msgs];
const checkSequence = async (
c: WebSocket,
msgs: { type: MessageType; error?: Errors }[],
): Promise<boolean> => {
return new Promise((resolve) => {
const restMessages = [...msgs];
const finish = (success = false): void => {
resolve(success);
};
const finish = (success = false): void => {
resolve(success);
};
c.onmessage = (event: object & { data?: string; }): void => {
const [mes] = restMessages;
c.onmessage = (event: object & { data?: string }): void => {
const [mes] = restMessages;
if (!mes) {
return finish();
}
if (!mes) {
return finish();
}
restMessages.shift();
restMessages.shift();
const message = JSON.parse(event.data as string);
if (message.type !== mes.type) {
return finish();
}
const message = JSON.parse(event.data as string);
if (message.type !== mes.type) {
return finish();
}
const isOk = !mes.error || message.payload?.msg === mes.error;
const isOk = !mes.error || message.payload?.msg === mes.error;
if (!isOk) {
return finish();
}
if (!isOk) {
return finish();
}
if (restMessages.length === 0) {
finish(true);
}
};
});
if (restMessages.length === 0) {
finish(true);
}
};
});
};
const createTestServer = ({ realm, 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 });
const createTestServer = ({
realm,
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; }) => {
const s = webSocketServer.socketServer;
s.emit('connection', socket, { url: socket.url });
server.on(
"connection",
(
socket: WebSocket & {
on?: (eventName: string, callback: () => void) => void;
},
) => {
const s = webSocketServer.socketServer;
s.emit("connection", socket, { url: socket.url });
socket.onclose = (): void => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1];
socket.onclose = (): void => {
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 => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1];
socket.onmessage = (event: object & { data?: string }): void => {
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> => {
server.close();
};
webSocketServer.destroy = async (): Promise<void> => {
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 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 });
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();
server2.stop();
});
it(`should check client's params`, async () => {
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 realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
const fakeURL = 'ws://localhost:8080/peerjs';
const getError = async (
url: string,
validError: Errors = Errors.INVALID_WS_PARAMETERS,
): Promise<boolean> => {
const webSocketServer = createTestServer({ url, realm, config });
const getError = async (url: string, validError: Errors = Errors.INVALID_WS_PARAMETERS): Promise<boolean> => {
const webSocketServer = createTestServer({ url, realm, config });
const ws = new WebSocket(url);
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);
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);
});
it(`should check concurrent limit`, async () => {
const realm = new Realm();
const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const fakeURL = "ws://localhost:8080/peerjs";
it(`should check concurrent limit`, async () => {
const realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 };
const fakeURL = 'ws://localhost:8080/peerjs';
const createClient = (id: string): Destroyable<WebSocket> => {
// id in the path ensures that all mock servers listen on different urls
const url = `${fakeURL}${id}?key=${config.key}&id=${id}&token=${id}`;
const webSocketServer = createTestServer({ url, realm, config });
const ws: Destroyable<WebSocket> = new WebSocket(url);
const createClient = (id: string): Destroyable<WebSocket> => {
// id in the path ensures that all mock servers listen on different urls
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> => {
ws.close();
ws.destroy = async (): Promise<void> => {
ws.close();
wait(10);
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, [
{ type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED }
])).toBe(true);
await wait(10);
await c1.destroy?.();
await c2.destroy?.();
expect(realm.getClientsIds().length).toBe(0);
await wait(10);
const c3 = createClient("3");
expect(realm.getClientsIds().length).toBe(0);
expect(await checkOpen(c3)).toBe(true);
const c3 = createClient('3');
expect(await checkOpen(c3)).toBe(true);
await c3.destroy?.();
});
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";
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[] = []) => {
return new Promise<ChildProcessWithoutNullStreams>((resolve, reject)=> {
const ls = spawn('node', [path.join(__dirname, '../', 'dist/bin/peerjs.js'), '--port', "9000", ...params]);
ls.stdout.once("data", ()=> resolve(ls))
ls.stderr.once("data", ()=>{
ls.kill()
reject()
})
})
}
return new Promise<ChildProcessWithoutNullStreams>((resolve, reject) => {
const ls = spawn("node", [
path.join(__dirname, "../", "dist/bin/peerjs.js"),
"--port",
"9000",
...params,
]);
ls.stdout.once("data", () => resolve(ls));
ls.stderr.once("data", () => {
ls.kill();
reject();
});
});
};

View File

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

View File

@ -1,148 +1,149 @@
#!/usr/bin/env node
import path from "node:path";
import {version} from "../package.json";
import { version } from "../package.json";
import fs from "node:fs";
const optimistUsageLength = 98;
import yargs from "yargs";
import { hideBin } from 'yargs/helpers'
import { PeerServer} from "../src";
import { hideBin } from "yargs/helpers";
import { PeerServer } from "../src";
import type { AddressInfo } from "node:net";
import type {CorsOptions} from "cors";
import type { CorsOptions } from "cors";
const y = yargs(hideBin(process.argv));
const portEnvIsSet = !!process.env["PORT"]
const portEnvIsSet = !!process.env["PORT"];
const opts = y
.usage("Usage: $0")
.wrap(Math.min(optimistUsageLength, y.terminalWidth()))
.options({
expire_timeout: {
demandOption: false,
alias: "t",
describe: "timeout (milliseconds)",
default: 5000,
},
concurrent_limit: {
demandOption: false,
alias: "c",
describe: "concurrent limit",
default: 5000,
},
alive_timeout: {
demandOption: false,
describe: "broken connection check timeout (milliseconds)",
default: 60000,
},
key: {
demandOption: false,
alias: "k",
describe: "connection key",
default: "peerjs",
},
sslkey: {
type: "string",
demandOption: false,
describe: "path to SSL key",
},
sslcert: {
type: "string",
demandOption: false,
describe: "path to SSL certificate",
},
host: {
type: "string",
demandOption: false,
alias: "H",
describe: "host",
},
port: {
type: "number",
demandOption: !portEnvIsSet,
alias: "p",
describe: "port",
},
path: {
type: "string",
demandOption: false,
describe: "custom path",
default: process.env["PEERSERVER_PATH"] || "/",
},
allow_discovery: {
type: "boolean",
demandOption: false,
describe: "allow discovery of peers",
},
proxied: {
type: "boolean",
demandOption: false,
describe: "Set true if PeerServer stays behind a reverse proxy",
default: false,
},
cors: {
type: "string",
array: true,
describe: "Set the list of CORS origins",
},
})
.boolean("allow_discovery").parseSync();
const opts = y
.usage("Usage: $0")
.wrap(Math.min(optimistUsageLength, y.terminalWidth()))
.options({
expire_timeout: {
demandOption: false,
alias: "t",
describe: "timeout (milliseconds)",
default: 5000,
},
concurrent_limit: {
demandOption: false,
alias: "c",
describe: "concurrent limit",
default: 5000,
},
alive_timeout: {
demandOption: false,
describe: "broken connection check timeout (milliseconds)",
default: 60000,
},
key: {
demandOption: false,
alias: "k",
describe: "connection key",
default: "peerjs",
},
sslkey: {
type: "string",
demandOption: false,
describe: "path to SSL key",
},
sslcert: {
type: "string",
demandOption: false,
describe: "path to SSL certificate",
},
host: {
type: "string",
demandOption: false,
alias: "H",
describe: "host",
},
port: {
type: "number",
demandOption: !portEnvIsSet,
alias: "p",
describe: "port",
},
path: {
type: "string",
demandOption: false,
describe: "custom path",
default: process.env["PEERSERVER_PATH"] || "/",
},
allow_discovery: {
type: "boolean",
demandOption: false,
describe: "allow discovery of peers",
},
proxied: {
type: "boolean",
demandOption: false,
describe: "Set true if PeerServer stays behind a reverse proxy",
default: false,
},
cors: {
type: "string",
array: true,
describe: "Set the list of CORS origins",
},
})
.boolean("allow_discovery")
.parseSync();
if(!opts.port){
opts.port= parseInt(process.env["PORT"] as string)
if (!opts.port) {
opts.port = parseInt(process.env["PORT"] as string);
}
if(opts.cors){
opts["corsOptions"] = {
origin: opts.cors
} satisfies CorsOptions;
if (opts.cors) {
opts["corsOptions"] = {
origin: opts.cors,
} satisfies CorsOptions;
}
process.on("uncaughtException", function (e) {
console.error("Error: " + e);
console.error("Error: " + e);
});
if (opts.sslkey || opts.sslcert) {
if (opts.sslkey && opts.sslcert) {
opts["ssl"] = {
key: fs.readFileSync(path.resolve(opts.sslkey)),
cert: fs.readFileSync(path.resolve(opts.sslcert)),
};
} else {
console.error(
"Warning: PeerServer will not run because either " +
"the key or the certificate has not been provided."
);
process.exit(1);
}
if (opts.sslkey && opts.sslcert) {
opts["ssl"] = {
key: fs.readFileSync(path.resolve(opts.sslkey)),
cert: fs.readFileSync(path.resolve(opts.sslcert)),
};
} else {
console.error(
"Warning: PeerServer will not run because either " +
"the key or the certificate has not been provided.",
);
process.exit(1);
}
}
const userPath = opts.path;
const server = PeerServer(opts, (server) => {
const { address: host, port } = server.address() as AddressInfo;
const { address: host, port } = server.address() as AddressInfo;
console.log(
"Started PeerServer on %s, port: %s, path: %s (v. %s)",
host,
port,
userPath || "/",
version
);
console.log(
"Started PeerServer on %s, port: %s, path: %s (v. %s)",
host,
port,
userPath || "/",
version,
);
const shutdownApp = () => {
server.close(() => {
console.log("Http server closed.");
const shutdownApp = () => {
server.close(() => {
console.log("Http server closed.");
process.exit(0);
});
};
process.exit(0);
});
};
process.on("SIGINT", shutdownApp);
process.on("SIGTERM", shutdownApp);
process.on("SIGINT", shutdownApp);
process.on("SIGTERM", shutdownApp);
});
server.on("connection", (client) => {
console.log(`Client connected: ${client.getId()}`);
console.log(`Client connected: ${client.getId()}`);
});
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} */
const config = {
testEnvironment: "node",
transform: {
"^.+\\.(t|j)sx?$": "@swc/jest",
},
transformIgnorePatterns: [
// "node_modules"
],
collectCoverageFrom: ["./src/**"]
testEnvironment: "node",
transform: {
"^.+\\.(t|j)sx?$": "@swc/jest",
},
transformIgnorePatterns: [
// "node_modules"
],
collectCoverageFrom: ["./src/**"],
};
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",
"version": "0.0.0-development",
"keywords": [
"peerjs",
"webrtc",
"p2p",
"rtc"
],
"description": "PeerJS server component",
"homepage": "https://peerjs.com",
"bugs": {
"url": "https://github.com/peers/peerjs-server/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/peers/peerjs-server"
},
"license": "MIT",
"contributors": [],
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/peer.d.ts",
"default": "./dist/module.mjs"
},
"require": {
"types": "./dist/peer.d.ts",
"default": "./dist/index.cjs"
}
}
},
"main": "dist/index.cjs",
"module": "dist/module.mjs",
"source": "src/index.ts",
"binary": "dist/bin/peerjs.js",
"types": "dist/peer.d.ts",
"bin": {
"peerjs": "dist/bin/peerjs.js"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
},
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
},
"files": [
"dist/"
],
"engines": {
"node": ">=14"
},
"targets": {
"binary": {
"source": "bin/peerjs.ts"
},
"main": {},
"module": {}
},
"scripts": {
"format": "prettier --write .",
"build": "parcel build",
"lint": "eslint --ext .js,.ts . && npm run check",
"check": "tsc --noEmit",
"test": "npm run lint && jest",
"coverage": "jest --coverage",
"start": "node dist/bin/peerjs.js --port ${PORT:=9000}",
"dev": "nodemon --watch src -e ts --exec 'npm run build && npm run start'",
"semantic-release": "semantic-release"
},
"dependencies": {
"@types/express": "^4.17.3",
"@types/ws": "^7.2.3 || ^8.0.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"node-fetch": "^3.3.0",
"ws": "^7.2.3 || ^8.0.0",
"yargs": "^17.6.2"
},
"devDependencies": {
"@codedependant/semantic-release-docker": "^4.3.0",
"@parcel/packager-ts": "^2.8.2",
"@parcel/transformer-typescript-types": "^2.8.2",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@swc/core": "^1.3.35",
"@swc/jest": "^0.2.24",
"@tsconfig/node16-strictest-esm": "^1.0.3",
"@types/cors": "^2.8.6",
"@types/jest": "^29.4.0",
"@types/node": "^14.18.33",
"@types/yargs": "^17.0.19",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0",
"jest": "^29.4.2",
"mock-socket": "^9.1.5",
"parcel": "^2.8.2",
"prettier": "^2.8.4",
"semantic-release": "^20.0.0",
"typescript": "^4.1.2"
}
"name": "peer",
"version": "0.0.0-development",
"keywords": [
"peerjs",
"webrtc",
"p2p",
"rtc"
],
"description": "PeerJS server component",
"homepage": "https://peerjs.com",
"bugs": {
"url": "https://github.com/peers/peerjs-server/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/peers/peerjs-server"
},
"license": "MIT",
"contributors": [],
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/peer.d.ts",
"default": "./dist/module.mjs"
},
"require": {
"types": "./dist/peer.d.ts",
"default": "./dist/index.cjs"
}
}
},
"main": "dist/index.cjs",
"module": "dist/module.mjs",
"source": "src/index.ts",
"binary": "dist/bin/peerjs.js",
"types": "dist/peer.d.ts",
"bin": {
"peerjs": "dist/bin/peerjs.js"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
},
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
},
"files": [
"dist/"
],
"engines": {
"node": ">=14"
},
"targets": {
"binary": {
"source": "bin/peerjs.ts"
},
"main": {},
"module": {}
},
"scripts": {
"format": "prettier --write .",
"build": "parcel build",
"lint": "eslint --ext .js,.ts . && npm run check",
"check": "tsc --noEmit",
"test": "npm run lint && jest",
"coverage": "jest --coverage",
"start": "node dist/bin/peerjs.js --port ${PORT:=9000}",
"dev": "nodemon --watch src -e ts --exec 'npm run build && npm run start'",
"semantic-release": "semantic-release"
},
"dependencies": {
"@types/express": "^4.17.3",
"@types/ws": "^7.2.3 || ^8.0.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"node-fetch": "^3.3.0",
"ws": "^7.2.3 || ^8.0.0",
"yargs": "^17.6.2"
},
"devDependencies": {
"@codedependant/semantic-release-docker": "^4.3.0",
"@parcel/packager-ts": "^2.8.2",
"@parcel/transformer-typescript-types": "^2.8.2",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@swc/core": "^1.3.35",
"@swc/jest": "^0.2.24",
"@tsconfig/node16-strictest-esm": "^1.0.3",
"@types/cors": "^2.8.6",
"@types/jest": "^29.4.0",
"@types/node": "^14.18.33",
"@types/yargs": "^17.0.19",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0",
"jest": "^29.4.2",
"mock-socket": "^9.1.5",
"parcel": "^2.8.2",
"prettier": "^2.8.4",
"semantic-release": "^20.0.0",
"typescript": "^4.1.2"
}
}

View File

@ -1,29 +1,29 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":assignAndReview(jonasgloning)"],
"labels": ["dependencies"],
"assignees": ["jonasgloning"],
"major": {
"dependencyDashboardApproval": true
},
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"addLabels": ["dev-dependencies"],
"automerge": true,
"automergeType": "branch"
},
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
}
],
"lockFileMaintenance": {
"enabled": true,
"automerge": true,
"automergeType": "branch"
}
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":assignAndReview(jonasgloning)"],
"labels": ["dependencies"],
"assignees": ["jonasgloning"],
"major": {
"dependencyDashboardApproval": true
},
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"addLabels": ["dev-dependencies"],
"automerge": true,
"automergeType": "branch"
},
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
}
],
"lockFileMaintenance": {
"enabled": true,
"automerge": true,
"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:
* 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:
* 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 publicContent from "../../app.json";
import PublicApi from "./v1/public";
import type {IConfig} from "../config";
import type {IRealm} from "../models/realm";
import type { IConfig } from "../config";
import type { IRealm } from "../models/realm";
export const Api = ({ config, realm, corsOptions }: {
config: IConfig;
realm: IRealm;
corsOptions: CorsOptions;
export const Api = ({
config,
realm,
corsOptions,
}: {
config: IConfig;
realm: IRealm;
corsOptions: CorsOptions;
}): express.Router => {
const app = express.Router();
const app = express.Router();
app.use(cors(corsOptions));
app.use(cors(corsOptions));
app.get("/", (_, res) => {
res.send(publicContent);
});
app.get("/", (_, res) => {
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 type {IConfig} from "../../../config";
import type {IRealm} from "../../../models/realm";
import type { IConfig } from "../../../config";
import type { IRealm } from "../../../models/realm";
export default ({ config, realm }: {
config: IConfig; realm: IRealm;
export default ({
config,
realm,
}: {
config: IConfig;
realm: IRealm;
}): express.Router => {
const app = express.Router();
const app = express.Router();
// Retrieve guaranteed random ID.
app.get("/id", (_, res: express.Response) => {
res.contentType("html");
res.send(realm.generateClientId(config.generateClientId));
});
// Retrieve guaranteed random ID.
app.get("/id", (_, res: express.Response) => {
res.contentType("html");
res.send(realm.generateClientId(config.generateClientId));
});
// Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
app.get("/peers", (_, res: express.Response) => {
if (config.allow_discovery) {
const clientsIds = realm.getClientsIds();
// Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
app.get("/peers", (_, res: express.Response) => {
if (config.allow_discovery) {
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 {CorsOptions} from "cors";
import type { WebSocketServer, ServerOptions } from "ws";
import type { CorsOptions } from "cors";
export interface IConfig {
readonly host: string;
readonly port: number;
readonly expire_timeout: number;
readonly alive_timeout: number;
readonly key: string;
readonly path: string;
readonly concurrent_limit: number;
readonly allow_discovery: boolean;
readonly proxied: boolean | string;
readonly cleanup_out_msgs: number;
readonly ssl?: {
key: string;
cert: string;
};
readonly generateClientId?: () => string;
readonly createWebSocketServer?: (options: ServerOptions) => WebSocketServer;
readonly corsOptions : CorsOptions;
readonly host: string;
readonly port: number;
readonly expire_timeout: number;
readonly alive_timeout: number;
readonly key: string;
readonly path: string;
readonly concurrent_limit: number;
readonly allow_discovery: boolean;
readonly proxied: boolean | string;
readonly cleanup_out_msgs: number;
readonly ssl?: {
key: string;
cert: string;
};
readonly generateClientId?: () => string;
readonly createWebSocketServer?: (options: ServerOptions) => WebSocketServer;
readonly corsOptions: CorsOptions;
}
const defaultConfig: IConfig = {
host: "::",
port: 9000,
expire_timeout: 5000,
alive_timeout: 60000,
key: "peerjs",
path: "/",
concurrent_limit: 5000,
allow_discovery: false,
proxied: false,
cleanup_out_msgs: 1000,
corsOptions: {origin: true},
host: "::",
port: 9000,
expire_timeout: 5000,
alive_timeout: 60000,
key: "peerjs",
path: "/",
concurrent_limit: 5000,
allow_discovery: false,
proxied: false,
cleanup_out_msgs: 1000,
corsOptions: { origin: true },
};
export default defaultConfig;

View File

@ -1,18 +1,18 @@
export enum Errors {
INVALID_KEY = "Invalid key provided",
INVALID_TOKEN = "Invalid token provided",
INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server",
CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit"
INVALID_KEY = "Invalid key provided",
INVALID_TOKEN = "Invalid token provided",
INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server",
CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit",
}
export enum MessageType {
OPEN = "OPEN",
LEAVE = "LEAVE",
CANDIDATE = "CANDIDATE",
OFFER = "OFFER",
ANSWER = "ANSWER",
EXPIRE = "EXPIRE",
HEARTBEAT = "HEARTBEAT",
ID_TAKEN = "ID-TAKEN",
ERROR = "ERROR"
OPEN = "OPEN",
LEAVE = "LEAVE",
CANDIDATE = "CANDIDATE",
OFFER = "OFFER",
ANSWER = "ANSWER",
EXPIRE = "EXPIRE",
HEARTBEAT = "HEARTBEAT",
ID_TAKEN = "ID-TAKEN",
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 https from "node:https";
import type {IConfig} from "./config";
import type { IConfig } from "./config";
import defaultConfig from "./config";
import type {PeerServerEvents} from "./instance";
import {createInstance} from "./instance";
import type { PeerServerEvents } from "./instance";
import { createInstance } from "./instance";
import type { IClient } from "./models/client";
import type { IMessage } from "./models/message";
export type {MessageType} from "./enums"
export type {IConfig, PeerServerEvents, IClient, IMessage}
export type { MessageType } from "./enums";
export type { IConfig, PeerServerEvents, IClient, IMessage };
function ExpressPeerServer(server: https.Server | http.Server, options?: Partial<IConfig>) {
const app = express();
function ExpressPeerServer(
server: https.Server | http.Server,
options?: Partial<IConfig>,
) {
const app = express();
const newOptions: IConfig = {
...defaultConfig,
...options
};
const newOptions: IConfig = {
...defaultConfig,
...options,
};
if (newOptions.proxied) {
app.set("trust proxy", newOptions.proxied === "false" ? false : !!newOptions.proxied);
}
if (newOptions.proxied) {
app.set(
"trust proxy",
newOptions.proxied === "false" ? false : !!newOptions.proxied,
);
}
app.on("mount", () => {
if (!server) {
throw new Error("Server is not passed to constructor - " +
"can't start PeerServer");
}
app.on("mount", () => {
if (!server) {
throw new Error(
"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) {
const app = express();
function PeerServer(
options: Partial<IConfig> = {},
callback?: (server: https.Server | http.Server) => void,
) {
const app = express();
let newOptions: IConfig = {
...defaultConfig,
...options
};
let newOptions: IConfig = {
...defaultConfig,
...options,
};
const port = newOptions.port;
const host = newOptions.host;
const port = newOptions.port;
const host = newOptions.host;
let server: https.Server | http.Server;
let server: https.Server | http.Server;
const { ssl, ...restOptions } = newOptions;
if (ssl && Object.keys(ssl).length) {
server = https.createServer(ssl, app);
const { ssl, ...restOptions } = newOptions;
if (ssl && Object.keys(ssl).length) {
server = https.createServer(ssl, app);
newOptions = restOptions;
} else {
server = http.createServer(app);
}
newOptions = restOptions;
} else {
server = http.createServer(app);
}
const peerjs = ExpressPeerServer(server, newOptions);
app.use(peerjs);
const peerjs = ExpressPeerServer(server, newOptions);
app.use(peerjs);
server.listen(port, host, () => callback?.(server));
server.listen(port, host, () => callback?.(server));
return peerjs;
return peerjs;
}
export {
ExpressPeerServer,
PeerServer
};
export { ExpressPeerServer, PeerServer };

View File

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

View File

@ -1,4 +1,7 @@
import type {IClient} from "../models/client";
import type {IMessage} from "../models/message";
import type { IClient } from "../models/client";
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 => {
if (client) {
const nowTime = new Date().getTime();
client.setLastPing(nowTime);
}
if (client) {
const nowTime = new Date().getTime();
client.setLastPing(nowTime);
}
return true;
return true;
};

View File

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

View File

@ -1,29 +1,29 @@
import type {MessageType} from "../enums";
import type {IClient} from "../models/client";
import type {IMessage} from "../models/message";
import type {Handler} from "./handler";
import type { MessageType } from "../enums";
import type { IClient } from "../models/client";
import type { IMessage } from "../models/message";
import type { Handler } from "./handler";
export interface IHandlersRegistry {
registerHandler(messageType: MessageType, handler: Handler): void;
handle(client: IClient | undefined, message: IMessage): boolean;
registerHandler(messageType: MessageType, handler: Handler): void;
handle(client: IClient | undefined, message: IMessage): boolean;
}
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 {
if (this.handlers.has(messageType)) return;
public registerHandler(messageType: MessageType, handler: Handler): void {
if (this.handlers.has(messageType)) return;
this.handlers.set(messageType, handler);
}
this.handlers.set(messageType, handler);
}
public handle(client: IClient | undefined, message: IMessage): boolean {
const { type } = message;
public handle(client: IClient | undefined, message: IMessage): boolean {
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 {HeartbeatHandler, TransmissionHandler} from "./handlers";
import type {IHandlersRegistry} from "./handlersRegistry";
import {HandlersRegistry} from "./handlersRegistry";
import type {IClient} from "../models/client";
import type {IMessage} from "../models/message";
import type {IRealm} from "../models/realm";
import type {Handler} from "./handler";
import { MessageType } from "../enums";
import { HeartbeatHandler, TransmissionHandler } from "./handlers";
import type { IHandlersRegistry } from "./handlersRegistry";
import { HandlersRegistry } from "./handlersRegistry";
import type { IClient } from "../models/client";
import type { IMessage } from "../models/message";
import type { IRealm } from "../models/realm";
import type { Handler } from "./handler";
export interface IMessageHandler {
handle(client: IClient | undefined, message: IMessage): boolean;
handle(client: IClient | undefined, message: IMessage): boolean;
}
export class MessageHandler implements IMessageHandler {
constructor(realm: IRealm, private readonly handlersRegistry: IHandlersRegistry = new HandlersRegistry()) {
const transmissionHandler: Handler = TransmissionHandler({ realm });
const heartbeatHandler: Handler = HeartbeatHandler;
constructor(
realm: IRealm,
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 => {
return transmissionHandler(client, {
type,
src,
dst,
payload,
});
};
const handleTransmission: Handler = (
client: IClient | undefined,
{ type, src, dst, payload }: IMessage,
): boolean => {
return transmissionHandler(client, {
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(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);
}
this.handlersRegistry.registerHandler(
MessageType.HEARTBEAT,
handleHeartbeat,
);
this.handlersRegistry.registerHandler(
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 {
return this.handlersRegistry.handle(client, message);
}
public handle(client: IClient | undefined, message: IMessage): boolean {
return this.handlersRegistry.handle(client, message);
}
}

View File

@ -1,57 +1,57 @@
import type WebSocket from "ws";
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 {
private readonly id: string;
private readonly token: string;
private socket: WebSocket | null = null;
private lastPing: number = new Date().getTime();
private readonly id: string;
private readonly token: string;
private socket: WebSocket | null = null;
private lastPing: number = new Date().getTime();
constructor({ id, token }: { id: string; token: string; }) {
this.id = id;
this.token = token;
}
constructor({ id, token }: { id: string; token: string }) {
this.id = id;
this.token = token;
}
public getId(): string {
return this.id;
}
public getId(): string {
return this.id;
}
public getToken(): string {
return this.token;
}
public getToken(): string {
return this.token;
}
public getSocket(): WebSocket | null {
return this.socket;
}
public getSocket(): WebSocket | null {
return this.socket;
}
public setSocket(socket: WebSocket | null): void {
this.socket = socket;
}
public setSocket(socket: WebSocket | null): void {
this.socket = socket;
}
public getLastPing(): number {
return this.lastPing;
}
public getLastPing(): number {
return this.lastPing;
}
public setLastPing(lastPing: number): void {
this.lastPing = lastPing;
}
public setLastPing(lastPing: number): void {
this.lastPing = lastPing;
}
public send<T>(data: T): void {
this.socket?.send(JSON.stringify(data));
}
public send<T>(data: T): void {
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 {
readonly type: MessageType;
readonly src: string;
readonly dst: string;
readonly payload?: string |undefined;
readonly type: MessageType;
readonly src: string;
readonly dst: string;
readonly payload?: string | undefined;
}

View File

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

View File

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

View File

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

View File

@ -1,88 +1,92 @@
import {MessageType} from "../../enums";
import type {IConfig} from "../../config";
import type {IMessageHandler} from "../../messageHandler";
import type {IRealm} from "../../models/realm";
import { MessageType } from "../../enums";
import type { IConfig } from "../../config";
import type { IMessageHandler } from "../../messageHandler";
import type { IRealm } from "../../models/realm";
export interface IMessagesExpire {
startMessagesExpiration(): void;
stopMessagesExpiration(): void;
startMessagesExpiration(): 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 {
private readonly realm: IRealm;
private readonly config: CustomConfig;
private readonly messageHandler: IMessageHandler;
private readonly realm: IRealm;
private readonly config: CustomConfig;
private readonly messageHandler: IMessageHandler;
private timeoutId: NodeJS.Timeout | null = null;
private timeoutId: NodeJS.Timeout | null = null;
constructor({ realm, config, messageHandler }: {
realm: IRealm;
config: CustomConfig;
messageHandler: IMessageHandler;
}) {
this.realm = realm;
this.config = config;
this.messageHandler = messageHandler;
}
constructor({
realm,
config,
messageHandler,
}: {
realm: IRealm;
config: CustomConfig;
messageHandler: IMessageHandler;
}) {
this.realm = realm;
this.config = config;
this.messageHandler = messageHandler;
}
public startMessagesExpiration(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
public startMessagesExpiration(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
// Clean up outstanding messages
this.timeoutId = setTimeout(() => {
this.pruneOutstanding();
// Clean up outstanding messages
this.timeoutId = setTimeout(() => {
this.pruneOutstanding();
this.timeoutId = null;
this.timeoutId = null;
this.startMessagesExpiration();
}, this.config.cleanup_out_msgs);
}
this.startMessagesExpiration();
}, this.config.cleanup_out_msgs);
}
public stopMessagesExpiration(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
public stopMessagesExpiration(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
private pruneOutstanding(): void {
const destinationClientsIds = this.realm.getClientsIdsWithQueue();
private pruneOutstanding(): void {
const destinationClientsIds = this.realm.getClientsIdsWithQueue();
const now = new Date().getTime();
const maxDiff = this.config.expire_timeout;
const now = new Date().getTime();
const maxDiff = this.config.expire_timeout;
const seen: Record<string, boolean> = {};
const seen: Record<string, boolean> = {};
for (const destinationClientId of destinationClientsIds) {
const messageQueue = this.realm.getMessageQueueById(destinationClientId);
for (const destinationClientId of destinationClientsIds) {
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) {
const seenKey = `${message.src}_${message.dst}`;
for (const message of messages) {
const seenKey = `${message.src}_${message.dst}`;
if (!seen[seenKey]) {
this.messageHandler.handle(undefined, {
type: MessageType.EXPIRE,
src: message.dst,
dst: message.src,
});
if (!seen[seenKey]) {
this.messageHandler.handle(undefined, {
type: MessageType.EXPIRE,
src: message.dst,
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 type {IncomingMessage} from "node:http";
import { EventEmitter } from "node:events";
import type { IncomingMessage } from "node:http";
import url from "node:url";
import type WebSocket from "ws";
import {Errors, MessageType} from "../../enums";
import type {IClient} from "../../models/client";
import {Client} from "../../models/client";
import type {IConfig} from "../../config";
import type {IRealm} from "../../models/realm";
import {WebSocketServer as Server} from "ws";
import type {Server as HttpServer} from "node:http";
import type {Server as HttpsServer} from "node:https";
import { Errors, MessageType } from "../../enums";
import type { IClient } from "../../models/client";
import { Client } from "../../models/client";
import type { IConfig } from "../../config";
import type { IRealm } from "../../models/realm";
import { WebSocketServer as Server } from "ws";
import type { Server as HttpServer } from "node:http";
import type { Server as HttpsServer } from "node:https";
export interface IWebSocketServer extends EventEmitter {
readonly path: string;
readonly path: string;
}
interface IAuthParams {
id?: string;
token?: string;
key?: string;
id?: string;
token?: 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 {
public readonly path: string;
private readonly realm: IRealm;
private readonly config: CustomConfig;
public readonly socketServer: Server;
public readonly path: string;
private readonly realm: IRealm;
private readonly config: CustomConfig;
public readonly socketServer: Server;
constructor({
server,
realm,
config,
}: {
server: HttpServer | HttpsServer;
realm: IRealm;
config: CustomConfig;
}) {
super();
constructor({ server, realm, config }: { server: HttpServer | HttpsServer; realm: IRealm; config: CustomConfig; }) {
super();
this.setMaxListeners(0);
this.setMaxListeners(0);
this.realm = realm;
this.config = config;
this.realm = realm;
this.config = config;
const path = this.config.path;
this.path = `${path}${path.endsWith("/") ? "" : "/"}${WS_PATH}`;
const path = this.config.path;
this.path = `${path}${path.endsWith('/') ? "" : "/"}${WS_PATH}`;
const options: WebSocket.ServerOptions = {
path: this.path,
server,
};
const options: WebSocket.ServerOptions = {
path: this.path,
server,
};
this.socketServer = config.createWebSocketServer
? config.createWebSocketServer(options)
: new Server(options);
this.socketServer = (
config.createWebSocketServer ?
config.createWebSocketServer(options) :
new Server(options)
);
this.socketServer.on("connection", (socket, req) =>
this._onSocketConnection(socket, req),
);
this.socketServer.on("error", (error: Error) => this._onSocketError(error));
}
this.socketServer.on("connection", (socket, req) => this._onSocketConnection(socket, req));
this.socketServer.on("error", (error: Error) => this._onSocketError(error));
}
private _onSocketConnection(socket: WebSocket, req: IncomingMessage): void {
// 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 {
// 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 { 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) {
return this._sendErrorAndClose(socket, Errors.INVALID_WS_PARAMETERS);
}
if (key !== this.config.key) {
return this._sendErrorAndClose(socket, Errors.INVALID_KEY);
}
if (key !== this.config.key) {
return this._sendErrorAndClose(socket, Errors.INVALID_KEY);
}
const client = this.realm.getClientById(id);
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) {
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 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 {
// handle error
this.emit("error", error);
}
private _registerClient({
socket,
id,
token,
}: {
socket: WebSocket;
id: string;
token: string;
}): void {
// Check concurrent limit
const clientsCount = this.realm.getClientsIds().length;
private _registerClient({ socket, id, token }:
{
socket: WebSocket;
id: string;
token: string;
}): void {
// Check concurrent limit
const clientsCount = this.realm.getClientsIds().length;
if (clientsCount >= this.config.concurrent_limit) {
return this._sendErrorAndClose(socket, Errors.CONNECTION_LIMIT_EXCEED);
}
if (clientsCount >= this.config.concurrent_limit) {
return this._sendErrorAndClose(socket, Errors.CONNECTION_LIMIT_EXCEED);
}
const newClient: IClient = new Client({ id, token });
this.realm.setClient(newClient, id);
socket.send(JSON.stringify({ type: MessageType.OPEN }));
const newClient: IClient = new Client({ id, token });
this.realm.setClient(newClient, id);
socket.send(JSON.stringify({ type: MessageType.OPEN }));
this._configureWS(socket, newClient);
}
this._configureWS(socket, newClient);
}
private _configureWS(socket: WebSocket, client: IClient): void {
client.setSocket(socket);
private _configureWS(socket: WebSocket, client: IClient): void {
client.setSocket(socket);
// Cleanup after a socket closes.
socket.on("close", () => {
if (client.getSocket() === socket) {
this.realm.removeClientById(client.getId());
this.emit("close", client);
}
});
// Cleanup after a socket closes.
socket.on("close", () => {
if (client.getSocket() === socket) {
this.realm.removeClientById(client.getId());
this.emit("close", client);
}
});
// Handle messages from peers.
socket.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
// Handle messages from peers.
socket.on("message", (data) => {
try {
const message = JSON.parse(data.toString());
message.src = client.getId();
message.src = client.getId();
this.emit("message", client, message);
} catch (e) {
this.emit("error", e);
}
});
this.emit("message", client, message);
} catch (e) {
this.emit("error", e);
}
});
this.emit("connection", client);
}
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.send(
JSON.stringify({
type: MessageType.ERROR,
payload: { msg }
})
);
socket.close();
}
socket.close();
}
}

View File

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