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
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|