Skip to content

Commit

Permalink
Merge pull request #1976 from ably/feature/1972-attachOnSubscribe
Browse files Browse the repository at this point in the history
Added `attachOnSubscribe` to `ARTRealtimeChannelOptions`.
  • Loading branch information
maratal authored Sep 30, 2024
2 parents ed47cc5 + e7ab691 commit cb3f23d
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 18 deletions.
8 changes: 6 additions & 2 deletions Source/ARTRealtimeChannel.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
28 changes: 28 additions & 0 deletions Source/ARTRealtimeChannelOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<ARTCipherParamsCompatible>)cipherParams {
if (self = [super initWithCipher:cipherParams]) {
_attachOnSubscribe = true;
}
return self;
}

- (NSStringDictionary *)params {
Expand Down Expand Up @@ -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
9 changes: 7 additions & 2 deletions Source/ARTRealtimePresence.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import "ARTProtocolMessage+Private.h"
#import "ARTEventEmitter+Private.h"
#import "ARTClientOptions.h"
#import "ARTRealtimeChannelOptions.h"

#pragma mark - ARTRealtimePresenceQuery

Expand Down Expand Up @@ -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];
Expand Down
12 changes: 8 additions & 4 deletions Source/include/Ably/ARTRealtimeChannel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 the `ARTRealtimeChannelOptions.attachOnSubscribe` channel option is set to `false`.
*
* @param onAttach An attach callback function.
* @param callback An event listener function.
Expand All @@ -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 the `ARTRealtimeChannelOptions.attachOnSubscribe` channel option is set to `false`.
*
* @param name The event name.
* @param callback An event listener function.
Expand Down
5 changes: 5 additions & 0 deletions Source/include/Ably/ARTRealtimeChannelOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions Source/include/Ably/ARTRealtimePresence.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN
- (ARTEventListener *_Nullable)subscribe:(ARTPresenceMessageCallback)callback;

/**
* Registers a listener that is called each time a `ARTPresenceMessage` is received on the channel, such as a new member entering the presence set. 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 that is called each time a `ARTPresenceMessage` is received on the channel, such as a new member entering the presence set. 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 the `ARTRealtimeChannelOptions.attachOnSubscribe` channel option is set to `false`.
*
* @param onAttach An attach callback function.
* @param callback An event listener function.
Expand All @@ -166,7 +166,7 @@ NS_ASSUME_NONNULL_BEGIN
- (ARTEventListener *_Nullable)subscribe:(ARTPresenceAction)action callback:(ARTPresenceMessageCallback)callback;

/**
* Registers a listener that is called each time a `ARTPresenceMessage` matching a given `ARTPresenceAction` is received on the channel, such as a new member entering the presence set. 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 that is called each time a `ARTPresenceMessage` matching a given `ARTPresenceAction` is received on the channel, such as a new member entering the presence set. 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 the `ARTRealtimeChannelOptions.attachOnSubscribe` channel option is set to `false`.
*
* @param action A `ARTPresenceAction` to register the listener for.
* @param onAttach An attach callback function.
Expand Down
63 changes: 59 additions & 4 deletions Test/Tests/RealtimeClientChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand All @@ -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() }
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
57 changes: 53 additions & 4 deletions Test/Tests/RealtimeClientPresenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand All @@ -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() }
Expand All @@ -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 {
Expand Down

0 comments on commit cb3f23d

Please sign in to comment.