Removing service
DELETE
addon/services/scheduler.js
-import Ember from 'ember';
-import RSVP from 'rsvp';
-import { registerWaiter, unregisterWaiter } from '@ember/test';
-import { run } from '@ember/runloop';
-import Service from '@ember/service';
-import { DEBUG } from '@glimmer/env';
-import { getOwner } from '@ember/application';
-import TaskQueue from '../task-queue';
-
-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.router = getOwner(this).lookup('router:main');
- this._useRAF = typeof requestAnimationFrame === 'function';
-
- this._initQueues();
- this._connectToRouter();
- },
-
- scheduleWork(queueName, callback) {
- const queue = this.queues[queueName];
-
- if (queue.isActive) {
- queue.enqueue(callback);
- } else {
- callback();
- }
-
- return callback;
- },
-
- cancelWork(queueName, token) {
- const queue = this.queues[queueName];
-
- queue.cancel(token);
- },
-
- flushQueue(queueName) {
- const queue = this.queues[queueName];
- queue.flush();
-
- 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 TaskQueue();
- }
- },
-
- _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();
- 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) {
- unregisterWaiter(this._waiter);
- this._waiter = null;
- }
-
- this._super(...arguments);
- },
- });
-}
-
-export default Scheduler;

DELETE
addon/task-queue.js
-import RSVP from 'rsvp';
-
-export default class TaskQueue {
- constructor() {
- this.reset();
- }
-
- enqueue(task) {
- this.tasks.push(task);
- }
-
- cancel(token) {
- let index = this.tasks.indexOf(token);
-
- if (index > -1) {
- this.tasks.splice(index, 1);
- }
- }
-
- flush() {
- this.isActive = false;
-
- for (let i = 0; i < this.tasks.length; i++) {
- this.tasks[i]();
- }
-
- this.tasks = [];
- this.isActive = true;
- }
-
- size() {
- return this.tasks.length;
- }
-
- reset() {
- this.tasks = [];
- this.isActive = true;
- this.afterPaintDeferred = RSVP.defer();
- this.afterPaintPromise = this.afterPaintDeferred.promise;
- }
-}

DELETE
app/services/scheduler.js
-export { default } from 'ember-app-scheduler/services/scheduler';

DELETE
tests/acceptance/deferred-render-with-service-test.js
-import { module, test } from 'qunit';
-import { setupApplicationTest } from 'ember-qunit';
-import { visit, find } from '@ember/test-helpers';
-
-module('Acceptance | deferred render', function(hooks) {
- setupApplicationTest(hooks);
-
- hooks.beforeEach(function() {
- this.scheduler = this.owner.lookup('service:scheduler');
- this.router = this.owner.lookup('router:main');
- });
-
- test('visiting /demo', async function(assert) {
- assert.expect(3);
- let done = assert.async();
-
- this.router.on('didTransition', () => {
- this.scheduler.queues['afterFirstRoutePaint'].afterPaintPromise.then(
- () => {
- const deferredElement = find('.deferred-container ul');
- assert.isVisible(
- deferredElement,
- 'Deferred content should be visible.'
- );
- const adElement = find('.ad-container h3');
- assert.isNotVisible(adElement, 'Ad should not be visible.');
- }
- );
-
- this.scheduler.queues['afterContentPaint'].afterPaintPromise.then(() => {
- const adElement = find('.ad-container h3');
- assert.isVisible(adElement, 'Ad should be visible.');
- done();
- });
- });
-
- await visit('/demo');
- });
-
- test('visiting /demo when requestAnimationFrame is not present', async function(assert) {
- this.scheduler._useRAF = false;
- assert.expect(3);
- let done = assert.async();
-
- this.router.on('didTransition', () => {
- this.scheduler.queues['afterFirstRoutePaint'].afterPaintPromise.then(
- () => {
- const deferredElement = find('.deferred-container ul');
- assert.isVisible(
- deferredElement,
- 'Deferred content should be visible.'
- );
- const adElement = find('.ad-container h3');
- assert.isNotVisible(adElement, 'Ad should not be visible.');
- }
- );
-
- this.scheduler.queues['afterContentPaint'].afterPaintPromise.then(() => {
- const adElement = find('.ad-container h3');
- assert.isVisible(adElement, 'Ad should be visible.');
- this.scheduler._useRAF = true;
- done();
- });
- });
-
- await visit('/demo');
- });
-});

