From c21ee5b0d8501f0a306906fffede8ee9aa88f6f6 Mon Sep 17 00:00:00 2001 From: Barabas Raffai Date: Sun, 24 Mar 2024 21:33:39 +0000 Subject: [PATCH] Reset all client state when disconnected --- frontend/lib/api/api_websocket.dart | 24 +- frontend/lib/audio_events.dart | 19 +- frontend/lib/core/message_transport.dart | 6 - frontend/lib/main.dart | 24 +- .../lib/midi_mapping/model/midi_learn.dart | 67 ++- .../midi_mapping/model/midi_learn_state.dart | 5 + frontend/lib/midi_mapping/model/service.dart | 33 +- .../lib/midi_mapping/view/midi_learn.dart | 2 + .../lib/midi_mapping/view/midi_mapping.dart | 463 +++++++++--------- frontend/lib/parameter.dart | 57 ++- .../lib/presets/model/presets_client.dart | 22 +- .../lib/presets/model/presets_repository.dart | 20 +- .../lib/presets/model/presets_service.dart | 14 +- .../presets/model/selected_preset_client.dart | 22 +- .../model/selected_preset_repository.dart | 16 +- .../lib/status/model/websocket_status.dart | 1 - 16 files changed, 437 insertions(+), 358 deletions(-) diff --git a/frontend/lib/api/api_websocket.dart b/frontend/lib/api/api_websocket.dart index a78a85c3..f4a07c75 100644 --- a/frontend/lib/api/api_websocket.dart +++ b/frontend/lib/api/api_websocket.dart @@ -67,16 +67,22 @@ sealed class ApiMessage with _$ApiMessage { } @riverpod -ApiWebsocket apiWebsocket(ApiWebsocketRef ref) { - return ApiWebsocket( - websocket: ref.watch( - robustWebsocketProvider( - Uri.parse(kShrapnelUri), +ApiWebsocket? apiWebsocket(ApiWebsocketRef ref) { + final isAlive = ref.watch(isAliveProvider); + if (isAlive) { + return ApiWebsocket( + websocket: ref.watch( + robustWebsocketProvider( + Uri.parse(kShrapnelUri), + ), ), - ), - ); + ); + } + + return null; } +// TODO make this implement MessageTransport class ApiWebsocket { ApiWebsocket({required RobustWebsocket websocket}) : _websocket = websocket; @@ -92,10 +98,6 @@ class ApiWebsocket { (event) => 'received: $event', ); - late final Stream connectionStream = _websocket.connectionStream; - - bool get isAlive => _websocket.isAlive; - void send(ApiMessage message) { _log.finest('sending: $message'); _websocket.sendMessage(message.toProto().writeToBuffer()); diff --git a/frontend/lib/audio_events.dart b/frontend/lib/audio_events.dart index b5f22df1..16cd63fb 100644 --- a/frontend/lib/audio_events.dart +++ b/frontend/lib/audio_events.dart @@ -38,13 +38,18 @@ sealed class AudioEventMessage with _$AudioEventMessage { } final audioClippingServiceProvider = AutoDisposeChangeNotifierProvider( - (ref) => AudioClippingService( - stream: ref - .watch(apiWebsocketProvider) - .stream - .whereType() - .map((event) => event.message), - ), + (ref) { + final websocket = ref.watch(apiWebsocketProvider); + if (websocket != null) { + return AudioClippingService( + stream: websocket.stream + .whereType() + .map((event) => event.message), + ); + } + + return null; + }, ); class AudioClippingService extends ChangeNotifier { diff --git a/frontend/lib/core/message_transport.dart b/frontend/lib/core/message_transport.dart index 0711c331..542cbd44 100644 --- a/frontend/lib/core/message_transport.dart +++ b/frontend/lib/core/message_transport.dart @@ -27,12 +27,6 @@ abstract class MessageTransport { /// Messages from the other side of the connection will appear in this stream. Stream get stream; - /// A null is emitted every time a connection is successfully created - Stream get connectionStream; - - /// Returns true if the connection is alive at the moment - bool get isAlive; - /// Must be called to clean up resource after the transport is no longer in /// use. void dispose(); diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index fc8573f8..3101715b 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -137,9 +137,10 @@ class MyHomePage extends riverpod.ConsumerWidget { child: IconButton( icon: const Icon(Icons.menu_book_outlined), key: const Key('midi-learn-button'), - onPressed: () { - ref.read(midiLearnServiceProvider.notifier).startLearning(); - }, + onPressed: ref.watch(midiLearnServiceProvider).maybeWhen( + loading: null, + orElse: () => () => startMidiLearning(ref), + ), ), ), Tooltip( @@ -277,9 +278,11 @@ class MyHomePage extends riverpod.ConsumerWidget { message: 'Input clipping', child: Icon( Icons.input, - color: ref.watch(audioClippingServiceProvider).inputIsClipped - ? Colors.red - : null, + color: + ref.watch(audioClippingServiceProvider)?.inputIsClipped ?? + false + ? Colors.red + : null, ), ), const SizedBox(width: 8), @@ -287,7 +290,10 @@ class MyHomePage extends riverpod.ConsumerWidget { message: 'Output clipping', child: Icon( Icons.output, - color: ref.watch(audioClippingServiceProvider).outputIsClipped + color: ref + .watch(audioClippingServiceProvider) + ?.outputIsClipped ?? + false ? Colors.red : null, ), @@ -303,6 +309,10 @@ class MyHomePage extends riverpod.ConsumerWidget { ), ); } + + void startMidiLearning(riverpod.WidgetRef ref) { + ref.read(midiLearnServiceProvider.notifier).startLearning(); + } } class ProvisioningPage extends StatelessWidget { diff --git a/frontend/lib/midi_mapping/model/midi_learn.dart b/frontend/lib/midi_mapping/model/midi_learn.dart index 97b3f308..b15e1bc1 100644 --- a/frontend/lib/midi_mapping/model/midi_learn.dart +++ b/frontend/lib/midi_mapping/model/midi_learn.dart @@ -32,22 +32,58 @@ import 'service.dart'; final _log = Logger('shrapnel.midi_mapping.model.midi_learn'); final midiLearnServiceProvider = - AutoDisposeStateNotifierProvider( - (ref) => MidiLearnService( - mappingService: ref.watch(midiMappingServiceProvider), - parameterUpdates: - ref.watch(parameterServiceProvider).parameterUpdates.map((e) => e.id), - midiMessages: ref - .watch(apiWebsocketProvider) - .stream - .whereType() - .map((event) => event.message) - .whereType() - .map((event) => event.message), - ), + AutoDisposeStateNotifierProvider( + (ref) { + return switch (( + ref.watch(midiMappingServiceProvider), + ref.watch(parameterServiceProvider), + ref.watch(apiWebsocketProvider), + )) { + (final midiMappingService?, final parameterService?, final websocket?) => + MidiLearnService( + mappingService: midiMappingService, + parameterUpdates: parameterService.parameterUpdates.map((e) => e.id), + midiMessages: websocket.stream + .whereType() + .map((event) => event.message) + .whereType() + .map((event) => event.message), + ), + _ => MidiLearnServiceLoading(), + }; + }, ); -class MidiLearnService extends StateNotifier { +abstract class MidiLearnServiceBase extends StateNotifier { + MidiLearnServiceBase(super._state); + + void startLearning(); + + void cancelLearning(); + + Future undoRemoveSimilarMappings(); +} + +class MidiLearnServiceLoading extends MidiLearnServiceBase { + MidiLearnServiceLoading() : super(const MidiLearnState.loading()); + + @override + void cancelLearning() { + throw StateError("Can't cancel learning while loading"); + } + + @override + void startLearning() { + throw StateError("Can't start learning while loading"); + } + + @override + Future undoRemoveSimilarMappings() { + throw StateError("Can't undo learning while loading"); + } +} + +class MidiLearnService extends MidiLearnServiceBase { MidiLearnService({ required this.mappingService, required this.parameterUpdates, @@ -61,6 +97,7 @@ class MidiLearnService extends StateNotifier { final Stream parameterUpdates; final Stream midiMessages; + @override void startLearning() { state.maybeWhen( idle: (_) => state = const MidiLearnState.waitForParameter(), @@ -136,10 +173,12 @@ class MidiLearnService extends StateNotifier { ); } + @override void cancelLearning() { state = const MidiLearnState.idle(null); } + @override Future undoRemoveSimilarMappings() async { unawaited( state.maybeWhen( diff --git a/frontend/lib/midi_mapping/model/midi_learn_state.dart b/frontend/lib/midi_mapping/model/midi_learn_state.dart index 53d6195b..c71dcab6 100644 --- a/frontend/lib/midi_mapping/model/midi_learn_state.dart +++ b/frontend/lib/midi_mapping/model/midi_learn_state.dart @@ -25,10 +25,15 @@ part 'midi_learn_state.freezed.dart'; @freezed class MidiLearnState with _$MidiLearnState { + const factory MidiLearnState.loading() = _Loading; + const factory MidiLearnState.idle( List>? duplicates, ) = _Idle; + const factory MidiLearnState.waitForParameter() = _WaitForParameter; + const factory MidiLearnState.waitForMidi(String id) = _WaitForMidi; + const factory MidiLearnState.savingMapping() = _SavingMapping; } diff --git a/frontend/lib/midi_mapping/model/service.dart b/frontend/lib/midi_mapping/model/service.dart index da8e4d7a..5c16f284 100644 --- a/frontend/lib/midi_mapping/model/service.dart +++ b/frontend/lib/midi_mapping/model/service.dart @@ -33,15 +33,23 @@ import '../model/models.dart'; final _log = Logger('midi_mapping_service'); final midiMappingTransportProvider = AutoDisposeProvider( - (ref) => MidiMappingTransport( - websocket: ref.watch(apiWebsocketProvider), - ), + (ref) { + return switch (ref.watch(apiWebsocketProvider)) { + final websocket? => MidiMappingTransport(websocket: websocket), + null => null, + }; + }, ); final midiMappingServiceProvider = AutoDisposeChangeNotifierProvider( - (ref) => MidiMappingService( - websocket: ref.watch(midiMappingTransportProvider), - ), + (ref) { + return switch (ref.watch(midiMappingTransportProvider)) { + final transport? => MidiMappingService( + websocket: transport, + ), + null => null, + }; + }, ); class MidiMappingTransport @@ -68,18 +76,12 @@ class MidiMappingTransport (event) => 'receive message: $event', ); - @override - Stream get connectionStream => websocket.connectionStream; - ApiWebsocket websocket; @override void dispose() { unawaited(_controller.close()); } - - @override - bool get isAlive => websocket.isAlive; } class MidiMappingService extends ChangeNotifier { @@ -87,13 +89,8 @@ class MidiMappingService extends ChangeNotifier { required this.websocket, }) { _mappingsView = UnmodifiableMapView(__mappings); - - websocket.connectionStream.listen((_) async => getMapping()); _subscription = websocket.stream.listen(_handleMessage); - - if (websocket.isAlive) { - unawaited(getMapping()); - } + unawaited(getMapping()); } static const responseTimeout = Duration(milliseconds: 500); diff --git a/frontend/lib/midi_mapping/view/midi_learn.dart b/frontend/lib/midi_mapping/view/midi_learn.dart index c407796d..cda648b6 100644 --- a/frontend/lib/midi_mapping/view/midi_learn.dart +++ b/frontend/lib/midi_mapping/view/midi_learn.dart @@ -32,6 +32,7 @@ class MidiLearnStatus extends ConsumerWidget { final midiLearnService = ref.read(midiLearnServiceProvider.notifier); final isInProgress = midiLearnState.when( + loading: () => false, idle: (_) => false, waitForParameter: () => true, waitForMidi: (_) => true, @@ -39,6 +40,7 @@ class MidiLearnStatus extends ConsumerWidget { ); final isCancellable = midiLearnState.when( + loading: () => false, idle: (_) => false, waitForParameter: () => true, waitForMidi: (_) => true, diff --git a/frontend/lib/midi_mapping/view/midi_mapping.dart b/frontend/lib/midi_mapping/view/midi_mapping.dart index e1f21f09..804e1e0e 100644 --- a/frontend/lib/midi_mapping/view/midi_mapping.dart +++ b/frontend/lib/midi_mapping/view/midi_mapping.dart @@ -53,194 +53,209 @@ class MidiMappingPage extends ConsumerWidget { DataColumn(label: Text('Target')), DataColumn(label: Text('Delete')), ], - rows: midiMappingService.mappings.entries.map( - (mapEntry) { - final mapping = MidiMappingEntry( - id: mapEntry.key, - mapping: mapEntry.value, - ); - return DataRow( - key: ValueKey(mapping.id), - cells: [ - DataCell( - MidiChannelDropdown( - key: Key('${mapping.id}-midi-channel-dropdown'), - value: mapping.mapping.midiChannel, - onChanged: (value) => unawaited( - midiMappingService.updateMapping( - mapping.copyWith.mapping(midiChannel: value!), + rows: switch (midiMappingService) { + final midiMappingService? => + midiMappingService.mappings.entries.map( + (mapEntry) { + final mapping = MidiMappingEntry( + id: mapEntry.key, + mapping: mapEntry.value, + ); + return DataRow( + key: ValueKey(mapping.id), + cells: [ + DataCell( + MidiChannelDropdown( + key: Key('${mapping.id}-midi-channel-dropdown'), + value: mapping.mapping.midiChannel, + onChanged: (value) => unawaited( + midiMappingService.updateMapping( + mapping.copyWith.mapping(midiChannel: value!), + ), + ), ), ), - ), - ), - DataCell( - MidiCCDropdown( - key: Key('${mapping.id}-cc-number-dropdown'), - value: mapping.mapping.ccNumber, - onChanged: (value) => unawaited( - midiMappingService.updateMapping( - mapping.copyWith.mapping(ccNumber: value!), + DataCell( + MidiCCDropdown( + key: Key('${mapping.id}-cc-number-dropdown'), + value: mapping.mapping.ccNumber, + onChanged: (value) => unawaited( + midiMappingService.updateMapping( + mapping.copyWith.mapping(ccNumber: value!), + ), + ), ), ), - ), - ), - DataCell( - ModeDropdown( - key: Key('${mapping.id}-mode-dropdown'), - value: mapping.mapping.mode, - onChanged: (value) { - if (value == mapping.mapping.mode) { - return; - } - - if (value == null) { - return; - } - - const parameterModes = [ - MidiMappingMode.parameter, - MidiMappingMode.toggle, - ]; - - final isTrivialChange = - parameterModes.contains(mapping.mapping.mode) && + DataCell( + ModeDropdown( + key: Key('${mapping.id}-mode-dropdown'), + value: mapping.mapping.mode, + onChanged: (value) { + if (value == mapping.mapping.mode) { + return; + } + + if (value == null) { + return; + } + + const parameterModes = [ + MidiMappingMode.parameter, + MidiMappingMode.toggle, + ]; + + final isTrivialChange = parameterModes + .contains(mapping.mapping.mode) && parameterModes.contains(value); - if (isTrivialChange) { - unawaited( - midiMappingService.updateMapping( - switch (mapping.mapping) { - final MidiMappingToggle toggle => switch ( - value) { - MidiMappingMode.toggle => - throw StateError(''), - MidiMappingMode.parameter => - mapping.copyWith( - mapping: MidiMapping.parameter( - midiChannel: toggle.midiChannel, - ccNumber: toggle.ccNumber, - parameterId: toggle.parameterId, - ), - ), - MidiMappingMode.button => + if (isTrivialChange) { + unawaited( + midiMappingService.updateMapping( + switch (mapping.mapping) { + final MidiMappingToggle toggle => switch ( + value) { + MidiMappingMode.toggle => + throw StateError(''), + MidiMappingMode.parameter => + mapping.copyWith( + mapping: MidiMapping.parameter( + midiChannel: toggle.midiChannel, + ccNumber: toggle.ccNumber, + parameterId: toggle.parameterId, + ), + ), + MidiMappingMode.button => + throw StateError(''), + }, + final MidiMappingParameter parameter => + switch (value) { + MidiMappingMode.toggle => + mapping.copyWith( + mapping: MidiMapping.toggle( + midiChannel: + parameter.midiChannel, + ccNumber: parameter.ccNumber, + parameterId: + parameter.parameterId, + ), + ), + MidiMappingMode.parameter => + throw StateError(''), + MidiMappingMode.button => + throw StateError(''), + }, + MidiMappingButton() => throw StateError(''), }, - final MidiMappingParameter parameter => - switch (value) { - MidiMappingMode.toggle => + ), + ); + } else { + unawaited( + showDialog( + context: context, + builder: (context) => EditMappingDialog( + mapping: mapping, + mode: value, + ), + ), + ); + } + }, + ), + ), + DataCell( + switch (mapping.mapping) { + final MidiMappingToggle midiMapping => Row( + children: [ + Text( + 'Parameter:', + style: + Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 8), + ParametersDropdown( + key: Key( + '${mapping.id}-parameter-id-dropdown', + ), + value: midiMapping.parameterId, + onChanged: (value) => unawaited( + midiMappingService.updateMapping( mapping.copyWith( - mapping: MidiMapping.toggle( - midiChannel: parameter.midiChannel, - ccNumber: parameter.ccNumber, - parameterId: parameter.parameterId, + mapping: midiMapping.copyWith( + parameterId: value!, ), ), - MidiMappingMode.parameter => - throw StateError(''), - MidiMappingMode.button => - throw StateError(''), - }, - MidiMappingButton() => throw StateError(''), - }, - ), - ); - } else { - unawaited( - showDialog( - context: context, - builder: (context) => EditMappingDialog( - mapping: mapping, - mode: value, - ), - ), - ); - } - }, - ), - ), - DataCell( - switch (mapping.mapping) { - final MidiMappingToggle midiMapping => Row( - children: [ - Text( - 'Parameter:', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(width: 8), - ParametersDropdown( - key: Key('${mapping.id}-parameter-id-dropdown'), - value: midiMapping.parameterId, - onChanged: (value) => unawaited( - midiMappingService.updateMapping( - mapping.copyWith( - mapping: midiMapping.copyWith( - parameterId: value!, ), ), ), - ), - ), - ], - ), - final MidiMappingParameter midiMapping => Row( - children: [ - Text( - 'Parameter:', - style: Theme.of(context).textTheme.titleMedium, + ], ), - const SizedBox(width: 8), - ParametersDropdown( - key: Key('${mapping.id}-parameter-id-dropdown'), - value: midiMapping.parameterId, - onChanged: (value) => unawaited( - midiMappingService.updateMapping( - mapping.copyWith( - mapping: midiMapping.copyWith( - parameterId: value!, + final MidiMappingParameter midiMapping => Row( + children: [ + Text( + 'Parameter:', + style: + Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 8), + ParametersDropdown( + key: Key( + '${mapping.id}-parameter-id-dropdown', + ), + value: midiMapping.parameterId, + onChanged: (value) => unawaited( + midiMappingService.updateMapping( + mapping.copyWith( + mapping: midiMapping.copyWith( + parameterId: value!, + ), + ), ), ), ), - ), - ), - ], - ), - final MidiMappingButton midiMapping => Row( - children: [ - Text( - 'Preset:', - style: Theme.of(context).textTheme.titleMedium, + ], ), - const SizedBox(width: 8), - PresetsDropdown( - key: Key('${mapping.id}-preset-id-dropdown'), - value: midiMapping.presetId, - onChanged: (value) => unawaited( - midiMappingService.updateMapping( - mapping.copyWith( - mapping: midiMapping.copyWith( - presetId: value!, + final MidiMappingButton midiMapping => Row( + children: [ + Text( + 'Preset:', + style: + Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 8), + PresetsDropdown( + key: + Key('${mapping.id}-preset-id-dropdown'), + value: midiMapping.presetId, + onChanged: (value) => unawaited( + midiMappingService.updateMapping( + mapping.copyWith( + mapping: midiMapping.copyWith( + presetId: value!, + ), + ), ), ), ), - ), + ], ), - ], + }, + ), + DataCell( + IconButton( + key: Key('${mapping.id}-delete-button'), + icon: const Icon(Icons.delete), + onPressed: () => unawaited( + midiMappingService.deleteMapping(id: mapping.id), + ), ), - }, - ), - DataCell( - IconButton( - key: Key('${mapping.id}-delete-button'), - icon: const Icon(Icons.delete), - onPressed: () => unawaited( - midiMappingService.deleteMapping(id: mapping.id), ), - ), - ), - ], - ); - }, - ).toList(growable: false), + ], + ); + }, + ).toList(growable: false), + null => [], + }, ), ), ), @@ -400,34 +415,38 @@ class CreateMappingDialogState extends ConsumerState { }, ), ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.pop(context); - unawaited( - mappings.createMapping( - switch (mode) { - null => - throw StateError('Mode has not been selected'), - MidiMappingMode.toggle => MidiMapping.toggle( - midiChannel: channel!, - ccNumber: ccNumber!, - parameterId: parameterId!, - ), - MidiMappingMode.parameter => MidiMapping.parameter( - midiChannel: channel!, - ccNumber: ccNumber!, - parameterId: parameterId!, - ), - MidiMappingMode.button => MidiMapping.button( - midiChannel: channel!, - ccNumber: ccNumber!, - presetId: presetId!, + onPressed: mappings == null + ? null + : () { + if (_formKey.currentState!.validate()) { + Navigator.pop(context); + unawaited( + mappings.createMapping( + switch (mode) { + null => throw StateError( + 'Mode has not been selected', + ), + MidiMappingMode.toggle => MidiMapping.toggle( + midiChannel: channel!, + ccNumber: ccNumber!, + parameterId: parameterId!, + ), + MidiMappingMode.parameter => + MidiMapping.parameter( + midiChannel: channel!, + ccNumber: ccNumber!, + parameterId: parameterId!, + ), + MidiMappingMode.button => MidiMapping.button( + midiChannel: channel!, + ccNumber: ccNumber!, + presetId: presetId!, + ), + }, ), - }, - ), - ); - } - }, + ); + } + }, child: const Text('Create'), ), ], @@ -634,37 +653,41 @@ class EditMappingDialogState extends ConsumerState { }, ), ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.pop(context); - unawaited( - mappings.updateMapping( - MidiMappingEntry( - id: widget.mapping.id, - mapping: switch (mode) { - null => - throw StateError('Mode has not been selected'), - MidiMappingMode.toggle => MidiMapping.toggle( - midiChannel: channel!, - ccNumber: ccNumber!, - parameterId: parameterId!, - ), - MidiMappingMode.parameter => MidiMapping.parameter( - midiChannel: channel!, - ccNumber: ccNumber!, - parameterId: parameterId!, - ), - MidiMappingMode.button => MidiMapping.button( - midiChannel: channel!, - ccNumber: ccNumber!, - presetId: presetId!, + onPressed: mappings == null + ? null + : () { + if (_formKey.currentState!.validate()) { + Navigator.pop(context); + unawaited( + mappings.updateMapping( + MidiMappingEntry( + id: widget.mapping.id, + mapping: switch (mode) { + null => throw StateError( + 'Mode has not been selected', + ), + MidiMappingMode.toggle => MidiMapping.toggle( + midiChannel: channel!, + ccNumber: ccNumber!, + parameterId: parameterId!, + ), + MidiMappingMode.parameter => + MidiMapping.parameter( + midiChannel: channel!, + ccNumber: ccNumber!, + parameterId: parameterId!, + ), + MidiMappingMode.button => MidiMapping.button( + midiChannel: channel!, + ccNumber: ccNumber!, + presetId: presetId!, + ), + }, ), - }, - ), - ), - ); - } - }, + ), + ); + } + }, child: const Text('Update'), ), ], diff --git a/frontend/lib/parameter.dart b/frontend/lib/parameter.dart index 3a4978e0..ff8304b9 100644 --- a/frontend/lib/parameter.dart +++ b/frontend/lib/parameter.dart @@ -28,7 +28,6 @@ import 'package:rxdart/rxdart.dart'; import 'api/api_websocket.dart'; import 'core/message_transport.dart'; import 'riverpod_util.dart'; -import 'robust_websocket.dart'; part 'parameter.freezed.dart'; @@ -57,16 +56,17 @@ class AudioParameterMetaData with _$AudioParameterMetaData { class AudioParameterDoubleModel extends _$AudioParameterDoubleModel { @override Stream build(String parameterId) { - if (ref.watch(isAliveProvider)) { - return ref.watch(parameterServiceProvider).getParameter(parameterId); - } else { - return const Stream.empty(); + final service = ref.watch(parameterServiceProvider); + if (service == null) { + return const Stream.empty(); } + + return service.getParameter(parameterId); } void onUserChanged(double value) { _log.finest('user updated parameter $parameterId to $value'); - ref.read(parameterServiceProvider).parameterUpdatedByUser( + ref.read(parameterServiceProvider)!.parameterUpdatedByUser( AudioParameterDoubleData(value: value, id: parameterId), ); } @@ -93,10 +93,13 @@ sealed class ParameterServiceInputMessage with _$ParameterServiceInputMessage { } @riverpod -ParameterTransport parameterTransport(ParameterTransportRef ref) => - ParameterTransport( - websocket: ref.watch(apiWebsocketProvider), - ); +ParameterTransport? parameterTransport(ParameterTransportRef ref) => + switch (ref.watch(apiWebsocketProvider)) { + final websocket? => ParameterTransport( + websocket: websocket, + ), + null => null, + }; class ParameterTransport implements @@ -112,12 +115,6 @@ class ParameterTransport final _controller = StreamController(); - @override - Stream get connectionStream => websocket.connectionStream; - - @override - bool get isAlive => websocket.isAlive; - @override Stream get stream => websocket.stream .whereType() @@ -134,12 +131,15 @@ class ParameterTransport @riverpod // ignore: unsupported_provider_value -ParameterService parameterService(ParameterServiceRef ref) => - ref.listenAndDisposeChangeNotifier( - ParameterService( - transport: ref.watch(parameterTransportProvider), - ), - ); +ParameterService? parameterService(ParameterServiceRef ref) => + switch (ref.watch(parameterTransportProvider)) { + final transport? => ref.listenAndDisposeChangeNotifier( + ParameterService( + transport: transport, + ), + ), + null => null, + }; class ParameterService extends ChangeNotifier { ParameterService({ @@ -157,13 +157,9 @@ class ParameterService extends ChangeNotifier { ), ); - _transport.stream.listen(_handleIncomingMessage); + _subscription = _transport.stream.listen(_handleIncomingMessage); - if (_transport.isAlive) { - _requestParameterInitialisation(); - } - _transport.connectionStream - .listen((_) => _requestParameterInitialisation()); + _requestParameterInitialisation(); } void _requestParameterInitialisation() { @@ -172,7 +168,7 @@ class ParameterService extends ChangeNotifier { } final _parameterUpdatesController = - StreamController(); + StreamController.broadcast(); /// Parameter updates performed by the user through the GUI late final Stream parameterUpdates = @@ -184,6 +180,8 @@ class ParameterService extends ChangeNotifier { final MessageTransport _transport; + late final StreamSubscription _subscription; + Stream getParameter(String parameterId) { return _parameters .putIfAbsent(parameterId, StreamController.broadcast) @@ -209,6 +207,7 @@ class ParameterService extends ChangeNotifier { void dispose() { unawaited(_sink.close()); unawaited(_parameterUpdatesController.close()); + unawaited(_subscription.cancel()); for (final controller in _parameters.values) { unawaited(controller.close()); } diff --git a/frontend/lib/presets/model/presets_client.dart b/frontend/lib/presets/model/presets_client.dart index b09a6fd9..5a8ee090 100644 --- a/frontend/lib/presets/model/presets_client.dart +++ b/frontend/lib/presets/model/presets_client.dart @@ -122,8 +122,11 @@ sealed class PresetsMessage with _$PresetsMessage { } @riverpod -PresetsTransport presetsTransport(PresetsTransportRef ref) => - PresetsTransport(websocket: ref.watch(apiWebsocketProvider)); +PresetsTransport? presetsTransport(PresetsTransportRef ref) => + switch (ref.watch(apiWebsocketProvider)) { + final websocket? => PresetsTransport(websocket: websocket), + null => null, + }; class PresetsTransport implements MessageTransport { @@ -149,23 +152,20 @@ class PresetsTransport (event) => 'receive message: $event', ); - @override - Stream get connectionStream => websocket.connectionStream; - ApiWebsocket websocket; @override void dispose() { unawaited(_controller.close()); } - - @override - bool get isAlive => websocket.isAlive; } @riverpod -PresetsClient presetsClient(PresetsClientRef ref) => - PresetsClient(transport: ref.watch(presetsTransportProvider)); +PresetsClient? presetsClient(PresetsClientRef ref) => + switch (ref.watch(presetsTransportProvider)) { + final transport? => PresetsClient(transport: transport), + null => null, + }; class PresetsClient { PresetsClient({ @@ -232,8 +232,6 @@ class PresetsClient { ); } - Stream get connectionStream => _transport.connectionStream; - void dispose() { _transport.dispose(); } diff --git a/frontend/lib/presets/model/presets_repository.dart b/frontend/lib/presets/model/presets_repository.dart index 7d17ef3b..cdd08b5e 100644 --- a/frontend/lib/presets/model/presets_repository.dart +++ b/frontend/lib/presets/model/presets_repository.dart @@ -28,21 +28,25 @@ import 'presets_service.dart'; part 'presets_repository.g.dart'; @riverpod -PresetsRepositoryBase presetsRepository(PresetsRepositoryRef ref) => - PresetsRepository( - client: ref.watch(presetsClientProvider), - ); +PresetsRepositoryBase? presetsRepository(PresetsRepositoryRef ref) => + switch (ref.watch(presetsClientProvider)) { + final client? => PresetsRepository( + client: client, + ), + null => null, + }; @riverpod Stream> presetsStream(PresetsStreamRef ref) => - ref.watch(presetsRepositoryProvider).presets; + switch (ref.watch(presetsRepositoryProvider)) { + final repository? => repository.presets, + null => const Stream.empty(), + }; class PresetsRepository implements PresetsRepositoryBase { PresetsRepository({required this.client}) { client.presetUpdates.listen(_handleNotification); - client.connectionStream.listen((_) { - unawaited(client.initialise()); - }); + unawaited(client.initialise()); } PresetsClient client; diff --git a/frontend/lib/presets/model/presets_service.dart b/frontend/lib/presets/model/presets_service.dart index 6715f07e..c47114eb 100644 --- a/frontend/lib/presets/model/presets_service.dart +++ b/frontend/lib/presets/model/presets_service.dart @@ -89,10 +89,10 @@ class PresetsService extends _$PresetsService { final presetsRepository = ref.read(presetsRepositoryProvider); final selectedPresetRepository = ref.read(selectedPresetRepositoryProvider); - if (parameters == null) { - throw StateError( - 'Attempted to create preset while parameters are loading', - ); + if (parameters == null || + presetsRepository == null || + selectedPresetRepository == null) { + throw StateError('Attempted to create preset while loading'); } final record = await presetsRepository.create( @@ -102,11 +102,11 @@ class PresetsService extends _$PresetsService { } Future select(int id) async { - await ref.read(selectedPresetRepositoryProvider).selectPreset(id); + await ref.read(selectedPresetRepositoryProvider)!.selectPreset(id); } Future delete(int id) async { - await ref.read(presetsRepositoryProvider).delete(id); + await ref.read(presetsRepositoryProvider)!.delete(id); } Future saveChanges() async { @@ -130,7 +130,7 @@ class PresetsService extends _$PresetsService { return; } - await ref.read(presetsRepositoryProvider).update( + await ref.read(presetsRepositoryProvider)!.update( currentPreset.copyWith( preset: currentPreset.preset.copyWith( parameters: parameters, diff --git a/frontend/lib/presets/model/selected_preset_client.dart b/frontend/lib/presets/model/selected_preset_client.dart index 50264645..5a20ecee 100644 --- a/frontend/lib/presets/model/selected_preset_client.dart +++ b/frontend/lib/presets/model/selected_preset_client.dart @@ -46,10 +46,13 @@ sealed class SelectedPresetMessage with _$SelectedPresetMessage { } @riverpod -SelectedPresetTransport selectedPresetTransport( +SelectedPresetTransport? selectedPresetTransport( SelectedPresetTransportRef ref, ) => - SelectedPresetTransport(websocket: ref.watch(apiWebsocketProvider)); + switch (ref.watch(apiWebsocketProvider)) { + final websocket? => SelectedPresetTransport(websocket: websocket), + null => null, + }; class SelectedPresetTransport implements MessageTransport { @@ -74,23 +77,20 @@ class SelectedPresetTransport .map((event) => event.message) .logFinest(_log, (event) => 'received message: $event'); - @override - Stream get connectionStream => _websocket.connectionStream; - final ApiWebsocket _websocket; @override void dispose() { unawaited(_controller.close()); } - - @override - bool get isAlive => _websocket.isAlive; } @riverpod -SelectedPresetClient selectedPresetClient(SelectedPresetClientRef ref) => - SelectedPresetClient(transport: ref.watch(selectedPresetTransportProvider)); +SelectedPresetClient? selectedPresetClient(SelectedPresetClientRef ref) => + switch (ref.watch(selectedPresetTransportProvider)) { + final transport? => SelectedPresetClient(transport: transport), + null => null, + }; class SelectedPresetClient { SelectedPresetClient({ @@ -112,6 +112,4 @@ class SelectedPresetClient { Future selectPreset(int presetId) async { _transport.sink.add(SelectedPresetMessage.write(selectedPreset: presetId)); } - - Stream get connectionStream => _transport.connectionStream; } diff --git a/frontend/lib/presets/model/selected_preset_repository.dart b/frontend/lib/presets/model/selected_preset_repository.dart index d3b2e87a..b76b1a6c 100644 --- a/frontend/lib/presets/model/selected_preset_repository.dart +++ b/frontend/lib/presets/model/selected_preset_repository.dart @@ -27,20 +27,24 @@ import 'selected_preset_client.dart'; part 'selected_preset_repository.g.dart'; @riverpod -SelectedPresetRepositoryBase selectedPresetRepository( +SelectedPresetRepositoryBase? selectedPresetRepository( SelectedPresetRepositoryRef ref, ) => - SelectedPresetRepository(client: ref.watch(selectedPresetClientProvider)); + switch (ref.watch(selectedPresetClientProvider)) { + final client? => SelectedPresetRepository(client: client), + null => null, + }; @riverpod Stream selectedPresetStream(SelectedPresetStreamRef ref) => - ref.watch(selectedPresetRepositoryProvider).selectedPreset; + switch (ref.watch(selectedPresetRepositoryProvider)) { + final repository? => repository.selectedPreset, + null => const Stream.empty(), + }; class SelectedPresetRepository implements SelectedPresetRepositoryBase { SelectedPresetRepository({required this.client}) { - client.connectionStream.listen((_) { - unawaited(client.initialise()); - }); + unawaited(client.initialise()); } SelectedPresetClient client; diff --git a/frontend/lib/status/model/websocket_status.dart b/frontend/lib/status/model/websocket_status.dart index b59a4f31..3223cbdc 100644 --- a/frontend/lib/status/model/websocket_status.dart +++ b/frontend/lib/status/model/websocket_status.dart @@ -19,7 +19,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../api/api_websocket.dart'; import '../../robust_websocket.dart'; import '../data/status.dart';