✨(y-webrtc-signaling) create signaling server
Create signaling server for WebRTC. It will be used for the collaborative editor, client will connect to this server to see their changes in real-time.
This commit is contained in:
14
src/frontend/apps/y-webrtc-signaling/.eslintrc.cjs
Normal file
14
src/frontend/apps/y-webrtc-signaling/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['impress/next'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: __dirname,
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules', '.eslintrc.js'],
|
||||
};
|
||||
5
src/frontend/apps/y-webrtc-signaling/nodemon.json
Normal file
5
src/frontend/apps/y-webrtc-signaling/nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node --esm ./src/server.ts"
|
||||
}
|
||||
29
src/frontend/apps/y-webrtc-signaling/package.json
Normal file
29
src/frontend/apps/y-webrtc-signaling/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "y-webrtc-signaling",
|
||||
"version": "0.1.0",
|
||||
"description": "WebRTC server for Yjs",
|
||||
"repository": "https://github.com/numerique-gouv/impress",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"dev": "nodemon --exec src/server.ts",
|
||||
"start": "node ./dist/server.js",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/ws": "8.5.10",
|
||||
"eslint-config-impress": "*",
|
||||
"nodemon": "3.1.0",
|
||||
"ts-jest": "29.1.2",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
144
src/frontend/apps/y-webrtc-signaling/src/server.ts
Normal file
144
src/frontend/apps/y-webrtc-signaling/src/server.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Based on https://github.com/yjs/y-webrtc/blob/master/bin/server.js
|
||||
*/
|
||||
import http from 'http';
|
||||
|
||||
import * as map from 'lib0/map';
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
|
||||
type MessageYJSType = {
|
||||
type: string;
|
||||
topics?: string[];
|
||||
topic?: string;
|
||||
clients?: number;
|
||||
};
|
||||
|
||||
type MessageYJSTypes = MessageYJSType | string;
|
||||
|
||||
const wsReadyStateConnecting = 0;
|
||||
const wsReadyStateOpen = 1;
|
||||
const pingTimeout = 30000;
|
||||
const port = process.env.PORT || 4444;
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const topics = new Map<string, Set<WebSocket>>();
|
||||
|
||||
const send = (conn: WebSocket, message: MessageYJSTypes) => {
|
||||
if (
|
||||
conn.readyState !== wsReadyStateConnecting &&
|
||||
conn.readyState !== wsReadyStateOpen
|
||||
) {
|
||||
conn.close();
|
||||
}
|
||||
try {
|
||||
conn.send(JSON.stringify(message));
|
||||
} catch (e) {
|
||||
conn.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup a new client
|
||||
*/
|
||||
const onconnection = (conn: WebSocket) => {
|
||||
const subscribedTopics = new Set<string>();
|
||||
let closed = false;
|
||||
// Check if connection is still alive
|
||||
let pongReceived = true;
|
||||
|
||||
const pingInterval = setInterval(() => {
|
||||
if (!pongReceived) {
|
||||
conn.close();
|
||||
clearInterval(pingInterval);
|
||||
} else {
|
||||
pongReceived = false;
|
||||
try {
|
||||
conn.ping();
|
||||
} catch (e) {
|
||||
conn.close();
|
||||
}
|
||||
}
|
||||
}, pingTimeout);
|
||||
|
||||
conn.on('pong', () => {
|
||||
pongReceived = true;
|
||||
});
|
||||
|
||||
conn.on('close', () => {
|
||||
subscribedTopics.forEach((topicName) => {
|
||||
const subs = topics.get(topicName) || new Set();
|
||||
subs.delete(conn);
|
||||
if (subs.size === 0) {
|
||||
topics.delete(topicName);
|
||||
}
|
||||
});
|
||||
subscribedTopics.clear();
|
||||
closed = true;
|
||||
});
|
||||
|
||||
conn.on('message', (message: MessageYJSTypes) => {
|
||||
if (typeof message === 'string' || message instanceof Buffer) {
|
||||
message = JSON.parse(message.toString()) as MessageYJSType;
|
||||
}
|
||||
|
||||
if (message && message.type && !closed) {
|
||||
switch (message.type) {
|
||||
case 'subscribe':
|
||||
(message.topics || []).forEach((topicName) => {
|
||||
if (typeof topicName === 'string') {
|
||||
// add conn to topic
|
||||
const topic = map.setIfUndefined(
|
||||
topics,
|
||||
topicName,
|
||||
() => new Set(),
|
||||
);
|
||||
topic.add(conn);
|
||||
// add topic to conn
|
||||
subscribedTopics.add(topicName);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'unsubscribe':
|
||||
(message.topics || []).forEach((topicName) => {
|
||||
const subs = topics.get(topicName);
|
||||
if (subs) {
|
||||
subs.delete(conn);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'publish':
|
||||
if (message.topic) {
|
||||
const receivers = topics.get(message.topic);
|
||||
if (receivers) {
|
||||
message.clients = receivers.size;
|
||||
receivers.forEach((receiver) => send(receiver, message));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
send(conn, { type: 'pong' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
wss.on('connection', onconnection);
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
response.end('okay');
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const handleAuth = (ws: WebSocket) => {
|
||||
wss.emit('connection', ws, request);
|
||||
};
|
||||
wss.handleUpgrade(request, socket, head, handleAuth);
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
|
||||
console.log('Signaling server running on localhost:', port);
|
||||
19
src/frontend/apps/y-webrtc-signaling/tsconfig.json
Normal file
19
src/frontend/apps/y-webrtc-signaling/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": false,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user