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

@duyquangnvx/sliced-store

v1.0.0

Published

Centralized store with feature-owned slices

Readme

sliced-store

Centralized state management with feature-owned slices. Each feature declares its own typed state shape, defaults, and optional middleware. The central store merges everything but each feature only accesses its own scoped handle.

┌─────────────────────────────────────────────────┐
│                  SlicedStore                     │
│  ┌───────────┐ ┌───────────┐ ┌───────────────┐  │
│  │  wallet   │ │ freeSpin  │ │  bonusPick    │  │
│  │ ───────── │ │ ───────── │ │ ───────────── │  │
│  │ balance   │ │ remaining │ │ picks         │  │
│  │ currency  │ │ total     │ │ revealed      │  │
│  │ bet       │ │ multiplier│ │ totalWin      │  │
│  └───────────┘ └───────────┘ └───────────────┘  │
│                                                  │
│  onChange ──→ full state (all slices)            │
│  slice.onChange ──→ only that slice's data       │
└─────────────────────────────────────────────────┘

Install

npm install sliced-store

Quick start

import { SlicedStore, defineSlice } from 'sliced-store';

// 1. Define slices (each feature owns its shape)
const walletSlice = defineSlice('wallet', {
    defaults: { balance: 1000, bet: 1 },
});

const spinSlice = defineSlice('spin', {
    defaults: { remaining: 5, multiplier: 1 },
});

// 2. Create store and register
const store = new SlicedStore();
const wallet = store.register(walletSlice);
const spin = store.register(spinSlice);

// 3. Read and write through typed handles
wallet.get('balance');    // 1000 (typed as number)
wallet.set('bet', 10);
wallet.update({ balance: 500, bet: 5 });
wallet.getAll();          // { balance: 500, bet: 5 }

API

defineSlice(name, config)

Pure data declaration for a slice. No side effects.

const slice = defineSlice('wallet', {
    defaults: { balance: 1000, bet: 1 },
    middleware: [balanceGuard],  // optional
});

All defaults values must be structuredClone-able (no functions, Symbols, DOM nodes, etc.).

SlicedStore

register(definition)

Register a slice and get back a typed SliceHandle.

const wallet = store.register(walletSlice);

Throws if a slice with the same name is already registered.

slice(name)

Get a readonly handle to another slice for cross-feature reads. The returned object is Object.freezed with no set or update methods.

const walletView = store.slice<WalletState>('wallet');
walletView.get('balance');  // works
walletView.set('bet', 10);  // TypeError — property doesn't exist

batch(fn)

Batch multiple updates. All signals (field, slice, global) fire once at the end.

store.batch(() => {
    wallet.set('bet', 10);
    wallet.set('balance', 500);
    spin.set('remaining', 0);
});
// One set of signals fires here, not three

If fn throws, all state mutations are rolled back and no signals fire:

store.batch(() => {
    wallet.set('bet', 99);
    throw new Error('abort');
});
wallet.get('bet'); // still 1 — rolled back

Nested batch() calls run inline within the outer batch.

getFullState()

Returns a frozen object with all slices namespaced by name.

store.getFullState();
// { wallet: { balance: 1000, bet: 1 }, spin: { remaining: 5, ... } }

snapshot() / restore(data)

Deep clone state for save/load. restore fires field and slice signals.

const saved = store.snapshot();
// ... later
store.restore(saved);

Both methods are batch-aware — calling restore inside batch() defers signals.

resetState()

Reset all slices to their defaults. Subscriptions are preserved. Batch-aware.

reset()

Reset all slices to defaults and clear all subscriptions.

unregister(name)

Remove a slice and clear its subscriptions.

onChange

Global signal that fires when any slice changes. Payload is the full merged state.

store.onChange.add((fullState) => {
    console.log('Something changed:', fullState);
});

SliceHandle

The typed handle returned by register().

| Method | Description | |--------|-------------| | get(key) | Get a single field value (cloned for objects) | | getAll() | Get full slice state snapshot (deep clone) | | set(key, value) | Set a single field | | update(partial) | Merge partial state | | on(key, callback) | Subscribe to a field. Returns unsubscribe fn. Callback receives (value, prev) | | onChange | Signal that fires on any field change in this slice | | computed(fn) | Create a derived signal (see below) | | name | The slice name |

Field subscriptions

const unsub = wallet.on('balance', (value, prev) => {
    console.log(`Balance changed: ${prev} → ${value}`);
});

// Later
unsub();

Computed signals

Derived values that only emit when the computed result actually changes.

const isRich = wallet.computed((state) => state.balance > 500);

isRich.value;  // current value: true

isRich.add((val) => console.log('Rich status:', val));

// Clean up when done
isRich.dispose();

dispose() detaches from the source signal and clears all listeners. After disposal, add() and once() throw, emit() is a no-op.

Middleware

Middleware intercepts updates before they are applied. Each middleware receives the current state and incoming partial, and can transform or block the update.

const balanceGuard: SliceMiddleware<WalletState> = (current, incoming) => {
    // Prevent negative balance
    if (incoming.balance !== undefined && incoming.balance < 0) {
        return { ...incoming, balance: 0 };
    }
    return incoming;
};

// Return null to block the update entirely
const freezeBet: SliceMiddleware<WalletState> = (current, incoming) => {
    if (incoming.bet !== undefined) return null;
    return incoming;
};

const slice = defineSlice('wallet', {
    defaults: { balance: 1000, bet: 1 },
    middleware: [balanceGuard, freezeBet],  // runs in order
});

Middleware errors are wrapped with context (middleware name, slice name, original error as cause).

Signal

Lightweight pub/sub primitive used internally and exported for direct use.

import { Signal } from 'sliced-store';

const signal = new Signal<number>();

const binding = signal.add((value) => console.log(value));
signal.once((value) => console.log('First only:', value));
signal.emit(42);

binding.detach();  // or binding.dispose()
signal.clear();    // remove all listeners

Error isolation: if a listener throws, all remaining listeners still run. The first error is re-thrown after all listeners have been called.

Safety guarantees

  • Defensive cloning — all values are structuredCloned at boundaries (register, set, update, get, getAll, restore, snapshot, reset). External code cannot silently mutate internal store state.
  • Batch rollback — if batch() throws, all mutations are rolled back using deep snapshots. No signals fire.
  • Readonly handlesslice() returns a runtime-frozen object with no write methods.
  • Middleware isolation — errors include middleware name/index, slice name, and the original error as cause.
  • Signal error isolation — one throwing listener does not prevent others from running.
  • ComputedSignal guardsadd/once throw after disposal, emit is a no-op.

Development

npm run build        # build with tsup
npm test             # run tests with vitest
npm run test:watch   # watch mode

License

ISC