1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:collection'; 7 8import 'package:meta/meta.dart'; 9 10/// Generates an [AppContext] value. 11/// 12/// Generators are allowed to return `null`, in which case the context will 13/// store the `null` value as the value for that type. 14typedef Generator = dynamic Function(); 15 16/// An exception thrown by [AppContext] when you try to get a [Type] value from 17/// the context, and the instantiation of the value results in a dependency 18/// cycle. 19class ContextDependencyCycleException implements Exception { 20 ContextDependencyCycleException._(this.cycle); 21 22 /// The dependency cycle (last item depends on first item). 23 final List<Type> cycle; 24 25 @override 26 String toString() => 'Dependency cycle detected: ${cycle.join(' -> ')}'; 27} 28 29/// The current [AppContext], as determined by the [Zone] hierarchy. 30/// 31/// This will be the first context found as we scan up the zone hierarchy, or 32/// the "root" context if a context cannot be found in the hierarchy. The root 33/// context will not have any values associated with it. 34/// 35/// This is guaranteed to never return `null`. 36AppContext get context => Zone.current[_Key.key] as AppContext ?? AppContext._root; 37 38/// A lookup table (mapping types to values) and an implied scope, in which 39/// code is run. 40/// 41/// [AppContext] is used to define a singleton injection context for code that 42/// is run within it. Each time you call [run], a child context (and a new 43/// scope) is created. 44/// 45/// Child contexts are created and run using zones. To read more about how 46/// zones work, see https://api.dart.dev/stable/dart-async/Zone-class.html. 47class AppContext { 48 AppContext._( 49 this._parent, 50 this.name, [ 51 this._overrides = const <Type, Generator>{}, 52 this._fallbacks = const <Type, Generator>{}, 53 ]); 54 55 final String name; 56 final AppContext _parent; 57 final Map<Type, Generator> _overrides; 58 final Map<Type, Generator> _fallbacks; 59 final Map<Type, dynamic> _values = <Type, dynamic>{}; 60 61 List<Type> _reentrantChecks; 62 63 /// Bootstrap context. 64 static final AppContext _root = AppContext._(null, 'ROOT'); 65 66 dynamic _boxNull(dynamic value) => value ?? _BoxedNull.instance; 67 68 dynamic _unboxNull(dynamic value) => value == _BoxedNull.instance ? null : value; 69 70 /// Returns the generated value for [type] if such a generator exists. 71 /// 72 /// If [generators] does not contain a mapping for the specified [type], this 73 /// returns `null`. 74 /// 75 /// If a generator existed and generated a `null` value, this will return a 76 /// boxed value indicating null. 77 /// 78 /// If a value for [type] has already been generated by this context, the 79 /// existing value will be returned, and the generator will not be invoked. 80 /// 81 /// If the generator ends up triggering a reentrant call, it signals a 82 /// dependency cycle, and a [ContextDependencyCycleException] will be thrown. 83 dynamic _generateIfNecessary(Type type, Map<Type, Generator> generators) { 84 if (!generators.containsKey(type)) 85 return null; 86 87 return _values.putIfAbsent(type, () { 88 _reentrantChecks ??= <Type>[]; 89 90 final int index = _reentrantChecks.indexOf(type); 91 if (index >= 0) { 92 // We're already in the process of trying to generate this type. 93 throw ContextDependencyCycleException._( 94 UnmodifiableListView<Type>(_reentrantChecks.sublist(index))); 95 } 96 97 _reentrantChecks.add(type); 98 try { 99 return _boxNull(generators[type]()); 100 } finally { 101 _reentrantChecks.removeLast(); 102 if (_reentrantChecks.isEmpty) 103 _reentrantChecks = null; 104 } 105 }); 106 } 107 108 /// Gets the value associated with the specified [type], or `null` if no 109 /// such value has been associated. 110 T get<T>() { 111 dynamic value = _generateIfNecessary(T, _overrides); 112 if (value == null && _parent != null) { 113 value = _parent.get<T>(); 114 } 115 return _unboxNull(value ?? _generateIfNecessary(T, _fallbacks)) as T; 116 } 117 118 /// Gets the value associated with the specified [type], or `null` if no 119 /// such value has been associated. 120 @Deprecated('use get<T> instead for type safety.') 121 Object operator [](Type type) { 122 dynamic value = _generateIfNecessary(type, _overrides); 123 if (value == null && _parent != null) 124 value = _parent[type]; 125 return _unboxNull(value ?? _generateIfNecessary(type, _fallbacks)); 126 } 127 128 /// Runs [body] in a child context and returns the value returned by [body]. 129 /// 130 /// If [overrides] is specified, the child context will return corresponding 131 /// values when consulted via [operator[]]. 132 /// 133 /// If [fallbacks] is specified, the child context will return corresponding 134 /// values when consulted via [operator[]] only if its parent context didn't 135 /// return such a value. 136 /// 137 /// If [name] is specified, the child context will be assigned the given 138 /// name. This is useful for debugging purposes and is analogous to naming a 139 /// thread in Java. 140 Future<V> run<V>({ 141 @required FutureOr<V> body(), 142 String name, 143 Map<Type, Generator> overrides, 144 Map<Type, Generator> fallbacks, 145 ZoneSpecification zoneSpecification, 146 }) async { 147 final AppContext child = AppContext._( 148 this, 149 name, 150 Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}), 151 Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}), 152 ); 153 return await runZoned<Future<V>>( 154 () async => await body(), 155 zoneValues: <_Key, AppContext>{_Key.key: child}, 156 zoneSpecification: zoneSpecification, 157 ); 158 } 159 160 @override 161 String toString() { 162 final StringBuffer buf = StringBuffer(); 163 String indent = ''; 164 AppContext ctx = this; 165 while (ctx != null) { 166 buf.write('AppContext'); 167 if (ctx.name != null) 168 buf.write('[${ctx.name}]'); 169 if (ctx._overrides.isNotEmpty) 170 buf.write('\n$indent overrides: [${ctx._overrides.keys.join(', ')}]'); 171 if (ctx._fallbacks.isNotEmpty) 172 buf.write('\n$indent fallbacks: [${ctx._fallbacks.keys.join(', ')}]'); 173 if (ctx._parent != null) 174 buf.write('\n$indent parent: '); 175 ctx = ctx._parent; 176 indent += ' '; 177 } 178 return buf.toString(); 179 } 180} 181 182/// Private key used to store the [AppContext] in the [Zone]. 183class _Key { 184 const _Key(); 185 186 static const _Key key = _Key(); 187 188 @override 189 String toString() => 'context'; 190} 191 192/// Private object that denotes a generated `null` value. 193class _BoxedNull { 194 const _BoxedNull(); 195 196 static const _BoxedNull instance = _BoxedNull(); 197} 198