cozy-games/packages/leaderboard/leaderboard-write.js

80 lines
3.1 KiB
JavaScript

import { buckets } from '@cozy-games/utils/date-bucket/date-bucket.js'
/**
* The WRITE surface of the leaderboard: submitting a completed game — the
* personal archive plus, if it qualifies, a ranked entry with denormalized
* day/week/month bucket keys. Importable WITHOUT any read/render code — no DOM,
* no list rendering, no `listScores` — so a consumer can wire writes to a
* separate, more-privileged backend instance (leaderboard-01) and pull in none
* of the read surface.
*
* The WRITE side calls `adapter.archive` (optional) and `adapter.addScore`; it
* also reads the ranking `config` once (an adapter call, not read/render code)
* to power the default qualifier.
*/
export class LeaderBoardWriter {
/**
* @param {Object} options
* @param {Object} options.adapter - storage backend; the WRITE side uses `addScore`, optional `archive`, and `getConfig`
* @param {(entry: Object) => boolean} [options.qualifies] - whether an entry is ranked; defaults to server passingStatus vs entry.status
*/
constructor(options = {}) {
this.adapter = options.adapter
this.qualifies = options.qualifies || (entry => this._defaultQualifies(entry))
// The default qualifier depends on the server's ranking config, so load it
// once. This is a config READ via the adapter — it pulls in no read/render
// module, keeping the write surface importable on its own.
Promise.resolve(this.adapter && this.adapter.getConfig ? this.adapter.getConfig() : undefined)
.then(config => { this.configuration = config })
.catch(() => {})
}
/**
* Default ranking gate: if the server config names a `passingStatus`, only
* entries whose `status` matches qualify; otherwise every entry qualifies.
*/
_defaultQualifies(entry) {
const passing = this.configuration && this.configuration.passingStatus
if (!passing) return true
return entry.status === passing
}
/**
* Submit a completed game. Always archives it (personal history); if it
* qualifies, also writes a ranked entry with denormalized bucket keys. Both
* writes go through the adapter, so the storage backend is pluggable. The
* caller owns display-name/nickname UX.
* @param {Object} entry - { name, playerId, score, category, time_stamp, status?, meta? }
*/
async submit(entry) {
if (this.adapter.archive) {
await this.adapter.archive({
playerId: entry.playerId,
score: entry.score,
category: entry.category,
time_stamp: entry.time_stamp,
meta: entry.meta
})
}
if (!this.qualifies(entry)) return
const stamp = entry.time_stamp instanceof Date ? entry.time_stamp : new Date(entry.time_stamp)
const bucket = buckets(stamp)
const scoreDoc = {
name: entry.name || 'Anonymous',
playerId: entry.playerId,
score: entry.score,
category: entry.category,
time_stamp: entry.time_stamp,
day: bucket.day,
week: bucket.week,
month: bucket.month
}
if (entry.meta) scoreDoc.meta = entry.meta
await this.adapter.addScore(entry.category, scoreDoc)
}
}