# NexaBase JavaScript SDK (`nexabase-console`)

SDK Client Resmi untuk **NexaBase**: Platform sinkronisasi database modern yang menyatukan fungsionalitas Firestore NoSQL, Real-Time Database, File Storage, dan Edge Authentication dengan dukungan **Offline-First (IndexedDB)** serta **Passwordless OTP**.

---

## 🚀 Fitur Utama

-   **⚡ Real-Time Synchronization & Anti-Flicker Engine**: Sinkronisasi data real-time dengan latency rendah menggunakan Server-Sent Events (SSE) yang dilengkapi *Deep Comparison Data Hashing* untuk mencegah re-render/flicker komponen UI saat sync.
-   **🚀 Latency Compensation (Optimistic UI & 0ms Local Render)**: Penulisan data (`setDoc`, `updateDoc`, `deleteDoc`, `writeBatch`) langsung memperbarui cache lokal IndexedDB dan memicu `onSnapshot` secara instan (0ms) sebelum konfirmasi jaringan selesai.
-   **🔄 Delta Updates (`docChanges()`)**: Dukungan native pelacakan perubahan inkremental (`added`, `modified`, `removed`) pada `QuerySnapshot` untuk manipulasi elemen UI secara presisi tanpa mengunduh ulang seluruh dataset.
-   **🔒 Concurrency Control & Transactions (`runTransaction`)**: Transaksi dua arah (Read-Before-Write) atomik dengan kontrol konkurensi (Optimistic Locking & Automatic Retry) untuk mencegah race conditions pada pemotongan stok, kuota, atau transaksi finansial.
-   **📦 Offline-First & Auto-Sync**: Sinkronisasi state lokal secara otomatis menggunakan **IndexedDB**. Data tetap tersimpan di browser jika koneksi terputus dan akan disinkronkan ke server secara otomatis saat kembali online.
-   **🔥 Modular Firestore API & TypeScript Generics**: Desain API modular modern mirip Firebase SDK (v9+) dengan dukungan penuh parameter tipe generik (TypeScript Generics) untuk menjamin tipe data yang aman (type-safe) secara end-to-end.
-   **🔑 Secure Auth & Passwordless OTP**: Dukungan penuh registrasi, login email/sandi, serta OTP instan via SMTP yang aman.
-   **📂 File Storage Integration**: Unggah aset media dan file blob ke cloud storage dengan satu perintah.

---

## 📦 Instalasi

Instal package melalui npm atau yarn:

```bash
npm install nexabase-console
# atau
yarn add nexabase-console
```

---

## 🛠️ Cara Penggunaan & Contoh Kode

### 1. Inisialisasi Aplikasi

```javascript
import { initializeApp, getFirestore } from 'nexabase-console';

const app = initializeApp({
  projectId: 'PROYEK_NEXABASE_ID_ANDA',
  apiKey: 'NEXABASE_API_KEY_ANDA' // Opsional, default endpoint mengarah ke https://db.nexabase.id
});

const db = getFirestore(app);
```

#### 🛡️ Keamanan & Autentikasi Ganda (API Key & Sesi)
Untuk mendukung arsitektur server-side proxy atau direct API key yang sangat fleksibel, SDK secara otomatis mengirimkan token Anda dalam dua format header sekaligus:
- **`Authorization: Bearer <token>`**: Digunakan untuk validasi token JWT sesi user.
- **`x-api-key: <api_key>`**: Digunakan untuk otentikasi aman tanpa sesi menggunakan API Key proyek.

Hal ini mencegah kegagalan otentikasi (Error `403 Forbidden: Please login`) saat memanggil metode di server menggunakan API Key proyek.

#### ⚡ Sinkronisasi Offline & Database-Priority Engine (IndexedDB Cache)
NexaBase SDK mengimplementasikan manajemen cache tangguh yang serupa dengan Firebase untuk menjamin keandalan aplikasi saat jaringan tidak stabil:
1. **Prioritas Database Utama (`network-first` secara default)**: SDK memprioritaskan pengambilan data dari database remote server untuk menjamin kesegaran data (*data freshness*).
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.
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*).
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).
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.

---

### 2. Firestore-like Modular API (NoSQL)

#### Menulis Data Baru (Set Doc)
```javascript
import { doc, setDoc } from 'nexabase-console';

const main = async () => {
  const profileRef = doc(db, 'users', 'budi_santoso');
  const res = await setDoc(profileRef, {
    name: 'Budi Santoso',
    email: 'budi@example.com',
    age: 28,
    hobbies: ['Coding', 'Cycling']
  });
  
  console.log("Data berhasil disimpan:", res);
};

main();
```

