Skip to content

Commit 3099fa3

Browse files
committed
feat(scope): move domWrite and domRead from RootScope to Scope
Closes dart-archive#1161
1 parent 3ec5d75 commit 3099fa3

File tree

3 files changed

+212
-42
lines changed

3 files changed

+212
-42
lines changed

lib/core/scope.dart

+141-41
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,32 @@ class ScopeEvent {
5555
}
5656
}
5757

58+
/**
59+
* [ConcurrentScopeModificationException] is thrown when `domWrite` or `domRead` is called from
60+
* one of the ascendants of the scope.
61+
*
62+
* The following examples will both throw [ConcurrentScopeModificationException]:
63+
*
64+
* rootScope.domWrite(() {
65+
* childScope.domWrite((){});
66+
* });
67+
*
68+
* rootScope.domRead(() {
69+
* childScope.domRead((){});
70+
* });
71+
*
72+
* Calling `domWrite` or `domRead` from the descendants is allowed. So the following will run:
73+
*
74+
* childScope.domWrite(() {
75+
* rootScope.domWrite((){});
76+
* });
77+
*
78+
*/
79+
class ConcurrentScopeModificationException implements Exception {
80+
String toString() =>
81+
"Either `domWrite` or `domRead` is called from one of the ascendants of the current scope";
82+
}
83+
5884
/**
5985
* Allows the configuration of [Scope.digest] iteration maximum time-to-live value. Digest keeps
6086
* checking the state of the watcher getters until it can execute one full iteration with no
@@ -127,6 +153,11 @@ class Scope {
127153

128154
Scope _parentScope;
129155

156+
_FunctionChain _domReadHead, _domReadTail;
157+
_FunctionChain _domWriteHead, _domWriteTail;
158+
bool _scheduledDomWrite = false;
159+
bool _scheduledDomRead = false;
160+
130161
Scope get parentScope => _parentScope;
131162

132163
final ScopeStats _stats;
@@ -413,6 +444,107 @@ class Scope {
413444
}
414445
return counts;
415446
}
447+
448+
/**
449+
* Internal. Use [View.domWrite] instead.
450+
*/
451+
void domWrite(fn()) {
452+
checkForConcurrentModificationOnDomWrite();
453+
454+
var chain = new _FunctionChain(fn);
455+
if (_domWriteHead == null) {
456+
_domWriteHead = _domWriteTail = chain;
457+
} else {
458+
_domWriteTail = _domWriteTail._next = chain;
459+
}
460+
461+
_setScheduledDomWrite();
462+
}
463+
464+
/**
465+
* Internal. Use [View.domRead] instead.
466+
*/
467+
void domRead(fn()) {
468+
checkForConcurrentModificationOnDomRead();
469+
470+
var chain = new _FunctionChain(fn);
471+
if (_domReadHead == null) {
472+
_domReadHead = _domReadTail = chain;
473+
} else {
474+
_domReadTail = _domReadTail._next = chain;
475+
}
476+
477+
_setScheduledDomRead();
478+
}
479+
480+
void _runDomWrites() {
481+
Scope child = _childHead;
482+
while (child != null) {
483+
child._runDomWrites();
484+
child = child._next;
485+
}
486+
487+
_callFunctionsInChain(_domWriteHead);
488+
_domWriteHead = _domWriteTail = null;
489+
_scheduledDomWrite = false;
490+
}
491+
492+
void _runDomReads() {
493+
Scope child = _childHead;
494+
while (child != null) {
495+
child._runDomReads();
496+
child = child._next;
497+
}
498+
499+
_callFunctionsInChain(_domReadHead);
500+
_domReadHead = _domReadTail = null;
501+
_scheduledDomRead = false;
502+
}
503+
504+
void _callFunctionsInChain(_FunctionChain chain) {
505+
while (chain != null) {
506+
try {
507+
chain.fn();
508+
} catch (e, s) {
509+
_exceptionHandler(e, s);
510+
}
511+
chain = chain._next;
512+
}
513+
}
514+
515+
void checkForConcurrentModificationOnDomWrite() {
516+
if (_scheduledDomWrite) return;
517+
518+
for(var scope = _parentScope; scope != null; scope = scope._parentScope) {
519+
if (scope._scheduledDomWrite) throw new ConcurrentScopeModificationException();
520+
}
521+
}
522+
523+
void checkForConcurrentModificationOnDomRead() {
524+
if (_scheduledDomRead) return;
525+
526+
for(var scope = _parentScope; scope != null; scope = scope._parentScope) {
527+
if (scope._scheduledDomRead) throw new ConcurrentScopeModificationException();
528+
}
529+
}
530+
531+
void _setScheduledDomWrite() {
532+
if (_scheduledDomWrite) return;
533+
534+
for(var scope = this; scope != null; scope = scope._parentScope) {
535+
scope._scheduledDomWrite = true;
536+
}
537+
}
538+
539+
void _setScheduledDomRead() {
540+
if (_scheduledDomRead) return;
541+
542+
for(var scope = this; scope != null; scope = scope._parentScope) {
543+
scope._scheduledDomRead = true;
544+
}
545+
}
546+
547+
ExceptionHandler get _exceptionHandler => rootScope._exceptionHandler;
416548
}
417549

