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) } }