import superjson from 'superjson' // Injected by @rollup/plugin-replace declare const __VERSION__: string /** * @class HyperStorage * @classdesc A lightweight wrapper for localStorage/sessionStorage * with efficient caching and type-preserving serialization. * * @source https://github.com/Khoeckman/HyperStorage */ class HyperStorage { /** Version of the library */ static readonly version = __VERSION__ static readonly superjson = superjson /** Key name under which the data is stored. */ readonly itemName: string /** Default value used when the key does not exist in storage. */ readonly defaultValue: T /** Function to encode values before storing. */ readonly #encodeFn: (value: T) => string /** Function to decode values when reading. */ readonly #decodeFn: (value: string) => T /** The underlying storage backend (defaults to `window.localStorage`). */ readonly storage: Storage /** Internally cached value to improve access speed. */ #value!: T /** * Creates a new HyperStorage instance. * * @param {string} itemName - The key name under which the data will be stored. * @param {T} [defaultValue] - Default value assigned to the key if it does not exist yet. * @param {Object} [options={}] - Optional configuration parameters. * @param {(value: T) => string} [options.encodeFn] - Optional function to encode stored values. * @param {(value: string) => string} [options.decodeFn] - Optional function to decode stored values. * @param {Storage} [options.storage=window.localStorage] - Optional custom storage backend. * * @throws {TypeError} If `itemName` is not a string. * @throws {TypeError} If `encodeFn` or `decodeFn` are defined but not functions. * @throws {TypeError} If `storage` does not implement the standard Storage API. */ constructor( itemName: string, defaultValue: T, options: { encodeFn?: (value: T) => string decodeFn?: (value: string) => T storage?: Storage } = {} ) { const { encodeFn, decodeFn, storage = window.localStorage } = options if (typeof itemName !== 'string') throw new TypeError('itemName is not a string') this.itemName = itemName this.defaultValue = defaultValue if (encodeFn && typeof encodeFn !== 'function') throw new TypeError('encodeFn is defined but is not a function') this.#encodeFn = encodeFn || ((v) => HyperStorage.superjson.stringify(v)) if (decodeFn && typeof decodeFn !== 'function') throw new TypeError('decodeFn is defined but is not a function') this.#decodeFn = decodeFn || ((v) => HyperStorage.superjson.parse(v)) if (!(storage instanceof Storage)) throw new TypeError('storage must be an instance of Storage') this.storage = storage this.sync() } /** * Writes a new value to {@link storage}. * * Automatically caches, stringifies and encodes the value. */ set value(value: T) { this.#value = value // Cache real value this.storage.setItem(this.itemName, this.#encodeFn(value)) } /** * Gets the stored value from cache. */ get value(): T { return this.#value } /** * Updates a single property on the stored object. * * @template {keyof T} K * @param {K} key The property to update. * @param {T[K]} value The new value for the property. * @returns {T} The updated stored value. */ set(key: K, value: T[K]): T /** * Updates the stored value using a {@link callback}. * * @param {(value: T) => T} callback Receives the current value and must return the new value. * @returns {T} The updated stored value. */ set(callback: (value: T) => T): T set(keyOrCallback: keyof T | ((value: T) => T), value?: T[keyof T]): T { if (typeof keyOrCallback === 'function') return (this.value = keyOrCallback(this.#value)) return (this.value = { ...this.#value, [keyOrCallback]: value, }) } /** * Synchronizes the {@link value stored value} with the value in {@link storage}. * * This is only necessary if the stored value may have been modified without using the {@link value setter} of this class. * Using this function is **not type safe**. * * @param decodeFn a function to decode the value read from `storage`. * @returns {unknown} The return type is `unknown` because data read from `storage` cannot be type-checked or trusted at compile time because it may have been modified externally. */ sync(decodeFn = this.#decodeFn): unknown { const json = this.storage.getItem(this.itemName) // Reset value to defaultValue if it does not exist in storage if (json === null) return this.reset() try { return (this.value = decodeFn(json)) } catch (err) { // Reset value to defaultValue if the incoming value is not properly encoded console.error(err) this.reset() return err } } /** * Resets {@link value} to {@link defaultValue}. * * Updates both the cache and the underlying {@link Storage}. * * @returns {T} the restored default value. */ reset(): T { return (this.value = this.defaultValue) } /** * Checks whether {@link value} equals {@link defaultValue}. * * Uses reference comparison for objects and strict equality for primitives. * * @returns {boolean} `true` if the current value matches the default, `false` otherwise. */ isDefault(): boolean { return this.#value === this.defaultValue } } export default HyperStorage