export type Action = ( getState: GetState, dispatch: Dispatch ) => T | void | Promise; export type Dispatch = (action: Action) => Promise; export type GetState = () => T; export type ActionCreator = ( ...args: U ) => Action; export const $name = Symbol('name'); export type NamedAction = Action & { [$name]: string; }; export type SelfDispatchingActionCreator = ( ...args: T ) => Promise; export type Middleware = (store: Store) => Store; const defaultLogger = (actionName: string, state: any) => console.warn(`Action<${actionName}>`, state); export interface StoreOptions { verbose?: boolean; logger?: ( logger: (...args: any) => void, actionName: string, state: T ) => void; middleware?: Middleware[]; } export const action = ( name: string, action: Action ): NamedAction => { (action as NamedAction)[$name] = name; return action as NamedAction; }; export const isNamedAction = ( action: Action ): action is NamedAction => { return (action as NamedAction)[$name] != null; }; const initializeStore = action('INITIALIZE_STORE', (getState) => getState() ) as NamedAction; export class Store extends EventTarget { #state: T; #verbose: boolean; #logger: (actionName: string, state: T) => void; constructor(initialState: T, options: StoreOptions = {}) { super(); this.#verbose = !!options.verbose; const { logger, middleware } = options; this.#logger = logger != null ? (actionName: string, state: T) => logger(defaultLogger, actionName, state) : defaultLogger; this.#state = initialState; let store: Store = this; if (middleware != null) { for (const ware of middleware) { store = ware(store); } } Promise.resolve().then(() => { store.dispatch(initializeStore); }); return store; } get state() { return this.#state; } async dispatch(action: Action): Promise { let newState = await action( () => this.state, (action: Action) => this.dispatch(action) ); if (newState == null) { return; } if (this.#verbose) { const name = isNamedAction(action) ? action[$name] : 'ANONYMOUS'; this.#logger(name, newState); } this.#state = newState; this.dispatchEvent(new CustomEvent('state-change')); } }