diff --git a/internal-docs/guides/reactivity/autotracked-rendering.md b/internal-docs/guides/reactivity/autotracked-rendering.md index 5646bb62b5c..7641fed5a3d 100644 --- a/internal-docs/guides/reactivity/autotracked-rendering.md +++ b/internal-docs/guides/reactivity/autotracked-rendering.md @@ -60,9 +60,9 @@ There are a few systems at play for autotracking: 5. the low-level VM is the actual VirtualMachine which inteprets all our opcodes -- it iterates until there are no more opcodes - **read: count** - - access `count`, which `@tracked`'s getter [defers to `trackedData`](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/metal/lib/tracked.ts#L155C28-L155C39) - - the [`trackedData`](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/metal/lib/tracked.ts#L5) is in `@glimmer/validator` instead of using tags _directly_. - - `trackedData` calls `consumeTag` when [the value is access](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/validator/lib/tracked-data.ts#L15) + - access `count`, which goes to the getter of `@tracked` in `packages/@ember/-internals/metal/lib/tracked.ts` + - the getter reads the `TrackedValue` that holds the field for this instance (`packages/@glimmer/validator/lib/tracked-value.ts`) + - `TrackedValue` calls `consumeTag` when the value is read - `consumeTag` adds the tag to the [`CURRENT_TRACKER`](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/validator/lib/tracking.ts#L116) - this is so that when any `{{ }}` regions of a template "detect" a dirty tag, they can individually re-render diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 67b09e2aa60..4ee01156e90 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -2,10 +2,11 @@ import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import { isEmberArray } from '@ember/array/-internals'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; +import type { UpdatableTag } from '@glimmer/interfaces'; +import { debug } from '@glimmer/validator/lib/debug'; import { consumeTag, untrack } from '@glimmer/validator/lib/tracking'; import { dirtyTagFor, tagFor } from '@glimmer/validator/lib/meta'; -import { trackedData } from '@glimmer/validator/lib/tracked-data'; -import { trackedValue, type TrackedValue } from '@glimmer/validator/lib/tracked-value'; +import { trackedValue, TrackedValue } from '@glimmer/validator/lib/tracked-value'; import type { ElementDescriptor } from '..'; import { CHAIN_PASS_THROUGH } from './chain-tags'; import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; @@ -324,6 +325,46 @@ if (DEBUG) { setClassicDecorator(tracked); } +// A tracked field notifies on every write, also when the value is the same: +// `this.items = this.items` is how code reports a change inside the value. +function neverEqual(): boolean { + return false; +} + +function readCell(cell: TrackedValue): unknown { + let value = cell.value; + + // Add the tag of the returned value if it is an array, since arrays + // should always cause updates if they are consumed and then changed + if (Array.isArray(value) || isEmberArray(value)) { + consumeTag(tagFor(value, '[]')); + } + + return value; +} + +function writeCell( + self: object, + key: string | symbol, + cell: TrackedValue, + newValue: unknown, + hasEquals: boolean +): void { + if (DEBUG) { + // TrackedValue has the same assertion, but it cannot name the object + // and the key. + debug.assertTagNotConsumed?.(tagFor(self, key), self, key); + } + + if (!hasEquals) { + cell.value = newValue; + } else if (!cell.set(newValue)) { + return; + } + + dirtyTagFor(self, SELF_TAG); +} + function descriptorForField( [target, key, desc]: ElementDescriptor, options?: { equals?: (a: any, b: any) => boolean; description?: string } @@ -333,34 +374,48 @@ function descriptorForField( !desc || (!desc.value && !desc.get && !desc.set) ); - let { getter, setter } = trackedData(key, desc ? desc.initializer : undefined); + let initializer = desc ? desc.initializer : undefined; + let hasInitializer = typeof initializer === 'function'; let equals = options?.equals; + // Each instance gets one TrackedValue for the field, at its first read or + // write. All of them share these options. + let cells = new WeakMap>(); + let cellOptions = { equals: equals ?? neverEqual, description: options?.description }; + + function createCell(self: object, value: unknown): TrackedValue { + // Other code can ask the registry for the tag of the field before the + // first read or write, so the value takes its tag from there. + let cell = new TrackedValue(value, cellOptions, tagFor(self, key) as UpdatableTag); + + cells.set(self, cell); + + return cell; + } + function get(this: object): unknown { - let value = getter(this); + let cell = cells.get(this); - // Add the tag of the returned value if it is an array, since arrays - // should always cause updates if they are consumed and then changed - if (Array.isArray(value) || isEmberArray(value)) { - consumeTag(tagFor(value, '[]')); + if (cell === undefined) { + cell = createCell(this, hasInitializer ? initializer!.call(this) : undefined); } - return value; + return readCell(cell); } function set(this: object, newValue: unknown): void { - if ( - equals !== undefined && - equals( - untrack(() => getter(this)), - newValue - ) - ) { - return; + let cell = cells.get(this); + + if (cell === undefined) { + // A first write does not run the initializer, unless `equals` needs the + // initial value to compare with. + cell = createCell( + this, + equals !== undefined && hasInitializer ? untrack(() => initializer!.call(this)) : undefined + ); } - setter(this, newValue); - dirtyTagFor(this, SELF_TAG); + writeCell(this, key, cell, newValue, equals !== undefined); } let newDesc = { @@ -416,29 +471,30 @@ function tracked2023( }); return; case 'accessor': { - let equals = options?.equals; + let name = dec.context.name; + let hasEquals = options?.equals !== undefined; + let cellOptions = { + equals: options?.equals ?? neverEqual, + description: options?.description, + }; + + // The storage of the accessor holds the TrackedValue, so a read or a + // write needs no map lookup. return { + init(this: object, initial: unknown) { + return new TrackedValue(initial, cellOptions, tagFor(this, name) as UpdatableTag); + }, get(this: object) { - consumeTag(tagFor(this, dec.context.name)); - let value = dec.value.get.call(this); - if (Array.isArray(value) || isEmberArray(value)) { - consumeTag(tagFor(value, '[]')); - } - return value; + return readCell(dec.value.get.call(this) as TrackedValue); }, set(this: object, value: unknown) { - if ( - equals !== undefined && - equals( - untrack(() => dec.value.get.call(this)), - value - ) - ) { - return; - } - dirtyTagFor(this, dec.context.name); - dirtyTagFor(this, SELF_TAG); - return dec.value.set.call(this, value); + writeCell( + this, + name, + dec.value.get.call(this) as TrackedValue, + value, + hasEquals + ); }, }; } diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.js b/packages/@ember/-internals/metal/tests/tracked/validation_test.js index e2eef6ed310..1bfad1886a3 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -43,6 +43,57 @@ moduleFor( assert.equal(validateTag(tag, snapshot), true); } + [`@test a write dirties the tag that tagForProperty gave before the first read or write`]( + assert + ) { + class Tracked { + @tracked first = 'Tom'; + } + + let obj = new Tracked(); + + let tag = tagForProperty(obj, 'first'); + let snapshot = valueForTag(tag); + + obj.first = 'Thomas'; + + assert.equal(validateTag(tag, snapshot), false); + } + + [`@test notifyPropertyChange dirties the tag that a read of a tracked field consumed`](assert) { + class Tracked { + @tracked first = 'Tom'; + } + + let obj = new Tracked(); + + let tag = track(() => obj.first); + let snapshot = valueForTag(tag); + + notifyPropertyChange(obj, 'first'); + + assert.equal(validateTag(tag, snapshot), false); + } + + [`@test a first write does not run the initializer of a tracked field`](assert) { + let runs = 0; + + class Tracked { + @tracked first = ++runs; + } + + let obj = new Tracked(); + + // Stable decorators run the initializer in the constructor. + let expected = import.meta.env.VITE_STABLE_DECORATORS ? 1 : 0; + + obj.first = 10; + + assert.equal(runs, expected); + assert.equal(obj.first, 10); + assert.equal(runs, expected); + } + [`@test autotracking should work with initializers (${import.meta.env.VITE_STABLE_DECORATORS ? 'stable' : 'legacy'} decorators)`]( assert ) { diff --git a/packages/@ember/-internals/routing/route-managers/root-outlet.ts b/packages/@ember/-internals/routing/route-managers/root-outlet.ts index df69300fa52..c568a7f7a0d 100644 --- a/packages/@ember/-internals/routing/route-managers/root-outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/root-outlet.ts @@ -16,8 +16,7 @@ import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; import { precompileTemplate } from '@ember/template-compilation'; import { DEBUG } from '@glimmer/env'; import type { OutletParent, OutletState } from './outlet-state'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { TrackedValue } from '@glimmer/validator/lib/tracked-value'; // `this` == <@outlet />; returned by `getSelf` const ROOT_OUTLET_TEMPLATE = precompileTemplate('{{this}}', { @@ -91,16 +90,17 @@ export interface UpdatableOutletRootState { set(root: OutletState): void; } +/** A set of the root always notifies, also with the same state object. */ +const ROOT_OPTIONS = Object.freeze({ equals: () => false, description: undefined }); + /** The chain's head: a reactive first level. */ export function createRootOutletState(initial: OutletState): UpdatableOutletRootState { - let tag = createTag(); - let current = initial; + let current = new TrackedValue(initial, ROOT_OPTIONS); let state: OutletParent = { outlets: { get main(): OutletState { - consumeTag(tag); - return current; + return current.value; }, }, }; @@ -108,8 +108,7 @@ export function createRootOutletState(initial: OutletState): UpdatableOutletRoot return { state, set(root: OutletState) { - current = root; - dirtyTag(tag); + current.value = root; }, }; } diff --git a/packages/@glimmer/reference/lib/iterable.ts b/packages/@glimmer/reference/lib/iterable.ts index 2c566c87520..2ca7a97c05b 100644 --- a/packages/@glimmer/reference/lib/iterable.ts +++ b/packages/@glimmer/reference/lib/iterable.ts @@ -3,8 +3,7 @@ import type { Nullable } from '@glimmer/interfaces'; import { getPath, toIterator } from '@glimmer/global-context'; import { EMPTY_ARRAY } from '@glimmer/util/lib/array-utils'; import { isIndexable } from '@glimmer/util/lib/collections'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { TrackedValue } from '@glimmer/validator/lib/tracked-value'; import type { Reference, ReferenceEnvironment } from './reference'; @@ -159,20 +158,18 @@ export function createIteratorRef(listRef: Reference, key: string) { }); } -export function createIteratorItemRef(_value: unknown) { - let value = _value; - let tag = createTag(); +const ITEM_OPTIONS = Object.freeze({ + equals: (a: unknown, b: unknown) => a === b, + description: undefined, +}); + +export function createIteratorItemRef(value: unknown) { + let item = new TrackedValue(value, ITEM_OPTIONS); return createComputeRef( - () => { - consumeTag(tag); - return value; - }, + () => item.value, (newValue) => { - if (value !== newValue) { - value = newValue; - dirtyTag(tag); - } + item.value = newValue; } ); } diff --git a/packages/@glimmer/validator/index.ts b/packages/@glimmer/validator/index.ts index 920b34148d7..30a00c353bb 100644 --- a/packages/@glimmer/validator/index.ts +++ b/packages/@glimmer/validator/index.ts @@ -16,7 +16,6 @@ export { trackedWeakMap } from './lib/collections/weak-map'; export { trackedWeakSet } from './lib/collections/weak-set'; export { debug } from './lib/debug'; export { dirtyTagFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; -export { trackedData } from './lib/tracked-data'; export { type Reactive, type ReadOnlyReactive, diff --git a/packages/@glimmer/validator/lib/collections/array.ts b/packages/@glimmer/validator/lib/collections/array.ts index 325acad042d..fa7b36cfd0a 100644 --- a/packages/@glimmer/validator/lib/collections/array.ts +++ b/packages/@glimmer/validator/lib/collections/array.ts @@ -6,6 +6,7 @@ // and it will blow up in JS in exactly the same way, so it is safe to assume // that properties within the getter have the correct type in TS. +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; import { createUpdatableTag, DIRTY_TAG } from '../validators'; @@ -75,7 +76,7 @@ class TrackedArray { const index = convertToInt(prop); if (index !== null) { - self.#readStorageFor(index); + consumeTag(tagFor(self, index, self.#storages)); consumeTag(self.#collection); return target[index]; @@ -137,7 +138,7 @@ class TrackedArray { const index = convertToInt(prop); if (index !== null) { - self.#dirtyStorageFor(index); + dirtyTagFor(self, index, self.#storages); self.#dirtyCollection(); } else if (prop === 'length') { self.#dirtyCollection(); @@ -156,25 +157,6 @@ class TrackedArray { #storages = new Map>(); - #readStorageFor(index: number) { - let storage = this.#storages.get(index); - - if (storage === undefined) { - storage = createUpdatableTag(); - this.#storages.set(index, storage); - } - - consumeTag(storage); - } - - #dirtyStorageFor(index: number): void { - const storage = this.#storages.get(index); - - if (storage) { - DIRTY_TAG(storage); - } - } - #dirtyCollection() { DIRTY_TAG(this.#collection); this.#storages.clear(); diff --git a/packages/@glimmer/validator/lib/collections/map.ts b/packages/@glimmer/validator/lib/collections/map.ts index feee8099d8f..80d29d92663 100644 --- a/packages/@glimmer/validator/lib/collections/map.ts +++ b/packages/@glimmer/validator/lib/collections/map.ts @@ -3,6 +3,7 @@ // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; import { createUpdatableTag, DIRTY_TAG } from '../validators'; @@ -24,25 +25,6 @@ export function trackedMap( const collection = createUpdatableTag(); const storages = new Map(); - function storageFor(key: Key): Tag { - let storage = storages.get(key); - - if (storage === undefined) { - storage = createUpdatableTag(); - storages.set(key, storage); - } - - return storage; - } - - function dirtyStorageFor(key: Key): void { - const storage = storages.get(key); - - if (storage) { - DIRTY_TAG(storage); - } - } - const proxy: Map = new Proxy(target, { get(target, prop, receiver) { if (prop === 'set') { @@ -57,7 +39,7 @@ export function trackedMap( if (isUnchanged) return proxy; } - dirtyStorageFor(key); + dirtyTagFor(target, key, storages); DIRTY_TAG(collection); target.set(key, value); @@ -70,7 +52,7 @@ export function trackedMap( return function (key: Key): boolean { if (!target.has(key)) return false; - dirtyStorageFor(key); + dirtyTagFor(target, key, storages); DIRTY_TAG(collection); storages.delete(key); @@ -92,7 +74,7 @@ export function trackedMap( if (prop === 'get') { return function (key: Key): Value | undefined { - consumeTag(storageFor(key)); + consumeTag(tagFor(target, key, storages)); return target.get(key); }; @@ -100,7 +82,7 @@ export function trackedMap( if (prop === 'has') { return function (key: Key): boolean { - consumeTag(storageFor(key)); + consumeTag(tagFor(target, key, storages)); return target.has(key); }; diff --git a/packages/@glimmer/validator/lib/collections/object.ts b/packages/@glimmer/validator/lib/collections/object.ts index 2c9948db36e..70b71ead1c7 100644 --- a/packages/@glimmer/validator/lib/collections/object.ts +++ b/packages/@glimmer/validator/lib/collections/object.ts @@ -1,5 +1,6 @@ import type { ReactiveOptions } from './types'; +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; import { createUpdatableTag, DIRTY_TAG } from '../validators'; @@ -8,25 +9,6 @@ class TrackedObject> { #storages = new Map>(); #collection = createUpdatableTag(); - #readStorageFor(key: PropertyKey) { - let storage = this.#storages.get(key); - - if (storage === undefined) { - storage = createUpdatableTag(); - this.#storages.set(key, storage); - } - - consumeTag(storage); - } - - #dirtyStorageFor(key: PropertyKey) { - const storage = this.#storages.get(key); - - if (storage) { - DIRTY_TAG(storage); - } - } - #dirtyCollection() { DIRTY_TAG(this.#collection); } @@ -53,13 +35,13 @@ class TrackedObject> { return new Proxy(clone, { get(target, prop) { - self.#readStorageFor(prop); + consumeTag(tagFor(self, prop, self.#storages)); return target[prop as keyof ObjectType]; }, has(target, prop) { - self.#readStorageFor(prop); + consumeTag(tagFor(self, prop, self.#storages)); return prop in target; }, @@ -81,7 +63,7 @@ class TrackedObject> { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment target[prop as keyof ObjectType] = value; - self.#dirtyStorageFor(prop); + dirtyTagFor(self, prop, self.#storages); self.#dirtyCollection(); return true; @@ -90,7 +72,7 @@ class TrackedObject> { deleteProperty(target, prop) { if (prop in target) { delete target[prop as keyof ObjectType]; - self.#dirtyStorageFor(prop); + dirtyTagFor(self, prop, self.#storages); self.#storages.delete(prop); self.#dirtyCollection(); } diff --git a/packages/@glimmer/validator/lib/collections/set.ts b/packages/@glimmer/validator/lib/collections/set.ts index ad912509fe5..9499c6f58e7 100644 --- a/packages/@glimmer/validator/lib/collections/set.ts +++ b/packages/@glimmer/validator/lib/collections/set.ts @@ -3,6 +3,7 @@ // interface are automatically supported without needing to manually // re-implement each one. +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; import { createUpdatableTag, DIRTY_TAG } from '../validators'; @@ -17,25 +18,6 @@ export function trackedSet( const collection = createUpdatableTag(); const storages = new Map(); - function storageFor(key: Value): Tag { - let storage = storages.get(key); - - if (storage === undefined) { - storage = createUpdatableTag(); - storages.set(key, storage); - } - - return storage; - } - - function dirtyStorageFor(key: Value): void { - const storage = storages.get(key); - - if (storage) { - DIRTY_TAG(storage); - } - } - const proxy: Set = new Proxy(target, { get(target, prop, receiver) { if (prop === 'add') { @@ -47,7 +29,7 @@ export function trackedSet( DIRTY_TAG(collection); } - dirtyStorageFor(value); + dirtyTagFor(target, value, storages); target.add(value); @@ -59,7 +41,7 @@ export function trackedSet( return function (value: Value): boolean { if (!target.has(value)) return false; - dirtyStorageFor(value); + dirtyTagFor(target, value, storages); DIRTY_TAG(collection); storages.delete(value); @@ -81,7 +63,7 @@ export function trackedSet( if (prop === 'has') { return function (value: Value): boolean { - consumeTag(storageFor(value)); + consumeTag(tagFor(target, value, storages)); return target.has(value); }; diff --git a/packages/@glimmer/validator/lib/collections/weak-map.ts b/packages/@glimmer/validator/lib/collections/weak-map.ts index b4e174647b3..15cdd12355a 100644 --- a/packages/@glimmer/validator/lib/collections/weak-map.ts +++ b/packages/@glimmer/validator/lib/collections/weak-map.ts @@ -2,8 +2,9 @@ // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import type { createUpdatableTag } from '../validators'; type Tag = ReturnType; @@ -21,25 +22,6 @@ export function trackedWeakMap( existing instanceof WeakMap ? existing : new WeakMap(existing); const storages = new WeakMap(); - function storageFor(key: Key): Tag { - let storage = storages.get(key); - - if (storage === undefined) { - storage = createUpdatableTag(); - storages.set(key, storage); - } - - return storage; - } - - function dirtyStorageFor(key: Key): void { - const storage = storages.get(key); - - if (storage) { - DIRTY_TAG(storage); - } - } - const proxy: WeakMap = new Proxy(target, { get(target, prop, receiver) { if (prop === 'set') { @@ -52,7 +34,7 @@ export function trackedWeakMap( if (isUnchanged) return proxy; } - dirtyStorageFor(key); + dirtyTagFor(target, key, storages); target.set(key, value); @@ -64,7 +46,7 @@ export function trackedWeakMap( return function (key: Key): boolean { if (!target.has(key)) return false; - dirtyStorageFor(key); + dirtyTagFor(target, key, storages); storages.delete(key); return target.delete(key); @@ -73,7 +55,7 @@ export function trackedWeakMap( if (prop === 'get') { return function (key: Key): Value | undefined { - consumeTag(storageFor(key)); + consumeTag(tagFor(target, key, storages)); return target.get(key); }; @@ -81,7 +63,7 @@ export function trackedWeakMap( if (prop === 'has') { return function (key: Key): boolean { - consumeTag(storageFor(key)); + consumeTag(tagFor(target, key, storages)); return target.has(key); }; diff --git a/packages/@glimmer/validator/lib/collections/weak-set.ts b/packages/@glimmer/validator/lib/collections/weak-set.ts index bcc64a82766..89c6acb6bea 100644 --- a/packages/@glimmer/validator/lib/collections/weak-set.ts +++ b/packages/@glimmer/validator/lib/collections/weak-set.ts @@ -2,8 +2,9 @@ // interface are automatically supported without needing to manually // re-implement each one. +import { dirtyTagFor, tagFor } from '../meta'; import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import type { createUpdatableTag } from '../validators'; type Tag = ReturnType; @@ -22,25 +23,6 @@ export function trackedWeakSet( const target = new WeakSet(data ?? []); const storages = new WeakMap(); - function storageFor(key: Value): Tag { - let storage = storages.get(key); - - if (storage === undefined) { - storage = createUpdatableTag(); - storages.set(key, storage); - } - - return storage; - } - - function dirtyStorageFor(key: Value): void { - const storage = storages.get(key); - - if (storage) { - DIRTY_TAG(storage); - } - } - const proxy: WeakSet = new Proxy(target, { get(target, prop, receiver) { if (prop === 'add') { @@ -70,7 +52,7 @@ export function trackedWeakSet( // Add to vals first to get better error message target.add(value); - dirtyStorageFor(value); + dirtyTagFor(target, value, storages); return proxy; }; @@ -80,7 +62,7 @@ export function trackedWeakSet( return function (value: Value): boolean { if (!target.has(value)) return false; - dirtyStorageFor(value); + dirtyTagFor(target, value, storages); storages.delete(value); return target.delete(value); @@ -89,7 +71,7 @@ export function trackedWeakSet( if (prop === 'has') { return function (value: Value): boolean { - consumeTag(storageFor(value)); + consumeTag(tagFor(target, value, storages)); return target.has(value); }; diff --git a/packages/@glimmer/validator/lib/meta.ts b/packages/@glimmer/validator/lib/meta.ts index 56e30cc7be1..db92ac51e61 100644 --- a/packages/@glimmer/validator/lib/meta.ts +++ b/packages/@glimmer/validator/lib/meta.ts @@ -15,18 +15,30 @@ function isObjectLike(u: T): u is Indexable & T { export type TagMeta = Map; +/** + * A map from a key to its tag. + * + * The registry keeps a `Map` for each object. + * A tracked collection passes its own `Map` or `WeakMap`, + * so its keys can be any value. + */ +export interface TagStore { + get(key: Key): UpdatableTag | undefined; + set(key: Key, tag: UpdatableTag): unknown; +} + const TRACKED_TAGS = new WeakMap(); -export function dirtyTagFor( +export function dirtyTagFor( obj: T, - key: keyof T | string | symbol, - meta?: TagMeta + key: Key, + meta?: TagStore ): void { if (DEBUG && !isObjectLike(obj)) { throw new Error(`BUG: Can't update a tag for a primitive`); } - let tags = meta === undefined ? TRACKED_TAGS.get(obj) : meta; + let tags = meta === undefined ? (TRACKED_TAGS.get(obj) as TagStore | undefined) : meta; // No tags have been setup for this object yet, return if (tags === undefined) return; @@ -36,7 +48,7 @@ export function dirtyTagFor( if (propertyTag !== undefined) { if (DEBUG) { - unwrap(debug.assertTagNotConsumed)(propertyTag, obj, key); + unwrap(debug.assertTagNotConsumed)(propertyTag, obj, key as keyof T); } DIRTY_TAG(propertyTag, true); @@ -55,12 +67,12 @@ export function tagMetaFor(obj: object): TagMeta { return tags; } -export function tagFor( +export function tagFor( obj: T, - key: keyof T | string | symbol, - meta?: TagMeta + key: Key, + meta?: TagStore ): UpdatableTag | ConstantTag { - let tags = meta === undefined ? tagMetaFor(obj) : meta; + let tags = meta === undefined ? (tagMetaFor(obj) as TagStore) : meta; let tag = tags.get(key); if (tag === undefined) { diff --git a/packages/@glimmer/validator/lib/tracked-data.ts b/packages/@glimmer/validator/lib/tracked-data.ts deleted file mode 100644 index 560f6d71a54..00000000000 --- a/packages/@glimmer/validator/lib/tracked-data.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { dirtyTagFor, tagFor } from './meta'; -import { consumeTag } from './tracking'; - -export type Getter = (self: T) => T[K] | undefined; -export type Setter = (self: T, value: T[K]) => void; - -export function trackedData( - key: K, - initializer?: (this: T) => T[K] -): { getter: Getter; setter: Setter } { - let values = new WeakMap(); - let hasInitializer = typeof initializer === 'function'; - - function getter(self: T) { - consumeTag(tagFor(self, key)); - - let value; - - // If the field has never been initialized, we should initialize it - if (hasInitializer && !values.has(self)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - value = initializer!.call(self); - values.set(self, value); - } else { - value = values.get(self); - } - - return value; - } - - function setter(self: T, value: T[K]): void { - dirtyTagFor(self, key); - values.set(self, value); - } - - return { getter, setter }; -} diff --git a/packages/@glimmer/validator/lib/tracked-value.ts b/packages/@glimmer/validator/lib/tracked-value.ts index a59a1000665..ebfbcfe99f1 100644 --- a/packages/@glimmer/validator/lib/tracked-value.ts +++ b/packages/@glimmer/validator/lib/tracked-value.ts @@ -42,7 +42,11 @@ export class TrackedValue implements Reactive { #update: ((updater: (value: Value) => Value) => void) | undefined; #freeze: (() => void) | undefined; - constructor(value: Value, options: ReactiveOptions) { + /** + * The `@tracked` decorator passes the tag that the tag registry has for the field. + * `tagFor` and `dirtyTagFor` then work on the tag of the value. + */ + constructor(value: Value, options: ReactiveOptions, tag?: UpdatableTag) { /** * If we let V8 try to completely optimize TrackedValue for * for each type of `value`, @@ -58,7 +62,7 @@ export class TrackedValue implements Reactive { this.#value = 0 as Value; this.#value = value; this.#options = options; - this.#tag = createUpdatableTag(); + this.#tag = tag === undefined ? createUpdatableTag() : tag; } /** diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 40d8d57f18a..4a08fa3a48f 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -13,7 +13,6 @@ import { isTracking, resetTracking, track, - trackedData, untrack, validateTag, valueForTag, @@ -519,84 +518,6 @@ module('@glimmer/validator: tracking', () => { } }); - module('trackedData', () => { - test('it creates a storage cell that can be accessed and updated', (assert) => { - class Foo { - foo = 123; - } - - let { getter, setter } = trackedData('foo'); - - let foo = new Foo(); - - setter(foo, 456); - assert.strictEqual(getter(foo), 456, 'value is set correctly'); - assert.strictEqual(foo.foo, 123, 'value is not set on the actual object'); - }); - - test('it can receive an initializer', (assert) => { - class Foo { - foo = 123; - bar = 456; - } - - let { getter } = trackedData('foo', function (this: Foo) { - return this.bar; - }); - - let foo = new Foo(); - - assert.strictEqual(getter(foo), 456, 'value is initialized correctly'); - assert.strictEqual(foo.foo, 123, 'value is not set on the actual object'); - }); - - test('it tracks changes to the storage cell', (assert) => { - class Foo { - foo = 123; - bar = 456; - } - - let { getter, setter } = trackedData('foo', function (this: Foo) { - return this.bar; - }); - - let foo = new Foo(); - let tag = track(() => { - assert.strictEqual(getter(foo), 456, 'value is set correctly'); - }); - - let snapshot = valueForTag(tag); - - setter(foo, 789); - assert.notOk(validateTag(tag, snapshot)); - }); - - if (DEBUG) { - test('it errors when attempting to update a value already consumed in the same transaction', (assert) => { - class Foo { - foo = 123; - bar = 456; - } - - let { getter, setter } = trackedData('foo', function (this: Foo) { - return this.bar; - }); - - let foo = new Foo(); - - assert.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - debug.runInTrackingTransaction!(() => { - track(() => { - getter(foo); - setter(foo, 789); - }); - }); - }, /You attempted to update `foo` on `Foo`/); - }); - } - }); - if (DEBUG) { module('debug', () => { test('it errors when attempting to update a value that has already been consumed in the same transaction', (assert) => {