418550
_mapEqual(Map a, Map b) => a.length == b.length &&
@@ -587,8 +719,6 @@ class RootScope extends Scope {
587719
final Map<String, AST> astCache = new HashMap<String, AST>();
588720

589721
_FunctionChain _runAsyncHead, _runAsyncTail;
590-
_FunctionChain _domWriteHead, _domWriteTail;
591-
_FunctionChain _domReadHead, _domReadTail;
592722

593723
final ScopeStats _scopeStats;
594724

@@ -734,37 +864,25 @@ class RootScope extends Scope {
734864
bool runObservers = true;
735865
try {
736866
do {
737-
if (_domWriteHead != null) _stats.domWriteStart();
738-
while (_domWriteHead != null) {
739-
try {
740-
_domWriteHead.fn();
741-
} catch (e, s) {
742-
_exceptionHandler(e, s);
743-
}
744-
_domWriteHead = _domWriteHead._next;
745-
if (_domWriteHead == null) _stats.domWriteEnd();
867+
if (_scheduledDomWrite) {
868+
_stats.domWriteStart();
869+
_runDomWrites();
870+
_stats.domWriteEnd();
746871
}
747-
_domWriteTail = null;
748872
if (runObservers) {
749873
runObservers = false;
750874
readOnlyGroup.detectChanges(exceptionHandler:_exceptionHandler,
751875
fieldStopwatch: _scopeStats.fieldStopwatch,
752876
evalStopwatch: _scopeStats.evalStopwatch,
753877
processStopwatch: _scopeStats.processStopwatch);
754878
}
755-
if (_domReadHead != null) _stats.domReadStart();
756-
while (_domReadHead != null) {
757-
try {
758-
_domReadHead.fn();
759-
} catch (e, s) {
760-
_exceptionHandler(e, s);
761-
}
762-
_domReadHead = _domReadHead._next;
763-
if (_domReadHead == null) _stats.domReadEnd();
879+
if (_scheduledDomRead) {
880+
_stats.domReadStart();
881+
_runDomReads();
882+
_stats.domReadEnd();
764883
}
765-
_domReadTail = null;
766884
_runAsyncFns();
767-
} while (_domWriteHead != null || _domReadHead != null || _runAsyncHead != null);
885+
} while (_scheduledDomWrite || _scheduledDomRead || _runAsyncHead != null);
768886
_stats.flushEnd();
769887
assert((() {
770888
_stats.flushAssertStart();
@@ -822,24 +940,6 @@ class RootScope extends Scope {
822940
return count;
823941
}
824942

825-
void domWrite(fn()) {
826-
var chain = new _FunctionChain(fn);
827-
if (_domWriteHead == null) {
828-
_domWriteHead = _domWriteTail = chain;
829-
} else {
830-
_domWriteTail = _domWriteTail._next = chain;
831-
}
832-
}
833-
834-
void domRead(fn()) {
835-
var chain = new _FunctionChain(fn);
836-
if (_domReadHead == null) {
837-
_domReadHead = _domReadTail = chain;
838-
} else {
839-
_domReadTail = _domReadTail._next = chain;
840-
}
841-
}
842-
843943
void destroy() {}
844944

845945
void _transitionState(String from, String to) {

lib/core_dom/view.dart

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ class View {
2121
void registerEvent(String eventName) {
2222
eventHandler.register(eventName);
2323
}
24+
25+
void domWrite(fn()) {
26+
scope.domWrite(fn);
27+
}
28+
29+
void domRead(fn()) {
30+
scope.domRead(fn);
31+
}
2432
}
2533

2634
/**

test/core/scope_spec.dart

+63-1
Original file line numberDiff line numberDiff line change
@@ -1558,7 +1558,7 @@ void main() {
15581558
module.bind(ExceptionHandler, toImplementation: LoggingExceptionHandler);
15591559
});
15601560

1561-
it(r'should run writes before reads', (RootScope rootScope, Logger logger, ExceptionHandler e) {
1561+
it('should run writes before reads', (RootScope rootScope, Logger logger, ExceptionHandler e) {
15621562
LoggingExceptionHandler exceptionHandler = e as LoggingExceptionHandler;
15631563
rootScope.domWrite(() {
15641564
logger('write1');
@@ -1579,6 +1579,68 @@ void main() {
15791579
expect(exceptionHandler.errors[0].error).toEqual('write1');
15801580
expect(exceptionHandler.errors[1].error).toEqual('read1');
15811581
});
1582+
1583+
it("should run writes of child scopes first", (RootScope rootScope, Logger logger) {
1584+
final childScope = rootScope.createChild({});
1585+
childScope.domWrite(() {
1586+
logger("child1");
1587+
});
1588+
rootScope.domWrite(() {
1589+
logger("root");
1590+
});
1591+
childScope.domWrite(() {
1592+
logger("child2");
1593+
});
1594+
1595+
rootScope.flush();
1596+
1597+
expect(logger).toEqual(['child1', 'child2', 'root']);
1598+
});
1599+
1600+
it("should run reads of child scopes first", (RootScope rootScope, Logger logger) {
1601+
final childScope = rootScope.createChild({});
1602+
childScope.domRead(() {
1603+
logger("child1");
1604+
});
1605+
rootScope.domRead(() {
1606+
logger("root");
1607+
});
1608+
childScope.domRead(() {
1609+
logger("child2");
1610+
});
1611+
1612+
rootScope.flush();
1613+
1614+
expect(logger).toEqual(['child1', 'child2', 'root']);
1615+
});
1616+
1617+
it("should throw when calling dom write on a child scope",
1618+
(RootScope rootScope, Logger logger, ExceptionHandler e) {
1619+
LoggingExceptionHandler handler = e as LoggingExceptionHandler;
1620+
1621+
final childScope = rootScope.createChild({});
1622+
rootScope.domWrite(() {
1623+
childScope.domWrite((){});
1624+
});
1625+
1626+
rootScope.flush();
1627+
1628+
expect(handler.errors[0].error).toBeAnInstanceOf(ConcurrentScopeModificationException);
1629+
});
1630+
1631+
it("should throw when calling dom read on a child scope",
1632+
(RootScope rootScope, Logger logger, ExceptionHandler e) {
1633+
LoggingExceptionHandler handler = e as LoggingExceptionHandler;
1634+
1635+
final childScope = rootScope.createChild({});
1636+
rootScope.domRead(() {
1637+
childScope.domRead((){});
1638+
});
1639+
1640+
rootScope.flush();
1641+
1642+
expect(handler.errors[0].error).toBeAnInstanceOf(ConcurrentScopeModificationException);
1643+
});
15821644
});
15831645

15841646
describe('exceptionHander', () {

0 commit comments

Comments
 (0)