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.mjs
CHANGED
|
@@ -359,20 +359,6 @@ var DocumentSnapshot = class _DocumentSnapshot {
|
|
|
359
359
|
}
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
-
// src/utils/documentNomalizer.ts
|
|
363
|
-
function normalizePayload(payload) {
|
|
364
|
-
const raw = payload?.document ?? payload?.data ?? payload;
|
|
365
|
-
if (!raw)
|
|
366
|
-
return null;
|
|
367
|
-
const doc = { ...raw };
|
|
368
|
-
doc.id = payload.id ?? payload._id;
|
|
369
|
-
delete doc._id;
|
|
370
|
-
return {
|
|
371
|
-
change: payload?.change ?? raw?.change ?? null,
|
|
372
|
-
data: doc
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
362
|
// src/database/DocumentRef.ts
|
|
377
363
|
function validateDocumentData(data, operation) {
|
|
378
364
|
if (data === null || data === void 0) {
|
|
@@ -398,40 +384,22 @@ var DocumentRef = class {
|
|
|
398
384
|
this.syncEngine = app.offline().syncEngine;
|
|
399
385
|
this.localStore = app.offline().localStore;
|
|
400
386
|
}
|
|
401
|
-
// ====================== GET ======================
|
|
402
387
|
async get() {
|
|
403
388
|
if (this.persistence) {
|
|
404
389
|
try {
|
|
405
390
|
const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
|
|
391
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
392
|
+
this.refreshFromRemote().catch(() => {
|
|
393
|
+
});
|
|
394
|
+
}
|
|
406
395
|
if (localSnap)
|
|
407
396
|
return localSnap;
|
|
408
397
|
} catch (error) {
|
|
409
398
|
console.error("[EdmaxLabs] Cache read error:", error);
|
|
410
399
|
}
|
|
411
400
|
}
|
|
412
|
-
|
|
413
|
-
const res = await new HttpsRequest({
|
|
414
|
-
method: "POST" /* POST */,
|
|
415
|
-
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
416
|
-
headers: {
|
|
417
|
-
authorization: this.app.getConfig().token,
|
|
418
|
-
"x-project": this.app.getConfig().project
|
|
419
|
-
},
|
|
420
|
-
body: {
|
|
421
|
-
collection: this.collection,
|
|
422
|
-
id: this.id,
|
|
423
|
-
single: true
|
|
424
|
-
}
|
|
425
|
-
}).sendRequest();
|
|
426
|
-
if (!res?.success || !res.document)
|
|
427
|
-
return null;
|
|
428
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
401
|
+
return this.fetchRemoteSnapshot();
|
|
433
402
|
}
|
|
434
|
-
// ====================== UPDATE ======================
|
|
435
403
|
async update(data) {
|
|
436
404
|
if (this._isUpdating) {
|
|
437
405
|
console.warn(`[DocumentRef] update recursion blocked on ${this.collection}/${this.id}`);
|
|
@@ -477,15 +445,13 @@ var DocumentRef = class {
|
|
|
477
445
|
baseRevision: old.revision
|
|
478
446
|
});
|
|
479
447
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
480
|
-
this.
|
|
481
|
-
this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
|
|
448
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "update").catch(console.error);
|
|
482
449
|
this.syncEngine?.flush().catch(console.error);
|
|
483
450
|
return snap;
|
|
484
451
|
} finally {
|
|
485
452
|
this._isUpdating = false;
|
|
486
453
|
}
|
|
487
454
|
}
|
|
488
|
-
// ====================== SET ======================
|
|
489
455
|
async set(data) {
|
|
490
456
|
validateDocumentData(data, "DocumentRef.set");
|
|
491
457
|
if (!this.persistence) {
|
|
@@ -500,6 +466,8 @@ var DocumentRef = class {
|
|
|
500
466
|
}).sendRequest();
|
|
501
467
|
return res?.success ? DocumentSnapshot.fromMap({ ...data, id: this.id }) : null;
|
|
502
468
|
}
|
|
469
|
+
const existing = await this.persistence.getDoc(this.collection, this.id);
|
|
470
|
+
const mutationType = existing?.exists && !existing.deleted ? "update" : "insert";
|
|
503
471
|
const updated = await this.persistence.upsertDoc({
|
|
504
472
|
collection: this.collection,
|
|
505
473
|
id: this.id,
|
|
@@ -508,22 +476,27 @@ var DocumentRef = class {
|
|
|
508
476
|
deleted: false,
|
|
509
477
|
pending: 1,
|
|
510
478
|
localOnly: false,
|
|
511
|
-
status: "pending"
|
|
479
|
+
status: "pending",
|
|
480
|
+
revision: existing?.revision,
|
|
481
|
+
lastSyncedAt: existing?.lastSyncedAt
|
|
512
482
|
});
|
|
513
483
|
await this.persistence.enqueueMutation({
|
|
514
484
|
mutationId: this.persistence.createMutationId(),
|
|
515
485
|
collection: this.collection,
|
|
516
486
|
documentId: this.id,
|
|
517
|
-
type:
|
|
518
|
-
payload: data
|
|
487
|
+
type: mutationType,
|
|
488
|
+
payload: data,
|
|
489
|
+
baseRevision: existing?.revision
|
|
519
490
|
});
|
|
520
491
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
521
|
-
this.
|
|
522
|
-
|
|
492
|
+
this.app.offline().realtimeBridge?.publishLocalChange(
|
|
493
|
+
this.collection,
|
|
494
|
+
this.id,
|
|
495
|
+
mutationType === "insert" ? "insert" : "update"
|
|
496
|
+
).catch(console.error);
|
|
523
497
|
this.syncEngine?.flush().catch(console.error);
|
|
524
498
|
return snap;
|
|
525
499
|
}
|
|
526
|
-
// ====================== DELETE ======================
|
|
527
500
|
async delete() {
|
|
528
501
|
if (this.persistence) {
|
|
529
502
|
await this.persistence.markDeleted(this.collection, this.id, 1);
|
|
@@ -534,8 +507,7 @@ var DocumentRef = class {
|
|
|
534
507
|
type: "delete",
|
|
535
508
|
payload: null
|
|
536
509
|
});
|
|
537
|
-
this.
|
|
538
|
-
this.localStore?.notifyCollectionChanged(this.collection, this.id, "delete");
|
|
510
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "delete").catch(console.error);
|
|
539
511
|
this.syncEngine?.flush().catch(console.error);
|
|
540
512
|
return true;
|
|
541
513
|
}
|
|
@@ -550,20 +522,135 @@ var DocumentRef = class {
|
|
|
550
522
|
}).sendRequest();
|
|
551
523
|
return !!res?.success;
|
|
552
524
|
}
|
|
553
|
-
// ====================== SNAPSHOT ======================
|
|
554
525
|
onSnapshot(callback) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
526
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
527
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
528
|
+
const localUnsub = this.localStore.subscribeToDocument(this.collection, this.id, callback);
|
|
529
|
+
const remoteUnsub = realtimeBridge.watchDocument(this.collection, this.id);
|
|
530
|
+
this.persistence.getDocSnapshot(this.collection, this.id).then((snapshot) => callback(snapshot, "initial")).catch(console.error);
|
|
531
|
+
return () => {
|
|
532
|
+
localUnsub();
|
|
533
|
+
remoteUnsub();
|
|
534
|
+
};
|
|
559
535
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
536
|
+
this.fetchRemoteSnapshot().then((snapshot) => callback(snapshot, "initial")).catch(console.error);
|
|
537
|
+
return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (payload, change) => {
|
|
538
|
+
if (change === "delete") {
|
|
539
|
+
callback(null, "delete");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
callback(DocumentSnapshot.fromMap(payload), change);
|
|
563
543
|
});
|
|
564
544
|
}
|
|
545
|
+
async fetchRemoteSnapshot() {
|
|
546
|
+
try {
|
|
547
|
+
const res = await new HttpsRequest({
|
|
548
|
+
method: "POST" /* POST */,
|
|
549
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
550
|
+
headers: {
|
|
551
|
+
authorization: this.app.getConfig().token,
|
|
552
|
+
"x-project": this.app.getConfig().project
|
|
553
|
+
},
|
|
554
|
+
body: {
|
|
555
|
+
collection: this.collection,
|
|
556
|
+
id: this.id,
|
|
557
|
+
single: true
|
|
558
|
+
}
|
|
559
|
+
}).sendRequest();
|
|
560
|
+
if (!res?.success || !res.document)
|
|
561
|
+
return null;
|
|
562
|
+
const snapshot = DocumentSnapshot.fromMap(res.document);
|
|
563
|
+
if (this.persistence) {
|
|
564
|
+
await this.persistence.applyRemoteDoc(this.collection, snapshot.data);
|
|
565
|
+
}
|
|
566
|
+
return snapshot;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error(`[DocumentRef] get(${this.collection}/${this.id}) failed:`, error);
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async refreshFromRemote() {
|
|
573
|
+
const snapshot = await this.fetchRemoteSnapshot();
|
|
574
|
+
if (this.persistence && snapshot) {
|
|
575
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, this.id, "update").catch(console.error);
|
|
576
|
+
}
|
|
577
|
+
return snapshot;
|
|
578
|
+
}
|
|
565
579
|
};
|
|
566
580
|
|
|
581
|
+
// src/database/RealtimeListeners.ts
|
|
582
|
+
function buildTargetKey(collection, filter = {}) {
|
|
583
|
+
return `${collection}:${stableStringify(filter)}`;
|
|
584
|
+
}
|
|
585
|
+
function stableStringify(value) {
|
|
586
|
+
if (value === null || typeof value !== "object") {
|
|
587
|
+
return JSON.stringify(value);
|
|
588
|
+
}
|
|
589
|
+
if (Array.isArray(value)) {
|
|
590
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
591
|
+
}
|
|
592
|
+
const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
593
|
+
return `{${entries.join(",")}}`;
|
|
594
|
+
}
|
|
595
|
+
function diffSnapshots(previous, next) {
|
|
596
|
+
const previousById = new Map(previous.map((snapshot, index) => [snapshot.id, { snapshot, index }]));
|
|
597
|
+
const nextById = new Map(next.map((snapshot, index) => [snapshot.id, { snapshot, index }]));
|
|
598
|
+
const changes = [];
|
|
599
|
+
next.forEach((snapshot, index) => {
|
|
600
|
+
const before = previousById.get(snapshot.id);
|
|
601
|
+
if (!before) {
|
|
602
|
+
changes.push({
|
|
603
|
+
type: "added",
|
|
604
|
+
doc: snapshot,
|
|
605
|
+
previousDoc: null,
|
|
606
|
+
oldIndex: -1,
|
|
607
|
+
newIndex: index
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (before.index !== index || !areSnapshotsEqual(before.snapshot, snapshot)) {
|
|
612
|
+
changes.push({
|
|
613
|
+
type: "modified",
|
|
614
|
+
doc: snapshot,
|
|
615
|
+
previousDoc: before.snapshot,
|
|
616
|
+
oldIndex: before.index,
|
|
617
|
+
newIndex: index
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
previous.forEach((snapshot, index) => {
|
|
622
|
+
if (nextById.has(snapshot.id))
|
|
623
|
+
return;
|
|
624
|
+
changes.push({
|
|
625
|
+
type: "removed",
|
|
626
|
+
doc: snapshot,
|
|
627
|
+
previousDoc: snapshot,
|
|
628
|
+
oldIndex: index,
|
|
629
|
+
newIndex: -1
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
return changes;
|
|
633
|
+
}
|
|
634
|
+
function applySnapshotChange(current, incoming, source) {
|
|
635
|
+
if (!incoming) {
|
|
636
|
+
return current;
|
|
637
|
+
}
|
|
638
|
+
if (source === "delete") {
|
|
639
|
+
return current.filter((snapshot) => snapshot.id !== incoming.id);
|
|
640
|
+
}
|
|
641
|
+
const next = [...current];
|
|
642
|
+
const index = next.findIndex((snapshot) => snapshot.id === incoming.id);
|
|
643
|
+
if (index === -1) {
|
|
644
|
+
next.push(incoming);
|
|
645
|
+
return next;
|
|
646
|
+
}
|
|
647
|
+
next[index] = incoming;
|
|
648
|
+
return next;
|
|
649
|
+
}
|
|
650
|
+
function areSnapshotsEqual(left, right) {
|
|
651
|
+
return stableStringify(left.data) === stableStringify(right.data);
|
|
652
|
+
}
|
|
653
|
+
|
|
567
654
|
// src/database/Query.ts
|
|
568
655
|
var Query = class {
|
|
569
656
|
constructor(app, collection) {
|
|
@@ -576,6 +663,84 @@ var Query = class {
|
|
|
576
663
|
this.filter.push(expression);
|
|
577
664
|
return this;
|
|
578
665
|
}
|
|
666
|
+
async get() {
|
|
667
|
+
const persistence = this.app.offline().persistence;
|
|
668
|
+
if (this.filter.length > 0 && persistence) {
|
|
669
|
+
const local = await this.getLocalFilteredSnapshots();
|
|
670
|
+
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
671
|
+
return local;
|
|
672
|
+
}
|
|
673
|
+
return this.refreshFromRemote();
|
|
674
|
+
}
|
|
675
|
+
if (persistence) {
|
|
676
|
+
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
677
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
678
|
+
this.refreshFromRemote().catch(() => {
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
return local;
|
|
682
|
+
}
|
|
683
|
+
return this.refreshFromRemote();
|
|
684
|
+
}
|
|
685
|
+
async update(data) {
|
|
686
|
+
const genfilter = this.buildFilter();
|
|
687
|
+
return new HttpsRequest({
|
|
688
|
+
method: "POST" /* POST */,
|
|
689
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
690
|
+
headers: { authorization: this.app.getConfig().token, "x-project": this.app.getConfig().project },
|
|
691
|
+
body: {
|
|
692
|
+
collection: this.collection,
|
|
693
|
+
filter: genfilter,
|
|
694
|
+
data
|
|
695
|
+
}
|
|
696
|
+
}).sendRequest();
|
|
697
|
+
}
|
|
698
|
+
onSnapshot(callback) {
|
|
699
|
+
const genfilter = this.buildFilter();
|
|
700
|
+
const persistence = this.app.offline().persistence;
|
|
701
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
702
|
+
if (persistence && this.localStore && realtimeBridge) {
|
|
703
|
+
const targetKey = buildTargetKey(this.collection, genfilter);
|
|
704
|
+
const localUnsub = this.localStore.subscribeToCollection(targetKey, callback);
|
|
705
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
706
|
+
this.collection,
|
|
707
|
+
genfilter,
|
|
708
|
+
() => this.getLocalFilteredSnapshots()
|
|
709
|
+
);
|
|
710
|
+
this.getLocalFilteredSnapshots().then((snapshots) => {
|
|
711
|
+
realtimeBridge.primeCollectionTarget(this.collection, genfilter, snapshots);
|
|
712
|
+
callback(snapshots, "initial");
|
|
713
|
+
}).catch(console.error);
|
|
714
|
+
return () => {
|
|
715
|
+
localUnsub();
|
|
716
|
+
remoteUnsub();
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return this.subscribeOnlineSnapshot(callback);
|
|
720
|
+
}
|
|
721
|
+
onChildListener(callbacks) {
|
|
722
|
+
const genfilter = this.buildFilter();
|
|
723
|
+
const persistence = this.app.offline().persistence;
|
|
724
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
725
|
+
if (persistence && this.localStore && realtimeBridge) {
|
|
726
|
+
const targetKey = buildTargetKey(this.collection, genfilter);
|
|
727
|
+
const localUnsub = this.localStore.subscribeToChildEvents(targetKey, callbacks);
|
|
728
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
729
|
+
this.collection,
|
|
730
|
+
genfilter,
|
|
731
|
+
() => this.getLocalFilteredSnapshots()
|
|
732
|
+
);
|
|
733
|
+
this.getLocalFilteredSnapshots().then((snapshots) => {
|
|
734
|
+
realtimeBridge.primeCollectionTarget(this.collection, genfilter, snapshots);
|
|
735
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
736
|
+
}).catch(console.error);
|
|
737
|
+
return () => {
|
|
738
|
+
localUnsub();
|
|
739
|
+
remoteUnsub();
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return this.subscribeOnlineChildListener(callbacks);
|
|
743
|
+
}
|
|
579
744
|
buildFilter() {
|
|
580
745
|
const mongoFilter = {};
|
|
581
746
|
this.filter.forEach(({ key, op, value }) => {
|
|
@@ -646,9 +811,7 @@ var Query = class {
|
|
|
646
811
|
if (this.filter.length === 0)
|
|
647
812
|
return docs;
|
|
648
813
|
return docs.filter(
|
|
649
|
-
(snapshot) => this.filter.every(
|
|
650
|
-
(expression) => this.matchesFilter(snapshot.data, expression)
|
|
651
|
-
)
|
|
814
|
+
(snapshot) => this.filter.every((expression) => this.matchesFilter(snapshot.data, expression))
|
|
652
815
|
);
|
|
653
816
|
}
|
|
654
817
|
async getLocalFilteredSnapshots() {
|
|
@@ -658,25 +821,6 @@ var Query = class {
|
|
|
658
821
|
const docs = await persistence.getCollectionSnapshots(this.collection);
|
|
659
822
|
return this.filterDocuments(docs);
|
|
660
823
|
}
|
|
661
|
-
async get() {
|
|
662
|
-
const persistence = this.app.offline().persistence;
|
|
663
|
-
if (this.filter.length > 0 && persistence) {
|
|
664
|
-
const local = await this.getLocalFilteredSnapshots();
|
|
665
|
-
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
666
|
-
return local;
|
|
667
|
-
}
|
|
668
|
-
return this.refreshFromRemote();
|
|
669
|
-
}
|
|
670
|
-
if (persistence) {
|
|
671
|
-
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
672
|
-
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
673
|
-
this.refreshFromRemote().catch(() => {
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
return local;
|
|
677
|
-
}
|
|
678
|
-
return this.refreshFromRemote();
|
|
679
|
-
}
|
|
680
824
|
async refreshFromRemote() {
|
|
681
825
|
try {
|
|
682
826
|
const genfilter = this.buildFilter();
|
|
@@ -692,69 +836,90 @@ var Query = class {
|
|
|
692
836
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
693
837
|
return [];
|
|
694
838
|
}
|
|
695
|
-
|
|
839
|
+
const snapshots = res.documents.map((d) => {
|
|
696
840
|
delete d.token;
|
|
697
841
|
d.id = d.id ?? d._id;
|
|
698
842
|
delete d._id;
|
|
699
843
|
return DocumentSnapshot.fromMap(d);
|
|
700
844
|
});
|
|
845
|
+
const persistence = this.app.offline().persistence;
|
|
846
|
+
if (!persistence) {
|
|
847
|
+
return snapshots;
|
|
848
|
+
}
|
|
849
|
+
for (const snapshot of snapshots) {
|
|
850
|
+
await persistence.applyRemoteDoc(this.collection, snapshot.data);
|
|
851
|
+
}
|
|
852
|
+
return this.getLocalFilteredSnapshots();
|
|
701
853
|
} catch {
|
|
702
854
|
return [];
|
|
703
855
|
}
|
|
704
856
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
857
|
+
subscribeOnlineSnapshot(callback) {
|
|
858
|
+
let current = [];
|
|
859
|
+
this.refreshFromRemote().then((snapshots) => {
|
|
860
|
+
current = snapshots;
|
|
861
|
+
callback(current, "initial");
|
|
862
|
+
}).catch(console.error);
|
|
863
|
+
return this.app.rtdb().subscribeToCollectionRaw(
|
|
864
|
+
this.collection,
|
|
865
|
+
(payload, change) => {
|
|
866
|
+
const source = change;
|
|
867
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
868
|
+
const next = applySnapshotChange(current, incoming, source);
|
|
869
|
+
current = this.filterDocuments(next);
|
|
870
|
+
callback(current, source, incoming.id);
|
|
871
|
+
},
|
|
872
|
+
this.buildFilter()
|
|
873
|
+
);
|
|
719
874
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
875
|
+
subscribeOnlineChildListener(callbacks) {
|
|
876
|
+
let current = [];
|
|
877
|
+
this.refreshFromRemote().then((snapshots) => {
|
|
878
|
+
current = snapshots;
|
|
879
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
880
|
+
}).catch(console.error);
|
|
881
|
+
return this.app.rtdb().subscribeToCollectionRaw(
|
|
882
|
+
this.collection,
|
|
883
|
+
(payload, change) => {
|
|
884
|
+
const source = change;
|
|
885
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
886
|
+
const next = this.filterDocuments(applySnapshotChange(current, incoming, source));
|
|
887
|
+
const childChanges = diffSnapshots(current, next);
|
|
888
|
+
current = next;
|
|
889
|
+
childChanges.forEach((childChange) => {
|
|
890
|
+
const context = {
|
|
891
|
+
snapshots: next,
|
|
892
|
+
source,
|
|
893
|
+
changedDocId: childChange.doc.id,
|
|
894
|
+
fromCache: false,
|
|
895
|
+
oldIndex: childChange.oldIndex,
|
|
896
|
+
newIndex: childChange.newIndex,
|
|
897
|
+
previousDoc: childChange.previousDoc
|
|
898
|
+
};
|
|
899
|
+
if (childChange.type === "added") {
|
|
900
|
+
callbacks.onChildAdded?.(childChange.doc, context);
|
|
901
|
+
} else if (childChange.type === "modified") {
|
|
902
|
+
callbacks.onChildUpdated?.(childChange.doc, context);
|
|
903
|
+
} else if (childChange.type === "removed") {
|
|
904
|
+
callbacks.onChildRemoved?.(childChange.doc, context);
|
|
739
905
|
}
|
|
740
|
-
) ?? (() => {
|
|
741
906
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
907
|
+
},
|
|
908
|
+
this.buildFilter()
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
emitInitialChildEvents(snapshots, callbacks) {
|
|
912
|
+
diffSnapshots([], snapshots).forEach((change) => {
|
|
913
|
+
callbacks.onChildAdded?.(change.doc, {
|
|
914
|
+
snapshots,
|
|
915
|
+
source: "initial",
|
|
916
|
+
changedDocId: change.doc.id,
|
|
917
|
+
fromCache: true,
|
|
918
|
+
oldIndex: change.oldIndex,
|
|
919
|
+
newIndex: change.newIndex,
|
|
920
|
+
previousDoc: change.previousDoc
|
|
748
921
|
});
|
|
749
|
-
|
|
750
|
-
localUnsub();
|
|
751
|
-
remoteUnsub?.();
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
755
|
-
const res = normalizePayload(payload);
|
|
756
|
-
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
757
|
-
}, genfilter);
|
|
922
|
+
});
|
|
758
923
|
}
|
|
759
924
|
};
|
|
760
925
|
|
|
@@ -786,6 +951,89 @@ var CollectionRef = class {
|
|
|
786
951
|
}
|
|
787
952
|
return local;
|
|
788
953
|
}
|
|
954
|
+
return this.fetchRemoteSnapshots();
|
|
955
|
+
}
|
|
956
|
+
async add(data) {
|
|
957
|
+
if (this.persistence) {
|
|
958
|
+
const localId = this.persistence.createLocalId();
|
|
959
|
+
const docRecord = await this.persistence.upsertDoc({
|
|
960
|
+
collection: this.collection,
|
|
961
|
+
id: localId,
|
|
962
|
+
data: { ...data, id: localId },
|
|
963
|
+
exists: true,
|
|
964
|
+
deleted: false,
|
|
965
|
+
pending: 1,
|
|
966
|
+
localOnly: true,
|
|
967
|
+
status: "pending"
|
|
968
|
+
});
|
|
969
|
+
await this.persistence.enqueueMutation({
|
|
970
|
+
mutationId: this.persistence.createMutationId(),
|
|
971
|
+
collection: this.collection,
|
|
972
|
+
documentId: localId,
|
|
973
|
+
type: "insert",
|
|
974
|
+
payload: docRecord.data
|
|
975
|
+
});
|
|
976
|
+
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
977
|
+
this.app.offline().realtimeBridge?.publishLocalChange(this.collection, localId, "insert").catch(console.error);
|
|
978
|
+
this.syncEngine?.flush().catch(console.error);
|
|
979
|
+
return snap;
|
|
980
|
+
}
|
|
981
|
+
const res = await new HttpsRequest({
|
|
982
|
+
method: "POST" /* POST */,
|
|
983
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
984
|
+
headers: {
|
|
985
|
+
authorization: this.app.getConfig().token,
|
|
986
|
+
"x-project": this.app.getConfig().project
|
|
987
|
+
},
|
|
988
|
+
body: { collection: this.collection, data: { ...data, id: "" } }
|
|
989
|
+
}).sendRequest();
|
|
990
|
+
if (!res?.success || !res.document)
|
|
991
|
+
return null;
|
|
992
|
+
return DocumentSnapshot.fromMap(res.document);
|
|
993
|
+
}
|
|
994
|
+
onSnapshot(callback) {
|
|
995
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
996
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
997
|
+
const targetKey = buildTargetKey(this.collection, {});
|
|
998
|
+
const localUnsub = this.localStore.subscribeToCollection(targetKey, callback);
|
|
999
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
1000
|
+
this.collection,
|
|
1001
|
+
{},
|
|
1002
|
+
() => this.persistence.getCollectionSnapshots(this.collection)
|
|
1003
|
+
);
|
|
1004
|
+
this.persistence.getCollectionSnapshots(this.collection).then((snapshots) => {
|
|
1005
|
+
realtimeBridge.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1006
|
+
callback(snapshots, "insert");
|
|
1007
|
+
}).catch(console.error);
|
|
1008
|
+
return () => {
|
|
1009
|
+
localUnsub();
|
|
1010
|
+
remoteUnsub();
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return this.subscribeOnlineSnapshot(callback);
|
|
1014
|
+
}
|
|
1015
|
+
onChildListener(callbacks) {
|
|
1016
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
1017
|
+
if (this.persistence && this.localStore && realtimeBridge) {
|
|
1018
|
+
const targetKey = buildTargetKey(this.collection, {});
|
|
1019
|
+
const localUnsub = this.localStore.subscribeToChildEvents(targetKey, callbacks);
|
|
1020
|
+
const remoteUnsub = realtimeBridge.watchCollection(
|
|
1021
|
+
this.collection,
|
|
1022
|
+
{},
|
|
1023
|
+
() => this.persistence.getCollectionSnapshots(this.collection)
|
|
1024
|
+
);
|
|
1025
|
+
this.persistence.getCollectionSnapshots(this.collection).then((snapshots) => {
|
|
1026
|
+
realtimeBridge.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1027
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
1028
|
+
}).catch(console.error);
|
|
1029
|
+
return () => {
|
|
1030
|
+
localUnsub();
|
|
1031
|
+
remoteUnsub();
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return this.subscribeOnlineChildListener(callbacks);
|
|
1035
|
+
}
|
|
1036
|
+
async refreshFromRemote() {
|
|
789
1037
|
try {
|
|
790
1038
|
const res = await new HttpsRequest({
|
|
791
1039
|
method: "POST" /* POST */,
|
|
@@ -802,18 +1050,25 @@ var CollectionRef = class {
|
|
|
802
1050
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
803
1051
|
return [];
|
|
804
1052
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
delete
|
|
809
|
-
return
|
|
1053
|
+
const normalized = res.documents.map((raw) => {
|
|
1054
|
+
const doc = { ...raw };
|
|
1055
|
+
doc.id = doc.id ?? doc._id;
|
|
1056
|
+
delete doc._id;
|
|
1057
|
+
return doc;
|
|
810
1058
|
});
|
|
1059
|
+
if (this.persistence) {
|
|
1060
|
+
await this.persistence.reconcileCollectionFromRemote(this.collection, normalized);
|
|
1061
|
+
const snapshots = await this.persistence.getCollectionSnapshots(this.collection);
|
|
1062
|
+
this.app.offline().realtimeBridge?.primeCollectionTarget(this.collection, {}, snapshots);
|
|
1063
|
+
return snapshots;
|
|
1064
|
+
}
|
|
1065
|
+
return normalized.map((doc) => DocumentSnapshot.fromMap(doc));
|
|
811
1066
|
} catch (error) {
|
|
812
|
-
console.error("[EdmaxLabs]
|
|
1067
|
+
console.error("[EdmaxLabs] refreshFromRemote failed:", error);
|
|
813
1068
|
return [];
|
|
814
1069
|
}
|
|
815
1070
|
}
|
|
816
|
-
async
|
|
1071
|
+
async fetchRemoteSnapshots() {
|
|
817
1072
|
try {
|
|
818
1073
|
const res = await new HttpsRequest({
|
|
819
1074
|
method: "POST" /* POST */,
|
|
@@ -830,17 +1085,6 @@ var CollectionRef = class {
|
|
|
830
1085
|
if (!res?.success || !Array.isArray(res.documents)) {
|
|
831
1086
|
return [];
|
|
832
1087
|
}
|
|
833
|
-
for (const raw of res.documents) {
|
|
834
|
-
const doc = { ...raw };
|
|
835
|
-
doc.id = doc.id ?? doc._id;
|
|
836
|
-
delete doc._id;
|
|
837
|
-
if (this.persistence) {
|
|
838
|
-
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (this.persistence) {
|
|
842
|
-
return await this.persistence.getCollectionSnapshots(this.collection);
|
|
843
|
-
}
|
|
844
1088
|
return res.documents.map((d) => {
|
|
845
1089
|
delete d.token;
|
|
846
1090
|
d.id = d.id ?? d._id;
|
|
@@ -848,59 +1092,68 @@ var CollectionRef = class {
|
|
|
848
1092
|
return DocumentSnapshot.fromMap(d);
|
|
849
1093
|
});
|
|
850
1094
|
} catch (error) {
|
|
851
|
-
console.error("[EdmaxLabs]
|
|
1095
|
+
console.error("[EdmaxLabs] Collection get failed:", error);
|
|
852
1096
|
return [];
|
|
853
1097
|
}
|
|
854
1098
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
await this.persistence.enqueueMutation({
|
|
869
|
-
mutationId: this.persistence.createMutationId(),
|
|
870
|
-
collection: this.collection,
|
|
871
|
-
documentId: localId,
|
|
872
|
-
type: "insert",
|
|
873
|
-
payload: docRecord.data
|
|
874
|
-
});
|
|
875
|
-
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
876
|
-
this.localStore?.emitDocument(this.collection, localId, snap, "insert");
|
|
877
|
-
this.localStore?.notifyCollectionChanged(this.collection, localId, "insert");
|
|
878
|
-
this.syncEngine?.flush().catch(console.error);
|
|
879
|
-
return snap;
|
|
880
|
-
}
|
|
881
|
-
const res = await new HttpsRequest({
|
|
882
|
-
method: "POST" /* POST */,
|
|
883
|
-
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
884
|
-
headers: {
|
|
885
|
-
authorization: this.app.getConfig().token,
|
|
886
|
-
"x-project": this.app.getConfig().project
|
|
887
|
-
},
|
|
888
|
-
body: { collection: this.collection, data: { ...data, id: "" } }
|
|
889
|
-
// server will generate id
|
|
890
|
-
}).sendRequest();
|
|
891
|
-
if (!res?.success || !res.document)
|
|
892
|
-
return null;
|
|
893
|
-
return DocumentSnapshot.fromMap(res.document);
|
|
1099
|
+
subscribeOnlineSnapshot(callback) {
|
|
1100
|
+
let current = [];
|
|
1101
|
+
this.fetchRemoteSnapshots().then((snapshots) => {
|
|
1102
|
+
current = snapshots;
|
|
1103
|
+
callback(current, "initial");
|
|
1104
|
+
}).catch(console.error);
|
|
1105
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, change) => {
|
|
1106
|
+
const source = change;
|
|
1107
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
1108
|
+
current = applySnapshotChange(current, incoming, source);
|
|
1109
|
+
callback(current, source, incoming.id);
|
|
1110
|
+
});
|
|
894
1111
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1112
|
+
subscribeOnlineChildListener(callbacks) {
|
|
1113
|
+
let current = [];
|
|
1114
|
+
this.fetchRemoteSnapshots().then((snapshots) => {
|
|
1115
|
+
current = snapshots;
|
|
1116
|
+
this.emitInitialChildEvents(snapshots, callbacks);
|
|
1117
|
+
}).catch(console.error);
|
|
1118
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, change) => {
|
|
1119
|
+
const source = change;
|
|
1120
|
+
const incoming = source === "delete" ? DocumentSnapshot.fromMap({ id: payload?.id ?? payload?._id }) : DocumentSnapshot.fromMap(payload);
|
|
1121
|
+
const next = applySnapshotChange(current, incoming, source);
|
|
1122
|
+
const childChanges = diffSnapshots(current, next);
|
|
1123
|
+
current = next;
|
|
1124
|
+
childChanges.forEach((childChange) => {
|
|
1125
|
+
const context = {
|
|
1126
|
+
snapshots: next,
|
|
1127
|
+
source,
|
|
1128
|
+
changedDocId: childChange.doc.id,
|
|
1129
|
+
fromCache: false,
|
|
1130
|
+
oldIndex: childChange.oldIndex,
|
|
1131
|
+
newIndex: childChange.newIndex,
|
|
1132
|
+
previousDoc: childChange.previousDoc
|
|
1133
|
+
};
|
|
1134
|
+
if (childChange.type === "added") {
|
|
1135
|
+
callbacks.onChildAdded?.(childChange.doc, context);
|
|
1136
|
+
} else if (childChange.type === "modified") {
|
|
1137
|
+
callbacks.onChildUpdated?.(childChange.doc, context);
|
|
1138
|
+
} else if (childChange.type === "removed") {
|
|
1139
|
+
callbacks.onChildRemoved?.(childChange.doc, context);
|
|
1140
|
+
}
|
|
899
1141
|
});
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
emitInitialChildEvents(snapshots, callbacks) {
|
|
1145
|
+
const childChanges = diffSnapshots([], snapshots);
|
|
1146
|
+
childChanges.forEach((change) => {
|
|
1147
|
+
const context = {
|
|
1148
|
+
snapshots,
|
|
1149
|
+
source: "initial",
|
|
1150
|
+
changedDocId: change.doc.id,
|
|
1151
|
+
fromCache: true,
|
|
1152
|
+
oldIndex: change.oldIndex,
|
|
1153
|
+
newIndex: change.newIndex,
|
|
1154
|
+
previousDoc: change.previousDoc
|
|
1155
|
+
};
|
|
1156
|
+
callbacks.onChildAdded?.(change.doc, context);
|
|
904
1157
|
});
|
|
905
1158
|
}
|
|
906
1159
|
};
|
|
@@ -950,6 +1203,7 @@ var Batch = class {
|
|
|
950
1203
|
const persistence = this.app.offline().persistence;
|
|
951
1204
|
const localStore = this.app.offline().localStore;
|
|
952
1205
|
const syncEngine = this.app.offline().syncEngine;
|
|
1206
|
+
const realtimeBridge = this.app.offline().realtimeBridge;
|
|
953
1207
|
if (persistence && localStore) {
|
|
954
1208
|
const results = [];
|
|
955
1209
|
for (const op of this.ops) {
|
|
@@ -978,8 +1232,7 @@ var Batch = class {
|
|
|
978
1232
|
payload: op.data
|
|
979
1233
|
});
|
|
980
1234
|
const snap = DocumentSnapshot.fromMap(upserted.data);
|
|
981
|
-
|
|
982
|
-
localStore.notifyCollectionChanged(op.collection, op.id, "insert");
|
|
1235
|
+
realtimeBridge?.publishLocalChange(op.collection, op.id, "insert").catch(console.error);
|
|
983
1236
|
results.push(snap);
|
|
984
1237
|
} else if (op.op === "delete") {
|
|
985
1238
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
@@ -1091,6 +1344,20 @@ function generateUUID() {
|
|
|
1091
1344
|
});
|
|
1092
1345
|
}
|
|
1093
1346
|
|
|
1347
|
+
// src/utils/documentNomalizer.ts
|
|
1348
|
+
function normalizePayload(payload) {
|
|
1349
|
+
const raw = payload?.document ?? payload?.data ?? payload;
|
|
1350
|
+
if (!raw)
|
|
1351
|
+
return null;
|
|
1352
|
+
const doc = { ...raw };
|
|
1353
|
+
doc.id = doc.id ?? raw?.id ?? raw?._id ?? payload?.id ?? payload?._id;
|
|
1354
|
+
delete doc._id;
|
|
1355
|
+
return {
|
|
1356
|
+
change: payload?.change ?? raw?.change ?? null,
|
|
1357
|
+
data: doc
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1094
1361
|
// src/database/Realtime.ts
|
|
1095
1362
|
var Realtime = class {
|
|
1096
1363
|
constructor(app) {
|
|
@@ -1159,8 +1426,12 @@ var Realtime = class {
|
|
|
1159
1426
|
const channel = `${lid}-${event}`;
|
|
1160
1427
|
const fn = (payload) => {
|
|
1161
1428
|
const normalized = normalizePayload(payload);
|
|
1162
|
-
if (!normalized)
|
|
1429
|
+
if (!normalized) {
|
|
1430
|
+
if (event === "delete") {
|
|
1431
|
+
callback(payload, "delete");
|
|
1432
|
+
}
|
|
1163
1433
|
return;
|
|
1434
|
+
}
|
|
1164
1435
|
callback(normalized.data, event);
|
|
1165
1436
|
};
|
|
1166
1437
|
this.socket.on(channel, fn);
|
|
@@ -1284,11 +1555,11 @@ var LocalStore = class {
|
|
|
1284
1555
|
constructor() {
|
|
1285
1556
|
this.documentListeners = /* @__PURE__ */ new Map();
|
|
1286
1557
|
this.collectionListeners = /* @__PURE__ */ new Map();
|
|
1558
|
+
this.childListeners = /* @__PURE__ */ new Map();
|
|
1287
1559
|
}
|
|
1288
1560
|
docKey(collection, id) {
|
|
1289
1561
|
return `${collection}:${id}`;
|
|
1290
1562
|
}
|
|
1291
|
-
// ===================== DOCUMENT LISTENERS =====================
|
|
1292
1563
|
subscribeToDocument(collection, id, callback) {
|
|
1293
1564
|
const key = this.docKey(collection, id);
|
|
1294
1565
|
if (!this.documentListeners.has(key)) {
|
|
@@ -1303,24 +1574,33 @@ var LocalStore = class {
|
|
|
1303
1574
|
}
|
|
1304
1575
|
};
|
|
1305
1576
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
this.collectionListeners.set(collection, /* @__PURE__ */ new Set());
|
|
1577
|
+
subscribeToCollection(targetKey, callback) {
|
|
1578
|
+
if (!this.collectionListeners.has(targetKey)) {
|
|
1579
|
+
this.collectionListeners.set(targetKey, /* @__PURE__ */ new Set());
|
|
1310
1580
|
}
|
|
1311
|
-
const listeners = this.collectionListeners.get(
|
|
1581
|
+
const listeners = this.collectionListeners.get(targetKey);
|
|
1312
1582
|
listeners.add(callback);
|
|
1313
1583
|
return () => {
|
|
1314
1584
|
listeners.delete(callback);
|
|
1315
1585
|
if (listeners.size === 0) {
|
|
1316
|
-
this.collectionListeners.delete(
|
|
1586
|
+
this.collectionListeners.delete(targetKey);
|
|
1587
|
+
this.childListeners.delete(targetKey);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
subscribeToChildEvents(targetKey, callbacks) {
|
|
1592
|
+
if (!this.childListeners.has(targetKey)) {
|
|
1593
|
+
this.childListeners.set(targetKey, /* @__PURE__ */ new Set());
|
|
1594
|
+
}
|
|
1595
|
+
const listeners = this.childListeners.get(targetKey);
|
|
1596
|
+
listeners.add(callbacks);
|
|
1597
|
+
return () => {
|
|
1598
|
+
listeners.delete(callbacks);
|
|
1599
|
+
if (listeners.size === 0) {
|
|
1600
|
+
this.childListeners.delete(targetKey);
|
|
1317
1601
|
}
|
|
1318
1602
|
};
|
|
1319
1603
|
}
|
|
1320
|
-
// ===================== EMITTERS =====================
|
|
1321
|
-
/**
|
|
1322
|
-
* Notify all listeners for a specific document
|
|
1323
|
-
*/
|
|
1324
1604
|
emitDocument(collection, id, snapshot, change) {
|
|
1325
1605
|
const key = this.docKey(collection, id);
|
|
1326
1606
|
this.documentListeners.get(key)?.forEach((cb) => {
|
|
@@ -1331,64 +1611,66 @@ var LocalStore = class {
|
|
|
1331
1611
|
}
|
|
1332
1612
|
});
|
|
1333
1613
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
* This is the most important fix — collection listeners now receive the full list.
|
|
1337
|
-
*/
|
|
1338
|
-
emitCollection(collection, snapshots, change, changedDocId) {
|
|
1339
|
-
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1614
|
+
emitCollection(targetKey, emission) {
|
|
1615
|
+
this.collectionListeners.get(targetKey)?.forEach((cb) => {
|
|
1340
1616
|
try {
|
|
1341
|
-
cb(snapshots,
|
|
1617
|
+
cb(emission.snapshots, emission.source, emission.changedDocId);
|
|
1342
1618
|
} catch (err) {
|
|
1343
|
-
console.error(`[EdmaxLabs] Error in collection listener for ${
|
|
1619
|
+
console.error(`[EdmaxLabs] Error in collection listener for ${targetKey}:`, err);
|
|
1344
1620
|
}
|
|
1345
1621
|
});
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1354
|
-
try {
|
|
1355
|
-
cb([], change, changedDocId);
|
|
1356
|
-
} catch (err) {
|
|
1357
|
-
console.error(`[EdmaxLabs] Error in collection listener for ${collection}:`, err);
|
|
1358
|
-
}
|
|
1622
|
+
const childChanges = emission.childChanges ?? [];
|
|
1623
|
+
if (childChanges.length === 0)
|
|
1624
|
+
return;
|
|
1625
|
+
this.childListeners.get(targetKey)?.forEach((callbacks) => {
|
|
1626
|
+
childChanges.forEach((change) => {
|
|
1627
|
+
this.dispatchChildEvent(callbacks, change, emission);
|
|
1628
|
+
});
|
|
1359
1629
|
});
|
|
1360
1630
|
}
|
|
1361
|
-
// ===================== UTILITY =====================
|
|
1362
|
-
/**
|
|
1363
|
-
* Clear all listeners (useful for testing or when persistence is disabled)
|
|
1364
|
-
*/
|
|
1365
1631
|
clearAllListeners() {
|
|
1366
1632
|
this.documentListeners.clear();
|
|
1367
1633
|
this.collectionListeners.clear();
|
|
1634
|
+
this.childListeners.clear();
|
|
1368
1635
|
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Get current listener count (for debugging / dev tools)
|
|
1371
|
-
*/
|
|
1372
1636
|
get listenerCount() {
|
|
1373
1637
|
let docCount = 0;
|
|
1374
1638
|
this.documentListeners.forEach((set) => docCount += set.size);
|
|
1375
1639
|
let collCount = 0;
|
|
1376
1640
|
this.collectionListeners.forEach((set) => collCount += set.size);
|
|
1641
|
+
this.childListeners.forEach((set) => collCount += set.size);
|
|
1377
1642
|
return { documents: docCount, collections: collCount };
|
|
1378
1643
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
removeCollectionListeners(collection) {
|
|
1383
|
-
this.collectionListeners.delete(collection);
|
|
1644
|
+
removeCollectionListeners(targetKey) {
|
|
1645
|
+
this.collectionListeners.delete(targetKey);
|
|
1646
|
+
this.childListeners.delete(targetKey);
|
|
1384
1647
|
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Remove all listeners for a specific document (useful for cleanup)
|
|
1387
|
-
*/
|
|
1388
1648
|
removeDocumentListeners(collection, id) {
|
|
1389
1649
|
const key = this.docKey(collection, id);
|
|
1390
1650
|
this.documentListeners.delete(key);
|
|
1391
1651
|
}
|
|
1652
|
+
dispatchChildEvent(callbacks, change, emission) {
|
|
1653
|
+
const context = {
|
|
1654
|
+
snapshots: emission.snapshots,
|
|
1655
|
+
source: emission.source,
|
|
1656
|
+
changedDocId: emission.changedDocId,
|
|
1657
|
+
fromCache: emission.fromCache ?? true,
|
|
1658
|
+
oldIndex: change.oldIndex,
|
|
1659
|
+
newIndex: change.newIndex,
|
|
1660
|
+
previousDoc: change.previousDoc
|
|
1661
|
+
};
|
|
1662
|
+
try {
|
|
1663
|
+
if (change.type === "added") {
|
|
1664
|
+
callbacks.onChildAdded?.(change.doc, context);
|
|
1665
|
+
} else if (change.type === "modified") {
|
|
1666
|
+
callbacks.onChildUpdated?.(change.doc, context);
|
|
1667
|
+
} else if (change.type === "removed") {
|
|
1668
|
+
callbacks.onChildRemoved?.(change.doc, context);
|
|
1669
|
+
}
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
console.error("[EdmaxLabs] Error in child listener:", err);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1392
1674
|
};
|
|
1393
1675
|
|
|
1394
1676
|
// ../../../../node_modules/idb/build/wrap-idb-value.js
|
|
@@ -1759,11 +2041,25 @@ var Persistence = class {
|
|
|
1759
2041
|
}
|
|
1760
2042
|
async getPendingMutations() {
|
|
1761
2043
|
const app = await this.getDb();
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2044
|
+
const pending = await app.getAllFromIndex("mutations", "by-status", "pending");
|
|
2045
|
+
return pending.sort((a, b) => a.createdAt - b.createdAt);
|
|
2046
|
+
}
|
|
2047
|
+
async getFailedMutations() {
|
|
2048
|
+
const app = await this.getDb();
|
|
2049
|
+
const failed = await app.getAllFromIndex("mutations", "by-status", "failed");
|
|
2050
|
+
return failed.sort((a, b) => a.createdAt - b.createdAt);
|
|
2051
|
+
}
|
|
2052
|
+
async recoverSyncingMutations() {
|
|
2053
|
+
const app = await this.getDb();
|
|
2054
|
+
const syncing = await app.getAllFromIndex("mutations", "by-status", "syncing");
|
|
2055
|
+
for (const mutation of syncing) {
|
|
2056
|
+
await app.put("mutations", {
|
|
2057
|
+
...mutation,
|
|
2058
|
+
status: "pending",
|
|
2059
|
+
updatedAt: this.now()
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
return syncing.length;
|
|
1767
2063
|
}
|
|
1768
2064
|
async getMutation(mutationId) {
|
|
1769
2065
|
const app = await this.getDb();
|
|
@@ -1868,6 +2164,27 @@ var Persistence = class {
|
|
|
1868
2164
|
}
|
|
1869
2165
|
return this.markDeleted(collection, id, 0);
|
|
1870
2166
|
}
|
|
2167
|
+
async reconcileCollectionFromRemote(collection, documents) {
|
|
2168
|
+
const app = await this.getDb();
|
|
2169
|
+
const existing = await app.getAllFromIndex("docs", "by-collection", collection);
|
|
2170
|
+
const incomingIds = /* @__PURE__ */ new Set();
|
|
2171
|
+
for (const data of documents) {
|
|
2172
|
+
const id = data.id ?? data._id;
|
|
2173
|
+
if (!id)
|
|
2174
|
+
continue;
|
|
2175
|
+
incomingIds.add(String(id));
|
|
2176
|
+
await this.applyRemoteDoc(collection, { ...data, id: String(id) });
|
|
2177
|
+
}
|
|
2178
|
+
for (const doc of existing) {
|
|
2179
|
+
if (doc.pending && doc.pending > 0)
|
|
2180
|
+
continue;
|
|
2181
|
+
if (!doc.exists || doc.deleted)
|
|
2182
|
+
continue;
|
|
2183
|
+
if (incomingIds.has(doc.id))
|
|
2184
|
+
continue;
|
|
2185
|
+
await this.markDeleted(collection, doc.id, 0);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
1871
2188
|
// ==================== UTILITIES ====================
|
|
1872
2189
|
createLocalId() {
|
|
1873
2190
|
return `local_${crypto.randomUUID()}`;
|
|
@@ -1897,112 +2214,169 @@ var RealtimeBridge = class {
|
|
|
1897
2214
|
this.app = app;
|
|
1898
2215
|
this.persistence = persistence;
|
|
1899
2216
|
this.store = store;
|
|
1900
|
-
this.
|
|
1901
|
-
this.
|
|
2217
|
+
this.collectionTargets = /* @__PURE__ */ new Map();
|
|
2218
|
+
this.documentTargets = /* @__PURE__ */ new Map();
|
|
1902
2219
|
}
|
|
1903
2220
|
docKey(collection, id) {
|
|
1904
2221
|
return `${collection}:${id}`;
|
|
1905
2222
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
const
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
2223
|
+
watchCollection(collection, filter = {}, read) {
|
|
2224
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2225
|
+
const existing = this.collectionTargets.get(targetKey);
|
|
2226
|
+
if (existing) {
|
|
2227
|
+
existing.refCount += 1;
|
|
2228
|
+
return () => this.releaseCollection(targetKey);
|
|
1911
2229
|
}
|
|
1912
|
-
this.emitCurrentCollection(collection);
|
|
1913
2230
|
const unsub = this.app.rtdb().subscribeToCollectionRaw(
|
|
1914
2231
|
collection,
|
|
1915
2232
|
async (payload, change) => {
|
|
1916
2233
|
if (change === "delete") {
|
|
1917
2234
|
const id = payload?.id || payload?._id;
|
|
1918
|
-
if (id)
|
|
2235
|
+
if (id) {
|
|
1919
2236
|
await this.handleRemoteDelete(collection, id);
|
|
1920
|
-
|
|
1921
|
-
|
|
2237
|
+
}
|
|
2238
|
+
return;
|
|
1922
2239
|
}
|
|
2240
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1923
2241
|
},
|
|
1924
2242
|
filter
|
|
1925
2243
|
);
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2244
|
+
this.collectionTargets.set(targetKey, {
|
|
2245
|
+
targetKey,
|
|
2246
|
+
collection,
|
|
2247
|
+
filter,
|
|
2248
|
+
read,
|
|
2249
|
+
refCount: 1,
|
|
2250
|
+
unsub,
|
|
2251
|
+
lastSnapshots: []
|
|
2252
|
+
});
|
|
2253
|
+
return () => this.releaseCollection(targetKey);
|
|
1932
2254
|
}
|
|
1933
2255
|
watchDocument(collection, id) {
|
|
1934
2256
|
const key = this.docKey(collection, id);
|
|
1935
|
-
|
|
1936
|
-
|
|
2257
|
+
const existing = this.documentTargets.get(key);
|
|
2258
|
+
if (existing) {
|
|
2259
|
+
existing.refCount += 1;
|
|
2260
|
+
return () => this.releaseDocument(key);
|
|
1937
2261
|
}
|
|
1938
|
-
this.emitCurrentDocument(collection, id);
|
|
1939
2262
|
const unsub = this.app.rtdb().subscribeToDocumentRaw(
|
|
1940
2263
|
collection,
|
|
1941
2264
|
id,
|
|
1942
2265
|
async (payload, change) => {
|
|
1943
2266
|
if (change === "delete") {
|
|
1944
2267
|
await this.handleRemoteDelete(collection, id);
|
|
1945
|
-
|
|
1946
|
-
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
2268
|
+
return;
|
|
1947
2269
|
}
|
|
2270
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1948
2271
|
}
|
|
1949
2272
|
);
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2273
|
+
this.documentTargets.set(key, {
|
|
2274
|
+
key,
|
|
2275
|
+
collection,
|
|
2276
|
+
id,
|
|
2277
|
+
refCount: 1,
|
|
2278
|
+
unsub
|
|
2279
|
+
});
|
|
2280
|
+
return () => this.releaseDocument(key);
|
|
2281
|
+
}
|
|
2282
|
+
async emitCurrentCollection(collection, filter = {}) {
|
|
2283
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2284
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2285
|
+
if (!target)
|
|
2286
|
+
return [];
|
|
2287
|
+
return this.refreshCollectionTarget(target, "initial");
|
|
2288
|
+
}
|
|
2289
|
+
primeCollectionTarget(collection, filter, snapshots) {
|
|
2290
|
+
const targetKey = buildTargetKey(collection, filter);
|
|
2291
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2292
|
+
if (!target)
|
|
2293
|
+
return;
|
|
2294
|
+
target.lastSnapshots = snapshots;
|
|
1956
2295
|
}
|
|
1957
|
-
// ===================== INTERNAL HANDLERS =====================
|
|
1958
2296
|
async emitCurrentDocument(collection, id) {
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
2297
|
+
const snapshot = await this.getCurrentDocumentSnapshot(collection, id);
|
|
2298
|
+
this.store.emitDocument(collection, id, snapshot, "initial");
|
|
2299
|
+
return snapshot;
|
|
1962
2300
|
}
|
|
1963
|
-
async
|
|
1964
|
-
const
|
|
1965
|
-
this.store.
|
|
2301
|
+
async publishLocalChange(collection, id, source) {
|
|
2302
|
+
const snapshot = await this.getCurrentDocumentSnapshot(collection, id);
|
|
2303
|
+
this.store.emitDocument(collection, id, snapshot, source);
|
|
2304
|
+
await this.refreshCollectionTargets(collection, source, id);
|
|
2305
|
+
}
|
|
2306
|
+
async onReconnect() {
|
|
2307
|
+
for (const target of this.collectionTargets.values()) {
|
|
2308
|
+
await this.refreshCollectionTarget(target, "initial");
|
|
2309
|
+
}
|
|
2310
|
+
for (const target of this.documentTargets.values()) {
|
|
2311
|
+
await this.emitCurrentDocument(target.collection, target.id);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
dispose() {
|
|
2315
|
+
this.collectionTargets.forEach((target) => target.unsub());
|
|
2316
|
+
this.documentTargets.forEach((target) => target.unsub());
|
|
2317
|
+
this.collectionTargets.clear();
|
|
2318
|
+
this.documentTargets.clear();
|
|
1966
2319
|
}
|
|
1967
|
-
async handleRemoteCreateOrUpdate(collection, raw,
|
|
2320
|
+
async handleRemoteCreateOrUpdate(collection, raw, source) {
|
|
1968
2321
|
const id = raw.id ?? raw._id;
|
|
1969
2322
|
if (!id)
|
|
1970
2323
|
return;
|
|
1971
2324
|
const saved = await this.persistence.applyRemoteDoc(collection, raw);
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
this.
|
|
2325
|
+
if (saved) {
|
|
2326
|
+
const snap = DocumentSnapshot.fromMap(saved.data);
|
|
2327
|
+
this.store.emitDocument(collection, id, snap, source);
|
|
2328
|
+
}
|
|
2329
|
+
await this.refreshCollectionTargets(collection, source, id, false);
|
|
1977
2330
|
}
|
|
1978
2331
|
async handleRemoteDelete(collection, id) {
|
|
1979
2332
|
await this.persistence.applyRemoteDelete(collection, id);
|
|
1980
2333
|
this.store.emitDocument(collection, id, null, "delete");
|
|
1981
|
-
this.
|
|
2334
|
+
await this.refreshCollectionTargets(collection, "delete", id, false);
|
|
1982
2335
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
* Replays pending mutations + refreshes all active subscriptions from cache
|
|
1987
|
-
*/
|
|
1988
|
-
async onReconnect() {
|
|
1989
|
-
for (const [key] of this.collectionUnsubs) {
|
|
1990
|
-
const collection = key.split(":")[0];
|
|
1991
|
-
await this.emitCurrentCollection(collection);
|
|
1992
|
-
}
|
|
1993
|
-
for (const [key] of this.documentUnsubs) {
|
|
1994
|
-
const [collection, id] = key.split(":");
|
|
1995
|
-
await this.emitCurrentDocument(collection, id);
|
|
1996
|
-
}
|
|
2336
|
+
async getCurrentDocumentSnapshot(collection, id) {
|
|
2337
|
+
const localDoc = await this.persistence.getDoc(collection, id);
|
|
2338
|
+
return localDoc && localDoc.exists && !localDoc.deleted ? DocumentSnapshot.fromMap(localDoc.data) : null;
|
|
1997
2339
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2340
|
+
async refreshCollectionTargets(collection, source, changedDocId, fromCache = true) {
|
|
2341
|
+
const targets = Array.from(this.collectionTargets.values()).filter(
|
|
2342
|
+
(target) => target.collection === collection
|
|
2343
|
+
);
|
|
2344
|
+
for (const target of targets) {
|
|
2345
|
+
await this.refreshCollectionTarget(target, source, changedDocId, fromCache);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async refreshCollectionTarget(target, source, changedDocId, fromCache = true) {
|
|
2349
|
+
const snapshots = await target.read();
|
|
2350
|
+
const childChanges = diffSnapshots(target.lastSnapshots, snapshots);
|
|
2351
|
+
target.lastSnapshots = snapshots;
|
|
2352
|
+
this.store.emitCollection(target.targetKey, {
|
|
2353
|
+
snapshots,
|
|
2354
|
+
source,
|
|
2355
|
+
changedDocId,
|
|
2356
|
+
fromCache,
|
|
2357
|
+
childChanges
|
|
2358
|
+
});
|
|
2359
|
+
return snapshots;
|
|
2360
|
+
}
|
|
2361
|
+
releaseCollection(targetKey) {
|
|
2362
|
+
const target = this.collectionTargets.get(targetKey);
|
|
2363
|
+
if (!target)
|
|
2364
|
+
return;
|
|
2365
|
+
target.refCount -= 1;
|
|
2366
|
+
if (target.refCount > 0)
|
|
2367
|
+
return;
|
|
2368
|
+
target.unsub();
|
|
2369
|
+
this.collectionTargets.delete(targetKey);
|
|
2370
|
+
}
|
|
2371
|
+
releaseDocument(key) {
|
|
2372
|
+
const target = this.documentTargets.get(key);
|
|
2373
|
+
if (!target)
|
|
2374
|
+
return;
|
|
2375
|
+
target.refCount -= 1;
|
|
2376
|
+
if (target.refCount > 0)
|
|
2377
|
+
return;
|
|
2378
|
+
target.unsub();
|
|
2379
|
+
this.documentTargets.delete(key);
|
|
2006
2380
|
}
|
|
2007
2381
|
};
|
|
2008
2382
|
|
|
@@ -2014,16 +2388,19 @@ var SyncEngine = class {
|
|
|
2014
2388
|
this.retryTimeout = null;
|
|
2015
2389
|
this.MAX_RETRIES = 5;
|
|
2016
2390
|
this.BASE_RETRY_DELAY = 2e3;
|
|
2391
|
+
this.handleOnline = () => this.onNetworkOnline();
|
|
2392
|
+
this.handleVisibilityChange = () => {
|
|
2393
|
+
if (document.visibilityState === "visible")
|
|
2394
|
+
this.onNetworkOnline();
|
|
2395
|
+
};
|
|
2017
2396
|
this.app = app;
|
|
2018
2397
|
this.persistence = persistence;
|
|
2019
2398
|
this.store = store;
|
|
2020
2399
|
this.realtimeBridge = realtimeBridge || null;
|
|
2400
|
+
this.persistence.recoverSyncingMutations().catch(console.error);
|
|
2021
2401
|
if (typeof window !== "undefined") {
|
|
2022
|
-
window.addEventListener("online",
|
|
2023
|
-
document.addEventListener("visibilitychange",
|
|
2024
|
-
if (document.visibilityState === "visible")
|
|
2025
|
-
this.onNetworkOnline();
|
|
2026
|
-
});
|
|
2402
|
+
window.addEventListener("online", this.handleOnline);
|
|
2403
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2027
2404
|
}
|
|
2028
2405
|
}
|
|
2029
2406
|
onNetworkOnline() {
|
|
@@ -2040,6 +2417,7 @@ var SyncEngine = class {
|
|
|
2040
2417
|
return;
|
|
2041
2418
|
this.syncing = true;
|
|
2042
2419
|
try {
|
|
2420
|
+
await this.persistence.recoverSyncingMutations();
|
|
2043
2421
|
const pending = await this.persistence.getPendingMutations();
|
|
2044
2422
|
for (const mutation of pending) {
|
|
2045
2423
|
if (mutation.retryCount >= this.MAX_RETRIES) {
|
|
@@ -2116,8 +2494,7 @@ var SyncEngine = class {
|
|
|
2116
2494
|
if (replaced) {
|
|
2117
2495
|
const snap = DocumentSnapshot.fromMap(replaced.data);
|
|
2118
2496
|
this.store.emitDocument(mutation.collection, oldId, snap, "insert");
|
|
2119
|
-
this.
|
|
2120
|
-
this.store.notifyCollectionChanged(mutation.collection, newId, "insert");
|
|
2497
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, newId, "insert");
|
|
2121
2498
|
}
|
|
2122
2499
|
return true;
|
|
2123
2500
|
}
|
|
@@ -2150,7 +2527,7 @@ var SyncEngine = class {
|
|
|
2150
2527
|
});
|
|
2151
2528
|
const snap = DocumentSnapshot.fromMap(local.data);
|
|
2152
2529
|
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "update");
|
|
2153
|
-
this.
|
|
2530
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, mutation.documentId, "update");
|
|
2154
2531
|
return true;
|
|
2155
2532
|
}
|
|
2156
2533
|
async syncDelete(mutation) {
|
|
@@ -2167,7 +2544,7 @@ var SyncEngine = class {
|
|
|
2167
2544
|
return false;
|
|
2168
2545
|
await this.persistence.markDeleted(mutation.collection, mutation.documentId, 0);
|
|
2169
2546
|
this.store.emitDocument(mutation.collection, mutation.documentId, null, "delete");
|
|
2170
|
-
this.
|
|
2547
|
+
await this.realtimeBridge?.publishLocalChange(mutation.collection, mutation.documentId, "delete");
|
|
2171
2548
|
return true;
|
|
2172
2549
|
}
|
|
2173
2550
|
scheduleRetry() {
|
|
@@ -2187,8 +2564,7 @@ var SyncEngine = class {
|
|
|
2187
2564
|
* Returns mutations that exceeded MAX_RETRIES
|
|
2188
2565
|
*/
|
|
2189
2566
|
async getFailedMutations() {
|
|
2190
|
-
|
|
2191
|
-
return all.filter((m) => m.status === "failed");
|
|
2567
|
+
return this.persistence.getFailedMutations();
|
|
2192
2568
|
}
|
|
2193
2569
|
/**
|
|
2194
2570
|
* Retry a specific failed mutation by resetting its retry count
|
|
@@ -2230,6 +2606,10 @@ var SyncEngine = class {
|
|
|
2230
2606
|
if (this.retryTimeout) {
|
|
2231
2607
|
clearTimeout(this.retryTimeout);
|
|
2232
2608
|
}
|
|
2609
|
+
if (typeof window !== "undefined") {
|
|
2610
|
+
window.removeEventListener("online", this.handleOnline);
|
|
2611
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2612
|
+
}
|
|
2233
2613
|
}
|
|
2234
2614
|
};
|
|
2235
2615
|
|