@hp4k1h5/terminordle
Version:
> multiplayer [wordle](https://www.powerlanguage.co.uk/wordle/) clone in your terminal
166 lines (137 loc) • 3.45 kB
text/typescript
import * as fs from 'fs'
import * as path from 'path'
import { createServer, Server } from 'https'
import * as dotenv from 'dotenv'
const p = path.resolve(__dirname, '../../../../.env')
dotenv.config({ path: p })
import { WebSocketServer } from 'ws'
import {
WS,
ClientMessage,
ClientMsgType,
ServerMessage,
ServerMsgType,
} from '../../lib/structs'
import { validateMsg, msg, err } from './msg'
import { remove, createSession, join, guess, again } from './session'
import { getRand, names, Log } from '../../util'
const server: Server | null =
process.env.NODE_ENV === 'production'
? createServer({
key: fs.readFileSync('' + process.env.SSL_KEY_PATH),
cert: fs.readFileSync('' + process.env.SSL_FULLCHAIN_PATH),
})
: null
const msgTypeToFn: {
[key in ServerMsgType]: (
ws: WS,
msg: ServerMessage,
log?: Log,
) => ClientMessage | void | undefined
} = {
create: createSession,
join: join,
guess: guess,
again,
}
const MAX_CNX = 1_000
export function createWSS(
port = 8080,
host = 'localhost',
log: Log | undefined,
) {
const opts = {
host,
backlog: 100,
maxPayload: 256,
clientTracking: true,
...(server ? { server } : { port }),
}
const wss = new WebSocketServer(opts)
wss.on('error', function (e) {
log && log.log({ err: e.toString() })
})
// heartbeat
const cycle = setInterval(function () {
wss.clients.forEach((client: WS) => {
if (client.is_alive === false) {
remove(client)
return
}
// reset
client.is_alive = false
client.ping()
})
}, 30_000)
wss.on('close', function close() {
clearInterval(cycle)
})
wss.on('connection', function (cnx: WS) {
if (wss.clients.size >= MAX_CNX) {
err(cnx, `overload : ${cnx}`, log)
cnx.close()
return
}
cnx.on('error', e => {
log && log.log({ err: e.toString(), user_id: cnx.user_id })
})
cnx.on('close', function () {
remove(cnx, log)
})
cnx.on('pong', heartbeat)
cnx.on('message', function (data: string) {
let message: ServerMessage
try {
message = validateMsg(cnx, data)
} catch (e) {
msg(cnx, { type: ClientMsgType.error, content: 'bad message' })
err(cnx, e, log)
return
}
let response: ClientMessage | undefined | void
try {
response = msgTypeToFn[message.type](cnx, message, log)
} catch (e) {
err(cnx, e, log)
return
}
response && msg(cnx, response)
})
try {
cnx.user_id = userId()
} catch (e) {
err(cnx, e, log)
remove(cnx)
return
}
log && log.log({ connection: 'established', with: cnx.user_id })
// send user id back
msg(cnx, {
type: ClientMsgType.user_id,
content: cnx.user_id,
user_id: cnx.user_id,
})
})
// ssl support
server && server.listen(port)
return wss
}
function heartbeat(this: WS) {
this.is_alive = true
return heartbeat
}
const nameList = Object.keys(names)
export function userId(): string | undefined {
let attempts = 0
const MAX_ATTEMPTS = 10
const _nameList = nameList.filter(name => names[name] === false)
let name
while (!name && attempts++ < MAX_ATTEMPTS) {
name = getRand(_nameList)
// double check names
if (names[name] === false) {
names[name] = true
return name
}
}
}