| Index: third_party/pkg/barback-0.13.0/lib/src/transform_node.dart
|
| diff --git a/third_party/pkg/barback-0.13.0/lib/src/transform_node.dart b/third_party/pkg/barback-0.13.0/lib/src/transform_node.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..e3d9067dd7d473c4aeb53c435e2699dc8adfe2c1
|
| --- /dev/null
|
| +++ b/third_party/pkg/barback-0.13.0/lib/src/transform_node.dart
|
| @@ -0,0 +1,515 @@
|
| +// 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 'declaring_transform.dart';
|
| +import 'errors.dart';
|
| +import 'lazy_transformer.dart';
|
| +import 'log.dart';
|
| +import 'phase.dart';
|
| +import 'stream_pool.dart';
|
| +import 'transform.dart';
|
| +import 'transformer.dart';
|
| +import 'utils.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;
|
| +
|
| + /// A string describing the location of [this] in the transformer graph.
|
| + final String _location;
|
| +
|
| + /// The subscription to [primary]'s [AssetNode.onStateChange] stream.
|
| + StreamSubscription _primarySubscription;
|
| +
|
| + /// The subscription to [phase]'s [Phase.onAsset] stream.
|
| + StreamSubscription<AssetNode> _phaseSubscription;
|
| +
|
| + /// Whether [this] is dirty and still has more processing to do.
|
| + bool get isDirty => !_state.isDone;
|
| +
|
| + /// Whether [transformer] is lazy and this transform has yet to be forced.
|
| + bool _isLazy;
|
| +
|
| + /// The subscriptions to each input's [AssetNode.onStateChange] stream.
|
| + final _inputSubscriptions = new Map<AssetId, StreamSubscription>();
|
| +
|
| + /// The controllers for the asset nodes emitted by this node.
|
| + final _outputControllers = new Map<AssetId, AssetNodeController>();
|
| +
|
| + final _missingInputs = new Set<AssetId>();
|
| +
|
| + /// The controller that's used to pass [primary] through [this] if it's not
|
| + /// consumed or overwritten.
|
| + ///
|
| + /// This needs an intervening controller to ensure that the output can be
|
| + /// marked dirty when determining whether [this] will consume or overwrite it,
|
| + /// and be marked removed if it does. [_passThroughController] will be null
|
| + /// if the asset is not being passed through.
|
| + AssetNodeController _passThroughController;
|
| +
|
| + /// A stream that emits an event whenever [this] is no longer dirty.
|
| + ///
|
| + /// This is synchronous in order to guarantee that it will emit an event as
|
| + /// soon as [isDirty] flips from `true` to `false`.
|
| + Stream get onDone => _onDoneController.stream;
|
| + final _onDoneController = new StreamController.broadcast(sync: true);
|
| +
|
| + /// A stream that emits any new assets emitted by [this].
|
| + ///
|
| + /// Assets are emitted synchronously to ensure that any changes are thoroughly
|
| + /// propagated as soon as they occur.
|
| + Stream<AssetNode> get onAsset => _onAssetController.stream;
|
| + final _onAssetController =
|
| + new StreamController<AssetNode>.broadcast(sync: true);
|
| +
|
| + /// A stream that emits an event whenever this transform logs an entry.
|
| + ///
|
| + /// This is synchronous because error logs can cause the transform to fail, so
|
| + /// we need to ensure that their processing isn't delayed until after the
|
| + /// transform or build has finished.
|
| + Stream<LogEntry> get onLog => _onLogPool.stream;
|
| + final _onLogPool = new StreamPool<LogEntry>.broadcast();
|
| +
|
| + /// The current state of [this].
|
| + var _state = _TransformNodeState.PROCESSING;
|
| +
|
| + /// Whether [this] has been marked as removed.
|
| + bool get _isRemoved => _onAssetController.isClosed;
|
| +
|
| + /// Whether the most recent run of this transform has declared that it
|
| + /// consumes the primary input.
|
| + ///
|
| + /// Defaults to `false`. This is not meaningful unless [_state] is
|
| + /// [_TransformNodeState.APPLIED].
|
| + bool _consumePrimary = false;
|
| +
|
| + TransformNode(this.phase, Transformer transformer, this.primary,
|
| + this._location)
|
| + : transformer = transformer,
|
| + _isLazy = transformer is LazyTransformer {
|
| + _primarySubscription = primary.onStateChange.listen((state) {
|
| + if (state.isRemoved) {
|
| + remove();
|
| + } else {
|
| + _dirty(primaryChanged: true);
|
| + }
|
| + });
|
| +
|
| + _phaseSubscription = phase.previous.onAsset.listen((node) {
|
| + if (_missingInputs.contains(node.id)) _dirty(primaryChanged: false);
|
| + });
|
| +
|
| + _process();
|
| + }
|
| +
|
| + /// 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() {
|
| + _onAssetController.close();
|
| + _onDoneController.close();
|
| + _primarySubscription.cancel();
|
| + _phaseSubscription.cancel();
|
| + _clearInputSubscriptions();
|
| + _clearOutputs();
|
| + if (_passThroughController != null) {
|
| + _passThroughController.setRemoved();
|
| + _passThroughController = null;
|
| + }
|
| + }
|
| +
|
| + /// If [transformer] is lazy, ensures that its concrete outputs will be
|
| + /// generated.
|
| + void force() {
|
| + // TODO(nweiz): we might want to have a timeout after which, if the
|
| + // transform's outputs have gone unused, we switch it back to lazy mode.
|
| + if (!_isLazy) return;
|
| + _isLazy = false;
|
| + _dirty(primaryChanged: false);
|
| + }
|
| +
|
| + /// Marks this transform as dirty.
|
| + ///
|
| + /// This causes all of the transform's outputs to be marked as dirty as well.
|
| + /// [primaryChanged] should be true if and only if [this] was set dirty
|
| + /// because [primary] changed.
|
| + void _dirty({bool primaryChanged: false}) {
|
| + if (!primaryChanged && _state.isNotPrimary) return;
|
| +
|
| + if (_passThroughController != null) _passThroughController.setDirty();
|
| + for (var controller in _outputControllers.values) {
|
| + controller.setDirty();
|
| + }
|
| +
|
| + if (_state.isDone) {
|
| + if (primaryChanged) {
|
| + _process();
|
| + } else {
|
| + _apply();
|
| + }
|
| + } else if (primaryChanged) {
|
| + _state = _TransformNodeState.NEEDS_IS_PRIMARY;
|
| + } else if (!_state.needsIsPrimary) {
|
| + _state = _TransformNodeState.NEEDS_APPLY;
|
| + }
|
| + }
|
| +
|
| + /// Determines whether [primary] is primary for [transformer], and if so runs
|
| + /// [transformer.apply].
|
| + void _process() {
|
| + // Clear all the old input subscriptions. If an input is re-used, we'll
|
| + // re-subscribe.
|
| + _clearInputSubscriptions();
|
| + _state = _TransformNodeState.PROCESSING;
|
| + primary.whenAvailable((_) {
|
| + _state = _TransformNodeState.PROCESSING;
|
| + return transformer.isPrimary(primary.asset.id);
|
| + }).catchError((error, stackTrace) {
|
| + // If the transform became dirty while processing, ignore any errors from
|
| + // it.
|
| + if (_state.needsIsPrimary || _isRemoved) return false;
|
| +
|
| + // 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(_wrapException(error, stackTrace));
|
| +
|
| + return false;
|
| + }).then((isPrimary) {
|
| + if (_isRemoved) return;
|
| + if (_state.needsIsPrimary) {
|
| + _process();
|
| + } else if (isPrimary) {
|
| + _apply();
|
| + } else {
|
| + _clearOutputs();
|
| + _emitPassThrough();
|
| + _state = _TransformNodeState.NOT_PRIMARY;
|
| + _onDoneController.add(null);
|
| + }
|
| + });
|
| + }
|
| +
|
| + /// Applies this transform.
|
| + void _apply() {
|
| + assert(!_onAssetController.isClosed);
|
| +
|
| + // Clear input subscriptions here as well as in [_process] because [_apply]
|
| + // may be restarted independently if only a secondary input changes.
|
| + _clearInputSubscriptions();
|
| + _state = _TransformNodeState.PROCESSING;
|
| + primary.whenAvailable((_) {
|
| + if (_state.needsIsPrimary) return null;
|
| + _state = _TransformNodeState.PROCESSING;
|
| + // TODO(nweiz): If [transformer] is a [DeclaringTransformer] but not a
|
| + // [LazyTransformer], we can get some mileage out of doing a declarative
|
| + // first so we know how to hook up the assets.
|
| + if (_isLazy) return _declareLazy();
|
| + return _applyImmediate();
|
| + }).catchError((error, stackTrace) {
|
| + // If the transform became dirty while processing, ignore any errors from
|
| + // it.
|
| + if (!_state.isProcessing || _isRemoved) return false;
|
| +
|
| + // 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(_wrapException(error, stackTrace));
|
| + return true;
|
| + }).then((hadError) {
|
| + if (_isRemoved) return;
|
| +
|
| + if (_state.needsIsPrimary) {
|
| + _process();
|
| + } else if (_state.needsApply) {
|
| + _apply();
|
| + } else {
|
| + assert(_state.isProcessing);
|
| + if (hadError) {
|
| + _clearOutputs();
|
| + _dontEmitPassThrough();
|
| + }
|
| +
|
| + _state = _TransformNodeState.APPLIED;
|
| + _onDoneController.add(null);
|
| + }
|
| + });
|
| + }
|
| +
|
| + /// Gets the asset for an input [id].
|
| + ///
|
| + /// If an input with [id] cannot be found, throws an [AssetNotFoundException].
|
| + Future<Asset> getInput(AssetId id) {
|
| + return phase.previous.getOutput(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) {
|
| + _missingInputs.add(id);
|
| + throw new AssetNotFoundException(id);
|
| + }
|
| +
|
| + _inputSubscriptions.putIfAbsent(node.id, () {
|
| + return node.onStateChange.listen((_) => _dirty(primaryChanged: false));
|
| + });
|
| +
|
| + return node.asset;
|
| + });
|
| + }
|
| +
|
| + /// Applies the transform so that it produces concrete (as opposed to lazy)
|
| + /// outputs.
|
| + ///
|
| + /// Returns whether or not the transformer logged an error.
|
| + Future<bool> _applyImmediate() {
|
| + var transformController = new TransformController(this);
|
| + _onLogPool.add(transformController.onLog);
|
| +
|
| + return syncFuture(() {
|
| + return transformer.apply(transformController.transform);
|
| + }).then((_) {
|
| + if (!_state.isProcessing || _onAssetController.isClosed) return false;
|
| + if (transformController.loggedError) return true;
|
| +
|
| + _consumePrimary = transformController.consumePrimary;
|
| +
|
| + var newOutputs = transformController.outputs;
|
| + // 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();
|
| + }
|
| +
|
| + // Emit or stop emitting the pass-through asset between removing and
|
| + // adding outputs to ensure there are no collisions.
|
| + if (!newOutputs.containsId(primary.id)) {
|
| + _emitPassThrough();
|
| + } else {
|
| + _dontEmitPassThrough();
|
| + }
|
| +
|
| + // 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;
|
| + _onAssetController.add(controller.node);
|
| + }
|
| + }
|
| +
|
| + return false;
|
| + });
|
| + }
|
| +
|
| + /// Applies the transform in declarative mode so that it produces lazy
|
| + /// outputs.
|
| + ///
|
| + /// Returns whether or not the transformer logged an error.
|
| + Future<bool> _declareLazy() {
|
| + var transformController = new DeclaringTransformController(this);
|
| +
|
| + return syncFuture(() {
|
| + return (transformer as LazyTransformer)
|
| + .declareOutputs(transformController.transform);
|
| + }).then((_) {
|
| + if (!_state.isProcessing || _onAssetController.isClosed) return false;
|
| + if (transformController.loggedError) return true;
|
| +
|
| + _consumePrimary = transformController.consumePrimary;
|
| +
|
| + var newIds = transformController.outputIds;
|
| + var invalidIds =
|
| + newIds.where((id) => id.package != phase.cascade.package).toSet();
|
| + for (var id in invalidIds) {
|
| + newIds.remove(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 (newIds.contains(id)) continue;
|
| + _outputControllers.remove(id).setRemoved();
|
| + }
|
| +
|
| + // Emit or stop emitting the pass-through asset between removing and
|
| + // adding outputs to ensure there are no collisions.
|
| + if (!newIds.contains(primary.id)) {
|
| + _emitPassThrough();
|
| + } else {
|
| + _dontEmitPassThrough();
|
| + }
|
| +
|
| + for (var id in newIds) {
|
| + var controller = _outputControllers[id];
|
| + if (controller != null) {
|
| + controller.setLazy(force);
|
| + } else {
|
| + var controller = new AssetNodeController.lazy(id, force, this);
|
| + _outputControllers[id] = controller;
|
| + _onAssetController.add(controller.node);
|
| + }
|
| + }
|
| +
|
| + return false;
|
| + });
|
| + }
|
| +
|
| + /// Cancels all subscriptions to secondary input nodes.
|
| + void _clearInputSubscriptions() {
|
| + _missingInputs.clear();
|
| + for (var subscription in _inputSubscriptions.values) {
|
| + subscription.cancel();
|
| + }
|
| + _inputSubscriptions.clear();
|
| + }
|
| +
|
| + /// Removes all output assets.
|
| + void _clearOutputs() {
|
| + // Remove all the previously-emitted assets.
|
| + for (var controller in _outputControllers.values) {
|
| + controller.setRemoved();
|
| + }
|
| + _outputControllers.clear();
|
| + }
|
| +
|
| + /// Emit the pass-through asset if it's not being emitted already.
|
| + void _emitPassThrough() {
|
| + assert(!_outputControllers.containsKey(primary.id));
|
| +
|
| + if (_consumePrimary) return;
|
| + if (_passThroughController == null) {
|
| + _passThroughController = new AssetNodeController.from(primary);
|
| + _onAssetController.add(_passThroughController.node);
|
| + } else {
|
| + _passThroughController.setAvailable(primary.asset);
|
| + }
|
| + }
|
| +
|
| + /// Stop emitting the pass-through asset if it's being emitted already.
|
| + void _dontEmitPassThrough() {
|
| + if (_passThroughController == null) return;
|
| + _passThroughController.setRemoved();
|
| + _passThroughController = null;
|
| + }
|
| +
|
| + BarbackException _wrapException(error, StackTrace stackTrace) {
|
| + if (error is! AssetNotFoundException) {
|
| + return new TransformerException(info, error, stackTrace);
|
| + } else {
|
| + return new MissingInputException(info, error.id);
|
| + }
|
| + }
|
| +
|
| + String toString() =>
|
| + "transform node in $_location for $transformer on $primary";
|
| +}
|
| +
|
| +/// The enum of states that [TransformNode] can be in.
|
| +class _TransformNodeState {
|
| + /// The transform node is running [Transformer.isPrimary] or
|
| + /// [Transformer.apply] and doesn't need to re-run them.
|
| + ///
|
| + /// If there are no external changes by the time the processing finishes, this
|
| + /// will transition to [APPLIED] or [NOT_PRIMARY] depending on the result of
|
| + /// [Transformer.isPrimary]. If the primary input changes, this will
|
| + /// transition to [NEEDS_IS_PRIMARY]. If a secondary input changes, this will
|
| + /// transition to [NEEDS_APPLY].
|
| + static final PROCESSING = const _TransformNodeState._("processing");
|
| +
|
| + /// The transform is running [Transformer.isPrimary] or [Transformer.apply],
|
| + /// but since it started the primary input changed, so it will need to re-run
|
| + /// [Transformer.isPrimary].
|
| + ///
|
| + /// This will always transition to [Transformer.PROCESSING].
|
| + static final NEEDS_IS_PRIMARY =
|
| + const _TransformNodeState._("needs isPrimary");
|
| +
|
| + /// The transform is running [Transformer.apply], but since it started a
|
| + /// secondary input changed, so it will need to re-run [Transformer.apply].
|
| + ///
|
| + /// If there are no external changes by the time [Transformer.apply] finishes,
|
| + /// this will transition to [PROCESSING]. If the primary input changes, this
|
| + /// will transition to [NEEDS_IS_PRIMARY].
|
| + static final NEEDS_APPLY = const _TransformNodeState._("needs apply");
|
| +
|
| + /// The transform has finished running [Transformer.apply], whether or not it
|
| + /// emitted an error.
|
| + ///
|
| + /// If the primary input or a secondary input changes, this will transition to
|
| + /// [PROCESSING].
|
| + static final APPLIED = const _TransformNodeState._("applied");
|
| +
|
| + /// The transform has finished running [Transformer.isPrimary], which returned
|
| + /// `false`.
|
| + ///
|
| + /// If the primary input changes, this will transition to [PROCESSING].
|
| + static final NOT_PRIMARY = const _TransformNodeState._("not primary");
|
| +
|
| + /// Whether [this] is [PROCESSING].
|
| + bool get isProcessing => this == _TransformNodeState.PROCESSING;
|
| +
|
| + /// Whether [this] is [NEEDS_IS_PRIMARY].
|
| + bool get needsIsPrimary => this == _TransformNodeState.NEEDS_IS_PRIMARY;
|
| +
|
| + /// Whether [this] is [NEEDS_APPLY].
|
| + bool get needsApply => this == _TransformNodeState.NEEDS_APPLY;
|
| +
|
| + /// Whether [this] is [APPLIED].
|
| + bool get isApplied => this == _TransformNodeState.APPLIED;
|
| +
|
| + /// Whether [this] is [NOT_PRIMARY].
|
| + bool get isNotPrimary => this == _TransformNodeState.NOT_PRIMARY;
|
| +
|
| + /// Whether the transform has finished running [Transformer.isPrimary] and
|
| + /// [Transformer.apply].
|
| + ///
|
| + /// Specifically, whether [this] is [APPLIED] or [NOT_PRIMARY].
|
| + bool get isDone => isApplied || isNotPrimary;
|
| +
|
| + final String name;
|
| +
|
| + const _TransformNodeState._(this.name);
|
| +
|
| + String toString() => name;
|
| +}
|
|
|