UNPKG

canner

Version:

Build CMS in few lines of code for different data sources

291 lines (268 loc) 8.3 kB
// @flow import * as React from 'react'; import {mutate as defaultMutate, ActionManager as DefaultAciontManager} from '../action'; import {isCompleteContain, genPaths} from './route'; import { isArray } from 'lodash'; import mapValues from 'lodash/mapValues'; import {groupBy} from 'lodash'; import { OnDeployManager } from '../onDeployManager'; import type {Action, ActionType} from '../action/types'; import type {HOCProps} from './types'; type State = { [string]: *, dataChanged: Object, } export default function withCache(Com: React.ComponentType<*>, options: { mutate: typeof defaultMutate, ActionManager: DefaultAciontManager, onDeployManager: OnDeployManager }) { const {mutate = defaultMutate, ActionManager = DefaultAciontManager} = options || {}; return class ComWithCache extends React.Component<HOCProps, State> { actionManager: ?ActionManager; onDeployManager: OnDeployManager; subscribers: { [key: string]: Array<{id: string, callback: Function}> } subscribers = {}; subscription: any; constructor(props: HOCProps) { super(props); const {routes, cacheActions, pattern, path} = this.props; if ((routes.length > 1 && isRoutesEndAtMe({routes, pattern, path})) || cacheActions ) { this.actionManager = new ActionManager(); this.onDeployManager = new OnDeployManager(); } this.state = { dataChanged: {} }; } addSubscriber = (key: string, id: string, callback: Function) => { const subscriber = { id, callback }; if (this.subscribers[key]) { this.subscribers[key].push(subscriber); } else { this.subscribers[key] = [subscriber]; } } unsubscribe = (key: string, subscriberId: string) => { this.subscribers[key] = this.subscribers[key].filter(subscriber => { return subscriber.id !== subscriberId; }); } publish = (key: string, id?: string) => { // $FlowFixMe if (!this.actionManager) { return; } const data = this.state[key]; if (!data) { return; } const actions = this.actionManager.getActions(key, id); const mutatedData = actions.reduce((result: any, action: Action<ActionType>) => { return mutate(result, action); }, data); (this.subscribers[key] || []).forEach(subscribe => { subscribe.callback(mutatedData); }); } fetch = (key: string) => { // the data will be mutated by cached actions const {fetch} = this.props; if (!this.actionManager) { return fetch(key); } const actions = this.actionManager.getActions(key); return fetch(key).then(data => { this.setState({ [key]: data }); this._subscribe(key); return actions.reduce((result: any, action: Action<ActionType>) => { return mutate(result, action); }, data); }); } _subscribe = (key: string) => { const {subscribe} = this.props; this.subscription = subscribe(key, (data) => { this.setState({ [key]: data }, () => this.publish(key)); }); } _unsubscribe = () => { if (this.subscription) { this.subscription.unsubscribe(); } } request = (action: Array<Action<ActionType>> | Action<ActionType>): Promise<*> => { // use action manager cache the actions // update state.actions const {request} = this.props; if (!this.actionManager) { // $FlowFixMe return request(action); } let key, id; if (isArray(action)) { // $FlowFixMe action.forEach(ac => { // $FlowFixMe this.actionManager.addAction(ac); }); key = action[0].payload.key; id = action[0].payload.id; } else { // $FlowFixMe this.actionManager.addAction(action); // $FlowFixMe key = action.payload.key; // $FlowFixMe id = action.payload.id; } this.updateDataChanged(); this.publish(key, id); return Promise.resolve(); } onDeploy = (key: string, callback: any) => { const {onDeploy} = this.props; if (!this.onDeployManager) { return onDeploy(key, callback); } return this.onDeployManager.registerCallback(key, callback); } removeOnDeploy = (key: string, callbackId: string) => { const {removeOnDeploy} = this.props; if (!this.onDeployManager) { return removeOnDeploy(key, callbackId); } return this.onDeployManager.unregisterCallback(key, callbackId); } _executeOnDeployCallback = (key: string, value: any) => { return this.onDeployManager.execute({ key, value }); } deploy = (key: string, id?: string): Promise<*> => { // request cached actions const {request, deploy, pattern} = this.props; if (!this.actionManager) { return deploy(key, id); } const originData = this.state[key]; let actions = this.actionManager.getActions(key, id); const mutatedData = actions.reduce((result: any, action: Action<ActionType>) => { return mutate(result, action); }, originData); const {error} = this._executeOnDeployCallback(key, mutatedData[key]); if (error) { return Promise.reject(); } // actions = actions.map(action => { // const {key, value} = action.payload; // hasError = hasError || error; // action.payload.value = data; // return action; // }); // $FlowFixMe this.actionManager.removeActions(key, id); this.updateDataChanged(); // $FlowFixMe request(actions); // if this cache is on the first layer, // it should call the deploy after request if (pattern.split('.').length === 1) { return deploy(key, id); } return Promise.resolve(); } reset = (key: string, id?: string): Promise<*> => { // remove sepicfic cached actions in actionManager const {reset} = this.props; if (!this.actionManager) { return reset(key, id); } this.actionManager.removeActions(key, id); this.updateDataChanged(); this.publish(key, id); return Promise.resolve(); } subscribe = (key: string, callback: Function) => { const {subscribe} = this.props; if (!this.actionManager) { return subscribe(key, callback); } const id = genSubscriberId(); this.addSubscriber(key, id, callback); return { unsubscribe: () => { this.unsubscribe(key, id); } } } updateDataChanged = () => { if (!this.actionManager) { return; } const actions = this.actionManager.getActions(); let dataChanged = groupBy(actions, (action => action.payload.key)); dataChanged = mapValues(dataChanged, value => { if (value[0].type === 'UPDATE_OBJECT') { return true; } return value.map(v => v.payload.id); }); this.setState({dataChanged}); } updateQuery = (paths: Array<string>, args: Object) => { const {updateQuery} = this.props; return updateQuery(paths, args) .then(reWatch => { if (reWatch) { this._unsubscribe(); this.fetch(paths[0]); } return reWatch; }); } render() { return ( <Com {...this.props} fetch={this.fetch} request={this.request} deploy={this.deploy} reset={this.reset} subscribe={this.subscribe} updateQuery={this.updateQuery} onDeploy={this.onDeploy} removeOnDeploy={this.removeOnDeploy} dataChanged={this.state.dataChanged} /> ); } } } export function isRoutesEndAtMe({ routes, path, pattern }: { routes: Array<string>, path: string, pattern: string }): boolean { const paths = genPaths(path, pattern); return (paths.length === routes.length && isCompleteContain(paths, routes)); } export function genSubscriberId() { return Math.random().toString(36).substr(2, 7); }