├── packages └── core │ ├── README.md │ ├── es │ ├── types │ │ ├── model.js │ │ ├── model.js.map │ │ ├── Action.js.map │ │ ├── Action.js │ │ ├── notification.d.ts │ │ ├── notification.js.map │ │ ├── Action.d.ts │ │ ├── notification.js │ │ └── model.d.ts │ ├── constants │ │ ├── symbols.d.ts │ │ ├── meta.d.ts │ │ ├── symbols.js │ │ ├── meta.js.map │ │ ├── symbols.js.map │ │ ├── meta.js │ │ ├── notification.d.ts │ │ ├── notification.js.map │ │ ├── notification.js │ │ ├── actionTypes.js.map │ │ ├── actionTypes.js │ │ └── actionTypes.d.ts │ ├── utils │ │ ├── function.d.ts │ │ ├── logic.js.map │ │ ├── function.js.map │ │ ├── logic.d.ts │ │ ├── logic.js │ │ ├── function.js │ │ ├── action.d.ts │ │ ├── action.js.map │ │ └── action.js │ ├── reducers │ │ ├── error.d.ts │ │ ├── loading.d.ts │ │ ├── error.js.map │ │ ├── loading.js.map │ │ ├── error.js │ │ └── loading.js │ ├── operators │ │ ├── endTo.js.map │ │ ├── endTo.d.ts │ │ ├── end.js.map │ │ ├── end.d.ts │ │ ├── endTo.js │ │ ├── end.js │ │ ├── createService.d.ts │ │ ├── createService.js.map │ │ └── createService.js │ ├── index.d.ts │ ├── index.js.map │ └── index.js │ ├── lib │ ├── types │ │ ├── model.js.map │ │ ├── model.js │ │ ├── Action.js.map │ │ ├── Action.js │ │ ├── notification.d.ts │ │ ├── notification.js.map │ │ ├── Action.d.ts │ │ ├── notification.js │ │ └── model.d.ts │ ├── constants │ │ ├── symbols.d.ts │ │ ├── meta.d.ts │ │ ├── meta.js.map │ │ ├── symbols.js.map │ │ ├── notification.d.ts │ │ ├── notification.js.map │ │ ├── symbols.js │ │ ├── meta.js │ │ ├── notification.js │ │ ├── actionTypes.js.map │ │ ├── actionTypes.d.ts │ │ └── actionTypes.js │ ├── utils │ │ ├── function.d.ts │ │ ├── logic.js.map │ │ ├── function.js.map │ │ ├── logic.d.ts │ │ ├── action.d.ts │ │ ├── logic.js │ │ ├── function.js │ │ ├── action.js.map │ │ └── action.js │ ├── reducers │ │ ├── error.d.ts │ │ ├── loading.d.ts │ │ ├── error.js.map │ │ ├── loading.js.map │ │ ├── error.js │ │ └── loading.js │ ├── operators │ │ ├── endTo.js.map │ │ ├── end.js.map │ │ ├── endTo.d.ts │ │ ├── end.d.ts │ │ ├── endTo.js │ │ ├── end.js │ │ ├── createService.d.ts │ │ ├── createService.js.map │ │ └── createService.js │ ├── index.d.ts │ └── index.js.map │ ├── test │ ├── mocha.opts │ ├── action.test.ts │ ├── operator.test.ts │ ├── init.test.ts │ ├── redux.test.ts │ ├── flow.test.ts │ └── model.test.ts │ ├── src │ ├── constants │ │ ├── symbols.ts │ │ ├── meta.ts │ │ ├── notification.ts │ │ └── actionTypes.ts │ ├── utils │ │ ├── function.ts │ │ ├── logic.ts │ │ └── action.ts │ ├── types │ │ ├── action.ts │ │ ├── Notification.ts │ │ └── Model.ts │ ├── operators │ │ ├── endTo.ts │ │ ├── end.ts │ │ └── createService.ts │ ├── reducers │ │ ├── error.ts │ │ └── loading.ts │ └── index.ts │ ├── index.d.ts │ ├── package.json │ └── package-lock.json ├── .vscode └── settings.json ├── logo.png ├── .coveralls.yml ├── docs ├── logo.png ├── public │ ├── error.png │ ├── spinner.png │ ├── error-notify.png │ └── success-notify.png ├── README.md ├── introduction │ ├── README.md │ ├── get-started.md │ └── concepts.md ├── .vuepress │ └── config.js └── basics │ ├── error-state.md │ ├── loading-state.md │ ├── README.md │ ├── change-and-patch.md │ └── custom-service.md ├── mocha.opts ├── lerna.json ├── examples ├── README.md ├── github │ ├── src │ │ ├── index.html │ │ ├── models │ │ │ ├── common.ts │ │ │ ├── user.ts │ │ │ └── repo.ts │ │ ├── services │ │ │ ├── user.ts │ │ │ ├── repo.ts │ │ │ └── types.ts │ │ ├── modules │ │ │ ├── repo │ │ │ │ ├── index.tsx │ │ │ │ ├── Table.tsx │ │ │ │ └── ActionPanel.tsx │ │ │ ├── user │ │ │ │ ├── index.tsx │ │ │ │ ├── Table.tsx │ │ │ │ └── ActionPanel.tsx │ │ │ └── app │ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── tsconfig.json │ └── webpack.config.js └── package.json ├── .travis.yml ├── tsconfig.json ├── TODO.md ├── .editorconfig ├── tslint.json ├── README.md └── package.json /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # reobservable core 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /packages/core/es/types/model.js: -------------------------------------------------------------------------------- 1 | //# sourceMappingURL=Model.js.map -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/logo.png -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: PdN5FiwxBcKCiYXIqoy3qA4Iri5ij7F1P -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/public/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/docs/public/error.png -------------------------------------------------------------------------------- /docs/public/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/docs/public/spinner.png -------------------------------------------------------------------------------- /docs/public/error-notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/docs/public/error-notify.png -------------------------------------------------------------------------------- /docs/public/success-notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reobservable/reobservable/HEAD/docs/public/success-notify.png -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers ts-node/register 2 | --require source-map-support/register 3 | --bail 4 | --colors 5 | packages/**/test/*.test.ts 6 | -------------------------------------------------------------------------------- /packages/core/es/types/model.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Model.js","sourceRoot":"","sources":["../../src/types/Model.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /packages/core/lib/types/model.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Model.js","sourceRoot":"","sources":["../../src/types/Model.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /packages/core/lib/types/model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=Model.js.map -------------------------------------------------------------------------------- /packages/core/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers ts-node/register 2 | --require source-map-support/register 3 | --full-trace 4 | --bail 5 | --colors 6 | test/*.test.ts 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.3", 3 | "packages": [ 4 | "packages/*", 5 | "examples" 6 | ], 7 | "lerna": "2.11.0", 8 | "npmClient": "npm" 9 | } 10 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # reobservable examples 2 | 3 | ## Install 4 | 5 | ``` 6 | tnpm i 7 | ``` 8 | 9 | ## Run Demo 10 | 11 | ``` 12 | tnpm run start:github 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/core/src/constants/symbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * symbols 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-19 16:55:51 5 | */ 6 | export const ALIAS = Symbol('alias') 7 | -------------------------------------------------------------------------------- /packages/core/es/constants/symbols.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * symbols 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-19 16:55:51 5 | */ 6 | export declare const ALIAS: unique symbol; 7 | -------------------------------------------------------------------------------- /packages/core/lib/constants/symbols.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * symbols 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-19 16:55:51 5 | */ 6 | export declare const ALIAS: unique symbol; 7 | -------------------------------------------------------------------------------- /packages/core/lib/types/Action.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/types/action.ts"],"names":[],"mappings":";;AACA;;;;GAIG;AACH,0CAAsD;AACtD,gDAA4C"} -------------------------------------------------------------------------------- /examples/github/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo Board 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/core/es/constants/meta.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * meta constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:30:32 5 | */ 6 | export declare const FLOW_END_INDICATOR: unique symbol; 7 | -------------------------------------------------------------------------------- /packages/core/lib/constants/meta.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * meta constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:30:32 5 | */ 6 | export declare const FLOW_END_INDICATOR: unique symbol; 7 | -------------------------------------------------------------------------------- /packages/core/es/constants/symbols.js: -------------------------------------------------------------------------------- 1 | /** 2 | * symbols 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-19 16:55:51 5 | */ 6 | export var ALIAS = Symbol('alias'); 7 | //# sourceMappingURL=symbols.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/meta.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/constants/meta.ts"],"names":[],"mappings":";AACA;;;;GAIG;;AAEU,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAA"} -------------------------------------------------------------------------------- /packages/core/src/constants/meta.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * meta constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:30:32 5 | */ 6 | 7 | export const FLOW_END_INDICATOR = Symbol('FLOW_END_INDICATOR') 8 | -------------------------------------------------------------------------------- /packages/core/es/constants/meta.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/constants/meta.ts"],"names":[],"mappings":"AACA;;;;GAIG;AAEH,MAAM,CAAC,IAAM,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAA"} -------------------------------------------------------------------------------- /packages/core/lib/constants/symbols.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"symbols.js","sourceRoot":"","sources":["../../src/constants/symbols.ts"],"names":[],"mappings":";;AAAA;;;;GAIG;AACU,QAAA,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA"} -------------------------------------------------------------------------------- /packages/core/es/constants/symbols.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"symbols.js","sourceRoot":"","sources":["../../src/constants/symbols.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,IAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA"} -------------------------------------------------------------------------------- /packages/core/es/utils/function.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * function utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 15:32:55 5 | */ 6 | /** 7 | * noop 8 | */ 9 | export declare function noop(...args: any[]): any; 10 | -------------------------------------------------------------------------------- /packages/core/es/constants/meta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * meta constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:30:32 5 | */ 6 | export var FLOW_END_INDICATOR = Symbol('FLOW_END_INDICATOR'); 7 | //# sourceMappingURL=meta.js.map -------------------------------------------------------------------------------- /packages/core/lib/utils/function.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * function utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 15:32:55 5 | */ 6 | /** 7 | * noop 8 | */ 9 | export declare function noop(...args: any[]): any; 10 | -------------------------------------------------------------------------------- /packages/core/es/types/Action.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/types/action.ts"],"names":[],"mappings":"AACA;;;;GAIG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA"} -------------------------------------------------------------------------------- /packages/core/src/utils/function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * function utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 15:32:55 5 | */ 6 | 7 | /** 8 | * noop 9 | */ 10 | export function noop(...args): any { 11 | // empty 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/constants/notification.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * notification constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:45:50 5 | */ 6 | 7 | export const LEVEL = { 8 | silent: 0, 9 | error: 1, 10 | all: 2 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/es/utils/logic.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"logic.js","sourceRoot":"","sources":["../../src/utils/logic.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,GAAQ;IAC5B,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAA;AAC1C,CAAC"} -------------------------------------------------------------------------------- /packages/core/es/utils/function.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"function.js","sourceRoot":"","sources":["../../src/utils/function.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,UAAU,IAAI;IAAC,cAAO;SAAP,UAAO,EAAP,qBAAO,EAAP,IAAO;QAAP,yBAAO;;IAC1B,QAAQ;AACV,CAAC"} -------------------------------------------------------------------------------- /packages/core/lib/utils/logic.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"logic.js","sourceRoot":"","sources":["../../src/utils/logic.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAEH;;;;;GAKG;AACH,SAAgB,KAAK,CAAC,GAAQ;IAC5B,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAA;AAC1C,CAAC;AAFD,sBAEC"} -------------------------------------------------------------------------------- /packages/core/es/constants/notification.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * notification constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:45:50 5 | */ 6 | export declare const LEVEL: { 7 | silent: number; 8 | error: number; 9 | all: number; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/es/types/Action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:19:14 5 | */ 6 | import { FLOW_END_INDICATOR } from '../constants/meta'; 7 | import { ALIAS } from '../constants/symbols'; 8 | //# sourceMappingURL=action.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/notification.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * notification constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:45:50 5 | */ 6 | export declare const LEVEL: { 7 | silent: number; 8 | error: number; 9 | all: number; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/lib/constants/notification.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"notification.js","sourceRoot":"","sources":["../../src/constants/notification.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAEU,QAAA,KAAK,GAAG;IACnB,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;CACP,CAAA"} -------------------------------------------------------------------------------- /packages/core/lib/utils/function.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"function.js","sourceRoot":"","sources":["../../src/utils/function.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAEH;;GAEG;AACH,SAAgB,IAAI;IAAC,cAAO;SAAP,UAAO,EAAP,qBAAO,EAAP,IAAO;QAAP,yBAAO;;IAC1B,QAAQ;AACV,CAAC;AAFD,oBAEC"} -------------------------------------------------------------------------------- /packages/core/es/constants/notification.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"notification.js","sourceRoot":"","sources":["../../src/constants/notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,IAAM,KAAK,GAAG;IACnB,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;CACP,CAAA"} -------------------------------------------------------------------------------- /packages/core/es/constants/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * notification constants 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:45:50 5 | */ 6 | export var LEVEL = { 7 | silent: 0, 8 | error: 1, 9 | all: 2 10 | }; 11 | //# sourceMappingURL=notification.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/symbols.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * symbols 5 | * @author yoyoyohamapi 6 | * @ignore created 2018-09-19 16:55:51 7 | */ 8 | exports.ALIAS = Symbol('alias'); 9 | //# sourceMappingURL=symbols.js.map -------------------------------------------------------------------------------- /packages/core/es/reducers/error.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * error reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:43:11 5 | */ 6 | import { Action } from '../types/action'; 7 | export default function error(state: { 8 | flows: {}; 9 | services: {}; 10 | }, action: Action): Object; 11 | -------------------------------------------------------------------------------- /packages/core/lib/reducers/error.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * error reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:43:11 5 | */ 6 | import { Action } from '../types/action'; 7 | export default function error(state: { 8 | flows: {}; 9 | services: {}; 10 | }, action: Action): Object; 11 | -------------------------------------------------------------------------------- /packages/core/es/reducers/loading.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * loading reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:36:49 5 | */ 6 | import { Action } from '../types/action'; 7 | export default function loading(state: { 8 | services: {}; 9 | flows: {}; 10 | }, action: Action): Object; 11 | -------------------------------------------------------------------------------- /packages/core/es/utils/logic.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * logic utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:13:16 5 | */ 6 | /** 7 | * is obj is nil(null or nil) 8 | * @param {any} obj 9 | * @returns {boolean} 10 | * @method isNil 11 | */ 12 | export declare function isNil(obj: any): boolean; 13 | -------------------------------------------------------------------------------- /packages/core/lib/constants/meta.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * meta constants 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-03 11:30:32 6 | */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.FLOW_END_INDICATOR = Symbol('FLOW_END_INDICATOR'); 9 | //# sourceMappingURL=meta.js.map -------------------------------------------------------------------------------- /packages/core/lib/reducers/loading.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * loading reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:36:49 5 | */ 6 | import { Action } from '../types/action'; 7 | export default function loading(state: { 8 | services: {}; 9 | flows: {}; 10 | }, action: Action): Object; 11 | -------------------------------------------------------------------------------- /packages/core/lib/utils/logic.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * logic utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:13:16 5 | */ 6 | /** 7 | * is obj is nil(null or nil) 8 | * @param {any} obj 9 | * @returns {boolean} 10 | * @method isNil 11 | */ 12 | export declare function isNil(obj: any): boolean; 13 | -------------------------------------------------------------------------------- /examples/github/src/models/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * common model 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-12 23:54:27 5 | */ 6 | 7 | export enum Order { 8 | Desc = 'desc', 9 | Asc = 'asc' 10 | } 11 | 12 | export interface Pagination { 13 | page: number 14 | pageSize: number 15 | total: number 16 | } 17 | -------------------------------------------------------------------------------- /examples/github/src/services/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * user service 3 | * @author yoyoyohamapi 4 | * @ingore created 2018-08-14 00:01:23 5 | */ 6 | import axios from 'axios' 7 | import { SearchParam } from '@models/user' 8 | 9 | export const fetch = (params: SearchParam) => 10 | axios.get('https://api.github.com/search/users', { params }) 11 | -------------------------------------------------------------------------------- /packages/core/lib/operators/endTo.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"endTo.js","sourceRoot":"","sources":["../../src/operators/endTo.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA;;;;GAIG;AACH,6BAAsD;AACtD,4CAAsC;AAEtC,0CAAsD;AAEtD;;;;;GAKG;AACH,SAAwB,KAAK,CAAC,MAAc;;IAC1C,OAAO,WAAI,CACT,iBAAK,cACA,MAAM,eACR,yBAAkB,IAAG,IAAI,OAC1B,CACH,CAAA;AACH,CAAC;AAPD,wBAOC"} -------------------------------------------------------------------------------- /examples/github/src/services/repo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * repo service 3 | * @author yoyoyohamapi 4 | * @ingore created 2018-08-14 00:01:23 5 | */ 6 | import axios from 'axios' 7 | import { SearchParam } from '@models/repo' 8 | 9 | export const fetch = (params: SearchParam) => 10 | axios.get('https://api.github.com/search/repositories', { params }) 11 | -------------------------------------------------------------------------------- /packages/core/lib/types/Action.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * action types 5 | * @author yoyoyohamapi 6 | * @ignore created 2018-08-03 11:19:14 7 | */ 8 | var meta_1 = require("../constants/meta"); 9 | var symbols_1 = require("../constants/symbols"); 10 | //# sourceMappingURL=action.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/notification.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * notification constants 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-13 14:45:50 6 | */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.LEVEL = { 9 | silent: 0, 10 | error: 1, 11 | all: 2 12 | }; 13 | //# sourceMappingURL=notification.js.map -------------------------------------------------------------------------------- /packages/core/src/utils/logic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * logic utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:13:16 5 | */ 6 | 7 | /** 8 | * is obj is nil(null or nil) 9 | * @param {any} obj 10 | * @returns {boolean} 11 | * @method isNil 12 | */ 13 | export function isNil(obj: any): boolean { 14 | return obj === null || obj === undefined 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/es/types/notification.d.ts: -------------------------------------------------------------------------------- 1 | declare type notificate = (content: string, duration?: number) => void; 2 | export interface Notification { 3 | info?: notificate; 4 | success?: notificate; 5 | warn?: notificate; 6 | error?: notificate; 7 | } 8 | export declare enum NotificationLevel { 9 | silent, 10 | erorr, 11 | all 12 | } 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/core/lib/types/notification.d.ts: -------------------------------------------------------------------------------- 1 | declare type notificate = (content: string, duration?: number) => void; 2 | export interface Notification { 3 | info?: notificate; 4 | success?: notificate; 5 | warn?: notificate; 6 | error?: notificate; 7 | } 8 | export declare enum NotificationLevel { 9 | silent, 10 | erorr, 11 | all 12 | } 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/core/es/utils/logic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logic utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:13:16 5 | */ 6 | /** 7 | * is obj is nil(null or nil) 8 | * @param {any} obj 9 | * @returns {boolean} 10 | * @method isNil 11 | */ 12 | export function isNil(obj) { 13 | return obj === null || obj === undefined; 14 | } 15 | //# sourceMappingURL=logic.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/actionTypes.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"actionTypes.js","sourceRoot":"","sources":["../../src/constants/actionTypes.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAEU,QAAA,oBAAoB,GAAG,mBAAmB,CAAA;AAC1C,QAAA,kBAAkB,GAAG,iBAAiB,CAAA;AACtC,QAAA,4BAA4B,GAAG,2BAA2B,CAAA;AAC1D,QAAA,0BAA0B,GAAG,yBAAyB,CAAA;AACtD,QAAA,gBAAgB,GAAG,eAAe,CAAA;AAClC,QAAA,wBAAwB,GAAG,uBAAuB,CAAA"} -------------------------------------------------------------------------------- /packages/core/lib/types/notification.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Notification.js","sourceRoot":"","sources":["../../src/types/Notification.ts"],"names":[],"mappings":";;AAAA;;;;GAIG;AACH,0DAAiD;AAWjD,IAAY,iBAIX;AAJD,WAAY,iBAAiB;IAC3B,gDAAS,oBAAK,CAAC,MAAM,YAAA,CAAA;IACrB,+CAAQ,oBAAK,CAAC,KAAK,WAAA,CAAA;IACnB,6CAAM,oBAAK,CAAC,GAAG,SAAA,CAAA;AACjB,CAAC,EAJW,iBAAiB,GAAjB,yBAAiB,KAAjB,yBAAiB,QAI5B"} -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ![logo](./logo.png) 2 | 3 | # Reobservable 4 | 5 | Redux + rxjs + redux-obersvable best practice. Inspired by dva, rematch. 6 | 7 | --------------- 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install @reobservable/core --save 13 | ``` 14 | 15 | ## Examples 16 | 17 | See [reobservble examples](https://github.com/reobservable/reobservable/tree/master/examples) 18 | -------------------------------------------------------------------------------- /packages/core/es/utils/function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * function utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 15:32:55 5 | */ 6 | /** 7 | * noop 8 | */ 9 | export function noop() { 10 | var args = []; 11 | for (var _i = 0; _i < arguments.length; _i++) { 12 | args[_i] = arguments[_i]; 13 | } 14 | // empty 15 | } 16 | //# sourceMappingURL=function.js.map -------------------------------------------------------------------------------- /packages/core/lib/operators/end.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"end.js","sourceRoot":"","sources":["../../src/operators/end.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA;;;;GAIG;AACH,6BAAsD;AACtD,4CAAoC;AAEpC,0CAAsD;AAEtD;;;;;GAKG;AACH,SAAwB,GAAG,CAAC,OAA+B;IACzD,OAAO,WAAI,CACT,eAAG,CAAC,UAAC,MAAc;;QAAK,OAAA,cACnB,OAAO,CAAC,MAAM,CAAC,eACjB,yBAAkB,IAAG,IAAI,OAC1B;IAHsB,CAGtB,CAAC,CACJ,CAAA;AACH,CAAC;AAPD,sBAOC"} -------------------------------------------------------------------------------- /packages/core/es/types/notification.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Notification.js","sourceRoot":"","sources":["../../src/types/Notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAA;AAWjD,MAAM,CAAN,IAAY,iBAIX;AAJD,WAAY,iBAAiB;IAC3B,gDAAS,KAAK,CAAC,MAAM,YAAA,CAAA;IACrB,+CAAQ,KAAK,CAAC,KAAK,WAAA,CAAA;IACnB,6CAAM,KAAK,CAAC,GAAG,SAAA,CAAA;AACjB,CAAC,EAJW,iBAAiB,KAAjB,iBAAiB,QAI5B"} -------------------------------------------------------------------------------- /packages/core/es/operators/endTo.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"endTo.js","sourceRoot":"","sources":["../../src/operators/endTo.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAA6B,MAAM,MAAM,CAAA;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,MAAc;;IAC1C,OAAO,IAAI,CACT,KAAK,cACA,MAAM,eACR,kBAAkB,IAAG,IAAI,OAC1B,CACH,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /packages/core/es/constants/actionTypes.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"actionTypes.js","sourceRoot":"","sources":["../../src/constants/actionTypes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,IAAM,oBAAoB,GAAG,mBAAmB,CAAA;AACvD,MAAM,CAAC,IAAM,kBAAkB,GAAG,iBAAiB,CAAA;AACnD,MAAM,CAAC,IAAM,4BAA4B,GAAG,2BAA2B,CAAA;AACvE,MAAM,CAAC,IAAM,0BAA0B,GAAG,yBAAyB,CAAA;AACnE,MAAM,CAAC,IAAM,gBAAgB,GAAG,eAAe,CAAA;AAC/C,MAAM,CAAC,IAAM,wBAAwB,GAAG,uBAAuB,CAAA"} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | 5 | cache: 6 | directories: 7 | - "node_modules" 8 | 9 | script: 10 | - npm run bootstrap 11 | - npm run docs:build 12 | - npm run test 13 | - npm run coverage 14 | 15 | deploy: 16 | - provider: pages 17 | skip-cleanup: true 18 | local-dir: docs/.vuepress/dist 19 | github-token: $GITHUB_TOKEN 20 | keep-history: true 21 | on: 22 | branch: master -------------------------------------------------------------------------------- /examples/github/src/services/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * service 3 | * @author yoyoyohamapi 4 | * @ingore created 2018-08-13 23:55:23 5 | */ 6 | import { AxiosResponse, AxiosError } from 'axios' 7 | import { ServiceConfig, ServiceFunc } from '@reobservable/core' 8 | 9 | export interface ApiService 10 | extends ServiceConfig, AxiosError> {} 11 | export interface ApiServiceFunc 12 | extends ServiceFunc, AxiosError> {} 13 | -------------------------------------------------------------------------------- /packages/core/es/operators/endTo.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * endTo operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { UnaryFunction, Observable } from 'rxjs'; 7 | import { Action, EndAction } from '../types/action'; 8 | /** 9 | * endTo 10 | * @param {Action} action 11 | * @returns {Observable} 12 | * @method endTo 13 | */ 14 | export default function endTo(action: Action): UnaryFunction, Observable>; 15 | -------------------------------------------------------------------------------- /packages/core/es/utils/action.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:10:52 5 | */ 6 | import { Action } from '../types/action'; 7 | /** 8 | * get action payload 9 | * @param {Action} action 10 | */ 11 | export declare function getPayload(action: Action): any; 12 | /** 13 | * action sanitizer for redux-dev-tools 14 | * @param {Action} action 15 | */ 16 | export declare function actionSanitizer(action: any): any; 17 | -------------------------------------------------------------------------------- /packages/core/lib/operators/endTo.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * endTo operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { UnaryFunction, Observable } from 'rxjs'; 7 | import { Action, EndAction } from '../types/action'; 8 | /** 9 | * endTo 10 | * @param {Action} action 11 | * @returns {Observable} 12 | * @method endTo 13 | */ 14 | export default function endTo(action: Action): UnaryFunction, Observable>; 15 | -------------------------------------------------------------------------------- /packages/core/lib/utils/action.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:10:52 5 | */ 6 | import { Action } from '../types/action'; 7 | /** 8 | * get action payload 9 | * @param {Action} action 10 | */ 11 | export declare function getPayload(action: Action): any; 12 | /** 13 | * action sanitizer for redux-dev-tools 14 | * @param {Action} action 15 | */ 16 | export declare function actionSanitizer(action: any): any; 17 | -------------------------------------------------------------------------------- /packages/core/src/types/action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:19:14 5 | */ 6 | import { FLOW_END_INDICATOR } from '../constants/meta' 7 | import { ALIAS } from '../constants/symbols' 8 | 9 | export interface Action { 10 | type: string 11 | payload?: any 12 | [ALIAS]?: string 13 | } 14 | 15 | export interface EndAction { 16 | type: string 17 | [FLOW_END_INDICATOR]: boolean 18 | payload?: any 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "moduleResolution": "node", 5 | "declaration": true, 6 | "target": "es5", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "types": [ 11 | "node", 12 | "mocha" 13 | ] 14 | }, 15 | "include": [ 16 | "packages/**/src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | // "./test/**/*", 21 | "**/*.spec.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/es/operators/end.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"end.js","sourceRoot":"","sources":["../../src/operators/end.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAA6B,MAAM,MAAM,CAAA;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,OAA+B;IACzD,OAAO,IAAI,CACT,GAAG,CAAC,UAAC,MAAc;;QAAK,OAAA,cACnB,OAAO,CAAC,MAAM,CAAC,eACjB,kBAAkB,IAAG,IAAI,OAC1B;IAHsB,CAGtB,CAAC,CACJ,CAAA;AACH,CAAC"} -------------------------------------------------------------------------------- /packages/core/es/types/Action.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:19:14 5 | */ 6 | import { FLOW_END_INDICATOR } from '../constants/meta'; 7 | import { ALIAS } from '../constants/symbols'; 8 | export interface Action { 9 | type: string; 10 | payload?: any; 11 | [ALIAS]?: string; 12 | } 13 | export interface EndAction { 14 | type: string; 15 | [FLOW_END_INDICATOR]: boolean; 16 | payload?: any; 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/lib/types/Action.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:19:14 5 | */ 6 | import { FLOW_END_INDICATOR } from '../constants/meta'; 7 | import { ALIAS } from '../constants/symbols'; 8 | export interface Action { 9 | type: string; 10 | payload?: any; 11 | [ALIAS]?: string; 12 | } 13 | export interface EndAction { 14 | type: string; 15 | [FLOW_END_INDICATOR]: boolean; 16 | payload?: any; 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/lib/utils/logic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * logic utils 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-03 13:13:16 6 | */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | /** 9 | * is obj is nil(null or nil) 10 | * @param {any} obj 11 | * @returns {boolean} 12 | * @method isNil 13 | */ 14 | function isNil(obj) { 15 | return obj === null || obj === undefined; 16 | } 17 | exports.isNil = isNil; 18 | //# sourceMappingURL=logic.js.map -------------------------------------------------------------------------------- /packages/core/lib/utils/function.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * function utils 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-13 15:32:55 6 | */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | /** 9 | * noop 10 | */ 11 | function noop() { 12 | var args = []; 13 | for (var _i = 0; _i < arguments.length; _i++) { 14 | args[_i] = arguments[_i]; 15 | } 16 | // empty 17 | } 18 | exports.noop = noop; 19 | //# sourceMappingURL=function.js.map -------------------------------------------------------------------------------- /packages/core/es/operators/end.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * end operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { UnaryFunction, Observable } from 'rxjs'; 7 | import { Action, EndAction } from '../types/action'; 8 | /** 9 | * end operator 10 | * @param {function(value: Action): EndAction} project 11 | * @returns {Observable} 12 | * @method end 13 | */ 14 | export default function end(project: (value: any) => Action): UnaryFunction, Observable>; 15 | -------------------------------------------------------------------------------- /packages/core/lib/operators/end.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * end operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { UnaryFunction, Observable } from 'rxjs'; 7 | import { Action, EndAction } from '../types/action'; 8 | /** 9 | * end operator 10 | * @param {function(value: Action): EndAction} project 11 | * @returns {Observable} 12 | * @method end 13 | */ 14 | export default function end(project: (value: any) => Action): UnaryFunction, Observable>; 15 | -------------------------------------------------------------------------------- /packages/core/lib/utils/action.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/utils/action.ts"],"names":[],"mappings":";;AAMA,iCAA+B;AAC/B,gDAA4C;AAE5C;;;GAGG;AACH,SAAgB,UAAU,CAAC,MAAc;IACvC,OAAO,aAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAA;AACpD,CAAC;AAFD,gCAEC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAAC,MAAM;IACpC,OAAO,MAAM,CAAC,eAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;QAC/C,IAAI,EAAE,MAAM,CAAC,eAAK,CAAC;KACpB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;AACb,CAAC;AAJD,0CAIC"} -------------------------------------------------------------------------------- /packages/core/src/constants/actionTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:27:09 5 | */ 6 | 7 | export const LOADING_START_ACTION = '@@LOADING_START@@' 8 | export const LOADING_END_ACTION = '@@LOADING_END@@' 9 | export const SERVICE_LOADING_START_ACTION = '@@SERVICE_LOADING_START@@' 10 | export const SERVICE_LOADING_END_ACTION = '@@SERVICE_LOADING_END@@' 11 | export const ERROR_SET_ACTION = '@@ERROR_SET@@' 12 | export const SERVICE_ERROR_SET_ACTION = '@@SERVICE_ERROR_SET@@' 13 | -------------------------------------------------------------------------------- /packages/core/src/types/Notification.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * notification type 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:16:13 5 | */ 6 | import { LEVEL } from '../constants/notification' 7 | 8 | type notificate = (content: string, duration?: number) => void 9 | 10 | export interface Notification { 11 | info?: notificate 12 | success?: notificate 13 | warn?: notificate 14 | error?: notificate 15 | } 16 | 17 | export enum NotificationLevel { 18 | silent = LEVEL.silent, 19 | erorr = LEVEL.error, 20 | all = LEVEL.all 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/es/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:27:09 5 | */ 6 | export var LOADING_START_ACTION = '@@LOADING_START@@'; 7 | export var LOADING_END_ACTION = '@@LOADING_END@@'; 8 | export var SERVICE_LOADING_START_ACTION = '@@SERVICE_LOADING_START@@'; 9 | export var SERVICE_LOADING_END_ACTION = '@@SERVICE_LOADING_END@@'; 10 | export var ERROR_SET_ACTION = '@@ERROR_SET@@'; 11 | export var SERVICE_ERROR_SET_ACTION = '@@SERVICE_ERROR_SET@@'; 12 | //# sourceMappingURL=actionTypes.js.map -------------------------------------------------------------------------------- /packages/core/es/utils/action.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action.js","sourceRoot":"","sources":["../../src/utils/action.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAE5C;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAA;AACpD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAM;IACpC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE;QAC/C,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC;KACpB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;AACb,CAAC"} -------------------------------------------------------------------------------- /packages/core/es/constants/actionTypes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:27:09 5 | */ 6 | export declare const LOADING_START_ACTION = "@@LOADING_START@@"; 7 | export declare const LOADING_END_ACTION = "@@LOADING_END@@"; 8 | export declare const SERVICE_LOADING_START_ACTION = "@@SERVICE_LOADING_START@@"; 9 | export declare const SERVICE_LOADING_END_ACTION = "@@SERVICE_LOADING_END@@"; 10 | export declare const ERROR_SET_ACTION = "@@ERROR_SET@@"; 11 | export declare const SERVICE_ERROR_SET_ACTION = "@@SERVICE_ERROR_SET@@"; 12 | -------------------------------------------------------------------------------- /packages/core/lib/constants/actionTypes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action types 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 11:27:09 5 | */ 6 | export declare const LOADING_START_ACTION = "@@LOADING_START@@"; 7 | export declare const LOADING_END_ACTION = "@@LOADING_END@@"; 8 | export declare const SERVICE_LOADING_START_ACTION = "@@SERVICE_LOADING_START@@"; 9 | export declare const SERVICE_LOADING_END_ACTION = "@@SERVICE_LOADING_END@@"; 10 | export declare const ERROR_SET_ACTION = "@@ERROR_SET@@"; 11 | export declare const SERVICE_ERROR_SET_ACTION = "@@SERVICE_ERROR_SET@@"; 12 | -------------------------------------------------------------------------------- /packages/core/es/utils/action.js: -------------------------------------------------------------------------------- 1 | import { isNil } from './logic'; 2 | import { ALIAS } from '../constants/symbols'; 3 | /** 4 | * get action payload 5 | * @param {Action} action 6 | */ 7 | export function getPayload(action) { 8 | return isNil(action.payload) ? {} : action.payload; 9 | } 10 | /** 11 | * action sanitizer for redux-dev-tools 12 | * @param {Action} action 13 | */ 14 | export function actionSanitizer(action) { 15 | return action[ALIAS] ? Object.assign({}, action, { 16 | type: action[ALIAS] 17 | }) : action; 18 | } 19 | //# sourceMappingURL=action.js.map -------------------------------------------------------------------------------- /packages/core/es/types/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * notification type 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:16:13 5 | */ 6 | import { LEVEL } from '../constants/notification'; 7 | export var NotificationLevel; 8 | (function (NotificationLevel) { 9 | NotificationLevel[NotificationLevel["silent"] = LEVEL.silent] = "silent"; 10 | NotificationLevel[NotificationLevel["erorr"] = LEVEL.error] = "erorr"; 11 | NotificationLevel[NotificationLevel["all"] = LEVEL.all] = "all"; 12 | })(NotificationLevel || (NotificationLevel = {})); 13 | //# sourceMappingURL=Notification.js.map -------------------------------------------------------------------------------- /packages/core/lib/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * action types 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-03 11:27:09 6 | */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.LOADING_START_ACTION = '@@LOADING_START@@'; 9 | exports.LOADING_END_ACTION = '@@LOADING_END@@'; 10 | exports.SERVICE_LOADING_START_ACTION = '@@SERVICE_LOADING_START@@'; 11 | exports.SERVICE_LOADING_END_ACTION = '@@SERVICE_LOADING_END@@'; 12 | exports.ERROR_SET_ACTION = '@@ERROR_SET@@'; 13 | exports.SERVICE_ERROR_SET_ACTION = '@@SERVICE_ERROR_SET@@'; 14 | //# sourceMappingURL=actionTypes.js.map -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## Feature 2 | 3 | - [x] create model's reducers 4 | - [x] create model's selectors 5 | - [x] create model's flows 6 | - [x] support flow loading 7 | - [x] support error loading 8 | - [x] support custom service & server creator 9 | - [x] support service loading 10 | - [x] support service error 11 | - [x] support custom redux config 12 | - [x] support action type alias 13 | - [ ] support global `state$` observing 14 | - [ ] support `model.change()` ... 15 | - [ ] `@reobservable/test` 16 | - [ ] `@reobservable/hooks` 17 | 18 | 19 | ## ecosystem 20 | 21 | - [ ] command line tools 22 | - [ ] vscode plugin 23 | - [ ] snippet 24 | - [x] github demo 25 | 26 | -------------------------------------------------------------------------------- /packages/core/src/operators/endTo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * endTo operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { pipe, UnaryFunction, Observable } from 'rxjs' 7 | import { mapTo } from 'rxjs/operators' 8 | import { Action, EndAction } from '../types/action' 9 | import { FLOW_END_INDICATOR } from '../constants/meta' 10 | 11 | /** 12 | * endTo 13 | * @param {Action} action 14 | * @returns {Observable} 15 | * @method endTo 16 | */ 17 | export default function endTo( 18 | action: Action 19 | ): UnaryFunction, Observable> { 20 | return pipe( 21 | mapTo({ 22 | ...action, 23 | [FLOW_END_INDICATOR]: true 24 | }) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/utils/action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action utils 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 13:10:52 5 | */ 6 | import { Action } from '../types/action' 7 | import { isNil } from './logic' 8 | import { ALIAS } from '../constants/symbols' 9 | 10 | /** 11 | * get action payload 12 | * @param {Action} action 13 | */ 14 | export function getPayload(action: Action) { 15 | return isNil(action.payload) ? {} : action.payload 16 | } 17 | 18 | /** 19 | * action sanitizer for redux-dev-tools 20 | * @param {Action} action 21 | */ 22 | export function actionSanitizer(action) { 23 | return action[ALIAS] 24 | ? Object.assign({}, action, { 25 | type: action[ALIAS] 26 | }) 27 | : action 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/lib/types/notification.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * notification type 5 | * @author yoyoyohamapi 6 | * @ignore created 2018-08-13 14:16:13 7 | */ 8 | var notification_1 = require("../constants/notification"); 9 | var NotificationLevel; 10 | (function (NotificationLevel) { 11 | NotificationLevel[NotificationLevel["silent"] = notification_1.LEVEL.silent] = "silent"; 12 | NotificationLevel[NotificationLevel["erorr"] = notification_1.LEVEL.error] = "erorr"; 13 | NotificationLevel[NotificationLevel["all"] = notification_1.LEVEL.all] = "all"; 14 | })(NotificationLevel = exports.NotificationLevel || (exports.NotificationLevel = {})); 15 | //# sourceMappingURL=Notification.js.map -------------------------------------------------------------------------------- /packages/core/lib/utils/action.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var logic_1 = require("./logic"); 4 | var symbols_1 = require("../constants/symbols"); 5 | /** 6 | * get action payload 7 | * @param {Action} action 8 | */ 9 | function getPayload(action) { 10 | return logic_1.isNil(action.payload) ? {} : action.payload; 11 | } 12 | exports.getPayload = getPayload; 13 | /** 14 | * action sanitizer for redux-dev-tools 15 | * @param {Action} action 16 | */ 17 | function actionSanitizer(action) { 18 | return action[symbols_1.ALIAS] ? Object.assign({}, action, { 19 | type: action[symbols_1.ALIAS] 20 | }) : action; 21 | } 22 | exports.actionSanitizer = actionSanitizer; 23 | //# sourceMappingURL=action.js.map -------------------------------------------------------------------------------- /examples/github/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": false, 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "baseUrl": "./src", 8 | "paths": { 9 | "@config/*": ["./config/*"], 10 | "@components/*": ["./components/*"], 11 | "@utils/*": ["./utils/*"], 12 | "@modules/*": ["./modules/*"], 13 | "@models/*": ["./models/*"], 14 | "@services/*": ["./services/*"] 15 | }, 16 | "lib": [ 17 | "es2015", 18 | "es6", 19 | "dom", 20 | "dom.iterable" 21 | ], 22 | "jsx": "react", 23 | }, 24 | "include": [ 25 | "./src/**/*" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "**/*.spec.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/operators/end.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * end operator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:31:20 5 | */ 6 | import { pipe, UnaryFunction, Observable } from 'rxjs' 7 | import { map } from 'rxjs/operators' 8 | import { Action, EndAction } from '../types/action' 9 | import { FLOW_END_INDICATOR } from '../constants/meta' 10 | 11 | /** 12 | * end operator 13 | * @param {function(value: Action): EndAction} project 14 | * @returns {Observable} 15 | * @method end 16 | */ 17 | export default function end( 18 | project: (value: any) => Action 19 | ): UnaryFunction, Observable> { 20 | return pipe( 21 | map((action: Action) => ({ 22 | ...project(action), 23 | [FLOW_END_INDICATOR]: true 24 | })) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,py}] 14 | charset = utf-8 15 | 16 | # 2 space indentation 17 | [*.py] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Tab indentation (no size specified) 22 | [Makefile] 23 | indent_style = tab 24 | 25 | # Indentation override for all JS under lib directory 26 | [lib/**.js] 27 | indent_style = space 28 | indent_size = 2 29 | 30 | # Matches the exact files either package.json or .travis.yml 31 | [{package.json,.travis.yml}] 32 | indent_style = space 33 | indent_size = 2 -------------------------------------------------------------------------------- /examples/github/src/modules/repo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * repo 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-08 14:50:22 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import ActionPanel from './ActionPanel' 9 | import Table from './Table' 10 | 11 | class Repo extends React.Component { 12 | componentDidMount() { 13 | this.props.dispatch({ 14 | type: 'repo/fetch' 15 | }) 16 | } 17 | 18 | componentWillUnmount() { 19 | this.props.dispatch({ 20 | type: 'repo/stopPolling' 21 | }) 22 | } 23 | 24 | render() { 25 | return ( 26 | <> 27 | 28 | 29 | 30 | ) 31 | } 32 | } 33 | 34 | const mapStateToProps = state => ({}) 35 | 36 | export default connect(mapStateToProps)(Repo) 37 | -------------------------------------------------------------------------------- /examples/github/src/modules/user/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * user module 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 21:15:39 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import ActionPanel from './ActionPanel' 9 | import Table from './Table' 10 | 11 | class User extends React.Component { 12 | componentDidMount() { 13 | this.props.dispatch({ 14 | type: 'user/fetch' 15 | }) 16 | } 17 | 18 | componentWillUnmount() { 19 | this.props.dispatch({ 20 | type: 'user/stopPolling' 21 | }) 22 | } 23 | 24 | render() { 25 | return ( 26 | <> 27 | 28 |
29 | 30 | ) 31 | } 32 | } 33 | 34 | const mapStateToProps = state => ({}) 35 | 36 | export default connect(mapStateToProps)(User) 37 | -------------------------------------------------------------------------------- /packages/core/lib/reducers/error.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/reducers/error.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAMA,wDAAyI;AAEzI,SAAwB,KAAK,CAAC,KAAiC,EAAE,MAAc;IAAjD,sBAAA,EAAA,UAAS,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAC;;IACrD,IAAA,kBAAI,CAAW;IACvB,IAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;IACpD,QAAQ,IAAI,EAAE;QACZ,eAAe;QACf,KAAK,kCAAoB,CAAC,CAAC;YACzB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,IAAI,UAEvB;SACF;QACD,KAAK,0CAA4B,CAAC,CAAC;YACjC,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,IAAI,UAE1B;SACF;QACD,KAAK,8BAAgB,CAAC,CAAC;YACrB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,OAAO,CAAC,KAAK,UAEhC;SACF;QACD,KAAK,sCAAwB,CAAC,CAAC;YAC7B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,OAAO,CAAC,KAAK,UAEnC;SACF;QACD;YACE,OAAO,KAAK,CAAA;KACf;AACH,CAAC;AA5CD,wBA4CC"} -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | I'm a big fan of [dvajs](https://github.com/dvajs/dva), it helps me get rid out of the boilerplate of redux. But I think redux-saga in dva sometimes can be a little bit verbose since I prefer functional reactive programming(frp) than imperative programming. 4 | 5 | I know that reinvent the weel could be a terrible practice, but in dva community, there is no plan to support frp. At last, I decided to create **reobservable**, which, in a world, is a state manager mixined dva architecture and frp features(RxJS + redux-observable. In reobservable, dva architecture wiped out the boilerplate code of redux, RxJS played an elite role in managing asynchronous task. 6 | 7 | If you: 8 | 9 | - love redux concept, love the predicatable, centralized, debuggable features 10 | - stuking with the boilerplate code of redux 11 | - want to use RxJS to managing asynchronous task 12 | 13 | try reobservable! -------------------------------------------------------------------------------- /packages/core/es/reducers/error.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/reducers/error.ts"],"names":[],"mappings":";;;;;;;;;;;AAMA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAEzI,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,KAAiC,EAAE,MAAc;IAAjD,sBAAA,EAAA,UAAS,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAC;;IACrD,IAAA,kBAAI,CAAW;IACvB,IAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;IACpD,QAAQ,IAAI,EAAE;QACZ,eAAe;QACf,KAAK,oBAAoB,CAAC,CAAC;YACzB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,IAAI,UAEvB;SACF;QACD,KAAK,4BAA4B,CAAC,CAAC;YACjC,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,IAAI,UAE1B;SACF;QACD,KAAK,gBAAgB,CAAC,CAAC;YACrB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,OAAO,CAAC,KAAK,UAEhC;SACF;QACD,KAAK,wBAAwB,CAAC,CAAC;YAC7B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,OAAO,CAAC,KAAK,UAEnC;SACF;QACD;YACE,OAAO,KAAK,CAAA;KACf;AACH,CAAC"} -------------------------------------------------------------------------------- /packages/core/es/operators/endTo.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | /** 13 | * endTo operator 14 | * @author yoyoyohamapi 15 | * @ignore created 2018-08-03 10:31:20 16 | */ 17 | import { pipe } from 'rxjs'; 18 | import { mapTo } from 'rxjs/operators'; 19 | import { FLOW_END_INDICATOR } from '../constants/meta'; 20 | /** 21 | * endTo 22 | * @param {Action} action 23 | * @returns {Observable} 24 | * @method endTo 25 | */ 26 | export default function endTo(action) { 27 | var _a; 28 | return pipe(mapTo(__assign({}, action, (_a = {}, _a[FLOW_END_INDICATOR] = true, _a)))); 29 | } 30 | //# sourceMappingURL=endTo.js.map -------------------------------------------------------------------------------- /packages/core/test/action.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * action test 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-19 17:10:46 5 | */ 6 | import { expect } from 'chai' 7 | import { Symbols } from '../src' 8 | import { actionSanitizer, getPayload } from '../src/utils/action' 9 | import { Action } from '../src/types/action' 10 | 11 | describe('action', () => { 12 | it('should payload be {} when payload is nil', () => { 13 | expect(getPayload({ type: 'undefined payload' })).to.deep.equal({}) 14 | expect(getPayload({ type: 'null payload`', payload: null })).to.deep.equal( 15 | {} 16 | ) 17 | }) 18 | 19 | it('should support action alias', () => { 20 | const action: Action = { 21 | type: 'foo', 22 | [Symbols.ALIAS]: 'FOO', 23 | payload: {} 24 | } 25 | 26 | expect(actionSanitizer(action)).to.deep.include({ 27 | type: 'FOO' 28 | }) 29 | 30 | expect(actionSanitizer({ type: 'foo', payload: {} })).to.deep.include({ 31 | type: 'foo' 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/core/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, Reducer, Store } from 'redux' 2 | import Model, { Selectors } from './src/types/Model' 3 | import { Notification } from './src/types/Notification' 4 | import { 5 | init, 6 | getSelectors, 7 | getService, 8 | NOTIFICATION_LEVEL, 9 | notificate 10 | } from './src' 11 | import * as Symbols from './src/constants/symbols' 12 | import { ServiceConfig, ServiceFunc } from './src/operators/createService' 13 | 14 | export as namespace ReObservable; 15 | 16 | export { 17 | Model, 18 | Notification, 19 | init, 20 | getSelectors, 21 | getService, 22 | notificate, 23 | ServiceConfig, 24 | ServiceFunc, 25 | NOTIFICATION_LEVEL, 26 | Symbols 27 | } 28 | 29 | export interface LoadingState { 30 | flows: { 31 | [stateName: string]: boolean; 32 | } 33 | services: { 34 | [stateName: string]: boolean; 35 | } 36 | } 37 | 38 | export interface ErrorState { 39 | flows: { 40 | [stateName: string]: any; 41 | } 42 | services: { 43 | [stateName: string]: any; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/es/operators/end.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | /** 13 | * end operator 14 | * @author yoyoyohamapi 15 | * @ignore created 2018-08-03 10:31:20 16 | */ 17 | import { pipe } from 'rxjs'; 18 | import { map } from 'rxjs/operators'; 19 | import { FLOW_END_INDICATOR } from '../constants/meta'; 20 | /** 21 | * end operator 22 | * @param {function(value: Action): EndAction} project 23 | * @returns {Observable} 24 | * @method end 25 | */ 26 | export default function end(project) { 27 | return pipe(map(function (action) { 28 | var _a; 29 | return (__assign({}, project(action), (_a = {}, _a[FLOW_END_INDICATOR] = true, _a))); 30 | })); 31 | } 32 | //# sourceMappingURL=end.js.map -------------------------------------------------------------------------------- /packages/core/lib/operators/endTo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | /** 15 | * endTo operator 16 | * @author yoyoyohamapi 17 | * @ignore created 2018-08-03 10:31:20 18 | */ 19 | var rxjs_1 = require("rxjs"); 20 | var operators_1 = require("rxjs/operators"); 21 | var meta_1 = require("../constants/meta"); 22 | /** 23 | * endTo 24 | * @param {Action} action 25 | * @returns {Observable} 26 | * @method endTo 27 | */ 28 | function endTo(action) { 29 | var _a; 30 | return rxjs_1.pipe(operators_1.mapTo(__assign({}, action, (_a = {}, _a[meta_1.FLOW_END_INDICATOR] = true, _a)))); 31 | } 32 | exports.default = endTo; 33 | //# sourceMappingURL=endTo.js.map -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reobservable/core", 3 | "version": "0.2.3", 4 | "description": "reobservable core package", 5 | "main": "./lib/index.js", 6 | "module": "./es/index.js", 7 | "scripts": { 8 | "lint": "tslint src/**/*.ts", 9 | "build:cjs": "cross-env tsc --rootDir ./src --outDir lib --module commonjs", 10 | "build:es": "cross-env tsc --rootDir ./src --outDir es --module es6", 11 | "clean": "rimraf lib es dist", 12 | "prepare": "npm run clean && npm run build:cjs && npm run build:es" 13 | }, 14 | "typings": "index.d.ts", 15 | "author": " ", 16 | "license": "MIT", 17 | "dependencies": { 18 | "invariant": "^2.2.4", 19 | "lodash.clonedeep": "^4.5.0", 20 | "lodash.merge": "^4.6.1", 21 | "lodash.mergewith": "^4.6.1", 22 | "redux": "^4.0.0", 23 | "redux-devtools-extension": "^2.13.5", 24 | "redux-observable": "^1.0.0", 25 | "rxjs": "^6.2.2" 26 | }, 27 | "sideEffects": false, 28 | "devDependencies": { 29 | "axios": "^0.18.0" 30 | }, 31 | "publishConfig": { 32 | "access": "public" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/lib/reducers/loading.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"loading.js","sourceRoot":"","sources":["../../src/reducers/loading.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;;;;;;;;;;;AAGH,wDAOiC;AAEjC,SAAwB,OAAO,CAAC,KAAiC,EAAE,MAAc;IAAjD,sBAAA,EAAA,UAAS,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAC;;IACvD,IAAA,kBAAI,CAAW;IACvB,IAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;IACpD,QAAQ,IAAI,EAAE;QACZ,KAAK,kCAAoB,CAAC,CAAC;YACzB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,IAAI,UAEvB;SACF;QAED,KAAK,gCAAkB,CAAC,CAAC;YACvB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,KAAK,UAExB;SACF;QAED,KAAK,0CAA4B,CAAC,CAAC;YACjC,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,IAAI,UAE1B;SACF;QAED,KAAK,wCAA0B,CAAC,CAAC;YAC/B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,KAAK,UAE3B;SACF;QAED,sBAAsB;QACtB,KAAK,8BAAgB,CAAC,CAAC;YACrB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,KAAK,UAExB;SACF;QAED,KAAK,sCAAwB,CAAC,CAAC;YAC7B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,KAAK,UAE3B;SACF;QAED;YACE,OAAO,KAAK,CAAA;KACf;AACH,CAAC;AApED,0BAoEC"} -------------------------------------------------------------------------------- /packages/core/lib/operators/end.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | /** 15 | * end operator 16 | * @author yoyoyohamapi 17 | * @ignore created 2018-08-03 10:31:20 18 | */ 19 | var rxjs_1 = require("rxjs"); 20 | var operators_1 = require("rxjs/operators"); 21 | var meta_1 = require("../constants/meta"); 22 | /** 23 | * end operator 24 | * @param {function(value: Action): EndAction} project 25 | * @returns {Observable} 26 | * @method end 27 | */ 28 | function end(project) { 29 | return rxjs_1.pipe(operators_1.map(function (action) { 30 | var _a; 31 | return (__assign({}, project(action), (_a = {}, _a[meta_1.FLOW_END_INDICATOR] = true, _a))); 32 | })); 33 | } 34 | exports.default = end; 35 | //# sourceMappingURL=end.js.map -------------------------------------------------------------------------------- /packages/core/es/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Store, Middleware, Reducer } from 'redux'; 2 | import Model, { Selectors } from './types/Model'; 3 | import { ServiceConfig, ServiceFunc } from './operators/createService'; 4 | import { Notification } from './types/Notification'; 5 | import { LEVEL as NOTIFICATION_LEVEL } from './constants/notification'; 6 | import * as Symbols from './constants/symbols'; 7 | interface Models { 8 | [modelName: string]: Model; 9 | } 10 | interface ReduxConfig { 11 | middleware?: Middleware | [Middleware]; 12 | rootReducer?: (reducer: Reducer, reducers: { 13 | [key: string]: Reducer; 14 | }) => Reducer; 15 | } 16 | interface Config { 17 | models?: Models; 18 | redux?: ReduxConfig; 19 | notification?: Notification; 20 | services?: { 21 | [serviceName: string]: ServiceConfig; 22 | }; 23 | } 24 | export declare type InitFunc = (config: Config) => Store; 25 | export declare const init: InitFunc; 26 | export declare function getSelectors(model: string): Selectors; 27 | export declare function getService(service: string): ServiceFunc; 28 | export declare function notificate(type: string, message: any): any; 29 | export { NOTIFICATION_LEVEL, Symbols }; 30 | -------------------------------------------------------------------------------- /packages/core/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Store, Middleware, Reducer } from 'redux'; 2 | import Model, { Selectors } from './types/Model'; 3 | import { ServiceConfig, ServiceFunc } from './operators/createService'; 4 | import { Notification } from './types/Notification'; 5 | import { LEVEL as NOTIFICATION_LEVEL } from './constants/notification'; 6 | import * as Symbols from './constants/symbols'; 7 | interface Models { 8 | [modelName: string]: Model; 9 | } 10 | interface ReduxConfig { 11 | middleware?: Middleware | [Middleware]; 12 | rootReducer?: (reducer: Reducer, reducers: { 13 | [key: string]: Reducer; 14 | }) => Reducer; 15 | } 16 | interface Config { 17 | models?: Models; 18 | redux?: ReduxConfig; 19 | notification?: Notification; 20 | services?: { 21 | [serviceName: string]: ServiceConfig; 22 | }; 23 | } 24 | export declare type InitFunc = (config: Config) => Store; 25 | export declare const init: InitFunc; 26 | export declare function getSelectors(model: string): Selectors; 27 | export declare function getService(service: string): ServiceFunc; 28 | export declare function notificate(type: string, message: any): any; 29 | export { NOTIFICATION_LEVEL, Symbols }; 30 | -------------------------------------------------------------------------------- /packages/core/es/reducers/loading.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"loading.js","sourceRoot":"","sources":["../../src/reducers/loading.ts"],"names":[],"mappings":"AAAA;;;;GAIG;;;;;;;;;;;;AAGH,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,4BAA4B,EAC5B,0BAA0B,EAC1B,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,0BAA0B,CAAA;AAEjC,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,KAAiC,EAAE,MAAc;IAAjD,sBAAA,EAAA,UAAS,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAC;;IACvD,IAAA,kBAAI,CAAW;IACvB,IAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;IACpD,QAAQ,IAAI,EAAE;QACZ,KAAK,oBAAoB,CAAC,CAAC;YACzB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,IAAI,UAEvB;SACF;QAED,KAAK,kBAAkB,CAAC,CAAC;YACvB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,KAAK,UAExB;SACF;QAED,KAAK,4BAA4B,CAAC,CAAC;YACjC,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,IAAI,UAE1B;SACF;QAED,KAAK,0BAA0B,CAAC,CAAC;YAC/B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,KAAK,UAE3B;SACF;QAED,sBAAsB;QACtB,KAAK,gBAAgB,CAAC,CAAC;YACrB,oBACK,KAAK,IACR,KAAK,eACA,KAAK,CAAC,KAAK,eACb,OAAO,CAAC,IAAI,IAAG,KAAK,UAExB;SACF;QAED,KAAK,wBAAwB,CAAC,CAAC;YAC7B,oBACK,KAAK,IACR,QAAQ,eACH,KAAK,CAAC,QAAQ,eAChB,OAAO,CAAC,OAAO,IAAG,KAAK,UAE3B;SACF;QAED;YACE,OAAO,KAAK,CAAA;KACf;AACH,CAAC"} -------------------------------------------------------------------------------- /examples/github/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Github Demo 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-08 15:50:46 5 | */ 6 | import * as React from 'react' 7 | import { render } from 'react-dom' 8 | import { Provider } from 'react-redux' 9 | import { routerMiddleware, connectRouter } from 'connected-react-router' 10 | import { message } from 'antd' 11 | 12 | import { init } from '@reobservable/core' 13 | import App from '@modules/app' 14 | import userModel from '@models/user' 15 | import repoModel from '@models/repo' 16 | import { ApiService } from '@services/types' 17 | 18 | import createHistory from 'history/createHashHistory' 19 | 20 | import 'antd/dist/antd.css' 21 | 22 | const history = createHistory() 23 | 24 | const store = init({ 25 | redux: { 26 | middleware: [routerMiddleware(history)], 27 | rootReducer(rootReducer) { 28 | return connectRouter(history)(rootReducer) 29 | } 30 | }, 31 | models: { 32 | user: userModel, 33 | repo: repoModel 34 | }, 35 | notification: message, 36 | services: { 37 | api: { 38 | templates: { 39 | success: resp => 'success', 40 | error: error => `error` 41 | } 42 | } as ApiService 43 | } 44 | }) 45 | 46 | render( 47 | 48 | 49 | , 50 | document.getElementById('app') 51 | ) 52 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Reobservable', 3 | description: 'Redux + rxjs + redux-obersvable best practice. Inspired by dva, rematch.', 4 | base: '/reobservable/', 5 | themeConfig: { 6 | repo: 'reobservable/reobservable', 7 | docsDir: 'docs', 8 | locales: { 9 | '/': { 10 | nav: [ 11 | { text: 'Introduction', link: '/introduction/' }, 12 | { text: 'Basics', link: '/basics/'}, 13 | { test: 'Advanced', link: '/advanced/'}, 14 | { text: 'API Reference', link: '/api/'} 15 | ], 16 | sidebar: { 17 | '/introduction/': [{ 18 | title: 'Introduction', 19 | collapsable: false, 20 | children: [ 21 | '', 22 | 'concepts', 23 | 'get-started', 24 | ] 25 | }], 26 | '/basics/': [{ 27 | title: 'Basics', 28 | collapsable: false, 29 | children: [ 30 | 'custom-service', 31 | 'change-and-patch', 32 | 'loading-state', 33 | 'error-state' 34 | ] 35 | }], 36 | '/advanced/': [{ 37 | title: 'Advanced', 38 | collapsable: false, 39 | children: [ 40 | ] 41 | }] 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /examples/github/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: path.resolve(__dirname, "./src/index.tsx"), 6 | output: { 7 | path: path.resolve(__dirname, "./dist"), 8 | filename: "bundle.js", 9 | publicPath: path.resolve(__dirname, "/") 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | exclude: /^node_modules/, 16 | use: { 17 | loader: "ts-loader", 18 | options: { 19 | configFile: path.resolve(__dirname, "./tsconfig.json") 20 | } 21 | } 22 | }, 23 | { 24 | test: /\.css?$/, 25 | loaders: ["style-loader", "css-loader"] 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | extensions: [".ts", ".tsx", ".js", ".json"], 31 | alias: { 32 | "@config": path.resolve(__dirname, "./src/config"), 33 | "@components": path.resolve(__dirname, "./src/components"), 34 | "@modules": path.resolve(__dirname, "./src/modules"), 35 | "@models": path.resolve(__dirname, "./src/models"), 36 | "@utils": path.resolve(__dirname, "./src/utils"), 37 | "@services": path.resolve(__dirname, "./src/services") 38 | } 39 | }, 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | template: path.resolve(__dirname, "./src/index.html") 43 | }) 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/es/types/model.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * model interface 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:47:25 5 | */ 6 | import { ActionsObservable, StateObservable } from 'redux-observable'; 7 | import { Action } from './action'; 8 | import endTo from '../operators/endTo'; 9 | import end from '../operators/end'; 10 | import { Observable } from 'rxjs'; 11 | import { ServiceFunc } from '../operators/createService'; 12 | export interface Reducers { 13 | readonly [reducerName: string]: (state: S, payload: any) => S; 14 | } 15 | export interface Dependencies { 16 | end: typeof end; 17 | endTo: typeof endTo; 18 | services: { 19 | [serviceName: string]: ServiceFunc; 20 | }; 21 | } 22 | export declare type Flow = (flow$: Observable, action$: ActionsObservable, state$: StateObservable, dependencies: Dependencies) => Observable; 23 | export interface Flows { 24 | readonly [flowName: string]: Flow; 25 | } 26 | export declare type Selector = (state: T, props?: P) => any; 27 | export interface Selectors { 28 | readonly [selectorName: string]: Selector; 29 | } 30 | export default interface Model { 31 | readonly name: string; 32 | readonly state?: S; 33 | readonly reducers?: Reducers; 34 | readonly flows?: Flows; 35 | readonly selectors?: Selectors; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/lib/types/model.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * model interface 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:47:25 5 | */ 6 | import { ActionsObservable, StateObservable } from 'redux-observable'; 7 | import { Action } from './action'; 8 | import endTo from '../operators/endTo'; 9 | import end from '../operators/end'; 10 | import { Observable } from 'rxjs'; 11 | import { ServiceFunc } from '../operators/createService'; 12 | export interface Reducers { 13 | readonly [reducerName: string]: (state: S, payload: any) => S; 14 | } 15 | export interface Dependencies { 16 | end: typeof end; 17 | endTo: typeof endTo; 18 | services: { 19 | [serviceName: string]: ServiceFunc; 20 | }; 21 | } 22 | export declare type Flow = (flow$: Observable, action$: ActionsObservable, state$: StateObservable, dependencies: Dependencies) => Observable; 23 | export interface Flows { 24 | readonly [flowName: string]: Flow; 25 | } 26 | export declare type Selector = (state: T, props?: P) => any; 27 | export interface Selectors { 28 | readonly [selectorName: string]: Selector; 29 | } 30 | export default interface Model { 31 | readonly name: string; 32 | readonly state?: S; 33 | readonly reducers?: Reducers; 34 | readonly flows?: Flows; 35 | readonly selectors?: Selectors; 36 | } 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "curly": true, 4 | "eofline": false, 5 | "align": [ 6 | true, 7 | "parameters" 8 | ], 9 | "class-name": true, 10 | "indent": [ 11 | true, 12 | "spaces" 13 | ], 14 | "max-line-length": [ 15 | true, 16 | 150 17 | ], 18 | "no-consecutive-blank-lines": true, 19 | "no-trailing-whitespace": true, 20 | "no-duplicate-variable": true, 21 | "no-duplicate-imports": true, 22 | "no-var-keyword": true, 23 | "no-empty": true, 24 | "no-var-requires": true, 25 | "no-require-imports": true, 26 | "one-line": [ 27 | true, 28 | "check-else", 29 | "check-whitespace", 30 | "check-open-brace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [ 38 | true, 39 | "never" 40 | ], 41 | "typedef-whitespace": [ 42 | true, 43 | { 44 | "call-signature": "nospace", 45 | "index-signature": "nospace", 46 | "parameter": "nospace", 47 | "property-declaration": "nospace", 48 | "variable-declaration": "nospace" 49 | } 50 | ], 51 | "import-spacing": true, 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/src/types/Model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * model interface 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:47:25 5 | */ 6 | import { ActionsObservable, StateObservable } from 'redux-observable' 7 | import { Action } from './action' 8 | import endTo from '../operators/endTo' 9 | import end from '../operators/end' 10 | import { Observable } from 'rxjs' 11 | import { ServiceFunc } from '../operators/createService' 12 | 13 | export interface Reducers { 14 | readonly [reducerName: string]: (state: S, payload: any) => S 15 | } 16 | 17 | export interface Dependencies { 18 | end: typeof end 19 | endTo: typeof endTo 20 | services: { 21 | [serviceName: string]: ServiceFunc; 22 | } 23 | } 24 | 25 | export type Flow = ( 26 | flow$: Observable, 27 | action$: ActionsObservable, 28 | state$: StateObservable, 29 | dependencies: Dependencies 30 | ) => Observable 31 | 32 | export interface Flows { 33 | readonly [flowName: string]: Flow 34 | } 35 | 36 | export type Selector = (state: T, props?: P) => any 37 | 38 | export interface Selectors { 39 | readonly [selectorName: string]: Selector 40 | } 41 | 42 | export default interface Model { 43 | readonly name: string 44 | readonly state?: S 45 | readonly reducers?: Reducers 46 | readonly flows?: Flows 47 | readonly selectors?: Selectors 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/reducers/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * error reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:43:11 5 | */ 6 | import { Action } from '../types/action' 7 | import { 8 | LOADING_START_ACTION, 9 | ERROR_SET_ACTION, 10 | SERVICE_ERROR_SET_ACTION, 11 | SERVICE_LOADING_START_ACTION 12 | } from '../constants/actionTypes' 13 | 14 | export default function error( 15 | state = { flows: {}, services: {} }, 16 | action: Action 17 | ): Object { 18 | const { type } = action 19 | const payload = action.payload ? action.payload : {} 20 | switch (type) { 21 | // flow 开始,重置错误 22 | case LOADING_START_ACTION: { 23 | return { 24 | ...state, 25 | flows: { 26 | ...state.flows, 27 | [payload.flow]: null 28 | } 29 | } 30 | } 31 | case SERVICE_LOADING_START_ACTION: { 32 | return { 33 | ...state, 34 | services: { 35 | ...state.services, 36 | [payload.service]: null 37 | } 38 | } 39 | } 40 | case ERROR_SET_ACTION: { 41 | return { 42 | ...state, 43 | flows: { 44 | ...state.flows, 45 | [payload.flow]: payload.error 46 | } 47 | } 48 | } 49 | case SERVICE_ERROR_SET_ACTION: { 50 | return { 51 | ...state, 52 | services: { 53 | ...state.services, 54 | [payload.service]: payload.error 55 | } 56 | } 57 | } 58 | default: 59 | return state 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/es/operators/createService.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * fromService creator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:26:55 5 | */ 6 | import { Observable } from 'rxjs'; 7 | import { Store } from 'redux'; 8 | import { Notification, NotificationLevel } from '../types/Notification'; 9 | interface ServiceTemplates { 10 | success: (resp: T) => string; 11 | error: (error: E) => string; 12 | } 13 | export interface ServiceConfig { 14 | templates?: ServiceTemplates; 15 | isSuccess?: (resp: T) => boolean; 16 | errorSelector?: (error: any) => any; 17 | } 18 | interface ServiceOptions { 19 | /** 等级 */ 20 | level: NotificationLevel; 21 | /** 消息模板 */ 22 | templates?: ServiceTemplates; 23 | /** 重试次数 */ 24 | retry?: number; 25 | /** 重试延迟 */ 26 | retryDelay?: number; 27 | /** 加载延迟 */ 28 | loadingDelay?: number; 29 | } 30 | interface Result { 31 | resp?: T | undefined; 32 | error?: E | undefined; 33 | success?: boolean | undefined; 34 | } 35 | export declare type ServiceFunc = (serviceName: string, service: Promise | Observable, options?: ServiceOptions) => [Observable>, Observable>]; 36 | export declare function partition(source$: Observable, predicate: (value: T, index: number) => boolean): Observable[]; 37 | /** 38 | * 创建 fromService creator 39 | * @param {Notification} notification 40 | */ 41 | export default function createFromService(notification: Notification, serviceConfig: ServiceConfig, store: Store): ServiceFunc; 42 | export {}; 43 | -------------------------------------------------------------------------------- /packages/core/lib/operators/createService.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * fromService creator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:26:55 5 | */ 6 | import { Observable } from 'rxjs'; 7 | import { Store } from 'redux'; 8 | import { Notification, NotificationLevel } from '../types/Notification'; 9 | interface ServiceTemplates { 10 | success: (resp: T) => string; 11 | error: (error: E) => string; 12 | } 13 | export interface ServiceConfig { 14 | templates?: ServiceTemplates; 15 | isSuccess?: (resp: T) => boolean; 16 | errorSelector?: (error: any) => any; 17 | } 18 | interface ServiceOptions { 19 | /** 等级 */ 20 | level: NotificationLevel; 21 | /** 消息模板 */ 22 | templates?: ServiceTemplates; 23 | /** 重试次数 */ 24 | retry?: number; 25 | /** 重试延迟 */ 26 | retryDelay?: number; 27 | /** 加载延迟 */ 28 | loadingDelay?: number; 29 | } 30 | interface Result { 31 | resp?: T | undefined; 32 | error?: E | undefined; 33 | success?: boolean | undefined; 34 | } 35 | export declare type ServiceFunc = (serviceName: string, service: Promise | Observable, options?: ServiceOptions) => [Observable>, Observable>]; 36 | export declare function partition(source$: Observable, predicate: (value: T, index: number) => boolean): Observable[]; 37 | /** 38 | * 创建 fromService creator 39 | * @param {Notification} notification 40 | */ 41 | export default function createFromService(notification: Notification, serviceConfig: ServiceConfig, store: Store): ServiceFunc; 42 | export {}; 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reobservable 2 | 3 | Redux + rxjs + redux-obersvable best practice. Inspired by dva, rematch. 4 | 5 | https://reobservable.github.io/reobservable/ 6 | 7 | --------------- 8 | 9 | [![Coverage Status](https://coveralls.io/repos/github/reobservable/reobservable/badge.svg?branch=master)](https://coveralls.io/github/reobservable/reobservable?branch=master) 10 | [![Build Status](https://travis-ci.com/reobservable/reobservable.svg?branch=master)](https://travis-ci.com/reobservable/reobservable) 11 | 12 | ## Motivation 13 | 14 | I'm a big fan of [dvajs](https://github.com/dvas/dva), it helps me get rid out of the boilerplate of redux. But I think redux-saga in dva sometimes can be a little bit verbose since I prefer functional reactive programming(frp) than imperative programming. 15 | 16 | I know that reinvent the weel could be a terrible practice, but in dva community, there is no plan to support frp. At last, I decided to create **reobservable**, which, in a world, is a state manager mixined dva architecture and frp features(RxJS + redux-observable. In reobservable, dva architecture wiped out the boilerplate code of redux, RxJS played an elite role in managing asynchronous task. 17 | 18 | If you: 19 | 20 | - love redux concept, love the predicatable, centralized, debuggable features 21 | - stuking with the boilerplate code of redux 22 | - want to use RxJS to managing asynchronous task 23 | 24 | try reobservable! 25 | 26 | ## Installation 27 | 28 | ``` 29 | npm install @reobservable/core --save 30 | ``` 31 | 32 | ## Examples 33 | 34 | See [reobservble examples](https://github.com/reobservable/reobservable/tree/master/examples) 35 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reobservable/examples", 3 | "version": "0.2.3", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "start:github": "cross-env webpack-dev-server --open --mode development --config ./github/webpack.config.js", 11 | "start:todo": "cross-env webpack --config ./todo/webpack.config.js --mode=development" 12 | }, 13 | "author": " ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@reobservable/core": "^0.2.3", 17 | "antd": "^3.7.0", 18 | "axios": "^0.18.0", 19 | "connected-react-router": "^4.3.0", 20 | "history": "^4.7.2", 21 | "invariant": "^2.2.4", 22 | "lodash.merge": "^4.6.1", 23 | "react": "^16.4.1", 24 | "react-dom": "^16.4.1", 25 | "react-redux": "^5.0.7", 26 | "react-router": "^4.3.1", 27 | "react-router-dom": "^4.3.1", 28 | "redux": "^4.0.0", 29 | "redux-observable": "^1.0.0", 30 | "reselect": "^3.0.1", 31 | "rxjs": "^6.2.2" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^10.5.7", 35 | "@types/react": "^16.4.8", 36 | "@types/react-dom": "^16.0.7", 37 | "@types/react-redux": "^6.0.6", 38 | "cross-env": "^5.2.0", 39 | "css-loader": "^1.0.0", 40 | "html-webpack-plugin": "^3.2.0", 41 | "style-loader": "^0.21.0", 42 | "ts-loader": "^4.4.2", 43 | "tsconfig-paths-webpack-plugin": "^3.2.0", 44 | "typescript": "^3.0.1", 45 | "webpack": "^4.16.1", 46 | "webpack-cli": "^3.0.8", 47 | "webpack-dev-server": "^3.1.4" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/basics/error-state.md: -------------------------------------------------------------------------------- 1 | # ErrorState State 2 | 3 | Sometimes, you may want know if a service or flow failed. If so, you may display a error message on your web page: 4 | 5 | ![](../public/error.png) 6 | 7 | In reobservable, there is a built-in sub-state called `error` in the root state: 8 | 9 | ```ts 10 | { 11 | error: { 12 | services: { 13 | fetchUser: 'bad request' 14 | }, 15 | flows: { 16 | 'user/fetch': 'cannot read id of undefined' 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | As you can see, there are two type of error state in reobservable. 23 | 24 | ## Service Error 25 | 26 | Service error indicates if a service is failed. You call a service inside a flow like: 27 | 28 | ```ts 29 | const model = { 30 | name: 'user', 31 | // .... 32 | flows: { 33 | fetch(flow$, action$, payload$, dependencies) { 34 | const { api } = dependencies.services 35 | 36 | return flow$.pipe( 37 | switchMap(action => { 38 | const [success$, error$] = api( 39 | // service name 40 | 'fetchUser', 41 | service.fetchUser(params) 42 | ) 43 | // ... 44 | }) 45 | ) 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | Because we named the service `fetchUser`, then we can inspect service error by: 52 | 53 | ```ts 54 | const mapStateToProps = (state: IState) => { 55 | const userFetchError = state.error.services['fetchUser'] 56 | } 57 | ``` 58 | 59 | ## Flow Error 60 | 61 | Flow error indicates if a flow is failed. Like service error, you can access flow error by: 62 | 63 | ```ts 64 | const mapStateToProps = (state: IState) => { 65 | const nameChangingError = state.loading.flows['user/changeName'] 66 | } 67 | ``` -------------------------------------------------------------------------------- /packages/core/es/reducers/error.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | import { LOADING_START_ACTION, ERROR_SET_ACTION, SERVICE_ERROR_SET_ACTION, SERVICE_LOADING_START_ACTION } from '../constants/actionTypes'; 13 | export default function error(state, action) { 14 | if (state === void 0) { state = { flows: {}, services: {} }; } 15 | var _a, _b, _c, _d; 16 | var type = action.type; 17 | var payload = action.payload ? action.payload : {}; 18 | switch (type) { 19 | // flow 开始,重置错误 20 | case LOADING_START_ACTION: { 21 | return __assign({}, state, { flows: __assign({}, state.flows, (_a = {}, _a[payload.flow] = null, _a)) }); 22 | } 23 | case SERVICE_LOADING_START_ACTION: { 24 | return __assign({}, state, { services: __assign({}, state.services, (_b = {}, _b[payload.service] = null, _b)) }); 25 | } 26 | case ERROR_SET_ACTION: { 27 | return __assign({}, state, { flows: __assign({}, state.flows, (_c = {}, _c[payload.flow] = payload.error, _c)) }); 28 | } 29 | case SERVICE_ERROR_SET_ACTION: { 30 | return __assign({}, state, { services: __assign({}, state.services, (_d = {}, _d[payload.service] = payload.error, _d)) }); 31 | } 32 | default: 33 | return state; 34 | } 35 | } 36 | //# sourceMappingURL=error.js.map -------------------------------------------------------------------------------- /packages/core/lib/reducers/error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | var actionTypes_1 = require("../constants/actionTypes"); 15 | function error(state, action) { 16 | if (state === void 0) { state = { flows: {}, services: {} }; } 17 | var _a, _b, _c, _d; 18 | var type = action.type; 19 | var payload = action.payload ? action.payload : {}; 20 | switch (type) { 21 | // flow 开始,重置错误 22 | case actionTypes_1.LOADING_START_ACTION: { 23 | return __assign({}, state, { flows: __assign({}, state.flows, (_a = {}, _a[payload.flow] = null, _a)) }); 24 | } 25 | case actionTypes_1.SERVICE_LOADING_START_ACTION: { 26 | return __assign({}, state, { services: __assign({}, state.services, (_b = {}, _b[payload.service] = null, _b)) }); 27 | } 28 | case actionTypes_1.ERROR_SET_ACTION: { 29 | return __assign({}, state, { flows: __assign({}, state.flows, (_c = {}, _c[payload.flow] = payload.error, _c)) }); 30 | } 31 | case actionTypes_1.SERVICE_ERROR_SET_ACTION: { 32 | return __assign({}, state, { services: __assign({}, state.services, (_d = {}, _d[payload.service] = payload.error, _d)) }); 33 | } 34 | default: 35 | return state; 36 | } 37 | } 38 | exports.default = error; 39 | //# sourceMappingURL=error.js.map -------------------------------------------------------------------------------- /packages/core/test/operator.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * operator test 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-14 19:12:37 5 | */ 6 | import { expect } from 'chai' 7 | import { TestScheduler } from 'rxjs/testing' 8 | import { FLOW_END_INDICATOR } from '../src/constants/meta' 9 | import end from '../src/operators/end' 10 | import endTo from '../src/operators/endTo' 11 | 12 | describe('operator', () => { 13 | let scheduler 14 | 15 | beforeEach(() => { 16 | scheduler = new TestScheduler((actual, expected) => { 17 | expect(actual).deep.equal(expected) 18 | }) 19 | }) 20 | 21 | it('#end', () => { 22 | scheduler.run(({ cold, expectObservable }) => { 23 | const current$ = cold('-a', { 24 | a: 1 25 | }) 26 | const endAction$ = current$.pipe( 27 | end(current => ({ 28 | type: 'SET_CURRENT_USER', 29 | payload: { 30 | current 31 | } 32 | })) 33 | ) 34 | 35 | expectObservable(endAction$).toBe('-e', { 36 | e: { 37 | type: 'SET_CURRENT_USER', 38 | payload: { 39 | current: 1 40 | }, 41 | [FLOW_END_INDICATOR]: true 42 | } 43 | }) 44 | }) 45 | }) 46 | 47 | it('#endTo', () => { 48 | scheduler.run(({ cold, expectObservable }) => { 49 | const clear$ = cold('-a') 50 | const endAction$ = clear$.pipe( 51 | endTo({ 52 | type: 'SET_CURRENT_USER', 53 | payload: { 54 | current: null 55 | } 56 | }) 57 | ) 58 | 59 | expectObservable(endAction$).toBe('-e', { 60 | e: { 61 | type: 'SET_CURRENT_USER', 62 | payload: { 63 | current: null 64 | }, 65 | [FLOW_END_INDICATOR]: true 66 | } 67 | }) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /packages/core/test/init.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * init test 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-10-08 10:23:13 5 | */ 6 | import { expect } from 'chai' 7 | import { init, notificate } from '../src' 8 | import Model from '../src/types/Model' 9 | 10 | describe('#init', () => { 11 | it('should throw error when model name conflicted', () => { 12 | expect(function() { 13 | init({ 14 | models: { 15 | user: { name: 'user', state: {} }, 16 | duplicatedUser: { name: 'user', state: {} } 17 | } 18 | }) 19 | }).to.throw('model user has been defined') 20 | }) 21 | 22 | it('should support empty init', () => { 23 | expect(function() { 24 | init({}) 25 | }).to.not.throw() 26 | }) 27 | 28 | it('should support custom notification', () => { 29 | const notification = { 30 | info(msg) { 31 | return msg 32 | }, 33 | success(msg) { 34 | return msg 35 | } 36 | } 37 | 38 | const user: Model<{ name: string }> = { 39 | name: 'user' 40 | } 41 | 42 | init({ 43 | models: { user }, 44 | notification 45 | }) 46 | 47 | expect(notificate('success', 'ok')).to.equal('ok') 48 | expect(notificate('info', 'none')).to.equal('none') 49 | expect(notificate('error', 'wrong')).to.be.undefined 50 | expect(notificate('none', 'none')).to.equal('none') 51 | }) 52 | 53 | it('should not notificate when message is nil', () => { 54 | const notification = { 55 | info(msg) { 56 | return msg 57 | }, 58 | success(msg) { 59 | return msg 60 | } 61 | } 62 | 63 | const user: Model<{ name: string }> = { 64 | name: 'user' 65 | } 66 | 67 | init({ 68 | models: { user }, 69 | notification 70 | }) 71 | 72 | expect(notificate('info', null)).to.be.undefined 73 | expect(notificate('info', undefined)).to.be.undefined 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packages/core/src/reducers/loading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * loading reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:36:49 5 | */ 6 | 7 | import { Action } from '../types/action' 8 | import { 9 | LOADING_START_ACTION, 10 | LOADING_END_ACTION, 11 | SERVICE_LOADING_START_ACTION, 12 | SERVICE_LOADING_END_ACTION, 13 | ERROR_SET_ACTION, 14 | SERVICE_ERROR_SET_ACTION 15 | } from '../constants/actionTypes' 16 | 17 | export default function loading( 18 | state = { services: {}, flows: {} }, 19 | action: Action 20 | ): Object { 21 | const { type } = action 22 | const payload = action.payload ? action.payload : {} 23 | switch (type) { 24 | case LOADING_START_ACTION: { 25 | return { 26 | ...state, 27 | flows: { 28 | ...state.flows, 29 | [payload.flow]: true 30 | } 31 | } 32 | } 33 | 34 | case LOADING_END_ACTION: { 35 | return { 36 | ...state, 37 | flows: { 38 | ...state.flows, 39 | [payload.flow]: false 40 | } 41 | } 42 | } 43 | 44 | case SERVICE_LOADING_START_ACTION: { 45 | return { 46 | ...state, 47 | services: { 48 | ...state.services, 49 | [payload.service]: true 50 | } 51 | } 52 | } 53 | 54 | case SERVICE_LOADING_END_ACTION: { 55 | return { 56 | ...state, 57 | services: { 58 | ...state.services, 59 | [payload.service]: false 60 | } 61 | } 62 | } 63 | 64 | // flow 出错时,停止 loading 65 | case ERROR_SET_ACTION: { 66 | return { 67 | ...state, 68 | flows: { 69 | ...state.flows, 70 | [payload.flow]: false 71 | } 72 | } 73 | } 74 | 75 | case SERVICE_ERROR_SET_ACTION: { 76 | return { 77 | ...state, 78 | services: { 79 | ...state.services, 80 | [payload.service]: false 81 | } 82 | } 83 | } 84 | 85 | default: 86 | return state 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/github/src/modules/repo/Table.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * repo list 3 | * @author yoyoyohamapi 4 | * @ignore 2018-08-08 14:51:42 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import { Table as AntTable } from 'antd' 9 | import { PaginationProps } from 'antd/lib/pagination' 10 | import { TableProps, ColumnProps } from 'antd/lib/table' 11 | import { LoadingState, getSelectors, Symbols } from '@reobservable/core' 12 | import { Repo, RepoState } from '@models/repo' 13 | 14 | interface StateMapper { 15 | repo: RepoState 16 | loading: LoadingState 17 | } 18 | 19 | const columns: ColumnProps[] = [ 20 | { 21 | key: '_rank', 22 | title: '排名', 23 | width: 150, 24 | render: (text: string, record, index: number) => index + 1 25 | }, 26 | { 27 | key: 'name', 28 | title: 'Repo', 29 | dataIndex: 'name', 30 | render: (text: string, record) => { 31 | return ( 32 | 33 | {text} 34 | 35 | ) 36 | } 37 | } 38 | ] 39 | 40 | const Table: React.SFC & DispatchProp> = props => { 41 | const { dataSource, pagination, loading, dispatch } = props 42 | const handleChange = (pagination: PaginationProps) => { 43 | dispatch({ 44 | type: 'repo/patch', 45 | [Symbols.ALIAS]: 'repo/changePagination', 46 | payload: { 47 | pagination: { 48 | page: pagination.current, 49 | pageSize: pagination.pageSize 50 | } 51 | } 52 | }) 53 | } 54 | 55 | return ( 56 | 66 | ) 67 | } 68 | 69 | const mapStateToProps = (state: StateMapper): TableProps => { 70 | const { repo, loading } = state 71 | return { 72 | dataSource: repo.list, 73 | pagination: getSelectors('repo').pagination(state), 74 | loading: repo.isSilentLoading ? false : loading.services['repo/fetch'] 75 | } 76 | } 77 | 78 | export default connect(mapStateToProps)(Table) 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reobservable", 3 | "version": "0.1.1", 4 | "description": "Redux + rxjs + redux-obersvable best practice", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/reobservable/reobservable" 9 | }, 10 | "directories": { 11 | "example": "examples" 12 | }, 13 | "scripts": { 14 | "lint": "lerna run lint", 15 | "bootstrap": "lerna bootstrap --hoist", 16 | "publish": "npm run test && lerna publish", 17 | "test": "nyc mocha --opts mocha.opts", 18 | "coverage": "nyc report --reporter=text-lcov | coveralls", 19 | "docs:dev": "vuepress dev docs", 20 | "docs:build": "vuepress build docs" 21 | }, 22 | "author": " ", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@types/chai": "^4.1.5", 26 | "@types/isomorphic-fetch": "^0.0.34", 27 | "@types/mocha": "^5.2.5", 28 | "@types/nock": "^9.3.0", 29 | "@types/sinon": "^5.0.1", 30 | "chai": "^4.1.2", 31 | "coveralls": "^3.0.2", 32 | "cross-env": "^5.2.0", 33 | "css-loader": "^2.1.0", 34 | "eslint": "^5.0.1", 35 | "eslint-config-standard": "^11.0.0", 36 | "eslint-plugin-import": "^2.13.0", 37 | "eslint-plugin-node": "^6.0.1", 38 | "eslint-plugin-promise": "^3.8.0", 39 | "eslint-plugin-standard": "^3.1.0", 40 | "fetch-mock": "^6.5.2", 41 | "isomorphic-fetch": "^2.2.1", 42 | "lerna": "^2.11.0", 43 | "mocha": "^5.2.0", 44 | "nock": "^9.6.1", 45 | "nyc": "^13.0.1", 46 | "pre-commit": "^1.2.2", 47 | "rimraf": "^2.6.2", 48 | "sinon": "^6.1.5", 49 | "source-map-support": "^0.5.9", 50 | "ts-node": "^7.0.1", 51 | "tslint": "^5.11.0", 52 | "typescript": "^3.0.1", 53 | "vuepress": "^0.14.4" 54 | }, 55 | "pre-commit": [ 56 | "lint" 57 | ], 58 | "nyc": { 59 | "extension": [ 60 | ".ts" 61 | ], 62 | "include": [ 63 | "packages/**/src/**/*.ts" 64 | ], 65 | "exclude": [ 66 | "**/*.d.ts" 67 | ], 68 | "reporter": [ 69 | "text-summary", 70 | "html" 71 | ], 72 | "sourceMap": true, 73 | "instrument": true 74 | }, 75 | "publishConfig": { 76 | "access": "public" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/github/src/modules/user/Table.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * user list 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 21:18:38 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import { Table as AntTable } from 'antd' 9 | import { PaginationProps } from 'antd/lib/pagination' 10 | import { TableProps, ColumnProps } from 'antd/lib/table' 11 | import { LoadingState, getSelectors, Symbols } from '@reobservable/core' 12 | import { User, UserState } from '@models/user' 13 | 14 | interface StateMapper { 15 | user: UserState 16 | loading: LoadingState 17 | } 18 | 19 | const columns: ColumnProps[] = [ 20 | { 21 | key: '_rank', 22 | title: '排名', 23 | width: 150, 24 | render: (text: string, record: User, index: number) => index + 1 25 | }, 26 | { 27 | key: 'username', 28 | dataIndex: 'login', 29 | title: '用户', 30 | render: (text: string, record: User) => { 31 | return ( 32 | 33 | {text} 34 | 35 | ) 36 | } 37 | } 38 | ] 39 | 40 | const Table: React.SFC & DispatchProp> = props => { 41 | const { dataSource, pagination, loading, dispatch } = props 42 | 43 | const handleChange = (pagination: PaginationProps) => { 44 | dispatch({ 45 | type: 'user/patch', 46 | [Symbols.ALIAS]: 'user/changePagination', 47 | payload: { 48 | pagination: { 49 | page: pagination.current, 50 | pageSize: pagination.pageSize 51 | } 52 | } 53 | }) 54 | } 55 | 56 | return ( 57 | 67 | ) 68 | } 69 | 70 | const mapStateToProps = (state: StateMapper): TableProps => { 71 | const { user, loading } = state 72 | return { 73 | dataSource: user.list, 74 | pagination: getSelectors('user').pagination(state), 75 | loading: user.isSilentLoading ? false : loading.services['user/fetch'] 76 | } 77 | } 78 | 79 | export default connect(mapStateToProps)(Table) 80 | -------------------------------------------------------------------------------- /examples/github/src/modules/app/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Github demo 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 20:24:38 5 | */ 6 | import * as React from 'react' 7 | import { connect } from 'react-redux' 8 | import { Route, Redirect } from 'react-router' 9 | import { Link } from 'react-router-dom' 10 | import { ConnectedRouter, RouterState } from 'connected-react-router' 11 | import { Layout, Menu } from 'antd' 12 | import createHistory from 'history/createHashHistory' 13 | import User from '@modules/user' 14 | import Repo from '@modules/repo' 15 | 16 | interface Props { 17 | path: string 18 | } 19 | 20 | const history = createHistory() 21 | 22 | const { Header, Content, Footer } = Layout 23 | 24 | class App extends React.Component { 25 | render() { 26 | const { path } = this.props 27 | return ( 28 | 29 | 30 |
31 | 37 | 38 | Users 39 | 40 | 41 | Repos 42 | 43 | 44 |
45 | 46 | 47 | 48 | } /> 49 | 50 | 51 | 52 | 53 | 54 |
Reobservable
55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | const mapStateToProps = ({ router }: { router: RouterState }): Props => ({ 62 | path: router.location.pathname 63 | }) 64 | 65 | export default connect(mapStateToProps)(App) 66 | -------------------------------------------------------------------------------- /docs/basics/loading-state.md: -------------------------------------------------------------------------------- 1 | # Loading State 2 | 3 | Sometimes, you may want know if a service or flow is running. If so, you may display a loading spinner on your web page: 4 | 5 | ![](../public/spinner.png) 6 | 7 | In reobservable, there is a built-in sub-state called `loading` in the root state: 8 | 9 | ```ts 10 | { 11 | loading: { 12 | services: { 13 | fetchUser: true 14 | }, 15 | flows: { 16 | 'user/fetch': true 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | As you can see, there are two type of loading state in reobservable. 23 | 24 | ## Service Loading 25 | 26 | Service loading indicates if a service is running. You call a service inside a flow like: 27 | 28 | ```ts 29 | const model = { 30 | name: 'user', 31 | // .... 32 | flows: { 33 | fetch(flow$, action$, payload$, dependencies) { 34 | const { api } = dependencies.services 35 | 36 | return flow$.pipe( 37 | switchMap(action => { 38 | const [success$, error$] = api( 39 | // service name 40 | 'fetchUser', 41 | service.fetchUser(params) 42 | ) 43 | // ... 44 | }) 45 | ) 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | Because we named the service `fetchUser`, then we can access it by: 52 | 53 | ```ts 54 | const mapStateToProps = (state: IState) => { 55 | const isUserFetching = state.loading.services['fetchUser'] 56 | } 57 | ``` 58 | 59 | ## Flow Loading 60 | 61 | Flow loading indicates if a flow is running. But in order to tell reobservable if a flow should be considered complete, you should use `end` or `endTo` operator to emit an end action: 62 | 63 | ```ts 64 | import { end } from '@reobservable/core' 65 | 66 | const model = { 67 | name: 'user', 68 | // .... 69 | flows: { 70 | changeName(flow$, action$, payload$, dependencies) { 71 | return flow$.pipe( 72 | end(action => ({ 73 | type: 'user/changeName', 74 | payload: { 75 | name: action.payload.name 76 | } 77 | })) 78 | ) 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | Then, you can access it by: 85 | 86 | ```ts 87 | const mapStateToProps = (state: IState) => { 88 | const isNameChanging = state.loading.flows['user/changeName'] 89 | } 90 | ``` -------------------------------------------------------------------------------- /packages/core/es/reducers/loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * loading reducer 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 12:36:49 5 | */ 6 | var __assign = (this && this.__assign) || function () { 7 | __assign = Object.assign || function(t) { 8 | for (var s, i = 1, n = arguments.length; i < n; i++) { 9 | s = arguments[i]; 10 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 11 | t[p] = s[p]; 12 | } 13 | return t; 14 | }; 15 | return __assign.apply(this, arguments); 16 | }; 17 | import { LOADING_START_ACTION, LOADING_END_ACTION, SERVICE_LOADING_START_ACTION, SERVICE_LOADING_END_ACTION, ERROR_SET_ACTION, SERVICE_ERROR_SET_ACTION } from '../constants/actionTypes'; 18 | export default function loading(state, action) { 19 | if (state === void 0) { state = { services: {}, flows: {} }; } 20 | var _a, _b, _c, _d, _e, _f; 21 | var type = action.type; 22 | var payload = action.payload ? action.payload : {}; 23 | switch (type) { 24 | case LOADING_START_ACTION: { 25 | return __assign({}, state, { flows: __assign({}, state.flows, (_a = {}, _a[payload.flow] = true, _a)) }); 26 | } 27 | case LOADING_END_ACTION: { 28 | return __assign({}, state, { flows: __assign({}, state.flows, (_b = {}, _b[payload.flow] = false, _b)) }); 29 | } 30 | case SERVICE_LOADING_START_ACTION: { 31 | return __assign({}, state, { services: __assign({}, state.services, (_c = {}, _c[payload.service] = true, _c)) }); 32 | } 33 | case SERVICE_LOADING_END_ACTION: { 34 | return __assign({}, state, { services: __assign({}, state.services, (_d = {}, _d[payload.service] = false, _d)) }); 35 | } 36 | // flow 出错时,停止 loading 37 | case ERROR_SET_ACTION: { 38 | return __assign({}, state, { flows: __assign({}, state.flows, (_e = {}, _e[payload.flow] = false, _e)) }); 39 | } 40 | case SERVICE_ERROR_SET_ACTION: { 41 | return __assign({}, state, { services: __assign({}, state.services, (_f = {}, _f[payload.service] = false, _f)) }); 42 | } 43 | default: 44 | return state; 45 | } 46 | } 47 | //# sourceMappingURL=loading.js.map -------------------------------------------------------------------------------- /packages/core/lib/reducers/loading.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * loading reducer 4 | * @author yoyoyohamapi 5 | * @ignore created 2018-08-03 12:36:49 6 | */ 7 | var __assign = (this && this.__assign) || function () { 8 | __assign = Object.assign || function(t) { 9 | for (var s, i = 1, n = arguments.length; i < n; i++) { 10 | s = arguments[i]; 11 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 12 | t[p] = s[p]; 13 | } 14 | return t; 15 | }; 16 | return __assign.apply(this, arguments); 17 | }; 18 | Object.defineProperty(exports, "__esModule", { value: true }); 19 | var actionTypes_1 = require("../constants/actionTypes"); 20 | function loading(state, action) { 21 | if (state === void 0) { state = { services: {}, flows: {} }; } 22 | var _a, _b, _c, _d, _e, _f; 23 | var type = action.type; 24 | var payload = action.payload ? action.payload : {}; 25 | switch (type) { 26 | case actionTypes_1.LOADING_START_ACTION: { 27 | return __assign({}, state, { flows: __assign({}, state.flows, (_a = {}, _a[payload.flow] = true, _a)) }); 28 | } 29 | case actionTypes_1.LOADING_END_ACTION: { 30 | return __assign({}, state, { flows: __assign({}, state.flows, (_b = {}, _b[payload.flow] = false, _b)) }); 31 | } 32 | case actionTypes_1.SERVICE_LOADING_START_ACTION: { 33 | return __assign({}, state, { services: __assign({}, state.services, (_c = {}, _c[payload.service] = true, _c)) }); 34 | } 35 | case actionTypes_1.SERVICE_LOADING_END_ACTION: { 36 | return __assign({}, state, { services: __assign({}, state.services, (_d = {}, _d[payload.service] = false, _d)) }); 37 | } 38 | // flow 出错时,停止 loading 39 | case actionTypes_1.ERROR_SET_ACTION: { 40 | return __assign({}, state, { flows: __assign({}, state.flows, (_e = {}, _e[payload.flow] = false, _e)) }); 41 | } 42 | case actionTypes_1.SERVICE_ERROR_SET_ACTION: { 43 | return __assign({}, state, { services: __assign({}, state.services, (_f = {}, _f[payload.service] = false, _f)) }); 44 | } 45 | default: 46 | return state; 47 | } 48 | } 49 | exports.default = loading; 50 | //# sourceMappingURL=loading.js.map -------------------------------------------------------------------------------- /examples/github/src/modules/repo/ActionPanel.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * repo action panel 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-18 19:07:25 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import { Row, Col, Select, Input } from 'antd' 9 | import { LoadingState } from '@reobservable/core' 10 | import { RepoState } from '@models/repo' 11 | 12 | interface StateMapper { 13 | repo: RepoState 14 | loading: LoadingState 15 | } 16 | 17 | const Option = Select.Option 18 | const Search = Input.Search 19 | 20 | const ORDER_LIST = [ 21 | { 22 | text: '按 star 数目排序', 23 | value: 'stars' 24 | }, 25 | { 26 | text: '按 fork 数目排序', 27 | value: 'forks' 28 | } 29 | ] 30 | 31 | interface Props { 32 | sort: string 33 | query: string 34 | disabledSearch: boolean 35 | } 36 | 37 | const ActionPanel: React.SFC = props => { 38 | const { dispatch, sort, disabledSearch } = props 39 | 40 | const handleSearch = (query: string) => { 41 | if (query === props.query) { 42 | dispatch({ type: 'repo/fetch' }) 43 | } else { 44 | dispatch({ 45 | type: 'repo/change', 46 | payload: { 47 | query 48 | } 49 | }) 50 | } 51 | dispatch({ 52 | type: 'repo/patch', 53 | payload: { 54 | pagination: { 55 | page: 1 56 | } 57 | } 58 | }) 59 | } 60 | 61 | const handleSortChange = (sort: string) => { 62 | dispatch({ 63 | type: 'repo/change', 64 | payload: { sort } 65 | }) 66 | } 67 | 68 | return ( 69 | 76 |
77 | 84 | 85 | 86 | 93 | 94 | 95 | ) 96 | } 97 | 98 | const mapStateToProps = ({ repo, loading }: StateMapper): Props => ({ 99 | sort: repo.sort, 100 | query: repo.query, 101 | disabledSearch: loading.services['repo/fetch'] 102 | }) 103 | 104 | export default connect(mapStateToProps)(ActionPanel) 105 | -------------------------------------------------------------------------------- /examples/github/src/modules/user/ActionPanel.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * user action panel 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 21:18:38 5 | */ 6 | import * as React from 'react' 7 | import { connect, DispatchProp } from 'react-redux' 8 | import { Row, Col, Select, Input } from 'antd' 9 | import { LoadingState } from '@reobservable/core' 10 | import { UserState } from '@models/user' 11 | 12 | interface StateMapper { 13 | user: UserState 14 | loading: LoadingState 15 | } 16 | 17 | const Option = Select.Option 18 | const Search = Input.Search 19 | 20 | const ORDER_LIST = [ 21 | { 22 | text: '按 followers 数目排序', 23 | value: 'followers' 24 | }, 25 | { 26 | text: '按 repos 数目排序', 27 | value: 'repositories' 28 | } 29 | ] 30 | 31 | interface Props { 32 | sort: string 33 | query: string 34 | disabledSearch: boolean 35 | } 36 | 37 | const ActionPanel: React.SFC = props => { 38 | const { dispatch, sort, disabledSearch } = props 39 | 40 | const handleSearch = (query: string) => { 41 | dispatch({ 42 | type: 'user/patch', 43 | payload: { 44 | pagination: { 45 | page: 1 46 | } 47 | } 48 | }) 49 | if (query === props.query) { 50 | dispatch({ type: 'user/fetch' }) 51 | } else { 52 | dispatch({ 53 | type: 'user/change', 54 | payload: { 55 | query 56 | } 57 | }) 58 | } 59 | } 60 | 61 | const handleSortChange = (sort: string) => { 62 | dispatch({ 63 | type: 'user/change', 64 | payload: { sort } 65 | }) 66 | } 67 | 68 | return ( 69 | 76 | 77 | 84 | 85 | 86 | 93 | 94 | 95 | ) 96 | } 97 | 98 | const mapStateToProps = ({ user, loading }: StateMapper): Props => ({ 99 | sort: user.sort, 100 | query: user.query, 101 | disabledSearch: loading.services['user/fetch'] 102 | }) 103 | 104 | export default connect(mapStateToProps)(ActionPanel) 105 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | Just like dva, things interact with state are placed in a model. Previously, in plain reudx, we should manage our actions, action types and reducers in different places. But in dva, rematch or reobervable, boilerplate code was moved into the framework, we can focus on state management in a single model. 4 | 5 | ```js 6 | const model = { 7 | name: 'user', 8 | state: { 9 | list: [], 10 | total: 0, 11 | searchKey: '', 12 | error: null 13 | }, 14 | reducers: { 15 | fetchSuccess(state, payload) { 16 | return { 17 | ...state, 18 | list: payload.list, 19 | total: payload.total 20 | } 21 | }, 22 | fetchError(state, payload) { 23 | return { ...state, error: payload.error } 24 | } 25 | }, 26 | flows: { 27 | fetch(flow$, action$, state$) { 28 | return flow$.pipe( 29 | withLatestFrom(state$, (action, state) => { 30 | return { 31 | searchKey: state.searchKey 32 | } 33 | }), 34 | switchMap(params => { 35 | return api.fetchUser(params).pipe( 36 | map(resp => ({ 37 | type: 'user/fetchSuccess', 38 | payload: { 39 | list: resp.items, 40 | total: resp.totalCount 41 | } 42 | })) 43 | catchError(error => of({ 44 | type: 'user/fetchError', 45 | payload: { error } 46 | })) 47 | ) 48 | }) 49 | ) 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | As shown above, a model is composed of the following parts: 56 | 57 | - **name** 58 | 59 | The name of the model which should be universally **unique**. It determines how to locate then state, action and reducer of the model. 60 | 61 | - **state** 62 | 63 | The initial state of the model. 64 | 65 | - **reduers** 66 | 67 | The key of any reducer in model's reducers is its **name**. For example, as shown above, we defined a reducer named `fetch`, then if the application dispatches a an action with type `user/fetchSuccess`, the reducer will be called to reduce the next state. 68 | 69 | In addition, if we defined a reducer named `bar/fetchSuccess` in this model, it will be responsible for the `bar/fetchSuccess` action. 70 | 71 | Now, you'd like to know that **reducer is used to interact with synchronous action**. 72 | 73 | - **flows** 74 | 75 | Just like reducers, every flow in flows is assiociated with an action by its name. Compare with reducer, flow is designed to **interact with asynchronous action**. 76 | 77 | A flow is like an epic in redux-observable, the concept of them is same, but the function signature is a little difference. The first argument of a flow is not `action$`, but a `flow$` which means `action$.ofType(model/flow)`. 78 | 79 | As shown above, we defiened a flow named `fetch`, when the application dispatches an `user/fetch` action, `flow$` in `fetch` will emmit value. 80 | -------------------------------------------------------------------------------- /docs/basics/change-and-patch.md: -------------------------------------------------------------------------------- 1 | # Change and Patch Action 2 | 3 | ## Defination 4 | 5 | There are two built-in sync action in reobservable: 6 | 7 | - `change` action: change the state declared in action payload 8 | - `patch` action: change the state **partly** declared in action payload. 9 | 10 | Correspondingly, every model in reobservable has two default reducer: 11 | 12 | - `change` reducer 13 | - `patch` reducer 14 | 15 | Change and patch reducer helps you quickly change the model state **without define any other reducers**. 16 | 17 | ## Difference 18 | 19 | Suppose we defined an user model: 20 | 21 | ```ts 22 | import { Model } from '@reobservable' 23 | 24 | const model: Model = { 25 | name: 'user', 26 | state: { 27 | list: [], 28 | pagination: { 29 | totolCount: 0, 30 | page: 0, 31 | pageSize: 10 32 | } 33 | }, 34 | reducers: { 35 | // ... 36 | }, 37 | flows: { 38 | // ... 39 | } 40 | } 41 | ``` 42 | 43 | Initially, the state of the model is: 44 | 45 | ```ts 46 | { 47 | list: [], 48 | pagination: { 49 | totolCount: 0, 50 | page: 0, 51 | pageSize: 10 52 | } 53 | } 54 | ``` 55 | 56 | if we dispatched an change action: 57 | 58 | ```ts 59 | { 60 | type: 'user/change', 61 | payload: { 62 | pagination: { 63 | totalCount: 1 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | then, new state will be: 70 | 71 | ```ts 72 | { 73 | list: [], 74 | pagination: { 75 | totalCount: 1 76 | } 77 | } 78 | ``` 79 | 80 | As you can see, `page`、 `pageSize` state disappeared since **change** reducer will be overrided the state declared in action payload directly. In this example, **change** reducer overrided the `pagination` state. 81 | 82 | Instead, we dispatched an change action: 83 | 84 | ```ts 85 | { 86 | type: 'user/patch', 87 | payload: { 88 | pagination: { 89 | totalCount: 1 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | then, new state will be: 96 | 97 | ```ts 98 | { 99 | list: [], 100 | pagination: { 101 | totalCount: 1, 102 | page: 0, 103 | pageSize: 10 104 | } 105 | } 106 | ``` 107 | 108 | Patch means **patch update** the state, in this example, **patch** reducer will only update the `totalCount` inside the `pagination`. 109 | 110 | ## Alias 111 | 112 | The abuse of change and patch action may cause your redux dev-tools looks unreadable: 113 | 114 | ``` 115 | user/change 116 | user/change 117 | post/change 118 | ... 119 | ``` 120 | 121 | To pursue a more clear action flow, we could provide a alias for change and patch action: 122 | 123 | ```ts 124 | import { Symbols } from '@reobservable/core' 125 | 126 | disaptch({ 127 | type: 'user/change', 128 | [Symbols.ALIAS]: 'user/changePagination', 129 | payload: { 130 | pagination: { 131 | totalCount: 1, 132 | page: 0, 133 | pageSize: 10 134 | } 135 | } 136 | }) 137 | ``` 138 | -------------------------------------------------------------------------------- /packages/core/lib/operators/createService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createService.js","sourceRoot":"","sources":["../../src/operators/createService.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /packages/core/es/operators/createService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"createService.js","sourceRoot":"","sources":["../../src/operators/createService.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /docs/introduction/get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## Installation 4 | 5 | ```bash 6 | npm install @reobservable/core 7 | ``` 8 | 9 | Suppose we want to build a user management app which should display a user list in the web page, the following steps should be followed: 10 | 11 | ## Define Service 12 | 13 | At first, you should declare the service that in charge of the communication of the frontend and backend. In this example, suppose you prefre [axios](https://github.com/axios/axios) for api request: 14 | 15 | ```ts 16 | import { AxiosResponse, AxiosError } from 'axios' 17 | import { ServiceConfig, ServiceFunc } from '@reobservable/core' 18 | 19 | interface ApiService extends ServiceConfig, AxiosError> {} 20 | 21 | const api: ApiService = { 22 | templates: { 23 | success: (resp) => 'ok!', 24 | error: (error) => 'error!' 25 | } 26 | } 27 | 28 | export default api 29 | ``` 30 | 31 | ## Define Models 32 | 33 | In this scenario, we should define a **user** model to aggregate state, actions, async actions & reducers. 34 | 35 | ```ts 36 | import { Model } from '@reobservable/core' 37 | import { merge } from 'rxjs' 38 | import { switchMap, withLatestFrom, map, mapTo } from 'rxjs/operators' 39 | import { IUserState, IState } from './types' 40 | import { fetchUsers } from './service' 41 | 42 | const user: Model = { 43 | name: 'user', 44 | state: { 45 | list: [], 46 | total: 0 47 | }, 48 | reducers: { 49 | fetchSuccess(state, payload) { 50 | return { 51 | ...state, 52 | list: payload.list, 53 | total: payload.total 54 | } 55 | }, 56 | fetchError(state, payload) { 57 | return { 58 | ...state, 59 | list: [], 60 | total: 0 61 | } 62 | } 63 | }, 64 | flows: { 65 | fetch(flow$, action$, state$, dependencies) { 66 | const { api } = dependencies.services 67 | return flow$.pipe( 68 | withLatestFrom(state$, (action, state) => { 69 | const params = { 70 | // construct params from state or action payload 71 | } 72 | return params 73 | }), 74 | switchMap(params => { 75 | const [success$, error$] = api( 76 | '/api/fetchUsers', 77 | fetchUsers(params) 78 | ) 79 | 80 | return merge( 81 | success$.pipe( 82 | map(resp => ({ 83 | type: 'user/fetchSuccess', 84 | payload: { 85 | list: resp.list, 86 | total: resp.total 87 | } 88 | })) 89 | ), 90 | error$.pipe( 91 | mapTo({type: 'user/fetchError'}) 92 | ) 93 | ) 94 | }) 95 | ) 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | ## Create Redux Store 102 | 103 | Finnaly, we could call `init(options)` to create redux store: 104 | 105 | ```tsx 106 | import * as React from 'react' 107 | import { render } from 'react-dom' 108 | import { Provider } from 'react-redux' 109 | import { init } from '@reobservable/core' 110 | 111 | import user from './models/user' 112 | import api from './services/api' 113 | 114 | const store = init({ 115 | services: { 116 | api 117 | }, 118 | models: { 119 | user 120 | } 121 | }) 122 | 123 | render( 124 | 125 | 126 | , 127 | document.getElementById('app') 128 | ) 129 | ``` -------------------------------------------------------------------------------- /packages/core/test/redux.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * redux config test 3 | * @author yooyoyohamapi 4 | * @ignore created 2018-10-08 09:38:26 5 | */ 6 | 7 | import * as sinon from 'sinon' 8 | import { expect } from 'chai' 9 | import { init } from '../src' 10 | import Model from '../src/types/Model' 11 | import { Middleware, combineReducers } from 'redux' 12 | 13 | describe('redux', () => { 14 | it('should support root reducer wrapper#1', () => { 15 | const model: Model<{ name: string }> = { 16 | name: 'model', 17 | state: { 18 | name: 'model' 19 | }, 20 | reducers: { 21 | changeName(state, payload) { 22 | return { ...state, name: payload.name } 23 | }, 24 | dangerouslyChangeName(state, payload) { 25 | return { ...state, name: payload.name } 26 | } 27 | } 28 | } 29 | const store = init({ 30 | redux: { 31 | rootReducer(rootReducer) { 32 | return function(state, action) { 33 | if (action.type === 'model/dangerouslyChangeName') { 34 | return state 35 | } else { 36 | return rootReducer(state, action) 37 | } 38 | } 39 | } 40 | }, 41 | models: { model } 42 | }) 43 | 44 | store.dispatch({ 45 | type: 'model/changeName', 46 | payload: { name: 'changed' } 47 | }) 48 | 49 | store.dispatch({ 50 | type: 'model/dangerouslyChangeName', 51 | payload: { name: 'dangerouslyChangedName' } 52 | }) 53 | 54 | const { name } = store.getState().model 55 | expect(name).to.equal('changed') 56 | }) 57 | 58 | it('should support root reducer wrapper#2', () => { 59 | const model: Model<{ name: string }> = { 60 | name: 'model', 61 | state: { 62 | name: 'model' 63 | }, 64 | reducers: { 65 | changeName(state, payload) { 66 | return { ...state, name: payload.name } 67 | }, 68 | dangerouslyChangeName(state, payload) { 69 | return { ...state, name: payload.name } 70 | } 71 | } 72 | } 73 | const store = init({ 74 | redux: { 75 | rootReducer(rootReducer, reducers) { 76 | return combineReducers({ 77 | ...reducers, 78 | count: (state = 0, action) => { 79 | if (action.type === 'count/incr') { 80 | return state + 1 81 | } else { 82 | return state 83 | } 84 | } 85 | }) 86 | } 87 | }, 88 | models: { model } 89 | }) 90 | 91 | store.dispatch({ 92 | type: 'model/changeName', 93 | payload: { name: 'changed' } 94 | }) 95 | 96 | store.dispatch({ 97 | type: 'count/incr' 98 | }) 99 | 100 | const { name } = store.getState().model 101 | const count = store.getState().count 102 | expect(name).to.equal('changed') 103 | expect(count).to.equal(1) 104 | }) 105 | 106 | it('should support single redux middleware', () => { 107 | const fake = sinon.fake() 108 | const middleware: Middleware = store => { 109 | return next => action => { 110 | return fake() 111 | } 112 | } 113 | 114 | const store = init({ 115 | redux: { 116 | middleware 117 | } 118 | }) 119 | 120 | store.dispatch({ type: 'fetch' }) 121 | 122 | expect(fake.calledOnce).to.be.true 123 | }) 124 | 125 | it('should support redux middleware list', () => { 126 | const fake = sinon.fake() 127 | const middleware: Middleware = store => { 128 | return next => action => { 129 | return fake() 130 | } 131 | } 132 | 133 | const store = init({ 134 | redux: { 135 | middleware: [middleware] 136 | } 137 | }) 138 | 139 | store.dispatch({ type: 'fetch' }) 140 | 141 | expect(fake.calledOnce).to.be.true 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /docs/introduction/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | Before creating a reobservable app, there are only two concepts you should konw. 4 | 5 | ## Model 6 | 7 | Same as dva, model defines a state machine which mixined **initial state**,**sync actions**, **async actions(flows)**, **reducers** in one place. 8 | 9 | ```ts 10 | import { Model } from '@reobservable' 11 | 12 | const model: Model = { 13 | name: 'user', 14 | state: { 15 | list: [], 16 | pagination: { 17 | totolCount: 0, 18 | page: 0, 19 | pageSize: 10 20 | } 21 | }, 22 | reducers: {}, 23 | flows: {} 24 | } 25 | 26 | export default model 27 | ``` 28 | 29 | As shown above, a model consists of five parts: 30 | 31 | **name** 32 | 33 | Model name, it indicates the scope of a model in redux store. In this example, we could get the user state by calling `store.getState().user`. Or in React component, we use react-redux: 34 | 35 | ```ts 36 | const mapStateToPros = (state: IState) => { 37 | return { 38 | totalCount: state.user.totalCount 39 | } 40 | } 41 | ``` 42 | 43 | **state** 44 | 45 | Initial state of a model. 46 | 47 | **reducers** 48 | 49 | Redux reducers. When a synchronous action dispatched, depends on its type, it may hit a reducer to return a new state. For example, if we dispatched an action: 50 | 51 | ```ts 52 | { 53 | type: 'user/fetchSuccess', 54 | payload: { 55 | list: [{nick: 'john', age: 21}, {nick: 'tom', age: 32}], 56 | totalCount: 2 57 | } 58 | } 59 | ``` 60 | 61 | then reobservable will lookup user model to determine if there was a `fetchSuccess` in the defined reducers. If user model has `fetchSuccess` reducer, it will be called, and then, we would get a new state of the user model. 62 | 63 | **flows** 64 | 65 | Async flows. Every flow defined in model flows have **four** parameters, and return an `ActionObservable`(An observable which emit action): 66 | 67 | ```ts 68 | const model = { 69 | name: 'user', 70 | // .... 71 | flows: { 72 | fetch(flow$, action$, payload$, dependencies) { 73 | return flow$.pipe( 74 | mapTo({ 75 | type: 'user/fetchSuccess', 76 | payload: {} 77 | }) 78 | ) 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | It's signature is very similar to the **epic** in [redux-observable](https://redux-observable.js.org/). The difference is the first parameter - `flow$`, it is equivalent to `action$.ofType('model/flow')`. 85 | 86 | For example, if we dispatched an action: 87 | 88 | ```ts 89 | { 90 | type: 'user/fetch', 91 | payload: {} 92 | } 93 | ``` 94 | 95 | `fetch` flow return in the user model will be responded. 96 | 97 | ## Service 98 | 99 | Service describe the communication between frontend and backend, in reobservable, you can define multiple services: 100 | 101 | ```ts 102 | import { AxiosResponse, AxiosError } from 'axios' 103 | import { ServiceConfig, ServiceFunc } from '@reobservable/core' 104 | 105 | interface ApiService extends ServiceConfig, AxiosError> {} 106 | 107 | const api: ApiService = { 108 | templates: { 109 | success: (resp) => 'ok!', 110 | error: (error) => 'error!' 111 | } 112 | } 113 | 114 | export default api 115 | ``` 116 | 117 | In reobservable, service was a dependency injected into model flows, and could be used in frp way: 118 | 119 | ```ts 120 | const model = { 121 | // .... 122 | flows: { 123 | fetch(flow$, action$, payload$, dependencies) { 124 | const { api } = dependencies.services 125 | return flow$.pipe( 126 | switchMap(action => { 127 | const [success$, error$] = api( 128 | 'fetch', 129 | fetch(action.payload.id) 130 | ) 131 | 132 | return merge( 133 | success$.pipe( 134 | // success stream 135 | ), 136 | error$.pipe( 137 | // error stream 138 | ) 139 | ) 140 | }) 141 | ) 142 | } 143 | } 144 | } 145 | ``` -------------------------------------------------------------------------------- /packages/core/test/flow.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * flow tests 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-09-21 15:10:10 5 | */ 6 | import { expect } from 'chai' 7 | import { Store } from 'redux' 8 | import { map, delay } from 'rxjs/operators' 9 | import { init } from '../src' 10 | import Model from '../src/types/Model' 11 | import end from '../src/operators/end' 12 | 13 | interface UserState { 14 | list: User[] 15 | current: number 16 | } 17 | 18 | interface PostState { 19 | list: Post[] 20 | current: number 21 | } 22 | 23 | interface User { 24 | id: number 25 | name: string 26 | age: number 27 | } 28 | 29 | interface Post { 30 | id: number 31 | title: string 32 | content: string 33 | } 34 | 35 | const user: Model = { 36 | name: 'user', 37 | state: { 38 | list: [], 39 | current: null 40 | }, 41 | flows: { 42 | throwError(flow$) { 43 | return flow$.pipe( 44 | map(({ payload }) => { 45 | if (payload.throw) { 46 | throw Error('error') 47 | } else { 48 | return { 49 | type: 'none' 50 | } 51 | } 52 | }) 53 | ) 54 | } 55 | } 56 | } 57 | 58 | const post: Model = { 59 | name: 'post', 60 | state: { 61 | list: [], 62 | current: null 63 | }, 64 | reducers: { 65 | fetchSuccess(state, payload) { 66 | return { 67 | ...state, 68 | list: payload.list 69 | } 70 | } 71 | }, 72 | flows: { 73 | fetch(flow$) { 74 | return flow$.pipe( 75 | delay(10), 76 | end(() => ({ 77 | type: 'post/fetchSuccess', 78 | payload: { 79 | list: [ 80 | { 81 | id: 1, 82 | title: '#1', 83 | content: '#1 content' 84 | }, 85 | { 86 | id: 2, 87 | title: '#2', 88 | content: '#2 content' 89 | }, 90 | { 91 | id: 3, 92 | title: '#3', 93 | content: '#3 content' 94 | } 95 | ] 96 | } 97 | })) 98 | ) 99 | } 100 | } 101 | } 102 | 103 | describe('flow', () => { 104 | let store: Store 105 | 106 | const initStore = () => { 107 | store = init({ 108 | models: { user, post } 109 | }) 110 | } 111 | 112 | before(() => { 113 | // tslint:disable-next-line:no-empty 114 | console.error = function() {} 115 | }) 116 | 117 | beforeEach(initStore) 118 | 119 | it('should set loading true when flow start', () => { 120 | store.dispatch({ 121 | type: 'post/fetch' 122 | }) 123 | 124 | const loading = store.getState().loading 125 | expect(loading.flows['post/fetch']).to.be.true 126 | }) 127 | 128 | it('should set loading false when flow end', done => { 129 | store.dispatch({ 130 | type: 'post/fetch' 131 | }) 132 | 133 | setTimeout(() => { 134 | const loading = store.getState().loading 135 | expect(loading.flows['post/fetch']).to.be.false 136 | done() 137 | }, 20) 138 | }) 139 | 140 | it('should catch flow error', () => { 141 | store.dispatch({ 142 | type: 'user/throwError', 143 | payload: { 144 | throw: true 145 | } 146 | }) 147 | 148 | const error = store.getState().error 149 | expect(error.flows['user/throwError']) 150 | .to.property('message') 151 | .be.equal('error') 152 | }) 153 | 154 | it('should reset error when action retry', () => { 155 | store.dispatch({ 156 | type: 'user/throwError', 157 | payload: { 158 | throw: true 159 | } 160 | }) 161 | 162 | store.dispatch({ 163 | type: 'user/throwError', 164 | payload: { 165 | throw: false 166 | } 167 | }) 168 | 169 | const error = store.getState().error 170 | expect(error.flows['user/throwError']).to.be.null 171 | }) 172 | 173 | it('should not stop current flow when other flow throw error', done => { 174 | store.dispatch({ 175 | type: 'user/throwError' 176 | }) 177 | 178 | store.dispatch({ 179 | type: 'post/fetch' 180 | }) 181 | 182 | setTimeout(() => { 183 | const state = store.getState() 184 | expect(state.post.list).to.not.empty 185 | done() 186 | }, 20) 187 | }) 188 | }) 189 | -------------------------------------------------------------------------------- /examples/github/src/models/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * user model 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 15:11:14 5 | */ 6 | import { isEqual } from 'lodash/fp' 7 | import { timer, Observable, merge } from 'rxjs' 8 | import { 9 | map, 10 | combineLatest, 11 | distinctUntilChanged, 12 | switchMap, 13 | startWith, 14 | takeUntil, 15 | mapTo 16 | } from 'rxjs/operators' 17 | import { createSelector } from 'reselect' 18 | import { PaginationProps } from 'antd/lib/pagination' 19 | import { Model, NOTIFICATION_LEVEL, getService } from '@reobservable/core' 20 | import { fetch } from '@services/user' 21 | import { Order, Pagination } from '@models/common' 22 | import { ApiServiceFunc } from '@services/types' 23 | 24 | export interface User { 25 | login: string 26 | id: number 27 | avatar_url: string 28 | url: string 29 | html_url: string 30 | } 31 | 32 | export interface UserState { 33 | list: User[] 34 | pagination: Pagination 35 | sort: string 36 | isSilentLoading: boolean 37 | query: string 38 | } 39 | 40 | export interface SearchParam { 41 | q: string 42 | language: string 43 | page: number 44 | per_page: number 45 | sort: string 46 | order: Order 47 | } 48 | 49 | export interface SearchResp { 50 | total_count: number 51 | items: User[] 52 | } 53 | 54 | const showTotal = total => `共 ${total} 条` 55 | 56 | const user: Model = { 57 | name: 'user', 58 | state: { 59 | list: [], 60 | pagination: { 61 | total: 0, 62 | page: 1, 63 | pageSize: 10 64 | }, 65 | sort: 'followers', 66 | isSilentLoading: false, 67 | query: '' 68 | }, 69 | selectors: { 70 | pagination: createSelector< 71 | { user: UserState }, 72 | Pagination, 73 | PaginationProps 74 | >( 75 | ({ user }) => user.pagination, 76 | ({ total, page, pageSize }) => ({ 77 | current: page, 78 | pageSize, 79 | total, 80 | showSizeChanger: true, 81 | showTotal 82 | }) 83 | ) 84 | }, 85 | reducers: { 86 | startPolling(state) { 87 | return { ...state, isSilentLoading: false } 88 | }, 89 | fetchSuccess(state, payload) { 90 | return { 91 | ...state, 92 | list: payload.list, 93 | pagination: { 94 | ...state.pagination, 95 | total: payload.total 96 | }, 97 | isSilentLoading: true 98 | } 99 | }, 100 | fetchError(state, payload) { 101 | return { 102 | ...state, 103 | list: [], 104 | pagination: { 105 | ...state.pagination, 106 | total: 0, 107 | page: 1 108 | } 109 | } 110 | } 111 | }, 112 | flows: { 113 | fetch(flow$, action$, state$) { 114 | const service: ApiServiceFunc = getService('api') 115 | const stopPolling$ = action$.ofType( 116 | 'user/stopPolling', 117 | 'user/fetchError' 118 | ) 119 | const params$: Observable = state$.pipe( 120 | map(({ user }: { user: UserState }) => { 121 | const { pagination, sort, query } = user 122 | return { 123 | q: `${query ? query + ' ' : ''}language:javascript`, 124 | language: 'javascript', 125 | page: pagination.page, 126 | per_page: pagination.pageSize, 127 | sort, 128 | order: Order.Desc 129 | } 130 | }), 131 | distinctUntilChanged(isEqual) 132 | ) 133 | 134 | return flow$.pipe( 135 | combineLatest(params$, (action, params) => params), 136 | switchMap(params => { 137 | const polling$ = timer(0, 15 * 1000).pipe( 138 | takeUntil(stopPolling$), 139 | switchMap(() => { 140 | const [success$, error$] = service('user/fetch', fetch(params), { 141 | level: NOTIFICATION_LEVEL.all 142 | }) 143 | return merge( 144 | success$.pipe( 145 | map(({ resp: { data } }) => ({ 146 | type: 'user/fetchSuccess', 147 | payload: { 148 | total: data.total_count, 149 | list: data.items 150 | } 151 | })) 152 | ), 153 | error$.pipe( 154 | mapTo({ 155 | type: 'user/fetchError', 156 | payload: {} 157 | }) 158 | ) 159 | ) 160 | }), 161 | startWith({ 162 | type: 'user/startPolling' 163 | }) 164 | ) 165 | return polling$ 166 | }) 167 | ) 168 | } 169 | } 170 | } 171 | 172 | export default user 173 | -------------------------------------------------------------------------------- /packages/core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reobservable/core", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.18.0", 9 | "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", 10 | "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", 11 | "dev": true, 12 | "requires": { 13 | "follow-redirects": "^1.3.0", 14 | "is-buffer": "^1.1.5" 15 | } 16 | }, 17 | "debug": { 18 | "version": "3.1.0", 19 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 20 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 21 | "dev": true, 22 | "requires": { 23 | "ms": "2.0.0" 24 | } 25 | }, 26 | "follow-redirects": { 27 | "version": "1.5.8", 28 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.8.tgz", 29 | "integrity": "sha512-sy1mXPmv7kLAMKW/8XofG7o9T+6gAjzdZK4AJF6ryqQYUa/hnzgiypoeUecZ53x7XiqKNEpNqLtS97MshW2nxg==", 30 | "dev": true, 31 | "requires": { 32 | "debug": "=3.1.0" 33 | } 34 | }, 35 | "invariant": { 36 | "version": "2.2.4", 37 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", 38 | "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", 39 | "requires": { 40 | "loose-envify": "^1.0.0" 41 | } 42 | }, 43 | "is-buffer": { 44 | "version": "1.1.6", 45 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 46 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", 47 | "dev": true 48 | }, 49 | "js-tokens": { 50 | "version": "4.0.0", 51 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 52 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 53 | }, 54 | "lodash.clonedeep": { 55 | "version": "4.5.0", 56 | "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", 57 | "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" 58 | }, 59 | "lodash.merge": { 60 | "version": "4.6.1", 61 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", 62 | "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" 63 | }, 64 | "lodash.mergewith": { 65 | "version": "4.6.1", 66 | "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", 67 | "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" 68 | }, 69 | "loose-envify": { 70 | "version": "1.4.0", 71 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 72 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 73 | "requires": { 74 | "js-tokens": "^3.0.0 || ^4.0.0" 75 | } 76 | }, 77 | "ms": { 78 | "version": "2.0.0", 79 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 80 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 81 | "dev": true 82 | }, 83 | "redux": { 84 | "version": "4.0.0", 85 | "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz", 86 | "integrity": "sha512-NnnHF0h0WVE/hXyrB6OlX67LYRuaf/rJcbWvnHHEPCF/Xa/AZpwhs/20WyqzQae5x4SD2F9nPObgBh2rxAgLiA==", 87 | "requires": { 88 | "loose-envify": "^1.1.0", 89 | "symbol-observable": "^1.2.0" 90 | } 91 | }, 92 | "redux-devtools-extension": { 93 | "version": "2.13.5", 94 | "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.5.tgz", 95 | "integrity": "sha512-QQ9BRy77oURHMdGys9rfQcCQDzXZ1T4oW+eUyE5Cg7DNVau69HJzc4YNDMOmpi0Dzpi1zOQgQ2rUpgJta4Lfqg==" 96 | }, 97 | "redux-observable": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/redux-observable/-/redux-observable-1.0.0.tgz", 100 | "integrity": "sha512-6bXnpqWTBeLaLQjXHyN1giXq4nLxCmv+SUkdmiwBgvmVxvDbdmydvL1Z7DGo0WItyzI/kqXQKiucUuTxnrPRkA==" 101 | }, 102 | "rxjs": { 103 | "version": "6.3.2", 104 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.2.tgz", 105 | "integrity": "sha512-hV7criqbR0pe7EeL3O66UYVg92IR0XsA97+9y+BWTePK9SKmEI5Qd3Zj6uPnGkNzXsBywBQWTvujPl+1Kn9Zjw==", 106 | "requires": { 107 | "tslib": "^1.9.0" 108 | } 109 | }, 110 | "symbol-observable": { 111 | "version": "1.2.0", 112 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", 113 | "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" 114 | }, 115 | "tslib": { 116 | "version": "1.9.3", 117 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 118 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/github/src/models/repo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * repo model 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-07-17 15:11:14 5 | */ 6 | import * as moment from 'moment' 7 | import { isEqual } from 'lodash/fp' 8 | import { timer, merge, Observable } from 'rxjs' 9 | import { 10 | map, 11 | combineLatest, 12 | distinctUntilChanged, 13 | switchMap, 14 | startWith, 15 | takeUntil, 16 | mapTo 17 | } from 'rxjs/operators' 18 | import { createSelector } from 'reselect' 19 | import { PaginationProps } from 'antd/lib/pagination' 20 | import { Model, NOTIFICATION_LEVEL, getService } from '@reobservable/core' 21 | import { fetch } from '@services/repo' 22 | import { Order, Pagination } from '@models/common' 23 | import { ApiServiceFunc } from '@services/types' 24 | 25 | export interface Repo { 26 | id: number 27 | name: string 28 | full_name: string 29 | html_url: string 30 | url: string 31 | homepage: string 32 | updated_at: string 33 | } 34 | 35 | export interface RepoState { 36 | list: Repo[] 37 | pagination: Pagination 38 | sort: string 39 | isSilentLoading: boolean 40 | query: string 41 | } 42 | 43 | export interface SearchParam { 44 | q: string 45 | page: number 46 | per_page: number 47 | sort: string 48 | order: Order 49 | } 50 | 51 | export interface SearchResp { 52 | total_count: number 53 | items: Repo[] 54 | } 55 | 56 | const showTotal = total => `共 ${total} 条` 57 | 58 | const repo: Model = { 59 | name: 'repo', 60 | state: { 61 | list: [], 62 | pagination: { 63 | total: 0, 64 | page: 1, 65 | pageSize: 10 66 | }, 67 | sort: 'stars', 68 | isSilentLoading: false, 69 | query: '' 70 | }, 71 | reducers: { 72 | startPolling(state) { 73 | return { ...state, isSilentLoading: false } 74 | }, 75 | fetchSuccess(state, payload) { 76 | return { 77 | ...state, 78 | list: payload.list, 79 | pagination: { 80 | ...state.pagination, 81 | total: payload.total 82 | }, 83 | isSilentLoading: true 84 | } 85 | }, 86 | fetchError(state, payload) { 87 | return { 88 | ...state, 89 | list: [], 90 | pagination: { 91 | ...state.pagination, 92 | total: 0, 93 | page: 1 94 | } 95 | } 96 | } 97 | }, 98 | selectors: { 99 | pagination: createSelector< 100 | { repo: RepoState }, 101 | Pagination, 102 | PaginationProps 103 | >( 104 | ({ repo }) => repo.pagination, 105 | ({ total, page, pageSize }) => ({ 106 | current: page, 107 | pageSize, 108 | total, 109 | showSizeChanger: true, 110 | showTotal 111 | }) 112 | ) 113 | }, 114 | flows: { 115 | fetch(flow$, action$, state$) { 116 | const service: ApiServiceFunc = getService('api') 117 | const stopPolling$ = action$.ofType( 118 | 'repo/stopPolling', 119 | 'repo/fetchError' 120 | ) 121 | const params$: Observable = state$.pipe( 122 | map(state => { 123 | const { pagination, sort, query } = state.repo 124 | return { 125 | q: `${query ? query + '+' : ''}language:javascript`, 126 | page: pagination.page, 127 | per_page: pagination.pageSize, 128 | sort, 129 | order: Order.Desc 130 | } 131 | }), 132 | distinctUntilChanged(isEqual) 133 | ) 134 | 135 | return flow$.pipe( 136 | combineLatest(params$, (action, params) => params), 137 | switchMap(params => { 138 | const polling$ = timer(0, 30 * 60 * 1000).pipe( 139 | takeUntil(stopPolling$), 140 | switchMap(() => { 141 | const [success$, error$] = service('repo/fetch', fetch(params), { 142 | level: NOTIFICATION_LEVEL.silent 143 | }) 144 | return merge( 145 | success$.pipe( 146 | map(({ resp: { data } }) => ({ 147 | type: 'rep/fetchSuccess', 148 | payload: { 149 | total: data.total_count, 150 | list: data.items.map((item: Repo) => ({ 151 | ...item, 152 | displayUpdated: moment(item.updated_at).format( 153 | 'YYYY-MM-DD HH:mm' 154 | ) 155 | })) 156 | } 157 | })) 158 | ), 159 | error$.pipe( 160 | mapTo({ 161 | type: 'repo/fetchError', 162 | payload: {} 163 | }) 164 | ) 165 | ) 166 | }), 167 | startWith({ 168 | type: 'repo/startPolling', 169 | payload: {} 170 | }) 171 | ) 172 | return polling$ 173 | }) 174 | ) 175 | } 176 | } 177 | } 178 | 179 | export default repo 180 | -------------------------------------------------------------------------------- /packages/core/es/operators/createService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fromService creator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:26:55 5 | */ 6 | import { from, of, Observable } from 'rxjs'; 7 | import { catchError, map, filter, tap, shareReplay, retryWhen, scan, delay } from 'rxjs/operators'; 8 | import { LEVEL } from '../constants/notification'; 9 | import { SERVICE_ERROR_SET_ACTION, SERVICE_LOADING_END_ACTION, SERVICE_LOADING_START_ACTION } from '../constants/actionTypes'; 10 | import { noop } from '../utils/function'; 11 | export function partition(source$, predicate) { 12 | return [ 13 | source$.pipe(filter(predicate)), 14 | source$.pipe(filter(function (v, i) { return !predicate(v, i); })), 15 | ]; 16 | } 17 | /** 18 | * 创建 fromService creator 19 | * @param {Notification} notification 20 | */ 21 | export default function createFromService(notification, serviceConfig, store) { 22 | /** 23 | * fromServiceOperators 24 | * @param service 25 | * @param options 26 | */ 27 | return function service(serviceName, service, options) { 28 | if (options === void 0) { options = { 29 | level: LEVEL.silent, 30 | retry: 0, 31 | retryDelay: 0, 32 | loadingDelay: 0 33 | }; } 34 | var _a = options.templates || serviceConfig.templates || {}, _b = _a.success, success = _b === void 0 ? noop : _b, _c = _a.error, error = _c === void 0 ? noop : _c; 35 | var level = options.level, _d = options.retry, retry = _d === void 0 ? 0 : _d, _e = options.retryDelay, retryDelay = _e === void 0 ? 0 : _e, _f = options.loadingDelay, loadingDelay = _f === void 0 ? 0 : _f; 36 | var timer = null; 37 | if (loadingDelay > 0) { 38 | timer = setTimeout(function () { return store.dispatch({ 39 | type: SERVICE_LOADING_START_ACTION, 40 | payload: { 41 | service: serviceName 42 | } 43 | }); }, loadingDelay); 44 | } 45 | else { 46 | store.dispatch({ 47 | type: SERVICE_LOADING_START_ACTION, 48 | payload: { 49 | service: serviceName 50 | } 51 | }); 52 | } 53 | var successNotificate = function (resp) { return level >= LEVEL.all && 54 | notification.success(success(resp)); }; 55 | var errorNotificate = function (err) { return level >= LEVEL.error && 56 | notification.error(error(err)); }; 57 | var service$ = (service instanceof Observable) 58 | ? service.pipe(retryWhen(function (error$) { return error$.pipe(scan(function (count, err) { 59 | if (count >= retry) { 60 | throw err; 61 | } 62 | else { 63 | return count + 1; 64 | } 65 | }, 0), delay(retryDelay)); })) 66 | : from(service); 67 | var response$ = (typeof serviceConfig.isSuccess) === 'function' 68 | ? service$.pipe(map(function (resp) { 69 | if (serviceConfig.isSuccess(resp)) { 70 | return { resp: resp, success: true }; 71 | } 72 | else { 73 | var error_1 = serviceConfig.errorSelector ? serviceConfig.errorSelector(resp) : 'error'; 74 | return { error: error_1 }; 75 | } 76 | })) 77 | : service$.pipe(map(function (resp) { 78 | return { resp: resp, success: true }; 79 | }), catchError(function (error) { 80 | return of({ 81 | error: serviceConfig.errorSelector ? serviceConfig.errorSelector(error) : error 82 | }); 83 | })); 84 | var _g = partition(response$.pipe(tap(function () { return timer && clearTimeout(timer); }), shareReplay(1)), (function (_a) { 85 | var success = _a.success; 86 | return success; 87 | })), success$ = _g[0], error$ = _g[1]; 88 | success$.subscribe({ 89 | next: function (_a) { 90 | var resp = _a.resp; 91 | successNotificate(resp); 92 | store.dispatch({ 93 | type: SERVICE_LOADING_END_ACTION, 94 | payload: { 95 | service: serviceName 96 | } 97 | }); 98 | } 99 | }); 100 | error$.subscribe({ 101 | next: (function (_a) { 102 | var error = _a.error; 103 | errorNotificate(error); 104 | store.dispatch({ 105 | type: SERVICE_ERROR_SET_ACTION, 106 | payload: { 107 | error: error, 108 | service: serviceName 109 | } 110 | }); 111 | }) 112 | }); 113 | return [ 114 | success$, 115 | error$ 116 | ]; 117 | }; 118 | } 119 | //# sourceMappingURL=createService.js.map -------------------------------------------------------------------------------- /packages/core/lib/operators/createService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * fromService creator 5 | * @author yoyoyohamapi 6 | * @ignore created 2018-08-13 14:26:55 7 | */ 8 | var rxjs_1 = require("rxjs"); 9 | var operators_1 = require("rxjs/operators"); 10 | var notification_1 = require("../constants/notification"); 11 | var actionTypes_1 = require("../constants/actionTypes"); 12 | var function_1 = require("../utils/function"); 13 | function partition(source$, predicate) { 14 | return [ 15 | source$.pipe(operators_1.filter(predicate)), 16 | source$.pipe(operators_1.filter(function (v, i) { return !predicate(v, i); })), 17 | ]; 18 | } 19 | exports.partition = partition; 20 | /** 21 | * 创建 fromService creator 22 | * @param {Notification} notification 23 | */ 24 | function createFromService(notification, serviceConfig, store) { 25 | /** 26 | * fromServiceOperators 27 | * @param service 28 | * @param options 29 | */ 30 | return function service(serviceName, service, options) { 31 | if (options === void 0) { options = { 32 | level: notification_1.LEVEL.silent, 33 | retry: 0, 34 | retryDelay: 0, 35 | loadingDelay: 0 36 | }; } 37 | var _a = options.templates || serviceConfig.templates || {}, _b = _a.success, success = _b === void 0 ? function_1.noop : _b, _c = _a.error, error = _c === void 0 ? function_1.noop : _c; 38 | var level = options.level, _d = options.retry, retry = _d === void 0 ? 0 : _d, _e = options.retryDelay, retryDelay = _e === void 0 ? 0 : _e, _f = options.loadingDelay, loadingDelay = _f === void 0 ? 0 : _f; 39 | var timer = null; 40 | if (loadingDelay > 0) { 41 | timer = setTimeout(function () { return store.dispatch({ 42 | type: actionTypes_1.SERVICE_LOADING_START_ACTION, 43 | payload: { 44 | service: serviceName 45 | } 46 | }); }, loadingDelay); 47 | } 48 | else { 49 | store.dispatch({ 50 | type: actionTypes_1.SERVICE_LOADING_START_ACTION, 51 | payload: { 52 | service: serviceName 53 | } 54 | }); 55 | } 56 | var successNotificate = function (resp) { return level >= notification_1.LEVEL.all && 57 | notification.success(success(resp)); }; 58 | var errorNotificate = function (err) { return level >= notification_1.LEVEL.error && 59 | notification.error(error(err)); }; 60 | var service$ = (service instanceof rxjs_1.Observable) 61 | ? service.pipe(operators_1.retryWhen(function (error$) { return error$.pipe(operators_1.scan(function (count, err) { 62 | if (count >= retry) { 63 | throw err; 64 | } 65 | else { 66 | return count + 1; 67 | } 68 | }, 0), operators_1.delay(retryDelay)); })) 69 | : rxjs_1.from(service); 70 | var response$ = (typeof serviceConfig.isSuccess) === 'function' 71 | ? service$.pipe(operators_1.map(function (resp) { 72 | if (serviceConfig.isSuccess(resp)) { 73 | return { resp: resp, success: true }; 74 | } 75 | else { 76 | var error_1 = serviceConfig.errorSelector ? serviceConfig.errorSelector(resp) : 'error'; 77 | return { error: error_1 }; 78 | } 79 | })) 80 | : service$.pipe(operators_1.map(function (resp) { 81 | return { resp: resp, success: true }; 82 | }), operators_1.catchError(function (error) { 83 | return rxjs_1.of({ 84 | error: serviceConfig.errorSelector ? serviceConfig.errorSelector(error) : error 85 | }); 86 | })); 87 | var _g = partition(response$.pipe(operators_1.tap(function () { return timer && clearTimeout(timer); }), operators_1.shareReplay(1)), (function (_a) { 88 | var success = _a.success; 89 | return success; 90 | })), success$ = _g[0], error$ = _g[1]; 91 | success$.subscribe({ 92 | next: function (_a) { 93 | var resp = _a.resp; 94 | successNotificate(resp); 95 | store.dispatch({ 96 | type: actionTypes_1.SERVICE_LOADING_END_ACTION, 97 | payload: { 98 | service: serviceName 99 | } 100 | }); 101 | } 102 | }); 103 | error$.subscribe({ 104 | next: (function (_a) { 105 | var error = _a.error; 106 | errorNotificate(error); 107 | store.dispatch({ 108 | type: actionTypes_1.SERVICE_ERROR_SET_ACTION, 109 | payload: { 110 | error: error, 111 | service: serviceName 112 | } 113 | }); 114 | }) 115 | }); 116 | return [ 117 | success$, 118 | error$ 119 | ]; 120 | }; 121 | } 122 | exports.default = createFromService; 123 | //# sourceMappingURL=createService.js.map -------------------------------------------------------------------------------- /packages/core/lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /docs/basics/custom-service.md: -------------------------------------------------------------------------------- 1 | # Custom Service 2 | 3 | In a modern web app, we should considering following parts when we dealing with the communication between the browser and the server: 4 | 5 | - How to **konw** if a service failed or not. 6 | - How to **konw** if a service is running or not. 7 | - How to **notify** the user of the service result. 8 | 9 | ## Define service & notification 10 | 11 | **Service** is an important concept in reobservable. You can custom a service through `init` api: 12 | 13 | ```ts 14 | import { AxiosResponse, AxiosError } from 'axios' 15 | import { ServiceConfig, ServiceFunc, init } from '@reobservable/core' 16 | import { message } from 'antd' 17 | 18 | interface IApiResp { 19 | data: { 20 | code: number, 21 | message: string, 22 | body: any 23 | } 24 | } 25 | 26 | interface ApiService extends ServiceConfig, AxiosError> {} 27 | 28 | const apiService: ApiService = { 29 | isSuccess: (resp) => resp.data.code === 0, 30 | errorSelector: (resp) => resp.data.message, 31 | templates: { 32 | success: (resp) => 'ok!', 33 | error: (error) => `Error: ${error}` 34 | } 35 | } 36 | 37 | const store = init({ 38 | models: { 39 | // modesl 40 | }, 41 | services: { 42 | api: apiService 43 | }, 44 | notification: message 45 | }) 46 | ``` 47 | 48 | In the above example, we: 49 | - Use Axios as our **service lib**. 50 | - **Declare how to know a service failed or not by override `isSuccess` property**: If code inside the response is equal to `0`, then reobservble know that service is success. 51 | - **Declare how to select error message when service failed by override `errorSelector` property**. We select `message` property from the response as our error message. 52 | - **Define a notification template by override `templates` property**. It determines what message should be notified to user in reobservbale. 53 | 54 | We also defined the **notification** in reobservable with [**Message**](https://ant.design/components/message/) in ant design. It implements the methods defined in the `Notification` interface: 55 | 56 | ```ts 57 | interface Notification { 58 | info?: notificate, 59 | success?: notificate, 60 | warn?: notificate, 61 | error?: notificate 62 | } 63 | ``` 64 | 65 | Then, when the service succeed, we will see: 66 | 67 | ![](../public/success-notify.png) 68 | 69 | and when the service failed with the following response: 70 | 71 | ```ts 72 | { 73 | code: -1, 74 | message: 'Invalid params' 75 | } 76 | ``` 77 | 78 | we will see: 79 | 80 | ![](../public/error-notify.png) 81 | 82 | > We can also override **nothing** when we custom a service. If we did this, then the service library we used will be in charge of how a service failed or not, and the 83 | 84 | ## Use service 85 | 86 | Then, we can call service function in a flow's dependencies: 87 | 88 | ```ts 89 | import { Model, NOTIFICATION_LEVEL } from '@reobservable/core' 90 | 91 | const model: Model = { 92 | name: 'user', 93 | state: { 94 | list: [] 95 | }, 96 | reducers: { 97 | fetchSuccess(state, payload) { 98 | return { ...state, list: payload.list } 99 | }, 100 | fetchError(state, payload) { 101 | return { ...state, list: [] } 102 | } 103 | }, 104 | flows: { 105 | fetch(flow$, action$, payload$, dependencies) { 106 | const { api } = dependencies.services 107 | 108 | return flow$.pipe( 109 | switchMap(action => { 110 | const [success$, error$] = api( 111 | 'user/fetch', 112 | service.fetchUser(params), 113 | { 114 | level: NOTIFICATION_LEVEL.error, 115 | templates: { 116 | error: (error) => 'Fetch users error!' 117 | } 118 | } 119 | ) 120 | return merge( 121 | success$.pipe(map(resp => ({type: 'user/fetchSuccess', payload: resp}))), 122 | error$.pipe(mapTo({type: 'user/fetchError'})) 123 | ) 124 | }) 125 | ) 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | The signature of the service function is: 132 | 133 | ```ts 134 | export type ServiceFunc = 135 | (serviceName: string, service: Promise | Observable, options?: ServiceOptions) => 136 | [Observable>, Observable>] 137 | ``` 138 | 139 | It take 3 parameters: 140 | 141 | - `serviceName`: the service name, it helps store to know how to pick up the loading or error state of the service. 142 | - `service`: is a Promise or an Observable object that in charge of communicating with the server. 143 | - `options`: the service options. We will describe it next. 144 | 145 | then return two observable: `success$` and `error$`. 146 | 147 | ## Custom options 148 | 149 | In a service call, you can custom following service options: 150 | 151 | ```ts 152 | interface ServiceOptions { 153 | level: NotificationLevel, 154 | templates?: ServiceTemplates, 155 | retry?: number, 156 | retryDelay?: number 157 | } 158 | ``` 159 | 160 | - `level`: the service notification level. It determines the timing of a service result notification, three different level supported currently: 161 | - `NOTIFICATION_LEVEL.silent`: don't notify. 162 | - `NOTIFICATION_LEVEL.error`: notify user only when service. 163 | - `NOTIFICATION_LEVEL.all`: notify user whether service is success or not. 164 | - `templates`: the same as the `templates` in service defination, but only act on this service call. 165 | - `retry`: max count of a service should retry. Default is `0`. 166 | - `retryDelay`: Delay ms of a service retry. Default is `0`. 167 | 168 | ## Loading & error 169 | 170 | We can pick up the loading and error state by service name: 171 | 172 | ```ts 173 | const mapStateToProps = (state: IState) => ({ 174 | isUserFetching: state.loading.services['user/fetch'], 175 | userFetchError: state.error.services['user/fetch'] 176 | }) 177 | ``` 178 | 179 | -------------------------------------------------------------------------------- /packages/core/src/operators/createService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * fromService creator 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-13 14:26:55 5 | */ 6 | import { from, of, Observable } from 'rxjs' 7 | import { 8 | catchError, 9 | map, 10 | filter, 11 | tap, 12 | shareReplay, 13 | retryWhen, 14 | scan, 15 | delay 16 | } from 'rxjs/operators' 17 | import { Store } from 'redux' 18 | import { Notification, NotificationLevel } from '../types/Notification' 19 | import { LEVEL } from '../constants/notification' 20 | import { 21 | SERVICE_ERROR_SET_ACTION, 22 | SERVICE_LOADING_END_ACTION, 23 | SERVICE_LOADING_START_ACTION 24 | } from '../constants/actionTypes' 25 | import { noop } from '../utils/function' 26 | 27 | interface ServiceTemplates { 28 | success: (resp: T) => string 29 | error: (error: E) => string 30 | } 31 | 32 | export interface ServiceConfig { 33 | templates?: ServiceTemplates 34 | isSuccess?: (resp: T) => boolean 35 | errorSelector?: (error: any) => any 36 | } 37 | 38 | interface ServiceOptions { 39 | /** 等级 */ 40 | level: NotificationLevel 41 | /** 消息模板 */ 42 | templates?: ServiceTemplates 43 | /** 重试次数 */ 44 | retry?: number 45 | /** 重试延迟 */ 46 | retryDelay?: number 47 | /** 加载延迟 */ 48 | loadingDelay?: number 49 | } 50 | 51 | interface Result { 52 | resp?: T | undefined 53 | error?: E | undefined 54 | success?: boolean | undefined 55 | } 56 | 57 | export type ServiceFunc = ( 58 | serviceName: string, 59 | service: Promise | Observable, 60 | options?: ServiceOptions 61 | ) => [Observable>, Observable>] 62 | 63 | export function partition( 64 | source$: Observable, 65 | predicate: (value: T, index: number) => boolean 66 | ) { 67 | return [ 68 | source$.pipe(filter(predicate)), 69 | source$.pipe(filter((v, i) => !predicate(v, i))) 70 | ] 71 | } 72 | 73 | /** 74 | * 创建 fromService creator 75 | * @param {Notification} notification 76 | */ 77 | export default function createFromService( 78 | notification: Notification, 79 | serviceConfig: ServiceConfig, 80 | store: Store 81 | ): ServiceFunc { 82 | /** 83 | * fromServiceOperators 84 | * @param service 85 | * @param options 86 | */ 87 | return function service( 88 | serviceName, 89 | service, 90 | options = { 91 | level: LEVEL.silent, 92 | retry: 0, 93 | retryDelay: 0, 94 | loadingDelay: 0 95 | } 96 | ) { 97 | const { success = noop, error = noop } = 98 | options.templates || serviceConfig.templates || {} 99 | const { level, retry = 0, retryDelay = 0, loadingDelay = 0 } = options 100 | 101 | let timer = null 102 | 103 | if (loadingDelay > 0) { 104 | timer = setTimeout( 105 | () => 106 | store.dispatch({ 107 | type: SERVICE_LOADING_START_ACTION, 108 | payload: { 109 | service: serviceName 110 | } 111 | }), 112 | loadingDelay 113 | ) 114 | } else { 115 | store.dispatch({ 116 | type: SERVICE_LOADING_START_ACTION, 117 | payload: { 118 | service: serviceName 119 | } 120 | }) 121 | } 122 | 123 | const successNotificate = (resp: T) => 124 | level >= LEVEL.all && notification.success(success(resp)) 125 | const errorNotificate = (err: E) => 126 | level >= LEVEL.error && notification.error(error(err)) 127 | 128 | const service$ = 129 | service instanceof Observable 130 | ? service.pipe( 131 | retryWhen(error$ => 132 | error$.pipe( 133 | scan((count, err) => { 134 | if (count >= retry) { 135 | throw err 136 | } else { 137 | return count + 1 138 | } 139 | }, 0), 140 | delay(retryDelay) 141 | ) 142 | ) 143 | ) 144 | : from(service) 145 | const response$ = 146 | typeof serviceConfig.isSuccess === 'function' 147 | ? service$.pipe( 148 | map(resp => { 149 | if (serviceConfig.isSuccess(resp)) { 150 | return { resp, success: true } 151 | } else { 152 | const error: E = serviceConfig.errorSelector 153 | ? serviceConfig.errorSelector(resp) 154 | : 'error' 155 | return { error } 156 | } 157 | }) 158 | ) 159 | : service$.pipe( 160 | map(resp => { 161 | return { resp, success: true } 162 | }), 163 | catchError((error: E) => { 164 | return of({ 165 | error: serviceConfig.errorSelector 166 | ? serviceConfig.errorSelector(error) 167 | : error 168 | }) 169 | }) 170 | ) 171 | 172 | const [success$, error$]: Observable>[] = partition< 173 | Result 174 | >( 175 | response$.pipe( 176 | tap(() => timer && clearTimeout(timer)), 177 | shareReplay(1) 178 | ), 179 | ({ success }) => success 180 | ) 181 | 182 | success$.subscribe({ 183 | next: ({ resp }) => { 184 | successNotificate(resp) 185 | store.dispatch({ 186 | type: SERVICE_LOADING_END_ACTION, 187 | payload: { 188 | service: serviceName 189 | } 190 | }) 191 | } 192 | }) 193 | 194 | error$.subscribe({ 195 | next: ({ error }) => { 196 | errorNotificate(error) 197 | store.dispatch({ 198 | type: SERVICE_ERROR_SET_ACTION, 199 | payload: { 200 | error, 201 | service: serviceName 202 | } 203 | }) 204 | } 205 | }) 206 | 207 | return [success$, error$] 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/core/es/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;AACA;;;;GAIG;AACH,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,YAAY,EAAsC,MAAM,kBAAkB,CAAA;AACjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,OAAO,CAAA;AACjG,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,SAAS,MAAM,WAAW,CAAA;AAEtC,OAAO,cAAc,MAAM,oBAAoB,CAAA;AAC/C,OAAO,YAAY,MAAM,kBAAkB,CAAA;AAE3C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAE5D,OAAO,GAAG,MAAM,iBAAiB,CAAA;AACjC,OAAO,KAAK,MAAM,mBAAmB,CAAA;AACrC,OAAO,aAA6C,MAAM,2BAA2B,CAAA;AACrF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AACpG,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACvC,OAAO,EAAE,KAAK,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAEtE,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAsBrC,IAAM,SAAS,GAA8C,EAAE,CAAA;AAE/D,IAAM,QAAQ,GAEV,EAAE,CAAA;AAEN,IAAI,YAAY,GAAiB;IAC/B,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;CACX,CAAA;AAED,IAAM,SAAS,GAAG,UAAC,QAAQ,EAAE,QAAQ;IACnC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;QAC3B,OAAO,QAAQ,CAAA;KAChB;AACH,CAAC,CAAA;AAED,IAAM,gBAAgB,GAAG,mBAAmB,CAAC;IAC3C,eAAe,iBAAA;CAChB,CAAC,CAAA;AAEF,MAAM,CAAC,IAAM,IAAI,GAAa,UAAC,MAAM;IAC3B,IAAA,kBAAW,EAAX,gCAAW,EAAE,iBAAU,EAAV,+BAAU,CAAW;IAC1C,YAAY,gBACP,YAAY,EACZ,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAC/B,CAAA;IAED,IAAM,KAAK,GAAG,EAAE,CAAA;IAEhB,IAAM,QAAQ,GAAG;QACf,OAAO,EAAE,cAAc;QACvB,KAAK,EAAE,YAAY;KACpB,CAAA;IAED;;;OAGG;IACH,IAAM,aAAa,GAAG,UAAC,KAAiB;QACtC,SAAS,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,WAAS,KAAK,CAAC,IAAI,sBAAmB,CAAC,CAAA;QACvF,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,UAAS,KAAmB,EAAE,MAAc;YAAnC,sBAAA,EAAA,QAAQ,KAAK,CAAC,KAAK;YACzC,IAAA,kBAAI,CAAW;YACvB,IAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;YAClC,QAAQ,IAAI,EAAE;gBACZ,KAAQ,KAAK,CAAC,IAAI,YAAS,CAAC,CAAC;oBAC3B,oBAAY,KAAK,EAAK,OAAO,EAAE;iBAChC;gBACD,KAAQ,KAAK,CAAC,IAAI,WAAQ,CAAC,CAAC;oBAC1B,OAAO,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;iBACvD;gBACD,KAAQ,KAAK,CAAC,IAAI,WAAQ,CAAC,CAAC;oBAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;wBAC1D,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,UAAC,QAAQ,EAAE,GAAG;4BAC7C,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE;gCACpC,QAAQ,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;6BAC5C;iCAAM;gCACL,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;6BAC3B;4BACD,OAAO,QAAQ,CAAA;wBACjB,CAAC,EAAE,EAAE,CAAC,CAAA;qBACP;yBAAM;wBACL,oBAAY,KAAK,CAAC,KAAK,EAAE;qBAC1B;iBACF;gBACD,OAAO,CAAC,CAAC;oBACD,IAAA,oBAA0C,EAAzC,iBAAS,EAAE,mBAAW,CAAmB;oBAEhD,IAAI,SAAS,KAAK,KAAK,CAAC,IAAI,IAAI,OAAO,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,UAAU,EAAE;wBACjF,OAAO,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;qBACnD;oBAED,IAAI,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,UAAU,EAAE;wBAC9C,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;qBAC5C;oBAED,OAAO,KAAK,CAAA;iBACd;aACD;QACH,CAAC,CAAA;IACH,CAAC,CAAA;IAED;;;;OAIG;IACH,IAAM,UAAU,GAAG,UAAC,UAAkB,EAAE,IAAU;QAChD,OAAO,SAAS,IAAI,CAAI,OAAkC,EAAE,MAA0B,EAAE,YAA0B;YAChH,IAAM,KAAK,GAAG,IAAI,CAChB,OAAO,CAAC,IAAI,CACV,MAAM,CAAiB,UAAU,CAAC,EAClC,GAAG,CAAC,UAAA,MAAM,IAAI,OAAA,cAAK,MAAM,IAAE,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,IAAE,EAA1C,CAA0C,CAAC,CAC1D,EACD,OAAO,EACP,MAAM,EACN,YAAY,CACb,CAAA;YAED,IAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAChC,MAAM,CAAC,UAAU,CAAC,EAClB,KAAK,CAAC;gBACJ,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;aAC9B,CAAC,CACH,CAAA;YAED,OAAO,KAAK,CACV,aAAa,EACb,KAAK,CAAC,IAAI,CACR,QAAQ,CAAC,UAAA,MAAM;gBACb,IAAI,CAAC,MAAM,EAAE;oBACX,OAAO,UAAU,CAAC,aAAa,CAAC,CAAA;iBACjC;gBACD,IAAI,MAAM,CAAC,kBAAkB,CAAC,EAAE;oBAC9B,IAAM,UAAU,GAAG;wBACjB,IAAI,EAAE,kBAAkB;wBACxB,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;qBAC9B,CAAA;oBACD,OAAO,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;iBAC9B;qBAAM;oBACL,OAAO,EAAE,CAAC,MAAM,CAAC,CAAA;iBAClB;YACH,CAAC,CAAC,EACF,UAAU,CAAC,UAAC,KAAK;gBACf,OAAO,CAAC,KAAK,CAAC,wBAAsB,UAAU,YAAS,EAAE,KAAK,CAAC,CAAA;gBAC/D,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,gBAAgB;oBACtB,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE;iBAC5C,CAAC,CAAA;YACJ,CAAC,CAAC,CACH,CACF,CAAA;QACH,CAAC,CAAA;IACH,CAAC,CAAA;IAED;;;OAGG;IACH,IAAM,WAAW,GAAG,UAAC,KAAiB;QACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAA,QAAQ;YACvC,IAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;YAClC,IAAM,UAAU,GAAM,KAAK,CAAC,IAAI,SAAI,QAAU,CAAA;YAC9C,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAA;QAC1C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,UAAA,GAAG;QAC7B,IAAM,KAAK,cACT,QAAQ,EAAE,EAAE,EACZ,SAAS,EAAE,EAAE,EACb,KAAK,EAAE,EAAE,EACT,KAAK,EAAE,EAAE,IACN,MAAM,CAAC,GAAG,CAAC,CACf,CAAA;QACD,aAAa,CAAC,KAAK,CAAC,CAAA;QACpB,WAAW,CAAC,KAAK,CAAC,CAAA;QAClB,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,SAAS,CAAA;IACzC,CAAC,CAAC,CAAA;IAEF,WAAW;IACX,IAAM,cAAc,GAAG,oBAAoB,CAAC;QAC1C,YAAY,EAAE;YACZ,GAAG,KAAA;YACH,KAAK,OAAA;YACL,QAAQ,UAAA;SACT;KACF,CAAC,CAAA;IAEF,IAAM,eAAe,GAAG,KAAK,CAAC,UAAU;QACtC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC;QACzE,CAAC,CAAC,EAAE,CAAA;IAEN,IAAM,UAAU,GAAG,eAAe,gBAChC,cAAc,SAAK,eAAe,EACnC,CAAA;IAED,IAAI,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC3C,IAAI,KAAK,CAAC,WAAW,EAAE;QACrB,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;KACvD;IAED,IAAM,KAAK,GAAG,WAAW,CACvB,WAAW,EACX,gBAAgB,CAAC,UAAU,CAAC,CAC7B,CAAA;IAED,cAAc;IACd,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAA,GAAG;QAC5C,IAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;QACpC,QAAQ,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,cAAc,CAAC,GAAG,CAAC,YAAY,eAAI,KAAK,EAAE,CAAA;IAE1C,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,SAAS,CAAC,KAAK,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAY;IACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE;QACnB,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;KAC1D;AACH,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAA"} -------------------------------------------------------------------------------- /packages/core/test/model.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * model test 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-14 19:12:12 5 | */ 6 | import { Store } from 'redux' 7 | import { mapTo } from 'rxjs/operators' 8 | import { expect } from 'chai' 9 | import * as sinon from 'sinon' 10 | import { init, getSelectors } from '../src' 11 | import Model from '../src/types/Model' 12 | 13 | interface User { 14 | id: number 15 | name: string 16 | age: number 17 | } 18 | 19 | interface Post { 20 | id: number 21 | title: string 22 | content: string 23 | } 24 | 25 | interface Pagination { 26 | page: number 27 | pageSize: number 28 | total: number 29 | } 30 | 31 | interface UserState { 32 | list: User[] 33 | pagination: Pagination 34 | current: number 35 | editingPost: number 36 | } 37 | 38 | interface PostState { 39 | list: Post[] 40 | pagination: Pagination 41 | current: string 42 | } 43 | 44 | const userInitialState = { 45 | list: [], 46 | pagination: { 47 | page: 1, 48 | pageSize: 10, 49 | total: 0 50 | }, 51 | current: null, 52 | editingPost: null 53 | } 54 | 55 | const user: Model = { 56 | name: 'user', 57 | state: userInitialState, 58 | reducers: { 59 | fetchSuccess(state, payload) { 60 | const { list, total } = payload 61 | return { ...state, list, pagination: { ...state.pagination, total } } 62 | }, 63 | 'post/create': function(state, payload) { 64 | const { id } = payload 65 | return { ...state, editingPost: id } 66 | }, 67 | init(state, payload) { 68 | return state 69 | } 70 | }, 71 | selectors: { 72 | youngsters: state => { 73 | const { list } = state.user 74 | return list.filter(({ age }) => age < 30) 75 | } 76 | }, 77 | flows: { 78 | fetch(flow$) { 79 | return flow$.pipe( 80 | mapTo({ 81 | type: 'user/fetchSuccess', 82 | payload: { 83 | list: [ 84 | { id: 1, name: 'Messi', age: 31 }, 85 | { id: 2, name: 'Neymar', age: 26 }, 86 | { id: 3, name: 'Ronaldo', age: 33 } 87 | ], 88 | total: 20 89 | } 90 | }) 91 | ) 92 | } 93 | } 94 | } 95 | 96 | const post: Model = { 97 | name: 'post', 98 | state: { 99 | list: [], 100 | pagination: { 101 | page: 1, 102 | pageSize: 10, 103 | total: 0 104 | }, 105 | current: null 106 | }, 107 | reducers: { 108 | init(state, payload) { 109 | return state 110 | } 111 | }, 112 | flows: {} 113 | } 114 | 115 | describe('model', () => { 116 | let store: Store 117 | 118 | const initStore = () => { 119 | store = init({ 120 | models: { user, post } 121 | }) 122 | } 123 | 124 | describe('#state', () => { 125 | beforeEach(initStore) 126 | 127 | it('should have model state', () => { 128 | const state = store.getState() 129 | expect(state).to.deep.include({ 130 | user: { 131 | list: [], 132 | pagination: { 133 | page: 1, 134 | pageSize: 10, 135 | total: 0 136 | }, 137 | current: null, 138 | editingPost: null 139 | }, 140 | post: { 141 | list: [], 142 | pagination: { 143 | page: 1, 144 | pageSize: 10, 145 | total: 0 146 | }, 147 | current: null 148 | } 149 | }) 150 | }) 151 | 152 | it('should have loading state', () => { 153 | const state = store.getState() 154 | expect(state).to.deep.include({ 155 | loading: { flows: {}, services: {} } 156 | }) 157 | }) 158 | 159 | it('should have error state', () => { 160 | const state = store.getState() 161 | expect(state).to.deep.include({ 162 | error: { flows: {}, services: {} } 163 | }) 164 | }) 165 | }) 166 | 167 | describe('#reducers', () => { 168 | beforeEach(initStore) 169 | 170 | it('should have custom reducers', () => { 171 | store.dispatch({ 172 | type: 'user/fetch' 173 | }) 174 | const { list, pagination } = store.getState().user 175 | expect(list).to.have.deep.members([ 176 | { id: 1, name: 'Messi', age: 31 }, 177 | { id: 2, name: 'Neymar', age: 26 }, 178 | { id: 3, name: 'Ronaldo', age: 33 } 179 | ]) 180 | expect(pagination.total).to.equal(20) 181 | }) 182 | 183 | it('should match model reducer correctly', () => { 184 | const spyUserInit = sinon.spy(user.reducers, 'init') 185 | const spyPostInit = sinon.spy(post.reducers, 'init') 186 | store.dispatch({ 187 | type: 'user/init' 188 | }) 189 | expect(spyUserInit.calledOnce).to.be.true 190 | expect(spyPostInit.notCalled).to.be.true 191 | }) 192 | 193 | it('should support reactive the reducer of others', () => { 194 | store.dispatch({ 195 | type: 'post/create', 196 | payload: { 197 | id: 1 198 | } 199 | }) 200 | const { editingPost } = store.getState().user 201 | expect(editingPost).to.equal(1) 202 | }) 203 | 204 | it('should have change reducer', () => { 205 | store.dispatch({ 206 | type: 'user/change', 207 | payload: { 208 | current: 1 209 | } 210 | }) 211 | const { current } = store.getState().user 212 | expect(current).to.equal(1) 213 | }) 214 | 215 | it('should have patch reducer', () => { 216 | store.dispatch({ 217 | type: 'user/patch', 218 | payload: { 219 | pagination: { 220 | total: 10 221 | }, 222 | current: 4, 223 | list: [{ id: 1, name: 'Messi', age: 31 }] 224 | } 225 | }) 226 | const { user } = store.getState() 227 | expect(user).to.deep.include({ 228 | list: [{ id: 1, name: 'Messi', age: 31 }], 229 | pagination: { 230 | page: 1, 231 | pageSize: 10, 232 | total: 10 233 | }, 234 | current: 4 235 | }) 236 | }) 237 | 238 | it('should have reset reducer', () => { 239 | store.dispatch({ 240 | type: 'user/patch', 241 | payload: { 242 | pagination: { page: 2 }, 243 | current: 4 244 | } 245 | }) 246 | 247 | store.dispatch({ 248 | type: 'user/reset' 249 | }) 250 | 251 | const { user } = store.getState() 252 | expect(user).not.to.equal(userInitialState) 253 | expect(user).to.deep.equal(userInitialState) 254 | }) 255 | 256 | it('should support reset partial state', () => { 257 | store.dispatch({ 258 | type: 'user/patch', 259 | payload: { 260 | current: 4, 261 | pagination: { page: 2 }, 262 | editingPost: 4 263 | } 264 | }) 265 | 266 | store.dispatch({ 267 | type: 'user/reset', 268 | payload: { 269 | states: ['pagination', 'current'] 270 | } 271 | }) 272 | 273 | const { user } = store.getState() 274 | expect(user).not.to.equal(userInitialState) 275 | expect(user).to.deep.equal({ 276 | list: [], 277 | current: null, 278 | pagination: userInitialState.pagination, 279 | editingPost: 4 280 | }) 281 | }) 282 | }) 283 | 284 | describe('#selectors', () => { 285 | beforeEach(initStore) 286 | 287 | it('should create selectors', () => { 288 | store.dispatch({ 289 | type: 'user/fetch' 290 | }) 291 | const youngsters = getSelectors('user').youngsters(store.getState()) 292 | expect(youngsters).to.deep.equal([{ id: 2, name: 'Neymar', age: 26 }]) 293 | }) 294 | }) 295 | }) 296 | -------------------------------------------------------------------------------- /packages/core/es/index.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | /** 13 | * reobservable core 14 | * @author yoyoyohamapi 15 | * @ignore created 2018-08-03 10:24:23 16 | */ 17 | import { merge, of, throwError } from 'rxjs'; 18 | import { mergeMap, mapTo, catchError, map } from 'rxjs/operators'; 19 | import { createEpicMiddleware, ofType, combineEpics } from 'redux-observable'; 20 | import { composeWithDevTools } from 'redux-devtools-extension'; 21 | import { combineReducers, applyMiddleware, createStore } from 'redux'; 22 | import * as mergeWith from 'lodash.mergewith'; 23 | import * as cloneDeep from 'lodash.clonedeep'; 24 | import * as invariant from 'invariant'; 25 | import loadingReducer from './reducers/loading'; 26 | import errorReducer from './reducers/error'; 27 | import { getPayload, actionSanitizer } from './utils/action'; 28 | import end from './operators/end'; 29 | import endTo from './operators/endTo'; 30 | import createService from './operators/createService'; 31 | import { LOADING_START_ACTION, LOADING_END_ACTION, ERROR_SET_ACTION } from './constants/actionTypes'; 32 | import { FLOW_END_INDICATOR } from './constants/meta'; 33 | import { noop } from './utils/function'; 34 | import { LEVEL as NOTIFICATION_LEVEL } from './constants/notification'; 35 | import * as Symbols from './constants/symbols'; 36 | import { isNil } from './utils/logic'; 37 | var selectors = {}; 38 | var services = {}; 39 | var notification = { 40 | info: noop, 41 | success: noop, 42 | error: noop, 43 | warn: noop 44 | }; 45 | var patchWith = function (objValue, srcValue) { 46 | if (Array.isArray(srcValue)) { 47 | return srcValue; 48 | } 49 | }; 50 | var composeEnhancers = composeWithDevTools({ 51 | actionSanitizer: actionSanitizer 52 | }); 53 | export var init = function (config) { 54 | var _a = config.models, models = _a === void 0 ? {} : _a, _b = config.redux, redux = _b === void 0 ? {} : _b; 55 | notification = __assign({}, notification, (config.notification || {})); 56 | var epics = []; 57 | var reducers = { 58 | loading: loadingReducer, 59 | error: errorReducer 60 | }; 61 | /** 62 | * 创建 model 的 reducer 63 | * @param {*} mode 64 | */ 65 | var createReducer = function (model) { 66 | invariant(!reducers.hasOwnProperty(model.name), "model " + model.name + " has been defined"); 67 | reducers[model.name] = function (state, action) { 68 | if (state === void 0) { state = model.state; } 69 | var type = action.type; 70 | var payload = getPayload(action); 71 | switch (type) { 72 | case model.name + "/change": { 73 | return __assign({}, state, payload); 74 | } 75 | case model.name + "/patch": { 76 | return mergeWith(cloneDeep(state), payload, patchWith); 77 | } 78 | case model.name + "/reset": { 79 | if (Array.isArray(payload.states) && payload.states.length) { 80 | return Object.keys(state).reduce(function (newState, key) { 81 | if (payload.states.indexOf(key) > -1) { 82 | newState[key] = cloneDeep(model.state[key]); 83 | } 84 | else { 85 | newState[key] = state[key]; 86 | } 87 | return newState; 88 | }, {}); 89 | } 90 | else { 91 | return __assign({}, model.state); 92 | } 93 | } 94 | default: { 95 | var _a = type.split('/'), modelName = _a[0], reducerName = _a[1]; 96 | if (modelName === model.name && typeof model.reducers[reducerName] === 'function') { 97 | return model.reducers[reducerName](state, payload); 98 | } 99 | if (typeof model.reducers[type] === 'function') { 100 | return model.reducers[type](state, payload); 101 | } 102 | return state; 103 | } 104 | } 105 | }; 106 | }; 107 | /** 108 | * 创建 epic 109 | * @param actionType 110 | * @param flow 111 | */ 112 | var createEpic = function (actionType, flow) { 113 | return function epic(action$, state$, dependencies) { 114 | var flow$ = flow(action$.pipe(ofType(actionType), map(function (action) { return (__assign({}, action, { payload: getPayload(action) })); })), action$, state$, dependencies); 115 | var loadingStart$ = action$.pipe(ofType(actionType), mapTo({ 116 | type: LOADING_START_ACTION, 117 | payload: { flow: actionType } 118 | })); 119 | return merge(loadingStart$, flow$.pipe(mergeMap(function (action) { 120 | if (!action) { 121 | return throwError('inner error'); 122 | } 123 | if (action[FLOW_END_INDICATOR]) { 124 | var hideAction = { 125 | type: LOADING_END_ACTION, 126 | payload: { flow: actionType } 127 | }; 128 | return of(action, hideAction); 129 | } 130 | else { 131 | return of(action); 132 | } 133 | }), catchError(function (error) { 134 | console.error("@reobservable[flow " + actionType + " error]", error); 135 | return of({ 136 | type: ERROR_SET_ACTION, 137 | payload: { flow: actionType, error: error } 138 | }); 139 | }))); 140 | }; 141 | }; 142 | /** 143 | * 创建 flows 144 | * @param {*} model 145 | */ 146 | var createFlows = function (model) { 147 | Object.keys(model.flows).forEach(function (flowName) { 148 | var flow = model.flows[flowName]; 149 | var actionType = model.name + "/" + flowName; 150 | epics.push(createEpic(actionType, flow)); 151 | }); 152 | }; 153 | Object.keys(models).forEach(function (key) { 154 | var model = __assign({ reducers: {}, selectors: {}, flows: {}, state: {} }, models[key]); 155 | createReducer(model); 156 | createFlows(model); 157 | selectors[model.name] = model.selectors; 158 | }); 159 | // 配置 redux 160 | var epicMiddleware = createEpicMiddleware({ 161 | dependencies: { 162 | end: end, 163 | endTo: endTo, 164 | services: services 165 | } 166 | }); 167 | var extraMiddleware = redux.middleware 168 | ? Array.isArray(redux.middleware) ? redux.middleware : [redux.middleware] 169 | : []; 170 | var middleware = applyMiddleware.apply(void 0, [epicMiddleware].concat(extraMiddleware)); 171 | var rootReducer = combineReducers(reducers); 172 | if (redux.rootReducer) { 173 | rootReducer = redux.rootReducer(rootReducer, reducers); 174 | } 175 | var store = createStore(rootReducer, composeEnhancers(middleware)); 176 | // 配置 services 177 | Object.keys(config.services || {}).forEach(function (key) { 178 | var service = config.services[key]; 179 | services[key] = createService(notification, service, store); 180 | }); 181 | epicMiddleware.run(combineEpics.apply(void 0, epics)); 182 | return store; 183 | }; 184 | export function getSelectors(model) { 185 | return selectors[model]; 186 | } 187 | export function getService(service) { 188 | return services[service]; 189 | } 190 | export function notificate(type, message) { 191 | if (!isNil(message)) { 192 | return (notification[type] || notification.info)(message); 193 | } 194 | } 195 | export { NOTIFICATION_LEVEL, Symbols }; 196 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * reobservable core 3 | * @author yoyoyohamapi 4 | * @ignore created 2018-08-03 10:24:23 5 | */ 6 | import { merge, of, throwError } from 'rxjs' 7 | import { mergeMap, mapTo, catchError, map } from 'rxjs/operators' 8 | import { 9 | createEpicMiddleware, 10 | ofType, 11 | combineEpics, 12 | ActionsObservable, 13 | StateObservable 14 | } from 'redux-observable' 15 | import { composeWithDevTools } from 'redux-devtools-extension' 16 | import { 17 | combineReducers, 18 | applyMiddleware, 19 | createStore, 20 | Store, 21 | Middleware, 22 | Reducer 23 | } from 'redux' 24 | import * as mergeWith from 'lodash.mergewith' 25 | import * as cloneDeep from 'lodash.clonedeep' 26 | import * as invariant from 'invariant' 27 | 28 | import loadingReducer from './reducers/loading' 29 | import errorReducer from './reducers/error' 30 | import Model, { Flow, Selectors, Dependencies } from './types/Model' 31 | import { getPayload, actionSanitizer } from './utils/action' 32 | import { Action } from './types/action' 33 | import end from './operators/end' 34 | import endTo from './operators/endTo' 35 | import createService, { 36 | ServiceConfig, 37 | ServiceFunc 38 | } from './operators/createService' 39 | import { 40 | LOADING_START_ACTION, 41 | LOADING_END_ACTION, 42 | ERROR_SET_ACTION 43 | } from './constants/actionTypes' 44 | import { FLOW_END_INDICATOR } from './constants/meta' 45 | import { Notification } from './types/Notification' 46 | import { noop } from './utils/function' 47 | import { LEVEL as NOTIFICATION_LEVEL } from './constants/notification' 48 | 49 | import * as Symbols from './constants/symbols' 50 | import { isNil } from './utils/logic' 51 | 52 | interface Models { 53 | [modelName: string]: Model 54 | } 55 | 56 | interface ReduxConfig { 57 | middleware?: Middleware | [Middleware] 58 | rootReducer?: ( 59 | reducer: Reducer, 60 | reducers: { [key: string]: Reducer } 61 | ) => Reducer 62 | } 63 | 64 | interface Config { 65 | models?: Models 66 | redux?: ReduxConfig 67 | notification?: Notification 68 | services?: { 69 | [serviceName: string]: ServiceConfig; 70 | } 71 | } 72 | 73 | export type InitFunc = (config: Config) => Store 74 | 75 | const selectors: { [selectorsName: string]: Selectors } = {} 76 | 77 | const services: { 78 | [stringName: string]: ServiceFunc; 79 | } = {} 80 | 81 | let notification: Notification = { 82 | info: noop, 83 | success: noop, 84 | error: noop, 85 | warn: noop 86 | } 87 | 88 | const patchWith = (objValue, srcValue) => { 89 | if (Array.isArray(srcValue)) { 90 | return srcValue 91 | } 92 | } 93 | 94 | const composeEnhancers = composeWithDevTools({ 95 | actionSanitizer 96 | }) 97 | 98 | export const init: InitFunc = config => { 99 | const { models = {}, redux = {} } = config 100 | notification = { 101 | ...notification, 102 | ...(config.notification || {}) 103 | } 104 | 105 | const epics = [] 106 | 107 | const reducers = { 108 | loading: loadingReducer, 109 | error: errorReducer 110 | } 111 | 112 | /** 113 | * 创建 model 的 reducer 114 | * @param {*} mode 115 | */ 116 | const createReducer = (model: Model) => { 117 | invariant( 118 | !reducers.hasOwnProperty(model.name), 119 | `model ${model.name} has been defined` 120 | ) 121 | reducers[model.name] = function(state = model.state, action: Action) { 122 | const { type } = action 123 | const payload = getPayload(action) 124 | switch (type) { 125 | case `${model.name}/change`: { 126 | return { ...state, ...payload } 127 | } 128 | case `${model.name}/patch`: { 129 | return mergeWith(cloneDeep(state), payload, patchWith) 130 | } 131 | case `${model.name}/reset`: { 132 | if (Array.isArray(payload.states) && payload.states.length) { 133 | return Object.keys(state).reduce((newState, key) => { 134 | if (payload.states.indexOf(key) > -1) { 135 | newState[key] = cloneDeep(model.state[key]) 136 | } else { 137 | newState[key] = state[key] 138 | } 139 | return newState 140 | }, {}) 141 | } else { 142 | return { ...model.state } 143 | } 144 | } 145 | default: { 146 | const [modelName, reducerName] = type.split('/') 147 | 148 | if ( 149 | modelName === model.name && 150 | typeof model.reducers[reducerName] === 'function' 151 | ) { 152 | return model.reducers[reducerName](state, payload) 153 | } 154 | 155 | if (typeof model.reducers[type] === 'function') { 156 | return model.reducers[type](state, payload) 157 | } 158 | 159 | return state 160 | } 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * 创建 epic 167 | * @param actionType 168 | * @param flow 169 | */ 170 | const createEpic = (actionType: string, flow: Flow) => { 171 | return function epic( 172 | action$: ActionsObservable, 173 | state$: StateObservable, 174 | dependencies: Dependencies 175 | ) { 176 | const flow$ = flow( 177 | action$.pipe( 178 | ofType(actionType), 179 | map(action => ({ ...action, payload: getPayload(action) })) 180 | ), 181 | action$, 182 | state$, 183 | dependencies 184 | ) 185 | 186 | const loadingStart$ = action$.pipe( 187 | ofType(actionType), 188 | mapTo({ 189 | type: LOADING_START_ACTION, 190 | payload: { flow: actionType } 191 | }) 192 | ) 193 | 194 | return merge( 195 | loadingStart$, 196 | flow$.pipe( 197 | mergeMap(action => { 198 | if (!action) { 199 | return throwError('inner error') 200 | } 201 | if (action[FLOW_END_INDICATOR]) { 202 | const hideAction = { 203 | type: LOADING_END_ACTION, 204 | payload: { flow: actionType } 205 | } 206 | return of(action, hideAction) 207 | } else { 208 | return of(action) 209 | } 210 | }), 211 | catchError(error => { 212 | console.error(`@reobservable[flow ${actionType} error]`, error) 213 | return of({ 214 | type: ERROR_SET_ACTION, 215 | payload: { flow: actionType, error: error } 216 | }) 217 | }) 218 | ) 219 | ) 220 | } 221 | } 222 | 223 | /** 224 | * 创建 flows 225 | * @param {*} model 226 | */ 227 | const createFlows = (model: Model) => { 228 | Object.keys(model.flows).forEach(flowName => { 229 | const flow = model.flows[flowName] 230 | const actionType = `${model.name}/${flowName}` 231 | epics.push(createEpic(actionType, flow)) 232 | }) 233 | } 234 | 235 | Object.keys(models).forEach(key => { 236 | const model = { 237 | reducers: {}, 238 | selectors: {}, 239 | flows: {}, 240 | state: {}, 241 | ...models[key] 242 | } 243 | createReducer(model) 244 | createFlows(model) 245 | selectors[model.name] = model.selectors 246 | }) 247 | 248 | // 配置 redux 249 | const epicMiddleware = createEpicMiddleware({ 250 | dependencies: { 251 | end, 252 | endTo, 253 | services 254 | } 255 | }) 256 | 257 | const extraMiddleware = redux.middleware 258 | ? Array.isArray(redux.middleware) 259 | ? redux.middleware 260 | : [redux.middleware] 261 | : [] 262 | 263 | const middleware = applyMiddleware(epicMiddleware, ...extraMiddleware) 264 | 265 | let rootReducer = combineReducers(reducers) 266 | if (redux.rootReducer) { 267 | rootReducer = redux.rootReducer(rootReducer, reducers) 268 | } 269 | 270 | const store = createStore(rootReducer, composeEnhancers(middleware)) 271 | 272 | // 配置 services 273 | Object.keys(config.services || {}).forEach(key => { 274 | const service = config.services[key] 275 | services[key] = createService(notification, service, store) 276 | }) 277 | 278 | epicMiddleware.run(combineEpics(...epics)) 279 | 280 | return store 281 | } 282 | 283 | export function getSelectors(model: string) { 284 | return selectors[model] 285 | } 286 | 287 | export function getService(service: string) { 288 | return services[service] 289 | } 290 | 291 | export function notificate(type: string, message: any) { 292 | if (!isNil(message)) { 293 | return (notification[type] || notification.info)(message) 294 | } 295 | } 296 | 297 | export { NOTIFICATION_LEVEL, Symbols } 298 | --------------------------------------------------------------------------------