Merge pull request #45 from Legonzaur/main

Cache /player on backend for fastest response
This commit is contained in:
Legonzaur
2023-05-31 19:37:02 +02:00
committed by GitHub
5 changed files with 64 additions and 198 deletions
+10 -5
View File
@@ -3,6 +3,7 @@ import { param } from 'express-validator'
import { validateErrors } from '../common' import { validateErrors } from '../common'
import db, { getHostList } from '../db/db' import db, { getHostList } from '../db/db'
import { sql } from 'kysely' import { sql } from 'kysely'
import { allData } from '../process/process'
const { max, sum } = db.fn const { max, sum } = db.fn
const router = Router() const router = Router()
@@ -31,8 +32,8 @@ const path = {
interface KillRecord { interface KillRecord {
kills: number kills: number
deaths?: number deaths: number
deaths_while_equipped?: number deaths_while_equipped: number
username?: string username?: string
max_distance: number max_distance: number
total_distance: number total_distance: number
@@ -72,11 +73,11 @@ router.get('/players',
(req, res) => { (req, res) => {
void (async () => { void (async () => {
let result = db.selectFrom('kill_view') let result = db.selectFrom('kill_view')
if (req.query) { let data
if (Object.keys(req.query).length > 0) {
result = processQueryArgs(result, req.query as unknown as string | string[]) result = processQueryArgs(result, req.query as unknown as string | string[])
}
const selection = result.select([sum<number>('kills').as('kills'), sum<number>('deaths').as('deaths'), sum<number>('deaths_with_weapon').as('deaths_while_equipped'), 'attacker_id', sql<string>`last(attacker_name)`.as('username'), sum<number>('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy('attacker_id') const selection = result.select([sum<number>('kills').as('kills'), sum<number>('deaths').as('deaths'), sum<number>('deaths_with_weapon').as('deaths_while_equipped'), 'attacker_id', sql<string>`last(attacker_name)`.as('username'), sum<number>('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy('attacker_id')
const data = (await selection.execute()).reduce<Record<string, KillRecord>>((acc, curr) => { data = (await selection.execute()).reduce<Record<string, KillRecord>>((acc, curr) => {
acc[curr.attacker_id] = { acc[curr.attacker_id] = {
kills: Number(curr.kills), kills: Number(curr.kills),
deaths: Number(curr.deaths), deaths: Number(curr.deaths),
@@ -87,6 +88,10 @@ router.get('/players',
} }
return acc return acc
}, {}) }, {})
} else {
data = allData
}
const dataString = JSON.stringify(data) const dataString = JSON.stringify(data)
const buffer = Buffer.from(dataString) const buffer = Buffer.from(dataString)
const size = buffer.length const size = buffer.length
+4
View File
@@ -5,6 +5,8 @@ import express from 'express'
import cors from 'cors' import cors from 'cors'
import client from './client/client' import client from './client/client'
import { dbReady } from './db/db' import { dbReady } from './db/db'
import processAll from './process/process'
import listenKills from './process/onKill'
dotenv.config() dotenv.config()
const cCPUs = os.cpus().length const cCPUs = os.cpus().length
@@ -38,6 +40,8 @@ new Promise((resolve, reject) => {
app.use('/', client) app.use('/', client)
await dbReady() await dbReady()
await processAll()
await listenKills()
const listenServer = app.listen(port, '0.0.0.0', () => { const listenServer = app.listen(port, '0.0.0.0', () => {
console.log(`Tone client api listening on port ${port}`) console.log(`Tone client api listening on port ${port}`)
resolve(listenServer) resolve(listenServer)
+16 -72
View File
@@ -1,5 +1,5 @@
import { Client } from 'pg' import { Client } from 'pg'
// import { allData } from './process' import { allData } from './process'
export const pgClient = new Client({ export const pgClient = new Client({
host: process.env.POSTGRES_HOST, host: process.env.POSTGRES_HOST,
@@ -12,53 +12,23 @@ export default async function listenKills () {
await pgClient.connect() await pgClient.connect()
await pgClient.query('LISTEN new_kill') await pgClient.query('LISTEN new_kill')
/* pgClient.on('notification', (data) => { pgClient.on('notification', (data) => {
if (!data.payload) return if (!data.payload) return
const payload = JSON.parse(data.payload) const payload = JSON.parse(data.payload)
console.log(`recieved kill for ${payload.attacker_name as string}`)
let killEntry = allData[payload.attacker_id]
let deathEntry = allData[payload.victim_id]
let killEntry = allData.find(
(e) =>
e.attacker_id == payload.attacker_id &&
e.cause_of_death == payload.cause_of_death &&
e.game_mode == payload.game_mode &&
e.host == payload.host &&
e.map == payload.map &&
e.servername == payload.servername
)
let deathEntry = allData.find(
(e) =>
e.attacker_id == payload.victim_id &&
e.cause_of_death == payload.cause_of_death &&
e.game_mode == payload.game_mode &&
e.host == payload.host &&
e.map == payload.map &&
e.servername == payload.servername
)
let deathWithWeaponEntry = allData.find(
(e) =>
e.attacker_id == payload.victim_id &&
e.cause_of_death == payload.victim_current_weapon &&
e.game_mode == payload.game_mode &&
e.host == payload.host &&
e.map == payload.map &&
e.servername == payload.servername
)
if (!killEntry) { if (!killEntry) {
killEntry = { killEntry = {
kills: 0, kills: 0,
deaths: 0, deaths: 0,
max_distance: 0, max_distance: 0,
total_distance: 0, total_distance: 0,
deaths_with_weapon: 0, deaths_while_equipped: 0,
attacker_name: payload.attacker_name, username: payload.attacker_name
attacker_id: payload.attacker_id,
cause_of_death: payload.cause_of_death,
game_mode: payload.game_mode,
host: payload.host,
map: payload.map,
servername: payload.servername
} }
allData.push(killEntry) allData[payload.attacker_id] = killEntry
} }
if (!deathEntry) { if (!deathEntry) {
@@ -67,47 +37,21 @@ export default async function listenKills () {
deaths: 0, deaths: 0,
max_distance: 0, max_distance: 0,
total_distance: 0, total_distance: 0,
deaths_with_weapon: 0, deaths_while_equipped: 0,
attacker_name: payload.victim_name, username: payload.attacker_name
attacker_id: payload.victim_id,
cause_of_death: payload.cause_of_death,
game_mode: payload.game_mode,
host: payload.host,
map: payload.map,
servername: payload.servername
} }
allData.push(deathEntry) allData[payload.victim_id] = deathEntry
} }
if (!deathWithWeaponEntry) { if (payload.victim_id !== payload.attacker_id) {
deathWithWeaponEntry = {
kills: 0,
deaths: 0,
max_distance: 0,
total_distance: 0,
deaths_with_weapon: 0,
attacker_name: payload.victim_name,
attacker_id: payload.victim_id,
cause_of_death: payload.victim_current_weapon,
game_mode: payload.game_mode,
host: payload.host,
map: payload.map,
servername: payload.servername
}
allData.push(deathWithWeaponEntry)
}
if (payload.victim_id != payload.attacker_id) {
killEntry.kills++ killEntry.kills++
killEntry.total_distance = Number(killEntry.total_distance) + Number(payload.distance) killEntry.total_distance = Number(killEntry.total_distance) + Number(payload.distance)
killEntry.max_distance = Math.max(killEntry.max_distance || 0, payload.distance) killEntry.max_distance = Math.max(killEntry.max_distance || 0, payload.distance)
} }
deathEntry.deaths++ deathEntry.deaths++
deathEntry.attacker_name = payload.victim_name deathEntry.deaths_while_equipped++
deathWithWeaponEntry.deaths_with_weapon++ deathEntry.username = payload.victim_name
deathWithWeaponEntry.attacker_name = payload.victim_name killEntry.username = payload.attacker_name
killEntry.attacker_name = payload.attacker_name })
}) */
return pgClient return pgClient
} }
+22 -110
View File
@@ -1,6 +1,6 @@
import db from '../db/db' import db from '../db/db'
import { sql } from 'kysely' import { sql } from 'kysely'
const { count, max, sum, coalesce } = db.fn const { max, sum } = db.fn
export let allData: Awaited<ReturnType<typeof processGlobalStats>> export let allData: Awaited<ReturnType<typeof processGlobalStats>>
@@ -12,119 +12,31 @@ async function processAll (): Promise<void> {
console.log('Starting data calculation...') console.log('Starting data calculation...')
const timeStart = new Date() const timeStart = new Date()
// allData = await processGlobalStats() allData = await processGlobalStats()
console.log(`Data calculation finished. Took + ${Math.abs(new Date().getTime() - timeStart.getTime()) / 1000} seconds`) console.log(`Data calculation finished. Took + ${Math.abs(new Date().getTime() - timeStart.getTime()) / 1000} seconds`)
} }
interface KillRecord {
kills: number
deaths: number
deaths_while_equipped: number
username?: string
max_distance: number
total_distance: number
}
async function processGlobalStats () { async function processGlobalStats () {
return await db const data = await db.selectFrom('kill_view').select(['attacker_id', sql<string>`last(attacker_name)`.as('username'), sum('kills').as('kills'), sum('deaths').as('deaths'), sum('deaths_with_weapon').as('deaths_while_equipped'), sum('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy('attacker_id').execute()
.with('kills', (db) => return data.reduce<Record<string, KillRecord>>((acc, curr) => {
db acc[curr.attacker_id] = {
.selectFrom('kill') kills: Number(curr.kills),
.select([ deaths: Number(curr.deaths),
count<number>('id').as('kills'), max_distance: Number(curr.max_distance),
coalesce( total_distance: Number(curr.total_distance),
max<number | null, 'distance'>('distance'), deaths_while_equipped: Number(curr.deaths_while_equipped)
sql<number>`0` }
).as('max_distance'), return acc
coalesce(sum<number | null>('distance'), sql<number>`0`).as( }, {})
'total_distance'
),
'attacker_id',
sql<string>`last(attacker_name)`.as('attacker_name'),
'cause_of_death',
'map',
'game_mode',
'servername',
'host'
])
.whereRef('attacker_id', '!=', 'victim_id')
.groupBy([
'attacker_id',
'cause_of_death',
'map',
'servername',
'host',
'game_mode'
])
)
.with('deaths', (db) =>
db
.selectFrom('kill')
.select([
count<number>('id').as('deaths'),
'victim_id',
sql<string>`last(victim_name)`.as('victim_name'),
'cause_of_death',
'map',
'game_mode',
'servername',
'host'
])
.groupBy([
'victim_id',
'cause_of_death',
'map',
'servername',
'host',
'game_mode'
])
).with('deathswithweapon', (db) =>
db
.selectFrom('kill')
.select([
count<number>('id').as('deaths'),
'victim_id',
'victim_current_weapon',
'map',
'game_mode',
'servername',
'host'
])
.groupBy([
'victim_id',
'victim_current_weapon',
'map',
'servername',
'host',
'game_mode'
])
)
.selectFrom('kills')
.fullJoin('deaths', (join) =>
join
.onRef('deaths.victim_id', '=', 'kills.attacker_id')
.onRef('deaths.cause_of_death', '=', 'kills.cause_of_death')
.onRef('deaths.servername', '=', 'kills.servername')
.onRef('deaths.host', '=', 'kills.host')
.onRef('deaths.game_mode', '=', 'kills.game_mode')
.onRef('deaths.map', '=', 'kills.map')
)
.leftJoin('deathswithweapon', (join) =>
join
.onRef('deathswithweapon.victim_id', '=', 'kills.attacker_id')
.onRef('deathswithweapon.victim_current_weapon', '=', 'kills.cause_of_death')
.onRef('deathswithweapon.servername', '=', 'kills.servername')
.onRef('deathswithweapon.host', '=', 'kills.host')
.onRef('deathswithweapon.game_mode', '=', 'kills.game_mode')
.onRef('deathswithweapon.map', '=', 'kills.map')
)
// .selectAll('kills')
.select([
sql<string>`COALESCE(kills.attacker_name, deaths.victim_name)`.as('attacker_name'),
sql<string>`COALESCE(kills.attacker_id, deaths.victim_id)`.as('attacker_id'),
sql<number>`COALESCE(kills.kills, 0)`.as('kills'),
sql<number>`COALESCE(kills.total_distance, 0)`.as('total_distance'),
sql<number>`COALESCE(deathswithweapon.deaths, 0)`.as('deaths_with_weapon'),
sql<number>`COALESCE(kills.max_distance, 0)`.as('max_distance'),
sql<number>`COALESCE(deaths.deaths, 0)`.as('deaths'),
sql<string>`COALESCE(kills.servername, deaths.servername)`.as('servername'),
sql<number>`COALESCE(kills.host, deaths.host)`.as('host'),
sql<string>`COALESCE(kills.cause_of_death, deaths.cause_of_death)`.as('cause_of_death'),
sql<string>`COALESCE(kills.map, deaths.map)`.as('map'),
sql<string>`COALESCE(kills.game_mode, deaths.game_mode)`.as('game_mode')
])
.execute()
} }
export default processAll export default processAll
+2 -1
View File
@@ -112,7 +112,6 @@ describe('realtime', () => {
test('update player', async () => { test('update player', async () => {
jest.setTimeout(15000) jest.setTimeout(15000)
const yea = waitFor(1000)
const response = await fetch(`http://127.0.0.1:3001/kill`, { const response = await fetch(`http://127.0.0.1:3001/kill`, {
method: "POST", // *GET, POST, PUT, DELETE, etc. method: "POST", // *GET, POST, PUT, DELETE, etc.
credentials: "same-origin", // include, *same-origin, omit credentials: "same-origin", // include, *same-origin, omit
@@ -123,10 +122,12 @@ describe('realtime', () => {
body: JSON.stringify(data), // body data type must match "Content-Type" header body: JSON.stringify(data), // body data type must match "Content-Type" header
}); });
expect(response.status).toBe(201) expect(response.status).toBe(201)
const yea = waitFor(1000)
await yea await yea
}) })
test('check player update', async () => { test('check player update', async () => {
const request = await fetch("http://127.0.0.1:3000/players") const request = await fetch("http://127.0.0.1:3000/players")
const data = await request.json() const data = await request.json()
const player = data["1"] const player = data["1"]