/*
* License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt
*/
import {flux} from '../Firefly.js';
import {take, fork, spawn, cancel, put} from 'redux-saga/effects';
import {isEmpty, get, isFunction, isUndefined, union, isArray, pick} from 'lodash';
import {uniqueID} from '../util/WebUtil.js';
import {TABLE_SEARCH} from '../tables/TablesCntlr.js';
import {getTblIdsByGroup, onTableLoaded} from '../tables/TableUtil';
import {isDebug} from '../util/WebUtil';
export const ADD_SAGA= 'MasterSaga.addSaga';
export const ADD_ACTION_WATCHER= 'MasterSaga.addActionWatcher';
export const CANCEL_ACTION_WATCHER= 'MasterSaga.cancelActionWatcher';
export const CANCEL_ALL_ACTION_WATCHER= 'MasterSaga.cancelAllActionWatcher';
export const ADD_TABLE_TYPE_WATCHER= 'MasterSaga.addTableTypeWatcher';
export const EDIT_TABLE_TYPE_WATCHER= 'MasterSaga.editTableTypeWatcher';
/**
*
* @param {generator} saga a generator function that uses redux-saga
* @param {{}} params this object is passed the to sega as the first parrmeter
*/
export function dispatchAddSaga(saga, params={}) {
flux.process({ type: ADD_SAGA, payload: { saga,params}});
}
/**
* Action watcher callback.
* @callback actionWatcherCallback
* @param {Action} action - the triggering action
* @param {function} cancelSelf - a function to cancel this watcher
* @param {object} params - params passed through dispatchAddActionWatcher or (if not undefined) the last returned value from the watcher callback
* @param {function} dispatch: flux's dispatcher
* @param {function} getState: flux's getState function
*/
/**
* @param {object} p
* @param {string} [p.id] a unique identifier for this watcher. This is needed for dispatchCancel*.
* When not given, a unique ID will be created. You can still cancel this watcher via
* callback's cancelSelf function.
* @param {string[]} p.actions an array of action types to watch
* @param {actionWatcherCallback} p.callback a callback function to handle the action(s).
* It is called with
* (action:Action, cancelSelf:Function, params:Object, dispatch:Function, getState:Function).
* {Action} action: the triggered action
* {Function} cancelSelf: a function to cancel this watcher
* {Object} params: the given params object, it the watcher callback returns a value
* then on the next call the last returned value
* {Function} dispatch: flux's dispatcher
* {Function} getState: flux's getState function
* @param {Object} p.params a pass-along parameter object to be sent when callback is called.
*/
export function dispatchAddActionWatcher({id, actions, callback, params={}}) {
flux.process({ type: ADD_ACTION_WATCHER, payload: {id, actions, callback, params}});
}
/**
* @global
* @public
* @name TableWatchFunc
* @function
* @summary Table watcher function
* @description Like a normal watch with following exceptions:
* <ul>
* <li> one started per table
* <li> tbl_id is first parameter.
* <li> The params object with always include a options and a sharedData property. They may be undefined.
* <li> The options object should be thought of as read only. It should not be modified.
* <li> It is call the first time with the action undefined. This can be for initialization. The table will beloaded
* </ul>
* @param {String} tbl_id the table id
* @param {Action} action - the action
* @param {Function} cancelSelf - function to cancel, should be call when table is removed
* @param {Object} params - params contents are created by the watcher with two exceptions, params will always contain:
* <ul>
* <li> options - The options object is the same as passed to dispatchAddTableTypeWatcherDef
* <li> sharedData - if the definition contains a shared data object it will be passed. sharedData provides a way
* for all the watcher with the same definition to share some state. This should only be used if absolutely necessary
* </ul>
*/
/**
* @global
* @public
* @name TestWatchFunc
* @function
* @desc Test to see if we should watch this table
* @param {TableModel} table
* @param {Action} action
* @param {Object} options
* @return {boolean} true if we should watch
*
*/
/**
* @summary Add a table type watcher definition. Test every table (existing or new) and if the test is passed then create
* a table type watcher for that table.
*
* @description A table type watcher, by using the payload definition will start a single watch for ever table that is newly loaded
* or previously loaded that passes the 'testTable' function test.
* These watchers will have the same life time of the table they are watching.
* A TableType watcher is like a normal watcher with following exceptions:
* <ul>
* <li> tbl_id is first parameter.
* <li> The params object with always include a options and a sharedData property. They may be undefined.
* <li> The options object should be thought of as read only. It should not be modified.
* <li> It is call the first time with the action undefined. This can be for initialization. The table will beloaded
* </ul>
*
* @param {object} p
* @param {String} p.id a unique identifier for this watcher
* @param {TestWatchFunc} p.testTable - function: testTable(table,action,options), return true, if we should watch this table type
* @param {TableWatchFunc} p.watcher - function watcher(tbl_id,action,cancelSelf,params), note- TableWatchFunc is called
* when the first time for initialization for the first call action is null
* @param {Object} [p.sharedData]
* @param {Object} [p.options]
* @param {Array.<String>} p.actions - array of string action id's
* @param {Array.<String>} p.excludes- excluded id list. if testTable return true then this
* list will force any watcher def with an id in the list to not watch
* @param {boolean} p.stopPropagation - like excludes but if true not only watcher will be added
* @param {boolean} p.enabled - if true this TableTypeWatcher will test and add, if false it will be skipped
* @param {boolean} p.allowMultiples - multiple defs of this type are allowed.
*
* @see TableWatchFunc
* @see TestWatchFunc
*
* @public
* @function dispatchAddTableTypeWatcherDef
* @memberof firefly.action
*/
export function dispatchAddTableTypeWatcherDef({id, actions, excludes= [], testTable= ()=>true,
watcher, options={}, enabled= true, stopPropagation= false,
sharedData, allowMultiples}) {
flux.process({
type: ADD_TABLE_TYPE_WATCHER,
payload: {id, actions, excludes, testTable, watcher, options, enabled, stopPropagation, sharedData,allowMultiples}
});
}
/**
* Change any key (but id) defined in dispatchAddTableTypeWatcherDef
* @param {object} p
* @param p.id
* @param p.changes any key that you want to change
*/
export function dispatchEditTableTypeWatcherDef({id, ...changes}) {
flux.process({ type: EDIT_TABLE_TYPE_WATCHER, payload:{id, changes}});
}
/**
* cancel the watcher with the given id.
* @param {string} id a unique identifier of the watcher to cancel
*/
export function dispatchCancelActionWatcher(id) {
flux.process({ type: CANCEL_ACTION_WATCHER, payload: {id}});
}
/**
* Cancel all watchers. Should only be called during re-init scenarios.
*/
export function dispatchCancelAllActionWatchers() {
flux.process({ type: CANCEL_ALL_ACTION_WATCHER});
}
/**
* This saga launches all the predefined Sagas then loops and waits for any ADD_SAGA actions and launches those Segas
*/
export function* masterSaga() {
let watchers = {};
// Start a saga from any action
while (true) {
const action= yield take([ADD_SAGA, ADD_ACTION_WATCHER, ADD_TABLE_TYPE_WATCHER, EDIT_TABLE_TYPE_WATCHER,
CANCEL_ACTION_WATCHER, CANCEL_ALL_ACTION_WATCHER]);
switch (action.type) {
case ADD_SAGA: {
const {getState, dispatch}= flux.getRedux();
const {saga,params}= action.payload;
// with fork every exception will bubble up from the child to the parent:
// an unhandled exception in one saga will cancel all sibling sagas
// with spawn, only the saga with the unhandled error will be cancelled
// the unhandled errors are caught by middleware and logged to console
if (isFunction(saga)) {
yield spawn(saga, params, dispatch, getState);
} else {
console.error('Can not add saga: callback must be a generator function');
}
break;
}
case ADD_ACTION_WATCHER: {
const {getState, dispatch}= flux.getRedux();
const {actions, callback, params}= action.payload;
if (actions && isFunction(callback)) {
const {id=callback.name+uniqueID()}= action.payload;
if (watchers[id]) {
yield cancel(watchers[id]);
}
const watcherSaga = createWatcherSaga({id, actions, callback, params, dispatch, getState});
const task = yield fork(watcherSaga, dispatch, getState);
watchers[id] = task;
isDebug() && console.log(`watcher ${id} added. #watcher: ${Object.keys(watchers).length}`);
} else {
console.error('Can not create action watcher: invalid actions or callback');
}
break;
}
case CANCEL_ACTION_WATCHER: {
const {id}= action.payload;
const task = watchers[id];
if (task) {
yield cancel(task);
Reflect.deleteProperty(watchers, id);
isDebug() && console.log(`watcher ${id} cancelled. #watcher: ${Object.keys(watchers).length}`);
}
break;
}
case CANCEL_ALL_ACTION_WATCHER: {
const ids = Object.keys(watchers);
for (let i = 0; i < ids.length; i++) {
const task = watchers[ids[i]];
yield cancel(task);
}
watchers = {};
setTimeout(initTTWatcher, 1);
break;
}
case ADD_TABLE_TYPE_WATCHER: {
addTableTypeWatcherDef(action.payload);
break;
}
case EDIT_TABLE_TYPE_WATCHER: {
const {changes,id}= action.payload;
editTTWatcherDef(id,changes, Object.keys(watchers));
break;
}
}
}
}
function createWatcherSaga({id, actions=[], callback, params, dispatch, getState}) {
const cancelSelf = ()=> dispatch({ type: CANCEL_ACTION_WATCHER, payload: {id}});
const saga = function* () {
let prevParams= params;
let returnedParams;
// loop exits when saga is cancelled
while (true) {
const action = yield take(actions);
try {
// the same callback can return modified parameters or undefined
// when undefined is returned use previous parameters
returnedParams = callback(action, cancelSelf, prevParams, dispatch, getState);
if (!isUndefined(returnedParams)) {
prevParams = returnedParams;
}
} catch (e) {
console.log(`Encounter error while executing watcher: ${id} error: ${e}`);
console.log(e);
}
}
};
return saga;
}
const TTW_PREFIX= 'tableWatch-';
let ttWatcherDefList= [];
const getTTWatcherDefList= () => ttWatcherDefList;
const insertTTWatcherDef= (def) => def && ttWatcherDefList.push(def);
function editTTWatcherDef(id, changes, watchersIdList) {
if (!id) return;
ttWatcherDefList= ttWatcherDefList.map( (def) => {
if (def.id!==id) return def;
const obj= pick(changes, ['testTable', 'watcher', 'sharedData', 'options',
'actions', 'excludes', 'stopPropagation', 'enabled']);
return {...def, ...obj};
});
watchersIdList
.filter( (wId) => wId.indexOf(`${TTW_PREFIX}${id}`)===0)
.forEach( (wId) => dispatchCancelActionWatcher(wId));
retroactiveTTStart(ttWatcherDefList.find( (def) => def.id===id ));
}
function addTableTypeWatcherDef(def) {
setTimeout(() => {
if (isEmpty(getTTWatcherDefList())) initTTWatcher();
// validate and start
if (!def.allowMultiples && ttWatcherDefList.find( (d) => d.id===def.id)) return;
if (isFunction(def.watcher) && isArray(def.actions) && def.id) insertTTWatcherDef(def);
else console.error('TableTypeWatcher: watcher, actions, and id are required.');
retroactiveTTStart(def);
}, 1);
}
function retroactiveTTStart(def) {
const idAry= getTblIdsByGroup();
idAry.forEach( (tbl_id) => startTableTypeWatchersForTable(tbl_id,null, () => [def]));
}
const initTTWatcher= () =>
dispatchAddActionWatcher(
{
actions: [TABLE_SEARCH],
id: 'masterTableTypeWatcher',
callback: masterTableTypeWatcher,
params: {getTTWatcherDefList}
});
/**
* watcher - for TABLE_SEARCH
* @param action
* @param cancelSelf
* @param params
*/
function masterTableTypeWatcher(action, cancelSelf, params) {
const tbl_id = get(action, 'payload.request.tbl_id');
if (!tbl_id || action.type!==TABLE_SEARCH) return;
startTableTypeWatchersForTable(tbl_id,action,params.getTTWatcherDefList);
}
function startTableTypeWatchersForTable(tbl_id, action, getDefList) {
onTableLoaded(tbl_id).then( (table) => {
table= get(table,'tableModel',table);
if (!table || table.error || !table.totalRows) return;
if (isDebug()) console.log(`new loaded table: ${tbl_id}`);
const defList= getDefList();
let excludeList= [];
let stopProp= false;
defList.forEach( (d) => {
const {id, sharedData, options, stopPropagation, enabled= true, excludes=[], actions}= d;
if (stopProp || !enabled || excludeList.includes(id)) return;
if (d.testTable(table, action, options)) {
stopProp= stopPropagation;
excludeList= union(excludeList, excludes);
let abort= false;
const initParams= d.watcher(tbl_id, undefined, ()=> (abort=true), {sharedData, options});
if (abort) return;
dispatchAddActionWatcher({
id:`${TTW_PREFIX}${id}-${tbl_id}`,
actions,
callback: (action,cancelSelf,params) => {
return d.watcher(tbl_id, action, cancelSelf, {...params, sharedData, options});
},
params:initParams});
}
});
});
}