edmaxlabs-core 1.3.5 → 1.3.7
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 +251 -26
- package/dist/index.cjs +581 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -40
- package/dist/index.d.ts +84 -40
- package/dist/index.mjs +581 -252
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -15
package/dist/index.mjs
CHANGED
|
@@ -52,29 +52,45 @@ var HttpsRequest = class {
|
|
|
52
52
|
this.isMultipart = isMultipart;
|
|
53
53
|
}
|
|
54
54
|
async sendRequest() {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
try {
|
|
56
|
+
if (this.isMultipart && this.file) {
|
|
57
|
+
const formData = new FormData();
|
|
58
|
+
Object.entries(this.body).forEach(([key, value]) => {
|
|
59
|
+
formData.append(key, value);
|
|
60
|
+
});
|
|
61
|
+
formData.append("file", this.file);
|
|
62
|
+
const response2 = await fetch(this.endpoint, {
|
|
63
|
+
method: this.method,
|
|
64
|
+
headers: this.headers,
|
|
65
|
+
body: formData
|
|
66
|
+
});
|
|
67
|
+
if (!response2.ok) {
|
|
68
|
+
const errorText = await response2.text().catch(() => "Network error");
|
|
69
|
+
throw new Error(`HTTP ${response2.status}: ${errorText}`);
|
|
70
|
+
}
|
|
71
|
+
return await response2.json().catch(() => ({}));
|
|
72
|
+
}
|
|
69
73
|
const response = await fetch(this.endpoint, {
|
|
70
74
|
method: this.method,
|
|
71
75
|
headers: {
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
...this.headers
|
|
74
78
|
},
|
|
75
|
-
body: JSON.stringify(this.body)
|
|
79
|
+
body: this.method !== "GET" /* GET */ ? JSON.stringify(this.body) : void 0
|
|
76
80
|
});
|
|
77
|
-
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorText = await response.text().catch(() => "Network error");
|
|
83
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
84
|
+
}
|
|
85
|
+
return await response.json().catch(() => ({}));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.name === "TypeError" && error.message.includes("fetch")) {
|
|
88
|
+
throw new Error("Network connection failed. Please check your internet connection.");
|
|
89
|
+
}
|
|
90
|
+
if (error.name === "AbortError") {
|
|
91
|
+
throw new Error("Request was cancelled.");
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
96
|
};
|
|
@@ -179,7 +195,8 @@ var _Authentication = class _Authentication {
|
|
|
179
195
|
method: "POST" /* POST */,
|
|
180
196
|
endpoint: this.client.getBaseUrl() + "/auth/rules/verify",
|
|
181
197
|
headers: {
|
|
182
|
-
authorization: this.client.getConfig().token
|
|
198
|
+
authorization: this.client.getConfig().token,
|
|
199
|
+
project: this.client.getConfig().project
|
|
183
200
|
},
|
|
184
201
|
body: {
|
|
185
202
|
path,
|
|
@@ -521,8 +538,8 @@ var Array2 = class {
|
|
|
521
538
|
try {
|
|
522
539
|
const res = await new HttpsRequest({
|
|
523
540
|
method: "POST" /* POST */,
|
|
524
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
525
|
-
headers: { authorization: this.app.getConfig().token },
|
|
541
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/show`,
|
|
542
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
526
543
|
body: {
|
|
527
544
|
collection: this.collection,
|
|
528
545
|
document: this.docID,
|
|
@@ -574,15 +591,15 @@ var Array2 = class {
|
|
|
574
591
|
this.collection,
|
|
575
592
|
this.docID,
|
|
576
593
|
DocumentSnapshot.fromMap({ ...doc.data, [this.key]: updatedArray }),
|
|
577
|
-
"
|
|
594
|
+
"update"
|
|
578
595
|
);
|
|
579
596
|
}
|
|
580
597
|
return ArraySnapshot.fromMap(payload);
|
|
581
598
|
}
|
|
582
599
|
const res = await new HttpsRequest({
|
|
583
600
|
method: "POST" /* POST */,
|
|
584
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
585
|
-
headers: { authorization: this.app.getConfig().token },
|
|
601
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/push`,
|
|
602
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
586
603
|
body: {
|
|
587
604
|
collection: this.collection,
|
|
588
605
|
document: this.docID,
|
|
@@ -608,8 +625,8 @@ var Array2 = class {
|
|
|
608
625
|
}
|
|
609
626
|
const res = await new HttpsRequest({
|
|
610
627
|
method: "POST" /* POST */,
|
|
611
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
612
|
-
headers: { authorization: this.app.getConfig().token },
|
|
628
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/update`,
|
|
629
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
613
630
|
body: {
|
|
614
631
|
collection: this.collection,
|
|
615
632
|
document: this.docID,
|
|
@@ -633,7 +650,47 @@ var Array2 = class {
|
|
|
633
650
|
}
|
|
634
651
|
};
|
|
635
652
|
|
|
653
|
+
// src/utils/documentNomalizer.ts
|
|
654
|
+
function normalizePayload(payload) {
|
|
655
|
+
const raw = payload?.document ?? payload?.data ?? payload;
|
|
656
|
+
if (!raw)
|
|
657
|
+
return null;
|
|
658
|
+
const doc = { ...raw };
|
|
659
|
+
doc.id = payload.id ?? payload._id;
|
|
660
|
+
delete doc._id;
|
|
661
|
+
return {
|
|
662
|
+
change: payload?.change ?? raw?.change ?? null,
|
|
663
|
+
data: doc
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
636
667
|
// src/database/DocumentRef.ts
|
|
668
|
+
function validateDocumentData(data, operation) {
|
|
669
|
+
if (data === null || data === void 0) {
|
|
670
|
+
throw new Error(`${operation}: data cannot be null or undefined`);
|
|
671
|
+
}
|
|
672
|
+
if (typeof data !== "object") {
|
|
673
|
+
throw new Error(`${operation}: data must be an object`);
|
|
674
|
+
}
|
|
675
|
+
if (data instanceof Array2) {
|
|
676
|
+
throw new Error(`${operation}: data cannot be an array`);
|
|
677
|
+
}
|
|
678
|
+
const reservedFields = ["id", "_id", "_createdAt", "_updatedAt", "_deleted"];
|
|
679
|
+
for (const field of reservedFields) {
|
|
680
|
+
if (field in data) {
|
|
681
|
+
throw new Error(`${operation}: '${field}' is a reserved field and cannot be set manually`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const dataSize = JSON.stringify(data).length;
|
|
685
|
+
if (dataSize > 1024 * 1024) {
|
|
686
|
+
throw new Error(`${operation}: document size (${Math.round(dataSize / 1024)}KB) exceeds maximum allowed size (1MB)`);
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
JSON.stringify(data);
|
|
690
|
+
} catch {
|
|
691
|
+
throw new Error(`${operation}: data contains circular references`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
637
694
|
var DocumentRef = class {
|
|
638
695
|
constructor(app, collection, id) {
|
|
639
696
|
this.app = app;
|
|
@@ -645,50 +702,24 @@ var DocumentRef = class {
|
|
|
645
702
|
}
|
|
646
703
|
async get() {
|
|
647
704
|
if (this.persistence) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
653
|
-
this.refreshFromRemote().catch(() => {
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
async refreshFromRemote() {
|
|
659
|
-
try {
|
|
660
|
-
const res = await new HttpsRequest({
|
|
661
|
-
method: "POST" /* POST */,
|
|
662
|
-
endpoint: `${this.app.getBaseUrl()}/app/read`,
|
|
663
|
-
headers: { authorization: this.app.getConfig().token },
|
|
664
|
-
body: {
|
|
665
|
-
collection: this.collection,
|
|
666
|
-
document: this.id
|
|
667
|
-
}
|
|
668
|
-
}).sendRequest();
|
|
669
|
-
if (!res?.success || !res.document)
|
|
705
|
+
try {
|
|
706
|
+
const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
|
|
707
|
+
if (localSnap)
|
|
708
|
+
return localSnap;
|
|
670
709
|
return null;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
id: res.document.id ?? res.document._id ?? this.id
|
|
674
|
-
};
|
|
675
|
-
delete doc._id;
|
|
676
|
-
if (this.persistence) {
|
|
677
|
-
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.error("[EdmaxLabs] Error reading from cache:", error);
|
|
678
712
|
}
|
|
679
|
-
const snap = DocumentSnapshot.fromMap(doc);
|
|
680
|
-
this.localStore?.emitDocument(this.collection, this.id, snap, "remote_update");
|
|
681
|
-
return snap;
|
|
682
|
-
} catch {
|
|
683
|
-
return null;
|
|
684
713
|
}
|
|
714
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).get();
|
|
685
715
|
}
|
|
686
716
|
async set(data) {
|
|
717
|
+
validateDocumentData(data, "DocumentRef.set");
|
|
687
718
|
if (!this.persistence) {
|
|
688
719
|
const res = await new HttpsRequest({
|
|
689
720
|
method: "POST" /* POST */,
|
|
690
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
691
|
-
headers: { authorization: this.app.getConfig().token },
|
|
721
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
722
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
692
723
|
body: { collection: this.collection, data: { ...data, id: this.id } }
|
|
693
724
|
}).sendRequest();
|
|
694
725
|
return res?.success ? DocumentSnapshot.fromMap(data) : null;
|
|
@@ -707,78 +738,85 @@ var DocumentRef = class {
|
|
|
707
738
|
mutationId: this.persistence.createMutationId(),
|
|
708
739
|
collection: this.collection,
|
|
709
740
|
documentId: this.id,
|
|
710
|
-
type: "
|
|
741
|
+
type: "insert",
|
|
711
742
|
// or "create" if you want to distinguish truly new docs
|
|
712
743
|
payload: data
|
|
713
744
|
});
|
|
714
745
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
715
|
-
this.localStore?.emitDocument(this.collection, this.id, snap, "
|
|
746
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "update");
|
|
716
747
|
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
717
|
-
this.localStore?.emitCollection(this.collection, currentCollection, "
|
|
748
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "update", this.id);
|
|
718
749
|
this.syncEngine?.flush().catch(console.error);
|
|
719
750
|
return snap;
|
|
720
751
|
}
|
|
721
752
|
async update(data) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
753
|
+
validateDocumentData(data, "DocumentRef.update");
|
|
754
|
+
if (this.persistence) {
|
|
755
|
+
const old = await this.persistence.getDoc(this.collection, this.id);
|
|
756
|
+
if (!old || old.deleted)
|
|
757
|
+
return null;
|
|
758
|
+
const mergedData = { ...old.data, ...data, id: this.id };
|
|
759
|
+
const updated = await this.persistence.upsertDoc({
|
|
760
|
+
collection: this.collection,
|
|
761
|
+
id: this.id,
|
|
762
|
+
data: mergedData,
|
|
763
|
+
exists: true,
|
|
764
|
+
deleted: false,
|
|
765
|
+
pending: 1,
|
|
766
|
+
localOnly: old.localOnly,
|
|
767
|
+
status: "pending",
|
|
768
|
+
revision: old.revision,
|
|
769
|
+
lastSyncedAt: old.lastSyncedAt
|
|
770
|
+
});
|
|
771
|
+
await this.persistence.enqueueMutation({
|
|
772
|
+
mutationId: this.persistence.createMutationId(),
|
|
773
|
+
collection: this.collection,
|
|
774
|
+
documentId: this.id,
|
|
775
|
+
type: "update",
|
|
776
|
+
payload: data,
|
|
777
|
+
baseRevision: old.revision
|
|
778
|
+
});
|
|
779
|
+
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
780
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "update");
|
|
781
|
+
this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
|
|
782
|
+
this.syncEngine?.flush().catch(console.error);
|
|
783
|
+
return snap;
|
|
784
|
+
}
|
|
785
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).update(data);
|
|
754
786
|
}
|
|
755
787
|
async delete() {
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return
|
|
771
|
-
}
|
|
772
|
-
array(key) {
|
|
773
|
-
return new Array2(this.app, this.collection, key, this.id);
|
|
788
|
+
if (this.persistence) {
|
|
789
|
+
await this.persistence.markDeleted(this.collection, this.id, 1);
|
|
790
|
+
await this.persistence.enqueueMutation({
|
|
791
|
+
mutationId: this.persistence.createMutationId(),
|
|
792
|
+
collection: this.collection,
|
|
793
|
+
documentId: this.id,
|
|
794
|
+
type: "delete",
|
|
795
|
+
payload: null
|
|
796
|
+
});
|
|
797
|
+
this.localStore?.emitDocument(this.collection, this.id, null, "empty");
|
|
798
|
+
this.localStore?.notifyCollectionChanged(this.collection, this.id, "delete");
|
|
799
|
+
this.syncEngine?.flush().catch(console.error);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).delete();
|
|
774
803
|
}
|
|
804
|
+
// array(key: string): Array {
|
|
805
|
+
// return new Array(this.app, this.collection, key, this.id);
|
|
806
|
+
// }
|
|
775
807
|
onSnapshot(callback) {
|
|
776
|
-
this.app.offline().
|
|
777
|
-
|
|
778
|
-
this.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
808
|
+
if (this.app.offline().persistence && this.app.offline().localStore && this.app.offline().realtimeBridge) {
|
|
809
|
+
this.app.offline().realtimeBridge?.watchDocument(this.collection, this.id);
|
|
810
|
+
return this.app.offline().localStore?.subscribeToDocument(
|
|
811
|
+
this.collection,
|
|
812
|
+
this.id,
|
|
813
|
+
callback
|
|
814
|
+
) || (() => {
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (snapshot, change) => {
|
|
818
|
+
const res = normalizePayload(snapshot);
|
|
819
|
+
callback(DocumentSnapshot.fromMap(res?.data), change);
|
|
782
820
|
});
|
|
783
821
|
}
|
|
784
822
|
};
|
|
@@ -789,6 +827,7 @@ var Query = class {
|
|
|
789
827
|
this.filter = [];
|
|
790
828
|
this.app = app;
|
|
791
829
|
this.collection = collection;
|
|
830
|
+
this.localStore = app.offline().localStore;
|
|
792
831
|
}
|
|
793
832
|
where(expression) {
|
|
794
833
|
this.filter.push(expression);
|
|
@@ -832,8 +871,59 @@ var Query = class {
|
|
|
832
871
|
});
|
|
833
872
|
return mongoFilter;
|
|
834
873
|
}
|
|
874
|
+
matchesFilter(document2, { key, op, value }) {
|
|
875
|
+
const fieldValue = document2[key];
|
|
876
|
+
switch (op) {
|
|
877
|
+
case "==":
|
|
878
|
+
case "===":
|
|
879
|
+
return fieldValue === value;
|
|
880
|
+
case "!=":
|
|
881
|
+
return fieldValue !== value;
|
|
882
|
+
case "<":
|
|
883
|
+
return fieldValue < value;
|
|
884
|
+
case "<=":
|
|
885
|
+
return fieldValue <= value;
|
|
886
|
+
case ">":
|
|
887
|
+
return fieldValue > value;
|
|
888
|
+
case ">=":
|
|
889
|
+
return fieldValue >= value;
|
|
890
|
+
case "in":
|
|
891
|
+
return Array.isArray(value) && value.includes(fieldValue);
|
|
892
|
+
case "nin":
|
|
893
|
+
return Array.isArray(value) && !value.includes(fieldValue);
|
|
894
|
+
case "contains":
|
|
895
|
+
if (fieldValue == null)
|
|
896
|
+
return false;
|
|
897
|
+
return String(fieldValue).toLowerCase().includes(String(value).toLowerCase());
|
|
898
|
+
default:
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
filterDocuments(docs) {
|
|
903
|
+
if (this.filter.length === 0)
|
|
904
|
+
return docs;
|
|
905
|
+
return docs.filter(
|
|
906
|
+
(snapshot) => this.filter.every(
|
|
907
|
+
(expression) => this.matchesFilter(snapshot.data, expression)
|
|
908
|
+
)
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
async getLocalFilteredSnapshots() {
|
|
912
|
+
const persistence = this.app.offline().persistence;
|
|
913
|
+
if (!persistence)
|
|
914
|
+
return [];
|
|
915
|
+
const docs = await persistence.getCollectionSnapshots(this.collection);
|
|
916
|
+
return this.filterDocuments(docs);
|
|
917
|
+
}
|
|
835
918
|
async get() {
|
|
836
919
|
const persistence = this.app.offline().persistence;
|
|
920
|
+
if (this.filter.length > 0 && persistence) {
|
|
921
|
+
const local = await this.getLocalFilteredSnapshots();
|
|
922
|
+
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
923
|
+
return local;
|
|
924
|
+
}
|
|
925
|
+
return this.refreshFromRemote();
|
|
926
|
+
}
|
|
837
927
|
if (persistence) {
|
|
838
928
|
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
839
929
|
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
@@ -849,8 +939,8 @@ var Query = class {
|
|
|
849
939
|
const genfilter = this.buildFilter();
|
|
850
940
|
const res = await new HttpsRequest({
|
|
851
941
|
method: "POST" /* POST */,
|
|
852
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
853
|
-
headers: { authorization: this.app.getConfig().token },
|
|
942
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
943
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
854
944
|
body: {
|
|
855
945
|
collection: this.collection,
|
|
856
946
|
filter: genfilter
|
|
@@ -874,8 +964,8 @@ var Query = class {
|
|
|
874
964
|
const genfilter = this.buildFilter();
|
|
875
965
|
const res = await new HttpsRequest({
|
|
876
966
|
method: "POST" /* POST */,
|
|
877
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
878
|
-
headers: { authorization: this.app.getConfig().token },
|
|
967
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
968
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
879
969
|
body: {
|
|
880
970
|
collection: this.collection,
|
|
881
971
|
filter: genfilter,
|
|
@@ -886,17 +976,42 @@ var Query = class {
|
|
|
886
976
|
}
|
|
887
977
|
onSnapshot(callback) {
|
|
888
978
|
const genfilter = this.buildFilter();
|
|
889
|
-
this.app.
|
|
890
|
-
|
|
891
|
-
(
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
979
|
+
const persistence = this.app.offline().persistence;
|
|
980
|
+
if (persistence) {
|
|
981
|
+
const remoteUnsub = this.app.offline().realtimeBridge?.watchCollection(this.collection, genfilter);
|
|
982
|
+
if (this.filter.length > 0) {
|
|
983
|
+
const emitFiltered = async (change, changedDocId) => {
|
|
984
|
+
const docs = await this.getLocalFilteredSnapshots();
|
|
985
|
+
callback(docs, change, changedDocId);
|
|
986
|
+
};
|
|
987
|
+
emitFiltered("insert").catch(console.error);
|
|
988
|
+
const localUnsub2 = this.localStore?.subscribeToCollection(
|
|
989
|
+
this.collection,
|
|
990
|
+
async (snapshots, change, changedDocId) => {
|
|
991
|
+
if (snapshots.length > 0) {
|
|
992
|
+
callback(this.filterDocuments(snapshots), change, changedDocId);
|
|
993
|
+
} else {
|
|
994
|
+
await emitFiltered(change, changedDocId);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
) ?? (() => {
|
|
998
|
+
});
|
|
999
|
+
return () => {
|
|
1000
|
+
localUnsub2();
|
|
1001
|
+
remoteUnsub?.();
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const localUnsub = this.localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
|
|
1005
|
+
});
|
|
1006
|
+
return () => {
|
|
1007
|
+
localUnsub();
|
|
1008
|
+
remoteUnsub?.();
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
1012
|
+
const res = normalizePayload(payload);
|
|
1013
|
+
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
1014
|
+
}, genfilter);
|
|
900
1015
|
}
|
|
901
1016
|
};
|
|
902
1017
|
|
|
@@ -916,20 +1031,22 @@ var CollectionRef = class {
|
|
|
916
1031
|
return new Query(this.app, this.collection);
|
|
917
1032
|
}
|
|
918
1033
|
async get() {
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
1034
|
+
if (this.persistence) {
|
|
1035
|
+
const local = await this.persistence.getCollectionSnapshots(this.collection);
|
|
1036
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
1037
|
+
this.refreshFromRemote().catch(() => {
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return local;
|
|
925
1041
|
}
|
|
926
|
-
|
|
1042
|
+
;
|
|
1043
|
+
return await this.app.getDatabase.collection(this.collection).get();
|
|
927
1044
|
}
|
|
928
1045
|
async refreshFromRemote() {
|
|
929
1046
|
try {
|
|
930
1047
|
const res = await new HttpsRequest({
|
|
931
1048
|
method: "POST" /* POST */,
|
|
932
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
1049
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
933
1050
|
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
934
1051
|
body: {
|
|
935
1052
|
collection: this.collection,
|
|
@@ -943,52 +1060,61 @@ var CollectionRef = class {
|
|
|
943
1060
|
const doc = { ...raw };
|
|
944
1061
|
doc.id = doc.id ?? doc._id;
|
|
945
1062
|
delete doc._id;
|
|
946
|
-
delete doc.token;
|
|
947
1063
|
if (this.persistence) {
|
|
948
1064
|
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
949
1065
|
}
|
|
950
1066
|
}
|
|
951
1067
|
if (this.persistence) {
|
|
952
|
-
|
|
953
|
-
this.localStore?.emitCollection(this.collection, updatedCollection, "remote_update");
|
|
954
|
-
return updatedCollection;
|
|
1068
|
+
return await this.persistence.getCollectionSnapshots(this.collection);
|
|
955
1069
|
}
|
|
956
|
-
return
|
|
1070
|
+
return res.documents.map((d) => {
|
|
1071
|
+
delete d.token;
|
|
1072
|
+
d.id = d.id ?? d._id;
|
|
1073
|
+
delete d._id;
|
|
1074
|
+
return DocumentSnapshot.fromMap(d);
|
|
1075
|
+
});
|
|
957
1076
|
} catch {
|
|
958
1077
|
return [];
|
|
959
1078
|
}
|
|
960
1079
|
}
|
|
961
1080
|
async add(data) {
|
|
962
|
-
if (
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1081
|
+
if (this.persistence) {
|
|
1082
|
+
const localId = this.persistence.createLocalId();
|
|
1083
|
+
const docRecord = await this.persistence.upsertDoc({
|
|
1084
|
+
collection: this.collection,
|
|
1085
|
+
id: localId,
|
|
1086
|
+
data: { ...data, id: localId },
|
|
1087
|
+
exists: true,
|
|
1088
|
+
deleted: false,
|
|
1089
|
+
pending: 1,
|
|
1090
|
+
localOnly: true,
|
|
1091
|
+
status: "pending"
|
|
1092
|
+
});
|
|
1093
|
+
await this.persistence.enqueueMutation({
|
|
1094
|
+
mutationId: this.persistence.createMutationId(),
|
|
1095
|
+
collection: this.collection,
|
|
1096
|
+
documentId: localId,
|
|
1097
|
+
type: "insert",
|
|
1098
|
+
payload: docRecord.data
|
|
1099
|
+
});
|
|
1100
|
+
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
1101
|
+
this.localStore?.emitDocument(this.collection, localId, snap, "insert");
|
|
1102
|
+
this.localStore?.notifyCollectionChanged(this.collection, localId, "insert");
|
|
1103
|
+
this.syncEngine?.flush().catch(console.error);
|
|
1104
|
+
return snap;
|
|
1105
|
+
}
|
|
1106
|
+
;
|
|
1107
|
+
return await this.app.getDatabase.collection(this.collection).add(data);
|
|
988
1108
|
}
|
|
989
1109
|
onSnapshot(callback) {
|
|
990
|
-
this.app.offline().
|
|
991
|
-
|
|
1110
|
+
if (this.app.offline().persistence) {
|
|
1111
|
+
this.app.offline().realtimeBridge?.watchCollection(this.collection);
|
|
1112
|
+
return this.app.offline().localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
1116
|
+
const res = normalizePayload(payload);
|
|
1117
|
+
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
992
1118
|
});
|
|
993
1119
|
}
|
|
994
1120
|
};
|
|
@@ -1008,6 +1134,15 @@ var Batch = class {
|
|
|
1008
1134
|
});
|
|
1009
1135
|
return this;
|
|
1010
1136
|
}
|
|
1137
|
+
create(docRef, data) {
|
|
1138
|
+
this.ops.push({
|
|
1139
|
+
op: "create",
|
|
1140
|
+
collection: docRef.collection,
|
|
1141
|
+
id: docRef.id,
|
|
1142
|
+
data
|
|
1143
|
+
});
|
|
1144
|
+
return this;
|
|
1145
|
+
}
|
|
1011
1146
|
update(docRef, data) {
|
|
1012
1147
|
this.ops.push({
|
|
1013
1148
|
op: "update",
|
|
@@ -1033,11 +1168,33 @@ var Batch = class {
|
|
|
1033
1168
|
const results = [];
|
|
1034
1169
|
for (const op of this.ops) {
|
|
1035
1170
|
try {
|
|
1036
|
-
if (op.op === "
|
|
1171
|
+
if (op.op === "update") {
|
|
1037
1172
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1038
|
-
const snap = await docRef.
|
|
1173
|
+
const snap = await docRef.update(op.data);
|
|
1039
1174
|
if (snap)
|
|
1040
1175
|
results.push(snap);
|
|
1176
|
+
} else if (op.op === "create" || op.op === "insert" || op.op === "set") {
|
|
1177
|
+
const upserted = await persistence.upsertDoc({
|
|
1178
|
+
collection: op.collection,
|
|
1179
|
+
id: op.id,
|
|
1180
|
+
data: { ...op.data, id: op.id },
|
|
1181
|
+
exists: true,
|
|
1182
|
+
deleted: false,
|
|
1183
|
+
pending: 1,
|
|
1184
|
+
localOnly: false,
|
|
1185
|
+
status: "pending"
|
|
1186
|
+
});
|
|
1187
|
+
await persistence.enqueueMutation({
|
|
1188
|
+
mutationId: persistence.createMutationId(),
|
|
1189
|
+
collection: op.collection,
|
|
1190
|
+
documentId: op.id,
|
|
1191
|
+
type: "insert",
|
|
1192
|
+
payload: op.data
|
|
1193
|
+
});
|
|
1194
|
+
const snap = DocumentSnapshot.fromMap(upserted.data);
|
|
1195
|
+
localStore.emitDocument(op.collection, op.id, snap, "insert");
|
|
1196
|
+
localStore.notifyCollectionChanged(op.collection, op.id, "insert");
|
|
1197
|
+
results.push(snap);
|
|
1041
1198
|
} else if (op.op === "delete") {
|
|
1042
1199
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1043
1200
|
await docRef.delete();
|
|
@@ -1051,10 +1208,8 @@ var Batch = class {
|
|
|
1051
1208
|
}
|
|
1052
1209
|
const res = await new HttpsRequest({
|
|
1053
1210
|
method: "POST" /* POST */,
|
|
1054
|
-
endpoint: `${this.app.getBaseUrl}/
|
|
1055
|
-
headers: {
|
|
1056
|
-
authorization: this.app.getConfig().token
|
|
1057
|
-
},
|
|
1211
|
+
endpoint: `${this.app.getBaseUrl()}/db/batch`,
|
|
1212
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1058
1213
|
body: { ops: this.ops }
|
|
1059
1214
|
}).sendRequest();
|
|
1060
1215
|
if (res?.error) {
|
|
@@ -1105,10 +1260,8 @@ var Database = class {
|
|
|
1105
1260
|
await transactionFn(tx);
|
|
1106
1261
|
const res = await new HttpsRequest({
|
|
1107
1262
|
method: "POST" /* POST */,
|
|
1108
|
-
endpoint: `${this.app.getBaseUrl}/
|
|
1109
|
-
headers: {
|
|
1110
|
-
authorization: this.app.getConfig().token
|
|
1111
|
-
},
|
|
1263
|
+
endpoint: `${this.app.getBaseUrl()}/db/transaction`,
|
|
1264
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1112
1265
|
body: { ops: tx.ops }
|
|
1113
1266
|
}).sendRequest();
|
|
1114
1267
|
if (res?.error) {
|
|
@@ -1219,10 +1372,10 @@ var Realtime = class {
|
|
|
1219
1372
|
this.events.forEach((event) => {
|
|
1220
1373
|
const channel = `${lid}-${event}`;
|
|
1221
1374
|
const fn = (payload) => {
|
|
1222
|
-
const normalized =
|
|
1375
|
+
const normalized = normalizePayload(payload);
|
|
1223
1376
|
if (!normalized)
|
|
1224
1377
|
return;
|
|
1225
|
-
callback(normalized.data,
|
|
1378
|
+
callback(normalized.data, event);
|
|
1226
1379
|
};
|
|
1227
1380
|
this.socket.on(channel, fn);
|
|
1228
1381
|
handlers.push({ event: channel, fn });
|
|
@@ -1242,7 +1395,7 @@ var Realtime = class {
|
|
|
1242
1395
|
this.events.forEach((event) => {
|
|
1243
1396
|
const channel = `${lid}-${event}`;
|
|
1244
1397
|
const fn = (payload) => {
|
|
1245
|
-
const normalized =
|
|
1398
|
+
const normalized = normalizePayload(payload);
|
|
1246
1399
|
if (!normalized) {
|
|
1247
1400
|
callback({ id }, "delete");
|
|
1248
1401
|
return;
|
|
@@ -1256,18 +1409,6 @@ var Realtime = class {
|
|
|
1256
1409
|
return () => this.cleanupSubscription(lid);
|
|
1257
1410
|
}
|
|
1258
1411
|
// ===================== PRIVATE HELPERS =====================
|
|
1259
|
-
normalizePayload(payload) {
|
|
1260
|
-
const raw = payload?.document ?? payload?.data ?? payload;
|
|
1261
|
-
if (!raw)
|
|
1262
|
-
return null;
|
|
1263
|
-
const doc = { ...raw };
|
|
1264
|
-
doc.id = doc.id ?? doc._id;
|
|
1265
|
-
delete doc._id;
|
|
1266
|
-
return {
|
|
1267
|
-
change: payload?.change ?? raw?.change ?? null,
|
|
1268
|
-
data: doc
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
1412
|
registerSubscription(lid, collection, filter, handlers) {
|
|
1272
1413
|
this.subscriptions.set(lid, { lid, collection, filter, handlers });
|
|
1273
1414
|
}
|
|
@@ -1312,10 +1453,8 @@ var Functions = class {
|
|
|
1312
1453
|
try {
|
|
1313
1454
|
const res = await new HttpsRequest({
|
|
1314
1455
|
method: "POST" /* POST */,
|
|
1315
|
-
endpoint: this.app.getBaseUrl + "/functions/call/" + functionName,
|
|
1316
|
-
headers: {
|
|
1317
|
-
authorization: this.app.getConfig().token
|
|
1318
|
-
},
|
|
1456
|
+
endpoint: this.app.getBaseUrl() + "/functions/call/" + functionName,
|
|
1457
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1319
1458
|
body: data
|
|
1320
1459
|
}).sendRequest();
|
|
1321
1460
|
return res;
|
|
@@ -1337,10 +1476,8 @@ var Hosting = class {
|
|
|
1337
1476
|
try {
|
|
1338
1477
|
const res = await new HttpsRequest({
|
|
1339
1478
|
method: "POST" /* POST */,
|
|
1340
|
-
endpoint: this.app.getBaseUrl + "/hosting/register",
|
|
1341
|
-
headers: {
|
|
1342
|
-
authorization: this.app.getConfig().token
|
|
1343
|
-
},
|
|
1479
|
+
endpoint: this.app.getBaseUrl() + "/hosting/register",
|
|
1480
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1344
1481
|
body: {
|
|
1345
1482
|
hostname: name,
|
|
1346
1483
|
project: this.app.getConfig().project
|
|
@@ -1421,6 +1558,20 @@ var LocalStore = class {
|
|
|
1421
1558
|
}
|
|
1422
1559
|
});
|
|
1423
1560
|
}
|
|
1561
|
+
/**
|
|
1562
|
+
* OPTIMIZED: Notify collection listeners that something changed, without fetching full collection.
|
|
1563
|
+
* Listeners can call getCollectionSnapshots() themselves if they need the full list.
|
|
1564
|
+
* This avoids expensive collection queries after every single mutation.
|
|
1565
|
+
*/
|
|
1566
|
+
notifyCollectionChanged(collection, changedDocId, change) {
|
|
1567
|
+
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1568
|
+
try {
|
|
1569
|
+
cb([], change, changedDocId);
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
console.error(`[EdmaxLabs] Error in collection listener for ${collection}:`, err);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1424
1575
|
// ===================== UTILITY =====================
|
|
1425
1576
|
/**
|
|
1426
1577
|
* Clear all listeners (useful for testing or when persistence is disabled)
|
|
@@ -1439,6 +1590,19 @@ var LocalStore = class {
|
|
|
1439
1590
|
this.collectionListeners.forEach((set) => collCount += set.size);
|
|
1440
1591
|
return { documents: docCount, collections: collCount };
|
|
1441
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Remove all listeners for a specific collection (useful for cleanup)
|
|
1595
|
+
*/
|
|
1596
|
+
removeCollectionListeners(collection) {
|
|
1597
|
+
this.collectionListeners.delete(collection);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Remove all listeners for a specific document (useful for cleanup)
|
|
1601
|
+
*/
|
|
1602
|
+
removeDocumentListeners(collection, id) {
|
|
1603
|
+
const key = this.docKey(collection, id);
|
|
1604
|
+
this.documentListeners.delete(key);
|
|
1605
|
+
}
|
|
1442
1606
|
};
|
|
1443
1607
|
|
|
1444
1608
|
// ../../../../node_modules/idb/build/wrap-idb-value.js
|
|
@@ -1650,8 +1814,9 @@ replaceTraps((oldTraps) => ({
|
|
|
1650
1814
|
|
|
1651
1815
|
// src/persistence/Persistence.ts
|
|
1652
1816
|
var Persistence = class {
|
|
1653
|
-
constructor() {
|
|
1654
|
-
this.
|
|
1817
|
+
constructor(appName = "default") {
|
|
1818
|
+
this.appName = appName;
|
|
1819
|
+
this.dbPromise = openDB(`edmaxlabs_offline_${appName}`, 1, {
|
|
1655
1820
|
upgrade(app, oldVersion) {
|
|
1656
1821
|
if (oldVersion < 1) {
|
|
1657
1822
|
if (!app.objectStoreNames.contains("docs")) {
|
|
@@ -1674,17 +1839,74 @@ var Persistence = class {
|
|
|
1674
1839
|
app.createObjectStore("meta");
|
|
1675
1840
|
}
|
|
1676
1841
|
}
|
|
1842
|
+
},
|
|
1843
|
+
blocked() {
|
|
1844
|
+
console.warn("[EdmaxLabs] IndexedDB blocked - another tab has the database open");
|
|
1845
|
+
},
|
|
1846
|
+
blocking() {
|
|
1847
|
+
console.warn("[EdmaxLabs] IndexedDB blocking - closing to allow upgrade");
|
|
1848
|
+
},
|
|
1849
|
+
terminated() {
|
|
1850
|
+
console.error("[EdmaxLabs] IndexedDB terminated unexpectedly");
|
|
1851
|
+
}
|
|
1852
|
+
}).catch((error) => {
|
|
1853
|
+
if (error.name === "QuotaExceededError") {
|
|
1854
|
+
console.error("[EdmaxLabs] IndexedDB quota exceeded - storage full");
|
|
1855
|
+
throw new Error("Storage quota exceeded. Please clear browser data or free up space.");
|
|
1677
1856
|
}
|
|
1857
|
+
if (error.name === "VersionError") {
|
|
1858
|
+
console.error("[EdmaxLabs] IndexedDB version conflict - clearing and retrying");
|
|
1859
|
+
indexedDB.deleteDatabase(`edmaxlabs_offline_${appName}`);
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
throw error;
|
|
1678
1863
|
});
|
|
1679
1864
|
}
|
|
1680
1865
|
docKey(collection, id) {
|
|
1681
|
-
return `${collection}:${id}`;
|
|
1866
|
+
return `${this.appName}:${collection}:${id}`;
|
|
1682
1867
|
}
|
|
1683
1868
|
now() {
|
|
1684
1869
|
return Date.now();
|
|
1685
1870
|
}
|
|
1686
1871
|
async getDb() {
|
|
1687
|
-
|
|
1872
|
+
try {
|
|
1873
|
+
return await this.dbPromise;
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
if (error.name === "QuotaExceededError") {
|
|
1876
|
+
throw new Error("Storage quota exceeded. Please clear browser data or free up space.");
|
|
1877
|
+
}
|
|
1878
|
+
if (error.name === "VersionError") {
|
|
1879
|
+
console.warn("[EdmaxLabs] IndexedDB version error - attempting recovery");
|
|
1880
|
+
indexedDB.deleteDatabase(`edmaxlabs_offline_${this.appName}`);
|
|
1881
|
+
this.dbPromise = openDB(`edmaxlabs_offline_${this.appName}`, 1, {
|
|
1882
|
+
upgrade(app, oldVersion) {
|
|
1883
|
+
if (oldVersion < 1) {
|
|
1884
|
+
if (!app.objectStoreNames.contains("docs")) {
|
|
1885
|
+
const docs = app.createObjectStore("docs", { keyPath: "key" });
|
|
1886
|
+
docs.createIndex("by-collection", "collection");
|
|
1887
|
+
docs.createIndex("by-id", "id");
|
|
1888
|
+
docs.createIndex("by-updatedAt", "updatedAt");
|
|
1889
|
+
docs.createIndex("by-pending", "pending");
|
|
1890
|
+
}
|
|
1891
|
+
if (!app.objectStoreNames.contains("mutations")) {
|
|
1892
|
+
const mutations = app.createObjectStore("mutations", {
|
|
1893
|
+
keyPath: "mutationId"
|
|
1894
|
+
});
|
|
1895
|
+
mutations.createIndex("by-status", "status");
|
|
1896
|
+
mutations.createIndex("by-collection", "collection");
|
|
1897
|
+
mutations.createIndex("by-documentId", "documentId");
|
|
1898
|
+
mutations.createIndex("by-createdAt", "createdAt");
|
|
1899
|
+
}
|
|
1900
|
+
if (!app.objectStoreNames.contains("meta")) {
|
|
1901
|
+
app.createObjectStore("meta");
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
return await this.dbPromise;
|
|
1907
|
+
}
|
|
1908
|
+
throw error;
|
|
1909
|
+
}
|
|
1688
1910
|
}
|
|
1689
1911
|
// ==================== DOCS ====================
|
|
1690
1912
|
async getDoc(collection, id) {
|
|
@@ -1757,6 +1979,25 @@ var Persistence = class {
|
|
|
1757
1979
|
]);
|
|
1758
1980
|
return [...pending, ...failed].sort((a, b) => a.createdAt - b.createdAt);
|
|
1759
1981
|
}
|
|
1982
|
+
async getMutation(mutationId) {
|
|
1983
|
+
const app = await this.getDb();
|
|
1984
|
+
return await app.get("mutations", mutationId) ?? null;
|
|
1985
|
+
}
|
|
1986
|
+
async resetMutation(mutationId) {
|
|
1987
|
+
const app = await this.getDb();
|
|
1988
|
+
const old = await app.get("mutations", mutationId);
|
|
1989
|
+
if (!old)
|
|
1990
|
+
return null;
|
|
1991
|
+
const next = {
|
|
1992
|
+
...old,
|
|
1993
|
+
status: "pending",
|
|
1994
|
+
retryCount: 0,
|
|
1995
|
+
updatedAt: this.now(),
|
|
1996
|
+
error: void 0
|
|
1997
|
+
};
|
|
1998
|
+
await app.put("mutations", next);
|
|
1999
|
+
return next;
|
|
2000
|
+
}
|
|
1760
2001
|
async setMutationStatus(mutationId, status, error) {
|
|
1761
2002
|
const app = await this.getDb();
|
|
1762
2003
|
const old = await app.get("mutations", mutationId);
|
|
@@ -1891,7 +2132,7 @@ var RealtimeBridge = class {
|
|
|
1891
2132
|
if (id)
|
|
1892
2133
|
await this.handleRemoteDelete(collection, id);
|
|
1893
2134
|
} else {
|
|
1894
|
-
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
2135
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1895
2136
|
}
|
|
1896
2137
|
},
|
|
1897
2138
|
filter
|
|
@@ -1916,7 +2157,7 @@ var RealtimeBridge = class {
|
|
|
1916
2157
|
if (change === "delete") {
|
|
1917
2158
|
await this.handleRemoteDelete(collection, id);
|
|
1918
2159
|
} else {
|
|
1919
|
-
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
2160
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1920
2161
|
}
|
|
1921
2162
|
}
|
|
1922
2163
|
);
|
|
@@ -1931,13 +2172,13 @@ var RealtimeBridge = class {
|
|
|
1931
2172
|
async emitCurrentDocument(collection, id) {
|
|
1932
2173
|
const localDoc = await this.persistence.getDoc(collection, id);
|
|
1933
2174
|
const snapshot = localDoc && localDoc.exists && !localDoc.deleted ? DocumentSnapshot.fromMap(localDoc.data) : null;
|
|
1934
|
-
this.store.emitDocument(collection, id, snapshot, "
|
|
2175
|
+
this.store.emitDocument(collection, id, snapshot, "insert");
|
|
1935
2176
|
}
|
|
1936
2177
|
async emitCurrentCollection(collection) {
|
|
1937
2178
|
const localDocs = await this.persistence.getCollectionSnapshots(collection);
|
|
1938
|
-
this.store.emitCollection(collection, localDocs, "
|
|
2179
|
+
this.store.emitCollection(collection, localDocs, "insert");
|
|
1939
2180
|
}
|
|
1940
|
-
async handleRemoteCreateOrUpdate(collection, raw) {
|
|
2181
|
+
async handleRemoteCreateOrUpdate(collection, raw, change) {
|
|
1941
2182
|
const id = raw.id ?? raw._id;
|
|
1942
2183
|
if (!id)
|
|
1943
2184
|
return;
|
|
@@ -1945,15 +2186,13 @@ var RealtimeBridge = class {
|
|
|
1945
2186
|
if (!saved)
|
|
1946
2187
|
return;
|
|
1947
2188
|
const snap = DocumentSnapshot.fromMap(saved.data);
|
|
1948
|
-
this.store.emitDocument(collection, id, snap,
|
|
1949
|
-
|
|
1950
|
-
this.store.emitCollection(collection, currentCollection, "remote_update", id);
|
|
2189
|
+
this.store.emitDocument(collection, id, snap, change);
|
|
2190
|
+
this.store.notifyCollectionChanged(collection, id, change);
|
|
1951
2191
|
}
|
|
1952
2192
|
async handleRemoteDelete(collection, id) {
|
|
1953
2193
|
await this.persistence.applyRemoteDelete(collection, id);
|
|
1954
|
-
this.store.emitDocument(collection, id, null, "
|
|
1955
|
-
|
|
1956
|
-
this.store.emitCollection(collection, currentCollection, "remote_delete", id);
|
|
2194
|
+
this.store.emitDocument(collection, id, null, "delete");
|
|
2195
|
+
this.store.notifyCollectionChanged(collection, id, "delete");
|
|
1957
2196
|
}
|
|
1958
2197
|
// ===================== LIFECYCLE =====================
|
|
1959
2198
|
/**
|
|
@@ -2029,7 +2268,7 @@ var SyncEngine = class {
|
|
|
2029
2268
|
try {
|
|
2030
2269
|
let success = false;
|
|
2031
2270
|
switch (mutation.type) {
|
|
2032
|
-
case "
|
|
2271
|
+
case "insert":
|
|
2033
2272
|
success = await this.syncCreate(mutation);
|
|
2034
2273
|
break;
|
|
2035
2274
|
case "update":
|
|
@@ -2055,7 +2294,7 @@ var SyncEngine = class {
|
|
|
2055
2294
|
await this.persistence.setMutationStatus(
|
|
2056
2295
|
mutation.mutationId,
|
|
2057
2296
|
"failed",
|
|
2058
|
-
error?.message ?? "Unknown
|
|
2297
|
+
error?.message ?? "Unknown insert error"
|
|
2059
2298
|
);
|
|
2060
2299
|
if (nextRetryCount < this.MAX_RETRIES) {
|
|
2061
2300
|
this.scheduleRetry();
|
|
@@ -2072,8 +2311,8 @@ var SyncEngine = class {
|
|
|
2072
2311
|
async syncCreate(mutation) {
|
|
2073
2312
|
const res = await new HttpsRequest({
|
|
2074
2313
|
method: "POST" /* POST */,
|
|
2075
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2076
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2314
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
2315
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2077
2316
|
body: {
|
|
2078
2317
|
collection: mutation.collection,
|
|
2079
2318
|
data: mutation.payload
|
|
@@ -2090,18 +2329,17 @@ var SyncEngine = class {
|
|
|
2090
2329
|
);
|
|
2091
2330
|
if (replaced) {
|
|
2092
2331
|
const snap = DocumentSnapshot.fromMap(replaced.data);
|
|
2093
|
-
this.store.emitDocument(mutation.collection, oldId, snap, "
|
|
2094
|
-
this.store.emitDocument(mutation.collection, newId, snap, "
|
|
2095
|
-
|
|
2096
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", newId);
|
|
2332
|
+
this.store.emitDocument(mutation.collection, oldId, snap, "insert");
|
|
2333
|
+
this.store.emitDocument(mutation.collection, newId, snap, "insert");
|
|
2334
|
+
this.store.notifyCollectionChanged(mutation.collection, newId, "insert");
|
|
2097
2335
|
}
|
|
2098
2336
|
return true;
|
|
2099
2337
|
}
|
|
2100
2338
|
async syncUpdate(mutation) {
|
|
2101
2339
|
const res = await new HttpsRequest({
|
|
2102
2340
|
method: "POST" /* POST */,
|
|
2103
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2104
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2341
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
2342
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2105
2343
|
body: {
|
|
2106
2344
|
collection: mutation.collection,
|
|
2107
2345
|
document: mutation.documentId,
|
|
@@ -2123,19 +2361,17 @@ var SyncEngine = class {
|
|
|
2123
2361
|
localOnly: false,
|
|
2124
2362
|
status: "synced",
|
|
2125
2363
|
lastSyncedAt: this.persistence["now"]?.() ?? Date.now()
|
|
2126
|
-
// fallback
|
|
2127
2364
|
});
|
|
2128
2365
|
const snap = DocumentSnapshot.fromMap(local.data);
|
|
2129
|
-
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "
|
|
2130
|
-
|
|
2131
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2366
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "update");
|
|
2367
|
+
this.store.notifyCollectionChanged(mutation.collection, mutation.documentId, "update");
|
|
2132
2368
|
return true;
|
|
2133
2369
|
}
|
|
2134
2370
|
async syncDelete(mutation) {
|
|
2135
2371
|
const res = await new HttpsRequest({
|
|
2136
2372
|
method: "POST" /* POST */,
|
|
2137
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2138
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2373
|
+
endpoint: `${this.app.getBaseUrl()}/db/delete`,
|
|
2374
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2139
2375
|
body: {
|
|
2140
2376
|
collection: mutation.collection,
|
|
2141
2377
|
document: mutation.documentId
|
|
@@ -2144,9 +2380,8 @@ var SyncEngine = class {
|
|
|
2144
2380
|
if (!res?.success)
|
|
2145
2381
|
return false;
|
|
2146
2382
|
await this.persistence.markDeleted(mutation.collection, mutation.documentId, 0);
|
|
2147
|
-
this.store.emitDocument(mutation.collection, mutation.documentId, null, "
|
|
2148
|
-
|
|
2149
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2383
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, null, "delete");
|
|
2384
|
+
this.store.notifyCollectionChanged(mutation.collection, mutation.documentId, "delete");
|
|
2150
2385
|
return true;
|
|
2151
2386
|
}
|
|
2152
2387
|
scheduleRetry() {
|
|
@@ -2161,6 +2396,50 @@ var SyncEngine = class {
|
|
|
2161
2396
|
async forceSync() {
|
|
2162
2397
|
return this.flush();
|
|
2163
2398
|
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Get all failed mutations for error handling/UI
|
|
2401
|
+
* Returns mutations that exceeded MAX_RETRIES
|
|
2402
|
+
*/
|
|
2403
|
+
async getFailedMutations() {
|
|
2404
|
+
const all = await this.persistence.getPendingMutations();
|
|
2405
|
+
return all.filter((m) => m.status === "failed");
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Retry a specific failed mutation by resetting its retry count
|
|
2409
|
+
* Useful for user-initiated recovery after fixing network/server issues
|
|
2410
|
+
*/
|
|
2411
|
+
async retryMutation(mutationId) {
|
|
2412
|
+
const reset = await this.persistence.resetMutation(mutationId);
|
|
2413
|
+
if (!reset)
|
|
2414
|
+
return false;
|
|
2415
|
+
this.flush().catch(console.error);
|
|
2416
|
+
return true;
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Retry all failed mutations at once
|
|
2420
|
+
*/
|
|
2421
|
+
async retryAllFailed() {
|
|
2422
|
+
const failed = await this.getFailedMutations();
|
|
2423
|
+
for (const mut of failed) {
|
|
2424
|
+
await this.persistence.setMutationStatus(mut.mutationId, "pending");
|
|
2425
|
+
}
|
|
2426
|
+
if (failed.length > 0) {
|
|
2427
|
+
this.flush().catch(console.error);
|
|
2428
|
+
}
|
|
2429
|
+
return failed.length;
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Remove a mutation entirely (user acknowledges the failure and wants to discard it)
|
|
2433
|
+
* Be careful: this means the operation will never sync to the server
|
|
2434
|
+
*/
|
|
2435
|
+
async removeMutation(mutationId) {
|
|
2436
|
+
try {
|
|
2437
|
+
await this.persistence.removeMutation(mutationId);
|
|
2438
|
+
return true;
|
|
2439
|
+
} catch {
|
|
2440
|
+
return false;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2164
2443
|
dispose() {
|
|
2165
2444
|
if (this.retryTimeout) {
|
|
2166
2445
|
clearTimeout(this.retryTimeout);
|
|
@@ -2234,9 +2513,7 @@ var StorageRef = class {
|
|
|
2234
2513
|
const res = await new HttpsRequest({
|
|
2235
2514
|
method: "POST" /* POST */,
|
|
2236
2515
|
endpoint: this.app.getBaseUrl() + "/storage/file/upload",
|
|
2237
|
-
headers: {
|
|
2238
|
-
authorization: this.app.getConfig().token
|
|
2239
|
-
},
|
|
2516
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2240
2517
|
file: srcFile,
|
|
2241
2518
|
isMultipart: true
|
|
2242
2519
|
}).sendRequest();
|
|
@@ -2253,9 +2530,7 @@ var StorageRef = class {
|
|
|
2253
2530
|
const res = await new HttpsRequest({
|
|
2254
2531
|
method: "POST" /* POST */,
|
|
2255
2532
|
endpoint: this.app.getBaseUrl() + "/storage/file/delete",
|
|
2256
|
-
headers: {
|
|
2257
|
-
authorization: this.app.getConfig().token
|
|
2258
|
-
},
|
|
2533
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2259
2534
|
body: {
|
|
2260
2535
|
file_id: id
|
|
2261
2536
|
}
|
|
@@ -2297,7 +2572,8 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2297
2572
|
this._realtime = new Realtime(this);
|
|
2298
2573
|
this._hosting = new Hosting(this);
|
|
2299
2574
|
if (persistence) {
|
|
2300
|
-
|
|
2575
|
+
const appName = config.app_name || config.project;
|
|
2576
|
+
this.persistence = new Persistence(appName);
|
|
2301
2577
|
this.localStore = new LocalStore();
|
|
2302
2578
|
this.realtimeBridge = new RealtimeBridge(this, this.persistence, this.localStore);
|
|
2303
2579
|
this.syncEngine = new SyncEngine(
|
|
@@ -2336,6 +2612,29 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2336
2612
|
}
|
|
2337
2613
|
return Authentication.instance;
|
|
2338
2614
|
}
|
|
2615
|
+
// ===================== OFFLINE UTILITIES =====================
|
|
2616
|
+
/** Check if offline features are enabled */
|
|
2617
|
+
get isOfflineEnabled() {
|
|
2618
|
+
return !!this.persistence;
|
|
2619
|
+
}
|
|
2620
|
+
/** Get current storage usage (approximate) */
|
|
2621
|
+
async getStorageUsage() {
|
|
2622
|
+
if (!this.persistence)
|
|
2623
|
+
return null;
|
|
2624
|
+
try {
|
|
2625
|
+
const usage = await this.persistence.getStorageUsage();
|
|
2626
|
+
return { used: usage, available: 50 * 1024 * 1024 };
|
|
2627
|
+
} catch {
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
/** Clear all cached data (nuclear option) */
|
|
2632
|
+
async clearCache() {
|
|
2633
|
+
if (this.persistence) {
|
|
2634
|
+
await this.persistence.clearAll();
|
|
2635
|
+
}
|
|
2636
|
+
this.localStore?.clearAllListeners();
|
|
2637
|
+
}
|
|
2339
2638
|
/** New clean offline namespace - highly recommended */
|
|
2340
2639
|
offline() {
|
|
2341
2640
|
return {
|
|
@@ -2343,9 +2642,39 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2343
2642
|
localStore: this.localStore,
|
|
2344
2643
|
syncEngine: this.syncEngine,
|
|
2345
2644
|
realtimeBridge: this.realtimeBridge,
|
|
2346
|
-
enabled: !!this.persistence
|
|
2645
|
+
enabled: !!this.persistence,
|
|
2646
|
+
// Add cleanup utilities
|
|
2647
|
+
clearListeners: () => this.localStore?.clearAllListeners(),
|
|
2648
|
+
getListenerCount: () => this.localStore?.listenerCount || { documents: 0, collections: 0 }
|
|
2347
2649
|
};
|
|
2348
2650
|
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Manually trigger sync of pending mutations
|
|
2653
|
+
* Useful for progressive sync or after network restoration
|
|
2654
|
+
*/
|
|
2655
|
+
async sync() {
|
|
2656
|
+
if (!this.syncEngine) {
|
|
2657
|
+
console.warn("[EdmaxLabs] Sync called but persistence is not enabled");
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
return this.syncEngine.forceSync();
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Get mutations that failed to sync (for error UI)
|
|
2664
|
+
*/
|
|
2665
|
+
async getFailedMutations() {
|
|
2666
|
+
if (!this.syncEngine)
|
|
2667
|
+
return [];
|
|
2668
|
+
return this.syncEngine.getFailedMutations();
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Retry all failed mutations
|
|
2672
|
+
*/
|
|
2673
|
+
async retrySync() {
|
|
2674
|
+
if (!this.syncEngine)
|
|
2675
|
+
return 0;
|
|
2676
|
+
return this.syncEngine.retryAllFailed();
|
|
2677
|
+
}
|
|
2349
2678
|
// Internal access (for internal classes only)
|
|
2350
2679
|
getConfig() {
|
|
2351
2680
|
return { ...this._config };
|