#### Memperbarui Sebagian Data (Update Doc)
```javascript
import { doc, updateDoc } from 'nexabase-console';

const editData = async () => {
  const profileRef = doc(db, 'users', 'budi_santoso');
  await updateDoc(profileRef, {
    age: 29 // Hanya memperbarui field age
  });
};
```

#### Membaca Satu Dokumen (Get Doc)
```javascript
import { doc, getDoc } from 'nexabase-console';

const readData = async () => {
  const profileRef = doc(db, 'users', 'budi_santoso');
  const snapshot = await getDoc(profileRef);
  
  if (snapshot.exists()) {
    console.log("Isi data:", snapshot.data());
  } else {
    console.log("Dokumen tidak ditemukan.");
  }
};
```

#### Mendengarkan Perubahan Data secara Real-Time (onSnapshot & docChanges)
```javascript
import { collection, onSnapshot } from 'nexabase-console';

const listenLiveChats = () => {
  const chatsRef = collection(db, 'chats');
  
  // onSnapshot menggunakan Anti-Flicker Engine (Deep Comparison)
  // - Langsung memancarkan cache lokal secara instan (0ms) tanpa layar putih
  // - Membandingkan hash data server sebelum re-render agar UI tidak berkedip
  const unsubscribe = onSnapshot(chatsRef, (querySnapshot) => {
    // 1. Ambil rincian pembaruan inkremental (Delta/Diff Updates)
    if (querySnapshot.docChanges) {
      const changes = querySnapshot.docChanges();
      changes.forEach((change) => {
        if (change.type === 'added') console.log("Pesan baru:", change.doc.data());
        if (change.type === 'modified') console.log("Pesan diedit:", change.doc.data());
        if (change.type === 'removed') console.log("Pesan dihapus:", change.doc.id);
      });
    }

    // 2. Iterasi seluruh koleksi
    querySnapshot.forEach((doc) => {
      console.log(doc.id, "=>", doc.data());
    });
  });
  
  // Panggil unsubscribe() untuk menghentikan pemantauan realtime
  // unsubscribe();
};
```

#### Agregasi Data & Hitung Dokumen (`getCountFromServer` / `getAggregateFromServer`)
Mendukung fungsi agregasi server 1:1 seperti Firebase SDK: `getCountFromServer()`, `getAggregateFromServer()`, `count()`, `sum()`, dan `average()`.

```javascript
import { 
  collection, 
  query, 
  where, 
  getCountFromServer, 
  getAggregateFromServer, 
  count, 
  sum, 
  average 
} from 'nexabase-console';

// 1. Menghitung total dokumen (getCountFromServer)
const productsRef = collection(db, 'products');
const qAvailable = query(productsRef, where('status', '==', 'active'));

const countSnapshot = await getCountFromServer(qAvailable);
console.log("Total produk aktif:", countSnapshot.data().count);

// 2. Agregasi multi-field (getAggregateFromServer)
const orderStats = await getAggregateFromServer(collection(db, 'orders'), {
  totalOrders: count(),
  totalRevenue: sum('totalAmount'),
  avgOrderValue: average('totalAmount')
});

console.log("Jumlah order:", orderStats.data().totalOrders);
console.log("Total pendapatan:", orderStats.data().totalRevenue);
console.log("Rata-rata transaksi:", orderStats.data().avgOrderValue);
```

#### Menggunakan Atomic Write Batch (Untuk Upload File / Excel Massal)
Jika Anda mengunggah data sekaligus dalam jumlah besar dari file (misal: import 500 resi dari Excel), hindari memanggil `setDoc` satu per satu di dalam _looping_. Gunakan fitur `writeBatch` yang membundel seluruh operasi Anda dalam satu pengiriman jaringan (hingga 500 operasi per eksekusi).

**Catatan:** Jika alur kerjanya adalah **kasir/admin melakukan scan barcode satu-per-satu lalu menekan enter**, maka Anda **TIDAK PERLU** menggunakan `writeBatch`. Menyimpan data satu-per-satu (`setDoc` / `addDoc`) setiap kali dienter adalah **cara yang paling benar dan normal** untuk kebutuhan _realtime_. Database dapat dengan mudah menangani 500 scan yang terjadi secara bertahap sepanjang hari.

```javascript
import { doc, writeBatch } from 'nexabase-console';

const executeBulkUpload = async (listResi) => {
  const batch = writeBatch(db);
  
  // ListResi adalah array berisi objek resi. Maksimal 500 per batch.
  listResi.forEach((resi) => {
    // Referensi dokumen
    const docRef = doc(db, 'resi', resi.nomor_resi);
    batch.set(docRef, resi);
  });
  
  // Eksekusi semua secara atomik (bersamaan dalam 1 request)
  const result = await batch.commit();
  console.log(`Berhasil mengunggah ${listResi.length} resi secara massal.`);
};
```

