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

@chapeaux/cpx-store

v0.10.0

Published

Chapeaux Reactive Store Component

Readme

CPX Store

Reactive state management with a plugin architecture, signal-inspired computed properties, and a headless core that runs anywhere JavaScript runs.

License TypeScript Web Components

Features

  • Framework-Agnostic — Works with vanilla JavaScript, React, Vue, Lit, Svelte, or server-side code
  • Headless CoreCPXStoreCore runs in Node, Deno, Bun, and Cloudflare Workers with no DOM
  • Plugin Architecture — Opt into middleware, history, persistence, and collaboration — or use none
  • Signal-Inspired Reactivity — Auto-tracked computed properties with no dependency arrays
  • Typed Selectors and Snapshots — Subscribe to paths or selected immutable state slices
  • Framework Adapters — Concurrent-safe React binding with SSR support, plus Vue and Svelte adapters under 300 B each
  • Microtask-Coalesced Events — Multiple mutations in one tick produce one event
  • Batching and Transactionsbatch() for explicit grouping, transaction() for rollback on error
  • Nested State and Arrays — Deep mutations and array mutators with dot-path change tracking
  • History Strategies — Per-property undo/redo with snapshot, patch, or none strategies
  • Persistent Storage — Optional localStorage with cross-tab sync
  • Pluggable Collaboration — BroadcastChannel, WebSocket, SSE, and Solid transports with conflict resolution
  • Store Composition — Feature domains discover each other through the document or a BroadcastChannel, across bundle boundaries
  • Redux DevTools — Per-path diffs, mutation provenance, and time travel
  • Zustand Drop-Inzustand-compat runs existing Zustand code unmodified, so migration is incremental
  • Zero Core Dependencies — React and Vue are optional peers used only by their own entry points

Installation

# Using JSR (recommended)
deno add jsr:@chapeaux/cpx-store

# Using npm
npm install @chapeaux/cpx-store

Quick Start

Browser — Web Component

import { CPXStore } from "@chapeaux/cpx-store";
import { historyPlugin } from "@chapeaux/cpx-store/plugins/history";

interface AppState {
  count: number;
}

class AppStore extends CPXStore<AppState> {
  constructor() {
    super({ count: 0 }, historyPlugin());
  }
}

customElements.define("app-store", AppStore);
<app-store id="store"></app-store>

<script>
const store = document.querySelector("#store");

store.addEventListener("change", (e) => {
  const { changes } = e.detail;
  if (changes.count) {
    console.log(`count: ${changes.count.old} → ${changes.count.val}`);
  }
});

store.state.count++;
store.undo();
</script>

Server / CLI — Headless

import { CPXStoreCore } from "@chapeaux/cpx-store/cpx-store-core";
import { historyPlugin } from "@chapeaux/cpx-store/plugins/history";

const store = new CPXStoreCore({ count: 0 }, historyPlugin())
  .computed("doubled", (state) => state.count * 2);

store.state.count = 5;
console.log(store.state.doubled); // 10

store.undo();
console.log(store.state.count); // 0

CPXStoreCore initializes immediately in the constructor. No DOM, no connectedCallback, no customElements.define. Same plugins, same API.

Constructor

// Browser
new CPXStore(initialState, ...plugins);

// Headless
new CPXStoreCore(initialState, ...plugins);
  • initialState — Primitives, plain objects, and arrays; class instances, cycles, and shared object references are rejected
  • ...plugins — Zero or more StorePlugin instances

CPXStoreCore infers its state type from initialState. Web Component subclasses should provide an explicit type, such as CPXStore<AppState>. Misspelled keys and invalid values then fail during type checking instead of silently becoming new state properties.

Plugins

Middleware

Runs before each mutation. Can throw to cancel.

import { middlewarePlugin } from "@chapeaux/cpx-store/plugins/middleware";

