Skip to content

Commit 1d9d6be

Browse files
Chris GarrettRobert Jackson
authored andcommitted
[BUGFIX] Tracked Props - Prevents autotracking ArrayProxy creation
This PR prevents an issue with immediate invalidation within the new autotracking transaction. ArrayProxy currently reads from one of its own properties, `hasArrayObservers`, and then immediately updates it with `notifyPropertyChange`. We avoid this by using a native descriptor instead of a computed, and not using `get()` to access it. This way, nothing is tracked during creation (even if it is mutated). (cherry picked from commit 03c776b)
1 parent deceab2 commit 1d9d6be

File tree

8 files changed

+143
-46
lines changed

8 files changed

+143
-46
lines changed

packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Object as EmberObject, A } from '@ember/-internals/runtime';
1+
import { Object as EmberObject, A, ArrayProxy, PromiseProxyMixin } from '@ember/-internals/runtime';
22
import {
33
EMBER_CUSTOM_COMPONENT_ARG_PROXY,
44
EMBER_METAL_TRACKED_PROPERTIES,
55
} from '@ember/canary-features';
66
import { computed, tracked, nativeDescDecorator as descriptor } from '@ember/-internals/metal';
7+
import { Promise } from 'rsvp';
78
import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers';
89
import GlimmerishComponent from '../../utils/glimmerish-component';
910
import { Component } from '../../utils/helpers';
@@ -47,6 +48,31 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
4748
this.assertText('max jackson | max jackson');
4849
}
4950

