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,13 +1,13 @@
--- ---
name: peer template name: peer template
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ""
labels: '' labels: ""
assignees: '' assignees: ""
--- ---
### I'm having an issue: ### I'm having an issue:
- Give an expressive description of what is went wrong - Give an expressive description of what is went wrong
- Version of `peer` you're experiencing this issue - Version of `peer` you're experiencing this issue
- Nodejs version? - Nodejs version?
@ -16,13 +16,15 @@ assignees: ''
- If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot - If you're getting an error or exception, please provide its full stack-trace as plain-text or screenshot
### I have a suggestion: ### I have a suggestion:
- Describe your feature / request - Describe your feature / request
- How you're going to use it? Give a usage example(s) - How you're going to use it? Give a usage example(s)
### Documentation is missing something or incorrect (have typos, etc.): ### Documentation is missing something or incorrect (have typos, etc.):
- Give an expressive description what you have changed/added and why - Give an expressive description what you have changed/added and why
- Make sure you're using correct markdown markup - 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) - 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 - Post addition/changes in issue, we will manage it
## Thank you, and do not forget to get rid of this default message ## Thank you, and do not forget to get rid of this default message

View File

@ -11,7 +11,6 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -25,7 +24,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' cache: "npm"
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- run: npm run lint - run: npm run lint

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { HandlersRegistry } from '../../src/messageHandler/handlersRegistry'; import { HandlersRegistry } from "../../src/messageHandler/handlersRegistry";
import type { Handler } from '../../src/messageHandler/handler'; import type { Handler } from "../../src/messageHandler/handler";
import { MessageType } from '../../src/enums'; import { MessageType } from "../../src/enums";
describe('HandlersRegistry', () => { describe("HandlersRegistry", () => {
it('should execute handler for message type', () => { it("should execute handler for message type", () => {
const handlersRegistry = new HandlersRegistry(); const handlersRegistry = new HandlersRegistry();
let handled = false; let handled = false;
@ -17,7 +17,11 @@ describe('HandlersRegistry', () => {
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,34 +1,34 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { MessageQueue } from '../../src/models/messageQueue'; import { MessageQueue } from "../../src/models/messageQueue";
import { MessageType } from '../../src/enums'; import { MessageType } from "../../src/enums";
import type { IMessage } from '../../src/models/message'; import type { IMessage } from "../../src/models/message";
import { wait } from '../utils'; import { wait } from "../utils";
describe('MessageQueue', () => { describe("MessageQueue", () => {
const createTestMessage = (): IMessage => { const createTestMessage = (): IMessage => {
return { return {
type: MessageType.OPEN, type: MessageType.OPEN,
src: 'src', src: "src",
dst: 'dst' dst: "dst",
}; };
}; };
describe('#addMessage', () => { describe("#addMessage", () => {
it('should add message to queue', () => { it("should add message to queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
queue.addMessage(createTestMessage()); queue.addMessage(createTestMessage());
expect(queue.getMessages().length).toBe(1); expect(queue.getMessages().length).toBe(1);
}); });
}); });
describe('#readMessage', () => { describe("#readMessage", () => {
it('should return undefined for empty queue', () => { it("should return undefined for empty queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
expect(queue.readMessage()).toBeUndefined(); expect(queue.readMessage()).toBeUndefined();
}); });
it('should return message if any exists in queue', () => { it("should return message if any exists in queue", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const message = createTestMessage(); const message = createTestMessage();
queue.addMessage(message); queue.addMessage(message);
@ -38,15 +38,15 @@ describe('MessageQueue', () => {
}); });
}); });
describe('#getLastReadAt', () => { describe("#getLastReadAt", () => {
it('should not be changed if no messages when read', () => { it("should not be changed if no messages when read", () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt(); const lastReadAt = queue.getLastReadAt();
queue.readMessage(); queue.readMessage();
expect(queue.getLastReadAt()).toBe(lastReadAt); expect(queue.getLastReadAt()).toBe(lastReadAt);
}); });
it('should be changed when read message', async () => { it("should be changed when read message", async () => {
const queue = new MessageQueue(); const queue = new MessageQueue();
const lastReadAt = queue.getLastReadAt(); const lastReadAt = queue.getLastReadAt();
queue.addMessage(createTestMessage()); queue.addMessage(createTestMessage());

View File

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

View File

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

View File

@ -1,33 +1,41 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Client } from '../../../src/models/client'; import { Client } from "../../../src/models/client";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import { CheckBrokenConnections } from '../../../src/services/checkBrokenConnections'; import { CheckBrokenConnections } from "../../../src/services/checkBrokenConnections";
import { wait } from '../../utils'; import { wait } from "../../utils";
describe('CheckBrokenConnections', () => { describe("CheckBrokenConnections", () => {
it('should remove client after 2 checks', async () => { it("should remove client after 2 checks", async () => {
const realm = new Realm(); const realm = new Realm();
const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2 const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 }); const checkBrokenConnections = new CheckBrokenConnections({
const client = new Client({ id: 'id', token: '' }); realm,
realm.setClient(client, 'id'); config: { alive_timeout: doubleCheckTime },
checkInterval: 30,
});
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
checkBrokenConnections.start(); checkBrokenConnections.start();
await wait(checkBrokenConnections.checkInterval * 2 + 30); await wait(checkBrokenConnections.checkInterval * 2 + 30);
expect(realm.getClientById('id')).toBeUndefined(); expect(realm.getClientById("id")).toBeUndefined();
checkBrokenConnections.stop(); checkBrokenConnections.stop();
}); });
it('should remove client after 1 ping', async () => { it("should remove client after 1 ping", async () => {
const realm = new Realm(); const realm = new Realm();
const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2 const doubleCheckTime = 55; //~ equals to checkBrokenConnections.checkInterval * 2
const checkBrokenConnections = new CheckBrokenConnections({ realm, config: { alive_timeout: doubleCheckTime }, checkInterval: 30 }); const checkBrokenConnections = new CheckBrokenConnections({
const client = new Client({ id: 'id', token: '' }); realm,
realm.setClient(client, 'id'); config: { alive_timeout: doubleCheckTime },
checkInterval: 30,
});
const client = new Client({ id: "id", token: "" });
realm.setClient(client, "id");
checkBrokenConnections.start(); checkBrokenConnections.start();
@ -38,7 +46,7 @@ describe('CheckBrokenConnections', () => {
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,40 +1,49 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Client } from '../../../src/models/client'; import { Client } from "../../../src/models/client";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import type { IMessage } from '../../../src/models/message'; import type { IMessage } from "../../../src/models/message";
import { MessagesExpire } from '../../../src/services/messagesExpire'; import { MessagesExpire } from "../../../src/services/messagesExpire";
import { MessageHandler } from '../../../src/messageHandler'; import { MessageHandler } from "../../../src/messageHandler";
import { MessageType } from '../../../src/enums'; import { MessageType } from "../../../src/enums";
import { wait } from '../../utils'; import { wait } from "../../utils";
describe('MessagesExpire', () => { describe("MessagesExpire", () => {
const createTestMessage = (dst: string): IMessage => { const createTestMessage = (dst: string): IMessage => {
return { return {
type: MessageType.OPEN, type: MessageType.OPEN,
src: 'src', src: "src",
dst, dst,
}; };
}; };
it('should remove client if no read from queue', async () => { it("should remove client if no read from queue", async () => {
const realm = new Realm(); const realm = new Realm();
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const checkInterval = 10; const checkInterval = 10;
const expireTimeout = 50; const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout }; const config = {
cleanup_out_msgs: checkInterval,
expire_timeout: expireTimeout,
};
const messagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire = new MessagesExpire({
realm,
config,
messageHandler,
});
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
realm.addMessageToQueue(client.getId(), createTestMessage('dst')); realm.addMessageToQueue(client.getId(), createTestMessage("dst"));
messagesExpire.startMessagesExpiration(); messagesExpire.startMessagesExpiration();
await wait(checkInterval * 2); await wait(checkInterval * 2);
expect(realm.getMessageQueueById(client.getId())?.getMessages().length).toBe(1); expect(
realm.getMessageQueueById(client.getId())?.getMessages().length,
).toBe(1);
await wait(expireTimeout); await wait(expireTimeout);
@ -43,19 +52,26 @@ describe('MessagesExpire', () => {
messagesExpire.stopMessagesExpiration(); messagesExpire.stopMessagesExpiration();
}); });
it('should fire EXPIRE message', async () => { it("should fire EXPIRE message", async () => {
const realm = new Realm(); const realm = new Realm();
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const checkInterval = 10; const checkInterval = 10;
const expireTimeout = 50; const expireTimeout = 50;
const config = { cleanup_out_msgs: checkInterval, expire_timeout: expireTimeout }; const config = {
cleanup_out_msgs: checkInterval,
expire_timeout: expireTimeout,
};
const messagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire = new MessagesExpire({
realm,
config,
messageHandler,
});
const client = new Client({ id: 'id', token: '' }); const client = new Client({ id: "id", token: "" });
realm.setClient(client, 'id'); realm.setClient(client, "id");
realm.addMessageToQueue(client.getId(), createTestMessage('dst1')); realm.addMessageToQueue(client.getId(), createTestMessage("dst1"));
realm.addMessageToQueue(client.getId(), createTestMessage('dst2')); realm.addMessageToQueue(client.getId(), createTestMessage("dst2"));
let handledCount = 0; let handledCount = 0;

View File

@ -1,32 +1,35 @@
import { describe, expect, it } from "@jest/globals"; import { describe, expect, it } from "@jest/globals";
import { Server, WebSocket } from 'mock-socket'; import { Server, WebSocket } from "mock-socket";
import type {Server as HttpServer} from 'node:http'; import type { Server as HttpServer } from "node:http";
import { Realm } from '../../../src/models/realm'; import { Realm } from "../../../src/models/realm";
import { WebSocketServer } from '../../../src/services/webSocketServer'; import { WebSocketServer } from "../../../src/services/webSocketServer";
import { Errors, MessageType } from '../../../src/enums'; import { Errors, MessageType } from "../../../src/enums";
import { wait } from '../../utils'; import { wait } from "../../utils";
type Destroyable<T> = T & { destroy?: () => Promise<void>; }; type Destroyable<T> = T & { destroy?: () => Promise<void> };
const checkOpen = async (c: WebSocket): Promise<boolean> => { const checkOpen = async (c: WebSocket): Promise<boolean> => {
return new Promise(resolve => { return new Promise((resolve) => {
c.onmessage = (event: object & { data?: string; }): void => { c.onmessage = (event: object & { data?: string }): void => {
const message = JSON.parse(event.data as string); const message = JSON.parse(event.data as string);
resolve(message.type === MessageType.OPEN); resolve(message.type === MessageType.OPEN);
}; };
}); });
}; };
const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Errors; }[]): Promise<boolean> => { const checkSequence = async (
return new Promise(resolve => { c: WebSocket,
msgs: { type: MessageType; error?: Errors }[],
): Promise<boolean> => {
return new Promise((resolve) => {
const restMessages = [...msgs]; const restMessages = [...msgs];
const finish = (success = false): void => { const finish = (success = false): void => {
resolve(success); resolve(success);
}; };
c.onmessage = (event: object & { data?: string; }): void => { c.onmessage = (event: object & { data?: string }): void => {
const [mes] = restMessages; const [mes] = restMessages;
if (!mes) { if (!mes) {
@ -53,16 +56,38 @@ const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Er
}); });
}; };
const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path: string; key: string; concurrent_limit: number; }; url: string; }): Destroyable<WebSocketServer> => { 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 server = new Server(url) as Server & HttpServer;
const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({ server, realm, config }); const webSocketServer: Destroyable<WebSocketServer> = new WebSocketServer({
server,
realm,
config,
});
server.on('connection', (socket: WebSocket & { on?: (eventName: string, callback: () => void) => void; }) => { server.on(
"connection",
(
socket: WebSocket & {
on?: (eventName: string, callback: () => void) => void;
},
) => {
const s = webSocketServer.socketServer; const s = webSocketServer.socketServer;
s.emit('connection', socket, { url: socket.url }); s.emit("connection", socket, { url: socket.url });
socket.onclose = (): void => { socket.onclose = (): void => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; const userId = socket.url
.split("?")[1]
?.split("&")
.find((p) => p.startsWith("id"))
?.split("=")[1];
if (!userId) return; if (!userId) return;
@ -72,11 +97,17 @@ const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path
if (!clientSocket) return; if (!clientSocket) return;
(clientSocket as unknown as WebSocket).listeners['server::close']?.forEach((s: () => void) => s()); (clientSocket as unknown as WebSocket).listeners[
"server::close"
]?.forEach((s: () => void) => s());
}; };
socket.onmessage = (event: object & { data?: string; }): void => { socket.onmessage = (event: object & { data?: string }): void => {
const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; const userId = socket.url
.split("?")[1]
?.split("&")
.find((p) => p.startsWith("id"))
?.split("=")[1];
if (!userId) return; if (!userId) return;
@ -86,9 +117,12 @@ const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path
if (!clientSocket) return; if (!clientSocket) return;
(clientSocket as unknown as WebSocket).listeners['server::message']?.forEach((s: (data: object) => void) => s(event)); (clientSocket as unknown as WebSocket).listeners[
"server::message"
]?.forEach((s: (data: object) => void) => s(event));
}; };
}); },
);
webSocketServer.destroy = async (): Promise<void> => { webSocketServer.destroy = async (): Promise<void> => {
server.close(); server.close();
@ -97,22 +131,25 @@ const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path
return webSocketServer; return webSocketServer;
}; };
describe('WebSocketServer', () => { describe("WebSocketServer", () => {
it("should return valid path", () => {
it('should return valid path', () => {
const realm = new Realm(); const realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const config2 = { ...config, path: 'path' }; const config2 = { ...config, path: "path" };
const server = new Server('path1') as Server & HttpServer; const server = new Server("path1") as Server & HttpServer;
const server2 = new Server('path2') 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(); server.stop();
server2.stop(); server2.stop();
@ -120,15 +157,20 @@ describe('WebSocketServer', () => {
it(`should check client's params`, async () => { it(`should check client's params`, async () => {
const realm = new Realm(); const realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const fakeURL = 'ws://localhost:8080/peerjs'; const fakeURL = "ws://localhost:8080/peerjs";
const getError = async (url: string, validError: Errors = Errors.INVALID_WS_PARAMETERS): Promise<boolean> => { const getError = async (
url: string,
validError: Errors = Errors.INVALID_WS_PARAMETERS,
): Promise<boolean> => {
const webSocketServer = createTestServer({ url, realm, config }); 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();
@ -140,13 +182,18 @@ describe('WebSocketServer', () => {
expect(await getError(fakeURL)).toBe(true); expect(await getError(fakeURL)).toBe(true);
expect(await getError(`${fakeURL}?key=${config.key}`)).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=${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}?key=notValidKey&id=userId&token=userToken`,
Errors.INVALID_KEY,
),
).toBe(true);
}); });
it(`should check concurrent limit`, async () => { it(`should check concurrent limit`, async () => {
const realm = new Realm(); const realm = new Realm();
const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const config = { path: "/", key: "testKey", concurrent_limit: 1 };
const fakeURL = 'ws://localhost:8080/peerjs'; const fakeURL = "ws://localhost:8080/peerjs";
const createClient = (id: string): Destroyable<WebSocket> => { const createClient = (id: string): Destroyable<WebSocket> => {
// id in the path ensures that all mock servers listen on different urls // id in the path ensures that all mock servers listen on different urls
@ -169,16 +216,17 @@ describe('WebSocketServer', () => {
return ws; return ws;
}; };
const c1 = createClient("1");
const c1 = createClient('1');
expect(await checkOpen(c1)).toBe(true); expect(await checkOpen(c1)).toBe(true);
const c2 = createClient('2'); const c2 = createClient("2");
expect(await checkSequence(c2, [ expect(
{ type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED } await checkSequence(c2, [
])).toBe(true); { type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED },
]),
).toBe(true);
await c1.destroy?.(); await c1.destroy?.();
await c2.destroy?.(); await c2.destroy?.();
@ -187,7 +235,7 @@ describe('WebSocketServer', () => {
expect(realm.getClientsIds().length).toBe(0); expect(realm.getClientsIds().length).toBe(0);
const c3 = createClient('3'); const c3 = createClient("3");
expect(await checkOpen(c3)).toBe(true); expect(await checkOpen(c3)).toBe(true);

View File

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

View File

@ -5,14 +5,14 @@ import {version} from "../package.json";
import fs from "node:fs"; import fs from "node:fs";
const optimistUsageLength = 98; const optimistUsageLength = 98;
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from 'yargs/helpers' import { hideBin } from "yargs/helpers";
import { PeerServer } from "../src"; import { PeerServer } from "../src";
import type { AddressInfo } from "node:net"; import type { AddressInfo } from "node:net";
import type { CorsOptions } from "cors"; import type { CorsOptions } from "cors";
const y = yargs(hideBin(process.argv)); const y = yargs(hideBin(process.argv));
const portEnvIsSet = !!process.env["PORT"] const portEnvIsSet = !!process.env["PORT"];
const opts = y const opts = y
.usage("Usage: $0") .usage("Usage: $0")
@ -86,14 +86,15 @@ const opts = y
describe: "Set the list of CORS origins", describe: "Set the list of CORS origins",
}, },
}) })
.boolean("allow_discovery").parseSync(); .boolean("allow_discovery")
.parseSync();
if (!opts.port) { if (!opts.port) {
opts.port= parseInt(process.env["PORT"] as string) opts.port = parseInt(process.env["PORT"] as string);
} }
if (opts.cors) { if (opts.cors) {
opts["corsOptions"] = { opts["corsOptions"] = {
origin: opts.cors origin: opts.cors,
} satisfies CorsOptions; } satisfies CorsOptions;
} }
process.on("uncaughtException", function (e) { process.on("uncaughtException", function (e) {
@ -109,7 +110,7 @@ if (opts.sslkey || opts.sslcert) {
} else { } else {
console.error( console.error(
"Warning: PeerServer will not run because either " + "Warning: PeerServer will not run because either " +
"the key or the certificate has not been provided." "the key or the certificate has not been provided.",
); );
process.exit(1); process.exit(1);
} }
@ -124,7 +125,7 @@ const server = PeerServer(opts, (server) => {
host, host,
port, port,
userPath || "/", userPath || "/",
version version,
); );
const shutdownApp = () => { const shutdownApp = () => {

View File

@ -7,7 +7,7 @@ const config = {
transformIgnorePatterns: [ transformIgnorePatterns: [
// "node_modules" // "node_modules"
], ],
collectCoverageFrom: ["./src/**"] collectCoverageFrom: ["./src/**"],
}; };
export default config; export default config;

View File

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

View File

@ -5,7 +5,11 @@ import PublicApi from "./v1/public";
import type { IConfig } from "../config"; import type { IConfig } from "../config";
import type { IRealm } from "../models/realm"; import type { IRealm } from "../models/realm";
export const Api = ({ config, realm, corsOptions }: { export const Api = ({
config,
realm,
corsOptions,
}: {
config: IConfig; config: IConfig;
realm: IRealm; realm: IRealm;
corsOptions: CorsOptions; corsOptions: CorsOptions;

View File

@ -2,8 +2,12 @@ import express from "express";
import type { IConfig } from "../../../config"; import type { IConfig } from "../../../config";
import type { IRealm } from "../../../models/realm"; import type { IRealm } from "../../../models/realm";
export default ({ config, realm }: { export default ({
config: IConfig; realm: IRealm; config,
realm,
}: {
config: IConfig;
realm: IRealm;
}): express.Router => { }): express.Router => {
const app = express.Router(); const app = express.Router();

View File

@ -1,4 +1,4 @@
import type {WebSocketServer, ServerOptions} from 'ws'; import type { WebSocketServer, ServerOptions } from "ws";
import type { CorsOptions } from "cors"; import type { CorsOptions } from "cors";
export interface IConfig { export interface IConfig {

View File

@ -2,7 +2,7 @@ export enum Errors {
INVALID_KEY = "Invalid key provided", INVALID_KEY = "Invalid key provided",
INVALID_TOKEN = "Invalid token provided", INVALID_TOKEN = "Invalid token provided",
INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server", INVALID_WS_PARAMETERS = "No id, token, or key supplied to websocket server",
CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit" CONNECTION_LIMIT_EXCEED = "Server has reached its concurrent user limit",
} }
export enum MessageType { export enum MessageType {
@ -14,5 +14,5 @@ export enum MessageType {
EXPIRE = "EXPIRE", EXPIRE = "EXPIRE",
HEARTBEAT = "HEARTBEAT", HEARTBEAT = "HEARTBEAT",
ID_TAKEN = "ID-TAKEN", ID_TAKEN = "ID-TAKEN",
ERROR = "ERROR" ERROR = "ERROR",
} }

View File

@ -9,39 +9,49 @@ import {createInstance} from "./instance";
import type { IClient } from "./models/client"; import type { IClient } from "./models/client";
import type { IMessage } from "./models/message"; import type { IMessage } from "./models/message";
export type {MessageType} from "./enums" export type { MessageType } from "./enums";
export type {IConfig, PeerServerEvents, IClient, IMessage} export type { IConfig, PeerServerEvents, IClient, IMessage };
function ExpressPeerServer(server: https.Server | http.Server, options?: Partial<IConfig>) { function ExpressPeerServer(
server: https.Server | http.Server,
options?: Partial<IConfig>,
) {
const app = express(); const app = express();
const newOptions: IConfig = { const newOptions: IConfig = {
...defaultConfig, ...defaultConfig,
...options ...options,
}; };
if (newOptions.proxied) { if (newOptions.proxied) {
app.set("trust proxy", newOptions.proxied === "false" ? false : !!newOptions.proxied); app.set(
"trust proxy",
newOptions.proxied === "false" ? false : !!newOptions.proxied,
);
} }
app.on("mount", () => { app.on("mount", () => {
if (!server) { if (!server) {
throw new Error("Server is not passed to constructor - " + throw new Error(
"can't start PeerServer"); "Server is not passed to constructor - " + "can't start PeerServer",
);
} }
createInstance({ app, server, options: newOptions }); createInstance({ app, server, options: newOptions });
}); });
return app as Express & PeerServerEvents return app as Express & PeerServerEvents;
} }
function PeerServer(options: Partial<IConfig> = {}, callback?: (server: https.Server | http.Server) => void) { function PeerServer(
options: Partial<IConfig> = {},
callback?: (server: https.Server | http.Server) => void,
) {
const app = express(); const app = express();
let newOptions: IConfig = { let newOptions: IConfig = {
...defaultConfig, ...defaultConfig,
...options ...options,
}; };
const port = newOptions.port; const port = newOptions.port;
@ -66,7 +76,4 @@ function PeerServer(options: Partial<IConfig> = {}, callback?: (server: https.Se
return peerjs; return peerjs;
} }
export { export { ExpressPeerServer, PeerServer };
ExpressPeerServer,
PeerServer
};

View File

@ -16,13 +16,20 @@ import type {IMessage} from "./models/message";
import type { IConfig } from "./config"; import type { IConfig } from "./config";
export interface PeerServerEvents { export interface PeerServerEvents {
on(event: 'connection', listener: (client: IClient) => void): this; on(event: "connection", listener: (client: IClient) => void): this;
on(event: "message", listener: (client: IClient, message: IMessage) => void): this; on(
event: "message",
listener: (client: IClient, message: IMessage) => void,
): this;
on(event: "disconnect", listener: (client: IClient) => void): this; on(event: "disconnect", listener: (client: IClient) => void): this;
on(event: "error", listener: (client: Error) => void): this; on(event: "error", listener: (client: Error) => void): this;
} }
export const createInstance = ({ app, server, options }: { export const createInstance = ({
app,
server,
options,
}: {
app: express.Application; app: express.Application;
server: HttpServer | HttpsServer; server: HttpServer | HttpsServer;
options: IConfig; options: IConfig;
@ -32,24 +39,31 @@ export const createInstance = ({ app, server, options }: {
const messageHandler = new MessageHandler(realm); const messageHandler = new MessageHandler(realm);
const api = Api({ config, realm, corsOptions: options.corsOptions }); const api = Api({ config, realm, corsOptions: options.corsOptions });
const messagesExpire: IMessagesExpire = new MessagesExpire({ realm, config, messageHandler }); const messagesExpire: IMessagesExpire = new MessagesExpire({
realm,
config,
messageHandler,
});
const checkBrokenConnections = new CheckBrokenConnections({ const checkBrokenConnections = new CheckBrokenConnections({
realm, realm,
config, config,
onClose: client => { onClose: (client) => {
app.emit("disconnect", client); app.emit("disconnect", client);
} },
}); });
app.use(options.path, api); app.use(options.path, api);
//use mountpath for WS server //use mountpath for WS server
const customConfig = { ...config, path: path.posix.join(app.path(), options.path, '/') }; const customConfig = {
...config,
path: path.posix.join(app.path(), options.path, "/"),
};
const wss: IWebSocketServer = new WebSocketServer({ const wss: IWebSocketServer = new WebSocketServer({
server, server,
realm, realm,
config: customConfig config: customConfig,
}); });
wss.on("connection", (client: IClient) => { wss.on("connection", (client: IClient) => {

View File

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

View File

@ -3,7 +3,11 @@ import type {IClient} from "../../../models/client";
import type { IMessage } from "../../../models/message"; import type { IMessage } from "../../../models/message";
import type { IRealm } from "../../../models/realm"; import type { IRealm } from "../../../models/realm";
export const TransmissionHandler = ({ realm }: { realm: IRealm; }): (client: IClient | undefined, message: IMessage) => boolean => { export const TransmissionHandler = ({
realm,
}: {
realm: IRealm;
}): ((client: IClient | undefined, message: IMessage) => boolean) => {
const handle = (client: IClient | undefined, message: IMessage) => { const handle = (client: IClient | undefined, message: IMessage) => {
const type = message.type; const type = message.type;
const srcId = message.src; const srcId = message.src;
@ -36,7 +40,7 @@ export const TransmissionHandler = ({ realm }: { realm: IRealm; }): (client: ICl
handle(client, { handle(client, {
type: MessageType.LEAVE, type: MessageType.LEAVE,
src: dstId, src: dstId,
dst: srcId dst: srcId,
}); });
} }
} else { } else {

View File

@ -12,11 +12,17 @@ export interface IMessageHandler {
} }
export class MessageHandler implements IMessageHandler { export class MessageHandler implements IMessageHandler {
constructor(realm: IRealm, private readonly handlersRegistry: IHandlersRegistry = new HandlersRegistry()) { constructor(
realm: IRealm,
private readonly handlersRegistry: IHandlersRegistry = new HandlersRegistry(),
) {
const transmissionHandler: Handler = TransmissionHandler({ realm }); const transmissionHandler: Handler = TransmissionHandler({ realm });
const heartbeatHandler: Handler = HeartbeatHandler; const heartbeatHandler: Handler = HeartbeatHandler;
const handleTransmission: Handler = (client: IClient | undefined, { type, src, dst, payload }: IMessage): boolean => { const handleTransmission: Handler = (
client: IClient | undefined,
{ type, src, dst, payload }: IMessage,
): boolean => {
return transmissionHandler(client, { return transmissionHandler(client, {
type, type,
src, src,
@ -25,14 +31,33 @@ export class MessageHandler implements IMessageHandler {
}); });
}; };
const handleHeartbeat = (client: IClient | undefined, message: IMessage) => heartbeatHandler(client, message); const handleHeartbeat = (client: IClient | undefined, message: IMessage) =>
heartbeatHandler(client, message);
this.handlersRegistry.registerHandler(MessageType.HEARTBEAT, handleHeartbeat); this.handlersRegistry.registerHandler(
this.handlersRegistry.registerHandler(MessageType.OFFER, handleTransmission); MessageType.HEARTBEAT,
this.handlersRegistry.registerHandler(MessageType.ANSWER, handleTransmission); handleHeartbeat,
this.handlersRegistry.registerHandler(MessageType.CANDIDATE, handleTransmission); );
this.handlersRegistry.registerHandler(MessageType.LEAVE, handleTransmission); this.handlersRegistry.registerHandler(
this.handlersRegistry.registerHandler(MessageType.EXPIRE, handleTransmission); MessageType.OFFER,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.ANSWER,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.CANDIDATE,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.LEAVE,
handleTransmission,
);
this.handlersRegistry.registerHandler(
MessageType.EXPIRE,
handleTransmission,
);
} }
public handle(client: IClient | undefined, message: IMessage): boolean { public handle(client: IClient | undefined, message: IMessage): boolean {

View File

@ -22,7 +22,7 @@ export class Client implements IClient {
private socket: WebSocket | null = null; private socket: WebSocket | null = null;
private lastPing: number = new Date().getTime(); private lastPing: number = new Date().getTime();
constructor({ id, token }: { id: string; token: string; }) { constructor({ id, token }: { id: string; token: string }) {
this.id = id; this.id = id;
this.token = token; this.token = token;
} }

View File

@ -4,17 +4,21 @@ import type {IRealm} from "../../models/realm";
const DEFAULT_CHECK_INTERVAL = 300; const DEFAULT_CHECK_INTERVAL = 300;
type CustomConfig = Pick<IConfig, 'alive_timeout'>; type CustomConfig = Pick<IConfig, "alive_timeout">;
export class CheckBrokenConnections { export class CheckBrokenConnections {
public readonly checkInterval: number; public readonly checkInterval: number;
private timeoutId: NodeJS.Timeout | null = null; private timeoutId: NodeJS.Timeout | null = null;
private readonly realm: IRealm; private readonly realm: IRealm;
private readonly config: CustomConfig; private readonly config: CustomConfig;
private readonly onClose?: (client: IClient) => void; private readonly onClose?: (client: IClient) => void;
constructor({ realm, config, checkInterval = DEFAULT_CHECK_INTERVAL, onClose }: { constructor({
realm,
config,
checkInterval = DEFAULT_CHECK_INTERVAL,
onClose,
}: {
realm: IRealm; realm: IRealm;
config: CustomConfig; config: CustomConfig;
checkInterval?: number; checkInterval?: number;

View File

@ -8,7 +8,7 @@ export interface IMessagesExpire {
stopMessagesExpiration(): void; stopMessagesExpiration(): void;
} }
type CustomConfig = Pick<IConfig, 'cleanup_out_msgs' | 'expire_timeout'>; type CustomConfig = Pick<IConfig, "cleanup_out_msgs" | "expire_timeout">;
export class MessagesExpire implements IMessagesExpire { export class MessagesExpire implements IMessagesExpire {
private readonly realm: IRealm; private readonly realm: IRealm;
@ -17,7 +17,11 @@ export class MessagesExpire implements IMessagesExpire {
private timeoutId: NodeJS.Timeout | null = null; private timeoutId: NodeJS.Timeout | null = null;
constructor({ realm, config, messageHandler }: { constructor({
realm,
config,
messageHandler,
}: {
realm: IRealm; realm: IRealm;
config: CustomConfig; config: CustomConfig;
messageHandler: IMessageHandler; messageHandler: IMessageHandler;

View File

@ -21,18 +21,28 @@ interface IAuthParams {
key?: string; key?: string;
} }
type CustomConfig = Pick<IConfig, 'path' | 'key' | 'concurrent_limit' | 'createWebSocketServer'>; type CustomConfig = Pick<
IConfig,
"path" | "key" | "concurrent_limit" | "createWebSocketServer"
>;
const WS_PATH = 'peerjs'; const WS_PATH = "peerjs";
export class WebSocketServer extends EventEmitter implements IWebSocketServer { export class WebSocketServer extends EventEmitter implements IWebSocketServer {
public readonly path: string; public readonly path: string;
private readonly realm: IRealm; private readonly realm: IRealm;
private readonly config: CustomConfig; private readonly config: CustomConfig;
public readonly socketServer: Server; public readonly socketServer: Server;
constructor({ server, realm, config }: { server: HttpServer | HttpsServer; realm: IRealm; config: CustomConfig; }) { constructor({
server,
realm,
config,
}: {
server: HttpServer | HttpsServer;
realm: IRealm;
config: CustomConfig;
}) {
super(); super();
this.setMaxListeners(0); this.setMaxListeners(0);
@ -41,28 +51,28 @@ export class WebSocketServer extends EventEmitter implements IWebSocketServer {
this.config = config; this.config = config;
const path = this.config.path; const path = this.config.path;
this.path = `${path}${path.endsWith('/') ? "" : "/"}${WS_PATH}`; this.path = `${path}${path.endsWith("/") ? "" : "/"}${WS_PATH}`;
const options: WebSocket.ServerOptions = { const options: WebSocket.ServerOptions = {
path: this.path, path: this.path,
server, server,
}; };
this.socketServer = ( this.socketServer = config.createWebSocketServer
config.createWebSocketServer ? ? config.createWebSocketServer(options)
config.createWebSocketServer(options) : : new Server(options);
new Server(options)
);
this.socketServer.on("connection", (socket, req) => this._onSocketConnection(socket, req)); this.socketServer.on("connection", (socket, req) =>
this._onSocketConnection(socket, req),
);
this.socketServer.on("error", (error: Error) => this._onSocketError(error)); this.socketServer.on("error", (error: Error) => this._onSocketError(error));
} }
private _onSocketConnection(socket: WebSocket, req: IncomingMessage): void { private _onSocketConnection(socket: WebSocket, req: IncomingMessage): void {
// An unhandled socket error might crash the server. Handle it first. // An unhandled socket error might crash the server. Handle it first.
socket.on("error", error => this._onSocketError(error)) 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;
@ -79,10 +89,12 @@ export class WebSocketServer extends EventEmitter implements IWebSocketServer {
if (client) { if (client) {
if (token !== client.getToken()) { if (token !== client.getToken()) {
// ID-taken, invalid token // ID-taken, invalid token
socket.send(JSON.stringify({ socket.send(
JSON.stringify({
type: MessageType.ID_TAKEN, type: MessageType.ID_TAKEN,
payload: { msg: "ID is taken" } payload: { msg: "ID is taken" },
})); }),
);
return socket.close(); return socket.close();
} }
@ -98,8 +110,11 @@ export class WebSocketServer extends EventEmitter implements IWebSocketServer {
this.emit("error", error); this.emit("error", error);
} }
private _registerClient({ socket, id, token }: private _registerClient({
{ socket,
id,
token,
}: {
socket: WebSocket; socket: WebSocket;
id: string; id: string;
token: string; token: string;
@ -149,8 +164,8 @@ export class WebSocketServer extends EventEmitter implements IWebSocketServer {
socket.send( socket.send(
JSON.stringify({ JSON.stringify({
type: MessageType.ERROR, type: MessageType.ERROR,
payload: { msg } payload: { msg },
}) }),
); );
socket.close(); socket.close();

View File

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