diff --git a/lib/spinify.dart b/lib/spinify.dart index 9009bb5..46a3b73 100644 --- a/lib/spinify.dart +++ b/lib/spinify.dart @@ -6,7 +6,7 @@ export 'src/model/command.dart'; export 'src/model/config.dart'; export 'src/model/history.dart'; export 'src/model/jwt.dart'; -export 'src/model/metrics.dart'; +export 'src/model/metric.dart' show SpinifyMetrics; export 'src/model/presence_stats.dart'; export 'src/model/pushes_stream.dart'; export 'src/model/reply.dart'; diff --git a/lib/src/model/metric.dart b/lib/src/model/metric.dart new file mode 100644 index 0000000..3ca2503 --- /dev/null +++ b/lib/src/model/metric.dart @@ -0,0 +1,177 @@ +import 'package:meta/meta.dart'; + +import 'state.dart'; + +/* +/// Subscription count +/// - total +/// - unsubscribed +/// - subscribing +/// - subscribed +/// +/// {@category Metrics} +/// {@category Entity} +typedef SpinifySubscriptionCount = ({ + int total, + int unsubscribed, + int subscribing, + int subscribed +}); */ + +/// {@template metrics} +/// Metrics of Spinify client. +/// {@endtemplate} +/// +/// {@category Metrics} +sealed class SpinifyMetrics implements Comparable { + /// {@macro metrics} + const SpinifyMetrics(); + + /// Timestamp of the metrics. + abstract final DateTime timestamp; + + /// The time when the client was initialized. + abstract final DateTime initializedAt; + + /// Next Command ID. + /// Incremented after each command. + abstract final int commandId; + + /// The current state of the client. + abstract final SpinifyState state; + + /* + /// The total number of messages & size of bytes sent. + final ({BigInt count, BigInt size}) transferred; + + /// The total number of messages & size of bytes received. + final ({BigInt count, BigInt size}) received; + + /// The number of subscriptions. + final ({ + SpinifySubscriptionCount client, + SpinifySubscriptionCount server + }) subscriptions; + + /// The average speed of the request/response in milliseconds. + /// - min - minimum speed + /// - avg - average speed + /// - max - maximum speed + final ({int min, int avg, int max}) speed; + + /// Is refresh active. + final bool isRefreshActive; + */ + + /// The total number of successful connections. + abstract final int connects; + + /// The time of the last connect. + abstract final DateTime? lastConnectAt; + + /// Last connected URL. + /// Used for reconnecting after connection lost. + /// If null, then client is not connected or interractively disconnected. + abstract final String? reconnectUrl; + + /// Number of reconnect attempts. + /// If null, then client is not connected yet or interractively disconnected. + abstract final int? reconnectAttempts; + + /// Next reconnect time in case of connection lost. + abstract final DateTime? nextReconnectAt; + + /// The total number of times the connection has been disconnected. + abstract final int disconnects; + + /// The time of the last disconnect. + abstract final DateTime? lastDisconnectAt; + + /// Convert metrics to JSON. + Map toJson() => {}; + + @override + int compareTo(SpinifyMetrics other) => timestamp.compareTo(other.timestamp); + + @override + String toString() => 'SpinifyMetrics{}'; +} + +@internal +@immutable +final class SpinifyMetrics$Immutable extends SpinifyMetrics { + const SpinifyMetrics$Immutable(); + + @override + DateTime get timestamp => throw UnimplementedError(); + + @override + DateTime get initializedAt => throw UnimplementedError(); + + @override + int get commandId => throw UnimplementedError(); + + @override + SpinifyState get state => throw UnimplementedError(); + + @override + int get connects => throw UnimplementedError(); + + @override + DateTime? get lastConnectAt => throw UnimplementedError(); + + @override + String? get reconnectUrl => throw UnimplementedError(); + + @override + int? get reconnectAttempts => throw UnimplementedError(); + + @override + DateTime? get nextReconnectAt => throw UnimplementedError(); + + @override + int get disconnects => throw UnimplementedError(); + + @override + DateTime? get lastDisconnectAt => throw UnimplementedError(); +} + +@internal +final class SpinifyMetrics$Mutable extends SpinifyMetrics { + SpinifyMetrics$Mutable(); + + @override + DateTime get timestamp => DateTime.now(); + + @override + final DateTime initializedAt = DateTime.now(); + + @override + int commandId = 1; + + @override + SpinifyState state = SpinifyState$Disconnected(); + + @override + int connects = 0; + + @override + DateTime? lastConnectAt; + + @override + String? reconnectUrl; + + @override + int? reconnectAttempts; + + @override + DateTime? nextReconnectAt; + + @override + int disconnects = 0; + + @override + DateTime? lastDisconnectAt; + + SpinifyMetrics$Immutable freeze() => const SpinifyMetrics$Immutable(); +} diff --git a/lib/src/model/metrics.dart b/lib/src/model/metrics.dart deleted file mode 100644 index e7f5863..0000000 --- a/lib/src/model/metrics.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:meta/meta.dart'; - -import 'state.dart'; - -/// Subscription count -/// - total -/// - unsubscribed -/// - subscribing -/// - subscribed -/// -/// {@category Metrics} -/// {@category Entity} -typedef SpinifySubscriptionCount = ({ - int total, - int unsubscribed, - int subscribing, - int subscribed -}); - -/// {@template metrics} -/// Metrics of Spinify client. -/// {@endtemplate} -/// -/// {@category Metrics} -/// {@category Entity} -@immutable -final class SpinifyMetrics implements Comparable { - /// {@macro metrics} - const SpinifyMetrics({ - required this.timestamp, - required this.initializedAt, - required this.state, - required this.transferred, - required this.received, - required this.reconnects, - required this.subscriptions, - required this.speed, - required this.lastUrl, - required this.lastConnectTime, - required this.lastDisconnectTime, - required this.disconnects, - required this.lastDisconnect, - required this.isRefreshActive, - }); - - /// Timestamp of the metrics. - final DateTime timestamp; - - /// The time when the client was initialized. - final DateTime initializedAt; - - /// The current state of the client. - final SpinifyState state; - - /// The total number of messages & size of bytes sent. - final ({BigInt count, BigInt size}) transferred; - - /// The total number of messages & size of bytes received. - final ({BigInt count, BigInt size}) received; - - /// The total number of times the connection has been re-established. - final ({int successful, int total}) reconnects; - - /// The number of subscriptions. - final ({ - SpinifySubscriptionCount client, - SpinifySubscriptionCount server - }) subscriptions; - - /// The speed of the request/response in milliseconds. - /// - min - minimum speed - /// - avg - average speed - /// - max - maximum speed - final ({int min, int avg, int max}) speed; - - /// The last URL used to connect. - final String? lastUrl; - - /// The time of the last connect. - final DateTime? lastConnectTime; - - /// The time of the last disconnect. - final DateTime? lastDisconnectTime; - - /// The total number of times the connection has been disconnected. - final int disconnects; - - /// The last disconnect reason. - final ({int? code, String? reason})? lastDisconnect; - - /// Is refresh active. - final bool isRefreshActive; - - @override - int compareTo(SpinifyMetrics other) => timestamp.compareTo(other.timestamp); - - /// Convert metrics to JSON. - Map toJson() => { - 'timestamp': timestamp.toIso8601String(), - 'initializedAt': initializedAt.toIso8601String(), - 'lastConnectTime': lastConnectTime?.toIso8601String(), - 'lastDisconnectTime': lastDisconnectTime?.toIso8601String(), - 'state': state.toJson(), - 'lastUrl': lastUrl, - 'reconnects': { - 'successful': reconnects.successful, - 'total': reconnects.total, - }, - 'subscriptions': >{ - 'client': { - 'total': subscriptions.client.total, - 'unsubscribed': subscriptions.client.unsubscribed, - 'subscribing': subscriptions.client.subscribing, - 'subscribed': subscriptions.client.subscribed, - }, - 'server': { - 'total': subscriptions.server.total, - 'unsubscribed': subscriptions.server.unsubscribed, - 'subscribing': subscriptions.server.subscribing, - 'subscribed': subscriptions.server.subscribed, - }, - }, - 'speed': { - 'min': speed.min, - 'avg': speed.avg, - 'max': speed.max, - }, - 'transferred': { - 'count': transferred.count, - 'size': transferred.size, - }, - 'received': { - 'count': received.count, - 'size': received.size, - }, - 'isRefreshActive': isRefreshActive, - 'disconnects': disconnects, - 'lastDisconnect': switch (lastDisconnect) { - (:int? code, :String? reason) => { - 'code': code, - 'reason': reason, - }, - _ => null, - }, - }; - - @override - String toString() => 'SpinifyMetrics{}'; -} diff --git a/lib/src/model/spinify_interface.dart b/lib/src/model/spinify_interface.dart index 0e8e8f6..834e99a 100644 --- a/lib/src/model/spinify_interface.dart +++ b/lib/src/model/spinify_interface.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'channel_push.dart'; import 'config.dart'; import 'history.dart'; -import 'metrics.dart'; +import 'metric.dart'; import 'presence_stats.dart'; import 'pushes_stream.dart'; import 'state.dart'; diff --git a/lib/src/spinify_impl.dart b/lib/src/spinify_impl.dart index e71cc28..e53b5ca 100644 --- a/lib/src/spinify_impl.dart +++ b/lib/src/spinify_impl.dart @@ -6,7 +6,7 @@ import 'model/channel_push.dart'; import 'model/command.dart'; import 'model/config.dart'; import 'model/history.dart'; -import 'model/metrics.dart'; +import 'model/metric.dart'; import 'model/presence_stats.dart'; import 'model/pushes_stream.dart'; import 'model/reply.dart'; @@ -32,8 +32,7 @@ abstract base class SpinifyBase implements ISpinify { } /// Counter for command messages. - int _commandId = 1; - int _getNextCommandId() => _commandId++; + int _getNextCommandId() => _metrics.commandId++; @override bool get isClosed => state.isClosed; @@ -46,6 +45,8 @@ abstract base class SpinifyBase implements ISpinify { late final SpinifyTransportBuilder _createTransport; ISpinifyTransport? _transport; + final SpinifyMetrics$Mutable _metrics = SpinifyMetrics$Mutable(); + /// Client initialization (from constructor). @mustCallSuper void _init() { @@ -105,8 +106,7 @@ abstract base class SpinifyBase implements ISpinify { /// Base mixin for Spinify client state management. base mixin SpinifyStateMixin on SpinifyBase { @override - SpinifyState get state => _state; - SpinifyState _state = SpinifyState$Disconnected(); + SpinifyState get state => _metrics.state; @override late final SpinifyStatesStream states = @@ -118,8 +118,8 @@ base mixin SpinifyStateMixin on SpinifyBase { @nonVirtual void _setState(SpinifyState state) { - final previous = _state; - _statesController.add(_state = state); + final previous = metrics.state; + _statesController.add(_metrics.state = state); config.logger?.call( const SpinifyLogLevel.config(), 'state_changed', @@ -244,7 +244,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { @override Future _onReply(SpinifyReply reply) async { assert( - reply.id >= 0 && reply.id <= _commandId, + reply.id >= 0 && reply.id <= metrics.commandId, 'Reply ID should be greater or equal to 0 ' 'and less or equal than command ID'); if (reply.id case int id when id > 0) { @@ -295,12 +295,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { /// Base mixin for Spinify client connection management (connect & disconnect). base mixin SpinifyConnectionMixin on SpinifyBase, SpinifyCommandMixin, SpinifyStateMixin { - /// Last connected URL. - /// Used for reconnecting after connection lost. - /// If null, then client is not connected or interractively disconnected. - String? _reconnectUrl; Timer? _reconnectTimer; - int? _reconnectAttempt; Completer? _readyCompleter; @protected @@ -315,8 +310,7 @@ base mixin SpinifyConnectionMixin await disconnect(); } on Object {/* ignore */} try { - _setState(SpinifyState$Connecting(url: url)); - _reconnectUrl = url; + _setState(SpinifyState$Connecting(url: _metrics.reconnectUrl = url)); // Create new transport. _transport = await _createTransport(url, config) @@ -487,19 +481,21 @@ base mixin SpinifyConnectionMixin Future _onConnected() async { await super._onConnected(); _tearDownReconnectTimer(); + _metrics.lastConnectAt = DateTime.now(); + _metrics.connects++; } void _setUpReconnectTimer() { _reconnectTimer?.cancel(); - final lastUrl = _reconnectUrl; + final lastUrl = _metrics.reconnectUrl; if (lastUrl == null) return; - final attempt = _reconnectAttempt ?? 0; + final attempt = _metrics.reconnectAttempts ?? 0; final delay = Backoff.nextDelay( attempt, config.connectionRetryInterval.min.inMilliseconds, config.connectionRetryInterval.max.inMilliseconds, ); - _reconnectAttempt = attempt + 1; + _metrics.reconnectAttempts = attempt + 1; if (delay <= Duration.zero) { if (!state.isDisconnected) return; config.logger?.call( @@ -524,7 +520,7 @@ base mixin SpinifyConnectionMixin 'delay': delay, }, ); - /* _nextReconnectionAttempt = DateTime.now().add(delay); */ + _metrics.nextReconnectAt = DateTime.now().add(delay); _reconnectTimer = Timer( delay, () { @@ -546,7 +542,9 @@ base mixin SpinifyConnectionMixin } void _tearDownReconnectTimer() { - _reconnectAttempt = null; + _metrics + ..reconnectAttempts = null + ..nextReconnectAt = null; _reconnectTimer?.cancel(); _reconnectTimer = null; } @@ -559,7 +557,7 @@ base mixin SpinifyConnectionMixin @override Future disconnect() async { - _reconnectUrl = null; + _metrics.reconnectUrl = null; _tearDownReconnectTimer(); if (state.isDisconnected) return Future.value(); await _transport?.disconnect(1000, 'Client disconnecting'); @@ -571,7 +569,9 @@ base mixin SpinifyConnectionMixin _refreshTimer?.cancel(); _transport = null; // Reconnect if that callback called not from disconnect method. - if (_reconnectUrl != null) _setUpReconnectTimer(); + if (_metrics.reconnectUrl != null) _setUpReconnectTimer(); + _metrics.lastDisconnectAt = DateTime.now(); + _metrics.disconnects++; await super._onDisconnected(); } @@ -743,7 +743,7 @@ base mixin SpinifyRPCMixin on SpinifyBase { /// Base mixin for Spinify client metrics management. base mixin SpinifyMetricsMixin on SpinifyBase { @override - SpinifyMetrics get metrics => throw UnimplementedError(); + SpinifyMetrics get metrics => _metrics.freeze(); } /// {@template spinify}