blob: 7967d6a7c034b3f7930024fec06b545e2083f1cd [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.graph.phase_output;
import 'dart:async';
import 'dart:collection';
import '../asset/asset_forwarder.dart';
import '../asset/asset_node.dart';
import '../errors.dart';
import 'phase.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.
///
/// When the asset being forwarding changes, the old value of [output] will be
/// marked as removed and a new value will replace it. Users of this class can
/// be notified of this using [onAsset].
class PhaseOutput {
/// The phase for which this is an output.
final Phase _phase;
/// A string describing the location of [this] in the transformer graph.
final String _location;
/// The asset node for this output.
AssetNode get output => _outputForwarder.node;
AssetForwarder _outputForwarder;
/// A stream that emits an [AssetNode] each time this output starts forwarding
/// a new asset.
Stream<AssetNode> get onAsset => _onAssetController.stream;
final _onAssetController =
new StreamController<AssetNode>.broadcast(sync: true);
/// 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, this._location)
: _outputForwarder = new AssetForwarder(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);
assert(!output.state.isRemoved);
_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() {
_outputForwarder.close();
_outputForwarder = new AssetForwarder(_assets.first);
_onAssetController.add(output);
}
/// Watches [node] to adjust [_assets] and [output] when it's removed.
void _watchAsset(AssetNode node) {
node.whenRemoved(() {
if (_assets.length == 1) {
assert(_assets.single == node);
_outputForwarder.close();
_onAssetController.close();
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) removeListeners();
// 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) {
// TODO(nweiz): report this through the output asset.
_phase.cascade.reportError(collisionException);
}
});
}
String toString() => "phase output in $_location for $output";
}