#### Menggunakan Transactions (`runTransaction`) — Concurrency Control
```javascript
import { doc, runTransaction } from 'nexabase-console';

async function potongStokAman(productId, variantId, qtyBeli) {
  const productRef = doc(db, 'products', productId);

  try {
    const hasil = await runTransaction(db, async (transaction) => {
      // 1. Baca dokumen di dalam transaksi (Read)
      const productSnap = await transaction.get(productRef);
      if (!productSnap.exists()) {
        throw new Error("Produk tidak ditemukan!");
      }

      const productData = productSnap.data();
      const variantIndex = productData.variants?.findIndex((v) => v.id === variantId);
      if (variantIndex === -1 || variantIndex === undefined) {
        throw new Error("Varian produk tidak ditemukan!");
      }

      const variant = productData.variants[variantIndex];
      const stokSaatIni = variant.stock;

      // 2. Validasi stok sebelum dipotong
      if (stokSaatIni < qtyBeli) {
        throw new Error(`Stok tidak mencukupi! Tersedia: ${stokSaatIni}, Diminta: ${qtyBeli}`);
      }

      // 3. Kalkulasi data baru
      const updatedVariants = [...productData.variants];
      updatedVariants[variantIndex] = {
        ...variant,
        stock: stokSaatIni - qtyBeli
      };

      // 4. Tulis perubahan di dalam transaksi (Write)
      transaction.update(productRef, {
        variants: updatedVariants,
        updatedAt: new Date().toISOString()
      });

      return { success: true, sisaStok: stokSaatIni - qtyBeli };
    });

    console.log("Transaksi Berhasil!", hasil);
  } catch (error) {
    console.error("Transaksi Gagal / Conflict:", error.message);
  }
}
```

---

### 3. Autentikasi Pengguna & OTP Tanpa Sandi (SDK v2.0.0)

#### Inisialisasi Auth & Realtime State Listener
```javascript
import { getAuth, NexaAuthError } from 'nexabase-console';

const auth = getAuth(app);

// Realtime Listener & Sync Sesi Multi-Tab via StorageEvent
const unsubscribe = auth.onAuthStateChanged((user) => {
  if (user) {
    console.log("User terautentikasi:", user.uid, "| Email:", user.email);
  } else {
    console.log("User ter-logout / belum login");
  }
});
```

#### Pendaftaran & Login Email/Password
```javascript
// 1. Registrasi Akun Baru
const register = async () => {
  try {
    const session = await auth.createUserWithEmailAndPassword(
      'budi@example.com', 
      'Password123!', 
      'Budi Santoso'
    );
    console.log("Registrasi Berhasil! UID:", session.user.uid);
  } catch (err) {
    if (err instanceof NexaAuthError && err.code === 'auth/email-already-in-use') {
      console.error("Email sudah terdaftar.");
    }
  }
};

// 2. Login User (Dukungan Automatic Single-Flight Token Refresh)
const login = async () => {
  try {
    const session = await auth.signInWithEmailAndPassword(
      'budi@example.com', 
      'Password123!'
    );
    console.log("Access Token JWT:", session.accessToken);
    console.log("Refresh Token:", session.refreshToken);
  } catch (err) {
    if (err instanceof NexaAuthError && err.code === 'auth/invalid-credential') {
      console.error("Email atau password salah.");
    }
  }
};
```

#### Login Passwordless dengan OTP (Email 6-Digit)
```javascript
// 1. Kirim OTP Ke Email (Berlaku 5 Menit, Hash Single-Use)
const sendMyOtp = async () => {
  const res = await auth.sendOtp('budi@example.com');
  console.log("Kode OTP berhasil terkirim:", res.message);
};

// 2. Verifikasi OTP dari Input Pengguna
const verifyMyOtp = async () => {
  try {
    const otpCode = '123456'; // Kode 6 digit yang dikirim ke email
    const session = await auth.signInWithOtp('budi@example.com', otpCode);
    console.log("Login OTP Berhasil! Welcome:", session.user.email);
  } catch (err) {
    if (err instanceof NexaAuthError) {
      if (err.code === 'auth/invalid-otp') console.error("OTP tidak valid.");
      if (err.code === 'auth/expired-otp') console.error("OTP kedaluwarsa.");
      if (err.code === 'auth/too-many-otp-attempts') console.error("Terlalu banyak percobaan.");
    }
  }
};
```

