import { Component } from 'react'; import { Context } from 'immutability-helper'; // util type PropType = Obj[Prop]; type VoidFunc = (arg?: any) => void; // define query commands export interface CommandMap { [ command: string ]: (...args: any) => any; } type UpdateCommands = { [K in keyof S]: any }; class UpdateQuery { constructor(public commands: UpdateCommands>) {} } export type NextState = Partial | UpdateCommands>; // define actions type ActionResult = void | Partial | Promise | UpdateQuery> | UpdateQuery; export type Action = (this: ActionSandbox, ...args: any[]) => ActionResult; interface ActionMap { [ name: string ]: Action; } // define Nanox actions export type NanoxAction = (...args: any[]) => Promise; export type NanoxActionMap = { [ K in keyof A ]: NanoxAction }; // define sandbox interface ActionSandbox { state: S; query(commands: UpdateCommands>): UpdateQuery; } // class Nanox interface InternalProps { actions: {}; commands?: CommandMap; } export default class Nanox

extends Component { public actions!: NanoxActionMap>; private updateContext!: Context; private obsoleteMessage = [ '*** Nanox version 0.2.0 has many breaking changes from version 0.1.x', '*** see https://github.com/ktty1220/nanox', '' ].join('\n'); constructor(props: P) { super(props); if (! ('actions' in (props || {}))) { throw new Error(`requires the action props\n${this.obsoleteMessage}`); } this.registerActions(props.actions); this.registerCommands(props.commands); } // setState hook protected onSetState(_nextState: NextState, _type: string): boolean | void { return true; } // clone object private clone(obj: OBJ): OBJ { return { ...obj }; } // obsolete protected dispatch(_action: string, ..._args: any[]): never { throw new Error(this.obsoleteMessage); } // apply action result to state private updateStore(result: ActionResult, resolve: VoidFunc, reject: VoidFunc): void { // null or undefined -> skip if (result == null) { resolve(); return; } // not object -> error const type = (Array.isArray(result)) ? 'array' : typeof result; if (type !== 'object') { reject(new Error(`invalid action result: ${result} is ${type}`)); return; } if (result instanceof Promise) { // Promise -> resolve -> updateStore result.then((data) => this.updateStore(data, resolve, reject)).catch(reject); return; } const isQuery = (result instanceof UpdateQuery); // object or UpdateQuery -> setState const nextState = (isQuery) ? (result as UpdateQuery).commands : result as Partial; if (this.onSetState(this.clone(nextState), (isQuery) ? 'query' : 'state') === false) { console.warn('setState() was blocked at onSetState()'); resolve(); return; } this.setState( (isQuery) ? (currentState) => this.updateContext.update(currentState, nextState as any) : nextState, resolve ); } // create function that execute action and update state private createAction(func: Action): NanoxAction { const self = this; return (...args: any[]) => ( new Promise((resolve, reject) => { // sandbox const sandbox: ActionSandbox = { get state() { return self.clone(self.state || {}) as S; }, query: (commands) => new UpdateQuery(commands) }; this.updateStore(func.apply(Object.freeze(sandbox), args), resolve, reject); }) ); } // convert actions props to nanox actions private registerActions(actionMap: ActionMap): void { if (this.actions != null) { throw new Error('actions are already registered'); } const actions = Object.freeze({ ...actionMap }); const nanoxActions = {}; for (const [ evt, func ] of Object.entries(actions)) { nanoxActions[evt] = this.createAction(func); } this.actions = Object.freeze(nanoxActions) as NanoxActionMap>; } // register custom commands for sandbox.query function private registerCommands(commandMap?: CommandMap): void { this.updateContext = new Context(); if (commandMap == null) return; for (const [ cmd, func ] of Object.entries(commandMap)) { this.updateContext.extend(cmd, func); } } }