| 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..6e7789a91fb766b5596573fbac3c41738c21f0aa 100644
|
| --- a/pkg/barback/lib/src/asset_node.dart
|
| +++ b/pkg/barback/lib/src/asset_node.dart
|
| @@ -8,30 +8,189 @@ 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;
|
| + 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;
|
|
|
| - /// The [TransformNode]s that consume this node's asset as an input.
|
| - final consumers = new Set<TransformNode>();
|
| + /// 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);
|
|
|
| - AssetId get id => asset.id;
|
| + /// Returns a Future that completes when the node's asset is available.
|
| + ///
|
| + /// If the asset is currently available, this completes 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 completes
|
| + /// synchronously.
|
| + Future get whenRemoved => _waitForState((state) => state.isRemoved);
|
|
|
| - AssetNode(this.asset);
|
| + /// 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();
|
| + });
|
|
|
| - /// 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);
|
| + return callback(asset).then((result) {
|
| + subscription.cancel();
|
| +
|
| + // 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);
|
| + return result;
|
| + });
|
| + });
|
| + }
|
|
|
| - this.asset = asset;
|
| - consumers.forEach((consumer) => consumer.dirty());
|
| + /// Returns a Future that completes as soon as the node is in a state that
|
| + /// matches [test].
|
| + ///
|
| + /// The Future completes 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;
|
| +
|
| + /// Creates a controller for a dirty node.
|
| + AssetNodeController(AssetId id)
|
| + : node = new AssetNode._(id);
|
| +
|
| + /// Creates a controller for an available node with the given concrete
|
| + /// [asset].
|
| + AssetNodeController.available(Asset asset)
|
| + : node = new AssetNode._available(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);
|
| + }
|
| +
|
| + /// 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 will get a new node.
|
| + void setRemoved() {
|
| + assert(node._state != AssetState.REMOVED);
|
| + node._state = AssetState.REMOVED;
|
| + node._asset = null;
|
| + node._stateChangeController.add(AssetState.REMOVED);
|
| + }
|
| +
|
| + /// 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);
|
| + 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 node has a concrete asset loaded, available, and up-to-date. The asset
|
| + /// is accessible via [AssetNode.asset]. An asset can only be marked available
|
| + /// again from the [AssetState.DIRTY] state.
|
| + static final AVAILABLE = const AssetState._("available");
|
| +
|
| + /// The asset is no longer available, possibly for good. A removed asset will
|
| + /// never enter another state.
|
| + static final REMOVED = const AssetState._("removed");
|
| +
|
| + /// The asset will exist in the future (unless it's removed), but the concrete
|
| + /// asset is not yet available.
|
| + static final DIRTY = const AssetState._("dirty");
|
| +
|
| + /// 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;
|
| +
|
| + const AssetState._(this.name);
|
| +
|
| + String toString() => name;
|
| }
|
|
|