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,341 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// HyperChess Core — hyperchess-store
|
|
3
|
+
// File: packages/store/src/adapters/postgres.ts
|
|
4
|
+
// Version: 1.0.0
|
|
5
|
+
// Copyright (c) 2026 HyperChess Developer Team
|
|
6
|
+
import { GameNotFoundError } from '../types/game-record';
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL adapter for production game storage
|
|
9
|
+
* Requires: npm install pg
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Connection pooling
|
|
13
|
+
* - LISTEN/NOTIFY for real-time updates
|
|
14
|
+
* - Full-text search ready
|
|
15
|
+
* - Automatic timestamp management
|
|
16
|
+
*
|
|
17
|
+
* Schema assumption: a `games` table must already exist — unlike the SQLite
|
|
18
|
+
* adapter this one never runs DDL, so migrations are the deployment's job. Its
|
|
19
|
+
* columns are `id` (primary key), `hpgn`, `hfen`, `players`, `metadata`,
|
|
20
|
+
* `result`, `user_id`, `synced`, `created_at`, `updated_at`. `players` and
|
|
21
|
+
* `metadata` are written as JSON strings and read back with `JSON.parse`, so
|
|
22
|
+
* they must be `text`/`json` rather than `jsonb` (a `jsonb` column would be
|
|
23
|
+
* returned pre-parsed by `pg` and fail to parse again). The timestamp columns
|
|
24
|
+
* are read as `Date` objects and converted to ISO strings.
|
|
25
|
+
*
|
|
26
|
+
* Real-time updates rely on a database-side trigger publishing
|
|
27
|
+
* `NOTIFY game_updates` with a `{ id, game }` JSON payload; without that trigger
|
|
28
|
+
* `watch()` still fires locally for writes made through this instance.
|
|
29
|
+
*/
|
|
30
|
+
export class PostgresGameStore {
|
|
31
|
+
/**
|
|
32
|
+
* Open a connection pool and begin listening for `game_updates` notifications.
|
|
33
|
+
*
|
|
34
|
+
* The pool is capped at 20 connections with a 2s connect timeout and 30s idle
|
|
35
|
+
* timeout. One extra connection is checked out and held for the lifetime of
|
|
36
|
+
* the store to service `LISTEN`; call {@link PostgresGameStore.close} to give
|
|
37
|
+
* it back.
|
|
38
|
+
*
|
|
39
|
+
* @param connectionString - Standard `postgres://` DSN passed to `pg.Pool`.
|
|
40
|
+
* @throws Error if the optional `pg` package is not installed.
|
|
41
|
+
*/
|
|
42
|
+
constructor(connectionString) {
|
|
43
|
+
this.listeners = new Map();
|
|
44
|
+
// Dynamic import to make pg optional
|
|
45
|
+
try {
|
|
46
|
+
const { Pool } = require('pg');
|
|
47
|
+
this.pool = new Pool({
|
|
48
|
+
connectionString,
|
|
49
|
+
max: 20,
|
|
50
|
+
idleTimeoutMillis: 30000,
|
|
51
|
+
connectionTimeoutMillis: 2000,
|
|
52
|
+
});
|
|
53
|
+
// Setup LISTEN for notifications
|
|
54
|
+
this.setupNotifications();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new Error('PostgreSQL adapter requires "pg" package: npm install pg');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Dedicate a pooled client to `LISTEN game_updates` and fan payloads out to
|
|
62
|
+
* local watchers. Failures are logged rather than thrown so a database without
|
|
63
|
+
* the notification trigger still yields a usable store.
|
|
64
|
+
*/
|
|
65
|
+
async setupNotifications() {
|
|
66
|
+
try {
|
|
67
|
+
this.client = await this.pool.connect();
|
|
68
|
+
await this.client.query('LISTEN game_updates');
|
|
69
|
+
this.client.on('notification', (msg) => {
|
|
70
|
+
try {
|
|
71
|
+
const payload = JSON.parse(msg.payload);
|
|
72
|
+
const callbacks = this.listeners.get(payload.id);
|
|
73
|
+
if (callbacks) {
|
|
74
|
+
callbacks.forEach((cb) => cb(payload.game));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.warn('Failed to parse notification:', error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.warn('Failed to setup notifications:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Upsert a game via `INSERT ... ON CONFLICT (id) DO UPDATE`.
|
|
88
|
+
*
|
|
89
|
+
* `updated_at` is always set to now. `created_at` is only meaningful on the
|
|
90
|
+
* insert path — the conflict branch deliberately leaves it untouched so the
|
|
91
|
+
* original creation time survives edits. `synced` defaults to `true` here,
|
|
92
|
+
* the opposite of the SQLite adapter, because a successful write to the
|
|
93
|
+
* central database *is* the synced state.
|
|
94
|
+
*
|
|
95
|
+
* @param game - Record to persist; a missing `id` is generated locally.
|
|
96
|
+
* @returns The id reported back by the `RETURNING` clause.
|
|
97
|
+
*/
|
|
98
|
+
async saveGame(game) {
|
|
99
|
+
const id = game.id || this.generateId();
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
const query = `
|
|
102
|
+
INSERT INTO games (
|
|
103
|
+
id, hpgn, hfen, players, metadata, result, user_id, synced, created_at, updated_at
|
|
104
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
105
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
106
|
+
hpgn = $2, hfen = $3, players = $4, metadata = $5,
|
|
107
|
+
result = $6, synced = $8, updated_at = $10
|
|
108
|
+
RETURNING id
|
|
109
|
+
`;
|
|
110
|
+
const result = await this.pool.query(query, [
|
|
111
|
+
id,
|
|
112
|
+
game.hpgn,
|
|
113
|
+
game.hfen,
|
|
114
|
+
game.players ? JSON.stringify(game.players) : null,
|
|
115
|
+
game.metadata ? JSON.stringify(game.metadata) : null,
|
|
116
|
+
game.result || null,
|
|
117
|
+
game.userId || null,
|
|
118
|
+
game.synced ?? true,
|
|
119
|
+
game.createdAt || now,
|
|
120
|
+
now,
|
|
121
|
+
]);
|
|
122
|
+
// Notify listeners
|
|
123
|
+
if (this.listeners.has(id)) {
|
|
124
|
+
const updated = await this.loadGame(id);
|
|
125
|
+
this.listeners.get(id)?.forEach((cb) => cb(updated));
|
|
126
|
+
}
|
|
127
|
+
return result.rows[0].id;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Fetch one row by primary key.
|
|
131
|
+
*
|
|
132
|
+
* @param id - Game id.
|
|
133
|
+
* @returns The row mapped back to camelCase {@link GameRecord} shape.
|
|
134
|
+
* @throws {@link GameNotFoundError} if the query returns no rows.
|
|
135
|
+
*/
|
|
136
|
+
async loadGame(id) {
|
|
137
|
+
const result = await this.pool.query('SELECT * FROM games WHERE id = $1', [id]);
|
|
138
|
+
if (result.rows.length === 0) {
|
|
139
|
+
throw new GameNotFoundError(id);
|
|
140
|
+
}
|
|
141
|
+
return this.rowToRecord(result.rows[0]);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Delete one row and forget any watchers registered for it.
|
|
145
|
+
*
|
|
146
|
+
* @param id - Game id.
|
|
147
|
+
* @throws {@link GameNotFoundError} if the `DELETE` affected no rows.
|
|
148
|
+
*/
|
|
149
|
+
async deleteGame(id) {
|
|
150
|
+
const result = await this.pool.query('DELETE FROM games WHERE id = $1', [id]);
|
|
151
|
+
if (result.rowCount === 0) {
|
|
152
|
+
throw new GameNotFoundError(id);
|
|
153
|
+
}
|
|
154
|
+
this.listeners.delete(id);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* List games with SQL-side filtering, ordering and pagination.
|
|
158
|
+
*
|
|
159
|
+
* Filters are appended as parameterised predicates, never string-interpolated.
|
|
160
|
+
* Ordering is by `created_at` and defaults to descending — the reverse of the
|
|
161
|
+
* memory adapter's default.
|
|
162
|
+
*
|
|
163
|
+
* @param options - `userId`/`result` filters, `sort` direction, `limit` and
|
|
164
|
+
* `offset`. An `offset` without a `limit` is passed through to Postgres,
|
|
165
|
+
* which permits it.
|
|
166
|
+
* @returns Matching rows in the requested order.
|
|
167
|
+
*/
|
|
168
|
+
async listGames(options) {
|
|
169
|
+
let query = 'SELECT * FROM games WHERE 1=1';
|
|
170
|
+
const params = [];
|
|
171
|
+
let paramCount = 1;
|
|
172
|
+
if (options?.userId) {
|
|
173
|
+
query += ` AND user_id = $${paramCount}`;
|
|
174
|
+
params.push(options.userId);
|
|
175
|
+
paramCount++;
|
|
176
|
+
}
|
|
177
|
+
if (options?.result) {
|
|
178
|
+
query += ` AND result = $${paramCount}`;
|
|
179
|
+
params.push(options.result);
|
|
180
|
+
paramCount++;
|
|
181
|
+
}
|
|
182
|
+
query += ` ORDER BY created_at ${options?.sort === 'asc' ? 'ASC' : 'DESC'}`;
|
|
183
|
+
if (options?.limit) {
|
|
184
|
+
query += ` LIMIT $${paramCount}`;
|
|
185
|
+
params.push(options.limit);
|
|
186
|
+
paramCount++;
|
|
187
|
+
}
|
|
188
|
+
if (options?.offset) {
|
|
189
|
+
query += ` OFFSET $${paramCount}`;
|
|
190
|
+
params.push(options.offset);
|
|
191
|
+
}
|
|
192
|
+
const result = await this.pool.query(query, params);
|
|
193
|
+
return result.rows.map((row) => this.rowToRecord(row));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Count matching rows with `COUNT(*)`, ignoring `limit`/`offset`.
|
|
197
|
+
*
|
|
198
|
+
* Postgres returns `count` as a bigint string, so the result is parsed to a
|
|
199
|
+
* number before being handed back.
|
|
200
|
+
*
|
|
201
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
202
|
+
* @returns Total number of matching rows.
|
|
203
|
+
*/
|
|
204
|
+
async countGames(options) {
|
|
205
|
+
let query = 'SELECT COUNT(*) as count FROM games WHERE 1=1';
|
|
206
|
+
const params = [];
|
|
207
|
+
if (options?.userId) {
|
|
208
|
+
query += ` AND user_id = $1`;
|
|
209
|
+
params.push(options.userId);
|
|
210
|
+
}
|
|
211
|
+
if (options?.result) {
|
|
212
|
+
query += ` AND result = $${params.length + 1}`;
|
|
213
|
+
params.push(options.result);
|
|
214
|
+
}
|
|
215
|
+
const result = await this.pool.query(query, params);
|
|
216
|
+
return parseInt(result.rows[0].count, 10);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Subscribe to changes for one game.
|
|
220
|
+
*
|
|
221
|
+
* Two paths feed the callback: writes made through this instance notify
|
|
222
|
+
* directly, and writes made by other processes arrive over the `game_updates`
|
|
223
|
+
* `LISTEN` channel — the latter only if the database publishes them. Watching
|
|
224
|
+
* is purely local bookkeeping and issues no query, so it is cheap.
|
|
225
|
+
*
|
|
226
|
+
* @param id - Game id to observe.
|
|
227
|
+
* @param callback - Receives the updated record.
|
|
228
|
+
* @returns Unsubscribe handle.
|
|
229
|
+
*/
|
|
230
|
+
watch(id, callback) {
|
|
231
|
+
if (!this.listeners.has(id)) {
|
|
232
|
+
this.listeners.set(id, new Set());
|
|
233
|
+
}
|
|
234
|
+
this.listeners.get(id).add(callback);
|
|
235
|
+
return () => {
|
|
236
|
+
const callbacks = this.listeners.get(id);
|
|
237
|
+
if (callbacks) {
|
|
238
|
+
callbacks.delete(callback);
|
|
239
|
+
if (callbacks.size === 0) {
|
|
240
|
+
this.listeners.delete(id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Probe the pool with `SELECT 1`.
|
|
247
|
+
*
|
|
248
|
+
* Confirms connectivity only — it does not verify that the `games` table
|
|
249
|
+
* exists or is readable.
|
|
250
|
+
*
|
|
251
|
+
* @returns `false` instead of throwing if the query fails.
|
|
252
|
+
*/
|
|
253
|
+
async isHealthy() {
|
|
254
|
+
try {
|
|
255
|
+
await this.pool.query('SELECT 1');
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Read every row, newest first, in a single unpaginated query.
|
|
264
|
+
*
|
|
265
|
+
* Materialises the whole table in memory — fine for backups, unsuitable for
|
|
266
|
+
* very large datasets.
|
|
267
|
+
*
|
|
268
|
+
* @returns All stored games.
|
|
269
|
+
*/
|
|
270
|
+
async exportAll() {
|
|
271
|
+
const result = await this.pool.query('SELECT * FROM games ORDER BY created_at DESC');
|
|
272
|
+
return result.rows.map((row) => this.rowToRecord(row));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Upsert a batch of games one statement at a time.
|
|
276
|
+
*
|
|
277
|
+
* Not wrapped in a transaction: a row that fails is logged and skipped, so a
|
|
278
|
+
* partial import is a possible outcome.
|
|
279
|
+
*
|
|
280
|
+
* @param games - Records to import.
|
|
281
|
+
* @returns How many were saved successfully.
|
|
282
|
+
*/
|
|
283
|
+
async importGames(games) {
|
|
284
|
+
let count = 0;
|
|
285
|
+
for (const game of games) {
|
|
286
|
+
try {
|
|
287
|
+
await this.saveGame(game);
|
|
288
|
+
count++;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.warn(`Failed to import game ${game.id}:`, error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return count;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Delete every row in the `games` table and drop all local watchers.
|
|
298
|
+
*
|
|
299
|
+
* @returns Number of rows deleted.
|
|
300
|
+
*/
|
|
301
|
+
async clear() {
|
|
302
|
+
const result = await this.pool.query('DELETE FROM games');
|
|
303
|
+
this.listeners.clear();
|
|
304
|
+
return result.rowCount;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Map a snake_case database row to a {@link GameRecord}, parsing the JSON
|
|
308
|
+
* columns and rendering `timestamptz` values as ISO 8601 strings.
|
|
309
|
+
*/
|
|
310
|
+
rowToRecord(row) {
|
|
311
|
+
return {
|
|
312
|
+
id: row.id,
|
|
313
|
+
hpgn: row.hpgn,
|
|
314
|
+
hfen: row.hfen,
|
|
315
|
+
players: row.players ? JSON.parse(row.players) : undefined,
|
|
316
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
317
|
+
result: row.result,
|
|
318
|
+
userId: row.user_id,
|
|
319
|
+
synced: row.synced,
|
|
320
|
+
createdAt: row.created_at?.toISOString(),
|
|
321
|
+
updatedAt: row.updated_at?.toISOString(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
generateId() {
|
|
325
|
+
return `game_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Release the `LISTEN` client and drain the pool.
|
|
329
|
+
*
|
|
330
|
+
* Must be called for the process to exit cleanly — the held notification
|
|
331
|
+
* client keeps an open socket that would otherwise pin the event loop. The
|
|
332
|
+
* store is unusable afterwards.
|
|
333
|
+
*/
|
|
334
|
+
async close() {
|
|
335
|
+
if (this.client) {
|
|
336
|
+
await this.client.release();
|
|
337
|
+
}
|
|
338
|
+
await this.pool.end();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { GameStore } from '../types/game-store';
|
|
2
|
+
import { GameRecord, GameQueryOptions, Unsubscribe } from '../types/game-record';
|
|
3
|
+
/**
|
|
4
|
+
* SQLite adapter for mobile and offline-first apps
|
|
5
|
+
* Requires: npm install better-sqlite3
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - WAL mode for concurrent access
|
|
9
|
+
* - Sync queue for offline-first architecture
|
|
10
|
+
* - Automatic timestamp management
|
|
11
|
+
* - Efficient local queries
|
|
12
|
+
*
|
|
13
|
+
* Schema is self-managing: the constructor runs `CREATE TABLE IF NOT EXISTS` for
|
|
14
|
+
* a `games` table plus indexes on `user_id`, `created_at` and `synced`, so no
|
|
15
|
+
* external migration step is needed. `players` and `metadata` are stored as JSON
|
|
16
|
+
* text; timestamps are stored as ISO 8601 strings (not SQLite date types), which
|
|
17
|
+
* is why lexicographic `ORDER BY created_at` sorts chronologically.
|
|
18
|
+
*
|
|
19
|
+
* Unlike the server-backed adapters this one treats `synced` as false by
|
|
20
|
+
* default: rows written locally are pending until something drains
|
|
21
|
+
* {@link SqliteGameStore.getSyncQueue} and calls
|
|
22
|
+
* {@link SqliteGameStore.markSynced}.
|
|
23
|
+
*
|
|
24
|
+
* `better-sqlite3` is synchronous; the async signatures exist purely to satisfy
|
|
25
|
+
* the {@link GameStore} contract and never actually yield.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SqliteGameStore implements GameStore {
|
|
28
|
+
private db;
|
|
29
|
+
private watchers;
|
|
30
|
+
private pollIntervals;
|
|
31
|
+
/**
|
|
32
|
+
* Open (or create) the database file and ensure the schema exists.
|
|
33
|
+
*
|
|
34
|
+
* Sets `journal_mode = WAL` so readers don't block the writer, and
|
|
35
|
+
* `synchronous = NORMAL`, which trades a small durability window on OS crash
|
|
36
|
+
* for markedly faster writes — an acceptable bargain for a local cache whose
|
|
37
|
+
* source of truth lives upstream.
|
|
38
|
+
*
|
|
39
|
+
* @param dbPath - Filesystem path to the database; `:memory:` is accepted for
|
|
40
|
+
* an ephemeral database.
|
|
41
|
+
* @throws Error if the optional `better-sqlite3` package is not installed.
|
|
42
|
+
*/
|
|
43
|
+
constructor(dbPath: string);
|
|
44
|
+
/**
|
|
45
|
+
* Create the `games` table and its lookup indexes if they are missing.
|
|
46
|
+
* Idempotent, so it runs unconditionally on every open.
|
|
47
|
+
*/
|
|
48
|
+
private initSchema;
|
|
49
|
+
/**
|
|
50
|
+
* Write a game with `INSERT OR REPLACE`.
|
|
51
|
+
*
|
|
52
|
+
* This is a whole-row replace, not a merge: columns absent from `game` are
|
|
53
|
+
* reset rather than preserved. `updated_at` is stamped with the current time,
|
|
54
|
+
* and `synced` defaults to 0 so the record enters the offline sync queue.
|
|
55
|
+
*
|
|
56
|
+
* @param game - Record to persist; a missing `id` is generated locally.
|
|
57
|
+
* @returns The id the row was written under.
|
|
58
|
+
*/
|
|
59
|
+
saveGame(game: GameRecord): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch one row by primary key.
|
|
62
|
+
*
|
|
63
|
+
* @param id - Game id.
|
|
64
|
+
* @returns The row mapped to {@link GameRecord} shape, with `synced` coerced
|
|
65
|
+
* from SQLite's integer boolean.
|
|
66
|
+
* @throws {@link GameNotFoundError} if no row matches.
|
|
67
|
+
*/
|
|
68
|
+
loadGame(id: string): Promise<GameRecord>;
|
|
69
|
+
/**
|
|
70
|
+
* Delete a row and tear down its watcher, including the backing poll timer.
|
|
71
|
+
*
|
|
72
|
+
* @param id - Game id.
|
|
73
|
+
* @throws {@link GameNotFoundError} if the statement changed no rows.
|
|
74
|
+
*/
|
|
75
|
+
deleteGame(id: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* List games, filtering and paginating in SQL.
|
|
78
|
+
*
|
|
79
|
+
* Ordering is by `created_at` and defaults to descending. Filter values are
|
|
80
|
+
* bound as parameters, never interpolated into the SQL text.
|
|
81
|
+
*
|
|
82
|
+
* @param options - `userId`/`result` filters, `sort` direction, `limit` and
|
|
83
|
+
* `offset`.
|
|
84
|
+
* @returns Matching rows in the requested order.
|
|
85
|
+
*/
|
|
86
|
+
listGames(options?: GameQueryOptions): Promise<GameRecord[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Count matching rows with `COUNT(*)`, ignoring `limit`/`offset`.
|
|
89
|
+
*
|
|
90
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
91
|
+
* @returns Total number of matching rows.
|
|
92
|
+
*/
|
|
93
|
+
countGames(options?: GameQueryOptions): Promise<number>;
|
|
94
|
+
/**
|
|
95
|
+
* Observe a game by polling.
|
|
96
|
+
*
|
|
97
|
+
* SQLite has no change-notification channel, so the first watcher for an id
|
|
98
|
+
* starts a 1s interval that re-reads the row and pushes it to every callback.
|
|
99
|
+
* Consequences worth knowing: updates arrive with up to a second of latency,
|
|
100
|
+
* the callback fires on every tick whether or not the row actually changed,
|
|
101
|
+
* and if the row disappears the poll stops and all watchers for that id are
|
|
102
|
+
* dropped without a final event.
|
|
103
|
+
*
|
|
104
|
+
* @param id - Game id to observe.
|
|
105
|
+
* @param callback - Receives the current record on each poll.
|
|
106
|
+
* @returns Unsubscribe handle; the interval is cleared once the last callback
|
|
107
|
+
* for the id is removed.
|
|
108
|
+
*/
|
|
109
|
+
watch(id: string, callback: (game: GameRecord) => void): Unsubscribe;
|
|
110
|
+
/**
|
|
111
|
+
* Clear the poll interval for an id and forget its callbacks. Safe to call
|
|
112
|
+
* for an id that is not being watched.
|
|
113
|
+
*/
|
|
114
|
+
private stopWatching;
|
|
115
|
+
/**
|
|
116
|
+
* Probe the open database handle with `SELECT 1`.
|
|
117
|
+
*
|
|
118
|
+
* @returns `false` instead of throwing if the handle is closed or the file is
|
|
119
|
+
* unreadable.
|
|
120
|
+
*/
|
|
121
|
+
isHealthy(): Promise<boolean>;
|
|
122
|
+
/**
|
|
123
|
+
* Return the games still pending upload — every row with `synced = 0`.
|
|
124
|
+
*
|
|
125
|
+
* Intended to be drained on reconnect and pushed to a remote store, marking
|
|
126
|
+
* each one with {@link SqliteGameStore.markSynced} as it lands.
|
|
127
|
+
*
|
|
128
|
+
* @returns Unsynced records, unordered.
|
|
129
|
+
*/
|
|
130
|
+
getSyncQueue(): Promise<GameRecord[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Flag a single game as uploaded, refreshing its `updated_at`.
|
|
133
|
+
*
|
|
134
|
+
* A no-op if the id is unknown — this reports nothing and throws nothing, so
|
|
135
|
+
* callers cannot use it to detect a missing row.
|
|
136
|
+
*
|
|
137
|
+
* @param id - Game id to mark.
|
|
138
|
+
*/
|
|
139
|
+
markSynced(id: string): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Mark every pending game as synced in one statement, without uploading
|
|
142
|
+
* anything. Use only when the queue is known to be reconciled — it discards
|
|
143
|
+
* the record of what still needs pushing. `updated_at` is left alone here.
|
|
144
|
+
*/
|
|
145
|
+
clearSyncQueue(): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Read every row, newest first, in one unpaginated query.
|
|
148
|
+
*
|
|
149
|
+
* @returns All stored games.
|
|
150
|
+
*/
|
|
151
|
+
exportAll(): Promise<GameRecord[]>;
|
|
152
|
+
/**
|
|
153
|
+
* Write a batch of games one statement at a time.
|
|
154
|
+
*
|
|
155
|
+
* Not wrapped in a transaction, so a failure part-way leaves earlier rows
|
|
156
|
+
* committed. Because it routes through `saveGame()`, imported rows land
|
|
157
|
+
* unsynced unless the source record says otherwise.
|
|
158
|
+
*
|
|
159
|
+
* @param games - Records to import.
|
|
160
|
+
* @returns How many were written successfully.
|
|
161
|
+
*/
|
|
162
|
+
importGames(games: GameRecord[]): Promise<number>;
|
|
163
|
+
/**
|
|
164
|
+
* Delete every row and stop all polling watchers.
|
|
165
|
+
*
|
|
166
|
+
* The table and indexes survive; only the data is removed.
|
|
167
|
+
*
|
|
168
|
+
* @returns Number of rows deleted.
|
|
169
|
+
*/
|
|
170
|
+
clear(): Promise<number>;
|
|
171
|
+
/**
|
|
172
|
+
* Map a snake_case row to a {@link GameRecord}, parsing the JSON text columns
|
|
173
|
+
* and coercing SQLite's integer `synced` flag to a boolean. Timestamps pass
|
|
174
|
+
* through unchanged because they are already stored as ISO 8601 strings.
|
|
175
|
+
*/
|
|
176
|
+
private rowToRecord;
|
|
177
|
+
private generateId;
|
|
178
|
+
/**
|
|
179
|
+
* Stop every poll timer and close the database handle.
|
|
180
|
+
*
|
|
181
|
+
* Required for the process to exit cleanly, since active `watch()` intervals
|
|
182
|
+
* would otherwise keep the event loop alive. The store is unusable afterwards.
|
|
183
|
+
*/
|
|
184
|
+
close(): void;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAC/C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,QAAQ,CAA2D;IAC3E,OAAO,CAAC,aAAa,CAA+B;IAEpD;;;;;;;;;;;OAWG;gBACS,MAAM,EAAE,MAAM;IAe1B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAqBlB;;;;;;;;;OASG;IACG,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BjD;;;;;;;OAOG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAW/C;;;;;OAKG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3C;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAgClE;;;;;OAKG;IACG,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB7D;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,WAAW;IAgCpE;;;OAGG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;OAKG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAWnC;;;;;;;OAOG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAK3C;;;;;;;OAOG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3C;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrC;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAKxC;;;;;;;;;OASG;IACG,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAevD;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAS9B;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,UAAU;IAIlB;;;;;OAKG;IACH,KAAK,IAAI,IAAI;CAMd"}
|