blob: 578b2fa20c8301b95cce949507f8ff91e626a5bd [file] [log] [blame]
// 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 '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);
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);
// 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;
}
}