edmaxlabs-core 2.5.6 → 2.6.6
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/README.md +337 -194
- package/dist/index.cjs +704 -324
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -54
- package/dist/index.d.ts +72 -54
- package/dist/index.mjs +704 -324
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -391,20 +391,6 @@ var DocumentSnapshot = class _DocumentSnapshot {
|
|
|
391
391
|
}
|
|
392
392
|
};
|
|
393
393
|
|
|
394
|
-
// src/utils/documentNomalizer.ts
|
|
395
|
-
function normalizePayload(payload) {
|
|
396
|
-
const raw = payload?.document ?? payload?.data ?? payload;
|
|
397
|
-
if (!raw)
|
|
398
|
-
return null;
|
|
399
|
-
const doc = { ...raw };
|
|
400
|
-
doc.id = payload.id ?? payload._id;
|
|
401
|
-
delete doc._id;
|
|
402
|
-
return {
|
|
403
|
-
change: payload?.change ?? raw?.change ?? null,
|
|
404
|
-
data: doc
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
394
|
// src/database/DocumentRef.ts
|
|
409
395
|
function validateDocumentData(data, operation) {
|
|
410
396
|
if (data === null || data === void 0) {
|
|
@@ -430,40 +416,22 @@ var DocumentRef = class {
|
|
|
430
416
|
this.syncEngine = app.offline().syncEngine;
|
|
431
417
|
this.localStore = app.offline().localStore;
|
|
432
418
|
}
|
|
433
|
-
// ====================== GET ======================
|
|
434
419
|
async get() {
|
|
435
420
|
if (this.persistence) {
|
|
436
421
|
try {
|
|
437
422
|
const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
|
|
423
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
424
|
+
this.refreshFromRemote().catch(() => {
|
|
425
|
+
});
|
|
426
|
+
}
|
|
438
427
|
if (localSnap)
|
|
439
428
|
return localSnap;
|
|
440
429
|
} catch (error) {
|
|
441
430
|
console.error("[EdmaxLabs] Cache read error:", error);
|
|
442
431
|
}
|
|
443
432
|
}
|
|
444
|
-
|
|
445
|
-
const res = await new HttpsRequest({
|
|
446
|
-
method: "POST" /* POST */,
|
|
447
|
-
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
448
|
-
headers: {
|
|
449
|
-
authorization: this.app.getConfig().token,
|
|
450
|
-
"x-project": this.app.getConfig().project
|
|
451
|
-
},
|
|
452
|
-
body: {
|
|
453
|
-
collection: this.collection,
|
|
454
|
-
id: this.id,
|
|
455
|
-
single: true
|
|
456
|
-
}
|
|
457
|
-
}).sendRequest();
|
|
458
|
-
if (!res?.success || !res.document)
|
|
459
|
-
return null;
|
|
460
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
461
|
-
} catch (error) {
|
|
462
|
-
console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
433
|
+
return this.fetchRemoteSnapshot();
|
|
465
434
|
}
|
|
466
|
-
// ====================== UPDATE ======================
|
|
467
435
|
async update(data) {
|
|
468
436
|
if (this._isUpdating) {
|
|
469
437
|
console.warn(`[DocumentRef] update recursion blocked on ${this.collection}/${this.id}`);
|
|
@@ -509,15 +477,13 @@ var DocumentRef = class {
|
|
|
509
477
|
baseRevision: old.revision
|
|
510
478
|
});
|
|
511
479
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
512
|
-
this.
|
|
513
|
-
this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
|
|
480
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "update").catch(console.error);
|
|
514
481
|
this.syncEngine?.flush().catch(console.error);
|
|
515
482
|
return snap;
|
|
516
483
|
} finally {
|
|
517
484
|
this._isUpdating = false;
|
|
518
485
|
}
|
|
519
486
|
}
|
|
520
|
-
// ====================== SET ======================
|
|
521
487
|
async set(data) {
|
|
522
488
|
validateDocumentData(data, "DocumentRef.set");
|
|
523
489
|
if (!this.persistence) {
|
|
@@ -532,6 +498,8 @@ var DocumentRef = class {
|
|
|
532
498
|
}).sendRequest();
|
|
533
499
|
return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
|
|
534
500
|
}
|
|
501
|
+
const existing = await this.persistence.getDoc(this.collection, this.id);
|
|
502
|
+
const mutationType = existing?.exists && !existing.deleted ? "update" : "insert";
|
|
535
503
|
const updated = await this.persistence.upsertDoc({
|
|
536
504
|
collection: this.collection,
|
|
537
505
|
id: this.id,
|
|
@@ -540,22 +508,27 @@ var DocumentRef = class {
|
|
|
540
508
|
deleted: false,
|
|
541
509
|
pending: 1,
|
|
542
510
|
localOnly: false,
|
|
543
|
-
status: "pending"
|
|
511
|
+
status: "pending",
|
|
512
|
+
revision: existing?.revision,
|
|
513
|
+
lastSyncedAt: existing?.lastSyncedAt
|
|
544
514
|
});
|
|
545
515
|
await this.persistence.enqueueMutation({
|
|
546
516
|
mutationId: this.persistence.createMutationId(),
|
|
547
517
|
collection: this.collection,
|
|
548
518
|
documentId: this.id,
|
|
549
|
-
type:
|
|
550
|
-
payload: data
|
|
519
|
+
type: mutationType,
|
|
520
|
+
payload: data,
|
|
521
|
+
baseRevision: existing?.revision
|
|
551
522
|
});
|
|
552
523
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
553
|
-
this.
|
|
554
|
-
|
|
524
|
+
this.app.offline().realtimeBridge?.publishLocalChange(
|
|
525
|
+
this.collection,
|
|
526
|
+
this.id,
|
|
527
|
+
mutationType === "insert" ? "insert" : "update"
|
|
528
|
+
).catch(console.error);
|
|
555
529
|
this.syncEngine?.flush().catch(console.error);
|
|
556
530
|
return snap;
|
|
557
531
|
}
|
|
558
|
-
// ====================== DELETE ======================
|
|
559
532
|
async delete() {
|
|
560
533
|
if (this.persistence) {
|
|
561
534
|
await this.persistence.markDeleted(this.collection, this.id, 1);
|
|
@@ -566,8 +539,7 @@ var DocumentRef = class {
|
|
|
566
539
|
type: "delete",
|
|
567
540
|
payload: null
|
|
568
541
|
});
|
|
569
|
-
this.
|
|
570
|
-
this.localStore?.notifyCollectionChanged(this.collection, this.id, "delete");
|
|
542
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "delete").catch(console.error);
|
|
571
543
|
this.syncEngine?.flush().catch(console.error);
|
|
572
544
|
return true;
|
|
573
545
|
}
|
|
@@ -582,20 +554,135 @@ var DocumentRef = class {
|
|
|
582
554
|
}).sendRequest();
|
|
583
555
|
return !!res?.success;
|
|
584
556
|
}
|
|
585
|
-
// ====================== SNAPSHOT ======================
|
|
586
557
|
onSnapshot(callback) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
558
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
559
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
560
|
+
const localUnsub = this.localStore.subscribeToDocument(this.collection, this.id, callback);
|
|
561
|
+
const remoteUnsub = realtimeBridge.watchDocument(this.collection, this.id);
|
|
562
|
+
this.persistence.getDocSnapshot(this.collection, this.id).then((snapshot) => callback(snapshot, "initial")).catch(console.error);
|
|
563
|
+
return () => {
|
|
564
|
+
localUnsub();
|
|
565
|
+
remoteUnsub();
|
|
566
|
+
};
|
|
591
567
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
568
|
+
this.fetchRemoteSnapshot().then((snapshot) => callback(snapshot, "initial")).catch(console.error);
|
|
569
|
+
return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (payload, change) => {
|
|
570
|
+
if (change === "delete") {
|
|
571
|
+
callback(null, "delete");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
callback(DocumentSnapshot.fromMap(payload), change);
|
|
595
575
|
});
|
|
596
576
|
}
|
|
577
|
+
async fetchRemoteSnapshot() {
|
|
578
|
+
try {
|
|
579
|
+
const res = await new HttpsRequest({
|
|
580
|
+
method: "POST" /* POST */,
|
|
581
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
582
|
+
headers: {
|
|
583
|
+
authorization: this.app.getConfig().token,
|
|
584
|
+
"x-project": this.app.getConfig().project
|
|
585
|
+
},
|
|
586
|
+
body: {
|
|
587
|
+
collection: this.collection,
|
|
588
|
+
id: this.id,
|
|
589
|
+
single: true
|
|
590
|
+
}
|
|
591
|
+
}).sendRequest();
|
|
592
|
+
if (!res?.success || !res.document)
|
|
593
|
+
return null;
|
|
594
|
+
const snapshot = DocumentSnapshot.fromMap(res.document);
|
|
595
|
+
if (this.persistence) {
|
|
596
|
+
await this.persistence.applyRemoteDoc(this.collection, snapshot.data);
|
|
597
|
+
}
|
|
598
|
+
return snapshot;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async refreshFromRemote() {
|
|
605
|
+
const snapshot = await this.fetchRemoteSnapshot();
|
|
606
|
+
if (this.persistence && snapshot) {
|
|
607
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "update").catch(console.error);
|
|
608
|
+
}
|
|
609
|
+
return snapshot;
|
|
610
|
+
}
|
|
597
611
|
};
|
|
598
612
|
|
|
613
|
+
// src/database/RealtimeListeners.ts
|
|
614
|
+
function buildTargetKey(collection, filter = {}) {
|
|
615
|
+
return `${collection}:${stableStringify(filter)}`;
|
|
616
|
+
}
|
|
617
|
+
function stableStringify(value) {
|
|
618
|
+
if (value === null || typeof value !== "object") {
|
|
619
|
+
return JSON.stringify(value);
|
|
620
|
+
}
|
|
621
|
+
if (Array.isArray(value)) {
|
|
622
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
623
|
+
}
|
|
624
|
+
const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
625
|
+
return `{${entries.join(",")}}`;
|
|
626
|
+
}
|
|
627
|
+
function diffSnapshots(previous, next) {
|
|
628
|
+
const previousById = new Map(previous.map((snapshot, index) => [snapshot.id, { snapshot, index }]));
|
|
629
|
+
const nextById = new Map(next.map((snapshot, index) => [snapshot.id, { snapshot, index }]));
|
|
630
|
+
const changes = [];
|
|
631
|
+
next.forEach((snapshot, index) => {
|
|
632
|
+
const before = previousById.get(snapshot.id);
|
|
633
|
+
if (!before) {
|
|
634
|
+
changes.push({
|
|
635
|
+
type: "added",
|
|
636
|
+
doc: snapshot,
|
|
637
|
+
previousDoc: null,
|
|
638
|
+
oldIndex: -1,
|
|
639
|
+
newIndex: index
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (before.index !== index || !areSnapshotsEqual(before.snapshot, snapshot)) {
|
|
644
|
+
changes.push({
|
|
645
|
+
type: "modified",
|
|
646
|
+
doc: snapshot,
|
|
647
|
+
previousDoc: before.snapshot,
|
|
648
|
+
oldIndex: before.index,
|
|
649
|
+
newIndex: index
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
previous.forEach((snapshot, index) => {
|
|
654
|
+
if (nextById.has(snapshot.id))
|
|
655
|
+
return;
|
|
656
|
+
changes.push({
|
|
657
|
+
type: "removed",
|
|
658
|
+
doc: snapshot,
|
|
659
|
+
previousDoc: snapshot,
|
|
660
|
+
oldIndex: index,
|
|
661
|
+
newIndex: -1
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
return changes;
|
|
665
|
+
}
|
|
666
|
+
function applySnapshotChange(current, incoming, source) {
|
|
667
|
+
if (!incoming) {
|
|
668
|
+
return current;
|
|
669
|
+
}
|
|
670
|
+
if (source === "delete") {
|
|
671
|
+
return current.filter((snapshot) => snapshot.id !== incoming.id);
|
|
672
|
+
}
|
|
673
|
+
const next = [...current];
|
|
674
|
+
const index = next.findIndex((snapshot) => snapshot.id === incoming.id);
|
|
675
|
+
if (index === -1) {
|
|
676
|
+
next.push(incoming);
|
|
677
|
+
return next;
|
|
678
|
+
}
|
|
679
|
+
next[index] = incoming;
|
|
680
|
+
return next;
|
|
681
|
+
}
|
|
682
|
+
function areSnapshotsEqual(left, right) {
|
|
683
|
+
return stableStringify(left.data) === stableStringify(right.data);
|
|
684
|
+
}
|
|
685
|
+
|
|
599
686
|
// src/database/Query.ts
|
|
600
687
|
var Query = class {
|
|
601
688
|
constructor(app, collection) {
|
|
@@ -608,6 +695,84 @@ var Query = class {
|
|
|
608
695
|
this.filter.push(expression);
|
|
609
696
|
return this;
|
|
610
697
|
}
|
|
698
|
+
async get() {
|
|
699
|
+
const persistence = this.app.offline().persistence;
|
|
700
|
+
if (this.filter.length > 0 && persistence) {
|
|
701
|
+
const local = await this.getLocalFilteredSnapshots();
|
|
702
|
+
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
703
|
+
return local;
|
|
704
|
+
}
|
|
705
|
+
return this.refreshFromRemote();
|
|
706
|
+
}
|
|
707
|
+
if (persistence) {
|
|
708
|
+
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
709
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
710
|
+
this.refreshFromRemote().catch(() => {
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return local;
|
|
714
|
+
}
|
|
715
|
+
return this.refreshFromRemote();
|
|
716
|
+
}
|
|
717
|
+
async update(data) {
|
|
718
|
+
const genfilter = this.buildFilter();
|
|
719
|
+
return new HttpsRequest({
|
|
720
|
+
method: "POST" /* POST */,
|
|
721
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
722
|
+
headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
|
|
723
|
+
body: {
|
|
724
|
+
collection: this.collection,
|
|
725
|
+
filter: genfilter,
|
|
726
|
+
data
|
|
727
|
+
}
|
|
728
|
+
}).sendRequest();
|
|
729
|
+
}
|
|
730
|
+
onSnapshot(callback) {
|
|
731
|
+
const genfilter = this.buildFilter();
|
|
732
|
+
const persistence = this.app.offline().persistence;
|
|
733
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
734
|
+
if (persistence && this.localStore && realtimeBridge) {
|
|
735
|
+
const targetKey = buildTargetKey(this.collection, genfilter);
|
|
736
|
+
const localUnsub = this.localStore.subscribeToCollection(targetKey, callback);
|
|
737
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
738
|
+
this.collection,
|
|
739
|
+
genfilter,
|
|
740
|
+
() => this.getLocalFilteredSnapshots()
|
|
741
|
+
);
|
|
742
|
+
this.getLocalFilteredSnapshots().then((snapshots) => {
|
|
743
|
+
realtimeBridge.primeCollectionTarget(this.collection, genfilter, snapshots);
|
|
744
|
+
callback(snapshots, "initial");
|
|
745
|
+
}).catch(console.error);
|
|
746
|
+
return () => {
|
|
747
|
+
localUnsub();
|
|
748
|
+
remoteUnsub();
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return this.subscribeOnlineSnapshot(callback);
|
|
752
|
+
}
|
|
753
|
+
onChildListener(callbacks) {
|
|
754
|
+
const genfilter = this.buildFilter();
|
|
755
|
+
const persistence = this.app.offline().persistence;
|
|
756
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
757
|
+
if (persistence && this.localStore && realtimeBridge) {
|
|
758
|
+
const targetKey = buildTargetKey(this.collection, genfilter);
|
|
759
|
+
const localUnsub = this.localStore.subscribeToChildEvents(targetKey, callbacks);
|
|
760
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
761
|
+
this.collection,
|
|
762
|
+
genfilter,
|
|
763
|
+
() => this.getLocalFilteredSnapshots()
|
|
764
|
+
);
|
|
765
|
+
this.getLocalFilteredSnapshots().then((snapshots) => {
|
|
766
|
+
realtimeBridge.primeCollectionTarget(this.collection, genfilter, snapshots);
|
|
767
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
768
|
+
}).catch(console.error);
|
|
769
|
+
return () => {
|
|
770
|
+
localUnsub();
|
|
771
|
+
remoteUnsub();
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
return this.subscribeOnlineChildListener(callbacks);
|
|
775
|
+
}
|
|
611
776
|
buildFilter() {
|
|
612
777
|
const mongoFilter = {};
|
|
613
778
|
this.filter.forEach(({ key, op, value }) => {
|
|
@@ -678,9 +843,7 @@ var Query = class {
|
|
|
678
843
|
if (this.filter.length === 0)
|
|
679
844
|
return docs;
|
|
680
845
|
return docs.filter(
|
|
681
|
-
(snapshot) => this.filter.every(
|
|
682
|
-
(expression) => this.matchesFilter(snapshot.data, expression)
|
|
683
|
-
)
|
|
846
|
+
(snapshot) => this.filter.every((expression) => this.matchesFilter(snapshot.data, expression))
|
|
684
847
|
);
|
|
685
848
|
}
|
|
686
849
|
async getLocalFilteredSnapshots() {
|
|
@@ -690,25 +853,6 @@ var Query = class {
|
|
|
690
853
|
const docs = await persistence.getCollectionSnapshots(this.collection);
|
|
691
854
|
return this.filterDocuments(docs);
|
|
692
855
|
}
|
|
693
|
-
async get() {
|
|
694
|
-
const persistence = this.app.offline().persistence;
|
|
695
|
-
if (this.filter.length > 0 && persistence) {
|
|
696
|
-
const local = await this.getLocalFilteredSnapshots();
|
|
697
|
-
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
698
|
-
return local;
|
|
699
|
-
}
|
|
700
|
-
return this.refreshFromRemote();
|
|
701
|
-
}
|
|
702
|
-
if (persistence) {
|
|
703
|
-
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
704
|
-
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
705
|
-
this.refreshFromRemote().catch(() => {
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
return local;
|
|
709
|
-
}
|
|
710
|
-
return this.refreshFromRemote();
|
|
711
|
-
}
|
|
712
856
|
async refreshFromRemote() {
|
|
713
857
|
try {
|
|
714
858
|
const genfilter = this.buildFilter();
|
|
@@ -724,69 +868,90 @@ var Query = class {
|
|
|
724
868
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
725
869
|
return [];
|
|
726
870
|
}
|
|
727
|
-
|
|
871
|
+
const snapshots = res.documents.map((d) => {
|
|
728
872
|
delete d.token;
|
|
729
873
|
d.id = d.id ?? d._id;
|
|
730
874
|
delete d._id;
|
|
731
875
|
return DocumentSnapshot.fromMap(d);
|
|
732
876
|
});
|
|
877
|
+
const persistence = this.app.offline().persistence;
|
|
878
|
+
if (!persistence) {
|
|
879
|
+
return snapshots;
|
|
880
|
+
}
|
|
881
|
+
for (const snapshot of snapshots) {
|
|
882
|
+
await persistence.applyRemoteDoc(this.collection, snapshot.data);
|
|
883
|
+
}
|
|
884
|
+
return this.getLocalFilteredSnapshots();
|
|
733
885
|
} catch {
|
|
734
886
|
return [];
|
|
735
887
|
}
|
|
736
888
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
889
|
+
subscribeOnlineSnapshot(callback) {
|
|
890
|
+
let current = [];
|
|
891
|
+
this.refreshFromRemote().then((snapshots) => {
|
|
892
|
+
current = snapshots;
|
|
893
|
+
callback(current, "initial");
|
|
894
|
+
}).catch(console.error);
|
|
895
|
+
return this.app.rtdb().subscribeToCollectionRaw(
|
|
896
|
+
this.collection,
|
|
897
|
+
(payload, change) => {
|
|
898
|
+
const source = change;
|
|
899
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
900
|
+
const next = applySnapshotChange(current, incoming, source);
|
|
901
|
+
current = this.filterDocuments(next);
|
|
902
|
+
callback(current, source, incoming.id);
|
|
903
|
+
},
|
|
904
|
+
this.buildFilter()
|
|
905
|
+
);
|
|
751
906
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
907
|
+
subscribeOnlineChildListener(callbacks) {
|
|
908
|
+
let current = [];
|
|
909
|
+
this.refreshFromRemote().then((snapshots) => {
|
|
910
|
+
current = snapshots;
|
|
911
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
912
|
+
}).catch(console.error);
|
|
913
|
+
return this.app.rtdb().subscribeToCollectionRaw(
|
|
914
|
+
this.collection,
|
|
915
|
+
(payload, change) => {
|
|
916
|
+
const source = change;
|
|
917
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
918
|
+
const next = this.filterDocuments(applySnapshotChange(current, incoming, source));
|
|
919
|
+
const childChanges = diffSnapshots(current, next);
|
|
920
|
+
current = next;
|
|
921
|
+
childChanges.forEach((childChange) => {
|
|
922
|
+
const context = {
|
|
923
|
+
snapshots: next,
|
|
924
|
+
source,
|
|
925
|
+
changedDocId: childChange.doc.id,
|
|
926
|
+
fromCache: false,
|
|
927
|
+
oldIndex: childChange.oldIndex,
|
|
928
|
+
newIndex: childChange.newIndex,
|
|
929
|
+
previousDoc: childChange.previousDoc
|
|
930
|
+
};
|
|
931
|
+
if (childChange.type === "added") {
|
|
932
|
+
callbacks.onChildAdded?.(childChange.doc, context);
|
|
933
|
+
} else if (childChange.type === "modified") {
|
|
934
|
+
callbacks.onChildUpdated?.(childChange.doc, context);
|
|
935
|
+
} else if (childChange.type === "removed") {
|
|
936
|
+
callbacks.onChildRemoved?.(childChange.doc, context);
|
|
771
937
|
}
|
|
772
|
-
) ?? (() => {
|
|
773
938
|
});
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
939
|
+
},
|
|
940
|
+
this.buildFilter()
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
emitInitialChildEvents(snapshots, callbacks) {
|
|
944
|
+
diffSnapshots([], snapshots).forEach((change) => {
|
|
945
|
+
callbacks.onChildAdded?.(change.doc, {
|
|
946
|
+
snapshots,
|
|
947
|
+
source: "initial",
|
|
948
|
+
changedDocId: change.doc.id,
|
|
949
|
+
fromCache: true,
|
|
950
|
+
oldIndex: change.oldIndex,
|
|
951
|
+
newIndex: change.newIndex,
|
|
952
|
+
previousDoc: change.previousDoc
|
|
780
953
|
});
|
|
781
|
-
|
|
782
|
-
localUnsub();
|
|
783
|
-
remoteUnsub?.();
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
787
|
-
const res = normalizePayload(payload);
|
|
788
|
-
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
789
|
-
}, genfilter);
|
|
954
|
+
});
|
|
790
955
|
}
|
|
791
956
|
};
|
|
792
957
|
|
|
@@ -818,6 +983,89 @@ var CollectionRef = class {
|
|
|
818
983
|
}
|
|
819
984
|
return local;
|
|
820
985
|
}
|
|
986
|
+
return this.fetchRemoteSnapshots();
|
|
987
|
+
}
|
|
988
|
+
async add(data) {
|
|
989
|
+
if (this.persistence) {
|
|
990
|
+
const localId = this.persistence.createLocalId();
|
|
991
|
+
const docRecord = await this.persistence.upsertDoc({
|
|
992
|
+
collection: this.collection,
|
|
993
|
+
id: localId,
|
|
994
|
+
data: { ...data, id: localId },
|
|
995
|
+
exists: true,
|
|
996
|
+
deleted: false,
|
|
997
|
+
pending: 1,
|
|
998
|
+
localOnly: true,
|
|
999
|
+
status: "pending"
|
|
1000
|
+
});
|
|
1001
|
+
await this.persistence.enqueueMutation({
|
|
1002
|
+
mutationId: this.persistence.createMutationId(),
|
|
1003
|
+
collection: this.collection,
|
|
1004
|
+
documentId: localId,
|
|
1005
|
+
type: "insert",
|
|
1006
|
+
payload: docRecord.data
|
|
1007
|
+
});
|
|
1008
|
+
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
1009
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, localId, "insert").catch(console.error);
|
|
1010
|
+
this.syncEngine?.flush().catch(console.error);
|
|
1011
|
+
return snap;
|
|
1012
|
+
}
|
|
1013
|
+
const res = await new HttpsRequest({
|
|
1014
|
+
method: "POST" /* POST */,
|
|
1015
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
1016
|
+
headers: {
|
|
1017
|
+
authorization: this.app.getConfig().token,
|
|
1018
|
+
"x-project": this.app.getConfig().project
|
|
1019
|
+
},
|
|
1020
|
+
body: { collection: this.collection, data: { ...data, id: "" } }
|
|
1021
|
+
}).sendRequest();
|
|
1022
|
+
if (!res?.success || !res.document)
|
|
1023
|
+
return null;
|
|
1024
|
+
return DocumentSnapshot.fromMap(res.document);
|
|
1025
|
+
}
|
|
1026
|
+
onSnapshot(callback) {
|
|
1027
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
1028
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
1029
|
+
const targetKey = buildTargetKey(this.collection, {});
|
|
1030
|
+
const localUnsub = this.localStore.subscribeToCollection(targetKey, callback);
|
|
1031
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
1032
|
+
this.collection,
|
|
1033
|
+
{},
|
|
1034
|
+
() => this.persistence.getCollectionSnapshots(this.collection)
|
|
1035
|
+
);
|
|
1036
|
+
this.persistence.getCollectionSnapshots(this.collection).then((snapshots) => {
|
|
1037
|
+
realtimeBridge.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1038
|
+
callback(snapshots, "insert");
|
|
1039
|
+
}).catch(console.error);
|
|
1040
|
+
return () => {
|
|
1041
|
+
localUnsub();
|
|
1042
|
+
remoteUnsub();
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
return this.subscribeOnlineSnapshot(callback);
|
|
1046
|
+
}
|
|
1047
|
+
onChildListener(callbacks) {
|
|
1048
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
1049
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
1050
|
+
const targetKey = buildTargetKey(this.collection, {});
|
|
1051
|
+
const localUnsub = this.localStore.subscribeToChildEvents(targetKey, callbacks);
|
|
1052
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
1053
|
+
this.collection,
|
|
1054
|
+
{},
|
|
1055
|
+
() => this.persistence.getCollectionSnapshots(this.collection)
|
|
1056
|
+
);
|
|
1057
|
+
this.persistence.getCollectionSnapshots(this.collection).then((snapshots) => {
|
|
1058
|
+
realtimeBridge.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1059
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
1060
|
+
}).catch(console.error);
|
|
1061
|
+
return () => {
|
|
1062
|
+
localUnsub();
|
|
1063
|
+
remoteUnsub();
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return this.subscribeOnlineChildListener(callbacks);
|
|
1067
|
+
}
|
|
1068
|
+
async refreshFromRemote() {
|
|
821
1069
|
try {
|
|
822
1070
|
const res = await new HttpsRequest({
|
|
823
1071
|
method: "POST" /* POST */,
|
|
@@ -834,18 +1082,25 @@ var CollectionRef = class {
|
|
|
834
1082
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
835
1083
|
return [];
|
|
836
1084
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
delete
|
|
841
|
-
return
|
|
1085
|
+
const normalized = res.documents.map((raw) => {
|
|
1086
|
+
const doc = { ...raw };
|
|
1087
|
+
doc.id = doc.id ?? doc._id;
|
|
1088
|
+
delete doc._id;
|
|
1089
|
+
return doc;
|
|
842
1090
|
});
|
|
1091
|
+
if (this.persistence) {
|
|
1092
|
+
await this.persistence.reconcileCollectionFromRemote(this.collection, normalized);
|
|
1093
|
+
const snapshots = await this.persistence.getCollectionSnapshots(this.collection);
|
|
1094
|
+
this.app.offline().realtimeBridge?.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1095
|
+
return snapshots;
|
|
1096
|
+
}
|
|
1097
|
+
return normalized.map((doc) => DocumentSnapshot.fromMap(doc));
|
|
843
1098
|
} catch (error) {
|
|
844
|
-
console.error("[EdmaxLabs]
|
|
1099
|
+
console.error("[EdmaxLabs] refreshFromRemote failed:", error);
|
|
845
1100
|
return [];
|
|
846
1101
|
}
|
|
847
1102
|
}
|
|
848
|
-
async
|
|
1103
|
+
async fetchRemoteSnapshots() {
|
|
849
1104
|
try {
|
|
850
1105
|
const res = await new HttpsRequest({
|
|
851
1106
|
method: "POST" /* POST */,
|
|
@@ -862,17 +1117,6 @@ var CollectionRef = class {
|
|
|
862
1117
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
863
1118
|
return [];
|
|
864
1119
|
}
|
|
865
|
-
for (const raw of res.documents) {
|
|
866
|
-
const doc = { ...raw };
|
|
867
|
-
doc.id = doc.id ?? doc._id;
|
|
868
|
-
delete doc._id;
|
|
869
|
-
if (this.persistence) {
|
|
870
|
-
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (this.persistence) {
|
|
874
|
-
return await this.persistence.getCollectionSnapshots(this.collection);
|
|
875
|
-
}
|
|
876
1120
|
return res.documents.map((d) => {
|
|
877
1121
|
delete d.token;
|
|
878
1122
|
d.id = d.id ?? d._id;
|
|
@@ -880,59 +1124,68 @@ var CollectionRef = class {
|
|
|
880
1124
|
return DocumentSnapshot.fromMap(d);
|
|
881
1125
|
});
|
|
882
1126
|
} catch (error) {
|
|
883
|
-
console.error("[EdmaxLabs]
|
|
1127
|
+
console.error("[EdmaxLabs] Collection get failed:", error);
|
|
884
1128
|
return [];
|
|
885
1129
|
}
|
|
886
1130
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
});
|
|
900
|
-
await this.persistence.enqueueMutation({
|
|
901
|
-
mutationId: this.persistence.createMutationId(),
|
|
902
|
-
collection: this.collection,
|
|
903
|
-
documentId: localId,
|
|
904
|
-
type: "insert",
|
|
905
|
-
payload: docRecord.data
|
|
906
|
-
});
|
|
907
|
-
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
908
|
-
this.localStore?.emitDocument(this.collection, localId, snap, "insert");
|
|
909
|
-
this.localStore?.notifyCollectionChanged(this.collection, localId, "insert");
|
|
910
|
-
this.syncEngine?.flush().catch(console.error);
|
|
911
|
-
return snap;
|
|
912
|
-
}
|
|
913
|
-
const res = await new HttpsRequest({
|
|
914
|
-
method: "POST" /* POST */,
|
|
915
|
-
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
916
|
-
headers: {
|
|
917
|
-
authorization: this.app.getConfig().token,
|
|
918
|
-
"x-project": this.app.getConfig().project
|
|
919
|
-
},
|
|
920
|
-
body: { collection: this.collection, data: { ...data, id: "" } }
|
|
921
|
-
// server will generate id
|
|
922
|
-
}).sendRequest();
|
|
923
|
-
if (!res?.success || !res.document)
|
|
924
|
-
return null;
|
|
925
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
1131
|
+
subscribeOnlineSnapshot(callback) {
|
|
1132
|
+
let current = [];
|
|
1133
|
+
this.fetchRemoteSnapshots().then((snapshots) => {
|
|
1134
|
+
current = snapshots;
|
|
1135
|
+
callback(current, "initial");
|
|
1136
|
+
}).catch(console.error);
|
|
1137
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, change) => {
|
|
1138
|
+
const source = change;
|
|
1139
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
1140
|
+
current = applySnapshotChange(current, incoming, source);
|
|
1141
|
+
callback(current, source, incoming.id);
|
|
1142
|
+
});
|
|
926
1143
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1144
|
+
subscribeOnlineChildListener(callbacks) {
|
|
1145
|
+
let current = [];
|
|
1146
|
+
this.fetchRemoteSnapshots().then((snapshots) => {
|
|
1147
|
+
current = snapshots;
|
|
1148
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
1149
|
+
}).catch(console.error);
|
|
1150
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, change) => {
|
|
1151
|
+
const source = change;
|
|
1152
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
1153
|
+
const next = applySnapshotChange(current, incoming, source);
|
|
1154
|
+
const childChanges = diffSnapshots(current, next);
|
|
1155
|
+
current = next;
|
|
1156
|
+
childChanges.forEach((childChange) => {
|
|
1157
|
+
const context = {
|
|
1158
|
+
snapshots: next,
|
|
1159
|
+
source,
|
|
1160
|
+
changedDocId: childChange.doc.id,
|
|
1161
|
+
fromCache: false,
|
|
1162
|
+
oldIndex: childChange.oldIndex,
|
|
1163
|
+
newIndex: childChange.newIndex,
|
|
1164
|
+
previousDoc: childChange.previousDoc
|
|
1165
|
+
};
|
|
1166
|
+
if (childChange.type === "added") {
|
|
1167
|
+
callbacks.onChildAdded?.(childChange.doc, context);
|
|
1168
|
+
} else if (childChange.type === "modified") {
|
|
1169
|
+
callbacks.onChildUpdated?.(childChange.doc, context);
|
|
1170
|
+
} else if (childChange.type === "removed") {
|
|
1171
|
+
callbacks.onChildRemoved?.(childChange.doc, context);
|
|
1172
|
+
}
|
|
931
1173
|
});
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
emitInitialChildEvents(snapshots, callbacks) {
|
|
1177
|
+
const childChanges = diffSnapshots([], snapshots);
|
|
1178
|
+
childChanges.forEach((change) => {
|
|
1179
|
+
const context = {
|
|
1180
|
+
snapshots,
|
|
1181
|
+
source: "initial",
|
|
1182
|
+
changedDocId: change.doc.id,
|
|
1183
|
+
fromCache: true,
|
|
1184
|
+
oldIndex: change.oldIndex,
|
|
1185
|
+
newIndex: change.newIndex,
|
|
1186
|
+
previousDoc: change.previousDoc
|
|
1187
|
+
};
|
|
1188
|
+
callbacks.onChildAdded?.(change.doc, context);
|
|
936
1189
|
});
|
|
937
1190
|
}
|
|
938
1191
|
};
|
|
@@ -982,6 +1235,7 @@ var Batch = class {
|
|
|
982
1235
|
const persistence = this.app.offline().persistence;
|
|
983
1236
|
const localStore = this.app.offline().localStore;
|
|
984
1237
|
const syncEngine = this.app.offline().syncEngine;
|
|
1238
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
985
1239
|
if (persistence && localStore) {
|
|
986
1240
|
const results = [];
|
|
987
1241
|
for (const op of this.ops) {
|
|
@@ -1010,8 +1264,7 @@ var Batch = class {
|
|
|
1010
1264
|
payload: op.data
|
|
1011
1265
|
});
|
|
1012
1266
|
const snap = DocumentSnapshot.fromMap(upserted.data);
|
|
1013
|
-
|
|
1014
|
-
localStore.notifyCollectionChanged(op.collection, op.id, "insert");
|
|
1267
|
+
realtimeBridge?.publishLocalChange(op.collection, op.id, "insert").catch(console.error);
|
|
1015
1268
|
results.push(snap);
|
|
1016
1269
|
} else if (op.op === "delete") {
|
|
1017
1270
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
@@ -1123,6 +1376,20 @@ function generateUUID() {
|
|
|
1123
1376
|
});
|
|
1124
1377
|
}
|
|
1125
1378
|
|
|
1379
|
+
// src/utils/documentNomalizer.ts
|
|
1380
|
+
function normalizePayload(payload) {
|
|
1381
|
+
const raw = payload?.document ?? payload?.data ?? payload;
|
|
1382
|
+
if (!raw)
|
|
1383
|
+
return null;
|
|
1384
|
+
const doc = { ...raw };
|
|
1385
|
+
doc.id = doc.id ?? raw?.id ?? raw?._id ?? payload?.id ?? payload?._id;
|
|
1386
|
+
delete doc._id;
|
|
1387
|
+
return {
|
|
1388
|
+
change: payload?.change ?? raw?.change ?? null,
|
|
1389
|
+
data: doc
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1126
1393
|
// src/database/Realtime.ts
|
|
1127
1394
|
var Realtime = class {
|
|
1128
1395
|
constructor(app) {
|
|
@@ -1191,8 +1458,12 @@ var Realtime = class {
|
|
|
1191
1458
|
const channel = `${lid}-${event}`;
|
|
1192
1459
|
const fn = (payload) => {
|
|
1193
1460
|
const normalized = normalizePayload(payload);
|
|
1194
|
-
if (!normalized)
|
|
1461
|
+
if (!normalized) {
|
|
1462
|
+
if (event === "delete") {
|
|
1463
|
+
callback(payload, "delete");
|
|
1464
|
+
}
|
|
1195
1465
|
return;
|
|
1466
|
+
}
|
|
1196
1467
|
callback(normalized.data, event);
|
|
1197
1468
|
};
|
|
1198
1469
|
this.socket.on(channel, fn);
|
|
@@ -1316,11 +1587,11 @@ var LocalStore = class {
|
|
|
1316
1587
|
constructor() {
|
|
1317
1588
|
this.documentListeners = /* @__PURE__ */ new Map();
|
|
1318
1589
|
this.collectionListeners = /* @__PURE__ */ new Map();
|
|
1590
|
+
this.childListeners = /* @__PURE__ */ new Map();
|
|
1319
1591
|
}
|
|
1320
1592
|
docKey(collection, id) {
|
|
1321
1593
|
return `${collection}:${id}`;
|
|
1322
1594
|
}
|
|
1323
|
-
// ===================== DOCUMENT LISTENERS =====================
|
|
1324
1595
|
subscribeToDocument(collection, id, callback) {
|
|
1325
1596
|
const key = this.docKey(collection, id);
|
|
1326
1597
|
if (!this.documentListeners.has(key)) {
|
|
@@ -1335,24 +1606,33 @@ var LocalStore = class {
|
|
|
1335
1606
|
}
|
|
1336
1607
|
};
|
|
1337
1608
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
this.collectionListeners.set(collection, /* @__PURE__ */ new Set());
|
|
1609
|
+
subscribeToCollection(targetKey, callback) {
|
|
1610
|
+
if (!this.collectionListeners.has(targetKey)) {
|
|
1611
|
+
this.collectionListeners.set(targetKey, /* @__PURE__ */ new Set());
|
|
1342
1612
|
}
|
|
1343
|
-
const listeners = this.collectionListeners.get(
|
|
1613
|
+
const listeners = this.collectionListeners.get(targetKey);
|
|
1344
1614
|
listeners.add(callback);
|
|
1345
1615
|
return () => {
|
|
1346
1616
|
listeners.delete(callback);
|
|
1347
1617
|
if (listeners.size === 0) {
|
|
1348
|
-
this.collectionListeners.delete(
|
|
1618
|
+
this.collectionListeners.delete(targetKey);
|
|
1619
|
+
this.childListeners.delete(targetKey);
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
subscribeToChildEvents(targetKey, callbacks) {
|
|
1624
|
+
if (!this.childListeners.has(targetKey)) {
|
|
1625
|
+
this.childListeners.set(targetKey, /* @__PURE__ */ new Set());
|
|
1626
|
+
}
|
|
1627
|
+
const listeners = this.childListeners.get(targetKey);
|
|
1628
|
+
listeners.add(callbacks);
|
|
1629
|
+
return () => {
|
|
1630
|
+
listeners.delete(callbacks);
|
|
1631
|
+
if (listeners.size === 0) {
|
|
1632
|
+
this.childListeners.delete(targetKey);
|
|
1349
1633
|
}
|
|
1350
1634
|
};
|
|
1351
1635
|
}
|
|
1352
|
-
// ===================== EMITTERS =====================
|
|
1353
|
-
/**
|
|
1354
|
-
* Notify all listeners for a specific document
|
|
1355
|
-
*/
|
|
1356
1636
|
emitDocument(collection, id, snapshot, change) {
|
|
1357
1637
|
const key = this.docKey(collection, id);
|
|
1358
1638
|
this.documentListeners.get(key)?.forEach((cb) => {
|
|
@@ -1363,64 +1643,66 @@ var LocalStore = class {
|
|
|
1363
1643
|
}
|
|
1364
1644
|
});
|
|
1365
1645
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
* This is the most important fix — collection listeners now receive the full list.
|
|
1369
|
-
*/
|
|
1370
|
-
emitCollection(collection, snapshots, change, changedDocId) {
|
|
1371
|
-
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1646
|
+
emitCollection(targetKey, emission) {
|
|
1647
|
+
this.collectionListeners.get(targetKey)?.forEach((cb) => {
|
|
1372
1648
|
try {
|
|
1373
|
-
cb(snapshots,
|
|
1649
|
+
cb(emission.snapshots, emission.source, emission.changedDocId);
|
|
1374
1650
|
} catch (err) {
|
|
1375
|
-
console.error(`[EdmaxLabs] Error in collection listener for ${
|
|
1651
|
+
console.error(`[EdmaxLabs] Error in collection listener for ${targetKey}:`, err);
|
|
1376
1652
|
}
|
|
1377
1653
|
});
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1386
|
-
try {
|
|
1387
|
-
cb([], change, changedDocId);
|
|
1388
|
-
} catch (err) {
|
|
1389
|
-
console.error(`[EdmaxLabs] Error in collection listener for ${collection}:`, err);
|
|
1390
|
-
}
|
|
1654
|
+
const childChanges = emission.childChanges ?? [];
|
|
1655
|
+
if (childChanges.length === 0)
|
|
1656
|
+
return;
|
|
1657
|
+
this.childListeners.get(targetKey)?.forEach((callbacks) => {
|
|
1658
|
+
childChanges.forEach((change) => {
|
|
1659
|
+
this.dispatchChildEvent(callbacks, change, emission);
|
|
1660
|
+
});
|
|
1391
1661
|
});
|
|
1392
1662
|
}
|
|
1393
|
-
// ===================== UTILITY =====================
|
|
1394
|
-
/**
|
|
1395
|
-
* Clear all listeners (useful for testing or when persistence is disabled)
|
|
1396
|
-
*/
|
|
1397
1663
|
clearAllListeners() {
|
|
1398
1664
|
this.documentListeners.clear();
|
|
1399
1665
|
this.collectionListeners.clear();
|
|
1666
|
+
this.childListeners.clear();
|
|
1400
1667
|
}
|
|
1401
|
-
/**
|
|
1402
|
-
* Get current listener count (for debugging / dev tools)
|
|
1403
|
-
*/
|
|
1404
1668
|
get listenerCount() {
|
|
1405
1669
|
let docCount = 0;
|
|
1406
1670
|
this.documentListeners.forEach((set) => docCount += set.size);
|
|
1407
1671
|
let collCount = 0;
|
|
1408
1672
|
this.collectionListeners.forEach((set) => collCount += set.size);
|
|
1673
|
+
this.childListeners.forEach((set) => collCount += set.size);
|
|
1409
1674
|
return { documents: docCount, collections: collCount };
|
|
1410
1675
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
removeCollectionListeners(collection) {
|
|
1415
|
-
this.collectionListeners.delete(collection);
|
|
1676
|
+
removeCollectionListeners(targetKey) {
|
|
1677
|
+
this.collectionListeners.delete(targetKey);
|
|
1678
|
+
this.childListeners.delete(targetKey);
|
|
1416
1679
|
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Remove all listeners for a specific document (useful for cleanup)
|
|
1419
|
-
*/
|
|
1420
1680
|
removeDocumentListeners(collection, id) {
|
|
1421
1681
|
const key = this.docKey(collection, id);
|
|
1422
1682
|
this.documentListeners.delete(key);
|
|
1423
1683
|
}
|
|
1684
|
+
dispatchChildEvent(callbacks, change, emission) {
|
|
1685
|
+
const context = {
|
|
1686
|
+
snapshots: emission.snapshots,
|
|
1687
|
+
source: emission.source,
|
|
1688
|
+
changedDocId: emission.changedDocId,
|
|
1689
|
+
fromCache: emission.fromCache ?? true,
|
|
1690
|
+
oldIndex: change.oldIndex,
|
|
1691
|
+
newIndex: change.newIndex,
|
|
1692
|
+
previousDoc: change.previousDoc
|
|
1693
|
+
};
|
|
1694
|
+
try {
|
|
1695
|
+
if (change.type === "added") {
|
|
1696
|
+
callbacks.onChildAdded?.(change.doc, context);
|
|
1697
|
+
} else if (change.type === "modified") {
|
|
1698
|
+
callbacks.onChildUpdated?.(change.doc, context);
|
|
1699
|
+
} else if (change.type === "removed") {
|
|
1700
|
+
callbacks.onChildRemoved?.(change.doc, context);
|
|
1701
|
+
}
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
console.error("[EdmaxLabs] Error in child listener:", err);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1424
1706
|
};
|
|
1425
1707
|
|
|
1426
1708
|
// ../../../../node_modules/idb/build/wrap-idb-value.js
|
|
@@ -1791,11 +2073,25 @@ var Persistence = class {
|
|
|
1791
2073
|
}
|
|
1792
2074
|
async getPendingMutations() {
|
|
1793
2075
|
const app = await this.getDb();
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2076
|
+
const pending = await app.getAllFromIndex("mutations", "by-status", "pending");
|
|
2077
|
+
return pending.sort((a, b) => a.createdAt - b.createdAt);
|
|
2078
|
+
}
|
|
2079
|
+
async getFailedMutations() {
|
|
2080
|
+
const app = await this.getDb();
|
|
2081
|
+
const failed = await app.getAllFromIndex("mutations", "by-status", "failed");
|
|
2082
|
+
return failed.sort((a, b) => a.createdAt - b.createdAt);
|
|
2083
|
+
}
|
|
2084
|
+
async recoverSyncingMutations() {
|
|
2085
|
+
const app = await this.getDb();
|
|
2086
|
+
const syncing = await app.getAllFromIndex("mutations", "by-status", "syncing");
|
|
2087
|
+
for (const mutation of syncing) {
|
|
2088
|
+
await app.put("mutations", {
|
|
2089
|
+
...mutation,
|
|
2090
|
+
status: "pending",
|
|
2091
|
+
updatedAt: this.now()
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
return syncing.length;
|
|
1799
2095
|
}
|
|
1800
2096
|
async getMutation(mutationId) {
|
|
1801
2097
|
const app = await this.getDb();
|
|
@@ -1900,6 +2196,27 @@ var Persistence = class {
|
|
|
1900
2196
|
}
|
|
1901
2197
|
return this.markDeleted(collection, id, 0);
|
|
1902
2198
|
}
|
|
2199
|
+
async reconcileCollectionFromRemote(collection, documents) {
|
|
2200
|
+
const app = await this.getDb();
|
|
2201
|
+
const existing = await app.getAllFromIndex("docs", "by-collection", collection);
|
|
2202
|
+
const incomingIds = /* @__PURE__ */ new Set();
|
|
2203
|
+
for (const data of documents) {
|
|
2204
|
+
const id = data.id ?? data._id;
|
|
2205
|
+
if (!id)
|
|
2206
|
+
continue;
|
|
2207
|
+
incomingIds.add(String(id));
|
|
2208
|
+
await this.applyRemoteDoc(collection, { ...data, id: String(id) });
|
|
2209
|
+
}
|
|
2210
|
+
for (const doc of existing) {
|
|
2211
|
+
if (doc.pending && doc.pending > 0)
|
|
2212
|
+
continue;
|
|
2213
|
+
if (!doc.exists || doc.deleted)
|
|
2214
|
+
continue;
|
|
2215
|
+
if (incomingIds.has(doc.id))
|
|
2216
|
+
continue;
|
|
2217
|
+
await this.markDeleted(collection, doc.id, 0);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
1903
2220
|
// ==================== UTILITIES ====================
|
|
1904
2221
|
createLocalId() {
|
|
1905
2222
|
return `local_${crypto.randomUUID()}`;
|
|
@@ -1929,112 +2246,169 @@ var RealtimeBridge = class {
|
|
|
1929
2246
|
this.app = app;
|
|
1930
2247
|
this.persistence = persistence;
|
|
1931
2248
|
this.store = store;
|
|
1932
|
-
this.
|
|
1933
|
-
this.
|
|
2249
|
+
this.collectionTargets = /* @__PURE__ */ new Map();
|
|
2250
|
+
this.documentTargets = /* @__PURE__ */ new Map();
|
|
1934
2251
|
}
|
|
1935
2252
|
docKey(collection, id) {
|
|
1936
2253
|
return `${collection}:${id}`;
|
|
1937
2254
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const
|
|
1941
|
-
if (
|
|
1942
|
-
|
|
2255
|
+
watchCollection(collection, filter = {}, read) {
|
|
2256
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2257
|
+
const existing = this.collectionTargets.get(targetKey);
|
|
2258
|
+
if (existing) {
|
|
2259
|
+
existing.refCount += 1;
|
|
2260
|
+
return () => this.releaseCollection(targetKey);
|
|
1943
2261
|
}
|
|
1944
|
-
this.emitCurrentCollection(collection);
|
|
1945
2262
|
const unsub = this.app.rtdb().subscribeToCollectionRaw(
|
|
1946
2263
|
collection,
|
|
1947
2264
|
async (payload, change) => {
|
|
1948
2265
|
if (change === "delete") {
|
|
1949
2266
|
const id = payload?.id || payload?._id;
|
|
1950
|
-
if (id)
|
|
2267
|
+
if (id) {
|
|
1951
2268
|
await this.handleRemoteDelete(collection, id);
|
|
1952
|
-
|
|
1953
|
-
|
|
2269
|
+
}
|
|
2270
|
+
return;
|
|
1954
2271
|
}
|
|
2272
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1955
2273
|
},
|
|
1956
2274
|
filter
|
|
1957
2275
|
);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2276
|
+
this.collectionTargets.set(targetKey, {
|
|
2277
|
+
targetKey,
|
|
2278
|
+
collection,
|
|
2279
|
+
filter,
|
|
2280
|
+
read,
|
|
2281
|
+
refCount: 1,
|
|
2282
|
+
unsub,
|
|
2283
|
+
lastSnapshots: []
|
|
2284
|
+
});
|
|
2285
|
+
return () => this.releaseCollection(targetKey);
|
|
1964
2286
|
}
|
|
1965
2287
|
watchDocument(collection, id) {
|
|
1966
2288
|
const key = this.docKey(collection, id);
|
|
1967
|
-
|
|
1968
|
-
|
|
2289
|
+
const existing = this.documentTargets.get(key);
|
|
2290
|
+
if (existing) {
|
|
2291
|
+
existing.refCount += 1;
|
|
2292
|
+
return () => this.releaseDocument(key);
|
|
1969
2293
|
}
|
|
1970
|
-
this.emitCurrentDocument(collection, id);
|
|
1971
2294
|
const unsub = this.app.rtdb().subscribeToDocumentRaw(
|
|
1972
2295
|
collection,
|
|
1973
2296
|
id,
|
|
1974
2297
|
async (payload, change) => {
|
|
1975
2298
|
if (change === "delete") {
|
|
1976
2299
|
await this.handleRemoteDelete(collection, id);
|
|
1977
|
-
|
|
1978
|
-
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
2300
|
+
return;
|
|
1979
2301
|
}
|
|
2302
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1980
2303
|
}
|
|
1981
2304
|
);
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2305
|
+
this.documentTargets.set(key, {
|
|
2306
|
+
key,
|
|
2307
|
+
collection,
|
|
2308
|
+
id,
|
|
2309
|
+
refCount: 1,
|
|
2310
|
+
unsub
|
|
2311
|
+
});
|
|
2312
|
+
return () => this.releaseDocument(key);
|
|
2313
|
+
}
|
|
2314
|
+
async emitCurrentCollection(collection, filter = {}) {
|
|
2315
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2316
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2317
|
+
if (!target)
|
|
2318
|
+
return [];
|
|
2319
|
+
return this.refreshCollectionTarget(target, "initial");
|
|
2320
|
+
}
|
|
2321
|
+
primeCollectionTarget(collection, filter, snapshots) {
|
|
2322
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2323
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2324
|
+
if (!target)
|
|
2325
|
+
return;
|
|
2326
|
+
target.lastSnapshots = snapshots;
|
|
1988
2327
|
}
|
|
1989
|
-
// ===================== INTERNAL HANDLERS =====================
|
|
1990
2328
|
async emitCurrentDocument(collection, id) {
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
|
|
2329
|
+
const snapshot = await this.getCurrentDocumentSnapshot(collection, id);
|
|
2330
|
+
this.store.emitDocument(collection, id, snapshot, "initial");
|
|
2331
|
+
return snapshot;
|
|
1994
2332
|
}
|
|
1995
|
-
async
|
|
1996
|
-
const
|
|
1997
|
-
this.store.
|
|
2333
|
+
async publishLocalChange(collection, id, source) {
|
|
2334
|
+
const snapshot = await this.getCurrentDocumentSnapshot(collection, id);
|
|
2335
|
+
this.store.emitDocument(collection, id, snapshot, source);
|
|
2336
|
+
await this.refreshCollectionTargets(collection, source, id);
|
|
2337
|
+
}
|
|
2338
|
+
async onReconnect() {
|
|
2339
|
+
for (const target of this.collectionTargets.values()) {
|
|
2340
|
+
await this.refreshCollectionTarget(target, "initial");
|
|
2341
|
+
}
|
|
2342
|
+
for (const target of this.documentTargets.values()) {
|
|
2343
|
+
await this.emitCurrentDocument(target.collection, target.id);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
dispose() {
|
|
2347
|
+
this.collectionTargets.forEach((target) => target.unsub());
|
|
2348
|
+
this.documentTargets.forEach((target) => target.unsub());
|
|
2349
|
+
this.collectionTargets.clear();
|
|
2350
|
+
this.documentTargets.clear();
|
|
1998
2351
|
}
|
|
1999
|
-
async handleRemoteCreateOrUpdate(collection, raw,
|
|
2352
|
+
async handleRemoteCreateOrUpdate(collection, raw, source) {
|
|
2000
2353
|
const id = raw.id ?? raw._id;
|
|
2001
2354
|
if (!id)
|
|
2002
2355
|
return;
|
|
2003
2356
|
const saved = await this.persistence.applyRemoteDoc(collection, raw);
|
|
2004
|
-
if (
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
this.
|
|
2357
|
+
if (saved) {
|
|
2358
|
+
const snap = DocumentSnapshot.fromMap(saved.data);
|
|
2359
|
+
this.store.emitDocument(collection, id, snap, source);
|
|
2360
|
+
}
|
|
2361
|
+
await this.refreshCollectionTargets(collection, source, id, false);
|
|
2009
2362
|
}
|
|
2010
2363
|
async handleRemoteDelete(collection, id) {
|
|
2011
2364
|
await this.persistence.applyRemoteDelete(collection, id);
|
|
2012
2365
|
this.store.emitDocument(collection, id, null, "delete");
|
|
2013
|
-
this.
|
|
2366
|
+
await this.refreshCollectionTargets(collection, "delete", id, false);
|
|
2014
2367
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
* Replays pending mutations + refreshes all active subscriptions from cache
|
|
2019
|
-
*/
|
|
2020
|
-
async onReconnect() {
|
|
2021
|
-
for (const [key] of this.collectionUnsubs) {
|
|
2022
|
-
const collection = key.split(":")[0];
|
|
2023
|
-
await this.emitCurrentCollection(collection);
|
|
2024
|
-
}
|
|
2025
|
-
for (const [key] of this.documentUnsubs) {
|
|
2026
|
-
const [collection, id] = key.split(":");
|
|
2027
|
-
await this.emitCurrentDocument(collection, id);
|
|
2028
|
-
}
|
|
2368
|
+
async getCurrentDocumentSnapshot(collection, id) {
|
|
2369
|
+
const localDoc = await this.persistence.getDoc(collection, id);
|
|
2370
|
+
return localDoc && localDoc.exists && !localDoc.deleted ? DocumentSnapshot.fromMap(localDoc.data) : null;
|
|
2029
2371
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2372
|
+
async refreshCollectionTargets(collection, source, changedDocId, fromCache = true) {
|
|
2373
|
+
const targets = Array.from(this.collectionTargets.values()).filter(
|
|
2374
|
+
(target) => target.collection === collection
|
|
2375
|
+
);
|
|
2376
|
+
for (const target of targets) {
|
|
2377
|
+
await this.refreshCollectionTarget(target, source, changedDocId, fromCache);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
async refreshCollectionTarget(target, source, changedDocId, fromCache = true) {
|
|
2381
|
+
const snapshots = await target.read();
|
|
2382
|
+
const childChanges = diffSnapshots(target.lastSnapshots, snapshots);
|
|
2383
|
+
target.lastSnapshots = snapshots;
|
|
2384
|
+
this.store.emitCollection(target.targetKey, {
|
|
2385
|
+
snapshots,
|
|
2386
|
+
source,
|
|
2387
|
+
changedDocId,
|
|
2388
|
+
fromCache,
|
|
2389
|
+
childChanges
|
|
2390
|
+
});
|
|
2391
|
+
return snapshots;
|
|
2392
|
+
}
|
|
2393
|
+
releaseCollection(targetKey) {
|
|
2394
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2395
|
+
if (!target)
|
|
2396
|
+
return;
|
|
2397
|
+
target.refCount -= 1;
|
|
2398
|
+
if (target.refCount > 0)
|
|
2399
|
+
return;
|
|
2400
|
+
target.unsub();
|
|
2401
|
+
this.collectionTargets.delete(targetKey);
|
|
2402
|
+
}
|
|
2403
|
+
releaseDocument(key) {
|
|
2404
|
+
const target = this.documentTargets.get(key);
|
|
2405
|
+
if (!target)
|
|
2406
|
+
return;
|
|
2407
|
+
target.refCount -= 1;
|
|
2408
|
+
if (target.refCount > 0)
|
|
2409
|
+
return;
|
|
2410
|
+
target.unsub();
|
|
2411
|
+
this.documentTargets.delete(key);
|
|
2038
2412
|
}
|
|
2039
2413
|
};
|
|
2040
2414
|
|
|
@@ -2046,16 +2420,19 @@ var SyncEngine = class {
|
|
|
2046
2420
|
this.retryTimeout = null;
|
|
2047
2421
|
this.MAX_RETRIES = 5;
|
|
2048
2422
|
this.BASE_RETRY_DELAY = 2e3;
|
|
2423
|
+
this.handleOnline = () => this.onNetworkOnline();
|
|
2424
|
+
this.handleVisibilityChange = () => {
|
|
2425
|
+
if (document.visibilityState === "visible")
|
|
2426
|
+
this.onNetworkOnline();
|
|
2427
|
+
};
|
|
2049
2428
|
this.app = app;
|
|
2050
2429
|
this.persistence = persistence;
|
|
2051
2430
|
this.store = store;
|
|
2052
2431
|
this.realtimeBridge = realtimeBridge || null;
|
|
2432
|
+
this.persistence.recoverSyncingMutations().catch(console.error);
|
|
2053
2433
|
if (typeof window !== "undefined") {
|
|
2054
|
-
window.addEventListener("online",
|
|
2055
|
-
document.addEventListener("visibilitychange",
|
|
2056
|
-
if (document.visibilityState === "visible")
|
|
2057
|
-
this.onNetworkOnline();
|
|
2058
|
-
});
|
|
2434
|
+
window.addEventListener("online", this.handleOnline);
|
|
2435
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2059
2436
|
}
|
|
2060
2437
|
}
|
|
2061
2438
|
onNetworkOnline() {
|
|
@@ -2072,6 +2449,7 @@ var SyncEngine = class {
|
|
|
2072
2449
|
return;
|
|
2073
2450
|
this.syncing = true;
|
|
2074
2451
|
try {
|
|
2452
|
+
await this.persistence.recoverSyncingMutations();
|
|
2075
2453
|
const pending = await this.persistence.getPendingMutations();
|
|
2076
2454
|
for (const mutation of pending) {
|
|
2077
2455
|
if (mutation.retryCount >= this.MAX_RETRIES) {
|
|
@@ -2148,8 +2526,7 @@ var SyncEngine = class {
|
|
|
2148
2526
|
if (replaced) {
|
|
2149
2527
|
const snap = DocumentSnapshot.fromMap(replaced.data);
|
|
2150
2528
|
this.store.emitDocument(mutation.collection, oldId, snap, "insert");
|
|
2151
|
-
this.
|
|
2152
|
-
this.store.notifyCollectionChanged(mutation.collection, newId, "insert");
|
|
2529
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, newId, "insert");
|
|
2153
2530
|
}
|
|
2154
2531
|
return true;
|
|
2155
2532
|
}
|
|
@@ -2182,7 +2559,7 @@ var SyncEngine = class {
|
|
|
2182
2559
|
});
|
|
2183
2560
|
const snap = DocumentSnapshot.fromMap(local.data);
|
|
2184
2561
|
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "update");
|
|
2185
|
-
this.
|
|
2562
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, mutation.documentId, "update");
|
|
2186
2563
|
return true;
|
|
2187
2564
|
}
|
|
2188
2565
|
async syncDelete(mutation) {
|
|
@@ -2199,7 +2576,7 @@ var SyncEngine = class {
|
|
|
2199
2576
|
return false;
|
|
2200
2577
|
await this.persistence.markDeleted(mutation.collection, mutation.documentId, 0);
|
|
2201
2578
|
this.store.emitDocument(mutation.collection, mutation.documentId, null, "delete");
|
|
2202
|
-
this.
|
|
2579
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, mutation.documentId, "delete");
|
|
2203
2580
|
return true;
|
|
2204
2581
|
}
|
|
2205
2582
|
scheduleRetry() {
|
|
@@ -2219,8 +2596,7 @@ var SyncEngine = class {
|
|
|
2219
2596
|
* Returns mutations that exceeded MAX_RETRIES
|
|
2220
2597
|
*/
|
|
2221
2598
|
async getFailedMutations() {
|
|
2222
|
-
|
|
2223
|
-
return all.filter((m) => m.status === "failed");
|
|
2599
|
+
return this.persistence.getFailedMutations();
|
|
2224
2600
|
}
|
|
2225
2601
|
/**
|
|
2226
2602
|
* Retry a specific failed mutation by resetting its retry count
|
|
@@ -2262,6 +2638,10 @@ var SyncEngine = class {
|
|
|
2262
2638
|
if (this.retryTimeout) {
|
|
2263
2639
|
clearTimeout(this.retryTimeout);
|
|
2264
2640
|
}
|
|
2641
|
+
if (typeof window !== "undefined") {
|
|
2642
|
+
window.removeEventListener("online", this.handleOnline);
|
|
2643
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2644
|
+
}
|
|
2265
2645
|
}
|
|
2266
2646
|
};
|
|
2267
2647
|
|