mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-03-27 17:38:17 -04:00
refactor, use external libraries, and add integration test for veilid_support
This commit is contained in:
parent
e622b7f949
commit
25a6a00fcf
@ -400,7 +400,7 @@ class AccountRepository {
|
|||||||
|
|
||||||
// Record not yet open, do it
|
// Record not yet open, do it
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final record = await pool.openOwned(
|
final record = await pool.openRecordOwned(
|
||||||
userLogin.accountRecordInfo.accountRecord,
|
userLogin.accountRecordInfo.accountRecord,
|
||||||
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
|
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
|
||||||
parent: localAccount.identityMaster.identityRecordKey);
|
parent: localAccount.identityMaster.identityRecordKey);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import 'package:bloc_tools/bloc_tools.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
class ActiveChatCubit extends Cubit<TypedKey?> with BlocTools {
|
class ActiveChatCubit extends Cubit<TypedKey?> {
|
||||||
ActiveChatCubit(super.initialState);
|
ActiveChatCubit(super.initialState);
|
||||||
|
|
||||||
void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) {
|
void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -85,7 +85,7 @@ class ContactInvitationListCubit
|
|||||||
// to and it will be eventually encrypted with the DH of the contact's
|
// to and it will be eventually encrypted with the DH of the contact's
|
||||||
// identity key
|
// identity key
|
||||||
late final Uint8List signedContactInvitationBytes;
|
late final Uint8List signedContactInvitationBytes;
|
||||||
await (await pool.create(
|
await (await pool.createRecord(
|
||||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||||
'LocalConversation',
|
'LocalConversation',
|
||||||
parent: _activeAccountInfo.accountRecordKey,
|
parent: _activeAccountInfo.accountRecordKey,
|
||||||
@ -114,7 +114,7 @@ class ContactInvitationListCubit
|
|||||||
// Create DHT unicast inbox for ContactRequest
|
// Create DHT unicast inbox for ContactRequest
|
||||||
// Subkey 0 is the ContactRequest from the initiator
|
// Subkey 0 is the ContactRequest from the initiator
|
||||||
// Subkey 1 will contain the invitation response accept/reject eventually
|
// Subkey 1 will contain the invitation response accept/reject eventually
|
||||||
await (await pool.create(
|
await (await pool.createRecord(
|
||||||
debugName: 'ContactInvitationListCubit::createInvitation::'
|
debugName: 'ContactInvitationListCubit::createInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: _activeAccountInfo.accountRecordKey,
|
parent: _activeAccountInfo.accountRecordKey,
|
||||||
@ -198,7 +198,7 @@ class ContactInvitationListCubit
|
|||||||
if (success && deletedItem != null) {
|
if (success && deletedItem != null) {
|
||||||
// Delete the contact request inbox
|
// Delete the contact request inbox
|
||||||
final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid();
|
final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid();
|
||||||
await (await pool.openOwned(contactRequestInbox,
|
await (await pool.openRecordOwned(contactRequestInbox,
|
||||||
debugName: 'ContactInvitationListCubit::deleteInvitation::'
|
debugName: 'ContactInvitationListCubit::deleteInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: accountRecordKey))
|
||||||
@ -250,7 +250,7 @@ class ContactInvitationListCubit
|
|||||||
contactRequestInboxKey) !=
|
contactRequestInboxKey) !=
|
||||||
-1;
|
-1;
|
||||||
|
|
||||||
await (await pool.openRead(contactRequestInboxKey,
|
await (await pool.openRecordRead(contactRequestInboxKey,
|
||||||
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
debugName: 'ContactInvitationListCubit::validateInvitation::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: _activeAccountInfo.accountRecordKey))
|
parent: _activeAccountInfo.accountRecordKey))
|
||||||
|
@ -34,7 +34,7 @@ class ContactRequestInboxCubit
|
|||||||
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
|
contactInvitationRecord.contactRequestInbox.recordKey.toVeilid();
|
||||||
final writer = TypedKeyPair(
|
final writer = TypedKeyPair(
|
||||||
kind: recordKey.kind, key: writerKey, secret: writerSecret);
|
kind: recordKey.kind, key: writerKey, secret: writerSecret);
|
||||||
return pool.openRead(recordKey,
|
return pool.openRecordRead(recordKey,
|
||||||
debugName: 'ContactRequestInboxCubit::_open::'
|
debugName: 'ContactRequestInboxCubit::_open::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
|
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
|
|
||||||
class InvitationGeneratorCubit extends FutureCubit<Uint8List> {
|
class InvitationGeneratorCubit extends FutureCubit<Uint8List> {
|
||||||
InvitationGeneratorCubit(super.fut);
|
InvitationGeneratorCubit(super.fut);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
|
@ -37,7 +37,7 @@ class ValidContactInvitation {
|
|||||||
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
|
||||||
final accountRecordKey = _activeAccountInfo.accountRecordKey;
|
final accountRecordKey = _activeAccountInfo.accountRecordKey;
|
||||||
|
|
||||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
||||||
debugName: 'ValidContactInvitation::accept::'
|
debugName: 'ValidContactInvitation::accept::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: accountRecordKey))
|
||||||
@ -100,7 +100,7 @@ class ValidContactInvitation {
|
|||||||
final accountRecordKey =
|
final accountRecordKey =
|
||||||
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||||
|
|
||||||
return (await pool.openWrite(_contactRequestInboxKey, _writer,
|
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
|
||||||
debugName: 'ValidContactInvitation::reject::'
|
debugName: 'ValidContactInvitation::reject::'
|
||||||
'ContactRequestInbox',
|
'ContactRequestInbox',
|
||||||
parent: accountRecordKey))
|
parent: accountRecordKey))
|
||||||
|
@ -48,7 +48,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final crypto = await _cachedConversationCrypto();
|
final crypto = await _cachedConversationCrypto();
|
||||||
final writer = _activeAccountInfo.conversationWriter;
|
final writer = _activeAccountInfo.conversationWriter;
|
||||||
final record = await pool.openWrite(
|
final record = await pool.openRecordWrite(
|
||||||
_localConversationRecordKey!, writer,
|
_localConversationRecordKey!, writer,
|
||||||
debugName: 'ConversationCubit::LocalConversation',
|
debugName: 'ConversationCubit::LocalConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountRecordKey,
|
||||||
@ -67,7 +67,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
// Open remote record key if it is specified
|
// Open remote record key if it is specified
|
||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
final crypto = await _cachedConversationCrypto();
|
final crypto = await _cachedConversationCrypto();
|
||||||
final record = await pool.openRead(_remoteConversationRecordKey,
|
final record = await pool.openRecordRead(_remoteConversationRecordKey,
|
||||||
debugName: 'ConversationCubit::RemoteConversation',
|
debugName: 'ConversationCubit::RemoteConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountRecordKey,
|
||||||
crypto: crypto);
|
crypto: crypto);
|
||||||
@ -226,14 +226,14 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
// Open with SMPL scheme for identity writer
|
// Open with SMPL scheme for identity writer
|
||||||
late final DHTRecord localConversationRecord;
|
late final DHTRecord localConversationRecord;
|
||||||
if (existingConversationRecordKey != null) {
|
if (existingConversationRecordKey != null) {
|
||||||
localConversationRecord = await pool.openWrite(
|
localConversationRecord = await pool.openRecordWrite(
|
||||||
existingConversationRecordKey, writer,
|
existingConversationRecordKey, writer,
|
||||||
debugName:
|
debugName:
|
||||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountRecordKey,
|
||||||
crypto: crypto);
|
crypto: crypto);
|
||||||
} else {
|
} else {
|
||||||
localConversationRecord = await pool.create(
|
localConversationRecord = await pool.createRecord(
|
||||||
debugName:
|
debugName:
|
||||||
'ConversationCubit::initLocalConversation::LocalConversation',
|
'ConversationCubit::initLocalConversation::LocalConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountRecordKey,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../account_manager/account_manager.dart';
|
import '../../../account_manager/account_manager.dart';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
|
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
|
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
@ -16,15 +17,12 @@ class BackgroundTicker extends StatefulWidget {
|
|||||||
|
|
||||||
class BackgroundTickerState extends State<BackgroundTicker> {
|
class BackgroundTickerState extends State<BackgroundTicker> {
|
||||||
Timer? _tickTimer;
|
Timer? _tickTimer;
|
||||||
bool _inTick = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_tickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (!_inTick) {
|
singleFuture(this, _onTick);
|
||||||
unawaited(_onTick());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +48,7 @@ class BackgroundTickerState extends State<BackgroundTicker> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_inTick = true;
|
// Tick DHT record pool
|
||||||
try {
|
await DHTRecordPool.instance.tick();
|
||||||
// Tick DHT record pool
|
|
||||||
unawaited(DHTRecordPool.instance.tick());
|
|
||||||
} finally {
|
|
||||||
_inTick = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../repository/processor_repository.dart';
|
import '../repository/processor_repository.dart';
|
||||||
|
7
packages/async_tools/.gitignore
vendored
7
packages/async_tools/.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
# https://dart.dev/guides/libraries/private-files
|
|
||||||
# Created by `dart pub`
|
|
||||||
.dart_tool/
|
|
||||||
|
|
||||||
# Avoid committing pubspec.lock for library packages; see
|
|
||||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
|
||||||
pubspec.lock
|
|
@ -1,15 +0,0 @@
|
|||||||
include: package:lint_hard/all.yaml
|
|
||||||
analyzer:
|
|
||||||
errors:
|
|
||||||
invalid_annotation_target: ignore
|
|
||||||
exclude:
|
|
||||||
- '**/*.g.dart'
|
|
||||||
- '**/*.freezed.dart'
|
|
||||||
- '**/*.pb.dart'
|
|
||||||
- '**/*.pbenum.dart'
|
|
||||||
- '**/*.pbjson.dart'
|
|
||||||
- '**/*.pbserver.dart'
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
unawaited_futures: true
|
|
||||||
avoid_positional_boolean_parameters: false
|
|
@ -1,2 +0,0 @@
|
|||||||
@echo off
|
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
|
@ -1,6 +0,0 @@
|
|||||||
// import 'package:async_tools/async_tools.dart';
|
|
||||||
|
|
||||||
// void main() {
|
|
||||||
// var awesome = Awesome();
|
|
||||||
// print('awesome: ${awesome.isAwesome}');
|
|
||||||
// }
|
|
@ -1,11 +0,0 @@
|
|||||||
/// Async Tools
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'src/async_tag_lock.dart';
|
|
||||||
export 'src/async_value.dart';
|
|
||||||
export 'src/delayed_wait_set.dart';
|
|
||||||
export 'src/serial_future.dart';
|
|
||||||
export 'src/single_future.dart';
|
|
||||||
export 'src/single_state_processor.dart';
|
|
||||||
export 'src/single_stateless_processor.dart';
|
|
||||||
export 'src/wait_set.dart';
|
|
@ -1,64 +0,0 @@
|
|||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
class _AsyncTagLockEntry {
|
|
||||||
_AsyncTagLockEntry()
|
|
||||||
: mutex = Mutex.locked(),
|
|
||||||
waitingCount = 0;
|
|
||||||
//
|
|
||||||
Mutex mutex;
|
|
||||||
int waitingCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AsyncTagLock<T> {
|
|
||||||
AsyncTagLock()
|
|
||||||
: _tableLock = Mutex(),
|
|
||||||
_locks = {};
|
|
||||||
|
|
||||||
Future<void> lockTag(T tag) async {
|
|
||||||
await _tableLock.protect(() async {
|
|
||||||
final lockEntry = _locks[tag];
|
|
||||||
if (lockEntry != null) {
|
|
||||||
lockEntry.waitingCount++;
|
|
||||||
await lockEntry.mutex.acquire();
|
|
||||||
lockEntry.waitingCount--;
|
|
||||||
} else {
|
|
||||||
_locks[tag] = _AsyncTagLockEntry();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isLocked(T tag) => _locks.containsKey(tag);
|
|
||||||
|
|
||||||
bool tryLock(T tag) {
|
|
||||||
final lockEntry = _locks[tag];
|
|
||||||
if (lockEntry != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_locks[tag] = _AsyncTagLockEntry();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void unlockTag(T tag) {
|
|
||||||
final lockEntry = _locks[tag]!;
|
|
||||||
if (lockEntry.waitingCount == 0) {
|
|
||||||
// If nobody is waiting for the mutex we can just drop it
|
|
||||||
_locks.remove(tag);
|
|
||||||
} else {
|
|
||||||
// Someone's waiting for the tag lock so release the mutex for it
|
|
||||||
lockEntry.mutex.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<R> protect<R>(T tag, {required Future<R> Function() closure}) async {
|
|
||||||
await lockTag(tag);
|
|
||||||
try {
|
|
||||||
return await closure();
|
|
||||||
} finally {
|
|
||||||
unlockTag(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
final Mutex _tableLock;
|
|
||||||
final Map<T, _AsyncTagLockEntry> _locks;
|
|
||||||
}
|
|
@ -1,209 +0,0 @@
|
|||||||
// ignore_for_file: avoid_catches_without_on_clauses
|
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
|
|
||||||
part 'async_value.freezed.dart';
|
|
||||||
|
|
||||||
/// An utility for safely manipulating asynchronous data.
|
|
||||||
///
|
|
||||||
/// By using [AsyncValue], you are guaranteed that you cannot forget to
|
|
||||||
/// handle the loading/error state of an asynchronous operation.
|
|
||||||
///
|
|
||||||
/// It also expose some utilities to nicely convert an [AsyncValue] to
|
|
||||||
/// a different object.
|
|
||||||
/// For example, a Flutter Widget may use [when] to convert an [AsyncValue]
|
|
||||||
/// into either a progress indicator, an error screen, or to show the data:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// /// A provider that asynchronously expose the current user
|
|
||||||
/// final userProvider = StreamProvider<User>((_) async* {
|
|
||||||
/// // fetch the user
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// class Example extends ConsumerWidget {
|
|
||||||
/// @override
|
|
||||||
/// Widget build(BuildContext context, ScopedReader watch) {
|
|
||||||
/// final AsyncValue<User> user = watch(userProvider);
|
|
||||||
///
|
|
||||||
/// return user.when(
|
|
||||||
/// loading: () => CircularProgressIndicator(),
|
|
||||||
/// error: (error, stack) => Text('Oops, something unexpected happened'),
|
|
||||||
/// data: (value) => Text('Hello ${user.name}'),
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If a consumer of an [AsyncValue] does not care about the loading/error
|
|
||||||
/// state, consider using [asData] to read the state:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// Widget build(BuildContext context, ScopedReader watch) {
|
|
||||||
/// // reads the data state directly – will be null during loading/error states
|
|
||||||
/// final User user = watch(userProvider).data?.value;
|
|
||||||
///
|
|
||||||
/// return Text('Hello ${user?.name}');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// - [AsyncValue.guard], to simplify transforming a [Future] into an
|
|
||||||
/// [AsyncValue].
|
|
||||||
/// - The package Freezed (https://github.com/rrousselgit/freezed), which have
|
|
||||||
/// generated this [AsyncValue] class and explains how [map]/[when] works.
|
|
||||||
@freezed
|
|
||||||
@sealed
|
|
||||||
abstract class AsyncValue<T> with _$AsyncValue<T> {
|
|
||||||
const AsyncValue._();
|
|
||||||
|
|
||||||
/// Creates an [AsyncValue] with a data.
|
|
||||||
///
|
|
||||||
/// The data can be `null`.
|
|
||||||
const factory AsyncValue.data(T value) = AsyncData<T>;
|
|
||||||
|
|
||||||
/// Creates an [AsyncValue] in loading state.
|
|
||||||
///
|
|
||||||
/// Prefer always using this constructor with the `const` keyword.
|
|
||||||
const factory AsyncValue.loading() = AsyncLoading<T>;
|
|
||||||
|
|
||||||
/// Creates an [AsyncValue] in error state.
|
|
||||||
///
|
|
||||||
/// The parameter [error] cannot be `null`.
|
|
||||||
factory AsyncValue.error(Object error, [StackTrace? stackTrace]) =
|
|
||||||
AsyncError<T>;
|
|
||||||
|
|
||||||
/// Transforms a [Future] that may fail into something that is safe to read.
|
|
||||||
///
|
|
||||||
/// This is useful to avoid having to do a tedious `try/catch`. Instead of:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// class MyNotifier extends StateNotifier<AsyncValue<MyData> {
|
|
||||||
/// MyNotifier(): super(const AsyncValue.loading()) {
|
|
||||||
/// _fetchData();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Future<void> _fetchData() async {
|
|
||||||
/// state = const AsyncValue.loading();
|
|
||||||
/// try {
|
|
||||||
/// final response = await dio.get('my_api/data');
|
|
||||||
/// final data = MyData.fromJson(response);
|
|
||||||
/// state = AsyncValue.data(data);
|
|
||||||
/// } catch (err, stack) {
|
|
||||||
/// state = AsyncValue.error(err, stack);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// which is redundant as the application grows and we need more and more of
|
|
||||||
/// this pattern – we can use [guard] to simplify it:
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// class MyNotifier extends StateNotifier<AsyncValue<MyData>> {
|
|
||||||
/// MyNotifier(): super(const AsyncValue.loading()) {
|
|
||||||
/// _fetchData();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Future<void> _fetchData() async {
|
|
||||||
/// state = const AsyncValue.loading();
|
|
||||||
/// // does the try/catch for us like previously
|
|
||||||
/// state = await AsyncValue.guard(() async {
|
|
||||||
/// final response = await dio.get('my_api/data');
|
|
||||||
/// return Data.fromJson(response);
|
|
||||||
/// });
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
static Future<AsyncValue<T>> guard<T>(Future<T> Function() future) async {
|
|
||||||
try {
|
|
||||||
return AsyncValue.data(await future());
|
|
||||||
} catch (err, stack) {
|
|
||||||
return AsyncValue.error(err, stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current data, or null if in loading/error.
|
|
||||||
///
|
|
||||||
/// This is safe to use, as Dart (will) have non-nullable types.
|
|
||||||
/// As such reading [asData] still forces to handle the loading/error cases
|
|
||||||
/// by having to check `data != null`.
|
|
||||||
///
|
|
||||||
/// ## Why does [AsyncValue<T>.data] return [AsyncData<T>] instead of [T]?
|
|
||||||
///
|
|
||||||
/// The motivation behind this decision is to allow differentiating between:
|
|
||||||
///
|
|
||||||
/// - There is a data, and it is `null`.
|
|
||||||
/// ```dart
|
|
||||||
/// // There is a data, and it is "null"
|
|
||||||
/// AsyncValue<Configuration> configs = AsyncValue.data(null);
|
|
||||||
///
|
|
||||||
/// print(configs.data); // AsyncValue(value: null)
|
|
||||||
/// print(configs.data.value); // null
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// - There is no data. [AsyncValue] is currently in loading/error state.
|
|
||||||
/// ```dart
|
|
||||||
/// // No data, currently loading
|
|
||||||
/// AsyncValue<Configuration> configs = AsyncValue.loading();
|
|
||||||
///
|
|
||||||
/// print(configs.data); // null, currently loading
|
|
||||||
/// print(configs.data.value); // throws null exception
|
|
||||||
/// ```
|
|
||||||
AsyncData<T>? get asData => map(
|
|
||||||
data: (data) => data,
|
|
||||||
loading: (_) => null,
|
|
||||||
error: (_) => null,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool get isData => asData != null;
|
|
||||||
|
|
||||||
/// Check if this is loading
|
|
||||||
AsyncLoading<T>? get asLoading => map(
|
|
||||||
data: (_) => null,
|
|
||||||
loading: (loading) => loading,
|
|
||||||
error: (_) => null,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool get isLoading => asLoading != null;
|
|
||||||
|
|
||||||
/// Check if this is an error
|
|
||||||
AsyncError<T>? get asError => map(
|
|
||||||
data: (_) => null,
|
|
||||||
loading: (_) => null,
|
|
||||||
error: (e) => e,
|
|
||||||
);
|
|
||||||
|
|
||||||
bool get isError => asError != null;
|
|
||||||
|
|
||||||
/// Shorthand for [when] to handle only the `data` case.
|
|
||||||
AsyncValue<R> whenData<R>(R Function(T value) cb) => when(
|
|
||||||
data: (value) {
|
|
||||||
try {
|
|
||||||
return AsyncValue.data(cb(value));
|
|
||||||
} catch (err, stack) {
|
|
||||||
return AsyncValue.error(err, stack);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loading: () => const AsyncValue.loading(),
|
|
||||||
error: AsyncValue.error,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Check two AsyncData instances for equality
|
|
||||||
bool equalsData(AsyncValue<T> other,
|
|
||||||
{required bool Function(T a, T b) equals}) =>
|
|
||||||
other.when(
|
|
||||||
data: (nd) => when(
|
|
||||||
data: (d) => equals(d, nd),
|
|
||||||
loading: () => true,
|
|
||||||
error: (_e, _st) => true),
|
|
||||||
loading: () => when(
|
|
||||||
data: (_) => true,
|
|
||||||
loading: () => false,
|
|
||||||
error: (_e, _st) => true),
|
|
||||||
error: (ne, nst) => when(
|
|
||||||
data: (_) => true,
|
|
||||||
loading: () => true,
|
|
||||||
error: (e, st) => e != ne || st != nst));
|
|
||||||
}
|
|
@ -1,480 +0,0 @@
|
|||||||
// coverage:ignore-file
|
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
|
||||||
|
|
||||||
part of 'async_value.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// FreezedGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
T _$identity<T>(T value) => value;
|
|
||||||
|
|
||||||
final _privateConstructorUsedError = UnsupportedError(
|
|
||||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
mixin _$AsyncValue<T> {
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult when<TResult extends Object?>({
|
|
||||||
required TResult Function(T value) data,
|
|
||||||
required TResult Function() loading,
|
|
||||||
required TResult Function(Object error, StackTrace? stackTrace) error,
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(T value)? data,
|
|
||||||
TResult? Function()? loading,
|
|
||||||
TResult? Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
|
||||||
TResult Function(T value)? data,
|
|
||||||
TResult Function()? loading,
|
|
||||||
TResult Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult map<TResult extends Object?>({
|
|
||||||
required TResult Function(AsyncData<T> value) data,
|
|
||||||
required TResult Function(AsyncLoading<T> value) loading,
|
|
||||||
required TResult Function(AsyncError<T> value) error,
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(AsyncData<T> value)? data,
|
|
||||||
TResult? Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult? Function(AsyncError<T> value)? error,
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeMap<TResult extends Object?>({
|
|
||||||
TResult Function(AsyncData<T> value)? data,
|
|
||||||
TResult Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult Function(AsyncError<T> value)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
abstract class $AsyncValueCopyWith<T, $Res> {
|
|
||||||
factory $AsyncValueCopyWith(
|
|
||||||
AsyncValue<T> value, $Res Function(AsyncValue<T>) then) =
|
|
||||||
_$AsyncValueCopyWithImpl<T, $Res, AsyncValue<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class _$AsyncValueCopyWithImpl<T, $Res, $Val extends AsyncValue<T>>
|
|
||||||
implements $AsyncValueCopyWith<T, $Res> {
|
|
||||||
_$AsyncValueCopyWithImpl(this._value, this._then);
|
|
||||||
|
|
||||||
// ignore: unused_field
|
|
||||||
final $Val _value;
|
|
||||||
// ignore: unused_field
|
|
||||||
final $Res Function($Val) _then;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
abstract class _$$AsyncDataImplCopyWith<T, $Res> {
|
|
||||||
factory _$$AsyncDataImplCopyWith(
|
|
||||||
_$AsyncDataImpl<T> value, $Res Function(_$AsyncDataImpl<T>) then) =
|
|
||||||
__$$AsyncDataImplCopyWithImpl<T, $Res>;
|
|
||||||
@useResult
|
|
||||||
$Res call({T value});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class __$$AsyncDataImplCopyWithImpl<T, $Res>
|
|
||||||
extends _$AsyncValueCopyWithImpl<T, $Res, _$AsyncDataImpl<T>>
|
|
||||||
implements _$$AsyncDataImplCopyWith<T, $Res> {
|
|
||||||
__$$AsyncDataImplCopyWithImpl(
|
|
||||||
_$AsyncDataImpl<T> _value, $Res Function(_$AsyncDataImpl<T>) _then)
|
|
||||||
: super(_value, _then);
|
|
||||||
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
@override
|
|
||||||
$Res call({
|
|
||||||
Object? value = freezed,
|
|
||||||
}) {
|
|
||||||
return _then(_$AsyncDataImpl<T>(
|
|
||||||
freezed == value
|
|
||||||
? _value.value
|
|
||||||
: value // ignore: cast_nullable_to_non_nullable
|
|
||||||
as T,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
|
|
||||||
class _$AsyncDataImpl<T> extends AsyncData<T> {
|
|
||||||
const _$AsyncDataImpl(this.value) : super._();
|
|
||||||
|
|
||||||
@override
|
|
||||||
final T value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'AsyncValue<$T>.data(value: $value)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other.runtimeType == runtimeType &&
|
|
||||||
other is _$AsyncDataImpl<T> &&
|
|
||||||
const DeepCollectionEquality().equals(other.value, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(value));
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
_$$AsyncDataImplCopyWith<T, _$AsyncDataImpl<T>> get copyWith =>
|
|
||||||
__$$AsyncDataImplCopyWithImpl<T, _$AsyncDataImpl<T>>(this, _$identity);
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult when<TResult extends Object?>({
|
|
||||||
required TResult Function(T value) data,
|
|
||||||
required TResult Function() loading,
|
|
||||||
required TResult Function(Object error, StackTrace? stackTrace) error,
|
|
||||||
}) {
|
|
||||||
return data(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(T value)? data,
|
|
||||||
TResult? Function()? loading,
|
|
||||||
TResult? Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
}) {
|
|
||||||
return data?.call(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
|
||||||
TResult Function(T value)? data,
|
|
||||||
TResult Function()? loading,
|
|
||||||
TResult Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (data != null) {
|
|
||||||
return data(value);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult map<TResult extends Object?>({
|
|
||||||
required TResult Function(AsyncData<T> value) data,
|
|
||||||
required TResult Function(AsyncLoading<T> value) loading,
|
|
||||||
required TResult Function(AsyncError<T> value) error,
|
|
||||||
}) {
|
|
||||||
return data(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(AsyncData<T> value)? data,
|
|
||||||
TResult? Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult? Function(AsyncError<T> value)? error,
|
|
||||||
}) {
|
|
||||||
return data?.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeMap<TResult extends Object?>({
|
|
||||||
TResult Function(AsyncData<T> value)? data,
|
|
||||||
TResult Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult Function(AsyncError<T> value)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (data != null) {
|
|
||||||
return data(this);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class AsyncData<T> extends AsyncValue<T> {
|
|
||||||
const factory AsyncData(final T value) = _$AsyncDataImpl<T>;
|
|
||||||
const AsyncData._() : super._();
|
|
||||||
|
|
||||||
T get value;
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
_$$AsyncDataImplCopyWith<T, _$AsyncDataImpl<T>> get copyWith =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
abstract class _$$AsyncLoadingImplCopyWith<T, $Res> {
|
|
||||||
factory _$$AsyncLoadingImplCopyWith(_$AsyncLoadingImpl<T> value,
|
|
||||||
$Res Function(_$AsyncLoadingImpl<T>) then) =
|
|
||||||
__$$AsyncLoadingImplCopyWithImpl<T, $Res>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class __$$AsyncLoadingImplCopyWithImpl<T, $Res>
|
|
||||||
extends _$AsyncValueCopyWithImpl<T, $Res, _$AsyncLoadingImpl<T>>
|
|
||||||
implements _$$AsyncLoadingImplCopyWith<T, $Res> {
|
|
||||||
__$$AsyncLoadingImplCopyWithImpl(
|
|
||||||
_$AsyncLoadingImpl<T> _value, $Res Function(_$AsyncLoadingImpl<T>) _then)
|
|
||||||
: super(_value, _then);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
|
|
||||||
class _$AsyncLoadingImpl<T> extends AsyncLoading<T> {
|
|
||||||
const _$AsyncLoadingImpl() : super._();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'AsyncValue<$T>.loading()';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other.runtimeType == runtimeType && other is _$AsyncLoadingImpl<T>);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => runtimeType.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult when<TResult extends Object?>({
|
|
||||||
required TResult Function(T value) data,
|
|
||||||
required TResult Function() loading,
|
|
||||||
required TResult Function(Object error, StackTrace? stackTrace) error,
|
|
||||||
}) {
|
|
||||||
return loading();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(T value)? data,
|
|
||||||
TResult? Function()? loading,
|
|
||||||
TResult? Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
}) {
|
|
||||||
return loading?.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
|
||||||
TResult Function(T value)? data,
|
|
||||||
TResult Function()? loading,
|
|
||||||
TResult Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (loading != null) {
|
|
||||||
return loading();
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult map<TResult extends Object?>({
|
|
||||||
required TResult Function(AsyncData<T> value) data,
|
|
||||||
required TResult Function(AsyncLoading<T> value) loading,
|
|
||||||
required TResult Function(AsyncError<T> value) error,
|
|
||||||
}) {
|
|
||||||
return loading(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(AsyncData<T> value)? data,
|
|
||||||
TResult? Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult? Function(AsyncError<T> value)? error,
|
|
||||||
}) {
|
|
||||||
return loading?.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeMap<TResult extends Object?>({
|
|
||||||
TResult Function(AsyncData<T> value)? data,
|
|
||||||
TResult Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult Function(AsyncError<T> value)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (loading != null) {
|
|
||||||
return loading(this);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class AsyncLoading<T> extends AsyncValue<T> {
|
|
||||||
const factory AsyncLoading() = _$AsyncLoadingImpl<T>;
|
|
||||||
const AsyncLoading._() : super._();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
abstract class _$$AsyncErrorImplCopyWith<T, $Res> {
|
|
||||||
factory _$$AsyncErrorImplCopyWith(
|
|
||||||
_$AsyncErrorImpl<T> value, $Res Function(_$AsyncErrorImpl<T>) then) =
|
|
||||||
__$$AsyncErrorImplCopyWithImpl<T, $Res>;
|
|
||||||
@useResult
|
|
||||||
$Res call({Object error, StackTrace? stackTrace});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
class __$$AsyncErrorImplCopyWithImpl<T, $Res>
|
|
||||||
extends _$AsyncValueCopyWithImpl<T, $Res, _$AsyncErrorImpl<T>>
|
|
||||||
implements _$$AsyncErrorImplCopyWith<T, $Res> {
|
|
||||||
__$$AsyncErrorImplCopyWithImpl(
|
|
||||||
_$AsyncErrorImpl<T> _value, $Res Function(_$AsyncErrorImpl<T>) _then)
|
|
||||||
: super(_value, _then);
|
|
||||||
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
@override
|
|
||||||
$Res call({
|
|
||||||
Object? error = null,
|
|
||||||
Object? stackTrace = freezed,
|
|
||||||
}) {
|
|
||||||
return _then(_$AsyncErrorImpl<T>(
|
|
||||||
null == error ? _value.error : error,
|
|
||||||
freezed == stackTrace
|
|
||||||
? _value.stackTrace
|
|
||||||
: stackTrace // ignore: cast_nullable_to_non_nullable
|
|
||||||
as StackTrace?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @nodoc
|
|
||||||
|
|
||||||
class _$AsyncErrorImpl<T> extends AsyncError<T> {
|
|
||||||
_$AsyncErrorImpl(this.error, [this.stackTrace]) : super._();
|
|
||||||
|
|
||||||
@override
|
|
||||||
final Object error;
|
|
||||||
@override
|
|
||||||
final StackTrace? stackTrace;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'AsyncValue<$T>.error(error: $error, stackTrace: $stackTrace)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other.runtimeType == runtimeType &&
|
|
||||||
other is _$AsyncErrorImpl<T> &&
|
|
||||||
const DeepCollectionEquality().equals(other.error, error) &&
|
|
||||||
(identical(other.stackTrace, stackTrace) ||
|
|
||||||
other.stackTrace == stackTrace));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(
|
|
||||||
runtimeType, const DeepCollectionEquality().hash(error), stackTrace);
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
@override
|
|
||||||
@pragma('vm:prefer-inline')
|
|
||||||
_$$AsyncErrorImplCopyWith<T, _$AsyncErrorImpl<T>> get copyWith =>
|
|
||||||
__$$AsyncErrorImplCopyWithImpl<T, _$AsyncErrorImpl<T>>(this, _$identity);
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult when<TResult extends Object?>({
|
|
||||||
required TResult Function(T value) data,
|
|
||||||
required TResult Function() loading,
|
|
||||||
required TResult Function(Object error, StackTrace? stackTrace) error,
|
|
||||||
}) {
|
|
||||||
return error(this.error, stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(T value)? data,
|
|
||||||
TResult? Function()? loading,
|
|
||||||
TResult? Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
}) {
|
|
||||||
return error?.call(this.error, stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
|
||||||
TResult Function(T value)? data,
|
|
||||||
TResult Function()? loading,
|
|
||||||
TResult Function(Object error, StackTrace? stackTrace)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (error != null) {
|
|
||||||
return error(this.error, stackTrace);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult map<TResult extends Object?>({
|
|
||||||
required TResult Function(AsyncData<T> value) data,
|
|
||||||
required TResult Function(AsyncLoading<T> value) loading,
|
|
||||||
required TResult Function(AsyncError<T> value) error,
|
|
||||||
}) {
|
|
||||||
return error(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult? mapOrNull<TResult extends Object?>({
|
|
||||||
TResult? Function(AsyncData<T> value)? data,
|
|
||||||
TResult? Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult? Function(AsyncError<T> value)? error,
|
|
||||||
}) {
|
|
||||||
return error?.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@optionalTypeArgs
|
|
||||||
TResult maybeMap<TResult extends Object?>({
|
|
||||||
TResult Function(AsyncData<T> value)? data,
|
|
||||||
TResult Function(AsyncLoading<T> value)? loading,
|
|
||||||
TResult Function(AsyncError<T> value)? error,
|
|
||||||
required TResult orElse(),
|
|
||||||
}) {
|
|
||||||
if (error != null) {
|
|
||||||
return error(this);
|
|
||||||
}
|
|
||||||
return orElse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class AsyncError<T> extends AsyncValue<T> {
|
|
||||||
factory AsyncError(final Object error, [final StackTrace? stackTrace]) =
|
|
||||||
_$AsyncErrorImpl<T>;
|
|
||||||
AsyncError._() : super._();
|
|
||||||
|
|
||||||
Object get error;
|
|
||||||
StackTrace? get stackTrace;
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
_$$AsyncErrorImplCopyWith<T, _$AsyncErrorImpl<T>> get copyWith =>
|
|
||||||
throw _privateConstructorUsedError;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
class DelayedWaitSet {
|
|
||||||
DelayedWaitSet();
|
|
||||||
|
|
||||||
void add(Future<void> Function() closure) {
|
|
||||||
_closures.add(closure);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> call() async {
|
|
||||||
final futures = _closures.map((c) => c()).toList();
|
|
||||||
_closures = [];
|
|
||||||
if (futures.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await futures.wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Future<void> Function()> _closures = [];
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
// Process a single future at a time per tag queued serially
|
|
||||||
//
|
|
||||||
// The closure function is called to produce the future that is to be executed.
|
|
||||||
// If a future with a particular tag is still executing, it is queued serially
|
|
||||||
// and executed when the previous tagged future completes.
|
|
||||||
// When a tagged serialFuture finishes executing, the onDone callback is called.
|
|
||||||
// If an unhandled exception happens in the closure future, the onError callback
|
|
||||||
// is called.
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'async_tag_lock.dart';
|
|
||||||
|
|
||||||
AsyncTagLock<Object> _keys = AsyncTagLock();
|
|
||||||
typedef SerialFutureQueueItem = Future<void> Function();
|
|
||||||
Map<Object, Queue<SerialFutureQueueItem>> _queues = {};
|
|
||||||
|
|
||||||
SerialFutureQueueItem _makeSerialFutureQueueItem<T>(
|
|
||||||
Future<T> Function() closure,
|
|
||||||
void Function(T)? onDone,
|
|
||||||
void Function(Object e, StackTrace? st)? onError) =>
|
|
||||||
() async {
|
|
||||||
try {
|
|
||||||
final out = await closure();
|
|
||||||
if (onDone != null) {
|
|
||||||
onDone(out);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
} catch (e, sp) {
|
|
||||||
if (onError != null) {
|
|
||||||
onError(e, sp);
|
|
||||||
} else {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void serialFuture<T>(Object tag, Future<T> Function() closure,
|
|
||||||
{void Function(T)? onDone,
|
|
||||||
void Function(Object e, StackTrace? st)? onError}) {
|
|
||||||
final queueItem = _makeSerialFutureQueueItem(closure, onDone, onError);
|
|
||||||
if (!_keys.tryLock(tag)) {
|
|
||||||
final queue = _queues[tag];
|
|
||||||
queue!.add(queueItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final queue = _queues[tag] = Queue.from([queueItem]);
|
|
||||||
unawaited(() async {
|
|
||||||
do {
|
|
||||||
final queueItem = queue.removeFirst();
|
|
||||||
await queueItem();
|
|
||||||
} while (queue.isNotEmpty);
|
|
||||||
_queues.remove(tag);
|
|
||||||
_keys.unlockTag(tag);
|
|
||||||
}());
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'async_tag_lock.dart';
|
|
||||||
|
|
||||||
AsyncTagLock<Object> _keys = AsyncTagLock();
|
|
||||||
|
|
||||||
/// Process a single future at a time per tag
|
|
||||||
///
|
|
||||||
/// The closure function is called to produce the future that is to be executed.
|
|
||||||
/// If a future with a particular tag is still executing, the onBusy callback
|
|
||||||
/// is called.
|
|
||||||
/// When a tagged singleFuture finishes executing, the onDone callback is called.
|
|
||||||
/// If an unhandled exception happens in the closure future, the onError callback
|
|
||||||
/// is called.
|
|
||||||
void singleFuture<T>(Object tag, Future<T> Function() closure,
|
|
||||||
{void Function()? onBusy,
|
|
||||||
void Function(T)? onDone,
|
|
||||||
void Function(Object e, StackTrace? st)? onError}) {
|
|
||||||
if (!_keys.tryLock(tag)) {
|
|
||||||
if (onBusy != null) {
|
|
||||||
onBusy();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
unawaited(() async {
|
|
||||||
try {
|
|
||||||
final out = await closure();
|
|
||||||
if (onDone != null) {
|
|
||||||
onDone(out);
|
|
||||||
}
|
|
||||||
// ignore: avoid_catches_without_on_clauses
|
|
||||||
} catch (e, sp) {
|
|
||||||
if (onError != null) {
|
|
||||||
onError(e, sp);
|
|
||||||
} else {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
_keys.unlockTag(tag);
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> singleFuturePause(Object tag) async => _keys.lockTag(tag);
|
|
||||||
void singleFutureResume(Object tag) => _keys.unlockTag(tag);
|
|
@ -1,67 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import '../async_tools.dart';
|
|
||||||
|
|
||||||
// Process a single state update at a time ensuring the most
|
|
||||||
// recent state gets processed asynchronously, possibly skipping
|
|
||||||
// states that happen while a previous state is still being processed.
|
|
||||||
//
|
|
||||||
// Eventually this will always process the most recent state passed to
|
|
||||||
// updateState.
|
|
||||||
//
|
|
||||||
// This is useful for processing state changes asynchronously without waiting
|
|
||||||
// from a synchronous execution context
|
|
||||||
class SingleStateProcessor<State> {
|
|
||||||
SingleStateProcessor();
|
|
||||||
|
|
||||||
void updateState(State newInputState, Future<void> Function(State) closure) {
|
|
||||||
// Use a singlefuture here to ensure we get dont lose any updates
|
|
||||||
// If the input stream gives us an update while we are
|
|
||||||
// still processing the last update, the most recent input state will
|
|
||||||
// be saved and processed eventually.
|
|
||||||
|
|
||||||
singleFuture(this, () async {
|
|
||||||
var newState = newInputState;
|
|
||||||
var newClosure = closure;
|
|
||||||
var done = false;
|
|
||||||
while (!done) {
|
|
||||||
await newClosure(newState);
|
|
||||||
|
|
||||||
// See if there's another state change to process
|
|
||||||
final nextState = _nextState;
|
|
||||||
final nextClosure = _nextClosure;
|
|
||||||
_nextState = null;
|
|
||||||
_nextClosure = null;
|
|
||||||
if (nextState != null) {
|
|
||||||
newState = nextState;
|
|
||||||
newClosure = nextClosure!;
|
|
||||||
} else {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, onBusy: () {
|
|
||||||
// Keep this state until we process again
|
|
||||||
_nextState = newInputState;
|
|
||||||
_nextClosure = closure;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pause() => singleFuturePause(this);
|
|
||||||
Future<void> resume() async {
|
|
||||||
// Process any next state before resuming the singlefuture
|
|
||||||
try {
|
|
||||||
final nextState = _nextState;
|
|
||||||
final nextClosure = _nextClosure;
|
|
||||||
_nextState = null;
|
|
||||||
_nextClosure = null;
|
|
||||||
if (nextState != null) {
|
|
||||||
await nextClosure!(nextState);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
singleFutureResume(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
State? _nextState;
|
|
||||||
Future<void> Function(State)? _nextClosure;
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import '../async_tools.dart';
|
|
||||||
|
|
||||||
// Process a single stateless update at a time ensuring each request
|
|
||||||
// gets processed asynchronously, and continuously while update is requested.
|
|
||||||
//
|
|
||||||
// This is useful for processing updates asynchronously without waiting
|
|
||||||
// from a synchronous execution context
|
|
||||||
class SingleStatelessProcessor {
|
|
||||||
SingleStatelessProcessor();
|
|
||||||
|
|
||||||
void update(Future<void> Function() closure) {
|
|
||||||
singleFuture(this, () async {
|
|
||||||
do {
|
|
||||||
_more = false;
|
|
||||||
await closure();
|
|
||||||
|
|
||||||
// See if another update was requested
|
|
||||||
} while (_more);
|
|
||||||
}, onBusy: () {
|
|
||||||
// Keep this state until we process again
|
|
||||||
_more = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Like update, but with a busy wrapper that
|
|
||||||
// clears once the updating is finished
|
|
||||||
void busyUpdate<T, S>(
|
|
||||||
Future<void> Function(Future<void> Function(void Function(S))) busy,
|
|
||||||
Future<void> Function(void Function(S)) closure) {
|
|
||||||
singleFuture(
|
|
||||||
this,
|
|
||||||
() async => busy((emit) async {
|
|
||||||
do {
|
|
||||||
_more = false;
|
|
||||||
await closure(emit);
|
|
||||||
|
|
||||||
// See if another update was requested
|
|
||||||
} while (_more);
|
|
||||||
}), onBusy: () {
|
|
||||||
// Keep this state until we process again
|
|
||||||
_more = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _more = false;
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
class WaitSet {
|
|
||||||
WaitSet();
|
|
||||||
|
|
||||||
void add(Future<void> Function() closure) {
|
|
||||||
_futures.add(Future.delayed(Duration.zero, closure));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> call() async {
|
|
||||||
final futures = _futures;
|
|
||||||
_futures = [];
|
|
||||||
if (futures.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await futures.wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Future<void>> _futures = [];
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
name: async_tools
|
|
||||||
description: Useful data structures and tools for async/Future code
|
|
||||||
version: 1.0.0
|
|
||||||
publish_to: none
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
|
||||||
|
|
||||||
# Add regular dependencies here.
|
|
||||||
dependencies:
|
|
||||||
freezed_annotation: ^2.4.1
|
|
||||||
mutex:
|
|
||||||
path: ../mutex
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
build_runner: ^2.4.8
|
|
||||||
freezed: ^2.4.7
|
|
||||||
lint_hard: ^4.0.0
|
|
||||||
test: ^1.25.2
|
|
@ -1,16 +0,0 @@
|
|||||||
// import 'package:async_tools/async_tools.dart';
|
|
||||||
// import 'package:test/test.dart';
|
|
||||||
|
|
||||||
// void main() {
|
|
||||||
// group('A group of tests', () {
|
|
||||||
// final awesome = Awesome();
|
|
||||||
|
|
||||||
// setUp(() {
|
|
||||||
// // Additional setup goes here.
|
|
||||||
// });
|
|
||||||
|
|
||||||
// test('First Test', () {
|
|
||||||
// expect(awesome.isAwesome, isTrue);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
7
packages/bloc_tools/.gitignore
vendored
7
packages/bloc_tools/.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
# https://dart.dev/guides/libraries/private-files
|
|
||||||
# Created by `dart pub`
|
|
||||||
.dart_tool/
|
|
||||||
|
|
||||||
# Avoid committing pubspec.lock for library packages; see
|
|
||||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
|
||||||
pubspec.lock
|
|
@ -1,15 +0,0 @@
|
|||||||
include: package:lint_hard/all.yaml
|
|
||||||
analyzer:
|
|
||||||
errors:
|
|
||||||
invalid_annotation_target: ignore
|
|
||||||
exclude:
|
|
||||||
- '**/*.g.dart'
|
|
||||||
- '**/*.freezed.dart'
|
|
||||||
- '**/*.pb.dart'
|
|
||||||
- '**/*.pbenum.dart'
|
|
||||||
- '**/*.pbjson.dart'
|
|
||||||
- '**/*.pbserver.dart'
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
unawaited_futures: true
|
|
||||||
avoid_positional_boolean_parameters: false
|
|
@ -1,6 +0,0 @@
|
|||||||
// import 'package:bloc_tools/bloc_tools.dart';
|
|
||||||
|
|
||||||
// void main() {
|
|
||||||
// var awesome = Awesome();
|
|
||||||
// print('awesome: ${awesome.isAwesome}');
|
|
||||||
// }
|
|
@ -1,11 +0,0 @@
|
|||||||
/// BLoC Tools
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'src/async_transformer_cubit.dart';
|
|
||||||
export 'src/bloc_busy_wrapper.dart';
|
|
||||||
export 'src/bloc_map_cubit.dart';
|
|
||||||
export 'src/bloc_tools_extension.dart';
|
|
||||||
export 'src/future_cubit.dart';
|
|
||||||
export 'src/state_map_follower.dart';
|
|
||||||
export 'src/stream_wrapper_cubit.dart';
|
|
||||||
export 'src/transformer_cubit.dart';
|
|
@ -1,48 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
|
|
||||||
// A cubit with state T that wraps another input cubit of state S and
|
|
||||||
// produces T fro S via an asynchronous transform closure
|
|
||||||
// The input cubit becomes 'owned' by the AsyncTransformerCubit and will
|
|
||||||
// be closed when the AsyncTransformerCubit closes.
|
|
||||||
|
|
||||||
class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
|
|
||||||
AsyncTransformerCubit(this.input, {required this.transform})
|
|
||||||
: super(const AsyncValue.loading()) {
|
|
||||||
_asyncTransform(input.state);
|
|
||||||
_subscription = input.stream.listen(_asyncTransform);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _asyncTransform(AsyncValue<S> newInputState) {
|
|
||||||
_singleStateProcessor.updateState(newInputState, (newState) async {
|
|
||||||
// Emit the transformed state
|
|
||||||
try {
|
|
||||||
if (newState is AsyncLoading<S>) {
|
|
||||||
emit(const AsyncValue.loading());
|
|
||||||
} else if (newState is AsyncError<S>) {
|
|
||||||
emit(AsyncValue.error(newState.error, newState.stackTrace));
|
|
||||||
} else {
|
|
||||||
final transformedState = await transform(newState.asData!.value);
|
|
||||||
emit(transformedState);
|
|
||||||
}
|
|
||||||
} on Exception catch (e, st) {
|
|
||||||
emit(AsyncValue.error(e, st));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _subscription.cancel();
|
|
||||||
await input.close();
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Cubit<AsyncValue<S>> input;
|
|
||||||
final SingleStateProcessor<AsyncValue<S>> _singleStateProcessor =
|
|
||||||
SingleStateProcessor();
|
|
||||||
Future<AsyncValue<T>> Function(S) transform;
|
|
||||||
late final StreamSubscription<AsyncValue<S>> _subscription;
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BlocBusyState<S> extends Equatable {
|
|
||||||
const BlocBusyState(this.state) : busy = false;
|
|
||||||
const BlocBusyState._busy(this.state) : busy = true;
|
|
||||||
final bool busy;
|
|
||||||
final S state;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [busy, state];
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin BlocBusyWrapper<S> on BlocBase<BlocBusyState<S>> {
|
|
||||||
Future<T> busyValue<T>(Future<T> Function(void Function(S) emit) closure) =>
|
|
||||||
_mutex.protect(() async {
|
|
||||||
void busyemit(S state) {
|
|
||||||
changedState = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn on busy state
|
|
||||||
emit(BlocBusyState._busy(state.state));
|
|
||||||
|
|
||||||
// Run the closure
|
|
||||||
final out = await closure(busyemit);
|
|
||||||
|
|
||||||
// If the closure did one or more 'busy emits' then
|
|
||||||
// take the most recent one and emit it for real
|
|
||||||
final finalState = changedState;
|
|
||||||
if (finalState != null && finalState != state.state) {
|
|
||||||
emit(BlocBusyState._busy(finalState));
|
|
||||||
} else {
|
|
||||||
emit(BlocBusyState._busy(state.state));
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> busy(Future<void> Function(void Function(S) emit) closure) =>
|
|
||||||
_mutex.protect(() async {
|
|
||||||
void busyemit(S state) {
|
|
||||||
changedState = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn on busy state
|
|
||||||
emit(BlocBusyState._busy(state.state));
|
|
||||||
|
|
||||||
// Run the closure
|
|
||||||
await closure(busyemit);
|
|
||||||
|
|
||||||
// If the closure did one or more 'busy emits' then
|
|
||||||
// take the most recent one and emit it for real and
|
|
||||||
// turn off the busy state
|
|
||||||
final finalState = changedState;
|
|
||||||
if (finalState != null && finalState != state.state) {
|
|
||||||
emit(BlocBusyState(finalState));
|
|
||||||
} else {
|
|
||||||
emit(BlocBusyState(state.state));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
void changeState(S state) {
|
|
||||||
if (_mutex.isLocked) {
|
|
||||||
changedState = state;
|
|
||||||
} else {
|
|
||||||
emit(BlocBusyState(state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isBusy => _mutex.isLocked;
|
|
||||||
|
|
||||||
final Mutex _mutex = Mutex();
|
|
||||||
S? changedState;
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import 'state_map_follower.dart';
|
|
||||||
|
|
||||||
typedef BlocMapState<K, S> = IMap<K, S>;
|
|
||||||
|
|
||||||
class _ItemEntry<S, B> {
|
|
||||||
_ItemEntry({required this.bloc, required this.subscription});
|
|
||||||
final B bloc;
|
|
||||||
final StreamSubscription<S> subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Streaming container cubit that is a map from some immutable key
|
|
||||||
// to a some other cubit's output state. Output state for this container
|
|
||||||
// cubit is an immutable map of the key to the output state of the contained
|
|
||||||
// cubits.
|
|
||||||
//
|
|
||||||
// K = Key type for the bloc map, used to look up some mapped cubit
|
|
||||||
// V = State type for the value, keys will look up values of this type
|
|
||||||
// B = Bloc/cubit type for the value, output states of type S
|
|
||||||
abstract class BlocMapCubit<K, V, B extends BlocBase<V>>
|
|
||||||
extends Cubit<BlocMapState<K, V>>
|
|
||||||
with StateMapFollowable<BlocMapState<K, V>, K, V> {
|
|
||||||
BlocMapCubit()
|
|
||||||
: _entries = {},
|
|
||||||
_tagLock = AsyncTagLock(),
|
|
||||||
super(IMap<K, V>());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _entries.values.map((e) => e.subscription.cancel()).wait;
|
|
||||||
await _entries.values.map((e) => e.bloc.close()).wait;
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
@override
|
|
||||||
// ignore: unnecessary_overrides
|
|
||||||
void emit(BlocMapState<K, V> state) {
|
|
||||||
super.emit(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> add(MapEntry<K, B> Function() create) {
|
|
||||||
// Create new element
|
|
||||||
final newElement = create();
|
|
||||||
final key = newElement.key;
|
|
||||||
final bloc = newElement.value;
|
|
||||||
|
|
||||||
return _tagLock.protect(key, closure: () async {
|
|
||||||
// Remove entry with the same key if it exists
|
|
||||||
await _internalRemove(key);
|
|
||||||
|
|
||||||
// Add entry with this key
|
|
||||||
_entries[key] = _ItemEntry(
|
|
||||||
bloc: bloc,
|
|
||||||
subscription: bloc.stream.listen((data) {
|
|
||||||
// Add sub-cubit's state to the map state
|
|
||||||
emit(state.add(key, data));
|
|
||||||
}));
|
|
||||||
|
|
||||||
emit(state.add(key, bloc.state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addState(K key, V value) =>
|
|
||||||
_tagLock.protect(key, closure: () async {
|
|
||||||
// Remove entry with the same key if it exists
|
|
||||||
await _internalRemove(key);
|
|
||||||
|
|
||||||
emit(state.add(key, value));
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> _internalRemove(K key) async {
|
|
||||||
final sub = _entries.remove(key);
|
|
||||||
if (sub != null) {
|
|
||||||
await sub.subscription.cancel();
|
|
||||||
await sub.bloc.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> remove(K key) => _tagLock.protect(key, closure: () async {
|
|
||||||
await _internalRemove(key);
|
|
||||||
emit(state.remove(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
R operate<R>(K key, {required R Function(B bloc) closure}) {
|
|
||||||
final bloc = _entries[key]!.bloc;
|
|
||||||
return closure(bloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
R? tryOperate<R>(K key, {required R Function(B bloc) closure}) {
|
|
||||||
final entry = _entries[key];
|
|
||||||
if (entry == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return closure(entry.bloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<R> operateAsync<R>(K key,
|
|
||||||
{required Future<R> Function(B bloc) closure}) =>
|
|
||||||
_tagLock.protect(key, closure: () async {
|
|
||||||
final bloc = _entries[key]!.bloc;
|
|
||||||
return closure(bloc);
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<R?> tryOperateAsync<R>(K key,
|
|
||||||
{required Future<R> Function(B bloc) closure}) =>
|
|
||||||
_tagLock.protect(key, closure: () async {
|
|
||||||
final entry = _entries[key];
|
|
||||||
if (entry == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return closure(entry.bloc);
|
|
||||||
});
|
|
||||||
|
|
||||||
/// StateMapFollowable /////////////////////////
|
|
||||||
@override
|
|
||||||
IMap<K, V> getStateMap(BlocMapState<K, V> s) => s;
|
|
||||||
|
|
||||||
final Map<K, _ItemEntry<V, B>> _entries;
|
|
||||||
final AsyncTagLock<K> _tagLock;
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
|
||||||
|
|
||||||
mixin BlocTools<State> on BlocBase<State> {
|
|
||||||
void withStateListen(void Function(State event)? onData,
|
|
||||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
|
||||||
if (onData != null) {
|
|
||||||
onData(state);
|
|
||||||
}
|
|
||||||
stream.listen(onData,
|
|
||||||
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
|
|
||||||
abstract class FutureCubit<State> extends Cubit<AsyncValue<State>> {
|
|
||||||
FutureCubit(Future<State> fut) : super(const AsyncValue.loading()) {
|
|
||||||
_initWait.add(() async => fut.then((value) {
|
|
||||||
emit(AsyncValue.data(value));
|
|
||||||
// ignore: avoid_types_on_closure_parameters
|
|
||||||
}, onError: (Object e, StackTrace stackTrace) {
|
|
||||||
emit(AsyncValue.error(e, stackTrace));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
FutureCubit.value(State state) : super(AsyncValue.data(state));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _initWait();
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
final WaitSet _initWait = WaitSet();
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
/// Mixin that automatically keeps two blocs/cubits in sync with each other
|
|
||||||
/// Useful for having a BlocMapCubit 'follow' the state of another input cubit.
|
|
||||||
/// As the state of the input cubit changes, the BlocMapCubit can add/remove
|
|
||||||
/// mapped Cubits that automatically process the input state reactively.
|
|
||||||
///
|
|
||||||
/// S = Input state type
|
|
||||||
/// K = Key derived from elements of input state
|
|
||||||
/// V = Value derived from elements of input state
|
|
||||||
mixin StateMapFollower<S extends Object, K, V> on Closable {
|
|
||||||
void follow(StateMapFollowable<S, K, V> followable) {
|
|
||||||
assert(_following == null, 'can only follow one followable at a time');
|
|
||||||
_following = followable;
|
|
||||||
_lastInputStateMap = IMap();
|
|
||||||
_subscription = followable.registerFollower(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> unfollow() async {
|
|
||||||
await _subscription?.cancel();
|
|
||||||
_subscription = null;
|
|
||||||
_following?.unregisterFollower(this);
|
|
||||||
_following = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@mustCallSuper
|
|
||||||
Future<void> close() async {
|
|
||||||
await unfollow();
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeFromState(K key);
|
|
||||||
Future<void> updateState(K key, V value);
|
|
||||||
|
|
||||||
void _updateFollow(IMap<K, V> newInputState) {
|
|
||||||
final following = _following;
|
|
||||||
if (following == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_singleStateProcessor.updateState(newInputState, (newStateMap) async {
|
|
||||||
for (final k in _lastInputStateMap.keys) {
|
|
||||||
if (!newStateMap.containsKey(k)) {
|
|
||||||
// deleted
|
|
||||||
await removeFromState(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final newEntry in newStateMap.entries) {
|
|
||||||
final v = _lastInputStateMap.get(newEntry.key);
|
|
||||||
if (v == null || v != newEntry.value) {
|
|
||||||
// added or changed
|
|
||||||
await updateState(newEntry.key, newEntry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep this state map for the next time
|
|
||||||
_lastInputStateMap = newStateMap;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
StateMapFollowable<S, K, V>? _following;
|
|
||||||
late IMap<K, V> _lastInputStateMap;
|
|
||||||
late StreamSubscription<IMap<K, V>>? _subscription;
|
|
||||||
final SingleStateProcessor<IMap<K, V>> _singleStateProcessor =
|
|
||||||
SingleStateProcessor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Interface that allows a StateMapFollower to follow some other class's
|
|
||||||
/// state changes
|
|
||||||
abstract mixin class StateMapFollowable<S extends Object, K, V>
|
|
||||||
implements StateStreamable<S> {
|
|
||||||
IMap<K, V> getStateMap(S state);
|
|
||||||
|
|
||||||
StreamSubscription<IMap<K, V>> registerFollower(
|
|
||||||
StateMapFollower<S, K, V> follower) {
|
|
||||||
final stateMapTransformer = StreamTransformer<S, IMap<K, V>>.fromHandlers(
|
|
||||||
handleData: (d, s) => s.add(getStateMap(d)));
|
|
||||||
|
|
||||||
if (_followers.isEmpty) {
|
|
||||||
// start transforming stream
|
|
||||||
_transformedStream = stream.transform(stateMapTransformer);
|
|
||||||
}
|
|
||||||
_followers.add(follower);
|
|
||||||
follower._updateFollow(getStateMap(state));
|
|
||||||
return _transformedStream!.listen((s) => follower._updateFollow(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregisterFollower(StateMapFollower<S, K, V> follower) {
|
|
||||||
_followers.remove(follower);
|
|
||||||
if (_followers.isEmpty) {
|
|
||||||
// stop transforming stream
|
|
||||||
_transformedStream = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<T> syncFollowers<T>(Future<T> Function() closure) async {
|
|
||||||
// pause all followers
|
|
||||||
await _followers.map((f) => f._singleStateProcessor.pause()).wait;
|
|
||||||
|
|
||||||
// run closure
|
|
||||||
final out = await closure();
|
|
||||||
|
|
||||||
// resume all followers and wait for current state map to be updated
|
|
||||||
final resumeState = getStateMap(state);
|
|
||||||
await _followers.map((f) async {
|
|
||||||
// Ensure the latest state has been updated
|
|
||||||
try {
|
|
||||||
f._updateFollow(resumeState);
|
|
||||||
} finally {
|
|
||||||
// Resume processing of the follower
|
|
||||||
await f._singleStateProcessor.resume();
|
|
||||||
}
|
|
||||||
}).wait;
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<IMap<K, V>>? _transformedStream;
|
|
||||||
final List<StateMapFollower<S, K, V>> _followers = [];
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
|
|
||||||
abstract class StreamWrapperCubit<State> extends Cubit<AsyncValue<State>> {
|
|
||||||
StreamWrapperCubit(Stream<State> stream, {State? defaultState})
|
|
||||||
: super(defaultState != null
|
|
||||||
? AsyncValue.data(defaultState)
|
|
||||||
: const AsyncValue.loading()) {
|
|
||||||
_subscription = stream.listen((event) => emit(AsyncValue.data(event)),
|
|
||||||
// ignore: avoid_types_on_closure_parameters
|
|
||||||
onError: (Object error, StackTrace stackTrace) {
|
|
||||||
emit(AsyncValue.error(error, stackTrace));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _subscription.cancel();
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
late final StreamSubscription<State> _subscription;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
|
||||||
|
|
||||||
class TransformerCubit<T, S> extends Cubit<T> {
|
|
||||||
TransformerCubit(this.input, {required this.transform})
|
|
||||||
: super(transform(input.state)) {
|
|
||||||
_subscription = input.stream.listen((event) => emit(transform(event)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _subscription.cancel();
|
|
||||||
await input.close();
|
|
||||||
await super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Cubit<S> input;
|
|
||||||
T Function(S) transform;
|
|
||||||
late final StreamSubscription<S> _subscription;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
name: bloc_tools
|
|
||||||
description: A starting point for Dart libraries or applications.
|
|
||||||
version: 1.0.0
|
|
||||||
publish_to: none
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
async_tools:
|
|
||||||
path: ../async_tools
|
|
||||||
bloc: ^8.1.3
|
|
||||||
equatable: ^2.0.5
|
|
||||||
fast_immutable_collections: ^10.1.1
|
|
||||||
freezed_annotation: ^2.4.1
|
|
||||||
meta: ^1.11.0
|
|
||||||
mutex:
|
|
||||||
path: ../mutex
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
build_runner: ^2.4.8
|
|
||||||
freezed: ^2.4.7
|
|
||||||
lint_hard: ^4.0.0
|
|
||||||
test: ^1.25.2
|
|
@ -1,16 +0,0 @@
|
|||||||
// import 'package:bloc_tools/bloc_tools.dart';
|
|
||||||
// import 'package:test/test.dart';
|
|
||||||
|
|
||||||
// void main() {
|
|
||||||
// group('A group of tests', () {
|
|
||||||
// final awesome = Awesome();
|
|
||||||
|
|
||||||
// setUp(() {
|
|
||||||
// // Additional setup goes here.
|
|
||||||
// });
|
|
||||||
|
|
||||||
// test('First Test', () {
|
|
||||||
// expect(awesome.isAwesome, isTrue);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
16
packages/mutex/.gitignore
vendored
16
packages/mutex/.gitignore
vendored
@ -1,16 +0,0 @@
|
|||||||
# Files and directories created by pub
|
|
||||||
.packages
|
|
||||||
.pub/
|
|
||||||
.dart_tool/
|
|
||||||
build/
|
|
||||||
packages
|
|
||||||
pubspec.lock
|
|
||||||
|
|
||||||
# Directory created by dartdoc
|
|
||||||
doc/api/
|
|
||||||
|
|
||||||
# JetBrains IDEs
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
@ -1,50 +0,0 @@
|
|||||||
## 3.1.0
|
|
||||||
|
|
||||||
- Increased minimum Dart SDK to 2.15.0 for `unawaited` function.
|
|
||||||
- Added development dependencies lints ^2.1.1 and pana: ^0.21.37.
|
|
||||||
- Fixed code to remove lint warnings.
|
|
||||||
|
|
||||||
## 3.0.1
|
|
||||||
|
|
||||||
- Fixed bug with new read mutexes preventing a write mutex from being acquired.
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
- BREAKING CHANGE: critical section functions must return a Future.
|
|
||||||
- This is unlikely to affect real-world code, since only functions
|
|
||||||
containing asynchronous code would be critical.
|
|
||||||
- Protect method returns Future to the value from the critical section.
|
|
||||||
|
|
||||||
## 2.0.0
|
|
||||||
|
|
||||||
- Null safety release.
|
|
||||||
|
|
||||||
## 2.0.0-nullsafety.0
|
|
||||||
|
|
||||||
- Pre-release version: updated library to null safety (Non-nullable by default).
|
|
||||||
- Removed support for Dart 1.x.
|
|
||||||
|
|
||||||
## 1.1.0
|
|
||||||
|
|
||||||
- Added protect, protectRead and protectWrite convenience methods.
|
|
||||||
- Improved tests to not depend on timing.
|
|
||||||
|
|
||||||
## 1.0.3
|
|
||||||
|
|
||||||
- Added an example.
|
|
||||||
|
|
||||||
## 1.0.2
|
|
||||||
|
|
||||||
- Code clean up to satisfy pana 0.13.2 health checks.
|
|
||||||
|
|
||||||
## 1.0.1
|
|
||||||
|
|
||||||
- Fixed dartanalyzer warnings.
|
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
- Updated the upper bound of the SDK constraint to <3.0.0.
|
|
||||||
|
|
||||||
## 0.0.1
|
|
||||||
|
|
||||||
- Initial version
|
|
@ -1,24 +0,0 @@
|
|||||||
Copyright (c) 2016, Hoylen Sue.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
* Neither the name of the <organization> nor the
|
|
||||||
names of its contributors may be used to endorse or promote products
|
|
||||||
derived from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
||||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,191 +0,0 @@
|
|||||||
# mutex
|
|
||||||
|
|
||||||
A library for creating locks to ensure mutual exclusion when
|
|
||||||
running critical sections of code.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Mutexes can be used to protect critical sections of code to prevent
|
|
||||||
race conditions.
|
|
||||||
|
|
||||||
Although Dart uses a single thread of execution, race conditions
|
|
||||||
can still occur when asynchronous operations are used inside
|
|
||||||
critical sections. For example,
|
|
||||||
|
|
||||||
x = 42;
|
|
||||||
synchronousOperations(); // this does not modify x
|
|
||||||
assert(x == 42); // x will NOT have changed
|
|
||||||
|
|
||||||
y = 42; // a variable that other asynchronous code can modify
|
|
||||||
await asynchronousOperations(); // this does NOT modify y, but...
|
|
||||||
// There is NO GUARANTEE other async code didn't run and change it!
|
|
||||||
assert(y == 42 || y != 42); // WARNING: y might have changed
|
|
||||||
|
|
||||||
An example is when Dart is used to implement a server-side Web server
|
|
||||||
that updates a database (assuming database transactions are not being
|
|
||||||
used). The update involves querying the database, performing
|
|
||||||
calculations on those retrieved values, and then updating the database
|
|
||||||
with the result. You don't want the database to be changed by
|
|
||||||
"something else" while performing the calculations, since the results
|
|
||||||
you would write will not incorporate those other changes. That
|
|
||||||
"something else" could be the same Web server handling another request
|
|
||||||
in parallel.
|
|
||||||
|
|
||||||
This package provides a normal mutex and a read-write mutex.
|
|
||||||
|
|
||||||
## Mutex
|
|
||||||
|
|
||||||
A mutex guarantees at most only one lock can exist at any one time.
|
|
||||||
|
|
||||||
If the lock has already been acquired, attempts to acquire another
|
|
||||||
lock will be blocked until the lock has been released.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
final m = Mutex();
|
|
||||||
```
|
|
||||||
|
|
||||||
Acquiring the lock before running the critical section of code,
|
|
||||||
and then releasing the lock.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await m.acquire();
|
|
||||||
// No other lock can be acquired until the lock is released
|
|
||||||
|
|
||||||
try {
|
|
||||||
// critical section with asynchronous code
|
|
||||||
await ...
|
|
||||||
} finally {
|
|
||||||
m.release();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### protect
|
|
||||||
|
|
||||||
The following code uses the _protect_ convenience method to do the
|
|
||||||
same thing as the above code. Use the convenence method whenever
|
|
||||||
possible, since it ensures the lock will always be released.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await m.protect(() async {
|
|
||||||
// critical section
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
If the critial section returns a Future to a value, the _protect_
|
|
||||||
convenience method will return a Future to that value.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final result = await m.protect<int>(() async {
|
|
||||||
// critical section
|
|
||||||
return valueFromCriticalSection;
|
|
||||||
});
|
|
||||||
// result contains the valueFromCriticalSection
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Read-write mutex
|
|
||||||
|
|
||||||
A read-write mutex allows multiple _reads locks_ to be exist
|
|
||||||
simultaneously, but at most only one _write lock_ can exist at any one
|
|
||||||
time. A _write lock_ and any _read locks_ cannot both exist together
|
|
||||||
at the same time.
|
|
||||||
|
|
||||||
If there is one or more _read locks_, attempts to acquire a _write
|
|
||||||
lock_ will be blocked until all the _read locks_ have been
|
|
||||||
released. But attempts to acquire more _read locks_ will not be
|
|
||||||
blocked. If there is a _write lock_, attempts to acquire any lock
|
|
||||||
(read or write) will be blocked until that _write lock_ is released.
|
|
||||||
|
|
||||||
A read-write mutex can also be described as a single-writer mutex,
|
|
||||||
multiple-reader mutex, or a reentrant lock.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
```
|
|
||||||
|
|
||||||
Acquiring a write lock:
|
|
||||||
|
|
||||||
await m.acquireWrite();
|
|
||||||
// No other locks (read or write) can be acquired until released
|
|
||||||
|
|
||||||
try {
|
|
||||||
// critical write section with asynchronous code
|
|
||||||
await ...
|
|
||||||
} finally {
|
|
||||||
m.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
Acquiring a read lock:
|
|
||||||
|
|
||||||
await m.acquireRead();
|
|
||||||
// No write lock can be acquired until all read locks are released,
|
|
||||||
// but additional read locks can be acquired.
|
|
||||||
|
|
||||||
try {
|
|
||||||
// critical read section with asynchronous code
|
|
||||||
await ...
|
|
||||||
} finally {
|
|
||||||
m.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
### protectWrite and protectRead
|
|
||||||
|
|
||||||
The following code uses the _protectWrite_ and _protectRead_
|
|
||||||
convenience methods to do the same thing as the above code. Use the
|
|
||||||
convenence method whenever possible, since it ensures the lock will
|
|
||||||
always be released.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await m.protectWrite(() async {
|
|
||||||
// critical write section
|
|
||||||
});
|
|
||||||
|
|
||||||
await m.protectRead(() async {
|
|
||||||
// critical read section
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
If the critial section returns a Future to a value, these convenience
|
|
||||||
methods will return a Future to that value.
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final result1 await m.protectWrite<String>(() async {
|
|
||||||
// critical write section
|
|
||||||
return valueFromCritialSection1;
|
|
||||||
});
|
|
||||||
// result1 contains the valueFromCriticalSection1
|
|
||||||
|
|
||||||
final result2 = await m.protectRead(() async {
|
|
||||||
// critical read section
|
|
||||||
return valueFromCritialSection2;
|
|
||||||
});
|
|
||||||
// result2 contains the valueFromCriticalSection2
|
|
||||||
```
|
|
||||||
|
|
||||||
## When mutual exclusion is not needed
|
|
||||||
|
|
||||||
The critical section should always contain some asynchronous code. If
|
|
||||||
the critical section only contains synchronous code, there is no need
|
|
||||||
to put it in a critical section. In Dart, synchronous code cannot be
|
|
||||||
interrupted, so there is no need to protect it using mutual exclusion.
|
|
||||||
|
|
||||||
Also, if the critical section does not involve data or shared
|
|
||||||
resources that can be accessed by other asynchronous code, it also
|
|
||||||
does not need to be protected. For example, if it only uses local
|
|
||||||
variables that other asynchronous code won't have access to: while the
|
|
||||||
other asynchronous code could run, it won't be able to make unexpected
|
|
||||||
changes to the local variables it can't access.
|
|
||||||
|
|
||||||
## Features and bugs
|
|
||||||
|
|
||||||
Please file feature requests and bugs at the [issue tracker][tracker].
|
|
||||||
|
|
||||||
[tracker]: https://github.com/hoylen/dart-mutex/issues
|
|
@ -1,15 +0,0 @@
|
|||||||
include: package:lint_hard/all.yaml
|
|
||||||
analyzer:
|
|
||||||
errors:
|
|
||||||
invalid_annotation_target: ignore
|
|
||||||
exclude:
|
|
||||||
- '**/*.g.dart'
|
|
||||||
- '**/*.freezed.dart'
|
|
||||||
- '**/*.pb.dart'
|
|
||||||
- '**/*.pbenum.dart'
|
|
||||||
- '**/*.pbjson.dart'
|
|
||||||
- '**/*.pbserver.dart'
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
unawaited_futures: true
|
|
||||||
avoid_positional_boolean_parameters: false
|
|
@ -1,114 +0,0 @@
|
|||||||
// Mutex example.
|
|
||||||
//
|
|
||||||
// This example demonstrates why a mutex is needed.
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
// Random asynchronous delays to try and simulate race conditions.
|
|
||||||
|
|
||||||
const _maxDelay = 500; // milliseconds
|
|
||||||
|
|
||||||
final _random = Random();
|
|
||||||
|
|
||||||
Future<void> randomDelay() async {
|
|
||||||
await Future<void>.delayed(
|
|
||||||
Duration(milliseconds: _random.nextInt(_maxDelay)));
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
/// Account balance.
|
|
||||||
///
|
|
||||||
/// The classical example of a race condition is when a bank account is updated
|
|
||||||
/// by different simultaneous operations.
|
|
||||||
|
|
||||||
int balance = 0;
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
/// Deposit without using mutex.
|
|
||||||
|
|
||||||
Future<void> unsafeUpdate(int id, int depositAmount) async {
|
|
||||||
// Random delay before updating starts
|
|
||||||
await randomDelay();
|
|
||||||
|
|
||||||
// Add the deposit to the balance. But this operation is not atomic if
|
|
||||||
// there are asynchronous operations in it (as simulated by the randomDelay).
|
|
||||||
|
|
||||||
final oldBalance = balance;
|
|
||||||
await randomDelay();
|
|
||||||
balance = oldBalance + depositAmount;
|
|
||||||
|
|
||||||
print(' [$id] added $depositAmount to $oldBalance -> $balance');
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
/// Deposit using mutex.
|
|
||||||
|
|
||||||
Mutex m = Mutex();
|
|
||||||
|
|
||||||
Future<void> safeUpdate(int id, int depositAmount) async {
|
|
||||||
// Random delay before updating starts
|
|
||||||
await randomDelay();
|
|
||||||
|
|
||||||
// Acquire the mutex before running the critical section of code
|
|
||||||
|
|
||||||
await m.protect(() async {
|
|
||||||
// critical section
|
|
||||||
|
|
||||||
// This is the same as the unsafe update. But since it is performed only
|
|
||||||
// when the mutex is acquired, it is safe: no other safe update can happen
|
|
||||||
// until this mutex is released.
|
|
||||||
|
|
||||||
final oldBalance = balance;
|
|
||||||
await randomDelay();
|
|
||||||
balance = oldBalance + depositAmount;
|
|
||||||
|
|
||||||
// end of critical section
|
|
||||||
|
|
||||||
print(' [$id] added $depositAmount to $oldBalance -> $balance');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
/// Make a series of deposits and see if the final balance is correct.
|
|
||||||
|
|
||||||
Future<void> makeDeposits({bool safe = true}) async {
|
|
||||||
print(safe ? 'Using mutex:' : 'Not using mutex:');
|
|
||||||
|
|
||||||
const numberDeposits = 10;
|
|
||||||
const amount = 10;
|
|
||||||
|
|
||||||
balance = 0;
|
|
||||||
|
|
||||||
// Create a set of operations, each attempting to deposit the same amount
|
|
||||||
// into the account.
|
|
||||||
|
|
||||||
final operations = <Future<void>>[];
|
|
||||||
for (var x = 0; x < numberDeposits; x++) {
|
|
||||||
final f = (safe) ? safeUpdate(x, amount) : unsafeUpdate(x, amount);
|
|
||||||
operations.add(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all the deposit operations to finish
|
|
||||||
|
|
||||||
await Future.wait<void>(operations);
|
|
||||||
|
|
||||||
// Check if all of the operations succeeded
|
|
||||||
|
|
||||||
final expected = numberDeposits * amount;
|
|
||||||
if (balance != expected) {
|
|
||||||
print('Error: deposits were lost (final balance $balance != $expected)');
|
|
||||||
} else {
|
|
||||||
print('Success: no deposits were lost');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
await makeDeposits(safe: false);
|
|
||||||
print('');
|
|
||||||
await makeDeposits(safe: true);
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
// Copyright (c) 2016, Hoylen Sue. All rights reserved. Use of this source code
|
|
||||||
// is governed by a BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
/// Mutual exclusion.
|
|
||||||
///
|
|
||||||
library mutex;
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'src/mutex.dart';
|
|
||||||
part 'src/read_write_mutex.dart';
|
|
@ -1,89 +0,0 @@
|
|||||||
part of mutex;
|
|
||||||
|
|
||||||
/// Mutual exclusion.
|
|
||||||
///
|
|
||||||
/// The [protect] method is a convenience method for acquiring a lock before
|
|
||||||
/// running critical code, and then releasing the lock afterwards. Using this
|
|
||||||
/// convenience method will ensure the lock is always released after use.
|
|
||||||
///
|
|
||||||
/// Usage:
|
|
||||||
///
|
|
||||||
/// m = Mutex();
|
|
||||||
///
|
|
||||||
/// await m.protect(() async {
|
|
||||||
/// // critical section
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// Alternatively, a lock can be explicitly acquired and managed. In this
|
|
||||||
/// situation, the program is responsible for releasing the lock after it
|
|
||||||
/// have been used. Failure to release the lock will prevent other code for
|
|
||||||
/// ever acquiring the lock.
|
|
||||||
///
|
|
||||||
/// m = Mutex();
|
|
||||||
///
|
|
||||||
/// await m.acquire();
|
|
||||||
/// try {
|
|
||||||
/// // critical section
|
|
||||||
/// }
|
|
||||||
/// finally {
|
|
||||||
/// m.release();
|
|
||||||
/// }
|
|
||||||
|
|
||||||
class Mutex {
|
|
||||||
//================================================================
|
|
||||||
// Constructors
|
|
||||||
Mutex() : _rwMutex = ReadWriteMutex();
|
|
||||||
Mutex.locked() : _rwMutex = ReadWriteMutex.writeLocked();
|
|
||||||
|
|
||||||
// Implemented as a ReadWriteMutex that is used only with write locks.
|
|
||||||
final ReadWriteMutex _rwMutex;
|
|
||||||
|
|
||||||
/// Indicates if a lock has been acquired and not released.
|
|
||||||
bool get isLocked => _rwMutex.isLocked;
|
|
||||||
|
|
||||||
/// Acquire a lock
|
|
||||||
///
|
|
||||||
/// Returns a future that will be completed when the lock has been acquired.
|
|
||||||
///
|
|
||||||
/// Consider using the convenience method [protect], otherwise the caller
|
|
||||||
/// is responsible for making sure the lock is released after it is no longer
|
|
||||||
/// needed. Failure to release the lock means no other code can acquire the
|
|
||||||
/// lock.
|
|
||||||
|
|
||||||
Future<void> acquire() => _rwMutex.acquireWrite();
|
|
||||||
|
|
||||||
/// Release a lock.
|
|
||||||
///
|
|
||||||
/// Release a lock that has been acquired.
|
|
||||||
|
|
||||||
void release() => _rwMutex.release();
|
|
||||||
|
|
||||||
/// Convenience method for protecting a function with a lock.
|
|
||||||
///
|
|
||||||
/// This method guarantees a lock is always acquired before invoking the
|
|
||||||
/// [criticalSection] function. It also guarantees the lock is always
|
|
||||||
/// released.
|
|
||||||
///
|
|
||||||
/// A critical section should always contain asynchronous code, since purely
|
|
||||||
/// synchronous code does not need to be protected inside a critical section.
|
|
||||||
/// Therefore, the critical section is a function that returns a _Future_.
|
|
||||||
/// If the critical section does not need to return a value, it should be
|
|
||||||
/// defined as returning `Future<void>`.
|
|
||||||
///
|
|
||||||
/// Returns a _Future_ whose value is the value of the _Future_ returned by
|
|
||||||
/// the critical section.
|
|
||||||
///
|
|
||||||
/// An exception is thrown if the critical section throws an exception,
|
|
||||||
/// or an exception is thrown while waiting for the _Future_ returned by
|
|
||||||
/// the critical section to complete. The lock is released, when those
|
|
||||||
/// exceptions occur.
|
|
||||||
|
|
||||||
Future<T> protect<T>(Future<T> Function() criticalSection) async {
|
|
||||||
await acquire();
|
|
||||||
try {
|
|
||||||
return await criticalSection();
|
|
||||||
} finally {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,304 +0,0 @@
|
|||||||
part of mutex;
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
/// Internal representation of a request for a lock.
|
|
||||||
///
|
|
||||||
/// This is instantiated for each acquire and, if necessary, it is added
|
|
||||||
/// to the waiting queue.
|
|
||||||
|
|
||||||
class _ReadWriteMutexRequest {
|
|
||||||
/// Internal constructor.
|
|
||||||
///
|
|
||||||
/// The [isRead] indicates if this is a request for a read lock (true) or a
|
|
||||||
/// request for a write lock (false).
|
|
||||||
|
|
||||||
_ReadWriteMutexRequest({required this.isRead});
|
|
||||||
|
|
||||||
/// Indicates if this is a read or write lock.
|
|
||||||
|
|
||||||
final bool isRead; // true = read lock requested; false = write lock requested
|
|
||||||
|
|
||||||
/// The job's completer.
|
|
||||||
///
|
|
||||||
/// This [Completer] will complete when the job has acquired the lock.
|
|
||||||
|
|
||||||
final Completer<void> completer = Completer<void>();
|
|
||||||
}
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
/// Mutual exclusion that supports read and write locks.
|
|
||||||
///
|
|
||||||
/// Multiple read locks can be simultaneously acquired, but at most only
|
|
||||||
/// one write lock can be acquired at any one time.
|
|
||||||
///
|
|
||||||
/// **Protecting critical code**
|
|
||||||
///
|
|
||||||
/// The [protectWrite] and [protectRead] are convenience methods for acquiring
|
|
||||||
/// locks and releasing them. Using them will ensure the locks are always
|
|
||||||
/// released after use.
|
|
||||||
///
|
|
||||||
/// Create the mutex:
|
|
||||||
///
|
|
||||||
/// m = ReadWriteMutex();
|
|
||||||
///
|
|
||||||
/// Code protected by a write lock:
|
|
||||||
///
|
|
||||||
/// await m.protectWrite(() {
|
|
||||||
/// // critical write section
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// Other code can be protected by a read lock:
|
|
||||||
///
|
|
||||||
/// await m.protectRead(() {
|
|
||||||
/// // critical read section
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// **Explicitly managing locks**
|
|
||||||
///
|
|
||||||
/// Alternatively, the locks can be explicitly acquired and managed. In this
|
|
||||||
/// situation, the program is responsible for releasing the locks after they
|
|
||||||
/// have been used. Failure to release the lock will prevent other code for
|
|
||||||
/// ever acquiring a lock.
|
|
||||||
///
|
|
||||||
/// Create the mutex:
|
|
||||||
///
|
|
||||||
/// m = ReadWriteMutex();
|
|
||||||
///
|
|
||||||
/// Some code can acquire a write lock:
|
|
||||||
///
|
|
||||||
/// await m.acquireWrite();
|
|
||||||
/// try {
|
|
||||||
/// // critical write section
|
|
||||||
/// assert(m.isWriteLocked);
|
|
||||||
/// } finally {
|
|
||||||
/// m.release();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Other code can acquire a read lock.
|
|
||||||
///
|
|
||||||
/// await m.acquireRead();
|
|
||||||
/// try {
|
|
||||||
/// // critical read section
|
|
||||||
/// assert(m.isReadLocked);
|
|
||||||
/// } finally {
|
|
||||||
/// m.release();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// The current implementation lets locks be acquired in first-in-first-out
|
|
||||||
/// order. This ensures there will not be any lock starvation, which can
|
|
||||||
/// happen if some locks are prioritised over others. Submit a feature
|
|
||||||
/// request issue, if there is a need for another scheduling algorithm.
|
|
||||||
|
|
||||||
class ReadWriteMutex {
|
|
||||||
//================================================================
|
|
||||||
// Constructors
|
|
||||||
ReadWriteMutex();
|
|
||||||
ReadWriteMutex.writeLocked() : _state = -1;
|
|
||||||
ReadWriteMutex.readLocked(int? count) : _state = count ?? 1 {
|
|
||||||
assert(_state > 0, "can't have a negative read lock count");
|
|
||||||
}
|
|
||||||
|
|
||||||
//================================================================
|
|
||||||
// Members
|
|
||||||
|
|
||||||
/// List of requests waiting for a lock on this mutex.
|
|
||||||
|
|
||||||
final _waiting = <_ReadWriteMutexRequest>[];
|
|
||||||
|
|
||||||
/// State of the mutex
|
|
||||||
|
|
||||||
int _state = 0; // -1 = write lock, +ve = number of read locks; 0 = no lock
|
|
||||||
|
|
||||||
//================================================================
|
|
||||||
// Methods
|
|
||||||
|
|
||||||
/// Indicates if a lock (read or write) has been acquired and not released.
|
|
||||||
bool get isLocked => _state != 0;
|
|
||||||
|
|
||||||
/// Indicates if a write lock has been acquired and not released.
|
|
||||||
bool get isWriteLocked => _state == -1;
|
|
||||||
|
|
||||||
/// Indicates if one or more read locks has been acquired and not released.
|
|
||||||
bool get isReadLocked => 0 < _state;
|
|
||||||
|
|
||||||
/// Indicates the number of waiters on this mutex
|
|
||||||
int get waiters => _waiting.length;
|
|
||||||
|
|
||||||
/// Acquire a read lock
|
|
||||||
///
|
|
||||||
/// Returns a future that will be completed when the lock has been acquired.
|
|
||||||
///
|
|
||||||
/// A read lock can not be acquired when there is a write lock on the mutex.
|
|
||||||
/// But it can be acquired if there are other read locks.
|
|
||||||
///
|
|
||||||
/// Consider using the convenience method [protectRead], otherwise the caller
|
|
||||||
/// is responsible for making sure the lock is released after it is no longer
|
|
||||||
/// needed. Failure to release the lock means no other code can acquire a
|
|
||||||
/// write lock.
|
|
||||||
|
|
||||||
Future<void> acquireRead() => _acquire(isRead: true);
|
|
||||||
|
|
||||||
/// Acquire a write lock
|
|
||||||
///
|
|
||||||
/// Returns a future that will be completed when the lock has been acquired.
|
|
||||||
///
|
|
||||||
/// A write lock can only be acquired when there are no other locks (neither
|
|
||||||
/// read locks nor write locks) on the mutex.
|
|
||||||
///
|
|
||||||
/// Consider using the convenience method [protectWrite], otherwise the caller
|
|
||||||
/// is responsible for making sure the lock is released after it is no longer
|
|
||||||
/// needed. Failure to release the lock means no other code can acquire the
|
|
||||||
/// lock (neither a read lock or a write lock).
|
|
||||||
|
|
||||||
Future<void> acquireWrite() => _acquire(isRead: false);
|
|
||||||
|
|
||||||
/// Release a lock.
|
|
||||||
///
|
|
||||||
/// Release the lock that was previously acquired.
|
|
||||||
///
|
|
||||||
/// When the lock is released, locks waiting to be acquired can be acquired
|
|
||||||
/// depending on the type of lock waiting and if other locks have been
|
|
||||||
/// acquired.
|
|
||||||
///
|
|
||||||
/// A [StateError] is thrown if the mutex does not currently have a lock on
|
|
||||||
/// it.
|
|
||||||
|
|
||||||
void release() {
|
|
||||||
if (_state == -1) {
|
|
||||||
// Write lock released
|
|
||||||
_state = 0;
|
|
||||||
} else if (0 < _state) {
|
|
||||||
// Read lock released
|
|
||||||
_state--;
|
|
||||||
} else if (_state == 0) {
|
|
||||||
throw StateError('no lock to release');
|
|
||||||
} else {
|
|
||||||
assert(false, 'invalid state');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are jobs waiting and the next job can acquire the mutex,
|
|
||||||
// let it acquire it and remove it from the queue.
|
|
||||||
//
|
|
||||||
// This is a while loop, because there could be multiple jobs on the
|
|
||||||
// queue waiting for a read-only mutex. So they can all be allowed to run.
|
|
||||||
|
|
||||||
while (_waiting.isNotEmpty) {
|
|
||||||
final nextJob = _waiting.first;
|
|
||||||
if (_jobAcquired(nextJob)) {
|
|
||||||
_waiting.removeAt(0);
|
|
||||||
} else {
|
|
||||||
// The next job cannot acquire the mutex. This only occurs when: the
|
|
||||||
// the currently running job has a write mutex (_state == -1); or the
|
|
||||||
// next job wants write mutex and there is a job currently running
|
|
||||||
// (regardless of what type of mutex it has acquired).
|
|
||||||
assert(_state < 0 || !nextJob.isRead,
|
|
||||||
'unexpected: next job cannot be acquired');
|
|
||||||
break; // no more can be removed from the queue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method for protecting a function with a read lock.
|
|
||||||
///
|
|
||||||
/// This method guarantees a read lock is always acquired before invoking the
|
|
||||||
/// [criticalSection] function. It also guarantees the lock is always
|
|
||||||
/// released.
|
|
||||||
///
|
|
||||||
/// A critical section should always contain asynchronous code, since purely
|
|
||||||
/// synchronous code does not need to be protected inside a critical section.
|
|
||||||
/// Therefore, the critical section is a function that returns a _Future_.
|
|
||||||
/// If the critical section does not need to return a value, it should be
|
|
||||||
/// defined as returning `Future<void>`.
|
|
||||||
///
|
|
||||||
/// Returns a _Future_ whose value is the value of the _Future_ returned by
|
|
||||||
/// the critical section.
|
|
||||||
///
|
|
||||||
/// An exception is thrown if the critical section throws an exception,
|
|
||||||
/// or an exception is thrown while waiting for the _Future_ returned by
|
|
||||||
/// the critical section to complete. The lock is released, when those
|
|
||||||
/// exceptions occur.
|
|
||||||
|
|
||||||
Future<T> protectRead<T>(Future<T> Function() criticalSection) async {
|
|
||||||
await acquireRead();
|
|
||||||
try {
|
|
||||||
return await criticalSection();
|
|
||||||
} finally {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method for protecting a function with a write lock.
|
|
||||||
///
|
|
||||||
/// This method guarantees a write lock is always acquired before invoking the
|
|
||||||
/// [criticalSection] function. It also guarantees the lock is always
|
|
||||||
/// released.
|
|
||||||
///
|
|
||||||
/// A critical section should always contain asynchronous code, since purely
|
|
||||||
/// synchronous code does not need to be protected inside a critical section.
|
|
||||||
/// Therefore, the critical section is a function that returns a _Future_.
|
|
||||||
/// If the critical section does not need to return a value, it should be
|
|
||||||
/// defined as returning `Future<void>`.
|
|
||||||
///
|
|
||||||
/// Returns a _Future_ whose value is the value of the _Future_ returned by
|
|
||||||
/// the critical section.
|
|
||||||
///
|
|
||||||
/// An exception is thrown if the critical section throws an exception,
|
|
||||||
/// or an exception is thrown while waiting for the _Future_ returned by
|
|
||||||
/// the critical section to complete. The lock is released, when those
|
|
||||||
/// exceptions occur.
|
|
||||||
|
|
||||||
Future<T> protectWrite<T>(Future<T> Function() criticalSection) async {
|
|
||||||
await acquireWrite();
|
|
||||||
try {
|
|
||||||
return await criticalSection();
|
|
||||||
} finally {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal acquire method.
|
|
||||||
///
|
|
||||||
/// Used to acquire a read lock (when [isRead] is true) or a write lock
|
|
||||||
/// (when [isRead] is false).
|
|
||||||
///
|
|
||||||
/// Returns a Future that completes when the lock has been acquired.
|
|
||||||
|
|
||||||
Future<void> _acquire({required bool isRead}) {
|
|
||||||
final newJob = _ReadWriteMutexRequest(isRead: isRead);
|
|
||||||
|
|
||||||
if (_waiting.isNotEmpty || !_jobAcquired(newJob)) {
|
|
||||||
// This new job cannot run yet. There are either other jobs already
|
|
||||||
// waiting, or there are no waiting jobs but this job cannot start
|
|
||||||
// because the mutex is currently acquired (namely, either this new job
|
|
||||||
// or the currently running job is read-write).
|
|
||||||
//
|
|
||||||
// Add the new job to the end of the queue.
|
|
||||||
|
|
||||||
_waiting.add(newJob);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newJob.completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine if the [job] can now acquire the lock.
|
|
||||||
///
|
|
||||||
/// If it can acquire the lock, the job's completer is completed, the
|
|
||||||
/// state updated, and true is returned. If not, false is returned.
|
|
||||||
///
|
|
||||||
/// A job for a read lock can only be acquired if there are no other locks
|
|
||||||
/// or there are read lock(s). A job for a write lock can only be acquired
|
|
||||||
/// if there are no other locks.
|
|
||||||
|
|
||||||
bool _jobAcquired(_ReadWriteMutexRequest job) {
|
|
||||||
assert(-1 <= _state, 'must not be write locked');
|
|
||||||
if (_state == 0 || (0 < _state && job.isRead)) {
|
|
||||||
// Can acquire
|
|
||||||
_state = (job.isRead) ? (_state + 1) : -1;
|
|
||||||
job.completer.complete();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
name: mutex
|
|
||||||
description: Mutual exclusion with implementation of normal and read-write mutex
|
|
||||||
version: 3.1.0
|
|
||||||
publish_to: none
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
lint_hard: ^4.0.0
|
|
||||||
pana: ^0.22.2
|
|
||||||
test: ^1.25.2
|
|
@ -1,102 +0,0 @@
|
|||||||
// Test contributed by "Cat-sushi"
|
|
||||||
// <https://github.com/hoylen/dart-mutex/issues/11>
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
// import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
//================================================================
|
|
||||||
// For debug output
|
|
||||||
//
|
|
||||||
// Uncomment the "stdout.write" line in the [debugWrite] method to enable
|
|
||||||
// debug output.
|
|
||||||
|
|
||||||
int numReadAcquired = 0;
|
|
||||||
int numReadReleased = 0;
|
|
||||||
|
|
||||||
enum State { waitingToAcquire, acquired, released }
|
|
||||||
|
|
||||||
const stateSymbol = <State, String>{
|
|
||||||
State.waitingToAcquire: '?',
|
|
||||||
State.acquired: '+',
|
|
||||||
State.released: '-'
|
|
||||||
};
|
|
||||||
|
|
||||||
var _outputCount = 0; // to manage line breaks
|
|
||||||
|
|
||||||
void debugOutput(String id, State state) {
|
|
||||||
debugWrite('$id${stateSymbol[state]} ');
|
|
||||||
|
|
||||||
_outputCount++;
|
|
||||||
if (_outputCount % 10 == 0) {
|
|
||||||
debugWrite('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void debugWrite(String str) {
|
|
||||||
// Uncomment to show what is happening
|
|
||||||
// stdout.write(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
//================================================================
|
|
||||||
|
|
||||||
Future<void> mySleep([int ms = 1000]) async {
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> sharedLoop1(ReadWriteMutex mutex, String symbol) async {
|
|
||||||
while (true) {
|
|
||||||
debugOutput(symbol, State.waitingToAcquire);
|
|
||||||
|
|
||||||
await mutex.protectRead(() async {
|
|
||||||
numReadAcquired++;
|
|
||||||
debugOutput(symbol, State.acquired);
|
|
||||||
|
|
||||||
await mySleep(100);
|
|
||||||
});
|
|
||||||
numReadReleased++;
|
|
||||||
|
|
||||||
debugOutput(symbol, State.released);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('exclusive lock tests', () {
|
|
||||||
test('test1', () async {
|
|
||||||
const numReadLoops = 5;
|
|
||||||
|
|
||||||
final mutex = ReadWriteMutex();
|
|
||||||
|
|
||||||
assert(numReadLoops < 26, 'too many read loops for lowercase letters');
|
|
||||||
debugWrite('Number of read loops: $numReadLoops\n');
|
|
||||||
|
|
||||||
for (var x = 0; x < numReadLoops; x++) {
|
|
||||||
final symbol = String.fromCharCode('a'.codeUnitAt(0) + x);
|
|
||||||
unawaited(sharedLoop1(mutex, symbol));
|
|
||||||
await mySleep(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mySleep();
|
|
||||||
|
|
||||||
debugWrite('\nAbout to acquireWrite'
|
|
||||||
' (reads: acquired=$numReadAcquired released=$numReadReleased'
|
|
||||||
' outstanding=${numReadAcquired - numReadReleased})\n');
|
|
||||||
_outputCount = 0; // reset line break
|
|
||||||
|
|
||||||
const writeSymbol = 'W';
|
|
||||||
|
|
||||||
debugOutput(writeSymbol, State.waitingToAcquire);
|
|
||||||
await mutex.acquireWrite();
|
|
||||||
debugOutput(writeSymbol, State.acquired);
|
|
||||||
mutex.release();
|
|
||||||
debugOutput(writeSymbol, State.released);
|
|
||||||
|
|
||||||
debugWrite('\nWrite mutex released\n');
|
|
||||||
_outputCount = 0; // reset line break
|
|
||||||
|
|
||||||
expect('a', 'a');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,486 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
|
|
||||||
class RWTester {
|
|
||||||
int _operation = 0;
|
|
||||||
final _operationSequences = <int>[];
|
|
||||||
|
|
||||||
/// Execution sequence of the operations done.
|
|
||||||
///
|
|
||||||
/// Each element corresponds to the position of the initial execution
|
|
||||||
/// order of the read/write operation future.
|
|
||||||
List<int> get operationSequences => _operationSequences;
|
|
||||||
|
|
||||||
ReadWriteMutex mutex = ReadWriteMutex();
|
|
||||||
|
|
||||||
/// Set to true to print out read/write to the balance during deposits
|
|
||||||
static const bool debugOutput = false;
|
|
||||||
|
|
||||||
final DateTime _startTime = DateTime.now();
|
|
||||||
|
|
||||||
void _debugPrint(String message) {
|
|
||||||
if (debugOutput) {
|
|
||||||
final t = DateTime.now().difference(_startTime).inMilliseconds;
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('$t: $message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
_operationSequences.clear();
|
|
||||||
_debugPrint('reset');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits [startDelay] and then invokes critical section with mutex.
|
|
||||||
///
|
|
||||||
/// Writes to [_operationSequences]. If the readwrite locks are respected
|
|
||||||
/// then the final state of the list will be in ascending order.
|
|
||||||
Future<void> writing(int startDelay, int sequence, int endDelay) async {
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: startDelay));
|
|
||||||
|
|
||||||
await mutex.protectWrite(() async {
|
|
||||||
final op = ++_operation;
|
|
||||||
_debugPrint('[$op] write start: <- $_operationSequences');
|
|
||||||
final tmp = _operationSequences;
|
|
||||||
expect(mutex.isWriteLocked, isTrue);
|
|
||||||
expect(_operationSequences, orderedEquals(tmp));
|
|
||||||
// Add the position of operation to the list of operations.
|
|
||||||
_operationSequences.add(sequence); // add position to list
|
|
||||||
expect(mutex.isWriteLocked, isTrue);
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: endDelay));
|
|
||||||
_debugPrint('[$op] write finish: -> $_operationSequences');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits [startDelay] and then invokes critical section with mutex.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
Future<void> reading(int startDelay, int sequence, int endDelay) async {
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: startDelay));
|
|
||||||
|
|
||||||
await mutex.protectRead(() async {
|
|
||||||
final op = ++_operation;
|
|
||||||
_debugPrint('[$op] read start: <- $_operationSequences');
|
|
||||||
expect(mutex.isReadLocked, isTrue);
|
|
||||||
_operationSequences.add(sequence); // add position to list
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: endDelay));
|
|
||||||
_debugPrint('[$op] read finish: <- $_operationSequences');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final account = RWTester();
|
|
||||||
|
|
||||||
setUp(account.reset);
|
|
||||||
|
|
||||||
test('multiple read locks', () async {
|
|
||||||
await Future.wait([
|
|
||||||
account.reading(0, 1, 1000),
|
|
||||||
account.reading(0, 2, 900),
|
|
||||||
account.reading(0, 3, 800),
|
|
||||||
account.reading(0, 4, 700),
|
|
||||||
account.reading(0, 5, 600),
|
|
||||||
account.reading(0, 6, 500),
|
|
||||||
account.reading(0, 7, 400),
|
|
||||||
account.reading(0, 8, 300),
|
|
||||||
account.reading(0, 9, 200),
|
|
||||||
account.reading(0, 10, 100),
|
|
||||||
]);
|
|
||||||
// The first future acquires the lock first and waits the longest to give it
|
|
||||||
// up. This should however not block any of the other read operations
|
|
||||||
// as such the reads should finish in ascending orders.
|
|
||||||
expect(
|
|
||||||
account.operationSequences,
|
|
||||||
orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multiple write locks', () async {
|
|
||||||
await Future.wait([
|
|
||||||
account.writing(0, 1, 100),
|
|
||||||
account.writing(0, 2, 100),
|
|
||||||
account.writing(0, 3, 100),
|
|
||||||
]);
|
|
||||||
// The first future writes first and holds the lock until 100 ms
|
|
||||||
// Even though the second future starts execution, the lock cannot be
|
|
||||||
// acquired until it is released by the first future.
|
|
||||||
// Therefore the sequence of operations will be in ascending order
|
|
||||||
// of the futures.
|
|
||||||
expect(
|
|
||||||
account.operationSequences,
|
|
||||||
orderedEquals(<int>[1, 2, 3]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('acquireWrite() before acquireRead()', () async {
|
|
||||||
const lockTimeout = Duration(milliseconds: 100);
|
|
||||||
|
|
||||||
final mutex = ReadWriteMutex();
|
|
||||||
|
|
||||||
await mutex.acquireWrite();
|
|
||||||
expect(mutex.isReadLocked, equals(false));
|
|
||||||
expect(mutex.isWriteLocked, equals(true));
|
|
||||||
|
|
||||||
// Since there is a write lock existing, a read lock cannot be acquired.
|
|
||||||
final readLock = mutex.acquireRead().timeout(lockTimeout);
|
|
||||||
expect(
|
|
||||||
() async => readLock,
|
|
||||||
throwsA(isA<TimeoutException>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('acquireRead() before acquireWrite()', () async {
|
|
||||||
const lockTimeout = Duration(milliseconds: 100);
|
|
||||||
|
|
||||||
final mutex = ReadWriteMutex();
|
|
||||||
|
|
||||||
await mutex.acquireRead();
|
|
||||||
expect(mutex.isReadLocked, equals(true));
|
|
||||||
expect(mutex.isWriteLocked, equals(false));
|
|
||||||
|
|
||||||
// Since there is a read lock existing, a write lock cannot be acquired.
|
|
||||||
final writeLock = mutex.acquireWrite().timeout(lockTimeout);
|
|
||||||
expect(
|
|
||||||
() async => writeLock,
|
|
||||||
throwsA(isA<TimeoutException>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mixture of read write locks execution order', () async {
|
|
||||||
await Future.wait([
|
|
||||||
account.reading(0, 1, 100),
|
|
||||||
account.reading(10, 2, 100),
|
|
||||||
account.reading(20, 3, 100),
|
|
||||||
account.writing(30, 4, 100),
|
|
||||||
account.writing(40, 5, 100),
|
|
||||||
account.writing(50, 6, 100),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
account.operationSequences,
|
|
||||||
orderedEquals(<int>[1, 2, 3, 4, 5, 6]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('protectRead', () {
|
|
||||||
test('lock obtained and released on success', () async {
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
await m.protectRead(() async {
|
|
||||||
// critical section
|
|
||||||
expect(m.isLocked, isTrue);
|
|
||||||
});
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('value returned from critical section', () async {
|
|
||||||
// These are the normal scenario of the critical section running
|
|
||||||
// successfully. It tests different return types from the
|
|
||||||
// critical section.
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
// returns Future<void>
|
|
||||||
await m.protectRead<void>(() async {});
|
|
||||||
|
|
||||||
// returns Future<int>
|
|
||||||
final number = await m.protectRead<int>(() async => 42);
|
|
||||||
expect(number, equals(42));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with value
|
|
||||||
final optionalNumber = await m.protectRead<int?>(() async => 1024);
|
|
||||||
expect(optionalNumber, equals(1024));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with null
|
|
||||||
final optionalNumberNull = await m.protectRead<int?>(() async => null);
|
|
||||||
expect(optionalNumberNull, isNull);
|
|
||||||
|
|
||||||
// returns Future<String>
|
|
||||||
final word = await m.protectRead<String>(() async => 'foobar');
|
|
||||||
expect(word, equals('foobar'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with value
|
|
||||||
final optionalWord = await m.protectRead<String?>(() async => 'baz');
|
|
||||||
expect(optionalWord, equals('baz'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with null
|
|
||||||
final optionalWordNull = await m.protectRead<String?>(() async => null);
|
|
||||||
expect(optionalWordNull, isNull);
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in synchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **synchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete. Even though the exception is synchronously
|
|
||||||
// raised by the critical section, it won't be thrown when _protect_
|
|
||||||
// is invoked. The _protect_ method always successfully returns a
|
|
||||||
// _Future_.
|
|
||||||
|
|
||||||
Future<int> criticalSection() {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
// synchronous exception
|
|
||||||
throw const FormatException('synchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ignore: unused_local_variable
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
fail('critical section did not throw synchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: invoking the criticalSection results in the exception
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protectRead<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('synchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in asynchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **asynchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete.
|
|
||||||
|
|
||||||
Future<int> criticalSection() async {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1), () {});
|
|
||||||
|
|
||||||
// asynchronous exception (since it must wait for the above line)
|
|
||||||
throw const FormatException('asynchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
// invoking the criticalSection does not result in the exception
|
|
||||||
try {
|
|
||||||
await resultFuture;
|
|
||||||
fail('critical section did not throw asynchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: exception happens on the await
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protectRead<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
|
|
||||||
// Even though the criticalSection throws the exception in synchronous
|
|
||||||
// code, protect causes it to become an asynchronous exception.
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('asynchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('protectWrite', () {
|
|
||||||
test('lock obtained and released on success', () async {
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
await m.protectWrite(() async {
|
|
||||||
// critical section
|
|
||||||
expect(m.isLocked, isTrue);
|
|
||||||
});
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('value returned from critical section', () async {
|
|
||||||
// These are the normal scenario of the critical section running
|
|
||||||
// successfully. It tests different return types from the
|
|
||||||
// critical section.
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
// returns Future<void>
|
|
||||||
await m.protectWrite<void>(() async {});
|
|
||||||
|
|
||||||
// returns Future<int>
|
|
||||||
final number = await m.protectWrite<int>(() async => 42);
|
|
||||||
expect(number, equals(42));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with value
|
|
||||||
final optionalNumber = await m.protectWrite<int?>(() async => 1024);
|
|
||||||
expect(optionalNumber, equals(1024));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with null
|
|
||||||
final optionalNumberNull = await m.protectWrite<int?>(() async => null);
|
|
||||||
expect(optionalNumberNull, isNull);
|
|
||||||
|
|
||||||
// returns Future<String>
|
|
||||||
final word = await m.protectWrite<String>(() async => 'foobar');
|
|
||||||
expect(word, equals('foobar'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with value
|
|
||||||
final optionalWord = await m.protectWrite<String?>(() async => 'baz');
|
|
||||||
expect(optionalWord, equals('baz'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with null
|
|
||||||
final optionalWordNull = await m.protectWrite<String?>(() async => null);
|
|
||||||
expect(optionalWordNull, isNull);
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in synchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **synchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete. Even though the exception is synchronously
|
|
||||||
// raised by the critical section, it won't be thrown when _protect_
|
|
||||||
// is invoked. The _protect_ method always successfully returns a
|
|
||||||
// _Future_.
|
|
||||||
|
|
||||||
Future<int> criticalSection() {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
// synchronous exception
|
|
||||||
throw const FormatException('synchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ignore: unused_local_variable
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
fail('critical section did not throw synchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: invoking the criticalSection results in the exception
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protectWrite<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('synchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in asynchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **asynchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete.
|
|
||||||
|
|
||||||
Future<int> criticalSection() async {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1), () {});
|
|
||||||
|
|
||||||
// asynchronous exception (since it must wait for the above line)
|
|
||||||
throw const FormatException('asynchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
// invoking the criticalSection does not result in the exception
|
|
||||||
try {
|
|
||||||
await resultFuture;
|
|
||||||
fail('critical section did not throw asynchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: exception happens on the await
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = ReadWriteMutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protectWrite<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
|
|
||||||
// Even though the criticalSection throws the exception in synchronous
|
|
||||||
// code, protect causes it to become an asynchronous exception.
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('asynchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,341 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
/// Account simulating the classic "simultaneous update" concurrency problem.
|
|
||||||
///
|
|
||||||
/// The deposit operation reads the balance, waits for a short time (where
|
|
||||||
/// problems can occur if the balance is changed) and then writes out the
|
|
||||||
/// new balance.
|
|
||||||
///
|
|
||||||
class Account {
|
|
||||||
int get balance => _balance;
|
|
||||||
int _balance = 0;
|
|
||||||
|
|
||||||
int _operation = 0;
|
|
||||||
|
|
||||||
Mutex mutex = Mutex();
|
|
||||||
|
|
||||||
/// Set to true to print out read/write to the balance during deposits
|
|
||||||
static const bool debugOutput = false;
|
|
||||||
|
|
||||||
/// Time used for calculating time offsets in debug messages.
|
|
||||||
final DateTime _startTime = DateTime.now();
|
|
||||||
|
|
||||||
void _debugPrint(String message) {
|
|
||||||
if (debugOutput) {
|
|
||||||
final t = DateTime.now().difference(_startTime).inMilliseconds;
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('$t: $message');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset([int startingBalance = 0]) {
|
|
||||||
_balance = startingBalance;
|
|
||||||
_debugPrint('reset: balance = $_balance');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits [startDelay] and then invokes critical section without mutex.
|
|
||||||
///
|
|
||||||
Future<void> depositUnsafe(
|
|
||||||
int amount, int startDelay, int dangerWindow) async {
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: startDelay));
|
|
||||||
|
|
||||||
await _depositCriticalSection(amount, dangerWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits [startDelay] and then invokes critical section with mutex.
|
|
||||||
///
|
|
||||||
Future<void> depositWithMutex(
|
|
||||||
int amount, int startDelay, int dangerWindow) async {
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: startDelay));
|
|
||||||
|
|
||||||
await mutex.acquire();
|
|
||||||
try {
|
|
||||||
expect(mutex.isLocked, isTrue);
|
|
||||||
await _depositCriticalSection(amount, dangerWindow);
|
|
||||||
expect(mutex.isLocked, isTrue);
|
|
||||||
} finally {
|
|
||||||
mutex.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Critical section of adding [amount] to the balance.
|
|
||||||
///
|
|
||||||
/// Reads the balance, then sleeps for [dangerWindow] milliseconds, before
|
|
||||||
/// saving the new balance. If not protected, another invocation of this
|
|
||||||
/// method while it is sleeping will read the balance before it is updated.
|
|
||||||
/// The one that saves its balance last will overwrite the earlier saved
|
|
||||||
/// balances (effectively those other deposits will be lost).
|
|
||||||
///
|
|
||||||
Future<void> _depositCriticalSection(int amount, int dangerWindow) async {
|
|
||||||
final op = ++_operation;
|
|
||||||
|
|
||||||
_debugPrint('[$op] read balance: $_balance');
|
|
||||||
|
|
||||||
final tmp = _balance;
|
|
||||||
|
|
||||||
await Future<void>.delayed(Duration(milliseconds: dangerWindow));
|
|
||||||
|
|
||||||
_balance = tmp + amount;
|
|
||||||
|
|
||||||
_debugPrint('[$op] write balance: $_balance (= $tmp + $amount)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//################################################################
|
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
const correctBalance = 68;
|
|
||||||
|
|
||||||
final account = Account();
|
|
||||||
|
|
||||||
test('without mutex', () async {
|
|
||||||
// First demonstrate that without mutex incorrect results are produced.
|
|
||||||
|
|
||||||
// Without mutex produces incorrect result
|
|
||||||
// 000. a reads 0
|
|
||||||
// 025. b reads 0
|
|
||||||
// 050. a writes 42
|
|
||||||
// 075. b writes 26
|
|
||||||
account.reset();
|
|
||||||
await Future.wait<void>([
|
|
||||||
account.depositUnsafe(42, 0, 50),
|
|
||||||
account.depositUnsafe(26, 25, 50) // result overwrites first deposit
|
|
||||||
]);
|
|
||||||
expect(account.balance, equals(26)); // incorrect: first deposit lost
|
|
||||||
|
|
||||||
// Without mutex produces incorrect result
|
|
||||||
// 000. b reads 0
|
|
||||||
// 025. a reads 0
|
|
||||||
// 050. b writes 26
|
|
||||||
// 075. a writes 42
|
|
||||||
account.reset();
|
|
||||||
await Future.wait([
|
|
||||||
account.depositUnsafe(42, 25, 50), // result overwrites second deposit
|
|
||||||
account.depositUnsafe(26, 0, 50)
|
|
||||||
]);
|
|
||||||
expect(account.balance, equals(42)); // incorrect: second deposit lost
|
|
||||||
});
|
|
||||||
|
|
||||||
test('with mutex', () async {
|
|
||||||
// Test correct results are produced with mutex
|
|
||||||
|
|
||||||
// With mutex produces correct result
|
|
||||||
// 000. a acquires lock
|
|
||||||
// 000. a reads 0
|
|
||||||
// 025. b is blocked
|
|
||||||
// 050. a writes 42
|
|
||||||
// 050. a releases lock
|
|
||||||
// 050. b acquires lock
|
|
||||||
// 050. b reads 42
|
|
||||||
// 100. b writes 68
|
|
||||||
account.reset();
|
|
||||||
await Future.wait([
|
|
||||||
account.depositWithMutex(42, 0, 50),
|
|
||||||
account.depositWithMutex(26, 25, 50)
|
|
||||||
]);
|
|
||||||
expect(account.balance, equals(correctBalance));
|
|
||||||
|
|
||||||
// With mutex produces correct result
|
|
||||||
// 000. b acquires lock
|
|
||||||
// 000. b reads 0
|
|
||||||
// 025. a is blocked
|
|
||||||
// 050. b writes 26
|
|
||||||
// 050. b releases lock
|
|
||||||
// 050. a acquires lock
|
|
||||||
// 050. a reads 26
|
|
||||||
// 100. a writes 68
|
|
||||||
account.reset();
|
|
||||||
await Future.wait([
|
|
||||||
account.depositWithMutex(42, 25, 50),
|
|
||||||
account.depositWithMutex(26, 0, 50)
|
|
||||||
]);
|
|
||||||
expect(account.balance, equals(correctBalance));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multiple acquires are serialized', () async {
|
|
||||||
// Demonstrate that sections running in a mutex are effectively serialized
|
|
||||||
const delay = 200; // milliseconds
|
|
||||||
account.reset();
|
|
||||||
await Future.wait([
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
account.depositWithMutex(1, 0, delay),
|
|
||||||
]);
|
|
||||||
expect(account.balance, equals(10));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('protect', () {
|
|
||||||
test('lock obtained and released on success', () async {
|
|
||||||
// This is the normal scenario of the critical section running
|
|
||||||
// successfully. The lock is acquired before running the critical
|
|
||||||
// section, and it is released after it runs (and will remain
|
|
||||||
// unlocked after the _protect_ method returns).
|
|
||||||
|
|
||||||
final m = Mutex();
|
|
||||||
|
|
||||||
await m.protect(() async {
|
|
||||||
// critical section: returns Future<void>
|
|
||||||
expect(m.isLocked, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('value returned from critical section', () async {
|
|
||||||
// These are the normal scenario of the critical section running
|
|
||||||
// successfully. It tests different return types from the
|
|
||||||
// critical section.
|
|
||||||
|
|
||||||
final m = Mutex();
|
|
||||||
|
|
||||||
// returns Future<void>
|
|
||||||
await m.protect<void>(() async {});
|
|
||||||
|
|
||||||
// returns Future<int>
|
|
||||||
final number = await m.protect<int>(() async => 42);
|
|
||||||
expect(number, equals(42));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with value
|
|
||||||
final optionalNumber = await m.protect<int?>(() async => 1024);
|
|
||||||
expect(optionalNumber, equals(1024));
|
|
||||||
|
|
||||||
// returns Future<int?> completes with null
|
|
||||||
final optionalNumberNull = await m.protect<int?>(() async => null);
|
|
||||||
expect(optionalNumberNull, isNull);
|
|
||||||
|
|
||||||
// returns Future<String>
|
|
||||||
final word = await m.protect<String>(() async => 'foobar');
|
|
||||||
expect(word, equals('foobar'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with value
|
|
||||||
final optionalWord = await m.protect<String?>(() async => 'baz');
|
|
||||||
expect(optionalWord, equals('baz'));
|
|
||||||
|
|
||||||
// returns Future<String?> completes with null
|
|
||||||
final optionalWordNull = await m.protect<String?>(() async => null);
|
|
||||||
expect(optionalWordNull, isNull);
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in synchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **synchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete. Even though the exception is synchronously
|
|
||||||
// raised by the critical section, it won't be thrown when _protect_
|
|
||||||
// is invoked. The _protect_ method always successfully returns a
|
|
||||||
// _Future_.
|
|
||||||
|
|
||||||
Future<int> criticalSection() {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
// synchronous exception
|
|
||||||
throw const FormatException('synchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ignore: unused_local_variable
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
fail('critical section did not throw synchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: invoking the criticalSection results in the exception
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = Mutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protect<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<void>>());
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('synchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exception in asynchronous code', () async {
|
|
||||||
// Tests what happens when an exception is raised in the **asynchronous**
|
|
||||||
// part of the critical section.
|
|
||||||
//
|
|
||||||
// Locks are correctly managed: the lock is obtained before executing
|
|
||||||
// the critical section, and is released when the exception is thrown
|
|
||||||
// by the _protect_ method.
|
|
||||||
//
|
|
||||||
// The exception is raised when waiting for the Future returned by
|
|
||||||
// _protect_ to complete.
|
|
||||||
|
|
||||||
Future<int> criticalSection() async {
|
|
||||||
final c = Completer<int>()..complete(42);
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1), () {});
|
|
||||||
|
|
||||||
// asynchronous exception (since it must wait for the above line)
|
|
||||||
throw const FormatException('asynchronous exception');
|
|
||||||
// ignore: dead_code
|
|
||||||
return c.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the criticalSection behaves as expected for the test
|
|
||||||
|
|
||||||
final resultFuture = criticalSection();
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
// invoking the criticalSection does not result in the exception
|
|
||||||
try {
|
|
||||||
await resultFuture;
|
|
||||||
fail('critical section did not throw asynchronous exception');
|
|
||||||
} on FormatException {
|
|
||||||
// expected: exception happens on the await
|
|
||||||
}
|
|
||||||
|
|
||||||
final m = Mutex();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Invoke protect to get the Future (this should succeed)
|
|
||||||
final resultFuture = m.protect<int>(criticalSection);
|
|
||||||
expect(resultFuture, isA<Future<int>>());
|
|
||||||
|
|
||||||
// Even though the criticalSection throws the exception in synchronous
|
|
||||||
// code, protect causes it to become an asynchronous exception.
|
|
||||||
|
|
||||||
// Wait for the Future (this should fail)
|
|
||||||
final result = await resultFuture;
|
|
||||||
expect(result, isNotNull);
|
|
||||||
fail('exception not thrown');
|
|
||||||
} on FormatException catch (e) {
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
expect(e.message, equals('asynchronous exception'));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(m.isLocked, isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,28 +1,15 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
include: package:lint_hard/all.yaml
|
||||||
# check for errors, warnings, and lints.
|
analyzer:
|
||||||
#
|
errors:
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
invalid_annotation_target: ignore
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
exclude:
|
||||||
# invoked from the command line by running `flutter analyze`.
|
- '**/*.g.dart'
|
||||||
|
- '**/*.freezed.dart'
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
- '**/*.pb.dart'
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
- '**/*.pbenum.dart'
|
||||||
include: package:flutter_lints/flutter.yaml
|
- '**/*.pbjson.dart'
|
||||||
|
- '**/*.pbserver.dart'
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at https://dart.dev/lints.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
unawaited_futures: true
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
avoid_positional_boolean_parameters: false
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
@Timeout(Duration(seconds: 60))
|
@Timeout(Duration(seconds: 120))
|
||||||
|
|
||||||
|
library veilid_support_integration_test;
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:veilid_test/veilid_test.dart';
|
||||||
|
|
||||||
import 'fixtures.dart';
|
import 'fixtures/fixtures.dart';
|
||||||
import 'test_dht_record_pool.dart';
|
import 'test_dht_record_pool.dart';
|
||||||
import 'test_dht_short_array.dart';
|
import 'test_dht_short_array.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final fixture = DefaultFixture();
|
final veilidFixture =
|
||||||
|
DefaultVeilidFixture(programName: 'veilid_support integration test');
|
||||||
|
final updateProcessorFixture =
|
||||||
|
UpdateProcessorFixture(veilidFixture: veilidFixture);
|
||||||
|
final tickerFixture =
|
||||||
|
TickerFixture(updateProcessorFixture: updateProcessorFixture);
|
||||||
|
final dhtRecordPoolFixture = DHTRecordPoolFixture(
|
||||||
|
tickerFixture: tickerFixture,
|
||||||
|
updateProcessorFixture: updateProcessorFixture);
|
||||||
|
|
||||||
group('Started Tests', () {
|
group('Started Tests', () {
|
||||||
setUpAll(fixture.setUp);
|
setUpAll(veilidFixture.setUp);
|
||||||
tearDownAll(fixture.tearDown);
|
tearDownAll(veilidFixture.tearDown);
|
||||||
|
|
||||||
// group('Crypto Tests', () {
|
// group('Crypto Tests', () {
|
||||||
// test('best cryptosystem', testBestCryptoSystem);
|
// test('best cryptosystem', testBestCryptoSystem);
|
||||||
@ -23,15 +34,32 @@ void main() {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
group('Attached Tests', () {
|
group('Attached Tests', () {
|
||||||
setUpAll(fixture.attach);
|
setUpAll(veilidFixture.attach);
|
||||||
tearDownAll(fixture.detach);
|
tearDownAll(veilidFixture.detach);
|
||||||
|
|
||||||
group('DHT Support Tests', () {
|
group('DHT Support Tests', () {
|
||||||
group('DHTRecordPool Tests', () {
|
setUpAll(updateProcessorFixture.setUp);
|
||||||
test('create pool', testDHTRecordPoolCreate);
|
setUpAll(tickerFixture.setUp);
|
||||||
});
|
tearDownAll(tickerFixture.tearDown);
|
||||||
|
tearDownAll(updateProcessorFixture.tearDown);
|
||||||
|
|
||||||
|
test('create pool', testDHTRecordPoolCreate);
|
||||||
|
|
||||||
|
// group('DHTRecordPool Tests', () {
|
||||||
|
// setUpAll(dhtRecordPoolFixture.setUp);
|
||||||
|
// tearDownAll(dhtRecordPoolFixture.tearDown);
|
||||||
|
|
||||||
|
// test('create/delete record', testDHTRecordCreateDelete);
|
||||||
|
// test('record scopes', testDHTRecordScopes);
|
||||||
|
// test('create/delete deep record', testDHTRecordDeepCreateDelete);
|
||||||
|
// });
|
||||||
|
|
||||||
group('DHTShortArray Tests', () {
|
group('DHTShortArray Tests', () {
|
||||||
test('create shortarray', testDHTShortArrayCreate);
|
setUpAll(dhtRecordPoolFixture.setUp);
|
||||||
|
tearDownAll(dhtRecordPoolFixture.tearDown);
|
||||||
|
|
||||||
|
// test('create shortarray', testDHTShortArrayCreateDelete);
|
||||||
|
test('add shortarray', testDHTShortArrayAdd);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:veilid/veilid.dart';
|
|
||||||
|
|
||||||
class DefaultFixture {
|
|
||||||
DefaultFixture();
|
|
||||||
|
|
||||||
StreamSubscription<VeilidUpdate>? _veilidUpdateSubscription;
|
|
||||||
Stream<VeilidUpdate>? _veilidUpdateStream;
|
|
||||||
final StreamController<VeilidUpdate> _updateStreamController =
|
|
||||||
StreamController.broadcast();
|
|
||||||
|
|
||||||
static final _fixtureMutex = Mutex();
|
|
||||||
|
|
||||||
Future<void> setUp() async {
|
|
||||||
await _fixtureMutex.acquire();
|
|
||||||
|
|
||||||
assert(_veilidUpdateStream == null, 'should not set up fixture twice');
|
|
||||||
|
|
||||||
final ignoreLogTargetsStr =
|
|
||||||
// ignore: do_not_use_environment
|
|
||||||
const String.fromEnvironment('IGNORE_LOG_TARGETS').trim();
|
|
||||||
final ignoreLogTargets = ignoreLogTargetsStr.isEmpty
|
|
||||||
? <String>[]
|
|
||||||
: ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList();
|
|
||||||
|
|
||||||
final Map<String, dynamic> platformConfigJson;
|
|
||||||
if (kIsWeb) {
|
|
||||||
final platformConfig = VeilidWASMConfig(
|
|
||||||
logging: VeilidWASMConfigLogging(
|
|
||||||
performance: VeilidWASMConfigLoggingPerformance(
|
|
||||||
enabled: true,
|
|
||||||
level: VeilidConfigLogLevel.debug,
|
|
||||||
logsInTimings: true,
|
|
||||||
logsInConsole: false,
|
|
||||||
ignoreLogTargets: ignoreLogTargets,
|
|
||||||
),
|
|
||||||
api: VeilidWASMConfigLoggingApi(
|
|
||||||
enabled: true,
|
|
||||||
level: VeilidConfigLogLevel.info,
|
|
||||||
ignoreLogTargets: ignoreLogTargets,
|
|
||||||
)));
|
|
||||||
platformConfigJson = platformConfig.toJson();
|
|
||||||
} else {
|
|
||||||
final platformConfig = VeilidFFIConfig(
|
|
||||||
logging: VeilidFFIConfigLogging(
|
|
||||||
terminal: VeilidFFIConfigLoggingTerminal(
|
|
||||||
enabled: false,
|
|
||||||
level: VeilidConfigLogLevel.debug,
|
|
||||||
ignoreLogTargets: ignoreLogTargets,
|
|
||||||
),
|
|
||||||
otlp: VeilidFFIConfigLoggingOtlp(
|
|
||||||
enabled: false,
|
|
||||||
level: VeilidConfigLogLevel.trace,
|
|
||||||
grpcEndpoint: 'localhost:4317',
|
|
||||||
serviceName: 'Veilid Tests',
|
|
||||||
ignoreLogTargets: ignoreLogTargets,
|
|
||||||
),
|
|
||||||
api: VeilidFFIConfigLoggingApi(
|
|
||||||
enabled: true,
|
|
||||||
// level: VeilidConfigLogLevel.debug,
|
|
||||||
level: VeilidConfigLogLevel.info,
|
|
||||||
ignoreLogTargets: ignoreLogTargets,
|
|
||||||
)));
|
|
||||||
platformConfigJson = platformConfig.toJson();
|
|
||||||
}
|
|
||||||
Veilid.instance.initializeVeilidCore(platformConfigJson);
|
|
||||||
|
|
||||||
var config = await getDefaultVeilidConfig(
|
|
||||||
isWeb: kIsWeb,
|
|
||||||
programName: 'Veilid Tests',
|
|
||||||
// ignore: avoid_redundant_argument_values, do_not_use_environment
|
|
||||||
bootstrap: const String.fromEnvironment('BOOTSTRAP'),
|
|
||||||
// ignore: avoid_redundant_argument_values, do_not_use_environment
|
|
||||||
networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'),
|
|
||||||
);
|
|
||||||
|
|
||||||
config =
|
|
||||||
config.copyWith(tableStore: config.tableStore.copyWith(delete: true));
|
|
||||||
config = config.copyWith(
|
|
||||||
protectedStore: config.protectedStore.copyWith(delete: true));
|
|
||||||
config =
|
|
||||||
config.copyWith(blockStore: config.blockStore.copyWith(delete: true));
|
|
||||||
|
|
||||||
final us =
|
|
||||||
_veilidUpdateStream = await Veilid.instance.startupVeilidCore(config);
|
|
||||||
|
|
||||||
_veilidUpdateSubscription = us.listen((update) {
|
|
||||||
if (update is VeilidLog) {
|
|
||||||
// print(update.message);
|
|
||||||
} else if (update is VeilidUpdateAttachment) {
|
|
||||||
} else if (update is VeilidUpdateConfig) {
|
|
||||||
} else if (update is VeilidUpdateNetwork) {
|
|
||||||
} else if (update is VeilidAppMessage) {
|
|
||||||
} else if (update is VeilidAppCall) {
|
|
||||||
} else if (update is VeilidUpdateValueChange) {
|
|
||||||
} else if (update is VeilidUpdateRouteChange) {
|
|
||||||
} else {
|
|
||||||
throw Exception('unexpected update: $update');
|
|
||||||
}
|
|
||||||
_updateStreamController.sink.add(update);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<VeilidUpdate> get updateStream => _updateStreamController.stream;
|
|
||||||
|
|
||||||
Future<void> attach() async {
|
|
||||||
await Veilid.instance.attach();
|
|
||||||
|
|
||||||
// Wait for attached state
|
|
||||||
while (true) {
|
|
||||||
final state = await Veilid.instance.getVeilidState();
|
|
||||||
var done = false;
|
|
||||||
if (state.attachment.publicInternetReady) {
|
|
||||||
switch (state.attachment.state) {
|
|
||||||
case AttachmentState.detached:
|
|
||||||
break;
|
|
||||||
case AttachmentState.attaching:
|
|
||||||
break;
|
|
||||||
case AttachmentState.detaching:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
done = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> detach() async {
|
|
||||||
await Veilid.instance.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tearDown() async {
|
|
||||||
assert(_veilidUpdateStream != null, 'should not tearDown without setUp');
|
|
||||||
|
|
||||||
final cancelFut = _veilidUpdateSubscription?.cancel();
|
|
||||||
await Veilid.instance.shutdownVeilidCore();
|
|
||||||
await cancelFut;
|
|
||||||
|
|
||||||
_veilidUpdateSubscription = null;
|
|
||||||
_veilidUpdateStream = null;
|
|
||||||
|
|
||||||
_fixtureMutex.release();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
import 'package:veilid_test/veilid_test.dart';
|
||||||
|
|
||||||
|
class DHTRecordPoolFixture implements TickerFixtureTickable {
|
||||||
|
DHTRecordPoolFixture(
|
||||||
|
{required this.tickerFixture, required this.updateProcessorFixture});
|
||||||
|
|
||||||
|
static final _fixtureMutex = Mutex();
|
||||||
|
UpdateProcessorFixture updateProcessorFixture;
|
||||||
|
TickerFixture tickerFixture;
|
||||||
|
|
||||||
|
Future<void> setUp() async {
|
||||||
|
await _fixtureMutex.acquire();
|
||||||
|
await DHTRecordPool.init();
|
||||||
|
tickerFixture.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tearDown() async {
|
||||||
|
assert(_fixtureMutex.isLocked, 'should not tearDown without setUp');
|
||||||
|
tickerFixture.unregister(this);
|
||||||
|
await DHTRecordPool.close();
|
||||||
|
_fixtureMutex.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onTick() async {
|
||||||
|
if (!updateProcessorFixture
|
||||||
|
.processorConnectionState.isPublicInternetReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await DHTRecordPool.instance.tick();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export 'dht_record_pool_fixture.dart';
|
@ -1,9 +1,201 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
Future<void> testDHTRecordPoolCreate() async {
|
Future<void> testDHTRecordPoolCreate() async {
|
||||||
// final cs = await Veilid.instance.bestCryptoSystem();
|
await DHTRecordPool.init(logger: debugPrintSynchronously);
|
||||||
// expect(await cs.defaultSaltLength(), equals(16));
|
final pool = DHTRecordPool.instance;
|
||||||
|
await pool.tick();
|
||||||
|
await DHTRecordPool.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testDHTRecordCreateDelete() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
|
||||||
|
// Close before delete
|
||||||
|
{
|
||||||
|
final rec = await pool.createRecord(debugName: 'test_create_delete 1');
|
||||||
|
expect(rec.isOpen, isTrue);
|
||||||
|
await rec.close();
|
||||||
|
expect(rec.isOpen, isFalse);
|
||||||
|
await pool.deleteRecord(rec.key);
|
||||||
|
// Set should fail
|
||||||
|
await expectLater(() async => rec.tryWriteBytes(utf8.encode('test')),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close after delete
|
||||||
|
{
|
||||||
|
final rec2 = await pool.createRecord(debugName: 'test_create_delete 2');
|
||||||
|
expect(rec2.isOpen, isTrue);
|
||||||
|
await pool.deleteRecord(rec2.key);
|
||||||
|
expect(rec2.isOpen, isTrue);
|
||||||
|
await rec2.close();
|
||||||
|
expect(rec2.isOpen, isFalse);
|
||||||
|
// Set should fail
|
||||||
|
await expectLater(() async => rec2.tryWriteBytes(utf8.encode('test')),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close after delete multiple
|
||||||
|
// Okay to request delete multiple times before close
|
||||||
|
{
|
||||||
|
final rec3 = await pool.createRecord(debugName: 'test_create_delete 3');
|
||||||
|
await pool.deleteRecord(rec3.key);
|
||||||
|
await pool.deleteRecord(rec3.key);
|
||||||
|
// Set should succeed still
|
||||||
|
await rec3.tryWriteBytes(utf8.encode('test'));
|
||||||
|
await rec3.close();
|
||||||
|
await rec3.close();
|
||||||
|
// Set should fail
|
||||||
|
await expectLater(() async => rec3.tryWriteBytes(utf8.encode('test')),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
// Delete already delete should fail
|
||||||
|
await expectLater(() async => pool.deleteRecord(rec3.key),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testDHTRecordScopes() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
|
||||||
|
// Delete scope with exception should propagate exception
|
||||||
|
{
|
||||||
|
final rec = await pool.createRecord(debugName: 'test_scope 1');
|
||||||
|
await expectLater(
|
||||||
|
() async => rec.deleteScope((recd) async {
|
||||||
|
throw Exception();
|
||||||
|
}),
|
||||||
|
throwsA(isA<Exception>()));
|
||||||
|
// Set should fail
|
||||||
|
await expectLater(() async => rec.tryWriteBytes(utf8.encode('test')),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete scope without exception
|
||||||
|
{
|
||||||
|
final rec2 = await pool.createRecord(debugName: 'test_scope 2');
|
||||||
|
try {
|
||||||
|
await rec2.deleteScope((rec2d) async {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
} on Exception {
|
||||||
|
assert(false, 'should not throw');
|
||||||
|
}
|
||||||
|
await rec2.close();
|
||||||
|
await pool.deleteRecord(rec2.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close scope without exception
|
||||||
|
{
|
||||||
|
final rec3 = await pool.createRecord(debugName: 'test_scope 3');
|
||||||
|
try {
|
||||||
|
await rec3.scope((rec3d) async {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
} on Exception {
|
||||||
|
assert(false, 'should not throw');
|
||||||
|
}
|
||||||
|
// Set should fail because scope closed it
|
||||||
|
await expectLater(() async => rec3.tryWriteBytes(utf8.encode('test')),
|
||||||
|
throwsA(isA<VeilidAPIException>()));
|
||||||
|
await pool.deleteRecord(rec3.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testDHTRecordGetSet() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
final valdata = utf8.encode('test');
|
||||||
|
|
||||||
|
// Test get without set
|
||||||
|
{
|
||||||
|
final rec = await pool.createRecord(debugName: 'test_get_set 1');
|
||||||
|
final val = await rec.get();
|
||||||
|
await pool.deleteRecord(rec.key);
|
||||||
|
expect(val, isNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test set then get
|
||||||
|
{
|
||||||
|
final rec2 = await pool.createRecord(debugName: 'test_get_set 2');
|
||||||
|
expect(await rec2.tryWriteBytes(valdata), isNull);
|
||||||
|
expect(await rec2.get(), equals(valdata));
|
||||||
|
// Invalid subkey should throw
|
||||||
|
await expectLater(
|
||||||
|
() async => rec2.get(subkey: 1), throwsA(isA<VeilidAPIException>()));
|
||||||
|
await pool.deleteRecord(rec2.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test set then delete then open then get
|
||||||
|
{
|
||||||
|
final rec3 = await pool.createRecord(debugName: 'test_get_set 3');
|
||||||
|
expect(await rec3.tryWriteBytes(valdata), isNull);
|
||||||
|
expect(await rec3.get(), equals(valdata));
|
||||||
|
await rec3.close();
|
||||||
|
await pool.deleteRecord(rec3.key);
|
||||||
|
final rec4 =
|
||||||
|
await pool.openRecordRead(rec3.key, debugName: 'test_get_set 4');
|
||||||
|
expect(await rec4.get(), equals(valdata));
|
||||||
|
await rec4.close();
|
||||||
|
await pool.deleteRecord(rec4.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testDHTRecordDeepCreateDelete() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
const numChildren = 20;
|
||||||
|
const numIterations = 10;
|
||||||
|
|
||||||
|
// Make root record
|
||||||
|
final recroot = await pool.createRecord(debugName: 'test_deep_create_delete');
|
||||||
|
|
||||||
|
for (var d = 0; d < numIterations; d++) {
|
||||||
|
// Make child set 1
|
||||||
|
var parent = recroot;
|
||||||
|
final children = <DHTRecord>[];
|
||||||
|
for (var n = 0; n < numChildren; n++) {
|
||||||
|
final child =
|
||||||
|
await pool.createRecord(debugName: 'deep $n', parent: parent.key);
|
||||||
|
children.add(child);
|
||||||
|
parent = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make child set 2
|
||||||
|
final children2 = <DHTRecord>[];
|
||||||
|
parent = recroot;
|
||||||
|
for (var n = 0; n < numChildren; n++) {
|
||||||
|
final child =
|
||||||
|
await pool.createRecord(debugName: 'deep2 $n ', parent: parent.key);
|
||||||
|
children2.add(child);
|
||||||
|
parent = child;
|
||||||
|
}
|
||||||
|
// Should fail to delete root
|
||||||
|
await expectLater(
|
||||||
|
() async => pool.deleteRecord(recroot.key), throwsA(isA<StateError>()));
|
||||||
|
|
||||||
|
// Close child set 1
|
||||||
|
await children.map((c) => c.close()).wait;
|
||||||
|
|
||||||
|
// Delete child set 1 in reverse order
|
||||||
|
for (var n = numChildren - 1; n >= 0; n--) {
|
||||||
|
await pool.deleteRecord(children[n].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should fail to delete root
|
||||||
|
await expectLater(
|
||||||
|
() async => pool.deleteRecord(recroot.key), throwsA(isA<StateError>()));
|
||||||
|
|
||||||
|
// Close child set 1
|
||||||
|
await children2.map((c) => c.close()).wait;
|
||||||
|
|
||||||
|
// Delete child set 2 in reverse order
|
||||||
|
for (var n = numChildren - 1; n >= 0; n--) {
|
||||||
|
await pool.deleteRecord(children2[n].key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be able to delete root now
|
||||||
|
await pool.deleteRecord(recroot.key);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,86 @@ import 'dart:convert';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
Future<void> testDHTShortArrayCreate() async {
|
Future<void> testDHTShortArrayCreateDelete() async {
|
||||||
// final cs = await Veilid.instance.bestCryptoSystem();
|
// Close before delete
|
||||||
// expect(await cs.defaultSaltLength(), equals(16));
|
{
|
||||||
|
final arr = await DHTShortArray.create(debugName: 'sa_create_delete 1');
|
||||||
|
expect(await arr.operate((r) async => r.length), isZero);
|
||||||
|
expect(arr.isOpen, isTrue);
|
||||||
|
await arr.close();
|
||||||
|
expect(arr.isOpen, isFalse);
|
||||||
|
await arr.delete();
|
||||||
|
// Operate should fail
|
||||||
|
await expectLater(() async => arr.operate((r) async => r.length),
|
||||||
|
throwsA(isA<StateError>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close after delete
|
||||||
|
{
|
||||||
|
final arr = await DHTShortArray.create(debugName: 'sa_create_delete 2');
|
||||||
|
await arr.delete();
|
||||||
|
// Operate should still succeed because things aren't closed
|
||||||
|
expect(await arr.operate((r) async => r.length), isZero);
|
||||||
|
await arr.close();
|
||||||
|
// Operate should fail
|
||||||
|
await expectLater(() async => arr.operate((r) async => r.length),
|
||||||
|
throwsA(isA<StateError>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close after delete multiple
|
||||||
|
// Okay to request delete multiple times before close
|
||||||
|
{
|
||||||
|
final arr = await DHTShortArray.create(debugName: 'sa_create_delete 3');
|
||||||
|
await arr.delete();
|
||||||
|
await arr.delete();
|
||||||
|
// Operate should still succeed because things aren't closed
|
||||||
|
expect(await arr.operate((r) async => r.length), isZero);
|
||||||
|
await arr.close();
|
||||||
|
await arr.close();
|
||||||
|
// Operate should fail
|
||||||
|
await expectLater(() async => arr.operate((r) async => r.length),
|
||||||
|
throwsA(isA<StateError>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> testDHTShortArrayAdd() async {
|
||||||
|
final arr = await DHTShortArray.create(debugName: 'sa_add 1');
|
||||||
|
|
||||||
|
final dataset =
|
||||||
|
Iterable<int>.generate(256).map((n) => utf8.encode('elem $n')).toList();
|
||||||
|
|
||||||
|
print('adding');
|
||||||
|
{
|
||||||
|
final (res, ok) = await arr.operateWrite((w) async {
|
||||||
|
for (var n = 0; n < dataset.length; n++) {
|
||||||
|
print('add $n');
|
||||||
|
final success = await w.tryAddItem(dataset[n]);
|
||||||
|
expect(success, isTrue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(res, isNull);
|
||||||
|
expect(ok, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
print('get all');
|
||||||
|
{
|
||||||
|
final dataset2 = await arr.operate((r) async => r.getAllItems());
|
||||||
|
expect(dataset2, equals(dataset));
|
||||||
|
}
|
||||||
|
|
||||||
|
print('clear');
|
||||||
|
{
|
||||||
|
final (res, ok) = await arr.operateWrite((w) async => w.tryClear());
|
||||||
|
expect(res, isTrue);
|
||||||
|
expect(ok, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
print('get all');
|
||||||
|
{
|
||||||
|
final dataset3 = await arr.operate((r) async => r.getAllItems());
|
||||||
|
expect(dataset3, isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
await arr.delete();
|
||||||
|
await arr.close();
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
4380113E2BE01E850006987E /* libveilid_flutter.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libveilid_flutter.a; path = "../../../../../veilid/target/lipo-ios/libveilid_flutter.a"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@ -76,6 +77,14 @@
|
|||||||
path = RunnerTests;
|
path = RunnerTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4380113D2BE01E850006987E /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4380113E2BE01E850006987E /* libveilid_flutter.a */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -94,6 +103,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
4380113D2BE01E850006987E /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -361,16 +371,22 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = W2TA5TB8Q5;
|
DEVELOPMENT_TEAM = XP5LBLT7M7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
@ -541,16 +557,22 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = W2TA5TB8Q5;
|
DEVELOPMENT_TEAM = XP5LBLT7M7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -564,16 +586,22 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = W2TA5TB8Q5;
|
DEVELOPMENT_TEAM = XP5LBLT7M7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Veilid Support Tests";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "org.veilid.packages.veilid-support.tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@ -24,6 +26,8 @@
|
|||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@ -41,9 +45,5 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -10,12 +10,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.11.0"
|
||||||
async_tools:
|
async_tools:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
path: "../../async_tools"
|
name: async_tools
|
||||||
relative: true
|
sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "1.0.0"
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -24,13 +25,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.4"
|
version: "8.1.4"
|
||||||
bloc_tools:
|
bloc_advanced_tools:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
path: "../../bloc_tools"
|
name: bloc_advanced_tools
|
||||||
relative: true
|
sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "1.0.0"
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -145,14 +147,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_lints:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: flutter_lints
|
|
||||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.2"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -221,14 +215,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
lints:
|
lint_hard:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lint_hard
|
||||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "4.0.0"
|
||||||
loggy:
|
loggy:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -261,13 +255,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.0"
|
||||||
mutex:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
path: "../../mutex"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "3.1.0"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -447,7 +434,7 @@ packages:
|
|||||||
path: "../../../../veilid/veilid-flutter"
|
path: "../../../../veilid/veilid-flutter"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.3.1"
|
version: "0.3.2"
|
||||||
veilid_support:
|
veilid_support:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -455,6 +442,13 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.2+0"
|
version: "1.0.2+0"
|
||||||
|
veilid_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
path: "../../../../veilid/veilid-flutter/packages/veilid_test"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.1.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,90 +1,27 @@
|
|||||||
name: example
|
name: example
|
||||||
description: "Veilid Support Example"
|
description: "Veilid Support Example"
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.4 <4.0.0'
|
sdk: '>=3.3.4 <4.0.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^1.0.6
|
|
||||||
veilid_support:
|
veilid_support:
|
||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
async_tools: ^0.1.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.1
|
lint_hard: ^4.0.0
|
||||||
mutex:
|
veilid_test:
|
||||||
path: ../../mutex
|
path: ../../../../veilid/veilid-flutter/packages/veilid_test
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
@ -57,6 +57,7 @@ class DHTRecord {
|
|||||||
DHTRecordCrypto get crypto => _crypto;
|
DHTRecordCrypto get crypto => _crypto;
|
||||||
OwnedDHTRecordPointer get ownedDHTRecordPointer =>
|
OwnedDHTRecordPointer get ownedDHTRecordPointer =>
|
||||||
OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!);
|
OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!);
|
||||||
|
bool get isOpen => _open;
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
if (!_open) {
|
if (!_open) {
|
||||||
|
@ -2,10 +2,10 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
import '../../../../veilid_support.dart';
|
import '../../../../veilid_support.dart';
|
||||||
@ -179,6 +179,13 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
_singleton = globalPool;
|
_singleton = globalPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> close() async {
|
||||||
|
if (_singleton != null) {
|
||||||
|
_singleton!._routingContext.close();
|
||||||
|
_singleton = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Veilid get veilid => _veilid;
|
Veilid get veilid => _veilid;
|
||||||
|
|
||||||
void log(String message) {
|
void log(String message) {
|
||||||
@ -191,8 +198,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
required DHTSchema schema,
|
required DHTSchema schema,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
TypedKey? parent}) async {
|
TypedKey? parent}) async {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
// Create the record
|
// Create the record
|
||||||
final recordDescriptor = await dhtctx.createDHTRecord(schema);
|
final recordDescriptor = await dhtctx.createDHTRecord(schema);
|
||||||
|
|
||||||
@ -225,8 +233,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
required TypedKey recordKey,
|
required TypedKey recordKey,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
TypedKey? parent}) async {
|
TypedKey? parent}) async {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
log('openDHTRecord: debugName=$debugName key=$recordKey');
|
log('openDHTRecord: debugName=$debugName key=$recordKey');
|
||||||
|
|
||||||
// If we are opening a key that already exists
|
// If we are opening a key that already exists
|
||||||
@ -303,8 +312,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
// Collect all dependencies (including the record itself)
|
// Collect all dependencies (including the record itself)
|
||||||
// in reverse (bottom-up/delete order)
|
// in reverse (bottom-up/delete order)
|
||||||
List<TypedKey> _collectChildrenInner(TypedKey recordKey) {
|
List<TypedKey> _collectChildrenInner(TypedKey recordKey) {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
final allDeps = <TypedKey>[];
|
final allDeps = <TypedKey>[];
|
||||||
final currentDeps = [recordKey];
|
final currentDeps = [recordKey];
|
||||||
while (currentDeps.isNotEmpty) {
|
while (currentDeps.isNotEmpty) {
|
||||||
@ -318,16 +328,18 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
return allDeps.reversedView;
|
return allDeps.reversedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _debugPrintChildren(TypedKey recordKey, {List<TypedKey>? allDeps}) {
|
String _debugChildren(TypedKey recordKey, {List<TypedKey>? allDeps}) {
|
||||||
allDeps ??= _collectChildrenInner(recordKey);
|
allDeps ??= _collectChildrenInner(recordKey);
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('Parent: $recordKey (${_state.debugNames[recordKey.toString()]})');
|
var out =
|
||||||
|
'Parent: $recordKey (${_state.debugNames[recordKey.toString()]})\n';
|
||||||
for (final dep in allDeps) {
|
for (final dep in allDeps) {
|
||||||
if (dep != recordKey) {
|
if (dep != recordKey) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print(' Child: $dep (${_state.debugNames[dep.toString()]})');
|
out += ' Child: $dep (${_state.debugNames[dep.toString()]})\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteRecordInner(TypedKey recordKey) async {
|
Future<void> _deleteRecordInner(TypedKey recordKey) async {
|
||||||
@ -343,8 +355,8 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
final allDeps = _collectChildrenInner(recordKey);
|
final allDeps = _collectChildrenInner(recordKey);
|
||||||
|
|
||||||
if (allDeps.singleOrNull != recordKey) {
|
if (allDeps.singleOrNull != recordKey) {
|
||||||
_debugPrintChildren(recordKey, allDeps: allDeps);
|
final dbgstr = _debugChildren(recordKey, allDeps: allDeps);
|
||||||
assert(false, 'must delete children first');
|
throw StateError('must delete children first: $dbgstr');
|
||||||
}
|
}
|
||||||
|
|
||||||
final ori = _opened[recordKey];
|
final ori = _opened[recordKey];
|
||||||
@ -359,7 +371,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _validateParentInner(TypedKey? parent, TypedKey child) {
|
void _validateParentInner(TypedKey? parent, TypedKey child) {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
|
|
||||||
final childJson = child.toJson();
|
final childJson = child.toJson();
|
||||||
final existingParent = _state.parentByChild[childJson];
|
final existingParent = _state.parentByChild[childJson];
|
||||||
@ -379,7 +393,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
|
|
||||||
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child,
|
Future<void> _addDependencyInner(TypedKey? parent, TypedKey child,
|
||||||
{required String debugName}) async {
|
{required String debugName}) async {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
if (_state.rootRecords.contains(child)) {
|
if (_state.rootRecords.contains(child)) {
|
||||||
// Dependency already added
|
// Dependency already added
|
||||||
@ -404,8 +420,9 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _removeDependenciesInner(List<TypedKey> childList) async {
|
Future<void> _removeDependenciesInner(List<TypedKey> childList) async {
|
||||||
assert(_mutex.isLocked, 'should be locked here');
|
if (!_mutex.isLocked) {
|
||||||
|
throw StateError('should be locked here');
|
||||||
|
}
|
||||||
var state = _state;
|
var state = _state;
|
||||||
|
|
||||||
for (final child in childList) {
|
for (final child in childList) {
|
||||||
@ -442,7 +459,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/// Create a root DHTRecord that has no dependent records
|
/// Create a root DHTRecord that has no dependent records
|
||||||
Future<DHTRecord> create({
|
Future<DHTRecord> createRecord({
|
||||||
required String debugName,
|
required String debugName,
|
||||||
VeilidRoutingContext? routingContext,
|
VeilidRoutingContext? routingContext,
|
||||||
TypedKey? parent,
|
TypedKey? parent,
|
||||||
@ -479,7 +496,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// Open a DHTRecord readonly
|
/// Open a DHTRecord readonly
|
||||||
Future<DHTRecord> openRead(TypedKey recordKey,
|
Future<DHTRecord> openRecordRead(TypedKey recordKey,
|
||||||
{required String debugName,
|
{required String debugName,
|
||||||
VeilidRoutingContext? routingContext,
|
VeilidRoutingContext? routingContext,
|
||||||
TypedKey? parent,
|
TypedKey? parent,
|
||||||
@ -508,7 +525,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// Open a DHTRecord writable
|
/// Open a DHTRecord writable
|
||||||
Future<DHTRecord> openWrite(
|
Future<DHTRecord> openRecordWrite(
|
||||||
TypedKey recordKey,
|
TypedKey recordKey,
|
||||||
KeyPair writer, {
|
KeyPair writer, {
|
||||||
required String debugName,
|
required String debugName,
|
||||||
@ -548,7 +565,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
/// This is primarily used for backing up private content on to the DHT
|
/// This is primarily used for backing up private content on to the DHT
|
||||||
/// to synchronizing it between devices. Because it is 'owned', the correct
|
/// to synchronizing it between devices. Because it is 'owned', the correct
|
||||||
/// parent must be specified.
|
/// parent must be specified.
|
||||||
Future<DHTRecord> openOwned(
|
Future<DHTRecord> openRecordOwned(
|
||||||
OwnedDHTRecordPointer ownedDHTRecordPointer, {
|
OwnedDHTRecordPointer ownedDHTRecordPointer, {
|
||||||
required String debugName,
|
required String debugName,
|
||||||
required TypedKey parent,
|
required TypedKey parent,
|
||||||
@ -556,7 +573,7 @@ class DHTRecordPool with TableDBBackedJson<DHTRecordPoolAllocations> {
|
|||||||
int defaultSubkey = 0,
|
int defaultSubkey = 0,
|
||||||
DHTRecordCrypto? crypto,
|
DHTRecordCrypto? crypto,
|
||||||
}) =>
|
}) =>
|
||||||
openWrite(
|
openRecordWrite(
|
||||||
ownedDHTRecordPointer.recordKey,
|
ownedDHTRecordPointer.recordKey,
|
||||||
ownedDHTRecordPointer.owner,
|
ownedDHTRecordPointer.owner,
|
||||||
debugName: debugName,
|
debugName: debugName,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
import '../../../veilid_support.dart';
|
import '../../../veilid_support.dart';
|
||||||
@ -43,7 +43,7 @@ class DHTShortArray {
|
|||||||
final schema = DHTSchema.smpl(
|
final schema = DHTSchema.smpl(
|
||||||
oCnt: 0,
|
oCnt: 0,
|
||||||
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]);
|
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]);
|
||||||
dhtRecord = await pool.create(
|
dhtRecord = await pool.createRecord(
|
||||||
debugName: debugName,
|
debugName: debugName,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
routingContext: routingContext,
|
routingContext: routingContext,
|
||||||
@ -52,7 +52,7 @@ class DHTShortArray {
|
|||||||
writer: smplWriter);
|
writer: smplWriter);
|
||||||
} else {
|
} else {
|
||||||
final schema = DHTSchema.dflt(oCnt: stride + 1);
|
final schema = DHTSchema.dflt(oCnt: stride + 1);
|
||||||
dhtRecord = await pool.create(
|
dhtRecord = await pool.createRecord(
|
||||||
debugName: debugName,
|
debugName: debugName,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
routingContext: routingContext,
|
routingContext: routingContext,
|
||||||
@ -80,7 +80,7 @@ class DHTShortArray {
|
|||||||
VeilidRoutingContext? routingContext,
|
VeilidRoutingContext? routingContext,
|
||||||
TypedKey? parent,
|
TypedKey? parent,
|
||||||
DHTRecordCrypto? crypto}) async {
|
DHTRecordCrypto? crypto}) async {
|
||||||
final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey,
|
final dhtRecord = await DHTRecordPool.instance.openRecordRead(headRecordKey,
|
||||||
debugName: debugName,
|
debugName: debugName,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
routingContext: routingContext,
|
routingContext: routingContext,
|
||||||
@ -103,7 +103,7 @@ class DHTShortArray {
|
|||||||
TypedKey? parent,
|
TypedKey? parent,
|
||||||
DHTRecordCrypto? crypto,
|
DHTRecordCrypto? crypto,
|
||||||
}) async {
|
}) async {
|
||||||
final dhtRecord = await DHTRecordPool.instance.openWrite(
|
final dhtRecord = await DHTRecordPool.instance.openRecordWrite(
|
||||||
headRecordKey, writer,
|
headRecordKey, writer,
|
||||||
debugName: debugName,
|
debugName: debugName,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
@ -144,21 +144,31 @@ class DHTShortArray {
|
|||||||
/// Get the record pointer foir this shortarray
|
/// Get the record pointer foir this shortarray
|
||||||
OwnedDHTRecordPointer get recordPointer => _head.recordPointer;
|
OwnedDHTRecordPointer get recordPointer => _head.recordPointer;
|
||||||
|
|
||||||
|
/// Check if the shortarray is open
|
||||||
|
bool get isOpen => _head.isOpen;
|
||||||
|
|
||||||
/// Free all resources for the DHTShortArray
|
/// Free all resources for the DHTShortArray
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await _watchController?.close();
|
await _watchController?.close();
|
||||||
|
_watchController = null;
|
||||||
await _head.close();
|
await _head.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Free all resources for the DHTShortArray and delete it from the DHT
|
/// Free all resources for the DHTShortArray and delete it from the DHT
|
||||||
|
/// Will wait until the short array is closed to delete it
|
||||||
Future<void> delete() async {
|
Future<void> delete() async {
|
||||||
await close();
|
await _head.delete();
|
||||||
await DHTRecordPool.instance.deleteRecord(recordKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs a closure that guarantees the DHTShortArray
|
/// Runs a closure that guarantees the DHTShortArray
|
||||||
/// will be closed upon exit, even if an uncaught exception is thrown
|
/// will be closed upon exit, even if an uncaught exception is thrown
|
||||||
Future<T> scope<T>(Future<T> Function(DHTShortArray) scopeFunction) async {
|
Future<T> scope<T>(Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||||
|
if (!isOpen) {
|
||||||
|
throw StateError('short array is not open"');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return await scopeFunction(this);
|
return await scopeFunction(this);
|
||||||
} finally {
|
} finally {
|
||||||
@ -171,6 +181,10 @@ class DHTShortArray {
|
|||||||
/// uncaught exception is thrown
|
/// uncaught exception is thrown
|
||||||
Future<T> deleteScope<T>(
|
Future<T> deleteScope<T>(
|
||||||
Future<T> Function(DHTShortArray) scopeFunction) async {
|
Future<T> Function(DHTShortArray) scopeFunction) async {
|
||||||
|
if (!isOpen) {
|
||||||
|
throw StateError('short array is not open"');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final out = await scopeFunction(this);
|
final out = await scopeFunction(this);
|
||||||
await close();
|
await close();
|
||||||
@ -182,11 +196,16 @@ class DHTShortArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runs a closure allowing read-only access to the shortarray
|
/// Runs a closure allowing read-only access to the shortarray
|
||||||
Future<T?> operate<T>(Future<T?> Function(DHTShortArrayRead) closure) async =>
|
Future<T?> operate<T>(Future<T?> Function(DHTShortArrayRead) closure) async {
|
||||||
_head.operate((head) async {
|
if (!isOpen) {
|
||||||
final reader = _DHTShortArrayRead._(head);
|
throw StateError('short array is not open"');
|
||||||
return closure(reader);
|
}
|
||||||
});
|
|
||||||
|
return _head.operate((head) async {
|
||||||
|
final reader = _DHTShortArrayRead._(head);
|
||||||
|
return closure(reader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs a closure allowing read-write access to the shortarray
|
/// Runs a closure allowing read-write access to the shortarray
|
||||||
/// Makes only one attempt to consistently write the changes to the DHT
|
/// Makes only one attempt to consistently write the changes to the DHT
|
||||||
@ -206,38 +225,48 @@ class DHTShortArray {
|
|||||||
/// succeeded, returning false will trigger another eventual consistency
|
/// succeeded, returning false will trigger another eventual consistency
|
||||||
/// attempt.
|
/// attempt.
|
||||||
Future<void> operateWriteEventual(
|
Future<void> operateWriteEventual(
|
||||||
Future<bool> Function(DHTShortArrayWrite) closure,
|
Future<bool> Function(DHTShortArrayWrite) closure,
|
||||||
{Duration? timeout}) async =>
|
{Duration? timeout}) async {
|
||||||
_head.operateWriteEventual((head) async {
|
if (!isOpen) {
|
||||||
final writer = _DHTShortArrayWrite._(head);
|
throw StateError('short array is not open"');
|
||||||
return closure(writer);
|
}
|
||||||
}, timeout: timeout);
|
|
||||||
|
return _head.operateWriteEventual((head) async {
|
||||||
|
final writer = _DHTShortArrayWrite._(head);
|
||||||
|
return closure(writer);
|
||||||
|
}, timeout: timeout);
|
||||||
|
}
|
||||||
|
|
||||||
/// Listen to and any all changes to the structure of this short array
|
/// Listen to and any all changes to the structure of this short array
|
||||||
/// regardless of where the changes are coming from
|
/// regardless of where the changes are coming from
|
||||||
Future<StreamSubscription<void>> listen(
|
Future<StreamSubscription<void>> listen(
|
||||||
void Function() onChanged,
|
void Function() onChanged,
|
||||||
) =>
|
) {
|
||||||
_listenMutex.protect(() async {
|
if (!isOpen) {
|
||||||
// If don't have a controller yet, set it up
|
throw StateError('short array is not open"');
|
||||||
if (_watchController == null) {
|
}
|
||||||
// Set up watch requirements
|
|
||||||
_watchController = StreamController<void>.broadcast(onCancel: () {
|
|
||||||
// If there are no more listeners then we can get
|
|
||||||
// rid of the controller and drop our subscriptions
|
|
||||||
unawaited(_listenMutex.protect(() async {
|
|
||||||
// Cancel watches of head record
|
|
||||||
await _head.cancelWatch();
|
|
||||||
_watchController = null;
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start watching head record
|
return _listenMutex.protect(() async {
|
||||||
await _head.watch();
|
// If don't have a controller yet, set it up
|
||||||
}
|
if (_watchController == null) {
|
||||||
// Return subscription
|
// Set up watch requirements
|
||||||
return _watchController!.stream.listen((_) => onChanged());
|
_watchController = StreamController<void>.broadcast(onCancel: () {
|
||||||
});
|
// If there are no more listeners then we can get
|
||||||
|
// rid of the controller and drop our subscriptions
|
||||||
|
unawaited(_listenMutex.protect(() async {
|
||||||
|
// Cancel watches of head record
|
||||||
|
await _head.cancelWatch();
|
||||||
|
_watchController = null;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start watching head record
|
||||||
|
await _head.watch();
|
||||||
|
}
|
||||||
|
// Return subscription
|
||||||
|
return _watchController!.stream.listen((_) => onChanged());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// Fields
|
// Fields
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:bloc_tools/bloc_tools.dart';
|
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
@ -50,13 +50,30 @@ class _DHTShortArrayHead {
|
|||||||
TypedKey get recordKey => _headRecord.key;
|
TypedKey get recordKey => _headRecord.key;
|
||||||
OwnedDHTRecordPointer get recordPointer => _headRecord.ownedDHTRecordPointer;
|
OwnedDHTRecordPointer get recordPointer => _headRecord.ownedDHTRecordPointer;
|
||||||
int get length => _index.length;
|
int get length => _index.length;
|
||||||
|
bool get isOpen => _headRecord.isOpen;
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
final futures = <Future<void>>[_headRecord.close()];
|
await _headMutex.protect(() async {
|
||||||
for (final lr in _linkedRecords) {
|
if (!isOpen) {
|
||||||
futures.add(lr.close());
|
return;
|
||||||
}
|
}
|
||||||
await Future.wait(futures);
|
final futures = <Future<void>>[_headRecord.close()];
|
||||||
|
for (final lr in _linkedRecords) {
|
||||||
|
futures.add(lr.close());
|
||||||
|
}
|
||||||
|
await Future.wait(futures);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete() async {
|
||||||
|
await _headMutex.protect(() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
final futures = <Future<void>>[pool.deleteRecord(_headRecord.key)];
|
||||||
|
for (final lr in _linkedRecords) {
|
||||||
|
futures.add(pool.deleteRecord(lr.key));
|
||||||
|
}
|
||||||
|
await Future.wait(futures);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> operate<T>(Future<T> Function(_DHTShortArrayHead) closure) async =>
|
Future<T> operate<T>(Future<T> Function(_DHTShortArrayHead) closure) async =>
|
||||||
@ -270,7 +287,7 @@ class _DHTShortArrayHead {
|
|||||||
final schema = DHTSchema.smpl(
|
final schema = DHTSchema.smpl(
|
||||||
oCnt: 0,
|
oCnt: 0,
|
||||||
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]);
|
members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]);
|
||||||
final dhtRecord = await pool.create(
|
final dhtRecord = await pool.createRecord(
|
||||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||||
parent: parent,
|
parent: parent,
|
||||||
routingContext: routingContext,
|
routingContext: routingContext,
|
||||||
@ -292,14 +309,14 @@ class _DHTShortArrayHead {
|
|||||||
TypedKey recordKey, int recordNumber) async {
|
TypedKey recordKey, int recordNumber) async {
|
||||||
final writer = _headRecord.writer;
|
final writer = _headRecord.writer;
|
||||||
return (writer != null)
|
return (writer != null)
|
||||||
? await DHTRecordPool.instance.openWrite(
|
? await DHTRecordPool.instance.openRecordWrite(
|
||||||
recordKey,
|
recordKey,
|
||||||
writer,
|
writer,
|
||||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||||
parent: _headRecord.key,
|
parent: _headRecord.key,
|
||||||
routingContext: _headRecord.routingContext,
|
routingContext: _headRecord.routingContext,
|
||||||
)
|
)
|
||||||
: await DHTRecordPool.instance.openRead(
|
: await DHTRecordPool.instance.openRecordRead(
|
||||||
recordKey,
|
recordKey,
|
||||||
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
debugName: '${_headRecord.debugName}_linked_$recordNumber',
|
||||||
parent: _headRecord.key,
|
parent: _headRecord.key,
|
||||||
|
@ -3,7 +3,6 @@ import 'dart:async';
|
|||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
|
|
||||||
import 'table_db.dart';
|
import 'table_db.dart';
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ extension IdentityMasterExtension on IdentityMaster {
|
|||||||
identityRecordKey.kind, identitySecret);
|
identityRecordKey.kind, identitySecret);
|
||||||
|
|
||||||
late final List<AccountRecordInfo> accountRecordInfo;
|
late final List<AccountRecordInfo> accountRecordInfo;
|
||||||
await (await pool.openRead(identityRecordKey,
|
await (await pool.openRecordRead(identityRecordKey,
|
||||||
debugName:
|
debugName:
|
||||||
'IdentityMaster::readAccountsFromIdentity::IdentityRecord',
|
'IdentityMaster::readAccountsFromIdentity::IdentityRecord',
|
||||||
parent: masterRecordKey,
|
parent: masterRecordKey,
|
||||||
@ -168,14 +168,14 @@ extension IdentityMasterExtension on IdentityMaster {
|
|||||||
|
|
||||||
// Open identity key for writing
|
// Open identity key for writing
|
||||||
veilidLoggy.debug('Opening identity record');
|
veilidLoggy.debug('Opening identity record');
|
||||||
return (await pool.openWrite(
|
return (await pool.openRecordWrite(
|
||||||
identityRecordKey, identityWriter(identitySecret),
|
identityRecordKey, identityWriter(identitySecret),
|
||||||
debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord',
|
debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord',
|
||||||
parent: masterRecordKey))
|
parent: masterRecordKey))
|
||||||
.scope((identityRec) async {
|
.scope((identityRec) async {
|
||||||
// Create new account to insert into identity
|
// Create new account to insert into identity
|
||||||
veilidLoggy.debug('Creating new account');
|
veilidLoggy.debug('Creating new account');
|
||||||
return (await pool.create(
|
return (await pool.createRecord(
|
||||||
debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord',
|
debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord',
|
||||||
parent: identityRec.key))
|
parent: identityRec.key))
|
||||||
.deleteScope((accountRec) async {
|
.deleteScope((accountRec) async {
|
||||||
@ -231,14 +231,14 @@ class IdentityMasterWithSecrets {
|
|||||||
|
|
||||||
// IdentityMaster DHT record is public/unencrypted
|
// IdentityMaster DHT record is public/unencrypted
|
||||||
veilidLoggy.debug('Creating master identity record');
|
veilidLoggy.debug('Creating master identity record');
|
||||||
return (await pool.create(
|
return (await pool.createRecord(
|
||||||
debugName:
|
debugName:
|
||||||
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
|
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
|
||||||
crypto: const DHTRecordCryptoPublic()))
|
crypto: const DHTRecordCryptoPublic()))
|
||||||
.deleteScope((masterRec) async {
|
.deleteScope((masterRec) async {
|
||||||
veilidLoggy.debug('Creating identity record');
|
veilidLoggy.debug('Creating identity record');
|
||||||
// Identity record is private
|
// Identity record is private
|
||||||
return (await pool.create(
|
return (await pool.createRecord(
|
||||||
debugName: 'IdentityMasterWithSecrets::create::IdentityRecord',
|
debugName: 'IdentityMasterWithSecrets::create::IdentityRecord',
|
||||||
parent: masterRec.key))
|
parent: masterRec.key))
|
||||||
.scope((identityRec) async {
|
.scope((identityRec) async {
|
||||||
@ -296,7 +296,7 @@ Future<IdentityMaster> openIdentityMaster(
|
|||||||
final pool = DHTRecordPool.instance;
|
final pool = DHTRecordPool.instance;
|
||||||
|
|
||||||
// IdentityMaster DHT record is public/unencrypted
|
// IdentityMaster DHT record is public/unencrypted
|
||||||
return (await pool.openRead(identityMasterRecordKey,
|
return (await pool.openRecordRead(identityMasterRecordKey,
|
||||||
debugName:
|
debugName:
|
||||||
'IdentityMaster::openIdentityMaster::IdentityMasterRecord'))
|
'IdentityMaster::openIdentityMaster::IdentityMasterRecord'))
|
||||||
.deleteScope((masterRec) async {
|
.deleteScope((masterRec) async {
|
||||||
|
@ -3,7 +3,6 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
import 'table_db.dart';
|
import 'table_db.dart';
|
||||||
|
@ -36,10 +36,11 @@ packages:
|
|||||||
async_tools:
|
async_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "../async_tools"
|
name: async_tools
|
||||||
relative: true
|
sha256: "972f68ab663724d86260a31e363c1355ff493308441b872bf4e7b8adc67c832c"
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "1.0.0"
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -48,13 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.3"
|
version: "8.1.3"
|
||||||
bloc_tools:
|
bloc_advanced_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "../bloc_tools"
|
name: bloc_advanced_tools
|
||||||
relative: true
|
sha256: bc0e1d5c26ae7df011464ab6abc2134dcfb668952acc87359abc7457cab091dd
|
||||||
source: path
|
url: "https://pub.dev"
|
||||||
version: "1.0.0"
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -409,13 +411,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
mutex:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "../mutex"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "3.1.0"
|
|
||||||
node_preamble:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -7,11 +7,9 @@ environment:
|
|||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.2.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async_tools:
|
async_tools: ^0.1.0
|
||||||
path: ../async_tools
|
|
||||||
bloc: ^8.1.3
|
bloc: ^8.1.3
|
||||||
bloc_tools:
|
bloc_advanced_tools: ^0.1.0
|
||||||
path: ../bloc_tools
|
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
fast_immutable_collections: ^10.1.1
|
fast_immutable_collections: ^10.1.1
|
||||||
@ -19,8 +17,6 @@ dependencies:
|
|||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.8.1
|
||||||
loggy: ^2.0.3
|
loggy: ^2.0.3
|
||||||
meta: ^1.11.0
|
meta: ^1.11.0
|
||||||
mutex:
|
|
||||||
path: ../mutex
|
|
||||||
|
|
||||||
protobuf: ^3.1.0
|
protobuf: ^3.1.0
|
||||||
veilid:
|
veilid:
|
||||||
|
19
pubspec.lock
19
pubspec.lock
@ -60,10 +60,10 @@ packages:
|
|||||||
async_tools:
|
async_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/async_tools"
|
path: "../dart_async_tools"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.0"
|
version: "0.1.0"
|
||||||
awesome_extensions:
|
awesome_extensions:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -96,13 +96,13 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.4"
|
version: "8.1.4"
|
||||||
bloc_tools:
|
bloc_advanced_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/bloc_tools"
|
path: "../bloc_advanced_tools"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.0"
|
version: "0.1.0"
|
||||||
blurry_modal_progress_hud:
|
blurry_modal_progress_hud:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -830,13 +830,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.1"
|
version: "2.9.1"
|
||||||
mutex:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "packages/mutex"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "3.1.0"
|
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1504,7 +1497,7 @@ packages:
|
|||||||
path: "../veilid/veilid-flutter"
|
path: "../veilid/veilid-flutter"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.3.1"
|
version: "0.3.2"
|
||||||
veilid_support:
|
veilid_support:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
14
pubspec.yaml
14
pubspec.yaml
@ -11,14 +11,12 @@ dependencies:
|
|||||||
animated_theme_switcher: ^2.0.10
|
animated_theme_switcher: ^2.0.10
|
||||||
ansicolor: ^2.0.2
|
ansicolor: ^2.0.2
|
||||||
archive: ^3.4.10
|
archive: ^3.4.10
|
||||||
async_tools:
|
async_tools: ^0.1.0
|
||||||
path: packages/async_tools
|
|
||||||
awesome_extensions: ^2.0.14
|
awesome_extensions: ^2.0.14
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
basic_utils: ^5.7.0
|
basic_utils: ^5.7.0
|
||||||
bloc: ^8.1.4
|
bloc: ^8.1.4
|
||||||
bloc_tools:
|
bloc_advanced_tools: ^0.1.0
|
||||||
path: packages/bloc_tools
|
|
||||||
blurry_modal_progress_hud: ^1.1.1
|
blurry_modal_progress_hud: ^1.1.1
|
||||||
change_case: ^2.0.1
|
change_case: ^2.0.1
|
||||||
charcode: ^1.3.1
|
charcode: ^1.3.1
|
||||||
@ -55,8 +53,6 @@ dependencies:
|
|||||||
meta: ^1.11.0
|
meta: ^1.11.0
|
||||||
mobile_scanner: ^4.0.1
|
mobile_scanner: ^4.0.1
|
||||||
motion_toast: ^2.9.1
|
motion_toast: ^2.9.1
|
||||||
mutex:
|
|
||||||
path: packages/mutex
|
|
||||||
pasteboard: ^0.2.0
|
pasteboard: ^0.2.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
@ -87,6 +83,12 @@ dependencies:
|
|||||||
xterm: ^4.0.0
|
xterm: ^4.0.0
|
||||||
zxing2: ^0.2.3
|
zxing2: ^0.2.3
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
async_tools:
|
||||||
|
path: ../dart_async_tools
|
||||||
|
bloc_advanced_tools:
|
||||||
|
path: ../bloc_advanced_tools
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.9
|
build_runner: ^2.4.9
|
||||||
freezed: ^2.5.2
|
freezed: ^2.5.2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user