Access Data Offline in JavaScript Applications

Oracle Backend with Firebase APIs can cache database data locally so that a web application can continue to read, query, listen to, and change documents when its connection to the backend is interrupted. When connectivity returns, the JavaScript SDK synchronizes queued changes with Oracle Backend with Firebase APIs through Oracle REST Data Services (ORDS).

Note: Offline database support is available in the modular JavaScript SDK beginning with release 26.3. It is not available in the JavaScript namespace SDK or the Android, iOS, and Flutter SDKs in this release.

Offline support is optional. After your application calls enableOffline() for an OracleDB instance, the SDK uses a local cache and a write queue with the existing document APIs. You do not need a separate set of APIs for offline writes.

This chapter covers the following topics:

How Offline Data Access Works

When offline support is enabled, the JavaScript SDK maintains a local view of the documents that the application has read or written. The SDK uses this local view in three ways:

When the application can reach the backend, the SDK refreshes cached data and sends queued writes. Security Rules and other database controls are evaluated by the backend when each write is processed. If the backend rejects a queued write, the SDK removes its optimistic effect from the local view.

The cache contains only data that the client has received or changed. It is not a complete copy of the database. A query evaluated against the cache can therefore return stale or incomplete results.

Before You Begin

Before enabling offline support:

The following example initializes an application and obtains its OracleDB instance:

import { initializeApp } from "fusabase/app";
import { getOracledb } from "fusabase/oracledb";

const app = initializeApp(fusabaseConfig);
const db = getOracledb(app);

Enable offline support before starting database reads, writes, or snapshot listeners. Configure offline support once for each OracleDB instance.

Configure Offline Persistence

Use enableOffline() to enable the local cache, queued writes, and synchronization. By default, enableOffline() uses IndexedDB persistence.

import {
  enableOffline,
  getOracledb,
  persistentLocalCache
} from "fusabase/oracledb";

const db = getOracledb(app);

await enableOffline(
  db,
  persistentLocalCache({
    cacheSizeBytes: 50 * 1024 * 1024,
    synchronizeTabs: true
  })
);

The OfflineOptions object supports the following properties:

Property Type Description
persistence "indexeddb" \| "memory" Selects persistent browser storage or an in-memory cache. The default is "indexeddb".
cacheSizeBytes number Sets the approximate maximum size of cached remote documents.
synchronizeTabs boolean Coordinates persistent offline state and backend synchronization across browser tabs. The default is false.

If the SDK cannot initialize the selected persistence mechanism, enableOffline() rejects its promise. The OracleDB instance remains available for normal online operations, but offline behavior is not enabled.

Use IndexedDB Persistence

IndexedDB persistence retains cached documents and pending writes after a page reload or browser restart. You can enable it with either enableOffline() or enableIndexedDbPersistence().

import { enableIndexedDbPersistence } from "fusabase/oracledb";

await enableIndexedDbPersistence(db);

The following call has the same persistence selection and also lets you configure cache size and multi-tab behavior:

await enableOffline(db, {
  persistence: "indexeddb",
  cacheSizeBytes: 50 * 1024 * 1024,
  synchronizeTabs: true
});

Because IndexedDB data remains on the device between sessions, consider whether the application handles sensitive data before enabling it on a shared or untrusted device.

Use Memory Persistence

Memory persistence retains cached documents and queued writes only while the application is running. The data is discarded when the page or application process ends.

import { enableMemoryPersistence } from "fusabase/oracledb";

await enableMemoryPersistence(db);

You can also select memory persistence through enableOffline():

await enableOffline(db, {
  persistence: "memory",
  cacheSizeBytes: 10 * 1024 * 1024
});

Memory persistence is useful when the application needs temporary offline behavior but must not retain data between sessions.

If the application closes before queued writes are synchronized, writes held only in memory are also discarded.

Configure Cache Size and Multi-Tab Synchronization

Use cacheSizeBytes to limit the approximate amount of space used by cached remote documents. When the cache exceeds the configured size, the SDK removes the oldest eligible cached documents first. Documents with pending local writes are not removed.

await enableOffline(
  db,
  persistentLocalCache({
    cacheSizeBytes: 25 * 1024 * 1024
  })
);

If you do not set cacheSizeBytes, the SDK does not apply a size threshold through this option.

For an application that can be open in more than one browser tab, enable synchronized tabs with persistent local storage:

await enableOffline(
  db,
  persistentLocalCache({
    synchronizeTabs: true
  })
);

