Akar Masalah: Batasan IEEE 754 dan Tipe NUMBER Oracle

Tipe data NUMBER pada Oracle Database mendukung hingga 38 digit signifikan. Pada skala enterprise, sequence ID, Primary Key, atau Snowflake ID yang disimpan di kolom NUMBER(19) atau NUMBER(20) sering kali melampaui nilai 9.007.199.254.740.991 (Number.MAX_SAFE_INTEGER atau 253 - 1).

JavaScript di runtime browser dan Node.js memproses tipe number menggunakan spesifikasi IEEE 754 double-precision floating-point (64-bit float). Format ini hanya mengalokasikan 53 bit untuk mantissa (significand). Ketika angka 64-bit mentah dikirim ke browser melalui serialisasi JSON standar, JavaScript membulatkan digit terakhir untuk menyesuaikan kapasitas bit tersebut.

// Contoh di V8 Console:
const serverId = 9007199254740995;
console.log(serverId);
// Output: 9007199254740996 (presisi hilang)

Gejala Hydration Mismatch pada Next.js dan Nuxt

Pada arsitektur Server-Side Rendering (SSR), server me-render HTML awal langsung dari data backend, lalu mengirim string HTML tersebut bersama payload serialisasi (seperti __NEXT_DATA__ atau state Nuxt) ke browser.

Jika ID Oracle 64-bit diserialisasikan sebagai numeric literal di dalam JSON payload, browser mengeksekusi JSON.parse() selama proses hidrasi. HTML server memuat ID yang ditulis sebelum pembulatan, sedangkan DOM virtual di browser mengacu pada angka yang telah terpotong presisinya.

Gejala umum muncul pada console browser:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Warning: Expected server HTML to contain a matching text content for:
Server: "9007199254740995"
Client: "9007199254740996"
  at span
  at OrderDetail (<anonymous>)

Ketidakcocokan ini juga sering terjadi pada atribut HTML, misalnya:

<!-- Server HTML render -->
<button data-id="9007199254740995">Lihat Detail</button>

<!-- Client DOM hydration -->
<button data-id="9007199254740996">Lihat Detail</button>

Dampaknya, interaksi berbasis event handler gagal mengeksekusi target data yang benar, atau framework merender ulang seluruh komponen secara paksa, merusak performa SSR.

Solusi 1: Global Driver Mapping di node-oracledb

Pendekatan paling efisien di backend Node.js adalah mengonfigurasi driver database resmi (node-oracledb) agar mengubah seluruh nilai numerik atau kolom spesifik menjadi string sebelum masuk ke V8 heap runtime.

Konfigurasi Global

const oracledb = require('oracledb');

// Paksa semua tipe NUMBER dikembalikan sebagai String
oracledb.fetchAsString = [ oracledb.NUMBER ];

async function getOrders() {
  const connection = await oracledb.getConnection(dbConfig);
  const result = await connection.execute(
    `SELECT order_id, total_amount FROM orders WHERE status = 'PENDING'`
  );
  // order_id sekarang bertipe String: "9007199254740995"
  await connection.close();
  return result.rows;
}

Konfigurasi Granular per Query

Jika tipe data floating point untuk kalkulasi (seperti total_amount) harus tetap berformat number, batasi konversi hanya pada kolom identifier menggunakan opsi fetchInfo:

const result = await connection.execute(
  `SELECT order_id, total_amount FROM orders WHERE status = 'PENDING'`,
  [],
  {
    fetchInfo: {
      ORDER_ID: { type: oracledb.STRING }
    }
  }
);

Solusi 2: Casting SQL Menggunakan TO_CHAR()

Jika codebase backend tidak memungkinkan perubahan konfigurasi driver database, lakukan konversi langsung pada level query SQL menggunakan fungsi bawaan Oracle:

SELECT 
  TO_CHAR(order_id) AS order_id,
  user_id,
  total_amount
FROM orders
WHERE created_at >= TRUNC(SYSDATE);

Kelemahan pendekatan ini adalah perlunya pengubahan manual pada setiap query yang mengambil sequence ID berukuran besar. Kelebihannya, tipe data yang diterima layer aplikasi langsung berstatus aman tanpa ketergantungan konfigurasi pool koneksi.

Solusi 3: Normalisasi di Boundary Layer API

Jika menggunakan ORM atau query builder yang menghasilkan tipe BigInt pada runtime Node.js, serialisasi JSON native akan gagal dengan pesan TypeError: Do not know how to serialize a BigInt.

Gunakan normalisasi DTO (Data Transfer Object) sebelum data dikembalikan ke halaman SSR atau API response:

// boundary/order.serializer.ts
export interface RawOrder {
  id: bigint;
  name: string;
}

export interface SerializedOrder {
  id: string;
  name: string;
}

export function serializeOrder(order: RawOrder): SerializedOrder {
  return {
    ...order,
    id: order.id.toString(), // Konversi BigInt ke String aman untuk JSON
  };
}
Catatan Keamanan Tipe: Jangan menggunakan JSON.parse custom replacer di browser untuk mengembalikan string ke BigInt jika UI hanya bertugas menampilkan teks atau mengirim ulang ID ke endpoint REST. Tangani ID sebagai tipe string murni (opaque token) di layer presentasi.

Verifikasi Debugging

Pastikan perbaikan berfungsi melalui langkah-langkah berikut:

  1. Periksa Raw SSR Payload: Gunakan curl -s http://localhost:3000/orders | grep '9007199254740995'. Pastikan ID dibungkus dengan tanda kutip ganda ("order_id": "9007199254740995"), bukan sebagai numeric literal.
  2. Cek Console Browser: Jalankan aplikasi pada mode development. Verifikasi bahwa pesan peringatan Hydration failed hilang sepenuhnya pada komponen yang merender identifier.
  3. Validasi Network Tab: Periksa tab Network browser, amati response JSON dari Server Actions atau getServerSideProps. Nilai ID harus teridentifikasi sebagai tipe string.