nexabase-console 1.1.3 → 1.1.5

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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/index.js +83 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,6 +42,21 @@ const app = initializeApp({
42
42
  const db = getFirestore(app);
43
43
  ```
44
44
 
45
+ #### 🛡️ Keamanan & Autentikasi Ganda (API Key & Sesi)
46
+ Untuk mendukung arsitektur server-side proxy atau direct API key yang sangat fleksibel, SDK secara otomatis mengirimkan token Anda dalam dua format header sekaligus:
47
+ - **`Authorization: Bearer <token>`**: Digunakan untuk validasi token JWT sesi user.
48
+ - **`x-api-key: <api_key>`**: Digunakan untuk otentikasi aman tanpa sesi menggunakan API Key proyek.
49
+
50
+ Hal ini mencegah kegagalan otentikasi (Error `403 Forbidden: Please login`) saat memanggil metode di server menggunakan API Key proyek.
51
+
52
+ #### ⚡ Sinkronisasi Offline & Database-Priority Engine (IndexedDB Cache)
53
+ NexaBase SDK mengimplementasikan manajemen cache tangguh yang serupa dengan Firebase untuk menjamin keandalan aplikasi saat jaringan tidak stabil:
54
+ 1. **Prioritas Database Utama (`network-first` secara default)**: SDK memprioritaskan pengambilan data dari database remote server untuk menjamin kesegaran data (*data freshness*).
55
+ 2. **Fast-Fail Network Timeout**: Setiap permintaan database remote memiliki batas waktu (*timeout*) selama 6 detik. Jika jaringan sangat lambat (*lie-fi*) atau offline, SDK akan gagal cepat dan mengalihkan pembacaan ke **IndexedDB local cache** secara transparan tanpa menghentikan aplikasi.
56
+ 3. **Pembaruan Optimistik Instan**: Mutasi data (`setDoc`, `patchDoc`, `deleteDoc`, `writeBatch`) langsung ditulis ke IndexedDB secara optimistik. Semua listener snapshot (`onSnapshot`) akan menerima perubahan secara instan agar UI terasa sangat responsif (*snappy*).
57
+ 4. **Heartbeat Polling Outbox (Setiap 10 Detik)**: Di samping mendengarkan event transisi jaringan `online`, SDK menjalankan polling latar belakang setiap 10 detik. Jika koneksi terdeteksi pulih, SDK akan mem-flush antrean mutasi offline secara berurutan (FIFO).
58
+ 5. **Deduplikasi & Proteksi Kemacetan Antrean**: SDK menggunakan kunci idempotensi (`X-Idempotency-Key`) untuk mencegah tulisan ganda. Selain itu, jika server membalas dengan kesalahan definitif sisi klien (seperti `400 Bad Request` atau `403 Forbidden`), SDK akan secara otomatis membuang job cacat tersebut dari antrean untuk mencegah antrean macet selamanya.
59
+
45
60
  ---
46
61
 
47
62
  ### 2. Firestore-like Modular API (NoSQL)
@@ -242,6 +257,11 @@ const uploadImage = async (fileBlob) => {
242
257
  };
243
258
  ```
244
259
 
260
+ #### 📂 Penanganan Tipe Konten Dinamis (Form Data Boundary)
261
+ Unggahan file menggunakan format multipart standard yang dibungkus dalam objek `FormData`. SDK ini secara otomatis mendeteksi payload `FormData` dan membebaskan header `Content-Type` bawaan agar browser dapat secara dinamis menetapkan tipe konten yang menyertakan kode batas unik (*multipart boundary*).
262
+
263
+ Ini menghilangkan bug berkas kosong (`{}`) yang disebabkan oleh tumpang tindihnya header `application/json` di Axios.
264
+
245
265
  ---
246
266
 
247
267
  ### 5. Keamanan Tipe TypeScript (Generics) & Kueri Lanjutan
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ class NexaApp {
38
38
  this.projectId = config.projectId;
39
39
  this.token = config.apiKey || null;
40
40
  this.enablePersistence = config.enablePersistence !== false;
41
- this.cacheStrategy = config.cacheStrategy || 'cache-first';
41
+ this.cacheStrategy = config.cacheStrategy || 'network-first';
42
42
  if (typeof window !== 'undefined' && window.localStorage) {
43
43
  const savedToken = localStorage.getItem(`nexa_token_${this.projectId}`);
44
44
  if (savedToken) {
@@ -52,8 +52,10 @@ class NexaApp {
52
52
  }
53
53
  this.endpoint = config.endpoint || defaultEndpoint;
54
54
  // Do not set default Content-Type globally to let Axios auto-detect FormData
55
+ // Include 6000ms timeout for reliable fast failover fallback to local IndexedDB cache when network is slow
55
56
  this.client = axios_1.default.create({
56
- baseURL: this.endpoint
57
+ baseURL: this.endpoint,
58
+ timeout: 6000
57
59
  });
58
60
  // Request interceptor to attach bearer/x-api-key token and boundary fix
59
61
  this.client.interceptors.request.use((req) => {
@@ -388,6 +390,12 @@ class NexaApp {
388
390
  if (typeof window !== 'undefined') {
389
391
  window.addEventListener('online', () => this._syncOfflineQueue());
390
392
  setTimeout(() => this._syncOfflineQueue(), 1000);
393
+ // Robust automatic heartbeat polling every 10 seconds to sync any pending queue jobs
394
+ setInterval(() => {
395
+ if (typeof navigator !== 'undefined' && navigator.onLine) {
396
+ this._syncOfflineQueue();
397
+ }
398
+ }, 10000);
391
399
  }
392
400
  }
393
401
  catch (err) {
@@ -499,7 +507,14 @@ class NexaApp {
499
507
  await this._saveOfflineQueue(currentQueue.filter((q) => q.id !== job.id));
500
508
  }
501
509
  catch (err) {
502
- break; // Stop syncing on network exception
510
+ // Discard invalid/unauthorized client-side failures (4xx) to prevent clogging the queue
511
+ if (err && err.response && err.response.status >= 400 && err.response.status < 500) {
512
+ console.error(`[NexaBase SDK] Discarding invalid offline job (${job.type} on ${job.path}):`, err.response.data);
513
+ const currentQueue = await this._getOfflineQueue();
514
+ await this._saveOfflineQueue(currentQueue.filter((q) => q.id !== job.id));
515
+ continue; // Move on to next job
516
+ }
517
+ break; // Stop syncing on transient network exceptions or server 5xx errors to retry later
503
518
  }
504
519
  }
505
520
  this._isSyncing = false;
@@ -662,6 +677,25 @@ const getDocs = async (queryOrCollection) => {
662
677
  await db._initPromise;
663
678
  const collectionPath = queryOrCollection.path;
664
679
  const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
680
+ // If cache-first strategy, try loading from local cache first
681
+ if (db.cacheStrategy === 'cache-first') {
682
+ const cached = (0, exports.getCachedDocs)(queryOrCollection);
683
+ if (cached && !cached.empty) {
684
+ // Background-sync silently to update the local cache
685
+ db.client.post(`/api/firestore/${db.projectId}/query`, {
686
+ collectionPath,
687
+ constraints
688
+ }).then(async (res) => {
689
+ const docs = res.data.documents || [];
690
+ for (const d of docs) {
691
+ await db._setCache(`${collectionPath}/${d.id}`, d.fields);
692
+ }
693
+ db._notifySnapshotCallbacks(collectionPath, 'bulk_update', null);
694
+ }).catch(() => { });
695
+ return cached;
696
+ }
697
+ }
698
+ // Network-first (or cache-first with empty cache): Prioritize the remote database
665
699
  try {
666
700
  const res = await db.client.post(`/api/firestore/${db.projectId}/query`, {
667
701
  collectionPath,
@@ -689,9 +723,11 @@ const getDocs = async (queryOrCollection) => {
689
723
  };
690
724
  }
691
725
  catch (err) {
692
- if (err && err.response) {
726
+ // If it's a definitive authorization or validation 4xx error, propagate it
727
+ if (err && err.response && err.response.status < 500) {
693
728
  throw err;
694
729
  }
730
+ // Failover: Smoothly fallback to the IndexedDB local cache on network/5xx exceptions
695
731
  return (0, exports.getCachedDocs)(queryOrCollection);
696
732
  }
697
733
  };
@@ -702,6 +738,21 @@ const getDoc = async (docRef) => {
702
738
  await db._initPromise;
703
739
  const segments = docRef.path.split('/');
704
740
  const docId = segments[segments.length - 1];
741
+ // If cache-first strategy, try loading from local cache first
742
+ if (db.cacheStrategy === 'cache-first') {
743
+ const cached = (0, exports.getCachedDoc)(docRef);
744
+ if (cached && cached.exists()) {
745
+ // Background-sync silently to update the local cache
746
+ db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`)
747
+ .then(async (res) => {
748
+ const data = res.data.data;
749
+ await db._setCache(docRef.path, data);
750
+ db._notifySnapshotCallbacks(docRef.path, 'document_written', data);
751
+ }).catch(() => { });
752
+ return cached;
753
+ }
754
+ }
755
+ // Network-first (or cache-first with empty cache): Prioritize the remote database
705
756
  try {
706
757
  const res = await db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`);
707
758
  const data = res.data.data;
@@ -714,9 +765,11 @@ const getDoc = async (docRef) => {
714
765
  };
715
766
  }
716
767
  catch (err) {
717
- if (err && err.response) {
768
+ // If it's a definitive authorization or validation 4xx error, propagate it
769
+ if (err && err.response && err.response.status < 500) {
718
770
  throw err;
719
771
  }
772
+ // Failover: Smoothly fallback to the IndexedDB local cache on network/5xx exceptions
720
773
  return (0, exports.getCachedDoc)(docRef);
721
774
  }
722
775
  };
@@ -827,26 +880,41 @@ const onSnapshot = (ref, callback) => {
827
880
  callback(cached);
828
881
  }
829
882
  }
883
+ let isInitial = true;
830
884
  let debounceTimer = null;
831
885
  const triggerCallback = () => {
832
886
  if (debounceTimer)
833
887
  clearTimeout(debounceTimer);
834
888
  debounceTimer = setTimeout(async () => {
835
889
  if (ref.type === 'collection' || ref.type === 'query') {
836
- callback((0, exports.getCachedDocs)(ref));
837
- try {
838
- const docs = await (0, exports.getDocs)(ref);
839
- callback(docs);
890
+ if (isInitial) {
891
+ isInitial = false;
892
+ callback((0, exports.getCachedDocs)(ref));
893
+ try {
894
+ const docs = await (0, exports.getDocs)(ref);
895
+ callback(docs);
896
+ }
897
+ catch (e) { }
898
+ }
899
+ else {
900
+ // Real-time updates: emit instantly and synchronously from cache.
901
+ // Eliminates redundant HTTP requests and rendering lag/flicker!
902
+ callback((0, exports.getCachedDocs)(ref));
840
903
  }
841
- catch (e) { }
842
904
  }
843
905
  else {
844
- callback((0, exports.getCachedDoc)(ref));
845
- try {
846
- const docData = await (0, exports.getDoc)(ref);
847
- callback(docData);
906
+ if (isInitial) {
907
+ isInitial = false;
908
+ callback((0, exports.getCachedDoc)(ref));
909
+ try {
910
+ const docData = await (0, exports.getDoc)(ref);
911
+ callback(docData);
912
+ }
913
+ catch (e) { }
914
+ }
915
+ else {
916
+ callback((0, exports.getCachedDoc)(ref));
848
917
  }
849
- catch (e) { }
850
918
  }
851
919
  }, 50); // Debounce to batch rapid updates
852
920
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "SDK Client resmi untuk NexaBase: Platform Sinkronisasi NoSQL, Realtime, File Storage, & Autentikasi Offline-First.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",