Skip to content

Commit

Permalink
[BUGFIX lts] Ensure destroy methods on CoreObject are invoked.
Browse files Browse the repository at this point in the history
When an instance of `CoreObject` is destroyed with `@ember/destroyable`
(or the polyfill when using Ember 3.20+), the instances own `destroy`
method is never called. In _general_ "this is fine" because the majority
of consumers are using `willDestroy` for their custom object teardown
needs. Unfortunately, this isn't _quite_ good enough and any consumers
that are directly relying on extending from `CoreObject` and having a
custom `destroy` method called will no longer function.

The prime example of this is the `Owner` instance itself. Currently,
running `destroy(owner)` **does not** actually destroy any instantiated
objects in the container. This is because `ContainerProxyMixin`
implements `destroy` in order to call `this.__container__.destroy()`
which is where the actual cleanup happens. That whole system should be
refactored to leveraging `@ember/destroyable` system directly, but in
the meantime this case (and any others where folks have implemented a
destroy method in Mixins or other subclasses of Ember.CoreObject) should
**absolutely** be fixed.

This fix ensures that any `destroy(someInstance)` calls on a
`CoreObject` directly invoke the `destroy` method on the instance in the
same manner as Ember < 3.20 (`.destroy()` is called eagerly, before
`willDestroy`).
  • Loading branch information
rwjblue committed Aug 25, 2020
1 parent 180e9b7 commit 5b1c358
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 12 deletions.
14 changes: 14 additions & 0 deletions packages/@ember/-internals/runtime/lib/system/core_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ const prototypeMixinMap = new WeakMap();

const initCalled = DEBUG ? new WeakSet() : undefined; // only used in debug builds to enable the proxy trap

const destroyCalled = new Set();

function ensureDestroyCalled(instance) {
if (!destroyCalled.has(instance)) {
instance.destroy();
}

destroyCalled.delete(instance);
}

function initialize(obj, properties) {
let m = meta(obj);

Expand Down Expand Up @@ -256,6 +266,7 @@ class CoreObject {
});
}

registerDestructor(self, ensureDestroyCalled, true);
registerDestructor(self, () => self.willDestroy());

// disable chains
Expand Down Expand Up @@ -512,6 +523,9 @@ class CoreObject {
@public
*/
destroy() {
// Used to ensure that manually calling `.destroy()` does not immediately call destroy again
destroyCalled.add(this);

destroy(this);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ContainerProxy from '../../lib/mixins/container_proxy';
import EmberObject from '../../lib/system/object';
import { run, schedule } from '@ember/runloop';
import { moduleFor, AbstractTestCase } from 'internal-test-helpers';
import { destroy } from '@glimmer/runtime';

moduleFor(
'@ember/-internals/runtime/mixins/container_proxy',
Expand Down Expand Up @@ -44,5 +45,26 @@ moduleFor(
this.instance.destroy();
});
}

'@test being destroyed by @ember/destroyable properly destroys the container and created instances'(
assert
) {
assert.expect(1);

this.registry.register(
'service:foo',
class FooService extends EmberObject {
willDestroy() {
assert.ok(true, 'is properly destroyed');
}
}
);

this.instance.lookup('service:foo');

run(() => {
destroy(this.instance);
});
}
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { get, set, observer } from '@ember/-internals/metal';
import CoreObject from '../../lib/system/core_object';
import { moduleFor, AbstractTestCase, buildOwner, runLoopSettled } from 'internal-test-helpers';
import { track } from '@glimmer/validator';
import { destroy } from '@glimmer/runtime';
import { run } from '@ember/runloop';

moduleFor(
'Ember.CoreObject',
Expand Down Expand Up @@ -129,5 +131,20 @@ moduleFor(
assert.ok(true, 'We did not error');
});
}
'@test destroy method is called when being destroyed by @ember/destroyable'(assert) {
assert.expect(1);

class TestObject extends CoreObject {
destroy() {
assert.ok(true, 'destroy was invoked');
}
}

let instance = TestObject.create();

run(() => {
destroy(instance);
});
}
}
);
29 changes: 17 additions & 12 deletions packages/@ember/application/tests/reset_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { get } from '@ember/-internals/metal';
import Controller from '@ember/controller';
import { Router } from '@ember/-internals/routing';
import { moduleFor, AutobootApplicationTestCase } from 'internal-test-helpers';
import { CoreObject } from '@ember/-internals/runtime';

moduleFor(
'Application - resetting',
Expand Down Expand Up @@ -71,31 +72,35 @@ moduleFor(
let eventDispatcherWasSetup = 0;
let eventDispatcherWasDestroyed = 0;

let mockEventDispatcher = {
class FakeEventDispatcher extends CoreObject {
setup() {
eventDispatcherWasSetup++;
},
}

destroy() {
if (this.isDestroying) {
return;
}
super.destroy();

eventDispatcherWasDestroyed++;
},
};
}
}

run(() => {
this.createApplication();
this.add('event_dispatcher:main', {
create: () => mockEventDispatcher,
});
this.add('event_dispatcher:main', FakeEventDispatcher);

assert.equal(eventDispatcherWasSetup, 0);
assert.equal(eventDispatcherWasDestroyed, 0);
assert.equal(eventDispatcherWasSetup, 0, 'not setup yet');
assert.equal(eventDispatcherWasDestroyed, 0, 'not destroyed yet');
});

assert.equal(eventDispatcherWasSetup, 1);
assert.equal(eventDispatcherWasDestroyed, 0);
assert.equal(eventDispatcherWasSetup, 1, 'setup');
assert.equal(eventDispatcherWasDestroyed, 0, 'still not destroyed');

this.application.reset();

assert.equal(eventDispatcherWasDestroyed, 1);
assert.equal(eventDispatcherWasDestroyed, 1, 'destroyed');
assert.equal(eventDispatcherWasSetup, 2, 'setup called after reset');
}

Expand Down

0 comments on commit 5b1c358

Please sign in to comment.