| // 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(); |
| } |
| } |