UNPKG

medama

Version:

A minimalist, powerful, and dependency-free reactive state management library for TypeScript and JavaScript applications.

253 lines (209 loc) 7.71 kB
import type { ReadState, Selector, SetState } from './medama.types'; export type SelectorTrigger = () => void; type UnregisterTriggerFromKeyHandle = () => void; /** * Function to register a selector's trigger with a state property. When * property value changes, registered trigger is called. Returns cleanup * function to unregister trigger. */ export type KeyHandle = (trigger: SelectorTrigger) => UnregisterTriggerFromKeyHandle; export type KeyHandleCollector = (keyHandle: KeyHandle) => void; export type RegisterSelectorTrigger<State extends object> = ( selectorTrigger: SelectorTrigger ) => ReadState<State>; type KeyHandleRecord = { keyHandle: KeyHandle; fireKey: () => void; }; type GetFromTarget<State> = <K extends string | symbol>( target: State, p: K ) => State[K & keyof State]; type SetToTarget<State> = <K extends string | symbol>( target: State, p: K, newValue: State[K & keyof State] ) => true; type ProxyHandler<State> = { get: GetFromTarget<State>; set: SetToTarget<State>; }; export type RunOverState<State extends object, V> = ( selector: Selector<State, V>, keyHandleCollector?: KeyHandleCollector ) => V; /** * Creates core state management system with controlled access and subscription * handling. Provides: * - Proxy-wrapped state object with access restrictions * - Dependency tracking during selector execution * - Subscription management for state changes * - Methods for reading and updating state * * @param initState Optional initial state values * @returns Methods for running selectors over state and updating state */ export const createStateImage = <State extends object>( initState?: Partial<State> ): { runOverState: RunOverState<State, unknown>; setState: SetState<State>; } => { let calculationAllowed = false; /** * Enforces authorized access to state properties. Throws error if attempting * to access state outside of: * - Selector execution * - State update operations */ const restrictCalculation = (): void => { if (!calculationAllowed) throw new Error('Medama Error: The object has no access to its properties'); }; /** * Temporarily allows state property access during execution of provided * function. Sets calculationAllowed flag to true before execution and resets * it after. Used during selector execution and state updates. * * @param toRun Function to execute with state access allowed * @returns Result of executed function */ const runWithRestrictionLifted = <V>(toRun: () => V): V => { calculationAllowed = true; return ([toRun(), (calculationAllowed = false)] as const)[0]; }; /** * Tracks current key handle collector during selector execution. Set when * running selector to collect state property dependencies. Used by proxy * handler to register key handles for accessed properties. Reset to undefined * after selector execution completes. */ let activeKeyHandleCollector: KeyHandleCollector | undefined; /** * Executes selector over state with dependency tracking. * - Sets active key handle collector for dependency tracking * - Runs selector with authorized access to state properties * - Resets collector after execution * * @param selector Function to run over state * @param keyHandleCollector Optional collector for registering dependencies * @returns Result of selector execution */ const runOverState: RunOverState<State, unknown> = (selector, keyHandleCollector?) => { activeKeyHandleCollector = keyHandleCollector; return ( [ runWithRestrictionLifted(() => selector(state)), (activeKeyHandleCollector = undefined), ] as const )[0]; }; const triggerJobStore: Partial<Record<keyof State, KeyHandleRecord>> = {}; const { addToQueue, runQueue } = createJobQueue(); /** * Creates a record to manage triggers for a state property. Contains: * - keyHandle: Registers selector triggers and returns cleanup function * - fireKey: Queues all registered triggers when property value changes Uses * Set to maintain unique triggers per property. */ const createKeyHandleRecord = (): KeyHandleRecord => { const triggerSet = new Set<SelectorTrigger>(); const keyHandle: KeyHandle = (trigger) => { triggerSet.add(trigger); return (): void => { triggerSet.delete(trigger); }; }; const fireKey = (): void => { addToQueue(triggerSet); }; return { keyHandle, fireKey }; }; /** * Updates state with new values and triggers subscriptions. * - Accepts either partial state object or state updater function * - Runs update with authorized access to state properties * - Executes all queued subscription triggers after update * * @param stateChange Partial state object or updater function * @returns Applied state changes */ const setState: SetState<State> = (stateChange) => ( [ runWithRestrictionLifted(() => { const mergeToState = typeof stateChange === 'function' ? stateChange(state) : stateChange; Object.assign(state, mergeToState); return mergeToState; }), runQueue(), ] as const )[0]; /** * Creates a Proxy handler for the state object with following * responsibilities: * - Tracks dependencies by registering key handles when * activeKeyHandleCollector is present (set during selector execution to * collect accessed state properties) * - Triggers subscriptions when state properties change by comparing old/new * values * - Enforces authorized access through calculationAllowed flag */ const proxyHandler: ProxyHandler<State> = { get: <K extends string | symbol>(target: State, p: K): State[K & keyof State] => { restrictCalculation(); if (activeKeyHandleCollector) { const { keyHandle } = (triggerJobStore[p as K & keyof State] ??= createKeyHandleRecord()); activeKeyHandleCollector(keyHandle); } return target[p as K & keyof State]; }, set: <K extends string | symbol>( target: State, p: K, newValue: State[K & keyof State] ): true => { restrictCalculation(); const oldValue = target[p as K & keyof State]; target[p as K & keyof State] = newValue; const { fireKey } = triggerJobStore[p as K & keyof State] ?? {}; if (fireKey && !Object.is(oldValue, newValue)) fireKey(); return true; }, }; /** * The state object wrapped in a Proxy to: * - Track property access for dependency collection * - Trigger subscriptions on property changes * - Enforce authorized access through restriction mechanism */ const state = new Proxy({ ...initState } as State, proxyHandler); return { runOverState, setState }; }; type AddToQueue = (triggerSet: Set<SelectorTrigger>) => void; type RunQueue = () => void; /** * Creates a queue system to manage selector trigger execution. * - Queue is populated when state properties change via Proxy's set handler * - Maintains unique set of triggers to avoid duplicate executions * - Provides methods to add triggers and process entire queue * - Clears queue after processing all triggers */ export const createJobQueue = (): { addToQueue: AddToQueue; runQueue: RunQueue; } => { const queue = new Set<SelectorTrigger>(); const addToQueue: AddToQueue = (triggerSet) => { triggerSet.forEach((trigger) => { queue.add(trigger); }); }; const runQueue: RunQueue = () => { queue.forEach((trigger) => { trigger(); }); queue.clear(); }; return { addToQueue, runQueue }; };