#### Mengambil ID Token untuk Request API / Server Verification
```javascript
// Mengambil token JWT (dengan auto-refresh jika hampir expired)
const idToken = await auth.getIdToken(/* forceRefresh */ false);

// Mengambil rincian klaim token (exp, authTime, claims)
const tokenResult = await auth.getIdTokenResult();
console.log("Token Kedaluwarsa:", tokenResult.expirationTime);
```

#### Update Profil, Reset Password & Sign Out
```javascript
// Update Profil
await auth.updateProfile({ name: 'Budi Santoso, M.T.', photoURL: 'https://example.com/photo.jpg' });

// Reset Password Email
await auth.sendPasswordResetEmail('budi@example.com');

// Revoke All Sessions
await auth.revokeAllSessions();

// === Opsi Logout Akun (Sign Out) ===
// 1. Method Instance (Rekomendasi)
await auth.signOut();

// 2. Fungsi Standar Modular
import { signOut } from 'nexabase-console';
await signOut(auth);

// 3. Namespace Helper
import { NexabaseAuth } from 'nexabase-console';
await NexabaseAuth.signOut(auth);

// 4. Shortcut dari objek App
await app.signOut();
```

---

### 4. File Storage API

```javascript
const uploadImage = async (fileBlob) => {
  const storageRef = app.storage().ref('avatars/andi.jpg');
  const result = await storageRef.put(fileBlob);
  
  console.log("Akses File URL Publik Anda:", result.url);
};
```

#### 📂 Penanganan Tipe Konten Dinamis (Form Data Boundary)
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*). 

Ini menghilangkan bug berkas kosong (`{}`) yang disebabkan oleh tumpang tindihnya header `application/json` di Axios.

---

### 5. Keamanan Tipe TypeScript (Generics) & Kueri Lanjutan

#### Dukungan TypeScript Generics
Kini Anda bisa mendefinisikan tipe data struktur dokumen Anda agar penulisan dan pembacaan data sepenuhnya aman (Type-safe).

```typescript
import { collection, doc, getDoc, getDocs } from 'nexabase-console';

interface Product {
  name: string;
  price: number;
  stock: number;
  tags: string[];
}

// 1. Definisikan tipe koleksi
const productsCol = collection<Product>(db, 'products');

// 2. Definisikan tipe referensi dokumen
const productRef = doc<Product>(db, 'products', 'prod_a');

// 3. Baca data dengan Autocomplete & Type Checking penuh
const snapshot = await getDoc(productRef);
if (snapshot.exists()) {
  const data = snapshot.data(); // Tipe data otomatis terdeteksi sebagai 'Product'
  console.log(data.name);  // Aman! Autocomplete tersedia
  console.log(data.price); // Aman!
}
```

#### Kompatibilitas QuerySnapshot (`docChanges()`)
Bagi Anda yang bermigrasi dari Firebase, objek `QuerySnapshot` yang dikembalikan oleh `getDocs` atau `onSnapshot` kini mendukung metode `.docChanges()` secara native:

```typescript
const snapshot = await getDocs(productsCol);
const changes = snapshot.docChanges(); // Mengembalikan array perubahan dokumen
```

#### Filter Offline Lengkap (Advanced Querying Operators)
Mekanisme kueri offline pada SDK NexaBase kini mendukung operator Firestore tingkat lanjut secara penuh demi keandalan luar biasa bahkan saat koneksi terputus:
- Operator dasar: `==`, `!=`, `>`, `<`, `>=`, `<=`
- Operator array & keanggotaan: `in`, `not-in`, `array-contains`, `array-contains-any`

---

## 🗄️ Dukungan Sinkronisasi Offline (IndexedDB)

NexaBase JS SDK dikembangkan dengan arsitektur **Offline-First**. 

1.  **Optimistic Rendering**: Jika perangkat offline, SDK akan langsung mengupdate cache IndexedDB lokal dan men-trigger callback subscriber `onSnapshot` agar UI pengguna langsung merespons secara instan.
2.  **Background Queue**: Setiap perubahan mutasi (`setDoc`, `updateDoc`, `deleteDoc`, `writeBatch`) saat offline disimpan secara aman di IndexedDB.
3.  **Automatic Resync**: Saat koneksi internet mendeteksi status `online`, antrean modifikasi akan dikirimkan kembali secara beruntun ke Server cloud NexaBase secara otomatis tanpa intervensi pengguna.

---

## 📄 Lisensi

Proyek ini dilisensikan di bawah Lisensi MIT. Bebas digunakan untuk keperluan pribadi, komersial, maupun edukasi.
