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

zerodrift

v1.0.4

Published

A TypeScript local-first sync engine: synchronous in-memory reads, optimistic writes, realtime SSE sync, offline IndexedDB persistence. Runs in the browser and in Node.

Readme

zerodrift

npm

A TypeScript sync engine with an intuitive model API that hides the hard parts of local reads, optimistic writes, offline recovery, and realtime convergence.

zerodrift lets you work with synced data like normal application state. Components and headless workers read records synchronously, mutate model fields directly, call save(), and subscribe with typed React hooks or store APIs. Under that simple surface, the engine does the synchronization work that would otherwise spread across your codebase.

The backend stays yours. Implement bootstrap, transaction, and event-stream endpoints in any stack, or start from the included Go backend and Next.js demo. In the browser, zerodrift persists models and queued writes to IndexedDB; in Node, it can run against memory or a custom storage adapter.

The result is less sync code in every feature. Define models with decorators or schema-as-data, wire the three transport functions, and build against a small, predictable API while browser tabs, clients, and Node processes converge in the background.

The design is inspired by Linear's sync engine; see Acknowledgments for prior art and attribution.

What you get

  • A small API for synced data: read records synchronously, mutate model fields directly, call save(), or use typed store namespaces generated from a schema.
  • App logic without cache choreography: fetching, invalidation, optimistic updates, reconnects, offline replay, and conflict rebasing live in the engine instead of every screen.
  • Optimistic writes with real recovery: local changes update immediately, batch into transaction POSTs, persist through reloads, and reconcile when matching server deltas arrive.
  • Relationships that stay live: references, inverse collections, owned collections, and indexed lookups update as records hydrate, load lazily, or arrive over SSE.
  • Schema or class models: use decorators (@ClientModel, @Property, @Reference) or schema-as-data (defineSchema(...), entityFromZod(...)) without reflect-metadata.
  • Memory you can shape: choose per-model LoadStrategy values for eager data, lazy tables, partial index-backed loading, local-only records, or ephemeral SSE-fed state.
  • Undo/redo built into the transaction layer: track field-level changes, group atomic multi-model edits, and include custom remote actions in the same undo stack.
  • React, browser, or headless Node: use <SyncProvider> and typed hooks in React, or run StoreManager directly in agents, workers, CLIs, and tests.
  • Your backend, your stack: implement three HTTP endpoints in any language, with a reference Go backend and Next.js demo included.

Install

npm install zerodrift

Optional packages depend on the surface you use:

npm install zod         # for entityFromZod(...) schema authoring
npm install eventsource # for Node/headless SSE clients

Decorator path: enable experimentalDecorators in your tsconfig.json (or the SWC/Babel equivalent). Unlike most decorator libraries, reflect-metadata is not needed.

Import paths

| Import | Use it for | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | zerodrift | StoreManager, BaseModel, decorators, MemoryAdapter, relation field types (RefCollection/BackRef/OwnedRefs), and the config / error / sync types. The curated, stable surface. | | zerodrift/schema | defineSchema, entityFromZod, field builders, links, extensions, and typed store.<entity>.* APIs. | | zerodrift/react | <SyncProvider> and React hooks: useRecord, useRecords, useRecordsByIndex, useRelation, useBatch, useUndoRedo, useBootstrapStatus. | | zerodrift/internal | Engine machinery (ObjectPool, TransactionQueue, SyncConnection, ModelRegistry, …) for tooling/tests. No stability promise — may change between releases. |

Define your models

Decorator models extend BaseModel and use decorators to declare fields and relationships.

import {
  BaseModel,
  ClientModel,
  Property,
  Reference,
  LazyReferenceCollection,
  LoadStrategy,
} from "zerodrift";
import type { RefCollection } from "zerodrift";

@ClientModel({ name: "Team", loadStrategy: LoadStrategy.Eager })
export class Team extends BaseModel {
  @Property() public name = "";

  @LazyReferenceCollection("Issue", { inverseOf: "teamId" })
  public issues: RefCollection<Issue>;
}

