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/ARTRealtimeChannelOptions.h b/Source/include/Ably/ARTRealtimeChannelOptions.h index 1bc622cd1..30d4ed90d 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; +/** + * Determines whether calling `subscribe` on a channel or presence object should trigger an implicit attach. 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..d700be963 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,29 @@ 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: { errorInfo in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays initialized + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + } + } + + // 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 +3383,28 @@ 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: { errorInfo in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays failed + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + } + } + // RTL7d func test__112__Channel__subscribe__should_deliver_the_message_even_if_there_is_an_error_while_decoding__using_crypto_data_128() throws { diff --git a/Test/Tests/RealtimeClientPresenceTests.swift b/Test/Tests/RealtimeClientPresenceTests.swift index dc2a545af..27a30c4a6 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,29 @@ 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: { errorInfo in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays initialized + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.initialized) + } + } + + // 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 +952,28 @@ 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: { errorInfo in + fail("Attach callback should not be called.") + }) { _ in } + // Make sure that channel stays failed + delay(1) { + XCTAssertEqual(channel.state, ARTRealtimeChannelState.failed) + } + } // RTP6c func test__028__Presence__subscribe__should_result_in_an_error_if_the_channel_moves_to_the_FAILED_state() throws {