ember-app-scheduler
Version:
Ember addon to schedule work at different phases of app life cycle.
210 lines (169 loc) • 4.6 kB
JavaScript
import Ember from 'ember';
import { DEBUG } from '@glimmer/env';
const {
run,
RSVP,
Service,
} = Ember;
class Token {
constructor() {
this._cancelled = false;
}
get cancelled() {
return this._cancelled;
}
cancel() {
this._cancelled = true;
}
}
class Queue {
constructor() {
this.reset();
}
reset() {
this.tasks = [];
this.isActive = true;
this.afterPaintDeferred = RSVP.defer();
this.afterPaintPromise = this.afterPaintDeferred.promise;
}
}
const Scheduler = Service.extend({
queueNames: ['afterFirstRoutePaint', 'afterContentPaint'],
init() {
this._super();
this._nextPaintFrame = null;
this._nextPaintTimeout = null;
this._nextAfterPaintPromise = null;
this._routerWillTransitionHandler = null;
this._routerDidTransitionHandler = null;
this._initQueues();
this._connectToRouter();
this._useRAF = typeof requestAnimationFrame === "function";
},
scheduleWork(queueName, callback) {
const queue = this.queues[queueName];
const token = new Token();
if (queue.isActive) {
queue.tasks.push(callback);
queue.tasks.push(token);
} else {
callback();
}
return token;
},
cancelWork(token) {
token.cancel();
},
flushQueue(queueName) {
const queue = this.queues[queueName];
queue.isActive = false;
for (let i = 0; i < queue.tasks.length; i += 2) {
const callback = queue.tasks[i];
const token = queue.tasks[i+1];
if (!token.cancelled) {
callback();
}
}
this._afterNextPaint()
.then(() => {
queue.afterPaintDeferred.resolve();
});
},
_initQueues() {
const queues = this.queues = Object.create(null);
const queueNames = this.queueNames;
for (let i = 0; i < queueNames.length; i++) {
queues[queueNames[i]] = new Queue();
}
},
_resetQueues() {
const queues = this.queues;
const queueNames = this.queueNames;
for (let i = 0; i < queueNames.length; i++) {
queues[queueNames[i]].reset();
}
},
_afterNextPaint() {
if (this._nextAfterPaintPromise) {
return this._nextAfterPaintPromise;
}
this._nextAfterPaintPromise = new RSVP.Promise((resolve) => {
if (this._useRAF) {
this._nextPaintFrame = requestAnimationFrame(() => this._rAFCallback(resolve));
} else {
this._rAFCallback(resolve);
}
});
return this._nextAfterPaintPromise;
},
_rAFCallback(resolve) {
this._nextPaintTimeout = run.later(() => {
this._nextAfterPaintPromise = null;
this._nextPaintFrame = null;
this._nextPaintTimeout = null;
resolve();
}, 0);
},
_connectToRouter() {
const router = this.get('router');
this._routerWillTransitionHandler = () => {
this._resetQueues();
};
this._routerDidTransitionHandler = () => {
this._afterNextPaint()
.then(() => {
this.flushQueue('afterFirstRoutePaint');
this._afterNextPaint()
.then(() => {
this.flushQueue('afterContentPaint');
});
});
};
router.on('willTransition', this._routerWillTransitionHandler);
router.on('didTransition', this._routerDidTransitionHandler);
},
willDestroy() {
this._super();
const router = this.get('router');
this.queues = null; // don't hold any references to uncompleted items
router.off('willTransition', this._routerWillTransitionHandler);
router.off('didTransition', this._routerDidTransitionHandler);
if (this._useRAF) {
cancelAnimationFrame(this._nextPaintFrame);
}
run.cancel(this._nextPaintTimeout);
}
});
if (DEBUG) {
Scheduler.reopen({
init() {
this._super(...arguments);
if (Ember.testing) {
this._waiter = () => !this.hasActiveQueue();
Ember.Test.registerWaiter(this._waiter);
}
},
/**
* Method to detect if there is still an active queue
* @return {Boolean}
*/
hasActiveQueue() {
if (!this.queues) {
return true;
}
const lastQueueName = this.queueNames[this.queueNames.length - 1];
const lastQueue = this.queues[lastQueueName];
const hasActiveQueue = lastQueue && lastQueue.isActive;
const hasTasks = lastQueue.tasks.length > 0;
return hasActiveQueue && hasTasks;
},
willDestroy() {
if (this._waiter) {
Ember.Test.unregisterWaiter(this._waiter);
this._waiter = null;
}
this._super(...arguments);
}
});
}
export default Scheduler;