// 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 = FutureOr Function(); typedef DisposeInstance = FutureOr Function(T instance); typedef BeforeExit = FutureOr 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._(), /// dispose: (something) => something._dispose(), /// beforeExit: (something) => something._beforeExit()); /// /// class SomeResource { /// SomeResource._(); /// /// Future 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 { /// Factory method which creates an instance of this resource. final CreateInstance _create; /// Optional method which is given an existing instance that is ready to be /// disposed. final DisposeInstance? _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 = >{}; Resource(this._create, {DisposeInstance? dispose, BeforeExit? beforeExit}) : _userDispose = dispose, _userBeforeExit = beforeExit; /// Fetches an actual instance of this resource for [manager]. Future _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 _dispose(ResourceManager manager) { assert(_instanceByManager.containsKey(manager)); final 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 = >{}; /// 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 = >{}; /// Fetches an instance of [resource]. Future fetch(Resource 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 disposeAll() { final 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 beforeExit() async { await Future.wait( _resourcesWithBeforeExit.map((r) async { return r._userBeforeExit?.call(); }), ); _resourcesWithBeforeExit.clear(); } }