import Rx from 'rxjs';
import {ResourceModel} from './resourceModel';
import {applyCrudReducer} from '../utils/createCrud';
import {Api} from '../utils/fetch';
import SI from 'seamless-immutable';
import {resourceCRUD} from './resourceCRUD';
const noop = d => d;
export class BaseResource {
static processData = res => res;
static validate = function(){};
static ASSIGN_METHODS = resourceCRUD;
constructor(resourceName, option = {}, initialState) {
this._sync(resourceName, option, initialState || option.initialState || {});
this.$model_ = this.defer();
this.$cacheHttps_ = new Map();
this.$subject_ = new Rx.Subject();
this.$subscripers_ = [];
}
getState() {
return this.$state_;
}
|
_sync(resourceName, option, initialState) {
const defaultActions = {
query: {
path: '/list',
method: 'get',
state: {
list: resourceCRUD.QUERY
}
},
get: {
method: 'get',
state: {
item: resourceCRUD.GET
}
},
create: {
method: 'post',
state: {
list: resourceCRUD.ADD
}
},
update: {
method: 'put',
state: {
list: resourceCRUD.UPDATE
}
},
delete: {
method: 'delete',
state: {
list: resourceCRUD.DELETE
}
}
}
option.actions = option.actions || defaultActions;
const actions = {};
for (let key in option.actions) {
let action;
let cid = option.actions[key].cid || option.cid;
if (defaultActions[key]) {
if (typeof option.actions[key] === 'string') {
action = Object.assign({
uri: option.actions[key]
}, defaultActions[key]);
} else if (option.actions[key] === true) {
action = Object.assign({
path: '/{${cid}}'
}, defaultActions[key]);
} else {
action = Object.assign({
path: '/{${cid}}'
}, defaultActions[key], {state: null}, option.actions[key]);
}
} else if (this[key]) {
throw new Error('actionName不能覆盖Resource已有的方法');
} else {
action = Object.assign({}, option.actions[key]);
}
if (action.initialState) {
Object.assign(initialState, action.initialState);
}
action.cid = cid;
action.changes = action.changes || [];
actions[key] = action;
let state = action.state;
if (state) {
for (let name in state) {
let func = state[name];
if(typeof func === 'string'){
func = resourceCRUD[func]
}
let changes = func(name, action, initialState);
if (changes) {
action.changes = action
.changes
.concat(changes);
}
}
}
const processData = action.processData
if(typeof processData === 'object'){
action.processData = (res, params) => {
const data = {};
let n;
for(let key in processData){
let d = res;
let tmp = processData[key].split('.');
while(n = tmp.shift()){
d = d[n];
}
data[key] = d;
}
return data;
}
}
if (!this[key]) {
this[key] = (params, callback) => {
return this.request(key, params, callback)
}
}
}
this.$state_ = SI(initialState);
this.$cid_ = option.cid;
this.$options_ = {
resourcePath: option.resourcePath || '',
addTimestamp: option.addTimestamp,
removeTrailingSlash: option.removeTrailingSlash,
actions: actions,
|
requestInterceptor: (ajaxOption) => {
const actionName = ajaxOption.name;
const action = this.$options_.actions[actionName];
const http = this
.$cacheHttps_
.get(actionName);
let promise;
let isSame = false;
if ((action.cache || action.distinct) && ajaxOption.method.toLocaleLowerCase() === 'get') {
if(http){
if(http.params == ajaxOption.params){
isSame = true;
}else{
const _params = ajaxOption.params || {};
const params = http.params || {};
const keys = Object.keys(_params);
if (keys.length == Object.keys(_params) && keys.length == keys.filter(key => params[key] === _params[key]).length) {
isSame = true;
}
}
if(isSame && action.cache){
if(http.resolved){
http.resolved = false;
promise = http.$observable.toPromise();
}else{
http.abort();
http.release();
}
}
}
}
if (!promise) {
const newHttp = this.createHttp(ajaxOption.params, () => {
this
.$subject_
.next({
name: actionName,
status: 'start',
ajaxOption: ajaxOption,
dispatch: newHttp.extraOption.dispatch,
state: this.applyState({
status: 'start',
record: {},
params: ajaxOption.params,
changes: action.changes.concat(newHttp.extraOption.changes || [])
}),
stateAction: newHttp.extraOption.action
});
});
this
.$cacheHttps_
.set(actionName, newHttp);
promise = Promise
.resolve(ajaxOption)
.then(opt => {
let promise;
if (http) {
if (action.distinct && isSame && !http.resolved) {
http.abort();
promise = Api.error(new Error('Request is stoped'));
}
if(!action.cache){
http.release();
}
}
if(!promise){
let err;
if(err = BaseResource.validate(opt.params, action)){
promise = Api.error(err);
}else{
opt.errorNotification = opt.errorNotification || newHttp.extraOption.errorNotification;
promise = Api(opt);
}
}
return promise;
});
}
return promise;
},
responseInterceptor: (promise, ajaxOption) => {
const newHttp = this
.$cacheHttps_
.get(ajaxOption.name);
const action = actions[ajaxOption.name];
if (!newHttp.resolved) {
if(newHttp.extraOption.callback){
promise.then(res => newHttp.extraOption.callback(null, res), err => newHttp.extraOption.callback(err));
}
promise.then(res => {
const processData = newHttp.extraOption.processData || action.processData || BaseResource.processData;
const data = processData(res, ajaxOption.params);
const state = this.applyState({
record: data,
status: 'success',
params: ajaxOption.params,
changes: action.changes.concat(newHttp.extraOption.changes || [])
});
this
.$subject_
.next({
name: ajaxOption.name,
status: 'success',
data: data,
dispatch: newHttp.extraOption.dispatch,
stateAction: newHttp.extraOption.action,
state: state
});
return data;
}, err => {
this
.$subject_
.next({name: ajaxOption.name, status: 'error', dispatch: newHttp.extraOption.dispatch, stateAction: newHttp.extraOption.action, state: this.$state_, error: err});
throw err;
});
}
if(!action.cache){
newHttp.release();
}
return promise;
}
}
}
|
applyState(payload) {
if(payload.changes.length){
return this.$state_ = applyCrudReducer(this.$state_, payload);
}else{
return null;
}
}
defer() {
return new ResourceModel(this.$options_);
}
createHttp(params, preCallback) {
const http = {
extraOption: {},
params: params,
resolved: false,
abort: () => {
if (http.resolved)
return;
http
.$subscription
.unsubscribe();
http.resolved = true;
},
subscriber: null,
observable: Rx
.Observable
.create(subscriber => {
http.subscriber = subscriber;
})
.flatMap(() => http.$observable),
$observable: null,
$subscription: null,
start: (promise, callback) => {
if (http.resolved)
return;
|
let extraOption;
if (typeof callback === 'object') {
extraOption = callback;
callback = extraOption.callback;
delete extraOption.callback;
http.extraOption = extraOption;
}
callback = callback || noop;
|
preCallback();
http.$observable = Rx
.Observable
.fromPromise(promise);
http.$subscription = http
.$observable
.subscribe(res => {
if (res instanceof Error) {
callback(res)
} else {
callback(null, res);
}
}, err => callback(err), () => http.resolved = true);
|
http.observable = http
.observable
.publish();
http
.observable
.connect();
},
release: () => {
if (http.subscriber) {
http
.subscriber
.next();
http
.subscriber
.complete();
http.subscriber = null;
}
}
}
return http;
}
request(actionName, params, callback) {
const action = this.$options_.actions[actionName];
if (!action) {
return Api.error(new Error('Request is null'));
}
const promise = this
.$model_
.request(actionName, params);
const http = this
.$cacheHttps_
.get(actionName);
promise.cancel = http
.abort
.bind(http);
http.start(promise, callback);
return promise;
}
subscribe(callback) {
var subscription = this.$subject_.subscribe(callback);
this
.$subscripers_
.push(subscription);
return subscription;
}
unsubscribe() {
this
.$subscripers_
.forEach(subscription => {
subscription.unsubscribe();
})
}
destroy() {
this.unsubscribe();
this
.$subject_
.complete();
}
}
|