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/dist/index.cjs CHANGED
@@ -84,29 +84,45 @@ var HttpsRequest = class {
84
84
  this.isMultipart = isMultipart;
85
85
  }
86
86
  async sendRequest() {
87
- if (this.isMultipart && this.file) {
88
- const formData = new FormData();
89
- Object.entries(this.body).forEach(([key, value]) => {
90
- formData.append(key, value);
91
- });
92
- formData.append("file", this.file);
93
- const response = await fetch(this.endpoint, {
94
- method: this.method,
95
- headers: this.headers,
96
- // Let browser handle multipart boundaries
97
- body: formData
98
- });
99
- return await response.json();
100
- } else {
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
- ...this.headers,
105
- "Content-Type": "application/json"
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
- return await response.json();
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()}/app/array/show`,
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
- "local_update"
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()}/app/array/push`,
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()}/app/array/update`,
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
- const localSnap = await this.persistence.getDocSnapshot(this.collection, this.id);
681
- if (localSnap)
682
- return localSnap;
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
- const doc = {
704
- ...res.document,
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()}/app/create`,
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: "update",
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, "local_update");
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, "local_update", this.id);
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
- if (!this.persistence)
755
- return null;
756
- const old = await this.persistence.getDoc(this.collection, this.id);
757
- if (!old || old.deleted)
758
- return null;
759
- const mergedData = { ...old.data, ...data, id: this.id };
760
- const updated = await this.persistence.upsertDoc({
761
- collection: this.collection,
762
- id: this.id,
763
- data: mergedData,
764
- exists: true,
765
- deleted: false,
766
- pending: 1,
767
- localOnly: old.localOnly,
768
- status: "pending",
769
- revision: old.revision,
770
- lastSyncedAt: old.lastSyncedAt
771
- });
772
- await this.persistence.enqueueMutation({
773
- mutationId: this.persistence.createMutationId(),
774
- collection: this.collection,
775
- documentId: this.id,
776
- type: "update",
777
- payload: data,
778
- baseRevision: old.revision
779
- });
780
- const snap = DocumentSnapshot.fromMap(updated.data);
781
- this.localStore?.emitDocument(this.collection, this.id, snap, "local_update");
782
- const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
783
- this.localStore?.emitCollection(this.collection, currentCollection, "local_update", this.id);
784
- this.syncEngine?.flush().catch(console.error);
785
- return snap;
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 (!this.persistence)
789
- return false;
790
- await this.persistence.markDeleted(this.collection, this.id, 1);
791
- await this.persistence.enqueueMutation({
792
- mutationId: this.persistence.createMutationId(),
793
- collection: this.collection,
794
- documentId: this.id,
795
- type: "delete",
796
- payload: null
797
- });
798
- this.localStore?.emitDocument(this.collection, this.id, null, "local_delete");
799
- const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
800
- this.localStore?.emitCollection(this.collection, currentCollection, "local_delete", this.id);
801
- this.syncEngine?.flush().catch(console.error);
802
- return true;
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().realtimeBridge?.watchDocument(this.collection, this.id);
809
- return this.localStore?.subscribeToDocument(
810
- this.collection,
811
- this.id,
812
- callback
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()}/app/read`,
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()}/app/update`,
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.rtdb().subscribeToCollectionRaw?.(
922
- this.collection,
923
- (payload, change) => {
924
- },
925
- genfilter
926
- );
927
- return this.app.offline().localStore?.subscribeToCollection(
928
- this.collection,
929
- callback
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 (!this.persistence)
952
- return [];
953
- const local = await this.persistence.getCollectionSnapshots(this.collection);
954
- if (typeof navigator === "undefined" || navigator.onLine) {
955
- this.refreshFromRemote().catch(() => {
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
- return local;
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()}/app/read`,
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
- const updatedCollection = await this.persistence.getCollectionSnapshots(this.collection);
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 (!this.persistence)
995
- return null;
996
- const localId = this.persistence.createLocalId();
997
- const docRecord = await this.persistence.upsertDoc({
998
- collection: this.collection,
999
- id: localId,
1000
- data: { ...data, id: localId },
1001
- exists: true,
1002
- deleted: false,
1003
- pending: 1,
1004
- localOnly: true,
1005
- status: "pending"
1006
- });
1007
- await this.persistence.enqueueMutation({
1008
- mutationId: this.persistence.createMutationId(),
1009
- collection: this.collection,
1010
- documentId: localId,
1011
- type: "create",
1012
- payload: docRecord.data
1013
- });
1014
- const snap = DocumentSnapshot.fromMap(docRecord.data);
1015
- this.localStore?.emitDocument(this.collection, localId, snap, "local_create");
1016
- const currentCollection = await this.persistence.getCollectionSnapshots(this.collection);
1017
- this.localStore?.emitCollection(this.collection, currentCollection, "local_create", localId);
1018
- this.syncEngine?.flush().catch(console.error);
1019
- return snap;
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().realtimeBridge?.watchCollection(this.collection);
1023
- return this.localStore?.subscribeToCollection(this.collection, callback) ?? (() => {
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 === "set" || op.op === "update") {
1203
+ if (op.op === "update") {
1069
1204
  const docRef = new DocumentRef(this.app, op.collection, op.id);
1070
- const snap = await docRef.set(op.data);
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}/app/batch`,
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}/app/transaction`,
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 = this.normalizePayload(payload);
1407
+ const normalized = normalizePayload(payload);
1255
1408
  if (!normalized)
1256
1409
  return;
1257
- callback(normalized.data, normalized.change ?? event);
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 = this.normalizePayload(payload);
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.dbPromise = openDB("edmaxlabs_offline", 1, {
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
- return this.dbPromise;
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, "init");
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, "init");
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, "remote_update");
1981
- const currentCollection = await this.persistence.getCollectionSnapshots(collection);
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, "remote_delete");
1987
- const currentCollection = await this.persistence.getCollectionSnapshots(collection);
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 "create":
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 sync error"
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()}/app/create`,
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, "sync");
2126
- this.store.emitDocument(mutation.collection, newId, snap, "sync");
2127
- const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
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()}/app/update`,
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, "sync");
2162
- const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
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()}/app/delete`,
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, "sync");
2180
- const currentCollection = await this.persistence.getCollectionSnapshots(mutation.collection);
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
- this.persistence = new Persistence();
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 };