blob: 5a91108ecd6cee347208a4f3c1fd448997c03a9e [file] [log] [blame]
// Copyright (c) 2019, 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:convert';
import 'package:async/async.dart';
import 'package:build/build.dart';
import 'package:crypto/crypto.dart';
import 'meta_module.dart';
import 'modules.dart';
Map<String, dynamic> _deserialize(List<int> bytes) =>
jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
List<int> _serialize(Map<String, dynamic> data) =>
utf8.encode(jsonEncode(data));
final metaModuleCache = DecodingCache.resource(
(m) => MetaModule.fromJson(_deserialize(m)), (m) => _serialize(m.toJson()));
final moduleCache = DecodingCache.resource(
(m) => Module.fromJson(_deserialize(m)), (m) => _serialize(m.toJson()));
/// A cache of objects decoded from written assets suitable for use as a
/// [Resource].
///
/// Instances that are decoded will be cached throughout the duration of a build
/// and invalidated between builds. Instances that are shared through the cache
/// should be treated as immutable to avoid leaking any information which was
/// not loaded from the underlying asset.
class DecodingCache<T> {
/// Create a [Resource] which can decoded instances of [T] serialized via json
/// to assets.
static Resource<DecodingCache<T>> resource<T>(
T Function(List<int>) fromBytes, List<int> Function(T) toBytes) =>
Resource<DecodingCache<T>>(() => DecodingCache._(fromBytes, toBytes),
dispose: (c) => c._dispose());
final _cached = <AssetId, _Entry<T>>{};
final T Function(List<int>) _fromBytes;
final List<int> Function(T) _toBytes;
DecodingCache._(this._fromBytes, this._toBytes);
void _dispose() {
_cached.removeWhere((_, entry) => entry.digest == null);
for (var entry in _cached.values) {
entry.needsCheck = true;
}
}
/// Find and deserialize a [T] stored in [id].
///
/// If the asset at [id] is unreadable the returned future will resolve to
/// `null`. If the instance is cached it will not be decoded again, but the
/// content dependencies will be tracked through [reader].
Future<T> find(AssetId id, AssetReader reader) async {
if (!await reader.canRead(id)) return null;
_Entry<T> entry;
if (!_cached.containsKey(id)) {
entry = _cached[id] = _Entry()
..needsCheck = false
..value = Result.capture(reader.readAsBytes(id).then(_fromBytes))
..digest = Result.capture(reader.digest(id));
} else {
entry = _cached[id];
if (entry.needsCheck) {
await (entry.onGoingCheck ??= () async {
var previousDigest = await Result.release(entry.digest);
entry.digest = Result.capture(reader.digest(id));
if (await Result.release(entry.digest) != previousDigest) {
entry.value =
Result.capture(reader.readAsBytes(id).then(_fromBytes));
}
entry
..needsCheck = false
..onGoingCheck = null;
}());
}
}
return Result.release(entry.value);
}
/// Serialized and write a [T] to [id].
///
/// The instance will be cached so that later calls to [find] may return the
/// instances without deserializing it.
Future<void> write(AssetId id, AssetWriter writer, T instance) async {
await writer.writeAsBytes(id, _toBytes(instance));
_cached[id] = _Entry()
..needsCheck = false
..value = Result.capture(Future.value(instance));
}
}
class _Entry<T> {
bool needsCheck = false;
Future<Result<T>> value;
Future<Result<Digest>> digest;
Future<void> onGoingCheck;
}