Inital commit with a 'scheduler' service to schedule work after the Ember route is first painted
MODIFY
README.md
README.md
# ember-app-scheduler
-This README outlines the details of collaborating on this Ember addon.
+This addon provides a service to schedule work at different phases of app life cycle.
## Installation
@@ -9,6 +9,19 @@ This README outlines the details of collaborating on this Ember addon.
* `npm install`
* `bower install`
+## Usage
+
+### Schedule work after the First Route Paint
+
+The `afterRoutePaint` queue can be used to schedule work after the route
+is painted.
+
+```javascript
+this.get('scheduler').scheduleWork('afterRoutePaint', () => {
+ // schedule work
+});
+```
+
## Running
* `ember serve`

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

ADD
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/services/scheduler.js
+import Ember from 'ember';
+
+class Token {
+ constructor() {
+ this._cancelled = false;
+ }
+
+ get cancelled() {
+ return this._cancelled;
+ }
+
+ cancel() {
+ this._cancelled = true;
+ }
+}
+
+class Queue {
+ constructor(queueName) {
+ this.queueName = queueName;
+ this.reset();
+ }
+
+ reset() {
+ this.tasks = [];
+ this.isActive = false;
+ }
+}
+
+export default Ember.Service.extend({
+ queueNames: ['afterRoutePaint'],
+
+ init() {
+ this._super(...arguments);
+ this._nextPaintFrame = null;
+ this._nextPaintTimeout = null;
+ this._nextAfterPaintPromise = null;
+ this._initQueues();
+ this._connectToRouter();
+ },
+
+ scheduleWork(queueName, callback) {
+ const queue = this.queues[queueName];
+ if (!queue.isActive) {
+ const token = new Token();
+ queue.tasks.push(callback);
+ queue.tasks.push(token);
+ return token;
+ } else {
+ callback();
+ }
+ },
+
+ 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();
+ }
+ }
+ },
+
+ _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 Ember.RSVP.Promise((resolve) => {
+ this._nextPaintFrame = requestAnimationFrame(() => {
+ this._nextPaintTimeout = Ember.run.later(() => {
+ this._nextAfterPaintPromise = null;
+ this._nextPaintFrame = null;
+ this._nextPaintTimeout = null;
+ resolve();
+ }, 0);
+ });
+ });
+
+ return this._nextAfterPaintPromise;
+ },
+
+ // TODO on destroy this should be disconnected I think
+ _connectToRouter() {
+ const router = this.get('router');
+
+ router.on('willTransition', () => {
+ this._resetQueues();
+ });
+
+ router.on('didTransition', () => {
+ this._afterNextPaint()
+ .then(() => {
+ this.flushQueue('afterRoutePaint');
+ });
+ });
+ },
+});

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

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

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

ADD
tests/dummy/app/components/defer-render.js
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ shouldRender: false,
+ scheduler: Ember.inject.service(),
+
+ init() {
+ this._super();
+ this.get('scheduler').scheduleWork('afterRoutePaint', () => {
+ Ember.run.join(() => {
+ this.set('shouldRender', true);
+ this.isRendered = true;
+ });
+ });
+ },
+});

MODIFY
tests/dummy/app/router.js
tests/dummy/app/router.js
@@ -7,6 +7,7 @@ const Router = Ember.Router.extend({
});
Router.map(function() {
+ this.route('deferred-content');
});
export default Router;

ADD
tests/dummy/app/routes/deferred-content.js
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model() {
+ let people = [];
+ for (let i = 1; i <= 10000; i++) {
+ people.push({
+ 'name': 'Person ' + i
+ });
+ }
+ return Ember.RSVP.hash({
+ people: people
+ });
+ },
+ actions: {
+ didTransition() {
+ return true;
+ }
+ }
+});

MODIFY
tests/dummy/app/templates/application.hbs
tests/dummy/app/templates/application.hbs
-{{!-- The following component displays Ember's default welcome message. --}}
-{{welcome-page}}
-{{!-- Feel free to remove this! --}}
+<h2 id="title">Welcome to app scheduler dummy app</h2>
{{outlet}}

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

ADD
tests/dummy/app/templates/deferred-content.hbs
+<div class="content">
+ <div style="width:50%;float:left">
+ <h3>Primary content</h3>
+ <ul>
+ {{#each model.people as |person index|}}
+ <li>Hello, {{person.name}}!</li>
+ {{/each}}
+ </ul>
+ </div>
+ <div style="width:50%;float:left">
+ <h3>Deferred content</h3>
+ {{#defer-render}}
+ <ul>
+ {{#each model.people as |person index|}}
+ <li>Hello, {{person.name}}!</li>
+ {{/each}}
+ </ul>
+ {{/defer-render}}
+ </div>
+</div>

ADD
tests/unit/initializers/app-scheduler-test.js
+import Ember from 'ember';
+import { initialize } from 'dummy/initializers/app-scheduler';
+import { module, test } from 'qunit';
+import destroyApp from '../../helpers/destroy-app';
+
+module('Unit | Initializer | app scheduler', {
+ beforeEach() {
+ Ember.run(() => {
+ this.application = Ember.Application.create();
+ this.application.deferReadiness();
+ });
+ },
+ afterEach() {
+ 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);
+});

Inital commit with a 'scheduler' service to schedule work after the Ember route is first painted