Index: pkg/barback/lib/src/asset_node.dart |
diff --git a/pkg/barback/lib/src/asset_node.dart b/pkg/barback/lib/src/asset_node.dart |
index ae2b70eea202f92caab48bbbfed1f68c5bd01633..d626455b7a64398859068d1843060242493c4832 100644 |
--- a/pkg/barback/lib/src/asset_node.dart |
+++ b/pkg/barback/lib/src/asset_node.dart |
@@ -8,30 +8,195 @@ import 'dart:async'; |
import 'asset.dart'; |
import 'asset_id.dart'; |
+import 'errors.dart'; |
import 'phase.dart'; |
import 'transform_node.dart'; |
-/// Describes an asset and its relationship to the build dependency graph. |
+/// Describes the current state of an asset as part of a transformation graph. |
/// |
-/// Keeps a cache of the last asset that was built for this node (i.e. for this |
-/// node's ID and phase) and tracks which transforms depend on it. |
+/// An asset node can be in one of three states (see [AssetState]). It provides |
+/// an [onStateChange] stream that emits an event whenever it changes state. |
+/// |
+/// Asset nodes are controlled using [AssetNodeController]s. |
class AssetNode { |
- Asset asset; |
+ /// The id of the asset that this node represents. |
+ final AssetId id; |
+ |
+ /// The current state of the asset node. |
+ AssetState get state => _state; |
+ AssetState _state; |
+ |
+ /// The concrete asset that this node represents. |
+ /// |
+ /// This is null unless [state] is [AssetState.AVAILABLE]. |
+ Asset get asset => _asset; |
Bob Nystrom
2013/07/31 20:05:41
Is it an error to access this when not available?
nweiz
2013/07/31 22:47:53
I think it's reasonable to access it and check for
|
+ Asset _asset; |
+ |
+ /// A broadcast stream that emits an event whenever the node changes state. |
+ /// |
+ /// This stream is synchronous to ensure that when a source asset is modified |
+ /// or removed, the appropriate portion of the asset graph is dirtied before |
+ /// any [Barback.getAssetById] calls emit newly-incorrect values. |
+ Stream<AssetState> get onStateChange => _stateChangeController.stream; |
+ |
+ /// This is synchronous so that a source being updated will always be |
+ /// propagated through the build graph before anything that depends on it is |
+ /// requested. |
+ final _stateChangeController = |
+ new StreamController<AssetState>.broadcast(sync: true); |
Bob Nystrom
2013/07/31 20:05:41
Indent +2.
nweiz
2013/07/31 22:47:53
Done.
|
+ |
+ /// Returns a Future that completes when the node's asset is available. |
+ /// |
+ /// If the asset is currently available, this will complete synchronously to |
+ /// ensure that the asset is still available in the [Future.then] callback. |
+ /// |
+ /// If the asset is removed before becoming available, this will throw an |
+ /// [AssetNotFoundException]. |
+ Future<Asset> get whenAvailable { |
+ return _waitForState((state) => state.isAvailable || state.isRemoved) |
+ .then((state) { |
+ if (state.isRemoved) throw new AssetNotFoundException(id); |
+ return asset; |
+ }); |
+ } |
+ |
+ /// Returns a Future that completes when the node's asset is removed. |
+ /// |
+ /// If the asset is already removed when this is called, it will complete |
Bob Nystrom
2013/07/31 20:05:41
"will complete" -> "completes"
nweiz
2013/07/31 22:47:53
Done.
|
+ /// synchronously. |
+ Future get whenRemoved => _waitForState((state) => state.isRemoved); |
+ |
+ /// Runs [callback] repeatedly until the node's asset has maintained the same |
+ /// value for the duration. |
+ /// |
+ /// This will run [callback] as soon as the asset is available (synchronously |
+ /// if it's available immediately). If the [state] changes at all while |
+ /// waiting for the Future returned by [callback] to complete, it will be |
+ /// re-run as soon as it completes and the asset is available again. This will |
+ /// continue until [state] doesn't change at all. |
+ /// |
+ /// If this asset is removed, this will throw an [AssetNotFoundException] as |
+ /// soon as [callback]'s Future is finished running. |
+ Future tryUntilStable(Future callback(Asset asset)) { |
+ return whenAvailable.then((asset) { |
+ var modifiedDuringCallback = false; |
+ var subscription; |
+ subscription = onStateChange.listen((_) { |
+ modifiedDuringCallback = true; |
+ subscription.cancel(); |
+ }); |
+ |
+ return callback(asset).then((result) { |
+ // If the asset was modified at all while running the callback, the |
+ // result was invalid and we should try again. |
+ if (modifiedDuringCallback) return tryUntilStable(callback); |
+ subscription.cancel(); |
Bob Nystrom
2013/07/31 20:05:41
Move this above the if statement.
nweiz
2013/07/31 22:47:53
Done.
|
+ return result; |
+ }); |
+ }); |
+ } |
+ |
+ /// Returns a Future that completes as soon as the node is in a state that |
+ /// matches [test]. |
+ /// |
+ /// The Future will complete synchronously if this is already in such a state. |
+ Future<AssetState> _waitForState(bool test(AssetState state)) { |
+ if (test(state)) return new Future.sync(() => state); |
+ return onStateChange.firstWhere(test); |
+ } |
+ |
+ AssetNode._(this.id) |
+ : _state = AssetState.DIRTY; |
+ |
+ AssetNode._available(Asset asset) |
+ : id = asset.id, |
+ _asset = asset, |
+ _state = AssetState.AVAILABLE; |
+} |
+ |
+/// The controller for an [AssetNode]. |
+/// |
+/// This controls which state the node is in. |
+class AssetNodeController { |
+ final AssetNode node; |
- /// The [TransformNode]s that consume this node's asset as an input. |
- final consumers = new Set<TransformNode>(); |
+ /// Creates a controller for a dirty node. |
+ AssetNodeController(AssetId id) |
+ : node = new AssetNode._(id); |
- AssetId get id => asset.id; |
+ /// Creates a controller for an available node with the given concrete |
+ /// [asset]. |
+ AssetNodeController.available(Asset asset) |
+ : node = new AssetNode._available(asset); |
- AssetNode(this.asset); |
+ /// Marks the node as [AssetState.DIRTY]. |
+ void setDirty() { |
+ assert(node._state != AssetState.REMOVED); |
+ node._state = AssetState.DIRTY; |
+ node._asset = null; |
+ node._stateChangeController.add(AssetState.DIRTY); |
+ } |
- /// Updates this node's generated asset value and marks all transforms that |
- /// use this as dirty. |
- void updateAsset(Asset asset) { |
- // Cannot update an asset to one with a different ID. |
- assert(id == asset.id); |
+ /// Marks the node as [AssetState.REMOVED]. |
+ /// |
+ /// Once a node is marked as removed, it can't be marked as any other state. |
+ /// If a new asset is created with the same id, it should have a new node. |
Bob Nystrom
2013/07/31 20:05:41
"should have" -> "will get"
nweiz
2013/07/31 22:47:53
Done.
|
+ void setRemoved() { |
+ assert(node._state != AssetState.REMOVED); |
+ node._state = AssetState.REMOVED; |
+ node._asset = null; |
+ node._stateChangeController.add(AssetState.REMOVED); |
+ } |
- this.asset = asset; |
- consumers.forEach((consumer) => consumer.dirty()); |
+ /// Marks the node as [AssetState.AVAILABLE] with the given concrete [asset]. |
+ /// |
+ /// It's an error to mark an already-available node as available. It should be |
+ /// marked as dirty first. |
+ void setAvailable(Asset asset) { |
+ assert(asset.id == node.id); |
+ assert(node._state != AssetState.REMOVED); |
+ assert(node._state != AssetState.AVAILABLE); |
Bob Nystrom
2013/07/31 20:05:41
assert(node._state == AssetState.DIRTY);
nweiz
2013/07/31 22:47:53
I intentionally avoided that because I'm planning
|
+ node._state = AssetState.AVAILABLE; |
+ node._asset = asset; |
+ node._stateChangeController.add(AssetState.AVAILABLE); |
} |
} |
+ |
+// TODO(nweiz): add an error state. |
+/// An enum of states that an [AssetNode] can be in. |
+class AssetState { |
+ /// The state of having an asset available. |
Bob Nystrom
2013/07/31 20:05:41
This sentence is strange. Just ditch it and use th
nweiz
2013/07/31 22:47:53
Done.
|
+ /// |
+ /// This state indicates that the node has a concrete asset loaded, available, |
Bob Nystrom
2013/07/31 20:05:41
Remove "This state indicates that".
nweiz
2013/07/31 22:47:53
Done.
|
+ /// and up-to-date. The asset is accessible via [AssetNode.asset]. An |
+ /// available asset may be marked available again, but first it will be marked |
+ /// [AssetState.DIRTY]. |
Bob Nystrom
2013/07/31 20:05:41
This reads weird. Are you saying that an asset in
nweiz
2013/07/31 22:47:53
Done.
|
+ static final AVAILABLE = new AssetState._("available"); |
+ |
+ /// The state of being removed. |
+ /// |
+ /// This state indicates that the asset is no longer available, possibly for |
+ /// good. A removed asset will never enter another state. |
Bob Nystrom
2013/07/31 20:05:41
Same as above. Remove first sentence, use this one
nweiz
2013/07/31 22:47:53
Done.
|
+ static final REMOVED = new AssetState._("removed"); |
+ |
+ /// The state of waiting to be generated. |
+ /// |
+ /// This state indicates that the asset will exist in the future (unless it's |
+ /// removed), but the concrete asset is not yet available. |
Bob Nystrom
2013/07/31 20:05:41
Ditto.
nweiz
2013/07/31 22:47:53
Done.
|
+ static final DIRTY = new AssetState._("dirty"); |
Bob Nystrom
2013/07/31 20:05:41
Make these all constants instead of final, or name
nweiz
2013/07/31 22:47:53
Done.
|
+ |
+ /// Whether this state is [AssetState.AVAILABLE]. |
+ bool get isAvailable => this == AssetState.AVAILABLE; |
+ |
+ /// Whether this state is [AssetState.REMOVED]. |
+ bool get isRemoved => this == AssetState.REMOVED; |
+ |
+ /// Whether this state is [AssetState.DIRTY]. |
+ bool get isDirty => this == AssetState.DIRTY; |
+ |
+ final String name; |
+ |
+ AssetState._(this.name); |
Bob Nystrom
2013/07/31 20:05:41
Make this a const constructor.
nweiz
2013/07/31 22:47:53
Done.
|
+ |
+ String toString() => name; |
+} |