From 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 Mon Sep 17 00:00:00 2001 From: Herbert Eiselt Date: Mon, 11 Feb 2019 14:54:12 +0100 Subject: Add sdnr wt odlux Add complete sdnr wireless transport app odlux core and apps Change-Id: I5dcbfb8f3b790e3bda7c8df67bd69d81958f65e5 Issue-ID: SDNC-576 Signed-off-by: Herbert Eiselt --- sdnr/wt/odlux/framework/src/flux/action.ts | 8 ++ sdnr/wt/odlux/framework/src/flux/connect.ts | 148 +++++++++++++++++++++++++ sdnr/wt/odlux/framework/src/flux/middleware.ts | 90 +++++++++++++++ sdnr/wt/odlux/framework/src/flux/store.ts | 81 ++++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 sdnr/wt/odlux/framework/src/flux/action.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/connect.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/middleware.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/store.ts (limited to 'sdnr/wt/odlux/framework/src/flux') diff --git a/sdnr/wt/odlux/framework/src/flux/action.ts b/sdnr/wt/odlux/framework/src/flux/action.ts new file mode 100644 index 000000000..8a90f24b2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/action.ts @@ -0,0 +1,8 @@ +/** + * Represents an action in the odlux flux architecture. + */ +export abstract class Action { } + +export interface IActionHandler { + (state: TState | undefined, action: TAction): TState; +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/connect.ts b/sdnr/wt/odlux/framework/src/flux/connect.ts new file mode 100644 index 000000000..0c8d36d0a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/connect.ts @@ -0,0 +1,148 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; + +import { Dispatch } from '../flux/store'; + +import { ApplicationStore, IApplicationStoreState } from '../store/applicationStore'; + +interface IApplicationStoreContext { + applicationStore: ApplicationStore; +} + +export interface IDispatcher { + dispatch: Dispatch; +} + +interface IApplicationStoreProps { + state: IApplicationStoreState; +} + +interface IDispatchProps { + dispatch: Dispatch; +} + +type FuncInfer = { + ([...args]: any): T; +}; + +type FunctionResult = T extends FuncInfer ? U : never; + +type ComponentDecoratorInfer = { + (wrappedComponent: React.ComponentType): React.ComponentClass; +}; + +export type Connect = + (TMapProps extends undefined ? IApplicationStoreProps : FunctionResult) & + (TMapDispatch extends undefined ? IDispatchProps : FunctionResult); + +export function connect(): ComponentDecoratorInfer; + +export function connect( + mapStateToProps: (state: IApplicationStoreState) => TStateProps +): ComponentDecoratorInfer; + +export function connect( + mapStateToProps: (state: IApplicationStoreState) => TStateProps, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer; + + +export function connect( + mapStateToProps: undefined, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer; + + +export function connect( + mapStateToProps?: ((state: IApplicationStoreState) => TStateProps), + mapDispatchToProps?: ((dispatcher: IDispatcher) => TDispatchProps) +) : + ((WrappedComponent: React.ComponentType) => React.ComponentType) { + + const injectApplicationStore = (WrappedComponent: React.ComponentType): React.ComponentType => { + + class StoreAdapter extends React.Component { + public static contextTypes = { ...WrappedComponent.contextTypes, applicationStore: PropTypes.object.isRequired }; + context: IApplicationStoreContext; + + render(): JSX.Element { + + if (isWrappedComponentIsVersion1(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && isWrappedComponentIsVersion2(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion3(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); + return element; + } else if (!mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion4(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); + return element; + } + throw new Error("Invalid arguments in connect."); + } + + componentDidMount(): void { + this.store && this.store.changed.addHandler(this.handleStoreChanged); + } + + componentWillUnmount(): void { + this.store && this.store.changed.removeHandler(this.handleStoreChanged); + } + + private get store(): ApplicationStore { + return this.context.applicationStore; + } + + private handleStoreChanged = () => { + this.forceUpdate(); + } + } + + return StoreAdapter; + } + + + return injectApplicationStore; + + /* inline methods */ + + function isWrappedComponentIsVersion1(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion2(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !!mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion3(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !!mapStateToProps && !!mapDispatchToProps; + } + + function isWrappedComponentIsVersion4(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !mapStateToProps && !!mapDispatchToProps; + } +} + +interface ApplicationStoreProviderProps extends React.Props { + applicationStore: ApplicationStore; +} + +export class ApplicationStoreProvider extends React.Component + implements /* React.ComponentLifecycle, */ React.ChildContextProvider { + + public static childContextTypes = { applicationStore: PropTypes.object.isRequired }; + + getChildContext(): IApplicationStoreContext { + return { + applicationStore: this.props.applicationStore + }; + } + + render(): JSX.Element { + return React.Children.only(this.props.children); + } +} + +export default connect; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/middleware.ts b/sdnr/wt/odlux/framework/src/flux/middleware.ts new file mode 100644 index 000000000..006d94551 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/middleware.ts @@ -0,0 +1,90 @@ +import { Action, IActionHandler } from './action'; +import { Store, Dispatch, Enhancer } from './store'; + +export interface MiddlewareArg { + dispatch: Dispatch; + getState: () => T; +} + +export interface Middleware { + (obj: MiddlewareArg): Function; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export type ActionHandlerMapObject = { + [K in keyof S]: IActionHandler +} + +export const combineActionHandler = (actionHandlers: ActionHandlerMapObject) : IActionHandler => { + const finalActionHandlers: ActionHandlerMapObject = {} as ActionHandlerMapObject; + Object.keys(actionHandlers).forEach(actionHandlerKey => { + const handler = actionHandlers[actionHandlerKey]; + if (typeof handler === 'function') { + finalActionHandlers[actionHandlerKey] = handler; + } + }); + + // ensure initialisation + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const initialState = actionHandler(undefined, initialisationAction); + if (typeof initialState === 'undefined') { + const errorMessage = `Action handler ${ key } returned undefiend during initialization.`; + throw new Error(errorMessage); + } + }); + + return function combination(state: TState = ({} as TState), action: TAction) { + let hasChanged = false; + const nextState : TState = {} as TState; + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const previousState = state[key]; + const nextStateKey = actionHandler(previousState, action); + if (typeof nextStateKey === 'undefined') { + const errorMessage = `Given ${ action.constructor } and action handler ${ key } returned undefiend.`; + throw new Error(errorMessage); + } + nextState[key] = nextStateKey; + hasChanged = hasChanged || nextStateKey !== previousState; + }); + return (hasChanged ? nextState : state) as TState; + }; +}; + +export const chainMiddleware = (...middlewares: Middleware[]): Enhancer => { + return (store: Store) => { + const middlewareAPI = { + getState() { return store.state }, + dispatch: (action: TAction) => store.dispatch(action) // we want to use the combinded dispatch + // we should NOT use the flux dispatcher here, since the action would affect ALL stores + }; + const chain = middlewares.map(middleware => middleware(middlewareAPI)); + return compose(...chain)(store.dispatch) as Dispatch; + } +}; + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for + * the resulting composite function. + * + * @param {...Function} funcs The functions to compose. + * @returns {Function} A function obtained by composing the argument functions + * from right to left. For example, compose(f, g, h) is identical to doing + * (...args) => f(g(h(...args))). + */ +const compose = (...funcs: Function[]) => { + if (funcs.length === 0) { + return (arg: any) => arg + } + + if (funcs.length === 1) { + return funcs[0] + } + + return funcs.reduce((a, b) => (...args: any[]) => a(b(...args))); +}; + diff --git a/sdnr/wt/odlux/framework/src/flux/store.ts b/sdnr/wt/odlux/framework/src/flux/store.ts new file mode 100644 index 000000000..7a2e578f9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/store.ts @@ -0,0 +1,81 @@ +import { Event } from "../common/event" + +import { Action } from './action'; +import { IActionHandler } from './action'; + +export interface Dispatch { + (action: TAction): TAction; +} + +export interface Enhancer { + (store: Store): Dispatch; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export class Store { + + constructor(actionHandler: IActionHandler, enhancer?: Enhancer) + constructor(actionHandler: IActionHandler, initialState: TStoreState, enhancer?: Enhancer) + constructor(actionHandler: IActionHandler, initialState?: TStoreState | Enhancer, enhancer?: Enhancer) { + if (typeof initialState === 'function') { + enhancer = initialState as Enhancer; + initialState = undefined; + } + + this._isDispatching = false; + + this.changed = new Event(); // sollten wir hier eventuell sogar den state mit übergeben ? + + this._actionHandler = actionHandler; + + this._state = initialState as TStoreState; + if (enhancer) this._dispatch = enhancer(this); + + this._dispatch(initialisationAction); + } + + public changed: Event; + + private _dispatch: Dispatch = (payload: TAction): TAction => { + if (payload == null || !(payload instanceof Action)) { + throw new Error( + 'Actions must inherit from type Action. ' + + 'Use a custom middleware for async actions.' + ); + } + + if (this._isDispatching) { + throw new Error('ActionHandler may not dispatch actions.'); + } + + const oldState = this._state; + try { + this._isDispatching = true; + this._state = this._actionHandler(oldState, payload); + } finally { + this._isDispatching = false; + } + + if (this._state !== oldState) { + this.changed.invoke(); + } + + return payload; + } + + public get dispatch(): Dispatch { + return this._dispatch; + } + + public get state() { + return this._state + } + + private _state: TStoreState; + private _isDispatching: boolean; + private _actionHandler: IActionHandler; + +} + -- cgit 1.2.3-korg