]>
Commit | Line | Data |
---|---|---|
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 | |
11 | Ext.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 |