blob: 587f2bdb4f1610700e2b483d51d0bee41dfd49d2 [file] [log] [blame]
// 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_node;
import 'dart:async';
import 'asset.dart';
import 'asset_id.dart';
import 'errors.dart';
import 'phase.dart';
import 'transform_node.dart';
/// Describes the current state of an asset as part of a transformation graph.
///
/// 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 {
/// The id of the asset that this node represents.
final AssetId id;
/// The transform that created this asset node.
///
/// This is `null` for source assets.
final TransformNode transform;
/// 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;
/// 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);
/// 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);
/// 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) {
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;
});
});
}
/// 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, this.transform)
: _state = AssetState.DIRTY;
AssetNode._available(Asset asset, this.transform)
: 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, [TransformNode transform])
: node = new AssetNode._(id, transform);
/// Creates a controller for an available node with the given concrete
/// [asset].
AssetNodeController.available(Asset asset, [TransformNode transform])
: node = new AssetNode._available(asset, transform);
/// 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;
}