> ## Documentation Index
> Fetch the complete documentation index at: https://chahine.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline-first

> Persist CRDT state locally so your app works instantly on load and survives network interruptions.

By default the SDK holds all CRDT state in memory — a page refresh clears everything and the client re-syncs from the server. Enabling persistence changes this: CRDT values are cached in IndexedDB and restored instantly on the next load, before the WebSocket connects.

## Setup

```ts theme={null}
import { Effect } from "effect";
import {
  MeridianClient,
  indexedDbStateStorage,
  localStorageSyncOpsAdapter,
} from "meridian-sdk";

const client = await Effect.runPromise(
  MeridianClient.create({
    url: "http://localhost:3000",
    namespace: "my-room",
    token,
    persistence: {
      state: indexedDbStateStorage(),    // CRDT snapshots → IndexedDB
      ops:   localStorageSyncOpsAdapter(), // pending op queue → localStorage
    },
  })
);

// Declare all handles first, then wait for the cache to load
const views = client.gcounter("gc:views");
const cart  = client.orset("or:cart");
await client.waitForRestore();

// values are now populated from cache — no network round-trip needed
console.log(views.value()); // e.g. 42
```

`waitForRestore()` resolves once all IndexedDB reads triggered by handle creation have completed. Call it **after** declaring every handle you need, not before.

## How it works

| What | Storage | When saved | When loaded |
| - | - | - | - |
| CRDT snapshots | IndexedDB | After every server delta | On handle creation (lazy, async) |
| Pending op queue | localStorage | On `close()`, `beforeunload`, and tab hidden (`visibilitychange`) | On `MeridianClient.create()` |
| Vector clocks | localStorage (opt-in) | On every observed op | On `VectorClockTracker` construction |

On reconnect the server sends a `Sync` delta that merges authoritatively on top of the cached state — the cache is never stale for long.

The `visibilitychange` flush covers most crash and background-kill scenarios: when the browser moves a tab to the background (mobile app switch, tab minimise) the queue is persisted immediately, before the process may be suspended or killed.

## Offline mode

Pass `offline: true` to never auto-connect. The client operates entirely from the local cache. Call `client.connect()` explicitly when you want to go online.

```ts theme={null}
const client = await Effect.runPromise(
  MeridianClient.create({
    url: "http://localhost:3000",
    namespace: "my-room",
    token,
    offline: true,
    persistence: { state: indexedDbStateStorage() },
  })
);

const views = client.gcounter("gc:views");
await client.waitForRestore();

views.value(); // from cache

// later, when network is available:
client.connect();
```

## Custom storage adapter

Both `StateStorage` (async) and `SyncStateStorage` (sync) are plain interfaces — implement them to use any backend:

```ts theme={null}
import type { StateStorage } from "meridian-sdk";

const myStorage: StateStorage = {
  load:   async (key) => { /* return Uint8Array | null */ },
  save:   async (key, data) => { /* persist Uint8Array */ },
  delete: async (key) => { /* remove key */ },
};
```

## Storage adapters

| Adapter | Interface | Use for |
| - | - | - |
| `indexedDbStateStorage(dbName?)` | `StateStorage` (async) | CRDT snapshots — large data, no quota issues |
| `localStorageSyncOpsAdapter(prefix?)` | `SyncStateStorage` (sync) | Pending op queue — must be sync for `beforeunload` safety |
| `memoryStateStorage` | `StateStorage` (async) | Testing — in-memory, not persisted |

## Multi-tab sync

Pass `tabSync: true` to keep all open tabs in the same namespace in sync instantly — without each tab waiting for its own server round-trip:

```ts theme={null}
const client = await Effect.runPromise(
  MeridianClient.create({
    url: "http://localhost:3000",
    namespace: "my-room",
    token,
    tabSync: true,
    persistence: {
      state: indexedDbStateStorage(),
      ops:   localStorageSyncOpsAdapter(),
    },
  })
);
```

When Tab A receives a server delta it broadcasts the raw delta bytes to every other tab sharing the same namespace via the [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel). Tab B applies the delta immediately to its local CRDT handles — the same delta will also arrive via Tab B's own WebSocket subscription, but re-applying it is safe because all CRDTs are convergent.

This is especially useful when Tab B is temporarily disconnected or backgrounded: it receives updates from Tab A without needing connectivity of its own.

> [!NOTE]
>
> `BroadcastChannel` is available in all modern browsers and in Node.js 18+. The SDK checks `typeof BroadcastChannel !== "undefined"` at construction time and silently skips tab sync if the API is absent.

## Caveats

* **Awareness** — ephemeral by design. Offline updates are dropped silently since awareness data is time-sensitive and not persisted.
* **SSR / Node** — `indexedDbStateStorage` rejects gracefully if `indexedDB` is not available. `localStorageSyncOpsAdapter` is a no-op if `localStorage` is unavailable.
* **Token rotation** — snapshot keys include the `client_id` from the token. If you issue a new token with a different `client_id`, the previous cache is ignored (not deleted automatically).
* **Long offline sessions** — the server GCs RGA and Tree tombstones after 24 hours (configurable via `MERIDIAN_CRDT_GC_GRACE_MS`). Clients offline longer than this grace period may not receive delete deltas for GC'd tombstones on reconnect.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.