-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.js
177 lines (146 loc) · 4.53 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const express = require('express')
const expressNunjucks = require('express-nunjucks')
const http = require('http')
const socketIo = require('socket.io')
const moment = require('moment')
const colors = require('colors/safe')
const fs = require('fs')
const path = require('path')
// ========== Constants
const PORT = 3000
const DATA_ROOT = path.join(__dirname, 'data')
const ROOMS_FILE = path.join(DATA_ROOT, 'rooms.json')
const STATIC_ROOTS = [path.join(__dirname, 'static'), DATA_ROOT]
const STATE_SAVE_MIN_DELAY = 1000
const STATE_FULL_REFRESH_DELAY = 5000
// ========== Globals
let app = express()
let httpServer = http.Server(app)
let io = socketIo(httpServer)
let clients = 0
// ========== Routes
loadRooms(rooms => {
let roomIds = Object.keys(rooms)
// Pages & assets
app.set('views', path.join(__dirname, '/static'))
expressNunjucks(app, { watch: true, noCache: true })
app.get('/', (req, res) => {
res.render('index', { rooms })
})
app.get('/room/:roomId', (req, res) => {
res.render('room', { roomId: req.params.roomId })
})
STATIC_ROOTS.map(root => app.use(express.static(root)))
// SocketIO
io.on('connection', function (socket) {
clients++
log(`Client joined: ${socket.id}. Total clients: ${clients}`)
let socketRoom = rooms[socket.handshake.query.roomId]
if (!socketRoom) {
return // Wrong room!
}
socket.join(socketRoom.id)
socket.emit('init', socketRoom)
io.emit('clients', clients)
socket.on('state update', stateData => {
for (let key in stateData) {
let stateDataItem = stateData[key]
if (typeof stateData[key] === 'object') {
socketRoom.state[key] = Object.assign(socketRoom.state[key] || {}, stateDataItem) // data can be a partial state
} else {
socketRoom.state[key] = stateDataItem // special data
}
}
io.to(socketRoom.id).emit('state update', stateData)
saveState(socketRoom)
})
socket.on('disconnect', function () {
clients--
log(`Client left: ${socket.id}. Total clients: ${clients}`)
io.emit('clients', clients)
})
})
setInterval(() => roomIds.map(roomId => {
let room = rooms[roomId]
io.to(roomId).emit('state update', room.state)
saveState(room)
}), STATE_FULL_REFRESH_DELAY)
httpServer.listen(3000, () => log(`Alakajam! Studio launched on *:${PORT}`))
})
// ========== State management
function loadRooms(callback) {
loadJSON(ROOMS_FILE, roomsData => {
let rooms = {}
for (let index in roomsData) {
loadRoom(index, roomsData, rooms, callback)
}
})
}
function loadRoom(index, roomsData, rooms, callback) {
let roomData = roomsData[index]
let dataPath = path.join(DATA_ROOT, roomData.data_path)
let statePath = path.join(DATA_ROOT, roomData.state_path)
// Load data & state
loadJSON(dataPath, data => {
loadJSON(statePath, state => {
roomData.data = data
roomData.state = state
rooms[roomData.id] = roomData
// Auto-refresh room data upon file change
fs.watch(dataPath, () => {
loadJSON(dataPath, data => {
console.log(`${dataPath} refreshed`)
roomData.data = data
})
})
// Fire callback when all rooms are loaded
if (Object.keys(rooms).length === roomsData.length) {
callback(rooms)
}
}, {})
})
}
function loadJSON(file, callback, defaultJSON = undefined) {
fs.readFile(file, (err, buffer) => {
if (!err) {
try {
callback(JSON.parse(buffer))
} catch (e) {
err = e
}
}
if (err) {
if (defaultJSON) {
callback(defaultJSON)
} else {
error(err)
}
}
})
}
function saveState(room) {
let statePath = path.join(DATA_ROOT, room.state_path)
let state = room.state || {}
let now = new Date().getTime()
if (!state.lastSaved || now - state.lastSaved > STATE_SAVE_MIN_DELAY) {
state.lastSaved = now
let stateString = JSON.stringify(state, null, 2)
fs.writeFile(statePath, stateString, (err) => {
if (err) error(err)
})
}
}
// ========== Misc functions
function log() {
let argsWithTimestamp = [getTimestamp(), colors.green('INFO')]
for (let argument of arguments) argsWithTimestamp.push(argument)
console.log.apply(null, argsWithTimestamp)
}
function error() {
let argsWithTimestamp = [getTimestamp(), colors.red('ERROR')]
for (let argument of arguments) argsWithTimestamp.push(argument)
console.log.apply(null, argsWithTimestamp)
}
function getTimestamp() {
return moment().format('YYYY-MM-DD HH:mm:ss')
}