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.1

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 @duyquangnvx/sliced-store

Quick start

import { SlicedStore, defineSlice } from '@duyquangnvx/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.batch({ 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. The returned object is Object.freezed with read and subscribe methods only (get, getAll, on, onChange, computed, name) — no set or batch.

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 notifications (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 notifications fires here, not three

If fn throws, all state mutations are rolled back and no notifications 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.

getState()

Returns a frozen, deep-cloned object with all slices namespaced by name.

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

snapshot() / restore(data)

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

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

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

resetState()

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

reset()

Reset all slices to defaults and clear all subscriptions (including store-level listeners).

unregister(name)

Remove a slice and clear its subscriptions.

has(name)

Check if a slice is registered. Returns boolean.

sliceNames

Getter that returns an array of all registered slice names.

store.sliceNames; // ['wallet', 'spin']

onChange(listener)

Subscribe to changes across all slices. Callback receives the full merged state. Returns an unsubscribe function.

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

// Later
unsub();

SliceHandle

The typed handle returned by register().

| Method | Description | |--------|-------------| | get(key) | Get a single field value | | getAll() | Get full slice state snapshot (deep clone) | | set(key, value) | Set a single field. Returns false if rejected by middleware | | batch(partial) | Set multiple fields. Returns array of rejected keys | | reset() | Reset all fields to their defaults via set() | | on(key, callback) | Subscribe to a field. Returns unsubscribe fn. Callback receives (value, prev) | | onChange(callback) | Subscribe to any field change in this slice. Returns unsubscribe fn | | computed(fn) | Create a derived value (see below) | | name | The slice name |

Field subscriptions

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

// Later
unsub();

Computed values

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

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

isRich.value;  // current value: true

const unsub = isRich.onChange((val) => console.log('Rich status:', val));

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

dispose() detaches from the source and clears all listeners. After disposal, onChange() throws and isDisposed returns true.

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: Middleware<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: Middleware<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).

Safety guarantees

  • Defensive cloning — defaults are structuredCloned at registration. getAll(), getState(), snapshot(), restore(), and resetState() deep-clone at boundaries. External code cannot silently mutate internal store state through these methods.
  • Batch rollback — if batch() throws, all mutations are rolled back using deep snapshots. No notifications 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.
  • Computed guardsonChange() throws after disposal, isDisposed indicates state.

Development

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

License

ISC