From 2e19e0d61050682b1a356a8f0853a0b10f41bb40 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 29 Jul 2020 22:15:10 -0400 Subject: [PATCH] [FEATURE] Add @ember/destroyable feature. The primary implementation happened upstream in Glimmer itself, this is adding API docs and module exports to be used. --- packages/@ember/canary-features/index.ts | 2 + packages/@ember/destroyable/index.ts | 265 +++++++++++++++++++++++ packages/ember/index.js | 23 +- tests/docs/expected.js | 5 + 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 packages/@ember/destroyable/index.ts diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts index e4e6b7c8329..189b8db78ad 100644 --- a/packages/@ember/canary-features/index.ts +++ b/packages/@ember/canary-features/index.ts @@ -22,6 +22,7 @@ export const DEFAULT_FEATURES = { EMBER_ROUTING_MODEL_ARG: true, EMBER_GLIMMER_IN_ELEMENT: true, EMBER_CACHE_API: null, + EMBER_DESTROYABLES: null, }; /** @@ -83,3 +84,4 @@ export const EMBER_GLIMMER_SET_COMPONENT_TEMPLATE = featureValue( export const EMBER_ROUTING_MODEL_ARG = featureValue(FEATURES.EMBER_ROUTING_MODEL_ARG); export const EMBER_GLIMMER_IN_ELEMENT = featureValue(FEATURES.EMBER_GLIMMER_IN_ELEMENT); export const EMBER_CACHE_API = featureValue(FEATURES.EMBER_CACHE_API); +export const EMBER_DESTROYABLES = featureValue(FEATURES.EMBER_DESTROYABLES); diff --git a/packages/@ember/destroyable/index.ts b/packages/@ember/destroyable/index.ts new file mode 100644 index 00000000000..379ab4711eb --- /dev/null +++ b/packages/@ember/destroyable/index.ts @@ -0,0 +1,265 @@ +export { + assertDestroyablesDestroyed, + associateDestroyableChild, + destroy, + enableDestroyableTracking, + isDestroying, + isDestroyed, +} from '@glimmer/runtime'; + +import { + registerDestructor as _registerDestructor, + unregisterDestructor as _unregisterDestructor, +} from '@glimmer/runtime'; + +/** + Ember manages the lifecycles and lifetimes of many built in constructs, such + as components, and does so in a hierarchical way - when a parent component is + destroyed, all of its children are destroyed as well. + + This destroyables API exposes the basic building blocks for destruction: + + * registering a function to be ran when an object is destroyyed + * checking if an object is in a destroying state + * associate an object as a child of another so that the child object will be destroyed + when the associated parent object is destroyed. + + @module @ember/destroyable + @category EMBER_DESTROYABLES + @public +*/ + +/** + This function is used to associate a destroyable object with a parent. When the parent + is destroyed, all registered children will also be destroyed. + + ```js + class CustomSelect extends Component { + constructor() { + // obj is now a child of the component. When the component is destroyed, + // obj will also be destroyed, and have all of its destructors triggered. + this.obj = associateDestroyableChild(this, {}); + } + } + ``` + + Returns the associated child for convenience. + + @method associateDestroyableChild + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} parent the destroyable to entangle the child destroyables lifetime with + @param {Object|Function} child the destroyable to be entangled with the parents lifetime + @param {Function} destructor the destructor to run when the destroyable object is destroyed + @returns {Object|Function} the child argument + @static + @public +*/ + +/** + Receives a destroyable, and returns true if the destroyable has begun destroying. Otherwise returns + false. + + ```js + let obj = {}; + isDestroying(obj); // false + destroy(obj); + isDestroying(obj); // true + // ...sometime later, after scheduled destruction + isDestroyed(obj); // true + isDestroying(obj); // true + ``` + + @method isDestroying + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} destroyable the object to check + @returns {Boolean} + @static + @public +*/ + +/** + Receives a destroyable, and returns true if the destroyable has finished destroying. Otherwise + returns false. + + ```js + let obj = {}; + + isDestroyed(obj); // false + destroy(obj); + + // ...sometime later, after scheduled destruction + + isDestroyed(obj); // true + ``` + + @method isDestroyed + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} destroyable the object to check + @returns {Boolean} + @static + @public +*/ + +/** + Initiates the destruction of a destroyable object. It runs all associated destructors, and then + destroys all children recursively. + + ```js + let obj = {}; + + registerDestructor(obj, () => console.log('destroyed!')); + + destroy(obj); // this will schedule the destructor to be called + + // ...some time later, during scheduled destruction + + // destroyed! + ``` + + Destruction via `destroy()` follows these steps: + + 1, Mark the destroyable such that `isDestroying(destroyable)` returns `true` + 2, Call `destroy()` on each of the destroyable's associated children + 3, Schedule calling the destroyable's destructors + 4, Schedule setting destroyable such that `isDestroyed(destroyable)` returns `true` + + This results in the entire tree of destroyables being first marked as destroying, + then having all of their destructors called, and finally all being marked as isDestroyed. + There won't be any in between states where some items are marked as `isDestroying` while + destroying, while others are not. + + @method destroy + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} destroyable the object to destroy + @static + @public +*/ + +/** + This function asserts that all objects which have associated destructors or associated children + have been destroyed at the time it is called. It is meant to be a low level hook that testing + frameworks can use to hook into and validate that all destroyables have in fact been destroyed. + + This function requires that `enableDestroyableTracking` was called previously, and is only + available in non-production builds. + + @method assertDestroyablesDestroyed + @category EMBER_DESTROYABLES + @for @ember/destroyable + @static + @public +*/ + +/** + This function instructs the destroyable system to keep track of all destroyables (their + children, destructors, etc). This enables a future usage of `assertDestroyablesDestroyed` + to be used to ensure that all destroyable tasks (registered destructors and associated children) + have completed when `assertDestroyablesDestroyed` is called. + + @method enableDestroyableTracking + @category EMBER_DESTROYABLES + @for @ember/destroyable + @static + @public +*/ + +/** + Receives a destroyable object and a destructor function, and associates the + function with it. When the destroyable is destroyed with destroy, or when its + parent is destroyed, the destructor function will be called. + + ```js + import { registerDestructor } from '@ember/destroyable'; + + class Modal extends Component { + @service resize; + + constructor() { + this.resize.register(this, this.layout); + + registerDestructor(this, () => this.resize.unregister(this)); + } + } + ``` + + Multiple destructors can be associated with a given destroyable, and they can be + associated over time, allowing libraries to dynamically add destructors as needed. + `registerDestructor` also returns the associated destructor function, for convenience. + + The destructor function is passed a single argument, which is the destroyable itself. + This allows the function to be reused multiple times for many destroyables, rather + than creating a closure function per destroyable. + + ```js + import { registerDestructor } from '@ember/destroyable'; + + function unregisterResize(instance) { + instance.resize.unregister(instance); + } + + class Modal extends Component { + @service resize; + + constructor() { + this.resize.register(this, this.layout); + + registerDestructor(this, unregisterResize); + } + } + ``` + + @method registerDestructor + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} destroyable the destroyable to register the destructor function with + @param {Function} destructor the destructor to run when the destroyable object is destroyed + @static + @public +*/ +export function registerDestructor( + destroyable: T, + destructor: (destroyable: T) => void +): (destroyable: T) => void { + return _registerDestructor(destroyable, destructor); +} + +/** + Receives a destroyable and a destructor function, and de-associates the destructor + from the destroyable. + + ```js + import { registerDestructor, unregisterDestructor } from '@ember/destroyable'; + + class Modal extends Component { + @service modals; + + constructor() { + this.modals.add(this); + + this.modalDestructor = registerDestructor(this, () => this.modals.remove(this)); + } + + @action pinModal() { + unregisterDestructor(this, this.modalDestructor); + } + } + ``` + + @method unregisterDestructor + @category EMBER_DESTROYABLES + @for @ember/destroyable + @param {Object|Function} destroyable the destroyable to unregister the destructor function from + @param {Function} destructor the destructor to remove from the destroyable + @static + @public +*/ +export function unregisterDestructor( + destroyable: T, + destructor: (destroyable: T) => void +): void { + return _unregisterDestructor(destroyable, destructor); +} diff --git a/packages/ember/index.js b/packages/ember/index.js index 4cc5462eab1..af42c3bd247 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -12,6 +12,7 @@ import { isEnabled, EMBER_GLIMMER_SET_COMPONENT_TEMPLATE, EMBER_CACHE_API, + EMBER_DESTROYABLES, } from '@ember/canary-features'; import * as EmberDebug from '@ember/debug'; import { assert, captureRenderTree, deprecate } from '@ember/debug'; @@ -138,7 +139,17 @@ import EngineInstance from '@ember/engine/instance'; import { assign, merge } from '@ember/polyfills'; import { LOGGER, EMBER_EXTEND_PROTOTYPES, JQUERY_INTEGRATION } from '@ember/deprecated-features'; import templateOnlyComponent from '@ember/component/template-only'; -import { destroy } from '@glimmer/runtime'; + +import { + assertDestroyablesDestroyed, + associateDestroyableChild, + destroy, + enableDestroyableTracking, + isDestroying, + isDestroyed, + registerDestructor, + unregisterDestructor, +} from '@ember/destroyable'; // ****@ember/-internals/environment**** @@ -341,6 +352,16 @@ if (EMBER_CACHE_API) { Ember._cacheIsConst = metal.isConst; } +if (EMBER_DESTROYABLES) { + Ember._registerDestructor = registerDestructor; + Ember._unregisterDestructor = unregisterDestructor; + Ember._associateDestroyableChild = associateDestroyableChild; + Ember._assertDestroyablesDestroyed = assertDestroyablesDestroyed; + Ember._enableDestroyableTracking = enableDestroyableTracking; + Ember._isDestroying = isDestroying; + Ember._isDestroyed = isDestroyed; +} + /** A function may be assigned to `Ember.onerror` to be called when Ember internals encounter an error. This is useful for specialized error handling diff --git a/tests/docs/expected.js b/tests/docs/expected.js index 037ec0a9b03..90ef27138f0 100644 --- a/tests/docs/expected.js +++ b/tests/docs/expected.js @@ -89,7 +89,9 @@ module.exports = { 'arrayContentDidChange', 'arrayContentWillChange', 'assert', + 'assertDestroyablesDestroyed', 'assign', + 'associateDestroyableChild', 'asyncEnd', 'asyncStart', 'attributeBindings', @@ -193,6 +195,7 @@ module.exports = { 'element', 'elementId', 'empty', + 'enableDestroyableTracking', 'end', 'endPropertyChanges', 'engine', @@ -436,6 +439,7 @@ module.exports = { 'register', 'registerAsyncHelper', 'registerDeprecationHandler', + 'registerDestructor', 'registerHelper', 'registerOptions', 'registerOptionsForType', @@ -565,6 +569,7 @@ module.exports = { 'uniqBy', 'unless', 'unregister', + 'unregisterDestructor', 'unregisterHelper', 'unregisterWaiter', 'unshiftObject',