With synchronized tabs enabled, the SDK coordinates as to which tab communicates with the backend. The tabs use the same persistent cache and queued-write state. If the active coordinating tab closes, another tab can take over backend synchronization.

Read and Query Cached Data

After offline support is enabled, the standard read APIs automatically use the cache when the backend is unavailable. You can also select the cache or server explicitly.

API Data source and behavior
getDoc() Reads from the backend when possible and falls back to the local cache when the backend is unavailable.
getDocs() Runs the query against the backend when possible and falls back to cached documents when the backend is unavailable.
getDocFromCache() Reads a document only from the enabled local cache.
getDocsFromCache() Evaluates a collection or query only against the enabled local cache.
getDocFromServer() Reads only from the backend and fails when network access is disabled or unavailable. A successful result updates the cache.
getDocsFromServer() Runs a query only against the backend and fails when network access is disabled or unavailable. A successful result updates the cache.

The following example reads a document from the cache:

import { doc, getDocFromCache } from "fusabase/oracledb";

const recipeRef = doc(db, "recipes", "cookies");
const snapshot = await getDocFromCache(recipeRef);

if (snapshot.exists()) {
  console.log("Cached recipe:", snapshot.data());
} else {
  console.log("The recipe is not in the local cache.");
}

The following example evaluates a query against cached documents:

import {
  collection,
  getDocsFromCache,
  query,
  where
} from "fusabase/oracledb";

const easyRecipes = query(
  collection(db, "recipes"),
  where("difficulty", "==", "Easy")
);

const snapshot = await getDocsFromCache(easyRecipes);
snapshot.forEach((recipe) => {
  console.log(recipe.id, recipe.data());
});

An empty cached query result does not prove that the backend contains no matching documents. It means that no matching documents were found in the client’s current local view. When the application requires complete, current backend results, use getDocsFromServer() and handle network errors.

Listen to Offline Data

Snapshot listeners receive changes from the local cache as well as the backend. This lets the user interface respond immediately to cached data and local writes.

Use the snapshot metadata to determine the state of the data:

By default, a listener might not run when only snapshot metadata changes. Set includeMetadataChanges to true if the application needs to observe transitions between cache and server data or between pending and resolved writes.

import {
  collection,
  onSnapshot,
  orderBy,
  query
} from "fusabase/oracledb";

const recipesQuery = query(
  collection(db, "recipes"),
  orderBy("title")
);

const unsubscribe = onSnapshot(
  recipesQuery,
  { includeMetadataChanges: true },
  (snapshot) => {
    const source = snapshot.metadata.fromCache ? "cache" : "server";
    const state = snapshot.metadata.hasPendingWrites
      ? "pending local changes"
      : "no pending local changes";

    console.log(`Received data from ${source}; ${state}.`);
    snapshot.forEach((recipe) => console.log(recipe.id, recipe.data()));
  },
  (error) => {
    console.error("Recipe listener failed:", error);
  }
);

// Call unsubscribe() when the listener is no longer needed.

To create a listener that observes only the local cache, set source to "cache":

const unsubscribe = onSnapshot(
  recipesQuery,
  {
    source: "cache",
    includeMetadataChanges: true
  },
  (snapshot) => {
    console.log("Cached recipes:", snapshot.docs);
  }
);

A cache-only listener does not open a remote listener for its target.

Write Data and Monitor Synchronization

When offline support is enabled, setDoc(), addDoc(), updateDoc(), and deleteDoc() apply supported changes to the local view and place them in the write queue. Snapshot listeners can observe the local result immediately.

By default, the promise returned by an offline-enabled write resolves after the write has been stored locally. It does not mean that the backend has accepted the write. Use snapshot metadata or waitForPendingWrites() when the application must distinguish local completion from backend synchronization.

import {
  doc,
  serverTimestamp,
  setDoc,
  waitForPendingWrites
} from "fusabase/oracledb";

const recipeRef = doc(db, "recipes", "cookies");

// Resolves after the change has been stored in the local write queue.
await setDoc(recipeRef, {
  title: "Chocolate Chip Cookies",
  updatedAt: serverTimestamp()
});

// Waits for writes queued before this call to stop being pending.
await waitForPendingWrites(db);

If network access is unavailable, waitForPendingWrites() remains pending until synchronization can resume and the earlier writes are acknowledged or rejected. It does not include writes added after the call. Call it again to wait for later writes.

