diff --git a/packages/yx_scope/lib/src/core/scope_state.dart b/packages/yx_scope/lib/src/core/scope_state.dart index 0fe6adc..ad81804 100644 --- a/packages/yx_scope/lib/src/core/scope_state.dart +++ b/packages/yx_scope/lib/src/core/scope_state.dart @@ -1,12 +1,46 @@ -enum ScopeState { - none, - initializing, - available, - disposing, +abstract class ScopeState { + ScopeState._(); + + factory ScopeState.none() = ScopeStateNone; + factory ScopeState.initializing() = ScopeStateInitializing; + factory ScopeState.available({required Scope scope}) = ScopeStateAvailable; + factory ScopeState.disposing() = ScopeStateDisposing; + + bool get none => this is ScopeStateNone; + + bool get initializing => this is ScopeStateInitializing; + + bool get available => this is ScopeStateAvailable; + + bool get disposing => this is ScopeStateDisposing; +} + +class ScopeStateNone extends ScopeState { + ScopeStateNone() : super._(); + + @override + String toString() => 'ScopeState<$Scope>.none'; +} + +class ScopeStateInitializing extends ScopeState { + ScopeStateInitializing() : super._(); + + @override + String toString() => 'ScopeState<$Scope>.initializing'; +} + +class ScopeStateAvailable extends ScopeState { + final Scope scope; + + ScopeStateAvailable({required this.scope}) : super._(); + + @override + String toString() => 'ScopeState<$Scope>.available'; } -extension ScopeStateExt on ScopeState { - bool get none => this == ScopeState.none; +class ScopeStateDisposing extends ScopeState { + ScopeStateDisposing() : super._(); - bool get available => this == ScopeState.available; + @override + String toString() => 'ScopeState<$Scope>.disposing'; } diff --git a/packages/yx_scope/lib/src/core/scope_state_holder.dart b/packages/yx_scope/lib/src/core/scope_state_holder.dart index 8133c9b..2e20352 100644 --- a/packages/yx_scope/lib/src/core/scope_state_holder.dart +++ b/packages/yx_scope/lib/src/core/scope_state_holder.dart @@ -1,21 +1,31 @@ part of '../base_scope_container.dart'; -typedef StateListener = void Function(S scope); +typedef StateListener = void Function(S? scope); + +typedef ScopeStateListener = void Function(ScopeState state); typedef RemoveStateListener = void Function(); class ScopeStateHolder { final _listeners = LinkedList>(); - Scope _scope; + ScopeState _state; bool _debugCanAddListeners = true; - ScopeStateHolder(Scope state) : _scope = state; + ScopeStateHolder(this._state); + + Scope? get scope { + final state = this.state; + if (state is ScopeStateAvailable) { + return state.scope; + } + return null; + } - Scope get scope => _scope; + ScopeState get state => _state; - void _setScope(Scope state) { - _scope = state; + void _updateState(ScopeState state) { + _state = state; final errors = []; final stackTraces = []; @@ -40,6 +50,10 @@ class ScopeStateHolder { /// The [listener] callback will be called immediately on addition and /// synchronously whenever [state] changes. /// + /// Note: This method only calls the callback when the state is [ScopeStateAvailable] + /// (passing the scope object) or when the scope is [ScopeStateNone] (passing null). + /// It doesn't trigger for [ScopeStateInitializing] or [ScopeStateDisposing] states. + /// /// Set [emitImmediately] to true if you want to an immediate execution /// of the [listener] with the current state. /// @@ -50,6 +64,44 @@ class ScopeStateHolder { RemoveStateListener listen( StateListener listener, { bool emitImmediately = false, + }) => + _listen( + (state) { + if (state is ScopeStateAvailable) { + listener(state.scope); + } else if (state is ScopeStateNone) { + listener(null); + } + }, + emitImmediately: emitImmediately, + ); + + /// Subscribes to the state. + /// + /// The [listener] callback will be called immediately on addition and + /// synchronously whenever [state] changes. + /// + /// Note: This method emits on every [ScopeState] change. + /// + /// Set [emitImmediately] to true if you want to an immediate execution + /// of the [listener] with the current state. + /// + /// To remove this [listener], call the function returned by [listen]. + /// + /// Listeners cannot add other listeners. + /// Adding and removing listeners has a constant time-complexity. + RemoveStateListener listenState( + ScopeStateListener listener, { + bool emitImmediately = false, + }) => + _listen( + (state) => listener(state), + emitImmediately: emitImmediately, + ); + + RemoveStateListener _listen( + void Function(ScopeState state) listener, { + bool emitImmediately = false, }) { assert(() { if (!_debugCanAddListeners) { @@ -64,7 +116,7 @@ class ScopeStateHolder { // Intentionally unsafe call of the listener before adding to the [_listeners] // so that if there is an exception — we throw it back to consumer // with an original stacktrace without adding to the [_listeners]. - listener(scope); + listener(state); } on Object catch (_) { rethrow; } finally { @@ -93,7 +145,7 @@ class ScopeStateHolder { class Entry extends LinkedListEntry> { Entry(this.listener); - final StateListener listener; + final ScopeStateListener listener; } /// An error thrown when tried to update the state of a [ScopeStateHolder], diff --git a/packages/yx_scope/lib/src/core_scope_holder.dart b/packages/yx_scope/lib/src/core_scope_holder.dart index 337b26e..9351c47 100644 --- a/packages/yx_scope/lib/src/core_scope_holder.dart +++ b/packages/yx_scope/lib/src/core_scope_holder.dart @@ -7,16 +7,13 @@ part of 'base_scope_container.dart'; /// Holder contains the state of a [BaseScopeContainer] — null or the scope itself. /// This is the core entity that provides access to the [BaseScopeContainer]. abstract class CoreScopeHolder - extends ScopeStateHolder with ScopeStateStreamable { + extends ScopeStateHolder with ScopeStateStreamable { final ScopeObserverInternal _scopeObserverInternal; final List? _depObservers; final List? _asyncDepObservers; - final _scopeStateHolder = ScopeStateHolder(ScopeState.none); Completer? _waitLifecycleCompleter; - ScopeState get _scopeState => _scopeStateHolder.scope; - CoreScopeHolder({ List? scopeObservers, List? depObservers, @@ -38,7 +35,7 @@ abstract class CoreScopeHolder _scopeObserverInternal = ScopeObserverInternal(scopeObservers), _depObservers = depObservers, _asyncDepObservers = asyncDepObservers, - super(null); + super(ScopeState.none()); /// Initialize scope. [Scope] becomes available and everyone can /// start working with it via [BaseScopeHolder]. @@ -57,77 +54,77 @@ abstract class CoreScopeHolder if (scope is! Scope) { throw ScopeException('You must implement $Scope for your $Container'); } - switch (_scopeState) { - case ScopeState.initializing: - throw ScopeException( - 'You are trying to initialize $Container that is initializing right now. ' - 'Given instances of the $Container might be different, ' - 'so do not call `create` sequentially without `drops`', - ); - case ScopeState.available: + + if (state.initializing) { + throw ScopeException( + 'You are trying to initialize $Container that is initializing right now. ' + 'Given instances of the $Container might be different, ' + 'so do not call `create` sequentially without `drops`', + ); + } + + if (state.available) { + throw ScopeException( + 'You are trying to initialize $Container that has been already initialized' + 'Given instances of the $Container might be different, ' + 'so do not call `create` sequentially without `drops`', + ); + } + + if (state.disposing) { + Logger.warning( + '$Container calls init method while disposing. ' + 'This is a weird situation and can lead to unexpected behaviour.', + ); + final currentCompleter = _waitLifecycleCompleter; + if (currentCompleter != null) { throw ScopeException( - 'You are trying to initialize $Container that has been already initialized' - 'Given instances of the $Container might be different, ' - 'so do not call `create` sequentially without `drops`', - ); - case ScopeState.disposing: - Logger.warning( - '$Container calls init method while disposing. ' - 'This is a weird situation and can lead to unexpected behaviour.', + 'Scope is already waiting for dispose in order to be recreated again. ' + 'Probably you have called `create` method without await a few times in a row.', ); - final currentCompleter = _waitLifecycleCompleter; - if (currentCompleter != null) { - throw ScopeException( - 'Scope is already waiting for dispose in order to be recreated again. ' - 'Probably you have called `create` method without await a few times in a row.', - ); - } - final completer = Completer.sync(); - _waitLifecycleCompleter = completer; - final removeListener = _scopeStateHolder.listen((state) { - if (state == ScopeState.none) { - completer.complete(); - } else { - completer.completeError( - ScopeError( - 'Unexpected state ($state) after ${ScopeState.disposing},' - ' must be ${ScopeState.none}', - ), - ); - } - }); - try { - await completer.future; - } on Object catch (e, s) { - Error.throwWithStackTrace( + } + final completer = Completer.sync(); + _waitLifecycleCompleter = completer; + final removeListener = listenState((state) { + if (state.none) { + completer.complete(); + } else { + completer.completeError( ScopeError( - '$e\n' - 'Unexpected exception when were waiting for dispose during initialization. ' - 'This is definitely an error in the library,' - ' please contact an owner, if you see this message.', + 'Unexpected state ($state) after ${ScopeState.disposing},' + ' must be ${ScopeState.none}', ), - s, ); - } finally { - _waitLifecycleCompleter = null; - removeListener(); - Logger.debug( - 'Wait for scope dispose has completed, state=$_scopeState', + } + }); + try { + await completer.future; + } on Object catch (e, s) { + Error.throwWithStackTrace( + ScopeError( + '$e\n' + 'Unexpected exception when were waiting for dispose during initialization. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ), + s, + ); + } finally { + _waitLifecycleCompleter = null; + removeListener(); + Logger.debug( + 'Wait for scope dispose has completed, state=$state', + ); + if (!state.none) { + throw ScopeError( + 'Scope initialization waited for dispose of the previous scope state, ' + 'it\'s expected to be ${ScopeState.none}, ' + 'but it appeared to be $state.' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', ); - if (!_scopeState.none) { - throw ScopeError( - 'Scope initialization waited for dispose of the previous scope state, ' - 'it\'s expected to be ${ScopeState.none}, ' - 'but it appeared to be $_scopeState.' - 'This is definitely an error in the library,' - ' please contact an owner, if you see this message.', - ); - } } - break; - case ScopeState.none: - // Everything is okay, scope can be initialized - break; + } } _prepareObservers(scope); @@ -202,96 +199,97 @@ abstract class CoreScopeHolder 'both non-nullable args (drop during initialization)', ); } - switch (_scopeState) { - case ScopeState.disposing: - assert( - false, - 'You are trying to dispose $Container that is disposing right now', - ); - return; - case ScopeState.none: - assert( - false, - 'You are trying to dispose $Container that has been already disposed or never existed', + + if (state.disposing) { + assert( + false, + 'You are trying to dispose $Container that is disposing right now', + ); + return; + } + + if (state.none) { + assert( + false, + 'You are trying to dispose $Container that has been already disposed or never existed', + ); + return; + } + + if (state.initializing) { + // if no initialized dependencies has been passed + // then this is a normal drop + if (initializedDeps == null && initializedScope == null) { + Logger.warning( + '$Container calls dispose method while initializing. ' + 'This is a weird situation and can lead to unexpected behaviour.', ); - return; - case ScopeState.initializing: - // if no initialized dependencies has been passed - // then this is a normal drop - if (initializedDeps == null && initializedScope == null) { - Logger.warning( - '$Container calls dispose method while initializing. ' - 'This is a weird situation and can lead to unexpected behaviour.', - ); - final currentCompleter = _waitLifecycleCompleter; - if (currentCompleter != null) { - throw ScopeException( - 'Scope is already waiting for initialization in order to be disposed again. ' - 'Probably you have called `drop` method without await a few times in a row.', - ); - } - final completer = Completer.sync(); - _waitLifecycleCompleter = completer; + final currentCompleter = _waitLifecycleCompleter; + if (currentCompleter != null) { + throw ScopeException( + 'Scope is already waiting for initialization in order to be disposed again. ' + 'Probably you have called `drop` method without await a few times in a row.', + ); + } + final completer = Completer.sync(); + _waitLifecycleCompleter = completer; - final removeListener = _scopeStateHolder.listen((state) { - if (state == ScopeState.available) { - completer.complete(); - } else { - completer.completeError( - ScopeError( - 'Unexpected state ($state) after ${ScopeState.initializing},' - ' must be ${ScopeState.available}', - ), - ); - } - }); - try { - await completer.future; - } on Object catch (e, s) { - Error.throwWithStackTrace( + final removeListener = listenState((state) { + if (state.available) { + completer.complete(); + } else { + completer.completeError( ScopeError( - '$e\n' - 'Unexpected exception when were waiting for initialization during dispose. ' - 'This is definitely an error in the library,' - ' please contact an owner, if you see this message.', + 'Unexpected state ($state) after ${ScopeState.initializing},' + ' must be ${ScopeState.available}', ), - s, - ); - } finally { - _waitLifecycleCompleter = null; - removeListener(); - Logger.debug( - 'Wait for scope initialization has completed, state=$_scopeState', ); - if (!_scopeState.available) { - throw ScopeError( - 'Scope dispose waited for initialization of the previous scope state, ' - 'it\'s expected to be ${ScopeState.available}, ' - 'but it appears to be $_scopeState. ' - 'This is definitely an error in the library,' - ' please contact an owner, if you see this message.', - ); - } } - } else { - Logger.warning( - '$Container calls dispose method, because of some exception. ' - 'See stacktrace below for more details.', + }); + try { + await completer.future; + } on Object catch (e, s) { + Error.throwWithStackTrace( + ScopeError( + '$e\n' + 'Unexpected exception when were waiting for initialization during dispose. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ), + s, + ); + } finally { + _waitLifecycleCompleter = null; + removeListener(); + Logger.debug( + 'Wait for scope initialization has completed, state=$state', ); + if (!state.available) { + throw ScopeError( + 'Scope dispose waited for initialization of the previous scope state, ' + 'it\'s expected to be ${ScopeState.available}, ' + 'but it appears to be $state. ' + 'This is definitely an error in the library,' + ' please contact an owner, if you see this message.', + ); + } } - // If no initialized dependencies has been passed it means this is an internal drop. - // It happens only when some Exception appeared during init. - // In this case we do not wait for availability and do the drop. - break; - case ScopeState.available: - // Everything is okay, scope can be disposed because now it's active - break; + } else { + Logger.warning( + '$Container calls dispose method, because of some exception. ' + 'See stacktrace below for more details.', + ); + } + // If no initialized dependencies has been passed it means this is an internal drop. + // It happens only when some Exception appeared during init. + // In this case we do not wait for availability and do the drop. } + final scope = this.scope as Container? ?? initializedScope; if (scope == null) { throw ScopeError( - '$Container must not be null if scope state is $_scopeState', + '$Container must not be null if scope state is $state', ); } @@ -345,32 +343,31 @@ abstract class CoreScopeHolder } // ignore: use_setters_to_change_properties - void _updateScope(ScopeState state) { - Logger.debug('$Container state: ${state.name}'); - _scopeStateHolder._setScope(state); + void _updateScope(ScopeState state) { + Logger.debug('$Container state: $state'); + _updateState(state); } - void _initializing() => _updateScope(ScopeState.initializing); + void _initializing() => _updateScope(ScopeState.initializing()); Future _disposed() async { try { - _setScope(null); // must be the first call in this method + _updateScope(ScopeState.none()); // must be the first call in this method } on NotifyListenerError catch (e, s) { Logger.error('Some listeners thrown an exception during dispose', e, s); } - _updateScope(ScopeState.none); } void _available(Scope scope) { try { - _setScope(scope); // must be the first call in this method + _updateScope(ScopeState.available( + scope: scope)); // must be the first call in this method } on NotifyListenerError catch (e, s) { Logger.error('Some listeners thrown an exception during init', e, s); } - _updateScope(ScopeState.available); } - void _disposing() => _updateScope(ScopeState.disposing); + void _disposing() => _updateScope(ScopeState.disposing()); void _prepareObservers(Container scope) { scope._depObserver._observers = _depObservers; diff --git a/packages/yx_scope/lib/src/scope_state_streamable.dart b/packages/yx_scope/lib/src/scope_state_streamable.dart index 58ee45a..1501d7c 100644 --- a/packages/yx_scope/lib/src/scope_state_streamable.dart +++ b/packages/yx_scope/lib/src/scope_state_streamable.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'base_scope_container.dart'; +import 'core/scope_state.dart'; mixin ScopeStateStreamable on ScopeStateHolder { - Stream get stream { - late StreamController controller; + Stream get stream { + late StreamController controller; late RemoveStateListener removeStateListener; void onListen() { @@ -18,7 +19,30 @@ mixin ScopeStateStreamable on ScopeStateHolder { await controller.close(); } - controller = StreamController( + controller = StreamController( + onListen: onListen, + onCancel: onCancel, + ); + + return controller.stream; + } + + Stream> get stateStream { + late StreamController> controller; + late RemoveStateListener removeStateListener; + + void onListen() { + removeStateListener = listenState((state) { + controller.add(state); + }); + } + + void onCancel() async { + removeStateListener(); + await controller.close(); + } + + controller = StreamController>( onListen: onListen, onCancel: onCancel, ); diff --git a/packages/yx_scope/lib/yx_scope.dart b/packages/yx_scope/lib/yx_scope.dart index 91daad5..c8fc5fd 100644 --- a/packages/yx_scope/lib/yx_scope.dart +++ b/packages/yx_scope/lib/yx_scope.dart @@ -9,6 +9,7 @@ export 'src/base_scope_container.dart' TestableScopeStateHolder; export 'src/core/async_lifecycle.dart'; export 'src/core/scope_exception.dart'; +export 'src/core/scope_state.dart'; export 'src/monitoring/observers.dart'; export 'src/monitoring/models/dep_id.dart'; export 'src/monitoring/models/scope_id.dart'; diff --git a/packages/yx_scope/test/scope_state_streamable_test.dart b/packages/yx_scope/test/scope_state_streamable_test.dart index d3f7bb0..826b389 100644 --- a/packages/yx_scope/test/scope_state_streamable_test.dart +++ b/packages/yx_scope/test/scope_state_streamable_test.dart @@ -9,95 +9,215 @@ void main() { ScopeObservatory.logger = const TestLogger(); }); - test('stream for scope happy path', () async { - final holder = _TestScopeHolder(); - - int counter = 0; - holder.stream.listen((scope) { - if (counter % 2 == 0) { - expect(scope, isNotNull); - } else { - expect(scope, isNull); - } + group('stream of scope', () { + test('stream for scope happy path', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + holder.stream.listen((scope) { + if (counter % 2 == 0) { + expect(scope, isNotNull); + } else { + expect(scope, isNull); + } + }); + + await holder.create(); + counter++; + await holder.drop(); + counter++; + await holder.create(); + counter++; + await holder.drop(); + counter++; }); - await holder.create(); - counter++; - await holder.drop(); - counter++; - await holder.create(); - counter++; - await holder.drop(); - counter++; - }); + test('after pausing stream of scope it emits buffered events', () async { + final holder = _TestScopeHolder(); - test('after pausing stream of scope it emits buffered events', () async { - final holder = _TestScopeHolder(); + int counter = 0; + final subscription = holder.stream.listen((scope) { + if (counter % 2 == 0) { + expect(scope, isNotNull); + } else { + expect(scope, isNull); + } + counter++; + }); - int counter = 0; - final subscription = holder.stream.listen((scope) { - if (counter % 2 == 0) { - expect(scope, isNotNull); - } else { - expect(scope, isNull); - } - counter++; - }); + await holder.create(); + await holder.drop(); - await holder.create(); - await holder.drop(); + subscription.pause(); - subscription.pause(); + await holder.create(); + await holder.drop(); + await holder.create(); - await holder.create(); - await holder.drop(); - await holder.create(); + subscription.resume(); - subscription.resume(); + await holder.drop(); + await holder.create(); + await holder.drop(); + + expect(counter, 7); + }); + + test('reusing the same instance of stream cause an exception', () async { + final holder = _TestScopeHolder(); + final stream = holder.stream; + + stream.listen((_) {}); + try { + stream.listen((_) {}); + fail('Must throw an exception'); + } on StateError catch (e) { + expect(e.message, 'Stream has already been listened to.'); + } + }); - await holder.drop(); - await holder.create(); - await holder.drop(); + test('two different get calls return different streams', () async { + final holder = _TestScopeHolder(); + final stream1 = holder.stream; + final stream2 = holder.stream; + expect(stream1, isNot(stream2)); + }); - expect(counter, 7); + test('ask for a stream creates new listener and cancelling removes it', + () async { + final holder = _TestScopeHolder(); + final testHolder = TestableScopeStateHolder(holder); + final stream1 = holder.stream; + expect(testHolder.listeners.isEmpty, isTrue); + final sub1 = stream1.listen((_) {}); + expect(testHolder.listeners.length, 1); + final stream2 = holder.stream; + expect(testHolder.listeners.length, 1); + final sub2 = stream2.listen((_) {}); + expect(testHolder.listeners.length, 2); + await sub1.cancel(); + expect(testHolder.listeners.length, 1); + await sub2.cancel(); + expect(testHolder.listeners.isEmpty, isTrue); + }); }); - test('reusing the same instance of stream cause an exception', () async { - final holder = _TestScopeHolder(); - final stream = holder.stream; + group('stream of state', () { + test('stream for scope happy path', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + holder.stateStream.listen((state) { + switch (counter % 4) { + case 0: + expect(state, isA>()); + break; + case 1: + expect(state, isA>()); + expect((state as ScopeStateAvailable).scope, + isA<_TestScopeContainer>()); + break; + case 2: + expect(state, isA>()); + break; + case 3: + expect(state, isA>()); + break; + default: + } + counter++; + }); + + await holder.create(); + await holder.drop(); + await holder.create(); + await holder.drop(); + await holder.create(); + await holder.drop(); + }); + + test('after pausing stream of scope it emits buffered events', () async { + final holder = _TestScopeHolder(); + + int counter = 0; + final subscription = holder.stateStream.listen((state) { + print(counter); + print(state); + switch (counter % 4) { + case 0: + expect(state, isA>()); + break; + case 1: + expect(state, isA>()); + expect((state as ScopeStateAvailable).scope, + isA<_TestScopeContainer>()); + break; + case 2: + expect(state, isA>()); + break; + case 3: + expect(state, isA>()); + break; + default: + } + counter++; + }); + + await holder.create(); + await holder.drop(); + + subscription.pause(); + + await holder.create(); + await holder.drop(); + await holder.create(); + + subscription.resume(); + + await holder.drop(); + await holder.create(); + await holder.drop(); + + expect(counter, 8); + }); + + test('reusing the same instance of stream cause an exception', () async { + final holder = _TestScopeHolder(); + final stream = holder.stateStream; - stream.listen((_) {}); - try { stream.listen((_) {}); - fail('Must throw an exception'); - } on StateError catch (e) { - expect(e.message, 'Stream has already been listened to.'); - } - }); + try { + stream.listen((_) {}); + fail('Must throw an exception'); + } on StateError catch (e) { + expect(e.message, 'Stream has already been listened to.'); + } + }); - test('two different get calls return different streams', () async { - final holder = _TestScopeHolder(); - final stream1 = holder.stream; - final stream2 = holder.stream; - expect(stream1, isNot(stream2)); - }); + test('two different get calls return different streams', () async { + final holder = _TestScopeHolder(); + final stream1 = holder.stateStream; + final stream2 = holder.stateStream; + expect(stream1, isNot(stream2)); + }); - test('ask for a stream creates new listener and cancelling removes it', - () async { - final holder = _TestScopeHolder(); - final testHolder = TestableScopeStateHolder(holder); - final stream1 = holder.stream; - expect(testHolder.listeners.isEmpty, isTrue); - final sub1 = stream1.listen((_) {}); - expect(testHolder.listeners.length, 1); - final stream2 = holder.stream; - expect(testHolder.listeners.length, 1); - final sub2 = stream2.listen((_) {}); - expect(testHolder.listeners.length, 2); - await sub1.cancel(); - expect(testHolder.listeners.length, 1); - await sub2.cancel(); - expect(testHolder.listeners.isEmpty, isTrue); + test('ask for a stream creates new listener and cancelling removes it', + () async { + final holder = _TestScopeHolder(); + final testHolder = TestableScopeStateHolder(holder); + final stream1 = holder.stateStream; + expect(testHolder.listeners.isEmpty, isTrue); + final sub1 = stream1.listen((_) {}); + expect(testHolder.listeners.length, 1); + final stream2 = holder.stateStream; + expect(testHolder.listeners.length, 1); + final sub2 = stream2.listen((_) {}); + expect(testHolder.listeners.length, 2); + await sub1.cancel(); + expect(testHolder.listeners.length, 1); + await sub2.cancel(); + expect(testHolder.listeners.isEmpty, isTrue); + }); }); } diff --git a/packages/yx_scope/test/scope_state_test.dart b/packages/yx_scope/test/scope_state_test.dart new file mode 100644 index 0000000..cf152c0 --- /dev/null +++ b/packages/yx_scope/test/scope_state_test.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:yx_scope/yx_scope.dart'; + +void main() { + test('scope state is valid according to updates', () async { + final scopeHolder = _TestScopeHolder(); + + final createCompleter = Completer(); + final dropCompleter = Completer(); + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.none, isTrue); + scopeHolder.create().then((_) => createCompleter.complete()); + + expect( + scopeHolder.state, isA>()); + expect(scopeHolder.state.initializing, isTrue); + await createCompleter.future; + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.available, isTrue); + + scopeHolder.drop().then((_) => dropCompleter.complete()); + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.disposing, isTrue); + + await dropCompleter.future; + + expect(scopeHolder.state, isA>()); + expect(scopeHolder.state.none, isTrue); + }); +} + +class _TestScopeHolder extends ScopeHolder<_TestScopeContainer> { + @override + _TestScopeContainer createContainer() => _TestScopeContainer(); +} + +class _TestScopeContainer extends ScopeContainer { + @override + List> get initializeQueue => [ + {_asyncDep} + ]; + + late final _asyncDep = rawAsyncDep( + () => Future.delayed(Duration.zero), + init: (dep) async => await dep, + dispose: (dep) async {}, + ); +}