Rewrite of app-scheduler to functional equivalent that utilizes promises vs. queues
MODIFY
README.md
README.md
@@ -17,7 +17,7 @@ This addon provides a service to schedule work in different paint phases of rend
The `afterFirstRoutePaint` queue can be used to schedule work after the route is first painted. This is useful for scenarios like rendering content outside viewport, rendering non critical content etc.
```javascript
-this.get('scheduler').on('afterFirstRoutePaint', () => {
+this.get('scheduler').scheduleWork('afterFirstRoutePaint', () => {
// schedule work
});
```
@@ -25,7 +25,7 @@ this.get('scheduler').on('afterFirstRoutePaint', () => {
The `afterContentPaint` queue can be used to schedule work after all the important content of the page has been painted. This is useful for scenarios like rendering ads, scheduling tracking work, rendering of popup overlays etc.
```javascript
-this.get('scheduler').on('afterContentPaint', () => {
+this.get('scheduler').scheduleWork('afterContentPaint', () => {
// schedule work
});
```

ADD
addon/index.js
+export {
+ didTransition,
+ afterFirstRoutePaint,
+ afterContentPaint,
+} from './scheduler';

DELETE
addon/initializers/app-scheduler.js
-export function initialize(application) {
- application.inject('service:scheduler', 'router', 'router:main');
-}
-
-export default {
- name: 'app-scheduler',
- initialize,
-};

DELETE
addon/instance-initializers/init-app-scheduler.js
-export function initialize(application) {
- application.lookup('service:scheduler');
-}
-
-export default {
- name: 'init-app-scheduler',
- initialize: initialize,
-};

ADD
addon/scheduler.js
+import RSVP from 'rsvp';
+import { run } from '@ember/runloop';
+
+const USE_RAF = typeof requestAnimationFrame === 'function';
+let _didTransition;
+let _afterFirstRoutePaint;
+let _afterContentPaint;
+
+reset();
+
+export function beginTransition() {
+ _didTransition = RSVP.defer();
+ _afterFirstRoutePaint = _didTransition.promise.then(() => afterNextPaint());
+ _afterContentPaint = _afterFirstRoutePaint.then(() => afterNextPaint());
+}
+
+export function endTransition() {
+ _didTransition.resolve();
+}
+
+export function setupRouter(router) {
+ router.on('willTransition', beginTransition);
+ router.on('didTransition', endTransition);
+}
+
+export function reset() {
+ _didTransition = RSVP.defer();
+ _afterFirstRoutePaint = _didTransition.promise.then(() => {});
+ _afterContentPaint = _afterFirstRoutePaint.then(() => {});
+ _didTransition.resolve();
+}
+
+/**
+ * Top level promise that represents the entry point for deferred work.
+ * Subsequent promises are chained off this promise, successively composing
+ * them together to approximate when painting has occurred.
+ */
+export function didTransition() {
+ return _didTransition.promise;
+}
+
+/**
+ * This promise, when resolved, approximates after the route is first painted.
+ * This can be used to schedule work to occur that is lower priority than initial
+ * work (content outside of the viewport, rendering non-critical content).
+ */
+export function afterFirstRoutePaint() {
+ return _afterFirstRoutePaint;
+}
+
+/**
+ * This promise, when resolved, approximates after content is painted.
+ */
+export function afterContentPaint() {
+ return _afterContentPaint;
+}
+
+function afterNextPaint() {
+ return new RSVP.Promise(resolve => {
+ if (USE_RAF) {
+ requestAnimationFrame(() => {
+ run.later(resolve, 0);
+ });
+ } else {
+ run.later(resolve, 0);
+ }
+ });
+}

MODIFY
addon/services/scheduler.js
addon/services/scheduler.js
@@ -4,6 +4,7 @@ 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({
@@ -11,17 +12,20 @@ const Scheduler = Service.extend({
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();
- this._useRAF = typeof requestAnimationFrame === 'function';
},
- on(queueName, callback) {
+ scheduleWork(queueName, callback) {
const queue = this.queues[queueName];
if (queue.isActive) {
@@ -33,7 +37,7 @@ const Scheduler = Service.extend({
return callback;
},
- off(queueName, token) {
+ cancelWork(queueName, token) {
const queue = this.queues[queueName];
queue.cancel(token);

DELETE
app/initializers/app-scheduler.js
-export {
- default,
- initialize,
-} from 'ember-app-scheduler/initializers/app-scheduler';

DELETE
app/instance-initializers/init-app-scheduler.js
-export {
- default,
- initialize,
-} from 'ember-app-scheduler/instance-initializers/init-app-scheduler';

MODIFY
tests/dummy/app/components/defer-render.js
tests/dummy/app/components/defer-render.js
@@ -9,18 +9,21 @@ export default Component.extend({
init() {
this._super();
- this._token = this.get('scheduler').on('afterFirstRoutePaint', () => {
+ 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').off('afterFirstRoutePaint', this._token);
+ this.get('scheduler').cancelWork('afterFirstRoutePaint', this._token);
}
},
});

MODIFY
tests/dummy/app/components/right-rail-ad.js
tests/dummy/app/components/right-rail-ad.js
@@ -9,18 +9,21 @@ export default Component.extend({
init() {
this._super();
- this._token = this.get('scheduler').on('afterContentPaint', () => {
+ 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').off('afterContentPaint', this._token);
+ this.get('scheduler').cancelWork('afterContentPaint', this._token);
}
},
});

ADD
tests/integration/deferred-render-in-component-test.js
+import { module, test } from 'qunit';
+import Component from '@ember/component';
+import hbs from 'htmlbars-inline-precompile';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, waitFor } from '@ember/test-helpers';
+import { beginTransition, endTransition } from 'ember-app-scheduler/scheduler';
+import { afterFirstRoutePaint, afterContentPaint } from 'ember-app-scheduler';
+
+module('deferred render in component', function(hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function() {
+ let testContext = this;
+ let name = 'component:deferred-render';
+
+ this.owner.register(
+ name,
+ Component.extend({
+ showFoo: false,
+ showBar: false,
+
+ init() {
+ this._super(...arguments);
+ testContext.componentInstance = this;
+ },
+ destroy() {
+ this._super(...arguments);
+ },
+
+ didInsertElement() {
+ afterFirstRoutePaint().then(() => {
+ this.set('showFoo', true);
+ });
+
+ afterContentPaint().then(() => {
+ this.set('showBar', true);
+ });
+ },
+ })
+ );
+
+ this.Component = this.owner.factoryFor
+ ? this.owner.factoryFor(name)
+ : this.owner._lookupFactory(name);
+ });
+
+ test('deferred element is visible following afterFirstRoutePaint', async function(assert) {
+ let foo;
+
+ this.owner.register(
+ 'template:components/deferred-render',
+ hbs`{{#if showFoo}}<span class="foo"></span>{{/if}}`
+ );
+
+ beginTransition();
+
+ await render(hbs`{{deferred-render}}`);
+
+ foo = find('.foo');
+
+ assert.isNotVisible(foo);
+
+ endTransition();
+
+ foo = await waitFor('.foo');
+
+ assert.isVisible(foo);
+ });
+
+ test('deferred element is visible following afterContentPaint', async function(assert) {
+ let foo;
+
+ this.owner.register(
+ 'template:components/deferred-render',
+ hbs`{{#if showBar}}<span class="bar"></span>{{/if}}`
+ );
+
+ beginTransition();
+
+ await render(hbs`{{deferred-render}}`);
+
+ foo = find('.bar');
+
+ assert.isNotVisible(foo);
+
+ endTransition();
+
+ foo = await waitFor('.bar');
+
+ assert.isVisible(foo);
+ });
+});

DELETE
tests/unit/initializers/app-scheduler-test.js
-import Application from '@ember/application';
-import { run } from '@ember/runloop';
-import { initialize } from 'dummy/initializers/app-scheduler';
-import { module, test } from 'qunit';
-import destroyApp from '../../helpers/destroy-app';
-
-module('Unit | Initializer | app scheduler', function(hooks) {
- hooks.beforeEach(function() {
- run(() => {
- this.application = Application.create();
- this.application.deferReadiness();
- });
- });
-
- hooks.afterEach(function() {
- destroyApp(this.application);
- });
-
- // Replace this with your real tests.
- test('it works', function(assert) {
- initialize(this.application);
-
- // you would normally confirm the results of the initializer here
- assert.ok(true);
- });
-});

MODIFY
tests/unit/services/scheduler-test.js
tests/unit/services/scheduler-test.js
import { run } from '@ember/runloop';
-import Evented from '@ember/object/evented';
-import Service from '@ember/service';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
@@ -10,15 +8,11 @@ module('Unit | Service | scheduler', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
- const MockRouterService = Service.extend(Evented);
-
- // At the current moment, the router service is injected via the `app-scheduler` initializer.
- this.router = MockRouterService.create();
let name = 'service:scheduler';
let factory = this.owner.factoryFor
? this.owner.factoryFor(name)
: this.owner._lookupFactory(name);
- this.scheduler = factory.create({ router: this.router });
+ this.scheduler = factory.create();
});
test('it should have no active queues', function(assert) {
@@ -32,11 +26,11 @@ module('Unit | Service | scheduler', function(hooks) {
let myWork = () => true;
- const workToken = this.scheduler.on(AFTER_CONTENT_PAINT, myWork);
+ const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
- this.scheduler.off(AFTER_CONTENT_PAINT, workToken);
+ this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
});
test('it should not have an active queue after flushing that queue', function(assert) {
@@ -44,7 +38,7 @@ module('Unit | Service | scheduler', function(hooks) {
let myWork = () => true;
- const workToken = this.scheduler.on(AFTER_CONTENT_PAINT, myWork);
+ const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
@@ -54,7 +48,7 @@ module('Unit | Service | scheduler', function(hooks) {
assert.notOk(this.scheduler.hasActiveQueue(), 'has no active queues');
- this.scheduler.off(AFTER_CONTENT_PAINT, workToken);
+ this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
});
test("it should have no active queues after the router's willTransition event", function(assert) {
@@ -62,17 +56,17 @@ module('Unit | Service | scheduler', function(hooks) {
let myWork = () => true;
- const workToken = this.scheduler.on(AFTER_CONTENT_PAINT, myWork);
+ const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
assert.ok(this.scheduler.hasActiveQueue(), 'has active queues');
run(() => {
- this.router.trigger('willTransition');
+ this.scheduler.router.trigger('willTransition');
});
assert.notOk(this.scheduler.hasActiveQueue(), 'has no active queues');
- this.scheduler.off(AFTER_CONTENT_PAINT, workToken);
+ this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
});
test('it should cancel work when given a token', function(assert) {
@@ -80,9 +74,9 @@ module('Unit | Service | scheduler', function(hooks) {
let myWork = () => true;
- const workToken = this.scheduler.on(AFTER_CONTENT_PAINT, myWork);
+ const workToken = this.scheduler.scheduleWork(AFTER_CONTENT_PAINT, myWork);
- this.scheduler.off(AFTER_CONTENT_PAINT, workToken);
+ this.scheduler.cancelWork(AFTER_CONTENT_PAINT, workToken);
assert.equal(
this.scheduler.queues[AFTER_CONTENT_PAINT].tasks.indexOf(workToken),

Rewrite of app-scheduler to functional equivalent that utilizes promises vs. queues