blob: 1bfb8d461c8fa3eb69197cc0f20f373bd140632e [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.phase_output;
import 'dart:async';
import 'dart:collection';
import 'asset_cascade.dart';
import 'asset_node.dart';
import 'errors.dart';
import 'phase.dart';
import 'utils.dart';
/// A class that handles a single output of a phase.
///
/// Normally there's only a single [AssetNode] for a phase's output, but it's
/// possible that multiple transformers in the same phase emit assets with the
/// same id, causing collisions. This handles those collisions by forwarding the
/// chronologically first asset.
class PhaseOutput {
/// The phase for which this is an output.
final Phase _phase;
/// The asset node for this output.
AssetNode get output => _outputController.node;
AssetNodeController _outputController;
/// The assets for this output.
///
/// If there's no collision, this will only have one element. Otherwise, it
/// will be ordered by which asset was added first.
final _assets = new Queue<AssetNode>();
/// The [AssetCollisionException] for this output, or null if there is no
/// collision currently.
AssetCollisionException get collisionException {
if (_assets.length == 1) return null;
return new AssetCollisionException(
_assets.where((asset) => asset.transform != null)
.map((asset) => asset.transform.info),
output.id);
}
PhaseOutput(this._phase, AssetNode output)
: _outputController = new AssetNodeController.from(output) {
assert(!output.state.isRemoved);
add(output);
}
/// Adds an asset node as an output with this id.
void add(AssetNode node) {
assert(node.id == output.id);
_assets.add(node);
_watchAsset(node);
}
/// Removes all existing listeners on [output] without actually closing
/// [this].
///
/// This marks [output] as removed, but immediately replaces it with a new
/// [AssetNode] in the same state as the old output. This is used when adding
/// a new [Phase] to cause consumers of the prior phase's outputs to be to
/// start consuming the new phase's outputs instead.
void removeListeners() {
_outputController.setRemoved();
_outputController = new AssetNodeController.from(_assets.first);
}
/// Watches [node] for state changes and adjusts [_assets] and [output]
/// appropriately when they occur.
void _watchAsset(AssetNode node) {
node.onStateChange.listen((state) {
if (state.isRemoved) {
_removeAsset(node);
return;
}
if (_assets.first != node) return;
if (state.isAvailable) {
_outputController.setAvailable(node.asset);
} else {
assert(state.isDirty);
_outputController.setDirty();
}
});
}
/// Removes [node] as an output.
void _removeAsset(AssetNode node) {
if (_assets.length == 1) {
assert(_assets.single == node);
_outputController.setRemoved();
return;
}
// If there was more than one asset, we're resolving a collision --
// possibly partially.
var wasFirst = _assets.first == node;
_assets.remove(node);
// If this was the first asset, we replace it with the next asset
// (chronologically).
if (wasFirst) {
var newOutput = _assets.first;
_outputController.setTransform(newOutput.transform);
if (newOutput.state.isAvailable) {
if (output.state.isAvailable) _outputController.setDirty();
_outputController.setAvailable(newOutput.asset);
} else {
assert(newOutput.isDirty);
if (!output.state.isDirty) _outputController.setDirty();
}
}
// If there's still a collision, report it. This lets the user know
// if they've successfully resolved the collision or not.
if (_assets.length > 1) {
// Pump the event queue to ensure that the removal of the input triggers
// a new build to which we can attach the error.
// TODO(nweiz): report this through the output asset.
newFuture(() => _phase.cascade.reportError(collisionException));
}
}
}