parent
31b767afbf
commit
bb96c54010
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- "8"
|
||||||
|
|
||||||
|
script:
|
||||||
|
- cd peer-exchange
|
||||||
|
- yarn
|
||||||
|
- yarn test
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
"name": "snex-peering",
|
"name": "snex-peering",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A generic WebRTC library and peering server.",
|
"description": "A generic WebRTC library and peering server.",
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server/server.js",
|
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@snex/peer-exchange",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Peer Exchange server for WebRTC",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Pontus Alexander <pontus.alexander@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^21.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
const {Client} = require('../models/client.js');
|
||||||
|
const {createConnectionHandler} = require('../handler.js');
|
||||||
|
|
||||||
|
describe('createConnectionHandler', () => {
|
||||||
|
it('listens to messages', () => {
|
||||||
|
const regMock = {
|
||||||
|
handleMessage: jest.fn(),
|
||||||
|
handleClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = createConnectionHandler(regMock);
|
||||||
|
|
||||||
|
const connMock = {
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler(connMock);
|
||||||
|
|
||||||
|
expect(connMock.on).toHaveBeenCalledTimes(2);
|
||||||
|
expect(connMock.on.mock.calls[0][0]).toBe('message');
|
||||||
|
expect(connMock.on.mock.calls[0][1]).toBeInstanceOf(Function);
|
||||||
|
expect(connMock.on.mock.calls[1][0]).toBe('close');
|
||||||
|
expect(connMock.on.mock.calls[1][1]).toBeInstanceOf(Function);
|
||||||
|
|
||||||
|
const messageCallback = connMock.on.mock.calls[0][1];
|
||||||
|
expect(regMock.handleMessage).toHaveBeenCalledTimes(0);
|
||||||
|
messageCallback('arbitrary message');
|
||||||
|
expect(regMock.handleMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][0]).toBeInstanceOf(Client);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][0].conn).toBe(connMock);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][1]).toBe('arbitrary message');
|
||||||
|
|
||||||
|
const disconnectCallback = connMock.on.mock.calls[1][1];
|
||||||
|
expect(regMock.handleClose).toHaveBeenCalledTimes(0);
|
||||||
|
disconnectCallback();
|
||||||
|
expect(regMock.handleClose).toHaveBeenCalledTimes(1);
|
||||||
|
expect(regMock.handleClose.mock.calls[0][0]).toBeInstanceOf(Client);
|
||||||
|
expect(regMock.handleClose.mock.calls[0][0].conn).toBe(connMock);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
const {Client} = require('./models/client.js');
|
||||||
|
|
||||||
|
function createConnectionHandler(registry) {
|
||||||
|
return function handleConnection(conn) {
|
||||||
|
const client = new Client(conn);
|
||||||
|
|
||||||
|
conn.on('message', message => {
|
||||||
|
registry.handleMessage(client, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', () => {
|
||||||
|
registry.handleClose(client);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createConnectionHandler,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
const {Channel} = require('../channel.js');
|
||||||
|
const {Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Channel', () => {
|
||||||
|
it('when joined by two parties broadcasting sends signal to each except sender', () => {
|
||||||
|
const channel = new Channel();
|
||||||
|
const client1 = new Client();
|
||||||
|
const client2 = new Client();
|
||||||
|
client1.send = jest.fn();
|
||||||
|
client2.send = jest.fn();
|
||||||
|
channel.join(client1);
|
||||||
|
channel.join(client2);
|
||||||
|
const data = {my: 'data'};
|
||||||
|
channel.broadcast(client1, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
|
||||||
|
const client3 = new Client();
|
||||||
|
client3.send = jest.fn();
|
||||||
|
channel.join(client3);
|
||||||
|
channel.broadcast(client1, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(2);
|
||||||
|
expect(client3.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
|
||||||
|
channel.leave(client2);
|
||||||
|
channel.broadcast(client3, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client1.send).toHaveBeenLastCalledWith(data);
|
||||||
|
expect(client3.send).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
const {ack, Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Client', () => {
|
||||||
|
it('send messages encoded as JSON to connection', () => {
|
||||||
|
const connMock = {
|
||||||
|
send: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new Client(connMock);
|
||||||
|
client.send({my: 'data'});
|
||||||
|
|
||||||
|
expect(connMock.send).toBeCalledWith('{"my":"data"}', ack);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
const {Registry} = require('../registry.js');
|
||||||
|
const {Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Registry', () => {
|
||||||
|
it('broadcasts message to everyone with same id', () => {
|
||||||
|
const reg = new Registry();
|
||||||
|
const client1 = new Client();
|
||||||
|
const client2 = new Client();
|
||||||
|
|
||||||
|
client1.send = jest.fn();
|
||||||
|
client2.send = jest.fn();
|
||||||
|
|
||||||
|
reg.handleMessage(client1, JSON.stringify({channelId: 'x', type: "greet"}));
|
||||||
|
reg.handleMessage(client2, JSON.stringify({channelId: 'x', type: "offer"}));
|
||||||
|
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client1.send).lastCalledWith({"channelId": "x", "type": "offer"});
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up when leaving', () => {
|
||||||
|
const reg = new Registry();
|
||||||
|
const client = new Client();
|
||||||
|
|
||||||
|
reg.handleMessage(client, JSON.stringify({channelId: 'x'}));
|
||||||
|
reg.handleDisconnect(client);
|
||||||
|
|
||||||
|
expect(reg.channels.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Channel
|
||||||
|
{
|
||||||
|
constructor() {
|
||||||
|
this.clients = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(originClient, data) {
|
||||||
|
[...this.clients]
|
||||||
|
.filter(candidateClient => candidateClient !== originClient)
|
||||||
|
.forEach(targetClient => targetClient.send(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
join(client) {
|
||||||
|
this.clients.add(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(client) {
|
||||||
|
this.clients.delete(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Channel,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
function ack(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log('Error sending message', msg, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client
|
||||||
|
{
|
||||||
|
constructor(conn) {
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
const msg = JSON.stringify(data);
|
||||||
|
this.conn.send(msg, ack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ack,
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const {Channel} = require('./channel');
|
||||||
|
|
||||||
|
class Registry {
|
||||||
|
constructor() {
|
||||||
|
this.channels = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client) {
|
||||||
|
this.channels.forEach((channel, id) => {
|
||||||
|
channel.leave(client);
|
||||||
|
if (channel.clients.size === 0) {
|
||||||
|
this.channels.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(client, message) {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
const {channelId} = data;
|
||||||
|
|
||||||
|
if (!this.channels.has(channelId)) {
|
||||||
|
this.channels.set(channelId, new Channel());
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.channels.get(channelId);
|
||||||
|
channel.join(client);
|
||||||
|
channel.broadcast(client, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Registry,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const ID_LENGTH = 12;
|
||||||
|
const URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
|
||||||
|
function generateRandomString(len = 6, chars = URL_SAFE) {
|
||||||
|
let cursor = 0;
|
||||||
|
return crypto.randomBytes(len).reduce((string, byte) => {
|
||||||
|
cursor += byte;
|
||||||
|
return string + chars[cursor % chars.length];
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createId() {
|
||||||
|
return generateRandomString(ID_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createId,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
const WebSocketServer = require('ws').Server;
|
||||||
|
const Session = require('./models/session');
|
||||||
|
const Client = require('./models/client');
|
||||||
|
const { createId } = require('./random');
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 9000;
|
||||||
|
|
||||||
|
const server = new WebSocketServer({port: 9000});
|
||||||
|
|
||||||
|
const sessions = new Map;
|
||||||
|
|
||||||
|
function createClient(conn, id = createId()) {
|
||||||
|
return new Client(conn, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSession(id = createId()) {
|
||||||
|
if (sessions.has(id)) {
|
||||||
|
throw new Error(`Session ${id} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = new Session(id);
|
||||||
|
console.log('Creating session', session);
|
||||||
|
|
||||||
|
sessions.set(id, session);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSession(id) {
|
||||||
|
return sessions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.on('connection', conn => {
|
||||||
|
console.log('Connection established');
|
||||||
|
const client = createClient(conn);
|
||||||
|
|
||||||
|
conn.on('message', msg => {
|
||||||
|
console.log('Message received', msg);
|
||||||
|
const data = JSON.parse(msg);
|
||||||
|
|
||||||
|
if (data.type === 'create-session') {
|
||||||
|
const session = createSession();
|
||||||
|
session.join(client);
|
||||||
|
|
||||||
|
client.send({
|
||||||
|
type: 'session-created',
|
||||||
|
id: session.id,
|
||||||
|
});
|
||||||
|
} else if (data.type === 'join-session') {
|
||||||
|
const session = getSession(data.id) || createSession(data.id);
|
||||||
|
session.join(client);
|
||||||
|
} else {
|
||||||
|
client.broadcast(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', () => {
|
||||||
|
console.log('Connection closed');
|
||||||
|
const session = client.session;
|
||||||
|
if (session) {
|
||||||
|
session.leave(client);
|
||||||
|
if (session.clients.size === 0) {
|
||||||
|
sessions.delete(session.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(sessions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Running on port ${PORT}`);
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue