diff --git a/packages/at_commons/CHANGELOG.md b/packages/at_commons/CHANGELOG.md index 34cf4d5a..995a80d3 100644 --- a/packages/at_commons/CHANGELOG.md +++ b/packages/at_commons/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.43 +- feat: Enhanced the monitor verb syntax + 1. added `strict` flag to allow client to request that only regex-matching notifications are sent - + e.g. do not send other 'control' type notifications like the 'statsNotifications' + 2. added `multiplexed` flag to allow client to indicate that + this socket is also being used for request-response interactions ## 3.0.42 - fix: Tightened the validation of 'public' key names. Keys like this: `public:@bob:foo.bar@alice` will now correctly be identified as not being valid. ## 3.0.41 diff --git a/packages/at_commons/analysis_options.yaml b/packages/at_commons/analysis_options.yaml index 1f4622fc..2a72aa66 100644 --- a/packages/at_commons/analysis_options.yaml +++ b/packages/at_commons/analysis_options.yaml @@ -5,9 +5,13 @@ include: package:lints/recommended.yaml # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. # Uncomment to specify additional rules. -# linter: -# rules: -# - camel_case_types +linter: + rules: + camel_case_types : true + unnecessary_string_interpolations : true + await_only_futures : true + unawaited_futures: true + depend_on_referenced_packages : false analyzer: # exclude: diff --git a/packages/at_commons/lib/src/at_constants.dart b/packages/at_commons/lib/src/at_constants.dart index 5ce8df62..9c86eae0 100644 --- a/packages/at_commons/lib/src/at_constants.dart +++ b/packages/at_commons/lib/src/at_constants.dart @@ -37,6 +37,9 @@ const String FROM = 'from'; const String TO = 'to'; const String KEY = 'key'; const String EPOCH_MILLIS = 'epochMillis'; +const String MONITOR_STRICT_MODE = 'strict'; +const String MONITOR_MULTIPLEXED_MODE = 'multiplexed'; +const String MONITOR_REGEX = 'regex'; const String ID = 'id'; const String OPERATION = 'operation'; const String SET_OPERATION = 'setOperation'; diff --git a/packages/at_commons/lib/src/verb/monitor_verb_builder.dart b/packages/at_commons/lib/src/verb/monitor_verb_builder.dart index ab593c69..5cd56b02 100644 --- a/packages/at_commons/lib/src/verb/monitor_verb_builder.dart +++ b/packages/at_commons/lib/src/verb/monitor_verb_builder.dart @@ -1,26 +1,60 @@ +import 'dart:collection'; + +import 'package:at_commons/at_commons.dart'; import 'package:at_commons/src/verb/verb_builder.dart'; /// Monitor builder generates a command that streams incoming notifications from the secondary server to -/// the current client. -/// ``` -/// // Receives all of the notifications -/// var builder = MonitorVerbBuilder(); -/// -/// // Receives notifications for those keys that matches a specific regex -/// var builder = MonitorVerbBuilder()..regex = '.alice'; -/// ``` +/// the current client. See also [VerbSyntax.monitor] class MonitorVerbBuilder implements VerbBuilder { + @Deprecated('not used') bool auth = true; - String? regex; + + String? _regex; + + /// The regular expression to be used when building the monitor command. + /// When [regex] is supplied, server will send notifications which match the regex. If [strict] + /// is true, then only those regex-matching notifications will be sent. If [strict] is false, + /// then other 'control' notifications (e.g. the statsNotification) which don't necessarily + /// match the [regex] will also be sent + String? get regex => _regex; + set regex (String? r) { + if (r != null && r.trim().isEmpty) { + r = null; + } + _regex = r; + } + + /// The timestamp, in milliseconds since epoch, to be used when building the monitor command. + /// When [lastNotificationTime] is supplied, server will only send notifications received at + /// or after that timestamp int? lastNotificationTime; + /// Whether this monitor command is to be built with the 'strict' flag or not. + /// When [strict] is true, server will only send notifications which match the [regex]; no other + /// 'control' notifications such as statsNotifications will be sent on this connection unless + /// they match the [regex] + bool strict = false; + + /// Whether this monitor command is to be built with the 'multiplexed' flag or not. + /// When [multiplexed] is true, the server will understand that this is a connection + /// which the client is using not just for notifications but also for request-response + /// interactions. In this case, the server will only send notifications once there is + /// no request currently in progress + bool multiplexed = false; + @override String buildCommand() { var monitorCommand = 'monitor'; + if (strict) { + monitorCommand += ':strict'; + } + if (multiplexed) { + monitorCommand += ':multiplexed'; + } if (lastNotificationTime != null) { - monitorCommand += ':${lastNotificationTime.toString()}'; + monitorCommand += ':$lastNotificationTime'; } - if (regex != null) { + if (regex != null && regex!.trim().isNotEmpty) { monitorCommand += ' $regex'; } monitorCommand += '\n'; @@ -31,4 +65,42 @@ class MonitorVerbBuilder implements VerbBuilder { bool checkParams() { return true; } + + /// Create a MonitorVerbBuilder from an atProtocol command string + static MonitorVerbBuilder getBuilder(String command) { + if (command != command.trim()) { + throw IllegalArgumentException( + 'Commands may not have leading or trailing whitespace'); + } + HashMap? verbParams = (VerbUtil.getVerbParam(VerbSyntax.monitor, command)); + if (verbParams == null) { + throw InvalidSyntaxException('Command does not match the monitor syntax'); + } + + var builder = MonitorVerbBuilder(); + builder.strict = verbParams[MONITOR_STRICT_MODE] == MONITOR_STRICT_MODE; + builder.multiplexed = verbParams[MONITOR_MULTIPLEXED_MODE] == MONITOR_MULTIPLEXED_MODE; + builder.regex = verbParams[MONITOR_REGEX]; + builder.lastNotificationTime = verbParams[EPOCH_MILLIS] == null ? null : int.parse(verbParams[EPOCH_MILLIS]!); + + return builder; + } + + @override + String toString() { + return 'MonitorVerbBuilder{regex: $regex, lastNotificationTime: $lastNotificationTime, strict: $strict, multiplexed: $multiplexed}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MonitorVerbBuilder && + runtimeType == other.runtimeType && + regex == other.regex && + lastNotificationTime == other.lastNotificationTime && + strict == other.strict && + multiplexed == other.multiplexed; + + @override + int get hashCode => regex.hashCode ^ lastNotificationTime.hashCode ^ strict.hashCode ^ multiplexed.hashCode; } diff --git a/packages/at_commons/lib/src/verb/syntax.dart b/packages/at_commons/lib/src/verb/syntax.dart index df88582e..a91c278c 100644 --- a/packages/at_commons/lib/src/verb/syntax.dart +++ b/packages/at_commons/lib/src/verb/syntax.dart @@ -70,7 +70,26 @@ class VerbSyntax { r':(?(([^:@\s]+)|(privatekey:at_secret)))' r'(@(?[^:@\s]+))?' r'$'; - static const monitor = r'^monitor(:(?\d+))?( (?.+))?$'; + + /// * When 'strict' is set, server will only send notifications which match the regex; no other + /// 'control' notifications such as statsNotifications will be sent on this connection unless + /// they match the regex + /// * When 'multiplexed' is set, the server will understand that this is a connection + /// which the client is using not just for notifications but also for request-response + /// interactions. In this case, the server will only send notifications when there is no request + /// currently being handled + /// * When 'epochMillis' is supplied, server will only send notifications received at or after + /// that timestamp + /// * When 'regex' is supplied, server will send notifications which match the regex. If 'strict' + /// is set, then only those regex-matching notifications will be sent. If 'strict' is not set, + /// then other 'control' notifications (e.g. the statsNotification) which don't necessarily + /// match the regex will also be sent + static const monitor = r'^monitor' + r'(:(?strict))?' + r'(:(?multiplexed))?' + r'(:(?\d+))?' + r'( (?.+))?' + r'$'; static const stream = r'^stream:((?init|send|receive|done|resume))?((@(?[^@:\s]+)))?( ?namespace:(?[\w-]+))?( ?startByte:(?\d+))?( (?[\w-]*))?( (?.* ))?((?\d*))?$'; diff --git a/packages/at_commons/pubspec.yaml b/packages/at_commons/pubspec.yaml index 02e49c25..e621f357 100644 --- a/packages/at_commons/pubspec.yaml +++ b/packages/at_commons/pubspec.yaml @@ -1,6 +1,6 @@ name: at_commons description: A library of Dart and Flutter utility classes that are used across other components of the atPlatform. -version: 3.0.42 +version: 3.0.43 repository: https://github.com/atsign-foundation/at_tools homepage: https://atsign.dev diff --git a/packages/at_commons/test/monitor_verb_builder_test.dart b/packages/at_commons/test/monitor_verb_builder_test.dart new file mode 100644 index 00000000..db2c8856 --- /dev/null +++ b/packages/at_commons/test/monitor_verb_builder_test.dart @@ -0,0 +1,184 @@ +import 'package:at_commons/at_builders.dart'; +import 'package:test/test.dart'; + +void main() { + int nowEpochMillis = DateTime.now().millisecondsSinceEpoch; + group('Monitor builder to command to builder to command round trip tests', () { + test('no params', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder(); + String c1 = b1.buildCommand(); + expect(c1, 'monitor\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('empty regex', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..regex=' \n \t'; + expect(b1.regex, null); + String c1 = b1.buildCommand(); + expect(c1, 'monitor\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('just regex', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..regex=r'\.wavi'; + String c1 = b1.buildCommand(); + expect(c1, 'monitor \\.wavi\n'); + expect(c1, + r'monitor \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('just lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:$nowEpochMillis\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('both regex and lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..regex=r'\.wavi' + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:$nowEpochMillis \\.wavi\n'); + expect(c1, + r'monitor:' + '$nowEpochMillis' + r' \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('just strict', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('just multiplexed', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..multiplexed = true; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:multiplexed\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('strict and regex', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true + ..regex=r'\.wavi'; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict \\.wavi\n'); + expect(c1, r'monitor:strict \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('multiplexed and regex', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..multiplexed = true + ..regex=r'\.wavi'; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:multiplexed \\.wavi\n'); + expect( + c1, + r'monitor:multiplexed \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('strict and lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict:$nowEpochMillis\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('multiplexed and lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..multiplexed = true + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:multiplexed:$nowEpochMillis\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('strict, multiplexed and regex', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true + ..multiplexed = true + ..regex=r'\.wavi'; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict:multiplexed \\.wavi\n'); + expect( + c1, + r'monitor:strict:multiplexed \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('strict, multiplexed and lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true + ..multiplexed = true + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict:multiplexed:$nowEpochMillis\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + test('strict, multiplexed, regex and lastNotificationTime', () { + MonitorVerbBuilder b1 = MonitorVerbBuilder() + ..strict = true + ..multiplexed = true + ..regex=r'\.wavi' + ..lastNotificationTime=nowEpochMillis; + String c1 = b1.buildCommand(); + expect(c1, 'monitor:strict:multiplexed:$nowEpochMillis \\.wavi\n'); + expect( + c1, + r'monitor:strict:multiplexed' + ':$nowEpochMillis' + r' \.wavi' + '\n'); + MonitorVerbBuilder b2 = MonitorVerbBuilder.getBuilder(c1.trim()); + expect(b2, b1); + String c2 = b2.buildCommand(); + expect (c2, c1); + }); + }); +} \ No newline at end of file