diff --git a/README.md b/README.md index e0224686b..d49da8972 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,30 @@ await channel.publish(messages: [ ably.Message()..name="event1"..data = {"hello": "world"} ]); ``` +Get realtime history + +```dart +void getHistory([ably.RealtimeHistoryParams params]) async { + var result = await channel.history(params); + + var messages = result.items; //get messages + var hasNextPage = result.hasNext(); //tells whether there are more results + if(hasNextPage){ + result = await result.next(); //fetches next page results + messages = result.items; + } + if(!hasNextPage){ + result = await result.first(); //fetches first page results + messages = result.items; + } +} + +// history with default params +getHistory(); + +// sorted and filtered history +getHistory(ably.RealtimeHistoryParams(direction: 'forwards', limit: 10)); +``` ## Caveats diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java index b72cacdae..e6cb673c4 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java @@ -88,6 +88,8 @@ T decode(Map jsonMap) { new CodecPair<>(self::encodePaginatedResult, null)); put(PlatformConstants.CodecTypes.restHistoryParams, new CodecPair<>(null, self::decodeRestHistoryParams)); + put(PlatformConstants.CodecTypes.realtimeHistoryParams, + new CodecPair<>(null, self::decodeRealtimeHistoryParams)); put(PlatformConstants.CodecTypes.errorInfo, new CodecPair<>(self::encodeErrorInfo, null)); put(PlatformConstants.CodecTypes.message, @@ -324,6 +326,33 @@ private Param[] decodeRestHistoryParams(Map jsonMap) { return params; } + private Param[] decodeRealtimeHistoryParams(Map jsonMap) { + if (jsonMap == null) return null; + Param[] params = new Param[jsonMap.size()]; + int index = 0; + final Object start = jsonMap.get(PlatformConstants.TxRealtimeHistoryParams.start); + final Object end = jsonMap.get(PlatformConstants.TxRealtimeHistoryParams.end); + final Object limit = jsonMap.get(PlatformConstants.TxRealtimeHistoryParams.limit); + final Object direction = jsonMap.get(PlatformConstants.TxRealtimeHistoryParams.direction); + final Object untilAttach = jsonMap.get(PlatformConstants.TxRealtimeHistoryParams.untilAttach); + if(start!=null) { + params[index++] = new Param(PlatformConstants.TxRealtimeHistoryParams.start, readValueAsLong(start)); + } + if(end!=null) { + params[index++] = new Param(PlatformConstants.TxRealtimeHistoryParams.end, readValueAsLong(end)); + } + if(limit!=null) { + params[index++] = new Param(PlatformConstants.TxRealtimeHistoryParams.limit, (Integer) limit); + } + if(direction!=null) { + params[index++] = new Param(PlatformConstants.TxRealtimeHistoryParams.direction, (String) direction); + } + if(untilAttach!=null) { + params[index] = new Param(PlatformConstants.TxRealtimeHistoryParams.untilAttach, (boolean) untilAttach); + } + return params; + } + private Message decodeChannelMessage(Map jsonMap) { if (jsonMap == null) return null; final Message o = new Message(); diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java index aaf3dd32c..97e00f0c7 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java @@ -71,6 +71,7 @@ private AblyMethodCallHandler(final MethodChannel channel, final OnHotRestart li // history _map.put(PlatformConstants.PlatformMethod.restHistory, this::getRestHistory); + _map.put(PlatformConstants.PlatformMethod.realtimeHistory, this::getRealtimeHistory); // paginated results _map.put(PlatformConstants.PlatformMethod.nextPage, this::getNextPage); @@ -223,9 +224,9 @@ private void getRestHistory(@NonNull MethodCall call, @NonNull MethodChannel.Res final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; this.>>ablyDo(message, (ablyLibrary, messageData) -> { final Map map = messageData.message; - final String channelName = (String) map.get(PlatformConstants.TxRestHistoryArguments.channelName); - Param[] params = (Param[]) map.get(PlatformConstants.TxRestHistoryArguments.params); - if(params == null){ + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { params = new Param[0]; } ablyLibrary @@ -426,6 +427,22 @@ public void onError(ErrorInfo reason) { }); } + private void getRealtimeHistory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; + this.>>ablyDo(message, (ablyLibrary, messageData) -> { + final Map map = messageData.message; + final String channelName = (String) map.get(PlatformConstants.TxTransportKeys.channelName); + Param[] params = (Param[]) map.get(PlatformConstants.TxTransportKeys.params); + if (params == null) { + params = new Param[0]; + } + ablyLibrary + .getRealtime(messageData.handle) + .channels.get(channelName) + .historyAsync(params, this.paginatedResponseHandler(result, null)); + }); + } + private void closeRealtime(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { final AblyFlutterMessage message = (AblyFlutterMessage) call.arguments; this.ablyDo(message, (ablyLibrary, realtimeHandle) -> { diff --git a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java index 4cd7e29b9..f8af28b0d 100644 --- a/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java +++ b/android/src/main/java/io/ably/flutter/plugin/generated/PlatformConstants.java @@ -18,6 +18,7 @@ static final public class CodecTypes { public static final byte tokenRequest = (byte) 134; public static final byte paginatedResult = (byte) 135; public static final byte restHistoryParams = (byte) 136; + public static final byte realtimeHistoryParams = (byte) 137; public static final byte errorInfo = (byte) 144; public static final byte connectionStateChange = (byte) 201; public static final byte channelStateChange = (byte) 202; @@ -47,6 +48,11 @@ static final public class PlatformMethod { public static final String firstPage = "firstPage"; } + static final public class TxTransportKeys { + public static final String channelName = "channelName"; + public static final String params = "params"; + } + static final public class TxAblyMessage { public static final String registrationHandle = "registrationHandle"; public static final String type = "type"; @@ -179,16 +185,19 @@ static final public class TxPaginatedResult { public static final String hasNext = "hasNext"; } - static final public class TxRestHistoryArguments { - public static final String channelName = "channelName"; - public static final String params = "params"; + static final public class TxRestHistoryParams { + public static final String start = "start"; + public static final String end = "end"; + public static final String direction = "direction"; + public static final String limit = "limit"; } - static final public class TxRestHistoryParams { + static final public class TxRealtimeHistoryParams { public static final String start = "start"; public static final String end = "end"; public static final String direction = "direction"; public static final String limit = "limit"; + public static final String untilAttach = "untilAttach"; } } diff --git a/bin/codegencontext.dart b/bin/codegencontext.dart index 89e2924e7..fb10b87a2 100644 --- a/bin/codegencontext.dart +++ b/bin/codegencontext.dart @@ -25,6 +25,7 @@ const List> _types = [ {'name': 'tokenRequest', 'value': 134}, {'name': 'paginatedResult', 'value': 135}, {'name': 'restHistoryParams', 'value': 136}, + {'name': 'realtimeHistoryParams', 'value': 137}, {'name': 'errorInfo', 'value': 144}, // Events @@ -79,6 +80,10 @@ const List> _platformMethods = [ ]; const List> _objects = [ + { + 'name': 'TransportKeys', + 'properties': ['channelName', 'params'] + }, { 'name': 'AblyMessage', 'properties': ['registrationHandle', 'type', 'message'] @@ -214,16 +219,22 @@ const List> _objects = [ 'properties': ['items', 'type', 'hasNext'] }, { - 'name': 'RestHistoryArguments', - 'properties': ['channelName', 'params'] + 'name': 'RestHistoryParams', + 'properties': [ + 'start', + 'end', + 'direction', + 'limit', + ] }, { - 'name': 'RestHistoryParams', + 'name': 'RealtimeHistoryParams', 'properties': [ 'start', 'end', 'direction', 'limit', + 'untilAttach', ] } ]; diff --git a/example/lib/main.dart b/example/lib/main.dart index 89835c211..d20a9795a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -37,6 +37,7 @@ class _MyAppState extends State { StreamSubscription _channelMessageSubscription; ably.Message channelMessage; ably.PaginatedResult _restHistory; + ably.PaginatedResult _realtimeHistory; //Storing different message types here to be publishable List messagesToPublish = [ @@ -447,44 +448,101 @@ class _MyAppState extends State { int msgCounter = 1; Widget sendRestMessage() => FlatButton( - onPressed: () async { - print('Sending rest message'); - try { - await _rest.channels - .get('test') - .publish(name: 'Hello', data: 'Flutter $msgCounter'); - print('Rest message sent.'); - setState(() { - ++msgCounter; - }); - } on ably.AblyException catch (e) { - print('Rest message sending failed:: $e :: ${e.errorInfo}'); - } - }, + onPressed: (_rest == null) + ? null + : () async { + print('Sending rest message'); + try { + await _rest.channels + .get('test') + .publish(name: 'Hello', data: 'Flutter $msgCounter'); + print('Rest message sent.'); + setState(() { + ++msgCounter; + }); + } on ably.AblyException catch (e) { + print('Rest message sending failed:: $e :: ${e.errorInfo}'); + } + }, color: Colors.yellow, child: const Text('Publish'), ); Widget getRestChannelHistory() => FlatButton( - onPressed: () async { - final next = _restHistory?.hasNext() ?? false; - print('Rest history: getting ${next ? 'next' : 'first'} page'); - try { - if (_restHistory == null || _restHistory.items.isEmpty) { - final result = await _rest.channels.get('test').history( - ably.RestHistoryParams(direction: 'forwards', limit: 10), - ); - _restHistory = result; - } else if (next) { - _restHistory = await _restHistory.next(); - } else { - _restHistory = await _restHistory.first(); - } - setState(() {}); - } on ably.AblyException catch (e) { - print('failed to get history:: $e :: ${e.errorInfo}'); - } - }, + onPressed: (_rest == null) + ? null + : () async { + final next = _restHistory?.hasNext() ?? false; + print('Rest history: getting ${next ? 'next' : 'first'} page'); + try { + if (_restHistory == null || _restHistory.items.isEmpty) { + final result = await _rest.channels.get('test').history( + ably.RestHistoryParams( + direction: 'forwards', limit: 10)); + _restHistory = result; + } else if (next) { + _restHistory = await _restHistory.next(); + } else { + _restHistory = await _restHistory.first(); + } + setState(() {}); + } on ably.AblyException catch (e) { + print('failed to get history:: $e :: ${e.errorInfo}'); + } + }, + onLongPress: (_rest == null) + ? null + : () async { + final result = await _rest.channels.get('test').history( + ably.RestHistoryParams(direction: 'forwards', limit: 10)); + setState(() { + _restHistory = result; + }); + }, + color: Colors.yellow, + child: const Text('Get history'), + ); + + Widget getRealtimeChannelHistory() => FlatButton( + onPressed: (_realtime == null) + ? null + : () async { + final next = _realtimeHistory?.hasNext() ?? false; + print('Rest history: getting ${next ? 'next' : 'first'} page'); + try { + if (_realtimeHistory == null || + _realtimeHistory.items.isEmpty) { + final result = + await _realtime.channels.get('test-channel').history( + ably.RealtimeHistoryParams( + direction: 'backwards', + limit: 10, + untilAttach: true, + ), + ); + _realtimeHistory = result; + } else if (next) { + _realtimeHistory = await _realtimeHistory.next(); + } else { + _realtimeHistory = await _realtimeHistory.first(); + } + setState(() {}); + } on ably.AblyException catch (e) { + print('failed to get history:: $e :: ${e.errorInfo}'); + } + }, + onLongPress: (_realtime == null) + ? null + : () async { + final result = + await _realtime.channels.get('test-channel').history( + ably.RealtimeHistoryParams( + direction: 'forwards', limit: 10), + ); + setState(() { + _realtimeHistory = result; + }); + }, color: Colors.yellow, child: const Text('Get history'), ); @@ -539,6 +597,12 @@ class _MyAppState extends State { ), Text('Message from channel: ${channelMessage?.data ?? '-'}'), createChannelPublishButton(), + getRealtimeChannelHistory(), + const Text('History'), + ..._realtimeHistory?.items + ?.map((m) => Text('${m.name}:${m.data?.toString()}')) + ?.toList() ?? + [], const Divider(), createRestButton(), Text('Rest: ' diff --git a/ios/Classes/AblyFlutterPlugin.m b/ios/Classes/AblyFlutterPlugin.m index 569c5696e..37f53c694 100644 --- a/ios/Classes/AblyFlutterPlugin.m +++ b/ios/Classes/AblyFlutterPlugin.m @@ -76,8 +76,8 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; AblyFlutter *const ably = [plugin ably]; AblyFlutterMessage *const messageData = message.message; NSMutableDictionary *const _dataMap = messageData.message; - NSString *const channelName = (NSString*)[_dataMap objectForKey: TxRestHistoryArguments_channelName]; - ARTDataQuery *const dataQuery = (ARTDataQuery*)[_dataMap objectForKey: TxRestHistoryArguments_params]; + NSString *const channelName = (NSString*)[_dataMap objectForKey: TxTransportKeys_channelName]; + ARTDataQuery *const dataQuery = (ARTDataQuery*)[_dataMap objectForKey: TxTransportKeys_params]; ARTRest *const client = [ably getRest:messageData.handle]; ARTRestChannel *const channel = [client.channels get:channelName]; const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { @@ -244,6 +244,35 @@ -(void)registerWithCompletionHandler:(FlutterResult)completionHandler; result(nil); }; +static const FlutterHandler _getRealtimeHistory = ^void(AblyFlutterPlugin *const plugin, FlutterMethodCall *const call, const FlutterResult result) { + AblyFlutterMessage *const message = call.arguments; + AblyFlutter *const ably = [plugin ably]; + AblyFlutterMessage *const messageData = message.message; + NSMutableDictionary *const _dataMap = messageData.message; + NSString *const channelName = (NSString*)[_dataMap objectForKey: TxTransportKeys_channelName]; + ARTRealtimeHistoryQuery *const dataQuery = (ARTRealtimeHistoryQuery*)[_dataMap objectForKey: TxTransportKeys_params]; + ARTRealtime *const client = [ably realtimeWithHandle:messageData.handle]; + ARTRealtimeChannel *const channel = [client.channels get:channelName]; + const id cbk = ^(ARTPaginatedResult * _Nullable paginatedResult, ARTErrorInfo * _Nullable error) { + if(error){ + result([ + FlutterError + errorWithCode:[NSString stringWithFormat: @"%ld", (long)error.code] + message:[NSString stringWithFormat:@"Unable to publish message to Ably server; err = %@", [error message]] + details:error + ]); + }else{ + NSNumber *const paginatedResultHandle = [ably setPaginatedResult:paginatedResult handle:nil]; + result([[AblyFlutterMessage alloc] initWithMessage:paginatedResult handle: paginatedResultHandle]); + } + }; + if (dataQuery) { + [channel history:dataQuery callback:cbk error: nil]; + } else { + [channel history:cbk]; + } +}; + @implementation AblyFlutterPlugin { long long _nextRegistration; @@ -304,6 +333,7 @@ -(instancetype)initWithChannel:(FlutterMethodChannel *const)channel streamsChann AblyPlatformMethod_setRealtimeChannelOptions: _setRealtimeChannelOptions, AblyPlatformMethod_publishRealtimeChannelMessage: _publishRealtimeChannelMessage, AblyPlatformMethod_restHistory: _getRestHistory, + AblyPlatformMethod_realtimeHistory: _getRealtimeHistory, AblyPlatformMethod_nextPage: _getNextPage, AblyPlatformMethod_firstPage: _getFirstPage, }; diff --git a/ios/Classes/codec/AblyFlutterReader.m b/ios/Classes/codec/AblyFlutterReader.m index 04d7a66b8..36a8616b0 100644 --- a/ios/Classes/codec/AblyFlutterReader.m +++ b/ios/Classes/codec/AblyFlutterReader.m @@ -34,6 +34,7 @@ + (AblyCodecDecoder) getDecoder:(const NSString*)type { [NSString stringWithFormat:@"%d", tokenDetailsCodecType]: readTokenDetails, [NSString stringWithFormat:@"%d", tokenRequestCodecType]: readTokenRequest, [NSString stringWithFormat:@"%d", restHistoryParamsCodecType]: readRestHistoryParams, + [NSString stringWithFormat:@"%d", realtimeHistoryParamsCodecType]: readRealtimeHistoryParams, }; return [_handlers objectForKey:[NSString stringWithFormat:@"%@", type]]; } @@ -233,4 +234,26 @@ +(ARTTokenParams *)tokenParamsFromDictionary: (NSDictionary *) dictionary { return o; }; +static AblyCodecDecoder readRealtimeHistoryParams = ^ARTRealtimeHistoryQuery*(NSDictionary *const dictionary) { + ARTRealtimeHistoryQuery *const o = [ARTRealtimeHistoryQuery new]; + ON_VALUE(^(const id value) { + o.start = [NSDate dateWithTimeIntervalSince1970:[value doubleValue]/1000]; + }, dictionary, TxRealtimeHistoryParams_start); + ON_VALUE(^(const id value) { + o.end = [NSDate dateWithTimeIntervalSince1970:[value doubleValue]/1000]; + }, dictionary, TxRealtimeHistoryParams_end); + ON_VALUE(^(NSString const *value) { + o.limit = [value integerValue]; + }, dictionary, TxRealtimeHistoryParams_limit); + ON_VALUE(^(NSString const *value) { + if([@"forwards" isEqual: value]){ + o.direction = ARTQueryDirectionForwards; + } else { + o.direction = ARTQueryDirectionBackwards; + } + }, dictionary, TxRealtimeHistoryParams_direction); + READ_VALUE(o, untilAttach, dictionary, TxRealtimeHistoryParams_untilAttach); + return o; +}; + @end diff --git a/ios/Classes/codec/AblyPlatformConstants.h b/ios/Classes/codec/AblyPlatformConstants.h index 3999cc0e0..80d59e7c8 100644 --- a/ios/Classes/codec/AblyPlatformConstants.h +++ b/ios/Classes/codec/AblyPlatformConstants.h @@ -15,6 +15,7 @@ typedef NS_ENUM(UInt8, _Value) { tokenRequestCodecType = 134, paginatedResultCodecType = 135, restHistoryParamsCodecType = 136, + realtimeHistoryParamsCodecType = 137, errorInfoCodecType = 144, connectionStateChangeCodecType = 201, channelStateChangeCodecType = 202, @@ -45,6 +46,11 @@ extern NSString *const AblyPlatformMethod_nextPage; extern NSString *const AblyPlatformMethod_firstPage; @end +@interface TxTransportKeys : NSObject +extern NSString *const TxTransportKeys_channelName; +extern NSString *const TxTransportKeys_params; +@end + @interface TxAblyMessage : NSObject extern NSString *const TxAblyMessage_registrationHandle; extern NSString *const TxAblyMessage_type; @@ -177,14 +183,17 @@ extern NSString *const TxPaginatedResult_type; extern NSString *const TxPaginatedResult_hasNext; @end -@interface TxRestHistoryArguments : NSObject -extern NSString *const TxRestHistoryArguments_channelName; -extern NSString *const TxRestHistoryArguments_params; -@end - @interface TxRestHistoryParams : NSObject extern NSString *const TxRestHistoryParams_start; extern NSString *const TxRestHistoryParams_end; extern NSString *const TxRestHistoryParams_direction; extern NSString *const TxRestHistoryParams_limit; @end + +@interface TxRealtimeHistoryParams : NSObject +extern NSString *const TxRealtimeHistoryParams_start; +extern NSString *const TxRealtimeHistoryParams_end; +extern NSString *const TxRealtimeHistoryParams_direction; +extern NSString *const TxRealtimeHistoryParams_limit; +extern NSString *const TxRealtimeHistoryParams_untilAttach; +@end diff --git a/ios/Classes/codec/AblyPlatformConstants.m b/ios/Classes/codec/AblyPlatformConstants.m index dfa01daf5..a5cce5fba 100644 --- a/ios/Classes/codec/AblyPlatformConstants.m +++ b/ios/Classes/codec/AblyPlatformConstants.m @@ -30,6 +30,11 @@ @implementation AblyPlatformMethod NSString *const AblyPlatformMethod_firstPage= @"firstPage"; @end +@implementation TxTransportKeys +NSString *const TxTransportKeys_channelName = @"channelName"; +NSString *const TxTransportKeys_params = @"params"; +@end + @implementation TxAblyMessage NSString *const TxAblyMessage_registrationHandle = @"registrationHandle"; NSString *const TxAblyMessage_type = @"type"; @@ -162,14 +167,17 @@ @implementation TxPaginatedResult NSString *const TxPaginatedResult_hasNext = @"hasNext"; @end -@implementation TxRestHistoryArguments -NSString *const TxRestHistoryArguments_channelName = @"channelName"; -NSString *const TxRestHistoryArguments_params = @"params"; -@end - @implementation TxRestHistoryParams NSString *const TxRestHistoryParams_start = @"start"; NSString *const TxRestHistoryParams_end = @"end"; NSString *const TxRestHistoryParams_direction = @"direction"; NSString *const TxRestHistoryParams_limit = @"limit"; @end + +@implementation TxRealtimeHistoryParams +NSString *const TxRealtimeHistoryParams_start = @"start"; +NSString *const TxRealtimeHistoryParams_end = @"end"; +NSString *const TxRealtimeHistoryParams_direction = @"direction"; +NSString *const TxRealtimeHistoryParams_limit = @"limit"; +NSString *const TxRealtimeHistoryParams_untilAttach = @"untilAttach"; +@end diff --git a/lib/src/codec.dart b/lib/src/codec.dart index f200601bc..c09b6cf07 100644 --- a/lib/src/codec.dart +++ b/lib/src/codec.dart @@ -64,6 +64,8 @@ class Codec extends StandardMessageCodec { _CodecPair(encodeTokenRequest, null), CodecTypes.paginatedResult: _CodecPair(null, decodePaginatedResult), + CodecTypes.realtimeHistoryParams: + _CodecPair(encodeRealtimeHistoryParams, null), CodecTypes.restHistoryParams: _CodecPair(encodeRestHistoryParams, null), @@ -101,6 +103,8 @@ class Codec extends StandardMessageCodec { return CodecTypes.tokenRequest; } else if (value is Message) { return CodecTypes.message; + } else if (value is RealtimeHistoryParams) { + return CodecTypes.realtimeHistoryParams; } else if (value is RestHistoryParams) { return CodecTypes.restHistoryParams; } else if (value is ErrorInfo) { @@ -260,6 +264,20 @@ class Codec extends StandardMessageCodec { return jsonMap; } + Map encodeRealtimeHistoryParams( + final RealtimeHistoryParams v) { + if (v == null) return null; + final jsonMap = {}; + writeToJson(jsonMap, TxRealtimeHistoryParams.start, + v.start?.millisecondsSinceEpoch); + writeToJson( + jsonMap, TxRealtimeHistoryParams.end, v.end?.millisecondsSinceEpoch); + writeToJson(jsonMap, TxRealtimeHistoryParams.direction, v.direction); + writeToJson(jsonMap, TxRealtimeHistoryParams.limit, v.limit); + writeToJson(jsonMap, TxRealtimeHistoryParams.untilAttach, v.untilAttach); + return jsonMap; + } + /// Encodes [AblyMessage] to a Map /// returns null of passed value [v] is null Map encodeAblyMessage(final AblyMessage v) { @@ -323,7 +341,7 @@ class Codec extends StandardMessageCodec { if (jsonMap == null) return null; return ClientOptions() - // AuthOptions (super class of ClientOptions) + // AuthOptions (super class of ClientOptions) ..authUrl = readFromJson(jsonMap, TxClientOptions.authUrl) ..authMethod = readFromJson(jsonMap, TxClientOptions.authMethod) @@ -337,10 +355,10 @@ class Codec extends StandardMessageCodec { ..queryTime = readFromJson(jsonMap, TxClientOptions.queryTime) ..useTokenAuth = readFromJson(jsonMap, TxClientOptions.useTokenAuth) - // ClientOptions + // ClientOptions ..clientId = readFromJson(jsonMap, TxClientOptions.clientId) ..logLevel = readFromJson(jsonMap, TxClientOptions.logLevel) - //TODO handle logHandler + //TODO handle logHandler ..tls = readFromJson(jsonMap, TxClientOptions.tls) ..restHost = readFromJson(jsonMap, TxClientOptions.restHost) ..realtimeHost = @@ -561,8 +579,8 @@ class Codec extends StandardMessageCodec { Message decodeChannelMessage(Map jsonMap) { if (jsonMap == null) return null; final message = Message( - name: readFromJson(jsonMap, TxMessage.name), - clientId: readFromJson(jsonMap, TxMessage.clientId), + name: readFromJson(jsonMap, TxMessage.name), + clientId: readFromJson(jsonMap, TxMessage.clientId), data: readFromJson(jsonMap, TxMessage.data)) ..id = readFromJson(jsonMap, TxMessage.id) ..connectionId = readFromJson(jsonMap, TxMessage.connectionId) diff --git a/lib/src/generated/platformconstants.dart b/lib/src/generated/platformconstants.dart index 25a670b26..8ce05d6f2 100644 --- a/lib/src/generated/platformconstants.dart +++ b/lib/src/generated/platformconstants.dart @@ -13,6 +13,7 @@ class CodecTypes { static const int tokenRequest = 134; static const int paginatedResult = 135; static const int restHistoryParams = 136; + static const int realtimeHistoryParams = 137; static const int errorInfo = 144; static const int connectionStateChange = 201; static const int channelStateChange = 202; @@ -45,6 +46,11 @@ class PlatformMethod { static const String firstPage = 'firstPage'; } +class TxTransportKeys { + static const String channelName = 'channelName'; + static const String params = 'params'; +} + class TxAblyMessage { static const String registrationHandle = 'registrationHandle'; static const String type = 'type'; @@ -177,14 +183,17 @@ class TxPaginatedResult { static const String hasNext = 'hasNext'; } -class TxRestHistoryArguments { - static const String channelName = 'channelName'; - static const String params = 'params'; +class TxRestHistoryParams { + static const String start = 'start'; + static const String end = 'end'; + static const String direction = 'direction'; + static const String limit = 'limit'; } -class TxRestHistoryParams { +class TxRealtimeHistoryParams { static const String start = 'start'; static const String end = 'end'; static const String direction = 'direction'; static const String limit = 'limit'; + static const String untilAttach = 'untilAttach'; } diff --git a/lib/src/impl/realtime/channels.dart b/lib/src/impl/realtime/channels.dart index 43b12f828..9b82aa05f 100644 --- a/lib/src/impl/realtime/channels.dart +++ b/lib/src/impl/realtime/channels.dart @@ -7,6 +7,7 @@ import 'package:pedantic/pedantic.dart'; import '../../../ably_flutter_plugin.dart'; import '../../spec/push/channels.dart'; import '../../spec/spec.dart' as spec; +import '../message.dart'; import '../platform_object.dart'; import 'realtime.dart'; @@ -40,8 +41,12 @@ class RealtimePlatformChannel extends PlatformObject @override Future> history([ spec.RealtimeHistoryParams params, - ]) { - throw UnimplementedError(); + ]) async { + final message = await invoke(PlatformMethod.realtimeHistory, { + TxTransportKeys.channelName: name, + if (params != null) TxTransportKeys.params: params + }); + return PaginatedResult.fromAblyMessage(message); } Map __payload; diff --git a/lib/src/impl/rest/channels.dart b/lib/src/impl/rest/channels.dart index 4610c3eb5..48d9fb546 100644 --- a/lib/src/impl/rest/channels.dart +++ b/lib/src/impl/rest/channels.dart @@ -39,8 +39,8 @@ class RestPlatformChannel extends PlatformObject implements spec.RestChannel { spec.RestHistoryParams params, ]) async { final message = await invoke(PlatformMethod.restHistory, { - TxRestHistoryArguments.channelName: name, - if (params != null) TxRestHistoryArguments.params: params + TxTransportKeys.channelName: name, + if (params != null) TxTransportKeys.params: params }); return PaginatedResult.fromAblyMessage(message); } diff --git a/lib/src/spec/common.dart b/lib/src/spec/common.dart index ab7863c40..d851a1bbb 100644 --- a/lib/src/spec/common.dart +++ b/lib/src/spec/common.dart @@ -1,3 +1,5 @@ +import 'package:ably_flutter_plugin/src/spec/spec.dart'; + import 'auth.dart'; import 'enums.dart'; import 'rest/ably_base.dart'; @@ -236,13 +238,13 @@ class TokenRequest { /// spec: https://docs.ably.io/client-lib-development-guide/features/#TE4 int ttl; - TokenRequest( - {this.keyName, - this.nonce, - this.clientId, - this.mac, - this.capability, - this.timestamp, + TokenRequest({ + this.keyName, + this.nonce, + this.clientId, + this.mac, + this.capability, + this.timestamp, this.ttl, }); @@ -316,8 +318,8 @@ class RealtimeHistoryParams extends RestHistoryParams { RealtimeHistoryParams({ DateTime start, DateTime end, - String direction, - int limit, + String direction = 'backwards', + int limit = 100, this.untilAttach, }) : super( start: start, diff --git a/lib/src/spec/message.dart b/lib/src/spec/message.dart index b4dea59ba..632e67cce 100644 --- a/lib/src/spec/message.dart +++ b/lib/src/spec/message.dart @@ -32,7 +32,7 @@ class Message { String toString() => 'Message id=$id timestamp=$timestamp clientId=$clientId' ' connectionId=$connectionId encoding=$encoding name=$name' ' data=$data extras=$extras'; - } +} abstract class MessageStatic { //TODO why is this class required? diff --git a/lib/src/spec/realtime/presence.dart b/lib/src/spec/realtime/presence.dart index 61c1b8bee..0c679c1cf 100644 --- a/lib/src/spec/realtime/presence.dart +++ b/lib/src/spec/realtime/presence.dart @@ -13,17 +13,17 @@ abstract class RealtimePresence { Future subscribe({ PresenceAction action, - List actions, + List actions, EventListener listener, //TODO(tiholic) check if this is the expected type for listener - }); + }); void unsubscribe({ PresenceAction action, - List actions, + List actions, EventListener listener, //TODO(tiholic) check if this is the expected type for listener - }); + }); Future enter([Object data]); diff --git a/lib/src/spec/rest/channels.dart b/lib/src/spec/rest/channels.dart index e60ea2487..0946f3d63 100644 --- a/lib/src/spec/rest/channels.dart +++ b/lib/src/spec/rest/channels.dart @@ -22,6 +22,7 @@ abstract class RestChannel { Presence presence; Future> history([RestHistoryParams params]); + Future publish({ Message message, List messages, diff --git a/test_integration/lib/test/realtime_history_test.dart b/test_integration/lib/test/realtime_history_test.dart new file mode 100644 index 000000000..532fbd3ae --- /dev/null +++ b/test_integration/lib/test/realtime_history_test.dart @@ -0,0 +1,98 @@ +import 'package:ably_flutter_plugin/ably_flutter_plugin.dart'; + +import '../test_dispatcher.dart'; +import 'app_key_provision_helper.dart'; +import 'encoders.dart'; +import 'realtime_publish_test.dart'; +import 'test_widget_abstract.dart'; + +class RealtimeHistoryTest extends TestWidget { + const RealtimeHistoryTest(TestDispatcherState dispatcher) : super(dispatcher); + + @override + TestWidgetState createState() => RealtimeHistoryTestState(); +} + +class RealtimeHistoryTestState extends TestWidgetState { + @override + Future test() async { + widget.dispatcher.reportLog('init start'); + final appKey = await provision('sandbox-'); + final logMessages = >[]; + + final realtime = Realtime( + options: ClientOptions.fromKey(appKey.toString()) + ..environment = 'sandbox' + ..clientId = 'someClientId' + ..logLevel = LogLevel.verbose + ..logHandler = + ({msg, exception}) => logMessages.add([msg, exception.toString()]), + ); + final channel = realtime.channels.get('test'); + await realtimeMessagesPublishUtil(channel); + + final paginatedResult = await channel.history(); + final historyDefault = await _history(channel); + await Future.delayed(const Duration(seconds: 2)); + + final historyLimit4 = + await _history(channel, RealtimeHistoryParams(limit: 4)); + await Future.delayed(const Duration(seconds: 2)); + + final historyLimit2 = + await _history(channel, RealtimeHistoryParams(limit: 2)); + await Future.delayed(const Duration(seconds: 2)); + + final historyForwardLimit4 = await _history( + channel, RealtimeHistoryParams(direction: 'forwards', limit: 4)); + await Future.delayed(const Duration(seconds: 2)); + + final time1 = DateTime.now(); + //TODO(tiholic) iOS fails without this delay + // - timestamp on message retrieved from history + // is earlier than expected when ran in CI + await Future.delayed(const Duration(seconds: 2)); + await channel.publish(name: 'history', data: 'test'); + //TODO(tiholic) understand why tests fail without this delay + await Future.delayed(const Duration(seconds: 2)); + + + final time2 = DateTime.now(); + await Future.delayed(const Duration(seconds: 2)); + await channel.publish(name: 'history', data: 'test2'); + await Future.delayed(const Duration(seconds: 2)); + + final historyWithStart = + await _history(channel, RealtimeHistoryParams(start: time1)); + final historyWithStartAndEnd = await _history( + channel, RealtimeHistoryParams(start: time1, end: time2)); + final historyAll = await _history(channel); + widget.dispatcher.reportTestCompletion({ + 'handle': await realtime.handle, + 'paginatedResult': encodePaginatedResult(paginatedResult, encodeMessage), + 'historyDefault': historyDefault, + 'historyLimit4': historyLimit4, + 'historyLimit2': historyLimit2, + 'historyForwardLimit4': historyForwardLimit4, + 'historyWithStart': historyWithStart, + 'historyWithStartAndEnd': historyWithStartAndEnd, + 'historyAll': historyAll, + 't1': time1.toIso8601String(), + 't2': time2.toIso8601String(), + 'log': logMessages, + }); + } + + Future>> _history( + RealtimeChannel channel, [ + RealtimeHistoryParams params, + ]) async { + var results = await channel.history(params); + final messages = encodeList(results.items, encodeMessage); + while (results.hasNext()) { + results = await results.next(); + messages.addAll(encodeList(results.items, encodeMessage)); + } + return messages; + } +} diff --git a/test_integration/lib/test/realtime_subscribe.dart b/test_integration/lib/test/realtime_subscribe.dart index b105ad180..33ac4be89 100644 --- a/test_integration/lib/test/realtime_subscribe.dart +++ b/test_integration/lib/test/realtime_subscribe.dart @@ -2,9 +2,9 @@ import 'package:ably_flutter_plugin/ably_flutter_plugin.dart'; import '../test_dispatcher.dart'; import 'app_key_provision_helper.dart'; +import 'encoders.dart'; import 'realtime_publish_test.dart'; import 'test_widget_abstract.dart'; -import 'encoders.dart'; class RealtimeSubscribeTest extends TestWidget { const RealtimeSubscribeTest(TestDispatcherState dispatcher) diff --git a/test_integration/lib/test/test_factory.dart b/test_integration/lib/test/test_factory.dart index 642d78058..6c4086a5c 100644 --- a/test_integration/lib/test/test_factory.dart +++ b/test_integration/lib/test/test_factory.dart @@ -2,6 +2,7 @@ import '../test_dispatcher.dart'; import 'app_key_provision_test.dart'; import 'platform_and_ably_version_test.dart'; import 'realtime_events_test.dart'; +import 'realtime_history_test.dart'; import 'realtime_publish_test.dart'; import 'realtime_publish_with_auth_callback_test.dart'; import 'realtime_subscribe.dart'; @@ -24,6 +25,7 @@ final testFactory = { TestName.restHistory: (d) => RestHistoryTest(d), TestName.restPublishWithAuthCallback: (d) => RestPublishWithAuthCallbackTest(d), + TestName.realtimeHistory: (d) => RealtimeHistoryTest(d), TestName.testHelperFlutterErrorTest: (d) => TestHelperFlutterErrorTest(d), TestName.testHelperUnhandledExceptionTest: (d) => TestHelperUnhandledExceptionTest(d), diff --git a/test_integration/lib/test/test_names.dart b/test_integration/lib/test/test_names.dart index 41981a4c8..085bc83e1 100644 --- a/test_integration/lib/test/test_names.dart +++ b/test_integration/lib/test/test_names.dart @@ -19,6 +19,8 @@ class TestName { static const String realtimePublish = 'realtimePublish'; static const String realtimeEvents = 'realtimeEvents'; static const String realtimeSubscribe = 'realtimeSubscribe'; + static const String realtimeHistory = 'realtimeHistory'; static const String realtimePublishWithAuthCallback = 'realtimePublishWithAuthCallback'; + // TODO(tiholic) handle realtimeHistoryWithAuthCallback } diff --git a/test_integration/test_driver/test_implementation/realtime_tests.dart b/test_integration/test_driver/test_implementation/realtime_tests.dart index 411841a3e..c9a7afc7e 100644 --- a/test_integration/test_driver/test_implementation/realtime_tests.dart +++ b/test_integration/test_driver/test_implementation/realtime_tests.dart @@ -231,3 +231,56 @@ Future testRealtimePublishWithAuthCallback(FlutterDriver driver) async { expect(response.payload['authCallbackInvoked'], isTrue); } + +Future testRealtimeHistory(FlutterDriver driver) async { + const message = TestControlMessage(TestName.realtimeHistory); + + final response = await getTestResponse(driver, message); + + expect(response.testName, message.testName); + + expect(response.payload['handle'], isA()); + expect(response.payload['handle'], greaterThan(0)); + + final paginatedResult = + response.payload['paginatedResult'] as Map; + + List> transform(items) => + List.from(items as List).map((t) => t as Map).toList(); + + final historyDefault = transform(response.payload['historyDefault']); + final historyLimit4 = transform(response.payload['historyLimit4']); + final historyLimit2 = transform(response.payload['historyLimit2']); + final historyForwardLimit4 = + transform(response.payload['historyForwardLimit4']); + final historyWithStart = transform(response.payload['historyWithStart']); + final historyWithStartAndEnd = + transform(response.payload['historyWithStartAndEnd']); + + expect(paginatedResult['hasNext'], false); + expect(paginatedResult['isLast'], true); + expect(paginatedResult['items'], isA()); + + expect(historyDefault.length, equals(8)); + expect(historyLimit4.length, equals(8)); + expect(historyLimit2.length, equals(8)); + expect(historyForwardLimit4.length, equals(8)); + expect(historyWithStart.length, equals(2)); + expect(historyWithStartAndEnd.length, equals(1)); + + testAllPublishedMessages(historyDefault.reversed.toList()); + testAllPublishedMessages(historyLimit4.reversed.toList()); + testAllPublishedMessages(historyLimit2.reversed.toList()); + testAllPublishedMessages(historyForwardLimit4); + + // start and no-end test (backward) + expect(historyWithStart[0]['name'], equals('history')); + expect(historyWithStart[0]['data'], equals('test2')); + + expect(historyWithStart[1]['name'], equals('history')); + expect(historyWithStart[1]['data'], equals('test')); + + // start and end test + expect(historyWithStartAndEnd[0]['name'], equals('history')); + expect(historyWithStartAndEnd[0]['data'], equals('test')); +} diff --git a/test_integration/test_driver/tests_config.dart b/test_integration/test_driver/tests_config.dart index 66cf964a2..724cf1305 100644 --- a/test_integration/test_driver/tests_config.dart +++ b/test_integration/test_driver/tests_config.dart @@ -24,6 +24,7 @@ final _tests = >{ 'should publish': testRealtimePublish, 'should subscribe to connection and channel': testRealtimeEvents, 'should subscribe to messages': testRealtimeSubscribe, + 'should retrieve history': testRealtimeHistory, 'should publish with authCallback': testRealtimePublishWithAuthCallback, }, // FlutterError seems to break the test app and needs to be run last