@pithy-sh/leaderboard 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board/registry.d.ts +24 -0
- package/dist/board/registry.d.ts.map +1 -0
- package/dist/board/registry.js +54 -0
- package/dist/capability.d.ts +45 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +75 -0
- package/dist/config/boardKey.d.ts +16 -0
- package/dist/config/boardKey.d.ts.map +1 -0
- package/dist/config/boardKey.js +18 -0
- package/dist/config/config.d.ts +98 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +123 -0
- package/dist/data/boardRecord.d.ts +38 -0
- package/dist/data/boardRecord.d.ts.map +1 -0
- package/dist/data/boardRecord.js +33 -0
- package/dist/data/entry.d.ts +31 -0
- package/dist/data/entry.d.ts.map +1 -0
- package/dist/data/entry.js +32 -0
- package/dist/data/lock.d.ts +20 -0
- package/dist/data/lock.d.ts.map +1 -0
- package/dist/data/lock.js +21 -0
- package/dist/data/tables.d.ts +27 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tables.js +31 -0
- package/dist/entry/store.d.ts +42 -0
- package/dist/entry/store.d.ts.map +1 -0
- package/dist/entry/store.js +86 -0
- package/dist/error/errors.d.ts +56 -0
- package/dist/error/errors.d.ts.map +1 -0
- package/dist/error/errors.js +78 -0
- package/dist/http/guard.d.ts +26 -0
- package/dist/http/guard.d.ts.map +1 -0
- package/dist/http/guard.js +50 -0
- package/dist/http/handlers.d.ts +81 -0
- package/dist/http/handlers.d.ts.map +1 -0
- package/dist/http/handlers.js +123 -0
- package/dist/http/routes.d.ts +48 -0
- package/dist/http/routes.d.ts.map +1 -0
- package/dist/http/routes.js +62 -0
- package/dist/http/schemas.d.ts +47 -0
- package/dist/http/schemas.d.ts.map +1 -0
- package/dist/http/schemas.js +38 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/migrations/0001_entries.d.ts +10 -0
- package/dist/migrations/0001_entries.d.ts.map +1 -0
- package/dist/migrations/0001_entries.js +35 -0
- package/dist/rank/lock.d.ts +39 -0
- package/dist/rank/lock.d.ts.map +1 -0
- package/dist/rank/lock.js +56 -0
- package/dist/rank/materialize.d.ts +97 -0
- package/dist/rank/materialize.d.ts.map +1 -0
- package/dist/rank/materialize.js +142 -0
- package/dist/rank/query.d.ts +52 -0
- package/dist/rank/query.d.ts.map +1 -0
- package/dist/rank/query.js +142 -0
- package/dist/rank/retryPolicy.d.ts +35 -0
- package/dist/rank/retryPolicy.d.ts.map +1 -0
- package/dist/rank/retryPolicy.js +39 -0
- package/dist/rank/segment.d.ts +35 -0
- package/dist/rank/segment.d.ts.map +1 -0
- package/dist/rank/segment.js +37 -0
- package/dist/rank/tiers.d.ts +15 -0
- package/dist/rank/tiers.d.ts.map +1 -0
- package/dist/rank/tiers.js +24 -0
- package/dist/rank/worker.d.ts +61 -0
- package/dist/rank/worker.d.ts.map +1 -0
- package/dist/rank/worker.entry.d.ts +59 -0
- package/dist/rank/worker.entry.d.ts.map +1 -0
- package/dist/rank/worker.entry.js +65 -0
- package/dist/rank/worker.js +54 -0
- package/dist/retention/prune.d.ts +41 -0
- package/dist/retention/prune.d.ts.map +1 -0
- package/dist/retention/prune.js +67 -0
- package/dist/seeds/example.d.ts +13 -0
- package/dist/seeds/example.d.ts.map +1 -0
- package/dist/seeds/example.js +69 -0
- package/dist/session/bookmark.d.ts +49 -0
- package/dist/session/bookmark.d.ts.map +1 -0
- package/dist/session/bookmark.js +43 -0
- package/dist/version.generated.d.ts +7 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +9 -0
- package/dist/window/schedule.d.ts +31 -0
- package/dist/window/schedule.d.ts.map +1 -0
- package/dist/window/schedule.js +108 -0
- package/package.json +21 -10
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* A single-row advisory lock in `pithy_leaderboard_locks` — the row in the table, one per lock name.
|
|
6
|
+
*
|
|
7
|
+
* It serializes the rank-refresh Workflow: at most one instance holds the `rank-refresh` lock at a time,
|
|
8
|
+
* so two overlapping cron fires can never interleave their chunked rank writes into an incoherent set.
|
|
9
|
+
*
|
|
10
|
+
* `z.output` is the app shape (a Date); `z.input` is the SQLite row (ms-epoch). Conversion runs through
|
|
11
|
+
* the core codec, per the round-trip rule.
|
|
12
|
+
*/
|
|
13
|
+
export declare const LeaderboardLock: z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
holder: z.ZodString;
|
|
16
|
+
acquiredAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type LeaderboardLock = z.output<typeof LeaderboardLock>;
|
|
19
|
+
export type LeaderboardLockRow = z.input<typeof LeaderboardLock>;
|
|
20
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/data/lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;iBAQkC,CAAC;AAC/D,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAC/D,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { SQLiteDate } from "@pithy-sh/core/src/data/codecs";
|
|
5
|
+
//#region src/data/lock.ts
|
|
6
|
+
/**
|
|
7
|
+
* A single-row advisory lock in `pithy_leaderboard_locks` — the row in the table, one per lock name.
|
|
8
|
+
*
|
|
9
|
+
* It serializes the rank-refresh Workflow: at most one instance holds the `rank-refresh` lock at a time,
|
|
10
|
+
* so two overlapping cron fires can never interleave their chunked rank writes into an incoherent set.
|
|
11
|
+
*
|
|
12
|
+
* `z.output` is the app shape (a Date); `z.input` is the SQLite row (ms-epoch). Conversion runs through
|
|
13
|
+
* the core codec, per the round-trip rule.
|
|
14
|
+
*/
|
|
15
|
+
const LeaderboardLock = z.object({
|
|
16
|
+
name: z.string().describe("The lock's identity — `rank-refresh` for the refresh pass. One row per name."),
|
|
17
|
+
holder: z.string().describe("A token unique to the instance currently holding the lock."),
|
|
18
|
+
acquiredAt: SQLiteDate.describe("When the current holder took the lock. A lock older than the stale horizon is treated as abandoned by a crashed instance and may be reclaimed.")
|
|
19
|
+
}).describe("One advisory lock in `pithy_leaderboard_locks`.");
|
|
20
|
+
//#endregion
|
|
21
|
+
export { LeaderboardLock };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
4
|
+
import { type DatabaseSchema } from "@pithy-sh/core/src/data/db";
|
|
5
|
+
import type { Kysely } from "kysely";
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import { LeaderboardBoardRecord } from "./boardRecord";
|
|
8
|
+
import { LeaderboardEntry } from "./entry";
|
|
9
|
+
import { LeaderboardLock } from "./lock";
|
|
10
|
+
/** The entry table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_entries` in the DDL. */
|
|
11
|
+
export declare const LEADERBOARD_ENTRIES_TABLE = "pithyLeaderboardEntries";
|
|
12
|
+
/** The board drift-guard table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_boards`. */
|
|
13
|
+
export declare const LEADERBOARD_BOARDS_TABLE = "pithyLeaderboardBoards";
|
|
14
|
+
/** The advisory-lock table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_locks`. */
|
|
15
|
+
export declare const LEADERBOARD_LOCKS_TABLE = "pithyLeaderboardLocks";
|
|
16
|
+
/** The leaderboard tables map. All are always present — none is behind a config flag. */
|
|
17
|
+
export declare function leaderboardTables(): Record<string, z.ZodObject>;
|
|
18
|
+
/** The typed Kysely database over the leaderboard tables. */
|
|
19
|
+
export type LeaderboardTables = {
|
|
20
|
+
[LEADERBOARD_ENTRIES_TABLE]: typeof LeaderboardEntry;
|
|
21
|
+
[LEADERBOARD_BOARDS_TABLE]: typeof LeaderboardBoardRecord;
|
|
22
|
+
[LEADERBOARD_LOCKS_TABLE]: typeof LeaderboardLock;
|
|
23
|
+
};
|
|
24
|
+
export type LeaderboardDatabase = Kysely<DatabaseSchema<LeaderboardTables>>;
|
|
25
|
+
/** Build the leaderboard database from the `DB` binding (CamelCasePlugin installed). */
|
|
26
|
+
export declare function leaderboardDatabase(d1: D1Database): LeaderboardDatabase;
|
|
27
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/data/tables.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAEzC,mGAAmG;AACnG,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AACnE,mGAAmG;AACnG,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,8FAA8F;AAC9F,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,yFAAyF;AACzF,wBAAgB,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAM/D;AAED,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,CAAC,yBAAyB,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACrD,CAAC,wBAAwB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAC1D,CAAC,uBAAuB,CAAC,EAAE,OAAO,eAAe,CAAC;CACnD,CAAC;AACF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAE5E,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,UAAU,GAAG,mBAAmB,CAMvE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { LeaderboardBoardRecord } from "./boardRecord.js";
|
|
4
|
+
import { LeaderboardEntry } from "./entry.js";
|
|
5
|
+
import { LeaderboardLock } from "./lock.js";
|
|
6
|
+
import { createDatabase } from "@pithy-sh/core/src/data/db";
|
|
7
|
+
//#region src/data/tables.ts
|
|
8
|
+
/** The entry table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_entries` in the DDL. */
|
|
9
|
+
const LEADERBOARD_ENTRIES_TABLE = "pithyLeaderboardEntries";
|
|
10
|
+
/** The board drift-guard table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_boards`. */
|
|
11
|
+
const LEADERBOARD_BOARDS_TABLE = "pithyLeaderboardBoards";
|
|
12
|
+
/** The advisory-lock table. `CamelCasePlugin` snake-cases it to `pithy_leaderboard_locks`. */
|
|
13
|
+
const LEADERBOARD_LOCKS_TABLE = "pithyLeaderboardLocks";
|
|
14
|
+
/** The leaderboard tables map. All are always present — none is behind a config flag. */
|
|
15
|
+
function leaderboardTables() {
|
|
16
|
+
return {
|
|
17
|
+
[LEADERBOARD_ENTRIES_TABLE]: LeaderboardEntry,
|
|
18
|
+
[LEADERBOARD_BOARDS_TABLE]: LeaderboardBoardRecord,
|
|
19
|
+
[LEADERBOARD_LOCKS_TABLE]: LeaderboardLock
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Build the leaderboard database from the `DB` binding (CamelCasePlugin installed). */
|
|
23
|
+
function leaderboardDatabase(d1) {
|
|
24
|
+
return createDatabase(d1, {
|
|
25
|
+
[LEADERBOARD_ENTRIES_TABLE]: LeaderboardEntry,
|
|
26
|
+
[LEADERBOARD_BOARDS_TABLE]: LeaderboardBoardRecord,
|
|
27
|
+
[LEADERBOARD_LOCKS_TABLE]: LeaderboardLock
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { LEADERBOARD_BOARDS_TABLE, LEADERBOARD_ENTRIES_TABLE, LEADERBOARD_LOCKS_TABLE, leaderboardDatabase, leaderboardTables };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { LeaderboardBoard } from "../config/config";
|
|
4
|
+
import { LeaderboardEntry } from "../data/entry";
|
|
5
|
+
import { type LeaderboardDatabase } from "../data/tables";
|
|
6
|
+
/**
|
|
7
|
+
* Reads and writes over `pithy_leaderboard_entries`.
|
|
8
|
+
*
|
|
9
|
+
* Every write is one `INSERT … ON CONFLICT DO UPDATE` against the `(boardId, windowKey, userId)` unique
|
|
10
|
+
* index. Folding the aggregation into the conflict clause — rather than reading the row, deciding in JS,
|
|
11
|
+
* and writing it back — is what keeps a submission a single statement: D1 is single-threaded, so a
|
|
12
|
+
* read-then-write would both double the round trips and open a race where two submissions from the same
|
|
13
|
+
* player interleave and one silently wins.
|
|
14
|
+
*
|
|
15
|
+
* Round-trip rule (CLAUDE.md §Data layer): read with `LeaderboardEntry.parse` (decode), write with
|
|
16
|
+
* `LeaderboardEntry.encode` (encode). No raw `0/1` or epoch numbers below.
|
|
17
|
+
*/
|
|
18
|
+
export interface EntryStore {
|
|
19
|
+
/** Fold a submission into the player's entry for this window, per the board's aggregation. */
|
|
20
|
+
submit(board: LeaderboardBoard, windowKey: string, userId: string, score: number, at: Date, visibleByDefault: boolean): Promise<void>;
|
|
21
|
+
/** The player's entry for this window, or undefined if they have never submitted. */
|
|
22
|
+
get(boardId: string, windowKey: string, userId: string): Promise<LeaderboardEntry | undefined>;
|
|
23
|
+
/** Set the player's own consent to appear. False if they have no entry. */
|
|
24
|
+
setVisibility(boardId: string, windowKey: string, userId: string, visible: boolean): Promise<boolean>;
|
|
25
|
+
/** Hide or unhide an entry as a moderator. False if they have no entry. */
|
|
26
|
+
hide(boardId: string, windowKey: string, userId: string, hidden: boolean): Promise<boolean>;
|
|
27
|
+
/** Delete an entry outright. False if they have no entry. */
|
|
28
|
+
remove(boardId: string, windowKey: string, userId: string): Promise<boolean>;
|
|
29
|
+
/** Delete entries on this board in windows chronologically before `cutoffWindow`. Returns rows deleted. */
|
|
30
|
+
pruneWindowsBefore(boardId: string, cutoffWindow: string): Promise<number>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The submission upsert, as a compilable query.
|
|
34
|
+
*
|
|
35
|
+
* Exported so `writeAmplification.workers.test.ts` can compile the real statement and read D1's own
|
|
36
|
+
* `meta.rows_written` for it. The cost model in `scripts/costModel.ts` assumes a fixed number of rows
|
|
37
|
+
* written per submission, and that assumption is load-bearing for every figure in docs/costs.md — a test
|
|
38
|
+
* that measured a hand-written approximation of this SQL would only prove the approximation's cost.
|
|
39
|
+
*/
|
|
40
|
+
export declare function submitQuery(db: LeaderboardDatabase, board: LeaderboardBoard, windowKey: string, userId: string, score: number, at: Date, visibleByDefault: boolean): import("kysely").InsertQueryBuilder<import("@pithy-sh/core/src/data/db").DatabaseSchema<import("../data/tables").LeaderboardTables>, "pithyLeaderboardEntries", import("kysely").InsertResult>;
|
|
41
|
+
export declare function entryStore(db: LeaderboardDatabase): EntryStore;
|
|
42
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/entry/store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErF;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,8FAA8F;IAC9F,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,IAAI,EACR,gBAAgB,EAAE,OAAO,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,qFAAqF;IACrF,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;IAC/F,2EAA2E;IAC3E,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtG,2EAA2E;IAC3E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5F,6DAA6D;IAC7D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7E,2GAA2G;IAC3G,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,mBAAmB,EACvB,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,IAAI,EACR,gBAAgB,EAAE,OAAO,kMAgE1B;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,mBAAmB,GAAG,UAAU,CA4D9D"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { LeaderboardEntry } from "../data/entry.js";
|
|
4
|
+
import { LEADERBOARD_ENTRIES_TABLE } from "../data/tables.js";
|
|
5
|
+
import { sql } from "kysely";
|
|
6
|
+
//#region src/entry/store.ts
|
|
7
|
+
/**
|
|
8
|
+
* The submission upsert, as a compilable query.
|
|
9
|
+
*
|
|
10
|
+
* Exported so `writeAmplification.workers.test.ts` can compile the real statement and read D1's own
|
|
11
|
+
* `meta.rows_written` for it. The cost model in `scripts/costModel.ts` assumes a fixed number of rows
|
|
12
|
+
* written per submission, and that assumption is load-bearing for every figure in docs/costs.md — a test
|
|
13
|
+
* that measured a hand-written approximation of this SQL would only prove the approximation's cost.
|
|
14
|
+
*/
|
|
15
|
+
function submitQuery(db, board, windowKey, userId, score, at, visibleByDefault) {
|
|
16
|
+
const insert = LeaderboardEntry.encode({
|
|
17
|
+
id: 0,
|
|
18
|
+
boardId: board.key,
|
|
19
|
+
windowKey,
|
|
20
|
+
userId,
|
|
21
|
+
score,
|
|
22
|
+
achievedAt: at,
|
|
23
|
+
submittedAt: at,
|
|
24
|
+
visible: visibleByDefault,
|
|
25
|
+
hidden: false,
|
|
26
|
+
rank: null
|
|
27
|
+
});
|
|
28
|
+
delete insert.id;
|
|
29
|
+
const entries = sql.table(LEADERBOARD_ENTRIES_TABLE);
|
|
30
|
+
const improves = board.direction === "desc" ? sql`excluded.score > ${entries}.score` : sql`excluded.score < ${entries}.score`;
|
|
31
|
+
const guardNoOps = board.aggregation === "best" && !board.trackActivity;
|
|
32
|
+
return db.insertInto(LEADERBOARD_ENTRIES_TABLE).values(insert).onConflict((oc) => {
|
|
33
|
+
const updated = oc.columns([
|
|
34
|
+
"boardId",
|
|
35
|
+
"windowKey",
|
|
36
|
+
"userId"
|
|
37
|
+
]).doUpdateSet(() => {
|
|
38
|
+
const submittedAt = sql`excluded.submitted_at`;
|
|
39
|
+
if (board.aggregation === "latest") return {
|
|
40
|
+
submittedAt,
|
|
41
|
+
score: sql`excluded.score`,
|
|
42
|
+
achievedAt: sql`excluded.achieved_at`
|
|
43
|
+
};
|
|
44
|
+
if (board.aggregation === "sum") return {
|
|
45
|
+
submittedAt,
|
|
46
|
+
score: sql`${entries}.score + excluded.score`,
|
|
47
|
+
achievedAt: sql`excluded.achieved_at`
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
submittedAt,
|
|
51
|
+
score: sql`CASE WHEN ${improves} THEN excluded.score ELSE ${entries}.score END`,
|
|
52
|
+
achievedAt: sql`CASE WHEN ${improves} THEN excluded.achieved_at ELSE ${entries}.achieved_at END`
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return guardNoOps ? updated.where(improves) : updated;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function entryStore(db) {
|
|
59
|
+
return {
|
|
60
|
+
async submit(board, windowKey, userId, score, at, visibleByDefault) {
|
|
61
|
+
await submitQuery(db, board, windowKey, userId, score, at, visibleByDefault).execute();
|
|
62
|
+
},
|
|
63
|
+
async get(boardId, windowKey, userId) {
|
|
64
|
+
const row = await db.selectFrom(LEADERBOARD_ENTRIES_TABLE).selectAll().where("boardId", "=", boardId).where("windowKey", "=", windowKey).where("userId", "=", userId).executeTakeFirst();
|
|
65
|
+
return row ? LeaderboardEntry.parse(row) : void 0;
|
|
66
|
+
},
|
|
67
|
+
async setVisibility(boardId, windowKey, userId, visible) {
|
|
68
|
+
const result = await db.updateTable(LEADERBOARD_ENTRIES_TABLE).set({ visible: LeaderboardEntry.shape.visible.encode(visible) }).where("boardId", "=", boardId).where("windowKey", "=", windowKey).where("userId", "=", userId).executeTakeFirst();
|
|
69
|
+
return Number(result.numUpdatedRows) > 0;
|
|
70
|
+
},
|
|
71
|
+
async hide(boardId, windowKey, userId, hidden) {
|
|
72
|
+
const result = await db.updateTable(LEADERBOARD_ENTRIES_TABLE).set({ hidden: LeaderboardEntry.shape.hidden.encode(hidden) }).where("boardId", "=", boardId).where("windowKey", "=", windowKey).where("userId", "=", userId).executeTakeFirst();
|
|
73
|
+
return Number(result.numUpdatedRows) > 0;
|
|
74
|
+
},
|
|
75
|
+
async remove(boardId, windowKey, userId) {
|
|
76
|
+
const result = await db.deleteFrom(LEADERBOARD_ENTRIES_TABLE).where("boardId", "=", boardId).where("windowKey", "=", windowKey).where("userId", "=", userId).executeTakeFirst();
|
|
77
|
+
return Number(result.numDeletedRows) > 0;
|
|
78
|
+
},
|
|
79
|
+
async pruneWindowsBefore(boardId, cutoffWindow) {
|
|
80
|
+
const result = await db.deleteFrom(LEADERBOARD_ENTRIES_TABLE).where("boardId", "=", boardId).where("windowKey", "<", cutoffWindow).executeTakeFirst();
|
|
81
|
+
return Number(result.numDeletedRows);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { entryStore, submitQuery };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
4
|
+
import type { MessageParams } from "@pithy-sh/core/src/i18n/catalog";
|
|
5
|
+
/**
|
|
6
|
+
* `@pithy-sh/leaderboard` throw sugar. The `leaderboard/*` codes live in core's closed `KitErrorPayload`
|
|
7
|
+
* union (CLAUDE.md §Errors: capabilities add their codes to the one union); these subclasses are the
|
|
8
|
+
* package-local vehicles that set one of those members — the same pattern as `@pithy-sh/media` and
|
|
9
|
+
* `@pithy-sh/turnstile`. Runtime code in this package throws one of these, never a plain `new Error`.
|
|
10
|
+
*/
|
|
11
|
+
/** Variable parts each subclass accepts; `code`/`status` are fixed by the subclass. */
|
|
12
|
+
interface LeaderboardErrorArgs {
|
|
13
|
+
/** Override the public, safe-to-expose message. */
|
|
14
|
+
message?: string;
|
|
15
|
+
/** A remediation hint (CLI action line). */
|
|
16
|
+
action?: string;
|
|
17
|
+
/** Internal context for logs + audit. Never serialized to clients. */
|
|
18
|
+
detail?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Values a translating client interpolates into its own wording for this code. Client-facing, so —
|
|
21
|
+
* unlike `action` and `detail` — these cross the boundary with `message`.
|
|
22
|
+
*/
|
|
23
|
+
params?: MessageParams;
|
|
24
|
+
}
|
|
25
|
+
export declare class LeaderboardBoardNotFoundError extends PithyError {
|
|
26
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export declare class LeaderboardEntryNotFoundError extends PithyError {
|
|
31
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
32
|
+
cause?: unknown;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export declare class LeaderboardScoreRejectedError extends PithyError {
|
|
36
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export declare class LeaderboardSubmitForbiddenError extends PithyError {
|
|
41
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export declare class LeaderboardBoardImmutableError extends PithyError {
|
|
46
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
47
|
+
cause?: unknown;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export declare class LeaderboardInvalidScheduleError extends PithyError {
|
|
51
|
+
constructor(args?: LeaderboardErrorArgs, options?: {
|
|
52
|
+
cause?: unknown;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/error/errors.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE;;;;;GAKG;AAEH,uFAAuF;AACvF,UAAU,oBAAoB;IAC5B,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,qBAAa,6BAA8B,SAAQ,UAAU;IAC3D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYzE;CACF;AAED,qBAAa,6BAA8B,SAAQ,UAAU;IAC3D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYzE;CACF;AAED,qBAAa,6BAA8B,SAAQ,UAAU;IAC3D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYzE;CACF;AAED,qBAAa,+BAAgC,SAAQ,UAAU;IAC7D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAczE;CACF;AAED,qBAAa,8BAA+B,SAAQ,UAAU;IAC5D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYzE;CACF;AAED,qBAAa,+BAAgC,SAAQ,UAAU;IAC7D,YAAY,IAAI,GAAE,oBAAyB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAYzE;CACF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
4
|
+
//#region src/error/errors.ts
|
|
5
|
+
var LeaderboardBoardNotFoundError = class extends PithyError {
|
|
6
|
+
constructor(args = {}, options) {
|
|
7
|
+
super({
|
|
8
|
+
code: "leaderboard/board_not_found",
|
|
9
|
+
status: 404,
|
|
10
|
+
message: args.message ?? "That board does not exist.",
|
|
11
|
+
action: args.action ?? "Check the board key against the `boards` list in pithy.config.ts.",
|
|
12
|
+
detail: args.detail,
|
|
13
|
+
params: args.params
|
|
14
|
+
}, options);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var LeaderboardEntryNotFoundError = class extends PithyError {
|
|
18
|
+
constructor(args = {}, options) {
|
|
19
|
+
super({
|
|
20
|
+
code: "leaderboard/entry_not_found",
|
|
21
|
+
status: 404,
|
|
22
|
+
message: args.message ?? "You have no score on that board yet.",
|
|
23
|
+
action: args.action ?? "Submit a score first.",
|
|
24
|
+
detail: args.detail,
|
|
25
|
+
params: args.params
|
|
26
|
+
}, options);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var LeaderboardScoreRejectedError = class extends PithyError {
|
|
30
|
+
constructor(args = {}, options) {
|
|
31
|
+
super({
|
|
32
|
+
code: "leaderboard/score_rejected",
|
|
33
|
+
status: 400,
|
|
34
|
+
message: args.message ?? "That score is outside the board's allowed range.",
|
|
35
|
+
action: args.action ?? "Submit a score within the board's min and max.",
|
|
36
|
+
detail: args.detail,
|
|
37
|
+
params: args.params
|
|
38
|
+
}, options);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var LeaderboardSubmitForbiddenError = class extends PithyError {
|
|
42
|
+
constructor(args = {}, options) {
|
|
43
|
+
super({
|
|
44
|
+
code: "leaderboard/submit_forbidden",
|
|
45
|
+
status: 403,
|
|
46
|
+
message: args.message ?? "This session may not submit scores.",
|
|
47
|
+
action: args.action ?? "Submit from your trusted server with the board's submit scope, or set `serverAuthoritative: false` to let clients post directly.",
|
|
48
|
+
detail: args.detail,
|
|
49
|
+
params: args.params
|
|
50
|
+
}, options);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var LeaderboardBoardImmutableError = class extends PithyError {
|
|
54
|
+
constructor(args = {}, options) {
|
|
55
|
+
super({
|
|
56
|
+
code: "leaderboard/board_immutable",
|
|
57
|
+
status: 409,
|
|
58
|
+
message: args.message ?? "That board's definition cannot change.",
|
|
59
|
+
action: args.action ?? "Give the changed board a new key. The old board keeps its recorded scores.",
|
|
60
|
+
detail: args.detail,
|
|
61
|
+
params: args.params
|
|
62
|
+
}, options);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var LeaderboardInvalidScheduleError = class extends PithyError {
|
|
66
|
+
constructor(args = {}, options) {
|
|
67
|
+
super({
|
|
68
|
+
code: "leaderboard/invalid_schedule",
|
|
69
|
+
status: 500,
|
|
70
|
+
message: args.message ?? "That board's window schedule is invalid.",
|
|
71
|
+
action: args.action ?? "Fix the board's `window` CRON expression in pithy.config.ts.",
|
|
72
|
+
detail: args.detail,
|
|
73
|
+
params: args.params
|
|
74
|
+
}, options);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
export { LeaderboardBoardImmutableError, LeaderboardBoardNotFoundError, LeaderboardEntryNotFoundError, LeaderboardInvalidScheduleError, LeaderboardScoreRejectedError, LeaderboardSubmitForbiddenError };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
4
|
+
import type { MiddlewareHandler } from "hono";
|
|
5
|
+
/**
|
|
6
|
+
* Route guards.
|
|
7
|
+
*
|
|
8
|
+
* These depend on the core `AuthContext` seam and nothing else — `@pithy-sh/auth` populates `c.var.auth`,
|
|
9
|
+
* and leaderboard never reaches into its internals (CLAUDE.md §HTTP). Without auth installed `c.var.auth`
|
|
10
|
+
* is null and every route below is denied, which is the correct failure: an entry with no stable player
|
|
11
|
+
* id cannot be keyed, upserted, or rate-limited, so an unauthenticated board is not a degraded board but
|
|
12
|
+
* an append log of unattributable scores.
|
|
13
|
+
*/
|
|
14
|
+
export declare function requireAuth(): MiddlewareHandler<PithyHonoEnv>;
|
|
15
|
+
/**
|
|
16
|
+
* Require the board's submit scope while `serverAuthoritative` is on.
|
|
17
|
+
*
|
|
18
|
+
* Every vendor that offers server-authoritative writes ships it **off**; Pithy ships it **on**. The scope
|
|
19
|
+
* is the seam: mint it for your own trusted server's token and never for a player's, and a device cannot
|
|
20
|
+
* post a score it invented. Turning `serverAuthoritative` off removes this gate entirely — which is the
|
|
21
|
+
* vendor default, and is why it is not ours.
|
|
22
|
+
*/
|
|
23
|
+
export declare function requireSubmitScope(serverAuthoritative: boolean, scope: string): MiddlewareHandler<PithyHonoEnv>;
|
|
24
|
+
/** Require the admin scope for moderation — hiding or removing another player's entry. */
|
|
25
|
+
export declare function requireAdminScope(scope: string): MiddlewareHandler<PithyHonoEnv>;
|
|
26
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/http/guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAE7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAG9C;;;;;;;;GAQG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAU7D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAS/G;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAWhF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { LeaderboardSubmitForbiddenError } from "../error/errors.js";
|
|
4
|
+
import { ForbiddenError, UnauthorizedError } from "@pithy-sh/core/src/error/pithyError";
|
|
5
|
+
//#region src/http/guard.ts
|
|
6
|
+
/**
|
|
7
|
+
* Route guards.
|
|
8
|
+
*
|
|
9
|
+
* These depend on the core `AuthContext` seam and nothing else — `@pithy-sh/auth` populates `c.var.auth`,
|
|
10
|
+
* and leaderboard never reaches into its internals (CLAUDE.md §HTTP). Without auth installed `c.var.auth`
|
|
11
|
+
* is null and every route below is denied, which is the correct failure: an entry with no stable player
|
|
12
|
+
* id cannot be keyed, upserted, or rate-limited, so an unauthenticated board is not a degraded board but
|
|
13
|
+
* an append log of unattributable scores.
|
|
14
|
+
*/
|
|
15
|
+
function requireAuth() {
|
|
16
|
+
return async (c, next) => {
|
|
17
|
+
if (!c.var.auth) throw new UnauthorizedError({
|
|
18
|
+
message: "Authentication required.",
|
|
19
|
+
action: "Sign in and retry with a valid session or bearer token."
|
|
20
|
+
});
|
|
21
|
+
await next();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Require the board's submit scope while `serverAuthoritative` is on.
|
|
26
|
+
*
|
|
27
|
+
* Every vendor that offers server-authoritative writes ships it **off**; Pithy ships it **on**. The scope
|
|
28
|
+
* is the seam: mint it for your own trusted server's token and never for a player's, and a device cannot
|
|
29
|
+
* post a score it invented. Turning `serverAuthoritative` off removes this gate entirely — which is the
|
|
30
|
+
* vendor default, and is why it is not ours.
|
|
31
|
+
*/
|
|
32
|
+
function requireSubmitScope(serverAuthoritative, scope) {
|
|
33
|
+
return async (c, next) => {
|
|
34
|
+
if (serverAuthoritative && !c.var.auth?.scopes.includes(scope)) throw new LeaderboardSubmitForbiddenError({ detail: `Submit requires the ${scope} scope; this session carries [${c.var.auth?.scopes.join(", ") ?? ""}].` });
|
|
35
|
+
await next();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Require the admin scope for moderation — hiding or removing another player's entry. */
|
|
39
|
+
function requireAdminScope(scope) {
|
|
40
|
+
return async (c, next) => {
|
|
41
|
+
if (!c.var.auth?.scopes.includes(scope)) throw new ForbiddenError({
|
|
42
|
+
message: "This session may not moderate leaderboard entries.",
|
|
43
|
+
action: `Retry with a token carrying the ${scope} scope.`,
|
|
44
|
+
detail: `Moderation requires the ${scope} scope; this session carries [${c.var.auth?.scopes.join(", ") ?? ""}].`
|
|
45
|
+
});
|
|
46
|
+
await next();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { requireAdminScope, requireAuth, requireSubmitScope };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { LeaderboardConfig } from "../config/config";
|
|
4
|
+
import type { LeaderboardDatabase } from "../data/tables";
|
|
5
|
+
import { type RankedEntry } from "../rank/query";
|
|
6
|
+
import type { HideBody, SegmentBody, SubmitScoreBody, VisibilityBody } from "./schemas";
|
|
7
|
+
/**
|
|
8
|
+
* The leaderboard handlers — pure functions over injected deps. Nothing here touches a Hono `Context`;
|
|
9
|
+
* the routes unwrap the request and hand the pieces down, which is what makes every case below testable
|
|
10
|
+
* without a request at all. Nothing here parses, either: the route line declares each shape with
|
|
11
|
+
* `zValidator`, so a handler receives values that are already the type it names.
|
|
12
|
+
*/
|
|
13
|
+
export interface HandlerDeps {
|
|
14
|
+
config: LeaderboardConfig;
|
|
15
|
+
db: LeaderboardDatabase;
|
|
16
|
+
/** The authenticated player, from the core AuthContext seam. Never from the request body. */
|
|
17
|
+
userId: string;
|
|
18
|
+
now: () => Date;
|
|
19
|
+
}
|
|
20
|
+
export interface BoardPage {
|
|
21
|
+
board: string;
|
|
22
|
+
window: string;
|
|
23
|
+
entries: RankedEntry[];
|
|
24
|
+
}
|
|
25
|
+
export declare function submitScore(deps: HandlerDeps, boardKey: string, body: SubmitScoreBody): Promise<{
|
|
26
|
+
window: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function readTop(deps: HandlerDeps, boardKey: string, query: {
|
|
29
|
+
window?: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
offset: number;
|
|
32
|
+
}): Promise<BoardPage>;
|
|
33
|
+
export declare function readSegment(deps: HandlerDeps, boardKey: string, body: SegmentBody): Promise<BoardPage>;
|
|
34
|
+
export interface OwnRank {
|
|
35
|
+
board: string;
|
|
36
|
+
window: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
score: number;
|
|
39
|
+
rank: number | null;
|
|
40
|
+
tier: string | null;
|
|
41
|
+
visible: boolean;
|
|
42
|
+
/** True when `rank` came from the stored column and may lag the score beside it. */
|
|
43
|
+
rankStale: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function readOwnRank(deps: HandlerDeps, boardKey: string, query: {
|
|
46
|
+
window?: string;
|
|
47
|
+
}): Promise<OwnRank>;
|
|
48
|
+
export declare function readAround(deps: HandlerDeps, boardKey: string, query: {
|
|
49
|
+
window?: string;
|
|
50
|
+
radius: number;
|
|
51
|
+
}): Promise<BoardPage>;
|
|
52
|
+
export declare function setOwnVisibility(deps: HandlerDeps, boardKey: string, query: {
|
|
53
|
+
window?: string;
|
|
54
|
+
}, body: VisibilityBody): Promise<{
|
|
55
|
+
visible: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
export declare function hideEntry(deps: HandlerDeps, boardKey: string, targetUserId: string, query: {
|
|
58
|
+
window?: string;
|
|
59
|
+
}, body: HideBody): Promise<{
|
|
60
|
+
hidden: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
export declare function removeEntry(deps: HandlerDeps, boardKey: string, targetUserId: string, query: {
|
|
63
|
+
window?: string;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
removed: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
/** The board set, as configured — what a client needs to render a board picker. */
|
|
68
|
+
export declare function listBoards(deps: HandlerDeps): {
|
|
69
|
+
boards: Array<{
|
|
70
|
+
key: string;
|
|
71
|
+
store: string;
|
|
72
|
+
direction: string;
|
|
73
|
+
aggregation: string;
|
|
74
|
+
window: string | null;
|
|
75
|
+
tiers: Array<{
|
|
76
|
+
key: string;
|
|
77
|
+
from: number;
|
|
78
|
+
}>;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/http/handlers.ts"],"names":[],"mappings":"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"}
|