storestack
Version:
Simple store solution
222 lines (221 loc) • 7 kB
JavaScript
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;
})();