diff --git a/packages/@glimmer/validator/index.ts b/packages/@glimmer/validator/index.ts index 40c764d9c..9ad43b4b2 100644 --- a/packages/@glimmer/validator/index.ts +++ b/packages/@glimmer/validator/index.ts @@ -13,8 +13,9 @@ if (globalObj[GLIMMER_VALIDATOR_REGISTRATION] === true) { globalObj[GLIMMER_VALIDATOR_REGISTRATION] = true; export { debug } from './lib/debug'; -export { dirtyTagFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; +export { dirtyTagFor, infoForTag, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; export { trackedData } from './lib/tracked-data'; +export * from './lib/tracked-utils'; export { beginTrackFrame, beginUntrackFrame, diff --git a/packages/@glimmer/validator/lib/meta.ts b/packages/@glimmer/validator/lib/meta.ts index 68ff0d94e..00fd780bb 100644 --- a/packages/@glimmer/validator/lib/meta.ts +++ b/packages/@glimmer/validator/lib/meta.ts @@ -1,6 +1,7 @@ -import type { ConstantTag, UpdatableTag } from '@glimmer/interfaces'; +import type { ConstantTag, Tag, UpdatableTag } from '@glimmer/interfaces'; import type { Indexable } from './utils'; +import type { MonomorphicTagImpl } from './validators'; import { debug } from './debug'; import { unwrap } from './utils'; @@ -64,8 +65,13 @@ export function tagFor( if (tag === undefined) { tag = createUpdatableTag(); + (tag as MonomorphicTagImpl).meta = { propertyKey: key, object: obj }; tags.set(key, tag); } return tag; } + +export function infoForTag(tag: Tag) { + return (tag as MonomorphicTagImpl).meta; +} diff --git a/packages/@glimmer/validator/lib/tracked-utils.ts b/packages/@glimmer/validator/lib/tracked-utils.ts new file mode 100644 index 000000000..6162e0fae --- /dev/null +++ b/packages/@glimmer/validator/lib/tracked-utils.ts @@ -0,0 +1,80 @@ +import type { Tag } from '@glimmer/interfaces'; + +import type { MonomorphicTagImpl } from './validators'; + +import { infoForTag, tagFor } from './meta'; +import { track } from './tracking'; +import { validateTag, valueForTag } from './validators'; + +type Info = { + tag: MonomorphicTagImpl; + prevValue: number; + dependencies: { + object: object; + propertyKey: string; + changed: boolean; + }[]; +}; + +export function getTrackedDependencies(obj: Record, property: string, info?: Info) { + info = info || ({} as Info); + const tag = info?.tag || track(() => obj[property]); + const dependencies = []; + // do not include tracked properties from dependencies + + const subtags = (Array.isArray(tag.subtag) ? [tag, ...tag.subtag] : [tag, tag.subtag]).filter( + (t) => !!t + ) as Tag[]; + for (const subtag of subtags) { + if (subtag === tag) continue; + dependencies.push({ ...infoForTag(subtag), tag: subtag }); + if (subtag.subtag && !Array.isArray(subtag.subtag)) { + dependencies.push({ ...infoForTag(subtag.subtag) }); + } + } + + let maxRevision = valueForTag(tag); + + const hasChange = (info.prevValue && maxRevision !== info.prevValue) || false; + let latestValue = info.prevValue || 0; + + info.dependencies = dependencies.map((t) => { + if (t.tag.lastValue > latestValue) { + latestValue = t.tag.lastValue; + } + const changed = hasChange && t.tag.lastValue > info!.prevValue; + return { object: t.object, propertyKey: t.propertyKey, changed }; + }); + + info.prevValue = maxRevision; + + return info; +} + +type TrackedInfo = { + changed: string[]; + propertyInfo: Record; +}; + +export function getChangedProperties(obj: object, trackedInfo?: TrackedInfo) { + trackedInfo = trackedInfo || ({} as TrackedInfo); + trackedInfo['changed'] = []; + trackedInfo.propertyInfo = trackedInfo.propertyInfo || {}; + for (const name in obj) { + const tag = tagFor(obj, name); + const revision = valueForTag(tag); + let tagInfo = trackedInfo.propertyInfo?.[name] || { + tag: tag, + revision, + }; + if (!tagInfo.tag) return; + trackedInfo.propertyInfo[name] = tagInfo; + + const changed = !validateTag(tagInfo.tag, tagInfo.revision); + tagInfo.revision = revision; + if (changed) { + trackedInfo['changed'].push(name); + } + } + return trackedInfo; +} diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index da94e3806..df55aefd5 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -90,7 +90,7 @@ function allowsCycles(tag: Tag): boolean { } } -class MonomorphicTagImpl { +export class MonomorphicTagImpl { static combine(this: void, tags: Tag[]): Tag { switch (tags.length) { case 0: @@ -112,6 +112,7 @@ class MonomorphicTagImpl { private isUpdating = false; public subtag: Tag | Tag[] | null = null; private subtagBufferCache: Revision | null = null; + public meta: any = null; [TYPE]: T; diff --git a/packages/@glimmer/validator/test/meta-test.ts b/packages/@glimmer/validator/test/meta-test.ts index cf6a739c7..f826fd60e 100644 --- a/packages/@glimmer/validator/test/meta-test.ts +++ b/packages/@glimmer/validator/test/meta-test.ts @@ -1,4 +1,4 @@ -import { dirtyTagFor, tagFor, validateTag, valueForTag } from '@glimmer/validator'; +import { dirtyTagFor, infoForTag, tagFor, validateTag, valueForTag } from '@glimmer/validator'; import { module, test } from './-utils'; @@ -18,4 +18,13 @@ module('@glimmer/validator: meta', () => { assert.notOk(validateTag(tag, snapshot)); }); + + test('it can provide the object and property for the tag given object', (assert) => { + let obj = {}; + let tag = tagFor(obj, 'foo'); + + let info = infoForTag(tag)!; + assert.strictEqual(info.object, obj); + assert.strictEqual(info.propertyKey, 'foo'); + }); }); diff --git a/packages/@glimmer/validator/test/tracked-utils-test.ts b/packages/@glimmer/validator/test/tracked-utils-test.ts new file mode 100644 index 000000000..57dea92bd --- /dev/null +++ b/packages/@glimmer/validator/test/tracked-utils-test.ts @@ -0,0 +1,99 @@ +import { getChangedProperties, getTrackedDependencies, trackedData } from '@glimmer/validator'; + +import { module, test } from './-utils'; + +module('@glimmer/validator: tracked-utils', () => { + class TestObject { + declare item1: string; + declare item2: string; + item3 = ''; + constructor() {} + + get getterWithTracked() { + return this.item1 + ' world' + this.item2; + } + } + + { + const { getter, setter } = trackedData('item1', () => ''); + Object.defineProperty(TestObject.prototype, 'item1', { + enumerable: true, + get(this) { + return getter(this); + }, + set(this, v) { + return setter(this, v); + }, + }); + } + { + const { getter, setter } = trackedData('item2', () => ''); + Object.defineProperty(TestObject.prototype, 'item2', { + enumerable: true, + get(this) { + return getter(this); + }, + set(this, v) { + return setter(this, v); + }, + }); + } + + test('it can detect changed properties', (assert) => { + const obj = new TestObject(); + let trackedInfo = getChangedProperties(obj); + assert.deepEqual(trackedInfo?.changed, []); + + obj.item1 = 'hello'; + + assert.deepEqual(getChangedProperties(obj, trackedInfo)?.changed, ['item1']); + assert.deepEqual(getChangedProperties(obj, trackedInfo)?.changed, []); + + obj.item1 = 'hi'; + obj.item2 = 'hi'; + assert.deepEqual(getChangedProperties(obj, trackedInfo)?.changed, ['item1', 'item2']); + }); + + test('it can detect tracked dependencies', (assert) => { + const obj = new TestObject(); + let info = getTrackedDependencies(obj, 'getterWithTracked'); + assert.deepEqual(info.dependencies, [ + { + changed: false, + object: obj, + propertyKey: 'item1', + }, + { + changed: false, + object: obj, + propertyKey: 'item2', + }, + ]); + + obj.item1 = 'hi'; + assert.deepEqual(getTrackedDependencies(obj, 'getterWithTracked', info).dependencies, [ + { + changed: true, + object: obj, + propertyKey: 'item1', + }, + { + changed: false, + object: obj, + propertyKey: 'item2', + }, + ]); + assert.deepEqual(getTrackedDependencies(obj, 'getterWithTracked', info).dependencies, [ + { + changed: false, + object: obj, + propertyKey: 'item1', + }, + { + changed: false, + object: obj, + propertyKey: 'item2', + }, + ]); + }); +});