@sourcebug/amos
Version:
A decentralized state manager for react
308 lines (281 loc) • 7.77 kB
text/typescript
/*
* @since 2020-11-03 13:31:31
* @author acrazing <joking.young@gmail.com>
*/
import { Action } from './action';
import { Box, Mutation } from './box';
import { Selector } from './selector';
import { Signal } from './signal';
import { AmosObject, defineAmosObject, isArray } from './utils';
/**
* the state snapshot in store
*
* @stable
*/
export type Snapshot = Record<string, unknown>;
/**
* dispatchable things
*
* @stable
*/
export type Dispatchable<R = any> = Mutation<R> | Action<R> | Signal<R>;
/**
* base amos signature, this is used for someone want to change the signature of useDispatch()
*
* @stable
*/
export interface AmosDispatch extends AmosObject<'store.dispatch'> {
<R>(task: Dispatchable<R>): R;
<R1>(tasks: readonly [Dispatchable<R1>]): [R1];
<R1, R2>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>]): [R1, R2];
<R1, R2, R3>(tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>]): [
R1,
R2,
R3,
];
<R1, R2, R3, R4>(
tasks: readonly [Dispatchable<R1>, Dispatchable<R2>, Dispatchable<R3>, Dispatchable<R4>],
): [R1, R2, R3, R4];
<R1, R2, R3, R4, R5>(
tasks: readonly [
Dispatchable<R1>,
Dispatchable<R2>,
Dispatchable<R3>,
Dispatchable<R4>,
Dispatchable<R5>,
],
): [R1, R2, R3, R4, R5];
<R1, R2, R3, R4, R5, R6>(
tasks: readonly [
Dispatchable<R1>,
Dispatchable<R2>,
Dispatchable<R3>,
Dispatchable<R4>,
Dispatchable<R5>,
Dispatchable<R6>,
],
): [R1, R2, R3, R4, R5, R6];
<R1, R2, R3, R4, R5, R6, R7>(
tasks: readonly [
Dispatchable<R1>,
Dispatchable<R2>,
Dispatchable<R3>,
Dispatchable<R4>,
Dispatchable<R5>,
Dispatchable<R6>,
Dispatchable<R7>,
],
): [R1, R2, R3, R4, R5, R6, R7];
<R1, R2, R3, R4, R5, R6, R7, R8>(
tasks: readonly [
Dispatchable<R1>,
Dispatchable<R2>,
Dispatchable<R3>,
Dispatchable<R4>,
Dispatchable<R5>,
Dispatchable<R6>,
Dispatchable<R7>,
Dispatchable<R8>,
],
): [R1, R2, R3, R4, R5, R6, R7, R8];
<R1, R2, R3, R4, R5, R6, R7, R8, R9>(
tasks: readonly [
Dispatchable<R1>,
Dispatchable<R2>,
Dispatchable<R3>,
Dispatchable<R4>,
Dispatchable<R5>,
Dispatchable<R6>,
Dispatchable<R7>,
Dispatchable<R8>,
Dispatchable<R9>,
],
): [R1, R2, R3, R4, R5, R6, R7, R8, R9];
<R>(tasks: readonly Dispatchable<R>[]): R[];
}
export interface Dispatch extends AmosDispatch {}
/**
* selectable things
*
* @stable
*/
export type Selectable<R = any> = Box<R> | Selector<R>;
/**
* select
*
* @stable
*/
export interface Select extends AmosObject<'store.select'> {
<R>(selectable: Selectable<R>, snapshot?: Snapshot): R;
}
/**
* Store
*
* @stable
*/
export interface Store {
/**
* get the state snapshot of the store.
*
* Please note that any mutation of the snapshot is silent.
*/
snapshot: () => Snapshot;
/**
* dispatch one or more dispatchable things.
*/
dispatch: Dispatch;
/**
* subscribe the mutations
* @param fn
*/
subscribe: (fn: (updatedState: Snapshot) => void) => () => void;
/**
* select a selectable thing
*/
select: Select;
/**
* whether to auto batch the updates
* @default false
*/
isAutoBatch?: boolean;
batchedUpdates: SchedulerFn;
}
type SchedulerFn = (cb: () => void) => void;
export let batchedUpdates: SchedulerFn = (cb) => cb();
export type StoreEnhancer = (store: Store) => Store;
let keydownFlag = false;
if (document) {
document.addEventListener('keydown', () => (keydownFlag = true));
document.addEventListener('keyup', () => (keydownFlag = false));
}
/**
* create a store
* @param preloadedState
* @param enhancers
*
* @stable
*/
export function createStore(preloadedState?: Snapshot, ...enhancers: StoreEnhancer[]): Store {
const state: Snapshot = {};
const boxes: Box[] = [];
const listeners: Set<(updatedSnapshot: Snapshot) => void> = new Set();
const dispatchingListeners: Set<(updatedSnapshot: Snapshot) => void> = new Set();
const ensure = (box: Box) => {
if (state.hasOwnProperty(box.key)) {
return;
}
let boxState = box.initialState;
if (preloadedState?.hasOwnProperty(box.key)) {
boxState = box.preload(preloadedState[box.key], boxState);
}
state[box.key] = boxState;
boxes.push(box);
};
let dispatchDepth = 0;
let dispatchingSnapshot: Snapshot = {};
let store: Store;
function flushDispatchQueue() {
if (--dispatchDepth === 0) {
if (Object.keys(dispatchingSnapshot).length > 0) {
const resultSnapshot = { ...dispatchingSnapshot };
store.batchedUpdates(() => {
[...listeners].forEach((fn) => fn(resultSnapshot));
const baseDispatchingListeners = [...dispatchingListeners];
baseDispatchingListeners.forEach((fn) => {
dispatchingListeners.delete(fn);
return fn(resultSnapshot);
});
});
}
}
}
const record = (key: string, newState: unknown) => {
if (newState !== state[key] || dispatchingSnapshot.hasOwnProperty(key)) {
dispatchingSnapshot[key] = newState;
state[key] = newState;
}
};
const exec = (dispatchable: Dispatchable) => {
switch (dispatchable.object) {
case 'action':
return dispatchable.actor(store.dispatch, store.select, ...dispatchable.args);
case 'mutation':
ensure(dispatchable.box);
record(
dispatchable.box.key,
dispatchable.mutator(state[dispatchable.box.key], ...dispatchable.args),
);
return dispatchable.result;
case 'signal':
for (const box of boxes) {
const fn = box.listeners[dispatchable.type];
fn && record(box.key, fn(state[box.key], dispatchable.data));
}
return dispatchable.data;
}
};
let selectingSnapshot: Snapshot | undefined;
store = {
snapshot: () => state,
isAutoBatch: false,
subscribe: function subscribe(fn) {
if (dispatchDepth > 0) {
dispatchingListeners.add(fn);
}
listeners.add(fn);
return () => {
listeners.delete(fn);
};
},
dispatch: defineAmosObject('store.dispatch', function dispatch(
tasks: Dispatchable | readonly Dispatchable[],
) {
if (++dispatchDepth === 1) {
dispatchingSnapshot = {};
}
let res;
try {
if (isArray(tasks)) {
res = tasks.map(exec);
} else {
res = exec(tasks);
}
} catch {}
if (store.isAutoBatch && !keydownFlag) {
Promise.resolve().then(flushDispatchQueue);
} else {
flushDispatchQueue();
}
return res;
} as any),
select: defineAmosObject('store.select', function select(
selectable: Selectable,
snapshot?: Snapshot,
) {
if (typeof selectable === 'function') {
if (snapshot) {
if (selectingSnapshot) {
throw new Error(`[Amos] recursive snapshot collection is not supported.`);
}
selectingSnapshot = snapshot;
try {
return selectable(store.select);
} finally {
selectingSnapshot = void 0;
}
} else {
return selectable(store.select);
}
} else {
ensure(selectable);
if (selectingSnapshot) {
selectingSnapshot[selectable.key] = state[selectable.key];
}
return state[selectable.key];
}
}) as any,
batchedUpdates: (cb) => cb(),
};
store = enhancers.reduce((previousValue, currentValue) => currentValue(previousValue), store);
return store;
}