blob: e043ba68212bb9e1147eff22c9d4d9a6b455740b [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is hand-formatted.
import 'dart:async';
import 'dart:convert';
import '../dart/model.dart';
/// Signature for the callback passed to [DynamicContent.subscribe].
typedef SubscriptionCallback = void Function(Object value);
/// Returns a copy of a data structure if it consists of only [DynamicMap]s,
/// [DynamicList]s, [int]s, [double]s, [bool]s, and [String]s.
///
/// This is relatively expensive as the entire data structure must be walked and
/// new objects created.
Object? deepClone(Object? template) {
if (template == null) {
return null;
} else if (template is DynamicMap) {
return template.map((String key, Object? value) => MapEntry<String, Object?>(key, deepClone(value)));
} else if (template is DynamicList) {
return template.map((Object? value) => deepClone(value)).toList();
} else {
assert(template is int || template is double || template is bool || template is String, 'unexpected state object type: ${template.runtimeType} ($template)');
return template;
}
}
/// Configuration data from the remote widgets.
///
/// Typically this represents the data model, and is updated frequently (or at
/// least, more frequently than the remote widget definitions) by the server
/// (or, indeed, by local code, in response to events or other activity).
///
/// ## Structure
///
/// A [DynamicContent] object represents a tree. A consumer (the remote widgets)
/// can subscribe to a node to obtain its value.
///
/// The root of a [DynamicContent] tree is a map of string-value pairs. The
/// values are:
///
/// * Other maps of string-value pairs ([DynamicMap]).
/// * Lists of values ([DynamicList]).
/// * Booleans ([bool]).
/// * Integers ([int]).
/// * Doubles ([double]).
/// * Strings ([String]).
///
/// The keys in the root map are independently updated. Typically each
/// represents a different category of data from the server that the server
/// updates independently, e.g. theming information and the app state might be
/// provided separately.
///
/// ## Updates
///
/// Data is updated using [update] and [updateAll]. The objects passed to those
/// methods are of the types described above.
///
/// Objects for [update] can be obtained in several ways:
///
/// 1. Dart maps, lists, and literals of the types given above ("raw data") can
/// be created directly in code. This is useful for configuring remote
/// widgets with local client information such as the current time, GPS
/// coordinates, system settings like dark mode, window dimensions, etc,
/// where the data was never encoded in the first place.
///
/// 2. A Remote Flutter Widgets binary data blob can be parsed using
/// [decodeDataBlob]. This is the preferred method for decoding data obtained
/// from the network. See [encodeDataBlob] for a function that generates data
/// in this format.
///
/// 3. A Remote Flutter Widgets text data file can be parsed using
/// [parseTextDataFile]. Decoding this text format is about ten times slower
/// than decoding the binary format and about five times slower than decoding
/// JSON, so it is discouraged in production applications. This text
/// representation of the Remote Flutter Widgets binary data blob format is
/// similar to JSON. This form is typically not used in applications; it is
/// more common for this format to be used on the server side, parsed and
/// then encoded in binary form for transmission to the client.
///
/// 4. Data in JSON form can be decoded using [JsonCodec.decode] (typically
/// using the [json] object); the JSON decoder creates the same types of data
/// structures as expected by [update]. This is not generally recommended but
/// may be appropriate if the data was obtained from a third-party source in
/// JSON form and could not be preprocessed by a server to convert the data
/// to the binary form described above. Numbers in JSON are interpreted as
/// doubles if they contain a decimal point or an `e` in their source
/// representation, and as integers otherwise. This can cause issues as the
/// [DynamicContent] and [DataSource] are strongly typed and distinguish
/// [int] and [double]. Explicit nulls in the JSON are an error (the data
/// format supported by [DynamicContent] does not support nulls). Decoding
/// JSON is about 1.5x slower than the binary format.
///
/// Subscribers are notified immediately after an update if their data changed.
///
/// ## References
///
/// To subscribe to a node, the [subscribe] method is used. The method returns
/// the current value. When the value later changes, the provided callback is
/// invoked with the new value.
///
/// The [unsubscribe] method must be called when the client no longer needs
/// updates (e.g. when the widget goes away).
///
/// To identify a node, a list of keys is used, giving the path from the root to
/// the node. Each key is either a string (to index into maps) or an integer (to
/// index into lists). If no node is identified, the [missing] value is
/// returned. Similarly, if a node goes away, subscribers are given the value
/// [missing] as the new value. It is not an error to subscribe to missing data.
/// It _is_ an error to add [missing] values to the data model, however.
///
/// The [LocalWidgetBuilder]s passed to a [LocalWidgetLibrary] use a
/// [DataSource] as their interface into the [DynamicContent]. To ensure the
/// integrity of the update mechanism, that interface only allows access to
/// leaves, not intermediate nodes (maps and lists).
///
/// It is an error to subscribe to the same key multiple times with the same
/// callback.
class DynamicContent {
/// Create a fresh [DynamicContent] object.
///
/// The `initialData` argument, if provided, is used to update all the keys
/// in the [DynamicContent], as if [updateAll] had been called.
DynamicContent([ DynamicMap? initialData ]) {
if (initialData != null) {
updateAll(initialData);
}
}
final _DynamicNode _root = _DynamicNode.root();
/// Update all the keys in the [DynamicContent].
///
/// Each key in the provided map is added to [DynamicContent], replacing any
/// data that was there previously, as if [update] had been called for each
/// key.
///
/// Existing keys that are not present in the given map are left unmodified.
void updateAll(DynamicMap initialData) {
for (final String key in initialData.keys) {
final Object value = initialData[key] ?? missing;
update(key, value);
}
}
/// Updates the content with the specified data.
///
/// The given `rootKey` is updated with the data `value`.
///
/// The `value` must consist exclusively of [DynamicMap], [DynamicList], [int],
/// [double], [bool], and [String] objects.
void update(String rootKey, Object value) {
_root.updateKey(rootKey, deepClone(value)!);
_scheduleCleanup();
}
/// Obtain the value at location `key`, and subscribe `callback` to that key
/// so that future [update]s will invoke the callback with the new value.
///
/// The value is always non-null; if the value is missing, the [missing]
/// object is used instead.
///
/// Use [unsubscribe] when the subscription is no longer needed.
Object subscribe(List<Object> key, SubscriptionCallback callback) {
return _root.subscribe(key, 0, callback);
}
/// Removes a subscription created by [subscribe].
void unsubscribe(List<Object> key, SubscriptionCallback callback) {
_root.unsubscribe(key, 0, callback);
}
bool _cleanupPending = false;
void _scheduleCleanup() {
if (!_cleanupPending) {
_cleanupPending = true;
scheduleMicrotask(() {
_cleanupPending = false;
_DynamicNode.cleanup();
});
}
}
@override
String toString() => '$runtimeType($_root)';
}
// Node in the [DynamicContent] tree. This should contain no [BlobNode]s.
class _DynamicNode {
_DynamicNode(this._key, this._parent, this._value) : assert(_value == missing || _hasValidType(_value));
_DynamicNode.root() : _key = missing, _parent = null, _value = DynamicMap(); // ignore: prefer_collection_literals
final Object _key;
final _DynamicNode? _parent;
Object _value;
final Set<SubscriptionCallback> _callbacks = <SubscriptionCallback>{};
final Map<Object, _DynamicNode> _children = <Object, _DynamicNode>{};
bool get isObsolete => _callbacks.isEmpty && _children.isEmpty;
static final Set<_DynamicNode> _obsoleteNodes = <_DynamicNode>{};
/// Allow garbage collection to collect unused nodes.
///
/// When a node has no subscribers, it is no longer needed (it can be
/// recreated if necessary from the raw data). In that situation, the node
/// adds itself to a list of "obsolete nodes", but the parent still references
/// it and therefore garbage collection would not notice that it is no longer
/// used.
///
/// This method solves this problem by disconnecting obsolete nodes from the
/// tree.
static void cleanup() {
while (_obsoleteNodes.isNotEmpty) {
final _DynamicNode node = _obsoleteNodes.first;
_obsoleteNodes.remove(node);
if (node.isObsolete) {
node._parent?._forget(node._key, node);
}
}
}
void _forget(Object childKey, _DynamicNode child) {
assert(_children[childKey] == child);
_children.remove(childKey);
if (isObsolete) {
_obsoleteNodes.add(this);
}
}
static bool _hasValidType(Object? value) {
if (value is DynamicMap) {
return value.values.every(_hasValidType);
}
if (value is DynamicList) {
return value.every(_hasValidType);
}
return value is int
|| value is double
|| value is bool
|| value is String;
}
_DynamicNode _prepare(Object childKey) {
assert(childKey is String || childKey is int);
if (!_children.containsKey(childKey)) {
Object value;
if (_value is DynamicMap) {
if (childKey is String && (_value as DynamicMap).containsKey(childKey)) {
value = (_value as DynamicMap)[childKey]!;
} else {
value = missing;
}
} else if (_value is DynamicList) {
if (childKey is int && childKey >= 0 && childKey < (_value as DynamicList).length) {
value = (_value as DynamicList)[childKey]!;
} else {
value = missing;
}
} else {
value = _value;
}
_children[childKey] = _DynamicNode(childKey, this, value);
}
return _children[childKey]!;
}
Object subscribe(List<Object> key, int index, SubscriptionCallback callback) {
_obsoleteNodes.remove(this);
if (index == key.length) {
assert(!_callbacks.contains(callback));
_callbacks.add(callback);
return _value;
}
final _DynamicNode child = _prepare(key[index]);
return child.subscribe(key, index + 1, callback);
}
void unsubscribe(List<Object> key, int index, SubscriptionCallback callback) {
if (index == key.length) {
assert(_callbacks.contains(callback));
_callbacks.remove(callback);
if (_callbacks.isEmpty) {
_obsoleteNodes.add(this);
}
} else {
assert(_children.containsKey(key[index]));
_children[key[index]]!.unsubscribe(key, index + 1, callback);
}
}
void update(Object value) {
assert(value == missing || _hasValidType(value), 'cannot update $this using $value');
if (value == _value) {
return;
}
_value = value;
if (value is DynamicMap) {
for (final Object childKey in _children.keys) {
if (childKey is String && value.containsKey(childKey)) {
_children[childKey]!.update(value[childKey]!);
} else {
_children[childKey]!.update(missing);
}
}
} else if (value is DynamicList) {
for (final Object childKey in _children.keys) {
if (childKey is int && childKey >= 0 && childKey < value.length) {
_children[childKey]!.update(value[childKey]!);
} else {
_children[childKey]!.update(missing);
}
}
} else {
for (final _DynamicNode child in _children.values) {
child.update(missing);
}
}
_sendUpdates(value);
}
void _sendUpdates(Object value) {
for (final SubscriptionCallback callback in _callbacks) {
callback(value);
}
}
void updateKey(String rootKey, Object value) {
assert(_value is DynamicMap);
assert(_hasValidType(value));
if ((_value as DynamicMap)[rootKey] == value) {
return;
}
(_value as DynamicMap)[rootKey] = value;
if (_children.containsKey(rootKey)) {
_children[rootKey]!.update(value);
}
}
@override
String toString() => '$_value';
}