Chromium Code Reviews| 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..316ab726c01ecfa5f11e4a8c8e17b56d6f0f3f39 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; |
| - |
| - /// The number of errors that have been logged since the current build |
| - /// started. |
| - int _numLogErrors; |
| + /// Whether [this] is dirty and still has more processing to do. |
| + bool get isDirty => _phases.any((phase) => phase.isDirty); |
| - /// 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<bool>.broadcast(sync: true); |
|
Bob Nystrom
2014/03/05 22:13:25
Ditch the <bool>.
nweiz
2014/03/06 00:29:08
Done.
|
| /// 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, Queue<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 completers = _pendingAssetRequests.putIfAbsent(id, () => new Queue()); |
|
Bob Nystrom
2014/03/05 22:13:25
Why do you need a queue here? Can you just reuse t
nweiz
2014/03/06 00:29:08
Done.
|
| + completers.add(new Completer.sync()); |
| + return completers.last.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,55 @@ 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(); |
| + phase.onAsset.listen(_providePendingAsset); |
| + |
| + phase.onDone.listen((_) { |
| + if (isDirty) return; |
| + |
| + // 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. |
| + _pendingAssetRequests.forEach((_, completers) { |
| + for (var completer in completers) { |
| + completer.complete(null); |
| + } |
| + }); |
| + _pendingAssetRequests.clear(); |
| + _onDoneController.add(null); |
| }); |
| - _phases.add(phase); |
| - } |
| - /// 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; |
| - }); |
| + _phases.add(phase); |
| } |
| - /// 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; |
| - } |
| - |
| - // 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(); |
| + /// Provide an asset to a pending [getAssetNode] call. |
| + void _providePendingAsset(AssetNode asset) { |
| + // If anyone's waiting for this asset, provide it to them. |
| + var requests = _pendingAssetRequests.remove(asset.id); |
| + if (requests == null) return; |
| - // Otherwise, everything is done. |
| - return null; |
| + for (var request in requests) { |
| + if (asset.state.isAvailable) { |
|
Bob Nystrom
2014/03/05 22:13:25
This seems weird to me. Doesn't it make more sense
nweiz
2014/03/06 00:29:08
N/A since I got rid of the queue.
|
| + request.complete(asset); |
| + continue; |
| } |
| - // Process that phase and then loop onto the next. |
| - return future.then((_) => _process()); |
| - }); |
| + // 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"; |