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/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"}