https://github.com/thoughtspile/banditstash
🤠🔒 TypeScript-first, extensible localStorage and sessionStorage wrapper, <500 bytes
https://github.com/thoughtspile/banditstash
localstorage sessionstorage typescript validation zod
Last synced: about 1 year ago
JSON representation
🤠🔒 TypeScript-first, extensible localStorage and sessionStorage wrapper, <500 bytes
- Host: GitHub
- URL: https://github.com/thoughtspile/banditstash
- Owner: thoughtspile
- License: mit
- Created: 2023-02-19T11:29:42.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2025-01-03T10:33:47.000Z (over 1 year ago)
- Last Synced: 2025-04-10T01:12:20.427Z (about 1 year ago)
- Topics: localstorage, sessionstorage, typescript, validation, zod
- Language: TypeScript
- Homepage:
- Size: 124 KB
- Stars: 62
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# banditstash
TypeScript-first, extensible local and sessionStorage wrapper:
- **Type-safe:** no sneaky bugs if storage is corrupted.
- **Sane defaults:** JSON serialization, runtime validation and catching errors out of the box.
- **Key scoping:** prevent collisions and access values with ease.
- **Tiny:** 400 bytes full build, or a 187-byte core with modular features.
- **Extensible:** replace JSON with any serializer or use your favorite validation library.
- **Familiar API:** no trickery, just good old getItem / setItem with stricter types.
- **Custom storage:** not limited to local / sessionStorage, works in SSR.
**Beware!** This is an early version of the package. API might change, bugs might exist.
Banditstash has a companion 400-byte type-checking library, [banditypes,](https://github.com/thoughtspile/banditypes) to make validation much more convenient without inflating your bundle.
## Install
```sh
npm install --save banditstash
```
## Basic usage
Default `banditStash` factory gives you:
- JSON serialization for convenience
- Type-safe access
- Runtime validation to prevent malformed objects from exploding at runtime
- Catching getItem / setItem errors
- Optional scoping to prevent key collisions
- Fallback for missing storage (e.g. in SSR)
```ts
import { banditStash, fail } from "banditstash";
// Passing explicit type parameter for outer type is recommended
const setStash = banditStash>({
storage: window.sessionStorage,
parse: (raw) => {
// parse must convert arbitrary JSON to Set...
if (Array.isArray(raw)) {
return new Set(raw.filter((x): x is string => typeof x === "string"));
}
// or throw error via fail()
fail();
},
// prepare must convert Set to a JSON-serializable format
prepare: (data) => [...data],
// If getItem can't return Set
fallback: () => new Set(),
// (optional) prefix all storage keys with "app:"
scope: "app",
});
// getItem always returns Set — either from storage or fallback:
const readMessages: Set = setStash.getItem("read-ids");
const isMessageRead = readMessages.has("id");
// setItem accepts Set and serializes it for you:
setStash.setItem("read-ids", new Set(["123", "234"]));
// removeItem is same as in raw storage
setStash.removeItem("read-ids");
// Bind key with .singleton() for easy access to a sinlge item:
const readStash = setStash.singleton("read-messages");
const ids = readStash.getItem();
readStash.setItem(ids.add("123"));
readStash.removeItem();
```
This 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.
Manual 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.
## Custom banditStashes
`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.
Using 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.
```ts
import { makeBanditStash, fail } from "banditstash";
const stringStore = makeBanditStash(localStorage).format({
parse: (data) => (data == null ? fail() : data),
});
const readonlyStringStore = stringStore.use((stash) => ({
getItem: stash.getItem,
setItem: () => {
throw new Error("setItem on readonly store");
},
removeItem: () => {
throw new Error("removeItem on readonly store");
},
}));
```
Stashes built using the default factory can be further enhanced with more plugins or formatters.
### Custom storage
Banditstash 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.
```ts
import { makeBanditStash, fail } from "banditstash";
const map = new Map();
const memoryStorage = makeBanditStash({
getItem: (key) => map.get(key) ?? fail(),
setItem: (key, value) => map.set(key, value),
removeItem: (key) => map.delete(key),
});
const universalStorage = makeBanditStash(
typeof window === "undefined"
? {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
}
: window.localStorage,
);
```
banditstash 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).
### Custom serializer
If 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:
```ts
import { makeBanditStash, fail } from "banditstash";
const numberStash = makeBanditStash(localStorage).format({
parse: (raw) => {
const num = Number(raw);
return Number.isNaN(num) ? fail() : num;
},
prepare: String,
});
```
Any serialization library, like [arson](https://github.com/benjamn/arson) or [devalue,](https://github.com/Rich-Harris/devalue) will work:
```ts
import { makeBanditStash, fail } from "banditstash";
import arson from "arson";
const dateStash = makeBanditStash(localStorage).format({
parse: arson.parse,
prepare: arson.stringify,
});
dateStash.setItem("registered", new Date(2022, 3, 16));
const registeredAt: Date = dateStash.getItem("registered");
```
Default JSON serialization is implemented via `json` formatter:
```ts
import { makeBanditStash, json } from "banditstash";
makeBanditStash(localStorage).format(json());
```
### Using a validation library
Manual 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:
```ts
import { makeBanditStash, fail } from "banditstash";
import { object, string, number, min, type Infer } from "superstruct";
const userSchema = object({
name: string(),
age: min(number(), 0),
});
const userStore = makeBanditStash(localStorage).format<
Infer
>({
parse: (raw) => userSchema.create(raw),
});
userStore.setItem("me", { name: "vladimir", age: 28 });
localStorage.set("broken", JSON.stringify({ name: "evil" }));
try {
userStore.getItem("broken");
} catch (err) {
console.log("validation failed");
}
```
Any 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.
### Scoping
`scope` plugin adds prefix to all keys to avoid key collisions. It's still useful even without TypeScript:
```ts
import { makeBanditStash, scope } from "banditstash";
const appStorage = makeBanditStash(localStorage).use(scope("app"));
const userStorage = appStorage.use(scope("user"));
const cacheStorage = appStorage.use(scope("cache"));
userStorage.getItem("avatar");
// equivalent to
localStorage.getItem("app:user:avatar");
```
### Runtime safety
Banditstash provides two helpers for catching runtime errors: `safeGet` to handle `getItem` errors, and `safeSet` for `setItem`:
```ts
import { makeBanditStash, safeGet, safeSet, json } from "banditstash";
const safeStorage = makeBanditStash(window.localStorage)
.format(json())
.use(safeGet(() => ({})))
.use(safeSet());
```
Note 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.
## API reference
### `banditStash(options)`
Creates a default stash with JSON serialization, validation and error handling. Specifying data type explicitly is recommended.
Options:
- `storage`: `localStorage`, `sessionStorage`, or an object with compatible `getItem`, `setItem`, and `removeItem` methods. If `undefined` is passed, `noStorage` is used to construct the instance.
- `parse`: a function that either converts a free-form JSON to the `Data` type, or throws an error, during `getItem`. Usually required.
- `prepare`: a function that converts `Data` to a JSON-serializable object during `setItem`. Required for non-serializable types like `Date`, `Map`, `Set`, etc.
- `fallback: (() => Data) | false` : value to return when `getItem` can't retrieve data from storage. If set to false, error will be thrown.
- `safeSet?: false` (optional): if false, setItem might throw. Defaults to true.
- `scope: string`: (optional) a prefix for all the keys in the storage.
### `fail()`
A helper to conveniently throw errors in `parse`:
```ts
{
parse: raw => raw.length === 10 ? raw : fail(),
// equivalent to
parse: raw => {
if (raw.length === 10) return raw;
throw new TypeError();
}
}
```
### `noStorage()`
A custom storage that throws on every access. Can be used when `Storage` is not available to safely construct `banditstash`:
```ts
import { makeBanditStash, noStorage } from "banditstash";
makeBanditStash(
typeof window === "undefined" ? noStorage() : window.localStorage,
);
```
Full `banditStash` falls back to `noStorage` if `storage` option is falsy.
### `makeBanditStash(storage)`
Creates a custom banditStash instance without formatters or plugins. `getItem`, `setItem` and `removeItem` are always bound to storage, `format`, `use` and `singleton` methods are added.
### `#BanditStash.getItem(key)`
Reads value from storage, passing it through `parse` pipeline. Returns a parsed `Data` type. Throws if parse fails and `safeGet` plugin is not used.
### `#BanditStash.setItem(key, value)`
Writes value to storage, passing it through `prepare` pipeline. Throws if prepare fails or `storage.setItem` throws, and `safeSet` is not used.
### `#BanditStash.removeItem(key)`
Removes value from storage.
### `#BanditStash.singleton(key)`
Returns 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.
### `#BanditStash.format(formatter)`
Returns a new stash that exposes data of `Outer` type. Formatter object contains 2 functions:
- `parse` maps data from Inner (storage) type to Outer or throws (use `fail()` helper).
- `prepare` maps data from Outer to Inner type.
If Outer is assignable to Inner (e.g. `string -> Json`), `prepare` is optional. If Inner is assignable to Outer (e.g. `string -> string`), `parse` is optional.
There is a built-in `json()` formatter that converts between JSON objects and strings.
### `#BanditStash.use(plugin)`
Return 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:
- `safeGet(() => fallback)` — return `fallback` instead of throwing error in `getItem`
- `safeSet()` — ignore errors in `setItem`
- `scope(prefix: string)` — prefix all keys with prefix, `'key' -> 'prefix:key'`
## License
[MIT License](./LICENSE)