From 862c6412c447311b481db95bc41ad2f6a7db282c Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 18 Nov 2014 17:26:38 -0800 Subject: [PATCH] feat(ChangeDetector): implement enabling/disabling watch group --- .../change_detection/src/change_detector.js | 2 +- modules/change_detection/src/record.js | 21 +- modules/change_detection/src/watch_group.js | 192 +++++++---- .../test/change_detector_spec.js | 1 + .../change_detection/test/watch_group_spec.js | 321 +++++++++--------- 5 files changed, 309 insertions(+), 228 deletions(-) diff --git a/modules/change_detection/src/change_detector.js b/modules/change_detection/src/change_detector.js index 1ed3a903e9..b5888c9cea 100644 --- a/modules/change_detection/src/change_detector.js +++ b/modules/change_detection/src/change_detector.js @@ -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()) { diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index 8069d2e2b1..7e94ae98f2 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -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); } } diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index b8832da19e..44ac0c28df 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -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; } /** diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index 8cadb72257..63de35197c 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -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'; diff --git a/modules/change_detection/test/watch_group_spec.js b/modules/change_detection/test/watch_group_spec.js index bead04e19c..aea09703ba 100644 --- a/modules/change_detection/test/watch_group_spec.js +++ b/modules/change_detection/test/watch_group_spec.js @@ -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' + ]); + }); + }); }); } \ No newline at end of file