80 lines
3.1 KiB
JavaScript
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)
|
|
}
|
|
}
|