middlewarePlugin([
  // Bare function — runs on every mutation
  (prop, val, oldVal) => console.log(`${prop}: ${oldVal} → ${val}`),

  // Filtered — only runs for matching properties
  { filter: /^editor\./, fn: (prop, val) => validate(val) },
  {
    filter: "count",
    fn: (prop, val) => {
      if (val < 0) throw new Error("negative");
    },
  },
]);

Filters accept a string (exact match or prefix), a RegExp, or a predicate function.

History

Undo/redo with configurable per-property strategies.

import { historyPlugin } from "@chapeaux/cpx-store/plugins/history";

historyPlugin({
  maxHistory: 100, // default 100
  defaultStrategy: "snapshot", // default
  strategies: {
    content: "patch", // store text diffs, not full copies
    cursor: "none", // exclude from history entirely
  },
  checkpointInterval: 20, // full snapshot every 20 patch ops
});

| Strategy | Stores | Use Case | | ---------- | ---------------------------- | ------------------------------ | | snapshot | Full old + new values | Small values (default) | | patch | Text diffs or JSON Patch ops | Large strings, objects | | none | Nothing | Cursor position, scroll offset |

Methods added to the store by this plugin: undo(), redo(), historyStrategy(prop, strategy), checkpoint(), clearHistory().

Persistence

localStorage save/restore with cross-tab sync via storage events.

import { persistencePlugin } from "@chapeaux/cpx-store/plugins/persistence";

// Browser — reads key from the persist HTML attribute
persistencePlugin();

// Headless or explicit key
persistencePlugin({ key: "my-app-state" });

// Persist only durable fields
persistencePlugin({
  key: "my-app-state",
  partialize: (state) => ({
    theme: state.theme,
    preferences: state.preferences,
  }),
});
<app-store persist="my-app-state"></app-store>

The plugin merges persisted fields over initial defaults, writes once per flush (not per mutation), and listens for storage events from other tabs. Fields omitted by partialize remain local and are not overwritten by cross-tab updates. In environments without localStorage, storage operations are silently skipped.

Collaboration

Pluggable sync transport with operation log and conflict resolution.

import { collabPlugin } from "@chapeaux/cpx-store/plugins/collab";
import { BroadcastChannelTransport } from "@chapeaux/cpx-store/transports/broadcast-channel";
import { WebSocketTransport } from "@chapeaux/cpx-store/transports/websocket";
import { SSETransport } from "@chapeaux/cpx-store/transports/sse";
import { SolidTransport } from "@chapeaux/cpx-store/transports/solid";

// Same-origin tab sync
collabPlugin({ transport: new BroadcastChannelTransport("my-channel") });

// Multi-user sync with automatic reconnection
collabPlugin({ transport: new WebSocketTransport("wss://example.com/sync") });

// Server-sent events (receive via EventSource, send via POST)
collabPlugin({
  transport: new SSETransport("/api/events", { apiUrl: "/api/state" }),
});

// Receive-only SSE (no outbound POST)
collabPlugin({ transport: new SSETransport("/api/events") });

// Decentralized sync via a Solid pod
collabPlugin({
  transport: new SolidTransport("https://pod.example.org/apps/state.json", {
    fetch: authenticatedFetch, // from @inrupt/solid-client-authn-browser or similar
  }),
});

// Custom conflict resolution
collabPlugin({
  transport: new WebSocketTransport("wss://example.com/sync"),
  resolver: {
    resolve(local, remote) {
      return remote.timestamp >= local.timestamp ? remote : local;
    },
  },
});

Methods added: getOperationLog(), disconnect().

Computed Properties

No dependency array — dependencies are auto-tracked during evaluation.

const cartStore = new CPXStoreCore({ price: 10, qty: 2 })
  .computed("total", (state) => state.price * state.qty);

cartStore.state.price = 5;
console.log(cartStore.state.total); // recomputed automatically

Conditional dependencies work correctly:

store.computed("value", (state) => {
  return state.useA ? state.a : state.b;
});
// Only tracks state.a when state.useA is true

Transitive dependencies chain automatically:

