Index: pkg/barback/lib/src/transform_node.dart |
diff --git a/pkg/barback/lib/src/transform_node.dart b/pkg/barback/lib/src/transform_node.dart |
index 84e42c0d0a5664bf10f2189bff6578099b1ed210..c047b5cf474d83aec3cb93450962ba4ba73395fb 100644 |
--- a/pkg/barback/lib/src/transform_node.dart |
+++ b/pkg/barback/lib/src/transform_node.dart |
@@ -25,94 +25,151 @@ class TransformNode { |
final Phase phase; |
/// The [Transformer] to apply to this node's inputs. |
- final Transformer _transformer; |
+ 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 |
- /// was run. |
+ /// began running. |
bool get isDirty => _isDirty; |
var _isDirty = true; |
/// The inputs read by this transform the last time it was run. |
/// |
- /// Used to tell if an input was removed in a later run. |
+ /// Used to tell if an input was added or removed in a later run. |
var _inputs = new Set<AssetNode>(); |
- /// The outputs created by this transform the last time it was run. |
- /// |
- /// Used to tell if an output was removed in a later run. |
- Set<AssetId> get outputs => _outputs; |
- var _outputs = new Set<AssetId>(); |
+ /// 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>(); |
+ |
+ TransformNode(this.phase, this.transformer, this.primary) { |
+ _primarySubscription = primary.onStateChange.listen((state) { |
+ if (state.isRemoved) { |
+ remove(); |
+ } else { |
+ _dirty(); |
+ } |
+ }); |
+ } |
- TransformNode(this.phase, this._transformer, this.primary); |
+ /// 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; |
+ _primarySubscription.cancel(); |
+ for (var subscription in _inputSubscriptions.values) { |
+ subscription.cancel(); |
+ } |
+ for (var controller in _outputControllers.values) { |
+ controller.setRemoved(); |
+ } |
+ } |
- /// Marks this transform as needing to be run. |
- void dirty() { |
+ /// 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(); |
+ } |
} |
/// Applies this transform. |
/// |
- /// Returns a [TransformOutputs] describing the resulting outputs compared to |
- /// previous runs. |
- Future<TransformOutputs> apply() { |
+ /// 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() { |
var newInputs = new Set<AssetNode>(); |
var newOutputs = new AssetSet(); |
var transform = createTransform(this, newInputs, newOutputs); |
- return _transformer.apply(transform).catchError((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. |
+ _isDirty = false; |
+ return transformer.apply(transform).catchError((error) { |
+ // If the transform became dirty while processing, ignore any errors from |
+ // it. |
+ if (_isDirty) return; |
+ |
+ // 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((_) { |
- _isDirty = false; |
- |
- // Stop watching any inputs that were removed. |
- for (var oldInput in _inputs) { |
- oldInput.consumers.remove(this); |
- } |
- |
- // Watch any new inputs so this transform will be re-processed when an |
- // input is modified. |
- for (var newInput in newInputs) { |
- newInput.consumers.add(this); |
- } |
- |
- _inputs = newInputs; |
- |
- // See which outputs are missing from the last run. |
- var outputIds = newOutputs.map((asset) => asset.id).toSet(); |
- var invalidIds = outputIds |
- .where((id) => id.package != phase.cascade.package).toSet(); |
- outputIds.removeAll(invalidIds); |
- |
- for (var id in invalidIds) { |
- // TODO(nweiz): report this as a warning rather than a failing error. |
- phase.cascade.reportError( |
- new InvalidOutputException(phase.cascade.package, id)); |
- } |
- |
- var removed = _outputs.difference(outputIds); |
- _outputs = outputIds; |
+ if (_isDirty) return []; |
- return new TransformOutputs(newOutputs, removed); |
+ _adjustInputs(newInputs); |
+ return _adjustOutputs(newOutputs); |
}); |
} |
-} |
-/// The result of running a [Transform], compared to the previous time it was |
-/// applied. |
-class TransformOutputs { |
- /// The outputs that are new or were modified since the last run. |
- final AssetSet updated; |
+ /// Adjusts the inputs of the transform to reflect the inputs consumed on its |
+ /// most recent run. |
+ void _adjustInputs(Set<AssetNode> newInputs) { |
+ // Stop watching any inputs that were removed. |
+ for (var oldInput in _inputs.difference(newInputs)) { |
+ _inputSubscriptions.remove(oldInput.id).cancel(); |
+ } |
+ |
+ // Watch any new inputs so this transform will be re-processed when an |
+ // input is modified. |
+ for (var newInput in newInputs.difference(_inputs)) { |
+ if (newInput.id == primary.id) continue; |
Bob Nystrom
2013/07/31 20:05:41
How can this case get hit?
nweiz
2013/07/31 22:47:53
This case is hit whenever the transform loads the
|
+ // TODO(nweiz): support the case where a new secondary input changes |
+ // after it's been loaded by the transform but before the transform has |
+ // finished running. |
+ _inputSubscriptions[newInput.id] = newInput.onStateChange |
+ .listen((_) => _dirty()); |
+ } |
+ |
+ _inputs = newInputs; |
+ } |
- /// The outputs that were created by the previous run but were not generated |
- /// by the most recent run. |
- final Set<AssetId> removed; |
+ /// 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 the wrong package are invalid. |
Bob Nystrom
2013/07/31 20:05:41
"for the wrong" -> "to a different"
nweiz
2013/07/31 22:47:53
Done.
|
+ 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(phase.cascade.package, id)); |
+ } |
+ |
+ // Remove outputs that used to exist but don't anymore. |
+ for (var id in _outputControllers.keys.toList()) { |
Bob Nystrom
2013/07/31 20:05:41
If we make AssetSet implement Set, you could do:
nweiz
2013/07/31 22:47:53
Eh, I think this is clear enough.
|
+ 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]; |
Bob Nystrom
2013/07/31 20:05:41
Use putIfAbsent() here?
nweiz
2013/07/31 22:47:53
That doesn't work well when we want to call [setAv
Bob Nystrom
2013/07/31 23:20:15
You could just call that from within the pubIfAbse
|
+ if (controller != null) { |
+ controller.setAvailable(asset); |
+ } else { |
+ var controller = new AssetNodeController.available(asset); |
+ _outputControllers[asset.id] = controller; |
+ brandNewOutputs.add(controller.node); |
+ } |
+ } |
- TransformOutputs(this.updated, this.removed); |
+ return brandNewOutputs; |
+ } |
} |