blob: 161143df7049beab7521caf94cd08d8000c245d7 [file]
// Copyright 2017, the Chromium 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.
part of cloud_firestore;
/// The entry point for accessing a Firestore.
///
/// You can get an instance by calling [Firestore.instance].
class Firestore {
@visibleForTesting
static const MethodChannel channel = const MethodChannel(
'plugins.flutter.io/cloud_firestore',
const StandardMethodCodec(const FirestoreMessageCodec()),
);
static final Map<int, StreamController<QuerySnapshot>> _queryObservers =
<int, StreamController<QuerySnapshot>>{};
static final Map<int, StreamController<DocumentSnapshot>> _documentObservers =
<int, StreamController<DocumentSnapshot>>{};
static final Map<int, TransactionHandler> _transactionHandlers =
<int, TransactionHandler>{};
static int _transactionHandlerId = 0;
Firestore._() {
channel.setMethodCallHandler((MethodCall call) {
if (call.method == 'QuerySnapshot') {
final QuerySnapshot snapshot =
new QuerySnapshot._(call.arguments, this);
_queryObservers[call.arguments['handle']].add(snapshot);
} else if (call.method == 'DocumentSnapshot') {
final DocumentSnapshot snapshot = new DocumentSnapshot._(
call.arguments['path'],
_asStringKeyedMap(call.arguments['data']),
this,
);
_documentObservers[call.arguments['handle']].add(snapshot);
} else if (call.method == 'DoTransaction') {
final int transactionId = call.arguments['transactionId'];
return _transactionHandlers[transactionId](
new Transaction(transactionId),
);
}
});
}
static Firestore _instance = new Firestore._();
/// Gets the instance of Firestore for the default Firebase app.
static Firestore get instance => _instance;
/// Gets a [CollectionReference] for the specified Firestore path.
CollectionReference collection(String path) {
assert(path != null);
return new CollectionReference._(this, path.split('/'));
}
/// Gets a [DocumentReference] for the specified Firestore path.
DocumentReference document(String path) {
assert(path != null);
return new DocumentReference._(this, path.split('/'));
}
/// Creates a write batch, used for performing multiple writes as a single
/// atomic operation.
///
/// Unlike transactions, write batches are persisted offline and therefore are
/// preferable when you don’t need to condition your writes on read data.
WriteBatch batch() => new WriteBatch._();
/// Executes the given TransactionHandler and then attempts to commit the
/// changes applied within an atomic transaction.
///
/// In the TransactionHandler, a set of reads and writes can be performed
/// atomically using the Transaction object passed to the TransactionHandler.
/// After the TransactionHandler is run, Firestore will attempt to apply the
/// changes to the server. If any of the data read has been modified outside
/// of this transaction since being read, then the transaction will be
/// retried by executing the updateBlock again. If the transaction still
/// fails after 5 retries, then the transaction will fail.
///
/// The TransactionHandler may be executed multiple times, it should be able
/// to handle multiple executions.
///
/// Data accessed with the transaction will not reflect local changes that
/// have not been committed. For this reason, it is required that all
/// reads are performed before any writes. Transactions must be performed
/// while online. Otherwise, reads will fail, and the final commit will fail.
///
/// By default transactions are limited to 5 seconds of execution time. This
/// timeout can be adjusted by setting the timeout parameter.
Future<Map<String, dynamic>> runTransaction(
TransactionHandler transactionHandler,
{Duration timeout: const Duration(seconds: 5)}) async {
assert(timeout.inMilliseconds > 0,
'Transaction timeout must be more than 0 milliseconds');
final int transactionId = _transactionHandlerId++;
_transactionHandlers[transactionId] = transactionHandler;
final Map<dynamic, dynamic> result = await channel.invokeMethod(
'Firestore#runTransaction', <String, dynamic>{
'transactionId': transactionId,
'transactionTimeout': timeout.inMilliseconds
});
return result?.cast<String, dynamic>() ?? <String, dynamic>{};
}
}
typedef Future<dynamic> TransactionHandler(Transaction transaction);
class Transaction {
int _transactionId;
Transaction(this._transactionId);
Future<DocumentSnapshot> get(DocumentReference documentReference) async {
final dynamic result = await Firestore.channel
.invokeMethod('Transaction#get', <String, dynamic>{
'transactionId': _transactionId,
'path': documentReference.path,
});
if (result != null) {
return new DocumentSnapshot._(documentReference.path,
result['data'].cast<String, dynamic>(), Firestore.instance);
} else {
return null;
}
}
Future<void> delete(DocumentReference documentReference) async {
return Firestore.channel
.invokeMethod('Transaction#delete', <String, dynamic>{
'transactionId': _transactionId,
'path': documentReference.path,
});
}
Future<void> update(
DocumentReference documentReference, Map<String, dynamic> data) async {
return Firestore.channel
.invokeMethod('Transaction#update', <String, dynamic>{
'transactionId': _transactionId,
'path': documentReference.path,
'data': data,
});
}
Future<void> set(
DocumentReference documentReference, Map<String, dynamic> data) async {
return Firestore.channel.invokeMethod('Transaction#set', <String, dynamic>{
'transactionId': _transactionId,
'path': documentReference.path,
'data': data,
});
}
}
class FirestoreMessageCodec extends StandardMessageCodec {
const FirestoreMessageCodec();
static const int _kDateTime = 128;
static const int _kGeoPoint = 129;
static const int _kDocumentReference = 130;
@override
void writeValue(WriteBuffer buffer, dynamic value) {
if (value is DateTime) {
buffer.putUint8(_kDateTime);
buffer.putInt64(value.millisecondsSinceEpoch);
} else if (value is GeoPoint) {
buffer.putUint8(_kGeoPoint);
buffer.putFloat64(value.latitude);
buffer.putFloat64(value.longitude);
} else if (value is DocumentReference) {
buffer.putUint8(_kDocumentReference);
final List<int> bytes = utf8.encoder.convert(value.path);
writeSize(buffer, bytes.length);
buffer.putUint8List(bytes);
} else {
super.writeValue(buffer, value);
}
}
@override
dynamic readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case _kDateTime:
return new DateTime.fromMillisecondsSinceEpoch(buffer.getInt64());
case _kGeoPoint:
return new GeoPoint(buffer.getFloat64(), buffer.getFloat64());
case _kDocumentReference:
final int length = readSize(buffer);
final String path = utf8.decoder.convert(buffer.getUint8List(length));
return Firestore.instance.document(path);
default:
return super.readValueOfType(type, buffer);
}
}
}
class GeoPoint {
final double latitude;
final double longitude;
const GeoPoint(this.latitude, this.longitude);
@override
bool operator ==(dynamic o) =>
o is GeoPoint && o.latitude == latitude && o.longitude == longitude;
@override
int get hashCode => hashValues(latitude, longitude);
}