| 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.transform_node; | 5 library barback.transform_node; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 | 8 |
| 9 import 'package:source_maps/span.dart'; | 9 import 'package:source_maps/span.dart'; |
| 10 | 10 |
| (...skipping 24 matching lines...) Expand all Loading... |
| 35 | 35 |
| 36 /// The node for the primary asset this transform depends on. | 36 /// The node for the primary asset this transform depends on. |
| 37 final AssetNode primary; | 37 final AssetNode primary; |
| 38 | 38 |
| 39 /// A string describing the location of [this] in the transformer graph. | 39 /// A string describing the location of [this] in the transformer graph. |
| 40 final String _location; | 40 final String _location; |
| 41 | 41 |
| 42 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. | 42 /// The subscription to [primary]'s [AssetNode.onStateChange] stream. |
| 43 StreamSubscription _primarySubscription; | 43 StreamSubscription _primarySubscription; |
| 44 | 44 |
| 45 /// True if an input has been modified since the last time this transform | 45 // TODO(nweiz): Remove this and move isPrimary computation into TransformNode. |
| 46 /// began running. | 46 /// Whether the parent [PhaseInput] is currently computing whether its input |
| 47 bool get isDirty => _isDirty; | 47 /// is primary for [this]. |
| 48 var _isDirty = true; | 48 bool _pendingIsPrimary = false; |
| 49 |
| 50 /// Whether [this] is dirty and still has more processing to do. |
| 51 bool get isDirty => _pendingIsPrimary || _isApplying; |
| 52 |
| 53 /// Whether any input has become dirty since [_apply] last started running. |
| 54 var _hasBecomeDirty = false; |
| 55 |
| 56 /// Whether [_apply] is currently running. |
| 57 var _isApplying = false; |
| 49 | 58 |
| 50 /// Whether [transformer] is lazy and this transform has yet to be forced. | 59 /// Whether [transformer] is lazy and this transform has yet to be forced. |
| 51 bool _isLazy; | 60 bool _isLazy; |
| 52 | 61 |
| 53 /// The subscriptions to each input's [AssetNode.onStateChange] stream. | 62 /// The subscriptions to each input's [AssetNode.onStateChange] stream. |
| 54 var _inputSubscriptions = new Map<AssetId, StreamSubscription>(); | 63 var _inputSubscriptions = new Map<AssetId, StreamSubscription>(); |
| 55 | 64 |
| 56 /// The controllers for the asset nodes emitted by this node. | 65 /// The controllers for the asset nodes emitted by this node. |
| 57 var _outputControllers = new Map<AssetId, AssetNodeController>(); | 66 var _outputControllers = new Map<AssetId, AssetNodeController>(); |
| 58 | 67 |
| 59 /// A stream that emits an event whenever this transform becomes dirty and | 68 /// A stream that emits an event whenever [this] is no longer dirty. |
| 60 /// needs to be re-run. | |
| 61 /// | 69 /// |
| 62 /// This may emit events when the transform was already dirty or while | 70 /// This is synchronous in order to guarantee that it will emit an event as |
| 63 /// processing transforms. Events are emitted synchronously to ensure that the | 71 /// soon as [isDirty] flips from `true` to `false`. |
| 64 /// dirty state is thoroughly propagated as soon as any assets are changed. | 72 Stream get onDone => _onDoneController.stream; |
| 65 Stream get onDirty => _onDirtyController.stream; | 73 final _onDoneController = new StreamController.broadcast(sync: true); |
| 66 final _onDirtyController = new StreamController.broadcast(sync: true); | 74 |
| 75 /// A stream that emits any new assets emitted by [this]. |
| 76 /// |
| 77 /// Assets are emitted synchronously to ensure that any changes are thoroughly |
| 78 /// propagated as soon as they occur. |
| 79 Stream<AssetNode> get onAsset => _onAssetController.stream; |
| 80 final _onAssetController = new StreamController<AssetNode>(sync: true); |
| 67 | 81 |
| 68 /// A stream that emits an event whenever this transform logs an entry. | 82 /// A stream that emits an event whenever this transform logs an entry. |
| 69 /// | 83 /// |
| 70 /// This is synchronous because error logs can cause the transform to fail, so | 84 /// This is synchronous because error logs can cause the transform to fail, so |
| 71 /// we need to ensure that their processing isn't delayed until after the | 85 /// we need to ensure that their processing isn't delayed until after the |
| 72 /// transform or build has finished. | 86 /// transform or build has finished. |
| 73 Stream<LogEntry> get onLog => _onLogController.stream; | 87 Stream<LogEntry> get onLog => _onLogController.stream; |
| 74 final _onLogController = new StreamController<LogEntry>.broadcast(sync: true); | 88 final _onLogController = new StreamController<LogEntry>.broadcast(sync: true); |
| 75 | 89 |
| 76 TransformNode(this.phase, Transformer transformer, this.primary, | 90 TransformNode(this.phase, Transformer transformer, this.primary, |
| 77 this._location) | 91 this._location) |
| 78 : transformer = transformer, | 92 : transformer = transformer, |
| 79 _isLazy = transformer is LazyTransformer { | 93 _isLazy = transformer is LazyTransformer { |
| 80 _primarySubscription = primary.onStateChange.listen((state) { | 94 _primarySubscription = primary.onStateChange.listen((state) { |
| 81 if (state.isRemoved) { | 95 if (state.isRemoved) { |
| 82 remove(); | 96 remove(); |
| 83 } else { | 97 } else { |
| 98 if (state.isDirty) _pendingIsPrimary = true; |
| 84 _dirty(); | 99 _dirty(); |
| 85 } | 100 } |
| 86 }); | 101 }); |
| 102 |
| 103 _apply(); |
| 87 } | 104 } |
| 88 | 105 |
| 89 /// The [TransformInfo] describing this node. | 106 /// The [TransformInfo] describing this node. |
| 90 /// | 107 /// |
| 91 /// [TransformInfo] is the publicly-visible representation of a transform | 108 /// [TransformInfo] is the publicly-visible representation of a transform |
| 92 /// node. | 109 /// node. |
| 93 TransformInfo get info => new TransformInfo(transformer, primary.id); | 110 TransformInfo get info => new TransformInfo(transformer, primary.id); |
| 94 | 111 |
| 95 /// Marks this transform as removed. | 112 /// Marks this transform as removed. |
| 96 /// | 113 /// |
| 97 /// This causes all of the transform's outputs to be marked as removed as | 114 /// This causes all of the transform's outputs to be marked as removed as |
| 98 /// well. Normally this will be automatically done internally based on events | 115 /// well. Normally this will be automatically done internally based on events |
| 99 /// from the primary input, but it's possible for a transform to no longer be | 116 /// from the primary input, but it's possible for a transform to no longer be |
| 100 /// valid even if its primary input still exists. | 117 /// valid even if its primary input still exists. |
| 101 void remove() { | 118 void remove() { |
| 102 _isDirty = true; | 119 _hasBecomeDirty = false; |
| 103 _onDirtyController.close(); | 120 _onAssetController.close(); |
| 121 _onDoneController.close(); |
| 104 _primarySubscription.cancel(); | 122 _primarySubscription.cancel(); |
| 105 for (var subscription in _inputSubscriptions.values) { | 123 for (var subscription in _inputSubscriptions.values) { |
| 106 subscription.cancel(); | 124 subscription.cancel(); |
| 107 } | 125 } |
| 108 for (var controller in _outputControllers.values) { | 126 for (var controller in _outputControllers.values) { |
| 109 controller.setRemoved(); | 127 controller.setRemoved(); |
| 110 } | 128 } |
| 111 } | 129 } |
| 112 | 130 |
| 113 /// If [transformer] is lazy, ensures that its concrete outputs will be | 131 /// If [transformer] is lazy, ensures that its concrete outputs will be |
| 114 /// generated. | 132 /// generated. |
| 115 void force() { | 133 void force() { |
| 116 // TODO(nweiz): we might want to have a timeout after which, if the | 134 // TODO(nweiz): we might want to have a timeout after which, if the |
| 117 // transform's outputs have gone unused, we switch it back to lazy mode. | 135 // transform's outputs have gone unused, we switch it back to lazy mode. |
| 118 if (!_isLazy) return; | 136 if (!_isLazy) return; |
| 119 _isLazy = false; | 137 _isLazy = false; |
| 120 _dirty(); | 138 _dirty(); |
| 121 } | 139 } |
| 122 | 140 |
| 141 // TODO(nweiz): remove this and move isPrimary computation into TransformNode. |
| 142 /// Mark that the parent [PhaseInput] has determined that its input is indeed |
| 143 /// primary for [this]. |
| 144 void markPrimary() { |
| 145 if (!_pendingIsPrimary) return; |
| 146 _pendingIsPrimary = false; |
| 147 if (!_isApplying) _apply(); |
| 148 } |
| 149 |
| 123 /// Marks this transform as dirty. | 150 /// Marks this transform as dirty. |
| 124 /// | 151 /// |
| 125 /// This causes all of the transform's outputs to be marked as dirty as well. | 152 /// This causes all of the transform's outputs to be marked as dirty as well. |
| 126 void _dirty() { | 153 void _dirty() { |
| 127 _isDirty = true; | |
| 128 for (var controller in _outputControllers.values) { | 154 for (var controller in _outputControllers.values) { |
| 129 controller.setDirty(); | 155 controller.setDirty(); |
| 130 } | 156 } |
| 131 _onDirtyController.add(null); | 157 |
| 158 _hasBecomeDirty = true; |
| 159 if (!_isApplying && !_pendingIsPrimary) _apply(); |
| 132 } | 160 } |
| 133 | 161 |
| 134 /// Applies this transform. | 162 /// Applies this transform. |
| 135 /// | 163 void _apply() { |
| 136 /// Returns a set of asset nodes representing the outputs from this transform | 164 assert(!_onAssetController.isClosed); |
| 137 /// that weren't emitted last time it was run. | |
| 138 Future<Set<AssetNode>> apply() { | |
| 139 assert(!_onDirtyController.isClosed); | |
| 140 | 165 |
| 141 // Clear all the old input subscriptions. If an input is re-used, we'll | 166 // Clear all the old input subscriptions. If an input is re-used, we'll |
| 142 // re-subscribe. | 167 // re-subscribe. |
| 143 for (var subscription in _inputSubscriptions.values) { | 168 for (var subscription in _inputSubscriptions.values) { |
| 144 subscription.cancel(); | 169 subscription.cancel(); |
| 145 } | 170 } |
| 146 _inputSubscriptions.clear(); | 171 _inputSubscriptions.clear(); |
| 147 | 172 |
| 148 _isDirty = false; | 173 _isApplying = true; |
| 174 primary.whenAvailable((_) { |
| 175 _hasBecomeDirty = false; |
| 149 | 176 |
| 150 return syncFuture(() { | |
| 151 // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a | 177 // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a |
| 152 // [LazyTransformer], we can get some mileage out of doing a declarative | 178 // [LazyTransformer], we can get some mileage out of doing a declarative |
| 153 // first so we know how to hook up the assets. | 179 // first so we know how to hook up the assets. |
| 154 if (_isLazy) return _declareLazy(); | 180 if (_isLazy) return _declareLazy(); |
| 155 return _applyImmediate(); | 181 return _applyImmediate(); |
| 156 }).catchError((error, stackTrace) { | 182 }).catchError((error, stackTrace) { |
| 157 // If the transform became dirty while processing, ignore any errors from | 183 // If the transform became dirty while processing, ignore any errors from |
| 158 // it. | 184 // it. |
| 159 if (_isDirty) return new Set(); | 185 if (_hasBecomeDirty || _onAssetController.isClosed) return; |
| 160 | 186 |
| 161 if (error is! MissingInputException) { | 187 if (error is! MissingInputException) { |
| 162 error = new TransformerException(info, error, stackTrace); | 188 error = new TransformerException(info, error, stackTrace); |
| 163 } | 189 } |
| 164 | 190 |
| 165 // Catch all transformer errors and pipe them to the results stream. This | 191 // Catch all transformer errors and pipe them to the results stream. This |
| 166 // is so a broken transformer doesn't take down the whole graph. | 192 // is so a broken transformer doesn't take down the whole graph. |
| 167 phase.cascade.reportError(error); | 193 phase.cascade.reportError(error); |
| 168 | 194 |
| 169 return new Set(); | 195 // Remove all the previously-emitted assets. |
| 196 for (var controller in _outputControllers.values) { |
| 197 controller.setRemoved(); |
| 198 } |
| 199 _outputControllers.clear(); |
| 200 }).then((_) { |
| 201 if (_onAssetController.isClosed) return; |
| 202 |
| 203 _isApplying = false; |
| 204 if (_hasBecomeDirty) { |
| 205 // Re-apply the transform if it became dirty while applying. |
| 206 if (!_pendingIsPrimary) _apply(); |
| 207 } else { |
| 208 assert(!isDirty); |
| 209 // Otherwise, notify the parent nodes that it's no longer dirty. |
| 210 _onDoneController.add(null); |
| 211 } |
| 170 }); | 212 }); |
| 171 } | 213 } |
| 172 | 214 |
| 173 /// Gets the asset for an input [id]. | 215 /// Gets the asset for an input [id]. |
| 174 /// | 216 /// |
| 175 /// If an input with that ID cannot be found, throws an | 217 /// If an input with that ID cannot be found, throws an |
| 176 /// [AssetNotFoundException]. | 218 /// [AssetNotFoundException]. |
| 177 Future<Asset> getInput(AssetId id) { | 219 Future<Asset> getInput(AssetId id) { |
| 178 return phase.getInput(id).then((node) { | 220 return phase.getInput(id).then((node) { |
| 179 // Throw if the input isn't found. This ensures the transformer's apply | 221 // Throw if the input isn't found. This ensures the transformer's apply |
| (...skipping 12 matching lines...) Expand all Loading... |
| 192 if (error is! AssetNotFoundException || error.id != id) throw error; | 234 if (error is! AssetNotFoundException || error.id != id) throw error; |
| 193 // If the node was removed before it could be loaded, treat it as though | 235 // If the node was removed before it could be loaded, treat it as though |
| 194 // it never existed and throw a MissingInputException. | 236 // it never existed and throw a MissingInputException. |
| 195 throw new MissingInputException(info, id); | 237 throw new MissingInputException(info, id); |
| 196 }); | 238 }); |
| 197 }); | 239 }); |
| 198 } | 240 } |
| 199 | 241 |
| 200 /// Applies the transform so that it produces concrete (as opposed to lazy) | 242 /// Applies the transform so that it produces concrete (as opposed to lazy) |
| 201 /// outputs. | 243 /// outputs. |
| 202 Future<Set<AssetNode>> _applyImmediate() { | 244 Future _applyImmediate() { |
| 203 var newOutputs = new AssetSet(); | 245 var newOutputs = new AssetSet(); |
| 204 var transform = new Transform(this, newOutputs, _log); | 246 var transform = new Transform(this, newOutputs, _log); |
| 205 | 247 |
| 206 return syncFuture(() => transformer.apply(transform)).then((_) { | 248 return syncFuture(() => transformer.apply(transform)).then((_) { |
| 207 if (_isDirty) return new Set(); | 249 if (_hasBecomeDirty || _onAssetController.isClosed) return; |
| 208 | 250 |
| 209 // Any ids that are for a different package are invalid. | 251 // Any ids that are for a different package are invalid. |
| 210 var invalidIds = newOutputs | 252 var invalidIds = newOutputs |
| 211 .map((asset) => asset.id) | 253 .map((asset) => asset.id) |
| 212 .where((id) => id.package != phase.cascade.package) | 254 .where((id) => id.package != phase.cascade.package) |
| 213 .toSet(); | 255 .toSet(); |
| 214 for (var id in invalidIds) { | 256 for (var id in invalidIds) { |
| 215 newOutputs.removeId(id); | 257 newOutputs.removeId(id); |
| 216 // TODO(nweiz): report this as a warning rather than a failing error. | 258 // TODO(nweiz): report this as a warning rather than a failing error. |
| 217 phase.cascade.reportError(new InvalidOutputException(info, id)); | 259 phase.cascade.reportError(new InvalidOutputException(info, id)); |
| 218 } | 260 } |
| 219 | 261 |
| 220 // Remove outputs that used to exist but don't anymore. | 262 // Remove outputs that used to exist but don't anymore. |
| 221 for (var id in _outputControllers.keys.toList()) { | 263 for (var id in _outputControllers.keys.toList()) { |
| 222 if (newOutputs.containsId(id)) continue; | 264 if (newOutputs.containsId(id)) continue; |
| 223 _outputControllers.remove(id).setRemoved(); | 265 _outputControllers.remove(id).setRemoved(); |
| 224 } | 266 } |
| 225 | 267 |
| 226 var brandNewOutputs = new Set<AssetNode>(); | |
| 227 // Store any new outputs or new contents for existing outputs. | 268 // Store any new outputs or new contents for existing outputs. |
| 228 for (var asset in newOutputs) { | 269 for (var asset in newOutputs) { |
| 229 var controller = _outputControllers[asset.id]; | 270 var controller = _outputControllers[asset.id]; |
| 230 if (controller != null) { | 271 if (controller != null) { |
| 231 controller.setAvailable(asset); | 272 controller.setAvailable(asset); |
| 232 } else { | 273 } else { |
| 233 var controller = new AssetNodeController.available(asset, this); | 274 var controller = new AssetNodeController.available(asset, this); |
| 234 _outputControllers[asset.id] = controller; | 275 _outputControllers[asset.id] = controller; |
| 235 brandNewOutputs.add(controller.node); | 276 _onAssetController.add(controller.node); |
| 236 } | 277 } |
| 237 } | 278 } |
| 238 | |
| 239 return brandNewOutputs; | |
| 240 }); | 279 }); |
| 241 } | 280 } |
| 242 | 281 |
| 243 /// Applies the transform in declarative mode so that it produces lazy | 282 /// Applies the transform in declarative mode so that it produces lazy |
| 244 /// outputs. | 283 /// outputs. |
| 245 Future<Set<AssetNode>> _declareLazy() { | 284 Future _declareLazy() { |
| 246 var newIds = new Set(); | 285 var newIds = new Set(); |
| 247 var transform = new DeclaringTransform(this, newIds, _log); | 286 var transform = new DeclaringTransform(this, newIds, _log); |
| 248 | 287 |
| 249 return syncFuture(() { | 288 return syncFuture(() { |
| 250 return (transformer as LazyTransformer).declareOutputs(transform); | 289 return (transformer as LazyTransformer).declareOutputs(transform); |
| 251 }).then((_) { | 290 }).then((_) { |
| 252 if (_isDirty) return new Set(); | 291 if (_hasBecomeDirty || _onAssetController.isClosed) return; |
| 253 | 292 |
| 254 var invalidIds = | 293 var invalidIds = |
| 255 newIds.where((id) => id.package != phase.cascade.package).toSet(); | 294 newIds.where((id) => id.package != phase.cascade.package).toSet(); |
| 256 for (var id in invalidIds) { | 295 for (var id in invalidIds) { |
| 257 newIds.remove(id); | 296 newIds.remove(id); |
| 258 // TODO(nweiz): report this as a warning rather than a failing error. | 297 // TODO(nweiz): report this as a warning rather than a failing error. |
| 259 phase.cascade.reportError(new InvalidOutputException(info, id)); | 298 phase.cascade.reportError(new InvalidOutputException(info, id)); |
| 260 } | 299 } |
| 261 | 300 |
| 262 // Remove outputs that used to exist but don't anymore. | 301 // Remove outputs that used to exist but don't anymore. |
| 263 for (var id in _outputControllers.keys.toList()) { | 302 for (var id in _outputControllers.keys.toList()) { |
| 264 if (newIds.contains(id)) continue; | 303 if (newIds.contains(id)) continue; |
| 265 _outputControllers.remove(id).setRemoved(); | 304 _outputControllers.remove(id).setRemoved(); |
| 266 } | 305 } |
| 267 | 306 |
| 268 var brandNewOutputs = new Set<AssetNode>(); | |
| 269 for (var id in newIds) { | 307 for (var id in newIds) { |
| 270 var controller = _outputControllers[id]; | 308 var controller = _outputControllers[id]; |
| 271 if (controller != null) { | 309 if (controller != null) { |
| 272 controller.setLazy(force); | 310 controller.setLazy(force); |
| 273 } else { | 311 } else { |
| 274 var controller = new AssetNodeController.lazy(id, force, this); | 312 var controller = new AssetNodeController.lazy(id, force, this); |
| 275 _outputControllers[id] = controller; | 313 _outputControllers[id] = controller; |
| 276 brandNewOutputs.add(controller.node); | 314 _onAssetController.add(controller.node); |
| 277 } | 315 } |
| 278 } | 316 } |
| 279 | |
| 280 return brandNewOutputs; | |
| 281 }); | 317 }); |
| 282 } | 318 } |
| 283 | 319 |
| 284 void _log(AssetId asset, LogLevel level, String message, Span span) { | 320 void _log(AssetId asset, LogLevel level, String message, Span span) { |
| 285 // If the log isn't already associated with an asset, use the primary. | 321 // If the log isn't already associated with an asset, use the primary. |
| 286 if (asset == null) asset = primary.id; | 322 if (asset == null) asset = primary.id; |
| 287 var entry = new LogEntry(info, asset, level, message, span); | 323 var entry = new LogEntry(info, asset, level, message, span); |
| 288 _onLogController.add(entry); | 324 _onLogController.add(entry); |
| 289 } | 325 } |
| 290 | 326 |
| 291 String toString() => | 327 String toString() => |
| 292 "transform node in $_location for $transformer on $primary"; | 328 "transform node in $_location for $transformer on $primary"; |
| 293 } | 329 } |
| OLD | NEW |