| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library barback.transform_node; |
| |
| import 'dart:async'; |
| |
| import 'asset.dart'; |
| import 'asset_id.dart'; |
| import 'asset_node.dart'; |
| import 'asset_set.dart'; |
| import 'barback_logger.dart'; |
| import 'errors.dart'; |
| import 'phase.dart'; |
| import 'transform.dart'; |
| import 'transformer.dart'; |
| |
| /// Describes a transform on a set of assets and its relationship to the build |
| /// dependency graph. |
| /// |
| /// Keeps track of whether it's dirty and needs to be run and which assets it |
| /// depends on. |
| class TransformNode { |
| /// The [Phase] that this transform runs in. |
| final Phase phase; |
| |
| /// The [Transformer] to apply to this node's inputs. |
| final Transformer transformer; |
| |
| /// The node for the primary asset this transform depends on. |
| final AssetNode primary; |
| |
| /// The subscription to [primary]'s [AssetNode.onStateChange] stream. |
| StreamSubscription _primarySubscription; |
| |
| /// True if an input has been modified since the last time this transform |
| /// began running. |
| bool get isDirty => _isDirty; |
| var _isDirty = true; |
| |
| /// The subscriptions to each input's [AssetNode.onStateChange] stream. |
| var _inputSubscriptions = new Map<AssetId, StreamSubscription>(); |
| |
| /// The controllers for the asset nodes emitted by this node. |
| var _outputControllers = new Map<AssetId, AssetNodeController>(); |
| |
| /// A stream that emits an event whenever this transform becomes dirty and |
| /// needs to be re-run. |
| /// |
| /// This may emit events when the transform was already dirty or while |
| /// processing transforms. Events are emitted synchronously to ensure that the |
| /// dirty state is thoroughly propagated as soon as any assets are changed. |
| Stream get onDirty => _onDirtyController.stream; |
| final _onDirtyController = new StreamController.broadcast(sync: true); |
| |
| /// A stream that emits an event whenever this transform logs an entry. |
| Stream<LogEntry> get onLog => _onLogController.stream; |
| final _onLogController = new StreamController<LogEntry>.broadcast(); |
| |
| TransformNode(this.phase, this.transformer, this.primary) { |
| _primarySubscription = primary.onStateChange.listen((state) { |
| if (state.isRemoved) { |
| remove(); |
| } else { |
| _dirty(); |
| } |
| }); |
| } |
| |
| /// The [TransformInfo] describing this node. |
| /// |
| /// [TransformInfo] is the publicly-visible representation of a transform |
| /// node. |
| TransformInfo get info => new TransformInfo(transformer, primary.id); |
| |
| /// Marks this transform as removed. |
| /// |
| /// This causes all of the transform's outputs to be marked as removed as |
| /// well. Normally this will be automatically done internally based on events |
| /// from the primary input, but it's possible for a transform to no longer be |
| /// valid even if its primary input still exists. |
| void remove() { |
| _isDirty = true; |
| _onDirtyController.close(); |
| _primarySubscription.cancel(); |
| for (var subscription in _inputSubscriptions.values) { |
| subscription.cancel(); |
| } |
| for (var controller in _outputControllers.values) { |
| controller.setRemoved(); |
| } |
| } |
| |
| /// Marks this transform as dirty. |
| /// |
| /// This causes all of the transform's outputs to be marked as dirty as well. |
| void _dirty() { |
| _isDirty = true; |
| for (var controller in _outputControllers.values) { |
| controller.setDirty(); |
| } |
| _onDirtyController.add(null); |
| } |
| |
| /// Applies this transform. |
| /// |
| /// Returns a set of asset nodes representing the outputs from this transform |
| /// that weren't emitted last time it was run. |
| Future<Set<AssetNode>> apply() { |
| assert(!_onDirtyController.isClosed); |
| |
| var newOutputs = new AssetSet(); |
| var transform = createTransform(this, newOutputs, _log); |
| |
| // Clear all the old input subscriptions. If an input is re-used, we'll |
| // re-subscribe. |
| for (var subscription in _inputSubscriptions.values) { |
| subscription.cancel(); |
| } |
| _inputSubscriptions.clear(); |
| |
| _isDirty = false; |
| return transformer.apply(transform).catchError((error) { |
| // If the transform became dirty while processing, ignore any errors from |
| // it. |
| if (_isDirty) return; |
| |
| if (error is! MissingInputException) { |
| error = new TransformerException(info, error); |
| } |
| |
| // Catch all transformer errors and pipe them to the results stream. |
| // This is so a broken transformer doesn't take down the whole graph. |
| phase.cascade.reportError(error); |
| |
| // Don't allow partial results from a failed transform. |
| newOutputs.clear(); |
| }).then((_) { |
| if (_isDirty) return new Set(); |
| |
| return _adjustOutputs(newOutputs); |
| }); |
| } |
| |
| /// Gets the asset for an input [id]. |
| /// |
| /// If an input with that ID cannot be found, throws an |
| /// [AssetNotFoundException]. |
| Future<Asset> getInput(AssetId id) { |
| return phase.getInput(id).then((node) { |
| // Throw if the input isn't found. This ensures the transformer's apply |
| // is exited. We'll then catch this and report it through the proper |
| // results stream. |
| if (node == null) throw new MissingInputException(info, id); |
| |
| // If the asset node is found, wait until its contents are actually |
| // available before we return them. |
| return node.whenAvailable.then((asset) { |
| _inputSubscriptions.putIfAbsent(node.id, |
| () => node.onStateChange.listen((_) => _dirty())); |
| |
| return asset; |
| }).catchError((error) { |
| if (error is! AssetNotFoundException || error.id != id) throw error; |
| // If the node was removed before it could be loaded, treat it as though |
| // it never existed and throw a MissingInputException. |
| throw new MissingInputException(info, id); |
| }); |
| }); |
| } |
| |
| /// Adjusts the outputs of the transform to reflect the outputs emitted on its |
| /// most recent run. |
| Set<AssetNode> _adjustOutputs(AssetSet newOutputs) { |
| // Any ids that are for a different package are invalid. |
| var invalidIds = newOutputs |
| .map((asset) => asset.id) |
| .where((id) => id.package != phase.cascade.package) |
| .toSet(); |
| for (var id in invalidIds) { |
| newOutputs.removeId(id); |
| // TODO(nweiz): report this as a warning rather than a failing error. |
| phase.cascade.reportError(new InvalidOutputException(info, id)); |
| } |
| |
| // Remove outputs that used to exist but don't anymore. |
| for (var id in _outputControllers.keys.toList()) { |
| if (newOutputs.containsId(id)) continue; |
| _outputControllers.remove(id).setRemoved(); |
| } |
| |
| var brandNewOutputs = new Set<AssetNode>(); |
| // Store any new outputs or new contents for existing outputs. |
| for (var asset in newOutputs) { |
| var controller = _outputControllers[asset.id]; |
| if (controller != null) { |
| controller.setAvailable(asset); |
| } else { |
| var controller = new AssetNodeController.available(asset, this); |
| _outputControllers[asset.id] = controller; |
| brandNewOutputs.add(controller.node); |
| } |
| } |
| |
| return brandNewOutputs; |
| } |
| |
| void _log(AssetId asset, LogLevel level, String message, Span span) { |
| // If the log isn't already associated with an asset, use the primary. |
| if (asset == null) asset = primary.id; |
| var info = new TransformInfo(transformer, primary.id); |
| var entry = new LogEntry(info, asset, level, message, span); |
| _onLogController.add(entry); |
| } |
| } |