switch to typescript

This commit is contained in:
Zutatensuppe 2021-05-17 00:27:47 +02:00
parent 031ca31c7e
commit 23559b1a3b
63 changed files with 7943 additions and 1397 deletions

12
src/server/Dirs.ts Normal file
View file

@ -0,0 +1,12 @@
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const BASE_DIR = `${__dirname}/../..`
export const DATA_DIR = `${BASE_DIR}/data`
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
export const UPLOAD_URL = `/uploads`
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`

70
src/server/Game.ts Normal file
View file

@ -0,0 +1,70 @@
import GameCommon from './../common/GameCommon'
import Util from './../common/Util'
import { Rng } from '../common/Rng'
import GameLog from './GameLog'
import { createPuzzle } from './Puzzle'
import Protocol from '../common/Protocol'
import GameStorage from './GameStorage'
async function createGameObject(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
return {
id: gameId,
rng: { type: 'Rng', obj: rng },
puzzle: await createPuzzle(rng, targetTiles, image, ts),
players: [],
evtInfos: {},
scoreMode,
}
}
async function createGame(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
GameLog.create(gameId)
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode)
GameCommon.setGame(gameObject.id, gameObject)
GameStorage.setDirty(gameId)
}
function addPlayer(gameId: string, playerId: string, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
if (idx === -1) {
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, diff)
} else {
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff)
}
GameCommon.addPlayer(gameId, playerId, ts)
GameStorage.setDirty(gameId)
}
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
GameStorage.setDirty(gameId)
return ret
}
export default {
createGameObject,
createGame,
addPlayer,
handleInput,
getAllGames: GameCommon.getAllGames,
getActivePlayers: GameCommon.getActivePlayers,
getFinishedTileCount: GameCommon.getFinishedTileCount,
getImageUrl: GameCommon.getImageUrl,
getTileCount: GameCommon.getTileCount,
exists: GameCommon.exists,
playerExists: GameCommon.playerExists,
get: GameCommon.get,
getStartTs: GameCommon.getStartTs,
getFinishTs: GameCommon.getFinishTs,
}

51
src/server/GameLog.ts Normal file
View file

@ -0,0 +1,51 @@
import fs from 'fs'
import { logger } from '../common/Util.js'
import { DATA_DIR } from '../server/Dirs.js'
const log = logger('GameLog.js')
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
const create = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
fs.appendFileSync(file, '')
}
}
const exists = (gameId: string) => {
const file = filename(gameId)
return fs.existsSync(file)
}
const _log = (gameId: string, ...args: Array<any>) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return
}
const str = JSON.stringify(args)
fs.appendFileSync(file, str + "\n")
}
const get = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return []
}
const lines = fs.readFileSync(file, 'utf-8').split("\n")
return lines.filter((line: string) => !!line).map((line: string) => {
try {
return JSON.parse(line)
} catch (e) {
log.log(line)
log.log(e)
}
})
}
export default {
create,
exists,
log: _log,
get,
}

48
src/server/GameSockets.ts Normal file
View file

@ -0,0 +1,48 @@
import { logger } from '../common/Util.js'
import WebSocket from 'ws'
const log = logger('GameSocket.js')
// Map<gameId, Socket[]>
const SOCKETS = {} as Record<string, Array<WebSocket>>
function socketExists(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
return false
}
return SOCKETS[gameId].includes(socket)
}
function removeSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
return
}
SOCKETS[gameId] = SOCKETS[gameId].filter((s: WebSocket) => s !== socket)
log.log('removed socket: ', gameId, socket.protocol)
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
}
function addSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
SOCKETS[gameId] = []
}
if (!SOCKETS[gameId].includes(socket)) {
SOCKETS[gameId].push(socket)
log.log('added socket: ', gameId, socket.protocol)
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
}
}
function getSockets(gameId: string) {
if (!(gameId in SOCKETS)) {
return []
}
return SOCKETS[gameId]
}
export default {
addSocket,
removeSocket,
socketExists,
getSockets,
}

93
src/server/GameStorage.ts Normal file
View file

@ -0,0 +1,93 @@
import fs from 'fs'
import GameCommon from './../common/GameCommon'
import Util, { logger } from './../common/Util'
import { Rng } from '../common/Rng'
import { DATA_DIR } from './Dirs'
import Time from './../common/Time'
const log = logger('GameStorage.js')
const DIRTY_GAMES = {} as any
function setDirty(gameId: string): void {
DIRTY_GAMES[gameId] = true
}
function setClean(gameId: string): void {
delete DIRTY_GAMES[gameId]
}
function loadGames(): void {
const files = fs.readdirSync(DATA_DIR)
for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/)
if (!m) {
continue
}
const gameId = m[1]
loadGame(gameId)
}
}
function loadGame(gameId: string): void {
const file = `${DATA_DIR}/${gameId}.json`
const contents = fs.readFileSync(file, 'utf-8')
let game
try {
game = JSON.parse(contents)
} catch {
log.log(`[ERR] unable to load game from file ${file}`);
}
if (typeof game.puzzle.data.started === 'undefined') {
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
}
if (typeof game.puzzle.data.finished === 'undefined') {
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find((t: any) => t.owner !== -1)
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players)
}
const gameObject = {
id: game.id,
rng: {
type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
},
puzzle: game.puzzle,
players: game.players,
evtInfos: {},
scoreMode: game.scoreMode || GameCommon.SCORE_MODE_FINAL,
}
GameCommon.setGame(gameObject.id, gameObject)
}
function persistGames() {
for (const gameId of Object.keys(DIRTY_GAMES)) {
persistGame(gameId)
}
}
function persistGame(gameId: string) {
const game = GameCommon.get(gameId)
if (game.id in DIRTY_GAMES) {
setClean(game.id)
}
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
id: game.id,
rng: {
type: game.rng.type,
obj: Rng.serialize(game.rng.obj),
},
puzzle: game.puzzle,
players: game.players,
scoreMode: game.scoreMode,
}))
log.info(`[INFO] persisted game ${game.id}`)
}
export default {
loadGames,
loadGame,
persistGames,
persistGame,
setDirty,
}

82
src/server/Images.ts Normal file
View file

@ -0,0 +1,82 @@
import sizeOf from 'image-size'
import fs from 'fs'
import exif from 'exif'
import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js'
const resizeImage = async (filename: string) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return
}
const imagePath = `${UPLOAD_DIR}/${filename}`
const imageOutPath = `${UPLOAD_DIR}/r/${filename}`
const orientation = await getExifOrientation(imagePath)
let sharpImg = sharp(imagePath, { failOnError: false })
// when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
if (orientation === 6) {
sharpImg = sharpImg.rotate()
} else if (orientation === 3) {
sharpImg = sharpImg.rotate().rotate()
} else if (orientation === 8) {
sharpImg = sharpImg.rotate().rotate().rotate()
}
const sizes = [
[150, 100],
[375, 210],
]
for (let [w,h] of sizes) {
console.log(w, h, imagePath)
await sharpImg.resize(w, h, { fit: 'contain' }).toFile(`${imageOutPath}-${w}x${h}.webp`)
}
}
async function getExifOrientation(imagePath: string) {
return new Promise((resolve, reject) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
if (error) {
resolve(0)
} else {
resolve(exifData.image.Orientation)
}
})
})
}
const allImages = () => {
const images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({
filename: f,
file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
}))
.sort((a, b) => {
return fs.statSync(b.file).mtime.getTime() -
fs.statSync(a.file).mtime.getTime()
})
return images
}
async function getDimensions(imagePath: string) {
let dimensions = sizeOf(imagePath)
const orientation = await getExifOrientation(imagePath)
// when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
if (orientation === 6 || orientation === 8) {
return {
width: dimensions.height,
height: dimensions.width,
}
}
return dimensions
}
export default {
allImages,
resizeImage,
getDimensions,
}

218
src/server/Puzzle.ts Normal file
View file

@ -0,0 +1,218 @@
import Util from '../common/Util'
import { Rng } from '../common/Rng'
import Images from './Images.js'
interface PuzzleInfo {
width: number
height: number
tileSize: number
tileMarginWidth: number
tileDrawSize: number
tiles: number
tilesX: number
tilesY: number
}
// cut size of each puzzle tile in the
// final resized version of the puzzle image
const TILE_SIZE = 64
async function createPuzzle(
rng: Rng,
targetTiles: number,
image: { file: string, url: string },
ts: number
) {
const imagePath = image.file
const imageUrl = image.url
// determine puzzle information from the image dimensions
const dim = await Images.getDimensions(imagePath)
if (!dim || !dim.width || !dim.height) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
}
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
let tiles = new Array(info.tiles)
for (let i = 0; i < tiles.length; i++) {
tiles[i] = { idx: i }
}
const shapes = determinePuzzleTileShapes(rng, info)
let positions = new Array(info.tiles)
for (let tile of tiles) {
const coord = Util.coordByTileIdx(info, tile.idx)
positions[tile.idx] ={
// instead of info.tileSize, we use info.tileDrawSize
// to spread the tiles a bit
x: coord.x * info.tileSize * 1.5,
y: coord.y * info.tileSize * 1.5,
}
}
const tableWidth = info.width * 3
const tableHeight = info.height * 3
const off = info.tileSize * 1.5
let last = {
x: info.width - (1 * off),
y: info.height - (2 * off),
}
let countX = Math.ceil(info.width / off) + 2
let countY = Math.ceil(info.height / off) + 2
let diffX = off
let diffY = 0
let index = 0
for (let pos of positions) {
pos.x = last.x
pos.y = last.y
last.x+=diffX
last.y+=diffY
index++
// did we move horizontally?
if (diffX !== 0) {
if (index === countX) {
diffY = diffX
countY++
diffX = 0
index = 0
}
} else {
if (index === countY) {
diffX = -diffY
countX++
diffY = 0
index = 0
}
}
}
// then shuffle the positions
positions = Util.shuffle(rng, positions)
tiles = tiles.map(tile => {
return Util.encodeTile({
idx: tile.idx, // index of tile in the array
group: 0, // if grouped with other tiles
z: 0, // z index of the tile
// who owns the tile
// 0 = free for taking
// -1 = finished
// other values: id of player who has the tile
owner: 0,
// physical current position of the tile (x/y in pixels)
// this position is the initial position only and is the
// value that changes when moving a tile
pos: positions[tile.idx],
})
})
// Complete puzzle object
return {
// tiles array
tiles,
// game data for puzzle, data changes during the game
data: {
// TODO: maybe calculate this each time?
maxZ: 0, // max z of all pieces
maxGroup: 0, // max group of all pieces
started: ts, // start timestamp
finished: 0, // finish timestamp
},
// static puzzle information. stays same for complete duration of
// the game
info: {
table: {
width: tableWidth,
height: tableHeight,
},
// information that was used to create the puzzle
targetTiles: targetTiles,
imageUrl,
width: info.width, // actual puzzle width (same as bitmap.width)
height: info.height, // actual puzzle height (same as bitmap.height)
tileSize: info.tileSize, // width/height of each tile (without tabs)
tileDrawSize: info.tileDrawSize, // width/height of each tile (with tabs)
tileMarginWidth: info.tileMarginWidth,
// offset in x and y when drawing tiles, so that they appear to be at pos
tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2,
// max distance between tile and destination that
// makes the tile snap to destination
snapDistance: info.tileSize / 2,
tiles: info.tiles, // the final number of tiles in the puzzle
tilesX: info.tilesX, // number of tiles each row
tilesY: info.tilesY, // number of tiles each col
// ( index => {x, y} )
// this is not the physical coordinate, but
// the tile_coordinate
// this can be used to determine where the
// final destination of a tile is
shapes: shapes, // tile shapes
},
}
}
function determinePuzzleTileShapes(
rng: Rng,
info: PuzzleInfo
) {
const tabs = [-1, 1]
const shapes = new Array(info.tiles)
for (let i = 0; i < info.tiles; i++) {
let coord = Util.coordByTileIdx(info, i)
shapes[i] = {
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
right: coord.x === info.tilesX - 1 ? 0 : Util.choice(rng, tabs),
left: coord.x === 0 ? 0 : shapes[i - 1].right * -1,
bottom: coord.y === info.tilesY - 1 ? 0 : Util.choice(rng, tabs),
}
}
return shapes.map(Util.encodeShape)
}
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
const w_ = w < h ? (w * h) : (w * w)
const h_ = w < h ? (h * h) : (w * h)
let size = 0
let tiles = 0
do {
size++
tiles = Math.floor(w_ / size) * Math.floor(h_ / size)
} while (tiles >= targetTiles)
size--
return {
tilesX: Math.round(w_ / size),
tilesY: Math.round(h_ / size),
}
}
const determinePuzzleInfo = (w: number, h: number, targetTiles: number) => {
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
const tiles = tilesX * tilesY
const tileSize = TILE_SIZE
const width = tilesX * tileSize
const height = tilesY * tileSize
const tileMarginWidth = tileSize * .5;
const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2)
return {
width,
height,
tileSize,
tileMarginWidth,
tileDrawSize,
tiles,
tilesX,
tilesY,
}
}
export {
createPuzzle,
}

View file

@ -0,0 +1,80 @@
import WebSocket from 'ws'
import { logger } from '../common/Util.js'
const log = logger('WebSocketServer.js')
/*
Example config
config = {
hostname: 'localhost',
port: 1338,
connectstring: `ws://localhost:1338/ws`,
}
*/
class EvtBus {
private _on: any
constructor() {
this._on = {} as any
}
on(type: string, callback: Function) {
this._on[type] = this._on[type] || []
this._on[type].push(callback)
}
dispatch(type: string, ...args: Array<any>) {
(this._on[type] || []).forEach((cb: Function) => {
cb(...args)
})
}
}
class WebSocketServer {
evt: EvtBus
private _websocketserver: WebSocket.Server|null
config: any
constructor(config: any) {
this.config = config
this._websocketserver = null
this.evt = new EvtBus()
}
on(type: string, callback: Function) {
this.evt.on(type, callback)
}
listen() {
this._websocketserver = new WebSocket.Server(this.config)
this._websocketserver.on('connection', (socket: WebSocket, request: Request) => {
const pathname = new URL(this.config.connectstring).pathname
if (request.url.indexOf(pathname) !== 0) {
log.log('bad request url: ', request.url)
socket.close()
return
}
socket.on('message', (data: any) => {
log.log(`ws`, socket.protocol, data)
this.evt.dispatch('message', {socket, data})
})
socket.on('close', () => {
this.evt.dispatch('close', {socket})
})
})
}
close() {
if (this._websocketserver) {
this._websocketserver.close()
}
}
notifyOne(data: any, socket: WebSocket) {
socket.send(JSON.stringify(data))
}
}
export default WebSocketServer

