runTransaction function
Runs handler in a transaction, retrying it if Firestore says to.
The handler may run more than once — that is what a transaction is — so it must not have effects outside the writes it records on the handle.
Implementation
Future<void> runTransaction(
Future<void> Function(FirestoreTransaction tx) handler,
) {
final done = Completer<void>();
final receive = RawReceivePort();
late FirestoreTransaction tx;
var txnId = 0;
receive.handler = (Object? message) async {
final bytes = message! as Uint8List;
final seq = ByteData.sublistView(bytes).getInt64(8, Endian.host);
if (seq < 0) {
receive.close();
final reason = decodeSnapshotValue(bytes);
if (!done.isCompleted) {
done.completeError(
FirestoreException(-1, '${reason ?? "failed"}', 'transaction'),
);
}
return;
}
if (seq == 0) {
receive.close();
if (!done.isCompleted) done.complete();
return;
}
// seq > 0 is an attempt. A retry arrives here again, so the buffer is
// cleared rather than accumulating what the previous attempt recorded.
tx._reset();
try {
await handler(tx);
} catch (e) {
// The handler decided against it. Abort rather than commit a partial
// set of writes, and let the error surface as the transaction's.
fdbFsTxnAbort(txnId);
if (!done.isCompleted) done.completeError(e);
return;
}
final encoded = tx._encodeWrites();
final buf = calloc<Uint8>(encoded.isEmpty ? 1 : encoded.length);
if (encoded.isNotEmpty) {
buf.asTypedList(encoded.length).setAll(0, encoded);
}
fdbFsTxnCommit(txnId, buf, encoded.length);
calloc.free(buf);
};
txnId = fdbFsTxnBegin(receive.sendPort.nativePort);
if (txnId <= 0) {
receive.close();
return Future.error(StateError('transaction failed to start ($txnId)'));
}
tx = FirestoreTransaction._(txnId);
return done.future;
}