Skip to content

Commit

Permalink
Merge pull request #19062 from emberjs/add-destroyables-api
Browse files Browse the repository at this point in the history
[FEATURE] Add @ember/destroyable feature.
  • Loading branch information
rwjblue authored Jul 30, 2020
2 parents 0bd4855 + 2e19e0d commit 49e7741
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/@ember/canary-features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

/**
Expand Down Expand Up @@ -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);
265 changes: 265 additions & 0 deletions packages/@ember/destroyable/index.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(
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<T extends object>(
destroyable: T,
destructor: (destroyable: T) => void
): void {
return _unregisterDestructor(destroyable, destructor);
}
23 changes: 22 additions & 1 deletion packages/ember/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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****

Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/docs/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ module.exports = {
'arrayContentDidChange',
'arrayContentWillChange',
'assert',
'assertDestroyablesDestroyed',
'assign',
'associateDestroyableChild',
'asyncEnd',
'asyncStart',
'attributeBindings',
Expand Down Expand Up @@ -193,6 +195,7 @@ module.exports = {
'element',
'elementId',
'empty',
'enableDestroyableTracking',
'end',
'endPropertyChanges',
'engine',
Expand Down Expand Up @@ -436,6 +439,7 @@ module.exports = {
'register',
'registerAsyncHelper',
'registerDeprecationHandler',
'registerDestructor',
'registerHelper',
'registerOptions',
'registerOptionsForType',
Expand Down Expand Up @@ -565,6 +569,7 @@ module.exports = {
'uniqBy',
'unless',
'unregister',
'unregisterDestructor',
'unregisterHelper',
'unregisterWaiter',
'unshiftObject',
Expand Down

0 comments on commit 49e7741

Please sign in to comment.