Compare commits

..

No commits in common. "master" and "v3.0.10" have entirely different histories.

24 changed files with 6919 additions and 3646 deletions

View File

@ -4,18 +4,13 @@ type: docker
name: default name: default
steps: steps:
- name: build - name: build
image: node:12 image: node:12
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
... ...

View File

@ -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'

View File

@ -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

View File

@ -1,6 +1,4 @@
language: node_js language: node_js
services:
- redis-server
node_js: node_js:
- "8" - "8"
- "12" - "12"

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }

View File

@ -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();

View File

@ -1,2 +1,2 @@
import { EventEmitter } from 'events' import EventEmitter from 'events'
export default new EventEmitter() export default new EventEmitter()

View File

@ -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
} }

View File

@ -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),
},
]]) ]])
}) })

View File

@ -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

View File

@ -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'}),
} }

View File

@ -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))
})
})
})

View File

@ -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,
})
}

View File

@ -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',
}], }],
}, },
], ],

View File

@ -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)
}
})
})
}
} }

View File

@ -1,3 +1,2 @@
export * from './store' export * from './store'
export * from './memory' export * from './memory'
export * from './redis'

View File

@ -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]
} }
} }

View File

@ -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)
}
}

View File

@ -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])
})
})
})
})
})

View File

@ -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>
} }

View File

@ -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>