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

@byearlybird/starling

v0.18.0

Published

Conflict-free replicated state for JavaScript. Bring your own reactivity.

Readme

Starling

Conflict-free replicated state for JavaScript. Bring your own reactivity.

Starling is a CRDT (conflict-free replicated data type) library that provides automatic conflict resolution for distributed data. It manages state with Last-Write-Wins semantics using hybrid logical clocks, giving you a solid foundation for building local-first, collaborative applications.

Installation

npm install @byearlybird/starling
# or
pnpm add @byearlybird/starling
# or
bun add @byearlybird/starling

Requires TypeScript 5 or higher.

Quick Example

import { createStore, collection } from "@byearlybird/starling";
import { z } from "zod";

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

const store = createStore({
  users: collection(userSchema, (data) => data.id),
});

store.put("users", { id: "1", name: "Alice" });
const user = store.get("users", "1"); // { id: "1", name: "Alice" }

Features

  • CRDT-based: Automatic conflict resolution with Last-Write-Wins semantics
  • Fast and synchronous: All operations are in-memory and synchronous
  • Framework agnostic: Works with React, Vue, Svelte, or vanilla JS
  • Type-safe: Full TypeScript support with type inference
  • Schema validation: Works with Zod, Valibot, ArkType, and more
  • Merge snapshots: Sync data between devices or users easily
  • Change events: Listen to data changes and integrate with your reactive system

Basic Usage

Creating a Store

A store holds one or more collections. Each collection has a schema that defines what data it can store.

import { createStore, collection } from "@byearlybird/starling";
import { z } from "zod";

const store = createStore({
  users: collection(
    z.object({
      id: z.string(),
      name: z.string(),
      email: z.string().optional(),
    }),
    (data) => data.id,
  ),
  notes: collection(
    z.object({
      id: z.string(),
      content: z.string(),
    }),
    (data) => data.id,
  ),
});

Adding Documents

Add new items to a collection using put():

store.put("users", {
  id: "1",
  name: "Alice",
  email: "[email protected]",
});

Or use transact() for atomic multi-collection operations:

store.transact((tx) => {
  tx.put("users", { id: "1", name: "Alice", email: "[email protected]" });
  tx.put("notes", { id: "n1", content: "Hello" });
});

Updating Documents

Update existing items using patch():

store.patch("users", "1", {
  email: "[email protected]",
});

Removing Documents

Remove items using remove():

store.remove("users", "1");

Reading Data

Read data directly from the store:

// Get a single item
const user = store.get("users", "1");

// Get all items as an array
const allUsers = store.list("users");

// You can easily derive other operations:
const userIds = allUsers.map((u) => u.id);
const hasUser = allUsers.some((u) => u.id === "1");

Listening to Changes

Subscribe to changes with subscribe():

// Subscribe to specific collections
const unsubscribe = store.subscribe(["users"], (event) => {
  console.log("Users collection changed:", event);
  const allUsers = store.list("users");
  // Update UI, invalidate queries, etc.
});

// Or subscribe to all changes
store.subscribe((event) => {
  console.log("Store changed:", event);
});

// Later, unsubscribe
unsubscribe();

Merging Data

Starling's core feature is conflict-free merging. When data changes in multiple places, Starling automatically resolves conflicts using timestamps.

Merge snapshots directly:

// Get current state as a snapshot
const snapshot = store.getState();

// Send to server or save locally
await sendToServer(snapshot);

// Later, merge a snapshot from another source
const remoteSnapshot = await fetchFromServer();
store.merge(remoteSnapshot);

Starling automatically resolves conflicts. If the same field was changed in both places, it keeps the change with the newer timestamp (Last-Write-Wins).

Reactivity Integration

Starling is framework-agnostic. Use subscribe() to integrate with your reactive system:

React with TanStack Query

import { useQuery, useQueryClient } from "@tanstack/react-query";

function useUsers() {
  const queryClient = useQueryClient();

  useEffect(() => {
    return store.subscribe(["users"], () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    });
  }, []);

  return useQuery({
    queryKey: ["users"],
    queryFn: () => store.list("users"),
  });
}

React with useSyncExternalStore

import { useSyncExternalStore } from "react";

function useUsers() {
  return useSyncExternalStore(
    (callback) => store.subscribe(["users"], () => callback()),
    () => store.list("users"),
  );
}

Svelte

import { writable } from "svelte/store";

const users = writable(store.list("users"));
store.subscribe(["users"], () => {
  users.set(store.list("users"));
});

Vue

import { ref } from "vue";

const users = ref(store.list("users"));
store.subscribe(["users"], () => {
  users.value = store.list("users");
});

Schema Support

Starling works with any library that follows the Standard Schema specification. This includes:

  • Zod - Most popular schema library
  • Valibot - Lightweight alternative
  • ArkType - TypeScript-first schemas

You can use any of these to define your data shapes. Starling will validate your data and give you full TypeScript types.

import { z } from "zod";
// or
import * as v from "valibot";
// or
import { type } from "arktype";

// All of these work the same way
const schema = z.object({ id: z.string(), name: z.string() });
// or
const schema = v.object({ id: v.string(), name: v.string() });
// or
const schema = type({ id: "string", name: "string" });

API Overview

Main Export

  • createStore(config) - Creates a new store with collections

Store Methods

  • get(collection, id) - Get a document by ID from a collection
  • list(collection) - Get all documents as an array from a collection
  • put(collection, data) - Insert or replace a document (upsert, revives tombstoned IDs)
  • patch(collection, id, data) - Partially update an existing document (throws if ID missing)
  • remove(collection, id) - Remove a document
  • transact(callback) - Execute operations atomically. Collections are cloned lazily on first access.
  • subscribe(callback) - Subscribe to all collection changes
  • subscribe(collections, callback) - Subscribe to changes in specific collections
  • getState() - Get current store state as a snapshot
  • merge(snapshot) - Merge a snapshot into the store

For full type definitions, see the TypeScript types exported from the package.

Package structure

  • lib/core/ – CRDT primitives: hybrid logical clock, per-field atoms (LWW), tombstones, and document/collection merging.
  • lib/store/ – Store API with collections, batching, queries, and change subscriptions.
  • lib/middleware/ – Optional middleware (e.g. persistence).

Development

# Install dependencies
bun install

# Build the library
bun run build

# Run tests
bun test

# Watch mode for development
bun run dev