UNPKG

pocket-state

Version:
226 lines (206 loc) 7.39 kB
import {Draft} from 'immer'; /** * A callback invoked when an event is emitted with a payload of type `T`. * Keep listeners pure and fast. Long-running side effects should live in * middleware/effects instead of listeners. */ export interface Listener<T = any> { (payload: T): void; } /** * Immer-style mutation function used for "mutable-looking" updates. * * - Receives a `draft` (a Proxy of your state). * - You can mutate the draft; Immer will produce the next immutable state. * - You may return a new value instead of mutating the draft (less common). * - Can be async, but it's recommended to await first, then mutate, to avoid races. */ export type MutateFn<T> = (draft: Draft<T>) => void | T | Promise<void | T>; /** * Store getter API. * * Usage: * ```ts * const all = getValues(); // → T (entire state) * const count = getValues('count'); // → T['count'] (one key) * ``` * * Notes: * - Results are expected to be reference-stable when state hasn't changed, * which helps React avoid unnecessary renders. * - If you want to support reading multiple keys at once, extend the type. */ export type UseStoreGet<T> = { (): T; <K extends keyof T>(key: K): T[K]; }; /** * Store setter API: * - Accepts a `patch` (partial) to shallow-merge into current state, * or a function that receives the current state and returns a partial. * The function may be async. * * Examples: * ```ts * setValue({ flag: true }); * setValue(s => ({ count: s.count + 1 })); * setValue(async s => { * const user = await fetchUser(); * return { user }; * }); * ``` * * Notes: * - The store is responsible for equality checks and only emits on real changes. * - Multiple updates in the same tick may be coalesced (implementation-dependent). * * `immer()` (if implemented) is a convenience to enable Immer-style updates. * This type does not define its runtime behavior; see your implementation. */ export type UseStoreSet<T> = { (patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>)): void; immer(): void; }; /** * Minimal pub/sub event emitter contract. * * Expected behavior: * - `on` registers a listener for an event name. * - `emit` broadcasts a payload to all listeners of that event. * - `off` removes a specific listener or all listeners for an event. * - `once` registers a listener that runs exactly once, then unregisters itself. * - `clear` removes listeners for all events. * * Implementation guidance: * - Snapshot listeners before emitting to stay safe if `on/off` happens during emit. * - `off(event, original)` should remove a `once` listener even if it's wrapped. */ export interface IEventEmitter { /** Register a listener for an event name. */ on<T = any>(event: string, listener: Listener<T>): void; /** Emit an event with a payload. */ emit<T = any>(event: string, payload: T): void; /** * Remove a listener or all listeners for an event. * Omit `listener` to remove all listeners for the event. */ off<T = any>(event: string, listener?: Listener<T>): void; /** Register a one-time listener that auto-unregisters after the first emit. */ once<T>(event: string, listener: Listener<T>): void; /** Remove all listeners for all events. */ clear(): void; /** Get number of subscribe */ getNumberOfSubscriber(event?: string): number; } /** * Reactive key-value store interface. * Supports: * - Reading current state (`getValues`) * - Updating via partials or functions (`setValue`) * - Immer-style updates (`setImmer`) when available * - Reset to the initial value (`reset`) * - Subscribing to the whole state or to a slice via a selector * * Important notes: * - Listeners are called only when the relevant data actually changes. * - `subscribe` returns an unsubscribe function — call it on unmount to avoid leaks. * - `subscribe` does **not** auto-invoke the listener initially (fits `useSyncExternalStore`); * if you need an initial call, read the snapshot and invoke it yourself at the call site. */ export interface Store<T> { /** * Read the full state or a specific property by key. * @param key Optional key within the state. * @returns Without `key` → the full state `T`. * With `key` → the value `T[K]`. */ getValue: UseStoreGet<T>; /** * Update state by shallow-merging `patch`, or by running a function * that returns a `patch`. The function may be async. */ setValue( patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>), ): void; /** * Returns the store's initial state value. * * - For arrays and objects → returns a shallow clone to avoid external mutations. * - For primitive types → returns the value as-is. * - The returned value represents the original initial state, not the current state. */ getInitialValue(): T; /** * Update state in Immer style. * @example * ```ts * store.setImmer(draft => { * draft.user.name = 'Hiep'; * draft.items.push({ id: 'x' }); * }); * ``` */ setImmer(updater: (draft: Draft<T>) => void): void; /** * Resets the state to the initial value (`initialState`) with a new reference (shallow clone). * * - If no argument is provided → the state is reset to `initialState` (new reference). * - If `initialValue` is provided: * - If it's an object/array → shallow-merge it into a clone of `initialState`. * - If it's a primitive → replace the state entirely with that value. * - Always creates a new reference; only updates if the result is not shallow-equal to the current state. */ reset(initialValue?: T | Partial<T>): void; /** * Subscribe to the entire state. * The listener is invoked **after commit** whenever state changes. * @returns Unsubscribe function. */ subscribe(listener: Listener<T>): () => void; /** * Subscribe to a derived slice of the state. * The listener fires only when the selector's result changes. * @returns Unsubscribe function. */ subscribe<S>(selector: (state: T) => S, listener: Listener<S>): () => void; /** * Checks whether the current state differs from the initial state. * * - For primitive types → compared using strict equality (`===`). * - For objects/arrays → compared using deep equality. * - Returns `true` if the state is modified, otherwise `false`. * * @example * ```ts * const store = createStore({ count: 0 }); * store.isDirty(); // false * * store.setValue({ count: 1 }); * store.isDirty(); // true * ``` */ isDirty(): boolean; /** Get number of subscriber for current store */ getNumberOfSubscriber(): number; } /** * Middleware that intercepts and transforms a `patch` before it is applied. * * Contract: * - Must call `next(patch)` to forward the update (similar to Redux middleware). * - Can read the current state via `getState()`. * - Useful for logging, validation, mapping, batching, devtools bridges, persistence, etc. * * @example Logging middleware: * ```ts * const logger: Middleware<State> = (next, get) => patch => { * console.log('Before:', get()); * next(patch); * console.log('After:', get()); * }; * ``` */ export type Middleware<T> = ( next: (patch: Partial<T>) => void, getState: () => T, ) => (patch: Partial<T>) => void;