282
src/server/main.ts Normal file
View file

@ -0,0 +1,282 @@
import WebSocketServer from './WebSocketServer'
import WebSocket from 'ws'
import express from 'express'
import multer from 'multer'
import Protocol from './../common/Protocol'
import Util, { logger } from './../common/Util'
import Game from './Game'
import bodyParser from 'body-parser'
import v8 from 'v8'
import fs from 'fs'
import GameLog from './GameLog'
import GameSockets from './GameSockets'
import Time from '../common/Time'
import Images from './Images'
import {
UPLOAD_DIR,
UPLOAD_URL,
PUBLIC_DIR,
} from './Dirs'
import GameCommon from '../common/GameCommon'
import GameStorage from './GameStorage'
let configFile = ''
let last = ''
for (const val of process.argv) {
if (last === '-c') {
configFile = val
}
last = val
}
if (configFile === '') {
process.exit(2)
}
const config = JSON.parse(String(fs.readFileSync(configFile)))
const log = logger('main.js')
const port = config.http.port
const hostname = config.http.hostname
const app = express()
const storage = multer.diskStorage({
destination: UPLOAD_DIR,
filename: function (req, file, cb) {
cb(null , file.originalname);
}
})
const upload = multer({storage}).single('file');
app.get('/api/conf', (req, res) => {
res.send({
WS_ADDRESS: config.ws.connectstring,
})
})
app.get('/api/newgame-data', (req, res) => {
res.send({
images: Images.allImages(),
})
})
app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp()
const games = [
...Game.getAllGames().map((game: any) => ({
id: game.id,
hasReplay: GameLog.exists(game.id),
started: Game.getStartTs(game.id),
finished: Game.getFinishTs(game.id),
tilesFinished: Game.getFinishedTileCount(game.id),
tilesTotal: Game.getTileCount(game.id),
players: Game.getActivePlayers(game.id, ts).length,
imageUrl: Game.getImageUrl(game.id),
})),
]
res.send({
gamesRunning: games.filter(g => !g.finished),
gamesFinished: games.filter(g => !!g.finished),
})
})
app.post('/upload', (req, res) => {
upload(req, res, async (err: any) => {
if (err) {
log.log(err)
res.status(400).send("Something went wrong!");
}
try {
await Images.resizeImage(req.file.filename)
} catch (err) {
log.log(err)
res.status(400).send("Something went wrong!");
}
res.send({
image: {
file: `${UPLOAD_DIR}/${req.file.filename}`,
url: `${UPLOAD_URL}/${req.file.filename}`,
},
})
})
})
app.post('/newgame', bodyParser.json(), async (req, res) => {
log.log(req.body.tiles, req.body.image)
const gameId = Util.uniqId()
if (!Game.exists(gameId)) {
const ts = Time.timestamp()
await Game.createGame(
gameId,
req.body.tiles,
req.body.image,
ts,
req.body.scoreMode
)
}
res.send({ id: gameId })
})
app.use('/uploads/', express.static(UPLOAD_DIR))
app.use('/', express.static(PUBLIC_DIR))
const wss = new WebSocketServer(config.ws);
const notify = (data: any, sockets: Array<WebSocket>) => {
// TODO: throttle?
for (let socket of sockets) {
wss.notifyOne(data, socket)
}
}
wss.on('close', async ({socket} : {socket: WebSocket}) => {
try {
const proto = socket.protocol.split('|')
const clientId = proto[0]
const gameId = proto[1]
GameSockets.removeSocket(gameId, socket)
} catch (e) {
log.error(e)
}
})
wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
try {
const proto = socket.protocol.split('|')
const clientId = proto[0]
const gameId = proto[1]
const msg = JSON.parse(data)
const msgType = msg[0]
switch (msgType) {
case Protocol.EV_CLIENT_INIT_REPLAY: {
if (!GameLog.exists(gameId)) {
throw `[gamelog ${gameId} does not exist... ]`
}
const log = GameLog.get(gameId)
const game = await Game.createGameObject(
gameId,
log[0][2],
log[0][3],
log[0][4],
log[0][5] || GameCommon.SCORE_MODE_FINAL
)
notify(
[Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game), log],
[socket]
)
} break
case Protocol.EV_CLIENT_INIT: {
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const ts = Time.timestamp()
Game.addPlayer(gameId, clientId, ts)
GameSockets.addSocket(gameId, socket)
const game = Game.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]
)
} break
case Protocol.EV_CLIENT_EVENT: {
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const clientSeq = msg[1]
const clientEvtData = msg[2]
const ts = Time.timestamp()
let sendGame = false
if (!Game.playerExists(gameId, clientId)) {
Game.addPlayer(gameId, clientId, ts)
sendGame = true
}
if (!GameSockets.socketExists(gameId, socket)) {
GameSockets.addSocket(gameId, socket)
sendGame = true
}
if (sendGame) {
const game = Game.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]
)
}
const changes = Game.handleInput(gameId, clientId, clientEvtData, ts)
notify(
[Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes],
GameSockets.getSockets(gameId)
)
} break
}
} catch (e) {
log.error(e)
}
})
GameStorage.loadGames()
const server = app.listen(
port,
hostname,
() => log.log(`server running on http://${hostname}:${port}`)
)
wss.listen()
const memoryUsageHuman = () => {
const totalHeapSize = v8.getHeapStatistics().total_available_size
let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`)
const used = process.memoryUsage().heapUsed / 1024 / 1024
log.log(`Mem: ${Math.round(used * 100) / 100}M`)
}
memoryUsageHuman()
// persist games in fixed interval
const persistInterval = setInterval(() => {
log.log('Persisting games...')
GameStorage.persistGames()
memoryUsageHuman()
}, config.persistence.interval)
const gracefulShutdown = (signal: any) => {
log.log(`${signal} received...`)
log.log('clearing persist interval...')
clearInterval(persistInterval)
log.log('persisting games...')
GameStorage.persistGames()
log.log('shutting down webserver...')
server.close()
log.log('shutting down websocketserver...')
wss.close()
log.log('shutting down...')
process.exit()
}
// used by nodemon
process.once('SIGUSR2', function () {
gracefulShutdown('SIGUSR2')
})
process.once('SIGINT', function (code) {
gracefulShutdown('SIGINT')
})
process.once('SIGTERM', function (code) {
gracefulShutdown('SIGTERM')
})