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