@topgunbuild/client 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,367 @@
1
+ import { LWWRecord, ORMapRecord, PredicateNode, LWWMap, ORMap, Timestamp, HLC } from '@topgunbuild/core';
2
+ export { LWWMap, LWWRecord, PredicateNode, Predicates } from '@topgunbuild/core';
3
+ import pino from 'pino';
4
+
5
+ interface OpLogEntry {
6
+ id?: number;
7
+ key: string;
8
+ op: 'PUT' | 'REMOVE' | 'OR_ADD' | 'OR_REMOVE';
9
+ value?: any;
10
+ record?: LWWRecord<any>;
11
+ orRecord?: ORMapRecord<any>;
12
+ orTag?: string;
13
+ hlc?: string;
14
+ timestamp?: any;
15
+ synced: number;
16
+ mapName: string;
17
+ }
18
+ interface IStorageAdapter {
19
+ initialize(dbName: string): Promise<void>;
20
+ close(): Promise<void>;
21
+ /**
22
+ * Waits for the storage adapter to be fully initialized.
23
+ * Optional - adapters that initialize synchronously may return immediately.
24
+ */
25
+ waitForReady?(): Promise<void>;
26
+ get<V>(key: string): Promise<LWWRecord<V> | ORMapRecord<V>[] | any | undefined>;
27
+ put(key: string, value: any): Promise<void>;
28
+ remove(key: string): Promise<void>;
29
+ getMeta(key: string): Promise<any>;
30
+ setMeta(key: string, value: any): Promise<void>;
31
+ batchPut(entries: Map<string, any>): Promise<void>;
32
+ appendOpLog(entry: Omit<OpLogEntry, 'id'>): Promise<number>;
33
+ getPendingOps(): Promise<OpLogEntry[]>;
34
+ markOpsSynced(lastId: number): Promise<void>;
35
+ getAllKeys(): Promise<string[]>;
36
+ }
37
+
38
+ interface QueryFilter {
39
+ where?: Record<string, any>;
40
+ predicate?: PredicateNode;
41
+ sort?: Record<string, 'asc' | 'desc'>;
42
+ limit?: number;
43
+ offset?: number;
44
+ }
45
+ /** Source of query results for proper handling of race conditions */
46
+ type QueryResultSource = 'local' | 'server';
47
+ /** Result item with _key field for client-side lookups */
48
+ type QueryResultItem<T> = T & {
49
+ _key: string;
50
+ };
51
+ declare class QueryHandle<T> {
52
+ readonly id: string;
53
+ private syncEngine;
54
+ private mapName;
55
+ private filter;
56
+ private listeners;
57
+ private currentResults;
58
+ constructor(syncEngine: SyncEngine, mapName: string, filter?: QueryFilter);
59
+ subscribe(callback: (results: QueryResultItem<T>[]) => void): () => void;
60
+ private loadInitialLocalData;
61
+ private hasReceivedServerData;
62
+ /**
63
+ * Called by SyncEngine when server sends initial results or by local storage load.
64
+ * Uses merge strategy instead of clear to prevent UI flickering.
65
+ *
66
+ * @param items - Array of key-value pairs
67
+ * @param source - 'local' for IndexedDB data, 'server' for QUERY_RESP from server
68
+ *
69
+ * Race condition protection:
70
+ * - Empty server responses are ignored until we receive non-empty server data
71
+ * - This prevents clearing local data when server hasn't loaded from storage yet
72
+ * - Works with any async storage adapter (PostgreSQL, SQLite, Redis, etc.)
73
+ */
74
+ onResult(items: {
75
+ key: string;
76
+ value: T;
77
+ }[], source?: QueryResultSource): void;
78
+ /**
79
+ * Called by SyncEngine when server sends a live update
80
+ */
81
+ onUpdate(key: string, value: T | null): void;
82
+ private notify;
83
+ private getSortedResults;
84
+ getFilter(): QueryFilter;
85
+ getMapName(): string;
86
+ }
87
+
88
+ type TopicCallback = (data: any, context: {
89
+ timestamp: number;
90
+ publisherId?: string;
91
+ }) => void;
92
+ declare class TopicHandle {
93
+ private engine;
94
+ private topic;
95
+ private listeners;
96
+ constructor(engine: SyncEngine, topic: string);
97
+ get id(): string;
98
+ /**
99
+ * Publish a message to the topic
100
+ */
101
+ publish(data: any): void;
102
+ /**
103
+ * Subscribe to the topic
104
+ */
105
+ subscribe(callback: TopicCallback): () => void;
106
+ private unsubscribe;
107
+ /**
108
+ * Called by SyncEngine when a message is received
109
+ */
110
+ onMessage(data: any, context: {
111
+ timestamp: number;
112
+ publisherId?: string;
113
+ }): void;
114
+ }
115
+
116
+ interface SyncEngineConfig {
117
+ nodeId: string;
118
+ serverUrl: string;
119
+ storageAdapter: IStorageAdapter;
120
+ reconnectInterval?: number;
121
+ }
122
+ declare class SyncEngine {
123
+ private readonly nodeId;
124
+ private readonly serverUrl;
125
+ private readonly storageAdapter;
126
+ private readonly reconnectInterval;
127
+ private readonly hlc;
128
+ private websocket;
129
+ private isOnline;
130
+ private isAuthenticated;
131
+ private opLog;
132
+ private maps;
133
+ private queries;
134
+ private topics;
135
+ private pendingLockRequests;
136
+ private lastSyncTimestamp;
137
+ private reconnectTimer;
138
+ private authToken;
139
+ private tokenProvider;
140
+ constructor(config: SyncEngineConfig);
141
+ private initConnection;
142
+ private scheduleReconnect;
143
+ private loadOpLog;
144
+ private saveOpLog;
145
+ registerMap(mapName: string, map: LWWMap<any, any> | ORMap<any, any>): void;
146
+ recordOperation(mapName: string, opType: 'PUT' | 'REMOVE' | 'OR_ADD' | 'OR_REMOVE', key: string, data: {
147
+ record?: LWWRecord<any>;
148
+ orRecord?: ORMapRecord<any>;
149
+ orTag?: string;
150
+ timestamp: Timestamp;
151
+ }): Promise<void>;
152
+ private syncPendingOperations;
153
+ private startMerkleSync;
154
+ setAuthToken(token: string): void;
155
+ setTokenProvider(provider: () => Promise<string | null>): void;
156
+ private sendAuth;
157
+ subscribeToQuery(query: QueryHandle<any>): void;
158
+ subscribeToTopic(topic: string, handle: TopicHandle): void;
159
+ unsubscribeFromTopic(topic: string): void;
160
+ publishTopic(topic: string, data: any): void;
161
+ private sendTopicSubscription;
162
+ /**
163
+ * Executes a query against local storage immediately
164
+ */
165
+ runLocalQuery(mapName: string, filter: QueryFilter): Promise<{
166
+ key: string;
167
+ value: any;
168
+ }[]>;
169
+ unsubscribeFromQuery(queryId: string): void;
170
+ private sendQuerySubscription;
171
+ requestLock(name: string, requestId: string, ttl: number): Promise<{
172
+ fencingToken: number;
173
+ }>;
174
+ releaseLock(name: string, requestId: string, fencingToken: number): Promise<boolean>;
175
+ private handleServerMessage;
176
+ getHLC(): HLC;
177
+ /**
178
+ * Closes the WebSocket connection and cleans up resources.
179
+ */
180
+ close(): void;
181
+ private resetMap;
182
+ }
183
+
184
+ interface ILock {
185
+ lock(ttl?: number): Promise<boolean>;
186
+ unlock(): Promise<void>;
187
+ isLocked(): boolean;
188
+ }
189
+ declare class DistributedLock implements ILock {
190
+ private syncEngine;
191
+ private name;
192
+ private fencingToken;
193
+ private _isLocked;
194
+ constructor(syncEngine: SyncEngine, name: string);
195
+ lock(ttl?: number): Promise<boolean>;
196
+ unlock(): Promise<void>;
197
+ isLocked(): boolean;
198
+ }
199
+
200
+ declare class TopGunClient {
201
+ private readonly nodeId;
202
+ private readonly syncEngine;
203
+ private readonly maps;
204
+ private readonly storageAdapter;
205
+ private readonly topicHandles;
206
+ constructor(config: {
207
+ nodeId?: string;
208
+ serverUrl: string;
209
+ storage: IStorageAdapter;
210
+ });
211
+ start(): Promise<void>;
212
+ setAuthToken(token: string): void;
213
+ setAuthTokenProvider(provider: () => Promise<string | null>): void;
214
+ /**
215
+ * Creates a live query subscription for a map.
216
+ */
217
+ query<T>(mapName: string, filter: QueryFilter): QueryHandle<T>;
218
+ /**
219
+ * Retrieves a distributed lock instance.
220
+ * @param name The name of the lock.
221
+ */
222
+ getLock(name: string): DistributedLock;
223
+ /**
224
+ * Retrieves a topic handle for Pub/Sub messaging.
225
+ * @param name The name of the topic.
226
+ */
227
+ topic(name: string): TopicHandle;
228
+ /**
229
+ * Retrieves an LWWMap instance. If the map doesn't exist locally, it's created.
230
+ * @param name The name of the map.
231
+ * @returns An LWWMap instance.
232
+ */
233
+ getMap<K, V>(name: string): LWWMap<K, V>;
234
+ /**
235
+ * Retrieves an ORMap instance. If the map doesn't exist locally, it's created.
236
+ * @param name The name of the map.
237
+ * @returns An ORMap instance.
238
+ */
239
+ getORMap<K, V>(name: string): ORMap<K, V>;
240
+ private restoreORMap;
241
+ private persistORMapKey;
242
+ private persistORMapTombstones;
243
+ /**
244
+ * Closes the client, disconnecting from the server and cleaning up resources.
245
+ */
246
+ close(): void;
247
+ }
248
+
249
+ interface TopGunConfig {
250
+ sync: string;
251
+ persist: 'indexeddb' | IStorageAdapter;
252
+ nodeId?: string;
253
+ }
254
+ type TopGunSchema = Record<string, any>;
255
+ declare class TopGun<T extends TopGunSchema = any> {
256
+ private client;
257
+ private initPromise;
258
+ [key: string]: any;
259
+ constructor(config: TopGunConfig);
260
+ /**
261
+ * Waits for the storage adapter to be fully initialized.
262
+ * This is optional - you can start using the database immediately.
263
+ * Operations are queued in memory and persisted once IndexedDB is ready.
264
+ */
265
+ waitForReady(): Promise<void>;
266
+ collection<K extends keyof T & string>(name: K): CollectionWrapper<T[K]>;
267
+ }
268
+ declare class CollectionWrapper<ItemType = any> {
269
+ private map;
270
+ constructor(map: LWWMap<string, ItemType>);
271
+ /**
272
+ * Sets an item in the collection.
273
+ * The item MUST have an 'id' or '_id' field.
274
+ */
275
+ set(value: ItemType): Promise<ItemType>;
276
+ /**
277
+ * Retrieves an item by ID.
278
+ * Returns the value directly (unwrapped from CRDT record).
279
+ */
280
+ get(key: string): ItemType | undefined;
281
+ /**
282
+ * Get the raw LWWRecord (including metadata like timestamp).
283
+ */
284
+ getRecord(key: string): LWWRecord<ItemType> | undefined;
285
+ get raw(): LWWMap<string, ItemType>;
286
+ }
287
+
288
+ /**
289
+ * Non-blocking IndexedDB adapter that allows immediate use before initialization completes.
290
+ *
291
+ * Operations are queued in memory and replayed once IndexedDB is ready.
292
+ * This enables true "memory-first" behavior where the UI can render immediately
293
+ * without waiting for IndexedDB to initialize (which can take 50-500ms).
294
+ */
295
+ declare class IDBAdapter implements IStorageAdapter {
296
+ private dbPromise?;
297
+ private db?;
298
+ private isReady;
299
+ private operationQueue;
300
+ private initPromise?;
301
+ /**
302
+ * Initializes IndexedDB in the background.
303
+ * Returns immediately - does NOT block on IndexedDB being ready.
304
+ * Use waitForReady() if you need to ensure initialization is complete.
305
+ */
306
+ initialize(dbName: string): Promise<void>;
307
+ /**
308
+ * Internal initialization that actually opens IndexedDB.
309
+ */
310
+ private initializeInternal;
311
+ /**
312
+ * Waits for IndexedDB to be fully initialized.
313
+ * Call this if you need guaranteed persistence before proceeding.
314
+ */
315
+ waitForReady(): Promise<void>;
316
+ /**
317
+ * Flushes all queued operations once IndexedDB is ready.
318
+ */
319
+ private flushQueue;
320
+ /**
321
+ * Queues an operation if not ready, or executes immediately if ready.
322
+ */
323
+ private queueOrExecute;
324
+ close(): Promise<void>;
325
+ get<V>(key: string): Promise<LWWRecord<V> | ORMapRecord<V>[] | any | undefined>;
326
+ getMeta(key: string): Promise<any>;
327
+ getPendingOps(): Promise<OpLogEntry[]>;
328
+ getAllKeys(): Promise<string[]>;
329
+ put(key: string, value: any): Promise<void>;
330
+ private putInternal;
331
+ remove(key: string): Promise<void>;
332
+ private removeInternal;
333
+ setMeta(key: string, value: any): Promise<void>;
334
+ private setMetaInternal;
335
+ batchPut(entries: Map<string, any>): Promise<void>;
336
+ private batchPutInternal;
337
+ appendOpLog(entry: any): Promise<number>;
338
+ private appendOpLogInternal;
339
+ markOpsSynced(lastId: number): Promise<void>;
340
+ private markOpsSyncedInternal;
341
+ }
342
+
343
+ /**
344
+ * Wraps an underlying storage adapter and encrypts data at rest using AES-GCM.
345
+ */
346
+ declare class EncryptedStorageAdapter implements IStorageAdapter {
347
+ private wrapped;
348
+ private key;
349
+ constructor(wrapped: IStorageAdapter, key: CryptoKey);
350
+ initialize(dbName: string): Promise<void>;
351
+ close(): Promise<void>;
352
+ get<V>(key: string): Promise<V | any | undefined>;
353
+ put(key: string, value: any): Promise<void>;
354
+ remove(key: string): Promise<void>;
355
+ getMeta(key: string): Promise<any>;
356
+ setMeta(key: string, value: any): Promise<void>;
357
+ batchPut(entries: Map<string, any>): Promise<void>;
358
+ appendOpLog(entry: Omit<OpLogEntry, 'id'>): Promise<number>;
359
+ getPendingOps(): Promise<OpLogEntry[]>;
360
+ markOpsSynced(lastId: number): Promise<void>;
361
+ getAllKeys(): Promise<string[]>;
362
+ private isEncryptedRecord;
363
+ }
364
+
365
+ declare const logger: pino.Logger<never, boolean>;
366
+
367
+ export { EncryptedStorageAdapter, IDBAdapter, type IStorageAdapter, type OpLogEntry, type QueryFilter, QueryHandle, type QueryResultItem, type QueryResultSource, SyncEngine, TopGun, TopGunClient, type TopicCallback, TopicHandle, logger };