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,211 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// HyperChess Core — hyperchess-store
|
|
3
|
+
// File: packages/store/src/adapters/memory.ts
|
|
4
|
+
// Version: 1.0.0
|
|
5
|
+
// Copyright (c) 2026 HyperChess Developer Team
|
|
6
|
+
import { GameNotFoundError } from '../types/game-record';
|
|
7
|
+
/**
|
|
8
|
+
* In-memory game store for testing and development.
|
|
9
|
+
*
|
|
10
|
+
* Backed by a plain `Map`, so all data is lost when the process exits. It is the
|
|
11
|
+
* package default export and the reference implementation of {@link GameStore}:
|
|
12
|
+
* every other adapter is expected to match its observable behaviour.
|
|
13
|
+
*
|
|
14
|
+
* Records are deep-copied with `structuredClone` on the way out (and into
|
|
15
|
+
* watcher callbacks) so a caller mutating a returned object cannot corrupt the
|
|
16
|
+
* store — a hazard the database adapters don't have because they deserialise
|
|
17
|
+
* fresh rows on every read.
|
|
18
|
+
*/
|
|
19
|
+
export class MemoryGameStore {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.games = new Map();
|
|
22
|
+
this.watchers = new Map();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Insert or replace a game, stamping `updatedAt` with the current time.
|
|
26
|
+
*
|
|
27
|
+
* The record fully replaces any existing entry rather than merging into it,
|
|
28
|
+
* and watchers of this id are notified synchronously before returning.
|
|
29
|
+
*
|
|
30
|
+
* @param game - Record to store; if `id` is empty a new one is generated.
|
|
31
|
+
* @returns The id the game was stored under.
|
|
32
|
+
*/
|
|
33
|
+
async saveGame(game) {
|
|
34
|
+
// Generate ID if not provided
|
|
35
|
+
const id = game.id || this.generateId();
|
|
36
|
+
const recordWithId = {
|
|
37
|
+
...game,
|
|
38
|
+
id,
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
this.games.set(id, recordWithId);
|
|
42
|
+
// Notify watchers
|
|
43
|
+
this.notifyWatchers(id, recordWithId);
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fetch a game by id.
|
|
48
|
+
*
|
|
49
|
+
* @param id - Game id to look up.
|
|
50
|
+
* @returns A deep copy of the stored record, safe for the caller to mutate.
|
|
51
|
+
* @throws {@link GameNotFoundError} if no game is stored under `id`.
|
|
52
|
+
*/
|
|
53
|
+
async loadGame(id) {
|
|
54
|
+
const game = this.games.get(id);
|
|
55
|
+
if (!game) {
|
|
56
|
+
throw new GameNotFoundError(id);
|
|
57
|
+
}
|
|
58
|
+
return structuredClone(game); // Deep copy to prevent mutations
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove a game and drop every watcher registered against it.
|
|
62
|
+
*
|
|
63
|
+
* Watchers are discarded silently — they receive no final notification, so a
|
|
64
|
+
* subscriber must treat "no further updates" as a possible deletion.
|
|
65
|
+
*
|
|
66
|
+
* @param id - Game id to remove.
|
|
67
|
+
* @throws {@link GameNotFoundError} if no game is stored under `id`.
|
|
68
|
+
*/
|
|
69
|
+
async deleteGame(id) {
|
|
70
|
+
if (!this.games.has(id)) {
|
|
71
|
+
throw new GameNotFoundError(id);
|
|
72
|
+
}
|
|
73
|
+
this.games.delete(id);
|
|
74
|
+
// Notify watchers
|
|
75
|
+
const watchers = this.watchers.get(id);
|
|
76
|
+
if (watchers) {
|
|
77
|
+
this.watchers.delete(id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* List games, filtering, sorting and paginating entirely in memory.
|
|
82
|
+
*
|
|
83
|
+
* Sorting is by `createdAt`, treating a missing timestamp as the epoch so
|
|
84
|
+
* undated records sort oldest. Note the default order is ascending here,
|
|
85
|
+
* whereas the SQL-backed adapters default to descending.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Filters (`userId`, `result`), `sort` direction and
|
|
88
|
+
* `offset`/`limit` window; omitting `limit` returns everything after
|
|
89
|
+
* `offset`.
|
|
90
|
+
* @returns Deep copies of the matching records.
|
|
91
|
+
*/
|
|
92
|
+
async listGames(options) {
|
|
93
|
+
let games = Array.from(this.games.values());
|
|
94
|
+
// Filter by userId if provided
|
|
95
|
+
if (options?.userId) {
|
|
96
|
+
games = games.filter((g) => g.userId === options.userId);
|
|
97
|
+
}
|
|
98
|
+
// Filter by result if provided
|
|
99
|
+
if (options?.result) {
|
|
100
|
+
games = games.filter((g) => g.result === options.result);
|
|
101
|
+
}
|
|
102
|
+
// Sort by date
|
|
103
|
+
const sortOrder = options?.sort === 'desc' ? -1 : 1;
|
|
104
|
+
games.sort((a, b) => {
|
|
105
|
+
const dateA = new Date(a.createdAt || 0).getTime();
|
|
106
|
+
const dateB = new Date(b.createdAt || 0).getTime();
|
|
107
|
+
return sortOrder * (dateA - dateB);
|
|
108
|
+
});
|
|
109
|
+
// Apply pagination
|
|
110
|
+
const offset = options?.offset || 0;
|
|
111
|
+
const limit = options?.limit || games.length;
|
|
112
|
+
games = games.slice(offset, offset + limit);
|
|
113
|
+
return games.map((g) => structuredClone(g));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Count games matching the filters.
|
|
117
|
+
*
|
|
118
|
+
* `limit` and `offset` are deliberately ignored so the result is the total
|
|
119
|
+
* size of the match set, not the size of one page.
|
|
120
|
+
*
|
|
121
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
122
|
+
* @returns Number of matching records.
|
|
123
|
+
*/
|
|
124
|
+
async countGames(options) {
|
|
125
|
+
let games = Array.from(this.games.values());
|
|
126
|
+
if (options?.userId) {
|
|
127
|
+
games = games.filter((g) => g.userId === options.userId);
|
|
128
|
+
}
|
|
129
|
+
if (options?.result) {
|
|
130
|
+
games = games.filter((g) => g.result === options.result);
|
|
131
|
+
}
|
|
132
|
+
return games.length;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Subscribe to writes for a single game.
|
|
136
|
+
*
|
|
137
|
+
* Fires only on subsequent `saveGame()` calls — there is no initial emission
|
|
138
|
+
* of the current value, and deletions produce no event.
|
|
139
|
+
*
|
|
140
|
+
* @param id - Game id to observe. Watching an id that does not exist yet is
|
|
141
|
+
* valid; the callback fires when it is first saved.
|
|
142
|
+
* @param callback - Receives a deep copy of the saved record.
|
|
143
|
+
* @returns Unsubscribe handle; the id's watcher set is dropped once empty.
|
|
144
|
+
*/
|
|
145
|
+
watch(id, callback) {
|
|
146
|
+
if (!this.watchers.has(id)) {
|
|
147
|
+
this.watchers.set(id, new Set());
|
|
148
|
+
}
|
|
149
|
+
this.watchers.get(id).add(callback);
|
|
150
|
+
// Return unsubscribe function
|
|
151
|
+
return () => {
|
|
152
|
+
const watchers = this.watchers.get(id);
|
|
153
|
+
if (watchers) {
|
|
154
|
+
watchers.delete(callback);
|
|
155
|
+
if (watchers.size === 0) {
|
|
156
|
+
this.watchers.delete(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Always resolves `true` — there is no backend that can be unreachable.
|
|
163
|
+
*/
|
|
164
|
+
async isHealthy() {
|
|
165
|
+
return true; // In-memory store is always healthy
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Dump every stored game, unfiltered and in insertion order.
|
|
169
|
+
*
|
|
170
|
+
* @returns Deep copies of all records.
|
|
171
|
+
*/
|
|
172
|
+
async exportAll() {
|
|
173
|
+
return Array.from(this.games.values()).map((g) => structuredClone(g));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Bulk-load records, overwriting any existing game with the same id.
|
|
177
|
+
*
|
|
178
|
+
* Each import goes through `saveGame()`, so `updatedAt` is rewritten to now
|
|
179
|
+
* and watchers fire — importing a backup is not a silent restore.
|
|
180
|
+
*
|
|
181
|
+
* @param games - Records to load.
|
|
182
|
+
* @returns The number of records supplied.
|
|
183
|
+
*/
|
|
184
|
+
async importGames(games) {
|
|
185
|
+
for (const game of games) {
|
|
186
|
+
await this.saveGame(game);
|
|
187
|
+
}
|
|
188
|
+
return games.length;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Drop every game and every registered watcher.
|
|
192
|
+
*
|
|
193
|
+
* @returns How many games were removed.
|
|
194
|
+
*/
|
|
195
|
+
async clear() {
|
|
196
|
+
const count = this.games.size;
|
|
197
|
+
this.games.clear();
|
|
198
|
+
this.watchers.clear();
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
notifyWatchers(id, game) {
|
|
202
|
+
const watchers = this.watchers.get(id);
|
|
203
|
+
if (watchers) {
|
|
204
|
+
watchers.forEach((callback) => callback(structuredClone(game)));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
generateId() {
|
|
208
|
+
return `game_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { GameStore } from '../types/game-store';
|
|
2
|
+
import { GameRecord, GameQueryOptions, Unsubscribe } from '../types/game-record';
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL adapter for production game storage
|
|
5
|
+
* Requires: npm install pg
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Connection pooling
|
|
9
|
+
* - LISTEN/NOTIFY for real-time updates
|
|
10
|
+
* - Full-text search ready
|
|
11
|
+
* - Automatic timestamp management
|
|
12
|
+
*
|
|
13
|
+
* Schema assumption: a `games` table must already exist — unlike the SQLite
|
|
14
|
+
* adapter this one never runs DDL, so migrations are the deployment's job. Its
|
|
15
|
+
* columns are `id` (primary key), `hpgn`, `hfen`, `players`, `metadata`,
|
|
16
|
+
* `result`, `user_id`, `synced`, `created_at`, `updated_at`. `players` and
|
|
17
|
+
* `metadata` are written as JSON strings and read back with `JSON.parse`, so
|
|
18
|
+
* they must be `text`/`json` rather than `jsonb` (a `jsonb` column would be
|
|
19
|
+
* returned pre-parsed by `pg` and fail to parse again). The timestamp columns
|
|
20
|
+
* are read as `Date` objects and converted to ISO strings.
|
|
21
|
+
*
|
|
22
|
+
* Real-time updates rely on a database-side trigger publishing
|
|
23
|
+
* `NOTIFY game_updates` with a `{ id, game }` JSON payload; without that trigger
|
|
24
|
+
* `watch()` still fires locally for writes made through this instance.
|
|
25
|
+
*/
|
|
26
|
+
export declare class PostgresGameStore implements GameStore {
|
|
27
|
+
private pool;
|
|
28
|
+
private listeners;
|
|
29
|
+
private client;
|
|
30
|
+
/**
|
|
31
|
+
* Open a connection pool and begin listening for `game_updates` notifications.
|
|
32
|
+
*
|
|
33
|
+
* The pool is capped at 20 connections with a 2s connect timeout and 30s idle
|
|
34
|
+
* timeout. One extra connection is checked out and held for the lifetime of
|
|
35
|
+
* the store to service `LISTEN`; call {@link PostgresGameStore.close} to give
|
|
36
|
+
* it back.
|
|
37
|
+
*
|
|
38
|
+
* @param connectionString - Standard `postgres://` DSN passed to `pg.Pool`.
|
|
39
|
+
* @throws Error if the optional `pg` package is not installed.
|
|
40
|
+
*/
|
|
41
|
+
constructor(connectionString: string);
|
|
42
|
+
/**
|
|
43
|
+
* Dedicate a pooled client to `LISTEN game_updates` and fan payloads out to
|
|
44
|
+
* local watchers. Failures are logged rather than thrown so a database without
|
|
45
|
+
* the notification trigger still yields a usable store.
|
|
46
|
+
*/
|
|
47
|
+
private setupNotifications;
|
|
48
|
+
/**
|
|
49
|
+
* Upsert a game via `INSERT ... ON CONFLICT (id) DO UPDATE`.
|
|
50
|
+
*
|
|
51
|
+
* `updated_at` is always set to now. `created_at` is only meaningful on the
|
|
52
|
+
* insert path — the conflict branch deliberately leaves it untouched so the
|
|
53
|
+
* original creation time survives edits. `synced` defaults to `true` here,
|
|
54
|
+
* the opposite of the SQLite adapter, because a successful write to the
|
|
55
|
+
* central database *is* the synced state.
|
|
56
|
+
*
|
|
57
|
+
* @param game - Record to persist; a missing `id` is generated locally.
|
|
58
|
+
* @returns The id reported back by the `RETURNING` clause.
|
|
59
|
+
*/
|
|
60
|
+
saveGame(game: GameRecord): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Fetch one row by primary key.
|
|
63
|
+
*
|
|
64
|
+
* @param id - Game id.
|
|
65
|
+
* @returns The row mapped back to camelCase {@link GameRecord} shape.
|
|
66
|
+
* @throws {@link GameNotFoundError} if the query returns no rows.
|
|
67
|
+
*/
|
|
68
|
+
loadGame(id: string): Promise<GameRecord>;
|
|
69
|
+
/**
|
|
70
|
+
* Delete one row and forget any watchers registered for it.
|
|
71
|
+
*
|
|
72
|
+
* @param id - Game id.
|
|
73
|
+
* @throws {@link GameNotFoundError} if the `DELETE` affected no rows.
|
|
74
|
+
*/
|
|
75
|
+
deleteGame(id: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* List games with SQL-side filtering, ordering and pagination.
|
|
78
|
+
*
|
|
79
|
+
* Filters are appended as parameterised predicates, never string-interpolated.
|
|
80
|
+
* Ordering is by `created_at` and defaults to descending — the reverse of the
|
|
81
|
+
* memory adapter's default.
|
|
82
|
+
*
|
|
83
|
+
* @param options - `userId`/`result` filters, `sort` direction, `limit` and
|
|
84
|
+
* `offset`. An `offset` without a `limit` is passed through to Postgres,
|
|
85
|
+
* which permits it.
|
|
86
|
+
* @returns Matching rows in the requested order.
|
|
87
|
+
*/
|
|
88
|
+
listGames(options?: GameQueryOptions): Promise<GameRecord[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Count matching rows with `COUNT(*)`, ignoring `limit`/`offset`.
|
|
91
|
+
*
|
|
92
|
+
* Postgres returns `count` as a bigint string, so the result is parsed to a
|
|
93
|
+
* number before being handed back.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Only `userId` and `result` are honoured.
|
|
96
|
+
* @returns Total number of matching rows.
|
|
97
|
+
*/
|
|
98
|
+
countGames(options?: GameQueryOptions): Promise<number>;
|
|
99
|
+
/**
|
|
100
|
+
* Subscribe to changes for one game.
|
|
101
|
+
*
|
|
102
|
+
* Two paths feed the callback: writes made through this instance notify
|
|
103
|
+
* directly, and writes made by other processes arrive over the `game_updates`
|
|
104
|
+
* `LISTEN` channel — the latter only if the database publishes them. Watching
|
|
105
|
+
* is purely local bookkeeping and issues no query, so it is cheap.
|
|
106
|
+
*
|
|
107
|
+
* @param id - Game id to observe.
|
|
108
|
+
* @param callback - Receives the updated record.
|
|
109
|
+
* @returns Unsubscribe handle.
|
|
110
|
+
*/
|
|
111
|
+
watch(id: string, callback: (game: GameRecord) => void): Unsubscribe;
|
|
112
|
+
/**
|
|
113
|
+
* Probe the pool with `SELECT 1`.
|
|
114
|
+
*
|
|
115
|
+
* Confirms connectivity only — it does not verify that the `games` table
|
|
116
|
+
* exists or is readable.
|
|
117
|
+
*
|
|
118
|
+
* @returns `false` instead of throwing if the query fails.
|
|
119
|
+
*/
|
|
120
|
+
isHealthy(): Promise<boolean>;
|
|
121
|
+
/**
|
|
122
|
+
* Read every row, newest first, in a single unpaginated query.
|
|
123
|
+
*
|
|
124
|
+
* Materialises the whole table in memory — fine for backups, unsuitable for
|
|
125
|
+
* very large datasets.
|
|
126
|
+
*
|
|
127
|
+
* @returns All stored games.
|
|
128
|
+
*/
|
|
129
|
+
exportAll(): Promise<GameRecord[]>;
|
|
130
|
+
/**
|
|
131
|
+
* Upsert a batch of games one statement at a time.
|
|
132
|
+
*
|
|
133
|
+
* Not wrapped in a transaction: a row that fails is logged and skipped, so a
|
|
134
|
+
* partial import is a possible outcome.
|
|
135
|
+
*
|
|
136
|
+
* @param games - Records to import.
|
|
137
|
+
* @returns How many were saved successfully.
|
|
138
|
+
*/
|
|
139
|
+
importGames(games: GameRecord[]): Promise<number>;
|
|
140
|
+
/**
|
|
141
|
+
* Delete every row in the `games` table and drop all local watchers.
|
|
142
|
+
*
|
|
143
|
+
* @returns Number of rows deleted.
|
|
144
|
+
*/
|
|
145
|
+
clear(): Promise<number>;
|
|
146
|
+
/**
|
|
147
|
+
* Map a snake_case database row to a {@link GameRecord}, parsing the JSON
|
|
148
|
+
* columns and rendering `timestamptz` values as ISO 8601 strings.
|
|
149
|
+
*/
|
|
150
|
+
private rowToRecord;
|
|
151
|
+
private generateId;
|
|
152
|
+
/**
|
|
153
|
+
* Release the `LISTEN` client and drain the pool.
|
|
154
|
+
*
|
|
155
|
+
* Must be called for the process to exit cleanly — the held notification
|
|
156
|
+
* client keeps an open socket that would otherwise pin the event loop. The
|
|
157
|
+
* store is unusable afterwards.
|
|
158
|
+
*/
|
|
159
|
+
close(): Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAqB,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpG;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IACjD,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,SAAS,CAA2D;IAC5E,OAAO,CAAC,MAAM,CAAM;IAEpB;;;;;;;;;;OAUG;gBACS,gBAAgB,EAAE,MAAM;IAkBpC;;;;OAIG;YACW,kBAAkB;IAqBhC;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAoCjD;;;;;;OAMG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAU/C;;;;;OAKG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU3C;;;;;;;;;;;OAWG;IACG,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAkClE;;;;;;;;OAQG;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;IAkBpE;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IASnC;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAKxC;;;;;;;;OAQG;IACG,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAevD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAM9B;;;OAGG;IACH,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,UAAU;IAIlB;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B"}
|