Some transforms, such as serverTimestamp(), use provisional values in the local view while a write is pending. The local view can change later when the SDK receives updated document data from the backend.

If a queued write is rejected by Security Rules, a write precondition, or another backend validation, the SDK rolls back its local effect. Keep metadata-enabled listeners active when the interface needs to show pending and resolved states.

Requirements for Offline Updates and Merges

An offline updateDoc() operation changes selected fields in an existing document. Similarly, setDoc() with merge: true depends on the document’s existing data. If the document has not been cached and the backend is unavailable, the SDK cannot construct the local result and the operation fails.

Read documents that the application expects to update before connectivity is lost. To create or replace a complete document without depending on cached state, use setDoc() without merge options.

Add Documents While Offline

addDoc() generates an identifier on the client when offline support is enabled, applies the new document locally, and queues the create operation. Offline automatic identifiers require an environment that provides crypto.randomUUID().

import { addDoc, collection } from "fusabase/oracledb";

const recipeRef = await addDoc(collection(db, "recipes"), {
  title: "Tea",
  category: "Drinks"
});

console.log("Local document path:", recipeRef.path);

Disable and Enable Network Access

Use disableNetwork() to make an offline-enabled OracleDB instance operate only against its local state. While network access is disabled:

import {
  disableNetwork,
  enableNetwork,
  waitForPendingWrites
} from "fusabase/oracledb";

await disableNetwork(db);

// Perform cached reads and queue local writes.

await enableNetwork(db);
await waitForPendingWrites(db);

enableNetwork() resumes remote reads, realtime listener synchronization, and delivery of queued writes. Both functions require offline support to have been enabled for the OracleDB instance.

Use Transactions and Batched Writes Offline

Transactions read current backend state before applying their writes. For that reason, runTransaction() requires network access and fails when the client is offline or when network access has been disabled.

When offline support is enabled, writeBatch() applies the complete batch to the local view and stores it as one queued unit. When connectivity is available, the SDK sends the queued batch to the backend. If the backend rejects the batch, the SDK rolls back the batch’s local effects.

import { doc, waitForPendingWrites, writeBatch } from "fusabase/oracledb";

const batch = writeBatch(db);

batch.set(doc(db, "recipes", "tea"), {
  title: "Tea",
  category: "Drinks"
});

batch.set(doc(db, "activity", "recipe-tea-created"), {
  type: "recipe-created",
  recipeId: "tea"
});

await batch.commit();
await waitForPendingWrites(db);

As with an individual update, a batched update or merged set requires the existing document to be available in the local cache when the backend cannot be reached.

Clear Persistent Offline Data

Use clearIndexedDbPersistence() to remove the IndexedDB cache for an OracleDB instance. This removes cached documents and queued writes. Only clear persistent data when the application intentionally needs to discard its complete offline state.

The following example clears old offline state before enabling persistence:

import {
  clearIndexedDbPersistence,
  enableIndexedDbPersistence
} from "fusabase/oracledb";

await clearIndexedDbPersistence(db);
await enableIndexedDbPersistence(db);

Do not clear persistence while the application is relying on pending writes.

Security and Offline Limitations

Offline support changes where the SDK can temporarily read and stage data. It does not change backend authorization or make the local cache authoritative.

Keep the following behaviors and limitations in mind:

Use getDocFromServer() or getDocsFromServer() when an operation must be based on current backend state. Handle network failure explicitly when using either function.

Offline API Summary

The modular JavaScript SDK exports the following offline APIs from fusabase/oracledb:

API Purpose
enableOffline() Enables the local cache, queued writes, and synchronization with configurable persistence.
enableIndexedDbPersistence() Enables persistent IndexedDB storage using default options.
enableMemoryPersistence() Enables an in-memory cache that is discarded when the application ends.
persistentLocalCache() Creates IndexedDB offline options with cache-size and tab-synchronization settings.
clearIndexedDbPersistence() Removes cached documents and queued writes from IndexedDB.
getDocFromCache() Reads a document from the local cache.
getDocsFromCache() Evaluates a collection or query against the local cache.
getDocFromServer() Reads a document only from the backend.
getDocsFromServer() Evaluates a collection or query only against the backend.
disableNetwork() Suspends backend reads, listeners, and queued-write delivery.
enableNetwork() Resumes backend access and synchronization.
waitForPendingWrites() Waits for writes queued before the call to stop being pending.

For complete signatures and type definitions, see the JavaScript SDK API reference for fusabase/oracledb.