feat(ChangeDetector): implement enabling/disabling watch group

This commit is contained in:
vsavkin 2014-11-18 17:26:38 -08:00
parent 41856ad3f0
commit 862c6412c4
5 changed files with 309 additions and 228 deletions

View File

@ -13,7 +13,7 @@ export class ChangeDetector {
detectChanges():int {
var count:int = 0;
for (var record = this._rootWatchGroup.headEnabledRecord;
for (var record = this._rootWatchGroup.findFirstEnabledRecord();
isPresent(record);
record = record.nextEnabled) {
if (record.check()) {

View File

@ -1,5 +1,5 @@
import {ProtoWatchGroup, WatchGroup} from './watch_group';
import {FIELD, isPresent, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
import {ListWrapper, MapWrapper} from 'facade/collection';
import {ClosureMap} from 'change_detection/parser/closure_map';
@ -105,6 +105,11 @@ export class Record {
this.funcOrValue = null;
this.args = null;
if (isBlank(protoRecord)) {
this.mode = MODE_STATE_MARKER;
return;
}
var type = protoRecord.recordType;
if (type === PROTO_RECORD_CONST) {
this.mode = MODE_STATE_CONST;
@ -135,6 +140,12 @@ export class Record {
}
}
static createMarker(wg:WatchGroup) {
var r = new Record(wg, null, null);
r.disabled = true;
return r;
}
check():boolean {
this.previousValue = this.currentValue;
this.currentValue = this._calculateNewValue();
@ -200,7 +211,13 @@ export class Record {
updateContext(value) {
this.context = value;
this.watchGroup.enableRecord(this);
if (! this.isMarkerRecord) {
this.watchGroup.enableRecord(this);
}
}
get isMarkerRecord() {
return isBlank(this.protoRecord);
}
}

View File

@ -77,110 +77,158 @@ export class WatchGroup {
@FIELD('final dispatcher:WatchGroupDispatcher')
@FIELD('final headRecord:Record')
@FIELD('final tailRecord:Record')
@FIELD('final disabled:boolean')
// TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher.
// but @Implements is not ready yet.
constructor(protoWatchGroup:ProtoWatchGroup, dispatcher) {
this.protoWatchGroup = protoWatchGroup;
this.dispatcher = dispatcher;
this.headRecord = null;
this.tailRecord = null;
this.headEnabledRecord = null;
this.tailEnabledRecord = null;
this.context = null;
this.childHead = null;
this.childTail = null;
this.next = null;
this.prev = null;
this.disabled = false;
this.headRecord = Record.createMarker(this);
this.tailRecord = Record.createMarker(this);
this.headRecord.next = this.tailRecord;
this.tailRecord.prev = this.headRecord;
}
/// addRecord must be called before addChild
/// addRecord assumes that all records are enabled
addRecord(record:Record) {
if (isPresent(this.tailRecord)) {
this.tailRecord.next = record;
this.tailRecord.nextEnabled = record;
record.prev = this.tailRecord;
record.prevEnabled = this.tailRecord;
this.tailRecord = this.tailEnabledRecord = record;
var lastRecord = this.tailRecord.prev;
} else {
this.headRecord = this.tailRecord = record;
this.headEnabledRecord = this.tailEnabledRecord = record;
lastRecord.next = record;
lastRecord.nextEnabled = record;
record.prev = lastRecord;
record.prevEnabled = lastRecord;
record.next = this.tailRecord;
this.tailRecord.prev = record;
}
addChild(child:WatchGroup) {
var lastRecord = this.tailRecord.prev;
var lastEnabledRecord = this.findLastEnabledRecord();
var firstEnabledChildRecord = child.findFirstEnabledRecord();
lastRecord.next = child.headRecord;
child.headRecord.prev = lastRecord;
child.tailRecord.next = this.tailRecord;
this.tailRecord.prev = child.tailRecord;
if (isPresent(lastEnabledRecord) && isPresent(firstEnabledChildRecord)) {
lastEnabledRecord.nextEnabled = firstEnabledChildRecord;
firstEnabledChildRecord.prevEnabled = lastEnabledRecord;
}
}
removeChild(child:WatchGroup) {
var firstEnabledChildRecord = child.findFirstEnabledRecord();
var lastEnabledChildRecord = child.findLastEnabledRecord();
var next = child.tailRecord.next;
var prev = child.headRecord.prev;
next.prev = prev;
prev.next = next;
var nextEnabled = lastEnabledChildRecord.nextEnabled;
var prevEnabled = firstEnabledChildRecord.prevEnabled;
if (isPresent(nextEnabled)) nextEnabled.prev = prevEnabled;
if (isPresent(prevEnabled)) prevEnabled.next = nextEnabled;
}
findFirstEnabledRecord() {
return this._nextEnabled(this.headRecord);
}
findLastEnabledRecord() {
return this._prevEnabled(this.tailRecord);
}
disableRecord(record:Record) {
var prev = record.prevEnabled;
var next = record.nextEnabled;
var prevEnabled = record.prevEnabled;
var nextEnabled = record.nextEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
record.disabled = true;
if (isPresent(prev)) {
prev.nextEnabled = next;
} else {
this.headEnabledRecord = next;
}
if (isPresent(next)) {
next.prevEnabled = prev;
} else {
this.tailEnabledRecord = prev;
}
}
enableRecord(record:Record) {
if (!record.disabled) return;
var prev = record.prev;
while (prev != null && prev.disabled) prev = prev.prev;
var prevEnabled = this._prevEnabled(record);
var nextEnabled = this._nextEnabled(record);
var next = record.next;
while (next != null && next.disabled) next = next.next;
record.prevEnabled = prevEnabled;
record.nextEnabled = nextEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = record;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = record;
record.disabled = false;
record.prevEnabled = prev;
record.nextEnabled = next;
if (isPresent(prev)) {
prev.nextEnabled = record;
} else {
this.headEnabledRecord = record;
}
if (isPresent(next)) {
next.prevEnabled = record;
} else {
this.tailEnabledRecord = record;
}
}
addChild(child:WatchGroup) {
if (isBlank(this.childTail)) {
this.childHead = this.childTail = child;
} else {
this.childTail.next = child;
child.prev = this.childTail;
this.childTail = child;
}
this._attachRecordsFromWatchGroup(child);
disableGroup(child:WatchGroup) {
var firstEnabledChildRecord = child.findFirstEnabledRecord();
var lastEnabledChildRecord = child.findLastEnabledRecord();
var nextEnabled = lastEnabledChildRecord.nextEnabled;
var prevEnabled = firstEnabledChildRecord.prevEnabled;
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
child.disabled = true;
}
_attachRecordsFromWatchGroup(child:WatchGroup) {
if (isPresent(this.tailRecord)) {
if (isPresent(child.headRecord)) {
this.tailRecord.next = child.headRecord;
this.tailRecord.nextEnabled = child.headRecord;
enableGroup(child:WatchGroup) {
var prevEnabledRecord = this._prevEnabled(child.headRecord);
var nextEnabledRecord = this._nextEnabled(child.tailRecord);
child.headRecord.prev = this.tailRecord;
child.headRecord.prevEnabled = this.tailRecord;
var firstEnabledChildRecord = child.findFirstEnabledRecord();
var lastEnabledChildRecord = child.findLastEnabledRecord();
if (isPresent(firstEnabledChildRecord) && isPresent(prevEnabledRecord)){
firstEnabledChildRecord.prevEnabled = prevEnabledRecord;
prevEnabledRecord.nextEnabled = firstEnabledChildRecord;
}
if (isPresent(lastEnabledChildRecord) && isPresent(nextEnabledRecord)){
lastEnabledChildRecord.nextEnabled = nextEnabledRecord;
nextEnabledRecord.prevEnabled = lastEnabledChildRecord;
}
child.disabled = false;
}
_nextEnabled(record:Record) {
record = record.next;
while (isPresent(record) && record !== this.tailRecord && record.disabled) {
if (record.isMarkerRecord && record.watchGroup.disabled) {
record = record.watchGroup.tailRecord.next;
} else {
record = record.next;
}
} else {
this.headRecord = child.headRecord;
this.headEnabledRecord = child.headEnabledRecord;
}
return record === this.tailRecord ? null : record;
}
this.tailRecord = child.tailRecord;
this.tailEnabledRecord = child.tailEnabledRecord;
_prevEnabled(record:Record) {
record = record.prev;
while (isPresent(record) && record !== this.headRecord && record.disabled) {
if (record.isMarkerRecord && record.watchGroup.disabled) {
record = record.watchGroup.headRecord.prev;
} else {
record = record.prev;
}
}
return record === this.headRecord ? null : record;
}
/**

View File

@ -1,5 +1,6 @@
import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
import {isPresent} from 'facade/lang';
import {List, ListWrapper, MapWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';

View File

@ -1,6 +1,7 @@
import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib';
import {List, ListWrapper, MapWrapper} from 'facade/collection';
import {isPresent} from 'facade/lang';
import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';
import {ClosureMap} from 'change_detection/parser/closure_map';
@ -11,180 +12,125 @@ import {
WatchGroup,
WatchGroupDispatcher,
ProtoRecord
} from 'change_detection/change_detector';
} from 'change_detection/change_detector';
import {Record} from 'change_detection/record';
export function main() {
function humanize(wg:WatchGroup, names:List) {
var lookupName = (item) =>
ListWrapper.last(
ListWrapper.find(names, (pair) => pair[0] === item));
var res = [];
var record = wg.findFirstEnabledRecord();
while (isPresent(record)) {
ListWrapper.push(res, lookupName(record));
record = record.nextEnabled;
}
return res;
}
function createRecord(wg) {
return new Record(wg, new ProtoRecord(null, null, null, null, null), null);
}
describe('watch group', () => {
describe("adding records", () => {
it("should add a record", () => {
var wg = new WatchGroup(null, null);
var record = createRecord(wg);
it("should add records", () => {
var wg = new WatchGroup(null, null);
var record1 = createRecord(wg);
var record2 = createRecord(wg);
wg.addRecord(record);
wg.addRecord(record1);
wg.addRecord(record2);
expect(wg.headRecord).toBe(record);
expect(wg.tailRecord).toBe(record);
expect(wg.headEnabledRecord).toBe(record);
expect(wg.tailEnabledRecord).toBe(record);
});
it("should add multiple records", () => {
var wg = new WatchGroup(null, null);
var record1 = createRecord(wg);
var record2 = createRecord(wg);
wg.addRecord(record1);
wg.addRecord(record2);
expect(wg.headRecord).toBe(record1);
expect(wg.tailRecord).toBe(record2);
expect(wg.headEnabledRecord).toBe(record1);
expect(wg.tailEnabledRecord).toBe(record2);
expect(record1.next).toBe(record2);
expect(record2.prev).toBe(record1);
});
expect(humanize(wg, [
[record1, 'record1'],
[record2, 'record2']
])).toEqual(['record1', 'record2']);
});
describe("adding children", () => {
it("should add child watch group", () => {
var parent = new WatchGroup(null, null);
var child1 = new WatchGroup(null, null);
var child2 = new WatchGroup(null, null);
describe("adding/removing child groups", () => {
var parent, child1, child2;
var childRecord1, childRecord2;
var recordNames;
beforeEach(() => {
parent = new WatchGroup(null, null);
child1 = new WatchGroup(null, null);
childRecord1 = createRecord(child1);
child1.addRecord(childRecord1);
child2 = new WatchGroup(null, null);
childRecord2 = createRecord(child2);
child2.addRecord(childRecord2);
recordNames = [
[childRecord1, 'record1'],
[childRecord2, 'record2'],
];
});
it("should add child groups", () => {
parent.addChild(child1);
parent.addChild(child2);
expect(parent.childHead).toBe(child1);
expect(parent.childTail).toBe(child2);
expect(child1.next).toBe(child2);
expect(child2.prev).toBe(child1);
expect(humanize(parent, recordNames)).toEqual(['record1', 'record2']);
});
it("should link all records", () => {
var parent = new WatchGroup(null, null);
var parentRecord = createRecord(parent);
parent.addRecord(parentRecord);
it("should remove children", () => {
parent.addChild(child1);
parent.addChild(child2);
var child = new WatchGroup(null, null);
var childRecord = createRecord(child);
child.addRecord(childRecord);
parent.removeChild(child1);
parent.addChild(child);
expect(humanize(parent, recordNames)).toEqual(['record2']);
expect(parent.headRecord).toBe(parentRecord);
expect(parent.tailRecord).toBe(childRecord);
parent.removeChild(child2);
expect(parent.headEnabledRecord).toBe(parentRecord);
expect(parent.tailEnabledRecord).toBe(childRecord);
expect(parentRecord.next).toBe(childRecord);
expect(childRecord.prev).toBe(parentRecord);
});
it("should work when parent has no records", () => {
var parent = new WatchGroup(null, null);
var child = new WatchGroup(null, null);
var childRecord = createRecord(child);
child.addRecord(childRecord);
parent.addChild(child);
expect(parent.headRecord).toBe(childRecord);
expect(parent.tailRecord).toBe(childRecord);
expect(parent.headEnabledRecord).toBe(childRecord);
expect(parent.tailEnabledRecord).toBe(childRecord);
});
it("should work when parent has no records and first child has no records", () => {
var parent = new WatchGroup(null, null);
var firstChild = new WatchGroup(null, null);
parent.addChild(firstChild);
var child = new WatchGroup(null, null);
var childRecord = createRecord(child);
child.addRecord(childRecord);
parent.addChild(child);
expect(parent.headRecord).toBe(childRecord);
expect(parent.tailRecord).toBe(childRecord);
expect(parent.headEnabledRecord).toBe(childRecord);
expect(parent.tailEnabledRecord).toBe(childRecord);
});
it("should work when second child has no records", () => {
var parent = new WatchGroup(null, null);
var firstChild = new WatchGroup(null, null);
var childRecord = createRecord(firstChild);
firstChild.addRecord(childRecord);
parent.addChild(firstChild);
var secondChild = new WatchGroup(null, null);
parent.addChild(secondChild);
expect(parent.childHead).toBe(firstChild);
expect(parent.childTail).toBe(secondChild);
});
// todo: vsavkin: enable after refactoring addChild
xit("should update head and tail of the parent when disabling the only record" +
"of the child", () => {
var parent = new WatchGroup(null, null);
var child = new WatchGroup(null, null);
var record = createRecord(child);
child.addRecord(record);
parent.addChild(child);
child.disableRecord(record);
expect(parent.headRecord).toBeNull();
expect(parent.tailRecord).toBeNull();
expect(humanize(parent, recordNames)).toEqual([]);
});
});
describe("enabling/disabling records", () => {
var wg;
var record1, record2, record3, record4;
var recordNames;
beforeEach(() => {
wg = new WatchGroup(null, null);
record1 = createRecord(wg);
record2 = createRecord(wg);
record3 = createRecord(wg);
record4 = createRecord(wg);
recordNames = [
[record1, 'record1'],
[record2, 'record2'],
[record3, 'record3'],
[record4, 'record4']
];
});
it("should disable a single record", () => {
var wg = new WatchGroup(null, null);
var record = createRecord(wg);
wg.addRecord(record);
wg.addRecord(record1);
wg.disableRecord(record);
wg.disableRecord(record1);
expect(wg.headEnabledRecord).toBeNull();
expect(wg.tailEnabledRecord).toBeNull();
expect(humanize(wg, recordNames)).toEqual([]);
});
it("should enable a single record", () => {
var wg = new WatchGroup(null, null);
var record = createRecord(wg);
wg.addRecord(record);
wg.disableRecord(record);
wg.addRecord(record1);
wg.disableRecord(record1);
wg.enableRecord(record);
wg.enableRecord(record1);
expect(wg.headEnabledRecord).toBe(record);
expect(wg.tailEnabledRecord).toBe(record);
expect(humanize(wg, recordNames)).toEqual(['record1']);
});
it("should disable a record", () => {
var wg = new WatchGroup(null, null);
var record1 = createRecord(wg);
var record2 = createRecord(wg);
var record3 = createRecord(wg);
var record4 = createRecord(wg);
wg.addRecord(record1);
wg.addRecord(record2);
wg.addRecord(record3);
@ -194,20 +140,12 @@ export function main() {
wg.disableRecord(record3);
expect(record2.disabled).toBeTruthy();
expect(record3.disabled).toBeTruthy();
expect(wg.headEnabledRecord).toBe(record1);
expect(wg.tailEnabledRecord).toBe(record4);
expect(record1.nextEnabled).toBe(record4);
expect(record4.prevEnabled).toBe(record1);
expect(humanize(wg, recordNames)).toEqual(['record1', 'record4']);
});
it("should enable a record", () => {
var wg = new WatchGroup(null, null);
var record1 = createRecord(wg);
var record2 = createRecord(wg);
var record3 = createRecord(wg);
var record4 = createRecord(wg);
wg.addRecord(record1);
wg.addRecord(record2);
wg.addRecord(record3);
@ -218,11 +156,88 @@ export function main() {
wg.enableRecord(record2);
wg.enableRecord(record3);
expect(record1.nextEnabled).toBe(record2);
expect(record2.nextEnabled).toBe(record3);
expect(record3.nextEnabled).toBe(record4);
expect(record4.prevEnabled).toBe(record3);
expect(humanize(wg, recordNames)).toEqual(['record1', 'record2', 'record3', 'record4']);
});
})
});
describe("enabling/disabling child groups", () => {
var child1, child2, child3, child4;
var record1, record2, record3, record4;
var recordNames;
beforeEach(() => {
child1 = new WatchGroup(null, null);
record1 = createRecord(child1);
child1.addRecord(record1);
child2 = new WatchGroup(null, null);
record2 = createRecord(child2);
child2.addRecord(record2);
child3 = new WatchGroup(null, null);
record3 = createRecord(child3);
child3.addRecord(record3);
child4 = new WatchGroup(null, null);
record4 = createRecord(child4);
child4.addRecord(record4);
recordNames = [
[record1, 'record1'],
[record2, 'record2'],
[record3, 'record3'],
[record4, 'record4']
];
});
it("should disable a single watch group", () => {
var parent = new WatchGroup(null, null);
parent.addChild(child1);
parent.disableGroup(child1);
expect(humanize(parent, recordNames)).toEqual([]);
});
it("should enable a single watch group", () => {
var parent = new WatchGroup(null, null);
parent.addChild(child1);
parent.disableGroup(child1);
parent.enableGroup(child1);
expect(humanize(parent, recordNames)).toEqual(['record1']);
});
it("should disable a watch group", () => {
var parent = new WatchGroup(null, null);
parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);
parent.addChild(child4);
parent.disableGroup(child2);
parent.disableGroup(child3);
expect(humanize(parent, recordNames)).toEqual(['record1', 'record4']);
});
it("should enable a watch group", () => {
var parent = new WatchGroup(null, null);
parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);
parent.addChild(child4);
parent.disableGroup(child2);
parent.disableGroup(child3);
parent.enableGroup(child2);
parent.enableGroup(child3);
expect(humanize(parent, recordNames)).toEqual([
'record1', 'record2', 'record3', 'record4'
]);
});
});
});
}