@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1384 @@
1
+ // src/SyncEngine.ts
2
+ import { HLC, LWWMap, ORMap, serialize, deserialize, evaluatePredicate } from "@topgunbuild/core";
3
+
4
+ // src/utils/logger.ts
5
+ import pino from "pino";
6
+ var isBrowser = typeof window !== "undefined";
7
+ var logLevel = typeof process !== "undefined" && process.env && process.env.LOG_LEVEL || "info";
8
+ var logger = pino({
9
+ level: logLevel,
10
+ transport: !isBrowser && (typeof process !== "undefined" && process.env.NODE_ENV !== "production") ? {
11
+ target: "pino-pretty",
12
+ options: {
13
+ colorize: true,
14
+ translateTime: "SYS:standard",
15
+ ignore: "pid,hostname"
16
+ }
17
+ } : void 0,
18
+ browser: {
19
+ asObject: true
20
+ }
21
+ });
22
+
23
+ // src/SyncEngine.ts
24
+ var SyncEngine = class {
25
+ constructor(config) {
26
+ this.websocket = null;
27
+ this.isOnline = false;
28
+ this.isAuthenticated = false;
29
+ this.opLog = [];
30
+ this.maps = /* @__PURE__ */ new Map();
31
+ this.queries = /* @__PURE__ */ new Map();
32
+ this.topics = /* @__PURE__ */ new Map();
33
+ this.pendingLockRequests = /* @__PURE__ */ new Map();
34
+ this.lastSyncTimestamp = 0;
35
+ this.reconnectTimer = null;
36
+ // NodeJS.Timeout
37
+ this.authToken = null;
38
+ this.tokenProvider = null;
39
+ this.nodeId = config.nodeId;
40
+ this.serverUrl = config.serverUrl;
41
+ this.storageAdapter = config.storageAdapter;
42
+ this.reconnectInterval = config.reconnectInterval || 5e3;
43
+ this.hlc = new HLC(this.nodeId);
44
+ this.initConnection();
45
+ this.loadOpLog();
46
+ }
47
+ initConnection() {
48
+ this.websocket = new WebSocket(this.serverUrl);
49
+ this.websocket.binaryType = "arraybuffer";
50
+ this.websocket.onopen = () => {
51
+ if (this.authToken || this.tokenProvider) {
52
+ logger.info("WebSocket connected. Sending auth...");
53
+ this.isOnline = true;
54
+ this.sendAuth();
55
+ } else {
56
+ logger.info("WebSocket connected. Waiting for auth token...");
57
+ this.isOnline = true;
58
+ }
59
+ };
60
+ this.websocket.onmessage = (event) => {
61
+ let message;
62
+ if (event.data instanceof ArrayBuffer) {
63
+ message = deserialize(new Uint8Array(event.data));
64
+ } else {
65
+ try {
66
+ message = JSON.parse(event.data);
67
+ } catch (e) {
68
+ logger.error({ err: e }, "Failed to parse message");
69
+ return;
70
+ }
71
+ }
72
+ this.handleServerMessage(message);
73
+ };
74
+ this.websocket.onclose = () => {
75
+ logger.info("WebSocket disconnected. Retrying...");
76
+ this.isOnline = false;
77
+ this.isAuthenticated = false;
78
+ this.scheduleReconnect();
79
+ };
80
+ this.websocket.onerror = (error) => {
81
+ logger.error({ err: error }, "WebSocket error");
82
+ };
83
+ }
84
+ scheduleReconnect() {
85
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
86
+ this.reconnectTimer = setTimeout(() => {
87
+ this.initConnection();
88
+ }, this.reconnectInterval);
89
+ }
90
+ async loadOpLog() {
91
+ const storedTimestamp = await this.storageAdapter.getMeta("lastSyncTimestamp");
92
+ if (storedTimestamp) {
93
+ this.lastSyncTimestamp = storedTimestamp;
94
+ }
95
+ const pendingOps = await this.storageAdapter.getPendingOps();
96
+ this.opLog = pendingOps.map((op) => ({
97
+ ...op,
98
+ id: String(op.id),
99
+ synced: false
100
+ }));
101
+ if (this.opLog.length > 0) {
102
+ logger.info({ count: this.opLog.length }, "Loaded pending operations from local storage");
103
+ }
104
+ }
105
+ async saveOpLog() {
106
+ await this.storageAdapter.setMeta("lastSyncTimestamp", this.lastSyncTimestamp);
107
+ }
108
+ registerMap(mapName, map) {
109
+ this.maps.set(mapName, map);
110
+ }
111
+ async recordOperation(mapName, opType, key, data) {
112
+ const opLogEntry = {
113
+ mapName,
114
+ opType,
115
+ key,
116
+ record: data.record,
117
+ orRecord: data.orRecord,
118
+ orTag: data.orTag,
119
+ timestamp: data.timestamp,
120
+ synced: false
121
+ };
122
+ const id = await this.storageAdapter.appendOpLog(opLogEntry);
123
+ opLogEntry.id = String(id);
124
+ this.opLog.push(opLogEntry);
125
+ if (this.isOnline && this.isAuthenticated) {
126
+ this.syncPendingOperations();
127
+ }
128
+ }
129
+ syncPendingOperations() {
130
+ const pending = this.opLog.filter((op) => !op.synced);
131
+ if (pending.length === 0) return;
132
+ logger.info({ count: pending.length }, "Syncing pending operations");
133
+ if (this.websocket?.readyState === WebSocket.OPEN) {
134
+ this.websocket.send(serialize({
135
+ type: "OP_BATCH",
136
+ payload: {
137
+ ops: pending
138
+ }
139
+ }));
140
+ }
141
+ }
142
+ startMerkleSync() {
143
+ for (const [mapName, map] of this.maps) {
144
+ if (map instanceof LWWMap) {
145
+ logger.info({ mapName }, "Starting Merkle sync for map");
146
+ this.websocket?.send(serialize({
147
+ type: "SYNC_INIT",
148
+ mapName,
149
+ lastSyncTimestamp: this.lastSyncTimestamp
150
+ }));
151
+ }
152
+ }
153
+ }
154
+ setAuthToken(token) {
155
+ this.authToken = token;
156
+ this.tokenProvider = null;
157
+ if (this.isOnline) {
158
+ this.sendAuth();
159
+ } else {
160
+ if (this.reconnectTimer) {
161
+ logger.info("Auth token set during backoff. Reconnecting immediately.");
162
+ clearTimeout(this.reconnectTimer);
163
+ this.reconnectTimer = null;
164
+ this.initConnection();
165
+ }
166
+ }
167
+ }
168
+ setTokenProvider(provider) {
169
+ this.tokenProvider = provider;
170
+ if (this.isOnline && !this.isAuthenticated) {
171
+ this.sendAuth();
172
+ }
173
+ }
174
+ async sendAuth() {
175
+ if (this.tokenProvider) {
176
+ try {
177
+ const token2 = await this.tokenProvider();
178
+ if (token2) {
179
+ this.authToken = token2;
180
+ }
181
+ } catch (err) {
182
+ logger.error({ err }, "Failed to get token from provider");
183
+ return;
184
+ }
185
+ }
186
+ const token = this.authToken;
187
+ if (!token) return;
188
+ this.websocket?.send(serialize({
189
+ type: "AUTH",
190
+ token
191
+ }));
192
+ }
193
+ subscribeToQuery(query) {
194
+ this.queries.set(query.id, query);
195
+ if (this.isOnline && this.isAuthenticated) {
196
+ this.sendQuerySubscription(query);
197
+ }
198
+ }
199
+ subscribeToTopic(topic, handle) {
200
+ this.topics.set(topic, handle);
201
+ if (this.isOnline && this.isAuthenticated) {
202
+ this.sendTopicSubscription(topic);
203
+ }
204
+ }
205
+ unsubscribeFromTopic(topic) {
206
+ this.topics.delete(topic);
207
+ if (this.isOnline && this.isAuthenticated) {
208
+ this.websocket?.send(serialize({
209
+ type: "TOPIC_UNSUB",
210
+ payload: { topic }
211
+ }));
212
+ }
213
+ }
214
+ publishTopic(topic, data) {
215
+ if (this.isOnline && this.isAuthenticated) {
216
+ this.websocket?.send(serialize({
217
+ type: "TOPIC_PUB",
218
+ payload: { topic, data }
219
+ }));
220
+ } else {
221
+ logger.warn({ topic }, "Dropped topic publish (offline)");
222
+ }
223
+ }
224
+ sendTopicSubscription(topic) {
225
+ this.websocket?.send(serialize({
226
+ type: "TOPIC_SUB",
227
+ payload: { topic }
228
+ }));
229
+ }
230
+ /**
231
+ * Executes a query against local storage immediately
232
+ */
233
+ async runLocalQuery(mapName, filter) {
234
+ const keys = await this.storageAdapter.getAllKeys();
235
+ const mapKeys = keys.filter((k) => k.startsWith(mapName + ":"));
236
+ const results = [];
237
+ for (const fullKey of mapKeys) {
238
+ const record = await this.storageAdapter.get(fullKey);
239
+ if (record && record.value) {
240
+ const actualKey = fullKey.slice(mapName.length + 1);
241
+ let matches = true;
242
+ if (filter.where) {
243
+ for (const [k, v] of Object.entries(filter.where)) {
244
+ if (record.value[k] !== v) {
245
+ matches = false;
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ if (matches && filter.predicate) {
251
+ if (!evaluatePredicate(filter.predicate, record.value)) {
252
+ matches = false;
253
+ }
254
+ }
255
+ if (matches) {
256
+ results.push({ key: actualKey, value: record.value });
257
+ }
258
+ }
259
+ }
260
+ return results;
261
+ }
262
+ unsubscribeFromQuery(queryId) {
263
+ this.queries.delete(queryId);
264
+ if (this.isOnline && this.isAuthenticated) {
265
+ this.websocket?.send(serialize({
266
+ type: "QUERY_UNSUB",
267
+ payload: { queryId }
268
+ }));
269
+ }
270
+ }
271
+ sendQuerySubscription(query) {
272
+ this.websocket?.send(serialize({
273
+ type: "QUERY_SUB",
274
+ payload: {
275
+ queryId: query.id,
276
+ mapName: query.getMapName(),
277
+ query: query.getFilter()
278
+ }
279
+ }));
280
+ }
281
+ requestLock(name, requestId, ttl) {
282
+ if (!this.isOnline || !this.isAuthenticated) {
283
+ return Promise.reject(new Error("Not connected or authenticated"));
284
+ }
285
+ return new Promise((resolve, reject) => {
286
+ const timer = setTimeout(() => {
287
+ if (this.pendingLockRequests.has(requestId)) {
288
+ this.pendingLockRequests.delete(requestId);
289
+ reject(new Error("Lock request timed out waiting for server response"));
290
+ }
291
+ }, 3e4);
292
+ this.pendingLockRequests.set(requestId, { resolve, reject, timer });
293
+ try {
294
+ this.websocket?.send(serialize({
295
+ type: "LOCK_REQUEST",
296
+ payload: { requestId, name, ttl }
297
+ }));
298
+ } catch (e) {
299
+ clearTimeout(timer);
300
+ this.pendingLockRequests.delete(requestId);
301
+ reject(e);
302
+ }
303
+ });
304
+ }
305
+ releaseLock(name, requestId, fencingToken) {
306
+ if (!this.isOnline) return Promise.resolve(false);
307
+ return new Promise((resolve, reject) => {
308
+ const timer = setTimeout(() => {
309
+ if (this.pendingLockRequests.has(requestId)) {
310
+ this.pendingLockRequests.delete(requestId);
311
+ resolve(false);
312
+ }
313
+ }, 5e3);
314
+ this.pendingLockRequests.set(requestId, { resolve, reject, timer });
315
+ try {
316
+ this.websocket?.send(serialize({
317
+ type: "LOCK_RELEASE",
318
+ payload: { requestId, name, fencingToken }
319
+ }));
320
+ } catch (e) {
321
+ clearTimeout(timer);
322
+ this.pendingLockRequests.delete(requestId);
323
+ resolve(false);
324
+ }
325
+ });
326
+ }
327
+ async handleServerMessage(message) {
328
+ switch (message.type) {
329
+ case "AUTH_REQUIRED":
330
+ this.sendAuth();
331
+ break;
332
+ case "AUTH_ACK": {
333
+ logger.info("Authenticated successfully");
334
+ const wasAuthenticated = this.isAuthenticated;
335
+ this.isAuthenticated = true;
336
+ this.syncPendingOperations();
337
+ if (!wasAuthenticated) {
338
+ this.startMerkleSync();
339
+ for (const query of this.queries.values()) {
340
+ this.sendQuerySubscription(query);
341
+ }
342
+ for (const topic of this.topics.keys()) {
343
+ this.sendTopicSubscription(topic);
344
+ }
345
+ }
346
+ break;
347
+ }
348
+ case "AUTH_FAIL":
349
+ logger.error({ error: message.error }, "Authentication failed");
350
+ this.isAuthenticated = false;
351
+ this.authToken = null;
352
+ break;
353
+ case "OP_ACK": {
354
+ const { lastId } = message.payload;
355
+ logger.info({ lastId }, "Received ACK for ops");
356
+ let maxSyncedId = -1;
357
+ this.opLog.forEach((op) => {
358
+ if (op.id && op.id <= lastId) {
359
+ op.synced = true;
360
+ const idNum = parseInt(op.id, 10);
361
+ if (!isNaN(idNum) && idNum > maxSyncedId) {
362
+ maxSyncedId = idNum;
363
+ }
364
+ }
365
+ });
366
+ if (maxSyncedId !== -1) {
367
+ this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
368
+ }
369
+ break;
370
+ }
371
+ case "LOCK_GRANTED": {
372
+ const { requestId, fencingToken } = message.payload;
373
+ const req = this.pendingLockRequests.get(requestId);
374
+ if (req) {
375
+ clearTimeout(req.timer);
376
+ this.pendingLockRequests.delete(requestId);
377
+ req.resolve({ fencingToken });
378
+ }
379
+ break;
380
+ }
381
+ case "LOCK_RELEASED": {
382
+ const { requestId, success } = message.payload;
383
+ const req = this.pendingLockRequests.get(requestId);
384
+ if (req) {
385
+ clearTimeout(req.timer);
386
+ this.pendingLockRequests.delete(requestId);
387
+ req.resolve(success);
388
+ }
389
+ break;
390
+ }
391
+ case "QUERY_RESP": {
392
+ const { queryId, results } = message.payload;
393
+ const query = this.queries.get(queryId);
394
+ if (query) {
395
+ query.onResult(results, "server");
396
+ }
397
+ break;
398
+ }
399
+ case "QUERY_UPDATE": {
400
+ const { queryId, key, value, type } = message.payload;
401
+ const query = this.queries.get(queryId);
402
+ if (query) {
403
+ query.onUpdate(key, type === "REMOVE" ? null : value);
404
+ }
405
+ break;
406
+ }
407
+ case "SERVER_EVENT": {
408
+ const { mapName, eventType, key, record, orRecord, orTag } = message.payload;
409
+ const localMap = this.maps.get(mapName);
410
+ if (localMap) {
411
+ if (localMap instanceof LWWMap && record) {
412
+ localMap.merge(key, record);
413
+ await this.storageAdapter.put(`${mapName}:${key}`, record);
414
+ } else if (localMap instanceof ORMap) {
415
+ if (eventType === "OR_ADD" && orRecord) {
416
+ localMap.apply(key, orRecord);
417
+ } else if (eventType === "OR_REMOVE" && orTag) {
418
+ localMap.applyTombstone(orTag);
419
+ }
420
+ }
421
+ }
422
+ break;
423
+ }
424
+ case "TOPIC_MESSAGE": {
425
+ const { topic, data, publisherId, timestamp } = message.payload;
426
+ const handle = this.topics.get(topic);
427
+ if (handle) {
428
+ handle.onMessage(data, { publisherId, timestamp });
429
+ }
430
+ break;
431
+ }
432
+ case "GC_PRUNE": {
433
+ const { olderThan } = message.payload;
434
+ logger.info({ olderThan: olderThan.millis }, "Received GC_PRUNE request");
435
+ for (const [name, map] of this.maps) {
436
+ if (map instanceof LWWMap) {
437
+ const removedKeys = map.prune(olderThan);
438
+ for (const key of removedKeys) {
439
+ await this.storageAdapter.remove(`${name}:${key}`);
440
+ }
441
+ if (removedKeys.length > 0) {
442
+ logger.info({ mapName: name, count: removedKeys.length }, "Pruned tombstones from LWWMap");
443
+ }
444
+ } else if (map instanceof ORMap) {
445
+ const removedTags = map.prune(olderThan);
446
+ if (removedTags.length > 0) {
447
+ logger.info({ mapName: name, count: removedTags.length }, "Pruned tombstones from ORMap");
448
+ }
449
+ }
450
+ }
451
+ break;
452
+ }
453
+ case "SYNC_RESET_REQUIRED": {
454
+ const { mapName } = message.payload;
455
+ logger.warn({ mapName }, "Sync Reset Required due to GC Age");
456
+ await this.resetMap(mapName);
457
+ this.websocket?.send(serialize({
458
+ type: "SYNC_INIT",
459
+ mapName,
460
+ lastSyncTimestamp: 0
461
+ }));
462
+ break;
463
+ }
464
+ case "SYNC_RESP_ROOT": {
465
+ const { mapName, rootHash, timestamp } = message.payload;
466
+ const map = this.maps.get(mapName);
467
+ if (map instanceof LWWMap) {
468
+ const localRootHash = map.getMerkleTree().getRootHash();
469
+ if (localRootHash !== rootHash) {
470
+ logger.info({ mapName, localRootHash, remoteRootHash: rootHash }, "Root hash mismatch, requesting buckets");
471
+ this.websocket?.send(serialize({
472
+ type: "MERKLE_REQ_BUCKET",
473
+ payload: { mapName, path: "" }
474
+ }));
475
+ } else {
476
+ logger.info({ mapName }, "Map is in sync");
477
+ }
478
+ }
479
+ if (timestamp) {
480
+ this.hlc.update(timestamp);
481
+ this.lastSyncTimestamp = timestamp.millis;
482
+ await this.saveOpLog();
483
+ }
484
+ break;
485
+ }
486
+ case "SYNC_RESP_BUCKETS": {
487
+ const { mapName, path, buckets } = message.payload;
488
+ const map = this.maps.get(mapName);
489
+ if (map instanceof LWWMap) {
490
+ const tree = map.getMerkleTree();
491
+ const localBuckets = tree.getBuckets(path);
492
+ for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
493
+ const localHash = localBuckets[bucketKey] || 0;
494
+ if (localHash !== remoteHash) {
495
+ const newPath = path + bucketKey;
496
+ this.websocket?.send(serialize({
497
+ type: "MERKLE_REQ_BUCKET",
498
+ payload: { mapName, path: newPath }
499
+ }));
500
+ }
501
+ }
502
+ }
503
+ break;
504
+ }
505
+ case "SYNC_RESP_LEAF": {
506
+ const { mapName, records } = message.payload;
507
+ const map = this.maps.get(mapName);
508
+ if (map instanceof LWWMap) {
509
+ let updateCount = 0;
510
+ for (const { key, record } of records) {
511
+ const updated = map.merge(key, record);
512
+ if (updated) {
513
+ updateCount++;
514
+ await this.storageAdapter.put(`${mapName}:${key}`, record);
515
+ }
516
+ }
517
+ if (updateCount > 0) {
518
+ logger.info({ mapName, count: updateCount }, "Synced records from server");
519
+ }
520
+ }
521
+ break;
522
+ }
523
+ }
524
+ if (message.timestamp) {
525
+ this.hlc.update(message.timestamp);
526
+ this.lastSyncTimestamp = message.timestamp.millis;
527
+ await this.saveOpLog();
528
+ }
529
+ }
530
+ getHLC() {
531
+ return this.hlc;
532
+ }
533
+ /**
534
+ * Closes the WebSocket connection and cleans up resources.
535
+ */
536
+ close() {
537
+ if (this.reconnectTimer) {
538
+ clearTimeout(this.reconnectTimer);
539
+ this.reconnectTimer = null;
540
+ }
541
+ if (this.websocket) {
542
+ this.websocket.onclose = null;
543
+ this.websocket.close();
544
+ this.websocket = null;
545
+ }
546
+ this.isOnline = false;
547
+ this.isAuthenticated = false;
548
+ logger.info("SyncEngine closed");
549
+ }
550
+ async resetMap(mapName) {
551
+ const map = this.maps.get(mapName);
552
+ if (map) {
553
+ if (map instanceof LWWMap) {
554
+ map.clear();
555
+ } else if (map instanceof ORMap) {
556
+ map.clear();
557
+ }
558
+ }
559
+ const allKeys = await this.storageAdapter.getAllKeys();
560
+ const mapKeys = allKeys.filter((k) => k.startsWith(mapName + ":"));
561
+ for (const key of mapKeys) {
562
+ await this.storageAdapter.remove(key);
563
+ }
564
+ logger.info({ mapName, removedStorageCount: mapKeys.length }, "Reset map: Cleared memory and storage");
565
+ }
566
+ };
567
+
568
+ // src/TopGunClient.ts
569
+ import { LWWMap as LWWMap2, ORMap as ORMap2 } from "@topgunbuild/core";
570
+
571
+ // src/QueryHandle.ts
572
+ var QueryHandle = class {
573
+ constructor(syncEngine, mapName, filter = {}) {
574
+ this.listeners = /* @__PURE__ */ new Set();
575
+ this.currentResults = /* @__PURE__ */ new Map();
576
+ // Track if we've received authoritative server response
577
+ this.hasReceivedServerData = false;
578
+ this.id = crypto.randomUUID();
579
+ this.syncEngine = syncEngine;
580
+ this.mapName = mapName;
581
+ this.filter = filter;
582
+ }
583
+ subscribe(callback) {
584
+ this.listeners.add(callback);
585
+ if (this.listeners.size === 1) {
586
+ this.syncEngine.subscribeToQuery(this);
587
+ } else {
588
+ callback(this.getSortedResults());
589
+ }
590
+ this.loadInitialLocalData().then((data) => {
591
+ if (this.currentResults.size === 0) {
592
+ this.onResult(data, "local");
593
+ }
594
+ });
595
+ return () => {
596
+ this.listeners.delete(callback);
597
+ if (this.listeners.size === 0) {
598
+ this.syncEngine.unsubscribeFromQuery(this.id);
599
+ }
600
+ };
601
+ }
602
+ async loadInitialLocalData() {
603
+ return this.syncEngine.runLocalQuery(this.mapName, this.filter);
604
+ }
605
+ /**
606
+ * Called by SyncEngine when server sends initial results or by local storage load.
607
+ * Uses merge strategy instead of clear to prevent UI flickering.
608
+ *
609
+ * @param items - Array of key-value pairs
610
+ * @param source - 'local' for IndexedDB data, 'server' for QUERY_RESP from server
611
+ *
612
+ * Race condition protection:
613
+ * - Empty server responses are ignored until we receive non-empty server data
614
+ * - This prevents clearing local data when server hasn't loaded from storage yet
615
+ * - Works with any async storage adapter (PostgreSQL, SQLite, Redis, etc.)
616
+ */
617
+ onResult(items, source = "server") {
618
+ console.log(`[QueryHandle:${this.mapName}] onResult called with ${items.length} items`, {
619
+ source,
620
+ currentResultsCount: this.currentResults.size,
621
+ newItemKeys: items.map((i) => i.key),
622
+ hasReceivedServerData: this.hasReceivedServerData
623
+ });
624
+ if (source === "server" && items.length === 0 && !this.hasReceivedServerData) {
625
+ console.log(`[QueryHandle:${this.mapName}] Ignoring empty server response - waiting for authoritative data`);
626
+ return;
627
+ }
628
+ if (source === "server" && items.length > 0) {
629
+ this.hasReceivedServerData = true;
630
+ }
631
+ const newKeys = new Set(items.map((i) => i.key));
632
+ const removedKeys = [];
633
+ for (const key of this.currentResults.keys()) {
634
+ if (!newKeys.has(key)) {
635
+ removedKeys.push(key);
636
+ this.currentResults.delete(key);
637
+ }
638
+ }
639
+ if (removedKeys.length > 0) {
640
+ console.log(`[QueryHandle:${this.mapName}] Removed ${removedKeys.length} keys:`, removedKeys);
641
+ }
642
+ for (const item of items) {
643
+ this.currentResults.set(item.key, item.value);
644
+ }
645
+ console.log(`[QueryHandle:${this.mapName}] After merge: ${this.currentResults.size} results`);
646
+ this.notify();
647
+ }
648
+ /**
649
+ * Called by SyncEngine when server sends a live update
650
+ */
651
+ onUpdate(key, value) {
652
+ if (value === null) {
653
+ this.currentResults.delete(key);
654
+ } else {
655
+ this.currentResults.set(key, value);
656
+ }
657
+ this.notify();
658
+ }
659
+ notify() {
660
+ const results = this.getSortedResults();
661
+ for (const listener of this.listeners) {
662
+ listener(results);
663
+ }
664
+ }
665
+ getSortedResults() {
666
+ const results = Array.from(this.currentResults.entries()).map(
667
+ ([key, value]) => ({ ...value, _key: key })
668
+ );
669
+ if (this.filter.sort) {
670
+ results.sort((a, b) => {
671
+ for (const [field, direction] of Object.entries(this.filter.sort)) {
672
+ const valA = a[field];
673
+ const valB = b[field];
674
+ if (valA < valB) return direction === "asc" ? -1 : 1;
675
+ if (valA > valB) return direction === "asc" ? 1 : -1;
676
+ }
677
+ return 0;
678
+ });
679
+ }
680
+ return results;
681
+ }
682
+ getFilter() {
683
+ return this.filter;
684
+ }
685
+ getMapName() {
686
+ return this.mapName;
687
+ }
688
+ };
689
+
690
+ // src/DistributedLock.ts
691
+ var DistributedLock = class {
692
+ constructor(syncEngine, name) {
693
+ this.fencingToken = null;
694
+ this._isLocked = false;
695
+ this.syncEngine = syncEngine;
696
+ this.name = name;
697
+ }
698
+ async lock(ttl = 1e4) {
699
+ const requestId = crypto.randomUUID();
700
+ try {
701
+ const result = await this.syncEngine.requestLock(this.name, requestId, ttl);
702
+ this.fencingToken = result.fencingToken;
703
+ this._isLocked = true;
704
+ return true;
705
+ } catch (e) {
706
+ return false;
707
+ }
708
+ }
709
+ async unlock() {
710
+ if (!this._isLocked || this.fencingToken === null) return;
711
+ const requestId = crypto.randomUUID();
712
+ try {
713
+ await this.syncEngine.releaseLock(this.name, requestId, this.fencingToken);
714
+ } finally {
715
+ this._isLocked = false;
716
+ this.fencingToken = null;
717
+ }
718
+ }
719
+ isLocked() {
720
+ return this._isLocked;
721
+ }
722
+ };
723
+
724
+ // src/TopicHandle.ts
725
+ var TopicHandle = class {
726
+ constructor(engine, topic) {
727
+ this.listeners = /* @__PURE__ */ new Set();
728
+ this.engine = engine;
729
+ this.topic = topic;
730
+ }
731
+ get id() {
732
+ return this.topic;
733
+ }
734
+ /**
735
+ * Publish a message to the topic
736
+ */
737
+ publish(data) {
738
+ this.engine.publishTopic(this.topic, data);
739
+ }
740
+ /**
741
+ * Subscribe to the topic
742
+ */
743
+ subscribe(callback) {
744
+ if (this.listeners.size === 0) {
745
+ this.engine.subscribeToTopic(this.topic, this);
746
+ }
747
+ this.listeners.add(callback);
748
+ return () => this.unsubscribe(callback);
749
+ }
750
+ unsubscribe(callback) {
751
+ this.listeners.delete(callback);
752
+ if (this.listeners.size === 0) {
753
+ this.engine.unsubscribeFromTopic(this.topic);
754
+ }
755
+ }
756
+ /**
757
+ * Called by SyncEngine when a message is received
758
+ */
759
+ onMessage(data, context) {
760
+ this.listeners.forEach((cb) => {
761
+ try {
762
+ cb(data, context);
763
+ } catch (e) {
764
+ console.error("Error in topic listener", e);
765
+ }
766
+ });
767
+ }
768
+ };
769
+
770
+ // src/TopGunClient.ts
771
+ var TopGunClient = class {
772
+ constructor(config) {
773
+ this.maps = /* @__PURE__ */ new Map();
774
+ this.topicHandles = /* @__PURE__ */ new Map();
775
+ this.nodeId = config.nodeId || crypto.randomUUID();
776
+ this.storageAdapter = config.storage;
777
+ const syncEngineConfig = {
778
+ nodeId: this.nodeId,
779
+ serverUrl: config.serverUrl,
780
+ storageAdapter: this.storageAdapter
781
+ };
782
+ this.syncEngine = new SyncEngine(syncEngineConfig);
783
+ }
784
+ async start() {
785
+ await this.storageAdapter.initialize("topgun_offline_db");
786
+ }
787
+ setAuthToken(token) {
788
+ this.syncEngine.setAuthToken(token);
789
+ }
790
+ setAuthTokenProvider(provider) {
791
+ this.syncEngine.setTokenProvider(provider);
792
+ }
793
+ /**
794
+ * Creates a live query subscription for a map.
795
+ */
796
+ query(mapName, filter) {
797
+ return new QueryHandle(this.syncEngine, mapName, filter);
798
+ }
799
+ /**
800
+ * Retrieves a distributed lock instance.
801
+ * @param name The name of the lock.
802
+ */
803
+ getLock(name) {
804
+ return new DistributedLock(this.syncEngine, name);
805
+ }
806
+ /**
807
+ * Retrieves a topic handle for Pub/Sub messaging.
808
+ * @param name The name of the topic.
809
+ */
810
+ topic(name) {
811
+ if (!this.topicHandles.has(name)) {
812
+ this.topicHandles.set(name, new TopicHandle(this.syncEngine, name));
813
+ }
814
+ return this.topicHandles.get(name);
815
+ }
816
+ /**
817
+ * Retrieves an LWWMap instance. If the map doesn't exist locally, it's created.
818
+ * @param name The name of the map.
819
+ * @returns An LWWMap instance.
820
+ */
821
+ getMap(name) {
822
+ if (this.maps.has(name)) {
823
+ const map = this.maps.get(name);
824
+ if (map instanceof LWWMap2) {
825
+ return map;
826
+ }
827
+ throw new Error(`Map ${name} exists but is not an LWWMap`);
828
+ }
829
+ const lwwMap = new LWWMap2(this.syncEngine.getHLC());
830
+ this.maps.set(name, lwwMap);
831
+ this.syncEngine.registerMap(name, lwwMap);
832
+ this.storageAdapter.getAllKeys().then(async (keys) => {
833
+ const mapPrefix = `${name}:`;
834
+ for (const fullKey of keys) {
835
+ if (fullKey.startsWith(mapPrefix)) {
836
+ const record = await this.storageAdapter.get(fullKey);
837
+ if (record && record.timestamp && !record.tag) {
838
+ const key = fullKey.substring(mapPrefix.length);
839
+ lwwMap.merge(key, record);
840
+ }
841
+ }
842
+ }
843
+ }).catch((err) => logger.error({ err }, "Failed to restore keys from storage"));
844
+ const originalSet = lwwMap.set.bind(lwwMap);
845
+ lwwMap.set = (key, value, ttlMs) => {
846
+ const record = originalSet(key, value, ttlMs);
847
+ this.storageAdapter.put(`${name}:${key}`, record).catch((err) => logger.error({ err }, "Failed to put record to storage"));
848
+ this.syncEngine.recordOperation(name, "PUT", String(key), { record, timestamp: record.timestamp }).catch((err) => logger.error({ err }, "Failed to record PUT op"));
849
+ return record;
850
+ };
851
+ const originalRemove = lwwMap.remove.bind(lwwMap);
852
+ lwwMap.remove = (key) => {
853
+ const tombstone = originalRemove(key);
854
+ this.storageAdapter.put(`${name}:${key}`, tombstone).catch((err) => logger.error({ err }, "Failed to put tombstone to storage"));
855
+ this.syncEngine.recordOperation(name, "REMOVE", String(key), { record: tombstone, timestamp: tombstone.timestamp }).catch((err) => logger.error({ err }, "Failed to record REMOVE op"));
856
+ return tombstone;
857
+ };
858
+ return lwwMap;
859
+ }
860
+ /**
861
+ * Retrieves an ORMap instance. If the map doesn't exist locally, it's created.
862
+ * @param name The name of the map.
863
+ * @returns An ORMap instance.
864
+ */
865
+ getORMap(name) {
866
+ if (this.maps.has(name)) {
867
+ const map = this.maps.get(name);
868
+ if (map instanceof ORMap2) {
869
+ return map;
870
+ }
871
+ throw new Error(`Map ${name} exists but is not an ORMap`);
872
+ }
873
+ const orMap = new ORMap2(this.syncEngine.getHLC());
874
+ this.maps.set(name, orMap);
875
+ this.syncEngine.registerMap(name, orMap);
876
+ this.restoreORMap(name, orMap);
877
+ const originalAdd = orMap.add.bind(orMap);
878
+ orMap.add = (key, value, ttlMs) => {
879
+ const record = originalAdd(key, value, ttlMs);
880
+ this.persistORMapKey(name, orMap, key);
881
+ this.syncEngine.recordOperation(name, "OR_ADD", String(key), { orRecord: record, timestamp: record.timestamp }).catch((err) => logger.error({ err }, "Failed to record OR_ADD op"));
882
+ return record;
883
+ };
884
+ const originalRemove = orMap.remove.bind(orMap);
885
+ orMap.remove = (key, value) => {
886
+ const tombstones = originalRemove(key, value);
887
+ const timestamp = this.syncEngine.getHLC().now();
888
+ this.persistORMapKey(name, orMap, key);
889
+ this.persistORMapTombstones(name, orMap);
890
+ for (const tag of tombstones) {
891
+ this.syncEngine.recordOperation(name, "OR_REMOVE", String(key), { orTag: tag, timestamp }).catch((err) => logger.error({ err }, "Failed to record OR_REMOVE op"));
892
+ }
893
+ return tombstones;
894
+ };
895
+ return orMap;
896
+ }
897
+ async restoreORMap(name, orMap) {
898
+ try {
899
+ const tombstoneKey = `__sys__:${name}:tombstones`;
900
+ const tombstones = await this.storageAdapter.getMeta(tombstoneKey);
901
+ if (Array.isArray(tombstones)) {
902
+ for (const tag of tombstones) {
903
+ orMap.applyTombstone(tag);
904
+ }
905
+ }
906
+ const keys = await this.storageAdapter.getAllKeys();
907
+ const mapPrefix = `${name}:`;
908
+ for (const fullKey of keys) {
909
+ if (fullKey.startsWith(mapPrefix)) {
910
+ const keyPart = fullKey.substring(mapPrefix.length);
911
+ const data = await this.storageAdapter.get(fullKey);
912
+ if (Array.isArray(data)) {
913
+ const records = data;
914
+ const key = keyPart;
915
+ for (const record of records) {
916
+ orMap.apply(key, record);
917
+ }
918
+ }
919
+ }
920
+ }
921
+ } catch (e) {
922
+ logger.error({ mapName: name, err: e }, "Failed to restore ORMap");
923
+ }
924
+ }
925
+ async persistORMapKey(mapName, orMap, key) {
926
+ const records = orMap.getRecords(key);
927
+ if (records.length > 0) {
928
+ await this.storageAdapter.put(`${mapName}:${key}`, records);
929
+ } else {
930
+ await this.storageAdapter.remove(`${mapName}:${key}`);
931
+ }
932
+ }
933
+ async persistORMapTombstones(mapName, orMap) {
934
+ const tombstoneKey = `__sys__:${mapName}:tombstones`;
935
+ const tombstones = orMap.getTombstones();
936
+ await this.storageAdapter.setMeta(tombstoneKey, tombstones);
937
+ }
938
+ /**
939
+ * Closes the client, disconnecting from the server and cleaning up resources.
940
+ */
941
+ close() {
942
+ this.syncEngine.close();
943
+ }
944
+ };
945
+
946
+ // src/adapters/IDBAdapter.ts
947
+ import { openDB } from "idb";
948
+ var IDBAdapter = class {
949
+ constructor() {
950
+ this.isReady = false;
951
+ this.operationQueue = [];
952
+ }
953
+ /**
954
+ * Initializes IndexedDB in the background.
955
+ * Returns immediately - does NOT block on IndexedDB being ready.
956
+ * Use waitForReady() if you need to ensure initialization is complete.
957
+ */
958
+ async initialize(dbName) {
959
+ this.initPromise = this.initializeInternal(dbName);
960
+ }
961
+ /**
962
+ * Internal initialization that actually opens IndexedDB.
963
+ */
964
+ async initializeInternal(dbName) {
965
+ try {
966
+ this.dbPromise = openDB(dbName, 2, {
967
+ upgrade(db) {
968
+ if (!db.objectStoreNames.contains("kv_store")) {
969
+ db.createObjectStore("kv_store", { keyPath: "key" });
970
+ }
971
+ if (!db.objectStoreNames.contains("op_log")) {
972
+ db.createObjectStore("op_log", { keyPath: "id", autoIncrement: true });
973
+ }
974
+ if (!db.objectStoreNames.contains("meta_store")) {
975
+ db.createObjectStore("meta_store", { keyPath: "key" });
976
+ }
977
+ }
978
+ });
979
+ this.db = await this.dbPromise;
980
+ this.isReady = true;
981
+ await this.flushQueue();
982
+ } catch (error) {
983
+ throw error;
984
+ }
985
+ }
986
+ /**
987
+ * Waits for IndexedDB to be fully initialized.
988
+ * Call this if you need guaranteed persistence before proceeding.
989
+ */
990
+ async waitForReady() {
991
+ if (this.isReady) return;
992
+ if (this.initPromise) {
993
+ await this.initPromise;
994
+ }
995
+ }
996
+ /**
997
+ * Flushes all queued operations once IndexedDB is ready.
998
+ */
999
+ async flushQueue() {
1000
+ const queue = this.operationQueue;
1001
+ this.operationQueue = [];
1002
+ for (const op of queue) {
1003
+ try {
1004
+ let result;
1005
+ switch (op.type) {
1006
+ case "put":
1007
+ result = await this.putInternal(op.args[0], op.args[1]);
1008
+ break;
1009
+ case "remove":
1010
+ result = await this.removeInternal(op.args[0]);
1011
+ break;
1012
+ case "setMeta":
1013
+ result = await this.setMetaInternal(op.args[0], op.args[1]);
1014
+ break;
1015
+ case "appendOpLog":
1016
+ result = await this.appendOpLogInternal(op.args[0]);
1017
+ break;
1018
+ case "markOpsSynced":
1019
+ result = await this.markOpsSyncedInternal(op.args[0]);
1020
+ break;
1021
+ case "batchPut":
1022
+ result = await this.batchPutInternal(op.args[0]);
1023
+ break;
1024
+ }
1025
+ op.resolve(result);
1026
+ } catch (error) {
1027
+ op.reject(error);
1028
+ }
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Queues an operation if not ready, or executes immediately if ready.
1033
+ */
1034
+ queueOrExecute(type, args, executor) {
1035
+ if (this.isReady) {
1036
+ return executor();
1037
+ }
1038
+ return new Promise((resolve, reject) => {
1039
+ this.operationQueue.push({ type, args, resolve, reject });
1040
+ });
1041
+ }
1042
+ async close() {
1043
+ if (this.db) {
1044
+ this.db.close();
1045
+ }
1046
+ }
1047
+ // ============================================
1048
+ // Read Operations - Wait for ready
1049
+ // ============================================
1050
+ async get(key) {
1051
+ await this.waitForReady();
1052
+ const result = await this.db?.get("kv_store", key);
1053
+ return result?.value;
1054
+ }
1055
+ async getMeta(key) {
1056
+ await this.waitForReady();
1057
+ const result = await this.db?.get("meta_store", key);
1058
+ return result?.value;
1059
+ }
1060
+ async getPendingOps() {
1061
+ await this.waitForReady();
1062
+ const all = await this.db?.getAll("op_log");
1063
+ return all?.filter((op) => op.synced === 0) || [];
1064
+ }
1065
+ async getAllKeys() {
1066
+ await this.waitForReady();
1067
+ return await this.db?.getAllKeys("kv_store") || [];
1068
+ }
1069
+ // ============================================
1070
+ // Write Operations - Queue if not ready
1071
+ // ============================================
1072
+ async put(key, value) {
1073
+ return this.queueOrExecute("put", [key, value], () => this.putInternal(key, value));
1074
+ }
1075
+ async putInternal(key, value) {
1076
+ await this.db?.put("kv_store", { key, value });
1077
+ }
1078
+ async remove(key) {
1079
+ return this.queueOrExecute("remove", [key], () => this.removeInternal(key));
1080
+ }
1081
+ async removeInternal(key) {
1082
+ await this.db?.delete("kv_store", key);
1083
+ }
1084
+ async setMeta(key, value) {
1085
+ return this.queueOrExecute("setMeta", [key, value], () => this.setMetaInternal(key, value));
1086
+ }
1087
+ async setMetaInternal(key, value) {
1088
+ await this.db?.put("meta_store", { key, value });
1089
+ }
1090
+ async batchPut(entries) {
1091
+ return this.queueOrExecute("batchPut", [entries], () => this.batchPutInternal(entries));
1092
+ }
1093
+ async batchPutInternal(entries) {
1094
+ const tx = this.db?.transaction("kv_store", "readwrite");
1095
+ if (!tx) return;
1096
+ await Promise.all(
1097
+ Array.from(entries.entries()).map(
1098
+ ([key, value]) => tx.store.put({ key, value })
1099
+ )
1100
+ );
1101
+ await tx.done;
1102
+ }
1103
+ async appendOpLog(entry) {
1104
+ return this.queueOrExecute("appendOpLog", [entry], () => this.appendOpLogInternal(entry));
1105
+ }
1106
+ async appendOpLogInternal(entry) {
1107
+ const entryToSave = { ...entry, synced: 0 };
1108
+ return await this.db?.add("op_log", entryToSave);
1109
+ }
1110
+ async markOpsSynced(lastId) {
1111
+ return this.queueOrExecute("markOpsSynced", [lastId], () => this.markOpsSyncedInternal(lastId));
1112
+ }
1113
+ async markOpsSyncedInternal(lastId) {
1114
+ const tx = this.db?.transaction("op_log", "readwrite");
1115
+ if (!tx) return;
1116
+ let cursor = await tx.store.openCursor();
1117
+ while (cursor) {
1118
+ if (cursor.value.id <= lastId) {
1119
+ const update = { ...cursor.value, synced: 1 };
1120
+ await cursor.update(update);
1121
+ }
1122
+ cursor = await cursor.continue();
1123
+ }
1124
+ await tx.done;
1125
+ }
1126
+ };
1127
+
1128
+ // src/TopGun.ts
1129
+ var handler = {
1130
+ get(target, prop, receiver) {
1131
+ if (prop in target || typeof prop === "symbol") {
1132
+ return Reflect.get(target, prop, receiver);
1133
+ }
1134
+ if (typeof prop === "string") {
1135
+ return target.collection(prop);
1136
+ }
1137
+ return void 0;
1138
+ }
1139
+ };
1140
+ var TopGun = class {
1141
+ constructor(config) {
1142
+ let storage;
1143
+ if (config.persist === "indexeddb") {
1144
+ storage = new IDBAdapter();
1145
+ } else if (typeof config.persist === "object") {
1146
+ storage = config.persist;
1147
+ } else {
1148
+ throw new Error(`Unsupported persist option: ${config.persist}`);
1149
+ }
1150
+ this.client = new TopGunClient({
1151
+ serverUrl: config.sync,
1152
+ storage,
1153
+ nodeId: config.nodeId
1154
+ });
1155
+ this.initPromise = this.client.start().catch((err) => {
1156
+ console.error("Failed to start TopGun client:", err);
1157
+ throw err;
1158
+ });
1159
+ return new Proxy(this, handler);
1160
+ }
1161
+ /**
1162
+ * Waits for the storage adapter to be fully initialized.
1163
+ * This is optional - you can start using the database immediately.
1164
+ * Operations are queued in memory and persisted once IndexedDB is ready.
1165
+ */
1166
+ async waitForReady() {
1167
+ await this.initPromise;
1168
+ }
1169
+ collection(name) {
1170
+ const map = this.client.getMap(name);
1171
+ return new CollectionWrapper(map);
1172
+ }
1173
+ };
1174
+ var CollectionWrapper = class {
1175
+ constructor(map) {
1176
+ this.map = map;
1177
+ }
1178
+ /**
1179
+ * Sets an item in the collection.
1180
+ * The item MUST have an 'id' or '_id' field.
1181
+ */
1182
+ async set(value) {
1183
+ const v = value;
1184
+ const key = v.id || v._id;
1185
+ if (!key) {
1186
+ throw new Error('Object must have an "id" or "_id" property to be saved in a collection.');
1187
+ }
1188
+ this.map.set(key, value);
1189
+ return Promise.resolve(value);
1190
+ }
1191
+ /**
1192
+ * Retrieves an item by ID.
1193
+ * Returns the value directly (unwrapped from CRDT record).
1194
+ */
1195
+ get(key) {
1196
+ return this.map.get(key);
1197
+ }
1198
+ /**
1199
+ * Get the raw LWWRecord (including metadata like timestamp).
1200
+ */
1201
+ getRecord(key) {
1202
+ return this.map.getRecord(key);
1203
+ }
1204
+ // Expose raw map if needed for advanced usage
1205
+ get raw() {
1206
+ return this.map;
1207
+ }
1208
+ };
1209
+
1210
+ // src/crypto/EncryptionManager.ts
1211
+ import { serialize as serialize2, deserialize as deserialize2 } from "@topgunbuild/core";
1212
+ var _EncryptionManager = class _EncryptionManager {
1213
+ /**
1214
+ * Encrypts data using AES-GCM.
1215
+ * Serializes data to MessagePack before encryption.
1216
+ */
1217
+ static async encrypt(key, data) {
1218
+ const encoded = serialize2(data);
1219
+ const iv = window.crypto.getRandomValues(new Uint8Array(_EncryptionManager.IV_LENGTH));
1220
+ const ciphertext = await window.crypto.subtle.encrypt(
1221
+ {
1222
+ name: _EncryptionManager.ALGORITHM,
1223
+ iv
1224
+ },
1225
+ key,
1226
+ encoded
1227
+ );
1228
+ return {
1229
+ iv,
1230
+ data: new Uint8Array(ciphertext)
1231
+ };
1232
+ }
1233
+ /**
1234
+ * Decrypts AES-GCM encrypted data.
1235
+ * Deserializes from MessagePack after decryption.
1236
+ */
1237
+ static async decrypt(key, record) {
1238
+ try {
1239
+ const plaintextBuffer = await window.crypto.subtle.decrypt(
1240
+ {
1241
+ name: _EncryptionManager.ALGORITHM,
1242
+ iv: record.iv
1243
+ },
1244
+ key,
1245
+ record.data
1246
+ );
1247
+ return deserialize2(new Uint8Array(plaintextBuffer));
1248
+ } catch (err) {
1249
+ console.error("Decryption failed", err);
1250
+ throw new Error("Failed to decrypt data: " + err);
1251
+ }
1252
+ }
1253
+ };
1254
+ _EncryptionManager.ALGORITHM = "AES-GCM";
1255
+ _EncryptionManager.IV_LENGTH = 12;
1256
+ var EncryptionManager = _EncryptionManager;
1257
+
1258
+ // src/adapters/EncryptedStorageAdapter.ts
1259
+ var EncryptedStorageAdapter = class {
1260
+ constructor(wrapped, key) {
1261
+ this.wrapped = wrapped;
1262
+ this.key = key;
1263
+ }
1264
+ async initialize(dbName) {
1265
+ return this.wrapped.initialize(dbName);
1266
+ }
1267
+ async close() {
1268
+ return this.wrapped.close();
1269
+ }
1270
+ // --- KV Operations ---
1271
+ async get(key) {
1272
+ const raw = await this.wrapped.get(key);
1273
+ if (!raw) {
1274
+ return void 0;
1275
+ }
1276
+ if (this.isEncryptedRecord(raw)) {
1277
+ try {
1278
+ return await EncryptionManager.decrypt(this.key, raw);
1279
+ } catch (e) {
1280
+ throw e;
1281
+ }
1282
+ }
1283
+ return raw;
1284
+ }
1285
+ async put(key, value) {
1286
+ const encrypted = await EncryptionManager.encrypt(this.key, value);
1287
+ const storedValue = {
1288
+ iv: encrypted.iv,
1289
+ data: encrypted.data
1290
+ };
1291
+ return this.wrapped.put(key, storedValue);
1292
+ }
1293
+ async remove(key) {
1294
+ return this.wrapped.remove(key);
1295
+ }
1296
+ // --- Metadata ---
1297
+ async getMeta(key) {
1298
+ const raw = await this.wrapped.getMeta(key);
1299
+ if (!raw) return void 0;
1300
+ if (this.isEncryptedRecord(raw)) {
1301
+ return EncryptionManager.decrypt(this.key, raw);
1302
+ }
1303
+ return raw;
1304
+ }
1305
+ async setMeta(key, value) {
1306
+ const encrypted = await EncryptionManager.encrypt(this.key, value);
1307
+ return this.wrapped.setMeta(key, {
1308
+ iv: encrypted.iv,
1309
+ data: encrypted.data
1310
+ });
1311
+ }
1312
+ // --- Batch ---
1313
+ async batchPut(entries) {
1314
+ const encryptedEntries = /* @__PURE__ */ new Map();
1315
+ for (const [key, value] of entries.entries()) {
1316
+ const encrypted = await EncryptionManager.encrypt(this.key, value);
1317
+ encryptedEntries.set(key, {
1318
+ iv: encrypted.iv,
1319
+ data: encrypted.data
1320
+ });
1321
+ }
1322
+ return this.wrapped.batchPut(encryptedEntries);
1323
+ }
1324
+ // --- OpLog ---
1325
+ async appendOpLog(entry) {
1326
+ const encryptedEntry = { ...entry };
1327
+ if (entry.value !== void 0) {
1328
+ const enc = await EncryptionManager.encrypt(this.key, entry.value);
1329
+ encryptedEntry.value = { iv: enc.iv, data: enc.data };
1330
+ }
1331
+ if (entry.record !== void 0) {
1332
+ const enc = await EncryptionManager.encrypt(this.key, entry.record);
1333
+ encryptedEntry.record = { iv: enc.iv, data: enc.data };
1334
+ }
1335
+ if (entry.orRecord !== void 0) {
1336
+ const enc = await EncryptionManager.encrypt(this.key, entry.orRecord);
1337
+ encryptedEntry.orRecord = { iv: enc.iv, data: enc.data };
1338
+ }
1339
+ return this.wrapped.appendOpLog(encryptedEntry);
1340
+ }
1341
+ async getPendingOps() {
1342
+ const ops = await this.wrapped.getPendingOps();
1343
+ return Promise.all(ops.map(async (op) => {
1344
+ const decryptedOp = { ...op };
1345
+ if (this.isEncryptedRecord(op.value)) {
1346
+ decryptedOp.value = await EncryptionManager.decrypt(this.key, op.value);
1347
+ }
1348
+ if (this.isEncryptedRecord(op.record)) {
1349
+ decryptedOp.record = await EncryptionManager.decrypt(this.key, op.record);
1350
+ }
1351
+ if (this.isEncryptedRecord(op.orRecord)) {
1352
+ decryptedOp.orRecord = await EncryptionManager.decrypt(this.key, op.orRecord);
1353
+ }
1354
+ return decryptedOp;
1355
+ }));
1356
+ }
1357
+ async markOpsSynced(lastId) {
1358
+ return this.wrapped.markOpsSynced(lastId);
1359
+ }
1360
+ // --- Iteration ---
1361
+ async getAllKeys() {
1362
+ return this.wrapped.getAllKeys();
1363
+ }
1364
+ // --- Helpers ---
1365
+ isEncryptedRecord(data) {
1366
+ return data && typeof data === "object" && data.iv instanceof Uint8Array && data.data instanceof Uint8Array;
1367
+ }
1368
+ };
1369
+
1370
+ // src/index.ts
1371
+ import { LWWMap as LWWMap3, Predicates } from "@topgunbuild/core";
1372
+ export {
1373
+ EncryptedStorageAdapter,
1374
+ IDBAdapter,
1375
+ LWWMap3 as LWWMap,
1376
+ Predicates,
1377
+ QueryHandle,
1378
+ SyncEngine,
1379
+ TopGun,
1380
+ TopGunClient,
1381
+ TopicHandle,
1382
+ logger
1383
+ };
1384
+ //# sourceMappingURL=index.mjs.map