@ClientModel({ name: "Issue", loadStrategy: LoadStrategy.Eager })
export class Issue extends BaseModel {
  @Property() public title = "";
  @Property() public priority = 0;

  @Property({ indexed: true })
  public teamId: string | null = null;

  @Reference("Team", { onDelete: "cascade" })
  public team: Team;
}

@Property fields are persisted and observable. @Reference, @ReferenceCollection, @OwnedCollection, and @BackReference describe relationships; Lazy* variants load on demand. loadStrategy controls whether a model loads during bootstrap or only when requested. Pass an explicit @ClientModel({ name }) — it's the registry key and the useRecord(Model, …) handle; without it the class name is used, which minifiers mangle in production.

See agent-docs/01-models-and-decorators.md for the full decorator reference.

Schema-first with Zod

If your record shapes already live in Zod, use entityFromZod(...) as the schema authoring path. Zod owns the field types; fields overrides add zerodrift metadata such as foreign keys and indexes.

import { z } from "zod";
import {
  createStore,
  defineSchema,
  entityFromZod,
  fields as s,
  link,
  LoadStrategy,
} from "zerodrift/schema";

const TeamRecord = z.object({
  id: z.string(),
  name: z.string(),
});

const IssueRecord = z.object({
  id: z.string(),
  title: z.string().default(""),
  priority: z.number().default(0),
  teamId: z.string().nullable(),
});

export const schema = defineSchema({
  entities: {
    team: entityFromZod(TeamRecord, {
      name: "Team",
      loadStrategy: LoadStrategy.Eager,
    }),
    issue: entityFromZod(IssueRecord, {
      name: "Issue",
      loadStrategy: LoadStrategy.Eager,
      fields: {
        teamId: s.refId("team").nullable().indexed(),
      },
    }),
  },
  links: {
    issueTeam: link({
      from: { entity: "issue", field: "teamId", as: "team" },
      to: { entity: "team", many: "issues", lazy: true },
      onDelete: "cascade",
    }),
  },
});

const store = createStore({ schema, storeManager: sm });

const issue = await store.issue.get(issueId);
const teamIssues = await store.issue.getByIndex("teamId", teamId);

// create / patch commit at the current boundary — no separate save():
const newIssue = store.issue.create({ title: "Fix hydration", teamId });
store.issue.patch(issue.id, { priority: 1 });

// draft() is the staged path — mutate, then save() or discardUnsavedChanges():
const d = store.issue.draft({ title: "" });
d.title = "Write tests";
d.save();

Both authoring paths compile to the same registry, so schema entities and decorator classes can coexist. See agent-docs/11-schema-first-authoring.md for extensions, typed subscriptions, Zod override forms, and coexistence details.

React quick start

Wrap your app in <SyncProvider> once. Import your model file as a side effect so decorators run before bootstrap.

import { SyncProvider } from "zerodrift/react";
import "./models";

export default function Providers({ children }) {
  return (
    <SyncProvider
      config={{
        workspaceId: "workspace-123",
        transport: {
          bootstrapFetcher: async (type, options) => {
            const res = await fetch(
              `/api/bootstrap?type=${type}&lastSyncId=${options?.sinceSyncId ?? 0}`,
            );
            return res.json();
          },
          transactionSender: async (batch) => {
            const res = await fetch("/api/transactions", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify(batch),
            });
            return res.json();
          },
          syncUrl: "/api/events",
        },
      }}
      fallback={<div>Loading...</div>}
    >
      {children}
    </SyncProvider>
  );
}

Common reads and writes. The read hooks take a handle — a model class (decorator path) or a store.<entity> namespace (schema path) — and infer the record type from it. Every result has the same shape: { data, isLoading, isLoaded, error, reload }.

const { data: issues } = useRecords(Issue);                 // T[]
const { data: issue } = useRecord(Issue, issueId);          // T | null
const { data: teamIssues } = useRecordsByIndex(Issue, "teamId", teamId);
const { data: comments } = useRelation(issue?.comments);    // a relation
const { phase } = useBootstrapStatus();

issue.title = "New title";
issue.save();

const batch = useBatch();
batch(() => {
  issue.priority = 1;
  issue.save();
});

