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_node; | |
6 | |
7 import 'dart:async'; | |
8 | |
9 import 'asset.dart'; | |
10 import 'asset_id.dart'; | |
11 import 'errors.dart'; | |
12 import 'transform_node.dart'; | |
13 import 'utils.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 |