blob: a1f73ea95eb981047bba62d610b48b68e9e2601c [file] [log] [blame]
// Copyright (c) 2017, 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.
import 'dart:async';
typedef CreateInstance<T> = FutureOr<T> Function();
typedef DisposeInstance<T> = FutureOr<void> Function(T instance);
typedef BeforeExit = FutureOr<void> Function();
/// A handle to create and cleanup some expensive object based on a lifecycle
/// defined by the build system.
///
/// It is unsafe to read or write global state during a build. The build system
/// may run incompatible builds where previously written global state is
/// invalid. [Resource] bridges the gap and allows a pattern for communicating
/// "global" level information during a build, with hooks to maintain isolation
/// between separate builds.
///
/// Reuse is based on the [Resource] identity. To allow for sharing within a
/// build, each value should be fetched with the same instance. Commonly the
/// [Resource] is a global variable. The values of type [T] should not be reused
/// or shared outside of the reuse provided by the build system through
/// `BuildStep.fetchResource`.
///
/// If a `dispose` callback is available it will be called between builds and it
/// should clean up any state that may not be valid on a subsequent build. If no
/// `dispose` callback is passed the value will be discarded between builds.
/// Only "universal" state should be retained by the instance after `dispose`.
/// Any state which is particular to a single build should be cleared or marked
/// dirty during dispose, and validated before subsequent use. For instance,
/// within a given build no asset content will change, however on subsequent
/// builds assets may have difference content. Asset digests may be useful for
/// validating caches that can be reused between builds.
///
/// If a `beforeExit` callback is available it will be called before a clean
/// exit of the build system for any resources fetched during any build.
///
/// The [Resource] lifecycle helps with the problem of leaking state across
/// separate builds, but it does not help with the problem of leaking state
/// during a single build. For consistent output and correct rerunning of
/// builders, the build system needs to track all of the inputs - any
/// information that is read - for a given build step.
///
/// Most resources should accept a `BuildStep`, or an `AssetReader` argument
/// and ensure that any assets which contribute to the result have an
/// interaction for each builder which uses that result. For example if a
/// resource is caching the result of an expensive computation on some asset, it
/// might read the asset and perform some work the first time it is used, and
/// call only `AssetReader.canRead` to verify the caller is allowed to access
/// the information before returning the cached result on subsequent calls.
///
/// Build system implementations should be the only users that directly
/// instantiate a [ResourceManager] since they can handle the lifecycle
/// guarantees in a sane way.
///
/// ```dart
/// final someResource = Resource<SomeResource>(() => SomeResource._(),
/// dispose: (something) => something._dispose(),
/// beforeExit: (something) => something._beforeExit());
///
/// class SomeResource {
/// SomeResource._();
///
/// Future<String> somethingUsefulForBuilders(AssetReader assetReader) async {
/// // Any information returned to the caller should be derived from content
/// // read through `assetReader`. Checking `assetReader.canRead` can
/// // prevent information leaks and allow the build system to track
/// // dependencies.
/// }
///
/// void _dispose() {
/// // Clear or invalidate any cached state that was read during the build.
/// }
///
/// void _beforeExit() {
/// // Shutdown or clean up any externally held resources.
/// }
/// }
/// ```
class Resource<T> {
/// Factory method which creates an instance of this resource.
final CreateInstance<T> _create;
/// Optional method which is given an existing instance that is ready to be
/// disposed.
final DisposeInstance<T>? _userDispose;
/// Optional method which is called before the process is going to exit.
///
/// This allows resources to do any final cleanup, and is not given an
/// instance.
final BeforeExit? _userBeforeExit;
/// A Future instance of this resource if one has ever been requested.
final _instanceByManager = <ResourceManager, Future<T>>{};
Resource(this._create, {DisposeInstance<T>? dispose, BeforeExit? beforeExit})
: _userDispose = dispose,
_userBeforeExit = beforeExit;
/// Fetches an actual instance of this resource for [manager].
Future<T> _fetch(ResourceManager manager) =>
_instanceByManager.putIfAbsent(manager, () async => await _create());
/// Disposes the actual instance of this resource for [manager] if present.
///
/// If there is no [_userDispose] the entire instance is assumed stale and it
/// is discarded, a fresh instance will be created with [_create] the next
/// time it is fetched.
///
/// If a [_userDispose] was provided, invoke it and assume the state can be
/// retained for the next build.
Future<void> _dispose(ResourceManager manager) {
assert(_instanceByManager.containsKey(manager));
var oldInstance = _instanceByManager[manager]!;
if (_userDispose != null) {
return oldInstance.then(_userDispose!);
} else {
_instanceByManager.remove(manager);
return Future.value(null);
}
}
}
/// Manages fetching and disposing of a group of [Resource]s.
///
/// This is an internal only API which should only be used by build system
/// implementations and not general end users. Instead end users should use
/// the `buildStep#fetchResource` method to get [Resource]s.
class ResourceManager {
final _resources = <Resource<void>>{};
/// The [Resource]s that we need to call `beforeExit` on.
///
/// We have to hang on to these forever, but they should be small in number,
/// and we don't hold on to the actual created instances, just the [Resource]
/// instances.
final _resourcesWithBeforeExit = <Resource<void>>{};
/// Fetches an instance of [resource].
Future<T> fetch<T>(Resource<T> resource) async {
if (resource._userBeforeExit != null) {
_resourcesWithBeforeExit.add(resource);
}
_resources.add(resource);
return resource._fetch(this);
}
/// Disposes of all [Resource]s fetched since the last call to [disposeAll].
Future<void> disposeAll() {
var done = Future.wait(_resources.map((r) => r._dispose(this)));
_resources.clear();
return done.then((_) => null);
}
/// Invokes the `beforeExit` callbacks of all [Resource]s that had one.
Future<void> beforeExit() async {
await Future.wait(_resourcesWithBeforeExit.map((r) async {
return r._userBeforeExit?.call();
}));
_resourcesWithBeforeExit.clear();
}
}