tweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloud
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
15 KiB
565 lines
15 KiB
/** |
|
* This class is used to bulk schedule a set of `Ext.util.Schedulable` items. The items |
|
* in the scheduler request time by calling their `schedule` method and when the time has |
|
* arrived its `react` method is called. |
|
* |
|
* The `react` methods are called in dependency order as determined by the sorting process. |
|
* The sorting process relies on each item to implement its own `sort` method. |
|
* |
|
* @private |
|
*/ |
|
Ext.define('Ext.util.Scheduler', { |
|
mixins: [ |
|
'Ext.mixin.Observable' |
|
], |
|
|
|
requires: [ |
|
'Ext.util.Bag' |
|
], |
|
|
|
busyCounter: 0, |
|
lastBusyCounter: 0, |
|
|
|
destroyed: false, |
|
|
|
firing: null, |
|
|
|
notifyIndex: -1, |
|
|
|
nextId: 0, |
|
|
|
orderedItems: null, |
|
|
|
passes: 0, |
|
|
|
scheduledCount: 0, |
|
|
|
validIdRe: null, |
|
|
|
config: { |
|
/** |
|
* @cfg {Number} cycleLimit |
|
* The maximum number of iterations to make over the items in one `notify` call. |
|
* This is used to prevent run away logic from looping infinitely. If this limit |
|
* is exceeded, an error is thrown (in development builds). |
|
* @private |
|
*/ |
|
cycleLimit: 5, |
|
|
|
/** |
|
* @cfg {String/Function} preSort |
|
* If provided the `Schedulable` items will be pre-sorted by this function or |
|
* property value before the dependency sort. |
|
*/ |
|
preSort: null, |
|
|
|
/** |
|
* @cfg {Number} tickDelay |
|
* The number of milliseconds to delay notification after the first `schedule` |
|
* request. |
|
*/ |
|
tickDelay: 5 |
|
}, |
|
|
|
constructor: function (config) { |
|
//<debug> |
|
if (Ext.util.Scheduler.instances) { |
|
Ext.util.Scheduler.instances.push(this); |
|
} else { |
|
Ext.util.Scheduler.instances = [ this ]; |
|
} |
|
this.id = Ext.util.Scheduler.count = (Ext.util.Scheduler.count || 0) + 1; |
|
//</debug> |
|
|
|
this.mixins.observable.constructor.call(this, config); |
|
|
|
this.items = new Ext.util.Bag(); |
|
}, |
|
|
|
destroy: function () { |
|
var me = this, |
|
timer = me.timer; |
|
|
|
if (timer) { |
|
window.clearTimeout(timer); |
|
me.timer = null; |
|
} |
|
|
|
me.destroyed = true; |
|
me.items.destroy(); |
|
me.items = me.orderedItems = null; |
|
|
|
me.destroy = Ext.emptyFn; |
|
|
|
//<debug> |
|
Ext.Array.remove(Ext.util.Scheduler.instances, this); |
|
//</debug> |
|
}, |
|
|
|
/** |
|
* Adds an item to the scheduler. This is called internally by the `constructor` of |
|
* `{@link Ext.util.Schedulable}`. |
|
* |
|
* @param {Object} item The item to add. |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
add: function (item) { |
|
var me = this, |
|
items = me.items; |
|
|
|
if (items === me.firing) { |
|
me.items = items = items.clone(); |
|
} |
|
|
|
item.id = item.id || ++me.nextId; |
|
item.scheduler = me; |
|
|
|
items.add(item); |
|
|
|
if (!me.sortMap) { |
|
// If we are sorting we don't want to invalidate this... we will pick up the |
|
// new items just fine. |
|
me.orderedItems = null; |
|
} |
|
}, |
|
|
|
/** |
|
* Removes an item to the scheduler. This is called internally by the `destroy` method |
|
* of `{@link Ext.util.Schedulable}`. |
|
* |
|
* @param {Object} item The item to remove. |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
remove: function (item) { |
|
var me = this, |
|
items = me.items; |
|
|
|
if (me.destroyed) { |
|
return; |
|
} |
|
|
|
//<debug> |
|
if (me.sortMap) { |
|
Ext.Error.raise('Items cannot be removed during sort'); |
|
} |
|
//</debug> |
|
|
|
if (items === me.firing) { |
|
me.items = items = items.clone(); |
|
} |
|
|
|
if (item.scheduled) { |
|
me.unscheduleItem(item); |
|
item.scheduled = false; |
|
} |
|
|
|
items.remove(item); |
|
|
|
me.orderedItems = null; |
|
}, |
|
|
|
/** |
|
* This method is called internally as needed to sort or resort the items in their |
|
* proper dependency order. |
|
* |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
sort: function () { |
|
var me = this, |
|
items = me.items, |
|
sortMap = {}, |
|
preSort = me.getPreSort(), |
|
i, item; |
|
|
|
me.orderedItems = []; |
|
me.sortMap = sortMap; |
|
|
|
//<debug> |
|
me.sortStack = []; |
|
//</debug> |
|
|
|
if (preSort) { |
|
items.sort(preSort); |
|
} |
|
|
|
items = items.items; // grab the items array |
|
|
|
// We reference items.length since items can be added during this loop |
|
for (i = 0; i < items.length; ++i) { |
|
item = items[i]; |
|
if (!sortMap[item.id]) { |
|
me.sortItem(item); |
|
} |
|
} |
|
|
|
me.sortMap = null; |
|
|
|
//<debug> |
|
me.sortStack = null; |
|
//</debug> |
|
}, |
|
|
|
/** |
|
* Adds one item to the sorted items array. This can be called by the `sort` method of |
|
* `{@link Ext.util.Sortable sortable}` objects to add an item on which it depends. |
|
* |
|
* @param {Object} item The item to add. |
|
* @return {Ext.util.Scheduler} This instance. |
|
* @since 5.0.0 |
|
*/ |
|
sortItem: function (item) { |
|
var me = this, |
|
sortMap = me.sortMap, |
|
orderedItems = me.orderedItems, |
|
itemId; |
|
|
|
if (!item.scheduler) { |
|
me.add(item); |
|
} |
|
|
|
itemId = item.id; |
|
|
|
//<debug> |
|
if (item.scheduler !== me) { |
|
Ext.Error.raise('Item ' + itemId + ' belongs to another Scheduler'); |
|
} |
|
|
|
me.sortStack.push(item); |
|
|
|
if (sortMap[itemId] === 0) { |
|
for (var cycle = [], i = 0; i < me.sortStack.length; ++i) { |
|
cycle[i] = me.sortStack[i].getFullName(); |
|
} |
|
Ext.Error.raise('Dependency cycle detected: ' + cycle.join('\n --> ')); |
|
} |
|
//</debug> |
|
|
|
if (!(itemId in sortMap)) { |
|
// In production builds the above "if" will kick out the items that have |
|
// already been added (which it must) but also those that are being added |
|
// and have created a cycle (by virtue of the setting to 0). This check |
|
// should not be needed if cycles were all detected and removed in dev but |
|
// this is better than infinite recursion. |
|
sortMap[itemId] = 0; |
|
|
|
if (!item.sort.$nullFn) { |
|
item.sort(); |
|
} |
|
|
|
sortMap[itemId] = 1; |
|
|
|
item.order = me.orderedItems.length; |
|
orderedItems.push(item); |
|
} |
|
|
|
//<debug> |
|
me.sortStack.pop(); |
|
//</debug> |
|
|
|
return me; |
|
}, |
|
|
|
/** |
|
* Adds multiple items to the sorted items array. This can be called by the `sort` |
|
* method of `{@link Ext.util.Sortable sortable}` objects to add items on which it |
|
* depends. |
|
* |
|
* @param {Object/Object[]} items The items to add. If this is an object, the values |
|
* are considered the items and the keys are ignored. |
|
* @return {Ext.util.Scheduler} This instance. |
|
* @since 5.0.0 |
|
*/ |
|
sortItems: function (items) { |
|
var me = this, |
|
sortItem = me.sortItem; |
|
|
|
if (items) { |
|
if (items instanceof Array) { |
|
Ext.each(items, sortItem, me); |
|
} else { |
|
Ext.Object.eachValue(items, sortItem, me); |
|
} |
|
} |
|
|
|
return me; |
|
}, |
|
|
|
applyPreSort: function (preSort) { |
|
if (typeof preSort === 'function') { |
|
return preSort; |
|
} |
|
|
|
var parts = preSort.split(','), |
|
direction = [], |
|
length = parts.length, |
|
c, i, s; |
|
|
|
for (i = 0; i < length; ++i) { |
|
direction[i] = 1; |
|
s = parts[i]; |
|
|
|
if ((c = s.charAt(0)) === '-') { |
|
direction[i] = -1; |
|
} else if (c !== '+') { |
|
c = 0; |
|
} |
|
|
|
if (c) { |
|
parts[i] = s.substring(1); |
|
} |
|
} |
|
|
|
return function (lhs, rhs) { |
|
var ret = 0, |
|
i, prop, v1, v2; |
|
|
|
for (i = 0; !ret && i < length; ++i) { |
|
prop = parts[i]; |
|
v1 = lhs[prop]; |
|
v2 = rhs[prop]; |
|
ret = direction[i] * ((v1 < v2) ? -1 : ((v2 < v1) ? 1 : 0)); |
|
} |
|
|
|
return ret; |
|
}; |
|
}, |
|
|
|
//------------------------------------------------------------------------- |
|
// Callback scheduling |
|
// <editor-fold> |
|
|
|
/** |
|
* This method can be called to force the delivery of any scheduled items. This is |
|
* called automatically on a timer when items request service. |
|
* |
|
* @since 5.0.0 |
|
*/ |
|
notify: function () { |
|
var me = this, |
|
timer = me.timer, |
|
cyclesLeft = me.getCycleLimit(), |
|
globalEvents = Ext.GlobalEvents, |
|
busyCounter, i, item, len, queue, firedEvent; |
|
|
|
if (timer) { |
|
window.clearTimeout(timer); |
|
me.timer = null; |
|
} |
|
|
|
//<debug> |
|
if (me.firing) { |
|
Ext.Error.raise('Notify cannot be called recursively'); |
|
} |
|
//</debug> |
|
|
|
while (me.scheduledCount) { |
|
if (cyclesLeft) { |
|
--cyclesLeft; |
|
} else { |
|
me.firing = null; |
|
//<debug> |
|
if (me.onCycleLimitExceeded) { |
|
me.onCycleLimitExceeded(); |
|
} |
|
//</debug> |
|
break; |
|
} |
|
|
|
if (!firedEvent) { |
|
firedEvent = true; |
|
if (globalEvents.hasListeners.beforebindnotify) { |
|
globalEvents.fireEvent('beforebindnotify', me); |
|
} |
|
} |
|
|
|
++me.passes; |
|
|
|
// We need to sort before we start firing because items can be added as we |
|
// loop. |
|
if (!(queue = me.orderedItems)) { |
|
me.sort(); |
|
queue = me.orderedItems; |
|
} |
|
|
|
len = queue.length; |
|
if (len) { |
|
me.firing = me.items; |
|
|
|
for (i = 0; i < len; ++i) { |
|
item = queue[i]; |
|
|
|
if (item.scheduled) { |
|
item.scheduled = false; |
|
--me.scheduledCount; |
|
me.notifyIndex = i; |
|
|
|
//Ext.log('React: ' + item.getFullName()); |
|
// This sequence allows the reaction to schedule items further |
|
// down the queue without a second pass but also to schedule an |
|
// item that is "upstream" or even itself. |
|
item.react(); |
|
|
|
if (!me.scheduledCount) { |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
me.firing = null; |
|
me.notifyIndex = -1; |
|
|
|
// The last thing we do is check for idle state transition (now that whatever |
|
// else that was queued up has been dispatched): |
|
if ((busyCounter = me.busyCounter) !== me.lastBusyCounter) { |
|
if (!(me.lastBusyCounter = busyCounter)) { |
|
// Since the counters are not equal, we were busy and are not anymore, |
|
// so we can fire the idle event: |
|
me.fireEvent('idle', me); |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* The method called by the timer. This cleans up the state and calls `notify`. |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
onTick: function () { |
|
this.timer = null; |
|
this.notify(); |
|
}, |
|
|
|
/** |
|
* Called to indicate that an item needs to be scheduled. This should not be called |
|
* directly. Call the item's `{@link Ext.util.Schedulable#schedule schedule}` method |
|
* instead. |
|
* @param {Object} item |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
scheduleItem: function (item) { |
|
var me = this; |
|
|
|
++me.scheduledCount; |
|
//Ext.log('Schedule: ' + item.getFullName()); |
|
|
|
if (!me.timer && !me.firing) { |
|
me.scheduleTick(); |
|
} |
|
}, |
|
|
|
/** |
|
* This method starts the timer that will execute the next `notify`. |
|
* @param {Object} item |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
scheduleTick: function () { |
|
var me = this; |
|
|
|
if (!me.destroyed && !me.timer) { |
|
me.timer = Ext.Function.defer(me.onTick, me.getTickDelay(), me); |
|
} |
|
}, |
|
|
|
/** |
|
* Called to indicate that an item needs to be removed from the schedule. This should |
|
* not be called directly. Call the item's `{@link Ext.util.Schedulable#unschedule unschedule}` |
|
* method instead. |
|
* @param {Object} item |
|
* @private |
|
* @since 5.0.0 |
|
*/ |
|
unscheduleItem: function (item) { |
|
if (this.scheduledCount) { |
|
--this.scheduledCount; |
|
} |
|
}, |
|
|
|
// </editor-fold> |
|
|
|
//------------------------------------------------------------------------- |
|
// Busy/Idle state tracking |
|
// <editor-fold> |
|
|
|
/** |
|
* This method should be called when items become busy or idle. These changes are |
|
* useful outside to do things like update modal masks or status indicators. The |
|
* changes are delivered as `busy` and `idle` events. |
|
* |
|
* @param {Number} adjustment Should be `1` or `-1` only to indicate transition to |
|
* busy state or from busy state, respectively. |
|
* @since 5.0.0 |
|
*/ |
|
adjustBusy: function (adjustment) { |
|
var me = this, |
|
busyCounter = me.busyCounter + adjustment; |
|
|
|
me.busyCounter = busyCounter; |
|
|
|
if (busyCounter) { |
|
// If we are now busy but were not previously, fire the busy event immediately |
|
// and update lastBusyCounter. |
|
if (!me.lastBusyCounter) { |
|
me.lastBusyCounter = busyCounter; |
|
me.fireEvent('busy', me); |
|
} |
|
} else if (me.lastBusyCounter && !me.timer) { |
|
// If we are now not busy but were previously, defer this to make sure that |
|
// we don't quickly start with some other activity. |
|
me.scheduleTick(); |
|
} |
|
}, |
|
|
|
/** |
|
* Returns `true` if this object contains one or more busy items. |
|
* @return {Boolean} |
|
* @since 5.0.0 |
|
*/ |
|
isBusy: function () { |
|
return !this.isIdle(); |
|
}, |
|
|
|
/** |
|
* Returns `true` if this object contains no busy items. |
|
* @return {Boolean} |
|
* @since 5.0.0 |
|
*/ |
|
isIdle: function () { |
|
return !(this.busyCounter + this.lastBusyCounter); |
|
}, |
|
|
|
// </editor-fold> |
|
|
|
debugHooks: { |
|
$enabled: false, // Disable by default |
|
|
|
onCycleLimitExceeded: function () { |
|
Ext.Error.raise('Exceeded cycleLimit ' + this.getCycleLimit()); |
|
}, |
|
|
|
scheduleItem: function (item) { |
|
if (!item) { |
|
Ext.Error.raise('scheduleItem: Invalid argument'); |
|
} |
|
Ext.log('Schedule item: ' + item.getFullName() + ' - ' + (this.scheduledCount+1)); |
|
if (item.order <= this.notifyIndex) { |
|
Ext.log.warn('Suboptimal order: ' + item.order + ' < ' + this.notifyIndex); |
|
} |
|
this.callParent([item]); |
|
}, |
|
|
|
unscheduleItem: function (item) { |
|
if (!this.scheduledCount) { |
|
Ext.Error.raise('Invalid scheduleCount'); |
|
} |
|
this.callParent([item]); |
|
Ext.log('Unschedule item: ' + item.getFullName() + ' - ' + this.scheduledCount); |
|
} |
|
} |
|
});
|
|
|