Skip to content

Commit

Permalink
[BUGFIX beta] Prevents autotracking ArrayProxy creation (#17834)
Browse files Browse the repository at this point in the history
 [BUGFIX beta] Prevents autotracking ArrayProxy creation
  • Loading branch information
rwjblue authored Nov 22, 2019
2 parents d38232f + 03c776b commit bfeeb20
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Object as EmberObject, A } from '@ember/-internals/runtime';
import { Object as EmberObject, A, ArrayProxy, PromiseProxyMixin } from '@ember/-internals/runtime';
import {
EMBER_CUSTOM_COMPONENT_ARG_PROXY,
EMBER_METAL_TRACKED_PROPERTIES,
} from '@ember/canary-features';
import { computed, tracked, nativeDescDecorator as descriptor } from '@ember/-internals/metal';
import { Promise } from 'rsvp';
import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers';
import GlimmerishComponent from '../../utils/glimmerish-component';
import { Component } from '../../utils/helpers';
Expand Down Expand Up @@ -47,6 +48,31 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
this.assertText('max jackson | max jackson');
}

'@test creating an array proxy inside a tracking context does not trigger backtracking assertion'() {
let PromiseArray = ArrayProxy.extend(PromiseProxyMixin);

class LoaderComponent extends GlimmerishComponent {
get data() {
if (!this._data) {
this._data = PromiseArray.create({
promise: Promise.resolve([1, 2, 3]),
});
}

return this._data;
}
}

this.registerComponent('loader', {
ComponentClass: LoaderComponent,
template: '{{#each this.data as |item|}}{{item}}{{/each}}',
});

this.render('<Loader/>');

this.assertText('123');
}

