diff --git a/.pubnub.yml b/.pubnub.yml index 0dd06ee9..f550efec 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,14 @@ --- changelog: + - date: 2024-12-10 + version: v5.0.0 + changes: + - type: feature + text: "BREAKING CHANGES: Default retry policy for subscription is set to Exponential." + - type: feature + text: "support for customMessageType in subscription, history, publish, signal and files features." + - type: bug + text: "Limiting delay to maximum allowable value for Linear retry policy." - date: 2024-04-15 version: v4.3.4 changes: @@ -452,7 +461,7 @@ supported-platforms: platforms: - "Dart SDK >=2.6.0 <3.0.0" version: "PubNub Dart SDK" -version: "4.3.4" +version: "5.0.0" sdks: - full-name: PubNub Dart SDK diff --git a/acceptance_tests/lib/src/steps/customMessageType/customMessageType_steps.dart b/acceptance_tests/lib/src/steps/customMessageType/customMessageType_steps.dart new file mode 100644 index 00000000..8f95fc6f --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/customMessageType_steps.dart @@ -0,0 +1,24 @@ +import '../../world.dart'; +import 'package:gherkin/gherkin.dart'; + +import 'step_given_keyset.dart'; +import 'step_then_error_response.dart'; +import 'step_then_messagesContainsType.dart'; +import 'step_then_receive.dart'; +import 'step_then_success_response.dart'; +import 'step_when_publish_with_type.dart'; +import 'step_when_sendFile.dart'; +import 'step_when_signal_with_type.dart'; +import 'step_when_subscribe.dart'; + +final List> customMessageTypeSteps = [ + StepGivenTheDemoKeyset(), + StepWhenIPublishWithCustomType(), + StepThenIReceiveSuccessfulResponsePublish(), + StepThenIReceivePublishErrorResponse(), + StepWhenISignalWithCustomType(), + StepWhenISubscribeChannalForCustomMessageType(), + StepThenIReceiveMessagesInSubscriptionResponse(), + StepThenReceivedMessagesHasMessageTypes(), + StepWhenISendFileCustomType(), +]; \ No newline at end of file diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_given_keyset.dart b/acceptance_tests/lib/src/steps/customMessageType/step_given_keyset.dart new file mode 100644 index 00000000..f4027d75 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_given_keyset.dart @@ -0,0 +1,18 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/pubnub.dart'; + +import '../../world.dart'; + +class StepGivenTheDemoKeyset extends GivenWithWorld { + @override + RegExp get pattern => RegExp(r'the demo keyset'); + + @override + Future executeStep() async { + world.keyset = Keyset( + publishKey: 'demo', + subscribeKey: 'demo', + userId: UserId('testCustomType') + ); + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_then_error_response.dart b/acceptance_tests/lib/src/steps/customMessageType/step_then_error_response.dart new file mode 100644 index 00000000..665a59df --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_then_error_response.dart @@ -0,0 +1,21 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:pubnub/pubnub.dart'; +import 'package:test/test.dart'; + +import '../../world.dart'; + +class StepThenIReceivePublishErrorResponse extends ThenWithWorld { + @override + RegExp get pattern => RegExp(r'I receive an error response'); + + @override + Future executeStep() async { + if(world.latestResultType == 'sendFile') { + var result = world.latestResult as PublishFileMessageResult; + this.expect(result.description?.toLowerCase(), contains('invalid_type')); + } else { + var result = world.latestResult as PublishException; + this.expect(result.message.toLowerCase(), contains('invalid_type')); + } + } +} \ No newline at end of file diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_then_messagesContainsType.dart b/acceptance_tests/lib/src/steps/customMessageType/step_then_messagesContainsType.dart new file mode 100644 index 00000000..2b2ff174 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_then_messagesContainsType.dart @@ -0,0 +1,17 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:test/expect.dart'; + +import '../../world.dart'; + +class StepThenReceivedMessagesHasMessageTypes + extends Then2WithWorld { + @override + RegExp get pattern => RegExp(r'response contains messages with {string} and {string} types'); + + @override + Future executeStep(String customMessageTypeOne, String customMessageTypeTwo) async { + world.messages.forEach((message) { + this.expect(message.customMessageType, anyOf([customMessageTypeOne, customMessageTypeTwo])); + }); + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_then_receive.dart b/acceptance_tests/lib/src/steps/customMessageType/step_then_receive.dart new file mode 100644 index 00000000..5b1de637 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_then_receive.dart @@ -0,0 +1,14 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepThenIReceiveMessagesInSubscriptionResponse + extends Then1WithWorld { + @override + RegExp get pattern => RegExp(r'I receive {int} messages in my subscribe response'); + + @override + Future executeStep(int count) async { + expect(world.messages.length, 2); + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_then_success_response.dart b/acceptance_tests/lib/src/steps/customMessageType/step_then_success_response.dart new file mode 100644 index 00000000..1fac61f3 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_then_success_response.dart @@ -0,0 +1,14 @@ +import 'package:gherkin/gherkin.dart'; +import 'package:test/expect.dart'; + +import '../../world.dart'; + +class StepThenIReceiveSuccessfulResponsePublish extends ThenWithWorld { + @override + RegExp get pattern => RegExp(r'I receive a successful response'); + + @override + Future executeStep() async { + this.expect(world.latestResultType, isNotNull); + } +} \ No newline at end of file diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_when_publish_with_type.dart b/acceptance_tests/lib/src/steps/customMessageType/step_when_publish_with_type.dart new file mode 100644 index 00000000..caf4c5a3 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_when_publish_with_type.dart @@ -0,0 +1,24 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepWhenIPublishWithCustomType extends When1WithWorld { + @override + RegExp get pattern => RegExp(r'I publish message with {string} customMessageType'); + + @override + Future executeStep(String customMesageType) async { + try { + world.latestResult = await world.pubnub.publish( + 'test', + 'hello', + keyset: world.keyset, + customMessageType: customMesageType, + ); + world.latestResultType = 'publish'; + } catch (e) { + world.latestResultType = 'publishWithCustomTypeFailure'; + world.latestResult = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_when_sendFile.dart b/acceptance_tests/lib/src/steps/customMessageType/step_when_sendFile.dart new file mode 100644 index 00000000..60922298 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_when_sendFile.dart @@ -0,0 +1,21 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepWhenISendFileCustomType + extends When1WithWorld { + @override + RegExp get pattern => + RegExp(r'I send a file with {string} customMessageType'); + + @override + Future executeStep(String customMesageType) async { + try { + world.latestResult = await world.pubnub.files.sendFile('test', 'helloFile.txt', [12,16], customMessageType: customMesageType); + world.latestResultType = 'sendFile'; + } catch (e) { + world.latestResultType = 'sendFileFailure'; + world.latestResult = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_when_signal_with_type.dart b/acceptance_tests/lib/src/steps/customMessageType/step_when_signal_with_type.dart new file mode 100644 index 00000000..fe06602d --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_when_signal_with_type.dart @@ -0,0 +1,26 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepWhenISignalWithCustomType + extends When1WithWorld { + @override + RegExp get pattern => + RegExp(r'I send a signal with {string} customMessageType'); + + @override + Future executeStep(String customMesageType) async { + try { + world.latestResult = await world.pubnub.signal( + 'test', + 'hello', + keyset: world.keyset, + customMessageType: customMesageType, + ); + world.latestResultType = 'publish'; + } catch (e) { + world.latestResultType = 'publishWithCustomTypeFailure'; + world.latestResult = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/customMessageType/step_when_subscribe.dart b/acceptance_tests/lib/src/steps/customMessageType/step_when_subscribe.dart new file mode 100644 index 00000000..28bdd245 --- /dev/null +++ b/acceptance_tests/lib/src/steps/customMessageType/step_when_subscribe.dart @@ -0,0 +1,27 @@ +import 'package:gherkin/gherkin.dart'; + +import '../../world.dart'; + +class StepWhenISubscribeChannalForCustomMessageType + extends When1WithWorld { + @override + RegExp get pattern => + RegExp(r'I subscribe to {string} channel'); + + @override + Future executeStep(String channel) async { + try { + var subscription = world.pubnub.subscribe(channels: {channel}); + subscription.messages.listen((messageEnvelope) { + world.messages.add(messageEnvelope); + }); + await Future.delayed(Duration(seconds: 2), () { + subscription.dispose(); + }); + world.latestResultType = 'subscription'; + } catch (e) { + world.latestResultType = 'subscriptionFailure'; + world.latestResult = e; + } + } +} diff --git a/acceptance_tests/lib/src/steps/steps.dart b/acceptance_tests/lib/src/steps/steps.dart index 24d70351..0c87061a 100644 --- a/acceptance_tests/lib/src/steps/steps.dart +++ b/acceptance_tests/lib/src/steps/steps.dart @@ -1,3 +1,4 @@ +import 'package:acceptance_tests/src/steps/customMessageType/customMessageType_steps.dart'; import 'package:gherkin/gherkin.dart'; import '../world.dart'; @@ -26,6 +27,7 @@ import 'steps_push.dart'; final List> steps = [ ...cryptoSteps, ...pamv3Steps, + ...customMessageTypeSteps, StepGivenChannel(), StepGivenDemoKeyset(), StepWhenIAddAMessageAction(), diff --git a/acceptance_tests/lib/src/world.dart b/acceptance_tests/lib/src/world.dart index a5ac9b25..9b1b2615 100644 --- a/acceptance_tests/lib/src/world.dart +++ b/acceptance_tests/lib/src/world.dart @@ -43,6 +43,7 @@ class PubNubWorld extends World { ), networking: NetworkingModule(origin: 'localhost:8090', ssl: false), ); + pubnub.keysets.defaultKeyset.fileMessagePublishRetryLimit = 0; } Future cleanup() async { diff --git a/pubnub/CHANGELOG.md b/pubnub/CHANGELOG.md index d29e04a7..4272697a 100644 --- a/pubnub/CHANGELOG.md +++ b/pubnub/CHANGELOG.md @@ -1,3 +1,9 @@ +## v5.0.0 +December 10 2024 + +#### Added +- BREAKING CHANGES: support for customMessageType in subscription, history, publish, signal and files features. + ## v4.3.4 April 15 2024 diff --git a/pubnub/README.md b/pubnub/README.md index f2a577f6..f5393d94 100644 --- a/pubnub/README.md +++ b/pubnub/README.md @@ -14,7 +14,7 @@ To add the package to your Dart or Flutter project, add `pubnub` as a dependency ```yaml dependencies: - pubnub: ^4.3.4 + pubnub: ^5.0.0 ``` After adding the dependency to `pubspec.yaml`, run the `dart pub get` command in the root directory of your project (the same that the `pubspec.yaml` is in). diff --git a/pubnub/lib/src/core/core.dart b/pubnub/lib/src/core/core.dart index e297e35b..4bc11d56 100644 --- a/pubnub/lib/src/core/core.dart +++ b/pubnub/lib/src/core/core.dart @@ -21,7 +21,7 @@ class Core { /// Internal module responsible for supervising. SupervisorModule supervisor = SupervisorModule(); - static String version = '4.3.4'; + static String version = '5.0.0'; Core( {Keyset? defaultKeyset, diff --git a/pubnub/lib/src/core/policies/retry_policy.dart b/pubnub/lib/src/core/policies/retry_policy.dart index a92e1975..6d41231f 100644 --- a/pubnub/lib/src/core/policies/retry_policy.dart +++ b/pubnub/lib/src/core/policies/retry_policy.dart @@ -36,12 +36,13 @@ class LinearRetryPolicy extends RetryPolicy { const LinearRetryPolicy({int? backoff, int? maxRetries, int? maximumDelay}) : backoff = backoff ?? 5, maximumDelay = maximumDelay ?? 60000, - super(maxRetries ?? 5); + super(maxRetries ?? 10); @override Duration getDelay(Fiber fiber) { return Duration( - milliseconds: (fiber.tries * backoff) + Random().nextInt(1000)); + milliseconds: min( + maximumDelay, (fiber.tries * backoff) + Random().nextInt(1000))); } } @@ -53,8 +54,8 @@ class ExponentialRetryPolicy extends RetryPolicy { final int maximumDelay; const ExponentialRetryPolicy({int? maxRetries, int? maximumDelay}) - : maximumDelay = maximumDelay ?? 60000, - super(maxRetries ?? 5); + : maximumDelay = maximumDelay ?? 150000, + super(maxRetries ?? 6); @override Duration getDelay(Fiber fiber) { diff --git a/pubnub/lib/src/dx/_endpoints/files.dart b/pubnub/lib/src/dx/_endpoints/files.dart index 90b11857..53b16209 100644 --- a/pubnub/lib/src/dx/_endpoints/files.dart +++ b/pubnub/lib/src/dx/_endpoints/files.dart @@ -74,9 +74,11 @@ class PublishFileMessageParams extends Parameters { bool? storeMessage; int? ttl; String? meta; + String? customMessageType; PublishFileMessageParams(this.keyset, this.channel, this.message, - {this.storeMessage, this.meta, this.ttl}); + {this.storeMessage, this.meta, this.ttl, this.customMessageType}); + @override Request toRequest() { var pathSegments = [ @@ -98,7 +100,8 @@ class PublishFileMessageParams extends Parameters { 'store': '0', 'uuid': keyset.uuid.value, if (ttl != null) 'ttl': ttl.toString(), - if (meta != null) 'meta': meta + if (meta != null) 'meta': meta, + if (customMessageType != null) 'custom_message_type': customMessageType, }; return Request.get( uri: Uri(pathSegments: pathSegments, queryParameters: queryParameters)); diff --git a/pubnub/lib/src/dx/_endpoints/history.dart b/pubnub/lib/src/dx/_endpoints/history.dart index 6cfb2299..3eec8bc7 100644 --- a/pubnub/lib/src/dx/_endpoints/history.dart +++ b/pubnub/lib/src/dx/_endpoints/history.dart @@ -158,8 +158,19 @@ class BatchHistoryResultEntry { /// for given `message`. PubNubException? error; - BatchHistoryResultEntry._(this.message, this.timetoken, this.uuid, - this.messageType, this.actions, this.meta, this.error); + /// If message has customMessageType, this will contain it. + /// Otherwise, it will be `null`. + String? customMessageType; + + BatchHistoryResultEntry._( + this.message, + this.timetoken, + this.uuid, + this.messageType, + this.actions, + this.meta, + this.error, + this.customMessageType); /// @nodoc factory BatchHistoryResultEntry.fromJson(Map object, @@ -195,7 +206,8 @@ class BatchHistoryResultEntry { MessageTypeExtension.fromInt(object['message_type']), object['actions'], object['meta'] == '' ? null : object['meta'], - error); + error, + object['custom_message_type']); } } diff --git a/pubnub/lib/src/dx/_endpoints/publish.dart b/pubnub/lib/src/dx/_endpoints/publish.dart index 14889764..a36757d1 100644 --- a/pubnub/lib/src/dx/_endpoints/publish.dart +++ b/pubnub/lib/src/dx/_endpoints/publish.dart @@ -9,9 +9,13 @@ class PublishParams extends Parameters { bool? storeMessage; int? ttl; bool? noReplication; + String? customMessageType; PublishParams(this.keyset, this.channel, this.message, - {this.storeMessage, this.ttl, this.noReplication}); + {this.storeMessage, + this.ttl, + this.noReplication, + this.customMessageType}); @override Request toRequest() { @@ -33,6 +37,7 @@ class PublishParams extends Parameters { if (meta != null) 'meta': meta, if (noReplication != null && noReplication == true) 'norep': 'true', if (keyset.authKey != null) 'auth': keyset.authKey, + if (customMessageType != null) 'custom_message_type': customMessageType, 'uuid': keyset.uuid.value, if (ttl != null) 'ttl': ttl.toString() }; diff --git a/pubnub/lib/src/dx/_endpoints/signal.dart b/pubnub/lib/src/dx/_endpoints/signal.dart index 4418ea0e..15af10f4 100644 --- a/pubnub/lib/src/dx/_endpoints/signal.dart +++ b/pubnub/lib/src/dx/_endpoints/signal.dart @@ -5,8 +5,10 @@ class SignalParams extends Parameters { Keyset keyset; String channel; String payload; + String? customMessageType; - SignalParams(this.keyset, this.channel, this.payload); + SignalParams(this.keyset, this.channel, this.payload, + {this.customMessageType}); @override Request toRequest() { @@ -21,6 +23,7 @@ class SignalParams extends Parameters { ]; var queryParameters = { + if (customMessageType != null) 'custom_message_type': customMessageType, if (keyset.authKey != null) 'auth': keyset.authKey, 'uuid': keyset.uuid.value, }; diff --git a/pubnub/lib/src/dx/channel/channel.dart b/pubnub/lib/src/dx/channel/channel.dart index f4058a93..b49f7b2c 100644 --- a/pubnub/lib/src/dx/channel/channel.dart +++ b/pubnub/lib/src/dx/channel/channel.dart @@ -41,13 +41,18 @@ class Channel { /// If set to `0`, message won't expire. /// If unset, expiration will fall back to default. Future publish(dynamic message, - {bool? storeMessage, int? ttl, dynamic meta, bool? fire}) { + {bool? storeMessage, + int? ttl, + dynamic meta, + bool? fire, + String? customMessageType}) { return _core.publish(name, message, storeMessage: storeMessage, ttl: ttl, keyset: _keyset, meta: meta, - fire: fire); + fire: fire, + customMessageType: customMessageType); } /// Returns [PaginatedChannelHistory]. Most useful in infinite list type scenario. diff --git a/pubnub/lib/src/dx/files/files.dart b/pubnub/lib/src/dx/files/files.dart index 56da781c..448257de 100644 --- a/pubnub/lib/src/dx/files/files.dart +++ b/pubnub/lib/src/dx/files/files.dart @@ -60,6 +60,7 @@ class FileDx { dynamic fileMessage, bool? storeFileMessage, int? fileMessageTtl, + String? customMessageType, dynamic fileMessageMeta, Keyset? keyset, String? using}) async { @@ -115,6 +116,7 @@ class FileDx { storeMessage: storeFileMessage, meta: fileMessageMeta, cipherKey: cipherKey, + customMessageType: customMessageType, keyset: keyset, using: using); } catch (e) { @@ -158,6 +160,7 @@ class FileDx { int? ttl, dynamic meta, CipherKey? cipherKey, + String? customMessageType, Keyset? keyset, String? using}) async { keyset ??= _core.keysets[using]; @@ -179,7 +182,10 @@ class FileDx { keyset: keyset, core: _core, params: PublishFileMessageParams(keyset, channel, messagePayload, - storeMessage: storeMessage, ttl: ttl, meta: meta), + storeMessage: storeMessage, + ttl: ttl, + meta: meta, + customMessageType: customMessageType), serialize: (object, [_]) => PublishFileMessageResult.fromJson(object)); } diff --git a/pubnub/lib/src/dx/publish/publish.dart b/pubnub/lib/src/dx/publish/publish.dart index afa72049..1ae9ca36 100644 --- a/pubnub/lib/src/dx/publish/publish.dart +++ b/pubnub/lib/src/dx/publish/publish.dart @@ -42,7 +42,8 @@ mixin PublishDx on Core { Map? meta, bool? storeMessage, int? ttl, - bool? fire}) async { + bool? fire, + String? customMessageType}) async { Ensure(channel).isNotEmpty('channel name'); Ensure(message).isNotNull('message'); @@ -61,7 +62,9 @@ mixin PublishDx on Core { } var params = PublishParams(keyset, channel, payload, - storeMessage: storeMessage, ttl: ttl); + storeMessage: storeMessage, + ttl: ttl, + customMessageType: customMessageType); if (meta != null) { params.meta = await super.parser.encode(meta); diff --git a/pubnub/lib/src/dx/signal/signal.dart b/pubnub/lib/src/dx/signal/signal.dart index d0ef5ad3..65e828d8 100644 --- a/pubnub/lib/src/dx/signal/signal.dart +++ b/pubnub/lib/src/dx/signal/signal.dart @@ -8,12 +8,13 @@ export '../_endpoints/signal.dart'; mixin SignalDx on Core { /// Publishes signal [message] to a [channel]. Future signal(String channel, dynamic message, - {Keyset? keyset, String? using}) async { + {String? customMessageType, Keyset? keyset, String? using}) async { keyset ??= keysets[using]; Ensure(keyset.publishKey).isNotNull('publishKey'); var payload = await super.parser.encode(message); - var params = SignalParams(keyset, channel, payload); + var params = SignalParams(keyset, channel, payload, + customMessageType: customMessageType); return defaultFlow( keyset: keyset, diff --git a/pubnub/lib/src/networking/networking.dart b/pubnub/lib/src/networking/networking.dart index 6db98304..d2677ebf 100644 --- a/pubnub/lib/src/networking/networking.dart +++ b/pubnub/lib/src/networking/networking.dart @@ -12,7 +12,7 @@ class NetworkingModule extends INetworkingModule { /// Retry policy. /// /// If retry policy is null, then retries are not attempted. - final RetryPolicy? retryPolicy; + RetryPolicy? retryPolicy = RetryPolicy.exponential(); /// Origin used for all requests. /// diff --git a/pubnub/lib/src/subscribe/envelope.dart b/pubnub/lib/src/subscribe/envelope.dart index a21f1523..3c6c5dab 100644 --- a/pubnub/lib/src/subscribe/envelope.dart +++ b/pubnub/lib/src/subscribe/envelope.dart @@ -12,6 +12,7 @@ class Envelope extends BaseMessage { final MessageType messageType; final int flags; final UUID uuid; + final String? customMessageType; final Timetoken? originalTimetoken; final int? originalRegion; @@ -37,6 +38,7 @@ class Envelope extends BaseMessage { required this.originalRegion, required this.region, required this.userMeta, + required this.customMessageType, this.error}) : super( content: content, @@ -55,6 +57,7 @@ class Envelope extends BaseMessage { messageType: MessageTypeExtension.fromInt(object['e']), flags: object['f'] as int, uuid: UUID(object['i'] ?? ''), + customMessageType: object['cmt'] as String?, originalTimetoken: object['o'] != null ? Timetoken(BigInt.parse('${object['o']['t']}')) : null, diff --git a/pubnub/pubspec.yaml b/pubnub/pubspec.yaml index d2e396fa..0bd785bb 100644 --- a/pubnub/pubspec.yaml +++ b/pubnub/pubspec.yaml @@ -1,6 +1,6 @@ name: pubnub description: PubNub SDK v5 for Dart lang (with Flutter support) that allows you to create real-time applications -version: 4.3.4 +version: 5.0.0 homepage: https://www.pubnub.com/docs/sdks/dart environment: diff --git a/pubnub/test/unit/dx/channel_test.dart b/pubnub/test/unit/dx/channel_test.dart index dd607a70..c57ab9d0 100644 --- a/pubnub/test/unit/dx/channel_test.dart +++ b/pubnub/test/unit/dx/channel_test.dart @@ -58,7 +58,8 @@ void main() { #storeMessage: null, #ttl: 60, #meta: null, - #fire: null + #fire: null, + #customMessageType: null })); }); });