{"_id":"@askrubberduck/redux-kit","_rev":"3-3f4818d42d0ac6b604f437b758714ad0","name":"@askrubberduck/redux-kit","dist-tags":{"latest":"0.7.0"},"versions":{"0.5.0":{"name":"@askrubberduck/redux-kit","version":"0.5.0","author":{"name":"askrubberduck"},"license":"MIT","_id":"@askrubberduck/redux-kit@0.5.0","maintainers":[{"name":"minuscle","email":"npm@askrubberduck.dev"}],"homepage":"https://github.com/askrubberduck/lib.redux-kit#readme","bugs":{"url":"https://github.com/askrubberduck/lib.redux-kit/issues"},"dist":{"shasum":"334cc5ac5ef12648159d3a5f7d93d40bb64f5e6e","tarball":"https://registry.npmjs.org/@askrubberduck/redux-kit/-/redux-kit-0.5.0.tgz","fileCount":84,"integrity":"sha512-AK4ALxVUjsGFrKV7nTDQ+GqDHSOcrDrZpBVUAGsKsYkTT00CUflyeg2wayIDeuBxvvT9GOp6HK2M5pta5azQsA==","signatures":[{"sig":"MEUCIFBqDtWHi1DVnfwP3mtPwXePEJqXM2M3eZYtuol7J/iQAiEAj2L33nMyU4QqnUPYA9vdY7aGZ7Uf9okjG/pkjaukzPg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@askrubberduck%2fredux-kit@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":151985},"main":"output/index.js","types":"output/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./output/index.d.ts","default":"./output/index.js"},"./react":{"types":"./output/react.d.ts","default":"./output/react.js"},"./store":{"types":"./output/store.d.ts","default":"./output/store.js"},"./testing":{"types":"./output/testing.d.ts","default":"./output/testing.js"},"./package.json":"./package.json"},"gitHead":"422615253eb66fbd83da38803f52454a0389ee25","scripts":{"test":"mocha","build":"tsc","clean":"rm -rf output","prepare":"npm run build","pretest":"npm run check:types","check:types":"tsc -p test/tsconfig.json --noEmit","check:circular":"dpdm --no-warning --no-tree --exit-code circular:1 './src/**/*.ts'","prepublishOnly":"npm run build"},"_npmUser":{"name":"minuscle","email":"npm@askrubberduck.dev"},"repository":{"url":"git+https://github.com/askrubberduck/lib.redux-kit.git","type":"git"},"_npmVersion":"10.9.8","description":"A small toolkit for redux stores: reducers, effects, watchers, subscriptions, local stores and action versioning.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"setimmediate":"^1.0.5"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.11","chai":"^6.2.2","dpdm":"^4.3.0","mocha":"^11.8.0","react":"^18.3.1","redux":"^5.0.1","lodash":"^4.18.1","prettier":"^3.9.6","typescript":"^7.0.2","@types/chai":"^5.2.3","@types/node":"^20","react-redux":"^9.3.0","@types/mocha":"^10.0.10","@types/react":"^18","@types/lodash":"^4.17.25","@types/setimmediate":"^1.0.4"},"peerDependencies":{"react":">=17","redux":">=4","lodash":"^4.17.21","react-redux":">=8"},"peerDependenciesMeta":{"react":{"optional":true},"redux":{"optional":true},"react-redux":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/redux-kit_0.5.0_1786364137790_0.6638735931412798","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@askrubberduck/redux-kit","version":"0.6.0","author":{"name":"askrubberduck"},"license":"MIT","_id":"@askrubberduck/redux-kit@0.6.0","maintainers":[{"name":"minuscle","email":"npm@askrubberduck.dev"}],"homepage":"https://github.com/askrubberduck/lib.redux-kit#readme","bugs":{"url":"https://github.com/askrubberduck/lib.redux-kit/issues"},"dist":{"shasum":"fe457fb98035b2c7dcf5b43e8eb072959a4c90dd","tarball":"https://registry.npmjs.org/@askrubberduck/redux-kit/-/redux-kit-0.6.0.tgz","fileCount":92,"integrity":"sha512-R+tyr9Y+trfykAWzXDzA3DlKpdLhxZDMwK+4ZrORGQGTZEx7zNXZGuHW6D2UH4xVUDkdTOWBE3X8EX9zXx3MUA==","signatures":[{"sig":"MEYCIQC7Dq/Boz+LzvwEj4uMOomko2MdmwYWUoCapq5pVZRWSAIhAJ/vNOR9i/dGwHyBD1kkOr0mE4JGlviCP2Ct23ubmL+n","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@askrubberduck%2fredux-kit@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":169229},"main":"output/index.js","types":"output/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./output/index.d.ts","default":"./output/index.js"},"./react":{"types":"./output/react.d.ts","default":"./output/react.js"},"./redux":{"types":"./output/redux.d.ts","default":"./output/redux.js"},"./store":{"types":"./output/store.d.ts","default":"./output/store.js"},"./testing":{"types":"./output/testing.d.ts","default":"./output/testing.js"},"./package.json":"./package.json"},"gitHead":"ffe7849c2a0f9b8e33ae7ccef504f70d828d7d1e","scripts":{"test":"mocha","build":"tsc","clean":"rm -rf output","prepare":"npm run build","pretest":"npm run check:types","check:types":"tsc -p test/tsconfig.json --noEmit","check:circular":"dpdm --no-warning --no-tree --exit-code circular:1 './src/**/*.ts'","prepublishOnly":"npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4a44bae1-4de7-4dfb-b0ee-a53a85a30134"}},"repository":{"url":"git+https://github.com/askrubberduck/lib.redux-kit.git","type":"git"},"_npmVersion":"11.16.0","description":"A small toolkit for redux stores: reducers, effects, watchers, subscriptions, local stores and action versioning.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"dequal":"^2.0.3"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.11","chai":"^6.2.2","dpdm":"^4.3.0","mocha":"^11.8.0","react":"^18.3.1","redux":"^5.0.1","prettier":"^3.9.6","typescript":"^7.0.2","@types/chai":"^5.2.3","@types/node":"^20","react-redux":"^9.3.0","@types/mocha":"^10.0.10","@types/react":"^18"},"peerDependencies":{"react":">=17","redux":">=4","react-redux":">=8"},"peerDependenciesMeta":{"react":{"optional":true},"redux":{"optional":true},"react-redux":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/redux-kit_0.6.0_1786371723938_0.4311255397824081","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@askrubberduck/redux-kit","version":"0.7.0","description":"A small toolkit for redux stores: reducers, effects, watchers, subscriptions, local stores and action versioning.","author":{"name":"askrubberduck"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/askrubberduck/lib.redux-kit.git"},"publishConfig":{"access":"public","provenance":true},"main":"output/index.js","types":"output/index.d.ts","exports":{".":{"types":"./output/index.d.ts","default":"./output/index.js"},"./react":{"types":"./output/react.d.ts","default":"./output/react.js"},"./redux":{"types":"./output/redux.d.ts","default":"./output/redux.js"},"./package.json":"./package.json","./testing":{"types":"./output/testing.d.ts","default":"./output/testing.js"},"./store":{"types":"./output/store.d.ts","default":"./output/store.js"},"./zustand":{"types":"./output/zustand.d.ts","default":"./output/zustand.js"}},"engines":{"node":">=20"},"scripts":{"build":"tsc","clean":"rm -rf output","prepare":"npm run build","check:circular":"dpdm --no-warning --no-tree --exit-code circular:1 './src/**/*.ts'","pretest":"npm run check:types","test":"mocha","prepublishOnly":"npm run build","check:types":"tsc -p test/tsconfig.json --noEmit"},"dependencies":{"dequal":"^2.0.3"},"peerDependencies":{"react":">=17","react-redux":">=8","redux":">=4","zustand":">=5"},"peerDependenciesMeta":{"react":{"optional":true},"react-redux":{"optional":true},"redux":{"optional":true},"zustand":{"optional":true}},"devDependencies":{"@types/chai":"^5.2.3","@types/mocha":"^10.0.10","@types/node":"^20","@types/react":"^18","chai":"^6.2.2","dpdm":"^4.3.0","mocha":"^11.8.0","prettier":"^3.9.6","react":"^18.3.1","react-redux":"^9.3.0","redux":"^5.0.1","tsx":"^4.23.11","typescript":"^7.0.2","zustand":"^5.0.14"},"gitHead":"807dd64585121472c652de99902fd1e566b69099","_id":"@askrubberduck/redux-kit@0.7.0","bugs":{"url":"https://github.com/askrubberduck/lib.redux-kit/issues"},"homepage":"https://github.com/askrubberduck/lib.redux-kit#readme","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-+8ZoUlbK8sWWSsGaoSWpL2Qp5Br4RfcLBH42L+fGrWD4aWi1emeutJbjT0vYhaQgV0UoBfjTIZZQxl4EyGm3oA==","shasum":"4bb495787815d31ed6d006488f6f69d6bca51c41","tarball":"https://registry.npmjs.org/@askrubberduck/redux-kit/-/redux-kit-0.7.0.tgz","fileCount":100,"unpackedSize":186519,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@askrubberduck%2fredux-kit@0.7.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDn26jtDjxNAB+UvBjM9yKTW74QGLL32bo90OnWeg+bygIgTDop1yrvaBFkY+X9uubSRw4IlicG/mvp0TFtLio5aJw="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:4a44bae1-4de7-4dfb-b0ee-a53a85a30134"}},"directories":{},"maintainers":[{"name":"minuscle","email":"npm@askrubberduck.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/redux-kit_0.7.0_1786373685501_0.277338469952374"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-10T12:15:37.633Z","modified":"2026-08-10T14:54:45.973Z","0.5.0":"2026-08-10T12:15:37.924Z","0.6.0":"2026-08-10T14:22:04.093Z","0.7.0":"2026-08-10T14:54:45.651Z"},"bugs":{"url":"https://github.com/askrubberduck/lib.redux-kit/issues"},"author":{"name":"askrubberduck"},"license":"MIT","homepage":"https://github.com/askrubberduck/lib.redux-kit#readme","repository":{"type":"git","url":"git+https://github.com/askrubberduck/lib.redux-kit.git"},"description":"A small toolkit for redux stores: reducers, effects, watchers, subscriptions, local stores and action versioning.","maintainers":[{"name":"minuscle","email":"npm@askrubberduck.dev"}],"readme":"# @askrubberduck/redux-kit\n\nA layer over redux that collapses one feature's moving parts — its reducers, the async work its\nactions trigger, its recomputed values, its state watchers and its error handling — into a single\nobject called a **domain**, and runs that object in either of two places: the application's redux\nstore, or one React component for as long as it is mounted.\n\nIt is not a redux replacement. The store host is redux, `createStore` returns a redux store, and\ndevtools and `react-redux`'s `Provider` take it unchanged.\n\n## What it solves\n\n**A feature is spread across files that have to agree, and nothing enforces it.** In a plain redux\napp one feature is a reducer, an action union, thunks or sagas registered somewhere else,\nselectors, and an entry in the root reducer map. Miss the last one and the code still compiles and\nstill runs — the feature is just silently inert. A domain carries its own name and the store builds\nthe state shape from the domains it is handed, so there is no map to fall out of sync.\n\n**Reducers spread state to signal a change, and then components re-render for no reason.** A fold\nhere returns a *partial*, which is diffed against current state before merging; when nothing\nactually differs the existing state object comes back by reference, so `shallowEqual` selectors do\nnot re-render. Returning `undefined` means \"leave it alone\" and `{}` is an explicit no-op. The\nmemoization you would otherwise hand-write is a property of the fold.\n\n**Async work lives away from the state it changes.** `on` registers a plain async function against\nan action type, in the same definition as the `reduce` for that type. No generators, no effect DSL,\nno separate middleware to register per feature. It receives `(action, dispatch, state)` and runs\nafter the fold.\n\n**State belonging to one screen has no good home.** A global slice outlives the screen and needs a\nreset action someone will forget; `useReducer` plus `useEffect` is a second architecture with no\nrecomputed values and no error path. `useDomain` hosts the same domain definition on a private bus,\nseeded per mount, discarded on unmount.\n\n**Dependent async work has no ordering primitive.** `await dispatch(...)` resolves once the entire\nchain that action set off has settled — every fold, and every reaction those dispatches triggered\nin turn. Actions nobody reacts to still settle, so an await never hangs.\n\n**Long-lived streams have a lifecycle someone has to own.** Subscriptions are opened per key with\nvariables, deduplicated, force-re-subscribed on a transport error or an error payload, and closed\non completion — by the store, not by a component's effect. Socket buses bind once at startup and\nroute by message type.\n\n**Slow responses overwrite fast ones.** Every action carries a monotonic `__version`, and a\nreaction's dispatches carry the version of the action that caused them, so a reduce can reject a\nresponse that belongs to an intent the user has already superseded. This is a protocol you opt into\nper feature, not something that happens automatically — see\n[reference/versioning.md](skills/redux-kit/reference/versioning.md).\n\n**Where it does not help:** there is no cancellation. Nothing here aborts in-flight work — no\n`takeLatest`, no `race`, no `AbortSignal` plumbing. The version stamp lets a late response be\n*ignored*; it does not stop it. For HTTP request/response, a query library that cancels is the\nbetter tool, and the two compose fine.\n\nAnything that feeds actions in from outside — a socket, a GraphQL subscription, an analytics tap —\nis an **adapter**, covered further down.\n\nTwo differences from redux-toolkit worth knowing before you read the examples: there is no immer,\nso a fold returns a partial rather than mutating a draft, and there are no generated action\ncreators — you write the action union and dispatch plain objects.\n\n## Install\n\n```bash\nnpm install @askrubberduck/redux-kit\n```\n\nThere are no required peers, and one dependency: `dequal`, for the structural comparison a fold and\na watcher both need. The root entry pulls in neither redux nor React. Add `react` for `/react`,\n`react-redux` for `/redux`, and `redux` for `/store` — each is an optional peer, declared so npm\ndoes not install one you never import.\n\n## A domain\n\n```ts\nimport { defineDomain } from '@askrubberduck/redux-kit'\n\nexport const cart = defineDomain<CartState, CartAction, RootState, RootAction>({\n  name: 'cart',\n  initial: { items: [], total: 0, itemCount: 0, isPending: false },\n})\n\n// a fold — (state, action), matching redux's own reducer signature. Returns a partial.\ncart.reduce('CART_ADD_ITEM', (state, action) => ({ items: [...state.items, action.item] }))\n\n// recomputed after every fold that changed something\ncart.recompute(state => ({ itemCount: state.items.length }))\n\n// async work for an action type — (action, dispatch, state), state last and always passed.\n// Store-hosted, `state` is the root state, which is what the last two generics declare.\ncart.on('CART_PRICING_REQUEST', async (action, dispatch, state) => {\n  const { total } = await api.priceCart(state.cart.items)\n  dispatch({ type: 'CART_PRICING_RESPONSE', total })\n})\n\n// reacts to a state change rather than an action. The first argument is a registration key,\n// unique across the host — it is not a state path.\ncart.watch('cart:pricing', s => ({ total: s.cart.total }), (next, previous, dispatch) => { ... })\n\ncart.onError(async (action, dispatch, error) => { ... })\n```\n\n## The two hosts\n\n```ts\n// a store — many domains, one shared action bus, session lifetime\nimport { createStore } from '@askrubberduck/redux-kit/store'\n\nexport const store = createStore<RootState, RootAction>({\n  domains: [cart, session],\n  middleware: reactions => [...reactions, analyticsMiddleware],\n})\n```\n\n```tsx\n// a component — one domain, a private bus, mount lifetime\nimport { useDomain } from '@askrubberduck/redux-kit/react'\n\nconst [state, dispatch] = useDomain(panel)\n```\n\nA domain never knew which host it was under, so redux is not the only store it can run in. `/zustand`\nhosts the same definitions over a zustand store — same middleware order, same watcher semantics, same\n`await dispatch(...)`:\n\n```ts\nimport { createZustandStore } from '@askrubberduck/redux-kit/zustand'\nimport { useStore } from 'zustand'\n\nexport const store = createZustandStore<RootState, RootAction>({ domains: [cart, session] })\n\n// a real StoreApi, so no Provider and no selector library\nconst total = useStore(store, s => s.cart.total)\n```\n\nOne behaviour genuinely differs, in this host's favour: zustand skips notifying subscribers when\n`setState` produces a reference-equal state, and a fold already returns the existing state when a\npartial changed nothing — so an action nobody reduced re-renders nothing at all, without a\n`shallowEqual` anywhere. Under redux every dispatch notifies and the selector comparison is what\nstops the render.\n\nWhat you give up is redux's ecosystem: no devtools, no `enhancer` option, no `react-redux`.\n\nFolds, recomputed values, reactions, watchers and error handling all run in both hosts. **One\nthing genuinely differs, and a domain that ignores it will not survive the move:** `reduce` and\n`recompute` always receive this domain's own state, but a reaction (`on`) and a watcher receive\nthe whole root state when store-hosted and only this domain's state when component-hosted. The\nthird and fourth type parameters exist to declare that root view — leave them off, as a\ncomponent-hosted domain does, and `state` is the domain's own.\n\nA domain that reads state in an `on` or a `watch` therefore belongs to one host or the other. One\nthat does not is portable as written.\n\n## Adapters\n\nAn adapter is built by its own factory and handed to the store. A domain does not carry one.\n\n```ts\nimport { createMessageRouter, createSubscriptions } from '@askrubberduck/redux-kit'\n\nconst prices = createSubscriptions<PriceSubscription, RootState, RootAction>()\nprices.handle(\n  'cart.prices',\n  variables => api.watchPrices(variables),\n  async (payload, dispatch) => dispatch({ type: 'CART_PRICING_RESPONSE', total: payload.total }),\n)\n\nconst inbox = createMessageRouter<Message, RootAction>(socket)\ninbox.handle('CART_CLEARED', (payload, dispatch) => dispatch({ type: 'CART_RESET' }))\n\ncreateStore<RootState, RootAction>({\n  domains: [cart, session],\n  adapters: { subscribers: [prices.subscriber], routers: [inbox] },\n})\n```\n\n## API\n\n| Export | Purpose |\n|---|---|\n| `defineDomain` | the only authoring API: state, folds, reactions, recomputed values, watchers, error handling |\n| `createSubscriptions` | live channels opened per key, with a lifecycle the store owns |\n| `createMessageRouter` | routes messages from an always-on bus |\n| `createAuditMiddleware` | read-only tap on the action stream |\n| `createDispatchRegistry` | routes an action back to one component instance |\n| `registerStore` / `storeDispatch` | dispatch from outside the component tree |\n| `configure` | inject logger, debug flag, unhandled-error handler |\n\nSubpaths: `/react` for `useDomain`, `/redux` for `createReduxHooks`, `/store` for `createStore`,\n`/zustand` for `createZustandStore`, `/testing` for `createTestStore`, `flush`, `makeStream`,\n`makeTransport` and the resets. Each one carries exactly the peer it needs — `/react` needs only\nReact, `/redux` adds react-redux, `/store` needs redux, `/zustand` needs zustand and nothing else —\nso a component-hosted domain never pulls a store dependency in.\n\n## Two rules that are easy to get wrong\n\n**State is last, dispatch immediately before it.** The one exception is `reduce`, which is\n`(state, action)` because that is redux's own reducer signature.\n\n**Adapters gate state on arity.** A subscription factory or handler receives state only if it\ndeclares the parameter, so a function-level default parameter silently costs you state. A default\ninside a destructuring pattern is safe. Domains do not do this — a reaction always receives state.\n\n## Agent instructions\n\nThe package ships its own agent instructions in `skills/redux-kit/`. `SKILL.md` covers the model\nand the domain API and routes to one file per topic in `reference/`, so an agent reads the one its\ntask needs rather than all four. Point at them from `AGENTS.md`:\n\n```md\n## @askrubberduck/redux-kit\n\nBefore writing a domain, store, adapter or React binding, locate the installed `@askrubberduck/redux-kit`\npackage and read `skills/redux-kit/SKILL.md` inside it, then whichever file in its `reference/`\ndirectory that file routes you to.\n```\n\nThat is the whole setup. Nothing is copied, so nothing goes stale on upgrade, and one entry covers\nevery agent that reads that file. It says *locate* rather than naming a path because\n`node_modules/@askrubberduck/redux-kit/` is not where the package lands under pnpm or in a hoisted\nworkspace, and a pointer to a path that does not exist fails silently.\n\n## Development\n\n```bash\nnpm install\nnpm test               # mocha; typechecks the tests first\nnpm run build\nnpm run check:circular\n```\n\nMIT\n","readmeFilename":"README.md"}