'@test tracked properties that are uninitialized do not throw an error'() {
let CountComponent = Component.extend({
count: tracked(),
Expand Down
3 changes: 1 addition & 2 deletions packages/@ember/-internals/metal/lib/array.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { arrayContentDidChange, arrayContentWillChange } from './array_events';
import { addListener, removeListener } from './events';
import { notifyPropertyChange } from './property_events';
import { get } from './property_get';

const EMPTY_ARRAY = Object.freeze([]);

Expand Down Expand Up @@ -84,7 +83,7 @@ function arrayObserversHelper(
): ObjectHasArrayObservers {
let willChange = (opts && opts.willChange) || 'arrayWillChange';
let didChange = (opts && opts.didChange) || 'arrayDidChange';
let hasObservers = get(obj, 'hasArrayObservers');
let hasObservers = obj.hasArrayObservers;

operation(obj, '@array:before', target, willChange);
operation(obj, '@array:change', target, didChange);
Expand Down
14 changes: 12 additions & 2 deletions packages/@ember/-internals/metal/lib/property_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DEBUG } from '@glimmer/env';
import { descriptorForProperty } from './descriptor_map';
import { isPath } from './path_cache';
import { tagForProperty } from './tags';
import { consume, isTracking } from './tracked';
import { consume, deprecateMutationsInAutotrackingTransaction, isTracking } from './tracked';

export const PROXY_CONTENT = symbol('PROXY_CONTENT');

Expand Down Expand Up @@ -143,7 +143,17 @@ export function get(obj: object, keyName: string): any {
!(keyName in obj) &&
typeof (obj as MaybeHasUnknownProperty).unknownProperty === 'function'
) {
return (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
if (DEBUG) {
let ret;

deprecateMutationsInAutotrackingTransaction!(() => {
ret = (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
});

return ret;
} else {
return (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
}
}
}
return value;
Expand Down
60 changes: 37 additions & 23 deletions packages/@ember/-internals/metal/lib/tracked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface AutotrackingTransactionSourceData {
error: Error;
}

let WARN_IN_AUTOTRACKING_TRANSACTION = false;
let DEPRECATE_IN_AUTOTRACKING_TRANSACTION = false;
let AUTOTRACKING_TRANSACTION: WeakMap<Tag, AutotrackingTransactionSourceData> | null = null;

export let runInAutotrackingTransaction: undefined | ((fn: () => void) => void);
Expand All @@ -28,40 +28,51 @@ export let assertTagNotConsumed:
let markTagAsConsumed: undefined | ((_tag: Tag, sourceError: Error) => void);

if (DEBUG) {
/**
* Creates a global autotracking transaction. This will prevent any backflow
* in any `track` calls within the transaction, even if they are not
* externally consumed.
*
* `runInAutotrackingTransaction` can be called within itself, and it will add
* onto the existing transaction if one exists.
*
* TODO: Only throw an error if the `track` is consumed.
*/
runInAutotrackingTransaction = (fn: () => void) => {
if (AUTOTRACKING_TRANSACTION !== null) {
// already in a transaction, continue and let the original transaction finish
fn();
return;
}
let previousDeprecateState = DEPRECATE_IN_AUTOTRACKING_TRANSACTION;
let previousTransactionState = AUTOTRACKING_TRANSACTION;

AUTOTRACKING_TRANSACTION = new WeakMap();
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = false;

if (previousTransactionState === null) {
// if there was no transaction start it. Otherwise, the transaction already exists.
AUTOTRACKING_TRANSACTION = new WeakMap();
}

try {
fn();
} finally {
AUTOTRACKING_TRANSACTION = null;
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = previousDeprecateState;
AUTOTRACKING_TRANSACTION = previousTransactionState;
}
};

/**
* Switches to deprecating within an autotracking transaction, if one exists.
* If `runInAutotrackingTransaction` is called within the callback of this
* method, it switches back to throwing an error, allowing zebra-striping of
* the types of errors that are thrown.
*
* Does not start an autotracking transaction.
*/
deprecateMutationsInAutotrackingTransaction = (fn: () => void) => {
assert(
'deprecations can only occur inside of an autotracking transaction',
AUTOTRACKING_TRANSACTION !== null
);

if (WARN_IN_AUTOTRACKING_TRANSACTION) {
// already in a transaction, continue and let the original transaction finish
fn();
return;
}

WARN_IN_AUTOTRACKING_TRANSACTION = true;
let previousDeprecateState = DEPRECATE_IN_AUTOTRACKING_TRANSACTION;
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = true;

try {
fn();
} finally {
WARN_IN_AUTOTRACKING_TRANSACTION = false;
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = previousDeprecateState;
}
};

Expand Down Expand Up @@ -137,7 +148,7 @@ if (DEBUG) {

if (!sourceData) return;

if (WARN_IN_AUTOTRACKING_TRANSACTION && !forceHardError) {
if (DEPRECATE_IN_AUTOTRACKING_TRANSACTION && !forceHardError) {
deprecate(makeAutotrackingErrorMessage(sourceData, obj, keyName), false, {
id: 'autotracking.mutation-after-consumption',
until: '4.0.0',
Expand Down Expand Up @@ -354,7 +365,7 @@ function descriptorForField([_target, key, desc]: [
get(): any {
let propertyTag = tagForProperty(this, key) as UpdatableTag;

if (CURRENT_TRACKER) CURRENT_TRACKER.add(propertyTag);
consume(propertyTag);

let value;

Expand All @@ -378,6 +389,9 @@ function descriptorForField([_target, key, desc]: [

set(newValue: any): void {
if (DEBUG) {
// No matter what, attempting to update a tracked property in an
// autotracking context after it has been read is invalid, even if we
// are otherwise warning, so always assert.
assertTagNotConsumed!(tagForProperty(this, key), this, key, true);
}

Expand Down
29 changes: 28 additions & 1 deletion packages/@ember/-internals/metal/tests/accessors/get_test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ENV } from '@ember/-internals/environment';
import { Object as EmberObject } from '@ember/-internals/runtime';
import { get, getWithDefault, Mixin, observer, computed } from '../..';
import { get, set, track, getWithDefault, Mixin, observer, computed } from '../..';
import { moduleFor, AbstractTestCase } from 'internal-test-helpers';
import { run } from '@ember/runloop';
import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';

function aget(x, y) {
return x[y];
Expand Down Expand Up @@ -290,6 +291,32 @@ moduleFor(
}
}

['@test gives helpful deprecation when a property tracked with `get` is mutated after access within unknownProperty within an autotracking transaction'](
assert
) {
if (!EMBER_METAL_TRACKED_PROPERTIES) {
assert.expect(0);
return;
}

class EmberObject {
foo = null;

unknownProperty() {
get(this, 'foo');
set(this, 'foo', 123);
}
}

let obj = new EmberObject();

expectDeprecation(() => {
track(() => {
get(obj, 'bar');
});
}, /You attempted to update `foo` on `EmberObject`, but it had already been used previously in the same computation/);
}

// ..........................................................
// BUGS
//
Expand Down
30 changes: 23 additions & 7 deletions packages/@ember/-internals/metal/tests/tracked/validation_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
import { EMBER_ARRAY } from '@ember/-internals/utils';
import { AbstractTestCase, moduleFor } from 'internal-test-helpers';
import { value, validate } from '@glimmer/reference';
import { DEBUG } from '@glimmer/env';

if (EMBER_METAL_TRACKED_PROPERTIES) {
moduleFor(
Expand Down Expand Up @@ -348,14 +347,31 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
let obj = new EmberObject();

expectAssertion(() => {
if (DEBUG) {
track(() => {
obj.value;
obj.value = 123;
});
}
track(() => {
obj.value;
obj.value = 123;
});
}, /You attempted to update `value` on `EmberObject`, but it had already been used previously in the same computation/);
}

['@test gives helpful assertion when a tracked property is mutated after access within unknownProperty within an autotracking transaction']() {
class EmberObject {
@tracked foo;

unknownProperty() {
this.foo;
this.foo = 123;
}
}

let obj = new EmberObject();

expectAssertion(() => {
track(() => {
get(obj, 'bar');
});
}, /You attempted to update `foo` on `EmberObject`, but it had already been used previously in the same computation/);
}
}
);
}
13 changes: 9 additions & 4 deletions packages/@ember/-internals/runtime/lib/mixins/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
removeArrayObserver,
arrayContentWillChange,
arrayContentDidChange,
nativeDescDecorator as descriptor,
} from '@ember/-internals/metal';
import { assert } from '@ember/debug';
import Enumerable from './enumerable';
Expand Down Expand Up @@ -564,8 +565,12 @@ const ArrayMixin = Mixin.create(Enumerable, {
@property {Boolean} hasArrayObservers
@public
*/
hasArrayObservers: nonEnumerableComputed(function() {
return hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
hasArrayObservers: descriptor({
configurable: true,
enumerable: false,
get() {
hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
},
}),

/**
Expand Down Expand Up @@ -695,7 +700,7 @@ const ArrayMixin = Mixin.create(Enumerable, {
foods.forEach((food) => food.eaten = true);
let output = '';
foods.forEach((item, index, array) =>
foods.forEach((item, index, array) =>
output += `${index + 1}/${array.length} ${item.name}\n`;
);
console.log(output);
Expand Down Expand Up @@ -725,7 +730,7 @@ const ArrayMixin = Mixin.create(Enumerable, {

/**
Alias for `mapBy`.
Returns the value of the named
property on all items in the enumeration.
Expand Down
12 changes: 6 additions & 6 deletions packages/@ember/-internals/runtime/lib/system/array_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ export default class ArrayProxy extends EmberObject {
this._arrangedContentRevision = value(this._arrangedContentTag);
}

this._addArrangedContentArrayObsever();
this._addArrangedContentArrayObserver();
}

willDestroy() {
this._removeArrangedContentArrayObsever();
this._removeArrangedContentArrayObserver();
}

/**
Expand Down Expand Up @@ -251,16 +251,16 @@ export default class ArrayProxy extends EmberObject {
let arrangedContent = get(this, 'arrangedContent');
let newLength = arrangedContent ? get(arrangedContent, 'length') : 0;

this._removeArrangedContentArrayObsever();
this._removeArrangedContentArrayObserver();
this.arrayContentWillChange(0, oldLength, newLength);

this._invalidate();

this.arrayContentDidChange(0, oldLength, newLength);
this._addArrangedContentArrayObsever();
this._addArrangedContentArrayObserver();
}

_addArrangedContentArrayObsever() {
_addArrangedContentArrayObserver() {
let arrangedContent = get(this, 'arrangedContent');
if (arrangedContent && !arrangedContent.isDestroyed) {
assert("Can't set ArrayProxy's content to itself", arrangedContent !== this);
Expand All @@ -275,7 +275,7 @@ export default class ArrayProxy extends EmberObject {
}
}

_removeArrangedContentArrayObsever() {
_removeArrangedContentArrayObserver() {
if (this._arrangedContent) {
removeArrayObserver(this._arrangedContent, this, ARRAY_OBSERVER_MAPPING);
}
Expand Down

0 comments on commit bfeeb20

Please sign in to comment.