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.
@@ -0,0 +1,328 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // HyperChess Core — hyperchess-store
3
+ // File: packages/store/src/adapters/supabase.ts
4
+ // Version: 1.0.0
5
+ // Copyright (c) 2026 HyperChess Developer Team
6
+ import { GameNotFoundError } from '../types/game-record';
7
+ /**
8
+ * Supabase adapter - PostgreSQL + Realtime + Auth
9
+ * Requires: npm install @supabase/supabase-js
10
+ *
11
+ * Features:
12
+ * - Realtime subscriptions via postgres_changes
13
+ * - Row Level Security aware (games scoped to authenticated user)
14
+ * - Range-based pagination
15
+ *
16
+ * Schema assumption: a `public.games` table exposed through PostgREST with
17
+ * columns `id` (primary key), `hpgn`, `hfen`, `players`, `metadata`, `result`,
18
+ * `user_id`, `synced`, `created_at`, `updated_at`. `players` and `metadata` are
19
+ * sent as objects and read back as objects, so those columns must be `json`/
20
+ * `jsonb` — the opposite of the raw Postgres adapter, which round-trips them as
21
+ * JSON strings.
22
+ *
23
+ * Because every call goes through PostgREST, Row Level Security applies: what a
24
+ * query returns depends on the key the client was built with. Under RLS a
25
+ * "missing" row and a forbidden one are indistinguishable, so
26
+ * {@link GameNotFoundError} can also mean "not visible to you". Realtime
27
+ * subscriptions likewise require the `games` table to be in the realtime
28
+ * publication.
29
+ *
30
+ * Errors from the client are surfaced as plain `Error`s carrying the PostgREST
31
+ * message rather than being swallowed — only `isHealthy()` degrades quietly.
32
+ */
33
+ export class SupabaseGameStore {
34
+ /**
35
+ * Create the underlying Supabase client.
36
+ *
37
+ * The key determines the store's effective permissions: an anon key leaves RLS
38
+ * in force (games scoped to the signed-in user), a service-role key bypasses
39
+ * it entirely and must never reach a browser.
40
+ *
41
+ * @param supabaseUrl - Project URL, e.g. `https://<ref>.supabase.co`.
42
+ * @param supabaseKey - Anon or service-role API key.
43
+ * @throws Error if the optional `@supabase/supabase-js` package is not
44
+ * installed.
45
+ */
46
+ constructor(supabaseUrl, supabaseKey) {
47
+ this.channels = new Map();
48
+ try {
49
+ const { createClient } = require('@supabase/supabase-js');
50
+ this.client = createClient(supabaseUrl, supabaseKey);
51
+ }
52
+ catch (error) {
53
+ throw new Error('Supabase adapter requires "@supabase/supabase-js" package: npm install @supabase/supabase-js');
54
+ }
55
+ }
56
+ /**
57
+ * Upsert a game, conflicting on `id`, and read the stored row back.
58
+ *
59
+ * `updated_at` is always stamped with the current time; `created_at` falls
60
+ * back to now only when the record carries none, which means re-saving a
61
+ * record whose `createdAt` was dropped will rewrite the creation time.
62
+ *
63
+ * @param game - Record to persist; a missing `id` is generated locally.
64
+ * @returns The id reported by the returned row.
65
+ * @throws Error carrying the PostgREST message if the upsert is rejected —
66
+ * including RLS denials.
67
+ */
68
+ async saveGame(game) {
69
+ const id = game.id || this.generateId();
70
+ const now = new Date().toISOString();
71
+ const { data, error } = await this.client
72
+ .from('games')
73
+ .upsert({
74
+ id,
75
+ hpgn: game.hpgn,
76
+ hfen: game.hfen,
77
+ players: game.players ?? null,
78
+ metadata: game.metadata ?? null,
79
+ result: game.result ?? null,
80
+ user_id: game.userId ?? null,
81
+ synced: game.synced ?? true,
82
+ created_at: game.createdAt || now,
83
+ updated_at: now,
84
+ }, { onConflict: 'id' })
85
+ .select()
86
+ .single();
87
+ if (error) {
88
+ throw new Error(`Supabase saveGame failed: ${error.message}`);
89
+ }
90
+ return data.id;
91
+ }
92
+ /**
93
+ * Fetch one row by id.
94
+ *
95
+ * Uses `maybeSingle()` so an absent row comes back as `null` data rather than
96
+ * a PostgREST error, letting the missing case be reported as a typed
97
+ * {@link GameNotFoundError}.
98
+ *
99
+ * @param id - Game id.
100
+ * @returns The row in {@link GameRecord} shape.
101
+ * @throws {@link GameNotFoundError} if no row is visible under `id`.
102
+ * @throws Error carrying the PostgREST message on a query failure.
103
+ */
104
+ async loadGame(id) {
105
+ const { data, error } = await this.client.from('games').select('*').eq('id', id).maybeSingle();
106
+ if (error) {
107
+ throw new Error(`Supabase loadGame failed: ${error.message}`);
108
+ }
109
+ if (!data) {
110
+ throw new GameNotFoundError(id);
111
+ }
112
+ return this.rowToRecord(data);
113
+ }
114
+ /**
115
+ * Delete a row, using the returned representation to prove it existed.
116
+ *
117
+ * The trailing `.select()` is what makes the not-found case detectable: a
118
+ * `DELETE` matching nothing is not an error to PostgREST, it simply returns an
119
+ * empty set.
120
+ *
121
+ * @param id - Game id.
122
+ * @throws {@link GameNotFoundError} if no row was deleted.
123
+ * @throws Error carrying the PostgREST message on a query failure.
124
+ */
125
+ async deleteGame(id) {
126
+ const { data, error } = await this.client.from('games').delete().eq('id', id).select();
127
+ if (error) {
128
+ throw new Error(`Supabase deleteGame failed: ${error.message}`);
129
+ }
130
+ if (!data || data.length === 0) {
131
+ throw new GameNotFoundError(id);
132
+ }
133
+ }
134
+ /**
135
+ * List games using PostgREST filters and range-based pagination.
136
+ *
137
+ * `range()` bounds are inclusive, so a page is expressed as
138
+ * `[offset, offset + limit - 1]`. An `offset` given without a `limit` falls
139
+ * back to a fixed 1000-row window, since PostgREST needs an upper bound.
140
+ *
141
+ * @param options - `userId`/`result` equality filters, `sort` direction
142
+ * (default descending by `created_at`), `limit` and `offset`.
143
+ * @returns Matching rows in the requested order.
144
+ * @throws Error carrying the PostgREST message on a query failure.
145
+ */
146
+ async listGames(options) {
147
+ let q = this.client.from('games').select('*');
148
+ if (options?.userId)
149
+ q = q.eq('user_id', options.userId);
150
+ if (options?.result)
151
+ q = q.eq('result', options.result);
152
+ q = q.order('created_at', { ascending: options?.sort === 'asc' });
153
+ if (options?.limit !== undefined) {
154
+ const from = options?.offset ?? 0;
155
+ q = q.range(from, from + options.limit - 1);
156
+ }
157
+ else if (options?.offset) {
158
+ q = q.range(options.offset, options.offset + 999);
159
+ }
160
+ const { data, error } = await q;
161
+ if (error) {
162
+ throw new Error(`Supabase listGames failed: ${error.message}`);
163
+ }
164
+ return (data ?? []).map((row) => this.rowToRecord(row));
165
+ }
166
+ /**
167
+ * Count matching rows without transferring them.
168
+ *
169
+ * `head: true` issues a `HEAD` request so only the count header comes back;
170
+ * `count: 'exact'` is accurate but does a full scan on large tables.
171
+ *
172
+ * @param options - Only `userId` and `result` are honoured.
173
+ * @returns Total number of matching rows, or 0 if the count header is absent.
174
+ * @throws Error carrying the PostgREST message on a query failure.
175
+ */
176
+ async countGames(options) {
177
+ let q = this.client.from('games').select('*', { count: 'exact', head: true });
178
+ if (options?.userId)
179
+ q = q.eq('user_id', options.userId);
180
+ if (options?.result)
181
+ q = q.eq('result', options.result);
182
+ const { count, error } = await q;
183
+ if (error) {
184
+ throw new Error(`Supabase countGames failed: ${error.message}`);
185
+ }
186
+ return count ?? 0;
187
+ }
188
+ /**
189
+ * Subscribe to a game over a Realtime `postgres_changes` channel.
190
+ *
191
+ * Listens for `UPDATE` events only — inserts and deletes are not delivered, so
192
+ * watching an id before it exists yields nothing until the first update after
193
+ * creation. Each id gets its own channel, tracked so `clear()` can tear them
194
+ * all down.
195
+ *
196
+ * @param id - Game id to observe.
197
+ * @param callback - Receives the new row from the change payload.
198
+ * @returns Unsubscribe handle that removes the channel.
199
+ */
200
+ watch(id, callback) {
201
+ const channelName = `games:${id}`;
202
+ const channel = this.client
203
+ .channel(channelName)
204
+ .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'games', filter: `id=eq.${id}` }, (payload) => callback(this.rowToRecord(payload.new)))
205
+ .subscribe();
206
+ this.channels.set(channelName, channel);
207
+ return () => {
208
+ this.client.removeChannel(channel);
209
+ this.channels.delete(channelName);
210
+ };
211
+ }
212
+ /**
213
+ * Probe with a bounded head request against the `games` table.
214
+ *
215
+ * Verifies both reachability and that the table is readable under the current
216
+ * key and RLS policy.
217
+ *
218
+ * @returns `false` instead of throwing on any error.
219
+ */
220
+ async isHealthy() {
221
+ try {
222
+ const { error } = await this.client.from('games').select('id', { head: true, count: 'exact' }).limit(1);
223
+ return !error;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * Read every visible row, newest first.
231
+ *
232
+ * No range is applied, so the result is still subject to the project's
233
+ * PostgREST `max-rows` setting — on a large table this may silently return a
234
+ * truncated backup.
235
+ *
236
+ * @returns All games visible to the current key.
237
+ * @throws Error carrying the PostgREST message on a query failure.
238
+ */
239
+ async exportAll() {
240
+ const { data, error } = await this.client.from('games').select('*').order('created_at', { ascending: false });
241
+ if (error) {
242
+ throw new Error(`Supabase exportAll failed: ${error.message}`);
243
+ }
244
+ return (data ?? []).map((row) => this.rowToRecord(row));
245
+ }
246
+ /**
247
+ * Upsert a batch of games with one request per record.
248
+ *
249
+ * Sequential and non-transactional; a record rejected by RLS or a constraint
250
+ * is logged and skipped, so a partial import is possible.
251
+ *
252
+ * @param games - Records to import.
253
+ * @returns How many were saved successfully.
254
+ */
255
+ async importGames(games) {
256
+ let count = 0;
257
+ for (const game of games) {
258
+ try {
259
+ await this.saveGame(game);
260
+ count++;
261
+ }
262
+ catch (error) {
263
+ console.warn(`Failed to import game ${game.id}:`, error);
264
+ }
265
+ }
266
+ return count;
267
+ }
268
+ /**
269
+ * Delete every visible row and close all Realtime channels.
270
+ *
271
+ * The `neq('id', '')` predicate exists because PostgREST refuses an unfiltered
272
+ * `DELETE` as a footgun guard; matching "id is not the empty string" selects
273
+ * everything while satisfying that requirement. RLS still applies, so this
274
+ * clears only what the current key can see.
275
+ *
276
+ * @returns Number of rows deleted.
277
+ * @throws Error carrying the PostgREST message on a query failure.
278
+ */
279
+ async clear() {
280
+ const { data, error } = await this.client.from('games').delete().neq('id', '').select();
281
+ if (error) {
282
+ throw new Error(`Supabase clear failed: ${error.message}`);
283
+ }
284
+ this.channels.forEach((channel) => this.client.removeChannel(channel));
285
+ this.channels.clear();
286
+ return data?.length ?? 0;
287
+ }
288
+ /**
289
+ * Map a snake_case PostgREST row to a {@link GameRecord}. The JSON columns
290
+ * arrive already deserialised, so only the key renaming and `null`-to-
291
+ * `undefined` normalisation is needed.
292
+ */
293
+ rowToRecord(row) {
294
+ return {
295
+ id: row.id,
296
+ hpgn: row.hpgn,
297
+ hfen: row.hfen,
298
+ players: row.players ?? undefined,
299
+ metadata: row.metadata ?? undefined,
300
+ result: row.result ?? undefined,
301
+ userId: row.user_id ?? undefined,
302
+ synced: row.synced ?? undefined,
303
+ createdAt: row.created_at,
304
+ updatedAt: row.updated_at,
305
+ };
306
+ }
307
+ generateId() {
308
+ return `game_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
309
+ }
310
+ // Row Level Security - games scoped to user
311
+ /**
312
+ * Resolve the id of the currently signed-in Supabase user.
313
+ *
314
+ * Useful for stamping `GameRecord.userId` so saved games line up with the
315
+ * `user_id` an RLS policy filters on.
316
+ *
317
+ * @returns The auth user id, or `null` when nobody is signed in or the lookup
318
+ * fails — the two cases are not distinguished.
319
+ */
320
+ async getAuthenticatedUser() {
321
+ const { data, error } = await this.client.auth.getUser();
322
+ if (error || !data?.user) {
323
+ return null;
324
+ }
325
+ return data.user.id;
326
+ }
327
+ }
328
+ //# sourceMappingURL=supabase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabase.js","sourceRoot":"","sources":["../../src/adapters/supabase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Persistence layer for HyperChess games.
3
+ *
4
+ * Every adapter implements the same {@link GameStore} contract over a different
5
+ * backend, so an application can swap storage without touching game logic. The
6
+ * database-backed adapters (`postgres`, `sqlite`, `firebase`, `supabase`) load
7
+ * their driver lazily via `require()` and throw from their constructor if the
8
+ * driver is absent — this keeps all four out of the dependency graph of a
9
+ * consumer that only uses one of them.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ export type { GameStore } from './types/game-store';
14
+ export { isStoreOnline, syncStores } from './types/game-store';
15
+ export type { GameRecord, GameQueryOptions, Unsubscribe } from './types/game-record';
16
+ export { GameNotFoundError, ValidationError, SyncError } from './types/game-record';
17
+ export { MemoryGameStore } from './adapters/memory';
18
+ export { PostgresGameStore } from './adapters/postgres';
19
+ export { SqliteGameStore } from './adapters/sqlite';
20
+ export { FirebaseGameStore } from './adapters/firebase';
21
+ export { SupabaseGameStore } from './adapters/supabase';
22
+ export { MemoryGameStore as default } from './adapters/memory';
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;GAWG;AAGH,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGpF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,EAAE,eAAe,IAAI,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // HyperChess Core — hyperchess-store
3
+ // File: packages/store/src/index.ts
4
+ // Version: 1.0.0
5
+ // Copyright (c) 2026 HyperChess Developer Team
6
+ export { isStoreOnline, syncStores } from './types/game-store';
7
+ export { GameNotFoundError, ValidationError, SyncError } from './types/game-record';
8
+ // Export adapters
9
+ export { MemoryGameStore } from './adapters/memory';
10
+ export { PostgresGameStore } from './adapters/postgres';
11
+ export { SqliteGameStore } from './adapters/sqlite';
12
+ export { FirebaseGameStore } from './adapters/firebase';
13
+ export { SupabaseGameStore } from './adapters/supabase';
14
+ // Default export is memory store for development
15
+ export { MemoryGameStore as default } from './adapters/memory';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qCAAqC;AACrC,oCAAoC;AACpC,iBAAiB;AACjB,+CAA+C;AAiB/C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEpF,kBAAkB;AAClB,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,iDAAiD;AACjD,OAAO,EAAE,eAAe,IAAI,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A complete game record (HPGN + metadata).
3
+ *
4
+ * This is the single wire/storage shape shared by every adapter. Adapters that
5
+ * persist to a relational backend flatten `players`/`metadata` into JSON columns
6
+ * and rename `userId` to `user_id`; consumers always see this camelCase form.
7
+ */
8
+ export interface GameRecord {
9
+ /** Unique game identifier */
10
+ id: string;
11
+ /** Game in HPGN format */
12
+ hpgn: string;
13
+ /** Starting position (HFEN) */
14
+ hfen: string;
15
+ /** Player names */
16
+ players?: {
17
+ white?: string;
18
+ black?: string;
19
+ };
20
+ /** Game metadata */
21
+ metadata?: {
22
+ event?: string;
23
+ site?: string;
24
+ date?: string;
25
+ round?: string;
26
+ whiteElo?: number;
27
+ blackElo?: number;
28
+ timeControl?: string;
29
+ };
30
+ /** Game result (1-0, 0-1, 1/2-1/2, *) */
31
+ result?: '1-0' | '0-1' | '1/2-1/2' | '*';
32
+ /** User/owner ID (for multi-user systems) */
33
+ userId?: string;
34
+ /** When created (ISO 8601) */
35
+ createdAt?: string;
36
+ /** When last modified (ISO 8601) */
37
+ updatedAt?: string;
38
+ /** Whether synced to cloud (for offline systems) */
39
+ synced?: boolean;
40
+ }
41
+ /**
42
+ * Query options for listing games.
43
+ *
44
+ * Filters are combined with AND. All fields are optional; an empty object means
45
+ * "every game, newest first" for the backend-query adapters and "every game,
46
+ * oldest first" for {@link GameRecord} sorting done in memory.
47
+ */
48
+ export interface GameQueryOptions {
49
+ /** Filter by user ID */
50
+ userId?: string;
51
+ /** Filter by result */
52
+ result?: '1-0' | '0-1' | '1/2-1/2' | '*';
53
+ /** Maximum results to return */
54
+ limit?: number;
55
+ /** Offset for pagination */
56
+ offset?: number;
57
+ /** Sort order (asc/desc by date) */
58
+ sort?: 'asc' | 'desc';
59
+ }
60
+ /**
61
+ * Teardown handle returned by `GameStore.watch()`. Calling it detaches the
62
+ * callback and, once the last callback for an id is gone, releases whatever
63
+ * backend resource backed the subscription (poll timer, channel, snapshot).
64
+ */
65
+ export type Unsubscribe = () => void;
66
+ /**
67
+ * Thrown when a game id does not resolve to a stored record.
68
+ *
69
+ * Raised by `loadGame()` and `deleteGame()` on every adapter, so callers can
70
+ * distinguish "absent" from a genuine backend failure.
71
+ *
72
+ * @param id - The game id that could not be resolved; embedded in the message.
73
+ */
74
+ export declare class GameNotFoundError extends Error {
75
+ constructor(id: string);
76
+ }
77
+ /**
78
+ * Thrown when a {@link GameRecord} fails structural validation before it is
79
+ * handed to a backend.
80
+ *
81
+ * @param message - Description of which field or invariant was violated.
82
+ */
83
+ export declare class ValidationError extends Error {
84
+ constructor(message: string);
85
+ }
86
+ /**
87
+ * Thrown when replication between two stores cannot complete — for example an
88
+ * offline SQLite store failing to flush its sync queue upstream.
89
+ *
90
+ * @param message - Description of what failed to sync and why.
91
+ */
92
+ export declare class SyncError extends Error {
93
+ constructor(message: string);
94
+ }
95
+ //# sourceMappingURL=game-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-record.d.ts","sourceRoot":"","sources":["../../src/types/game-record.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,EAAE,EAAE,MAAM,CAAC;IAEX,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IAEb,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IAEb,mBAAmB;IACnB,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF,oBAAoB;IACpB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IAEF,yCAAyC;IACzC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,GAAG,CAAC;IAEzC,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,uBAAuB;IACvB,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,GAAG,CAAC;IAEzC,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,oCAAoC;IACpC,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,EAAE,EAAE,MAAM;CAIvB;AAED;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // HyperChess Core — hyperchess-store
3
+ // File: packages/store/src/types/game-record.ts
4
+ // Version: 1.0.0
5
+ // Copyright (c) 2026 HyperChess Developer Team
6
+ /**
7
+ * Thrown when a game id does not resolve to a stored record.
8
+ *
9
+ * Raised by `loadGame()` and `deleteGame()` on every adapter, so callers can
10
+ * distinguish "absent" from a genuine backend failure.
11
+ *
12
+ * @param id - The game id that could not be resolved; embedded in the message.
13
+ */
14
+ export class GameNotFoundError extends Error {
15
+ constructor(id) {
16
+ super(`Game not found: ${id}`);
17
+ this.name = 'GameNotFoundError';
18
+ }
19
+ }
20
+ /**
21
+ * Thrown when a {@link GameRecord} fails structural validation before it is
22
+ * handed to a backend.
23
+ *
24
+ * @param message - Description of which field or invariant was violated.
25
+ */
26
+ export class ValidationError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = 'ValidationError';
30
+ }
31
+ }
32
+ /**
33
+ * Thrown when replication between two stores cannot complete — for example an
34
+ * offline SQLite store failing to flush its sync queue upstream.
35
+ *
36
+ * @param message - Description of what failed to sync and why.
37
+ */
38
+ export class SyncError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = 'SyncError';
42
+ }
43
+ }
44
+ //# sourceMappingURL=game-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-record.js","sourceRoot":"","sources":["../../src/types/game-record.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qCAAqC;AACrC,gDAAgD;AAChD,iBAAiB;AACjB,+CAA+C;AAmF/C;;;;;;;GAOG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,EAAU;QACpB,KAAK,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,99 @@
1
+ import { GameRecord, GameQueryOptions, Unsubscribe } from './game-record';
2
+ /**
3
+ * Universal game storage interface
4
+ * Implementations: PostgreSQL, SQLite, Firebase, Supabase, Memory
5
+ *
6
+ * All methods are async for compatibility with remote backends
7
+ */
8
+ export interface GameStore {
9
+ /**
10
+ * Save a new game or update existing
11
+ * @param game Game record to save
12
+ * @returns ID of saved game (auto-generated if not provided)
13
+ */
14
+ saveGame(game: GameRecord): Promise<string>;
15
+ /**
16
+ * Load a game by ID
17
+ * @param id Game ID
18
+ * @returns Game record or throws GameNotFoundError
19
+ */
20
+ loadGame(id: string): Promise<GameRecord>;
21
+ /**
22
+ * Delete a game
23
+ * @param id Game ID
24
+ */
25
+ deleteGame(id: string): Promise<void>;
26
+ /**
27
+ * List games (optionally filtered)
28
+ * @param options Query options
29
+ * @returns Array of game records
30
+ */
31
+ listGames(options?: GameQueryOptions): Promise<GameRecord[]>;
32
+ /**
33
+ * Count games (optionally filtered)
34
+ * @param options Query options (omit limit/offset for count)
35
+ * @returns Total count
36
+ */
37
+ countGames(options?: GameQueryOptions): Promise<number>;
38
+ /**
39
+ * Watch a game for real-time updates
40
+ * @param id Game ID
41
+ * @param callback Called when game changes
42
+ * @returns Unsubscribe function
43
+ */
44
+ watch(id: string, callback: (game: GameRecord) => void): Unsubscribe;
45
+ /**
46
+ * Health check - verify backend is accessible
47
+ * @returns true if backend is operational
48
+ */
49
+ isHealthy(): Promise<boolean>;
50
+ /**
51
+ * Export all games (useful for backups)
52
+ * @returns Array of all games
53
+ */
54
+ exportAll?(): Promise<GameRecord[]>;
55
+ /**
56
+ * Import games from backup
57
+ * @param games Array of games to import
58
+ * @returns Number of imported games
59
+ */
60
+ importGames?(games: GameRecord[]): Promise<number>;
61
+ /**
62
+ * Clear all games (dangerous!)
63
+ * @returns Number of games deleted
64
+ */
65
+ clear?(): Promise<number>;
66
+ }
67
+ /**
68
+ * Check if a game store is reachable, without letting a backend failure escape.
69
+ *
70
+ * Wraps `isHealthy()` so that a driver-level throw (connection refused, expired
71
+ * credentials) reads as "offline" rather than crashing the caller — the intended
72
+ * use is an availability probe on a sync path, not error reporting.
73
+ *
74
+ * @param store - Store to probe.
75
+ * @returns `true` only if `isHealthy()` resolved `true`; `false` if it resolved
76
+ * `false` or threw.
77
+ */
78
+ export declare function isStoreOnline(store: GameStore): Promise<boolean>;
79
+ /**
80
+ * Copy games from one store into another, one record at a time.
81
+ *
82
+ * Best-effort and non-transactional: a game that fails to save is logged and
83
+ * skipped so a single bad record cannot abort the whole sync, which matters when
84
+ * draining an offline queue on reconnect. Records are not deleted from the
85
+ * source, so this is a copy rather than a move.
86
+ *
87
+ * @param fromStore - Source of truth to read from.
88
+ * @param toStore - Destination the games are written into.
89
+ * @param options - `userId` restricts the copy to one owner's games;
90
+ * `overwrite` deletes the destination record first so the source version wins
91
+ * outright instead of being merged by the destination's upsert semantics.
92
+ * @returns How many games were saved successfully — may be fewer than the number
93
+ * read from the source.
94
+ */
95
+ export declare function syncStores(fromStore: GameStore, toStore: GameStore, options?: {
96
+ userId?: string;
97
+ overwrite?: boolean;
98
+ }): Promise<number>;
99
+ //# sourceMappingURL=game-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-store.d.ts","sourceRoot":"","sources":["../../src/types/game-store.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAE1C;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtC;;;;OAIG;IACH,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAE7D;;;;OAIG;IACH,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAExD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,WAAW,CAAC;IAErE;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9B;;;OAGG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAEpC;;;;OAIG;IACH,WAAW,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnD;;;OAGG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAMtE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAC9B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,SAAS,EAClB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD,OAAO,CAAC,MAAM,CAAC,CA0BjB"}