Chromium Code Reviews| Index: pkg/barback/lib/src/asset_graph.dart |
| diff --git a/pkg/barback/lib/src/asset_graph.dart b/pkg/barback/lib/src/asset_graph.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..d5dcf295dd750cbc6789cfd522be9ae0d390e845 |
| --- /dev/null |
| +++ b/pkg/barback/lib/src/asset_graph.dart |
| @@ -0,0 +1,206 @@ |
| +// 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.asset_graph; |
| + |
| +import 'dart:async'; |
| +import 'dart:collection'; |
| + |
| +import 'asset.dart'; |
| +import 'asset_id.dart'; |
| +import 'asset_provider.dart'; |
| +import 'errors.dart'; |
| +import 'change_batch.dart'; |
| +import 'phase.dart'; |
| +import 'transformer.dart'; |
| + |
| +/// The main build dependency manager. |
| +/// |
| +/// For any given input file, it can tell which output files are affected by |
| +/// it, and vice versa. |
| +class AssetGraph { |
| + final AssetProvider _provider; |
| + |
| + final _phases = <Phase>[]; |
| + |
| + Stream<ProcessResult> get results => _resultsController.stream; |
| + final _resultsController = new StreamController<ProcessResult>.broadcast(); |
| + |
| + /// A future that completes when the currently running build process finishes. |
| + /// |
| + /// If no build it in progress, is `null`. |
| + Future _processDone; |
| + |
| + ChangeBatch _sourceChanges; |
| + |
| + /// Creates a new [AssetGraph]. |
| + /// |
| + /// It loads source assets using [provider] and then uses [transformerPhases] |
| + /// to generate output files from them. |
| + //TODO(rnystrom): Better way of specifying transformers and their ordering. |
| + AssetGraph(this._provider, |
| + Iterable<Iterable<Transformer>> transformerPhases) { |
| + // Flatten the phases to a list so we can traverse backwards to wire up |
| + // each phase to its next. |
| + transformerPhases = transformerPhases.toList(); |
| + |
| + // Each phase writes its outputs as inputs to the next phase after it. |
| + // Add a phase at the end for the final outputs of the last phase. |
| + transformerPhases.add([]); |
| + |
| + Phase nextPhase = null; |
| + for (var i = transformerPhases.length - 1; i >= 0; i--) { |
|
nweiz
2013/06/20 23:06:08
List has a [reversed] getter these days :).
Bob Nystrom
2013/06/21 00:13:20
Done.
|
| + nextPhase = new Phase(this, _phases.length, |
| + transformerPhases[i].toList(), nextPhase); |
| + _phases.insert(0, nextPhase); |
| + } |
| + } |
| + |
| + /// Gets the asset identified by [id]. |
| + /// |
| + /// If [id] is for a generated or transformed asset, this will wait until |
| + /// it has been created and return it. If the asset cannot be found, throws |
| + /// [AssetNotFoundException]. |
| + Future<Asset> getAssetById(AssetId id) { |
| + // TODO(rnystrom): Waiting for the entire build to complete is unnecessary |
| + // in some cases. Should optimize: |
| + // * [id] may be generated before the compilation is finished. We should |
| + // be able to quickly check whether there are any more in-place |
| + // transformations that can be run on it. If not, we can return it early. |
| + // * If everything is compiled, something that didn't output [id] is |
| + // dirtied, and then [id] is requested, we can return it immediately, |
| + // since anything overwriting it at that point is an error. |
| + // * If [id] has never been generated and all active transformers provide |
| + // metadata about the file names of assets it can emit, we can prove that |
| + // none of them can emit [id] and fail early. |
| + return _waitForProcess().then((_) { |
| + // Each phase's inputs are the outputs of the previous phase. Find the |
| + // last phase that contains the asset. Since the last phase has no |
| + // transformers, this will find the latest output for that id. |
| + // TODO(rnystrom): Currently does not omit assets that are actually used |
|
nweiz
2013/06/20 23:06:08
Style nit: I like separating informative comments
Bob Nystrom
2013/06/21 00:13:20
Done.
|
| + // as inputs for transformers. This means you can request and get a |
|
nweiz
2013/06/20 23:06:08
"a an" -> "an"
Bob Nystrom
2013/06/21 00:13:20
Done.
|
| + // an asset that should be "consumed" because it's used to generate the |
| + // real asset you care about. Need to figure out how we want to handle |
| + // that and what use cases there are related to it. |
| + for (var i = _phases.length - 1; i >= 0; i--) { |
| + var node = _phases[i].inputs[id]; |
| + if (node != null) { |
| + // By the time we get here, the asset should have been built. |
| + assert(node.asset != null); |
| + return node.asset; |
| + } |
| + } |
| + |
| + // Couldn't find it. |
| + var error = new AssetNotFoundException(id); |
| + reportError(error); |
| + throw error; |
| + }); |
| + } |
| + |
| + /// Adds [sources] to the graph's known set of source assets. |
| + /// |
| + /// Begins applying any transforms that can consume any of the sources. If a |
| + /// given source is already known, it is considered modified and all |
| + /// transforms that use it will be re-applied. |
| + void updateSources(Iterable<AssetId> sources) { |
| + if (_sourceChanges == null) _sourceChanges = new ChangeBatch(); |
| + _sourceChanges.update(sources); |
| + |
| + _waitForProcess(); |
| + } |
| + |
| + /// Removes [removed] from the graph's known set of source assets. |
| + void removeSources(Iterable<AssetId> removed) { |
| + if (_sourceChanges == null) _sourceChanges = new ChangeBatch(); |
| + _sourceChanges.remove(removed); |
| + |
| + _waitForProcess(); |
| + } |
| + |
| + /// Reports a process result with the given error then throws it. |
| + void reportError(error) { |
| + _resultsController.add(new ProcessResult(error)); |
| + } |
| + |
| + /// 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; |
| + return _processDone = _process().whenComplete(() { |
| + _processDone = null; |
| + // Report the build completion. |
| + // TODO(rnystrom): Put some useful data in here. |
| + _resultsController.add(new ProcessResult()); |
| + }); |
| + } |
| + |
| + /// Starts the background processing. |
| + /// |
| + /// Returns a future that completes when all assets have been processed. |
| + Future _process() { |
| + return _processSourceChanges().then((_) { |
| + // 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 (_sourceChanges != null) return _process(); |
| + |
| + // Otherwise, everything is done. |
| + return; |
| + } |
| + |
| + // Process that phase and then loop onto the next. |
| + return future.then((_) => _process()); |
| + }); |
| + } |
| + |
| + /// Processes the current batch of changes to source assets. |
| + Future _processSourceChanges() { |
| + // Always pump the event loop. This ensures a bunch of synchronous source |
| + // changes are processed in a single batch even when the first one starts |
| + // the build process. |
| + return new Future(() { |
| + if (_sourceChanges == null) return null; |
| + |
| + // Take the current batch to ensure it doesn't get added to while we're |
| + // processing it. |
| + var changes = _sourceChanges; |
| + _sourceChanges = null; |
| + |
| + var updated = new Map<AssetId, Asset>(); |
| + var futures = []; |
| + for (var id in changes.updated) { |
| + // TODO(rnystrom): Catch all errors from provider and route to results. |
| + futures.add(_provider.getAsset(id).then((asset) { |
| + updated[id] = asset; |
| + })); |
| + } |
| + |
| + return Future.wait(futures).then((_) { |
| + _phases.first.updateInputs(updated, changes.removed); |
| + }); |
| + }); |
| + } |
| +} |
| + |
| +/// Used to report build results back from the asynchronous build process |
| +/// running in the background. |
| +class ProcessResult { |
| + /// The error that occurred, or `null` if the result is not an error. |
| + final error; |
| + |
| + ProcessResult([this.error]); |
| +} |