• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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