/* * 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}); } }); }); }