51+
'@test creating an array proxy inside a tracking context does not trigger backtracking assertion'() {
52+
let PromiseArray = ArrayProxy.extend(PromiseProxyMixin);
53+
54+
class LoaderComponent extends GlimmerishComponent {
55+
get data() {
56+
if (!this._data) {
57+
this._data = PromiseArray.create({
58+
promise: Promise.resolve([1, 2, 3]),
59+
});
60+
}
61+
62+
return this._data;
63+
}
64+
}
65+
66+
this.registerComponent('loader', {
67+
ComponentClass: LoaderComponent,
68+
template: '{{#each this.data as |item|}}{{item}}{{/each}}',
69+
});
70+
71+
this.render('<Loader/>');
72+
73+
this.assertText('123');
74+
}
75+
5076
'@test tracked properties that are uninitialized do not throw an error'() {
5177
let CountComponent = Component.extend({
5278
count: tracked(),

packages/@ember/-internals/metal/lib/array.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { arrayContentDidChange, arrayContentWillChange } from './array_events';
22
import { addListener, removeListener } from './events';
33
import { notifyPropertyChange } from './property_events';
4-
import { get } from './property_get';
54

65
const EMPTY_ARRAY = Object.freeze([]);
76

@@ -84,7 +83,7 @@ function arrayObserversHelper(
8483
): ObjectHasArrayObservers {
8584
let willChange = (opts && opts.willChange) || 'arrayWillChange';
8685
let didChange = (opts && opts.didChange) || 'arrayDidChange';
87-
let hasObservers = get(obj, 'hasArrayObservers');
86+
let hasObservers = obj.hasArrayObservers;
8887

8988
operation(obj, '@array:before', target, willChange);
9089
operation(obj, '@array:change', target, didChange);

packages/@ember/-internals/metal/lib/property_get.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { DEBUG } from '@glimmer/env';
88
import { descriptorForProperty } from './descriptor_map';
99
import { isPath } from './path_cache';
1010
import { tagForProperty } from './tags';
11-
import { consume, isTracking } from './tracked';
11+
import { consume, deprecateMutationsInAutotrackingTransaction, isTracking } from './tracked';
1212

1313
export const PROXY_CONTENT = symbol('PROXY_CONTENT');
1414

@@ -143,7 +143,17 @@ export function get(obj: object, keyName: string): any {
143143
!(keyName in obj) &&
144144
typeof (obj as MaybeHasUnknownProperty).unknownProperty === 'function'
145145
) {
146-
return (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
146+
if (DEBUG) {
147+
let ret;
148+
149+
deprecateMutationsInAutotrackingTransaction!(() => {
150+
ret = (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
151+
});
152+
153+
return ret;
154+
} else {
155+
return (obj as MaybeHasUnknownProperty).unknownProperty!(keyName);
156+
}
147157
}
148158
}
149159
return value;

packages/@ember/-internals/metal/lib/tracked.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface AutotrackingTransactionSourceData {
1313
error: Error;
1414
}
1515

16-
let WARN_IN_AUTOTRACKING_TRANSACTION = false;
16+
let DEPRECATE_IN_AUTOTRACKING_TRANSACTION = false;
1717
let AUTOTRACKING_TRANSACTION: WeakMap<Tag, AutotrackingTransactionSourceData> | null = null;
1818

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

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

38-
AUTOTRACKING_TRANSACTION = new WeakMap();
45+
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = false;
46+
47+
if (previousTransactionState === null) {
48+
// if there was no transaction start it. Otherwise, the transaction already exists.
49+
AUTOTRACKING_TRANSACTION = new WeakMap();
50+
}
3951

4052
try {
4153
fn();
4254
} finally {
43-
AUTOTRACKING_TRANSACTION = null;
55+
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = previousDeprecateState;
56+
AUTOTRACKING_TRANSACTION = previousTransactionState;
4457
}
4558
};
4659

60+
/**
61+
* Switches to deprecating within an autotracking transaction, if one exists.
62+
* If `runInAutotrackingTransaction` is called within the callback of this
63+
* method, it switches back to throwing an error, allowing zebra-striping of
64+
* the types of errors that are thrown.
65+
*
66+
* Does not start an autotracking transaction.
67+
*/
4768
deprecateMutationsInAutotrackingTransaction = (fn: () => void) => {
48-
assert(
49-
'deprecations can only occur inside of an autotracking transaction',
50-
AUTOTRACKING_TRANSACTION !== null
51-
);
52-
53-
if (WARN_IN_AUTOTRACKING_TRANSACTION) {
54-
// already in a transaction, continue and let the original transaction finish
55-
fn();
56-
return;
57-
}
58-
59-
WARN_IN_AUTOTRACKING_TRANSACTION = true;
69+
let previousDeprecateState = DEPRECATE_IN_AUTOTRACKING_TRANSACTION;
70+
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = true;
6071

6172
try {
6273
fn();
6374
} finally {
64-
WARN_IN_AUTOTRACKING_TRANSACTION = false;
75+
DEPRECATE_IN_AUTOTRACKING_TRANSACTION = previousDeprecateState;
6576
}
6677
};
6778

@@ -137,7 +148,7 @@ if (DEBUG) {
137148

138149
if (!sourceData) return;
139150

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

357-
if (CURRENT_TRACKER) CURRENT_TRACKER.add(propertyTag);
368+
consume(propertyTag);
358369

359370
let value;
360371

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

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

packages/@ember/-internals/metal/tests/accessors/get_test.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { ENV } from '@ember/-internals/environment';
22
import { Object as EmberObject } from '@ember/-internals/runtime';
3-
import { get, getWithDefault, Mixin, observer, computed } from '../..';
3+
import { get, set, track, getWithDefault, Mixin, observer, computed } from '../..';
44
import { moduleFor, AbstractTestCase } from 'internal-test-helpers';
55
import { run } from '@ember/runloop';
6+
import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
67

78
function aget(x, y) {
89
return x[y];
@@ -290,6 +291,32 @@ moduleFor(
290291
}
291292
}
292293

294+
['@test gives helpful deprecation when a property tracked with `get` is mutated after access within unknownProperty within an autotracking transaction'](
295+
assert
296+
) {
297+
if (!EMBER_METAL_TRACKED_PROPERTIES) {
298+
assert.expect(0);
299+
return;
300+
}
301+
302+
class EmberObject {
303+
foo = null;
304+
305+
unknownProperty() {
306+
get(this, 'foo');
307+
set(this, 'foo', 123);
308+
}
309+
}
310+
311+
let obj = new EmberObject();
312+
313+
expectDeprecation(() => {
314+
track(() => {
315+
get(obj, 'bar');
316+
});
317+
}, /You attempted to update `foo` on `EmberObject`, but it had already been used previously in the same computation/);
318+
}
319+
293320
// ..........................................................
294321
// BUGS
295322
//

packages/@ember/-internals/metal/tests/tracked/validation_test.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
1313
import { EMBER_ARRAY } from '@ember/-internals/utils';
1414
import { AbstractTestCase, moduleFor } from 'internal-test-helpers';
1515
import { value, validate } from '@glimmer/reference';
16-
import { DEBUG } from '@glimmer/env';
1716

1817
if (EMBER_METAL_TRACKED_PROPERTIES) {
1918
moduleFor(
@@ -348,14 +347,31 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
348347
let obj = new EmberObject();
349348

350349
expectAssertion(() => {
351-
if (DEBUG) {
352-
track(() => {
353-
obj.value;
354-
obj.value = 123;
355-
});
356-
}
350+
track(() => {
351+
obj.value;
352+
obj.value = 123;
353+
});
357354
}, /You attempted to update `value` on `EmberObject`, but it had already been used previously in the same computation/);
358355
}
356+
357+
['@test gives helpful assertion when a tracked property is mutated after access within unknownProperty within an autotracking transaction']() {
358+
class EmberObject {
359+
@tracked foo;
360+
361+
unknownProperty() {
362+
this.foo;
363+
this.foo = 123;
364+
}
365+
}
366+
367+
let obj = new EmberObject();
368+
369+
expectAssertion(() => {
370+
track(() => {
371+
get(obj, 'bar');
372+
});
373+
}, /You attempted to update `foo` on `EmberObject`, but it had already been used previously in the same computation/);
374+
}
359375
}
360376
);
361377
}

packages/@ember/-internals/runtime/lib/mixins/array.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
removeArrayObserver,
2020
arrayContentWillChange,
2121
arrayContentDidChange,
22+
nativeDescDecorator as descriptor,
2223
} from '@ember/-internals/metal';
2324
import { assert } from '@ember/debug';
2425
import Enumerable from './enumerable';
@@ -564,8 +565,12 @@ const ArrayMixin = Mixin.create(Enumerable, {
564565
@property {Boolean} hasArrayObservers
565566
@public
566567
*/
567-
hasArrayObservers: nonEnumerableComputed(function() {
568-
return hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
568+
hasArrayObservers: descriptor({
569+
configurable: true,
570+
enumerable: false,
571+
get() {
572+
hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
573+
},
569574
}),
570575

571576
/**
@@ -695,7 +700,7 @@ const ArrayMixin = Mixin.create(Enumerable, {
695700
foods.forEach((food) => food.eaten = true);
696701
697702
let output = '';
698-
foods.forEach((item, index, array) =>
703+
foods.forEach((item, index, array) =>
699704
output += `${index + 1}/${array.length} ${item.name}\n`;
700705
);
701706
console.log(output);
@@ -725,7 +730,7 @@ const ArrayMixin = Mixin.create(Enumerable, {
725730

726731
/**
727732
Alias for `mapBy`.
728-
733+
729734
Returns the value of the named
730735
property on all items in the enumeration.
731736

packages/@ember/-internals/runtime/lib/system/array_proxy.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,11 @@ export default class ArrayProxy extends EmberObject {
111111
this._arrangedContentRevision = value(this._arrangedContentTag);
112112
}
113113

114-
this._addArrangedContentArrayObsever();
114+
this._addArrangedContentArrayObserver();
115115
}
116116

117117
willDestroy() {
118-
this._removeArrangedContentArrayObsever();
118+
this._removeArrangedContentArrayObserver();
119119
}
120120

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

254-
this._removeArrangedContentArrayObsever();
254+
this._removeArrangedContentArrayObserver();
255255
this.arrayContentWillChange(0, oldLength, newLength);
256256

257257
this._invalidate();
258258

259259
this.arrayContentDidChange(0, oldLength, newLength);
260-
this._addArrangedContentArrayObsever();
260+
this._addArrangedContentArrayObserver();
261261
}
262262

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

278-
_removeArrangedContentArrayObsever() {
278+
_removeArrangedContentArrayObserver() {
279279
if (this._arrangedContent) {
280280
removeArrayObserver(this._arrangedContent, this, ARRAY_OBSERVER_MAPPING);
281281
}

0 commit comments

Comments
 (0)