{"id":14156648,"url":"https://github.com/thoughtspile/banditstash","last_synced_at":"2025-04-10T01:12:25.738Z","repository":{"id":78621710,"uuid":"603718902","full_name":"thoughtspile/banditstash","owner":"thoughtspile","description":"🤠🔒 TypeScript-first, extensible localStorage and sessionStorage wrapper, \u003c500 bytes","archived":false,"fork":false,"pushed_at":"2025-01-03T10:33:47.000Z","size":127,"stargazers_count":62,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-10T01:12:20.427Z","etag":null,"topics":["localstorage","sessionstorage","typescript","validation","zod"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoughtspile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-19T11:29:42.000Z","updated_at":"2025-03-15T08:50:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"4d3034dd-60d0-4aa1-b232-f1b919b0675a","html_url":"https://github.com/thoughtspile/banditstash","commit_stats":{"total_commits":38,"total_committers":2,"mean_commits":19.0,"dds":0.02631578947368418,"last_synced_commit":"c7386d8ae2031104dc60925197af49de8dd3dc88"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditstash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditstash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditstash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtspile%2Fbanditstash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoughtspile","download_url":"https://codeload.github.com/thoughtspile/banditstash/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248137890,"owners_count":21053775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["localstorage","sessionstorage","typescript","validation","zod"],"created_at":"2024-08-17T08:07:32.051Z","updated_at":"2025-04-10T01:12:25.711Z","avatar_url":"https://github.com/thoughtspile.png","language":"TypeScript","funding_links":[],"categories":["typescript"],"sub_categories":[],"readme":"# banditstash\n\nTypeScript-first, extensible local and sessionStorage wrapper:\n\n- **Type-safe:** no sneaky bugs if storage is corrupted.\n- **Sane defaults:** JSON serialization, runtime validation and catching errors out of the box.\n- **Key scoping:** prevent collisions and access values with ease.\n- **Tiny:** 400 bytes full build, or a 187-byte core with modular features.\n- **Extensible:** replace JSON with any serializer or use your favorite validation library.\n- **Familiar API:** no trickery, just good old getItem / setItem with stricter types.\n- **Custom storage:** not limited to local / sessionStorage, works in SSR.\n\n**Beware!** This is an early version of the package. API might change, bugs might exist.\n\nBanditstash has a companion 400-byte type-checking library, [banditypes,](https://github.com/thoughtspile/banditypes) to make validation much more convenient without inflating your bundle.\n\n## Install\n\n```sh\nnpm install --save banditstash\n```\n\n## Basic usage\n\nDefault `banditStash` factory gives you:\n\n- JSON serialization for convenience\n- Type-safe access\n- Runtime validation to prevent malformed objects from exploding at runtime\n- Catching getItem / setItem errors\n- Optional scoping to prevent key collisions\n- Fallback for missing storage (e.g. in SSR)\n\n```ts\nimport { banditStash, fail } from \"banditstash\";\n\n// Passing explicit type parameter for outer type is recommended\nconst setStash = banditStash\u003cSet\u003cstring\u003e\u003e({\n  storage: window.sessionStorage,\n  parse: (raw) =\u003e {\n    // parse must convert arbitrary JSON to Set\u003cstring\u003e...\n    if (Array.isArray(raw)) {\n      return new Set(raw.filter((x): x is string =\u003e typeof x === \"string\"));\n    }\n    // or throw error via fail()\n    fail();\n  },\n  // prepare must convert Set to a JSON-serializable format\n  prepare: (data) =\u003e [...data],\n  // If getItem can't return Set\u003cstring\u003e\n  fallback: () =\u003e new Set\u003cstring\u003e(),\n  // (optional) prefix all storage keys with \"app:\"\n  scope: \"app\",\n});\n\n// getItem always returns Set\u003cstring\u003e — either from storage or fallback:\nconst readMessages: Set\u003cstring\u003e = setStash.getItem(\"read-ids\");\nconst isMessageRead = readMessages.has(\"id\");\n\n// setItem accepts Set\u003cstring\u003e and serializes it for you:\nsetStash.setItem(\"read-ids\", new Set([\"123\", \"234\"]));\n\n// removeItem is same as in raw storage\nsetStash.removeItem(\"read-ids\");\n\n// Bind key with .singleton() for easy access to a sinlge item:\nconst readStash = setStash.singleton(\"read-messages\");\nconst ids = readStash.getItem();\nreadStash.setItem(ids.add(\"123\"));\nreadStash.removeItem();\n```\n\nThis setup catches errors from both `getItem` (validation fails, invalid JSON in storage, missing storage) and `setItem` (full or missing storage, failed serialization). This can be disabled with explicit `fallback: false` and `safeSet: false`, respectively — useful for debugging, or to show an explicit error message to the user.\n\nManual object validation is quite tedious, so I suggest the companion validator — [banditypes.](https://github.com/thoughtspile/banditypes) If you want something more established, every other validation library — superstruct, zod, io-ts — also integrates easily.\n\n## Custom banditStashes\n\n`banditstash` is designed to be modular and extensible via plugins. In fact, default `banditStash` is just a combination of 3 plugins — `safeGet`, `safeSet`, and `scope` — and two formatters, `json` and your custom formatter defined via `prepare` and `parse`. **Plugins** modify getItem / setItem / removeItem behavior (like wrapping in try / catch, changing keys, or whatever.) **Formatters** are a special case of plugins that validate and transform the stored value during getItem and setItem.\n\nUsing the base `makeBanditStash` factory with `use` and `format` methods, you can further reduce bundle size (down to 187 bytes without plugins) or modify the behavior of your stores. Plugins and formatters are chainable and _always_ return a new object.\n\n```ts\nimport { makeBanditStash, fail } from \"banditstash\";\n\nconst stringStore = makeBanditStash(localStorage).format\u003cstring\u003e({\n  parse: (data) =\u003e (data == null ? fail() : data),\n});\nconst readonlyStringStore = stringStore.use((stash) =\u003e ({\n  getItem: stash.getItem,\n  setItem: () =\u003e {\n    throw new Error(\"setItem on readonly store\");\n  },\n  removeItem: () =\u003e {\n    throw new Error(\"removeItem on readonly store\");\n  },\n}));\n```\n\nStashes built using the default factory can be further enhanced with more plugins or formatters.\n\n### Custom storage\n\nBanditstash is not limited to browser Storage APIs — you can provide any object with `getItem`, `setItem` and `removeItem` methods that accept string key. The values needn't be strings, and makeBanditStash will infer storage value type.\n\n```ts\nimport { makeBanditStash, fail } from \"banditstash\";\n\nconst map = new Map\u003cstring, number\u003e();\nconst memoryStorage = makeBanditStash({\n  getItem: (key) =\u003e map.get(key) ?? fail(),\n  setItem: (key, value) =\u003e map.set(key, value),\n  removeItem: (key) =\u003e map.delete(key),\n});\n\nconst universalStorage = makeBanditStash(\n  typeof window === \"undefined\"\n    ? {\n        getItem: () =\u003e null,\n        setItem: () =\u003e {},\n        removeItem: () =\u003e {},\n      }\n    : window.localStorage,\n);\n```\n\nbanditstash provides one built-in custom storage — `noStorage`. It throws error on any access, but lets you construct a banditstash instance when no storage is available (e.g. in SSR).\n\n### Custom serializer\n\nIf JSON does not satisfy you as a storage format, you can easily use your own serializer. Here's an example of manually serializing a number:\n\n```ts\nimport { makeBanditStash, fail } from \"banditstash\";\n\nconst numberStash = makeBanditStash(localStorage).format\u003cnumber\u003e({\n  parse: (raw) =\u003e {\n    const num = Number(raw);\n    return Number.isNaN(num) ? fail() : num;\n  },\n  prepare: String,\n});\n```\n\nAny serialization library, like [arson](https://github.com/benjamn/arson) or [devalue,](https://github.com/Rich-Harris/devalue) will work:\n\n```ts\nimport { makeBanditStash, fail } from \"banditstash\";\nimport arson from \"arson\";\n\nconst dateStash = makeBanditStash(localStorage).format\u003cDate\u003e({\n  parse: arson.parse,\n  prepare: arson.stringify,\n});\ndateStash.setItem(\"registered\", new Date(2022, 3, 16));\nconst registeredAt: Date = dateStash.getItem(\"registered\");\n```\n\nDefault JSON serialization is implemented via `json` formatter:\n\n```ts\nimport { makeBanditStash, json } from \"banditstash\";\n\nmakeBanditStash(localStorage).format(json());\n```\n\n### Using a validation library\n\nManual type-checking can get tedious. Banditstash plays nicely with any validation library, as long as you `throw` (or `fail()`) on invalid values. I recommend either the 400-byte companion library [banditypes](https://github.com/thoughtspile/banditypes) or [superstruct](https://docs.superstructjs.org/) — it's small and modular, just like banditstash:\n\n```ts\nimport { makeBanditStash, fail } from \"banditstash\";\nimport { object, string, number, min, type Infer } from \"superstruct\";\n\nconst userSchema = object({\n  name: string(),\n  age: min(number(), 0),\n});\n\nconst userStore = makeBanditStash(localStorage).format\u003c\n  Infer\u003ctypeof userSchema\u003e\n\u003e({\n  parse: (raw) =\u003e userSchema.create(raw),\n});\n\nuserStore.setItem(\"me\", { name: \"vladimir\", age: 28 });\nlocalStorage.set(\"broken\", JSON.stringify({ name: \"evil\" }));\ntry {\n  userStore.getItem(\"broken\");\n} catch (err) {\n  console.log(\"validation failed\");\n}\n```\n\nAny other validation library — [zod,](https://zod.dev/) [io-ts,](https://gcanti.github.io/io-ts/) [yup,](https://github.com/jquense/yup) etc — is similarly easy to add.\n\n### Scoping\n\n`scope` plugin adds prefix to all keys to avoid key collisions. It's still useful even without TypeScript:\n\n```ts\nimport { makeBanditStash, scope } from \"banditstash\";\n\nconst appStorage = makeBanditStash(localStorage).use(scope(\"app\"));\n\nconst userStorage = appStorage.use(scope(\"user\"));\nconst cacheStorage = appStorage.use(scope(\"cache\"));\n\nuserStorage.getItem(\"avatar\");\n// equivalent to\nlocalStorage.getItem(\"app:user:avatar\");\n```\n\n### Runtime safety\n\nBanditstash provides two helpers for catching runtime errors: `safeGet` to handle `getItem` errors, and `safeSet` for `setItem`:\n\n```ts\nimport { makeBanditStash, safeGet, safeSet, json } from \"banditstash\";\nconst safeStorage = makeBanditStash(window.localStorage)\n  .format(json())\n  .use(safeGet(() =\u003e ({})))\n  .use(safeSet());\n```\n\nNote that, due to chaining, `safeGet` and `safeSet` only handle errors from plugins applied _above_ them, so it's best to use these in the tail of the chain.\n\n## API reference\n\n### `banditStash\u003cData\u003e(options)`\n\nCreates a default stash with JSON serialization, validation and error handling. Specifying data type explicitly is recommended.\n\nOptions:\n\n- `storage`: `localStorage`, `sessionStorage`, or an object with compatible `getItem`, `setItem`, and `removeItem` methods. If `undefined` is passed, `noStorage` is used to construct the instance.\n- `parse`: a function that either converts a free-form JSON to the `Data` type, or throws an error, during `getItem`. Usually required.\n- `prepare`: a function that converts `Data` to a JSON-serializable object during `setItem`. Required for non-serializable types like `Date`, `Map`, `Set`, etc.\n- `fallback: (() =\u003e Data) | false` : value to return when `getItem` can't retrieve data from storage. If set to false, error will be thrown.\n- `safeSet?: false` (optional): if false, setItem might throw. Defaults to true.\n- `scope: string`: (optional) a prefix for all the keys in the storage.\n\n### `fail()`\n\nA helper to conveniently throw errors in `parse`:\n\n```ts\n{\n  parse: raw =\u003e raw.length === 10 ? raw : fail(),\n  // equivalent to\n  parse: raw =\u003e {\n    if (raw.length === 10) return raw;\n    throw new TypeError();\n  }\n}\n```\n\n### `noStorage()`\n\nA custom storage that throws on every access. Can be used when `Storage` is not available to safely construct `banditstash`:\n\n```ts\nimport { makeBanditStash, noStorage } from \"banditstash\";\n\nmakeBanditStash(\n  typeof window === \"undefined\" ? noStorage() : window.localStorage,\n);\n```\n\nFull `banditStash` falls back to `noStorage` if `storage` option is falsy.\n\n### `makeBanditStash(storage)`\n\nCreates a custom banditStash instance without formatters or plugins. `getItem`, `setItem` and `removeItem` are always bound to storage, `format`, `use` and `singleton` methods are added.\n\n### `#BanditStash\u003cT\u003e.getItem(key)`\n\nReads value from storage, passing it through `parse` pipeline. Returns a parsed `Data` type. Throws if parse fails and `safeGet` plugin is not used.\n\n### `#BanditStash.setItem(key, value)`\n\nWrites value to storage, passing it through `prepare` pipeline. Throws if prepare fails or `storage.setItem` throws, and `safeSet` is not used.\n\n### `#BanditStash.removeItem(key)`\n\nRemoves value from storage.\n\n### `#BanditStash.singleton(key)`\n\nReturns a singleton store whose `getItem`, `setItem` and `removeItem` can be called without key. Singleton stores don't support formatters and plugins, so make sure it's called last.\n\n### `#BanditStash\u003cInner\u003e.format\u003cOuter\u003e(formatter)`\n\nReturns a new stash that exposes data of `Outer` type. Formatter object contains 2 functions:\n\n- `parse` maps data from Inner (storage) type to Outer or throws (use `fail()` helper).\n- `prepare` maps data from Outer to Inner type.\n\nIf Outer is assignable to Inner (e.g. `string -\u003e Json`), `prepare` is optional. If Inner is assignable to Outer (e.g. `string -\u003e string`), `parse` is optional.\n\nThere is a built-in `json()` formatter that converts between JSON objects and strings.\n\n### `#BanditStash.use(plugin)`\n\nReturn a new stash with `getItem`, `setItem` or `removeItem` behavior modified by the plugin. Plugin is a function called with the original stash. At this point plugin API is unstable, so prefer built-in plugins:\n\n- `safeGet(() =\u003e fallback)` — return `fallback` instead of throwing error in `getItem`\n- `safeSet()` — ignore errors in `setItem`\n- `scope(prefix: string)` — prefix all keys with prefix, `'key' -\u003e 'prefix:key'`\n\n## License\n\n[MIT License](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtspile%2Fbanditstash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoughtspile%2Fbanditstash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtspile%2Fbanditstash/lists"}