{"_id":"@dgkit/signal-storage","_rev":"4-c0ccdd54e00faa4d0e1de29d00abf6ab","name":"@dgkit/signal-storage","dist-tags":{"latest":"0.1.2"},"versions":{"0.0.0":{"name":"@dgkit/signal-storage","version":"0.0.0","keywords":["angular","standalone","ssr","signal-storage","dgkit"],"author":{"name":"Danylo Grynyk","email":"danielgrynyk@gmail.com"},"license":"MIT","_id":"@dgkit/signal-storage@0.0.0","maintainers":[{"name":"grynyk","email":"danielgrynyk@gmail.com"}],"homepage":"https://github.com/grynyk/dgkit/tree/main/packages/signal-storage#readme","bugs":{"url":"https://github.com/grynyk/dgkit/issues"},"dist":{"shasum":"39975a01c5f287fad55e0975e27f21dc51318720","tarball":"https://registry.npmjs.org/@dgkit/signal-storage/-/signal-storage-0.0.0.tgz","fileCount":7,"integrity":"sha512-FIqILvL3MxldzpISoLnGNQAeSZUntAppcwaSLRrO++2V+tYYMO6FXVMgGONTofWyqAJPOP3SafDbLfsxx7IbNg==","signatures":[{"sig":"MEUCIA8VUE6xolz6W0mrlIDnAyg/8feh/HcxmsYlUOOIIA1cAiEA0gLfxPKKAljvUvV0pR4eie4EnUIA5hjLQU+fo+/hAA0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dgkit%2fsignal-storage@0.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":29111},"type":"module","module":"fesm2022/dgkit-signal-storage.mjs","exports":{".":{"types":"./types/dgkit-signal-storage.d.ts","default":"./fesm2022/dgkit-signal-storage.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"d7e0518d26a1cb84b933341184177fea76e5eda4","typings":"types/dgkit-signal-storage.d.ts","_npmUser":{"name":"grynyk","email":"danielgrynyk@gmail.com"},"repository":{"url":"git+https://github.com/grynyk/dgkit.git","type":"git","directory":"packages/signal-storage"},"_npmVersion":"11.6.2","description":"Type-safe, bidirectional synchronization between Angular signals and localStorage/sessionStorage, with cross-tab sync.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"tslib":"^2.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"rxjs":"^6.5.3 || ^7.4.0","@angular/core":">=18.0.0 <22.0.0","@angular/common":">=18.0.0 <22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/signal-storage_0.0.0_1784914528405_0.5050335198414952","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@dgkit/signal-storage","version":"0.1.0","keywords":["angular","standalone","ssr","signal-storage","dgkit"],"author":{"name":"Danylo Grynyk","email":"danielgrynyk@gmail.com"},"license":"MIT","_id":"@dgkit/signal-storage@0.1.0","maintainers":[{"name":"grynyk","email":"danielgrynyk@gmail.com"}],"homepage":"https://github.com/grynyk/dgkit/tree/main/packages/signal-storage#readme","bugs":{"url":"https://github.com/grynyk/dgkit/issues"},"dist":{"shasum":"d1dbf527838f350c6ff67a10b1d881f477dc8f6c","tarball":"https://registry.npmjs.org/@dgkit/signal-storage/-/signal-storage-0.1.0.tgz","fileCount":7,"integrity":"sha512-7C3OcXRQhAnCBCvK/CVrKHB36EdEIrA87gzjXsZ+oObHbVJOllHrqJiEd0HbO7FeV2JDi/VnUixMmV8eUHvW/g==","signatures":[{"sig":"MEUCIQDP5Z0EOzbv8KS3MYKBHg3MBzS8Q/hiFwoMEl+GJ9PCegIgVjHstSIZAK9oplciopyUlSyu2ygk6vpm5autZlJSRrs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dgkit%2fsignal-storage@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":29111},"type":"module","module":"fesm2022/dgkit-signal-storage.mjs","exports":{".":{"types":"./types/dgkit-signal-storage.d.ts","default":"./fesm2022/dgkit-signal-storage.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"4c035ab24716f9bdf962669bef7d245f42b97b21","typings":"types/dgkit-signal-storage.d.ts","_npmUser":{"name":"grynyk","email":"danielgrynyk@gmail.com"},"repository":{"url":"git+https://github.com/grynyk/dgkit.git","type":"git","directory":"packages/signal-storage"},"_npmVersion":"11.6.2","description":"Type-safe, bidirectional synchronization between Angular signals and localStorage/sessionStorage, with cross-tab sync.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"tslib":"^2.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"rxjs":"^6.5.3 || ^7.4.0","@angular/core":">=18.0.0 <22.0.0","@angular/common":">=18.0.0 <22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/signal-storage_0.1.0_1784915474656_0.7523678866561814","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@dgkit/signal-storage","version":"0.1.1","keywords":["angular","standalone","ssr","signal-storage","dgkit"],"author":{"name":"Danylo Grynyk","email":"danielgrynyk@gmail.com"},"license":"MIT","_id":"@dgkit/signal-storage@0.1.1","maintainers":[{"name":"grynyk","email":"danielgrynyk@gmail.com"}],"homepage":"https://github.com/grynyk/dgkit/tree/main/packages/signal-storage#readme","bugs":{"url":"https://github.com/grynyk/dgkit/issues"},"dist":{"shasum":"3e77a7e0a75d6d681499f02123f9a6f5c3adfb64","tarball":"https://registry.npmjs.org/@dgkit/signal-storage/-/signal-storage-0.1.1.tgz","fileCount":7,"integrity":"sha512-zGsRUrRkI6L9QNCG25G69sWqXj50Z4T/qn5trVXl4vTJdDJSg/W+p/58qYrBmd/3PE7Ah0Ogy/oynyLIhiSYGw==","signatures":[{"sig":"MEQCIAWXTkW1MrItfy2lJ/bsqNdQA1rKsyAwJaJkg8uAh9Q/AiBgD/OvZqfQyNTFZAu55J68YDqt91PHITxrK2br37gLZg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dgkit%2fsignal-storage@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":29413},"type":"module","module":"fesm2022/dgkit-signal-storage.mjs","exports":{".":{"types":"./types/dgkit-signal-storage.d.ts","default":"./fesm2022/dgkit-signal-storage.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"b57e4b0193dc465e58452ab76addebb226ac6ffc","typings":"types/dgkit-signal-storage.d.ts","_npmUser":{"name":"grynyk","email":"danielgrynyk@gmail.com"},"repository":{"url":"git+https://github.com/grynyk/dgkit.git","type":"git","directory":"packages/signal-storage"},"_npmVersion":"11.6.2","description":"Type-safe, bidirectional synchronization between Angular signals and localStorage/sessionStorage, with cross-tab sync.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"tslib":"^2.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"rxjs":"^6.5.3 || ^7.4.0","@angular/core":">=18.0.0 <22.0.0","@angular/common":">=18.0.0 <22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/signal-storage_0.1.1_1785930824674_0.7014894974022574","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dgkit/signal-storage","version":"0.1.2","description":"Type-safe, bidirectional synchronization between Angular signals and localStorage/sessionStorage, with cross-tab sync.","author":{"name":"Danylo Grynyk","email":"danielgrynyk@gmail.com"},"license":"MIT","homepage":"https://github.com/grynyk/dgkit/tree/main/packages/signal-storage#readme","bugs":{"url":"https://github.com/grynyk/dgkit/issues"},"repository":{"type":"git","url":"git+https://github.com/grynyk/dgkit.git","directory":"packages/signal-storage"},"keywords":["angular","standalone","ssr","signal-storage","dgkit"],"sideEffects":false,"publishConfig":{"access":"public"},"peerDependencies":{"@angular/common":">=18.0.0 <22.0.0","@angular/core":">=18.0.0 <22.0.0","rxjs":"^6.5.3 || ^7.4.0"},"dependencies":{"tslib":"^2.3.0"},"module":"fesm2022/dgkit-signal-storage.mjs","typings":"types/dgkit-signal-storage.d.ts","exports":{"./package.json":{"default":"./package.json"},".":{"types":"./types/dgkit-signal-storage.d.ts","default":"./fesm2022/dgkit-signal-storage.mjs"}},"type":"module","gitHead":"9feef546280123103cdbf57fc425b6fff6a0dc4e","_id":"@dgkit/signal-storage@0.1.2","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-Jjr1SWGtE6lo4fO/x7WaVQ3ZAQhHQvrtFL83lMvXAqWLuVYusX7ciCz7HATqH2ZnNkP8EtYFcS2Q0NQBsC4wbg==","shasum":"25fde8e25b6090ca5391de1d2e9e9eaa7b5c8503","tarball":"https://registry.npmjs.org/@dgkit/signal-storage/-/signal-storage-0.1.2.tgz","fileCount":7,"unpackedSize":29506,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dgkit%2fsignal-storage@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGEZN8EQEU5OKllLUogIMQFAFRvkuBz9dvRw2hdAgUrqAiEAkaxE7yNLs0s15SKaYgdkdy76oXTZS/b1gVidSZkEAic="}]},"_npmUser":{"name":"grynyk","email":"danielgrynyk@gmail.com"},"directories":{},"maintainers":[{"name":"grynyk","email":"danielgrynyk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/signal-storage_0.1.2_1786579334501_0.29265578095397893"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-24T17:35:28.294Z","modified":"2026-08-13T00:02:15.021Z","0.0.0":"2026-07-24T17:35:28.538Z","0.1.0":"2026-07-24T17:51:14.786Z","0.1.1":"2026-08-05T11:53:44.805Z","0.1.2":"2026-08-13T00:02:14.637Z"},"bugs":{"url":"https://github.com/grynyk/dgkit/issues"},"author":{"name":"Danylo Grynyk","email":"danielgrynyk@gmail.com"},"license":"MIT","homepage":"https://github.com/grynyk/dgkit/tree/main/packages/signal-storage#readme","keywords":["angular","standalone","ssr","signal-storage","dgkit"],"repository":{"type":"git","url":"git+https://github.com/grynyk/dgkit.git","directory":"packages/signal-storage"},"description":"Type-safe, bidirectional synchronization between Angular signals and localStorage/sessionStorage, with cross-tab sync.","maintainers":[{"name":"grynyk","email":"danielgrynyk@gmail.com"}],"readme":"# @dgkit/signal-storage\n\n[![npm](https://img.shields.io/npm/v/@dgkit/signal-storage)](https://www.npmjs.com/package/@dgkit/signal-storage) [![Playground](https://img.shields.io/badge/playground-live-blueviolet)](https://grynyk.github.io/dgkit/#/)\n\n**Type-safe, bidirectional synchronization between an Angular signal and\n`localStorage`/`sessionStorage`.**\n\n```ts\nconst theme = injectStorageSignal('theme', 'light');\n\ntheme(); // 'light' — or whatever was already in storage\ntheme.set('dark'); // updates the signal and persists\ntheme.update((t) => (t === 'light' ? 'dark' : 'light'));\ntheme.remove(); // clear storage, revert to the default\n```\n\n- ✅ **Reads on init** — seeded from any value already in storage\n- ✅ **Persists on write** — `set()`/`update()` write through automatically\n- ✅ **Cross-tab sync** — another tab changing the same key updates this\n  signal too, via the native `storage` event (no echo loop: same-tab writes\n  never trigger that event)\n- ✅ **SSR-safe** — behaves as in-memory-only state on the server; `set()`\n  never throws, `isSupported()` reports `false`\n- ✅ **Never throws** — quota errors, private-browsing restrictions and\n  corrupted stored JSON all degrade to a dev-mode warning, never a crash\n- ✅ **Debounced writes** — coalesce rapid updates into one storage write;\n  a pending write is always flushed on destroy, never silently dropped\n- ✅ **Pluggable serialization** — swap `JSON` for a custom codec (`Date`,\n  `Map`, anything)\n- ✅ Zoneless-friendly — no `NgZone`, no `ChangeDetectorRef` assumptions\n\n## Installation\n\n```bash\nyarn add @dgkit/signal-storage\n```\n\n## Compatibility\n\nWorks with **Angular 18, 19, 20 and 21**.\n\n| Dependency | Supported range      |\n| ---------- | -------------------- |\n| Angular    | `>=18.0.0 <22.0.0`   |\n| RxJS       | `^6.5.3` or `^7.4.0` |\n| TypeScript | `>=5.4`              |\n\nAngular and RxJS are **peer dependencies** — they are never bundled into the\npackage.\n\n## Quick start\n\n```ts\nimport { Component } from '@angular/core';\nimport { injectStorageSignal } from '@dgkit/signal-storage';\n\n@Component({\n  standalone: true,\n  template: `\n    <button (click)=\"theme.set(theme() === 'light' ? 'dark' : 'light')\">\n      Theme: {{ theme() }}\n    </button>\n  `,\n})\nexport class ExampleComponent {\n  // Field initializers run in an injection context, so this is all you need.\n  protected readonly theme = injectStorageSignal('theme', 'light');\n}\n```\n\n## API\n\n```ts\nconst value = injectStorageSignal(key, defaultValue, options?);\n\nvalue();                 // Signal read — T\nvalue.set(next);         // write — updates the signal and persists\nvalue.update(updater);   // derive a new value from the current one\nvalue.remove();          // clear the key, revert to defaultValue\nvalue.isSupported();     // Signal<boolean> — false during SSR / when storage is unreachable\n```\n\n### Options\n\n| Option       | Type                      | Default     | Description                                                                                  |\n| ------------ | ------------------------- | ----------- | -------------------------------------------------------------------------------------------- |\n| `storage`    | `'local' \\| 'session'`    | `'local'`   | Which `Storage` object to use.                                                               |\n| `serializer` | `{ parse, stringify }`    | `JSON`      | Custom (de)serialization for values `JSON` can't round-trip.                                 |\n| `syncTabs`   | `boolean`                 | `true`      | Listen for the same key changing in another tab/window via the `storage` event.              |\n| `debounce`   | `number`                  | `0`         | Debounce writes, in ms. `0` writes synchronously. A pending write is flushed on destroy.     |\n| `equal`      | `(a: T, b: T) => boolean` | `Object.is` | Equality used to skip redundant signal updates, both for local writes and cross-tab updates. |\n\n### Custom serializer example\n\n```ts\nconst lastVisit = injectStorageSignal('lastVisit', new Date(0), {\n  serializer: {\n    parse: (raw) => new Date(raw),\n    stringify: (value) => value.toISOString(),\n  },\n});\n```\n\n## Behavior details\n\n### Cross-tab sync\n\nBy default, changes to the same key made in another tab or window are\nreflected here automatically. This relies on the native `storage` event,\nwhich the browser only fires in _other_ tabs — the tab that made the change\nnever receives it, so there's no risk of an update loop. Set\n`syncTabs: false` to opt out.\n\n### Errors never throw\n\n- **Corrupted stored JSON** (or a custom serializer that throws) on read: the\n  signal falls back to `defaultValue` and logs a dev-mode warning.\n- **Write failures** (quota exceeded, Safari private-browsing restrictions):\n  the signal still updates in memory; the write is dropped with a dev-mode\n  warning instead of crashing the app.\n- **Storage unreachable at all** (SSR, or accessing `localStorage` throws):\n  `isSupported()` is `false` and the signal behaves as ordinary in-memory\n  state — `set()`/`update()`/`remove()` all still work, they just don't\n  persist.\n\n### Debounce\n\n- `debounce: 0` (default): every `set()`/`update()` writes to storage\n  synchronously.\n- `debounce > 0`: writes within the window are coalesced — only the latest\n  value is written, once. A write still pending at teardown is flushed\n  immediately rather than lost.\n\n## Development\n\nThis package lives in the [`dgkit`](../../README.md) Nx monorepo.\n\n```bash\nyarn nx build signal-storage        # ng-packagr production build\nyarn nx test signal-storage         # Vitest + coverage\nyarn nx lint signal-storage         # ESLint\nyarn nx typecheck signal-storage    # tsc --noEmit\n```\n\n## Testing\n\nRun with `yarn nx test signal-storage`. The suite covers initial reads,\nwrites, debounce, cross-tab sync (via dispatched `StorageEvent`s), custom\nserializers, and degraded/SSR environments. Coverage thresholds are enforced.\n\n## Contributing\n\nContributions are welcome — see the repository\n[CONTRIBUTING guide](../../CONTRIBUTING.md).\n\n## License\n\n[MIT](../../LICENSE)\n","readmeFilename":"README.md"}