reworked debounce functionality...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2018-09-01 23:45:00 +03:00
parent afae3656f8
commit 4b65f4ee25
3 changed files with 128 additions and 150 deletions

View File

@ -847,30 +847,71 @@ module.Cache = ImageGridFeatures.Feature({
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// Timers... // Timers...
// Create a debounced action...
//
// options format:
// {
// timeout: number,
// returns: 'cached' | 'dropped',
// callback: function(retriggered, args),
// }
//
var debounce = var debounce =
module.debounce = module.debounce =
function(timeout, func){ function(options, func){
func = timeout instanceof Function ? timeout : func // parse args...
var f = function(...args){ func = options instanceof Function ? options : func
return this.debounceActionCall({ options = options instanceof Function ? {} : options
action: func,
args: args, if(typeof(options) == typeof(123)){
tag: func instanceof Function ? options.timeout = options
(func.name || f.name)
: func,
timeout: timeout instanceof Function ? null : timeout,
returns: 'cached',
retrigger: true,
})
} }
// 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(){ f.toString = function(){
return `// debounced...\n${doc([func.toString()])}` return `// debounced...\n${doc([func.toString()])}`
} }
// NOTE: this will force Action(..) to set the .name to the action name...
Object.defineProperty(f, 'name', { value: '<action-name>' })
return f return f
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
var TimersActions = actions.Actions({ var TimersActions = actions.Actions({
config: { config: {
// //
@ -1100,120 +1141,6 @@ var TimersActions = actions.Actions({
// Action debounce... // Action debounce...
// //
debounceActionCall: ['- System/',
doc`Debounce the action call...
.debounceActionCall(call)
-> result
Call format:
{
// action name of function to be called...
action: <name> | <function>,
// arguments to be passed to action/function...
args: <array>,
// tag to identify the call...
//
// Defaults to action name, optional for actions and
// required for functions...
tag: <tag> | null,
// timeout to drop calls within (optional).
//
// defaults to .config['debounce-action-timeout'] then
// to 200.
timeout: <number> | null,
// controls how action return value is handled:
//
// Values:
// 'cached' - cache the value and return it for
// every call within the timeout.
// 'dropped' - ignore return values.
//
// NOTE: this is designed to produce uniform results
// without and exceptions.
returns: 'cached' | 'dropped',
// if true re trigger the action after timeout if it was
// called after the initial call but before the timeout
// ended...
retrigger: <bool>,
}
NOTE: this does not affect actions called directly in any way.
`,
function(call){
var action = call.action
var args = call.args || []
var tag = call.tag || call.action
var timeout = call.timeout
|| this.config['debounce-action-timeout']
|| 200
var returns = call.returns || 'cached'
var retrigger = call.retrigger || false
// 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 attr = '__debounce_'+ tag
// repeated call...
if(this[attr]){
if(retrigger){
this[attr +'_retriggered'] = true
}
var res = returns == 'cached' ?
this[attr +'_return']
: undefined
// setup and first call...
} else {
// NOTE: we are ignoring the return value here so as to
// make the first and repeated call uniform...
var context = this
var res = (action instanceof Function ?
action
: action.split('.')
.reduce(function(res, e){
context = res
return res[e]
}, this))
.call(context, ...args)
// cache the return value...
if(returns == 'cached'){
this[attr +'_return'] = res
// drop the return value...
} else {
res = undefined
}
this[attr] = setTimeout(function(){
delete this[attr]
delete this[attr +'_return']
// retrigger...
if(this[attr +'_retriggered']){
delete this[attr +'_retriggered']
tag == action ?
this.debounce(timeout, action, ...args)
: this.debounce(timeout, tag, action, ...args)
}
}.bind(this), timeout)
}
return res
}],
// shorthand...
debounce: ['- System/', debounce: ['- System/',
doc`Debounce action call... doc`Debounce action call...
@ -1227,6 +1154,19 @@ var TimersActions = actions.Actions({
.debounce(tag, func, ...) .debounce(tag, func, ...)
.debounce(timeout, tag, func, ...) .debounce(timeout, tag, func, ...)
Generic debounce:
.debounce(options, action, ...)
.debounce(options, func, ...)
options format:
{
tag: <string>,
timeout: <milliseconds>,
returns: 'cached' | 'dropped',
callback: <function>,
}
Protocol: Protocol:
- call - call
- start timeout timer - start timeout timer
@ -1244,26 +1184,58 @@ var TimersActions = actions.Actions({
`, `,
function(...args){ function(...args){
// parse the args... // parse the args...
var timeout = typeof(args[0]) == typeof(123) ? 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() args.shift()
: (this.config['debounce-action-timeout'] || 200) : (this.config['debounce-action-timeout'] || 200),
}
// NOTE: this[tag] must not be callable, otherwise we treat it // NOTE: this[tag] must not be callable, otherwise we treat it
// as an action... // as an action...
var tag = (args[0] instanceof Function var tag = (args[0] instanceof Function
|| this[args[0]] instanceof Function) ? || this[args[0]] instanceof Function) ?
args[0] args[0]
: args.shift() : args.shift()
var action = args.shift() }
return this.debounceActionCall({ // sanity check: when debouncing a function a tag is required...
action: action, if(tag instanceof Function){
args: args, throw new TypeError('debounce: when passing a function a tag is required.')
tag: tag, }
timeout: timeout,
// XXX var action = args.shift()
returns: 'dropped', var attr = '__debounce_'+ tag
retrigger: true,
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)
}], }],
}) })

View File

@ -60,6 +60,9 @@ var ExampleActions = actions.Actions({
return args return args
})], })],
testDebounce: ['Test/',
core.debounce(1000, 'exampleAction: ... -- docs...')],
// a normal method... // a normal method...
exampleMethod: function(){ exampleMethod: function(){
console.log('example method:', [].slice.call(arguments)) console.log('example method:', [].slice.call(arguments))

View File

@ -555,9 +555,12 @@ var KeyboardActions = actions.Actions({
}, this) }, this)
return data.debounce ? return data.debounce ?
// debounce... // debounce...
this.debounce( this.debounce({
data.debounce, timeout: data.debounce,
'tag:'+data.action, tag: 'tag:'+data.action,
retrigger: true,
returns: 'dropped',
},
meth.bind(context), ...data.arguments) meth.bind(context), ...data.arguments)
// direct call... // direct call...
: meth.call(context, ...data.arguments) }, : meth.call(context, ...data.arguments) },