cry-synced-db-client 0.1.1

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.js ADDED
@@ -0,0 +1,1339 @@
1
+ // src/index.ts
2
+ import Dexie2 from "dexie";
3
+
4
+ // src/db/SyncedDb.ts
5
+ import { ObjectId } from "bson";
6
+
7
+ // src/utils/conflictResolution.ts
8
+ function resolveConflict(local, external) {
9
+ if (local._rev !== undefined && external._rev !== undefined) {
10
+ if (external._rev <= local._rev) {
11
+ return local;
12
+ }
13
+ }
14
+ return mergeObjects(local, external);
15
+ }
16
+ function mergeObjects(local, external) {
17
+ const result = { ...local };
18
+ for (const key of Object.keys(external)) {
19
+ if (key === "_id" || key === "_dirty" || key === "_localChangedAt") {
20
+ continue;
21
+ }
22
+ const localValue = local[key];
23
+ const externalValue = external[key];
24
+ if (localValue === undefined) {
25
+ result[key] = externalValue;
26
+ continue;
27
+ }
28
+ if (externalValue === undefined) {
29
+ continue;
30
+ }
31
+ if (Array.isArray(localValue) && Array.isArray(externalValue)) {
32
+ result[key] = mergeArrays(localValue, externalValue);
33
+ } else if (isPlainObject(localValue) && isPlainObject(externalValue)) {
34
+ result[key] = mergeObjects(localValue, externalValue);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ function mergeArrays(local, external) {
40
+ if (local.length === 0) {
41
+ return [...external];
42
+ }
43
+ const firstLocal = local[0];
44
+ const firstExternal = external[0];
45
+ if (typeof firstLocal === "string" || typeof firstExternal === "string") {
46
+ return [...new Set([...local, ...external])];
47
+ }
48
+ if (isPlainObject(firstLocal) || isPlainObject(firstExternal)) {
49
+ return mergeObjectArrays(local, external);
50
+ }
51
+ return [...new Set([...local, ...external])];
52
+ }
53
+ function mergeObjectArrays(local, external) {
54
+ const result = [...local];
55
+ const localIds = new Map;
56
+ for (let i = 0;i < local.length; i++) {
57
+ const item = local[i];
58
+ if (item && typeof item === "object" && "_id" in item) {
59
+ localIds.set(String(item._id), i);
60
+ }
61
+ }
62
+ for (const extItem of external) {
63
+ if (!extItem || typeof extItem !== "object") {
64
+ result.push(extItem);
65
+ continue;
66
+ }
67
+ if ("_id" in extItem) {
68
+ const localIndex = localIds.get(String(extItem._id));
69
+ if (localIndex !== undefined) {
70
+ result[localIndex] = mergeObjects(result[localIndex], extItem);
71
+ } else {
72
+ result.push(extItem);
73
+ }
74
+ } else {
75
+ result.push(extItem);
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+ function isPlainObject(value) {
81
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
82
+ }
83
+
84
+ // src/utils/localQuery.ts
85
+ function matchesQuery(item, query) {
86
+ for (const [key, condition] of Object.entries(query)) {
87
+ if (!matchesCondition(item, key, condition)) {
88
+ return false;
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+ function matchesCondition(item, key, condition) {
94
+ const value = getNestedValue(item, key);
95
+ if (condition === null || typeof condition !== "object") {
96
+ return value === condition;
97
+ }
98
+ if (condition instanceof Date) {
99
+ if (value instanceof Date) {
100
+ return value.getTime() === condition.getTime();
101
+ }
102
+ return false;
103
+ }
104
+ if (Array.isArray(condition)) {
105
+ return condition.includes(value);
106
+ }
107
+ for (const [op, opValue] of Object.entries(condition)) {
108
+ if (!matchesOperator(value, op, opValue)) {
109
+ return false;
110
+ }
111
+ }
112
+ return true;
113
+ }
114
+ function matchesOperator(value, operator, operand) {
115
+ switch (operator) {
116
+ case "$eq":
117
+ return equals(value, operand);
118
+ case "$ne":
119
+ return !equals(value, operand);
120
+ case "$gt":
121
+ return value > operand;
122
+ case "$gte":
123
+ return value >= operand;
124
+ case "$lt":
125
+ return value < operand;
126
+ case "$lte":
127
+ return value <= operand;
128
+ case "$in":
129
+ if (!Array.isArray(operand))
130
+ return false;
131
+ return operand.some((item) => equals(value, item));
132
+ case "$nin":
133
+ if (!Array.isArray(operand))
134
+ return true;
135
+ return !operand.some((item) => equals(value, item));
136
+ case "$exists":
137
+ return operand ? value !== undefined : value === undefined;
138
+ case "$regex": {
139
+ if (typeof value !== "string")
140
+ return false;
141
+ const regex = operand instanceof RegExp ? operand : new RegExp(operand);
142
+ return regex.test(value);
143
+ }
144
+ case "$elemMatch":
145
+ if (!Array.isArray(value))
146
+ return false;
147
+ return value.some((elem) => {
148
+ if (typeof operand === "object" && operand !== null) {
149
+ for (const [k, v] of Object.entries(operand)) {
150
+ if (!matchesCondition(elem, k, v)) {
151
+ return false;
152
+ }
153
+ }
154
+ return true;
155
+ }
156
+ return equals(elem, operand);
157
+ });
158
+ case "$size":
159
+ return Array.isArray(value) && value.length === operand;
160
+ case "$all":
161
+ if (!Array.isArray(value) || !Array.isArray(operand))
162
+ return false;
163
+ return operand.every((item) => value.some((v) => equals(v, item)));
164
+ default:
165
+ if (typeof value === "object" && value !== null) {
166
+ return matchesCondition(value, operator, operand);
167
+ }
168
+ return false;
169
+ }
170
+ }
171
+ function getNestedValue(obj, path) {
172
+ const parts = path.split(".");
173
+ let current = obj;
174
+ for (const part of parts) {
175
+ if (current === null || current === undefined) {
176
+ return;
177
+ }
178
+ current = current[part];
179
+ }
180
+ return current;
181
+ }
182
+ function equals(a, b) {
183
+ if (a === b)
184
+ return true;
185
+ if (a instanceof Date && b instanceof Date) {
186
+ return a.getTime() === b.getTime();
187
+ }
188
+ if (a && b && typeof a.toString === "function" && typeof b.toString === "function") {
189
+ if (a.constructor?.name === "ObjectId" || b.constructor?.name === "ObjectId") {
190
+ return String(a) === String(b);
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+ function filterByQuery(items, query) {
196
+ if (!query || Object.keys(query).length === 0) {
197
+ return items;
198
+ }
199
+ return items.filter((item) => matchesQuery(item, query));
200
+ }
201
+
202
+ // src/utils/crashRecovery.ts
203
+ var STORAGE_PREFIX = "synced-db-pending:";
204
+ function savePendingWrite(tenant, collection, id, data) {
205
+ if (typeof localStorage === "undefined")
206
+ return;
207
+ const key = `${STORAGE_PREFIX}${tenant}:${collection}:${String(id)}`;
208
+ const pending = {
209
+ tenant,
210
+ collection,
211
+ id: String(id),
212
+ data,
213
+ timestamp: Date.now()
214
+ };
215
+ try {
216
+ localStorage.setItem(key, JSON.stringify(pending));
217
+ } catch {
218
+ console.warn("Failed to save pending write to localStorage");
219
+ }
220
+ }
221
+ function clearPendingWrite(tenant, collection, id) {
222
+ if (typeof localStorage === "undefined")
223
+ return;
224
+ const key = `${STORAGE_PREFIX}${tenant}:${collection}:${String(id)}`;
225
+ try {
226
+ localStorage.removeItem(key);
227
+ } catch {}
228
+ }
229
+ function getPendingWrites(tenant) {
230
+ if (typeof localStorage === "undefined")
231
+ return [];
232
+ const pending = [];
233
+ const prefix = `${STORAGE_PREFIX}${tenant}:`;
234
+ try {
235
+ for (let i = 0;i < localStorage.length; i++) {
236
+ const key = localStorage.key(i);
237
+ if (key && key.startsWith(prefix)) {
238
+ const value = localStorage.getItem(key);
239
+ if (value) {
240
+ try {
241
+ const parsed = JSON.parse(value);
242
+ pending.push(parsed);
243
+ } catch {
244
+ localStorage.removeItem(key);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ } catch {}
250
+ return pending.sort((a, b) => a.timestamp - b.timestamp);
251
+ }
252
+
253
+ // src/db/SyncedDb.ts
254
+ var DEFAULT_DEBOUNCE_MS = 1000;
255
+ var DEFAULT_REST_TIMEOUT_MS = 1e4;
256
+ var DEFAULT_SYNC_TIMEOUT_MS = 30000;
257
+ var MAX_RETRY_COUNT = 3;
258
+
259
+ class SyncedDb {
260
+ tenant;
261
+ collections = new Map;
262
+ dexieDb;
263
+ inMemDb;
264
+ restInterface;
265
+ serverUpdateNotifier;
266
+ restTimeoutMs;
267
+ syncTimeoutMs;
268
+ debounceMs;
269
+ onForcedOffline;
270
+ onSync;
271
+ online = false;
272
+ syncing = false;
273
+ syncLock = false;
274
+ initialized = false;
275
+ pendingChanges = new Map;
276
+ unsubscribeServerUpdates;
277
+ beforeUnloadHandler;
278
+ updaterId;
279
+ constructor(config) {
280
+ this.tenant = config.tenant;
281
+ this.dexieDb = config.dexieDb;
282
+ this.inMemDb = config.inMemDb;
283
+ this.restInterface = config.restInterface;
284
+ this.serverUpdateNotifier = config.serverUpdateNotifier;
285
+ this.restTimeoutMs = config.restTimeoutMs ?? DEFAULT_REST_TIMEOUT_MS;
286
+ this.syncTimeoutMs = config.syncTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS;
287
+ this.debounceMs = config.debounceMs ?? DEFAULT_DEBOUNCE_MS;
288
+ this.onForcedOffline = config.onForcedOffline;
289
+ this.onSync = config.onSync;
290
+ this.updaterId = Math.random().toString(36).substring(2, 15);
291
+ for (const col of config.collections) {
292
+ this.collections.set(col.name, col);
293
+ }
294
+ }
295
+ async init() {
296
+ if (this.initialized)
297
+ return;
298
+ await this.recoverPendingWrites();
299
+ for (const [name] of this.collections) {
300
+ const data = await this.dexieDb.getAll(name);
301
+ const activeData = data.filter((item) => !item._deleted);
302
+ this.inMemDb.saveCollection(name, activeData);
303
+ }
304
+ if (this.serverUpdateNotifier) {
305
+ this.unsubscribeServerUpdates = this.serverUpdateNotifier.subscribe((payload) => this.handleServerUpdate(payload));
306
+ }
307
+ if (typeof window !== "undefined") {
308
+ this.beforeUnloadHandler = () => {
309
+ if (this.initialized && this.pendingChanges.size > 0) {
310
+ console.warn(`SyncedDb: ${this.pendingChanges.size} pending changes not flushed. ` + `Call close() before page unload to ensure data is saved.`);
311
+ }
312
+ };
313
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
314
+ }
315
+ this.initialized = true;
316
+ }
317
+ async close() {
318
+ await this.flushAllPendingChanges();
319
+ if (this.unsubscribeServerUpdates) {
320
+ this.unsubscribeServerUpdates();
321
+ this.unsubscribeServerUpdates = undefined;
322
+ }
323
+ if (typeof window !== "undefined" && this.beforeUnloadHandler) {
324
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
325
+ this.beforeUnloadHandler = undefined;
326
+ }
327
+ this.initialized = false;
328
+ }
329
+ isOnline() {
330
+ return this.online;
331
+ }
332
+ setOnline(online) {
333
+ const wasOffline = !this.online;
334
+ if (online && wasOffline && this.initialized) {
335
+ this.tryGoOnline().catch((err) => {
336
+ console.error("Failed to go online:", err);
337
+ });
338
+ } else {
339
+ this.online = online;
340
+ }
341
+ }
342
+ goOffline(reason) {
343
+ this.online = false;
344
+ if (this.onForcedOffline) {
345
+ try {
346
+ this.onForcedOffline(reason);
347
+ } catch (err) {
348
+ console.error("onForcedOffline callback failed:", err);
349
+ }
350
+ }
351
+ }
352
+ async tryGoOnline() {
353
+ try {
354
+ const pingResult = await this.withSyncTimeout(this.restInterface.ping(), "ping");
355
+ if (!pingResult) {
356
+ console.warn("Ping failed - staying offline");
357
+ return;
358
+ }
359
+ this.online = true;
360
+ await this.sync();
361
+ } catch (err) {
362
+ console.warn("Failed to go online (ping failed or timed out):", err);
363
+ this.online = false;
364
+ }
365
+ }
366
+ withRestTimeout(promise, operation) {
367
+ return new Promise((resolve, reject) => {
368
+ const timer = setTimeout(() => {
369
+ reject(new Error(`REST timeout: ${operation} took longer than ${this.restTimeoutMs}ms`));
370
+ }, this.restTimeoutMs);
371
+ promise.then((result) => {
372
+ clearTimeout(timer);
373
+ resolve(result);
374
+ }).catch((err) => {
375
+ clearTimeout(timer);
376
+ reject(err);
377
+ });
378
+ });
379
+ }
380
+ withSyncTimeout(promise, operation) {
381
+ return new Promise((resolve, reject) => {
382
+ const timer = setTimeout(() => {
383
+ reject(new Error(`Sync timeout: ${operation} took longer than ${this.syncTimeoutMs}ms`));
384
+ }, this.syncTimeoutMs);
385
+ promise.then((result) => {
386
+ clearTimeout(timer);
387
+ resolve(result);
388
+ }).catch((err) => {
389
+ clearTimeout(timer);
390
+ reject(err);
391
+ });
392
+ });
393
+ }
394
+ async findById(collection, id) {
395
+ this.assertCollection(collection);
396
+ const item = await this.dexieDb.getById(collection, id);
397
+ if (!item || item._deleted)
398
+ return null;
399
+ return this.stripLocalFields(item);
400
+ }
401
+ async findByIds(collection, ids) {
402
+ this.assertCollection(collection);
403
+ const items = await Promise.all(ids.map((id) => this.dexieDb.getById(collection, id)));
404
+ const results = [];
405
+ for (const item of items) {
406
+ if (item && !item._deleted) {
407
+ results.push(this.stripLocalFields(item));
408
+ }
409
+ }
410
+ return results;
411
+ }
412
+ async findOne(collection, query) {
413
+ this.assertCollection(collection);
414
+ const all = await this.dexieDb.getAll(collection);
415
+ const active = all.filter((item) => !item._deleted);
416
+ const filtered = filterByQuery(active, query);
417
+ if (filtered.length === 0)
418
+ return null;
419
+ return this.stripLocalFields(filtered[0]);
420
+ }
421
+ async find(collection, query) {
422
+ this.assertCollection(collection);
423
+ const all = await this.dexieDb.getAll(collection);
424
+ const active = all.filter((item) => !item._deleted);
425
+ const filtered = query ? filterByQuery(active, query) : active;
426
+ return filtered.map((item) => this.stripLocalFields(item));
427
+ }
428
+ async aggregate(collection, pipeline, opts) {
429
+ this.assertCollection(collection);
430
+ if (!this.online) {
431
+ return [];
432
+ }
433
+ return this.withRestTimeout(this.restInterface.aggregate(collection, pipeline, opts), "aggregate");
434
+ }
435
+ async save(collection, id, update) {
436
+ this.assertCollection(collection);
437
+ const existing = await this.dexieDb.getById(collection, id);
438
+ if (!existing) {
439
+ console.warn(`SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`);
440
+ }
441
+ const newData = {
442
+ ...update,
443
+ _dirty: true,
444
+ _localChangedAt: new Date,
445
+ _lastUpdaterId: this.updaterId
446
+ };
447
+ this.schedulePendingChange(collection, id, newData);
448
+ if (existing?._deleted) {} else {
449
+ this.inMemDb.save(collection, id, update);
450
+ }
451
+ const merged = { ...existing || { _id: id }, ...update };
452
+ return this.stripLocalFields(merged);
453
+ }
454
+ async upsert(collection, query, update) {
455
+ this.assertCollection(collection);
456
+ const existing = await this.findOne(collection, query);
457
+ if (existing) {
458
+ return this.save(collection, existing._id, update);
459
+ } else {
460
+ const id = new ObjectId;
461
+ const newDoc = { _id: id, ...update };
462
+ return this.insert(collection, newDoc);
463
+ }
464
+ }
465
+ async insert(collection, data) {
466
+ this.assertCollection(collection);
467
+ const id = data._id || new ObjectId;
468
+ const existing = await this.dexieDb.getById(collection, id);
469
+ if (existing && !existing._deleted) {
470
+ console.warn(`SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`);
471
+ }
472
+ const newData = {
473
+ _id: id,
474
+ ...data,
475
+ _dirty: true,
476
+ _localChangedAt: new Date,
477
+ _lastUpdaterId: this.updaterId
478
+ };
479
+ this.schedulePendingChange(collection, id, newData);
480
+ this.inMemDb.insert(collection, this.stripLocalFields(newData));
481
+ return this.stripLocalFields(newData);
482
+ }
483
+ async deleteOne(collection, id) {
484
+ this.assertCollection(collection);
485
+ const existing = await this.dexieDb.getById(collection, id);
486
+ if (!existing || existing._deleted) {
487
+ return null;
488
+ }
489
+ const deleteUpdate = {
490
+ _deleted: new Date,
491
+ _dirty: true,
492
+ _localChangedAt: new Date,
493
+ _lastUpdaterId: this.updaterId
494
+ };
495
+ this.schedulePendingChange(collection, id, deleteUpdate);
496
+ this.inMemDb.deleteOne(collection, id);
497
+ return this.stripLocalFields(existing);
498
+ }
499
+ async deleteMany(collection, query) {
500
+ this.assertCollection(collection);
501
+ const items = await this.find(collection, query);
502
+ if (items.length === 0)
503
+ return 0;
504
+ const existingItems = await Promise.all(items.map((item) => this.dexieDb.getById(collection, item._id)));
505
+ const now = new Date;
506
+ let count = 0;
507
+ for (let i = 0;i < items.length; i++) {
508
+ const item = items[i];
509
+ const existing = existingItems[i];
510
+ if (!item || !existing || existing._deleted)
511
+ continue;
512
+ const deleteUpdate = {
513
+ _deleted: now,
514
+ _dirty: true,
515
+ _localChangedAt: now,
516
+ _lastUpdaterId: this.updaterId
517
+ };
518
+ this.schedulePendingChange(collection, item._id, deleteUpdate);
519
+ this.inMemDb.deleteOne(collection, item._id);
520
+ count++;
521
+ }
522
+ return count;
523
+ }
524
+ async hardDeleteOne(collection, id) {
525
+ this.assertCollection(collection);
526
+ if (!this.online) {
527
+ throw new Error("hardDeleteOne requires online connection");
528
+ }
529
+ const existing = await this.dexieDb.getById(collection, id);
530
+ if (!existing) {
531
+ return null;
532
+ }
533
+ await this.withRestTimeout(this.restInterface.deleteOne(collection, { _id: id }), "hardDeleteOne");
534
+ await this.dexieDb.deleteOne(collection, id);
535
+ this.inMemDb.deleteOne(collection, id);
536
+ return this.stripLocalFields(existing);
537
+ }
538
+ async hardDelete(collection, query) {
539
+ this.assertCollection(collection);
540
+ if (!this.online) {
541
+ throw new Error("hardDelete requires online connection");
542
+ }
543
+ const items = await this.find(collection, query);
544
+ if (items.length === 0)
545
+ return 0;
546
+ const existingItems = await Promise.all(items.map((item) => this.dexieDb.getById(collection, item._id)));
547
+ const toDelete = [];
548
+ for (let i = 0;i < items.length; i++) {
549
+ const item = items[i];
550
+ const existing = existingItems[i];
551
+ if (item && existing) {
552
+ toDelete.push({ id: item._id, existing });
553
+ }
554
+ }
555
+ if (toDelete.length === 0)
556
+ return 0;
557
+ const maxConcurrency = 10;
558
+ const queue = [...toDelete];
559
+ const results = [];
560
+ await Promise.all(Array.from({ length: Math.min(maxConcurrency, queue.length) }, async () => {
561
+ while (queue.length > 0) {
562
+ const item = queue.pop();
563
+ if (!item)
564
+ break;
565
+ try {
566
+ await this.withRestTimeout(this.restInterface.deleteOne(collection, { _id: item.id }), "hardDelete");
567
+ await this.dexieDb.deleteOne(collection, item.id);
568
+ this.inMemDb.deleteOne(collection, item.id);
569
+ results.push(true);
570
+ } catch (err) {
571
+ console.error(`Failed to hard delete ${String(item.id)}:`, err);
572
+ results.push(false);
573
+ }
574
+ }
575
+ }));
576
+ return results.filter(Boolean).length;
577
+ }
578
+ async ping(timeoutMs) {
579
+ const timeout = timeoutMs ?? this.restTimeoutMs;
580
+ try {
581
+ const result = await new Promise((resolve, reject) => {
582
+ const timer = setTimeout(() => {
583
+ reject(new Error(`Ping timeout after ${timeout}ms`));
584
+ }, timeout);
585
+ this.restInterface.ping().then((result2) => {
586
+ clearTimeout(timer);
587
+ resolve(result2);
588
+ }).catch((err) => {
589
+ clearTimeout(timer);
590
+ reject(err);
591
+ });
592
+ });
593
+ return result;
594
+ } catch {
595
+ return false;
596
+ }
597
+ }
598
+ async sync() {
599
+ if (!this.online || this.syncLock)
600
+ return;
601
+ this.syncLock = true;
602
+ this.syncing = true;
603
+ const startTime = Date.now();
604
+ let receivedCount = 0;
605
+ let sentCount = 0;
606
+ let conflictsResolved = 0;
607
+ try {
608
+ await this.flushAllPendingChanges();
609
+ const syncSpecs = [];
610
+ const configMap = new Map;
611
+ for (const [collectionName, config] of this.collections) {
612
+ const meta = await this.dexieDb.getSyncMeta(collectionName);
613
+ syncSpecs.push({
614
+ collection: collectionName,
615
+ timestamp: meta?.lastSyncTs || 0,
616
+ query: config.query,
617
+ opts: { returnDeleted: true }
618
+ });
619
+ configMap.set(collectionName, config);
620
+ }
621
+ const allServerData = await this.withSyncTimeout(this.restInterface.findNewerMany(syncSpecs), "findNewerMany");
622
+ for (const [collectionName, config] of this.collections) {
623
+ const serverData = allServerData[collectionName] || [];
624
+ receivedCount += serverData.length;
625
+ const stats = await this.syncCollectionWithData(collectionName, config, serverData);
626
+ sentCount += stats.sentCount;
627
+ conflictsResolved += stats.conflictsResolved;
628
+ }
629
+ if (this.onSync) {
630
+ try {
631
+ this.onSync({
632
+ durationMs: Date.now() - startTime,
633
+ receivedCount,
634
+ sentCount,
635
+ conflictsResolved,
636
+ success: true
637
+ });
638
+ } catch (err) {
639
+ console.error("onSync callback failed:", err);
640
+ }
641
+ }
642
+ } catch (err) {
643
+ const reason = err instanceof Error ? err.message : String(err);
644
+ console.error("Sync failed, going offline:", err);
645
+ this.goOffline(`Sync failed: ${reason}`);
646
+ if (this.onSync) {
647
+ try {
648
+ this.onSync({
649
+ durationMs: Date.now() - startTime,
650
+ receivedCount,
651
+ sentCount,
652
+ conflictsResolved,
653
+ success: false,
654
+ error: err instanceof Error ? err : new Error(String(err))
655
+ });
656
+ } catch (callbackErr) {
657
+ console.error("onSync callback failed:", callbackErr);
658
+ }
659
+ }
660
+ throw err;
661
+ } finally {
662
+ this.syncing = false;
663
+ this.syncLock = false;
664
+ }
665
+ }
666
+ isSyncing() {
667
+ return this.syncing;
668
+ }
669
+ async upsertBatch(collection, batch) {
670
+ this.assertCollection(collection);
671
+ if (!this.online) {
672
+ throw new Error("upsertBatch requires online connection");
673
+ }
674
+ return this.withRestTimeout(this.restInterface.upsertBatch(collection, batch), "upsertBatch");
675
+ }
676
+ getMemoryCollection(collection) {
677
+ this.assertCollection(collection);
678
+ return this.inMemDb.getAll(collection);
679
+ }
680
+ getDebounceMs() {
681
+ return this.debounceMs;
682
+ }
683
+ assertCollection(name) {
684
+ if (!this.collections.has(name)) {
685
+ throw new Error(`Collection "${name}" not configured`);
686
+ }
687
+ }
688
+ stripLocalFields(item) {
689
+ const { _dirty, _localChangedAt, ...rest } = item;
690
+ return rest;
691
+ }
692
+ getPendingKey(collection, id) {
693
+ return `${collection}:${String(id)}`;
694
+ }
695
+ schedulePendingChange(collection, id, data, retryCount = 0) {
696
+ const key = this.getPendingKey(collection, id);
697
+ const existing = this.pendingChanges.get(key);
698
+ if (existing) {
699
+ clearTimeout(existing.timer);
700
+ }
701
+ const fullData = existing ? { ...existing.data, ...data } : { _id: id, ...data };
702
+ savePendingWrite(this.tenant, collection, id, fullData);
703
+ const timer = setTimeout(() => {
704
+ this.executePendingChange(key);
705
+ }, this.debounceMs);
706
+ const newRetryCount = retryCount > 0 ? retryCount : existing?.retryCount ?? 0;
707
+ this.pendingChanges.set(key, {
708
+ collection,
709
+ id,
710
+ data: fullData,
711
+ timer,
712
+ retryCount: newRetryCount
713
+ });
714
+ }
715
+ async executePendingChange(key) {
716
+ const pending = this.pendingChanges.get(key);
717
+ if (!pending)
718
+ return;
719
+ this.pendingChanges.delete(key);
720
+ try {
721
+ const existing = await this.dexieDb.getById(pending.collection, pending.id);
722
+ if (existing) {
723
+ await this.dexieDb.save(pending.collection, pending.id, pending.data);
724
+ } else {
725
+ await this.dexieDb.insert(pending.collection, {
726
+ _id: pending.id,
727
+ ...pending.data
728
+ });
729
+ }
730
+ clearPendingWrite(this.tenant, pending.collection, pending.id);
731
+ } catch (err) {
732
+ console.error("Failed to write to Dexie:", err);
733
+ const newRetryCount = pending.retryCount + 1;
734
+ if (newRetryCount >= MAX_RETRY_COUNT) {
735
+ console.error(`Max retry count (${MAX_RETRY_COUNT}) reached for pending change ${key}. ` + `Data remains in localStorage for crash recovery.`);
736
+ return;
737
+ }
738
+ this.schedulePendingChange(pending.collection, pending.id, pending.data, newRetryCount);
739
+ }
740
+ }
741
+ async flushAllPendingChanges() {
742
+ const promises = [];
743
+ for (const [key, pending] of this.pendingChanges) {
744
+ clearTimeout(pending.timer);
745
+ promises.push(this.executePendingChange(key));
746
+ }
747
+ await Promise.all(promises);
748
+ }
749
+ async recoverPendingWrites() {
750
+ const pending = getPendingWrites(this.tenant);
751
+ for (const write of pending) {
752
+ try {
753
+ const existing = await this.dexieDb.getById(write.collection, write.id);
754
+ if (existing) {
755
+ await this.dexieDb.save(write.collection, write.id, write.data);
756
+ } else {
757
+ await this.dexieDb.insert(write.collection, write.data);
758
+ }
759
+ clearPendingWrite(this.tenant, write.collection, write.id);
760
+ } catch (err) {
761
+ console.error("Failed to recover pending write:", err);
762
+ }
763
+ }
764
+ }
765
+ async syncSingleCollection(collectionName) {
766
+ const config = this.collections.get(collectionName);
767
+ if (!config)
768
+ return;
769
+ const meta = await this.dexieDb.getSyncMeta(collectionName);
770
+ const serverData = await this.restInterface.findNewer(collectionName, meta?.lastSyncTs || 0, config.query, { returnDeleted: true });
771
+ await this.syncCollectionWithData(collectionName, config, serverData);
772
+ }
773
+ async syncCollectionWithData(collectionName, config, serverData) {
774
+ let maxTs;
775
+ let conflictsResolved = 0;
776
+ const dexieBatch = [];
777
+ const inMemSaveBatch = [];
778
+ const inMemDeleteIds = [];
779
+ for (const serverItem of serverData) {
780
+ const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
781
+ if (serverItem._ts) {
782
+ if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
783
+ maxTs = serverItem._ts;
784
+ }
785
+ }
786
+ if (localItem) {
787
+ if (localItem._dirty) {
788
+ conflictsResolved++;
789
+ const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem);
790
+ dexieBatch.push({
791
+ ...resolved,
792
+ _dirty: true,
793
+ _localChangedAt: localItem._localChangedAt
794
+ });
795
+ if (!resolved._deleted) {
796
+ inMemSaveBatch.push(this.stripLocalFields(resolved));
797
+ } else {
798
+ inMemDeleteIds.push(serverItem._id);
799
+ }
800
+ } else {
801
+ dexieBatch.push({
802
+ ...serverItem,
803
+ _dirty: false
804
+ });
805
+ if (!serverItem._deleted) {
806
+ inMemSaveBatch.push(this.stripLocalFields(serverItem));
807
+ } else {
808
+ inMemDeleteIds.push(serverItem._id);
809
+ }
810
+ }
811
+ } else {
812
+ dexieBatch.push({
813
+ ...serverItem,
814
+ _dirty: false
815
+ });
816
+ if (!serverItem._deleted) {
817
+ inMemSaveBatch.push(this.stripLocalFields(serverItem));
818
+ }
819
+ }
820
+ }
821
+ if (dexieBatch.length > 0) {
822
+ await this.dexieDb.saveMany(collectionName, dexieBatch);
823
+ }
824
+ if (inMemSaveBatch.length > 0) {
825
+ this.inMemDb.saveMany(collectionName, inMemSaveBatch);
826
+ }
827
+ for (const id of inMemDeleteIds) {
828
+ this.inMemDb.deleteOne(collectionName, id);
829
+ }
830
+ const dirtyItems = await this.dexieDb.getDirty(collectionName);
831
+ const deletedItems = dirtyItems.filter((item) => item._deleted);
832
+ const saveItems = dirtyItems.filter((item) => !item._deleted);
833
+ let sentCount = 0;
834
+ for (const item of deletedItems) {
835
+ try {
836
+ await this.restInterface.deleteOne(collectionName, { _id: item._id });
837
+ await this.dexieDb.deleteOne(collectionName, item._id);
838
+ sentCount++;
839
+ } catch (err) {
840
+ console.error(`Failed to sync deleted item ${String(item._id)}:`, err);
841
+ }
842
+ }
843
+ for (const item of saveItems) {
844
+ try {
845
+ const saved = await this.restInterface.save(collectionName, this.stripLocalFields(item), item._id);
846
+ await this.dexieDb.save(collectionName, item._id, {
847
+ ...saved,
848
+ _dirty: false
849
+ });
850
+ this.inMemDb.save(collectionName, item._id, this.stripLocalFields(saved));
851
+ if (saved._ts) {
852
+ if (!maxTs || this.compareTimestamps(saved._ts, maxTs) > 0) {
853
+ maxTs = saved._ts;
854
+ }
855
+ }
856
+ sentCount++;
857
+ } catch (err) {
858
+ console.error(`Failed to sync item ${String(item._id)}:`, err);
859
+ }
860
+ }
861
+ if (maxTs) {
862
+ await this.dexieDb.setSyncMeta(collectionName, maxTs);
863
+ }
864
+ return { conflictsResolved, sentCount };
865
+ }
866
+ resolveCollectionConflict(collectionName, config, local, external) {
867
+ if (config.resolveSyncConflict) {
868
+ return config.resolveSyncConflict(local, external);
869
+ }
870
+ return resolveConflict(local, external);
871
+ }
872
+ compareTimestamps(a, b) {
873
+ const aT = typeof a === "object" && "t" in a ? a.t : 0;
874
+ const bT = typeof b === "object" && "t" in b ? b.t : 0;
875
+ if (aT !== bT)
876
+ return aT - bT;
877
+ const aI = typeof a === "object" && "i" in a ? a.i : 0;
878
+ const bI = typeof b === "object" && "i" in b ? b.i : 0;
879
+ return aI - bI;
880
+ }
881
+ async handleServerUpdate(payload) {
882
+ const collectionName = payload.collection;
883
+ if (!this.collections.has(collectionName))
884
+ return;
885
+ switch (payload.operation) {
886
+ case "insert":
887
+ case "update": {
888
+ const items = await this.restInterface.findNewer(collectionName, 0, { _id: payload._id });
889
+ const serverItem = items[0];
890
+ if (serverItem) {
891
+ await this.handleServerItemUpdate(collectionName, serverItem);
892
+ }
893
+ break;
894
+ }
895
+ case "delete": {
896
+ await this.handleServerItemDelete(collectionName, payload._id);
897
+ break;
898
+ }
899
+ case "updateMany":
900
+ case "deleteMany": {
901
+ await this.syncSingleCollection(collectionName);
902
+ break;
903
+ }
904
+ case "batch": {
905
+ const deleteIds = [];
906
+ const updateIds = [];
907
+ for (const item of payload.data) {
908
+ if (item.operation === "delete") {
909
+ deleteIds.push(item._id);
910
+ } else {
911
+ updateIds.push(item._id);
912
+ }
913
+ }
914
+ if (deleteIds.length > 0) {
915
+ await Promise.all(deleteIds.map((id) => this.handleServerItemDelete(collectionName, id)));
916
+ }
917
+ if (updateIds.length > 0) {
918
+ const serverItems = await Promise.all(updateIds.map((id) => this.restInterface.findNewer(collectionName, 0, { _id: id }).then((items) => items[0])));
919
+ await Promise.all(serverItems.filter((item) => !!item).map((serverItem) => this.handleServerItemUpdate(collectionName, serverItem)));
920
+ }
921
+ break;
922
+ }
923
+ }
924
+ }
925
+ async handleServerItemUpdate(collectionName, serverItem) {
926
+ const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
927
+ const pendingKey = this.getPendingKey(collectionName, serverItem._id);
928
+ const pendingChange = this.pendingChanges.get(pendingKey);
929
+ const hasPendingChanges = !!pendingChange;
930
+ if (hasPendingChanges) {
931
+ const currentMemItem = this.inMemDb.getById(collectionName, serverItem._id);
932
+ if (currentMemItem) {
933
+ const config = this.collections.get(collectionName);
934
+ const resolved = this.resolveCollectionConflict(collectionName, config, currentMemItem, serverItem);
935
+ if (!resolved._deleted) {
936
+ this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(resolved));
937
+ }
938
+ pendingChange.data = {
939
+ ...pendingChange.data,
940
+ ...this.getNewFieldsFromServer(currentMemItem, serverItem)
941
+ };
942
+ }
943
+ return;
944
+ }
945
+ if (localItem) {
946
+ if (serverItem._lastUpdaterId === this.updaterId && serverItem._rev !== undefined && localItem._rev !== undefined && serverItem._rev === localItem._rev + 1) {
947
+ await this.dexieDb.save(collectionName, serverItem._id, {
948
+ _rev: serverItem._rev,
949
+ _ts: serverItem._ts,
950
+ _dirty: false
951
+ });
952
+ return;
953
+ }
954
+ if (localItem._dirty) {
955
+ const config = this.collections.get(collectionName);
956
+ const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem);
957
+ await this.dexieDb.save(collectionName, serverItem._id, {
958
+ ...resolved,
959
+ _dirty: true
960
+ });
961
+ if (!resolved._deleted) {
962
+ this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(resolved));
963
+ }
964
+ } else {
965
+ await this.dexieDb.save(collectionName, serverItem._id, {
966
+ ...serverItem,
967
+ _dirty: false
968
+ });
969
+ if (!serverItem._deleted) {
970
+ this.inMemDb.save(collectionName, serverItem._id, this.stripLocalFields(serverItem));
971
+ } else {
972
+ this.inMemDb.deleteOne(collectionName, serverItem._id);
973
+ }
974
+ }
975
+ } else {
976
+ await this.dexieDb.insert(collectionName, {
977
+ ...serverItem,
978
+ _dirty: false
979
+ });
980
+ if (!serverItem._deleted) {
981
+ this.inMemDb.insert(collectionName, this.stripLocalFields(serverItem));
982
+ }
983
+ }
984
+ }
985
+ getNewFieldsFromServer(local, server) {
986
+ const newFields = {};
987
+ for (const key of Object.keys(server)) {
988
+ if (key !== "_id" && key !== "_dirty" && key !== "_localChangedAt" && local[key] === undefined) {
989
+ newFields[key] = server[key];
990
+ }
991
+ }
992
+ return newFields;
993
+ }
994
+ async handleServerItemDelete(collectionName, id) {
995
+ const localItem = await this.dexieDb.getById(collectionName, id);
996
+ if (!localItem)
997
+ return;
998
+ if (localItem._dirty) {
999
+ await this.dexieDb.save(collectionName, id, {
1000
+ _deleted: new Date
1001
+ });
1002
+ } else {
1003
+ await this.dexieDb.deleteOne(collectionName, id);
1004
+ }
1005
+ this.inMemDb.deleteOne(collectionName, id);
1006
+ }
1007
+ }
1008
+ // src/db/DexieDb.ts
1009
+ import Dexie from "dexie";
1010
+ var SYNC_META_TABLE = "_sync_meta";
1011
+
1012
+ class DexieDb extends Dexie {
1013
+ tenant;
1014
+ collections = new Map;
1015
+ syncMeta;
1016
+ constructor(tenant, collectionConfigs) {
1017
+ super(`synced-db-${tenant}`);
1018
+ this.tenant = tenant;
1019
+ const schema = {};
1020
+ schema[SYNC_META_TABLE] = "[tenant+collection]";
1021
+ for (const config of collectionConfigs) {
1022
+ const additionalIndexes = config.indexes || [];
1023
+ const indexes = ["_id", "_dirty", ...additionalIndexes.map(String)];
1024
+ schema[config.name] = indexes.join(", ");
1025
+ }
1026
+ this.version(1).stores(schema);
1027
+ this.syncMeta = this.table(SYNC_META_TABLE);
1028
+ for (const config of collectionConfigs) {
1029
+ this.collections.set(config.name, this.table(config.name));
1030
+ }
1031
+ }
1032
+ getTable(collection) {
1033
+ const table = this.collections.get(collection);
1034
+ if (!table) {
1035
+ throw new Error(`Collection "${collection}" not configured`);
1036
+ }
1037
+ return table;
1038
+ }
1039
+ idToString(id) {
1040
+ return String(id);
1041
+ }
1042
+ async save(collection, id, data) {
1043
+ const table = this.getTable(collection);
1044
+ const key = this.idToString(id);
1045
+ const existing = await table.get(key);
1046
+ if (existing) {
1047
+ await table.update(key, {
1048
+ ...data,
1049
+ _dirty: true,
1050
+ _localChangedAt: new Date
1051
+ });
1052
+ } else {
1053
+ await table.put({
1054
+ _id: id,
1055
+ ...data,
1056
+ _dirty: true,
1057
+ _localChangedAt: new Date
1058
+ });
1059
+ }
1060
+ }
1061
+ async insert(collection, data) {
1062
+ const table = this.getTable(collection);
1063
+ await table.put({
1064
+ ...data,
1065
+ _dirty: true,
1066
+ _localChangedAt: new Date
1067
+ });
1068
+ }
1069
+ async saveMany(collection, items) {
1070
+ if (items.length === 0)
1071
+ return;
1072
+ const table = this.getTable(collection);
1073
+ await table.bulkPut(items);
1074
+ }
1075
+ async deleteOne(collection, id) {
1076
+ const table = this.getTable(collection);
1077
+ const key = this.idToString(id);
1078
+ await table.delete(key);
1079
+ }
1080
+ async saveCollection(collection, data) {
1081
+ const table = this.getTable(collection);
1082
+ await table.clear();
1083
+ if (data.length > 0) {
1084
+ await table.bulkPut(data);
1085
+ }
1086
+ }
1087
+ async deleteCollection(collection) {
1088
+ const table = this.getTable(collection);
1089
+ await table.clear();
1090
+ }
1091
+ async getById(collection, id) {
1092
+ const table = this.getTable(collection);
1093
+ const key = this.idToString(id);
1094
+ return await table.get(key);
1095
+ }
1096
+ async getAll(collection) {
1097
+ const table = this.getTable(collection);
1098
+ return await table.toArray();
1099
+ }
1100
+ async count(collection) {
1101
+ const table = this.getTable(collection);
1102
+ return await table.count();
1103
+ }
1104
+ async getDirty(collection) {
1105
+ const table = this.getTable(collection);
1106
+ return await table.filter((item) => item._dirty === true).toArray();
1107
+ }
1108
+ async markDirty(collection, id) {
1109
+ const table = this.getTable(collection);
1110
+ const key = this.idToString(id);
1111
+ await table.update(key, {
1112
+ _dirty: true,
1113
+ _localChangedAt: new Date
1114
+ });
1115
+ }
1116
+ async markClean(collection, id) {
1117
+ const table = this.getTable(collection);
1118
+ const key = this.idToString(id);
1119
+ await table.update(key, {
1120
+ _dirty: false
1121
+ });
1122
+ }
1123
+ async getSyncMeta(collection) {
1124
+ return await this.syncMeta.get([this.tenant, collection]);
1125
+ }
1126
+ async setSyncMeta(collection, lastSyncTs) {
1127
+ await this.syncMeta.put({
1128
+ tenant: this.tenant,
1129
+ collection,
1130
+ lastSyncTs
1131
+ });
1132
+ }
1133
+ async deleteSyncMeta(collection) {
1134
+ await this.syncMeta.delete([this.tenant, collection]);
1135
+ }
1136
+ getTenant() {
1137
+ return this.tenant;
1138
+ }
1139
+ }
1140
+ // src/db/RestProxy.ts
1141
+ import { serialize } from "cry-helpers";
1142
+ import { encode, decode } from "notepack.io";
1143
+ var pack = (x) => encode(serialize.encode(x));
1144
+ var toBuffer = (x) => {
1145
+ if (typeof Buffer !== "undefined") {
1146
+ return Buffer.from(x);
1147
+ }
1148
+ return x;
1149
+ };
1150
+ var unpack = (x) => serialize.decode(decode(toBuffer(x)));
1151
+ var DEFAULT_TIMEOUT = 5000;
1152
+ var DEFAULT_PROGRESS_CHUNK_SIZE = 16384;
1153
+
1154
+ class RestProxy {
1155
+ endpoint;
1156
+ tenant;
1157
+ apiKey;
1158
+ defaultTimeoutMs;
1159
+ audit;
1160
+ timeRequests;
1161
+ timeRequestsPrint;
1162
+ onProgressCallback;
1163
+ progressChunkSize;
1164
+ globalSignal;
1165
+ _lastRequestMs = 0;
1166
+ _totalRequestMs = 0;
1167
+ _requestCount = 0;
1168
+ constructor(config) {
1169
+ this.endpoint = config.endpoint;
1170
+ this.tenant = config.tenant;
1171
+ this.apiKey = config.apiKey;
1172
+ this.defaultTimeoutMs = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT;
1173
+ this.audit = config.audit ?? {};
1174
+ this.timeRequestsPrint = config.timeRequestsPrint ?? false;
1175
+ this.timeRequests = config.timeRequests ?? this.timeRequestsPrint;
1176
+ this.onProgressCallback = config.onProgressCallback;
1177
+ this.progressChunkSize = config.progressChunkSize ?? DEFAULT_PROGRESS_CHUNK_SIZE;
1178
+ this.globalSignal = config.signal;
1179
+ }
1180
+ setSignal(signal) {
1181
+ this.globalSignal = signal;
1182
+ }
1183
+ async close() {}
1184
+ setAudit(audit) {
1185
+ this.audit = audit;
1186
+ }
1187
+ getLastRequestMs() {
1188
+ return this._lastRequestMs;
1189
+ }
1190
+ getTotalRequestMs() {
1191
+ return this._totalRequestMs;
1192
+ }
1193
+ getRequestCount() {
1194
+ return this._requestCount;
1195
+ }
1196
+ resetTimingStats() {
1197
+ this._lastRequestMs = 0;
1198
+ this._totalRequestMs = 0;
1199
+ this._requestCount = 0;
1200
+ }
1201
+ async restCall(operation, payload = {}, options) {
1202
+ const timeout = options?.timeoutMs ?? this.defaultTimeoutMs;
1203
+ const externalSignal = options?.signal ?? this.globalSignal;
1204
+ const onProgress = options?.onProgress ?? this.onProgressCallback;
1205
+ const startTime = this.timeRequests ? performance.now() : 0;
1206
+ const data = {
1207
+ payload: {
1208
+ ...payload,
1209
+ db: this.tenant,
1210
+ operation
1211
+ },
1212
+ audit: {
1213
+ tenant: this.tenant,
1214
+ user: this.audit.user,
1215
+ naprava: this.audit.device
1216
+ }
1217
+ };
1218
+ const body = pack(data);
1219
+ const totalBytes = body.byteLength;
1220
+ const requestUrl = this.apiKey ? `${this.endpoint}?apikey=${this.apiKey}` : this.endpoint;
1221
+ if (onProgress) {
1222
+ onProgress(0, totalBytes);
1223
+ }
1224
+ if (onProgress) {
1225
+ const numChunks = Math.ceil(totalBytes / this.progressChunkSize);
1226
+ for (let i = 1;i <= numChunks; i++) {
1227
+ const sentBytes = Math.min(i * this.progressChunkSize, totalBytes);
1228
+ onProgress(sentBytes, totalBytes);
1229
+ }
1230
+ }
1231
+ const controller = new AbortController;
1232
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1233
+ const combinedSignal = externalSignal ? this.combineSignals(externalSignal, controller.signal) : controller.signal;
1234
+ try {
1235
+ const response = await fetch(requestUrl, {
1236
+ method: "POST",
1237
+ headers: {
1238
+ "Content-Type": "application/octet-stream"
1239
+ },
1240
+ body,
1241
+ signal: combinedSignal
1242
+ });
1243
+ clearTimeout(timeoutId);
1244
+ if (!response.ok) {
1245
+ const errorText = await response.text();
1246
+ throw new Error(`REST call failed: ${response.status} - ${errorText}`);
1247
+ }
1248
+ const buffer = await response.arrayBuffer();
1249
+ const result = unpack(new Uint8Array(buffer));
1250
+ if (this.timeRequests) {
1251
+ const elapsed = performance.now() - startTime;
1252
+ this._lastRequestMs = elapsed;
1253
+ this._totalRequestMs += elapsed;
1254
+ this._requestCount++;
1255
+ if (this.timeRequestsPrint) {
1256
+ console.log(`[RestProxy] ${operation}: ${elapsed.toFixed(2)}ms (total: ${this._totalRequestMs.toFixed(2)}ms, count: ${this._requestCount})`);
1257
+ }
1258
+ }
1259
+ return result;
1260
+ } catch (err) {
1261
+ clearTimeout(timeoutId);
1262
+ if (err.name === "AbortError") {
1263
+ if (controller.signal.aborted && !externalSignal?.aborted) {
1264
+ throw new Error(`REST call timeout after ${timeout}ms: ${operation}`);
1265
+ }
1266
+ throw new Error(`REST call aborted: ${operation}`);
1267
+ }
1268
+ throw err;
1269
+ }
1270
+ }
1271
+ combineSignals(signal1, signal2) {
1272
+ const controller = new AbortController;
1273
+ const abort = () => controller.abort();
1274
+ if (signal1.aborted || signal2.aborted) {
1275
+ controller.abort();
1276
+ return controller.signal;
1277
+ }
1278
+ signal1.addEventListener("abort", abort, { once: true });
1279
+ signal2.addEventListener("abort", abort, { once: true });
1280
+ return controller.signal;
1281
+ }
1282
+ async ping() {
1283
+ try {
1284
+ const result = await this.restCall("ping", {}, { timeoutMs: 100 });
1285
+ return result !== undefined && result !== null;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ async findNewer(collection, timestamp, query, opts) {
1291
+ return await this.restCall("findNewer", {
1292
+ collection,
1293
+ timestamp,
1294
+ query,
1295
+ opts
1296
+ });
1297
+ }
1298
+ async findNewerMany(spec) {
1299
+ return await this.restCall("findNewerMany", { spec });
1300
+ }
1301
+ async latestTimestamp(collection) {
1302
+ return await this.restCall("latestTimestamp", {
1303
+ collection
1304
+ });
1305
+ }
1306
+ async latestTimestamps(collections) {
1307
+ return await this.restCall("latestTimestamps", {
1308
+ collections
1309
+ });
1310
+ }
1311
+ async save(collection, update, id) {
1312
+ return await this.restCall("save", {
1313
+ collection,
1314
+ update,
1315
+ id: id ? String(id) : undefined
1316
+ });
1317
+ }
1318
+ async insert(collection, insert) {
1319
+ return await this.restCall("insert", { collection, insert });
1320
+ }
1321
+ async deleteOne(collection, query) {
1322
+ return await this.restCall("deleteOne", { collection, query });
1323
+ }
1324
+ async aggregate(collection, pipeline, opts) {
1325
+ return await this.restCall("aggregate", { collection, pipeline, opts });
1326
+ }
1327
+ async upsertBatch(collection, batch) {
1328
+ return await this.restCall("upsertBatch", { collection, batch });
1329
+ }
1330
+ }
1331
+ export {
1332
+ resolveConflict,
1333
+ matchesQuery,
1334
+ filterByQuery,
1335
+ SyncedDb,
1336
+ RestProxy,
1337
+ DexieDb,
1338
+ Dexie2 as Dexie
1339
+ };