DELETE
tests/dummy/app/components/defer-render.js
-import { join } from '@ember/runloop';
-import { inject as service } from '@ember/service';
-import Component from '@ember/component';
-
-export default Component.extend({
- shouldRender: false,
- scheduler: service(),
- classNames: ['deferred-container'],
-
- init() {
- this._super();
- this._token = this.get('scheduler').scheduleWork(
- 'afterFirstRoutePaint',
- () => {
- join(() => {
- this.set('shouldRender', true);
- this.isRendered = true;
- });
- }
- );
- },
-
- willDestroyElement() {
- this._super(...arguments);
- if (this._token) {
- this.get('scheduler').cancelWork('afterFirstRoutePaint', this._token);
- }
- },
-});

DELETE
tests/dummy/app/components/right-rail-ad.js
-import { join } from '@ember/runloop';
-import { inject as service } from '@ember/service';
-import Component from '@ember/component';
-
-export default Component.extend({
- shouldRender: false,
- scheduler: service(),
- classNames: ['ad-container'],
-
- init() {
- this._super();
- this._token = this.get('scheduler').scheduleWork(
- 'afterContentPaint',
- () => {
- join(() => {
- this.set('shouldRender', true);
- this.isRendered = true;
- });
- }
- );
- },
-
- willDestroyElement() {
- this._super(...arguments);
- if (this._token) {
- this.get('scheduler').cancelWork('afterContentPaint', this._token);
- }
- },
-});

