UNPKG

@known-as-bmf/store

Version:
268 lines (238 loc) 6.33 kB
import { produce } from 'immer'; import { createHookable } from '@known-as-bmf/hookable'; import { errors } from './errors'; import { SubscriptionCallback, Selector, StateChangedEvent, Hooks, Store, Middleware, } from './types'; /** * Current subscriptions to store changes. * * @internal */ const SUBSCRIPTIONS: unique symbol = Symbol(); /** * Current state of the store. * * @internal */ const STATE: unique symbol = Symbol(); /** * State transformation (middleware). * * @internal */ const SET_STATE: unique symbol = Symbol(); /** * Internal subscription type. * * @internal */ type Subscription<S, R> = [Selector<S, R>, SubscriptionCallback<S>]; /** * The store class. * * @internal */ class StoreImpl<S> implements Store<S> { public declare [SUBSCRIPTIONS]: Set<Subscription<S, unknown>>; public declare [STATE]: S; public declare [SET_STATE]: (state: S) => S; } /** * Check if a value is a Store instance. * * @param input - The value to check. * * @internal */ function isStore<S = unknown>(input: unknown): input is StoreImpl<S> { return input instanceof StoreImpl; } /** * Function to assert that the store is a valid store instance. * * @param store - The store to assert. * * @internal */ function assertStore<S>(store: Store<S>): asserts store is StoreImpl<S> { if (!isStore(store)) { throw new TypeError(errors.invalidStore); } } /** * Get the current value of a store. * * @param store - The store. * * @internal */ function getState<S>(store: StoreImpl<S>): S { return store[STATE]; } /** * Set the current value of a store. * * @param store - The store. * @param state - The new state. * * @internal */ function setState<S>(store: StoreImpl<S>, state: S): void | never { store[SET_STATE](state); } /** * Notify all subscribers to the store that the state has changed. * If a selector was provided when subscribing, * only notify if the desired value has changed. * * @param store - The store instance. * @param event - The change event. * * @internal */ function notifySubscribers<S>( store: StoreImpl<S>, event: StateChangedEvent<S> ): void { store[SUBSCRIPTIONS].forEach(([selector, callback]) => { if (selector(event.previous) !== selector(event.current)) { callback(event); } }); } /** * Create a store. * * @param initialState - The initial value of the state. * @param middleware - Middleware to use for this store. You can compose multiple * middlewares with `composeMiddlewares` and `pipeMiddlewares`. * * @public */ export function of<S>(initialState: S, middleware?: Middleware<S>): Store<S> { const store: StoreImpl<S> = Object.defineProperties( Object.create(StoreImpl.prototype), { [SUBSCRIPTIONS]: { value: new Set<Subscription<S, unknown>>(), writable: true, }, [STATE]: { value: undefined, writable: true }, } ) as StoreImpl<S>; const setStateHook = createHookable( (s: S): S => { store[STATE] = s; return s; } ); Object.defineProperty(store, SET_STATE, { value: setStateHook }); const hooks: Hooks<S> = { stateDidChange: setStateHook.leave, stateWillChange: setStateHook.enter, transformState: setStateHook.transformInput, }; if (middleware) { middleware(store, hooks); } setState(store, initialState); return store; } /** * Return the current state of a store. * * @param store - The store you want to get the current state from. * * @throws `TypeError` if the store is not a correct `Store` instance. * * @public */ export function deref<S>(store: Store<S>): S { assertStore(store); return getState(store); } /** * Change the state of a store using a function. * * @param store - The store of which you want to change the state. * @param mutationFn - The function used to compute the value of the future state. * * @throws `TypeError` if the store is not a correct `Store` instance. * * @public */ export function swap<S>(store: Store<S>, mutationFn: (current: S) => S): void { assertStore(store); const previous = getState(store); const current = produce(previous, mutationFn) as S; setState(store, current); notifySubscribers(store, { previous, current }); } /** * Change the state of a store with a new one. * * @param store - The store of which you want to change the state. * @param current - The new state. * * @throws `TypeError` if the store is not a correct `Store` instance. * * @public */ export function set<S>(store: Store<S>, current: S): void { assertStore(store); const previous = getState(store); setState(store, current); notifySubscribers(store, { previous, current }); } /** * Subscribe to state changes. * * @param store - The store you want to subscribe to. * @param callback - The function to call when the state changes. * * @returns An unsubscribe function for this specific subscription. * * @throws `TypeError` if the store is not a correct `Store` instance. * * @public */ export function subscribe<S>( store: Store<S>, callback: SubscriptionCallback<S> ): () => void; /** * Subscribe to state changes. * * @param store - The store you want to subscribe to. * @param callback - The function to call when the state changes. * @param selector - The selector function, narrowing down the part of the state you want to subscribe to. * * @returns An unsubscribe function for this specific subscription. * * @throws `TypeError` if the store is not a correct `Store` instance. * * @public */ export function subscribe<S, R>( store: Store<S>, callback: SubscriptionCallback<S>, selector: Selector<S, R> ): () => void; export function subscribe<S>( store: Store<S>, callback: SubscriptionCallback<S>, selector: Selector<S, unknown> = (s): S => s ): () => void { assertStore(store); const subscription: Subscription<S, unknown> = [selector, callback]; store[SUBSCRIPTIONS].add(subscription); return () => { store[SUBSCRIPTIONS].delete(subscription); }; }