Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c1947a80 | |||
| 9ceb59e5fe | |||
| e28eb73962 | |||
| dc72a6389a | |||
| 5a03779139 | |||
| 5173b15c82 | |||
| 4173ca0169 | |||
| f26b72a996 | |||
| 80eb39b5b8 | |||
| d6104bae14 | |||
| 170c52eefa | |||
| 41705177c5 | |||
| 27d2459e1d |
17
.drone.yml
17
.drone.yml
@ -4,13 +4,18 @@ type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: node:12
|
||||
commands:
|
||||
- npm install
|
||||
- npm run ci
|
||||
- name: build
|
||||
image: node:12
|
||||
commands:
|
||||
- npm install
|
||||
- npm run ci
|
||||
environment:
|
||||
TEST_REDIS_HOST: redis
|
||||
services:
|
||||
- name: redis
|
||||
image: redis:5-alpine
|
||||
---
|
||||
kind: signature
|
||||
hmac: a49a1e7c428472d0237bb2ba73511965607384f45114941b869c6a9eff7aef70
|
||||
hmac: 6cf23314158a6b508ef5240c110304b4b8c8f155d837c393ad1beb7a75a7d990
|
||||
|
||||
...
|
||||
|
||||
@ -51,6 +51,7 @@ rules:
|
||||
'@typescript-eslint/triple-slash-reference':
|
||||
- warn
|
||||
- path: always
|
||||
'@typescript-eslint/no-empty-function': off
|
||||
overrides:
|
||||
- files:
|
||||
- '*.test.ts'
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
stages:
|
||||
- test
|
||||
|
||||
test:
|
||||
image: node:12
|
||||
stage: test
|
||||
variables:
|
||||
TEST_REDIS_HOST: redis
|
||||
script:
|
||||
- npm install
|
||||
- npm run ci
|
||||
services:
|
||||
- redis:5-alpine
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
language: node_js
|
||||
services:
|
||||
- redis-server
|
||||
node_js:
|
||||
- "8"
|
||||
- "12"
|
||||
|
||||
15
README.md
15
README.md
@ -134,6 +134,19 @@ unix domain socket.
|
||||
|
||||
To access the server, go to http://localhost:3000 (or another port).
|
||||
|
||||
# Multiple Instances and Redis
|
||||
|
||||
Redis can be used to allow users connected to different instances to connect.
|
||||
The following needs to be added to `config.yaml` to enable Redis:
|
||||
|
||||
```yaml
|
||||
store:
|
||||
type: redis
|
||||
host: 127.0.0.1 # redis host
|
||||
port: 6379 # redis port
|
||||
prefix: peercalls # all instances must use the same prefix
|
||||
```
|
||||
|
||||
# Logging
|
||||
|
||||
By default, Peer Calls server will log only basic information. Client-side
|
||||
@ -187,7 +200,7 @@ For more details, see here:
|
||||
- [x] Reduce production build size by removing Pug. (Fixed in 2d14e5f c743f19)
|
||||
- [x] Add ability to share files (Fixed in 3877893)
|
||||
- [ ] Enable node cluster support (to scale vertically).
|
||||
- [ ] Add Socket.IO support for Redis (to scale horizontally).
|
||||
- [x] Add Socket.IO support for Redis (to scale horizontally).
|
||||
- [ ] Generate public keys for each client, and allow each client to accept,
|
||||
deny, and remember allowed/denied connections to specific peers.
|
||||
- [ ] Add support for browser push notifications
|
||||
|
||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:5-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
||||
9715
package-lock.json
generated
9715
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "peer-calls",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.12",
|
||||
"description": "Group peer to peer video calls for anybody.",
|
||||
"repository": "https://github.com/jeremija/peer-calls",
|
||||
"main": "lib/index.js",
|
||||
@ -50,68 +50,72 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"ejs": "^2.7.4",
|
||||
"ejs": "^3.0.1",
|
||||
"express": "^4.17.1",
|
||||
"ioredis": "^4.16.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"lodash": "^4.17.15",
|
||||
"socket.io": "^2.3.0",
|
||||
"uuid": "^3.3.3"
|
||||
"socket.io-redis": "^5.2.0",
|
||||
"uuid": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/polyfill": "^7.7.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/polyfill": "^7.8.7",
|
||||
"@babel/preset-env": "^7.8.7",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/ejs": "^2.6.3",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/jest": "^25.1.0",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/lodash": "^4.14.148",
|
||||
"@types/node": "^12.12.8",
|
||||
"@types/react": "^16.9.11",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/react-transition-group": "^4.2.3",
|
||||
"@types/ejs": "^3.0.1",
|
||||
"@types/express": "^4.17.3",
|
||||
"@types/ioredis": "^4.14.9",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/node": "^13.9.1",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-transition-group": "^4.2.4",
|
||||
"@types/redux-logger": "^3.0.7",
|
||||
"@types/simple-peer": "^9.6.0",
|
||||
"@types/socket.io": "^2.1.4",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/socket.io-redis": "^1.0.25",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"@types/uuid": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.23.0",
|
||||
"@typescript-eslint/parser": "^2.23.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-minify": "^0.5.1",
|
||||
"babelify": "^10.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"chastifol": "^4.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.4.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"core-js": "^3.6.4",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"jest": "^25.1.0",
|
||||
"loose-envify": "^1.4.0",
|
||||
"node-sass": "4.13.1",
|
||||
"nodemon": "^1.19.4",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"node-sass": "^4.13.1",
|
||||
"nodemon": "^2.0.2",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"redux": "^4.0.4",
|
||||
"redux": "^4.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"screenfull": "^5.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"screenfull": "^5.0.2",
|
||||
"simple-peer": "^9.6.2",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^25.1.0",
|
||||
"ts-node": "^8.5.2",
|
||||
"ts-jest": "^25.2.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"tsify": "^4.0.1",
|
||||
"typescript": "^3.7.2",
|
||||
"typescript": "^3.8.3",
|
||||
"watchify": "^3.11.1",
|
||||
"webrtc-adapter": "^7.5.0"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import EventEmitter from 'events'
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
const Peer = jest.fn().mockImplementation(() => {
|
||||
const peer = new EventEmitter();
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
import EventEmitter from 'events'
|
||||
import { EventEmitter } from 'events'
|
||||
export default new EventEmitter()
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { NICKNAME_SET } from '../constants'
|
||||
|
||||
interface NicknameSetPayload {
|
||||
export interface NicknameSetPayload {
|
||||
nickname: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface NicknameSetAction {
|
||||
export interface NicknameSetAction {
|
||||
type: 'NICKNAME_SET'
|
||||
payload: NicknameSetPayload
|
||||
}
|
||||
|
||||
@ -65,7 +65,10 @@ describe('server/app', () => {
|
||||
expect((handleSocket as jest.Mock).mock.calls).toEqual([[
|
||||
socket,
|
||||
io,
|
||||
jasmine.any(MemoryStore),
|
||||
{
|
||||
socketIdByUserId: jasmine.any(MemoryStore),
|
||||
userIdBySocketId: jasmine.any(MemoryStore),
|
||||
},
|
||||
]])
|
||||
})
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { config } from './config'
|
||||
import _debug from 'debug'
|
||||
import bodyParser from 'body-parser'
|
||||
import _debug from 'debug'
|
||||
import ejs from 'ejs'
|
||||
import express from 'express'
|
||||
import handleSocket from './socket'
|
||||
import path from 'path'
|
||||
import { createServer } from './server'
|
||||
import SocketIO from 'socket.io'
|
||||
import { config } from './config'
|
||||
import { configureStores } from './configureStores'
|
||||
import call from './routes/call'
|
||||
import index from './routes/index'
|
||||
import ejs from 'ejs'
|
||||
import { MemoryStore } from './store'
|
||||
import { createServer } from './server'
|
||||
import handleSocket from './socket'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
const logRequest = _debug('peercalls:requests')
|
||||
@ -49,7 +49,7 @@ router.use('/call', call)
|
||||
router.use('/', index)
|
||||
app.use(BASE_URL, router)
|
||||
|
||||
const store = new MemoryStore()
|
||||
io.on('connection', socket => handleSocket(socket, io, store))
|
||||
const stores = configureStores(io, config.store)
|
||||
io.on('connection', socket => handleSocket(socket, io, stores))
|
||||
|
||||
export default server
|
||||
|
||||
@ -21,12 +21,27 @@ export interface Config {
|
||||
cert: string
|
||||
key: string
|
||||
}
|
||||
store?: StoreConfig
|
||||
}
|
||||
|
||||
export interface StoreRedisConfig {
|
||||
host: string
|
||||
port: number
|
||||
prefix: string
|
||||
type: 'redis'
|
||||
}
|
||||
|
||||
export interface StoreMemoryConfig {
|
||||
type: 'memory'
|
||||
}
|
||||
|
||||
export type StoreConfig = StoreRedisConfig | StoreMemoryConfig
|
||||
|
||||
const cfg = readConfig()
|
||||
|
||||
export const config: Config = {
|
||||
baseUrl: cfg.get('baseUrl', ''),
|
||||
iceServers: cfg.get('iceServers'),
|
||||
ssl: cfg.get('ssl', undefined),
|
||||
store: cfg.get('store', {type: 'memory'}),
|
||||
}
|
||||
|
||||
42
src/server/configureStores.test.ts
Normal file
42
src/server/configureStores.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
jest.mock('ioredis')
|
||||
|
||||
import Redis from 'ioredis'
|
||||
import SocketIO from 'socket.io'
|
||||
import { configureStores } from './configureStores'
|
||||
import { MemoryStore, RedisStore } from './store'
|
||||
|
||||
describe('configureStores', () => {
|
||||
|
||||
describe('memory', () => {
|
||||
it('should be in memory when no params specified', () => {
|
||||
const io = SocketIO()
|
||||
const stores = configureStores(io)
|
||||
expect(stores.socketIdByUserId).toEqual(jasmine.any(MemoryStore))
|
||||
expect(stores.userIdBySocketId).toEqual(jasmine.any(MemoryStore))
|
||||
})
|
||||
|
||||
it('should be in memory when type="memory"', () => {
|
||||
const io = SocketIO()
|
||||
const stores = configureStores(io)
|
||||
expect(stores.socketIdByUserId).toEqual(jasmine.any(MemoryStore))
|
||||
expect(stores.userIdBySocketId).toEqual(jasmine.any(MemoryStore))
|
||||
})
|
||||
})
|
||||
|
||||
describe('redis', () => {
|
||||
it('should be redis when type="redis"', () => {
|
||||
const io = SocketIO()
|
||||
const stores = configureStores(io, {
|
||||
type: 'redis',
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
prefix: 'peercalls',
|
||||
})
|
||||
expect(io.adapter().pubClient).toEqual(jasmine.any(Redis))
|
||||
expect(io.adapter().subClient).toEqual(jasmine.any(Redis))
|
||||
expect(stores.socketIdByUserId).toEqual(jasmine.any(RedisStore))
|
||||
expect(stores.userIdBySocketId).toEqual(jasmine.any(RedisStore))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
55
src/server/configureStores.ts
Normal file
55
src/server/configureStores.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import _debug from 'debug'
|
||||
import Redis from 'ioredis'
|
||||
import redisAdapter from 'socket.io-redis'
|
||||
import { StoreConfig, StoreRedisConfig } from './config'
|
||||
import { Stores } from './socket'
|
||||
import { MemoryStore, RedisStore } from './store'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
export function configureStores(
|
||||
io: SocketIO.Server,
|
||||
config: StoreConfig = { type: 'memory'},
|
||||
): Stores {
|
||||
switch (config.type) {
|
||||
case 'redis':
|
||||
debug('Using redis store: %s:%s', config.host, config.port)
|
||||
configureRedis(io, config)
|
||||
return {
|
||||
socketIdByUserId: new RedisStore(
|
||||
createRedisClient(config),
|
||||
[config.prefix, 'socketIdByUserId'].join(':'),
|
||||
),
|
||||
userIdBySocketId: new RedisStore(
|
||||
createRedisClient(config),
|
||||
[config.prefix, 'socketIdByUserId'].join(':'),
|
||||
),
|
||||
}
|
||||
default:
|
||||
debug('Using in-memory store')
|
||||
return {
|
||||
socketIdByUserId: new MemoryStore(),
|
||||
userIdBySocketId: new MemoryStore(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function configureRedis(
|
||||
io: SocketIO.Server,
|
||||
config: StoreRedisConfig,
|
||||
) {
|
||||
const pubClient = createRedisClient(config)
|
||||
const subClient = createRedisClient(config)
|
||||
io.adapter(redisAdapter({
|
||||
key: 'peercalls',
|
||||
pubClient,
|
||||
subClient,
|
||||
}))
|
||||
}
|
||||
|
||||
function createRedisClient(config: StoreRedisConfig) {
|
||||
return new Redis({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
})
|
||||
}
|
||||
@ -1,76 +1,64 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { Socket } from 'socket.io'
|
||||
import { TypedIO, ServerSocket } from '../shared'
|
||||
import { TypedIO } from '../shared'
|
||||
import handleSocket from './socket'
|
||||
import { MemoryStore, Store } from './store'
|
||||
|
||||
describe('server/socket', () => {
|
||||
type SocketMock = Socket & {
|
||||
type NamespaceMock = Socket & {
|
||||
id: string
|
||||
room?: string
|
||||
join: jest.Mock
|
||||
leave: jest.Mock
|
||||
emit: jest.Mock
|
||||
clients: (callback: (
|
||||
err: Error | undefined, clients: string[]
|
||||
) => void) => void
|
||||
}
|
||||
|
||||
let socket: SocketMock
|
||||
let socket: NamespaceMock
|
||||
let io: TypedIO & {
|
||||
in: jest.Mock<(room: string) => SocketMock>
|
||||
to: jest.Mock<(room: string) => SocketMock>
|
||||
in: jest.Mock<(room: string) => NamespaceMock>
|
||||
to: jest.Mock<(room: string) => NamespaceMock>
|
||||
}
|
||||
let rooms: Record<string, {emit: any}>
|
||||
const socket0 = {
|
||||
id: 'socket0',
|
||||
}
|
||||
const socket1 = {
|
||||
id: 'socket1',
|
||||
}
|
||||
const socket2 = {
|
||||
id: 'socket2',
|
||||
}
|
||||
let emitPromise: Promise<void>
|
||||
beforeEach(() => {
|
||||
socket = new EventEmitter() as SocketMock
|
||||
socket = new EventEmitter() as NamespaceMock
|
||||
socket.id = 'socket0'
|
||||
socket.join = jest.fn()
|
||||
socket.leave = jest.fn()
|
||||
rooms = {}
|
||||
|
||||
let emitResolve: () => void
|
||||
emitPromise = new Promise(resolve => {
|
||||
emitResolve = resolve
|
||||
})
|
||||
|
||||
const socketsByRoom: Record<string, string[]> = {
|
||||
room1: [socket0.id],
|
||||
room2: [socket0.id],
|
||||
room3: [socket0.id, socket1.id, socket2.id],
|
||||
}
|
||||
|
||||
io = {} as any
|
||||
io.in = io.to = jest.fn().mockImplementation(room => {
|
||||
return (rooms[room] = rooms[room] || {
|
||||
emit: jest.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
const sockets = {
|
||||
socket0: {
|
||||
id: 'socket0',
|
||||
userId: 'socket0_userid',
|
||||
},
|
||||
socket1: {
|
||||
id: 'socket1',
|
||||
userId: 'socket1_userid',
|
||||
},
|
||||
socket2: {
|
||||
id: 'socket2',
|
||||
userId: 'socket2_userid',
|
||||
},
|
||||
}
|
||||
|
||||
io.sockets = {
|
||||
sockets: sockets as any,
|
||||
adapter: {
|
||||
rooms: {
|
||||
room1: {
|
||||
socket0: true,
|
||||
} as any,
|
||||
room2: {
|
||||
socket0: true,
|
||||
} as any,
|
||||
room3: {
|
||||
sockets: {
|
||||
socket0: true,
|
||||
socket1: true,
|
||||
socket2: true,
|
||||
},
|
||||
} as any,
|
||||
emit: jest.fn().mockImplementation(() => emitResolve()),
|
||||
clients: callback => {
|
||||
callback(undefined, socketsByRoom[room] || [])
|
||||
},
|
||||
} as any,
|
||||
} as any
|
||||
|
||||
socket.leave = jest.fn()
|
||||
socket.join = jest.fn()
|
||||
} as NamespaceMock)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be a function', () => {
|
||||
@ -78,24 +66,35 @@ describe('server/socket', () => {
|
||||
})
|
||||
|
||||
describe('socket events', () => {
|
||||
let store: Store
|
||||
let stores: {
|
||||
userIdBySocketId: Store
|
||||
socketIdByUserId: Store
|
||||
}
|
||||
beforeEach(() => {
|
||||
store = new MemoryStore()
|
||||
handleSocket(socket, io, store)
|
||||
stores = {
|
||||
userIdBySocketId: new MemoryStore(),
|
||||
socketIdByUserId: new MemoryStore(),
|
||||
}
|
||||
stores.socketIdByUserId.set('a', socket0.id)
|
||||
stores.userIdBySocketId.set(socket0.id, 'a')
|
||||
stores.socketIdByUserId.set('b', socket1.id)
|
||||
stores.userIdBySocketId.set(socket1.id, 'b')
|
||||
stores.socketIdByUserId.set('c', socket2.id)
|
||||
stores.userIdBySocketId.set(socket2.id, 'c')
|
||||
handleSocket(socket, io, stores)
|
||||
})
|
||||
|
||||
describe('signal', () => {
|
||||
it('should broadcast signal to specific user', () => {
|
||||
store.set('a', 'a-socket-id')
|
||||
;(socket as ServerSocket) .userId = 'b'
|
||||
it('should broadcast signal to specific user', async () => {
|
||||
const signal = { type: 'signal' }
|
||||
|
||||
socket.emit('signal', { userId: 'a', signal })
|
||||
socket.emit('signal', { userId: 'b', signal })
|
||||
await emitPromise
|
||||
|
||||
expect(io.to.mock.calls).toEqual([[ 'a-socket-id' ]])
|
||||
expect((io.to('a-socket-id').emit as jest.Mock).mock.calls).toEqual([[
|
||||
expect(io.to.mock.calls).toEqual([[ socket1.id ]])
|
||||
expect((io.to(socket1.id).emit as jest.Mock).mock.calls).toEqual([[
|
||||
'signal', {
|
||||
userId: 'b',
|
||||
userId: 'a',
|
||||
signal,
|
||||
},
|
||||
]])
|
||||
@ -103,45 +102,48 @@ describe('server/socket', () => {
|
||||
})
|
||||
|
||||
describe('ready', () => {
|
||||
it('should call socket.leave if socket.room', () => {
|
||||
it('never calls socket.leave', async () => {
|
||||
socket.room = 'room1'
|
||||
socket.emit('ready', {
|
||||
userId: 'socket0_userid',
|
||||
userId: 'a',
|
||||
room: 'room2',
|
||||
})
|
||||
await emitPromise
|
||||
|
||||
expect(socket.leave.mock.calls).toEqual([[ 'room1' ]])
|
||||
expect(socket.leave.mock.calls).toEqual([])
|
||||
expect(socket.join.mock.calls).toEqual([[ 'room2' ]])
|
||||
})
|
||||
|
||||
it('should call socket.join to room', () => {
|
||||
it('should call socket.join to room', async () => {
|
||||
socket.emit('ready', {
|
||||
userId: 'socket0_userid',
|
||||
userId: 'b',
|
||||
room: 'room3',
|
||||
})
|
||||
await emitPromise
|
||||
expect(socket.join.mock.calls).toEqual([[ 'room3' ]])
|
||||
})
|
||||
|
||||
it('should emit users', () => {
|
||||
it('should emit users', async () => {
|
||||
socket.emit('ready', {
|
||||
userId: 'socket0_userid',
|
||||
userId: 'a',
|
||||
room: 'room3',
|
||||
})
|
||||
await emitPromise
|
||||
|
||||
expect(io.to.mock.calls).toEqual([[ 'room3' ]])
|
||||
// expect(io.to.mock.calls).toEqual([[ 'room3' ]])
|
||||
expect((io.to('room3').emit as jest.Mock).mock.calls).toEqual([
|
||||
[
|
||||
'users', {
|
||||
initiator: 'socket0_userid',
|
||||
initiator: 'a',
|
||||
users: [{
|
||||
socketId: 'socket0',
|
||||
userId: 'socket0_userid',
|
||||
socketId: socket0.id,
|
||||
userId: 'a',
|
||||
}, {
|
||||
socketId: 'socket1',
|
||||
userId: 'socket1_userid',
|
||||
socketId: socket1.id,
|
||||
userId: 'b',
|
||||
}, {
|
||||
socketId: 'socket2',
|
||||
userId: 'socket2_userid',
|
||||
socketId: socket2.id,
|
||||
userId: 'c',
|
||||
}],
|
||||
},
|
||||
],
|
||||
|
||||
@ -1,44 +1,54 @@
|
||||
'use strict'
|
||||
import _debug from 'debug'
|
||||
import map from 'lodash/map'
|
||||
import { ServerSocket, TypedIO } from '../shared'
|
||||
import { Store } from './store'
|
||||
|
||||
const debug = _debug('peercalls:socket')
|
||||
|
||||
export interface Stores {
|
||||
userIdBySocketId: Store
|
||||
socketIdByUserId: Store
|
||||
}
|
||||
|
||||
export default function handleSocket(
|
||||
socket: ServerSocket,
|
||||
io: TypedIO,
|
||||
store: Store,
|
||||
stores: Stores,
|
||||
) {
|
||||
socket.once('disconnect', () => {
|
||||
if (socket.userId) {
|
||||
store.remove(socket.userId)
|
||||
socket.once('disconnect', async () => {
|
||||
const userId = await stores.userIdBySocketId.get(socket.id)
|
||||
if (userId) {
|
||||
await Promise.all([
|
||||
stores.userIdBySocketId.remove(socket.id),
|
||||
stores.socketIdByUserId.remove(userId),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('signal', payload => {
|
||||
socket.on('signal', async payload => {
|
||||
// debug('signal: %s, payload: %o', socket.userId, payload)
|
||||
const socketId = store.get(payload.userId)
|
||||
const socketId = await stores.socketIdByUserId.get(payload.userId)
|
||||
const userId = await stores.userIdBySocketId.get(socket.id)
|
||||
if (socketId) {
|
||||
io.to(socketId).emit('signal', {
|
||||
userId: socket.userId,
|
||||
userId,
|
||||
signal: payload.signal,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('ready', payload => {
|
||||
socket.on('ready', async payload => {
|
||||
const { userId, room } = payload
|
||||
debug('ready: %s, room: %s', userId, room)
|
||||
if (socket.room) socket.leave(socket.room)
|
||||
socket.userId = userId
|
||||
store.set(userId, socket.id)
|
||||
socket.room = room
|
||||
// no need to leave rooms because there will be only one room for the
|
||||
// duration of the socket connection
|
||||
await Promise.all([
|
||||
stores.socketIdByUserId.set(userId, socket.id),
|
||||
stores.userIdBySocketId.set(socket.id, userId),
|
||||
])
|
||||
socket.join(room)
|
||||
socket.room = room
|
||||
|
||||
const users = getUsers(room)
|
||||
const users = await getUsers(room)
|
||||
|
||||
debug('ready: %s, room: %s, users: %o', userId, room, users)
|
||||
|
||||
@ -48,14 +58,25 @@ export default function handleSocket(
|
||||
})
|
||||
})
|
||||
|
||||
function getUsers (room: string) {
|
||||
return map(io.sockets.adapter.rooms[room].sockets, (_, socketId) => {
|
||||
const userSocket = io.sockets.sockets[socketId] as ServerSocket
|
||||
return {
|
||||
socketId: socketId,
|
||||
userId: userSocket.userId,
|
||||
}
|
||||
})
|
||||
async function getUsers (room: string) {
|
||||
const socketIds = await getClientsInRoom(room)
|
||||
const userIds = await stores.userIdBySocketId.getMany(socketIds)
|
||||
return socketIds.map((socketId, i) => ({
|
||||
socketId,
|
||||
userId: userIds[i],
|
||||
}))
|
||||
}
|
||||
|
||||
async function getClientsInRoom(room: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
io.in(room).clients((err: Error, clients: string[]) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(clients)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './store'
|
||||
export * from './memory'
|
||||
export * from './redis'
|
||||
|
||||
@ -3,15 +3,23 @@ import { Store } from './store'
|
||||
export class MemoryStore implements Store {
|
||||
store: Record<string, string> = {}
|
||||
|
||||
get(key: string): string | undefined {
|
||||
return this.store[key]
|
||||
async getMany(keys: string[]): Promise<Array<string | undefined>> {
|
||||
return keys.map(key => this.syncGet(key))
|
||||
}
|
||||
|
||||
set(key: string, value: string) {
|
||||
private syncGet(key: string): string | undefined {
|
||||
return this.store[key]
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
return this.syncGet(key)
|
||||
}
|
||||
|
||||
async set(key: string, value: string) {
|
||||
this.store[key] = value
|
||||
}
|
||||
|
||||
remove(key: string) {
|
||||
async remove(key: string) {
|
||||
delete this.store[key]
|
||||
}
|
||||
}
|
||||
|
||||
54
src/server/store/redis.ts
Normal file
54
src/server/store/redis.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import Redis from 'ioredis'
|
||||
import { Store } from './store'
|
||||
import _debug from 'debug'
|
||||
|
||||
const debug = _debug('peercalls:redis')
|
||||
|
||||
interface RedisClient {
|
||||
get: Redis.Redis['get']
|
||||
set: Redis.Redis['set']
|
||||
del: Redis.Redis['del']
|
||||
}
|
||||
export class RedisStore implements Store {
|
||||
constructor(
|
||||
protected readonly redis: RedisClient,
|
||||
protected readonly prefix: string,
|
||||
) {
|
||||
}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return [this.prefix, key].filter(k => !!k).join(':')
|
||||
}
|
||||
|
||||
private nullToUndefined(value: string | null): string | undefined {
|
||||
if (value === null) {
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
async getMany(keys: string[]): Promise<Array<string | undefined>> {
|
||||
const result = await Promise.all(
|
||||
keys.map(key => this.redis.get(this.getKey(key))))
|
||||
return result.map(this.nullToUndefined)
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
key = this.getKey(key)
|
||||
debug('get %s', key)
|
||||
const result = await this.redis.get(key)
|
||||
return this.nullToUndefined(result)
|
||||
}
|
||||
|
||||
async set(key: string, value: string) {
|
||||
key = this.getKey(key)
|
||||
debug('set %s %s', key, value)
|
||||
await this.redis.set(key, value)
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
key = this.getKey(key)
|
||||
debug('del %s', key)
|
||||
await this.redis.del(key)
|
||||
}
|
||||
}
|
||||
47
src/server/store/store.test.ts
Normal file
47
src/server/store/store.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { MemoryStore, RedisStore } from './'
|
||||
import Redis from 'ioredis'
|
||||
import { Store } from './store'
|
||||
|
||||
describe('store', () => {
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.TEST_REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.TEST_REDIS_PORT!) || 6379,
|
||||
enableOfflineQueue: true,
|
||||
})
|
||||
|
||||
const testCases: Array<{name: string, store: Store}> = [{
|
||||
name: MemoryStore.name,
|
||||
store: new MemoryStore(),
|
||||
}, {
|
||||
name: RedisStore.name,
|
||||
store: new RedisStore(redis, 'peercallstest'),
|
||||
}]
|
||||
|
||||
afterAll(() => {
|
||||
redis.disconnect()
|
||||
})
|
||||
|
||||
testCases.forEach(({name, store}) => {
|
||||
describe(name, () => {
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
store.remove('a'),
|
||||
store.remove('b'),
|
||||
])
|
||||
})
|
||||
describe('set, get, getMany', () => {
|
||||
it('sets and retreives value(s)', async () => {
|
||||
await store.set('a', 'A')
|
||||
await store.set('b', 'B')
|
||||
expect(await store.get('a')).toBe('A')
|
||||
expect(await store.get('b')).toBe('B')
|
||||
expect(await store.remove('b'))
|
||||
expect(await store.get('c')).toBe(undefined)
|
||||
expect(await store.getMany(['a', 'b', 'c']))
|
||||
.toEqual(['A', undefined, undefined])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,6 @@
|
||||
export interface Store {
|
||||
set(key: string, value: string): void
|
||||
get(key: string): string | undefined
|
||||
remove(key: string): void
|
||||
set(key: string, value: string): Promise<void>
|
||||
get(key: string): Promise<string | undefined>
|
||||
getMany(keys: string[]): Promise<Array<string | undefined>>
|
||||
remove(key: string): Promise<void>
|
||||
}
|
||||
|
||||
@ -28,8 +28,7 @@ export interface SocketEvent {
|
||||
|
||||
export type ServerSocket =
|
||||
Omit<SocketIO.Socket, TypedEmitterKeys> &
|
||||
TypedEmitter<SocketEvent> &
|
||||
{ userId?: string, room?: string }
|
||||
TypedEmitter<SocketEvent>
|
||||
|
||||
export type TypedIO = SocketIO.Server & {
|
||||
to(roomName: string): TypedEmitter<SocketEvent>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user