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.
- package/README.md +20 -0
- package/dist/index.js +83 -15
- 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 || '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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