From 2ad0f1608cc8106f6e2a91fb11875a50dce9b352 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sat, 21 Sep 2024 00:54:06 +0200 Subject: [PATCH] Added `attachOnSubscribe` to `ARTRealtimeChannelOptions` and updated tests (RTL7g, RTL7h, RTP6d, RTP6e). --- Source/ARTRealtimeChannel.m | 8 ++- Source/ARTRealtimeChannelOptions.m | 28 +++++++++ Source/ARTRealtimePresence.m | 9 ++- Source/include/Ably/ARTRealtimeChannel.h | 12 ++-- .../include/Ably/ARTRealtimeChannelOptions.h | 5 ++ Test/Tests/RealtimeClientChannelTests.swift | 63 +++++++++++++++++-- Test/Tests/RealtimeClientPresenceTests.swift | 57 +++++++++++++++-- 7 files changed, 166 insertions(+), 16 deletions(-) diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 189b14eb9..31147a7bd 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -435,12 +435,16 @@ - (ARTEventListener *)_subscribe:(nullable NSString *)name onAttach:(nullable AR __block ARTEventListener *listener = nil; dispatch_sync(_queue, ^{ + ARTRealtimeChannelOptions *options = self.getOptions_nosync; + BOOL attachOnSubscribe = options != nil ? options.attachOnSubscribe : true; if (self.state_nosync == ARTRealtimeChannelFailed) { - if (onAttach) onAttach([ARTErrorInfo createWithCode:ARTErrorChannelOperationFailedInvalidState message:@"attempted to subscribe while channel is in FAILED state."]); + if (onAttach && attachOnSubscribe) { // RTL7h + onAttach([ARTErrorInfo createWithCode:ARTErrorChannelOperationFailedInvalidState message:@"attempted to subscribe while channel is in FAILED state."]); + } ARTLogWarn(self.logger, @"R:%p C:%p (%@) subscribe of '%@' has been ignored (attempted to subscribe while channel is in FAILED state)", self->_realtime, self, self.name, name == nil ? @"all" : name); return; } - if (self.shouldAttach) { // RTL7c + if (self.shouldAttach && attachOnSubscribe) { // RTL7g [self _attach:onAttach]; } listener = name == nil ? [self.messagesEventEmitter on:cb] : [self.messagesEventEmitter on:name callback:cb]; diff --git a/Source/ARTRealtimeChannelOptions.m b/Source/ARTRealtimeChannelOptions.m index a65606e96..ad08ed8fa 100644 --- a/Source/ARTRealtimeChannelOptions.m +++ b/Source/ARTRealtimeChannelOptions.m @@ -4,6 +4,21 @@ @implementation ARTRealtimeChannelOptions { NSStringDictionary *_params; ARTChannelMode _modes; + BOOL _attachOnSubscribe; +} + +- (instancetype)init { + if (self = [super init]) { + _attachOnSubscribe = true; + } + return self; +} + +- (instancetype)initWithCipher:(id)cipherParams { + if (self = [super initWithCipher:cipherParams]) { + _attachOnSubscribe = true; + } + return self; } - (NSStringDictionary *)params { @@ -32,4 +47,17 @@ - (void)setModes:(ARTChannelMode)modes { _modes = modes; } +- (BOOL)attachOnSubscribe { + return _attachOnSubscribe; +} + +- (void)setAttachOnSubscribe:(BOOL)value { + if (self.isFrozen) { + @throw [NSException exceptionWithName:NSObjectInaccessibleException + reason:[NSString stringWithFormat:@"%@: You can't change options after you've passed it to receiver.", self.class] + userInfo:nil]; + } + _attachOnSubscribe = value; +} + @end diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 8d27e63dc..aa5770e8c 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -16,6 +16,7 @@ #import "ARTProtocolMessage+Private.h" #import "ARTEventEmitter+Private.h" #import "ARTClientOptions.h" +#import "ARTRealtimeChannelOptions.h" #pragma mark - ARTRealtimePresenceQuery @@ -497,12 +498,16 @@ - (ARTEventListener *)_subscribe:(ARTPresenceAction)action onAttach:(nullable AR __block ARTEventListener *listener = nil; dispatch_sync(_queue, ^{ + ARTRealtimeChannelOptions *options = self->_channel.getOptions_nosync; + BOOL attachOnSubscribe = options != nil ? options.attachOnSubscribe : true; if (self->_channel.state_nosync == ARTRealtimeChannelFailed) { - if (onAttach) onAttach([ARTErrorInfo createWithCode:ARTErrorChannelOperationFailedInvalidState message:@"attempted to subscribe while channel is in Failed state."]); + if (onAttach && attachOnSubscribe) { // RTL7h + onAttach([ARTErrorInfo createWithCode:ARTErrorChannelOperationFailedInvalidState message:@"attempted to subscribe while channel is in Failed state."]); + } ARTLogWarn(self.logger, @"R:%p C:%p (%@) presence subscribe to '%@' action(s) has been ignored (attempted to subscribe while channel is in FAILED state)", self->_realtime, self->_channel, self->_channel.name, ARTPresenceActionToStr(action)); return; } - if (self->_channel.shouldAttach) { // RTP6c + if (self->_channel.shouldAttach && attachOnSubscribe) { // RTP6c [self->_channel _attach:onAttach]; } listener = action == ARTPresenceActionAll ? [_eventEmitter on:cb] : [_eventEmitter on:[ARTEvent newWithPresenceAction:action] callback:cb]; diff --git a/Source/include/Ably/ARTRealtimeChannel.h b/Source/include/Ably/ARTRealtimeChannel.h index 3ec95f04b..6ed068b9c 100644 --- a/Source/include/Ably/ARTRealtimeChannel.h +++ b/Source/include/Ably/ARTRealtimeChannel.h @@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)attach; /** - * Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using `-[ARTRealtimeChannelProtocol subscribe:]`. Any resulting channel state change will be emitted to any listeners registered using the `-[ARTEventEmitter on:]` or `-[ARTEventEmitter once:]` methods. A callback may optionally be passed in to this call to be notified of success or failure of the operation. As a convenience, `attach:` is called implicitly if `-[ARTRealtimeChannelProtocol subscribe:]` for the channel is called, or `-[ARTRealtimePresenceProtocol enter:]` or `-[ARTRealtimePresenceProtocol subscribe:]` are called on the `ARTRealtimePresence` object for this channel. + * Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using `-[ARTRealtimeChannelProtocol subscribe:]`. Any resulting channel state change will be emitted to any listeners registered using the `-[ARTEventEmitter on:]` or `-[ARTEventEmitter once:]` methods. A callback may optionally be passed in to this call to be notified of success or failure of the operation. As a convenience, `attach:` is called implicitly if `-[ARTRealtimeChannelProtocol subscribe:]` is called on the channel or `-[ARTRealtimePresenceProtocol subscribe:]` is called on the `ARTRealtimePresence` object for this channel, unless you’ve set the `ARTRealtimeChannelOptions.attachOnSubscribe` channel option to `false`. It is also called implicitly if `-[ARTRealtimePresenceProtocol enter:]` is called on the `ARTRealtimePresence` object for this channel. * * @param callback A success or failure callback function. */ @@ -69,12 +69,14 @@ NS_ASSUME_NONNULL_BEGIN * @param callback An event listener function. * * @return An `ARTEventListener` object. + * + * @see See `subscribeWithAttachCallback:` for more details. */ - (ARTEventListener *_Nullable)subscribe:(ARTMessageCallback)callback; /** * Registers a listener for messages on this channel. The caller supplies a listener function, which is called each time one or more messages arrives on the channel. - * An attach callback may optionally be passed in to this call to be notified of success or failure of the channel `-[ARTRealtimeChannelProtocol attach]` operation. + * An attach callback may optionally be passed in to this call to be notified of success or failure of the channel `-[ARTRealtimeChannelProtocol attach]` operation. It will not be called if `ARTRealtimeChannelOptions.attachOnSubscribe` channel option was set to `false`. * * @param onAttach An attach callback function. * @param callback An event listener function. @@ -90,11 +92,13 @@ NS_ASSUME_NONNULL_BEGIN * @param callback An event listener function. * * @return An `ARTEventListener` object. - */ + * + * @see See `subscribeWithAttachCallback:` for more details. +*/ - (ARTEventListener *_Nullable)subscribe:(NSString *)name callback:(ARTMessageCallback)callback; /** - * Registers a listener for messages with a given event `name` on this channel. The caller supplies a listener function, which is called each time one or more matching messages arrives on the channel. A callback may optionally be passed in to this call to be notified of success or failure of the channel `-[ARTRealtimeChannelProtocol attach]` operation. + * Registers a listener for messages with a given event `name` on this channel. The caller supplies a listener function, which is called each time one or more matching messages arrives on the channel. A callback may optionally be passed in to this call to be notified of success or failure of the channel `-[ARTRealtimeChannelProtocol attach]` operation. It will not be called if `ARTRealtimeChannelOptions.attachOnSubscribe` channel option was set to `false`. * * @param name The event name. * @param callback An event listener function. diff --git a/Source/include/Ably/ARTRealtimeChannelOptions.h b/Source/include/Ably/ARTRealtimeChannelOptions.h index 1bc622cd1..04671da63 100644 --- a/Source/include/Ably/ARTRealtimeChannelOptions.h +++ b/Source/include/Ably/ARTRealtimeChannelOptions.h @@ -42,6 +42,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) ARTChannelMode modes; +/** + * A boolean which determines whether calling `subscribe` on a `ARTRealtimeChannel` or `ARTRealtimePresense` object should trigger an implicit attach (for realtime client libraries only). Defaults to true. + */ +@property (nonatomic) BOOL attachOnSubscribe; + @end NS_ASSUME_NONNULL_END diff --git a/Test/Tests/RealtimeClientChannelTests.swift b/Test/Tests/RealtimeClientChannelTests.swift index 548e98ba2..57c3e948d 100644 --- a/Test/Tests/RealtimeClientChannelTests.swift +++ b/Test/Tests/RealtimeClientChannelTests.swift @@ -3314,8 +3314,8 @@ class RealtimeClientChannelTests: XCTestCase { } } - // RTL7c - func test__109__Channel__subscribe__should_implicitly_attach_the_channel() throws { + // RTL7g + func test__109__Channel__subscribe__should_implicitly_attach_the_channel_if_options_attachOnSubscribe_is_true() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -3340,8 +3340,32 @@ class RealtimeClientChannelTests: XCTestCase { expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) } - // RTL7c - func test__110__Channel__subscribe__should_result_in_an_error_if_channel_is_in_the_FAILED_state() throws { + // RTL7h + func test__109b__Channel__subscribe__should_not_implicitly_attach_the_channel_if_options_attachOnSubscribe_is_false() throws { + let test = Test() + let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) + defer { client.dispose(); client.close() } + + let channelOptions = ARTRealtimeChannelOptions() + channelOptions.attachOnSubscribe = false + let channel = client.channels.get(test.uniqueChannelName(), options: channelOptions) + + // Initialized + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + channel.subscribe(attachCallback: { _ in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays initialized + waitUntil(timeout: testTimeout) { done in + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + done() + } + } + } + + // RTL7g + func test__110__Channel__subscribe__should_result_in_an_error_if_channel_is_in_the_FAILED_state_and_options_attachOnSubscribe_is_true() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -3362,6 +3386,31 @@ class RealtimeClientChannelTests: XCTestCase { } } + // RTL7g + func test__110b__Channel__subscribe__should_not_result_in_an_error_if_channel_is_in_the_FAILED_state_and_options_attachOnSubscribe_is_false() throws { + let test = Test() + let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) + defer { client.dispose(); client.close() } + + let channelOptions = ARTRealtimeChannelOptions() + channelOptions.attachOnSubscribe = false + let channel = client.channels.get(test.uniqueChannelName(), options: channelOptions) + + channel.internal.onError(AblyTests.newErrorProtocolMessage()) + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + + channel.subscribe(attachCallback: { _ in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays failed + waitUntil(timeout: testTimeout) { done in + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + done() + } + } + } + // RTL7d func test__112__Channel__subscribe__should_deliver_the_message_even_if_there_is_an_error_while_decoding__using_crypto_data_128() throws { @@ -4826,5 +4875,11 @@ class RealtimeClientChannelTests: XCTestCase { } XCTAssertNotNil(exception3) XCTAssertEqual(exception3!.name, NSExceptionName.objectInaccessibleException) + + let exception4 = tryInObjC { + channelOptions.attachOnSubscribe = false // frozen + } + XCTAssertNotNil(exception4) + XCTAssertEqual(exception4!.name, NSExceptionName.objectInaccessibleException) } } diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index dc2a545af..916b6f70b 100644 --- a/Test/Tests/RealtimeClientPresenceTests.swift +++ b/Test/Tests/RealtimeClientPresenceTests.swift @@ -886,8 +886,8 @@ class RealtimeClientPresenceTests: XCTestCase { // RTP6 - // RTP6c - func test__026__Presence__subscribe__should_implicitly_attach_the_channel() throws { + // RTP6d + func test__026__Presence__subscribe__should_implicitly_attach_the_channel_if_options_attachOnSubscribe_is_true() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -912,8 +912,32 @@ class RealtimeClientPresenceTests: XCTestCase { expect(channel.state).toEventually(equal(ARTRealtimeChannelState.attached), timeout: testTimeout) } - // RTP6c - func test__027__Presence__subscribe__should_result_in_an_error_if_the_channel_is_in_the_FAILED_state() throws { + // RTP6d + func test__026b__Presence__subscribe__should_not_implicitly_attach_the_channel_if_options_attachOnSubscribe_is_false() throws { + let test = Test() + let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) + defer { client.dispose(); client.close() } + + let channelOptions = ARTRealtimeChannelOptions() + channelOptions.attachOnSubscribe = false + let channel = client.channels.get(test.uniqueChannelName(), options: channelOptions) + + // Initialized + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + channel.presence.subscribe(attachCallback: { _ in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays initialized + waitUntil(timeout: testTimeout) { done in + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + done() + } + } + } + + // RTP6d + func test__027__Presence__subscribe__should_result_in_an_error_if_the_channel_is_in_the_FAILED_state_and_options_attachOnSubscribe_is_true() throws { let test = Test() let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) defer { client.dispose(); client.close() } @@ -931,6 +955,31 @@ class RealtimeClientPresenceTests: XCTestCase { }) } } + + // RTP6e + func test__027b__Presence__subscribe__should_not_result_in_an_error_if_the_channel_is_in_the_FAILED_state_and_options_attachOnSubscribe_is_false() throws { + let test = Test() + let client = ARTRealtime(options: try AblyTests.commonAppSetup(for: test)) + defer { client.dispose(); client.close() } + + let channelOptions = ARTRealtimeChannelOptions() + channelOptions.attachOnSubscribe = false + let channel = client.channels.get(test.uniqueChannelName(), options: channelOptions) + + channel.internal.onError(AblyTests.newErrorProtocolMessage()) + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + + channel.presence.subscribe(attachCallback: { _ in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays failed + waitUntil(timeout: testTimeout) { done in + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + done() + } + } + } // RTP6c func test__028__Presence__subscribe__should_result_in_an_error_if_the_channel_moves_to_the_FAILED_state() throws {