hyperchess-store 0.1.0
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/LICENSE +674 -0
- package/README.md +41 -0
- package/dist/adapters/firebase.d.ts +157 -0
- package/dist/adapters/firebase.d.ts.map +1 -0
- package/dist/adapters/firebase.js +300 -0
- package/dist/adapters/firebase.js.map +1 -0
- package/dist/adapters/memory.d.ts +110 -0
- package/dist/adapters/memory.d.ts.map +1 -0
- package/dist/adapters/memory.js +211 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/postgres.d.ts +161 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +341 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +186 -0
- package/dist/adapters/sqlite.d.ts.map +1 -0
- package/dist/adapters/sqlite.js +379 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/adapters/supabase.d.ts +180 -0
- package/dist/adapters/supabase.d.ts.map +1 -0
- package/dist/adapters/supabase.js +328 -0
- package/dist/adapters/supabase.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/types/game-record.d.ts +95 -0
- package/dist/types/game-record.d.ts.map +1 -0
- package/dist/types/game-record.js +44 -0
- package/dist/types/game-record.js.map +1 -0
- package/dist/types/game-store.d.ts +99 -0
- package/dist/types/game-store.d.ts.map +1 -0
- package/dist/types/game-store.js +66 -0
- package/dist/types/game-store.js.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# hyperchess-store
|
|
2
|
+
|
|
3
|
+
Game persistence for [HyperChess](https://github.com/KyrpyDev/hyperchess-core) — one
|
|
4
|
+
`GameStore` interface, five adapters. Save, load, list, and watch games without coupling your app
|
|
5
|
+
to a backend.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { MemoryGameStore } from 'hyperchess-store/memory'; // zero-config, great for tests
|
|
9
|
+
import { SqliteGameStore } from 'hyperchess-store/sqlite'; // better-sqlite3
|
|
10
|
+
import { PostgresGameStore } from 'hyperchess-store/postgres'; // pg
|
|
11
|
+
import { FirebaseGameStore } from 'hyperchess-store/firebase'; // firebase
|
|
12
|
+
import { SupabaseGameStore } from 'hyperchess-store/supabase'; // @supabase/supabase-js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Every backend driver is an **optional peer dependency** — install only what you use; the memory
|
|
16
|
+
adapter needs nothing.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { MemoryGameStore } from 'hyperchess-store/memory';
|
|
22
|
+
|
|
23
|
+
const store = new MemoryGameStore();
|
|
24
|
+
|
|
25
|
+
const id = await store.saveGame(record); // record: GameRecord
|
|
26
|
+
const game = await store.loadGame(id);
|
|
27
|
+
const recent = await store.listGames({ limit: 10 });
|
|
28
|
+
|
|
29
|
+
const unsubscribe = store.watch(id, (updated) => {
|
|
30
|
+
console.log('game changed', updated);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All adapters implement the same `GameStore` interface (`saveGame`, `loadGame`, `deleteGame`,
|
|
35
|
+
`listGames`, `countGames`, `watch`, `isHealthy`, `exportAll`, `importGames`, `clear`), so
|
|
36
|
+
swapping backends is a one-line change. `syncStores` copies games between two stores — e.g.
|
|
37
|
+
local SQLite up to Postgres.
|
|
38
|
+
|
|
39
|
+
Part of the [HyperChess Core](https://github.com/KyrpyDev/hyperchess-core) monorepo —
|
|
40
|
+
see the repository README for the full stack, contributing guide, and license
|
|
41
|
+
(GPL-3.0-or-later).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { GameStore } from '../types/game-store';
|
|
2
|
+
import { GameRecord, GameQueryOptions, Unsubscribe } from '../types/game-record';
|
|
3
|
+
/**
|
|
4
|
+
* Firebase Firestore adapter for real-time multiplayer
|
|
5
|
+
* Requires: npm install firebase
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Real-time updates via onSnapshot
|
|
9
|
+
* - Server-side count (getCountFromServer) with client fallback
|
|
10
|
+
* - Batched deletes (respects Firestore's 500-op batch limit)
|
|
11
|
+
*
|
|
12
|
+
* Schema assumption: a single top-level `games` collection whose document id is
|
|
13
|
+
* the game id, so `id` is never stored as a field. Fields are camelCase and
|
|
14
|
+
* written as native Firestore values — `players` and `metadata` are nested maps
|
|
15
|
+
* rather than JSON strings, unlike the SQL adapters. Timestamps are ISO 8601
|
|
16
|
+
* strings, not Firestore `Timestamp`s, which keeps ordering lexicographic and
|
|
17
|
+
* comparable across backends.
|
|
18
|
+
*
|
|
19
|
+
* Composite queries (`listGames` with a filter plus `orderBy('createdAt')`)
|
|
20
|
+
* require matching composite indexes in the project's Firestore configuration;
|
|
21
|
+
* without them Firestore rejects the query at runtime.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FirebaseGameStore implements GameStore {
|
|
24
|
+
private db;
|
|
25
|
+
private fs;
|
|
26
|
+
/**
|
|
27
|
+
* Bind the store to an already-initialised Firebase app.
|
|
28
|
+
*
|
|
29
|
+
* The whole `firebase/firestore` module is captured rather than individual
|
|
30
|
+
* functions, because the modular v9+ SDK exposes everything as free functions
|
|
31
|
+
* that must be called with the `Firestore` instance.
|
|
32
|
+
*
|
|
33
|
+
* @param firebaseApp - A `FirebaseApp` from `initializeApp()`; authentication
|
|
34
|
+
* and security-rule context come from that app, not from this store.
|
|
35
|
+
* @throws Error if the optional `firebase` package is not installed.
|
|
36
|
+
*/
|
|
37
|
+
constructor(firebaseApp: any);
|
|
38
|
+
/** Reference to the top-level `games` collection this adapter operates on. */
|
|
39
|
+
private collectionRef;
|
|
40
|
+
/**
|
|
41
|
+
* Write a game as a merged document under `games/{id}`.
|
|
42
|
+
*
|
|
43
|
+
* Reads the existing document first so an update preserves its original
|
|
44
|
+
* `createdAt` — that costs an extra round trip on every save, but it is the
|
|
45
|
+
* only way to keep creation time stable given `{ merge: true }` would happily
|
|
46
|
+
* overwrite it. Optional fields are written as explicit `null` rather than
|
|
47
|
+
* omitted, so a cleared field is actually cleared instead of surviving the
|
|
48
|
+
* merge.
|
|
49
|
+
*
|
|
50
|
+
* @param game - Record to persist; a missing `id` is generated locally.
|
|
51
|
+
* @returns The document id the game was written to.
|
|
52
|
+
*/
|
|
53
|
+
saveGame(game: GameRecord): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Fetch one document by id.
|
|
56
|
+
*
|
|
57
|
+
* @param id - Game id, used directly as the document id.
|
|
58
|
+
* @returns The document data with `id` reattached from the document key.
|
|
59
|
+
* @throws {@link GameNotFoundError} if the document does not exist.
|
|
60
|
+
*/
|
|
61
|
+
loadGame(id: string): Promise<GameRecord>;
|
|
62
|
+
/**
|
|
63
|
+
* Delete a document, first confirming it exists.
|
|
64
|
+
*
|
|
65
|
+
* The existence check is a deliberate extra read: Firestore's `deleteDoc`
|
|
66
|
+
* succeeds silently on a missing document, which would make it impossible to
|
|
67
|
+
* report {@link GameNotFoundError} consistently with the other adapters.
|
|
68
|
+
*
|
|
69
|
+
* @param id - Game id.
|
|
70
|
+
* @throws {@link GameNotFoundError} if the document does not exist.
|
|
71
|
+
*/
|
|
72
|
+
deleteGame(id: string): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* List games, filtering and ordering server-side.
|
|
75
|
+
*
|
|
76
|
+
* Firestore has no `OFFSET`, so pagination is emulated by fetching
|
|
77
|
+
* `limit + offset` documents and slicing locally — deep pages therefore cost
|
|
78
|
+
* proportionally more reads. With neither `limit` nor `offset` set, no limit
|
|
79
|
+
* constraint is applied and the whole collection is read.
|
|
80
|
+
*
|
|
81
|
+
* @param options - `userId`/`result` equality filters, `sort` direction
|
|
82
|
+
* (default descending by `createdAt`), `limit` and `offset`.
|
|
83
|
+
* @returns Matching games in the requested order.
|
|
84
|
+
*/
|
|
85
|
+
listGames(options?: GameQueryOptions): Promise<GameRecord[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Count matching documents, preferring the server-side aggregation.
|
|
88
|
+
*
|
|
89
|
+
* Uses `getCountFromServer` where the installed SDK provides it, which bills a
|
|
90
|
+
* single aggregation query instead of a read per document. Older SDKs fall
|
|
91
|
+
* back to fetching every match and taking `snap.size`, which is correct but
|
|
92
|
+
* far more expensive.
|
|
93
|
+
*
|
|
94
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
95
|
+
* @returns Total number of matching documents.
|
|
96
|
+
*/
|
|
97
|
+
countGames(options?: GameQueryOptions): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Subscribe to a document with Firestore's native `onSnapshot` listener.
|
|
100
|
+
*
|
|
101
|
+
* This is the only adapter with true push updates and no polling. The callback
|
|
102
|
+
* fires immediately with the current value on attach, then on every remote
|
|
103
|
+
* change. Deletions are swallowed — a snapshot for a removed document does not
|
|
104
|
+
* invoke the callback.
|
|
105
|
+
*
|
|
106
|
+
* @param id - Game id to observe.
|
|
107
|
+
* @param callback - Receives the document each time it changes.
|
|
108
|
+
* @returns Firestore's own unsubscribe function.
|
|
109
|
+
*/
|
|
110
|
+
watch(id: string, callback: (game: GameRecord) => void): Unsubscribe;
|
|
111
|
+
/**
|
|
112
|
+
* Probe by reading at most one document from the `games` collection.
|
|
113
|
+
*
|
|
114
|
+
* Stricter than a bare connectivity check: a security rule that denies reads
|
|
115
|
+
* will report unhealthy, which is the useful answer for a store.
|
|
116
|
+
*
|
|
117
|
+
* @returns `false` instead of throwing if the read fails.
|
|
118
|
+
*/
|
|
119
|
+
isHealthy(): Promise<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* Read the entire `games` collection, newest first.
|
|
122
|
+
*
|
|
123
|
+
* Costs one document read per game, so this is a backup operation rather than
|
|
124
|
+
* something to call on a request path.
|
|
125
|
+
*
|
|
126
|
+
* @returns All stored games.
|
|
127
|
+
*/
|
|
128
|
+
exportAll(): Promise<GameRecord[]>;
|
|
129
|
+
/**
|
|
130
|
+
* Write a batch of games one document at a time.
|
|
131
|
+
*
|
|
132
|
+
* Sequential rather than batched, and each save costs a read plus a write
|
|
133
|
+
* because of the `createdAt` preservation in `saveGame()`. Failures are logged
|
|
134
|
+
* and skipped, so a partial import is possible.
|
|
135
|
+
*
|
|
136
|
+
* @param games - Records to import.
|
|
137
|
+
* @returns How many were written successfully.
|
|
138
|
+
*/
|
|
139
|
+
importGames(games: GameRecord[]): Promise<number>;
|
|
140
|
+
/**
|
|
141
|
+
* Delete every document in the `games` collection.
|
|
142
|
+
*
|
|
143
|
+
* Firestore caps a write batch at 500 operations, so deletions are chunked and
|
|
144
|
+
* each chunk committed in turn. That makes this non-atomic: a failure midway
|
|
145
|
+
* leaves earlier chunks already deleted.
|
|
146
|
+
*
|
|
147
|
+
* @returns Number of documents deleted.
|
|
148
|
+
*/
|
|
149
|
+
clear(): Promise<number>;
|
|
150
|
+
/**
|
|
151
|
+
* Rebuild a {@link GameRecord} from a document's id and data, normalising the
|
|
152
|
+
* `null`s Firestore stores for absent optional fields back to `undefined`.
|
|
153
|
+
*/
|
|
154
|
+
private docToRecord;
|
|
155
|
+
private generateId;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=firebase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase.d.ts","sourceRoot":"","sources":["../../src/adapters/firebase.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,EAAE,CAAM;IAEhB;;;;;;;;;;OAUG;gBACS,WAAW,EAAE,GAAG;IAU5B,8EAA8E;IAC9E,OAAO,CAAC,aAAa;IAIrB;;;;;;;;;;;;OAYG;IACG,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IA4BjD;;;;;;OAMG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAW/C;;;;;;;;;OASG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY3C;;;;;;;;;;;OAWG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAqBlE;;;;;;;;;;OAUG;IACG,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB7D;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,WAAW;IASpE;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAUnC;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAMxC;;;;;;;;;OASG;IACG,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAevD;;;;;;;;OAQG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAqB9B;;;OAGG;IACH,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,UAAU;CAGnB"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// HyperChess Core — hyperchess-store
|
|
3
|
+
// File: packages/store/src/adapters/firebase.ts
|
|
4
|
+
// Version: 1.0.0
|
|
5
|
+
// Copyright (c) 2026 HyperChess Developer Team
|
|
6
|
+
import { GameNotFoundError } from '../types/game-record';
|
|
7
|
+
/**
|
|
8
|
+
* Firebase Firestore adapter for real-time multiplayer
|
|
9
|
+
* Requires: npm install firebase
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Real-time updates via onSnapshot
|
|
13
|
+
* - Server-side count (getCountFromServer) with client fallback
|
|
14
|
+
* - Batched deletes (respects Firestore's 500-op batch limit)
|
|
15
|
+
*
|
|
16
|
+
* Schema assumption: a single top-level `games` collection whose document id is
|
|
17
|
+
* the game id, so `id` is never stored as a field. Fields are camelCase and
|
|
18
|
+
* written as native Firestore values — `players` and `metadata` are nested maps
|
|
19
|
+
* rather than JSON strings, unlike the SQL adapters. Timestamps are ISO 8601
|
|
20
|
+
* strings, not Firestore `Timestamp`s, which keeps ordering lexicographic and
|
|
21
|
+
* comparable across backends.
|
|
22
|
+
*
|
|
23
|
+
* Composite queries (`listGames` with a filter plus `orderBy('createdAt')`)
|
|
24
|
+
* require matching composite indexes in the project's Firestore configuration;
|
|
25
|
+
* without them Firestore rejects the query at runtime.
|
|
26
|
+
*/
|
|
27
|
+
export class FirebaseGameStore {
|
|
28
|
+
/**
|
|
29
|
+
* Bind the store to an already-initialised Firebase app.
|
|
30
|
+
*
|
|
31
|
+
* The whole `firebase/firestore` module is captured rather than individual
|
|
32
|
+
* functions, because the modular v9+ SDK exposes everything as free functions
|
|
33
|
+
* that must be called with the `Firestore` instance.
|
|
34
|
+
*
|
|
35
|
+
* @param firebaseApp - A `FirebaseApp` from `initializeApp()`; authentication
|
|
36
|
+
* and security-rule context come from that app, not from this store.
|
|
37
|
+
* @throws Error if the optional `firebase` package is not installed.
|
|
38
|
+
*/
|
|
39
|
+
constructor(firebaseApp) {
|
|
40
|
+
try {
|
|
41
|
+
const firestoreApi = require('firebase/firestore');
|
|
42
|
+
this.fs = firestoreApi;
|
|
43
|
+
this.db = firestoreApi.getFirestore(firebaseApp);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error('Firebase adapter requires "firebase" package: npm install firebase');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Reference to the top-level `games` collection this adapter operates on. */
|
|
50
|
+
collectionRef() {
|
|
51
|
+
return this.fs.collection(this.db, 'games');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Write a game as a merged document under `games/{id}`.
|
|
55
|
+
*
|
|
56
|
+
* Reads the existing document first so an update preserves its original
|
|
57
|
+
* `createdAt` — that costs an extra round trip on every save, but it is the
|
|
58
|
+
* only way to keep creation time stable given `{ merge: true }` would happily
|
|
59
|
+
* overwrite it. Optional fields are written as explicit `null` rather than
|
|
60
|
+
* omitted, so a cleared field is actually cleared instead of surviving the
|
|
61
|
+
* merge.
|
|
62
|
+
*
|
|
63
|
+
* @param game - Record to persist; a missing `id` is generated locally.
|
|
64
|
+
* @returns The document id the game was written to.
|
|
65
|
+
*/
|
|
66
|
+
async saveGame(game) {
|
|
67
|
+
const { doc, getDoc, setDoc } = this.fs;
|
|
68
|
+
const id = game.id || this.generateId();
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const ref = doc(this.db, 'games', id);
|
|
71
|
+
const existing = await getDoc(ref);
|
|
72
|
+
const createdAt = existing.exists() ? existing.data().createdAt : game.createdAt || now;
|
|
73
|
+
await setDoc(ref, {
|
|
74
|
+
hpgn: game.hpgn,
|
|
75
|
+
hfen: game.hfen,
|
|
76
|
+
players: game.players ?? null,
|
|
77
|
+
metadata: game.metadata ?? null,
|
|
78
|
+
result: game.result ?? null,
|
|
79
|
+
userId: game.userId ?? null,
|
|
80
|
+
synced: game.synced ?? true,
|
|
81
|
+
createdAt,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
}, { merge: true });
|
|
84
|
+
return id;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch one document by id.
|
|
88
|
+
*
|
|
89
|
+
* @param id - Game id, used directly as the document id.
|
|
90
|
+
* @returns The document data with `id` reattached from the document key.
|
|
91
|
+
* @throws {@link GameNotFoundError} if the document does not exist.
|
|
92
|
+
*/
|
|
93
|
+
async loadGame(id) {
|
|
94
|
+
const { doc, getDoc } = this.fs;
|
|
95
|
+
const snap = await getDoc(doc(this.db, 'games', id));
|
|
96
|
+
if (!snap.exists()) {
|
|
97
|
+
throw new GameNotFoundError(id);
|
|
98
|
+
}
|
|
99
|
+
return this.docToRecord(id, snap.data());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Delete a document, first confirming it exists.
|
|
103
|
+
*
|
|
104
|
+
* The existence check is a deliberate extra read: Firestore's `deleteDoc`
|
|
105
|
+
* succeeds silently on a missing document, which would make it impossible to
|
|
106
|
+
* report {@link GameNotFoundError} consistently with the other adapters.
|
|
107
|
+
*
|
|
108
|
+
* @param id - Game id.
|
|
109
|
+
* @throws {@link GameNotFoundError} if the document does not exist.
|
|
110
|
+
*/
|
|
111
|
+
async deleteGame(id) {
|
|
112
|
+
const { doc, getDoc, deleteDoc } = this.fs;
|
|
113
|
+
const ref = doc(this.db, 'games', id);
|
|
114
|
+
const snap = await getDoc(ref);
|
|
115
|
+
if (!snap.exists()) {
|
|
116
|
+
throw new GameNotFoundError(id);
|
|
117
|
+
}
|
|
118
|
+
await deleteDoc(ref);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* List games, filtering and ordering server-side.
|
|
122
|
+
*
|
|
123
|
+
* Firestore has no `OFFSET`, so pagination is emulated by fetching
|
|
124
|
+
* `limit + offset` documents and slicing locally — deep pages therefore cost
|
|
125
|
+
* proportionally more reads. With neither `limit` nor `offset` set, no limit
|
|
126
|
+
* constraint is applied and the whole collection is read.
|
|
127
|
+
*
|
|
128
|
+
* @param options - `userId`/`result` equality filters, `sort` direction
|
|
129
|
+
* (default descending by `createdAt`), `limit` and `offset`.
|
|
130
|
+
* @returns Matching games in the requested order.
|
|
131
|
+
*/
|
|
132
|
+
async listGames(options) {
|
|
133
|
+
const { query, where, orderBy, limit: fsLimit, getDocs } = this.fs;
|
|
134
|
+
const constraints = [];
|
|
135
|
+
if (options?.userId)
|
|
136
|
+
constraints.push(where('userId', '==', options.userId));
|
|
137
|
+
if (options?.result)
|
|
138
|
+
constraints.push(where('result', '==', options.result));
|
|
139
|
+
constraints.push(orderBy('createdAt', options?.sort === 'asc' ? 'asc' : 'desc'));
|
|
140
|
+
// Firestore has no offset primitive; over-fetch to (limit + offset) and slice client-side.
|
|
141
|
+
const fetchLimit = (options?.limit ?? 0) + (options?.offset ?? 0);
|
|
142
|
+
if (fetchLimit > 0)
|
|
143
|
+
constraints.push(fsLimit(fetchLimit));
|
|
144
|
+
const snap = await getDocs(query(this.collectionRef(), ...constraints));
|
|
145
|
+
let rows = snap.docs.map((d) => this.docToRecord(d.id, d.data()));
|
|
146
|
+
if (options?.offset)
|
|
147
|
+
rows = rows.slice(options.offset);
|
|
148
|
+
if (options?.limit)
|
|
149
|
+
rows = rows.slice(0, options.limit);
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Count matching documents, preferring the server-side aggregation.
|
|
154
|
+
*
|
|
155
|
+
* Uses `getCountFromServer` where the installed SDK provides it, which bills a
|
|
156
|
+
* single aggregation query instead of a read per document. Older SDKs fall
|
|
157
|
+
* back to fetching every match and taking `snap.size`, which is correct but
|
|
158
|
+
* far more expensive.
|
|
159
|
+
*
|
|
160
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
161
|
+
* @returns Total number of matching documents.
|
|
162
|
+
*/
|
|
163
|
+
async countGames(options) {
|
|
164
|
+
const { query, where, getCountFromServer, getDocs } = this.fs;
|
|
165
|
+
const constraints = [];
|
|
166
|
+
if (options?.userId)
|
|
167
|
+
constraints.push(where('userId', '==', options.userId));
|
|
168
|
+
if (options?.result)
|
|
169
|
+
constraints.push(where('result', '==', options.result));
|
|
170
|
+
const q = query(this.collectionRef(), ...constraints);
|
|
171
|
+
if (getCountFromServer) {
|
|
172
|
+
const snap = await getCountFromServer(q);
|
|
173
|
+
return snap.data().count;
|
|
174
|
+
}
|
|
175
|
+
const snap = await getDocs(q);
|
|
176
|
+
return snap.size;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Subscribe to a document with Firestore's native `onSnapshot` listener.
|
|
180
|
+
*
|
|
181
|
+
* This is the only adapter with true push updates and no polling. The callback
|
|
182
|
+
* fires immediately with the current value on attach, then on every remote
|
|
183
|
+
* change. Deletions are swallowed — a snapshot for a removed document does not
|
|
184
|
+
* invoke the callback.
|
|
185
|
+
*
|
|
186
|
+
* @param id - Game id to observe.
|
|
187
|
+
* @param callback - Receives the document each time it changes.
|
|
188
|
+
* @returns Firestore's own unsubscribe function.
|
|
189
|
+
*/
|
|
190
|
+
watch(id, callback) {
|
|
191
|
+
const { doc, onSnapshot } = this.fs;
|
|
192
|
+
return onSnapshot(doc(this.db, 'games', id), (snap) => {
|
|
193
|
+
if (snap.exists()) {
|
|
194
|
+
callback(this.docToRecord(id, snap.data()));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Probe by reading at most one document from the `games` collection.
|
|
200
|
+
*
|
|
201
|
+
* Stricter than a bare connectivity check: a security rule that denies reads
|
|
202
|
+
* will report unhealthy, which is the useful answer for a store.
|
|
203
|
+
*
|
|
204
|
+
* @returns `false` instead of throwing if the read fails.
|
|
205
|
+
*/
|
|
206
|
+
async isHealthy() {
|
|
207
|
+
try {
|
|
208
|
+
const { query, limit: fsLimit, getDocs } = this.fs;
|
|
209
|
+
await getDocs(query(this.collectionRef(), fsLimit(1)));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Read the entire `games` collection, newest first.
|
|
218
|
+
*
|
|
219
|
+
* Costs one document read per game, so this is a backup operation rather than
|
|
220
|
+
* something to call on a request path.
|
|
221
|
+
*
|
|
222
|
+
* @returns All stored games.
|
|
223
|
+
*/
|
|
224
|
+
async exportAll() {
|
|
225
|
+
const { query, orderBy, getDocs } = this.fs;
|
|
226
|
+
const snap = await getDocs(query(this.collectionRef(), orderBy('createdAt', 'desc')));
|
|
227
|
+
return snap.docs.map((d) => this.docToRecord(d.id, d.data()));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Write a batch of games one document at a time.
|
|
231
|
+
*
|
|
232
|
+
* Sequential rather than batched, and each save costs a read plus a write
|
|
233
|
+
* because of the `createdAt` preservation in `saveGame()`. Failures are logged
|
|
234
|
+
* and skipped, so a partial import is possible.
|
|
235
|
+
*
|
|
236
|
+
* @param games - Records to import.
|
|
237
|
+
* @returns How many were written successfully.
|
|
238
|
+
*/
|
|
239
|
+
async importGames(games) {
|
|
240
|
+
let count = 0;
|
|
241
|
+
for (const game of games) {
|
|
242
|
+
try {
|
|
243
|
+
await this.saveGame(game);
|
|
244
|
+
count++;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.warn(`Failed to import game ${game.id}:`, error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return count;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Delete every document in the `games` collection.
|
|
254
|
+
*
|
|
255
|
+
* Firestore caps a write batch at 500 operations, so deletions are chunked and
|
|
256
|
+
* each chunk committed in turn. That makes this non-atomic: a failure midway
|
|
257
|
+
* leaves earlier chunks already deleted.
|
|
258
|
+
*
|
|
259
|
+
* @returns Number of documents deleted.
|
|
260
|
+
*/
|
|
261
|
+
async clear() {
|
|
262
|
+
const { doc, getDocs, writeBatch } = this.fs;
|
|
263
|
+
const snap = await getDocs(this.collectionRef());
|
|
264
|
+
const docs = snap.docs;
|
|
265
|
+
const BATCH_LIMIT = 500; // Firestore's max writes per batch
|
|
266
|
+
let deleted = 0;
|
|
267
|
+
for (let i = 0; i < docs.length; i += BATCH_LIMIT) {
|
|
268
|
+
const batch = writeBatch(this.db);
|
|
269
|
+
const chunk = docs.slice(i, i + BATCH_LIMIT);
|
|
270
|
+
for (const d of chunk) {
|
|
271
|
+
batch.delete(doc(this.db, 'games', d.id));
|
|
272
|
+
}
|
|
273
|
+
await batch.commit();
|
|
274
|
+
deleted += chunk.length;
|
|
275
|
+
}
|
|
276
|
+
return deleted;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Rebuild a {@link GameRecord} from a document's id and data, normalising the
|
|
280
|
+
* `null`s Firestore stores for absent optional fields back to `undefined`.
|
|
281
|
+
*/
|
|
282
|
+
docToRecord(id, data) {
|
|
283
|
+
return {
|
|
284
|
+
id,
|
|
285
|
+
hpgn: data.hpgn,
|
|
286
|
+
hfen: data.hfen,
|
|
287
|
+
players: data.players ?? undefined,
|
|
288
|
+
metadata: data.metadata ?? undefined,
|
|
289
|
+
result: data.result ?? undefined,
|
|
290
|
+
userId: data.userId ?? undefined,
|
|
291
|
+
synced: data.synced ?? undefined,
|
|
292
|
+
createdAt: data.createdAt,
|
|
293
|
+
updatedAt: data.updatedAt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
generateId() {
|
|
297
|
+
return `game_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=firebase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firebase.js","sourceRoot":"","sources":["../../src/adapters/firebase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GameStore } from '../types/game-store';
|
|
2
|
+
import { GameRecord, GameQueryOptions } from '../types/game-record';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory game store for testing and development.
|
|
5
|
+
*
|
|
6
|
+
* Backed by a plain `Map`, so all data is lost when the process exits. It is the
|
|
7
|
+
* package default export and the reference implementation of {@link GameStore}:
|
|
8
|
+
* every other adapter is expected to match its observable behaviour.
|
|
9
|
+
*
|
|
10
|
+
* Records are deep-copied with `structuredClone` on the way out (and into
|
|
11
|
+
* watcher callbacks) so a caller mutating a returned object cannot corrupt the
|
|
12
|
+
* store — a hazard the database adapters don't have because they deserialise
|
|
13
|
+
* fresh rows on every read.
|
|
14
|
+
*/
|
|
15
|
+
export declare class MemoryGameStore implements GameStore {
|
|
16
|
+
private games;
|
|
17
|
+
private watchers;
|
|
18
|
+
/**
|
|
19
|
+
* Insert or replace a game, stamping `updatedAt` with the current time.
|
|
20
|
+
*
|
|
21
|
+
* The record fully replaces any existing entry rather than merging into it,
|
|
22
|
+
* and watchers of this id are notified synchronously before returning.
|
|
23
|
+
*
|
|
24
|
+
* @param game - Record to store; if `id` is empty a new one is generated.
|
|
25
|
+
* @returns The id the game was stored under.
|
|
26
|
+
*/
|
|
27
|
+
saveGame(game: GameRecord): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch a game by id.
|
|
30
|
+
*
|
|
31
|
+
* @param id - Game id to look up.
|
|
32
|
+
* @returns A deep copy of the stored record, safe for the caller to mutate.
|
|
33
|
+
* @throws {@link GameNotFoundError} if no game is stored under `id`.
|
|
34
|
+
*/
|
|
35
|
+
loadGame(id: string): Promise<GameRecord>;
|
|
36
|
+
/**
|
|
37
|
+
* Remove a game and drop every watcher registered against it.
|
|
38
|
+
*
|
|
39
|
+
* Watchers are discarded silently — they receive no final notification, so a
|
|
40
|
+
* subscriber must treat "no further updates" as a possible deletion.
|
|
41
|
+
*
|
|
42
|
+
* @param id - Game id to remove.
|
|
43
|
+
* @throws {@link GameNotFoundError} if no game is stored under `id`.
|
|
44
|
+
*/
|
|
45
|
+
deleteGame(id: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* List games, filtering, sorting and paginating entirely in memory.
|
|
48
|
+
*
|
|
49
|
+
* Sorting is by `createdAt`, treating a missing timestamp as the epoch so
|
|
50
|
+
* undated records sort oldest. Note the default order is ascending here,
|
|
51
|
+
* whereas the SQL-backed adapters default to descending.
|
|
52
|
+
*
|
|
53
|
+
* @param options - Filters (`userId`, `result`), `sort` direction and
|
|
54
|
+
* `offset`/`limit` window; omitting `limit` returns everything after
|
|
55
|
+
* `offset`.
|
|
56
|
+
* @returns Deep copies of the matching records.
|
|
57
|
+
*/
|
|
58
|
+
listGames(options?: GameQueryOptions): Promise<GameRecord[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Count games matching the filters.
|
|
61
|
+
*
|
|
62
|
+
* `limit` and `offset` are deliberately ignored so the result is the total
|
|
63
|
+
* size of the match set, not the size of one page.
|
|
64
|
+
*
|
|
65
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
66
|
+
* @returns Number of matching records.
|
|
67
|
+
*/
|
|
68
|
+
countGames(options?: GameQueryOptions): Promise<number>;
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to writes for a single game.
|
|
71
|
+
*
|
|
72
|
+
* Fires only on subsequent `saveGame()` calls — there is no initial emission
|
|
73
|
+
* of the current value, and deletions produce no event.
|
|
74
|
+
*
|
|
75
|
+
* @param id - Game id to observe. Watching an id that does not exist yet is
|
|
76
|
+
* valid; the callback fires when it is first saved.
|
|
77
|
+
* @param callback - Receives a deep copy of the saved record.
|
|
78
|
+
* @returns Unsubscribe handle; the id's watcher set is dropped once empty.
|
|
79
|
+
*/
|
|
80
|
+
watch(id: string, callback: (game: GameRecord) => void): () => void;
|
|
81
|
+
/**
|
|
82
|
+
* Always resolves `true` — there is no backend that can be unreachable.
|
|
83
|
+
*/
|
|
84
|
+
isHealthy(): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Dump every stored game, unfiltered and in insertion order.
|
|
87
|
+
*
|
|
88
|
+
* @returns Deep copies of all records.
|
|
89
|
+
*/
|
|
90
|
+
exportAll(): Promise<GameRecord[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Bulk-load records, overwriting any existing game with the same id.
|
|
93
|
+
*
|
|
94
|
+
* Each import goes through `saveGame()`, so `updatedAt` is rewritten to now
|
|
95
|
+
* and watchers fire — importing a backup is not a silent restore.
|
|
96
|
+
*
|
|
97
|
+
* @param games - Records to load.
|
|
98
|
+
* @returns The number of records supplied.
|
|
99
|
+
*/
|
|
100
|
+
importGames(games: GameRecord[]): Promise<number>;
|
|
101
|
+
/**
|
|
102
|
+
* Drop every game and every registered watcher.
|
|
103
|
+
*
|
|
104
|
+
* @returns How many games were removed.
|
|
105
|
+
*/
|
|
106
|
+
clear(): Promise<number>;
|
|
107
|
+
private notifyWatchers;
|
|
108
|
+
private generateId;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,MAAM,sBAAsB,CAAC;AAEvF;;;;;;;;;;;GAWG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,QAAQ,CAA2D;IAE3E;;;;;;;;OAQG;IACG,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBjD;;;;;;OAMG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAQ/C;;;;;;;;OAQG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3C;;;;;;;;;;;OAWG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA6BlE;;;;;;;;OAQG;IACG,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAc7D;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAmBnE;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAInC;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAIxC;;;;;;;;OAQG;IACG,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAOvD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAO9B,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,UAAU;CAGnB"}
|