Skip to content

Commit

Permalink
add first assistant implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
megazazik committed Apr 10, 2020
1 parent a7a4ea9 commit 6337c84
Show file tree
Hide file tree
Showing 20 changed files with 4,156 additions and 30 deletions.
3,677 changes: 3,677 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 15 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@
"build": "tsc && tsc -p tsconfig.es.json",
"version": "npm run build && conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
"commit": "git-cz",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "ts-node node_modules/tape/bin/tape ./src/**/*.spec.ts"
},
"keywords": [
"react",
"context",
"ref"
"encaps",
"redux",
"middleware"
],
"author": "megazazik <[email protected]>",
"license": "MIT",
"devDependencies": {
"@types/react": "^16.9.3",
"@types/react-dom": "^16.9.0",
"@types/tape": "^4.2.34",
"commitizen": "^4.0.3",
"conventional-changelog-cli": "^2.0.23",
"cz-conventional-changelog": "^3.0.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"typescript": "^3.6.3"
"encaps": "^0.8.1",
"redux": "^4.0.5",
"tape": "^4.13.2",
"ts-node": "^8.8.1",
"typescript": "^3.8.3"
},
"peerDependencies": {
"react": "^16.8.0",
"react-dom": "^16.8.0"
"encaps": "^0.8.1"
},
"repository": {
"type": "git",
Expand All @@ -42,12 +42,13 @@
"path": "cz-conventional-changelog"
}
},
"dependencies": {},
"dependencies": {
"eventemitter3": "^4.0.0"
},
"files": [
"dist",
"dist-es",
"declarations",
"CHANGELOG.md",
"README.md"
]
}
}
246 changes: 246 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';
import { IModel, bindActionCreators } from 'encaps';
import EventEmitter from 'eventemitter3';
// ------------------------------------------
import './tmp';
// ------------------------------------------

const getStateSymbol = Symbol('getState');
const actionsSymbol = Symbol('actions');
const dispatchSymbol = Symbol('dispatch');
const subscribeSymbol = Symbol('subscribe');
const actionsEventEmitterSymbol = Symbol('actionsEe');
const initSymbol = Symbol('init');
const onDestroySymbol = Symbol('onDestroy');

const getDispatch = <A>(
dispatch: (action: any) => void,
actions: A
): { (action: any): void } & A =>
Object.assign(
(action: any) => dispatch(action),
bindActionCreators(actions as any, dispatch)
);

type Unsubscribe = () => void;

const BEFORE_ACTION_EVENT = 'before';
const AFTER_ACTION_EVENT = 'after';

export abstract class Assistant<M extends IModel<any, any>> {
private prevState: Readonly<ReturnType<M['reducer']>>;
private unsubscribes = new Set<Unsubscribe>();

public [actionsSymbol]: M['actions'];
public [getStateSymbol]: () => Readonly<ReturnType<M['reducer']>>;
public [dispatchSymbol]: { (action: any): void };
public [subscribeSymbol]: (callback: () => void) => Unsubscribe;
public [actionsEventEmitterSymbol]: EventEmitter;
public [onDestroySymbol]: () => void;

protected onDestroy() {}

protected onInit() {}

public get state() {
return this[getStateSymbol]();
}

protected onChange(callback: () => void) {
return this.addUnsubscribe(
this[subscribeSymbol](() => {
if (this.prevState !== this.state) {
callback();
}
})
);
}

protected afterAction(
type: string,
callback: (action: any) => void
): Unsubscribe;
protected afterAction(callback: (action: any) => void): Unsubscribe;
protected afterAction(
type: string | ((action: any) => void),
callback?: (action: any) => void
) {
const afterCallback = (action: any) => {
if (typeof type === 'string') {
if (action.type === type) {
callback(action);
}
return;
}
type(action);
};
this[actionsEventEmitterSymbol].on(AFTER_ACTION_EVENT, afterCallback);
return this.addUnsubscribe(() => {
this[actionsEventEmitterSymbol].removeListener(
AFTER_ACTION_EVENT,
afterCallback
);
});
}

protected beforeAction(
type: string,
callback: (action: any) => void
): Unsubscribe;
protected beforeAction(callback: (action: any) => void): Unsubscribe;
protected beforeAction(
type: string | ((action: any) => void),
callback?: (action: any) => void
) {
const beforeCallback = (action: any) => {
if (typeof type === 'string') {
if (action.type === type) {
callback(action);
}
return;
}
type(action);
};
this[actionsEventEmitterSymbol].on(BEFORE_ACTION_EVENT, beforeCallback);

return this.addUnsubscribe(() => {
this[actionsEventEmitterSymbol].removeListener(
BEFORE_ACTION_EVENT,
beforeCallback
);
});
}

protected dispatch: {
(action: any): void;
} & M['actions'];

public [initSymbol]() {
this.dispatch = getDispatch(this[dispatchSymbol], this[actionsSymbol]);
this.onInit();

/**
* сохраняем state перед каждым action, чтобы проверить на изменения
*/
this.beforeAction(() => {
this.prevState = this.state;
});
}

private addUnsubscribe(unsubscribe: Unsubscribe) {
const newUnsubscribe = () => {
unsubscribe();
this.unsubscribes.delete(newUnsubscribe);
};
this.unsubscribes.add(newUnsubscribe);
return newUnsubscribe;
}

public destroy() {
/** destroy children */
this.assistants.forEach((assistant) => {
assistant.destroy();
});
this.onDestroy();
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this[onDestroySymbol]();
}

private readonly assistants = new Set<Assistant<any>>();

protected createAssistant<A extends Assistant<any>>(
create: () => A,
select = (state: any) => state
): A {
const newAssistant = createAssistant(
create,
select,
() => this.state,
this[dispatchSymbol],
this[subscribeSymbol],
this[actionsSymbol],
this[actionsEventEmitterSymbol],
() => {
this.assistants.delete(newAssistant);
}
);
this.assistants.add(newAssistant);
return newAssistant;
}
}

