Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 65213c3

Browse files
committed
feat(scope): add internal streams consistency checks
1 parent 1188ab0 commit 65213c3

File tree

2 files changed

+69
-22
lines changed

2 files changed

+69
-22
lines changed

lib/core/scope.dart

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,12 @@ class Scope {
157157
// such as # of watches, checks/1ms, field checks, function checks, etc
158158
final WatchGroup _readWriteGroup;
159159
final WatchGroup _readOnlyGroup;
160-
final int _depth;
161-
final int _index;
162160

163161
Scope _childHead, _childTail, _next, _prev;
164162
_Streams _streams;
165-
int _nextChildIndex = 0;
166163

167-
Scope(Object this.context, this.rootScope, this._parentScope, this._depth,
168-
this._index, this._readWriteGroup, this._readOnlyGroup);
164+
Scope(Object this.context, this.rootScope, this._parentScope,
165+
this._readWriteGroup, this._readOnlyGroup);
169166

170167
/**
171168
* A [watch] sets up a watch in the [digest] phase of the [apply] cycle.
@@ -175,7 +172,7 @@ class Scope {
175172
*/
176173
Watch watch(expression, ReactionFn reactionFn,
177174
{context, FilterMap filters, bool readOnly: false}) {
178-
assert(isAttached);
175+
_assertInternalStateConsistency();
179176
assert(expression != null);
180177
AST ast;
181178
Watch watch;
@@ -207,7 +204,7 @@ class Scope {
207204
}
208205

209206
dynamic eval(expression, [Map locals]) {
210-
assert(isAttached);
207+
_assertInternalStateConsistency();
211208
assert(expression == null ||
212209
expression is String ||
213210
expression is Function);
@@ -239,22 +236,21 @@ class Scope {
239236
}
240237

241238
ScopeEvent emit(String name, [data]) {
242-
assert(isAttached);
239+
_assertInternalStateConsistency();
243240
return _Streams.emit(this, name, data);
244241
}
245242
ScopeEvent broadcast(String name, [data]) {
246-
assert(isAttached);
243+
_assertInternalStateConsistency();
247244
return _Streams.broadcast(this, name, data);
248245
}
249246
ScopeStream on(String name) {
250-
assert(isAttached);
247+
_assertInternalStateConsistency();
251248
return _Streams.on(this, rootScope._exceptionHandler, name);
252249
}
253250

254251
Scope createChild(Object childContext) {
255-
assert(isAttached);
252+
_assertInternalStateConsistency();
256253
var child = new Scope(childContext, rootScope, this,
257-
_depth + 1, _nextChildIndex++,
258254
_readWriteGroup.newGroup(childContext),
259255
_readOnlyGroup.newGroup(childContext));
260256
var next = null;
@@ -267,7 +263,7 @@ class Scope {
267263
}
268264

269265
void destroy() {
270-
assert(isAttached);
266+
_assertInternalStateConsistency();
271267
broadcast(ScopeEvent.DESTROY);
272268
_Streams.destroy(this);
273269

@@ -289,9 +285,54 @@ class Scope {
289285
_readWriteGroup.remove();
290286
_readOnlyGroup.remove();
291287
_parentScope = null;
288+
_assertInternalStateConsistency();
289+
}
290+
291+
_assertInternalStateConsistency() {
292+
assert((() {
293+
rootScope._verifyStreams(null, '', []);
294+
return true;
295+
})());
296+
}
297+
298+
Map<bool,int> _verifyStreams(parentScope, prefix, log) {
299+
assert(_parentScope == parentScope);
300+
var counts = {};
301+
var typeCounts = _streams == null ? {} : _streams._typeCounts;
302+
log..add(prefix)..add(hashCode)..add(' ')..add(typeCounts)..add('\n');
303+
if (_streams == null) {
304+
} else if (_streams._scope == this) {
305+
_streams._streams.keys.forEach((k){
306+
counts[k] = 1 + (counts.containsKey(k) ? counts[k] : 0);
307+
});
308+
}
309+
var childScope = _childHead;
310+
while(childScope != null) {
311+
childScope._verifyStreams(this, ' $prefix', log).forEach((k, v) {
312+
counts[k] = v + (counts.containsKey(k) ? counts[k] : 0);
313+
});
314+
childScope = childScope._next;
315+
}
316+
if(!_mapEqual(counts, typeCounts)) {
317+
throw 'Streams actual: $counts != bookkeeping: $typeCounts\n'
318+
'Offending scope: [scope: ${this.hashCode}]\n'
319+
'${log.join('')}';
320+
}
321+
return counts;
292322
}
293323
}
294324

325+
_mapEqual(Map a, Map b) {
326+
if (a.length == b.length) {
327+
var equal = true;
328+
a.forEach((k, v) {
329+
equal = equal && b.containsKey(k) && v == b[k];
330+
});
331+
return equal;
332+
} else {
333+
return false;
334+
}
335+
}
295336

296337
class RootScope extends Scope {
297338
static final STATE_APPLY = 'apply';
@@ -314,7 +355,7 @@ class RootScope extends Scope {
314355
RootScope(Object context, this._astParser, this._parser,
315356
GetterCache cacheGetter, FilterMap filterMap,
316357
this._exceptionHandler, this._ttl, this._zone)
317-
: super(context, null, null, 0, 0,
358+
: super(context, null, null,
318359
new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context),
319360
new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context))
320361
{
@@ -558,15 +599,19 @@ class _Streams {
558599

559600
static void destroy(Scope scope) {
560601
var toBeDeletedStreams = scope._streams;
561-
if (toBeDeletedStreams == null) return;
562-
scope = scope._parentScope; // skip current state as not to delete listeners
563-
while (scope != null && scope._streams == toBeDeletedStreams) {
564-
scope._streams = null;
565-
scope = scope._parentScope;
602+
if (toBeDeletedStreams == null) return; // no streams to clean up
603+
var parentScope = scope._parentScope; // skip current scope as not to delete listeners
604+
// find the parent-most scope which still has our stream to be deleted.
605+
while (parentScope != null && parentScope._streams == toBeDeletedStreams) {
606+
parentScope._streams = null;
607+
parentScope = parentScope._parentScope;
566608
}
567-
if (scope == null) return;
568-
var parentStreams = scope._streams;
609+
// At this point scope is the parent-most scope which has its own typeCounts
610+
if (parentScope == null) return;
611+
var parentStreams = parentScope._streams;
569612
assert(parentStreams != toBeDeletedStreams);
613+
// remove typeCounts from the scope to be destroyed from the parent
614+
// typeCounts
570615
toBeDeletedStreams._typeCounts.forEach(
571616
(name, count) => parentStreams._addCount(name, -count));
572617
}
@@ -592,6 +637,7 @@ class _Streams {
592637
assert(count >= 0);
593638
if (count == 0) {
594639
lastStreams._typeCounts.remove(name);
640+
if (_scope == scope) _streams.remove(name);
595641
} else {
596642
lastStreams._typeCounts[name] = count;
597643
}

test/core/scope_spec.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,8 @@ main() => describe('scope', () {
550550
it(r'should broadcast the destroy event', inject((RootScope rootScope) {
551551
var log = [];
552552
first.on(ScopeEvent.DESTROY).listen((s) => log.add('first'));
553-
first.createChild({}).on(ScopeEvent.DESTROY).listen((s) => log.add('first-child'));
553+
var child = first.createChild({});
554+
child.on(ScopeEvent.DESTROY).listen((s) => log.add('first-child'));
554555

555556
first.destroy();
556557
expect(log).toEqual(['first', 'first-child']);

0 commit comments

Comments
 (0)