| Index: pkg/barback/lib/src/asset_cascade.dart
|
| diff --git a/pkg/barback/lib/src/asset_cascade.dart b/pkg/barback/lib/src/asset_cascade.dart
|
| index 8c30be3d0647a4b89d45e18b1c8674edf8ba0a98..e9af69aef65f61b990ac35396f2a457e7059db06 100644
|
| --- a/pkg/barback/lib/src/asset_cascade.dart
|
| +++ b/pkg/barback/lib/src/asset_cascade.dart
|
| @@ -12,14 +12,12 @@ import 'asset_id.dart';
|
| import 'asset_node.dart';
|
| import 'asset_set.dart';
|
| import 'log.dart';
|
| -import 'build_result.dart';
|
| import 'cancelable_future.dart';
|
| import 'errors.dart';
|
| import 'package_graph.dart';
|
| import 'phase.dart';
|
| import 'stream_pool.dart';
|
| import 'transformer.dart';
|
| -import 'utils.dart';
|
|
|
| /// The asset cascade for an individual package.
|
| ///
|
| @@ -53,80 +51,45 @@ class AssetCascade {
|
|
|
| final _phases = <Phase>[];
|
|
|
| - /// A stream that emits a [BuildResult] each time the build is completed,
|
| - /// whether or not it succeeded.
|
| - ///
|
| - /// If an unexpected error in barback itself occurs, it will be emitted
|
| - /// through this stream's error channel.
|
| - Stream<BuildResult> get results => _resultsController.stream;
|
| - final _resultsController = new StreamController<BuildResult>.broadcast();
|
| -
|
| /// A stream that emits any errors from the cascade or the transformers.
|
| ///
|
| /// This emits errors as they're detected. If an error occurs in one part of
|
| /// the cascade, unrelated parts will continue building.
|
| - ///
|
| - /// This will not emit programming errors from barback itself. Those will be
|
| - /// emitted through the [results] stream's error channel.
|
| Stream<BarbackException> get errors => _errorsController.stream;
|
| - final _errorsController = new StreamController<BarbackException>.broadcast();
|
| -
|
| - /// A stream that emits an event whenever this cascade becomes dirty.
|
| - ///
|
| - /// After this stream emits an event, [results] will emit an event once the
|
| - /// cascade is no longer dirty.
|
| - ///
|
| - /// This may emit events when the cascade was already dirty. Events are
|
| - /// emitted synchronously to ensure that the dirty state is thoroughly
|
| - /// propagated as soon as any assets are changed.
|
| - Stream get onDirty => _onDirtyPool.stream;
|
| - final _onDirtyPool = new StreamPool.broadcast();
|
| -
|
| - /// A controller whose stream feeds into [_onDirtyPool].
|
| - final _onDirtyController = new StreamController.broadcast(sync: true);
|
| + final _errorsController =
|
| + new StreamController<BarbackException>.broadcast(sync: true);
|
|
|
| /// A stream that emits an event whenever any transforms in this cascade logs
|
| /// an entry.
|
| Stream<LogEntry> get onLog => _onLogPool.stream;
|
| final _onLogPool = new StreamPool<LogEntry>.broadcast();
|
|
|
| - /// The errors that have occurred since the current build started.
|
| - ///
|
| - /// This will be empty if no build is occurring.
|
| - Queue<BarbackException> _accumulatedErrors;
|
| + /// Whether [this] is dirty and still has more processing to do.
|
| + bool get isDirty => _phases.any((phase) => phase.isDirty);
|
|
|
| - /// The number of errors that have been logged since the current build
|
| - /// started.
|
| - int _numLogErrors;
|
| -
|
| - /// A future that completes when the currently running build process finishes.
|
| + /// A stream that emits an event whenever [this] is no longer dirty.
|
| ///
|
| - /// If no build it in progress, is `null`.
|
| - Future _processDone;
|
| -
|
| - /// Whether any source assets have been updated or removed since processing
|
| - /// last began.
|
| - var _newChanges = false;
|
| + /// 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);
|
|
|
| /// Returns all currently-available output assets from this cascade.
|
| AssetSet get availableOutputs =>
|
| new AssetSet.from(_phases.last.availableOutputs.map((node) => node.asset));
|
|
|
| + /// A map of asset ids to completers for [getAssetNode] requests.
|
| + ///
|
| + /// If an asset node is requested before it's available, we put a completer in
|
| + /// this map to wait for the asset to be generated. If it's not generated, the
|
| + /// completer should complete to `null`.
|
| + final _pendingAssetRequests = new Map<AssetId, Completer<AssetNode>>();
|
| +
|
| /// Creates a new [AssetCascade].
|
| ///
|
| /// It loads source assets within [package] using [provider].
|
| AssetCascade(this.graph, this.package) {
|
| - _onDirtyPool.add(_onDirtyController.stream);
|
| - _addPhase(new Phase(this, [], package));
|
| -
|
| - // Keep track of logged errors so we can know that the build failed.
|
| - onLog.listen((entry) {
|
| - if (entry.level == LogLevel.ERROR) {
|
| - // TODO(nweiz): keep track of stack chain.
|
| - _accumulatedErrors.add(
|
| - new TransformerException(entry.transform, entry.message, null));
|
| - }
|
| - });
|
| + _addPhase(new Phase(this, package));
|
| }
|
|
|
| /// Gets the asset identified by [id].
|
| @@ -148,18 +111,29 @@ class AssetCascade {
|
| // metadata about the file names of assets it can emit, we can prove that
|
| // none of them can emit [id] and fail early.
|
| return _phases.last.getOutput(id).then((node) {
|
| - // If the requested asset is available, we can just return it.
|
| - if (node != null && node.state.isAvailable) return node;
|
| -
|
| - if (_processDone != null) {
|
| - // If there's a build running, that build might generate the asset, so
|
| - // we wait for it to complete and then try again.
|
| - return _processDone.then((_) => getAssetNode(id));
|
| + if (node != null) {
|
| + // If the requested asset is available, we can just return it.
|
| + if (node.state.isAvailable) return node;
|
| +
|
| + // If the requested asset exists but isn't yet available, wait to see if
|
| + // it becomes available. If it's removed before becoming available, try
|
| + // again, since it could be generated again.
|
| + node.force();
|
| + return node.whenAvailable((_) => node).catchError((error) {
|
| + if (error is! AssetNotFoundException) throw error;
|
| + return getAssetNode(id);
|
| + });
|
| }
|
|
|
| - // If the asset hasn't been built and nothing is building now, the asset
|
| - // won't be generated, so we return null.
|
| - return null;
|
| + // If the cascade isn't dirty, the phase won't generate the requested
|
| + // asset in the future.
|
| + if (!isDirty) return null;
|
| +
|
| + // If the cascade is dirty, store a completer for the asset node. If it's
|
| + // generated in the future, we'll complete this completer.
|
| + var completer = _pendingAssetRequests.putIfAbsent(id,
|
| + () => new Completer.sync());
|
| + return completer.future;
|
| });
|
| }
|
|
|
| @@ -222,7 +196,9 @@ class AssetCascade {
|
| continue;
|
| }
|
|
|
| - _addPhase(_phases.last.addPhase(transformers[i]));
|
| + var phase = _phases.last.addPhase();
|
| + _addPhase(phase);
|
| + phase.updateTransformers(transformers[i]);
|
| }
|
|
|
| if (transformers.length == 0) {
|
| @@ -242,68 +218,51 @@ class AssetCascade {
|
| }
|
|
|
| void reportError(BarbackException error) {
|
| - _accumulatedErrors.add(error);
|
| _errorsController.add(error);
|
| }
|
|
|
| - /// Add [phase] to the end of [_phases] and watch its [onDirty] stream.
|
| + /// Add [phase] to the end of [_phases] and watch its streams.
|
| void _addPhase(Phase phase) {
|
| - _onDirtyPool.add(phase.onDirty);
|
| _onLogPool.add(phase.onLog);
|
| - phase.onDirty.listen((_) {
|
| - _newChanges = true;
|
| - _waitForProcess();
|
| - });
|
| - _phases.add(phase);
|
| - }
|
| + phase.onAsset.listen(_providePendingAsset);
|
|
|
| - /// Starts the build process asynchronously if there is work to be done.
|
| - ///
|
| - /// Returns a future that completes with the background processing is done.
|
| - /// If there is no work to do, returns a future that completes immediately.
|
| - /// All errors that occur during processing will be caught (and routed to the
|
| - /// [results] stream) before they get to the returned future, so it is safe
|
| - /// to discard it.
|
| - Future _waitForProcess() {
|
| - if (_processDone != null) return _processDone;
|
| -
|
| - _accumulatedErrors = new Queue();
|
| - _numLogErrors = 0;
|
| - return _processDone = _process().then((_) {
|
| - // Report the build completion.
|
| - // TODO(rnystrom): Put some useful data in here.
|
| - _resultsController.add(
|
| - new BuildResult(_accumulatedErrors));
|
| - _processDone = null;
|
| - _accumulatedErrors = null;
|
| - });
|
| - }
|
| + phase.onDone.listen((_) {
|
| + if (isDirty) return;
|
|
|
| - /// Starts the background processing.
|
| - ///
|
| - /// Returns a future that completes when all assets have been processed.
|
| - Future _process() {
|
| - _newChanges = false;
|
| - return newFuture(() {
|
| - // Find the first phase that has work to do and do it.
|
| - var future;
|
| - for (var phase in _phases) {
|
| - future = phase.process();
|
| - if (future != null) break;
|
| + // This cascade has finished building. If anyone's still waiting for
|
| + // assets, cut off the wait; we won't be generating them, at least until a
|
| + // source asset changes.
|
| + for (var completer in _pendingAssetRequests.values) {
|
| + completer.complete(null);
|
| }
|
| + _pendingAssetRequests.clear();
|
| + _onDoneController.add(null);
|
| + });
|
|
|
| - // If all phases are done and no new updates have come in, we're done.
|
| - if (future == null) {
|
| - // If changes have come in, start over.
|
| - if (_newChanges) return _process();
|
| + _phases.add(phase);
|
| + }
|
|
|
| - // Otherwise, everything is done.
|
| - return null;
|
| - }
|
| + /// Provide an asset to a pending [getAssetNode] call.
|
| + void _providePendingAsset(AssetNode asset) {
|
| + // If anyone's waiting for this asset, provide it to them.
|
| + var request = _pendingAssetRequests.remove(asset.id);
|
| + if (request == null) return;
|
|
|
| - // Process that phase and then loop onto the next.
|
| - return future.then((_) => _process());
|
| - });
|
| + if (asset.state.isAvailable) {
|
| + request.complete(asset);
|
| + return;
|
| + }
|
| +
|
| + // A lazy asset may be emitted while still dirty. If so, we wait until
|
| + // it's either available or removed before trying again to access it. We
|
| + // retry the entire [getAsset] process because the state of the graph may
|
| + // have changed dramatically by the time it's available.
|
| + assert(asset.state.isDirty);
|
| + asset.force();
|
| + asset.whenStateChanges()
|
| + .then((_) => getAssetNode(asset.id))
|
| + .then(request.complete)
|
| + .catchError(request.completeError);
|
| }
|
|
|
| String toString() => "cascade for $package";
|
|
|