blob: a7ff806c7fb9e6d4704b12d854fac0fa1f45b8b6 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. 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';
import 'dart:typed_data';
import '../sledge.dart';
import '../sledge_connection_id.dart';
import '../transaction.dart';
import '../uint8list_ops.dart';
import 'change.dart';
import 'document_id.dart';
import 'leaf_value.dart';
import 'node_value.dart';
import 'value_observer.dart';
import 'values/last_one_wins_value.dart';
enum _DocumentFieldType { public, private }
/// The state a document is in.
enum DocumentState {
/// The document is available for reading and writing.
/// The document has been scheduled for deletion in the current transaction.
/// Reading and writing to the document is forbidden.
/// The document has been deleted.
/// Reading and writing to the document is forbidden.
/// Represents structured data that can be stored in Sledge.
class Document implements ValueObserver {
final Sledge _sledge;
final DocumentId _documentId;
NodeValue _value;
final Map<Uint8List, LeafValue> _fields;
final ConnectionId _connectionId;
static const int _identifierLength = 21;
final StreamController<void> _changeController =
/// A value that is set to true for all documents that exist.
/// If the Document object was created in a rollbacked transaction, then this
/// field is false and all operations on this object are invalid.
final LastOneWinsValue<bool> _documentExists = LastOneWinsValue<bool>();
DocumentState _state = DocumentState.available;
/// The name of the private field holding [_documentExists].
static const String _documentExistsFieldName = 'documentExists';
/// Default constructor.
Document(this._sledge, this._documentId)
: _fields = newUint8ListMap<LeafValue>(),
_connectionId = _sledge.connectionId {
_value = _documentId.schema.newValue(_connectionId);
// Add to [_fields] all the public fields of [_value].
_value.collectFields().forEach((final String key, final LeafValue value) {
Uint8List identifier =
_createIdentifierForField(key, _DocumentFieldType.public);
assert(identifier.length == _identifierLength);
_fields[identifier] = value;
// Add to [_fields] the private fields.
Uint8List documentExistsValueIdentifier = _createIdentifierForField(
_documentExistsFieldName, _DocumentFieldType.private);
_fields[documentExistsValueIdentifier] = _documentExists;
// Observe all the values contained by [_fields].
_fields.forEach((Uint8List key, LeafValue value) { = this;
Uint8List _createIdentifierForField(String key, _DocumentFieldType type) {
Uint8List hashOfKey = hash(getUint8ListFromString(key));
Uint8List prefix = Uint8List.fromList([type.index]);
return concatUint8Lists(prefix, hashOfKey);
/// Returns this document's documentId.
DocumentId get documentId => _documentId;
/// Returns the state the document is in.
DocumentState get state => _state;
/// Gets the change for all fields of this document.
Change getChange() {
Change result = Change();
for (final field in _fields.entries) {
return result;
/// Ends the transaction for all fields of this document.
void completeTransaction() {
for (final leafValue in _fields.values) {
if (_state == DocumentState.pendingDeletion) {
_state = DocumentState.deleted;
// Allows the Dart VM to free some memory.
// TODO: evaluate if `_changeController` needs to be updated.
// TODO: evaluate if the cache of Documents needs to be updated.
/// Applies [change] to fields of this document.
void applyChange(final Change change) {
if (_state == DocumentState.available) {
Map<Uint8List, Change> splittedChanges =
for (final splittedChange in splittedChanges.entries) {
/// Returns a stream generating an event each time a document changes.
Stream<void> get onChange =>;
/// Rolls back all local modifications on the state and on all fields of
/// this document.
void rollbackChange() {
for (final leafValue in _fields.values) {
if (_state == DocumentState.pendingDeletion) {
_state = DocumentState.available;
/// Throws an exception if the document is in an invalid state.
void _checkExistsState() {
if (!_documentExists.value) {
throw StateError('Value access to a non-existing document.');
if (_state != DocumentState.available) {
throw StateError("Can't access a document that is deleted.");
void _changeState() {
Transaction currentTransaction = _sledge.currentTransaction;
if (currentTransaction == null) {
throw StateError('Document was changed outside of a transaction.');
/// Sets [_documentExists] to |true| so that Ledger has a trace that this
/// document was created. This makes the Document object valid.
void makeExist() {
_documentExists.value = true;
/// Deletes the document.
void delete() {
_state = DocumentState.pendingDeletion;
void valueWasChanged() {
/// Returns the Value associated with [fieldName].
/// If [fieldName] does not have any associated Value, an ArgumentError
/// exception is thrown.
dynamic operator [](String fieldName) {
return _value[fieldName];