From cf55da239ada16d04dd9d6e3e4a6391b6ff1f365 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 21 Jan 2015 18:16:34 -0800 Subject: [PATCH] Introduce instance initializers This commit introduces a new (feature flagged) API for adding instance initializers. Instance initializers differ from normal initializers in that they are passed the app instance rather than a registry, and therefore can access instances from the container in a safe way. This design not only allows us to avoid expensive app setup for each FastBoot request, it also minimizes the amount of work required between acceptance test runs (once the testing infrastructure is updated to take advantage of it). This commit also removes a previously introduced deprecation that was not behind a feature flag. That deprecation (when emitted with the feature flag enabled) now points to a comprehensive deprecation guide. --- FEATURES.md | 21 ++ features.json | 1 + packages/container/lib/registry.js | 12 +- .../lib/system/application.js | 92 ++++-- .../tests/system/initializers_test.js | 20 ++ .../system/instance_initializers_test.js | 284 ++++++++++++++++++ 6 files changed, 397 insertions(+), 33 deletions(-) create mode 100644 packages/ember-application/tests/system/instance_initializers_test.js diff --git a/FEATURES.md b/FEATURES.md index 6eba48b47ab..91c8a673601 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -5,6 +5,27 @@ for a detailed explanation. ## Feature Flags +* `ember-application-instance-initializers` + + Splits apart initializers into two phases: + + * Boot-time Initializers that receive a registry, and use it to set up + code structures + * Instance Initializers that receive an application instance, and use + it to set up application state per run of the application. + + In FastBoot, each request will have its own run of the application, + and will only need to run the instance initializers. + + In the future, tests will also be able to use this differentiation to + run just the instance initializers per-test. + + With this change, `App.initializer` becomes a "boot-time" initializer, + and issues a deprecation warning if instances are accessed. + + Apps should migrate any initializers that require instances to the new + `App.instanceInitializer` API. + * `ember-application-initializer-context` Sets the context of the initializer function to its object instead of the diff --git a/features.json b/features.json index 8b5573546b4..572628786bf 100644 --- a/features.json +++ b/features.json @@ -15,6 +15,7 @@ "ember-testing-checkbox-helpers": null, "ember-metal-stream": null, "ember-htmlbars-each-with-index": true, + "ember-application-instance-initializers": null, "ember-application-initializer-context": null }, "debugStatements": [ diff --git a/packages/container/lib/registry.js b/packages/container/lib/registry.js index 1bef0bb348c..8356bb04a49 100644 --- a/packages/container/lib/registry.js +++ b/packages/container/lib/registry.js @@ -136,13 +136,21 @@ Registry.prototype = { lookup: function(fullName, options) { Ember.assert('Create a container on the registry (with `registry.container()`) before calling `lookup`.', this._defaultContainer); - Ember.deprecate('`lookup` should not be called on a Registry. Call `lookup` directly on an associated Container instead.'); + + if (Ember.FEATURES.isEnabled('ember-application-instance-initializers')) { + Ember.deprecate('`lookup` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container.', { url: "http://emberjs.com/guides/deprecations#toc_deprecate-access-to-instances-in-initializers" }); + } + return this._defaultContainer.lookup(fullName, options); }, lookupFactory: function(fullName) { Ember.assert('Create a container on the registry (with `registry.container()`) before calling `lookupFactory`.', this._defaultContainer); - Ember.deprecate('`lookupFactory` should not be called on a Registry. Call `lookupFactory` directly on an associated Container instead.'); + + if (Ember.FEATURES.isEnabled('ember-application-instance-initializers')) { + Ember.deprecate('`lookupFactory` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container.', { url: "http://emberjs.com/guides/deprecations#toc_deprecate-access-to-instances-in-initializers" }); + } + return this._defaultContainer.lookupFactory(fullName); }, diff --git a/packages/ember-application/lib/system/application.js b/packages/ember-application/lib/system/application.js index 53a122e337c..a8303d6554f 100644 --- a/packages/ember-application/lib/system/application.js +++ b/packages/ember-application/lib/system/application.js @@ -528,7 +528,9 @@ var Application = Namespace.extend(DeferredMixin, { _initialize: function() { if (this.isDestroyed) { return; } - this.runInitializers(); + this.runInitializers(this.__registry__); + this.runInstanceInitializers(this.__instance__); + runLoadHooks('application', this); // At this point, any initializers or load hooks that would have wanted @@ -627,12 +629,31 @@ var Application = Namespace.extend(DeferredMixin, { @private @method runInitializers */ - runInitializers: function() { - var initializersByName = get(this.constructor, 'initializers'); + runInitializers: function(registry) { + var App = this; + this._runInitializer('initializers', function(name, initializer) { + Ember.assert("No application initializer named '" + name + "'", !!initializer); + + if (Ember.FEATURES.isEnabled("ember-application-initializer-context")) { + initializer.initialize(registry, App); + } else { + var ref = initializer.initialize; + ref(registry, App); + } + }); + }, + + runInstanceInitializers: function(instance) { + this._runInitializer('instanceInitializers', function(name, initializer) { + Ember.assert("No instance initializer named '" + name + "'", !!initializer); + initializer.initialize(instance); + }); + }, + + _runInitializer: function(bucketName, cb) { + var initializersByName = get(this.constructor, bucketName); var initializers = props(initializersByName); - var registry = this.__registry__; var graph = new DAG(); - var namespace = this; var initializer; for (var i = 0; i < initializers.length; i++) { @@ -641,15 +662,7 @@ var Application = Namespace.extend(DeferredMixin, { } graph.topsort(function (vertex) { - var initializer = vertex.value; - Ember.assert("No application initializer named '" + vertex.name + "'", !!initializer); - - if (Ember.FEATURES.isEnabled("ember-application-initializer-context")) { - initializer.initialize(registry, namespace); - } else { - var ref = initializer.initialize; - ref(registry, namespace); - } + cb(vertex.name, vertex.value); }); }, @@ -737,8 +750,21 @@ var Application = Namespace.extend(DeferredMixin, { } }); +if (Ember.FEATURES.isEnabled('ember-application-instance-initializers')) { + Application.reopen({ + instanceInitializer: function(options) { + this.constructor.instanceInitializer(options); + } + }); + + Application.reopenClass({ + instanceInitializer: buildInitializerMethod('instanceInitializers', 'instance initializer') + }); +} + Application.reopenClass({ initializers: create(null), + instanceInitializers: create(null), /** Initializer receives an object which has the following attributes: @@ -863,23 +889,7 @@ Application.reopenClass({ @method initializer @param initializer {Object} */ - initializer: function(initializer) { - // If this is the first initializer being added to a subclass, we are going to reopen the class - // to make sure we have a new `initializers` object, which extends from the parent class' using - // prototypal inheritance. Without this, attempting to add initializers to the subclass would - // pollute the parent class as well as other subclasses. - if (this.superclass.initializers !== undefined && this.superclass.initializers === this.initializers) { - this.reopenClass({ - initializers: create(this.initializers) - }); - } - - Ember.assert("The initializer '" + initializer.name + "' has already been registered", !this.initializers[initializer.name]); - Ember.assert("An initializer cannot be registered without an initialize function", canInvoke(initializer, 'initialize')); - Ember.assert("An initializer cannot be registered without a name property", initializer.name !== undefined); - - this.initializers[initializer.name] = initializer; - }, + initializer: buildInitializerMethod('initializers', 'initializer'), /** This creates a registry with the default Ember naming conventions. @@ -1046,4 +1056,24 @@ function logLibraryVersions() { } } +function buildInitializerMethod(bucketName, humanName) { + return function(initializer) { + // If this is the first initializer being added to a subclass, we are going to reopen the class + // to make sure we have a new `initializers` object, which extends from the parent class' using + // prototypal inheritance. Without this, attempting to add initializers to the subclass would + // pollute the parent class as well as other subclasses. + if (this.superclass[bucketName] !== undefined && this.superclass[bucketName] === this[bucketName]) { + var attrs = {}; + attrs[bucketName] = create(this[bucketName]); + this.reopenClass(attrs); + } + + Ember.assert("The " + humanName + " '" + initializer.name + "' has already been registered", !this[bucketName][initializer.name]); + Ember.assert("An " + humanName + " cannot be registered without an initialize function", canInvoke(initializer, 'initialize')); + Ember.assert("An " + humanName + " cannot be registered without a name property", initializer.name !== undefined); + + this[bucketName][initializer.name] = initializer; + }; +} + export default Application; diff --git a/packages/ember-application/tests/system/initializers_test.js b/packages/ember-application/tests/system/initializers_test.js index 1fc3410682d..f751c23f4ff 100644 --- a/packages/ember-application/tests/system/initializers_test.js +++ b/packages/ember-application/tests/system/initializers_test.js @@ -2,6 +2,7 @@ import run from "ember-metal/run_loop"; import Application from "ember-application/system/application"; import { indexOf } from "ember-metal/array"; import jQuery from "ember-views/system/jquery"; +import Registry from "container/registry"; var app; @@ -33,6 +34,25 @@ test("initializers require proper 'name' and 'initialize' properties", function( }); +test("initializers are passed a registry and App", function() { + var MyApplication = Application.extend(); + + MyApplication.initializer({ + name: 'initializer', + initialize: function(registry, App) { + ok(registry instanceof Registry, "initialize is passed a registry"); + ok(App instanceof Application, "initialize is passed an Application"); + } + }); + + run(function() { + app = MyApplication.create({ + router: false, + rootElement: '#qunit-fixture' + }); + }); +}); + test("initializers can be registered in a specified order", function() { var order = []; var MyApplication = Application.extend(); diff --git a/packages/ember-application/tests/system/instance_initializers_test.js b/packages/ember-application/tests/system/instance_initializers_test.js new file mode 100644 index 00000000000..a99fd59116c --- /dev/null +++ b/packages/ember-application/tests/system/instance_initializers_test.js @@ -0,0 +1,284 @@ +import run from "ember-metal/run_loop"; +import Application from "ember-application/system/application"; +import ApplicationInstance from "ember-application/system/application-instance"; +import { indexOf } from "ember-metal/array"; +import jQuery from "ember-views/system/jquery"; + +var app; + +if (Ember.FEATURES.isEnabled('ember-application-instance-initializers')) { + QUnit.module("Ember.Application instance initializers", { + setup: function() { + }, + + teardown: function() { + if (app) { + run(function() { app.destroy(); }); + } + } + }); + + test("initializers require proper 'name' and 'initialize' properties", function() { + var MyApplication = Application.extend(); + + expectAssertion(function() { + run(function() { + MyApplication.instanceInitializer({ name: 'initializer' }); + }); + }); + + expectAssertion(function() { + run(function() { + MyApplication.instanceInitializer({ initialize: Ember.K }); + }); + }); + + }); + + test("initializers are passed an app instance", function() { + var MyApplication = Application.extend(); + + MyApplication.instanceInitializer({ + name: 'initializer', + initialize: function(instance) { + ok(instance instanceof ApplicationInstance, "initialize is passed an application instance"); + } + }); + + run(function() { + app = MyApplication.create({ + router: false, + rootElement: '#qunit-fixture' + }); + }); + }); + + test("initializers can be registered in a specified order", function() { + var order = []; + var MyApplication = Application.extend(); + MyApplication.instanceInitializer({ + name: 'fourth', + after: 'third', + initialize: function(registry) { + order.push('fourth'); + } + }); + + MyApplication.instanceInitializer({ + name: 'second', + after: 'first', + before: 'third', + initialize: function(registry) { + order.push('second'); + } + }); + + MyApplication.instanceInitializer({ + name: 'fifth', + after: 'fourth', + before: 'sixth', + initialize: function(registry) { + order.push('fifth'); + } + }); + + MyApplication.instanceInitializer({ + name: 'first', + before: 'second', + initialize: function(registry) { + order.push('first'); + } + }); + + MyApplication.instanceInitializer({ + name: 'third', + initialize: function(registry) { + order.push('third'); + } + }); + + MyApplication.instanceInitializer({ + name: 'sixth', + initialize: function(registry) { + order.push('sixth'); + } + }); + + run(function() { + app = MyApplication.create({ + router: false, + rootElement: '#qunit-fixture' + }); + }); + + deepEqual(order, ['first', 'second', 'third', 'fourth', 'fifth', 'sixth']); + }); + + test("initializers can have multiple dependencies", function () { + var order = []; + var a = { + name: "a", + before: "b", + initialize: function(registry) { + order.push('a'); + } + }; + var b = { + name: "b", + initialize: function(registry) { + order.push('b'); + } + }; + var c = { + name: "c", + after: "b", + initialize: function(registry) { + order.push('c'); + } + }; + var afterB = { + name: "after b", + after: "b", + initialize: function(registry) { + order.push("after b"); + } + }; + var afterC = { + name: "after c", + after: "c", + initialize: function(registry) { + order.push("after c"); + } + }; + + Application.instanceInitializer(b); + Application.instanceInitializer(a); + Application.instanceInitializer(afterC); + Application.instanceInitializer(afterB); + Application.instanceInitializer(c); + + run(function() { + app = Application.create({ + router: false, + rootElement: '#qunit-fixture' + }); + }); + + ok(indexOf.call(order, a.name) < indexOf.call(order, b.name), 'a < b'); + ok(indexOf.call(order, b.name) < indexOf.call(order, c.name), 'b < c'); + ok(indexOf.call(order, b.name) < indexOf.call(order, afterB.name), 'b < afterB'); + ok(indexOf.call(order, c.name) < indexOf.call(order, afterC.name), 'c < afterC'); + }); + + test("initializers set on Application subclasses should not be shared between apps", function() { + var firstInitializerRunCount = 0; + var secondInitializerRunCount = 0; + var FirstApp = Application.extend(); + FirstApp.instanceInitializer({ + name: 'first', + initialize: function(registry) { + firstInitializerRunCount++; + } + }); + var SecondApp = Application.extend(); + SecondApp.instanceInitializer({ + name: 'second', + initialize: function(registry) { + secondInitializerRunCount++; + } + }); + jQuery('#qunit-fixture').html('
'); + run(function() { + FirstApp.create({ + router: false, + rootElement: '#qunit-fixture #first' + }); + }); + equal(firstInitializerRunCount, 1, 'first initializer only was run'); + equal(secondInitializerRunCount, 0, 'first initializer only was run'); + run(function() { + SecondApp.create({ + router: false, + rootElement: '#qunit-fixture #second' + }); + }); + equal(firstInitializerRunCount, 1, 'second initializer only was run'); + equal(secondInitializerRunCount, 1, 'second initializer only was run'); + }); + + test("initializers are concatenated", function() { + var firstInitializerRunCount = 0; + var secondInitializerRunCount = 0; + var FirstApp = Application.extend(); + FirstApp.instanceInitializer({ + name: 'first', + initialize: function(registry) { + firstInitializerRunCount++; + } + }); + + var SecondApp = FirstApp.extend(); + SecondApp.instanceInitializer({ + name: 'second', + initialize: function(registry) { + secondInitializerRunCount++; + } + }); + + jQuery('#qunit-fixture').html('
'); + run(function() { + FirstApp.create({ + router: false, + rootElement: '#qunit-fixture #first' + }); + }); + equal(firstInitializerRunCount, 1, 'first initializer only was run when base class created'); + equal(secondInitializerRunCount, 0, 'first initializer only was run when base class created'); + firstInitializerRunCount = 0; + run(function() { + SecondApp.create({ + router: false, + rootElement: '#qunit-fixture #second' + }); + }); + equal(firstInitializerRunCount, 1, 'first initializer was run when subclass created'); + equal(secondInitializerRunCount, 1, 'second initializers was run when subclass created'); + }); + + test("initializers are per-app", function() { + expect(0); + var FirstApp = Application.extend(); + FirstApp.instanceInitializer({ + name: 'shouldNotCollide', + initialize: function(registry) {} + }); + + var SecondApp = Application.extend(); + SecondApp.instanceInitializer({ + name: 'shouldNotCollide', + initialize: function(registry) {} + }); + }); + + if (Ember.FEATURES.isEnabled("ember-application-initializer-context")) { + test("initializers should be executed in their own context", function() { + expect(1); + var MyApplication = Application.extend(); + + MyApplication.instanceInitializer({ + name: 'coolBabeInitializer', + myProperty: 'coolBabe', + initialize: function(registry, application) { + equal(this.myProperty, 'coolBabe', 'should have access to its own context'); + } + }); + + run(function() { + app = MyApplication.create({ + router: false, + rootElement: '#qunit-fixture' + }); + }); + }); + } +}