export interface IModule<
M extends IModel<any, any>,
Effects extends Array<{ new (): Assistant<M> }>
> {
model: M;
effects?: Effects;
children?: Array<IModule<any, any>>;
}

export const createModule = <
M extends IModel<any, any>,
Effects extends Array<{ new (): Assistant<M> }>
>(
module: IModule<M, Effects>
) => module;

export const enhancer: (
module: IModule<IModel<any, any>, any[]>
) => StoreEnhancer = (module) => (createStore) => {
const newCreateStore: StoreEnhancerStoreCreator<{}, {}> = (
reducer,
preloadedState
) => {
const store = createStore<any, any>(reducer, preloadedState);
const actionsEmitter = new EventEmitter();
const dispatch = (action: any, ...args: any[]) => {
actionsEmitter.emit(BEFORE_ACTION_EVENT, action);
const result = (store.dispatch as any)(action, ...args);
actionsEmitter.emit(AFTER_ACTION_EVENT, action);
return result;
};

const enhancedStore = { ...store, dispatch };

/** @todo доработать очистку помощников */
module.effects.forEach((EffectConstructor) => {
createAssistant(
() => new EffectConstructor(),
(state) => state,
enhancedStore.getState,
enhancedStore.dispatch,
enhancedStore.subscribe,
module.model.actions,
actionsEmitter
);
});
return enhancedStore;
};
return newCreateStore;
};

/** @todo дописать типы */
function createAssistant<A extends Assistant<any>>(
assistantCreator: () => A,
select: (state: any) => any,
getState: () => any,
dispatch: (action: any) => void,
subscribe: (callback: () => void) => Unsubscribe,
actions: any,
eventemitter: EventEmitter,
onDestroy = () => {}
) {
const effect = assistantCreator();

effect[actionsSymbol] = actions;
effect[getStateSymbol] = () => select(getState());
effect[dispatchSymbol] = dispatch;
effect[subscribeSymbol] = subscribe;
effect[actionsEventEmitterSymbol] = eventemitter;
effect[onDestroySymbol] = onDestroy;

effect[initSymbol]();

return effect;
}
1 change: 0 additions & 1 deletion src/index.tsx

This file was deleted.

63 changes: 63 additions & 0 deletions src/indexDecoTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
class Action<T> {
payload: T;
}

// const reduce = <T>(
// prototype: Object,
// name: string | symbol,
// descriptor: TypedPropertyDescriptor<T>
// ) => {};

const decoTest = <P, S>() => (
prototype: Object,
name: string | symbol,
descriptor: TypedPropertyDescriptor<(a: Action<P>) => S>
) => {};

type ReducerKeys<T, M extends {}> = {
[K in keyof M]?: M[K] extends string ? K : never;
}[keyof M];

class Model<T> {
public readonly state: T;
public async setState(_: Partial<T>): Promise<void> {}
}

const reducer = <R extends string>(...reducers: R[]) => <
T,
M extends {
new (...args: any[]): Model<T> &
{ [K in R]: (state: T, action: Action<any>) => T };
}
>(
model: M
) => {};

interface IState {
value: number;
}

export class Form extends Model<IState> {
public increment(action: Action<string>) {
return this.state.value + 1;
}

public decrement({ payload }: Action<number>) {
return { value: this.state.value - payload };
}
}

/* @ */ reducer('increment', 'decrement');
export class FormModel extends Model<IState> {
public increment(state: IState, { payload }: Action<number>) {
return state.value + payload;
}

public decrement(state: IState, { payload }: Action<number>) {
return { value: state.value - payload };
}
}

const Test = (p: string) => class {};

class TT extends Test('sdf') {}
5 changes: 5 additions & 0 deletions src/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const metadata = new WeakMap<any[]>();

export const setEffects = (model: any, data: any) => {
metadata.set(model, data);
};
14 changes: 14 additions & 0 deletions src/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import tape from 'tape';
import { build } from 'encaps';
import middleware from '..';

tape((t) => {
const model = build()
.initState(() => ({ value: 10 }))
.handlers({
setValue: 'value',
});
// .wrap(middleware({}));

t.end();
});
Loading

0 comments on commit 6337c84

Please sign in to comment.