OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library barback.asset.asset_node; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import '../errors.dart'; |
| 10 import '../graph/transform_node.dart'; |
| 11 import '../utils.dart'; |
| 12 import 'asset.dart'; |
| 13 import 'asset_id.dart'; |
| 14 |
| 15 /// Describes the current state of an asset as part of a transformation graph. |
| 16 /// |
| 17 /// An asset node can be in one of three states (see [AssetState]). It provides |
| 18 /// an [onStateChange] stream that emits an event whenever it changes state. |
| 19 /// |
| 20 /// Asset nodes are controlled using [AssetNodeController]s. |
| 21 class AssetNode { |
| 22 /// The id of the asset that this node represents. |
| 23 final AssetId id; |
| 24 |
| 25 /// The [AssetNode] from which [this] is forwarded. |
| 26 /// |
| 27 /// For nodes that aren't forwarded, this will return [this]. Otherwise, it |
| 28 /// will return the first node in the forwarding chain. |
| 29 /// |
| 30 /// This is used to determine whether two nodes are forwarded from the same |
| 31 /// source. |
| 32 AssetNode get origin => _origin == null ? this : _origin; |
| 33 AssetNode _origin; |
| 34 |
| 35 /// The transform that created this asset node. |
| 36 /// |
| 37 /// This is `null` for source assets. It can change if the upstream transform |
| 38 /// that created this asset changes; this change will *not* cause an |
| 39 /// [onStateChange] event. |
| 40 TransformNode get transform => _transform; |
| 41 TransformNode _transform; |
| 42 |
| 43 /// The current state of the asset node. |
| 44 AssetState get state => _state; |
| 45 AssetState _state; |
| 46 |
| 47 /// The concrete asset that this node represents. |
| 48 /// |
| 49 /// This is null unless [state] is [AssetState.AVAILABLE]. |
| 50 Asset get asset => _asset; |
| 51 Asset _asset; |
| 52 |
| 53 /// The callback to be called to notify this asset node's creator that the |
| 54 /// concrete asset should be generated. |
| 55 /// |
| 56 /// This is null for non-lazy asset nodes (see [AssetNodeController.lazy]). |
| 57 /// Once this is called, it's set to null and [this] is no longer considered |
| 58 /// lazy. |
| 59 Function _lazyCallback; |
| 60 |
| 61 /// Whether this node is lazy, meaning that [force] must be called to |
| 62 /// guarantee that it will eventually become available. |
| 63 bool get isLazy => _lazyCallback != null || |
| 64 (_origin != null && _origin.isLazy); |
| 65 |
| 66 /// A broadcast stream that emits an event whenever the node changes state. |
| 67 /// |
| 68 /// This stream is synchronous to ensure that when a source asset is modified |
| 69 /// or removed, the appropriate portion of the asset graph is dirtied before |
| 70 /// any [Barback.getAssetById] calls emit newly-incorrect values. |
| 71 Stream<AssetState> get onStateChange => _stateChangeController.stream; |
| 72 |
| 73 /// This is synchronous so that a source being updated will always be |
| 74 /// propagated through the build graph before anything that depends on it is |
| 75 /// requested. |
| 76 final _stateChangeController = |
| 77 new StreamController<AssetState>.broadcast(sync: true); |
| 78 |
| 79 /// Calls [callback] when the node's asset is available. |
| 80 /// |
| 81 /// If the asset is currently available, this calls [callback] synchronously |
| 82 /// to ensure that the asset is still available. |
| 83 /// |
| 84 /// The return value of [callback] is piped to the returned Future. If the |
| 85 /// asset is removed before becoming available, the returned future will throw |
| 86 /// an [AssetNotFoundException]. |
| 87 Future whenAvailable(callback(Asset asset)) { |
| 88 return _waitForState((state) => state.isAvailable || state.isRemoved, |
| 89 (state) { |
| 90 if (state.isRemoved) throw new AssetNotFoundException(id); |
| 91 return callback(asset); |
| 92 }); |
| 93 } |
| 94 |
| 95 /// Calls [callback] when the node's asset is removed. |
| 96 /// |
| 97 /// If the asset is already removed when this is called, it calls [callback] |
| 98 /// synchronously. |
| 99 /// |
| 100 /// The return value of [callback] is piped to the returned Future. |
| 101 Future whenRemoved(callback()) => |
| 102 _waitForState((state) => state.isRemoved, (_) => callback()); |
| 103 |
| 104 /// Returns a [Future] that completes when [state] changes from its current |
| 105 /// value to any other value. |
| 106 /// |
| 107 /// The returned [Future] will contain the new state. |
| 108 Future<AssetState> whenStateChanges() { |
| 109 var startState = state; |
| 110 return _waitForState((state) => state != startState, (state) => state); |
| 111 } |
| 112 |
| 113 /// Calls [callback] as soon as the node is in a state that matches [test]. |
| 114 /// |
| 115 /// [callback] is called synchronously if this is already in such a state. |
| 116 /// |
| 117 /// The return value of [callback] is piped to the returned Future. |
| 118 Future _waitForState(bool test(AssetState state), |
| 119 callback(AssetState state)) { |
| 120 if (test(state)) return syncFuture(() => callback(state)); |
| 121 return onStateChange.firstWhere(test).then((_) => callback(state)); |
| 122 } |
| 123 |
| 124 AssetNode._(this.id, this._transform, this._origin) |
| 125 : _state = AssetState.RUNNING; |
| 126 |
| 127 AssetNode._available(Asset asset, this._transform, this._origin) |
| 128 : id = asset.id, |
| 129 _asset = asset, |
| 130 _state = AssetState.AVAILABLE; |
| 131 |
| 132 AssetNode._lazy(this.id, this._transform, this._origin, this._lazyCallback) |
| 133 : _state = AssetState.RUNNING; |
| 134 |
| 135 /// If [this] is lazy, force it to generate a concrete asset; otherwise, do |
| 136 /// nothing. |
| 137 /// |
| 138 /// See [AssetNodeController.lazy]. |
| 139 void force() { |
| 140 if (_origin != null) { |
| 141 _origin.force(); |
| 142 } else if (_lazyCallback != null) { |
| 143 _lazyCallback(); |
| 144 _lazyCallback = null; |
| 145 } |
| 146 } |
| 147 |
| 148 String toString() => "${isLazy ? 'lazy' : state} asset $id"; |
| 149 } |
| 150 |
| 151 /// The controller for an [AssetNode]. |
| 152 /// |
| 153 /// This controls which state the node is in. |
| 154 class AssetNodeController { |
| 155 final AssetNode node; |
| 156 |
| 157 /// Creates a controller for a dirty node. |
| 158 AssetNodeController(AssetId id, [TransformNode transform]) |
| 159 : node = new AssetNode._(id, transform, null); |
| 160 |
| 161 /// Creates a controller for an available node with the given concrete |
| 162 /// [asset]. |
| 163 AssetNodeController.available(Asset asset, [TransformNode transform]) |
| 164 : node = new AssetNode._available(asset, transform, null); |
| 165 |
| 166 /// Creates a controller for a lazy node. |
| 167 /// |
| 168 /// For the most part, this node works like any other dirty node. However, the |
| 169 /// owner of its controller isn't expected to do the work to make it available |
| 170 /// as soon as possible like they would for a non-lazy node. Instead, when its |
| 171 /// value is needed, [callback] will fire to indicate that it should be made |
| 172 /// available as soon as possible. |
| 173 /// |
| 174 /// [callback] is guaranteed to only fire once. |
| 175 AssetNodeController.lazy(AssetId id, void callback(), |
| 176 [TransformNode transform]) |
| 177 : node = new AssetNode._lazy(id, transform, null, callback); |
| 178 |
| 179 /// Creates a controller for a node whose initial state matches the current |
| 180 /// state of [node]. |
| 181 /// |
| 182 /// [AssetNode.origin] of the returned node will automatically be set to |
| 183 /// `node.origin`. |
| 184 /// |
| 185 /// If [node] is lazy, the returned node will also be lazy. |
| 186 AssetNodeController.from(AssetNode node) |
| 187 : node = new AssetNode._(node.id, node.transform, node.origin) { |
| 188 if (node.state.isAvailable) { |
| 189 setAvailable(node.asset); |
| 190 } else if (node.state.isRemoved) { |
| 191 setRemoved(); |
| 192 } |
| 193 } |
| 194 |
| 195 /// Marks the node as [AssetState.RUNNING]. |
| 196 void setDirty() { |
| 197 assert(node._state != AssetState.REMOVED); |
| 198 node._asset = null; |
| 199 node._lazyCallback = null; |
| 200 |
| 201 // Don't re-emit a dirty event to avoid cases where we try to dispatch an |
| 202 // event while handling another event (e.g. an output is marked lazy, which |
| 203 // causes it to be forced, which causes it to be marked dirty). |
| 204 if (node._state.isDirty) return; |
| 205 node._state = AssetState.RUNNING; |
| 206 node._stateChangeController.add(AssetState.RUNNING); |
| 207 } |
| 208 |
| 209 /// Marks the node as [AssetState.REMOVED]. |
| 210 /// |
| 211 /// Once a node is marked as removed, it can't be marked as any other state. |
| 212 /// If a new asset is created with the same id, it will get a new node. |
| 213 void setRemoved() { |
| 214 assert(node._state != AssetState.REMOVED); |
| 215 node._state = AssetState.REMOVED; |
| 216 node._asset = null; |
| 217 node._lazyCallback = null; |
| 218 node._stateChangeController.add(AssetState.REMOVED); |
| 219 } |
| 220 |
| 221 /// Marks the node as [AssetState.AVAILABLE] with the given concrete [asset]. |
| 222 /// |
| 223 /// It's an error to mark an already-available node as available. It should be |
| 224 /// marked as dirty first. |
| 225 void setAvailable(Asset asset) { |
| 226 assert(asset.id == node.id); |
| 227 assert(node._state != AssetState.REMOVED); |
| 228 assert(node._state != AssetState.AVAILABLE); |
| 229 node._state = AssetState.AVAILABLE; |
| 230 node._asset = asset; |
| 231 node._lazyCallback = null; |
| 232 node._stateChangeController.add(AssetState.AVAILABLE); |
| 233 } |
| 234 |
| 235 /// Marks the node as [AssetState.RUNNING] and lazy. |
| 236 /// |
| 237 /// Lazy nodes aren't expected to have their values generated until needed. |
| 238 /// Once it's necessary, [callback] will be called. [callback] is guaranteed |
| 239 /// to be called only once. |
| 240 /// |
| 241 /// See also [AssetNodeController.lazy]. |
| 242 void setLazy(void callback()) { |
| 243 assert(node._state != AssetState.REMOVED); |
| 244 node._state = AssetState.RUNNING; |
| 245 node._asset = null; |
| 246 node._lazyCallback = callback; |
| 247 node._stateChangeController.add(AssetState.RUNNING); |
| 248 } |
| 249 |
| 250 String toString() => "controller for $node"; |
| 251 } |
| 252 |
| 253 // TODO(nweiz): add an error state. |
| 254 /// An enum of states that an [AssetNode] can be in. |
| 255 class AssetState { |
| 256 /// The node has a concrete asset loaded, available, and up-to-date. The asset |
| 257 /// is accessible via [AssetNode.asset]. An asset can only be marked available |
| 258 /// again from the [AssetState.RUNNING] state. |
| 259 static final AVAILABLE = const AssetState._("available"); |
| 260 |
| 261 /// The asset is no longer available, possibly for good. A removed asset will |
| 262 /// never enter another state. |
| 263 static final REMOVED = const AssetState._("removed"); |
| 264 |
| 265 /// The asset will exist in the future (unless it's removed), but the concrete |
| 266 /// asset is not yet available. |
| 267 static final RUNNING = const AssetState._("dirty"); |
| 268 |
| 269 /// Whether this state is [AssetState.AVAILABLE]. |
| 270 bool get isAvailable => this == AssetState.AVAILABLE; |
| 271 |
| 272 /// Whether this state is [AssetState.REMOVED]. |
| 273 bool get isRemoved => this == AssetState.REMOVED; |
| 274 |
| 275 /// Whether this state is [AssetState.RUNNING]. |
| 276 bool get isDirty => this == AssetState.RUNNING; |
| 277 |
| 278 final String name; |
| 279 |
| 280 const AssetState._(this.name); |
| 281 |
| 282 String toString() => name; |
| 283 } |
OLD | NEW |