]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/util/Scheduler.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / util / Scheduler.js
CommitLineData
6527f429
DM
1/**\r
2 * This class is used to bulk schedule a set of `Ext.util.Schedulable` items. The items\r
3 * in the scheduler request time by calling their `schedule` method and when the time has\r
4 * arrived its `react` method is called.\r
5 *\r
6 * The `react` methods are called in dependency order as determined by the sorting process.\r
7 * The sorting process relies on each item to implement its own `sort` method.\r
8 *\r
9 * @private\r
10 */\r
11Ext.define('Ext.util.Scheduler', {\r
12 mixins: [\r
13 'Ext.mixin.Observable'\r
14 ],\r
15\r
16 requires: [\r
17 'Ext.util.Bag'\r
18 ],\r
19\r
20 busyCounter: 0,\r
21 lastBusyCounter: 0,\r
22\r
23 destroyed: false,\r
24\r
25 firing: null,\r
26\r
27 notifyIndex: -1,\r
28\r
29 nextId: 0,\r
30\r
31 orderedItems: null,\r
32\r
33 passes: 0,\r
34\r
35 scheduledCount: 0,\r
36\r
37 validIdRe: null,\r
38\r
39 config: {\r
40 /**\r
41 * @cfg {Number} cycleLimit\r
42 * The maximum number of iterations to make over the items in one `notify` call.\r
43 * This is used to prevent run away logic from looping infinitely. If this limit\r
44 * is exceeded, an error is thrown (in development builds).\r
45 * @private\r
46 */\r
47 cycleLimit: 5,\r
48\r
49 /**\r
50 * @cfg {String/Function} preSort\r
51 * If provided the `Schedulable` items will be pre-sorted by this function or\r
52 * property value before the dependency sort.\r
53 */\r
54 preSort: null,\r
55\r
56 /**\r
57 * @cfg {Number} tickDelay\r
58 * The number of milliseconds to delay notification after the first `schedule`\r
59 * request.\r
60 */\r
61 tickDelay: 5\r
62 },\r
63\r
64 /**\r
65 * @property {Boolean} suspendOnNotify\r
66 * `true` to suspend layouts when the scheduler is triggering bindings. Setting this to `false`\r
67 * may mean multiple layout runs on a single bind call which could affect performance.\r
68 */\r
69 suspendOnNotify: true,\r
70\r
71 constructor: function (config) {\r
72 //<debug>\r
73 if (Ext.util.Scheduler.instances) {\r
74 Ext.util.Scheduler.instances.push(this);\r
75 } else {\r
76 Ext.util.Scheduler.instances = [ this ];\r
77 }\r
78 this.id = Ext.util.Scheduler.count = (Ext.util.Scheduler.count || 0) + 1;\r
79 //</debug>\r
80\r
81 this.mixins.observable.constructor.call(this, config);\r
82\r
83 this.items = new Ext.util.Bag();\r
84 },\r
85\r
86 destroy: function () {\r
87 var me = this,\r
88 timer = me.timer;\r
89\r
90 if (timer) {\r
91 window.clearTimeout(timer);\r
92 me.timer = null;\r
93 }\r
94 me.items.destroy();\r
95 me.items = me.orderedItems = null;\r
96\r
97 me.callParent();\r
98\r
99 //<debug>\r
100 Ext.Array.remove(Ext.util.Scheduler.instances, this);\r
101 //</debug>\r
102 },\r
103\r
104 /**\r
105 * Adds an item to the scheduler. This is called internally by the `constructor` of\r
106 * `{@link Ext.util.Schedulable}`.\r
107 *\r
108 * @param {Object} item The item to add.\r
109 * @private\r
110 * @since 5.0.0\r
111 */\r
112 add: function (item) {\r
113 var me = this,\r
114 items = me.items;\r
115\r
116 if (items === me.firing) {\r
117 me.items = items = items.clone();\r
118 }\r
119\r
120 item.id = item.id || ++me.nextId;\r
121 item.scheduler = me;\r
122 \r
123 items.add(item);\r
124\r
125 if (!me.sortMap) {\r
126 // If we are sorting we don't want to invalidate this... we will pick up the\r
127 // new items just fine.\r
128 me.orderedItems = null;\r
129 }\r
130 },\r
131\r
132 /**\r
133 * Removes an item to the scheduler. This is called internally by the `destroy` method\r
134 * of `{@link Ext.util.Schedulable}`.\r
135 *\r
136 * @param {Object} item The item to remove.\r
137 * @private\r
138 * @since 5.0.0\r
139 */\r
140 remove: function (item) {\r
141 var me = this,\r
142 items = me.items;\r
143\r
144 if (me.destroyed) {\r
145 return;\r
146 }\r
147\r
148 //<debug>\r
149 if (me.sortMap) {\r
150 Ext.raise('Items cannot be removed during sort');\r
151 }\r
152 //</debug>\r
153\r
154 if (items === me.firing) {\r
155 me.items = items = items.clone();\r
156 }\r
157\r
158 if (item.scheduled) {\r
159 me.unscheduleItem(item);\r
160 item.scheduled = false;\r
161 }\r
162\r
163 items.remove(item);\r
164\r
165 me.orderedItems = null;\r
166 },\r
167\r
168 /**\r
169 * This method is called internally as needed to sort or resort the items in their\r
170 * proper dependency order.\r
171 *\r
172 * @private\r
173 * @since 5.0.0\r
174 */\r
175 sort: function () {\r
176 var me = this,\r
177 items = me.items,\r
178 sortMap = {},\r
179 preSort = me.getPreSort(),\r
180 i, item;\r
181\r
182 me.orderedItems = [];\r
183 me.sortMap = sortMap;\r
184\r
185 //<debug>\r
186 me.sortStack = [];\r
187 //</debug>\r
188\r
189 if (preSort) {\r
190 items.sort(preSort);\r
191 }\r
192\r
193 items = items.items; // grab the items array\r
194\r
195 // We reference items.length since items can be added during this loop\r
196 for (i = 0; i < items.length; ++i) {\r
197 item = items[i];\r
198 if (!sortMap[item.id]) {\r
199 me.sortItem(item);\r
200 }\r
201 }\r
202\r
203 me.sortMap = null;\r
204\r
205 //<debug>\r
206 me.sortStack = null;\r
207 //</debug>\r
208 },\r
209\r
210 /**\r
211 * Adds one item to the sorted items array. This can be called by the `sort` method of\r
212 * `{@link Ext.util.Sortable sortable}` objects to add an item on which it depends.\r
213 *\r
214 * @param {Object} item The item to add.\r
215 * @return {Ext.util.Scheduler} This instance.\r
216 * @since 5.0.0\r
217 */\r
218 sortItem: function (item) {\r
219 var me = this,\r
220 sortMap = me.sortMap,\r
221 orderedItems = me.orderedItems,\r
222 itemId;\r
223\r
224 if (!item.scheduler) {\r
225 me.add(item);\r
226 }\r
227\r
228 itemId = item.id;\r
229\r
230 //<debug>\r
231 if (item.scheduler !== me) {\r
232 Ext.raise('Item ' + itemId + ' belongs to another Scheduler');\r
233 }\r
234\r
235 me.sortStack.push(item);\r
236\r
237 if (sortMap[itemId] === 0) {\r
238 for (var cycle = [], i = 0; i < me.sortStack.length; ++i) {\r
239 cycle[i] = me.sortStack[i].getFullName();\r
240 }\r
241 Ext.raise('Dependency cycle detected: ' + cycle.join('\n --> '));\r
242 }\r
243 //</debug>\r
244\r
245 if (!(itemId in sortMap)) {\r
246 // In production builds the above "if" will kick out the items that have\r
247 // already been added (which it must) but also those that are being added\r
248 // and have created a cycle (by virtue of the setting to 0). This check\r
249 // should not be needed if cycles were all detected and removed in dev but\r
250 // this is better than infinite recursion.\r
251 sortMap[itemId] = 0;\r
252\r
253 if (!item.sort.$nullFn) {\r
254 item.sort();\r
255 }\r
256\r
257 sortMap[itemId] = 1;\r
258\r
259 item.order = me.orderedItems.length;\r
260 orderedItems.push(item);\r
261 }\r
262\r
263 //<debug>\r
264 me.sortStack.pop();\r
265 //</debug>\r
266\r
267 return me;\r
268 },\r
269\r
270 /**\r
271 * Adds multiple items to the sorted items array. This can be called by the `sort`\r
272 * method of `{@link Ext.util.Sortable sortable}` objects to add items on which it\r
273 * depends.\r
274 *\r
275 * @param {Object/Object[]} items The items to add. If this is an object, the values\r
276 * are considered the items and the keys are ignored.\r
277 * @return {Ext.util.Scheduler} This instance.\r
278 * @since 5.0.0\r
279 */\r
280 sortItems: function (items) {\r
281 var me = this,\r
282 sortItem = me.sortItem;\r
283\r
284 if (items) {\r
285 if (items instanceof Array) {\r
286 Ext.each(items, sortItem, me);\r
287 } else {\r
288 Ext.Object.eachValue(items, sortItem, me);\r
289 }\r
290 }\r
291\r
292 return me;\r
293 },\r
294\r
295 applyPreSort: function (preSort) {\r
296 if (typeof preSort === 'function') {\r
297 return preSort;\r
298 }\r
299\r
300 var parts = preSort.split(','),\r
301 direction = [],\r
302 length = parts.length,\r
303 c, i, s;\r
304\r
305 for (i = 0; i < length; ++i) {\r
306 direction[i] = 1;\r
307 s = parts[i];\r
308\r
309 if ((c = s.charAt(0)) === '-') {\r
310 direction[i] = -1;\r
311 } else if (c !== '+') {\r
312 c = 0;\r
313 }\r
314\r
315 if (c) {\r
316 parts[i] = s.substring(1);\r
317 }\r
318 }\r
319\r
320 return function (lhs, rhs) {\r
321 var ret = 0,\r
322 i, prop, v1, v2;\r
323\r
324 for (i = 0; !ret && i < length; ++i) {\r
325 prop = parts[i];\r
326 v1 = lhs[prop];\r
327 v2 = rhs[prop];\r
328 ret = direction[i] * ((v1 < v2) ? -1 : ((v2 < v1) ? 1 : 0));\r
329 }\r
330\r
331 return ret;\r
332 };\r
333 },\r
334\r
335 //-------------------------------------------------------------------------\r
336 // Callback scheduling\r
337 // <editor-fold>\r
338\r
339 /**\r
340 * This method can be called to force the delivery of any scheduled items. This is\r
341 * called automatically on a timer when items request service.\r
342 *\r
343 * @since 5.0.0\r
344 */\r
345 notify: function () {\r
346 var me = this,\r
347 timer = me.timer,\r
348 cyclesLeft = me.getCycleLimit(),\r
349 globalEvents = Ext.GlobalEvents,\r
350 suspend = me.suspendOnNotify,\r
351 busyCounter, i, item, len, queue, firedEvent;\r
352\r
353 if (timer) {\r
354 window.clearTimeout(timer);\r
355 me.timer = null;\r
356 }\r
357\r
358 //<debug>\r
359 if (me.firing) {\r
360 Ext.raise('Notify cannot be called recursively');\r
361 }\r
362 //</debug>\r
363\r
364 if (suspend) {\r
365 Ext.suspendLayouts();\r
366 }\r
367\r
368 while (me.scheduledCount) {\r
369 if (cyclesLeft) {\r
370 --cyclesLeft;\r
371 } else {\r
372 me.firing = null;\r
373 //<debug>\r
374 if (me.onCycleLimitExceeded) {\r
375 me.onCycleLimitExceeded();\r
376 }\r
377 //</debug>\r
378 break;\r
379 }\r
380\r
381 if (!firedEvent) {\r
382 firedEvent = true;\r
383 if (globalEvents.hasListeners.beforebindnotify) {\r
384 globalEvents.fireEvent('beforebindnotify', me);\r
385 }\r
386 }\r
387\r
388 ++me.passes;\r
389\r
390 // We need to sort before we start firing because items can be added as we\r
391 // loop.\r
392 if (!(queue = me.orderedItems)) {\r
393 me.sort();\r
394 queue = me.orderedItems;\r
395 }\r
396\r
397 len = queue.length;\r
398 if (len) {\r
399 me.firing = me.items;\r
400\r
401 for (i = 0; i < len; ++i) {\r
402 item = queue[i];\r
403\r
404 if (item.scheduled) {\r
405 item.scheduled = false;\r
406 --me.scheduledCount;\r
407 me.notifyIndex = i;\r
408\r
409 //Ext.log('React: ' + item.getFullName());\r
410 // This sequence allows the reaction to schedule items further\r
411 // down the queue without a second pass but also to schedule an\r
412 // item that is "upstream" or even itself.\r
413 item.react();\r
414\r
415 if (!me.scheduledCount) {\r
416 break;\r
417 }\r
418 }\r
419 }\r
420 }\r
421 }\r
422\r
423 me.firing = null;\r
424 me.notifyIndex = -1;\r
425\r
426 if (suspend) {\r
427 Ext.resumeLayouts(true);\r
428 }\r
429\r
430 // The last thing we do is check for idle state transition (now that whatever\r
431 // else that was queued up has been dispatched):\r
432 if ((busyCounter = me.busyCounter) !== me.lastBusyCounter) {\r
433 if (!(me.lastBusyCounter = busyCounter)) {\r
434 // Since the counters are not equal, we were busy and are not anymore,\r
435 // so we can fire the idle event:\r
436 me.fireEvent('idle', me);\r
437 }\r
438 }\r
439 },\r
440\r
441 /**\r
442 * The method called by the timer. This cleans up the state and calls `notify`.\r
443 * @private\r
444 * @since 5.0.0\r
445 */\r
446 onTick: function () {\r
447 this.timer = null;\r
448 this.notify();\r
449 },\r
450\r
451 /**\r
452 * Called to indicate that an item needs to be scheduled. This should not be called\r
453 * directly. Call the item's `{@link Ext.util.Schedulable#schedule schedule}` method\r
454 * instead.\r
455 * @param {Object} item\r
456 * @private\r
457 * @since 5.0.0\r
458 */\r
459 scheduleItem: function (item) {\r
460 var me = this;\r
461\r
462 ++me.scheduledCount;\r
463 //Ext.log('Schedule: ' + item.getFullName());\r
464\r
465 if (!me.timer && !me.firing) {\r
466 me.scheduleTick();\r
467 }\r
468 },\r
469\r
470 /**\r
471 * This method starts the timer that will execute the next `notify`.\r
472 * @param {Object} item\r
473 * @private\r
474 * @since 5.0.0\r
475 */\r
476 scheduleTick: function () {\r
477 var me = this;\r
478\r
479 if (!me.destroyed && !me.timer) {\r
480 me.timer = Ext.Function.defer(me.onTick, me.getTickDelay(), me);\r
481 }\r
482 },\r
483\r
484 /**\r
485 * Called to indicate that an item needs to be removed from the schedule. This should\r
486 * not be called directly. Call the item's `{@link Ext.util.Schedulable#unschedule unschedule}`\r
487 * method instead.\r
488 * @param {Object} item\r
489 * @private\r
490 * @since 5.0.0\r
491 */\r
492 unscheduleItem: function (item) {\r
493 if (this.scheduledCount) {\r
494 --this.scheduledCount;\r
495 }\r
496 },\r
497\r
498 // </editor-fold>\r
499\r
500 //-------------------------------------------------------------------------\r
501 // Busy/Idle state tracking\r
502 // <editor-fold>\r
503\r
504 /**\r
505 * This method should be called when items become busy or idle. These changes are\r
506 * useful outside to do things like update modal masks or status indicators. The\r
507 * changes are delivered as `busy` and `idle` events.\r
508 *\r
509 * @param {Number} adjustment Should be `1` or `-1` only to indicate transition to\r
510 * busy state or from busy state, respectively.\r
511 * @since 5.0.0\r
512 */\r
513 adjustBusy: function (adjustment) {\r
514 var me = this,\r
515 busyCounter = me.busyCounter + adjustment;\r
516\r
517 me.busyCounter = busyCounter;\r
518\r
519 if (busyCounter) {\r
520 // If we are now busy but were not previously, fire the busy event immediately\r
521 // and update lastBusyCounter.\r
522 if (!me.lastBusyCounter) {\r
523 me.lastBusyCounter = busyCounter;\r
524 me.fireEvent('busy', me);\r
525 }\r
526 } else if (me.lastBusyCounter && !me.timer) {\r
527 // If we are now not busy but were previously, defer this to make sure that\r
528 // we don't quickly start with some other activity.\r
529 me.scheduleTick();\r
530 }\r
531 },\r
532\r
533 /**\r
534 * Returns `true` if this object contains one or more busy items.\r
535 * @return {Boolean}\r
536 * @since 5.0.0\r
537 */\r
538 isBusy: function () {\r
539 return !this.isIdle();\r
540 },\r
541\r
542 /**\r
543 * Returns `true` if this object contains no busy items.\r
544 * @return {Boolean}\r
545 * @since 5.0.0\r
546 */\r
547 isIdle: function () {\r
548 return !(this.busyCounter + this.lastBusyCounter);\r
549 },\r
550\r
551 // </editor-fold>\r
552\r
553 debugHooks: {\r
554 $enabled: false, // Disable by default\r
555\r
556 onCycleLimitExceeded: function () {\r
557 Ext.raise('Exceeded cycleLimit ' + this.getCycleLimit());\r
558 },\r
559\r
560 scheduleItem: function (item) {\r
561 if (!item) {\r
562 Ext.raise('scheduleItem: Invalid argument');\r
563 }\r
564 Ext.log('Schedule item: ' + item.getFullName() + ' - ' + (this.scheduledCount+1));\r
565 if (item.order <= this.notifyIndex) {\r
566 Ext.log.warn('Suboptimal order: ' + item.order + ' < ' + this.notifyIndex);\r
567 }\r
568 this.callParent([item]);\r
569 },\r
570\r
571 unscheduleItem: function (item) {\r
572 if (!this.scheduledCount) {\r
573 Ext.raise('Invalid scheduleCount');\r
574 }\r
575 this.callParent([item]);\r
576 Ext.log('Unschedule item: ' + item.getFullName() + ' - ' + this.scheduledCount);\r
577 }\r
578 }\r
579});\r