{"_id":"@duyquangnvx/sliced-store","_rev":"2-b736f1657427df3cc7b555b869f803b2","name":"@duyquangnvx/sliced-store","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@duyquangnvx/sliced-store","version":"1.0.0","keywords":[],"author":"","license":"ISC","_id":"@duyquangnvx/sliced-store@1.0.0","maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"homepage":"https://github.com/duyquangnvx/sliced-store#readme","bugs":{"url":"https://github.com/duyquangnvx/sliced-store/issues"},"dist":{"shasum":"5804da535169039419dd7776ca0881f817e4d6f9","tarball":"https://registry.npmjs.org/@duyquangnvx/sliced-store/-/sliced-store-1.0.0.tgz","fileCount":8,"integrity":"sha512-zYNLghae0QYQfeuAiE+EDSEyRBevlNJ5+CEn1R5l1zQCVzzcLl6wG6FhnNpevdmYo8BzYsjtifrPFLBp77szxA==","signatures":[{"sig":"MEUCIDIpTdkCiPHzpHjc5/PewMtY+TsA1fvoaI2LMRfytKfEAiEAlCo0qfrRkltgj/3A8yn2DXXjKyX5JfYK413qFmWlxSE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":94407},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"296e3fca49820eda9052ec8bc68e091d097cbddb","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"},"repository":{"url":"git+https://github.com/duyquangnvx/sliced-store.git","type":"git"},"_npmVersion":"10.9.2","description":"Centralized store with feature-owned slices","directories":{},"_nodeVersion":"22.16.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.0.18","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/sliced-store_1.0.0_1770799322017_0.8298093450546713","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@duyquangnvx/sliced-store","version":"1.0.1","description":"Centralized store with feature-owned slices","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest"},"repository":{"type":"git","url":"git+https://github.com/duyquangnvx/sliced-store.git"},"publishConfig":{"access":"public"},"keywords":[],"author":"","license":"ISC","bugs":{"url":"https://github.com/duyquangnvx/sliced-store/issues"},"homepage":"https://github.com/duyquangnvx/sliced-store#readme","devDependencies":{"tsup":"^8.5.1","typescript":"^5.9.3","vitest":"^4.0.18"},"_id":"@duyquangnvx/sliced-store@1.0.1","gitHead":"06ea1b30180faec272509ca0022fa3f63c862ba7","_nodeVersion":"22.16.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-i8NUUWaOacfxJW/wnHHdYlzXfFUnoijQC7RJG8P+KlYvlconZa0UPyKFsqBa+OtXD7mownm9wKdU458oBEd0FA==","shasum":"39ebd7ce71ce48f074d9925bc52336f5df6875b2","tarball":"https://registry.npmjs.org/@duyquangnvx/sliced-store/-/sliced-store-1.0.1.tgz","fileCount":8,"unpackedSize":94462,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDoC1BOJIrcIUhLaikofwmZbi9Anhctt5OFF/7ZD2Y9HAIgSHrqCRLkF+vTI4h4i6T6X9diMjxpbH5/qZsJzT2lpu8="}]},"_npmUser":{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"},"directories":{},"maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sliced-store_1.0.1_1771911087426_0.46929296052439495"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-11T08:42:01.914Z","modified":"2026-02-24T05:31:27.724Z","1.0.0":"2026-02-11T08:42:02.164Z","1.0.1":"2026-02-24T05:31:27.593Z"},"bugs":{"url":"https://github.com/duyquangnvx/sliced-store/issues"},"license":"ISC","homepage":"https://github.com/duyquangnvx/sliced-store#readme","keywords":[],"repository":{"type":"git","url":"git+https://github.com/duyquangnvx/sliced-store.git"},"description":"Centralized store with feature-owned slices","maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"readme":"# sliced-store\n\nCentralized 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.\n\n```\n┌─────────────────────────────────────────────────┐\n│                  SlicedStore                     │\n│  ┌───────────┐ ┌───────────┐ ┌───────────────┐  │\n│  │  wallet   │ │ freeSpin  │ │  bonusPick    │  │\n│  │ ───────── │ │ ───────── │ │ ───────────── │  │\n│  │ balance   │ │ remaining │ │ picks         │  │\n│  │ currency  │ │ total     │ │ revealed      │  │\n│  │ bet       │ │ multiplier│ │ totalWin      │  │\n│  └───────────┘ └───────────┘ └───────────────┘  │\n│                                                  │\n│  onChange ──→ full state (all slices)            │\n│  slice.onChange ──→ only that slice's data       │\n└─────────────────────────────────────────────────┘\n```\n\n## Install\n\n```bash\nnpm install @duyquangnvx/sliced-store\n```\n\n## Quick start\n\n```ts\nimport { SlicedStore, defineSlice } from '@duyquangnvx/sliced-store';\n\n// 1. Define slices (each feature owns its shape)\nconst walletSlice = defineSlice('wallet', {\n    defaults: { balance: 1000, bet: 1 },\n});\n\nconst spinSlice = defineSlice('spin', {\n    defaults: { remaining: 5, multiplier: 1 },\n});\n\n// 2. Create store and register\nconst store = new SlicedStore();\nconst wallet = store.register(walletSlice);\nconst spin = store.register(spinSlice);\n\n// 3. Read and write through typed handles\nwallet.get('balance');    // 1000 (typed as number)\nwallet.set('bet', 10);\nwallet.batch({ balance: 500, bet: 5 });\nwallet.getAll();          // { balance: 500, bet: 5 }\n```\n\n## API\n\n### `defineSlice(name, config)`\n\nPure data declaration for a slice. No side effects.\n\n```ts\nconst slice = defineSlice('wallet', {\n    defaults: { balance: 1000, bet: 1 },\n    middleware: [balanceGuard],  // optional\n});\n```\n\nAll `defaults` values must be `structuredClone`-able (no functions, Symbols, DOM nodes, etc.).\n\n### `SlicedStore`\n\n#### `register(definition)`\n\nRegister a slice and get back a typed `SliceHandle`.\n\n```ts\nconst wallet = store.register(walletSlice);\n```\n\nThrows if a slice with the same name is already registered.\n\n#### `slice(name)`\n\nGet a **readonly** handle to another slice. The returned object is `Object.freeze`d with read and subscribe methods only (`get`, `getAll`, `on`, `onChange`, `computed`, `name`) — no `set` or `batch`.\n\n```ts\nconst walletView = store.slice<WalletState>('wallet');\nwalletView.get('balance');  // works\nwalletView.set('bet', 10);  // TypeError — property doesn't exist\n```\n\n#### `batch(fn)`\n\nBatch multiple updates. All notifications (field, slice, global) fire once at the end.\n\n```ts\nstore.batch(() => {\n    wallet.set('bet', 10);\n    wallet.set('balance', 500);\n    spin.set('remaining', 0);\n});\n// One set of notifications fires here, not three\n```\n\nIf `fn` throws, all state mutations are **rolled back** and no notifications fire:\n\n```ts\nstore.batch(() => {\n    wallet.set('bet', 99);\n    throw new Error('abort');\n});\nwallet.get('bet'); // still 1 — rolled back\n```\n\nNested `batch()` calls run inline within the outer batch.\n\n#### `getState()`\n\nReturns a frozen, deep-cloned object with all slices namespaced by name.\n\n```ts\nstore.getState();\n// { wallet: { balance: 1000, bet: 1 }, spin: { remaining: 5, ... } }\n```\n\n#### `snapshot()` / `restore(data)`\n\nDeep clone state for save/load. `restore` fires field and slice notifications.\n\n```ts\nconst saved = store.snapshot();\n// ... later\nstore.restore(saved);\n```\n\nBoth methods are batch-aware — calling `restore` inside `batch()` defers notifications.\n\n#### `resetState()`\n\nReset all slices to their defaults. Subscriptions are preserved. Batch-aware.\n\n#### `reset()`\n\nReset all slices to defaults **and** clear all subscriptions (including store-level listeners).\n\n#### `unregister(name)`\n\nRemove a slice and clear its subscriptions.\n\n#### `has(name)`\n\nCheck if a slice is registered. Returns `boolean`.\n\n#### `sliceNames`\n\nGetter that returns an array of all registered slice names.\n\n```ts\nstore.sliceNames; // ['wallet', 'spin']\n```\n\n#### `onChange(listener)`\n\nSubscribe to changes across all slices. Callback receives the full merged state. Returns an unsubscribe function.\n\n```ts\nconst unsub = store.onChange((fullState) => {\n    console.log('Something changed:', fullState);\n});\n\n// Later\nunsub();\n```\n\n### `SliceHandle`\n\nThe typed handle returned by `register()`.\n\n| Method | Description |\n|--------|-------------|\n| `get(key)` | Get a single field value |\n| `getAll()` | Get full slice state snapshot (deep clone) |\n| `set(key, value)` | Set a single field. Returns `false` if rejected by middleware |\n| `batch(partial)` | Set multiple fields. Returns array of rejected keys |\n| `reset()` | Reset all fields to their defaults via `set()` |\n| `on(key, callback)` | Subscribe to a field. Returns unsubscribe fn. Callback receives `(value, prev)` |\n| `onChange(callback)` | Subscribe to any field change in this slice. Returns unsubscribe fn |\n| `computed(fn)` | Create a derived value (see below) |\n| `name` | The slice name |\n\n### Field subscriptions\n\n```ts\nconst unsub = wallet.on('balance', (value, prev) => {\n    console.log(`Balance changed: ${prev} → ${value}`);\n});\n\n// Later\nunsub();\n```\n\n### Computed values\n\nDerived values that only notify when the computed result actually changes.\n\n```ts\nconst isRich = wallet.computed((state) => state.balance > 500);\n\nisRich.value;  // current value: true\n\nconst unsub = isRich.onChange((val) => console.log('Rich status:', val));\n\n// Clean up when done\nisRich.dispose();\n```\n\n`dispose()` detaches from the source and clears all listeners. After disposal, `onChange()` throws and `isDisposed` returns `true`.\n\n### Middleware\n\nMiddleware intercepts updates before they are applied. Each middleware receives the current state and incoming partial, and can transform or block the update.\n\n```ts\nconst balanceGuard: Middleware<WalletState> = (current, incoming) => {\n    // Prevent negative balance\n    if (incoming.balance !== undefined && incoming.balance < 0) {\n        return { ...incoming, balance: 0 };\n    }\n    return incoming;\n};\n\n// Return null to block the update entirely\nconst freezeBet: Middleware<WalletState> = (current, incoming) => {\n    if (incoming.bet !== undefined) return null;\n    return incoming;\n};\n\nconst slice = defineSlice('wallet', {\n    defaults: { balance: 1000, bet: 1 },\n    middleware: [balanceGuard, freezeBet],  // runs in order\n});\n```\n\nMiddleware errors are wrapped with context (middleware name, slice name, original error as `cause`).\n\n## Safety guarantees\n\n- **Defensive cloning** — defaults are `structuredClone`d at registration. `getAll()`, `getState()`, `snapshot()`, `restore()`, and `resetState()` deep-clone at boundaries. External code cannot silently mutate internal store state through these methods.\n- **Batch rollback** — if `batch()` throws, all mutations are rolled back using deep snapshots. No notifications fire.\n- **Readonly handles** — `slice()` returns a runtime-frozen object with no write methods.\n- **Middleware isolation** — errors include middleware name/index, slice name, and the original error as `cause`.\n- **Computed guards** — `onChange()` throws after disposal, `isDisposed` indicates state.\n\n## Development\n\n```bash\nnpm run build        # build with tsup\nnpm test             # run tests with vitest\nnpm run test:watch   # watch mode\n```\n\n## License\n\nISC\n","readmeFilename":"README.md"}