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.cjs
CHANGED
|
@@ -84,29 +84,45 @@ var HttpsRequest = class {
|
|
|
84
84
|
this.isMultipart = isMultipart;
|
|
85
85
|
}
|
|
86
86
|
async sendRequest() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
try {
|
|
88
|
+
if (this.isMultipart && this.file) {
|
|
89
|
+
const formData = new FormData();
|
|
90
|
+
Object.entries(this.body).forEach(([key, value]) => {
|
|
91
|
+
formData.append(key, value);
|
|
92
|
+
});
|
|
93
|
+
formData.append("file", this.file);
|
|
94
|
+
const response2 = await fetch(this.endpoint, {
|
|
95
|
+
method: this.method,
|
|
96
|
+
headers: this.headers,
|
|
97
|
+
body: formData
|
|
98
|
+
});
|
|
99
|
+
if (!response2.ok) {
|
|
100
|
+
const errorText = await response2.text().catch(() => "Network error");
|
|
101
|
+
throw new Error(`HTTP ${response2.status}: ${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
return await response2.json().catch(() => ({}));
|
|
104
|
+
}
|
|
101
105
|
const response = await fetch(this.endpoint, {
|
|
102
106
|
method: this.method,
|
|
103
107
|
headers: {
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
...this.headers
|
|
106
110
|
},
|
|
107
|
-
body: JSON.stringify(this.body)
|
|
111
|
+
body: this.method !== "GET" /* GET */ ? JSON.stringify(this.body) : void 0
|
|
108
112
|
});
|
|
109
|
-
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const errorText = await response.text().catch(() => "Network error");
|
|
115
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
116
|
+
}
|
|
117
|
+
return await response.json().catch(() => ({}));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.name === "TypeError" && error.message.includes("fetch")) {
|
|
120
|
+
throw new Error("Network connection failed. Please check your internet connection.");
|
|
121
|
+
}
|
|
122
|
+
if (error.name === "AbortError") {
|
|
123
|
+
throw new Error("Request was cancelled.");
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
};
|
|
@@ -211,7 +227,8 @@ var _Authentication = class _Authentication {
|
|
|
211
227
|
method: "POST" /* POST */,
|
|
212
228
|
endpoint: this.client.getBaseUrl() + "/auth/rules/verify",
|
|
213
229
|
headers: {
|
|
214
|
-
authorization: this.client.getConfig().token
|
|
230
|
+
authorization: this.client.getConfig().token,
|
|
231
|
+
project: this.client.getConfig().project
|
|
215
232
|
},
|
|
216
233
|
body: {
|
|
217
234
|
path,
|
|
@@ -553,8 +570,8 @@ var Array2 = class {
|
|
|
553
570
|
try {
|
|
554
571
|
const res = await new HttpsRequest({
|
|
555
572
|
method: "POST" /* POST */,
|
|
556
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
557
|
-
headers: { authorization: this.app.getConfig().token },
|
|
573
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/show`,
|
|
574
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
558
575
|
body: {
|
|
559
576
|
collection: this.collection,
|
|
560
577
|
document: this.docID,
|
|
@@ -606,15 +623,15 @@ var Array2 = class {
|
|
|
606
623
|
this.collection,
|
|
607
624
|
this.docID,
|
|
608
625
|
DocumentSnapshot.fromMap({ ...doc.data, [this.key]: updatedArray }),
|
|
609
|
-
"
|
|
626
|
+
"update"
|
|
610
627
|
);
|
|
611
628
|
}
|
|
612
629
|
return ArraySnapshot.fromMap(payload);
|
|
613
630
|
}
|
|
614
631
|
const res = await new HttpsRequest({
|
|
615
632
|
method: "POST" /* POST */,
|
|
616
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
617
|
-
headers: { authorization: this.app.getConfig().token },
|
|
633
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/push`,
|
|
634
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
618
635
|
body: {
|
|
619
636
|
collection: this.collection,
|
|
620
637
|
document: this.docID,
|
|
@@ -640,8 +657,8 @@ var Array2 = class {
|
|
|
640
657
|
}
|
|
641
658
|
const res = await new HttpsRequest({
|
|
642
659
|
method: "POST" /* POST */,
|
|
643
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
644
|
-
headers: { authorization: this.app.getConfig().token },
|
|
660
|
+
endpoint: `${this.app.getBaseUrl()}/db/array/update`,
|
|
661
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
645
662
|
body: {
|
|
646
663
|
collection: this.collection,
|
|
647
664
|
document: this.docID,
|
|
@@ -665,7 +682,47 @@ var Array2 = class {
|
|
|
665
682
|
}
|
|
666
683
|
};
|
|
667
684
|
|
|
685
|
+
// src/utils/documentNomalizer.ts
|
|
686
|
+
function normalizePayload(payload) {
|
|
687
|
+
const raw = payload?.document ?? payload?.data ?? payload;
|
|
688
|
+
if (!raw)
|
|
689
|
+
return null;
|
|
690
|
+
const doc = { ...raw };
|
|
691
|
+
doc.id = payload.id ?? payload._id;
|
|
692
|
+
delete doc._id;
|
|
693
|
+
return {
|
|
694
|
+
change: payload?.change ?? raw?.change ?? null,
|
|
695
|
+
data: doc
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
668
699
|
// src/database/DocumentRef.ts
|
|
700
|
+
function validateDocumentData(data, operation) {
|
|
701
|
+
if (data === null || data === void 0) {
|
|
702
|
+
throw new Error(`${operation}: data cannot be null or undefined`);
|
|
703
|
+
}
|
|
704
|
+
if (typeof data !== "object") {
|
|
705
|
+
throw new Error(`${operation}: data must be an object`);
|
|
706
|
+
}
|
|
707
|
+
if (data instanceof Array2) {
|
|
708
|
+
throw new Error(`${operation}: data cannot be an array`);
|
|
709
|
+
}
|
|
710
|
+
const reservedFields = ["id", "_id", "_createdAt", "_updatedAt", "_deleted"];
|
|
711
|
+
for (const field of reservedFields) {
|
|
712
|
+
if (field in data) {
|
|
713
|
+
throw new Error(`${operation}: '${field}' is a reserved field and cannot be set manually`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const dataSize = JSON.stringify(data).length;
|
|
717
|
+
if (dataSize > 1024 * 1024) {
|
|
718
|
+
throw new Error(`${operation}: document size (${Math.round(dataSize / 1024)}KB) exceeds maximum allowed size (1MB)`);
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
JSON.stringify(data);
|
|
722
|
+
} catch {
|
|
723
|
+
throw new Error(`${operation}: data contains circular references`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
669
726
|
var DocumentRef = class {
|
|
670
727
|
constructor(app, collection, id) {
|
|
671
728
|
this.app = app;
|
|
@@ -677,50 +734,24 @@ var DocumentRef = class {
|
|
|
677
734
|
}
|
|
678
735
|
async get() {
|
|
679
736
|
if (this.persistence) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
685
|
-
this.refreshFromRemote().catch(() => {
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
return null;
|
|
689
|
-
}
|
|
690
|
-
async refreshFromRemote() {
|
|
691
|
-
try {
|
|
692
|
-
const res = await new HttpsRequest({
|
|
693
|
-
method: "POST" /* POST */,
|
|
694
|
-
endpoint: `${this.app.getBaseUrl()}/app/read`,
|
|
695
|
-
headers: { authorization: this.app.getConfig().token },
|
|
696
|
-
body: {
|
|
697
|
-
collection: this.collection,
|
|
698
|
-
document: this.id
|
|
699
|
-
}
|
|
700
|
-
}).sendRequest();
|
|
701
|
-
if (!res?.success || !res.document)
|
|
737
|
+
try {
|
|
738
|
+
const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
|
|
739
|
+
if (localSnap)
|
|
740
|
+
return localSnap;
|
|
702
741
|
return null;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
id: res.document.id ?? res.document._id ?? this.id
|
|
706
|
-
};
|
|
707
|
-
delete doc._id;
|
|
708
|
-
if (this.persistence) {
|
|
709
|
-
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.error("[EdmaxLabs] Error reading from cache:", error);
|
|
710
744
|
}
|
|
711
|
-
const snap = DocumentSnapshot.fromMap(doc);
|
|
712
|
-
this.localStore?.emitDocument(this.collection, this.id, snap, "remote_update");
|
|
713
|
-
return snap;
|
|
714
|
-
} catch {
|
|
715
|
-
return null;
|
|
716
745
|
}
|
|
746
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).get();
|
|
717
747
|
}
|
|
718
748
|
async set(data) {
|
|
749
|
+
validateDocumentData(data, "DocumentRef.set");
|
|
719
750
|
if (!this.persistence) {
|
|
720
751
|
const res = await new HttpsRequest({
|
|
721
752
|
method: "POST" /* POST */,
|
|
722
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
723
|
-
headers: { authorization: this.app.getConfig().token },
|
|
753
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
754
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
724
755
|
body: { collection: this.collection, data: { ...data, id: this.id } }
|
|
725
756
|
}).sendRequest();
|
|
726
757
|
return res?.success ? DocumentSnapshot.fromMap(data) : null;
|
|
@@ -739,78 +770,85 @@ var DocumentRef = class {
|
|
|
739
770
|
mutationId: this.persistence.createMutationId(),
|
|
740
771
|
collection: this.collection,
|
|
741
772
|
documentId: this.id,
|
|
742
|
-
type: "
|
|
773
|
+
type: "insert",
|
|
743
774
|
// or "create" if you want to distinguish truly new docs
|
|
744
775
|
payload: data
|
|
745
776
|
});
|
|
746
777
|
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
747
|
-
this.localStore?.emitDocument(this.collection, this.id, snap, "
|
|
778
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "update");
|
|
748
779
|
const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
|
|
749
|
-
this.localStore?.emitCollection(this.collection, currentCollection, "
|
|
780
|
+
this.localStore?.emitCollection(this.collection, currentCollection, "update", this.id);
|
|
750
781
|
this.syncEngine?.flush().catch(console.error);
|
|
751
782
|
return snap;
|
|
752
783
|
}
|
|
753
784
|
async update(data) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
785
|
+
validateDocumentData(data, "DocumentRef.update");
|
|
786
|
+
if (this.persistence) {
|
|
787
|
+
const old = await this.persistence.getDoc(this.collection, this.id);
|
|
788
|
+
if (!old || old.deleted)
|
|
789
|
+
return null;
|
|
790
|
+
const mergedData = { ...old.data, ...data, id: this.id };
|
|
791
|
+
const updated = await this.persistence.upsertDoc({
|
|
792
|
+
collection: this.collection,
|
|
793
|
+
id: this.id,
|
|
794
|
+
data: mergedData,
|
|
795
|
+
exists: true,
|
|
796
|
+
deleted: false,
|
|
797
|
+
pending: 1,
|
|
798
|
+
localOnly: old.localOnly,
|
|
799
|
+
status: "pending",
|
|
800
|
+
revision: old.revision,
|
|
801
|
+
lastSyncedAt: old.lastSyncedAt
|
|
802
|
+
});
|
|
803
|
+
await this.persistence.enqueueMutation({
|
|
804
|
+
mutationId: this.persistence.createMutationId(),
|
|
805
|
+
collection: this.collection,
|
|
806
|
+
documentId: this.id,
|
|
807
|
+
type: "update",
|
|
808
|
+
payload: data,
|
|
809
|
+
baseRevision: old.revision
|
|
810
|
+
});
|
|
811
|
+
const snap = DocumentSnapshot.fromMap(updated.data);
|
|
812
|
+
this.localStore?.emitDocument(this.collection, this.id, snap, "update");
|
|
813
|
+
this.localStore?.notifyCollectionChanged(this.collection, this.id, "update");
|
|
814
|
+
this.syncEngine?.flush().catch(console.error);
|
|
815
|
+
return snap;
|
|
816
|
+
}
|
|
817
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).update(data);
|
|
786
818
|
}
|
|
787
819
|
async delete() {
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
array(key) {
|
|
805
|
-
return new Array2(this.app, this.collection, key, this.id);
|
|
820
|
+
if (this.persistence) {
|
|
821
|
+
await this.persistence.markDeleted(this.collection, this.id, 1);
|
|
822
|
+
await this.persistence.enqueueMutation({
|
|
823
|
+
mutationId: this.persistence.createMutationId(),
|
|
824
|
+
collection: this.collection,
|
|
825
|
+
documentId: this.id,
|
|
826
|
+
type: "delete",
|
|
827
|
+
payload: null
|
|
828
|
+
});
|
|
829
|
+
this.localStore?.emitDocument(this.collection, this.id, null, "empty");
|
|
830
|
+
this.localStore?.notifyCollectionChanged(this.collection, this.id, "delete");
|
|
831
|
+
this.syncEngine?.flush().catch(console.error);
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
return await this.app.getDatabase.collection(this.collection).doc(this.id).delete();
|
|
806
835
|
}
|
|
836
|
+
// array(key: string): Array {
|
|
837
|
+
// return new Array(this.app, this.collection, key, this.id);
|
|
838
|
+
// }
|
|
807
839
|
onSnapshot(callback) {
|
|
808
|
-
this.app.offline().
|
|
809
|
-
|
|
810
|
-
this.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
840
|
+
if (this.app.offline().persistence && this.app.offline().localStore && this.app.offline().realtimeBridge) {
|
|
841
|
+
this.app.offline().realtimeBridge?.watchDocument(this.collection, this.id);
|
|
842
|
+
return this.app.offline().localStore?.subscribeToDocument(
|
|
843
|
+
this.collection,
|
|
844
|
+
this.id,
|
|
845
|
+
callback
|
|
846
|
+
) || (() => {
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return this.app.rtdb().subscribeToDocumentRaw(this.collection, this.id, (snapshot, change) => {
|
|
850
|
+
const res = normalizePayload(snapshot);
|
|
851
|
+
callback(DocumentSnapshot.fromMap(res?.data), change);
|
|
814
852
|
});
|
|
815
853
|
}
|
|
816
854
|
};
|
|
@@ -821,6 +859,7 @@ var Query = class {
|
|
|
821
859
|
this.filter = [];
|
|
822
860
|
this.app = app;
|
|
823
861
|
this.collection = collection;
|
|
862
|
+
this.localStore = app.offline().localStore;
|
|
824
863
|
}
|
|
825
864
|
where(expression) {
|
|
826
865
|
this.filter.push(expression);
|
|
@@ -864,8 +903,59 @@ var Query = class {
|
|
|
864
903
|
});
|
|
865
904
|
return mongoFilter;
|
|
866
905
|
}
|
|
906
|
+
matchesFilter(document2, { key, op, value }) {
|
|
907
|
+
const fieldValue = document2[key];
|
|
908
|
+
switch (op) {
|
|
909
|
+
case "==":
|
|
910
|
+
case "===":
|
|
911
|
+
return fieldValue === value;
|
|
912
|
+
case "!=":
|
|
913
|
+
return fieldValue !== value;
|
|
914
|
+
case "<":
|
|
915
|
+
return fieldValue < value;
|
|
916
|
+
case "<=":
|
|
917
|
+
return fieldValue <= value;
|
|
918
|
+
case ">":
|
|
919
|
+
return fieldValue > value;
|
|
920
|
+
case ">=":
|
|
921
|
+
return fieldValue >= value;
|
|
922
|
+
case "in":
|
|
923
|
+
return Array.isArray(value) && value.includes(fieldValue);
|
|
924
|
+
case "nin":
|
|
925
|
+
return Array.isArray(value) && !value.includes(fieldValue);
|
|
926
|
+
case "contains":
|
|
927
|
+
if (fieldValue == null)
|
|
928
|
+
return false;
|
|
929
|
+
return String(fieldValue).toLowerCase().includes(String(value).toLowerCase());
|
|
930
|
+
default:
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
filterDocuments(docs) {
|
|
935
|
+
if (this.filter.length === 0)
|
|
936
|
+
return docs;
|
|
937
|
+
return docs.filter(
|
|
938
|
+
(snapshot) => this.filter.every(
|
|
939
|
+
(expression) => this.matchesFilter(snapshot.data, expression)
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
async getLocalFilteredSnapshots() {
|
|
944
|
+
const persistence = this.app.offline().persistence;
|
|
945
|
+
if (!persistence)
|
|
946
|
+
return [];
|
|
947
|
+
const docs = await persistence.getCollectionSnapshots(this.collection);
|
|
948
|
+
return this.filterDocuments(docs);
|
|
949
|
+
}
|
|
867
950
|
async get() {
|
|
868
951
|
const persistence = this.app.offline().persistence;
|
|
952
|
+
if (this.filter.length > 0 && persistence) {
|
|
953
|
+
const local = await this.getLocalFilteredSnapshots();
|
|
954
|
+
if (typeof navigator === "undefined" || !navigator.onLine) {
|
|
955
|
+
return local;
|
|
956
|
+
}
|
|
957
|
+
return this.refreshFromRemote();
|
|
958
|
+
}
|
|
869
959
|
if (persistence) {
|
|
870
960
|
const local = await persistence.getCollectionSnapshots(this.collection);
|
|
871
961
|
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
@@ -881,8 +971,8 @@ var Query = class {
|
|
|
881
971
|
const genfilter = this.buildFilter();
|
|
882
972
|
const res = await new HttpsRequest({
|
|
883
973
|
method: "POST" /* POST */,
|
|
884
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
885
|
-
headers: { authorization: this.app.getConfig().token },
|
|
974
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
975
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
886
976
|
body: {
|
|
887
977
|
collection: this.collection,
|
|
888
978
|
filter: genfilter
|
|
@@ -906,8 +996,8 @@ var Query = class {
|
|
|
906
996
|
const genfilter = this.buildFilter();
|
|
907
997
|
const res = await new HttpsRequest({
|
|
908
998
|
method: "POST" /* POST */,
|
|
909
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
910
|
-
headers: { authorization: this.app.getConfig().token },
|
|
999
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
1000
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
911
1001
|
body: {
|
|
912
1002
|
collection: this.collection,
|
|
913
1003
|
filter: genfilter,
|
|
@@ -918,17 +1008,42 @@ var Query = class {
|
|
|
918
1008
|
}
|
|
919
1009
|
onSnapshot(callback) {
|
|
920
1010
|
const genfilter = this.buildFilter();
|
|
921
|
-
this.app.
|
|
922
|
-
|
|
923
|
-
(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1011
|
+
const persistence = this.app.offline().persistence;
|
|
1012
|
+
if (persistence) {
|
|
1013
|
+
const remoteUnsub = this.app.offline().realtimeBridge?.watchCollection(this.collection, genfilter);
|
|
1014
|
+
if (this.filter.length > 0) {
|
|
1015
|
+
const emitFiltered = async (change, changedDocId) => {
|
|
1016
|
+
const docs = await this.getLocalFilteredSnapshots();
|
|
1017
|
+
callback(docs, change, changedDocId);
|
|
1018
|
+
};
|
|
1019
|
+
emitFiltered("insert").catch(console.error);
|
|
1020
|
+
const localUnsub2 = this.localStore?.subscribeToCollection(
|
|
1021
|
+
this.collection,
|
|
1022
|
+
async (snapshots, change, changedDocId) => {
|
|
1023
|
+
if (snapshots.length > 0) {
|
|
1024
|
+
callback(this.filterDocuments(snapshots), change, changedDocId);
|
|
1025
|
+
} else {
|
|
1026
|
+
await emitFiltered(change, changedDocId);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
) ?? (() => {
|
|
1030
|
+
});
|
|
1031
|
+
return () => {
|
|
1032
|
+
localUnsub2();
|
|
1033
|
+
remoteUnsub?.();
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const localUnsub = this.localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
|
|
1037
|
+
});
|
|
1038
|
+
return () => {
|
|
1039
|
+
localUnsub();
|
|
1040
|
+
remoteUnsub?.();
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
1044
|
+
const res = normalizePayload(payload);
|
|
1045
|
+
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
1046
|
+
}, genfilter);
|
|
932
1047
|
}
|
|
933
1048
|
};
|
|
934
1049
|
|
|
@@ -948,20 +1063,22 @@ var CollectionRef = class {
|
|
|
948
1063
|
return new Query(this.app, this.collection);
|
|
949
1064
|
}
|
|
950
1065
|
async get() {
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
1066
|
+
if (this.persistence) {
|
|
1067
|
+
const local = await this.persistence.getCollectionSnapshots(this.collection);
|
|
1068
|
+
if (typeof navigator === "undefined" || navigator.onLine) {
|
|
1069
|
+
this.refreshFromRemote().catch(() => {
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
return local;
|
|
957
1073
|
}
|
|
958
|
-
|
|
1074
|
+
;
|
|
1075
|
+
return await this.app.getDatabase.collection(this.collection).get();
|
|
959
1076
|
}
|
|
960
1077
|
async refreshFromRemote() {
|
|
961
1078
|
try {
|
|
962
1079
|
const res = await new HttpsRequest({
|
|
963
1080
|
method: "POST" /* POST */,
|
|
964
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
1081
|
+
endpoint: `${this.app.getBaseUrl()}/db/read`,
|
|
965
1082
|
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
966
1083
|
body: {
|
|
967
1084
|
collection: this.collection,
|
|
@@ -975,52 +1092,61 @@ var CollectionRef = class {
|
|
|
975
1092
|
const doc = { ...raw };
|
|
976
1093
|
doc.id = doc.id ?? doc._id;
|
|
977
1094
|
delete doc._id;
|
|
978
|
-
delete doc.token;
|
|
979
1095
|
if (this.persistence) {
|
|
980
1096
|
await this.persistence.applyRemoteDoc(this.collection, doc);
|
|
981
1097
|
}
|
|
982
1098
|
}
|
|
983
1099
|
if (this.persistence) {
|
|
984
|
-
|
|
985
|
-
this.localStore?.emitCollection(this.collection, updatedCollection, "remote_update");
|
|
986
|
-
return updatedCollection;
|
|
1100
|
+
return await this.persistence.getCollectionSnapshots(this.collection);
|
|
987
1101
|
}
|
|
988
|
-
return
|
|
1102
|
+
return res.documents.map((d) => {
|
|
1103
|
+
delete d.token;
|
|
1104
|
+
d.id = d.id ?? d._id;
|
|
1105
|
+
delete d._id;
|
|
1106
|
+
return DocumentSnapshot.fromMap(d);
|
|
1107
|
+
});
|
|
989
1108
|
} catch {
|
|
990
1109
|
return [];
|
|
991
1110
|
}
|
|
992
1111
|
}
|
|
993
1112
|
async add(data) {
|
|
994
|
-
if (
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1113
|
+
if (this.persistence) {
|
|
1114
|
+
const localId = this.persistence.createLocalId();
|
|
1115
|
+
const docRecord = await this.persistence.upsertDoc({
|
|
1116
|
+
collection: this.collection,
|
|
1117
|
+
id: localId,
|
|
1118
|
+
data: { ...data, id: localId },
|
|
1119
|
+
exists: true,
|
|
1120
|
+
deleted: false,
|
|
1121
|
+
pending: 1,
|
|
1122
|
+
localOnly: true,
|
|
1123
|
+
status: "pending"
|
|
1124
|
+
});
|
|
1125
|
+
await this.persistence.enqueueMutation({
|
|
1126
|
+
mutationId: this.persistence.createMutationId(),
|
|
1127
|
+
collection: this.collection,
|
|
1128
|
+
documentId: localId,
|
|
1129
|
+
type: "insert",
|
|
1130
|
+
payload: docRecord.data
|
|
1131
|
+
});
|
|
1132
|
+
const snap = DocumentSnapshot.fromMap(docRecord.data);
|
|
1133
|
+
this.localStore?.emitDocument(this.collection, localId, snap, "insert");
|
|
1134
|
+
this.localStore?.notifyCollectionChanged(this.collection, localId, "insert");
|
|
1135
|
+
this.syncEngine?.flush().catch(console.error);
|
|
1136
|
+
return snap;
|
|
1137
|
+
}
|
|
1138
|
+
;
|
|
1139
|
+
return await this.app.getDatabase.collection(this.collection).add(data);
|
|
1020
1140
|
}
|
|
1021
1141
|
onSnapshot(callback) {
|
|
1022
|
-
this.app.offline().
|
|
1023
|
-
|
|
1142
|
+
if (this.app.offline().persistence) {
|
|
1143
|
+
this.app.offline().realtimeBridge?.watchCollection(this.collection);
|
|
1144
|
+
return this.app.offline().localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
return this.app.rtdb().subscribeToCollectionRaw(this.collection, (payload, changes) => {
|
|
1148
|
+
const res = normalizePayload(payload);
|
|
1149
|
+
callback([DocumentSnapshot.fromMap(res?.data)], changes);
|
|
1024
1150
|
});
|
|
1025
1151
|
}
|
|
1026
1152
|
};
|
|
@@ -1040,6 +1166,15 @@ var Batch = class {
|
|
|
1040
1166
|
});
|
|
1041
1167
|
return this;
|
|
1042
1168
|
}
|
|
1169
|
+
create(docRef, data) {
|
|
1170
|
+
this.ops.push({
|
|
1171
|
+
op: "create",
|
|
1172
|
+
collection: docRef.collection,
|
|
1173
|
+
id: docRef.id,
|
|
1174
|
+
data
|
|
1175
|
+
});
|
|
1176
|
+
return this;
|
|
1177
|
+
}
|
|
1043
1178
|
update(docRef, data) {
|
|
1044
1179
|
this.ops.push({
|
|
1045
1180
|
op: "update",
|
|
@@ -1065,11 +1200,33 @@ var Batch = class {
|
|
|
1065
1200
|
const results = [];
|
|
1066
1201
|
for (const op of this.ops) {
|
|
1067
1202
|
try {
|
|
1068
|
-
if (op.op === "
|
|
1203
|
+
if (op.op === "update") {
|
|
1069
1204
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1070
|
-
const snap = await docRef.
|
|
1205
|
+
const snap = await docRef.update(op.data);
|
|
1071
1206
|
if (snap)
|
|
1072
1207
|
results.push(snap);
|
|
1208
|
+
} else if (op.op === "create" || op.op === "insert" || op.op === "set") {
|
|
1209
|
+
const upserted = await persistence.upsertDoc({
|
|
1210
|
+
collection: op.collection,
|
|
1211
|
+
id: op.id,
|
|
1212
|
+
data: { ...op.data, id: op.id },
|
|
1213
|
+
exists: true,
|
|
1214
|
+
deleted: false,
|
|
1215
|
+
pending: 1,
|
|
1216
|
+
localOnly: false,
|
|
1217
|
+
status: "pending"
|
|
1218
|
+
});
|
|
1219
|
+
await persistence.enqueueMutation({
|
|
1220
|
+
mutationId: persistence.createMutationId(),
|
|
1221
|
+
collection: op.collection,
|
|
1222
|
+
documentId: op.id,
|
|
1223
|
+
type: "insert",
|
|
1224
|
+
payload: op.data
|
|
1225
|
+
});
|
|
1226
|
+
const snap = DocumentSnapshot.fromMap(upserted.data);
|
|
1227
|
+
localStore.emitDocument(op.collection, op.id, snap, "insert");
|
|
1228
|
+
localStore.notifyCollectionChanged(op.collection, op.id, "insert");
|
|
1229
|
+
results.push(snap);
|
|
1073
1230
|
} else if (op.op === "delete") {
|
|
1074
1231
|
const docRef = new DocumentRef(this.app, op.collection, op.id);
|
|
1075
1232
|
await docRef.delete();
|
|
@@ -1083,10 +1240,8 @@ var Batch = class {
|
|
|
1083
1240
|
}
|
|
1084
1241
|
const res = await new HttpsRequest({
|
|
1085
1242
|
method: "POST" /* POST */,
|
|
1086
|
-
endpoint: `${this.app.getBaseUrl}/
|
|
1087
|
-
headers: {
|
|
1088
|
-
authorization: this.app.getConfig().token
|
|
1089
|
-
},
|
|
1243
|
+
endpoint: `${this.app.getBaseUrl()}/db/batch`,
|
|
1244
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1090
1245
|
body: { ops: this.ops }
|
|
1091
1246
|
}).sendRequest();
|
|
1092
1247
|
if (res?.error) {
|
|
@@ -1137,10 +1292,8 @@ var Database = class {
|
|
|
1137
1292
|
await transactionFn(tx);
|
|
1138
1293
|
const res = await new HttpsRequest({
|
|
1139
1294
|
method: "POST" /* POST */,
|
|
1140
|
-
endpoint: `${this.app.getBaseUrl}/
|
|
1141
|
-
headers: {
|
|
1142
|
-
authorization: this.app.getConfig().token
|
|
1143
|
-
},
|
|
1295
|
+
endpoint: `${this.app.getBaseUrl()}/db/transaction`,
|
|
1296
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1144
1297
|
body: { ops: tx.ops }
|
|
1145
1298
|
}).sendRequest();
|
|
1146
1299
|
if (res?.error) {
|
|
@@ -1251,10 +1404,10 @@ var Realtime = class {
|
|
|
1251
1404
|
this.events.forEach((event) => {
|
|
1252
1405
|
const channel = `${lid}-${event}`;
|
|
1253
1406
|
const fn = (payload) => {
|
|
1254
|
-
const normalized =
|
|
1407
|
+
const normalized = normalizePayload(payload);
|
|
1255
1408
|
if (!normalized)
|
|
1256
1409
|
return;
|
|
1257
|
-
callback(normalized.data,
|
|
1410
|
+
callback(normalized.data, event);
|
|
1258
1411
|
};
|
|
1259
1412
|
this.socket.on(channel, fn);
|
|
1260
1413
|
handlers.push({ event: channel, fn });
|
|
@@ -1274,7 +1427,7 @@ var Realtime = class {
|
|
|
1274
1427
|
this.events.forEach((event) => {
|
|
1275
1428
|
const channel = `${lid}-${event}`;
|
|
1276
1429
|
const fn = (payload) => {
|
|
1277
|
-
const normalized =
|
|
1430
|
+
const normalized = normalizePayload(payload);
|
|
1278
1431
|
if (!normalized) {
|
|
1279
1432
|
callback({ id }, "delete");
|
|
1280
1433
|
return;
|
|
@@ -1288,18 +1441,6 @@ var Realtime = class {
|
|
|
1288
1441
|
return () => this.cleanupSubscription(lid);
|
|
1289
1442
|
}
|
|
1290
1443
|
// ===================== PRIVATE HELPERS =====================
|
|
1291
|
-
normalizePayload(payload) {
|
|
1292
|
-
const raw = payload?.document ?? payload?.data ?? payload;
|
|
1293
|
-
if (!raw)
|
|
1294
|
-
return null;
|
|
1295
|
-
const doc = { ...raw };
|
|
1296
|
-
doc.id = doc.id ?? doc._id;
|
|
1297
|
-
delete doc._id;
|
|
1298
|
-
return {
|
|
1299
|
-
change: payload?.change ?? raw?.change ?? null,
|
|
1300
|
-
data: doc
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
1444
|
registerSubscription(lid, collection, filter, handlers) {
|
|
1304
1445
|
this.subscriptions.set(lid, { lid, collection, filter, handlers });
|
|
1305
1446
|
}
|
|
@@ -1344,10 +1485,8 @@ var Functions = class {
|
|
|
1344
1485
|
try {
|
|
1345
1486
|
const res = await new HttpsRequest({
|
|
1346
1487
|
method: "POST" /* POST */,
|
|
1347
|
-
endpoint: this.app.getBaseUrl + "/functions/call/" + functionName,
|
|
1348
|
-
headers: {
|
|
1349
|
-
authorization: this.app.getConfig().token
|
|
1350
|
-
},
|
|
1488
|
+
endpoint: this.app.getBaseUrl() + "/functions/call/" + functionName,
|
|
1489
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1351
1490
|
body: data
|
|
1352
1491
|
}).sendRequest();
|
|
1353
1492
|
return res;
|
|
@@ -1369,10 +1508,8 @@ var Hosting = class {
|
|
|
1369
1508
|
try {
|
|
1370
1509
|
const res = await new HttpsRequest({
|
|
1371
1510
|
method: "POST" /* POST */,
|
|
1372
|
-
endpoint: this.app.getBaseUrl + "/hosting/register",
|
|
1373
|
-
headers: {
|
|
1374
|
-
authorization: this.app.getConfig().token
|
|
1375
|
-
},
|
|
1511
|
+
endpoint: this.app.getBaseUrl() + "/hosting/register",
|
|
1512
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
1376
1513
|
body: {
|
|
1377
1514
|
hostname: name,
|
|
1378
1515
|
project: this.app.getConfig().project
|
|
@@ -1453,6 +1590,20 @@ var LocalStore = class {
|
|
|
1453
1590
|
}
|
|
1454
1591
|
});
|
|
1455
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* OPTIMIZED: Notify collection listeners that something changed, without fetching full collection.
|
|
1595
|
+
* Listeners can call getCollectionSnapshots() themselves if they need the full list.
|
|
1596
|
+
* This avoids expensive collection queries after every single mutation.
|
|
1597
|
+
*/
|
|
1598
|
+
notifyCollectionChanged(collection, changedDocId, change) {
|
|
1599
|
+
this.collectionListeners.get(collection)?.forEach((cb) => {
|
|
1600
|
+
try {
|
|
1601
|
+
cb([], change, changedDocId);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
console.error(`[EdmaxLabs] Error in collection listener for ${collection}:`, err);
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1456
1607
|
// ===================== UTILITY =====================
|
|
1457
1608
|
/**
|
|
1458
1609
|
* Clear all listeners (useful for testing or when persistence is disabled)
|
|
@@ -1471,6 +1622,19 @@ var LocalStore = class {
|
|
|
1471
1622
|
this.collectionListeners.forEach((set) => collCount += set.size);
|
|
1472
1623
|
return { documents: docCount, collections: collCount };
|
|
1473
1624
|
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Remove all listeners for a specific collection (useful for cleanup)
|
|
1627
|
+
*/
|
|
1628
|
+
removeCollectionListeners(collection) {
|
|
1629
|
+
this.collectionListeners.delete(collection);
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Remove all listeners for a specific document (useful for cleanup)
|
|
1633
|
+
*/
|
|
1634
|
+
removeDocumentListeners(collection, id) {
|
|
1635
|
+
const key = this.docKey(collection, id);
|
|
1636
|
+
this.documentListeners.delete(key);
|
|
1637
|
+
}
|
|
1474
1638
|
};
|
|
1475
1639
|
|
|
1476
1640
|
// ../../../../node_modules/idb/build/wrap-idb-value.js
|
|
@@ -1682,8 +1846,9 @@ replaceTraps((oldTraps) => ({
|
|
|
1682
1846
|
|
|
1683
1847
|
// src/persistence/Persistence.ts
|
|
1684
1848
|
var Persistence = class {
|
|
1685
|
-
constructor() {
|
|
1686
|
-
this.
|
|
1849
|
+
constructor(appName = "default") {
|
|
1850
|
+
this.appName = appName;
|
|
1851
|
+
this.dbPromise = openDB(`edmaxlabs_offline_${appName}`, 1, {
|
|
1687
1852
|
upgrade(app, oldVersion) {
|
|
1688
1853
|
if (oldVersion < 1) {
|
|
1689
1854
|
if (!app.objectStoreNames.contains("docs")) {
|
|
@@ -1706,17 +1871,74 @@ var Persistence = class {
|
|
|
1706
1871
|
app.createObjectStore("meta");
|
|
1707
1872
|
}
|
|
1708
1873
|
}
|
|
1874
|
+
},
|
|
1875
|
+
blocked() {
|
|
1876
|
+
console.warn("[EdmaxLabs] IndexedDB blocked - another tab has the database open");
|
|
1877
|
+
},
|
|
1878
|
+
blocking() {
|
|
1879
|
+
console.warn("[EdmaxLabs] IndexedDB blocking - closing to allow upgrade");
|
|
1880
|
+
},
|
|
1881
|
+
terminated() {
|
|
1882
|
+
console.error("[EdmaxLabs] IndexedDB terminated unexpectedly");
|
|
1883
|
+
}
|
|
1884
|
+
}).catch((error) => {
|
|
1885
|
+
if (error.name === "QuotaExceededError") {
|
|
1886
|
+
console.error("[EdmaxLabs] IndexedDB quota exceeded - storage full");
|
|
1887
|
+
throw new Error("Storage quota exceeded. Please clear browser data or free up space.");
|
|
1709
1888
|
}
|
|
1889
|
+
if (error.name === "VersionError") {
|
|
1890
|
+
console.error("[EdmaxLabs] IndexedDB version conflict - clearing and retrying");
|
|
1891
|
+
indexedDB.deleteDatabase(`edmaxlabs_offline_${appName}`);
|
|
1892
|
+
throw error;
|
|
1893
|
+
}
|
|
1894
|
+
throw error;
|
|
1710
1895
|
});
|
|
1711
1896
|
}
|
|
1712
1897
|
docKey(collection, id) {
|
|
1713
|
-
return `${collection}:${id}`;
|
|
1898
|
+
return `${this.appName}:${collection}:${id}`;
|
|
1714
1899
|
}
|
|
1715
1900
|
now() {
|
|
1716
1901
|
return Date.now();
|
|
1717
1902
|
}
|
|
1718
1903
|
async getDb() {
|
|
1719
|
-
|
|
1904
|
+
try {
|
|
1905
|
+
return await this.dbPromise;
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
if (error.name === "QuotaExceededError") {
|
|
1908
|
+
throw new Error("Storage quota exceeded. Please clear browser data or free up space.");
|
|
1909
|
+
}
|
|
1910
|
+
if (error.name === "VersionError") {
|
|
1911
|
+
console.warn("[EdmaxLabs] IndexedDB version error - attempting recovery");
|
|
1912
|
+
indexedDB.deleteDatabase(`edmaxlabs_offline_${this.appName}`);
|
|
1913
|
+
this.dbPromise = openDB(`edmaxlabs_offline_${this.appName}`, 1, {
|
|
1914
|
+
upgrade(app, oldVersion) {
|
|
1915
|
+
if (oldVersion < 1) {
|
|
1916
|
+
if (!app.objectStoreNames.contains("docs")) {
|
|
1917
|
+
const docs = app.createObjectStore("docs", { keyPath: "key" });
|
|
1918
|
+
docs.createIndex("by-collection", "collection");
|
|
1919
|
+
docs.createIndex("by-id", "id");
|
|
1920
|
+
docs.createIndex("by-updatedAt", "updatedAt");
|
|
1921
|
+
docs.createIndex("by-pending", "pending");
|
|
1922
|
+
}
|
|
1923
|
+
if (!app.objectStoreNames.contains("mutations")) {
|
|
1924
|
+
const mutations = app.createObjectStore("mutations", {
|
|
1925
|
+
keyPath: "mutationId"
|
|
1926
|
+
});
|
|
1927
|
+
mutations.createIndex("by-status", "status");
|
|
1928
|
+
mutations.createIndex("by-collection", "collection");
|
|
1929
|
+
mutations.createIndex("by-documentId", "documentId");
|
|
1930
|
+
mutations.createIndex("by-createdAt", "createdAt");
|
|
1931
|
+
}
|
|
1932
|
+
if (!app.objectStoreNames.contains("meta")) {
|
|
1933
|
+
app.createObjectStore("meta");
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
return await this.dbPromise;
|
|
1939
|
+
}
|
|
1940
|
+
throw error;
|
|
1941
|
+
}
|
|
1720
1942
|
}
|
|
1721
1943
|
// ==================== DOCS ====================
|
|
1722
1944
|
async getDoc(collection, id) {
|
|
@@ -1789,6 +2011,25 @@ var Persistence = class {
|
|
|
1789
2011
|
]);
|
|
1790
2012
|
return [...pending, ...failed].sort((a, b) => a.createdAt - b.createdAt);
|
|
1791
2013
|
}
|
|
2014
|
+
async getMutation(mutationId) {
|
|
2015
|
+
const app = await this.getDb();
|
|
2016
|
+
return await app.get("mutations", mutationId) ?? null;
|
|
2017
|
+
}
|
|
2018
|
+
async resetMutation(mutationId) {
|
|
2019
|
+
const app = await this.getDb();
|
|
2020
|
+
const old = await app.get("mutations", mutationId);
|
|
2021
|
+
if (!old)
|
|
2022
|
+
return null;
|
|
2023
|
+
const next = {
|
|
2024
|
+
...old,
|
|
2025
|
+
status: "pending",
|
|
2026
|
+
retryCount: 0,
|
|
2027
|
+
updatedAt: this.now(),
|
|
2028
|
+
error: void 0
|
|
2029
|
+
};
|
|
2030
|
+
await app.put("mutations", next);
|
|
2031
|
+
return next;
|
|
2032
|
+
}
|
|
1792
2033
|
async setMutationStatus(mutationId, status, error) {
|
|
1793
2034
|
const app = await this.getDb();
|
|
1794
2035
|
const old = await app.get("mutations", mutationId);
|
|
@@ -1923,7 +2164,7 @@ var RealtimeBridge = class {
|
|
|
1923
2164
|
if (id)
|
|
1924
2165
|
await this.handleRemoteDelete(collection, id);
|
|
1925
2166
|
} else {
|
|
1926
|
-
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
2167
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1927
2168
|
}
|
|
1928
2169
|
},
|
|
1929
2170
|
filter
|
|
@@ -1948,7 +2189,7 @@ var RealtimeBridge = class {
|
|
|
1948
2189
|
if (change === "delete") {
|
|
1949
2190
|
await this.handleRemoteDelete(collection, id);
|
|
1950
2191
|
} else {
|
|
1951
|
-
await this.handleRemoteCreateOrUpdate(collection, payload);
|
|
2192
|
+
await this.handleRemoteCreateOrUpdate(collection, payload, change);
|
|
1952
2193
|
}
|
|
1953
2194
|
}
|
|
1954
2195
|
);
|
|
@@ -1963,13 +2204,13 @@ var RealtimeBridge = class {
|
|
|
1963
2204
|
async emitCurrentDocument(collection, id) {
|
|
1964
2205
|
const localDoc = await this.persistence.getDoc(collection, id);
|
|
1965
2206
|
const snapshot = localDoc && localDoc.exists && !localDoc.deleted ? DocumentSnapshot.fromMap(localDoc.data) : null;
|
|
1966
|
-
this.store.emitDocument(collection, id, snapshot, "
|
|
2207
|
+
this.store.emitDocument(collection, id, snapshot, "insert");
|
|
1967
2208
|
}
|
|
1968
2209
|
async emitCurrentCollection(collection) {
|
|
1969
2210
|
const localDocs = await this.persistence.getCollectionSnapshots(collection);
|
|
1970
|
-
this.store.emitCollection(collection, localDocs, "
|
|
2211
|
+
this.store.emitCollection(collection, localDocs, "insert");
|
|
1971
2212
|
}
|
|
1972
|
-
async handleRemoteCreateOrUpdate(collection, raw) {
|
|
2213
|
+
async handleRemoteCreateOrUpdate(collection, raw, change) {
|
|
1973
2214
|
const id = raw.id ?? raw._id;
|
|
1974
2215
|
if (!id)
|
|
1975
2216
|
return;
|
|
@@ -1977,15 +2218,13 @@ var RealtimeBridge = class {
|
|
|
1977
2218
|
if (!saved)
|
|
1978
2219
|
return;
|
|
1979
2220
|
const snap = DocumentSnapshot.fromMap(saved.data);
|
|
1980
|
-
this.store.emitDocument(collection, id, snap,
|
|
1981
|
-
|
|
1982
|
-
this.store.emitCollection(collection, currentCollection, "remote_update", id);
|
|
2221
|
+
this.store.emitDocument(collection, id, snap, change);
|
|
2222
|
+
this.store.notifyCollectionChanged(collection, id, change);
|
|
1983
2223
|
}
|
|
1984
2224
|
async handleRemoteDelete(collection, id) {
|
|
1985
2225
|
await this.persistence.applyRemoteDelete(collection, id);
|
|
1986
|
-
this.store.emitDocument(collection, id, null, "
|
|
1987
|
-
|
|
1988
|
-
this.store.emitCollection(collection, currentCollection, "remote_delete", id);
|
|
2226
|
+
this.store.emitDocument(collection, id, null, "delete");
|
|
2227
|
+
this.store.notifyCollectionChanged(collection, id, "delete");
|
|
1989
2228
|
}
|
|
1990
2229
|
// ===================== LIFECYCLE =====================
|
|
1991
2230
|
/**
|
|
@@ -2061,7 +2300,7 @@ var SyncEngine = class {
|
|
|
2061
2300
|
try {
|
|
2062
2301
|
let success = false;
|
|
2063
2302
|
switch (mutation.type) {
|
|
2064
|
-
case "
|
|
2303
|
+
case "insert":
|
|
2065
2304
|
success = await this.syncCreate(mutation);
|
|
2066
2305
|
break;
|
|
2067
2306
|
case "update":
|
|
@@ -2087,7 +2326,7 @@ var SyncEngine = class {
|
|
|
2087
2326
|
await this.persistence.setMutationStatus(
|
|
2088
2327
|
mutation.mutationId,
|
|
2089
2328
|
"failed",
|
|
2090
|
-
error?.message ?? "Unknown
|
|
2329
|
+
error?.message ?? "Unknown insert error"
|
|
2091
2330
|
);
|
|
2092
2331
|
if (nextRetryCount < this.MAX_RETRIES) {
|
|
2093
2332
|
this.scheduleRetry();
|
|
@@ -2104,8 +2343,8 @@ var SyncEngine = class {
|
|
|
2104
2343
|
async syncCreate(mutation) {
|
|
2105
2344
|
const res = await new HttpsRequest({
|
|
2106
2345
|
method: "POST" /* POST */,
|
|
2107
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2108
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2346
|
+
endpoint: `${this.app.getBaseUrl()}/db/create`,
|
|
2347
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2109
2348
|
body: {
|
|
2110
2349
|
collection: mutation.collection,
|
|
2111
2350
|
data: mutation.payload
|
|
@@ -2122,18 +2361,17 @@ var SyncEngine = class {
|
|
|
2122
2361
|
);
|
|
2123
2362
|
if (replaced) {
|
|
2124
2363
|
const snap = DocumentSnapshot.fromMap(replaced.data);
|
|
2125
|
-
this.store.emitDocument(mutation.collection, oldId, snap, "
|
|
2126
|
-
this.store.emitDocument(mutation.collection, newId, snap, "
|
|
2127
|
-
|
|
2128
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", newId);
|
|
2364
|
+
this.store.emitDocument(mutation.collection, oldId, snap, "insert");
|
|
2365
|
+
this.store.emitDocument(mutation.collection, newId, snap, "insert");
|
|
2366
|
+
this.store.notifyCollectionChanged(mutation.collection, newId, "insert");
|
|
2129
2367
|
}
|
|
2130
2368
|
return true;
|
|
2131
2369
|
}
|
|
2132
2370
|
async syncUpdate(mutation) {
|
|
2133
2371
|
const res = await new HttpsRequest({
|
|
2134
2372
|
method: "POST" /* POST */,
|
|
2135
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2136
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2373
|
+
endpoint: `${this.app.getBaseUrl()}/db/update`,
|
|
2374
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2137
2375
|
body: {
|
|
2138
2376
|
collection: mutation.collection,
|
|
2139
2377
|
document: mutation.documentId,
|
|
@@ -2155,19 +2393,17 @@ var SyncEngine = class {
|
|
|
2155
2393
|
localOnly: false,
|
|
2156
2394
|
status: "synced",
|
|
2157
2395
|
lastSyncedAt: this.persistence["now"]?.() ?? Date.now()
|
|
2158
|
-
// fallback
|
|
2159
2396
|
});
|
|
2160
2397
|
const snap = DocumentSnapshot.fromMap(local.data);
|
|
2161
|
-
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "
|
|
2162
|
-
|
|
2163
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2398
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, snap, "update");
|
|
2399
|
+
this.store.notifyCollectionChanged(mutation.collection, mutation.documentId, "update");
|
|
2164
2400
|
return true;
|
|
2165
2401
|
}
|
|
2166
2402
|
async syncDelete(mutation) {
|
|
2167
2403
|
const res = await new HttpsRequest({
|
|
2168
2404
|
method: "POST" /* POST */,
|
|
2169
|
-
endpoint: `${this.app.getBaseUrl()}/
|
|
2170
|
-
headers: { authorization: this.app.getConfig().token },
|
|
2405
|
+
endpoint: `${this.app.getBaseUrl()}/db/delete`,
|
|
2406
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2171
2407
|
body: {
|
|
2172
2408
|
collection: mutation.collection,
|
|
2173
2409
|
document: mutation.documentId
|
|
@@ -2176,9 +2412,8 @@ var SyncEngine = class {
|
|
|
2176
2412
|
if (!res?.success)
|
|
2177
2413
|
return false;
|
|
2178
2414
|
await this.persistence.markDeleted(mutation.collection, mutation.documentId, 0);
|
|
2179
|
-
this.store.emitDocument(mutation.collection, mutation.documentId, null, "
|
|
2180
|
-
|
|
2181
|
-
this.store.emitCollection(mutation.collection, currentCollection, "sync", mutation.documentId);
|
|
2415
|
+
this.store.emitDocument(mutation.collection, mutation.documentId, null, "delete");
|
|
2416
|
+
this.store.notifyCollectionChanged(mutation.collection, mutation.documentId, "delete");
|
|
2182
2417
|
return true;
|
|
2183
2418
|
}
|
|
2184
2419
|
scheduleRetry() {
|
|
@@ -2193,6 +2428,50 @@ var SyncEngine = class {
|
|
|
2193
2428
|
async forceSync() {
|
|
2194
2429
|
return this.flush();
|
|
2195
2430
|
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Get all failed mutations for error handling/UI
|
|
2433
|
+
* Returns mutations that exceeded MAX_RETRIES
|
|
2434
|
+
*/
|
|
2435
|
+
async getFailedMutations() {
|
|
2436
|
+
const all = await this.persistence.getPendingMutations();
|
|
2437
|
+
return all.filter((m) => m.status === "failed");
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Retry a specific failed mutation by resetting its retry count
|
|
2441
|
+
* Useful for user-initiated recovery after fixing network/server issues
|
|
2442
|
+
*/
|
|
2443
|
+
async retryMutation(mutationId) {
|
|
2444
|
+
const reset = await this.persistence.resetMutation(mutationId);
|
|
2445
|
+
if (!reset)
|
|
2446
|
+
return false;
|
|
2447
|
+
this.flush().catch(console.error);
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Retry all failed mutations at once
|
|
2452
|
+
*/
|
|
2453
|
+
async retryAllFailed() {
|
|
2454
|
+
const failed = await this.getFailedMutations();
|
|
2455
|
+
for (const mut of failed) {
|
|
2456
|
+
await this.persistence.setMutationStatus(mut.mutationId, "pending");
|
|
2457
|
+
}
|
|
2458
|
+
if (failed.length > 0) {
|
|
2459
|
+
this.flush().catch(console.error);
|
|
2460
|
+
}
|
|
2461
|
+
return failed.length;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Remove a mutation entirely (user acknowledges the failure and wants to discard it)
|
|
2465
|
+
* Be careful: this means the operation will never sync to the server
|
|
2466
|
+
*/
|
|
2467
|
+
async removeMutation(mutationId) {
|
|
2468
|
+
try {
|
|
2469
|
+
await this.persistence.removeMutation(mutationId);
|
|
2470
|
+
return true;
|
|
2471
|
+
} catch {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2196
2475
|
dispose() {
|
|
2197
2476
|
if (this.retryTimeout) {
|
|
2198
2477
|
clearTimeout(this.retryTimeout);
|
|
@@ -2266,9 +2545,7 @@ var StorageRef = class {
|
|
|
2266
2545
|
const res = await new HttpsRequest({
|
|
2267
2546
|
method: "POST" /* POST */,
|
|
2268
2547
|
endpoint: this.app.getBaseUrl() + "/storage/file/upload",
|
|
2269
|
-
headers: {
|
|
2270
|
-
authorization: this.app.getConfig().token
|
|
2271
|
-
},
|
|
2548
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2272
2549
|
file: srcFile,
|
|
2273
2550
|
isMultipart: true
|
|
2274
2551
|
}).sendRequest();
|
|
@@ -2285,9 +2562,7 @@ var StorageRef = class {
|
|
|
2285
2562
|
const res = await new HttpsRequest({
|
|
2286
2563
|
method: "POST" /* POST */,
|
|
2287
2564
|
endpoint: this.app.getBaseUrl() + "/storage/file/delete",
|
|
2288
|
-
headers: {
|
|
2289
|
-
authorization: this.app.getConfig().token
|
|
2290
|
-
},
|
|
2565
|
+
headers: { authorization: this.app.getConfig().token, project: this.app.getConfig().project },
|
|
2291
2566
|
body: {
|
|
2292
2567
|
file_id: id
|
|
2293
2568
|
}
|
|
@@ -2329,7 +2604,8 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2329
2604
|
this._realtime = new Realtime(this);
|
|
2330
2605
|
this._hosting = new Hosting(this);
|
|
2331
2606
|
if (persistence) {
|
|
2332
|
-
|
|
2607
|
+
const appName = config.app_name || config.project;
|
|
2608
|
+
this.persistence = new Persistence(appName);
|
|
2333
2609
|
this.localStore = new LocalStore();
|
|
2334
2610
|
this.realtimeBridge = new RealtimeBridge(this, this.persistence, this.localStore);
|
|
2335
2611
|
this.syncEngine = new SyncEngine(
|
|
@@ -2368,6 +2644,29 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2368
2644
|
}
|
|
2369
2645
|
return Authentication.instance;
|
|
2370
2646
|
}
|
|
2647
|
+
// ===================== OFFLINE UTILITIES =====================
|
|
2648
|
+
/** Check if offline features are enabled */
|
|
2649
|
+
get isOfflineEnabled() {
|
|
2650
|
+
return !!this.persistence;
|
|
2651
|
+
}
|
|
2652
|
+
/** Get current storage usage (approximate) */
|
|
2653
|
+
async getStorageUsage() {
|
|
2654
|
+
if (!this.persistence)
|
|
2655
|
+
return null;
|
|
2656
|
+
try {
|
|
2657
|
+
const usage = await this.persistence.getStorageUsage();
|
|
2658
|
+
return { used: usage, available: 50 * 1024 * 1024 };
|
|
2659
|
+
} catch {
|
|
2660
|
+
return null;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
/** Clear all cached data (nuclear option) */
|
|
2664
|
+
async clearCache() {
|
|
2665
|
+
if (this.persistence) {
|
|
2666
|
+
await this.persistence.clearAll();
|
|
2667
|
+
}
|
|
2668
|
+
this.localStore?.clearAllListeners();
|
|
2669
|
+
}
|
|
2371
2670
|
/** New clean offline namespace - highly recommended */
|
|
2372
2671
|
offline() {
|
|
2373
2672
|
return {
|
|
@@ -2375,9 +2674,39 @@ var _EdmaxLabs = class _EdmaxLabs {
|
|
|
2375
2674
|
localStore: this.localStore,
|
|
2376
2675
|
syncEngine: this.syncEngine,
|
|
2377
2676
|
realtimeBridge: this.realtimeBridge,
|
|
2378
|
-
enabled: !!this.persistence
|
|
2677
|
+
enabled: !!this.persistence,
|
|
2678
|
+
// Add cleanup utilities
|
|
2679
|
+
clearListeners: () => this.localStore?.clearAllListeners(),
|
|
2680
|
+
getListenerCount: () => this.localStore?.listenerCount || { documents: 0, collections: 0 }
|
|
2379
2681
|
};
|
|
2380
2682
|
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Manually trigger sync of pending mutations
|
|
2685
|
+
* Useful for progressive sync or after network restoration
|
|
2686
|
+
*/
|
|
2687
|
+
async sync() {
|
|
2688
|
+
if (!this.syncEngine) {
|
|
2689
|
+
console.warn("[EdmaxLabs] Sync called but persistence is not enabled");
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
return this.syncEngine.forceSync();
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Get mutations that failed to sync (for error UI)
|
|
2696
|
+
*/
|
|
2697
|
+
async getFailedMutations() {
|
|
2698
|
+
if (!this.syncEngine)
|
|
2699
|
+
return [];
|
|
2700
|
+
return this.syncEngine.getFailedMutations();
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Retry all failed mutations
|
|
2704
|
+
*/
|
|
2705
|
+
async retrySync() {
|
|
2706
|
+
if (!this.syncEngine)
|
|
2707
|
+
return 0;
|
|
2708
|
+
return this.syncEngine.retryAllFailed();
|
|
2709
|
+
}
|
|
2381
2710
|
// Internal access (for internal classes only)
|
|
2382
2711
|
getConfig() {
|
|
2383
2712
|
return { ...this._config };
|