MODIFY
tests/dummy/app/router.js
tests/dummy/app/router.js
@@ -7,7 +7,6 @@ const Router = EmberRouter.extend({
});
Router.map(function() {
- this.route('demo');
this.route('first-paint');
this.route('content-paint');
this.route('aborted-paint');

DELETE
tests/dummy/app/routes/demo.js
-import { hash } from 'rsvp';
-import Route from '@ember/routing/route';
-
-export default Route.extend({
- model() {
- let people = [];
- for (let i = 1; i <= 10; i++) {
- people.push({
- name: 'Person ' + i,
- });
- }
-
- let cats = [];
- for (let i = 1; i <= 5; i++) {
- cats.push({
- name: 'Cat ' + i,
- });
- }
-
- return hash({
- people: people,
- cats: cats,
- });
- },
-});

MODIFY
tests/dummy/app/templates/application.hbs
tests/dummy/app/templates/application.hbs
<h2 id="title">Welcome to app scheduler dummy app</h2>
{{#link-to "index"}}Home{{/link-to}}
-{{#link-to "demo"}}Demo{{/link-to}}
{{#link-to "first-paint"}}First Paint{{/link-to}}
{{#link-to "content-paint"}}Content Paint{{/link-to}}
{{#link-to "aborted-paint"}}Aborted Paint{{/link-to}}

DELETE
tests/dummy/app/templates/components/defer-render.hbs
-{{#if shouldRender}}
- {{yield}}
-{{/if}}

DELETE
tests/dummy/app/templates/components/right-rail-ad.hbs
-{{#if shouldRender}}
- {{yield}}
-{{/if}}

DELETE
tests/dummy/app/templates/demo.hbs
-<div class="content">
- <div style="width:70%;float:left">
- <h3>Primary content</h3>
- <ul>
- {{#each model.people as |person index|}}
- <li>Hello, {{person.name}}!</li>
- {{/each}}
- </ul>
-
- <h3>Secondary content</h3>
- {{#defer-render}}
- <ul>
- {{#each model.cats as |cat index|}}
- <li>Hello, {{cat.name}}!</li>
- {{/each}}
- </ul>
- {{/defer-render}}
- </div>
-
- <div style="width:30%;float:left">
- {{#right-rail-ad}}
- <h3>Sample Ads Content</h3>
- {{/right-rail-ad}}
- </div>
-</div>

DELETE
tests/unit/services/scheduler-test.js
-import { run } from '@ember/runloop';
-import { module, test } from 'qunit';
-import { setupTest } from 'ember-qunit';
-
-const AFTER_CONTENT_PAINT = 'afterContentPaint';
-
-module('Unit | Service | scheduler', function(hooks) {
- setupTest(hooks);
-
- hooks.beforeEach(function() {
- let name = 'service:scheduler';
- let factory = this.owner.factoryFor
- ? this.owner.factoryFor(name)
- : this.owner._lookupFactory(name);
- this.scheduler = factory.create();
- });
-
- test('it should have no active queues', function(assert) {
- assert.expect(1);
-
- assert.notOk(this.scheduler.hasActiveQueue(), 'has no active queues');
- });
-
- test('it should have an active queue after scheduling work', function(assert) {
- assert.expect(1);
-
- let myWork = () => true;
-
- const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
-
- assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
-
- this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
- });
-
- test('it should not have an active queue after flushing that queue', function(assert) {
- assert.expect(2);
-
- let myWork = () => true;
-
- const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
-
- assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
-
- run(() => {
- this.scheduler.flushQueue(AFTER_CONTENT_PAINT);
- });
-
- assert.notOk(this.scheduler.hasActiveQueue(), 'has no active queues');
-
- this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
- });
-
- test("it should have no active queues after the router's willTransition event", function(assert) {
- assert.expect(2);
-
- let myWork = () => true;
-
- const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
-
- assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
-
- run(() => {
- this.scheduler.router.trigger('willTransition');
- });
-
- assert.notOk(this.scheduler.hasActiveQueue(), 'has no active queues');
-
- this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
- });
-
- test('it should cancel work when given a token', function(assert) {
- assert.expect(1);
-
- let myWork = () => true;
-
- const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
-
- this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
-
- assert.equal(
- this.scheduler.queues[AFTER_CONTENT_PAINT].tasks.indexOf(workToken),
- -1,
- 'token is cancelled'
- );
- });
-});

DELETE
tests/unit/task-queue-test.js
-import { module, test } from 'qunit';
-import TaskQueue from 'ember-app-scheduler/task-queue';
-
-module(`Unit | ember-app-scheduler | Queue`, function(hooks) {
- hooks.beforeEach(function() {
- this.taskQueue = new TaskQueue();
-
- this.createTask = () => {
- return () => {};
- };
- });
-
- test('queue size is 0 when instantiated', function(assert) {
- assert.expect(1);
-
- assert.equal(this.taskQueue.size(), 0, 'size is 0');
- });
-
- test('queue size is 1 when item is enqueued', function(assert) {
- assert.expect(1);
-
- this.taskQueue.enqueue(this.createTask());
-
- assert.equal(this.taskQueue.size(), 1, 'size is 1');
- });
-
- test('task is enqueued', function(assert) {
- assert.expect(1);
-
- const task = this.createTask();
-
- this.taskQueue.enqueue(task);
-
- assert.equal(task, this.taskQueue.tasks[0], 'task is correctly enqueued');
- });
-
- test('queue flushes correctly', function(assert) {
- assert.expect(2);
-
- const firstTask = this.createTask();
- const secondTask = this.createTask();
-
- this.taskQueue.enqueue(firstTask);
- this.taskQueue.enqueue(secondTask);
-
- this.taskQueue.flush();
-
- assert.equal(this.taskQueue.size(), 0, 'size is 0');
- assert.ok(this.taskQueue.isActive, 'queue is active');
- });
-});

Removing service