Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tracked utilities for ember-inspector #1489

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/@glimmer/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we export * here, should we do anything in the function implementations themselves to no-op in production?

export {
beginTrackFrame,
beginUntrackFrame,
Expand Down
8 changes: 7 additions & 1 deletion packages/@glimmer/validator/lib/meta.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -64,8 +65,13 @@ export function tagFor<T extends object>(

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;
}
80 changes: 80 additions & 0 deletions packages/@glimmer/validator/lib/tracked-utils.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>, 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<string, any>;
};

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;
}
3 changes: 2 additions & 1 deletion packages/@glimmer/validator/lib/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function allowsCycles(tag: Tag): boolean {
}
}

class MonomorphicTagImpl<T extends MonomorphicTagId = MonomorphicTagId> {
export class MonomorphicTagImpl<T extends MonomorphicTagId = MonomorphicTagId> {
static combine(this: void, tags: Tag[]): Tag {
switch (tags.length) {
case 0:
Expand All @@ -112,6 +112,7 @@ class MonomorphicTagImpl<T extends MonomorphicTagId = MonomorphicTagId> {
private isUpdating = false;
public subtag: Tag | Tag[] | null = null;
private subtagBufferCache: Revision | null = null;
public meta: any = null;

[TYPE]: T;

Expand Down
11 changes: 10 additions & 1 deletion packages/@glimmer/validator/test/meta-test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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');
});
});
99 changes: 99 additions & 0 deletions packages/@glimmer/validator/test/tracked-utils-test.ts
Original file line number Diff line number Diff line change
@@ -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<TestObject, 'item1'>('item1', () => '');
Object.defineProperty(TestObject.prototype, 'item1', {
enumerable: true,
get(this) {
return getter(this);
},
set(this, v) {
return setter(this, v);
},
});
}
{
const { getter, setter } = trackedData<TestObject, 'item2'>('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',
},
]);
});
});
Loading