UNPKG

storestack

Version:
222 lines (221 loc) 7 kB
import cloneDeep from 'clone-deep'; import { nanoid } from 'nanoid'; (() => { if (!('structuredClone' in window)) { window.structuredClone = cloneDeep; } })(); export class DuplicateObserverError extends Error { constructor() { super('This observer is already attached to the store.'); } } export class UnknownObserverError extends Error { constructor() { super('Attempted removal of unattached observer.'); } } export class MemoryAllocationError extends Error { constructor() { super('Attempted to insert store at already allocated memory address without explicit override.'); } } export class NullPointerError extends Error { constructor() { super('Attempted to access unallocated memory address.'); } } export class Store { #observers = []; #state; #previousState = null; get state() { return window.structuredClone(this.#state); } get prevState() { return window.structuredClone(this.#previousState); } static newPointer() { return nanoid(); } constructor(state) { this.#state = state; } attach(observer, skipOnExist) { const isExist = this.#observers.includes(observer); if (isExist && !skipOnExist) { throw new DuplicateObserverError(); } else if (isExist && skipOnExist) { return; } this.#observers.push(observer); } detach(observer) { const observerIndex = this.#observers.indexOf(observer); if (observerIndex === -1) { throw new UnknownObserverError(); } this.#observers.splice(observerIndex, 1); } notify() { for (const observer of this.#observers) { observer.update(this); } } set(options) { this.#previousState = window.structuredClone(this.#state); if (typeof options === 'function') { this.#state = window.structuredClone(options(window.structuredClone(this.state))); } else { this.#state = window.structuredClone(options); } this.notify(); } } export function useStore(state, options) { if (typeof options === 'object') { const pointer = options.pointer ?? nanoid(); const callback = options.onChange ?? (() => null); const observers = options.observers ?? []; const override = options.override ?? false; class StoreObserver { update(subject) { callback(subject.state); } } observers.push(new StoreObserver()); if (override) { const store = new Store(state); for (const observer of observers) { try { store.attach(observer); } catch (error) { if (options.errorHandling?.verbose === true) { console.error(error); } if (options.errorHandling?.stopOnError === true) { throw error; } } } Stores.addStoreAtPointer(store, pointer, { override: true }); } else { try { Stores.upsert(state, pointer, ...observers); } catch (error) { if (options.errorHandling?.verbose === true) { console.error(error); } if (options.errorHandling?.stopOnError === true) { throw error; } } } return pointer; } else { return Stores.addStore(new Store(state)); } } export class StoreObserver { #callback; constructor(callback) { this.#callback = callback; } update(subject) { this.#callback(subject.state, subject.prevState); } } export function useObserver(callback) { return new StoreObserver(callback); } export class StoreStack { static configure() { if (typeof window.stores === 'undefined' || !(window.stores instanceof StoreStack)) { window.stores = new StoreStack(); } } static attach(globalObservers) { return new StoreStack(globalObservers); } constructor(globalObservers) { globalObservers && (this.#globalObservers = globalObservers); } addGlobalObserver(...globalObservers) { this.#globalObservers = [...this.#globalObservers, ...globalObservers]; } #stores = {}; #globalObservers = []; addStore(newItem) { const ptr = nanoid(); this.#stores[ptr] = newItem; return ptr; } addStoreAtPointer(newItem, pointer, options) { if (typeof this.#stores[pointer] !== 'undefined' && !options?.override) { if (options?.verbose) { console.error('Error: Cannot add store at pointer, address is already allocated.'); } throw new MemoryAllocationError(); } this.#stores[pointer] = newItem; } upsert(defaultValue, pointer, ...observers) { if (typeof this.#stores[pointer] === 'undefined') { this.#stores[pointer] = new Store(defaultValue); } for (const observer of observers) { try { this.#stores[pointer].attach(observer); } catch (error) { throw error; } } } removeStore(ptr, options) { if (typeof this.#stores[ptr] === 'undefined') { if (options?.verbose) { console.error('Error: The pointer address points to unallocated memory.'); } throw new NullPointerError(); } delete this.#stores[ptr]; } get(ptr) { if (typeof this.#stores[ptr] !== 'undefined') { return this.#stores[ptr]; } return undefined; } useState({ pointer, defaultValue, observers, useGlobalObserver, }) { const concatObservers = (() => { let temp = []; observers && temp.push(...observers); useGlobalObserver !== false && temp.push(...this.#globalObservers); return [...temp]; })(); if (typeof this.#stores[pointer] === 'undefined') { this.#stores[pointer] = new Store(defaultValue); } concatObservers.forEach((observer) => this.#stores[pointer].attach(observer, true)); const setter = (newState) => { if (typeof newState === 'function') { return this.#stores[pointer].set(newState(window.structuredClone(this.#stores[pointer].state))); } else { this.#stores[pointer].set(newState); } }; return [() => this.#stores[pointer].state, setter]; } } export const Stores = (function () { StoreStack.configure(); return window.stores; })();