const { undo, redo, canUndo, canRedo } = useUndoRedo();

Schema-authored stores pass the namespace as the handle — same hooks, typed record + .indexed()-constrained index keys:

const { data: issue } = useRecord(store.issue, issueId);
const { data: teams } = useRecords(store.team);
const { data: teamIssues } = useRecordsByIndex(store.issue, "teamId", teamId);

See agent-docs/08-react-integration.md for hook return shapes, context-driven id generation, Storybook seeding, and testing patterns.

Headless usage

The same StoreManager runs without React or a browser. Use MemoryAdapter for in-process agents and tests, or implement StorageAdapter for durable storage.

import EventSource from "eventsource";
import { MemoryAdapter, StoreManager } from "zerodrift";
import "./models";

const sm = new StoreManager({
  workspaceId: "agent-1",
  transport: {
    bootstrapFetcher,
    transactionSender,
    syncUrl: "http://localhost:8081/api/events",
    sseClientFactory: (url) => new EventSource(url),
  },
  persistence: { storageAdapter: new MemoryAdapter() },
});

await sm.bootstrap();

See agent-docs/09-headless-and-agents.md for reactivity outside React, shared vs isolated agent state, refresh APIs, and observability.

Backend protocol

The client needs three endpoints:

| Endpoint | Purpose | | ------------------------ | ------------------------------------ | | GET /api/bootstrap | Fetch initial or partial model data. | | POST /api/transactions | Accept queued client mutations. | | GET /api/events | Stream delta packets over SSE. |

Bootstrap returns records grouped by model name:

{
  "lastSyncId": 5205,
  "subscribedSyncGroups": ["workspace-abc"],
  "models": {
    "Issue": [{ "id": "...", "title": "...", "teamId": "..." }],
    "Team": [{ "id": "...", "name": "..." }]
  },
  "backendDatabaseVersion": 1
}

Transactions send inserts, updates, deletes, and archives:

{
  "transactions": [
    {
      "id": "uuid",
      "action": "U",
      "modelName": "Issue",
      "modelId": "uuid",
      "changes": {
        "title": { "oldValue": "Old", "newValue": "New" }
      }
    }
  ]
}

The response should include the latest committed sync id:

{ "success": true, "lastSyncId": 5206 }

SSE messages are delta packets:

{
  "syncId": 5206,
  "syncActions": [
    {
      "modelName": "Issue",
      "modelId": "uuid",
      "action": "U",
      "data": { "title": "New title", "priority": 1 }
    }
  ],
  "addedSyncGroups": [],
  "removedSyncGroups": []
}

The client reconnects with ?lastSyncId=<id> so the server can replay missed events. See agent-docs/07-realtime-sync.md for SSE details and agent-docs/05-sync-groups.md for scoped event delivery.

Run the demo

A reference Go backend + Next.js app that exercises the full sync loop locally live in examples/. See examples/README.md for the one-command-each setup:

cd examples && make start-backend && make run-webapp

Documentation

Deeper material lives in agent-docs/:

Project structure

.                      # the publishable zerodrift package
|-- src/
|-- agent-docs/         # architecture and API notes
`-- examples/           # self-contained runnable demo (own Makefile + compose)
    |-- webapp/         # Next.js demo app
    |-- go/             # reference Go backend
    |-- docker-compose.yml
    `-- Makefile

Tech stack

  • Client: TypeScript, MobX, IndexedDB, EventSource (SSE)
  • Reference server: Go, Gin, Bun ORM, Postgres (LISTEN/NOTIFY), pgx
  • Protocol: append-only changelog, monotonic sync id, sync group filtering

Acknowledgments

zerodrift was informed by public writing and talks on local-first sync engines. Two especially helpful references were Wenzhao Hu's "Reverse Engineering Linear's Sync Engine: A Detailed Study" (wzhudev/reverse-linear-sync-engine) and Tuomas Artman's React Helsinki talk on Linear's realtime sync.

This project is an independent TypeScript implementation and is not affiliated with Linear.

License

MIT — see LICENSE. The MIT grant covers zerodrift's own code. See NOTICE for inspiration and attribution notes.