From 0ce01744478e9250c9ff6061bbdfaa2eef4287ce Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Wed, 29 May 2019 08:12:00 -0700 Subject: [PATCH] [FEATURE] Adds sync observers back to tracked properties This PR flushes synchronous observers in the tracked feature flag path during each `notifyPropertyChange`, which gives these observers similar timing semantics to what they currently have without the feature flag enabled. These changes are meant to only affect observers, and while the metadata required is stored with listeners, this PR doesn't add "async listeners" as a concept to the system. The most notable change is the addition of the `suspended` property to active observers. This property is used by setters which are being set (computed property setters, and in the future `dependentKeyCompat` setters), and when observers are triggered to prevent re-entry. --- .../@ember/-internals/environment/lib/env.ts | 14 +++ packages/@ember/-internals/meta/lib/meta.ts | 26 +++- packages/@ember/-internals/metal/index.ts | 7 +- .../@ember/-internals/metal/lib/chain-tags.ts | 2 +- .../@ember/-internals/metal/lib/computed.ts | 15 ++- .../@ember/-internals/metal/lib/events.ts | 5 +- packages/@ember/-internals/metal/lib/mixin.ts | 78 ++++++++---- .../@ember/-internals/metal/lib/observer.ts | 116 +++++++++++++----- .../-internals/metal/lib/property_events.ts | 11 +- .../-internals/metal/tests/observer_test.js | 32 ++++- .../-internals/routing/lib/system/route.ts | 16 --- .../runtime/lib/system/core_object.js | 2 +- .../tests/system/array_proxy/length_test.js | 6 +- packages/@ember/-internals/utils/lib/super.ts | 6 +- packages/@ember/runloop/index.js | 6 +- tests/docs/expected.js | 1 + 16 files changed, 237 insertions(+), 106 deletions(-) diff --git a/packages/@ember/-internals/environment/lib/env.ts b/packages/@ember/-internals/environment/lib/env.ts index fab88cf6687..65463301d69 100644 --- a/packages/@ember/-internals/environment/lib/env.ts +++ b/packages/@ember/-internals/environment/lib/env.ts @@ -112,6 +112,20 @@ export const ENV = { */ _JQUERY_INTEGRATION: true, + /** + Whether the app defaults to using async observers. + + This is not intended to be set directly, as the implementation may change in + the future. Use `@ember/optional-features` instead. + + @property _DEFAULT_ASYNC_OBSERVERS + @for EmberENV + @type Boolean + @default false + @private + */ + _DEFAULT_ASYNC_OBSERVERS: false, + /** Controls the maximum number of scheduled rerenders without "settling". In general, applications should not need to modify this environment variable, but please diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index 3d54fd98906..2a60290e56c 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -76,6 +76,7 @@ interface StringListener { target: null; method: string; kind: ListenerKind.ADD | ListenerKind.ONCE | ListenerKind.REMOVE; + sync: boolean; } interface FunctionListener { @@ -83,6 +84,7 @@ interface FunctionListener { target: object | null; method: Function; kind: ListenerKind.ADD | ListenerKind.ONCE | ListenerKind.REMOVE; + sync: boolean; } type Listener = StringListener | FunctionListener; @@ -528,13 +530,14 @@ export class Meta { eventName: string, target: object | null, method: Function | string, - once: boolean + once: boolean, + sync: boolean ) { if (DEBUG) { counters!.addToListenersCalls++; } - this.pushListener(eventName, target, method, once ? ListenerKind.ONCE : ListenerKind.ADD); + this.pushListener(eventName, target, method, once ? ListenerKind.ONCE : ListenerKind.ADD, sync); } removeFromListeners(eventName: string, target: object | null, method: Function | string): void { @@ -549,7 +552,8 @@ export class Meta { event: string, target: object | null, method: Function | string, - kind: ListenerKind.ADD | ListenerKind.ONCE | ListenerKind.REMOVE + kind: ListenerKind.ADD | ListenerKind.ONCE | ListenerKind.REMOVE, + sync = false ): void { let listeners = this.writableListeners(); @@ -585,9 +589,11 @@ export class Meta { target, method, kind, + sync, } as Listener); } else { let listener = listeners[i]; + // If the listener is our own function listener and we are trying to // remove it, we want to splice it out entirely so we don't hold onto a // reference. @@ -598,8 +604,20 @@ export class Meta { ) { listeners.splice(i, 1); } else { + assert( + `You attempted to add an observer for the same method on '${ + event.split(':')[0] + }' twice to ${target} as both sync and async. Observers must be either sync or async, they cannot be both. This is likely a mistake, you should either remove the code that added the observer a second time, or update it to always be sync or async. The method was ${method}.`, + !( + listener.kind === ListenerKind.ADD && + kind === ListenerKind.ADD && + listener.sync !== sync + ) + ); + // update own listener listener.kind = kind; + listener.sync = sync; } } } @@ -761,7 +779,7 @@ export class Meta { result = [] as any[]; } - result.push(listener.event); + result.push(listener); } } } diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index c69d2b77557..6708d17b3bb 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -51,12 +51,7 @@ export { default as getProperties } from './lib/get_properties'; export { default as setProperties } from './lib/set_properties'; export { default as expandProperties } from './lib/expand_properties'; -export { - addObserver, - activateObserver, - removeObserver, - flushInvalidActiveObservers, -} from './lib/observer'; +export { addObserver, activateObserver, removeObserver, flushAsyncObservers } from './lib/observer'; export { Mixin, aliasMethod, mixin, observer, applyMixin } from './lib/mixin'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; export { tagForProperty, tagFor, markObjectAsDirty, UNKNOWN_PROPERTY_TAG } from './lib/tags'; diff --git a/packages/@ember/-internals/metal/lib/chain-tags.ts b/packages/@ember/-internals/metal/lib/chain-tags.ts index 073070e0b6e..b048d817a93 100644 --- a/packages/@ember/-internals/metal/lib/chain-tags.ts +++ b/packages/@ember/-internals/metal/lib/chain-tags.ts @@ -17,7 +17,7 @@ export function finishLazyChains(obj: any, key: string, value: any) { } if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { - lazyTags.clear(); + lazyTags.length = 0; return; } diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index 3d444d28610..6081b3d9606 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -30,6 +30,7 @@ import { isClassicDecorator, } from './descriptor_map'; import expandProperties from './expand_properties'; +import { setObserverSuspended } from './observer'; import { defineProperty } from './properties'; import { notifyPropertyChange } from './property_events'; import { set } from './property_set'; @@ -667,7 +668,19 @@ export class ComputedProperty extends ComputedDescriptor { let hadCachedValue = cache.has(keyName); let cachedValue = cache.get(keyName); - let ret = this._setter!.call(obj, keyName, value, cachedValue); + let ret; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + setObserverSuspended(obj, keyName, true); + + try { + ret = this._setter!.call(obj, keyName, value, cachedValue); + } finally { + setObserverSuspended(obj, keyName, false); + } + } else { + ret = this._setter!.call(obj, keyName, value, cachedValue); + } // allows setter to return the same value that is cached already if (hadCachedValue && cachedValue === ret) { diff --git a/packages/@ember/-internals/metal/lib/events.ts b/packages/@ember/-internals/metal/lib/events.ts index 0dd230bc06e..4fbfb161938 100644 --- a/packages/@ember/-internals/metal/lib/events.ts +++ b/packages/@ember/-internals/metal/lib/events.ts @@ -41,7 +41,8 @@ export function addListener( eventName: string, target: object | Function | null, method?: Function | string, - once?: boolean + once?: boolean, + sync = true ): void { assert( 'You must pass at least an object and event name to addListener', @@ -53,7 +54,7 @@ export function addListener( target = null; } - metaFor(obj).addToListeners(eventName, target, method!, once === true); + metaFor(obj).addToListeners(eventName, target, method!, once === true, sync); } /** diff --git a/packages/@ember/-internals/metal/lib/mixin.ts b/packages/@ember/-internals/metal/lib/mixin.ts index d8dd8dcbd75..b6dce62fc6e 100644 --- a/packages/@ember/-internals/metal/lib/mixin.ts +++ b/packages/@ember/-internals/metal/lib/mixin.ts @@ -1,6 +1,7 @@ /** @module @ember/object */ +import { ENV } from '@ember/-internals/environment'; import { Meta, meta as metaFor, peekMeta } from '@ember/-internals/meta'; import { getListeners, @@ -395,20 +396,23 @@ if (ALIAS_METHOD) { }; } -function updateObserversAndListeners( - obj: object, - key: string, - paths: string[] | undefined, - updateMethod: ( - obj: object, - path: string, - target: object | Function | null, - method: string - ) => void -) { - if (paths) { - for (let i = 0; i < paths.length; i++) { - updateMethod(obj, paths[i], null, key); +function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { + let observers = getObservers(fn); + let listeners = getListeners(fn); + + if (observers !== undefined) { + let updateObserver = add ? addObserver : removeObserver; + + for (let i = 0; i < observers.paths.length; i++) { + updateObserver(obj, observers.paths[i], null, key, observers.sync); + } + } + + if (listeners !== undefined) { + let updateListener = add ? addListener : removeListener; + + for (let i = 0; i < listeners.length; i++) { + updateListener(obj, listeners[i], null, key); } } } @@ -420,13 +424,11 @@ function replaceObserversAndListeners( next: Function | null ): void { if (typeof prev === 'function') { - updateObserversAndListeners(obj, key, getObservers(prev), removeObserver); - updateObserversAndListeners(obj, key, getListeners(prev), removeListener); + updateObserversAndListeners(obj, key, prev, false); } if (typeof next === 'function') { - updateObserversAndListeners(obj, key, getObservers(next), addObserver); - updateObserversAndListeners(obj, key, getListeners(next), addListener); + updateObserversAndListeners(obj, key, next, true); } } @@ -845,6 +847,8 @@ if (ALIAS_METHOD) { // OBSERVER HELPER // +type ObserverDefinition = { dependentKeys: string[]; fn: Function; sync: boolean }; + /** Specify a method that observes property changes. @@ -870,24 +874,48 @@ if (ALIAS_METHOD) { @public @static */ -export function observer(...args: (string | Function)[]) { - let func = args.pop(); - let _paths = args; +export function observer(...args: (string | Function)[]): Function; +export function observer(definition: ObserverDefinition): Function; +export function observer(...args: (string | Function | ObserverDefinition)[]) { + let funcOrDef = args.pop(); + + assert( + 'observer must be provided a function or an observer definition', + typeof funcOrDef === 'function' || (typeof funcOrDef === 'object' && funcOrDef !== null) + ); + + let func, dependentKeys, sync; + + if (typeof funcOrDef === 'function') { + func = funcOrDef; + dependentKeys = args; + sync = !ENV._DEFAULT_ASYNC_OBSERVERS; + } else { + func = (funcOrDef as ObserverDefinition).fn; + dependentKeys = (funcOrDef as ObserverDefinition).dependentKeys; + sync = (funcOrDef as ObserverDefinition).sync; + } assert('observer called without a function', typeof func === 'function'); assert( 'observer called without valid path', - _paths.length > 0 && _paths.every(p => typeof p === 'string' && Boolean(p.length)) + Array.isArray(dependentKeys) && + dependentKeys.length > 0 && + dependentKeys.every(p => typeof p === 'string' && Boolean(p.length)) ); + assert('observer called without sync', typeof sync === 'boolean'); let paths: string[] = []; let addWatchedProperty = (path: string) => paths.push(path); - for (let i = 0; i < _paths.length; ++i) { - expandProperties(_paths[i] as string, addWatchedProperty); + for (let i = 0; i < dependentKeys.length; ++i) { + expandProperties(dependentKeys[i] as string, addWatchedProperty); } - setObservers(func as Function, paths); + setObservers(func as Function, { + paths, + sync, + }); return func; } diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index def5ed01dee..973ce28eebf 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -1,3 +1,4 @@ +import { ENV } from '@ember/-internals/environment'; import { peekMeta } from '@ember/-internals/meta'; import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { schedule } from '@ember/runloop'; @@ -12,9 +13,12 @@ interface ActiveObserver { path: string; lastRevision: number; count: number; + suspended: boolean; } -const ACTIVE_OBSERVERS: Map> = new Map(); +const SYNC_DEFAULT = !ENV._DEFAULT_ASYNC_OBSERVERS; +const SYNC_OBSERVERS: Map> = new Map(); +const ASYNC_OBSERVERS: Map> = new Map(); /** @module @ember/object @@ -34,17 +38,18 @@ export function addObserver( obj: any, path: string, target: object | Function | null, - method: string | Function | undefined + method?: string | Function, + sync = SYNC_DEFAULT ): void { let eventName = changeEvent(path); - addListener(obj, eventName, target, method); + addListener(obj, eventName, target, method, false, sync); if (EMBER_METAL_TRACKED_PROPERTIES) { let meta = peekMeta(obj); if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { - activateObserver(obj, eventName); + activateObserver(obj, eventName, sync); } } else { watch(obj, path); @@ -65,7 +70,8 @@ export function removeObserver( obj: any, path: string, target: object | Function | null, - method: string | Function | undefined + method?: string | Function, + sync = SYNC_DEFAULT ): void { let eventName = changeEvent(path); @@ -73,7 +79,7 @@ export function removeObserver( let meta = peekMeta(obj); if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { - deactivateObserver(obj, eventName); + deactivateObserver(obj, eventName, sync); } } else { unwatch(obj, path); @@ -82,16 +88,18 @@ export function removeObserver( removeListener(obj, eventName, target, method); } -function getOrCreateActiveObserversFor(target: object) { - if (!ACTIVE_OBSERVERS.has(target)) { - ACTIVE_OBSERVERS.set(target, new Map()); +function getOrCreateActiveObserversFor(target: object, sync: boolean) { + let observerMap = sync === true ? SYNC_OBSERVERS : ASYNC_OBSERVERS; + + if (!observerMap.has(target)) { + observerMap.set(target, new Map()); } - return ACTIVE_OBSERVERS.get(target)!; + return observerMap.get(target)!; } -export function activateObserver(target: object, eventName: string) { - let activeObservers = getOrCreateActiveObserversFor(target); +export function activateObserver(target: object, eventName: string, sync = false) { + let activeObservers = getOrCreateActiveObserversFor(target, sync); if (activeObservers.has(eventName)) { activeObservers.get(eventName)!.count++; @@ -104,12 +112,15 @@ export function activateObserver(target: object, eventName: string) { path, tag, lastRevision: tag.value(), + suspended: false, }); } } -export function deactivateObserver(target: object, eventName: string) { - let activeObservers = ACTIVE_OBSERVERS.get(target); +export function deactivateObserver(target: object, eventName: string, sync = false) { + let observerMap = sync === true ? SYNC_OBSERVERS : ASYNC_OBSERVERS; + + let activeObservers = observerMap.get(target); if (activeObservers !== undefined) { let observer = activeObservers.get(eventName)!; @@ -120,7 +131,7 @@ export function deactivateObserver(target: object, eventName: string) { activeObservers.delete(eventName); if (activeObservers.size === 0) { - ACTIVE_OBSERVERS.delete(target); + observerMap.delete(target); } } } @@ -134,52 +145,91 @@ export function deactivateObserver(target: object, eventName: string) { * @param target */ export function revalidateObservers(target: object) { - if (!ACTIVE_OBSERVERS.has(target)) { - return; + if (ASYNC_OBSERVERS.has(target)) { + ASYNC_OBSERVERS.get(target)!.forEach(observer => { + observer.tag = getChainTagsForKey(target, observer.path); + observer.lastRevision = observer.tag.value(); + }); } - ACTIVE_OBSERVERS.get(target)!.forEach(observer => { - observer.tag = getChainTagsForKey(target, observer.path); - observer.lastRevision = observer.tag.value(); - }); + if (SYNC_OBSERVERS.has(target)) { + SYNC_OBSERVERS.get(target)!.forEach(observer => { + observer.tag = getChainTagsForKey(target, observer.path); + observer.lastRevision = observer.tag.value(); + }); + } } let lastKnownRevision = 0; -export function flushInvalidActiveObservers(shouldSchedule = true) { +export function flushAsyncObservers() { if (lastKnownRevision === CURRENT_TAG.value()) { return; } lastKnownRevision = CURRENT_TAG.value(); - ACTIVE_OBSERVERS.forEach((activeObservers, target) => { + ASYNC_OBSERVERS.forEach((activeObservers, target) => { let meta = peekMeta(target); if (meta && (meta.isSourceDestroying() || meta.isMetaDestroyed())) { - ACTIVE_OBSERVERS.delete(target); + ASYNC_OBSERVERS.delete(target); return; } activeObservers.forEach((observer, eventName) => { if (!observer.tag.validate(observer.lastRevision)) { - let sendObserver = () => { + schedule('actions', () => { try { sendEvent(target, eventName, [target, observer.path]); } finally { observer.tag = getChainTagsForKey(target, observer.path); observer.lastRevision = observer.tag.value(); } - }; - - if (shouldSchedule) { - schedule('actions', sendObserver); - } else { - // TODO: we need to schedule eagerly in exactly one location (_internalReset), - // for query params. We should get rid of this ASAP - sendObserver(); + }); + } + }); + }); +} + +export function flushSyncObservers() { + // When flushing synchronous observers, we know that something has changed (we + // only do this during a notifyPropertyChange), so there's no reason to check + // a global revision. + + SYNC_OBSERVERS.forEach((activeObservers, target) => { + let meta = peekMeta(target); + + if (meta && (meta.isSourceDestroying() || meta.isMetaDestroyed())) { + SYNC_OBSERVERS.delete(target); + return; + } + + activeObservers.forEach((observer, eventName) => { + if (!observer.suspended && !observer.tag.validate(observer.lastRevision)) { + try { + observer.suspended = true; + sendEvent(target, eventName, [target, observer.path]); + } finally { + observer.suspended = false; + observer.tag = getChainTagsForKey(target, observer.path); + observer.lastRevision = observer.tag.value(); } } }); }); } + +export function setObserverSuspended(target: object, property: string, suspended: boolean) { + let activeObservers = SYNC_OBSERVERS.get(target); + + if (!activeObservers) { + return; + } + + let observer = activeObservers.get(changeEvent(property)); + + if (observer) { + observer.suspended = suspended; + } +} diff --git a/packages/@ember/-internals/metal/lib/property_events.ts b/packages/@ember/-internals/metal/lib/property_events.ts index 2d2795776ba..cfe6aaccc48 100644 --- a/packages/@ember/-internals/metal/lib/property_events.ts +++ b/packages/@ember/-internals/metal/lib/property_events.ts @@ -5,6 +5,7 @@ import { DEBUG } from '@glimmer/env'; import changeEvent from './change_event'; import { descriptorForProperty } from './descriptor_map'; import { sendEvent } from './events'; +import { flushSyncObservers } from './observer'; import ObserverSet from './observer_set'; import { markObjectAsDirty } from './tags'; import { assertNotRendered } from './transaction'; @@ -61,6 +62,10 @@ function notifyPropertyChange(obj: object, keyName: string, _meta?: Meta | null) markObjectAsDirty(obj, keyName, meta); } + if (EMBER_METAL_TRACKED_PROPERTIES && deferred <= 0) { + flushSyncObservers(); + } + if (PROPERTY_DID_CHANGE in obj) { obj[PROPERTY_DID_CHANGE](keyName); } @@ -153,7 +158,11 @@ function beginPropertyChanges(): void { function endPropertyChanges(): void { deferred--; if (deferred <= 0) { - observerSet.flush(); + if (EMBER_METAL_TRACKED_PROPERTIES) { + flushSyncObservers(); + } else { + observerSet.flush(); + } } } diff --git a/packages/@ember/-internals/metal/tests/observer_test.js b/packages/@ember/-internals/metal/tests/observer_test.js index 7798dfc5bd5..d83af0d45a5 100644 --- a/packages/@ember/-internals/metal/tests/observer_test.js +++ b/packages/@ember/-internals/metal/tests/observer_test.js @@ -35,7 +35,31 @@ moduleFor( expectAssertion(() => { observer(null); + }, 'observer must be provided a function or an observer definition'); + + expectAssertion(() => { + observer({}); }, 'observer called without a function'); + + expectAssertion(() => { + observer({ + fn() {}, + }); + }, 'observer called without valid path'); + + expectAssertion(() => { + observer({ + fn() {}, + dependentKeys: [], + }); + }, 'observer called without valid path'); + + expectAssertion(() => { + observer({ + fn() {}, + dependentKeys: ['foo'], + }); + }, 'observer called without sync'); } async ['@test observer should fire when property is modified'](assert) { @@ -343,16 +367,12 @@ moduleFor( fooCount++; }); - if (!EMBER_METAL_TRACKED_PROPERTIES) { - beginPropertyChanges(); - } + beginPropertyChanges(); set(obj, 'foo', 'BIFF'); set(obj, 'foo', 'BAZ'); - if (!EMBER_METAL_TRACKED_PROPERTIES) { - endPropertyChanges(); - } + endPropertyChanges(); await runLoopSettled(); diff --git a/packages/@ember/-internals/routing/lib/system/route.ts b/packages/@ember/-internals/routing/lib/system/route.ts index c2fa3e0790f..46a37ed7b75 100644 --- a/packages/@ember/-internals/routing/lib/system/route.ts +++ b/packages/@ember/-internals/routing/lib/system/route.ts @@ -1,7 +1,6 @@ import { computed, defineProperty, - flushInvalidActiveObservers, get, getProperties, isEmpty, @@ -19,7 +18,6 @@ import { } from '@ember/-internals/runtime'; import { EMBER_FRAMEWORK_OBJECT_OWNER_ARGUMENT, - EMBER_METAL_TRACKED_PROPERTIES, EMBER_ROUTING_BUILD_ROUTEINFO_METADATA, } from '@ember/canary-features'; import { assert, deprecate, info, isTesting } from '@ember/debug'; @@ -364,13 +362,6 @@ class Route extends EmberObject implements IRoute { controller._qpDelegate = get(this, '_qp.states.inactive'); this.resetController(controller, isExiting, transition); - - // TODO: Once tags are enabled by default, we should refactor QP changes to - // use autotracking. This will likely be a large refactor, and for now we - // just need to trigger observers eagerly. - if (EMBER_METAL_TRACKED_PROPERTIES) { - flushInvalidActiveObservers(false); - } } /** @@ -954,13 +945,6 @@ class Route extends EmberObject implements IRoute { if (this._environment.options.shouldRender) { this.renderTemplate(controller, context); } - - // TODO: Once tags are enabled by default, we should refactor QP changes to - // use autotracking. This will likely be a large refactor, and for now we - // just need to trigger observers eagerly. - if (EMBER_METAL_TRACKED_PROPERTIES) { - flushInvalidActiveObservers(false); - } } /* diff --git a/packages/@ember/-internals/runtime/lib/system/core_object.js b/packages/@ember/-internals/runtime/lib/system/core_object.js index 1b1fb7ab1d6..8cc88131219 100644 --- a/packages/@ember/-internals/runtime/lib/system/core_object.js +++ b/packages/@ember/-internals/runtime/lib/system/core_object.js @@ -145,7 +145,7 @@ function initialize(obj, properties) { if (observerEvents !== undefined) { for (let i = 0; i < observerEvents.length; i++) { - activateObserver(obj, observerEvents[i]); + activateObserver(obj, observerEvents[i].event, observerEvents[i].sync); } } } else { diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js index 8218074dab7..e7314eb0ce9 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js @@ -168,6 +168,9 @@ moduleFor( e: observer('colors.content.[]', () => eCalled++), }).create(); + // bootstrap aliases + obj.length; + obj.set( 'model', ArrayProxy.create({ @@ -175,9 +178,6 @@ moduleFor( }) ); - // bootstrap aliases - obj.length; - await runLoopSettled(); assert.equal(obj.get('colors.content.length'), 3); diff --git a/packages/@ember/-internals/utils/lib/super.ts b/packages/@ember/-internals/utils/lib/super.ts index 9e45afb55af..c143420c116 100644 --- a/packages/@ember/-internals/utils/lib/super.ts +++ b/packages/@ember/-internals/utils/lib/super.ts @@ -38,10 +38,8 @@ function hasSuper(func: Function) { const OBSERVERS_MAP = new WeakMap(); -export function setObservers(func: Function, observers?: string[]) { - if (observers) { - OBSERVERS_MAP.set(func, observers); - } +export function setObservers(func: Function, observers: { paths: string[]; sync: boolean }) { + OBSERVERS_MAP.set(func, observers); } export function getObservers(func: Function) { diff --git a/packages/@ember/runloop/index.js b/packages/@ember/runloop/index.js index d65e42ade90..78071ac120a 100644 --- a/packages/@ember/runloop/index.js +++ b/packages/@ember/runloop/index.js @@ -1,6 +1,6 @@ import { assert } from '@ember/debug'; import { onErrorTarget } from '@ember/-internals/error-handling'; -import { flushInvalidActiveObservers } from '@ember/-internals/metal'; +import { flushAsyncObservers } from '@ember/-internals/metal'; import Backburner from 'backburner'; import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; @@ -17,7 +17,7 @@ function onEnd(current, next) { currentRunLoop = next; if (EMBER_METAL_TRACKED_PROPERTIES) { - flushInvalidActiveObservers(); + flushAsyncObservers(); } } @@ -26,7 +26,7 @@ let flush; if (EMBER_METAL_TRACKED_PROPERTIES) { flush = function(queueName, next) { if (queueName === 'render' || queueName === _rsvpErrorQueue) { - flushInvalidActiveObservers(); + flushAsyncObservers(); } next(); diff --git a/tests/docs/expected.js b/tests/docs/expected.js index bf658c30c21..2575a1b2dd7 100644 --- a/tests/docs/expected.js +++ b/tests/docs/expected.js @@ -10,6 +10,7 @@ module.exports = { '[]', '_APPLICATION_TEMPLATE_WRAPPER', '_JQUERY_INTEGRATION', + '_DEFAULT_ASYNC_OBSERVERS', '_RERENDER_LOOP_LIMIT', '_TEMPLATE_ONLY_GLIMMER_COMPONENTS', 'Input',