/********************************************************************** * * Core features... * Setup the life-cycle and the base interfaces for features to use... * * Defined here: * - protocol action constructor * - config value toggler constructor * - meta actions * - .isToggler(..) predicate * - .preActionHandler(..) used to toggler special args * - ImageGrid root object/constructor * - ImageGridFeatures object * * * Features: * - logger * - introspection * - lifecycle * base life-cycle events (start/stop/..) * base abort api * - serialization * base methods to handle loading, serialization and cloning... * - cache * basic action/prop caching api... * - timers * wrapper around setInterval(..), setTimeout(..) and friends, * provides persistent timer triggers and introspection... * - util * - journal * action journaling and undo/redo functionality * XXX needs revision... * - changes * change tracking * - workspace * XXX needs revision... * - tasks * XXX not yet used * - self-test * basic framework for running test actions at startup... * * * XXX some actions use the .clone(..) action/protocol, should this be * defined here??? * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ // XXX var DEBUG = typeof(DEBUG) != 'undefined' ? DEBUG : true var types = require('lib/types') var runner = require('lib/types/runner') var util = require('lib/util') var object = require('lib/object') var actions = require('lib/actions') var features = require('lib/features') var toggler = require('lib/toggler') /*********************************************************************/ // NOTE: if no toggler state is set this assumes that the first state // is the default... // NOTE: default states is [false, true] var makeConfigToggler = module.makeConfigToggler = function(attr, states, a, b){ states = states || [false, true] var pre = a // XXX is this a good default??? //var post = b || function(action){ action != null && this.focusImage() } var post = b return toggler.Toggler(null, function(_, action){ var lst = states instanceof Array ? states : states instanceof Function ? states.call(this) : states // get attr path... var a = attr.split(/\./g) var cfg = a.slice(0, -1) .reduce(function(res, cur){ return res[cur] }, this.config) if(action == null){ var val = cfg[a.pop()] return val == null ? (lst[lst.indexOf('none')] || lst[0]) : val } else { cfg[a[a.length-1]] = action this.config[a[0]] = this.config[a[0]] } }, states, pre, post) } /*********************************************************************/ // Root ImageGrid.viewer object constructor... // // This adds: // - toggler as action compatibility // var ImageGridMetaActions = module.ImageGridMetaActions = { // Test if the action is a Toggler... // isToggler: actions.doWithRootAction(function(action){ return action instanceof toggler.Toggler }), // Handle special cases where we need to get the action result early, // without calling handlers... // // These include: // - toggler action special command handling (like: '?', '??', ..) // preActionHandler: actions.doWithRootAction(function(action, name, handlers, args){ // Special case: do not call handlers for toggler state queries... // // NOTE: if the root handler is instance of Toggler (jli) and // the action is called with '?'/'??' as argument, then the // toggler will be called with the argument and return the // result bypassing the handlers. // NOTE: an action is considered a toggler only if it's base action // is a toggler (instance of Toggler), thus, the same "top" // action can be or not be a toggler in different contexts. // // For more info on togglers see: lib/toggler.js if(this.isToggler(name) && args.length == 1 && (args[0] == '?' || args[0] == '??')){ return { result: handlers.slice(-1)[0].pre.apply(this, args), } } }), } ImageGridMetaActions.__proto__ = actions.MetaActions var ImageGrid = module.ImageGrid = object.Constructor('ImageGrid', ImageGridMetaActions) // Root ImageGrid feature set.... var ImageGridFeatures = module.ImageGridFeatures = new features.FeatureSet() // setup base instance constructor... ImageGridFeatures.__actions__ = function(){ return actions.Actions(ImageGrid()) } //--------------------------------------------------------------------- // Setup runtime info... // XXX add chrome-app... // XXX add cordova... // XXX add mobile... // XXX add widget... // XXX should this contain feature versions??? var runtime = ImageGridFeatures.runtime = {} // nw or node... if(typeof(process) != 'undefined'){ // node... runtime.node = true // Electron... if(process.versions['electron'] != null){ runtime.electron = true runtime.desktop = true // nw.js 0.13+ } else if(typeof(nw) != 'undefined'){ runtime.nw = true runtime.desktop = true // NOTE: jli is patching the Date object and with two separate // instances we'll need to sync things up... // XXX HACK: if node and chrome Date implementations ever // significantly diverge this will break things + this is // a potential data leak between contexts... //global.Date = window.Date // XXX this is less of a hack but it is still an implicit util.patchDate(global.Date) util.patchDate(window.Date) // node... } else { // XXX patch Date... // XXX this will not work directly as we will need to explicitly // require jli... //patchDate(global.Date) } } // browser... // NOTE: we're avoiding detecting browser specifics for as long as possible, // this will minimize the headaches of supporting several non-standard // versions of code... if(typeof(window) != 'undefined'){ runtime.browser = true } /*********************************************************************/ // Logger... var LoggerActions = actions.Actions({ config: { // NOTE: if set to 0 no log limit is applied... 'log-size': 10000, }, Logger: object.Constructor('BaseLogger', { doc: `Logger object constructor...`, quiet: false, __context: null, get context(){ return this.__context || this.root.__context }, root: null, get isRoot(){ return this === this.root }, __path: null, get path(){ return (this.__path = this.__path == null ? [] : this.__path) }, set path(value){ this.__path = value }, // NOTE: if set to 0 no log limit is applied... // NOTE: writing to this will modify .context.config['log-size'] // if a available and .__max_size otherwise... __max_size: null, get max_size(){ return this.__max_size != null ? this.__max_size // this.context.config['log-size'] : ((this.context || {}).config || {})['log-size'] || 10000 }, set max_size(value){ return this.context ? (this.context.config['log-size'] = value) : this.__max_size = value }, // NOTE: to disable log retention in .log set this to false... __log: null, get log(){ return this.__log === false ? false : this.__log ? this.__log : this.isRoot ? (this.__log = this.__log || []) : this.root.log }, // log management... clear: function(){ this.log && this.log.splice(0, this.log.length) return this }, // Format log to string... // // Full log... // .log2str() // -> str // // Slice log... // .log2str(from) // .log2str(from, to) // -> str // // Specific item... // .log2str(date, path, status, rest) // .log2str([date, path, status, rest]) // -> str // NOTE: this form does not depend on context... // // // NOTE: the later form is useful for filtering: // logger.log // .filter(..) // .map(logger.log2str) // .join('\n') // log2str: function(){ return (arguments.length == 0 ? (this.log || []) : arguments[0] instanceof Array ? [arguments[0]] : arguments.length < 2 ? (this.log ? this.log.slice(arguments[0], arguments[1]) : []) : [arguments]) .map(function([date, path, status, rest]){ return `[${ new Date(date).getTimeStamp(true) }] ` + path.join(': ') + (path.length > 0 ? ': ' : '') + status + (rest.length > 1 ? ':\n\t' : rest.length == 1 ? ': ' : '') + rest.join(': ') }) .join('\n') }, print: function(...args){ console.log(this.log2str(...args)) return this }, // main API... // // .push(str, ...) // // .push(str, ..., attrs) // push: function(...msg){ attrs = typeof(msg.last()) != typeof('str') ? msg.pop() : {} return msg.length == 0 ? this : Object.assign( this.constructor(), attrs, { root: this.root, path: this.path.concat(msg), }) }, pop: function(){ return (this.root === this || this.path.length == 1) ? this : Object.assign( this.constructor(), { root: this.root, path: this.path.slice(0, -1), }) }, emit: function(status, ...rest){ // write to log... this.log !== false && this.log.push([ Date.now(), this.path, status, rest]) // maintain log size... this.log !== false && (this.max_size > 0 && this.log.length > this.max_size && this.log.splice(0, this.log.length - this.max_size)) // call context log handler... this.context && this.context.handleLogItem && this.context.handleLogItem(this, this.path, status, ...rest) return this }, __call__: function(_, status, ...rest){ return this.emit(status, ...rest) }, __init__: function(context){ this.__context = context this.root = this }, }), __logger: null, get logger(){ return (this.__logger = this.__logger || this.Logger(this)) }, // XXX move this to console-logger??? handleLogItem: ['- System/', function(logger, path, status, ...rest){ logger.quiet || console.log( path.join(': ') + (path.length > 0 ? ': ' : '') + status + (rest.length > 1 ? ':\n\t' : rest.length == 1 ? ': ' : ''), ...rest) }], }) var Logger = module.Logger = ImageGridFeatures.Feature({ title: '', tag: 'logger', depends: [], actions: LoggerActions, }) //--------------------------------------------------------------------- // Introspection... // Normalize doc strings... // // This will remove indent padding from all lines in a doc string. // // This is useful for documenting actions using ES6 template/multi-line // strings and keep them sane in terms of indent... // // Example: // someAction: ['Test/Some action title', // core.doc`This is an example... // mult-iline... // ...doc string that will be normalized and look the same but` // without the indent...`, // function(){ ... }] // // NOTE: this will ignore the first line's indent so it can be started // right at the string start. // // XXX might be a good idea to move this to a more generic spot like lib/util.js... var doc = module.doc = object.doc var text = module.text = object.text // Indicate that an action is not intended for direct use... // // NOTE: this will not do anything but mark the action. var notUserCallable = module.notUserCallable = function(func){ func.__not_user_callable__ = true return func } // NOTE: this is the same as event but user-callable... var UserEvent = module.UserEvent = function(func){ func.__event__ = true return func } // XXX rename??? var Event = module.Event = function(func){ func.__event__ = true return notUserCallable(func) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var IntrospectionActions = actions.Actions({ get useractions(){ return this.cache('useractions', function(d){ return d instanceof Array ? d.slice() : this.actions.filter(this.isUserCallable.bind(this)) }) }, get events(){ return this.cache('events', function(d){ return d instanceof Array ? d.slice() : this.actions.filter(this.isEvent.bind(this)) }) }, isUserCallable: // XXX should this check only the root action or the whole set??? // ...in other words: can we make an action non-user-callable // anywhere other than the root action? // IMO no... //function(action){ // return this.getActionAttr(action, '__not_user_callable__') != true }], actions.doWithRootAction(function(action){ return action.__not_user_callable__ != true }), isEvent: actions.doWithRootAction(function(action){ return !!action.__event__ }), // XXX revise... getActionMode: ['- Interface/', doc`Get action browse mode... Get and action's .mode(..) method and return its result. Action .mode can be: - action method. - alias, name of action to get the method from. The action .mode(..) method is called in the context of actions. Basic example: someAction: ['Path/To/Some action', {mode: function(){ ... }}, function(){ ... }], someOtherAction: ['Path/To/Some action', // alias {mode: 'someAction'}, function(){ ... }], Usage pattern: // for cases where we need to define an explicit mode... actionModeX: ['- System/', {mode: function(){ return this.actionModeX() }}, core.notUserCallable(function(){ return ... })], someAction: [ // use the mode... {mode: 'actionModeX'}, function(){ ... }], `, function(action, mode_cache){ var m = action var visited = [m] var last // check cache... if(m in (mode_cache || {})){ return mode_cache[m] } // handle aliases... do { last = m m = this.getActionAttr(m, 'mode') // check cache... if(m in (mode_cache || {})){ return mode_cache[m] } // check for loops... if(m && visited[m] != null){ m = null break } visited.push(m) } while(typeof(m) == typeof('str')) //return m ? m.call(this) : undefined return m ? // no cache... (mode_cache == null ? m.call(this) // cache hit... : last in mode_cache ? mode_cache[last] // call check and populate cache... : (mode_cache[action] = mode_cache[last] = m.call(this))) : actions.UNDEFINED }], }) var Introspection = module.Introspection = ImageGridFeatures.Feature({ title: '', tag: 'introspection', depends: [ 'cache' ], actions: IntrospectionActions, }) //--------------------------------------------------------------------- // System life-cycle... // XXX should his have state??? // ...if so, should this be a toggler??? var LifeCycleActions = actions.Actions({ config: { // if set indicates the timeput after which the application quits // wating for .declareReady() and forcefully triggers .ready() 'declare-ready-timeout': 15000, }, __stop_handler: null, __ready: null, __ready_announce_requested: null, __ready_announce_requests: null, // introspection... isStarted: function(){ return !!this.__stop_handler }, isStopped: function(){ return !this.__stop_handler }, isReady: function(){ return !!this.__ready }, // XXX is this the right name for this??? get runtimeState(){ return this.isStopped() ? 'stopped' : this.isReady() ? 'ready' : this.isStarted() ? 'started' : undefined }, // XXX not implemented... // ...this should be triggered on first run and after updates... setup: ['- System/', doc``, Event(function(mode){ // System started event... // // Not intended for direct use. })], start: ['- System/', doc`Start core action/event .start() This action triggers system start, sets up basic runtime, prepares for shutdown (stop) and handles the .ready() event. Attributes set here: .runtime - indicates the runtime ImageGrid is running. this currently supports: node, browser, nw, unknown This will trigger .declareReady() if no action called .requestReadyAnnounce() This will trigger .started() event when done. NOTE: .runtime attribute will not be available on the .pre handler phase. NOTE: .requestReadyAnnounce() should be called exclusively on the .pre handler phase as this will check and trigger the .ready() event before the .post phase starts. NOTE: handlers bound to this action/event will get called on the start *event* thus handlers bound when the system is already started will not get called until next start, to bind a handler to the started *state* bind to 'started' / .started() `, function(){ var that = this this.logger && this.logger.emit('start') // NOTE: jQuery currently provides no way to check if an event // is bound so we'll need to keep track manually... if(this.__stop_handler == null){ var stop = this.__stop_handler = function(){ that.stop() } } else { return } // set the runtime... var runtime = this.runtime = ImageGridFeatures.runtime // nw.js... if(runtime.nw){ // this handles both reload and close... $(window).on('beforeunload', stop) // NOTE: we are using both events as some of them do not // get triggered in specific conditions and some do, // for example, this gets triggered when the window's // 'X' is clicked while does not on reload... this.__nw_stop_handler = function(){ var w = this try{ that // wait till ALL the handlers finish before // exiting... .on('stop.post', function(){ // XXX might be broken in nw13 -- test!!! //w.close(true) nw.App.quit() }) .stop() // in case something breaks exit... // XXX not sure if this is correct... } catch(e){ console.log('ERROR:', e) DEBUG || nw.App.quit() //this.close(true) } } nw.Window.get().on('close', this.__nw_stop_handler) // electron... } else if(runtime.electron){ $(window).on('beforeunload', stop) // node... } else if(runtime.node){ process.on('exit', stop) // browser... } else if(runtime.browser){ $(window).on('beforeunload', stop) // other... } else { // XXX console.warn('Unknown runtime:', runtime) } // handle ready event... // ...if no one requested to do it. if(this.__ready_announce_requested == null || this.__ready_announce_requested <= 0){ // in the browser world trigger .declareReady(..) on load event... if(runtime.browser){ $(function(){ that.declareReady('start') }) } else { this.declareReady('start') } } // ready timeout -> force ready... this.config['declare-ready-timeout'] > 0 && !this.__ready_announce_timeout && (this.__ready_announce_timeout = setTimeout(function(){ // cleanup... delete this.__ready_announce_timeout if((this.__ready_announce_requests || new Set()).size == 0){ delete this.__ready_announce_requests } // force start... if(!this.isReady()){ // report... this.logger && this.logger.push('start') .emit('forcing ready.') .emit('stalled:', this.__ready_announce_requested, ...(this.__ready_announce_requests || [])) // force ready... this.__ready = !!this.ready() // cleanup... delete this.__ready_announce_requested delete this.__ready_announce_requests } }.bind(this), this.config['declare-ready-timeout'])) // trigger the started event... this.started() }], started: ['- System/System started event', doc` `, Event(function(){ // System started event... // // Not intended for direct use. })], // NOTE: it is recommended to use this protocol in such a way that // the .ready() handler would recover from a stalled // .requestReadyAnnounce() call... ready: ['- System/System ready event', doc`Ready core event The ready event is fired right after start is done. Any feature can request to announce 'ready' itself when it is done by calling .requestReadyAnnounce(). If .requestReadyAnnounce() is called, then the caller is required to also call .declareReady(). .ready() will actually be triggered only after when .declareReady() is called the same number of times as .requestReadyAnnounce(). NOTE: at this point the system does not track the caller "honesty", so it is the caller's responsibility to follow the protocol. `, Event(function(){ // System ready event... // // Not intended for direct use, use .declareReady() to initiate. this.logger && this.logger.emit('ready') })], // NOTE: this calls .ready() once per session. declareReady: ['- System/Declare system ready', doc`Declare ready state .declareReady() This will call .ready() but only in the following conditions: - .requestReadyAnnounce() has not been called. - .requestReadyAnnounce() has been called the same number of times as .declareReady() NOTE: this will call .ready() only once per start/stop cycle. `, function(message){ this.__ready_announce_requested && (this.__ready_announce_requested -= 1) message && this.__ready_announce_requests instanceof Set && this.__ready_announce_requests.delete(message) if(!this.__ready_announce_requested || this.__ready_announce_requested <= 0){ this.__ready = this.__ready || !!this.ready() delete this.__ready_announce_requested } }], requestReadyAnnounce: ['- System/', doc`Request to announce the .ready() event. .requestReadyAnnounce() .requestReadyAnnounce(message) This enables a feature to delay the .ready() call until it is ready, this is useful for async or long stuff that can block or slow down the .ready() phase. To indicate readiness, .declareReady() should be used. The system will call .ready() automatically when the last subscriber who called .requestReadyAnnounce() calls .declareReady(), i.e. .declareReady() must be called at least as many times as .requestReadyAnnounce() The actual .ready() should never get called directly. NOTE: if this is called, .ready() will not get triggered automatically by the system. `, function(message){ message && (this.__ready_announce_requests = this.__ready_announce_requests || new Set()) && this.__ready_announce_requests.add(message) return (this.__ready_announce_requested = (this.__ready_announce_requested || 0) + 1) }], stop: ['- System/', doc`Stop core action .stop() This will cleanup and unbind stop events. The goal of this is to prepare for system shutdown. NOTE: it is good practice for the bound handlers to set the system to a state from which their corresponding start/ready handlers can run cleanly. `, function(){ // browser... if(this.__stop_handler && this.runtime.browser){ $(window).off('beforeunload', this.__stop_handler) } // nw... if(this.__nw_stop_handler && this.runtime.nw){ nw.Window.get().removeAllListeners('close') delete this.__nw_stop_handler } // node... if(this.__stop_handler && this.runtime.node){ process.removeAllListeners('exit') } delete this.__ready delete this.__stop_handler this.logger && this.logger.emit('stop') // trigger the stopped event... this.stopped() }], stopped: ['- System/System stopped event', doc` `, Event(function(){ // System stopped event... // // Not intended for direct use. })], // XXX not implemented... // ...this should be triggered before uninstall... cleanup: ['- System/', doc``, Event(function(){ // System started event... // // Not intended for direct use. })], // trigger core events... // // NOTE: we do not need to do .one(..) as it is implemented via .on(..) // // XXX EXPERIMENTAL... // ...should this be an action??? on: ['- System/', function(evt, ...rest){ var func = rest.slice().pop() evt = typeof(evt) == typeof('') ? evt.split(/\s/g) : evt // we trigger the handler AFTER it is registered... return function(){ // started... Math.max( evt.indexOf('started'), evt.indexOf('started.pre'), evt.indexOf('started.post')) >= 0 && this.isStarted() && func.call(this) // ready... // NOTE: we are ignoring the '.pre' events here as we are already // in the specific state... Math.max( evt.indexOf('ready'), evt.indexOf('ready.post')) >= 0 && this.isReady() && func.call(this) // started... Math.max( evt.indexOf('stopped'), evt.indexOf('stopped.pre'), evt.indexOf('stopped.post')) >= 0 && this.isStopped() && func.call(this) } }], // helpers... restart: ['System/Soft restart', doc`Soft restart This will stop, clear and then start ImageGrid. `, function(){ this .stop() .clear() .start() }], }) var LifeCycle = module.LifeCycle = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'lifecycle', suggested: [ 'logger', ], priority: 'high', actions: LifeCycleActions, }) //--------------------------------------------------------------------- // Serialization... var SerializationActions = actions.Actions({ clone: ['- System/', function(full){ return actions.MetaActions.clone.call(this, full) }], json: ['- System/', function(){ return {} }], load: ['- System/', function(data, merge){ !merge && this.clear() }], clear: ['- Sustem/', function(){ }], }) var Serialization = module.Serialization = ImageGridFeatures.Feature({ title: '', tag: 'serialization', actions: SerializationActions, }) //--------------------------------------------------------------------- // Cache... // XXX should this be in actions.js??? // XXX revise: cache group naming... // currently the used groups are: // Session groups -- cleared on .clear() ('cache') // session-* // view-* // View groups -- cleared by crop/collection ('crop', 'collections') // view-* // Changes groups -- cleared when specific changes are made ('changes') // *-data // *-images // ... // This approach seems not flexible enough... // Ideas: // - use keywords in group names?? // XXX should we consider persistent caches -- localStorage??? // XXX would be nice to have a simple cachedAction(name, cache-tag, expire, func) // action wrapper that would not require anything from the action and // just not call it if already called... var CacheActions = actions.Actions({ config: { // Enable/disable caching... 'cache': true, // Control pre-caching... // // This can be: // true - sync pre-cache (recursion) // 0 - semi-sync pre-cache // number - delay in milliseconds between pre-cache chunks // false - pre-caching disabled 'pre-cache': 0, // Cache chunk length in ms... // // Caching is done in a series of chunks set by this separated by // timeouts set by .config['pre-cache'] to let other stuff run... 'pre-cache-chunk': 8, // Control pre-cache progress display... // // This can be: // false - never show progress // true - always show progress // number - show progress if number of milliseconds has // passed and we are not done yet... // // NOTE: progress will only be displayed if .showProgress(..) // action is available... 'pre-cache-progress': 3000, // Groups to be cleared at the longest on session change... // // These include by default: // 'session' - will live through the whole session. // 'view' - cleared when view changes // 'cache-session-groups': [ 'session', 'view', ], // XXX handler cache.. }, // Cache utility method... // // .cache(title, handler) // -> value // // .cache(group, title, handler) // -> value // // // Example use: // someAction: [ // function(){ // return this.cache('someAction', // function(data){ // if(data){ // // clone/update the data... // // NOTE: this should be faster than the construction // // branch below or this will defeat the purpose // // of caching... // ... // // } else { // // get the data... // ... // } // return data // }) }], // // XXX what should the default group be??? // XXX should this be an action??? __cache: null, cache: function(title, handler){ var group = 'global' // caching disabled... if(!(this.config || {}).cache){ return handler.call(this) } arguments.length > 2 && ([group, title, handler] = arguments) var cache = this.__cache = this.__cache || {} cache = cache[group] = cache[group] || {} return (cache[title] = title in cache ? // pass the cached data for cloning/update to the handler... handler.call(this, cache[title]) : handler.call(this)) }, clearCache: ['System/Clear cache', doc` Clear cache fully... .clearCache() Clear title (global group)... .clearCache(title) Clear title from group... .clearCache(group, title) Clear out the full group... .clearCache(group, '*') NOTE: a group can be a string, list or a regexp object. `, function(title){ var that = this // full clear... if(arguments.length == 0 || (arguments[0] == '*' && arguments[1] == '*')){ delete this.__cache // partial clear... } else { var group = 'global' // both group and title given... arguments.length > 1 && ([group, title] = arguments) // regexp... // NOTE: these are only supported in groups... if(group != '*' && group.includes('*')){ group = new RegExp('^'+ group +'$', 'i') group = Object.keys(this.__cache || {}) .filter(function(g){ return group.test(g) }) } // clear title from each group... if(group == '*' || group instanceof Array || group instanceof RegExp){ ;(group instanceof Array ? group : group instanceof RegExp ? Object.keys(this.__cache || {}) .filter(function(g){ return group.test(g) }) : Object.keys(this.__cache || {})) .forEach(function(group){ that.clearCache(group, title) }) // clear multiple titles... } else if(title instanceof Array){ title.forEach(function(title){ delete ((that.__cache || {})[group] || {})[title] }) // clear group... } else if(title == '*'){ delete (this.__cache || {})[group] // clear title from group... } else { delete ((this.__cache || {})[group] || {})[title] } } }], // special caches... // sessionCache: ['- System/', doc`Add to session cache... .sessionCache(title, handler) -> value This is a shorthand to: .cache('session', title, handler) -> value NOTE: also see .cache(..) `, 'cache: "session" ...'], // XXX doc: what are we precaching??? preCache: ['System/Run pre-cache', doc`Run pre-cache... Do an async pre-cache... .preCache() Do a sync pre-cache... .preCache(true) NOTE: both "modes" of doing a pre-cache run in the main thread, the difference is that the "async" version lets JS run frames between processing sync chunks... NOTE: this will not drop the existing cache, to do this run .clearCache() first or run .reCache(..). `, function(t){ if(this.config.cache){ var t = t || this.config['pre-cache'] || 0 var c = this.config['pre-cache-chunk'] || 8 var done = 0 var attrs = [] for(var k in this){ attrs.push(k) } var l = attrs.length var started = Date.now() var show = this.config['pre-cache-progress'] var tick = function(){ var a = Date.now() var b = a if(attrs.length == 0){ return } while(b - a < c){ this[attrs.pop()] b = Date.now() done += 1 this.showProgress && (show === true || (show && b - started > show)) && this.showProgress('Caching', done, l) } t === true ? tick() : setTimeout(tick, t) }.bind(this) tick() } }], reCache: ['System/Re-cache', function(t){ this .clearCache() .preCache(t) }], toggleHandlerCache: ['System/Action handler cache', makeConfigToggler('action-handler-cache', ['off', 'on']/*, function(state){}*/)], resetHanlerCache: ['System/Reset action handler cache', function(){ delete this.__handler_cache }], }) var Cache = module.Cache = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'cache', // NOTE: we use .showProgress(..) of 'ui-progress' but we do not // need it to work, thus we do not declare it as a dependency... //depends: [], actions: CacheActions, handlers: [ // System... ['start.pre', function(){ this.clearCache() var t = this.config['pre-cache'] t === true ? this.preCache('now') : t >= 0 ? this.preCache() : false }], ['start', function(){ // XXX this breaks loading... // ...not sure why, but when switched on manually // there seems to be no problems... //this.toggleHandlerCache(this.config['action-handler-cache'] || 'on') }], /*/ XXX clear cache when feature/action topology changes... [[ 'inlineMixin', 'inlineMixout', // XXX not sure about this... 'mixout', ], function(){ // XXX should this trigger a recache??? this.clearCache() }], //*/ // clear session cache... ['clear', //'clearCache: "(session|view)(-.*)?" "*" -- Clear session cache'], function(){ this.clearCache(`(${ (this.config['cache-session-groups'] || ['session', 'view']) .join('|') })(-.*)?`) }], ], }) //--------------------------------------------------------------------- // Timers... // Create a debounced action... // // debounce() // debounce(, ) // debounce(, ) // -> function // // options format: // { // timeout: number, // returns: 'cached' | 'dropped', // callback: function(retriggered, args), // } // var debounce = module.debounce = function(options, func){ // parse args... var args = [...arguments] func = args.pop() options = args.pop() || {} if(typeof(options) == typeof(123)){ options.timeout = options } // closure state... var res = undefined var debounced = false var retriggered = 0 var f = function(...args){ if(!debounced){ res = func instanceof Function ? func.call(this, ...args) // alias... : this.parseStringAction.callAction(this, func, ...args) res = options.returns != 'cahced' ? res : undefined // start the timer... debounced = setTimeout( function(){ // callback... options.callback instanceof Function && options.callback.call(this, retriggered, args) // cleanup... retriggered = 0 res = undefined debounced = false }.bind(this), options.timeout || this.config['debounce-action-timeout'] || 200) } else { retriggered++ return res } } f.toString = function(){ return `// debounced...\n${ doc([ func instanceof Function ? func.toString() : func ])}` } return f } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var TimersActions = actions.Actions({ config: { // // Format: // { // : { // // action code (string)... // action: , // // interval in milliseconds... // ms: , // }, // ... // } 'persistent-intervals': null, // A timeout to wait between calls to actions triggered via // .debounce(..) 'debounce-action-timeout': 200, }, // XXX should we store more metadata (ms?) and provide introspection // for these??? __timeouts: null, __intervals: null, __persistent_intervals: null, // Introspection... // // NOTE: these are not editable... get timeouts(){ return Object.assign({}, this.__timeouts || {}) }, get intervals(){ return { volatile: Object.assign({}, this.__intervals || {}), persistent: JSON.parse(JSON.stringify( this.config['persistent-intervals'] || {})), } }, // XXX should these be actions??? isTimeout: function(id){ return id in (this.__timeouts || {}) }, isInterval: function(id){ return id in (this.__intervals || {}) }, isPersistentInterval: function(id){ return id in (this.config['persistent-intervals'] || {}) }, isPersistentIntervalActive: function(id){ return this.isPersistentInterval(id) && (id in (this.__persistent_intervals || {})) }, isTimer: function(id){ return this.isInterval(id) || this.isPersistentInterval(id) || this.isTimeout(id) }, // General API... // // NOTE: we are not trying to re-implement the native scheduler here // just extend it and unify it's uses... setTimeout: ['- System/', function(id, func, ms){ var timeouts = this.__timeouts = this.__timeouts || {} this.clearTimeout(id) timeouts[id] = setTimeout( function(){ // cleanup... // NOTE: we are doing this before we run to avoid // leakage due to errors... delete timeouts[id] // run... func instanceof Function ? func.call(this) : this.call(func) }.bind(this), ms || 0) }], clearTimeout: ['- System/', function(id){ var timeouts = this.__timeouts = this.__timeouts || {} clearTimeout(timeouts[id]) delete timeouts[id] }], setInterval: ['- System/', function(id, func, ms){ var intervals = this.__intervals = this.__intervals || {} id in intervals && clearInterval(intervals[id]) intervals[id] = setInterval( (func instanceof Function ? func : function(){ this.call(func) }) .bind(this), ms || 0) }], clearInterval: ['- System/', function(id){ var intervals = this.__intervals = this.__intervals || {} clearInterval(intervals[id]) delete intervals[id] }], setPersistentInterval: ['- System/', doc` Restart interval id... .setPersistentInterval(id) Save/start interval id... .setPersistentInterval(id, action, ms) `, function(id, action, ms){ var intervals = this.__persistent_intervals = this.__persistent_intervals || {} // NOTE: we set this later iff we make a change... var cfg = this.config['persistent-intervals'] || {} // get defaults... action = action ? action : cfg[id].action ms = ms ? ms : cfg[id].ms // checks... if(!ms || !action){ console.error('Persistent interval: both action and ms must be set.') return } if(typeof(action) != typeof('str')){ console.error('Persistent interval: handler must be a string.') return } id in intervals && clearInterval(intervals[id]) this.config['persistent-intervals'] = cfg cfg[id] = { action: action, ms: ms, } intervals[id] = setInterval( function(){ this.call(action) }.bind(this), ms || 0) }], clearPersistentInterval: ['- System/', function(id, stop_only){ var intervals = this.__persistent_intervals = this.__persistent_intervals || {} clearInterval(intervals[id]) delete intervals[id] if(!stop_only){ delete this.config['persistent-intervals'][id] } }], // XXX revise name (???) // XXX do we need actions other than start/stop ??? persistentIntervals: ['- System/', doc` Start/restart all persistent interval timers... .persistentIntervals('start') .persistentIntervals('restart') Stop all persistent interval timers... .persistentIntervals('stop') NOTE: 'start' and 'restart' are the same, both exist for mnemonics. `, function(action){ var ids = Object.keys(this.config['persistent-intervals'] || {}) // start/restart... ;(action == 'start' || action == 'restart') ? ids.forEach(function(id){ this.setPersistentInterval(id) }.bind(this)) // stop... : action == 'stop' ? ids.forEach(function(id){ this.clearPersistentInterval(id, true) }.bind(this)) // unknown action... : console.error('persistentIntervals: unknown action:', action) }], // Events... // // XXX should these be "aligned" to real time??? // ...i.e. everyHour is triggered on the XX:00:00 and not relative // to start time? // XXX should we macro these??? /*/ XXX would be nice to trigger these ONLY if there are handlers... everySecond: ['- System/', Event(function(){ // XXX })], //*/ everyMinute: ['- System/', Event(function(){ // XXX })], every2Minutes: ['- System/', Event(function(){ // XXX })], every5Minutes: ['- System/', Event(function(){ // XXX })], every10Minutes: ['- System/', Event(function(){ // XXX })], every30Minutes: ['- System/', Event(function(){ // XXX })], everyHour: ['- System/', Event(function(){ // XXX })], // Action debounce... // debounce: ['- System/', doc`Debounce action call... Debouncing prevents an action from being called more than once every timeout milliseconds. Debounce call an action... .debounce(action, ...) .debounce(timeout, action, ...) .debounce(tag, action, ...) .debounce(timeout, tag, action, ...) Debounce call a function... .debounce(tag, func, ...) .debounce(timeout, tag, func, ...) Generic debounce: .debounce(options, action, ...) .debounce(options, func, ...) options format: { // debounce timeout... timeout: , // tag to group action call debouncing (optional) tag: , // controls how the return value is handled: // 'cached' - during the timeout the first return value // is cached and re-returned on each call // during the timeout. // 'dropped' - all return values are ignored/dropped // // NOTE: these, by design, enable only stable/uniform behavior // without introducing any special cases and gotchas... returns: 'cached' | 'dropped', // if true the action will get retriggered after the timeout // is over but only if it was triggered during the timeout... // // NOTE: if the action is triggered more often than timeout/200 // times, then it will not retrigger, this prevents an extra // call after, for example, sitting on a key and triggering // key repeat... retrigger: , // a function, if given will be called when the timeout is up. callback: function(, ), } NOTE: when using a tag, it must not resolve to and action, i.e. this[tag] must not be callable... NOTE: this ignores action return value and returns this... NOTE: this uses core.debounce(..) adding a retrigger option to it... `, function(...args){ // parse the args... if(!(args[0] instanceof Function || typeof(args[0]) == typeof(123) || typeof(args[0]) == typeof('str'))){ var options = args.shift() var tag = options.tag || args[0].name || args[0] } else { var options = { timeout: typeof(args[0]) == typeof(123) ? args.shift() : (this.config['debounce-action-timeout'] || 200), } // NOTE: this[tag] must not be callable, otherwise we treat it // as an action... var tag = (args[0] instanceof Function || this[args[0]] instanceof Function) ? args[0] : args.shift() } // sanity check: when debouncing a function a tag is required... if(tag instanceof Function){ throw new TypeError('debounce: when passing a function a tag is required.') } var action = args.shift() var attr = '__debounce_'+ tag options = Object.assign(Object.create(options), { callback: function(retriggered, args){ // cleanup... delete this[attr] // call the original callback... options.__proto__.callback && options.__proto__.callback.call(that, ...args) if(options.retrigger && retriggered > 0 // this prevents an extra action after "sitting" // on the keyboard and triggering key repeat... && retriggered < (options.timeout || 200) / 200){ var func = this[attr] = this[attr] || debounce(options, action) func.call(this, ...args) } }, }) var func = this[attr] = this[attr] || debounce(options, action) return func.call(this, ...args) }], }) var Timers = module.Timers = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'timers', depends: [ ], actions: TimersActions, handlers: [ // start persistent timers... // XXX should this be start or ready??? ['start', function(){ this.persistentIntervals('start') }], // stop all timers... ['stop', function(){ Object.keys(this.__intervals || {}) .forEach(function(id){ this.clearInterval(id) }.bind(this)) Object.keys(this.__timeouts || {}) .forEach(function(id){ this.clearTimeout(id) }.bind(this)) this.persistentIntervals('stop') }], // fixed timer actions... // XXX not sure about these... ['start', function(){ var m = 1000*60 this .setInterval('everyMinute', 'everyMinute', m) .setInterval('every2Minutes', 'every2Minutes', m*2) .setInterval('every5Minutes', 'every5Minutes', m*5) .setInterval('every10Minutes', 'every10Minutes', m*10) .setInterval('every30Minutes', 'every30Minutes', m*30) .setInterval('everyHour', 'everyHour', m*60) }], ], }) //--------------------------------------------------------------------- // Util... var UtilActions = actions.Actions({ mergeConfig: ['- System/', doc`Merge a config object into .config `, function(config){ config = config instanceof Function ? config.call(this) : typeof(config) == typeof('str') ? this.config[config] : config Object.assign(this.config, config) }], }) var Util = module.Util = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'util', actions: UtilActions, }) //--------------------------------------------------------------------- // Journal... // // This feature logs actions that either have the journal attribute set // to true or have an undo method/alias... // // Example: // someAction: ['Path/to/Some action', // // just journal the action, but it can't be undone... // {journal: true}, // function(){ // ... // }], // // otherAction: ['Path/to/Other action', // // journal and provide undo functionality... // {undo: function(){ // ... // }}, // function(){ // ... // }], // // someOtherAction: ['Path/to/Some other action', // // use .otherAction(..) to undo... // {undo: 'otherAction'}, // function(){ // ... // }], // // NOTE: .undo has priority over .journal, so there is no point of // defining both .journal and .undo action attributes, one is // enough. // // // XXX would be great to add a mechanism define how to reverse actions... // ...one way to do this at this point is to revert to last state // and re-run the journal until the desired event... // XXX need to define a clear journaling strategy in the lines of: // - save state clears journal and adds a state load action // - .load(..) clears journal // XXX need a way to store additional info in the journal... // can either be done as: // - a hook (action handler and/or attr) // - inline code inside the action... // can't say I like #2 as it will mess the code up... // XXX needs careful testing... var JournalActions = actions.Actions({ clone: [function(full){ return function(res){ res.rjournal = null res.journal = null if(full && this.hasOwnProperty('journal') && this.journal){ res.journal = JSON.parse(JSON.stringify(this.journal)) } } }], journal: null, rjournal: null, journalable: null, // XXX doc supported attrs: // undo // undoable // getUndoState // XXX should the action have control over what gets journaled and how??? // XXX should aliases support explicit undo??? updateJournalableActions: ['System/Update list of journalable actions', doc` NOTE: action aliases can not handle undo. `, function(){ var that = this var handler = function(action){ return function(){ var cur = this.current var args = [...arguments] var data = { type: 'basic', action: action, args: args, // the current image before the action... current: cur, // the target (current) image after action... target: this.current, } // test if we need to journal this action signature... var test = that.getActionAttr(action, 'undoable') if(test && !test.call(that, data)){ return } // get additional undo state... var update = that.getActionAttr(action, 'getUndoState') while(typeof(update) == typeof('str')){ update = that.getActionAttr(update, 'getUndoState') } update && update instanceof Function && update.call(that, data) // journal after the action is done... return function(){ this.journalPush(data) } } } this.journalable = this.actions .filter(function(action){ // skip aliases... return !(that[action] instanceof actions.Alias) && (!!that.getActionAttr(action, 'undo') || !!that.getActionAttr(action, 'journal')) }) // reset the handler .map(function(action){ that .off(action+'.pre', 'journal-handler') .on(action+'.pre', 'journal-handler', handler(action)) return action }) }], journalPush: ['- System/Journal/Add an item to journal', function(data){ // clear the reverse journal... this.rjournal && (this.rjournal = null) this.journal = (this.hasOwnProperty('journal') || this.journal) ? this.journal || [] : [] this.journal.push(data) }], clearJournal: ['System/Journal/Clear the action journal', function(){ // NOTE: overwriting here is better as it will keep // shadowing the parent's .journal in case we // are cloned. // NOTE: either way this will have no effect as we // only use the local .journal but the user may // get confused... //delete this.journal this.journal && (this.journal = null) this.rjournal && (this.rjournal = null) }], runJournal: ['- System/Journal/Run journal', //{journal: true}, function(journal){ var that = this journal.forEach(function(e){ // load state... that .focusImage(e.current) // run action... [e.action].apply(that, e.args) }) }], // XXX needs very careful revision... // - should this be thread safe??? (likely not) // - should the undo action have side-effects on the // journal/rjournal or should we clean them out??? // (currently cleaned) // XXX should we control what gets pushed to the journal??? // XXX should we run undo of every action that supports it in the chain??? // ...i.e. multiple extending actions can support undo // XXX will also need to handle aliases in chain... undo: ['Edit/Undo', doc`Undo last action from .journal that can be undone .undo() This will shift the action from .journal to .rjournal preparing it for .redo() NOTE: this will remove all the non undoable actions from the .journal up until and including the undone action. NOTE: only the undone action is pushed to .rjournal `, {mode: function(){ return (this.journal && this.journal.length > 0) || 'disabled' }}, function(){ var journal = this.journal.slice() || [] var rjournal = this.rjournal = (this.hasOwnProperty('rjournal') || this.rjournal) ? this.rjournal || [] : [] for(var i = journal.length-1; i >= 0; i--){ var a = journal[i] // see if the action has an explicit undo attr... var undo = this.getActionAttr(a.action, 'undo') // general undo... if(undo){ // restore focus to where it was when the action // was called... this.focusImage(a.current) // call the undo method/action... // NOTE: this is likely to have side-effect on the // journal and maybe even rjournal... // NOTE: these side-effects are cleaned out later. var undo = undo instanceof Function ? // pass the action name... undo.call(this, a) : typeof(undo) == typeof('str') ? // XXX pass journal structure as-is... (???) this[undo].apply(this, a.args) : null // push the undone command to the reverse journal... rjournal.push(journal.splice(i, 1)[0]) // restore journal state... // NOTE: calling the undo action would have cleared // the rjournal and added stuff to the journal // so we will need to restore things... this.journal = journal this.rjournal = rjournal break } } }], redo: ['Edit/Redo', doc`Redo an action from .rjournal .redo() Essentially this will remove and re-run the last action in .rjournal `, {mode: function(){ return (this.rjournal && this.rjournal.length > 0) || 'disabled' }}, function(){ if(!this.rjournal || this.rjournal.length == 0){ return } this.runJournal([this.rjournal.pop()]) }], }) var Journal = module.Journal = ImageGridFeatures.Feature({ title: 'Action Journal', tag: 'journal', depends: [ 'serialization', ], actions: JournalActions, // XXX need to drop journal on save... // XXX rotate/truncate journal??? // XXX need to check that all the listed actions are clean -- i.e. // running the journal will produce the same results as user // actions that generated the journal. handlers: [ // log state, action and its args... ['start', function(){ this.updateJournalableActions() }], ], }) //--------------------------------------------------------------------- // Changes API... var ChangesActions = actions.Actions({ // This can be: // - null/undefined - write all // - true - write all // - false - write nothing // - { // // write/skip data... // data: , // // // write/skip images or write a diff including the given // // s only... // images: | [ , ... ], // // // write/skip tags... // tags: , // // // write/skip bookmarks... // bookmarked: , // // // write/skip selected... // selected: , // // // feature specific custom flags... // ... // } // // NOTE: in the complex format all fields ar optional; if a field // is not included it is not written (same as when set to false) // NOTE: .current is written always. // // XXX this should be a prop to enable correct changes tracking via // events... chages: null, get _changes(){ return this.__changes }, // XXX proxy to .markChanged(..) set _changes(value){}, clone: [function(full){ return function(res){ res.changes = null if(full && this.hasOwnProperty('changes') && this.changes){ res.changes = JSON.parse(JSON.stringify(this.changes)) } } }], // XXX this should also track .changes... // ...would also need to make this applicable to changes, // i.e. x.markChanged(x.changes) markChanged: ['- System/', doc`Mark data sections as changed... Mark everything changed... .markChanged('all') Mark nothing changed... .markChanged('none') Mark section(s) as changed... .markChanged(
) .markChanged(
, ..) .markChanged([
, ..]) Mark item(s) of section as changed... .markChanged(
, [, .. ]) NOTE: items must be strings... NOTE: when marking section items, the new items will be added to the set of already marked items. NOTE: when .changes is null (i.e. everything changed, marked via .markChanged('all')) then calling this with anything other than 'none' will have no effect. `, function(section, items){ var that = this var args = section instanceof Array ? section : [...arguments] //var changes = this.changes = var changes = this.hasOwnProperty('changes') ? this.changes || {} : {} // all... if(args.length == 1 && args[0] == 'all'){ this.changes = true // none... } else if(args.length == 1 && args[0] == 'none'){ this.changes = false // everything is marked changed, everything will be saved // anyway... // NOTE: to reset this use .markChanged('none') and then // manually add the desired changes... } else if(this.changes == null){ return // section items... } else if(items instanceof Array) { if(changes[section] === true){ return } changes[section] = (changes[section] || []) .concat(items) .unique() this.changes = changes // section(s)... } else { args.forEach(function(arg){ changes[arg] = true }) this.changes = changes } }], }) var Changes = module.Changes = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'changes', depends: [ 'serialization', ], actions: ChangesActions, handlers: [ // handle changes... ['json', function(res, mode){ if(this.changes != null){ res.changes = JSON.parse(JSON.stringify(this.changes)) } }], ['load', function(_, data){ if(data.changes){ this.changes = JSON.parse(JSON.stringify(data.changes)) } }], // clear caches relating to stuff we just changed... ['markChanged', function(_, section){ section = (section instanceof Array ? section : [section]) .map(function(section){ return '.*-'+section }) this.clearCache(section, '*') }], ], }) //--------------------------------------------------------------------- // Workspace... // // Basic protocol: // A participating feature should: // - react to .saveWorkspace(..) by saving it's relevant state data to the // object returned by the .saveWorkspace() action. // NOTE: it is recommended that a feature save its relevant .config // data as-is. // NOTE: no other action or state change should be triggered by this. // - react to .loadWorkspace(..) by loading it's state from the returned // object... // NOTE: this can be active, i.e. a feature may call actions when // handling this. // - react to .toggleChrome(..) and switch on and off the chrome // visibility... (XXX) // // // Helpers... var makeWorkspaceConfigWriter = module.makeWorkspaceConfigWriter = function(keys, callback){ return function(workspace){ var that = this var data = keys instanceof Function ? keys.call(this) : keys // store data... data.forEach(function(key){ workspace[key] = JSON.parse(JSON.stringify(that.config[key])) }) callback && callback.call(this, workspace) } } // XXX should this delete a prop if it's not in the loading workspace??? // XXX only replace a prop if it has changed??? // XXX handle defaults -- when a workspace was just created... var makeWorkspaceConfigLoader = module.makeWorkspaceConfigLoader = function(keys, callback){ return function(workspace){ var that = this var data = keys instanceof Function ? keys.call(this) : keys // load data... data.forEach(function(key){ // the key exists... if(key in workspace){ that.config[key] = JSON.parse(JSON.stringify(workspace[key])) // no key set... // XXX is this the right way to go??? } else { delete that.config[key] } }) callback && callback.call(this, workspace) } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX need a way to handle defaults... var WorkspaceActions = actions.Actions({ config: { 'load-workspace': 'default', 'workspace': 'default', 'workspaces': {}, }, get workspace(){ return this.config.workspace }, set workspace(value){ this.loadWorkspace(value) }, get workspaces(){ return this.config.workspaces }, getWorkspace: ['- Workspace/', function(){ return this.saveWorkspace(null) }], // NOTE: these are mainly triggers for other features to save/load // their specific states... // NOTE: handlers should only set data on the workspace object passively, // no activity is recommended. // NOTE: if null is passed this will only get the data, but will // save nothing. this us useful for introspection and temporary // context storage. // // XXX for some reason this does not get saved with .config... saveWorkspace: ['Workspace/Save Workspace', function(name){ if(!this.config.hasOwnProperty('workspaces')){ this.config['workspaces'] = JSON.parse(JSON.stringify(this.config['workspaces'])) } var res = {} if(name !== null){ this.config['workspaces'][name || this.config.workspace] = res } return res }], // NOTE: merging the state data is the responsibility of the feature // ...this is done so as not to restrict the feature to one // specific way to do stuff... loadWorkspace: ['Workspace/Load Workspace', function(name){ name = name || this.config.workspace // get a workspace by name and load it... if(typeof(name) == typeof('str')){ this.config.workspace = name return this.workspaces[name] || {} // we got the workspace object... } else { return name } }], // NOTE: this will not save the current workspace... toggleWorkspace: ['Workspace/workspace', makeConfigToggler('workspace', function(){ return Object.keys(this.config['workspaces']) }, function(state){ this.loadWorkspace(state) })], // XXX should we keep the stack unique??? pushWorkspace: ['- Workspace/', function(name){ name = name || this.workspace var stack = this.__workspace_stack = this.__workspace_stack || [] this.saveWorkspace() if(stack.slice(-1)[0] == name){ return } this.workspace != name && this.loadWorkspace(name) stack.push(name) }], popWorkspace: ['- Workspace/', function(){ var stack = this.__workspace_stack if(!stack || stack.length == 0){ return } this.saveWorkspace() this.loadWorkspace(stack.pop()) }], }) var Workspace = module.Workspace = ImageGridFeatures.Feature({ title: '', tag: 'workspace', depends: [ 'lifecycle', ], actions: WorkspaceActions, handlers: [ ['start', function(){ this.loadWorkspace(this.config['load-workspace'] || 'default') }], // NOTE: this needs to be done before the .config is saved... ['stop.pre', function(){ this.saveWorkspace() }], ], }) //--------------------------------------------------------------------- // Tasks... // Task wrapper... // // This simply makes tasks actions discoverable... var Task = module.Task = function(func){ func.__task__ = true return func } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Task action action helpers... // // NOTE: for examples see: // features/examples.js: // ExampleActions.exampleTask(..) // ExampleActions.exampleSessionTask(..) // // NOTE: we can pass sync/async to this in two places, in definition: // var action = taskAction('some title', 'sync', function(..){ .. }) // or // var action = taskAction('sync', 'some title', function(..){ .. }) // // and on call: // action('sync', ..) // // during the later form 'sync' is passed to .Task(..) in the correct // position... // (see ig-types' runner.TaskManager(..) for more info) // // XXX might be nice to add metadata like start times and the like... var taskAction = module.taskAction = function(title, func){ var pre_args = [...arguments] func = pre_args.pop() title = pre_args .filter(function(t){ return t != 'sync' && t != 'async' }) .pop() var action return (action = object.mixin( Task(function(...args){ if(args[0] == 'sync' || args[0] == 'async'){ pre_args = [args.shift(), title] } return this.tasks.Task(...pre_args, func.bind(this), ...args) }), { __task_title__: title, toString: function(){ return `core.taskAction('${ title }', \n\t${ object.normalizeIndent('\t'+func.toString()) })` }, })) } var sessionTaskAction = module.sessionTaskAction = function(title, func){ return object.mixin( taskAction(...arguments), { __session_task__: true }) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX add a task manager UI... var TaskActions = actions.Actions({ config: { }, // tests... isTask: function(action){ return !!this.getActionAttr(action, '__task__') }, isSessionTask: function(action){ return !!this.getActionAttr(action, '__session_task__') }, // list actions that generate tasks... // // XXX cache these??? get taskActions(){ var test = this.isTask.bind(this) return this.actions.filter(test) }, get sessionTaskActions(){ var test = this.isSessionTask.bind(this) return this.actions.filter(test) }, // task manager... // __task_manager__: runner.TaskManager, __tasks: null, get tasks(){ return (this.__tasks = this.__tasks || this.__task_manager__()) }, // session tasks are stopped when the index is cleared... get sessionTasks(){ return this.tasks.titled(...this.sessionTaskActions) }, }) var Tasks = module.Tasks = ImageGridFeatures.Feature({ title: '', tag: 'tasks', depends: [ ], actions: TaskActions, handlers: [ ['clear', 'sessionTasks.stop'], ], }) //--------------------------------------------------------------------- // Self test framework... // Indicate an action to be a self-test action... // // Self test actions are run by .selfTest(..) // // XXX should we set an action attr or a func attr here??? var selfTest = module.selfTest = function(func){ func.__self_test__ = true return func } var SelfTestActions = actions.Actions({ config: { 'run-selftest-on-start': true, }, selfTest: ['System/Run self test', selfTest(function(mode){ var that = this var logger = this.logger && this.logger.push('Self test') var tests = this.actions .filter(function(action){ return action != 'selfTest' && (that[action].func.__self_test__ || that.getActionAttr(action, 'self_test'))}) logger && tests.forEach(function(action){ logger.emit('found', action) }) tests.forEach(function(action){ that[action]() logger && logger.emit('done', action) }) })], }) var SelfTest = module.SelfTest = ImageGridFeatures.Feature({ title: '', doc: '', tag: 'self-test', depends: [ 'lifecycle' ], suggested: [ 'logger', ], priority: 'low', actions: SelfTestActions, handlers: [ ['start', function(){ this.config['run-selftest-on-start'] && this.selfTest() }] ], }) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })