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

@claritylabs/cl-sync

v0.1.1

Published

Reusable local-first browser sync primitives for Convex and other backends

Readme

@claritylabs/cl-sync

Reusable local-first browser sync primitives for React apps.

cl-sync is intentionally app-agnostic. The core package owns IndexedDB persistence, a normalized in-memory store, selector subscriptions, schema metadata, scoped migrations, and a durable mutation outbox. Apps provide collection definitions, auth scopes, conflict behavior, and backend adapters. Convex is included as an optional adapter, but the core can be used with any API.

Packages

  • @claritylabs/cl-sync/core - store, collections, schema metadata, migrations, outbox, persistence.
  • @claritylabs/cl-sync/react - provider and React hooks.
  • @claritylabs/cl-sync/convex - Convex query/mutation adapter.

Use this when a UI should hydrate from a scoped local cache immediately, reconcile with a server in the background, and keep user actions responsive through optimistic local reducers.

Core Concepts

  • Scope: { appId, environment, userId, orgId } isolates persisted data. Switching user or org creates a different IndexedDB scope.
  • Collections: named record sets with a stable ID resolver, optional persistence, redaction, sorting, and cache keys derived from query args.
  • Schema metadata: app-owned version and collection metadata stored with the local scope.
  • Migrations: app-owned functions that can rewrite scoped records, collection states, outbox items, or metadata during hydration.
  • Outbox: queued mutations persist across reloads. Register mutation definitions after boot, then flush pending items when the app is hydrated and online.

Non-Convex Usage

import {
  createSyncStore,
  defineCollection,
  defineMutation,
  type SyncRecord,
} from "@claritylabs/cl-sync/core";

type Todo = SyncRecord & {
  id: string;
  title: string;
  completed: boolean;
};

const todoCollection = defineCollection<Todo, { listId: string }>({
  name: "todos",
  getId: (todo) => todo.id,
  deriveKey: (args) => args.listId,
  sort: (a, b) => a.title.localeCompare(b.title),
});

const createTodo = defineMutation<{ id: string; title: string; listId: string }, { ok: true }>({
  name: "todos.create",
  reducer: (store, args) => {
    const current = store.getCollection(todoCollection, { listId: args.listId }) ?? [];
    void store.upsertCollection(todoCollection, { listId: args.listId }, [
      ...current,
      { id: args.id, title: args.title, completed: false },
    ]);
  },
  flush: async (args, clientMutationId) => {
    const response = await fetch("/api/todos", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ ...args, clientMutationId }),
    });
    if (!response.ok) throw new Error(`Create failed: ${response.status}`);
    return await response.json() as { ok: true };
  },
});

const store = createSyncStore({
  scope: {
    appId: "todo-app",
    environment: import.meta.env.MODE,
    userId: currentUser.id,
  },
  schema: {
    version: 2,
    collections: [
      { name: "todos", version: 2, fields: ["id", "title", "completed"] },
    ],
  },
  migrations: [{
    version: 2,
    name: "rename text to title",
    migrate: (context) => {
      for (const record of context.getRecords("todos")) {
        if (typeof record.text === "string" && typeof record.title !== "string") {
          context.setRecord("todos", record.id as string, {
            ...record,
            title: record.text,
          });
        }
      }
    },
  }],
  mutations: [createTodo],
});

await store.hydrate();
await store.flushPendingMutations();

React Usage

import {
  SyncProvider,
  useSyncCollection,
  useSyncMutation,
  useSyncStatus,
} from "@claritylabs/cl-sync/react";

export function App() {
  return (
    <SyncProvider store={store} mutations={[createTodo]} flushOnHydrate>
      <TodoList />
    </SyncProvider>
  );
}

function TodoList() {
  const todos = useSyncCollection(todoCollection, { listId: "inbox" }) ?? [];
  const create = useSyncMutation(createTodo);
  const status = useSyncStatus();

  return (
    <button
      disabled={!status.hydrated}
      onClick={() => create({ id: crypto.randomUUID(), listId: "inbox", title: "Ship it" })}
    >
      Add todo
    </button>
  );
}

flushOnHydrate registers the supplied mutations before hydration resolves, then calls store.flushPendingMutations(). You can call the core API yourself if you want to flush only after an auth token refresh, network check, or feature gate.

Convex Adapter Usage

import { ConvexReactClient } from "convex/react";
import { api } from "./convex/_generated/api";
import { createSyncStore } from "@claritylabs/cl-sync/core";
import {
  defineConvexCollection,
  defineConvexMutation,
  subscribeConvexCollection,
} from "@claritylabs/cl-sync/convex";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

const todos = defineConvexCollection({
  name: "todos",
  query: api.todos.list,
  getId: (todo) => todo._id,
});

const createTodo = defineConvexMutation(convex, {
  name: "todos.create",
  mutation: api.todos.create,
  mapArgs: (args: { title: string }, clientMutationId) => ({
    ...args,
    clientMutationId,
  }),
});

const store = createSyncStore({
  scope: { appId: "todo-app", environment: "prod", userId: currentUser.id },
  schema: { version: 1, collections: [{ name: "todos", version: 1 }] },
  mutations: [createTodo],
});

await store.hydrate();
const unsubscribe = subscribeConvexCollection(store, convex, todos, {});
await store.flushPendingMutations();

The adapter only translates Convex query snapshots and mutation calls. Collection naming, persistence policy, reducers, migrations, and conflict handling stay in the app.

API Notes

  • store.getPersistedSchemaVersion() returns the scoped schema version loaded during hydration.
  • store.getMeta(key) reads migration metadata or app metadata stored with the scope.
  • store.registerMutation(definition) and store.registerMutations(definitions) connect hydrated outbox rows back to executable mutation definitions.
  • store.flushPendingMutations({ includeFailed, predicate }) flushes registered pending mutations in creation order and reports flushed, failed, and skipped IDs without dropping rows that cannot run.
  • store.clearScope() clears records, collection states, outbox rows, and metadata for only the active scope.