const doubledStore = new CPXStoreCore({ base: 2 })
  .computed("doubled", (state) => state.base * 2);
const store = doubledStore
  .computed("quadrupled", (state) => state.doubled * 2);

Computed values are included in change sets when their output changes, so path and selector subscriptions can observe them directly. They are readonly and cannot collide with stored keys.

Batching and Transactions

Multiple mutations in the same tick are coalesced into one event by default. For explicit control:

// Synchronous flush at end of block
store.batch(() => {
  store.state.a = 1;
  store.state.b = 2;
}); // one event

// Rollback on error
store.transaction(() => {
  store.state.balance -= 100;
  if (store.state.balance < 0) throw new Error("insufficient");
}); // state unchanged, no event

// Async action; only the synchronous commit is batched
await store.dispatch(async () => {
  const response = await fetch("/api");
  const items = await response.json();
  store.transaction(() => {
    store.state.items = items;
    store.state.loading = false;
  });
});

dispatch() does not hold a global batch open across await. This prevents unrelated writes and concurrent actions from blocking each other. Use a synchronous batch() or transaction() around the final state commit when one notification is required.

Nested State

Deep object properties are accessible through recursive Proxies:

const store = new CPXStoreCore({
  editor: {
    file1: { content: "hello", dirty: false },
    file2: { content: "world", dirty: true },
  },
});

store.state.editor.file1.content = "updated";
// Change tracked as prop: "editor.file1.content"

store.state.items.push({ id: "new" });
// Array mutators replace the containing array and report prop: "items"

Each nested path gets its own reactive signal, so computed values that read state.editor.file1.content are not invalidated when file2 changes.

Events

Browser (CPXStore)

store.addEventListener("change", (e) => {
  const { changes } = e.detail;
  // changes is an object: { propName: { old, val }, ... }
  for (const [prop, { old, val }] of Object.entries(changes)) {
    console.log(`${prop}: ${old} → ${val}`);
  }
});

A global app-state-update event is also dispatched on window with { store: tagName, changes }.

Headless (CPXStoreCore)

const unsub = store.onChange((changes) => {
  // changes is a Map<string, { old, val }>
  for (const [prop, { old, val }] of changes) {
    console.log(`${prop}: ${old} → ${val}`);
  }
});

// Only runs for this path, its ancestors, or its descendants
const unsubPath = store.onChange(handler, { paths: ["editor.file1.content"] });

// Selector subscription with equality filtering
const unsubSelector = store.subscribe(
  (snapshot) => snapshot.editor.file1.content,
  (content, previous) => console.log(content, previous),
);

// Later:
unsub();

getSnapshot() returns an immutable, identity-stable snapshot. A new snapshot is published once per flush and structurally shares unchanged branches.

React

import { useCPXStore } from "@chapeaux/cpx-store/react";

function Counter({ store }) {
  const count = useCPXStore(store, (state) => state.count);
  return <button onClick={() => store.state.count++}>{count}</button>;
}

Selectors run against immutable snapshots, so object and array selections have stable identities. Pass equalityFn for selectors that intentionally return fresh objects. For SSR, create one store per request and pass getServerSnapshot when the server snapshot differs from store.getSnapshot().

Vue

import { useCPXStore } from "@chapeaux/cpx-store/vue";

const count = useCPXStore(store, (state) => state.count);

Returns a shallowRef — snapshots are already deep-frozen and structurally shared, so Vue's deep reactivity would walk a tree that cannot change in place. The subscription is released automatically when the surrounding effect scope is disposed; outside a component, call count.stop().

Svelte

<script>
  import { toSvelteStore } from "@chapeaux/cpx-store/svelte";
  const count = toSvelteStore(store, (state) => state.count);
</script>

<button on:click={() => store.state.count++}>{$count}</button>

A plain readable store with no framework import, so $-prefixed auto-subscription works and the adapter adds 218 B. The subscriber runs only when the selected slice changes.

