mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
3277 lines
87 KiB
JavaScript
Executable File
3277 lines
87 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
* 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:
|
|
* - util
|
|
* - introspection
|
|
* - logger
|
|
* - 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...
|
|
* - journal
|
|
* action journaling and undo/redo functionality
|
|
* XXX needs revision...
|
|
* - changes
|
|
* change tracking
|
|
* - workspace
|
|
* XXX needs revision...
|
|
* - tasks
|
|
* tasks -- manage long running actions
|
|
* queue -- manage lots of small actions as a single task
|
|
* - self-test
|
|
* basic framework for running test actions at startup...
|
|
*
|
|
*
|
|
* XXX some actions use the .clone(..) action/protocol, should this be
|
|
* defined here???
|
|
* XXX should this be split into a generic app building lib?
|
|
*
|
|
**********************************************************************/
|
|
((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')
|
|
|
|
|
|
// code/text normalization...
|
|
var doc = module.doc = actions.doc
|
|
var text = module.text = object.text
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// Root ImageGrid.viewer object constructor...
|
|
//
|
|
// This adds:
|
|
// - toggler as action compatibility
|
|
//
|
|
var ImageGridMetaActions =
|
|
module.ImageGridMetaActions = {
|
|
__proto__: actions.MetaActions,
|
|
|
|
// 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),
|
|
}
|
|
}
|
|
}),
|
|
}
|
|
|
|
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 PWA / 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
|
|
// node mode...
|
|
&& typeof(document) != 'undefined'){
|
|
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 }
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// Util...
|
|
|
|
// Toggle value in .config...
|
|
//
|
|
// 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)
|
|
}
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
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,
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// Introspection...
|
|
|
|
// 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:
|
|
<function> - action method.
|
|
<action-name> - 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,
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// 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...`,
|
|
|
|
root: null,
|
|
parent: null,
|
|
|
|
// Quiet mode...
|
|
//
|
|
// NOTE: if local mode is not defined this will get the mode of
|
|
// the nearest parent...
|
|
// XXX need these to be persistent...
|
|
// XXX add support for log levels...
|
|
__quiet: null,
|
|
get quiet(){
|
|
var cur = this
|
|
while(cur.__quiet == null && cur.parent){
|
|
cur = cur.parent }
|
|
return !!cur.__quiet },
|
|
set quiet(value){
|
|
value == null ?
|
|
(delete this.__quiet)
|
|
: (this.__quiet = !!value) },
|
|
|
|
__context: null,
|
|
get context(){
|
|
return this.__context || this.root.__context },
|
|
|
|
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){
|
|
// settings...
|
|
var attrs = typeof(msg.last()) != typeof('str') ?
|
|
msg.pop()
|
|
: {}
|
|
return msg.length == 0 ?
|
|
this
|
|
: Object.assign(
|
|
this.constructor(),
|
|
attrs,
|
|
{
|
|
root: this.root,
|
|
parent: this,
|
|
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)) },
|
|
set logger(value){
|
|
this.__logger = value },
|
|
|
|
// XXX move this to console-logger???
|
|
// XXX should this be an action???
|
|
handleLogItem: ['- System/',
|
|
function(logger, path, status, ...rest){
|
|
logger.quiet
|
|
|| logger.root.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,
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// System life-cycle...
|
|
|
|
// XXX potential pitfall: setting up new features without restarting...
|
|
// this can happen for instance in ig.js when starting a minimal
|
|
// imagegrid instance and then adding new features -- these new
|
|
// features will not get their .start() (and friends) run...
|
|
// There are three stages here:
|
|
// - feature setup
|
|
// things the feature needs to run -- <feature>.setup(..)
|
|
// - app start
|
|
// things the app wants to do on start
|
|
// - ???
|
|
// things feature action needs to run in cli should be
|
|
// documented and not depend on .start(..)
|
|
// ...or there should be a way to "start" the new features...
|
|
// XXX put this in the docs...
|
|
// 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.push('System').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.push('System').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.push('System').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 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...
|
|
// ...to do this we'll need to be able to select a value by args
|
|
// from the cache this will require a diff mattch or something
|
|
// similar...
|
|
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: null,
|
|
cache: doc('Get or set cache value',
|
|
doc`Get or set cache value
|
|
|
|
.cache(title, handler)
|
|
-> value
|
|
|
|
.cache(group, title, handler)
|
|
-> value
|
|
|
|
|
|
Currently the used groups are:
|
|
Session groups -- cleared on .clear() (feature: 'cache')
|
|
session-*
|
|
view-*
|
|
View groups -- cleared by crop/collection (feature: 'crop', 'collections')
|
|
view-*
|
|
Changes groups -- cleared when specific changes are made (feature: 'changes')
|
|
*-data
|
|
*-images
|
|
...
|
|
|
|
|
|
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
|
|
}) }],
|
|
|
|
|
|
NOTE: since this is here to help speed things up, introducing a
|
|
small but not necessary overhead by making this an action is
|
|
not logical...
|
|
`,
|
|
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(<func>)
|
|
// debounce(<timeout>, <func>)
|
|
// debounce(<options>, <func>)
|
|
// -> function
|
|
//
|
|
// options format:
|
|
// {
|
|
// timeout: number,
|
|
// returns: 'cached' | 'dropped',
|
|
// callback: function(retriggered, args),
|
|
//
|
|
// postcall: true,
|
|
// }
|
|
//
|
|
// XXX might be a good ide to move this someplace generic...
|
|
// XXX this is not debouncing pre/post calls, just the base action...
|
|
var debounce =
|
|
module.debounce =
|
|
function(options, func){
|
|
// parse args...
|
|
var args = [...arguments]
|
|
func = args.pop()
|
|
options = args.pop() || {}
|
|
|
|
typeof(options) == typeof(123)
|
|
&& (options.timeout = options)
|
|
|
|
// closure state...
|
|
var res
|
|
var last_args
|
|
var debounced = false
|
|
var retriggered = 0
|
|
|
|
// call the action...
|
|
var call = function(context, ...args){
|
|
return func instanceof Function ?
|
|
func.call(context, ...args)
|
|
// alias...
|
|
: this.parseStringAction.callAction(context, func, ...args) }
|
|
|
|
return object.mixin(
|
|
function(...args){
|
|
var retrigger
|
|
// call...
|
|
if(!debounced){
|
|
res = call(this, ...args)
|
|
res = options.returns != 'cahced' ?
|
|
res
|
|
: undefined
|
|
|
|
// start the timer...
|
|
debounced = setTimeout(
|
|
function(){
|
|
var c
|
|
// callback...
|
|
options.callback instanceof Function
|
|
&& (c = options.callback.call(this, retriggered, args))
|
|
// retrigger...
|
|
options.postcall
|
|
&& retriggered > 0
|
|
&& c !== false
|
|
// XXX should this be a debounced call or a normal call...
|
|
// XXX this is not the actual action thus no
|
|
// handlers will be triggered...
|
|
&& call(this, ...last_args)
|
|
// cleanup...
|
|
retriggered = 0
|
|
res = undefined
|
|
debounced = false }.bind(this),
|
|
options.timeout
|
|
|| this.config['debounce-action-timeout']
|
|
|| 200)
|
|
// skip...
|
|
} else {
|
|
retriggered++
|
|
last_args = args
|
|
return res } },
|
|
{
|
|
toString: function(){
|
|
return `// debounced...\n${
|
|
doc([ func instanceof Function ?
|
|
func.toString()
|
|
: func ])}` },
|
|
}) }
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
var TimersActions = actions.Actions({
|
|
config: {
|
|
//
|
|
// Format:
|
|
// {
|
|
// <id>: {
|
|
// // action code (string)...
|
|
// action: <action>,
|
|
// // interval in milliseconds...
|
|
// ms: <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: <milliseconds>,
|
|
|
|
// tag to group action call debouncing (optional)
|
|
tag: <string>,
|
|
|
|
// 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: <bool>,
|
|
|
|
// a function, if given will be called when the timeout is up.
|
|
callback: function(<retrigger-count>, <args>),
|
|
}
|
|
|
|
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)
|
|
}],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// 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...
|
|
|
|
var ChangesActions = actions.Actions({
|
|
// This can be:
|
|
// - null/undefined - write all
|
|
// - true - write all
|
|
// - false - write nothing
|
|
// - {
|
|
// // write/skip data...
|
|
// data: <bool>,
|
|
//
|
|
// // write/skip images or write a diff including the given
|
|
// // <gid>s only...
|
|
// images: <bool> | [ <gid>, ... ],
|
|
//
|
|
// // write/skip tags...
|
|
// tags: <bool>,
|
|
//
|
|
// // write/skip bookmarks...
|
|
// bookmarked: <bool>,
|
|
//
|
|
// // write/skip selected...
|
|
// selected: <bool>,
|
|
//
|
|
// // 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(<section>)
|
|
.markChanged(<section>, ..)
|
|
.markChanged([<section>, ..])
|
|
|
|
Mark item(s) of section as changed...
|
|
.markChanged(<section>, [<item>, .. ])
|
|
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 and Queues...
|
|
|
|
// Task wrapper...
|
|
//
|
|
// This simply makes tasks actions discoverable...
|
|
var Task =
|
|
module.Task =
|
|
function(func){
|
|
func.__task__ = true
|
|
return func }
|
|
|
|
|
|
// Task 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)
|
|
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 (object.mixin(
|
|
action = Task(function(...args){
|
|
if(args[0] == 'sync' || args[0] == 'async'){
|
|
pre_args = [args.shift(), title] }
|
|
return Object.assign(
|
|
this.tasks.Task(...pre_args, func.bind(this), ...args),
|
|
// make this searchable by .tasks.named(..)...
|
|
{
|
|
__session_task__: !!action.__session_task__,
|
|
name: action.name,
|
|
}) }),
|
|
{
|
|
title,
|
|
toString: function(){
|
|
return `core.taskAction('${ action.name }', \n\t${
|
|
object.normalizeIndent('\t'+func.toString()) })` },
|
|
})) }
|
|
|
|
var sessionTaskAction =
|
|
module.sessionTaskAction =
|
|
function(title, func){
|
|
return object.mixin(
|
|
taskAction(...arguments),
|
|
{ __session_task__: true }) }
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// Queued wrapper...
|
|
var Queued =
|
|
module.Queued =
|
|
function(func){
|
|
func.__queued__ = true
|
|
return Task(func) }
|
|
|
|
|
|
// Queued action...
|
|
//
|
|
// queuedAction(title, func)
|
|
// queuedAction(title, options, func)
|
|
// -> action
|
|
//
|
|
// func(..)
|
|
// -> res
|
|
//
|
|
// action(..)
|
|
// -> promise(res)
|
|
//
|
|
//
|
|
// The idea here is that each time a queued action is called it is run
|
|
// in a queue, and while it is running all consecutive calls are queued
|
|
// and run according to the queue policy.
|
|
//
|
|
//
|
|
// NOTE: for examples see:
|
|
// features/examples.js:
|
|
// ExampleActions.exampleQueuedAction(..)
|
|
// ExampleActions.exampleMultipleQueuedAction(..)
|
|
//
|
|
// XXX handle errors... (???)
|
|
// XXX revise logging and logger passing...
|
|
var queuedAction =
|
|
module.queuedAction =
|
|
function(title, func){
|
|
var args = [...arguments]
|
|
func = args.pop()
|
|
var [title, opts] = args
|
|
|
|
var action
|
|
return object.mixin(
|
|
action = Queued(function(...args){
|
|
var that = this
|
|
return new Promise(function(resolve, reject){
|
|
Object.assign(
|
|
that.queue(title, opts || {})
|
|
.push(function(){
|
|
var res = func.call(that, ...args)
|
|
resolve(res)
|
|
return res }),
|
|
{
|
|
__session_task__: !!action.__session_task__,
|
|
title: action.name,
|
|
}) }) }),
|
|
{
|
|
title,
|
|
toString: function(){
|
|
return `core.queuedAction('${action.name}',\n\t${
|
|
object.normalizeIndent( '\t'+ func.toString() ) })` },
|
|
}) }
|
|
|
|
var sessionQueueAction =
|
|
module.sessionQueueAction =
|
|
function(title, func){
|
|
return object.mixin(
|
|
queuedAction(...arguments),
|
|
{ __session_task__: true }) }
|
|
|
|
|
|
// Queue action handler...
|
|
//
|
|
// queueHandler(title[, opts], func)
|
|
// queueHandler(title[, opts], arg_handler, func)
|
|
// -> action
|
|
//
|
|
// Chained queue handler...
|
|
// queueHandler(title[, opts], queueHandler(..), func)
|
|
// -> action
|
|
//
|
|
//
|
|
// Prepare args...
|
|
// arg_handler(queue, items, ...args)
|
|
// -> [items, ...args]
|
|
//
|
|
// Prepare args in sync mode...
|
|
// arg_handler('sync', items, ...args)
|
|
// -> [items, ...args]
|
|
//
|
|
//
|
|
// Call action...
|
|
// action(items, ...args)
|
|
// -> promise
|
|
//
|
|
// Call action in sync mode...
|
|
// action('sync', items, ...args)
|
|
// -> promise
|
|
//
|
|
//
|
|
// Action function...
|
|
// func(item, ...args)
|
|
// -> res
|
|
//
|
|
//
|
|
// This is different from queuedAction(..) in that what is queued is not
|
|
// the action itself but rather the first argument to that action and the
|
|
// action is used by the queue to handle each item. The rest of the
|
|
// arguments are passed to each call.
|
|
//
|
|
// In 'sync' mode the action is run outside of queue/task right away, this
|
|
// is done because for a queue we can only control the sync start, i.e.
|
|
// the first task execution, the rest depends on queue configuration
|
|
// thus making the final behaviour unpredictable.
|
|
//
|
|
//
|
|
// NOTE: sync-mode actions do not externally log anything, basic progress
|
|
// logging is handled by the queue/task which is not created in sync
|
|
// mode.
|
|
// NOTE: since the sync-mode can block it must be used very carefully.
|
|
// NOTE: for an example of chaining several queues see features/examples's:
|
|
// .exampleChainedQueueHandler(..)
|
|
// NOTE: when chaining queues, in 'sync' mode all queues in the chain will
|
|
// be run sync...
|
|
// NOTE: when chaining arg_handler(..) will get one queue per level of
|
|
// chaining, but in 'sync' mode only one 'sync' is passed...
|
|
// NOTE: when calling this multiple times for the same queue each call
|
|
// will call all the stages but since items are processes async the
|
|
// later calls' later stages may end up with empty input queues,
|
|
// e.g. for:
|
|
// [1,2,3].map(e => ig.exampleChainedQueueHandler(e))
|
|
// .exampleChainedQueueHandler(..) is called once per input and thus
|
|
// the first two stages are called sync and by the time the last
|
|
// stage of the first call is triggered (async) all the inputs are
|
|
// ready thus the first call will process all the inputs and the
|
|
// later calls will get empty inputs (unless any new inputs are while
|
|
// processing added)...
|
|
// i.e. within a queue/task async processing model there is no guarantee
|
|
// that the item will be processed in the same call tree that it
|
|
// was added in...
|
|
//
|
|
// XXX might be a good idea to split this into a generic and domain parts
|
|
// and move the generic part into types/runner...
|
|
// XXX check if item is already in queue (???)
|
|
// ...how do we identify item uniqueness??
|
|
var queueHandler =
|
|
module.queueHandler =
|
|
function(title, func){
|
|
var args = [...arguments]
|
|
func = args.pop()
|
|
var arg_handler =
|
|
typeof(args.last()) == 'function'
|
|
&& args.pop()
|
|
var [title, opts] = args
|
|
|
|
var action
|
|
return object.mixin(
|
|
action = Queued(function(items, ...args){
|
|
var that = this
|
|
var inputs = [...arguments]
|
|
|
|
// sync start...
|
|
if(inputs[0] == 'sync' || inputs[0] == 'async'){
|
|
var [sync, [items, ...args]] = [inputs.shift(), inputs] }
|
|
|
|
// XXX see arg_handler(..) note below...
|
|
var q
|
|
|
|
// pre-process args (sync)...
|
|
arg_handler
|
|
&& (inputs = arg_handler.call(this,
|
|
sync == 'sync' ?
|
|
sync
|
|
// XXX should this be a queue???
|
|
// ...seems like this is either 'sync' or
|
|
// undefined but never a queue at this stage...
|
|
: q,
|
|
...inputs))
|
|
// special-case: empty inputs -- no need to handle anything...
|
|
if(inputs instanceof Array
|
|
&& (inputs.length == 0
|
|
|| (inputs[0] || []).length == 0)){
|
|
return Promise.resolve(inputs[0] || []) }
|
|
|
|
// Define the runner and prepare...
|
|
//
|
|
// sync mode -- run action outside of queue...
|
|
// NOTE: running the queue in sync mode is not practical as
|
|
// the results may depend on queue configuration and
|
|
// size...
|
|
if(sync == 'sync'){
|
|
var run = function([items, ...args]){
|
|
return Promise.all(
|
|
(items instanceof Array ?
|
|
items
|
|
: [items])
|
|
.map(function(item){
|
|
var res = func.call(that, item, ...args)
|
|
return res === runner.SKIP ?
|
|
[]
|
|
: [res] })
|
|
.flat()) }
|
|
// queue mode...
|
|
} else {
|
|
// prep queue...
|
|
q = that.queue(title,
|
|
Object.assign(
|
|
{},
|
|
opts || {},
|
|
{
|
|
__session_task__: !!action.__session_task__,
|
|
handler: function([item, args]){
|
|
return func.call(that, item, ...(args || [])) },
|
|
}))
|
|
q.title = action.name
|
|
|
|
var run = function([items, ...args]){
|
|
// fill the queue...
|
|
// NOTE: we are also adding a ref to args here to keep things consistent...
|
|
args.length > 0
|
|
&& (args = [args])
|
|
q.add(items instanceof Array ?
|
|
items
|
|
// move the inputs out of the input array...
|
|
// NOTE: this will prevent the items from getting
|
|
// processed multiple times when the action
|
|
// is called multiple times...
|
|
.splice(0, items.length)
|
|
.map(function(e){
|
|
return [e, ...args] })
|
|
: [[items, ...args]])
|
|
// XXX do we .flat(..) the results???
|
|
//return q.promise() } }
|
|
return q.promise()
|
|
.then(function(res){
|
|
return res && res.flat() }) } }
|
|
|
|
// run...
|
|
return (inputs instanceof Promise
|
|
|| inputs instanceof runner.FinalizableQueue) ?
|
|
inputs.then(
|
|
function(items){
|
|
return run([items, ...args]) },
|
|
function(){
|
|
q && q.abort() })
|
|
: run(inputs) }),
|
|
{
|
|
title,
|
|
arg_handler,
|
|
handler: func,
|
|
|
|
toString: function(){
|
|
// XXX add opts if given...
|
|
return `core.queueHandler('${action.name}',\n${
|
|
(arg_handler ?
|
|
object.normalizeIndent('\t'+arg_handler.toString()).indent('\t') + ',\n'
|
|
: '')
|
|
+ object.normalizeIndent('\t'+func.toString()).indent('\t') })` },
|
|
}) }
|
|
|
|
var sessionQueueHandler =
|
|
module.sessionQueueHandler =
|
|
function(title, func){
|
|
return object.mixin(
|
|
queueHandler(...arguments),
|
|
{ __session_task__: true }) }
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// XXX revise logging and logger passing...
|
|
// XXX add a task manager UI...
|
|
// XXX might be a good idea to confirm session task stops when loading a
|
|
// new index...
|
|
var TaskActions = actions.Actions({
|
|
config: {
|
|
'context-exclude-attrs': [
|
|
// NOTE: these are commented out so as to reuse contexts where
|
|
// possible... (XXX)
|
|
//'__links',
|
|
//'__isolated',
|
|
|
|
// keep all the tasks/queues in one pool...
|
|
//
|
|
// NOTE: a linked context in here can stop main tasks and
|
|
// vise versa...
|
|
// XXX what else should we isolate from the clone???
|
|
'__tasks',
|
|
'__queues',
|
|
|
|
'features',
|
|
],
|
|
},
|
|
|
|
// Tasks...
|
|
//
|
|
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(){
|
|
return this.cache('taskActions', function(data){
|
|
return data
|
|
|| this.actions.filter(this.isTask.bind(this)) }) },
|
|
get sessionTaskActions(){
|
|
return this.cache('sessionTaskActions', function(data){
|
|
return data
|
|
|| this.actions.filter(this.isSessionTask.bind(this)) }) },
|
|
|
|
// task manager...
|
|
//
|
|
__task_manager__: runner.TaskManager,
|
|
__tasks: null,
|
|
get tasks(){
|
|
return (this.__tasks =
|
|
this.__tasks
|
|
|| this.__task_manager__()) },
|
|
// NOTE: session tasks are stopped when the index is cleared...
|
|
// XXX do we need to cache this...
|
|
// ...if yes then we'll need to also clear/update the cache
|
|
// every time a task is run/stopped...
|
|
get sessionTasks(){
|
|
return this.tasks
|
|
.filter(function(task){
|
|
return task.__session_task__ }) },
|
|
|
|
|
|
// Queue (task)...
|
|
//
|
|
isQueued: function(action){
|
|
return !!this.getActionAttr(action, '__queued__') },
|
|
// XXX cache this???
|
|
// XXX need to get running tasks by action name...
|
|
get queuedActions(){
|
|
return this.cache('queuedActions', function(data){
|
|
return data
|
|
|| this.actions.filter(this.isQueued.bind(this)) }) },
|
|
|
|
// XXX need a way to reference the queue again...
|
|
// .tasks.titled(name) will return a list...
|
|
__queues: null,
|
|
get queues(){
|
|
return (this.__queues = this.__queues || {}) },
|
|
|
|
// XXX test hidden progress...
|
|
// XXX revise logging and logger passing...
|
|
// XXX need better error flow...
|
|
queue: doc('Get or create a queue task',
|
|
doc`Get or create a queue task...
|
|
|
|
.queue(name)
|
|
.queue(name, options)
|
|
-> queue
|
|
|
|
If a queue with the given name already exits it will be returned
|
|
and options and logger are ignored.
|
|
|
|
options format:
|
|
{
|
|
nonAbortable: <bool>,
|
|
quiet: <bool>,
|
|
hideProgress: <bool>,
|
|
...
|
|
}
|
|
|
|
|
|
NOTE: when a task queue is stopped it will clear and cleanup, this is
|
|
different to how normal queue behaves.
|
|
NOTE: for queue-specific options see ig-types/runner's Queue(..)
|
|
`,
|
|
function(name, options){
|
|
var that = this
|
|
|
|
var queue = this.queues[name]
|
|
|
|
// create a new queue...
|
|
if(queue == null){
|
|
var abort = function(){
|
|
options.nonAbortable
|
|
|| queue
|
|
.abort() }
|
|
var cleanup = function(){
|
|
return function(){
|
|
queue.stop()
|
|
// XXX handle error state...
|
|
//logger
|
|
// && logger.emit('close')
|
|
delete that.queues[name] } }
|
|
|
|
options = options || {}
|
|
var logger = options.logger || this.logger
|
|
//logger = logger && logger.push(name)
|
|
logger = logger
|
|
&& logger.push(name, {onclose: abort, quiet: !!options.quiet})
|
|
logger
|
|
&& (options.logger = logger)
|
|
|
|
queue = this.queues[name] =
|
|
runner.FinalizableQueue(options || {})
|
|
|
|
// setup logging...
|
|
var suffix = (options || {}).hideProgress ?
|
|
' (hidden)'
|
|
: ''
|
|
queue
|
|
.on('tasksAdded', function(evt, t){
|
|
this.logger && this.logger.emit('added'+suffix, t) })
|
|
// NOTE: t can be anything including an array, so to
|
|
// avoid confusion we wrap it in an array this
|
|
// one call means one emit...
|
|
.on('taskCompleted', function(evt, t, r){
|
|
this.logger && this.logger.emit('done'+suffix, [t]) })
|
|
.on('taskFailed', function(evt, t, err){
|
|
this.logger && this.logger.emit('skipped'+suffix, t, err) })
|
|
.on('stop', function(){
|
|
this.logger && this.logger.emit('reset') })
|
|
.on('abort', function(){
|
|
this.logger && this.logger.emit('reset') })
|
|
// cleanup...
|
|
queue
|
|
.then(
|
|
cleanup('done'),
|
|
cleanup('error')) }
|
|
|
|
// add queue as task...
|
|
this.tasks.includes(queue)
|
|
|| this.tasks.Task(name, queue)
|
|
|
|
return queue }),
|
|
|
|
|
|
|
|
// contexts (XXX EXPERIMENTAL)
|
|
//
|
|
// XXX would be nice to have a context manager:
|
|
// - context id's (index? ...sparse array?)
|
|
// - manager API
|
|
// - create/remove
|
|
// - context api (feature)
|
|
// .then(..)/.catch(..)/.finally(..)
|
|
// XXX is peer stuff just a special context???
|
|
// ...feels like yes
|
|
// XXX is context manager a special case of task manager???
|
|
// XXX move to a separate feature...
|
|
__contexts: null,
|
|
get contexts(){},
|
|
|
|
// XXX this should delete the clone when done...
|
|
// XXX need a common context API to make management possible...
|
|
ContextTask: ['- System/',
|
|
doc``,
|
|
function(type, action, ...args){
|
|
var that = this
|
|
var context = this[type]
|
|
|
|
var res = context[action](...args)
|
|
|
|
var cleanup = function(){
|
|
// XXX
|
|
}
|
|
|
|
res.finally ?
|
|
res.finally(cleanup)
|
|
: cleanup()
|
|
|
|
return res === context ?
|
|
undefined
|
|
: res }],
|
|
|
|
// Links...
|
|
//
|
|
// XXX after this is stabilized, do we need session tasks and its complexities???
|
|
__links: null,
|
|
get links(){
|
|
var links = this.__linked = this.__linked || {}
|
|
// remove 'current' if it does not match the current index...
|
|
// XXX revise the test...
|
|
var c = links.current
|
|
if(c && (c.data !== this.data || c.images !== this.images)){
|
|
links.previous = c
|
|
delete links.current }
|
|
return links },
|
|
get linked(){
|
|
return this.link() },
|
|
link: ['- System/',
|
|
doc`Get/create links...
|
|
|
|
Get/create link to current state...
|
|
.link()
|
|
.link('current')
|
|
-> current-link
|
|
|
|
Get link to previous state if present...
|
|
.link('previous')
|
|
-> previous-link
|
|
-> undefined
|
|
|
|
Get/create a titled link...
|
|
.link(title)
|
|
-> link
|
|
|
|
A link is a separate ImageGrid instance that links to the parent's
|
|
state and explicitly disabled ui features.
|
|
|
|
A link will reflect the data changes but when the main index is
|
|
cleared or reloaded it will retain the old data.
|
|
Care must be taken as this is true in both directions and changes
|
|
to link state are reflected on the link .parent, this is useful
|
|
when updating state in the background but can bite the user if not
|
|
used carefully.
|
|
|
|
This effectively enables us to isolate a context for long running
|
|
actions/tasks and make them independent of the main state.
|
|
|
|
Example:
|
|
ig.linked.readAllMetadata()
|
|
|
|
|
|
NOTE: links are relatively cheap as almost no data is copied but
|
|
they can be a source of a memory "leak" if not cleaned out
|
|
as they prevent data from being garbage collected...
|
|
NOTE: 'current' and 'previous' links are reserved.
|
|
NOTE: 'previous' are a special case as they can not be created
|
|
via .link(..).
|
|
`,
|
|
function(title='current'){
|
|
var that = this
|
|
var links = this.links
|
|
// get link already created...
|
|
// NOTE: 'current' and 'previous' links are handled by the
|
|
// .links prop...
|
|
var link = links[title]
|
|
if(link){
|
|
return link }
|
|
// prevent creating previous links...
|
|
if(title == 'previous'){
|
|
return actions.UNDEFINED }
|
|
// create a link...
|
|
// NOTE: we intentionally disable ui here and do not trigger .start()...
|
|
return (links[title] =
|
|
Object.assign(
|
|
// new base object...
|
|
// XXX add a 'link' feature...
|
|
ImageGridFeatures.setup([
|
|
...this.features.input,
|
|
'-ui',
|
|
'link-context',
|
|
]),
|
|
// clone data...
|
|
// NOTE: this can shadow parts of the new base object
|
|
// so we'll need to exclude some stuff...
|
|
Object.assign({}, this)
|
|
.run(function(){
|
|
// remove excluded attrs...
|
|
;(that.config['context-exclude-attrs']
|
|
|| [ 'features' ])
|
|
.forEach(function(key){
|
|
delete this[key] }.bind(this)) }),
|
|
// context-specific data...
|
|
{
|
|
// link metadata...
|
|
parent: this,
|
|
title: title,
|
|
// XXX change this when data/images changes...
|
|
// ...a prop in the link feature...
|
|
type: 'link',
|
|
// link configuration...
|
|
logger: that.logger
|
|
.push(`Linked ${ Object.keys(links).length }`),
|
|
})) }],
|
|
|
|
|
|
// XXX would be nice to have an ability to partially clone the instance...
|
|
// ...currently we can do a full clone and remove things we do
|
|
// not want...
|
|
// XXX this does not copy aliases...
|
|
// XXX might be a good idea to add a 'IsolatedTask' feature/mixin to
|
|
// handle cleanup (via .done() action)
|
|
// XXX should this be a prop -- .isolated???
|
|
__isolated: null,
|
|
get isolated(){
|
|
return (this.__isolated = this.__isolated || []) },
|
|
isolate: ['- System/',
|
|
function(){
|
|
var clones = this.isolated
|
|
|
|
var clone = this.clone(true)
|
|
// reset actions to exclude UI...
|
|
clone.__proto__ = ImageGridFeatures.setup([...this.features.input, '-ui'])
|
|
clone.parent = this
|
|
// link clone in...
|
|
clone.logger = this.logger.push(['Task', clones.length].join(' '))
|
|
|
|
clone.context_id = clones.push(clone)
|
|
return clone }],
|
|
|
|
})
|
|
|
|
var Tasks =
|
|
module.Tasks = ImageGridFeatures.Feature({
|
|
title: '',
|
|
|
|
tag: 'tasks',
|
|
|
|
depends: [ ],
|
|
|
|
actions: TaskActions,
|
|
|
|
handlers: [
|
|
// stop session tasks...
|
|
['clear',
|
|
// XXX BUG: for some reason calling .abort here does not work...
|
|
//'sessionTasks.stop'],
|
|
'sessionTasks.abort'],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// 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 })
|