npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ffdb/sync-client

v0.3.14

Published

Runtime-neutral FFDB logical offline-sync orchestration

Readme

@ffdb/sync-client

Runtime-neutral orchestration for FFDB's logical offline-sync protocol. The package is ESM and its runtime code uses standard JavaScript only, so the same OfflineSyncClient class can run in a modern browser, React Native, or Node.js. The runtime must also support @ffdb/client (or supply its fetch implementation) and an authenticated end-user session.

pnpm add --save-exact @ffdb/[email protected] @ffdb/[email protected]

The matching GitHub Release also provides a checksum-listed ffdb-sync-client-0.3.14.tgz for verified offline installation.

import { FFDBClient, MemorySessionStore } from "@ffdb/client";
import { OfflineSyncClient } from "@ffdb/sync-client";
import { IndexedDbReplica } from "@ffdb/sync-client/browser";

const api = new FFDBClient({
  baseUrl: "https://data.example.com",
  projectId: "your-project-id",
  sessionStore: new MemorySessionStore("example"),
});
await api.auth.signIn(email, password);

const replica = new IndexedDbReplica(`ffdb-${projectId}-${userId}`);
const sync = new OfflineSyncClient(api, replica);
await sync.sync();

const note = await sync.getRow("notes", noteId);
const notes = await sync.listRows("notes");

MemoryReplica is useful for tests, examples, and process-lifetime caches. It is not durable: a reload, app termination, or Node process exit loses its rows, pending mutations, rejected mutations, and cursor.

What the orchestrator does

One sync() run is single-flight and performs these steps:

  1. Fetch and transactionally replace the replica from an RLS-filtered snapshot when no cursor exists.
  2. Push queued mutations in batches of at most 100 and transactionally remove or reject each pending record from the server's per-mutation result.
  3. Pull from the pre-push cursor so server-authoritative changes produced by the accepted local mutations are stored in the replica.
  4. Apply ordered upserts/deletes and the replacement cursor in one adapter transaction, continuing while has_more is true.
  5. Replace the snapshot on resnapshot_required or invalidate_scope.

mutate() validates a mutation, then atomically queues it and applies its insert, partial update, or delete to the visible local rows. getRow() and listRows() therefore reflect an edit as soon as durable enqueue succeeds. Server row version and sequence metadata remain authoritative: applied writes are replaced by the following pull, while duplicate, superseded, and rejected outcomes atomically invalidate the old cursor and trigger a fresh snapshot so optimistic state cannot survive when no new logical change exists. If that recovery snapshot is interrupted, the missing cursor forces the next sync to retry it. Pending edits are replayed over any snapshot taken before they are pushed.

The client exposes idle, snapshot, push, pull, and error phases through state and subscribe(). lastChangedAtMs advances only when a pull or replacement snapshot changes the replica, so a UI can reload its local view without rerendering on every idle wait. An AbortSignal cancels the active network work. A second concurrent sync() call joins the first call and therefore uses the first call's signal.

Automatic sync

Automatic sync is opt-in and runtime-neutral:

const live = sync.startAutoSync();

// React Native / Expo lifecycle integration:
const subscription = AppState.addEventListener("change", (state) => {
  live.setActive(state === "active");
});

// Optional connectivity integration (for example, NetInfo):
live.setOnline(isOnline);

// Graceful shutdown or component cleanup:
subscription.remove();
live.stop();

The default controller syncs immediately, debounces optimistic mutations for 250 ms, waits up to 25 seconds for an authenticated server change hint, and falls back to a 15-second poll when connected to an older FFDB server that returns the wait immediately. Failures use bounded exponential backoff with jitter. Inactive/offline controllers pause, a focus/online wake catches up immediately, concurrent work is deduplicated, and Node timers are unref()ed. The cursor pull remains authoritative and RLS-filtered; a wake hint never contains row data or bypasses authorization.

Runtime matrix

| Runtime | Does OfflineSyncClient run? | Bundled replica | Required application setup | | --- | --- | --- | --- | | Browser | Yes, in modern ESM/fetch runtimes | IndexedDbReplica from @ffdb/sync-client/browser | Use a database name scoped to the project and signed-in user; delete or rotate it when that authorization scope is removed | | React Native / Expo | Yes, with compatible fetch, URL, Headers, and AbortController globals | MemoryReplica; durable NativeSQLiteReplica is exported by @ffdb/react-native | Supply a secure session store and a NativeSQLiteDriver wrapper for the chosen SQLite library | | Node.js | Yes in ESM Node 24+ | NodeSQLiteReplica from @ffdb/sync-client/node | Give each project/user authorization scope its own protected SQLite path and close the replica during graceful shutdown |

The browser and Node adapters persist snapshots, cursors, pending mutations, and rejections transactionally. All bundled replicas expose the same deterministic getRow(table, primaryKey) and listRows(table) read surface, plus bounded getPending(limit) and getRejected(limit) bookkeeping reads. NodeSQLiteReplica uses Node 24's built-in node:sqlite; it does not add a native npm dependency. Importing the core package does not open a database or start background work until the application constructs a replica and calls startAutoSync(). React DOM applications can use useAutoSync from @ffdb/react for visibility, focus, and browser connectivity integration; React Native and Node applications use the same controller directly.

import { NodeSQLiteReplica } from "@ffdb/sync-client/node";

const replica = new NodeSQLiteReplica("/var/lib/my-app/ffdb-user.sqlite3");
const sync = new OfflineSyncClient(api, replica);
const live = sync.startAutoSync();
try {
  // Service work runs while the process is active. Local mutations wake it.
} finally {
  live.stop();
  await replica.close();
}

Replica adapter contract

A persistent adapter must guarantee:

  • transaction() commits all callback writes atomically and rolls them all back when the callback rejects;
  • snapshot rows and cursor are replaced in one transaction;
  • pulled changes and cursor advance in one transaction;
  • pending removal/rejection and push-result processing are atomic;
  • enqueue and its optimistic row insert/update/delete are atomic;
  • cursor values remain opaque and are never parsed, logged, or constructed;
  • getRow() returns one decoded record or null, while listRows() returns only the requested table in canonical primary-key JSON order;
  • getPending(limit) is deterministic and returns at most limit records;
  • getRejected(limit) is deterministic and retains the stable error code and rejection timestamp needed for user-visible resolution;
  • enqueueing the same mutation ID and payload is idempotent, while reusing the ID for different content is rejected;
  • local data is isolated and protected according to the runtime's threat model.

The local read surface deliberately stops at primary-key lookup and deterministic table listing. It does not expose the private SQLite/IndexedDB connection or accept arbitrary local SQL. Filter, group, and index the returned typed records in application code, or implement a custom ReplicaAdapter when the product needs a richer safe read model.

Boundaries

  • Sync is end-user-only and always remains subject to the server's current RLS.
  • Server sequence, not client time, orders conflicts.
  • Mutation batches contain 1–100 records; pull batches contain 1–1,000 changes.
  • Rejected mutations move to adapter rejection bookkeeping and are readable through getRejected(). This package does not provide conflict-resolution UI or an automatic retry policy for them.
  • The package does not cache arbitrary FFDBClient.query() results, rewrite SQL, expose a local SQLite connection, encrypt local storage, or migrate an application's own schema.