Migrating from Zustand

@chapeaux/cpx-store/zustand-compat implements the vanilla Zustand surface over a real CPX store, so an existing codebase moves one module at a time rather than in a single rewrite:

- import { createStore } from "zustand/vanilla";
+ import { createStore } from "@chapeaux/cpx-store/zustand-compat";

const useBearStore = createStore((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

useBearStore.getState().increase(3);

Everything the Zustand surface cannot express is one property away — the real store is on .cpx, so you can adopt computed values, transactions and path-filtered subscriptions incrementally:

useBearStore.cpx.computed("crowd", (state) => state.bears > 10);
useBearStore.cpx.onChange((changes) => …, { paths: ["bears"] });

Two behavioural differences, stated loudly because silent ones are worse:

  • setState flushes inline. Zustand notifies subscribers before setState returns; CPX normally coalesces writes into a microtask. The shim preserves Zustand's timing, so a setState loop pays per-call subscriber cost exactly as it does today. Drop the shim for a given store to get the coalescing back.
  • Actions live on the store, not in the state tree. CPX state holds only serialisable values, so functions in the initializer are kept beside the state rather than inside it. They remain reachable from getState(), but they do not appear in cpx.getSnapshot() — which is what makes snapshots structurally comparable and JSON-serialisable.

You do not need an immer equivalent: direct mutation is the API, and the immutable snapshot is produced for you.

Inspector

<script type="module">
import "@chapeaux/cpx-store/inspector";
</script>

<app-store id="store"></app-store>
<cpx-store-inspector for="store"></cpx-store-inspector>

Renders tracked paths, current values, subscriber counts, dependent computeds and the flush count. It is a separate entry point and is never imported by the core.

Sync

Apply remote state without triggering outbound sync:

store.sync({ count: 42, theme: "dark" });

Override onSyncReceived for side effects:

class MyStore extends CPXStore {
  onSyncReceived(newState, oldState) {
    if (newState.theme !== oldState.theme) {
      document.body.className = newState.theme;
    }
  }
}

SSR Hydration

// Server
import { CPXStoreCore } from "@chapeaux/cpx-store/cpx-store-core";

const store = new CPXStoreCore({ user: null, items: [] });
store.state.user = await db.getUser(sessionId);
store.state.items = await db.getItems(store.state.user.id);

const html = `<script>window.__STATE__ = ${
  JSON.stringify(store.toJSON())
}</script>`;
// Client
import { CPXStore } from "@chapeaux/cpx-store";
import { historyPlugin } from "@chapeaux/cpx-store/plugins/history";

class MyStore extends CPXStore {
  constructor() {
    super(window.__STATE__, historyPlugin());
  }
}
customElements.define("my-store", MyStore);

Working with Large Data Structures

The nested proxy system creates signals only for paths read while a computed is tracking dependencies; ordinary read-only traversal no longer grows the signal map. Very large or high-churn collections can still be better served by a purpose-built structure plus a version counter.

The recommended pattern: store a version counter in the proxied state, and keep the heavy data structure outside the proxy. Computed values and change handlers react to the counter bump, then read from the external structure directly.

class IDEStore extends CPXStore {
  fileTree = new FileTree();
  diagnostics = new Map();

  constructor() {
    super(
      {
        fileTreeVersion: 0,
        diagnosticVersion: 0,
        selectedFile: null,
        openTabs: [],
        theme: "dark",
      },
      historyPlugin({
        strategies: {
          fileTreeVersion: "none",
          diagnosticVersion: "none",
        },
      }),
      persistencePlugin(),
    );
  }

  // One proxy write per batch of tree changes
  updateFileTree(changes) {
    this.fileTree.applyBatch(changes);
    this.state.fileTreeVersion++;
  }

  // One proxy write per diagnostic update
  setDiagnostics(uri, entries) {
    this.diagnostics.set(uri, entries);
    this.state.diagnosticVersion++;
  }
}

Computed values that depend on the version counter re-evaluate when it bumps:

store.computed("errorCount", () => {
  store.state.diagnosticVersion; // subscribe to changes
  let count = 0;
  for (const entries of store.diagnostics.values()) {
    count += entries.filter((d) => d.severity === "error").length;
  }
  return count;
});

The store manages coordination state (what is selected, what is open, what version are we on). The heavy data lives in purpose-built structures that are optimized for their specific access patterns. Change events tell the UI that something changed; the UI reads the external structure to find out what.

This mirrors how production applications use Redux or Zustand: you store IDs and metadata in the state tree, not the full dataset. The difference is that cpx-store makes this explicit through the version-counter pattern rather than hiding it behind normalization libraries.

Project Structure

cpx-store/
├── src/
│   ├── cpx-store-core.ts         # Headless core (CPXStoreCoreMixin + CPXStoreCore)
│   ├── cpx-store.ts              # Web Component wrapper (CPXStore)
│   ├── reactivity.ts             # ReactiveState, ReactiveComputed
│   ├── react.ts                  # useSyncExternalStore React adapter
│   ├── vue.ts                    # shallowRef adapter
│   ├── svelte.ts                 # Readable-store adapter (no framework import)
│   ├── zustand-compat.ts         # Vanilla Zustand surface over a CPX store
│   ├── composition.ts            # Store hub, atomic multi-store transactions
│   ├── inspector.ts              # <cpx-store-inspector> element
│   ├── types.ts                  # StorePlugin, SyncTransport, StateOperation
│   ├── plugins/
│   │   ├── middleware.ts          # Filterable middleware
│   │   ├── history.ts            # Undo/redo with strategies
│   │   ├── persistence.ts        # localStorage + cross-tab sync
│   │   ├── devtools.ts           # Redux DevTools protocol
│   │   └── collab.ts             # Collaboration transport
│   ├── transports/
│   │   ├── broadcast-channel.ts   # BroadcastChannel transport
│   │   ├── websocket.ts          # WebSocket transport with reconnection
│   │   ├── sse.ts                # SSE transport with reconnection
│   │   ├── solid.ts             # Solid pod transport with Notifications
│   │   └── element-mesh.ts       # DOM-discovered mesh transport
│   ├── utils/
│   │   ├── nested-proxy.ts       # Recursive Proxy factory
│   │   ├── immutable.ts          # Cloning, frozen snapshots, structural sharing
│   │   └── json-patch.ts         # RFC 6902 diff/apply
│   └── stores/
│       └── cpx-scheme-store.ts   # Example store
├── test/
│   ├── cpx-store.spec.ts         # Core browser tests
│   ├── cpx-store-core.test.ts    # Headless Deno tests
│   ├── hardening.test.ts         # Correctness and regression tests
│   ├── perf-budget.test.ts       # Blocking performance gate (operation counts)
│   ├── adapters.test.ts          # Zustand-compat and Svelte adapter tests
│   ├── composition.test.ts       # Hub, bridge, and DevTools tests
│   ├── react.test.ts             # React selector and lifecycle tests
│   ├── nested-state.spec.ts      # Nested proxy tests
│   ├── history-strategies.spec.ts # History strategy tests
│   ├── collab.spec.ts            # Collaboration tests
│   ├── sse.spec.ts               # SSE transport tests
│   └── scheme-store.spec.ts      # Example store tests
├── demo/                         # Demo application
├── deno.json
├── tsconfig.json
└── web-test-runner.config.mjs

Development

deno install               # Install dependencies
deno task test:browser     # Run browser tests (Chromium + Firefox)
deno task test:logic       # Run headless and React tests
deno task test:types       # Compile public type-contract tests
deno task test:watch       # Watch mode
deno task build            # Build
deno task serve            # Dev server

Browser Support

  • Chrome/Edge 54+
  • Firefox 63+
  • Safari 10.1+

License

SEE LICENSE IN LICENSE

Author

Luke Dary[email protected]lukedary.com