pocket-state
Version:
tiny global store
188 lines (167 loc) • 5.02 kB
text/typescript
// store.ts
import {IEventEmitter, Listener, Middleware, Store, UseStoreGet} from './type';
import {EventEmitter} from './event';
import {Draft, produce} from 'immer';
import {shallow} from './shallowEqual';
export function createStore<T>(
initialState: T,
middlewares: Middleware<T>[] = [],
equalityFn?: (a: any, b: any) => boolean,
): Store<T> {
const emitter: IEventEmitter = new EventEmitter();
const _initialState = Array.isArray(initialState)
? (initialState as any).slice()
: initialState && typeof initialState === 'object'
? {...initialState}
: initialState;
let state = _initialState;
const areEqual = equalityFn ?? shallow;
const emitState = () => {
emitter.emit('state', state);
};
function baseSet(delta: Partial<T>) {
const nextState = Array.isArray(state)
? (delta as unknown as T)
: {...state, ...delta};
if (!areEqual(state, nextState)) {
state = nextState;
emitState();
}
}
const setFn = middlewares.reduceRight(
(next, mw) => mw(next, () => state),
baseSet as (patch: Partial<T>) => void,
);
const getValue = ((key?: keyof T) => {
if (key === undefined) return state;
return state[key];
}) as UseStoreGet<T>;
function subscribe(selectorOrListener: any, maybeListener?: any) {
let wrapped: Listener<any>;
if (typeof maybeListener === 'function') {
const selector: (s: T) => any = selectorOrListener;
let prevSlice = selector(state);
wrapped = (next: T) => {
const slice = selector(next);
if (!areEqual(slice, prevSlice)) {
prevSlice = slice;
maybeListener(slice);
}
};
} else {
const listener: Listener<T> = selectorOrListener;
let prev = state;
wrapped = (next: T) => {
if (!areEqual(next, prev)) {
prev = next;
listener(next);
}
};
}
emitter.on('state', wrapped);
return () => emitter.off('state', wrapped);
}
function setValue(
patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>),
): void {
let resolved = typeof patch === 'function' ? patch(state) : patch;
if (resolved instanceof Promise) {
resolved
.then(res => {
if (res && typeof res === 'object') {
setFn(res as Partial<T>);
}
})
.catch(error => console.warn('[store.setValue] patch error:', error));
} else {
if (resolved && typeof resolved === 'object') {
setFn(resolved as Partial<T>);
}
}
}
function setImmer(updater: (draft: Draft<T>) => void): void {
try {
const nextState = produce(state, updater);
if (areEqual(state, nextState)) return;
if (Array.isArray(state)) {
setFn(nextState as unknown as Partial<T>);
return;
}
const delta = {} as Partial<T>;
let changed = false;
for (const k in nextState as any) {
const nv = (nextState as any)[k];
const ov = (state as any)[k];
if (nv !== ov) {
(delta as any)[k] = nv;
changed = true;
}
}
for (const k in state as any) {
if (!(k in (nextState as any))) {
(delta as any)[k] = undefined;
changed = true;
}
}
if (changed) setFn(delta);
} catch (e) {
console.warn('[store.setImmer] error:', e);
}
}
function reset(): void;
function reset(initialValue?: T | Partial<T>): void;
function reset(initialValue?: T | Partial<T>) {
const isObj = (
v: unknown,
): v is Record<string | symbol | number, unknown> =>
typeof v === 'object' && v !== null;
const cloneShallow = <U>(src: U): U => {
if (Array.isArray(src)) return (src as any).slice();
if (isObj(src)) return {...(src as any)} as U;
return src;
};
let next = cloneShallow(initialState) as T;
if (initialValue !== undefined) {
if (Array.isArray(initialValue)) {
next = (initialValue as any).slice();
} else if (isObj(initialValue)) {
Object.assign(next as any, initialValue);
} else {
const current = getValue();
if (!Object.is(current as any, initialValue as any)) {
setFn(initialValue as unknown as Partial<T>);
}
return;
}
}
const current = getValue();
if (!areEqual(current, next)) {
setFn(next as unknown as Partial<T>);
}
}
function getInitialValue(): T {
if (Array.isArray(_initialState)) {
return (_initialState as any).slice();
}
if (_initialState && typeof _initialState === 'object') {
return {..._initialState};
}
return _initialState;
}
function isDirty() {
return !areEqual(state, _initialState);
}
function getNumberOfSubscriber() {
return emitter.getNumberOfSubscriber();
}
return {
getValue,
getInitialValue,
setValue,
setImmer,
reset,
subscribe,
isDirty,
getNumberOfSubscriber,
};
}