UNPKG

@cedx/webstorage

Version:

Service for interacting with the Web Storage.

214 lines (185 loc) 6.63 kB
import {StorageEvent} from "./StorageEvent.js"; /** * Provides access to the [Web Storage](https://developer.mozilla.org/docs/Web/API/Web_Storage_API). */ export class Storage extends EventTarget implements Disposable, Iterable<[string, any], void, void> { /** * The `change` event type. */ static readonly changeEvent = "webstorage:change"; /** * The underlying data store. */ readonly #backend: globalThis.Storage; /** * A string prefixed to every key so that it is unique globally in the whole storage. */ readonly #keyPrefix: string; /** * Creates a new storage service. * @param backend The underlying data store. * @param options An object providing values to initialize this instance. */ private constructor(backend: globalThis.Storage, options: StorageOptions = {}) { super(); this.#backend = backend; this.#keyPrefix = options.keyPrefix ?? ""; if (options.listenToGlobalEvents) addEventListener("storage", this.#dispatchGlobalEvent); } /** * The keys of this storage. */ get keys(): Set<string> { const keys = Array.from(new Array(this.#backend.length), (_, index) => this.#backend.key(index)!); return new Set(this.#keyPrefix ? keys.filter(key => key.startsWith(this.#keyPrefix)).map(key => key.slice(this.#keyPrefix.length)) : keys); } /** * The number of entries in this storage. */ get length(): number { return this.#keyPrefix ? this.keys.size : this.#backend.length; } /** * Creates a new local storage service. * @param options An object providing values to initialize the service. * @returns The newly created service. */ static local(options: StorageOptions = {}): Storage { return new this(localStorage, options); } /** * Creates a new session storage service. * @param options An object providing values to initialize the service. * @returns The newly created service. */ static session(options: StorageOptions = {}): Storage { return new this(sessionStorage, options); } /** * Releases any resources associated with this object. */ [Symbol.dispose](): void { return this.dispose(); } /** * Returns a new iterator that allows iterating the entries of this storage. * @returns An iterator for the entries of this storage. */ *[Symbol.iterator](): Iterator<[string, any], void, void> { for (const key of this.keys) yield [key, this.get(key)]; } /** * Removes all entries from this storage. */ clear(): void { if (this.#keyPrefix) for (const key of this.keys) this.delete(key); else { this.#backend.clear(); this.dispatchEvent(new StorageEvent(Storage.changeEvent, null)); } } /** * Removes the value associated with the specified key. * @param key The storage key. * @returns The value associated with the key before it was removed. */ delete<T>(key: string): T|null { // eslint-disable-line @typescript-eslint/no-unnecessary-type-parameters const oldValue = this.get<T>(key); this.#backend.removeItem(this.#buildKey(key)); this.dispatchEvent(new StorageEvent(Storage.changeEvent, key, oldValue)); return oldValue; } /** * Cancels the subscription to the global storage events. */ dispose(): void { removeEventListener("storage", this.#dispatchGlobalEvent); } /** * Gets the deserialized value associated with the specified key. * @param key The storage key. * @returns The storage value, or `null` if the key does not exist or the value cannot be deserialized. */ get<T>(key: string): T|null { // eslint-disable-line @typescript-eslint/no-unnecessary-type-parameters try { return JSON.parse(this.#get(key) ?? "") as T; } catch { return null; } } /** * Gets a value indicating whether this storage contains the specified key. * @param key The storage key. * @returns `true` if this storage contains the specified key, otherwise `false`. */ has(key: string): boolean { return this.#get(key) != null; } /** * Registers a function that will be invoked whenever the `change` event is triggered. * @param listener The event handler to register. * @returns This instance. */ onChange(listener: (event: StorageEvent) => void): this { this.addEventListener(Storage.changeEvent, listener as EventListener); return this; } /** * Serializes and associates a given `value` with the specified `key`. * @param key The storage key. * @param value The storage value. * @returns This instance. */ set(key: string, value: unknown): this { const oldValue = this.get(key); this.#backend.setItem(this.#buildKey(key), JSON.stringify(value)); this.dispatchEvent(new StorageEvent(Storage.changeEvent, key, oldValue, value)); return this; } /** * Returns a JSON representation of this object. * @returns The JSON representation of this object. */ toJSON(): Array<[string, any]> { return Array.from(this); } /** * Builds a normalized storage key from the given key. * @param key The original key. * @returns The normalized storage key. */ #buildKey(key: string): string { return `${this.#keyPrefix}${key}`; } /** * Dispatches the specified global event. * @param event The dispatched event. */ readonly #dispatchGlobalEvent: (event: globalThis.StorageEvent) => void = event => { if (event.storageArea != this.#backend || (event.key && !event.key.startsWith(this.#keyPrefix))) return; let oldValue = null; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment try { oldValue = JSON.parse(event.oldValue ?? ""); } catch { /* Noop */ } let newValue = null; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment try { newValue = JSON.parse(event.newValue ?? ""); } catch { /* Noop */ } this.dispatchEvent(new StorageEvent(Storage.changeEvent, event.key?.slice(this.#keyPrefix.length) ?? null, oldValue, newValue)); }; /** * Gets the value associated to the specified key. * @param key The storage key. * @returns The storage value, or `null` if the key does not exist. */ #get(key: string): string|null { return this.#backend.getItem(this.#buildKey(key)); } } /** * Defines the options of a {@link Storage} instance. */ export type StorageOptions = Partial<{ /** * A string prefixed to every key so that it is unique globally in the whole storage. */ keyPrefix: string; /** * Value indicating whether to listen to the global storage events. */ listenToGlobalEvents: boolean; }>;