blob: 45926e28edfa20c804a25a5dfdc20e86036035c5 [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.
available,
/// The document has been scheduled for deletion in the current transaction.
/// Reading and writing to the document is forbidden.
pendingDeletion,
/// The document has been deleted.
/// Reading and writing to the document is forbidden.
deleted
}
/// 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 =
new StreamController<void>.broadcast();
/// 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 = new 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) {
value.observer = this;
});
makeExist();
}
Uint8List _createIdentifierForField(String key, _DocumentFieldType type) {
Uint8List hashOfKey = hash(getUint8ListFromString(key));
Uint8List prefix = new 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 = new Change();
for (final field in _fields.entries) {
result.addAll(field.value.getChange().withPrefix(field.key));
}
return result;
}
/// Ends the transaction for all fields of this document.
void completeTransaction() {
for (final leafValue in _fields.values) {
leafValue.completeTransaction();
}
if (_state == DocumentState.pendingDeletion) {
_state = DocumentState.deleted;
// Allows the Dart VM to free some memory.
_fields.clear();
// 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 =
change.splitByPrefix(_identifierLength);
for (final splittedChange in splittedChanges.entries) {
_fields[splittedChange.key].applyChange(splittedChange.value);
}
}
_changeController.add(null);
}
/// Returns a stream generating an event each time a document changes.
Stream<void> get onChange => _changeController.stream;
/// Rolls back all local modifications on the state and on all fields of
/// this document.
void rollbackChange() {
for (final leafValue in _fields.values) {
leafValue.rollbackChange();
}
if (_state == DocumentState.pendingDeletion) {
_state = DocumentState.available;
}
}
/// Throws an exception if the document is in an invalid state.
void _checkExistsState() {
if (!_documentExists.value) {
throw new StateError('Value access to a non-existing document.');
}
if (_state != DocumentState.available) {
throw new StateError("Can't access a document that is deleted.");
}
}
void _changeState() {
_checkExistsState();
Transaction currentTransaction = _sledge.currentTransaction;
if (currentTransaction == null) {
throw new StateError('Document was changed outside of a transaction.');
}
currentTransaction.documentWasModified(this);
}
/// 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() {
_changeState();
_state = DocumentState.pendingDeletion;
}
@override
void valueWasChanged() {
_changeState();
}
/// Returns the Value associated with [fieldName].
/// If [fieldName] does not have any associated Value, an ArgumentError
/// exception is thrown.
dynamic operator [](String fieldName) {
_checkExistsState();
return _value[fieldName];
}
}