{"_id":"@bartlomiej-bykowy/toy-store","_rev":"2-f8184b7ef33da0ae5e27024d60a1034b","name":"@bartlomiej-bykowy/toy-store","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@bartlomiej-bykowy/toy-store","version":"1.0.0","_id":"@bartlomiej-bykowy/toy-store@1.0.0","maintainers":[{"name":"bartlomiej-bykowy","email":"bartlomiej.bykowy@gmail.com"}],"dist":{"shasum":"cce442cd8c7feee116cbfa952ec9420f9127994c","tarball":"https://registry.npmjs.org/@bartlomiej-bykowy/toy-store/-/toy-store-1.0.0.tgz","fileCount":10,"integrity":"sha512-/isxoes9rSaoc1BJlJZ0ud62TIRaWJZ5p7JSkSEpk8/uGlTD1Ft+iXfFSbd9W5KjqBEvH8zDPLRHqqCVP5wCng==","signatures":[{"sig":"MEYCIQCM2YDrwH64moXcu3g4Yiuk+1U9nqMhHB1hJm54l8lgvAIhAI/COZOna3dEy5uw+QkX1xZeObPHyu357eiq3xmOpWKT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65781},"main":"dist/index.cjs","type":"module","_from":"file:bartlomiej-bykowy-toy-store-1.0.0.tgz","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"eslint .","test":"vitest --run","build":"tsup","test-watch":"vitest","format:check":"prettier --check \"src/**/*.{ts,md}\"","format:write":"prettier --write \"src/**/*.{ts,md}\""},"_npmUser":{"name":"bartlomiej-bykowy","email":"bartlomiej.bykowy@gmail.com"},"_resolved":"/tmp/f2c2ec790ef50f66cc3f9b19bc6cd181/bartlomiej-bykowy-toy-store-1.0.0.tgz","_integrity":"sha512-/isxoes9rSaoc1BJlJZ0ud62TIRaWJZ5p7JSkSEpk8/uGlTD1Ft+iXfFSbd9W5KjqBEvH8zDPLRHqqCVP5wCng==","_npmVersion":"10.8.2","description":"A simple state manager for vanilla JS/TS SPA apps","directories":{},"_nodeVersion":"20.19.6","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jiti":"^2.6.1","tsup":"^8.5.1","vite":"^7.2.4","jsdom":"^27.2.0","eslint":"^9.39.1","vitest":"^4.0.12","globals":"^16.5.0","prettier":"3.6.2","@eslint/js":"^9.39.1","typescript":"~5.9.3","typescript-eslint":"^8.48.0","eslint-config-prettier":"^10.1.8"},"_npmOperationalInternal":{"tmp":"tmp/toy-store_1.0.0_1764796825091_0.46129657494137954","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@bartlomiej-bykowy/toy-store","version":"1.0.1","description":"A simple state manager for vanilla JS/TS SPA apps","main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"devDependencies":{"@eslint/js":"^9.39.1","eslint":"^9.39.1","eslint-config-prettier":"^10.1.8","globals":"^16.5.0","jiti":"^2.6.1","jsdom":"^27.2.0","prettier":"3.6.2","tsup":"^8.5.1","typescript":"~5.9.3","typescript-eslint":"^8.48.0","vite":"^7.2.4","vitest":"^4.0.12"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","test":"vitest --run","test-watch":"vitest","format:write":"prettier --write \"src/**/*.{ts,md}\"","format:check":"prettier --check \"src/**/*.{ts,md}\"","lint":"eslint ."},"_id":"@bartlomiej-bykowy/toy-store@1.0.1","_integrity":"sha512-0C82iayadKF3Sm9SzVa/WFMUuU+xQrEUVhiRmALxHaWasSLsSBzV9dTBAK1tPGD6XUSBPdj9YjpL61B7qwY8uA==","_resolved":"/tmp/e4993d63430450b1305aec135704ee0e/bartlomiej-bykowy-toy-store-1.0.1.tgz","_from":"file:bartlomiej-bykowy-toy-store-1.0.1.tgz","_nodeVersion":"20.19.6","_npmVersion":"10.8.2","dist":{"integrity":"sha512-0C82iayadKF3Sm9SzVa/WFMUuU+xQrEUVhiRmALxHaWasSLsSBzV9dTBAK1tPGD6XUSBPdj9YjpL61B7qwY8uA==","shasum":"92dc0ab556ac746867740bbc14ed1db8bef6122e","tarball":"https://registry.npmjs.org/@bartlomiej-bykowy/toy-store/-/toy-store-1.0.1.tgz","fileCount":10,"unpackedSize":65777,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICeN8mo7R3C8ay9vkOOpOVDUAAwIS6Hcl/iQEzXpvfznAiAvxXa97a46fm9HwiGRbpW43Mmhlf9OlFO3P4/YCv++BQ=="}]},"_npmUser":{"name":"bartlomiej-bykowy","email":"bartlomiej.bykowy@gmail.com"},"directories":{},"maintainers":[{"name":"bartlomiej-bykowy","email":"bartlomiej.bykowy@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/toy-store_1.0.1_1764797462902_0.634421851496124"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-03T21:20:25.015Z","modified":"2025-12-03T21:31:03.308Z","1.0.0":"2025-12-03T21:20:25.268Z","1.0.1":"2025-12-03T21:31:03.116Z"},"description":"A simple state manager for vanilla JS/TS SPA apps","maintainers":[{"name":"bartlomiej-bykowy","email":"bartlomiej.bykowy@gmail.com"}],"readme":"# toy-state-manager\n\ntoy-state-manager is a tiny, dependency-free, fully typed state management system inspired by Pinia/Vuex and designed to demonstrate how reactivity, watchers, getters, batching and state proxies work under the hood.\n\nIt is framework-agnostic, works with plain JavaScript or TypeScript, and is built around Proxies, microtask batching, and computed getters with caching.\n\nTo see it in action, check the demo app:\n👉 [https://github.com/bartlomiej-bykowy/toy-shop](https://github.com/bartlomiej-bykowy/toy-shop)\n\n---\n\n## ⚠️ This package was created as a side/hobby project and it's not meant to be used in production.\n\n---\n\n## 🚀 Features\n\n- 🎯 Zero dependencies\n- 🧱 Proxy-based reactivity (mutations trigger watchers automatically)\n- 🧵 Microtask batching (multiple synchronous writes = one notification)\n- 🔍 Watchers (global and key-specific watchers)\n- 🧮 Getters with caching (omputed values re-compute only when needed)\n- 🛠️ Actions bound to the store instance\n- 🔄 $patch() for partial updates\n- 🧼 $reset() for full or partial reset\n- 🗃️ Store registry (prevents accidental duplicate store creation)\n- 🧪 Fully typed APIs with TS inference\n\n---\n\n## 📦 Installation\n\n```bash\nnpm install @bartlomiej-bykowy/toy-store\n# or\npnpm add @bartlomiej-bykowy/toy-store\n# or\nyarn add @bartlomiej-bykowy/toy-store\n```\n\n---\n\n## 🕹️ Basic Usage\n\n### 1. Define a store\n\n```ts\nimport { defineStore } from \"@bartlomiej-bykowy/toy-store\";\n\ntype State = { count: number };\ntype Getters<S> = {\n  double: (state: S) => void;\n};\ntype Actions = {\n  increment: () => void;\n};\n\nexport const useCounter = defineStore<State, Getters<State>, Actions>({\n  id: \"counter\",\n  state: () => ({ count: 0 }),\n\n  getters: {\n    double: (state) => state.count * 2\n  },\n\n  actions: {\n    increment() {\n      this.$state.count++;\n    }\n  }\n});\n```\n\n### 2. Use the store\n\n```ts\nconst counter = useCounter();\n\ncounter.increment();\nconsole.log(counter.$state.count); // 1\nconsole.log(counter.double); // 2\n```\n\n---\n\n## 🔭 API\n\n#### `defineStore(config)`\n\nCreates a store factory function.\n\n#### Type definition:\n\n```ts\ntype State = Record<string, any>;\ntype Getters<S extends State> = Record<string, (state: S, getters: any) => any>;\ntype Actions = Record<string, (...args: any[]) => void | Promise<void>>;\n\ntype StoreConfig<S extends State, G extends Getters<S>, A extends Actions> = {\n  id: string;\n  state: () => S;\n  getters?: G;\n  actions?: A & ThisType<StoreInstance<S, G, A>>;\n};\n```\n\n#### Usage\n\n```ts\nconst useStore = defineStore<S, G<S>, A>({\n  id: \"products\",\n  state: () => ({ products: [], cart: [] }),\n  getters: { ... },\n  actions: { ... }\n});\n```\n\nCalling it returns the store instance:\n\n```ts\nconst store = useStore();\n```\n\n#### Config fields\n\n| Field     | Description                       |\n| --------- | --------------------------------- |\n| `id`      | Store identifier (must be unique) |\n| `state`   | Function returning initial state  |\n| `getters` | Computed values with caching      |\n| `actions` | Business logic; can mutate state  |\n\n### 🏪 Store Instance\n\nEvery store has the following fields:\n\n```ts\n{\n  $id: string\n  $state: S\n  $patch(partialState)\n  $reset(...keys)\n  $subscribe(cb)\n  $subscribeKey(key, cb)\n  ...getters // as computed values\n  ...actions\n}\n```\n\n#### `$state`\n\nReactive state proxy.\nMutations trigger watchers:\n\n```ts\nstore.$state.count = 10;\n```\n\n#### `$patch(partialState)`\n\nPartial updates:\n\n```ts\nstore.$patch({ count: 5 });\n```\n\n#### $reset(...keys)\n\nReset the entire state:\n\n```ts\nstore.$reset();\n```\n\nOr reset selected fields:\n\n```ts\nstore.$reset(\"products\", \"cart\");\n```\n\nInternally uses a deep clone of the original initial state.\n\n#### $subscribe(callback)\n\nWatches the entire state.\n\n```ts\nconst unsubscribe = store.$subscribe((oldState, newState) => {\n  console.log(\"State changed:\", oldState, \"→\", newState);\n});\n\n// later:\nunsubscribe();\n```\n\n#### $subscribeKey(key, callback)\n\nWatches a specific key:\n\n```ts\nstore.$subscribeKey(\"count\", (oldValue, newValue) => {\n  console.log(\"count:\", oldValue, \"→\", newValue);\n});\n```\n\n### 🧮 Getters\n\nGetters behave like computed properties:\n\n```ts\ndouble: (state) => state.count * 2;\n```\n\nThey are:\n\n- memorized (cached),\n- recomputed only when state changes,\n- attached directly to the store:\n\n```ts\nstore.double;\n```\n\n### ⚙️ Actions\n\nActions are bound to the store instance:\n\n```ts\nactions: {\n  addProduct(product) {\n    this.$state.products = [...this.$state.products, product];\n  }\n}\n```\n\nAsync actions are supported:\n\n```ts\nactions: {\n  async fetchData() {\n    const data = await fetch(\"/api\");\n    this.$state.items = await data.json();\n  }\n}\n```\n\n## 🔍 Reactivity & Batching\n\nAll state changes happen through a Proxy.\n\n#### **Example**\n\n```ts\nstore.$state.a = 1;\nstore.$state.b = 2;\n```\n\nOnly **one** watcher call occurs. It's because internally:\n\n- first mutation snapshots oldState,\n- schedules a microtask,\n- collects changed keys,\n- flushes everything at once at the end of the tick.\n\nIt's good for performance and make updates predictable.\n\n## 🔐 Store Registry\n\nEach store is registered by its `id`.\n\n```ts\nconst useA = defineStore({ id: \"x\", ... });\nconst useB = defineStore({ id: \"x\", ... }); // ❌ throws error\n```\n\n## 🧪 TypeScript Support\n\nEverything is fully typed, including:\n\n- inference of state shape,\n- getters return types,\n- action this types,\n- key-specific watchers,\n- partial & deep state updates.\n\nExample:\n\n```ts\nconst store = useStore();\n\nstore.$patch({ count: 10 }); // typed\nstore.$subscribeKey(\"count\", (old, next) => {}); // typed\nstore.double; // typed getter\nstore.increment(); // typed action\n```\n\n## 🧠 Why shallow copy for oldState but deep copy for newState?\n\n**oldState (shallow copy)**\nUsed only for comparisons and watchers — cheap and safe.\n\n**newState (deep copy)**\nGiven to watchers so they cannot accidentally mutate your store through reference sharing.\n\n## ⚠️ Dev Notes\n\n- The store requires state to be a function `(state: () => ({...}))`, otherwise multiple instances would share a single state object.\n- Direct array mutation (push) won't trigger watchers; use immutable updates:\n\n```ts\nthis.$state.items = [...this.$state.items, item];\n```\n\n## 🧭 Example Store\n\n```ts\nexport type Product = {\n  id: number;\n  name: string;\n  desc: string;\n  img: string;\n  price: number;\n};\ntype ProductsState = { products: Product[]; cart: Product[] };\ntype Getters<S> = {\n  productsInCart: (state: S) => number;\n};\ntype Actions = {\n  loadProducts: (products: Product[]) => void;\n  addToCart: (products: Product[]) => void;\n  removeFromCart: (id: number) => Product[];\n  clearCart: () => void;\n};\n\nconst useProductsStore = defineStore<\n  ProductsState,\n  Getters<ProductsState>,\n  Actions\n>({\n  id: \"test\",\n  state: () => ({\n    products: [],\n    cart: []\n  }),\n  getters: {\n    productsInCart: (state) => state.cart.length\n  },\n  actions: {\n    loadProducts(products: Product[]) {\n      this.$state.products = products;\n    },\n    addToCart(products: Product[]) {\n      this.$state.cart = [...this.$state.cart, ...products];\n    },\n    removeFromCart(id: number) {\n      const newCart = this.$state.cart.filter((product) => product.id !== id);\n      this.$state.cart = newCart;\n      return newCart;\n    },\n    clearCart() {\n      this.$state.cart = [];\n    }\n  }\n});\n\nexport const productsStore = useProductsStore();\n```\n","readmeFilename":"README.npm.md"}