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