OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library barback.asset_node; | 5 library barback.asset_node; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 | 8 |
9 import 'asset.dart'; | 9 import 'asset.dart'; |
10 import 'asset_id.dart'; | 10 import 'asset_id.dart'; |
| 11 import 'errors.dart'; |
11 import 'phase.dart'; | 12 import 'phase.dart'; |
12 import 'transform_node.dart'; | 13 import 'transform_node.dart'; |
13 | 14 |
14 /// Describes an asset and its relationship to the build dependency graph. | 15 /// Describes the current state of an asset as part of a transformation graph. |
15 /// | 16 /// |
16 /// Keeps a cache of the last asset that was built for this node (i.e. for this | 17 /// An asset node can be in one of three states (see [AssetState]). It provides |
17 /// node's ID and phase) and tracks which transforms depend on it. | 18 /// an [onStateChange] stream that emits an event whenever it changes state. |
| 19 /// |
| 20 /// Asset nodes are controlled using [AssetNodeController]s. |
18 class AssetNode { | 21 class AssetNode { |
19 Asset asset; | 22 /// The id of the asset that this node represents. |
| 23 final AssetId id; |
20 | 24 |
21 /// The [TransformNode]s that consume this node's asset as an input. | 25 /// The current state of the asset node. |
22 final consumers = new Set<TransformNode>(); | 26 AssetState get state => _state; |
| 27 AssetState _state; |
23 | 28 |
24 AssetId get id => asset.id; | 29 /// The concrete asset that this node represents. |
| 30 /// |
| 31 /// This is null unless [state] is [AssetState.AVAILABLE]. |
| 32 Asset get asset => _asset; |
| 33 Asset _asset; |
25 | 34 |
26 AssetNode(this.asset); | 35 /// A broadcast stream that emits an event whenever the node changes state. |
| 36 /// |
| 37 /// This stream is synchronous to ensure that when a source asset is modified |
| 38 /// or removed, the appropriate portion of the asset graph is dirtied before |
| 39 /// any [Barback.getAssetById] calls emit newly-incorrect values. |
| 40 Stream<AssetState> get onStateChange => _stateChangeController.stream; |
27 | 41 |
28 /// Updates this node's generated asset value and marks all transforms that | 42 /// This is synchronous so that a source being updated will always be |
29 /// use this as dirty. | 43 /// propagated through the build graph before anything that depends on it is |
30 void updateAsset(Asset asset) { | 44 /// requested. |
31 // Cannot update an asset to one with a different ID. | 45 final _stateChangeController = |
32 assert(id == asset.id); | 46 new StreamController<AssetState>.broadcast(sync: true); |
33 | 47 |
34 this.asset = asset; | 48 /// Returns a Future that completes when the node's asset is available. |
35 consumers.forEach((consumer) => consumer.dirty()); | 49 /// |
| 50 /// If the asset is currently available, this completes synchronously to |
| 51 /// ensure that the asset is still available in the [Future.then] callback. |
| 52 /// |
| 53 /// If the asset is removed before becoming available, this will throw an |
| 54 /// [AssetNotFoundException]. |
| 55 Future<Asset> get whenAvailable { |
| 56 return _waitForState((state) => state.isAvailable || state.isRemoved) |
| 57 .then((state) { |
| 58 if (state.isRemoved) throw new AssetNotFoundException(id); |
| 59 return asset; |
| 60 }); |
| 61 } |
| 62 |
| 63 /// Returns a Future that completes when the node's asset is removed. |
| 64 /// |
| 65 /// If the asset is already removed when this is called, it completes |
| 66 /// synchronously. |
| 67 Future get whenRemoved => _waitForState((state) => state.isRemoved); |
| 68 |
| 69 /// Runs [callback] repeatedly until the node's asset has maintained the same |
| 70 /// value for the duration. |
| 71 /// |
| 72 /// This will run [callback] as soon as the asset is available (synchronously |
| 73 /// if it's available immediately). If the [state] changes at all while |
| 74 /// waiting for the Future returned by [callback] to complete, it will be |
| 75 /// re-run as soon as it completes and the asset is available again. This will |
| 76 /// continue until [state] doesn't change at all. |
| 77 /// |
| 78 /// If this asset is removed, this will throw an [AssetNotFoundException] as |
| 79 /// soon as [callback]'s Future is finished running. |
| 80 Future tryUntilStable(Future callback(Asset asset)) { |
| 81 return whenAvailable.then((asset) { |
| 82 var modifiedDuringCallback = false; |
| 83 var subscription; |
| 84 subscription = onStateChange.listen((_) { |
| 85 modifiedDuringCallback = true; |
| 86 subscription.cancel(); |
| 87 }); |
| 88 |
| 89 return callback(asset).then((result) { |
| 90 subscription.cancel(); |
| 91 |
| 92 // If the asset was modified at all while running the callback, the |
| 93 // result was invalid and we should try again. |
| 94 if (modifiedDuringCallback) return tryUntilStable(callback); |
| 95 return result; |
| 96 }); |
| 97 }); |
| 98 } |
| 99 |
| 100 /// Returns a Future that completes as soon as the node is in a state that |
| 101 /// matches [test]. |
| 102 /// |
| 103 /// The Future completes synchronously if this is already in such a state. |
| 104 Future<AssetState> _waitForState(bool test(AssetState state)) { |
| 105 if (test(state)) return new Future.sync(() => state); |
| 106 return onStateChange.firstWhere(test); |
| 107 } |
| 108 |
| 109 AssetNode._(this.id) |
| 110 : _state = AssetState.DIRTY; |
| 111 |
| 112 AssetNode._available(Asset asset) |
| 113 : id = asset.id, |
| 114 _asset = asset, |
| 115 _state = AssetState.AVAILABLE; |
| 116 } |
| 117 |
| 118 /// The controller for an [AssetNode]. |
| 119 /// |
| 120 /// This controls which state the node is in. |
| 121 class AssetNodeController { |
| 122 final AssetNode node; |
| 123 |
| 124 /// Creates a controller for a dirty node. |
| 125 AssetNodeController(AssetId id) |
| 126 : node = new AssetNode._(id); |
| 127 |
| 128 /// Creates a controller for an available node with the given concrete |
| 129 /// [asset]. |
| 130 AssetNodeController.available(Asset asset) |
| 131 : node = new AssetNode._available(asset); |
| 132 |
| 133 /// Marks the node as [AssetState.DIRTY]. |
| 134 void setDirty() { |
| 135 assert(node._state != AssetState.REMOVED); |
| 136 node._state = AssetState.DIRTY; |
| 137 node._asset = null; |
| 138 node._stateChangeController.add(AssetState.DIRTY); |
| 139 } |
| 140 |
| 141 /// Marks the node as [AssetState.REMOVED]. |
| 142 /// |
| 143 /// Once a node is marked as removed, it can't be marked as any other state. |
| 144 /// If a new asset is created with the same id, it will get a new node. |
| 145 void setRemoved() { |
| 146 assert(node._state != AssetState.REMOVED); |
| 147 node._state = AssetState.REMOVED; |
| 148 node._asset = null; |
| 149 node._stateChangeController.add(AssetState.REMOVED); |
| 150 } |
| 151 |
| 152 /// Marks the node as [AssetState.AVAILABLE] with the given concrete [asset]. |
| 153 /// |
| 154 /// It's an error to mark an already-available node as available. It should be |
| 155 /// marked as dirty first. |
| 156 void setAvailable(Asset asset) { |
| 157 assert(asset.id == node.id); |
| 158 assert(node._state != AssetState.REMOVED); |
| 159 assert(node._state != AssetState.AVAILABLE); |
| 160 node._state = AssetState.AVAILABLE; |
| 161 node._asset = asset; |
| 162 node._stateChangeController.add(AssetState.AVAILABLE); |
36 } | 163 } |
37 } | 164 } |
| 165 |
| 166 // TODO(nweiz): add an error state. |
| 167 /// An enum of states that an [AssetNode] can be in. |
| 168 class AssetState { |
| 169 /// The node has a concrete asset loaded, available, and up-to-date. The asset |
| 170 /// is accessible via [AssetNode.asset]. An asset can only be marked available |
| 171 /// again from the [AssetState.DIRTY] state. |
| 172 static final AVAILABLE = const AssetState._("available"); |
| 173 |
| 174 /// The asset is no longer available, possibly for good. A removed asset will |
| 175 /// never enter another state. |
| 176 static final REMOVED = const AssetState._("removed"); |
| 177 |
| 178 /// The asset will exist in the future (unless it's removed), but the concrete |
| 179 /// asset is not yet available. |
| 180 static final DIRTY = const AssetState._("dirty"); |
| 181 |
| 182 /// Whether this state is [AssetState.AVAILABLE]. |
| 183 bool get isAvailable => this == AssetState.AVAILABLE; |
| 184 |
| 185 /// Whether this state is [AssetState.REMOVED]. |
| 186 bool get isRemoved => this == AssetState.REMOVED; |
| 187 |
| 188 /// Whether this state is [AssetState.DIRTY]. |
| 189 bool get isDirty => this == AssetState.DIRTY; |
| 190 |
| 191 final String name; |
| 192 |
| 193 const AssetState._(this.name); |
| 194 |
| 195 String toString() => name; |
| 196 } |
OLD | NEW |