Skip to content

Commit

Permalink
Autotrack, take 2
Browse files Browse the repository at this point in the history
  • Loading branch information
wycats committed Feb 21, 2018
1 parent dea414f commit db88676
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 29 deletions.
3 changes: 2 additions & 1 deletion packages/@glimmer/component/src/component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert } from "@glimmer/util";
import { metaFor } from "./tracked";
import { CURRENT_TAG } from "@glimmer/reference";

export interface Bounds {
firstNode: Node;
Expand Down Expand Up @@ -266,7 +267,7 @@ class Component {

set args(args) {
this.__args__ = args;
metaFor(this).dirtyableTagFor("args").inner.dirty();
metaFor(this).updatableTagFor("args").inner.update(CURRENT_TAG);
}

/** @private
Expand Down
113 changes: 91 additions & 22 deletions packages/@glimmer/component/src/tracked.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { DEBUG } from "@glimmer/env";
import { Tag, DirtyableTag, TagWrapper, combine, CONSTANT_TAG } from "@glimmer/reference";
import { dict, Dict } from "@glimmer/util";
import { Tag, DirtyableTag, UpdatableTag, TagWrapper, combine, CONSTANT_TAG, CURRENT_TAG } from "@glimmer/reference";
import { dict, Dict, Option } from "@glimmer/util";

/**
* An object that that tracks @tracked properties that were consumed.
*/
class Tracker {
private tags = new Set<Tag>();

add(tag: Tag) {
this.tags.add(tag);
}

combine(): Tag {
let tags: Tag[] = [];
this.tags.forEach(tag => tags.push(tag));
return combine(tags);
}
}

/**
* @decorator
Expand Down Expand Up @@ -82,19 +99,62 @@ export function tracked(...dependencies: any[]): any {
}
}

/**
* Whenever a tracked computed property is entered, the current tracker is
* saved off and a new tracker is replaced.
*
* Any tracked properties consumed are added to the current tracker.
*
* When a tracked computed property is exited, the tracker's tags are
* combined and added to the parent tracker.
*
* The consequence is that each tracked computed property has a tag
* that corresponds to the tracked properties consumed inside of
* itself, including child tracked computed properties.
*/
let CURRENT_TRACKER: Option<Tracker> = null;

function descriptorForTrackedComputedProperty(target: any, key: any, descriptor: PropertyDescriptor, dependencies: string[]): PropertyDescriptor {
let meta = metaFor(target);
meta.trackedProperties[key] = true;
meta.trackedPropertyDependencies[key] = dependencies || [];

let get = descriptor.get as Function;
let set = descriptor.set as Function;

function getter(this: any) {
// Swap the parent tracker for a new tracker
let old = CURRENT_TRACKER;
let tracker = CURRENT_TRACKER = new Tracker();

// Call the getter
let ret = get.call(this);

// Swap back the parent tracker
CURRENT_TRACKER = old;

// Combine the tags in the new tracker and add them to the parent tracker
let tag = tracker.combine();
if (CURRENT_TRACKER) CURRENT_TRACKER.add(tag);

// Update the UpdatableTag for this property with the tag for all of the
// consumed dependencies.
metaFor(this).updatableTagFor(key).inner.update(tag);

return ret;
}

return {
enumerable: true,
configurable: false,
get: descriptor.get,
get: getter,
set: function() {
metaFor(this).dirtyableTagFor(key).inner.dirty();
descriptor.set.apply(this, arguments);
propertyDidChange();
// Bump the global revision counter
EPOCH.inner.dirty();

// Mark the UpdatableTag for this property with the current tag.
metaFor(this).updatableTagFor(key).inner.update(CURRENT_TAG);
set.apply(this, arguments);
}
};
}
Expand Down Expand Up @@ -125,11 +185,16 @@ function installTrackedProperty(target: any, key: Key) {
configurable: true,

get() {
if (CURRENT_TRACKER) CURRENT_TRACKER.add(metaFor(this).updatableTagFor(key));
return this[shadowKey];
},

set(newValue) {
metaFor(this).dirtyableTagFor(key).inner.dirty();
// Bump the global revision counter
EPOCH.inner.dirty();

// Mark the UpdatableTag for this property with the current tag.
metaFor(this).updatableTagFor(key).inner.update(CURRENT_TAG);
this[shadowKey] = newValue;
propertyDidChange();
}
Expand All @@ -152,13 +217,13 @@ function installTrackedProperty(target: any, key: Key) {
*/
export default class Meta {
tags: Dict<Tag>;
computedPropertyTags: Dict<TagWrapper<DirtyableTag>>;
computedPropertyTags: Dict<TagWrapper<UpdatableTag>>;
trackedProperties: Dict<boolean>;
trackedPropertyDependencies: Dict<string[]>;

constructor(parent: Meta) {
this.tags = dict<Tag>();
this.computedPropertyTags = dict<TagWrapper<DirtyableTag>>();
this.computedPropertyTags = dict<TagWrapper<UpdatableTag>>();
this.trackedProperties = parent ? Object.create(parent.trackedProperties) : dict<boolean>();
this.trackedPropertyDependencies = parent ? Object.create(parent.trackedPropertyDependencies) : dict<string[]>();
}
Expand All @@ -168,8 +233,8 @@ export default class Meta {
* by e.g. Glimmer VM to detect when a property should be re-rendered. Think
* of this as the "public-facing" tag.
*
* For static tracked properties, this is a single DirtyableTag. For computed
* properties, it is a combinator of the property's DirtyableTag as well as
* For static tracked properties, this is a single UpdatableTag. For computed
* properties, it is a combinator of the property's UpdatableTag as well as
* all of its dependencies' tags.
*/
tagFor(key: Key): Tag {
Expand All @@ -186,31 +251,31 @@ export default class Meta {

/**
* The tag used internally to invalidate when a tracked property is set. For
* static properties, this is the same DirtyableTag returned from `tagFor`.
* For computed properties, it is the DirtyableTag used as one of the tags in
* static properties, this is the same UpdatableTag returned from `tagFor`.
* For computed properties, it is the UpdatableTag used as one of the tags in
* the tag combinator of the CP and its dependencies.
*/
dirtyableTagFor(key: Key): TagWrapper<DirtyableTag> {
updatableTagFor(key: Key): TagWrapper<UpdatableTag> {
let dependencies = this.trackedPropertyDependencies[key];
let tag;

if (dependencies) {
// The key is for a computed property.
tag = this.computedPropertyTags[key];
if (tag) { return tag; }
return this.computedPropertyTags[key] = DirtyableTag.create();
return this.computedPropertyTags[key] = UpdatableTag.create(CURRENT_TAG);
} else {
// The key is for a static property.
tag = this.tags[key];
if (tag) { return tag as TagWrapper<DirtyableTag>; }
return this.tags[key] = DirtyableTag.create();
if (tag) { return tag as TagWrapper<UpdatableTag>; }
return this.tags[key] = UpdatableTag.create(CURRENT_TAG);
}
}
}

function combinatorForComputedProperties(meta: Meta, key: Key, dependencies: Key[] | void): Tag {
// Start off with the tag for the CP's own dirty state.
let tags: Tag[] = [meta.dirtyableTagFor(key)];
let tags: Tag[] = [meta.updatableTagFor(key)];

// Next, add in all of the tags for its dependencies.
if (dependencies && dependencies.length) {
Expand Down Expand Up @@ -243,6 +308,8 @@ function hasOwnProperty(obj: any, key: symbol) {
return hOP.call(obj, key);
}

const EPOCH = DirtyableTag.create();

let propertyDidChange = function() {};

export function setPropertyDidChange(cb: () => void) {
Expand Down Expand Up @@ -300,7 +367,7 @@ export function tagForProperty(obj: any, key: string, throwError: UntrackedPrope
*/
function installDevModeErrorInterceptor(obj: object, key: string, throwError: UntrackedPropertyErrorThrower) {
let target = obj;
let descriptor: PropertyDescriptor;
let descriptor: Option<PropertyDescriptor> = null;

// Find the descriptor for the current property. We may need to walk the
// prototype chain to do so. If the property is undefined, we may never get a
Expand All @@ -316,16 +383,18 @@ function installDevModeErrorInterceptor(obj: object, key: string, throwError: Un
// If possible, define a property descriptor that passes through the current
// value on reads but throws an exception on writes.
if (descriptor) {
let { get, value } = descriptor;

if (descriptor.configurable || !hasOwnDescriptor) {
Object.defineProperty(obj, key, {
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,

get() {
if (descriptor.get) {
return descriptor.get.call(this);
if (get) {
return get.call(this);
} else {
return descriptor.value;
return value;
}
},

Expand Down
88 changes: 82 additions & 6 deletions packages/@glimmer/component/test/browser/tracked-property-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,14 @@ test('can track a computed property', (assert) => {

test('tracked computed properties are invalidated when their dependencies are invalidated', (assert) => {
class TrackedPerson {
@tracked('fullName')
get salutation() {
@tracked get salutation() {
return `Hello, ${this.fullName}!`;
}

@tracked('firstName', 'lastName')
get fullName() {
@tracked get fullName() {
return `${this.firstName} ${this.lastName}`;
}

set fullName(fullName) {
let [firstName, lastName] = fullName.split(' ');
this.firstName = firstName;
Expand All @@ -192,8 +191,8 @@ test('tracked computed properties are invalidated when their dependencies are in
}

let obj = new TrackedPerson();
assert.strictEqual(obj.salutation, 'Hello, Tom Dale!');
assert.strictEqual(obj.fullName, 'Tom Dale');
assert.strictEqual(obj.salutation, 'Hello, Tom Dale!', `the saluation field is valid`);
assert.strictEqual(obj.fullName, 'Tom Dale', `the fullName field is valid`);

let tag = tagForProperty(obj, 'salutation');
let snapshot = tag.value();
Expand All @@ -219,6 +218,83 @@ test('tracked computed properties are invalidated when their dependencies are in
assert.strictEqual(tag.validate(snapshot), true);
});

test('nested @tracked in multiple objects', (assert) => {
class TrackedPerson {
@tracked get salutation() {
return `Hello, ${this.fullName}!`;
}

@tracked get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}

set fullName(fullName: string) {
let [firstName, lastName] = fullName.split(' ');
this.firstName = firstName;
this.lastName = lastName;
}

toString() {
return this.fullName;
}

@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
}

class TrackedContact {
@tracked email: string;
@tracked person: TrackedPerson;

constructor(person: TrackedPerson, email: string) {
this.person = person;
this.email = email;
}

@tracked get contact(): string {
return `${this.person} @ ${this.email}`;
}
}

let obj = new TrackedContact(new TrackedPerson(), '[email protected]');
assert.strictEqual(obj.contact, 'Tom Dale @ [email protected]', `the contact field is valid`);
assert.strictEqual(obj.person.fullName, 'Tom Dale', `the fullName field is valid`);
let person = obj.person;

let tag = tagForProperty(obj, 'contact');
let snapshot = tag.value();
assert.ok(tag.validate(snapshot), 'tag should be valid to start');

person.firstName = 'Edsger';
person.lastName = 'Dijkstra';
assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after nested dependency is set');
assert.strictEqual(person.fullName, 'Edsger Dijkstra');
assert.strictEqual(obj.contact, 'Edsger Dijkstra @ [email protected]');

snapshot = tag.value();
assert.strictEqual(tag.validate(snapshot), true);

person.fullName = 'Alan Kay';
assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after chained dependency is set');
assert.strictEqual(person.fullName, 'Alan Kay');
assert.strictEqual(person.firstName, 'Alan');
assert.strictEqual(person.lastName, 'Kay');
assert.strictEqual(obj.contact, 'Alan Kay @ [email protected]');

snapshot = tag.value();
assert.strictEqual(tag.validate(snapshot), true);

obj.email = "[email protected]";
assert.strictEqual(tag.validate(snapshot), false, 'tag is invalidated after chained dependency is set');
assert.strictEqual(person.fullName, 'Alan Kay');
assert.strictEqual(person.firstName, 'Alan');
assert.strictEqual(person.lastName, 'Kay');
assert.strictEqual(obj.contact, 'Alan Kay @ [email protected]');

snapshot = tag.value();
assert.strictEqual(tag.validate(snapshot), true);
});

module('[@glimmer/component] Tracked Property Warning in Development Mode');

if (DEBUG) {
Expand Down

0 comments on commit db88676

Please sign in to comment.