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 338910d5477b5b364ccfa31febf5a01d48856a5d..40a615cbd462356708b37dee8ba4f4e35500dac6 100644 |
--- a/pkg/barback/lib/src/asset_cascade.dart |
+++ b/pkg/barback/lib/src/asset_cascade.dart |
@@ -11,7 +11,8 @@ import 'package:stack_trace/stack_trace.dart'; |
import 'asset.dart'; |
import 'asset_id.dart'; |
-import 'asset_set.dart'; |
+import 'asset_node.dart'; |
+import 'cancelable_future.dart'; |
import 'errors.dart'; |
import 'change_batch.dart'; |
import 'package_graph.dart'; |
@@ -38,6 +39,17 @@ class AssetCascade { |
/// the current app. |
final PackageGraph _graph; |
+ /// The controllers for the [AssetNode]s that provide information about this |
+ /// cascade's package's source assets. |
+ final _sourceControllerMap = new Map<AssetId, AssetNodeController>(); |
+ |
+ /// Futures for source assets that are currently being loaded. |
+ /// |
+ /// These futures are cancelable so that if an asset is updated after a load |
+ /// has been kicked off, the previous load can be ignored in favor of a new |
+ /// one. |
+ final _loadingSources = new Map<AssetId, CancelableFuture<Asset>>(); |
+ |
final _phases = <Phase>[]; |
/// A stream that emits a [BuildResult] each time the build is completed, |
@@ -68,7 +80,9 @@ class AssetCascade { |
/// If no build it in progress, is `null`. |
Future _processDone; |
- ChangeBatch _sourceChanges; |
+ /// Whether any source assets have been updated or removed since processing |
+ /// last began. |
+ var _newChanges = false; |
/// Creates a new [AssetCascade]. |
/// |
@@ -106,54 +120,95 @@ class AssetCascade { |
// * [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 (_processDone == null ? new Future.value() : _processDone).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 |
- // as inputs for transformers. This means you can request and get 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; |
- } |
+ return newFuture(() { |
+ var node = _getAssetNode(id); |
+ |
+ // If the requested asset is available, we can just return it. |
+ if (node != null) return node.asset; |
+ |
+ // If there's a build running, that build might generate the asset, so we |
+ // wait for it to complete and then try again. |
+ if (_processDone != null) { |
+ return _processDone.then((_) => getAssetById(id)); |
} |
- // Couldn't find it. |
+ // If the asset hasn't been built and nothing is building now, the asset |
+ // won't be generated, so we throw an error. |
throw new AssetNotFoundException(id); |
}); |
} |
+ // Returns the post-transformation asset node for [id], if one is available. |
+ // |
+ // This will only return a node that has an asset available, and only if that |
+ // node is guaranteed not to be consumed by any transforms. If the phase is |
+ // still working to figure out if a node will be consumed by a transformer, |
+ // that node won't be returned. |
+ AssetNode _getAssetNode(AssetId id) { |
+ // 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. |
+ for (var i = _phases.length - 1; i >= 0; i--) { |
+ var node = _phases[i].getUnconsumedInput(id); |
+ if (node != null) return node; |
+ } |
+ |
+ return null; |
+ } |
+ |
/// 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(); |
- assert(sources.every((id) => id.package == package)); |
- _sourceChanges.update(sources); |
+ _newChanges = true; |
+ |
+ for (var id in sources) { |
+ var controller = _sourceControllerMap[id]; |
+ if (controller != null) { |
+ controller.setDirty(); |
+ } else { |
+ _sourceControllerMap[id] = new AssetNodeController(id); |
+ _phases.first.addInput(_sourceControllerMap[id].node); |
+ } |
+ |
+ // If this source was already loading, cancel the old load, since it may |
+ // return out-of-date contents for the asset. |
+ if (_loadingSources.containsKey(id)) _loadingSources[id].cancel(); |
+ |
+ _loadingSources[id] = |
+ new CancelableFuture<Asset>(_graph.provider.getAsset(id)); |
+ _loadingSources[id].whenComplete(() { |
+ _loadingSources.remove(id); |
+ }).then((asset) { |
+ var controller = _sourceControllerMap[id].setAvailable(asset); |
+ }).catchError((error) { |
+ reportError(error); |
+ |
+ // TODO(nweiz): propagate error information through asset nodes. |
+ _sourceControllerMap.remove(id).setRemoved(); |
+ }); |
+ } |
_waitForProcess(); |
} |
/// Removes [removed] from the graph's known set of source assets. |
void removeSources(Iterable<AssetId> removed) { |
- if (_sourceChanges == null) _sourceChanges = new ChangeBatch(); |
- assert(removed.every((id) => id.package == package)); |
- _sourceChanges.remove(removed); |
+ _newChanges = true; |
+ |
+ removed.forEach((id) { |
+ // If the source was being loaded, cancel that load. |
+ if (_loadingSources.containsKey(id)) _loadingSources.remove(id).cancel(); |
+ |
+ var controller = _sourceControllerMap.remove(id); |
+ // Don't choke if an id is double-removed for some reason. |
+ if (controller != null) controller.setRemoved(); |
+ }); |
_waitForProcess(); |
} |
@@ -198,7 +253,8 @@ class AssetCascade { |
/// |
/// Returns a future that completes when all assets have been processed. |
Future _process() { |
- return _processSourceChanges().then((_) { |
+ _newChanges = false; |
+ return newFuture(() { |
// Find the first phase that has work to do and do it. |
var future; |
for (var phase in _phases) { |
@@ -209,7 +265,7 @@ class AssetCascade { |
// 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(); |
+ if (_newChanges) return _process(); |
// Otherwise, everything is done. |
return; |
@@ -219,42 +275,6 @@ class AssetCascade { |
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 newFuture(() { |
- 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 AssetSet(); |
- var futures = []; |
- for (var id in changes.updated) { |
- // TODO(rnystrom): Catch all errors from provider and route to results. |
- futures.add(_graph.provider.getAsset(id).then((asset) { |
- updated.add(asset); |
- }).catchError((error) { |
- if (error is AssetNotFoundException) { |
- // Handle missing asset errors like regular missing assets. |
- reportError(error); |
- } else { |
- // It's an unexpected error, so rethrow it. |
- throw error; |
- } |
- })); |
- } |
- |
- return Future.wait(futures).then((_) { |
- _phases.first.updateInputs(updated, changes.removed); |
- }); |
- }); |
- } |
} |
/// An event indicating that the cascade has finished building all assets. |