Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Source/ARTRealtimeChannel.m
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,17 @@ - (void)internalAttach:(ARTCallback)callback withParams:(ARTAttachRequestParams
[self attachAfterChecks];
}

- (BOOL)shouldUseAttachResume {
// Check if channel options explicitly override attach resume behavior
NSNumber *channelAttachResume = self.options_nosync.attachResume;
if (channelAttachResume != nil) {
return [channelAttachResume boolValue];
}

// Use default behavior based on current channel state
return self.attachResume;
}

- (void)attachAfterChecks {
ARTProtocolMessage *attachMessage = [[ARTProtocolMessage alloc] init];
attachMessage.action = ARTProtocolMessageAttach;
Expand All @@ -1011,7 +1022,7 @@ - (void)attachAfterChecks {
attachMessage.params = self.options_nosync.params;
attachMessage.flags = self.options_nosync.modes;

if (self.attachResume) {
if ([self shouldUseAttachResume]) {
attachMessage.flags = attachMessage.flags | ARTProtocolMessageFlagAttachResume;
}

Expand Down
15 changes: 15 additions & 0 deletions Source/ARTRealtimeChannelOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ @implementation ARTRealtimeChannelOptions {
NSStringDictionary *_params;
ARTChannelMode _modes;
BOOL _attachOnSubscribe;
NSNumber *_attachResume;
}

- (instancetype)init {
Expand All @@ -27,6 +28,7 @@ - (id)copyWithZone:(NSZone *)zone {
copied->_params = _params;
copied->_modes = _modes;
copied->_attachOnSubscribe = _attachOnSubscribe;
copied->_attachResume = _attachResume;

return copied;
}
Expand Down Expand Up @@ -70,4 +72,17 @@ - (void)setAttachOnSubscribe:(BOOL)value {
_attachOnSubscribe = value;
}

- (NSNumber *)attachResume {
return _attachResume;
}

- (void)setAttachResume:(NSNumber *)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];
}
_attachResume = value;
}

@end
2 changes: 2 additions & 0 deletions Source/PrivateHeaders/Ably/ARTRealtimeChannel+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTChannelStateChange *)

- (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data;

- (BOOL)shouldUseAttachResume;

// MARK: - Plugins

/// Provides the implementation for `-[ARTPluginAPI setPluginDataValue:forKey:channel]`. See documentation for that method in `APPluginAPIProtocol`.
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 @@ -63,6 +63,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic) BOOL attachOnSubscribe;

/**
* A nullable boolean which controls whether the channel should attempt to resume from the last known position when reattaching. When set to false, the channel will not use attach resume and will rely solely on message history. When nil (default), the standard attach resume behavior is used. Defaults to nil.
*/
@property (nonatomic, nullable) NSNumber *attachResume;

@end

NS_ASSUME_NONNULL_END
136 changes: 136 additions & 0 deletions Test/Tests/RealtimeClientChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,142 @@ class RealtimeClientChannelTests: XCTestCase {
}
}

// RTL4j3
func test__048a__Channel__attach__attach_resume__should_respect_channel_options_attachResume_property() throws {
let test = Test()
let options = try AblyTests.commonAppSetup(for: test)

let channelName = test.uniqueChannelName()
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

guard let transport = client.internal.transport as? TestProxyTransport else {
fail("Expecting TestProxyTransport"); return
}

// Test with attachResume explicitly set to false
let channelOptionsDisabled = ARTRealtimeChannelOptions()
channelOptionsDisabled.attachResume = NSNumber(value: false)

let channelWithDisabledResume = client.channels.get(channelName, options: channelOptionsDisabled)

waitUntil(timeout: testTimeout) { done in
channelWithDisabledResume.attach { error in
XCTAssertNil(error)
done()
}
}

let attachMessagesDisabled = transport.protocolMessagesSent.filter { $0.action == .attach }
XCTAssertEqual(attachMessagesDisabled.count, 1)

guard let attachMessageDisabled = attachMessagesDisabled.first else {
fail("ATTACH message is missing"); return
}

// Should NOT have the attach resume flag when explicitly disabled
XCTAssertEqual(attachMessageDisabled.flags & Int64(ARTProtocolMessageFlag.attachResume.rawValue), 0)

// Reset transport for next test
transport.protocolMessagesSent.removeAll()

// Test with attachResume explicitly set to true
let channelOptionsEnabled = ARTRealtimeChannelOptions()
channelOptionsEnabled.attachResume = NSNumber(value: true)

let channelNameEnabled = test.uniqueChannelName()
let channelWithEnabledResume = client.channels.get(channelNameEnabled, options: channelOptionsEnabled)

waitUntil(timeout: testTimeout) { done in
channelWithEnabledResume.attach { error in
XCTAssertNil(error)
done()
}
}

let attachMessagesEnabled = transport.protocolMessagesSent.filter { $0.action == .attach }
XCTAssertEqual(attachMessagesEnabled.count, 1)

guard let attachMessageEnabled = attachMessagesEnabled.first else {
fail("ATTACH message is missing"); return
}

// Should have the attach resume flag when explicitly enabled
expect(attachMessageEnabled.flags & Int64(ARTProtocolMessageFlag.attachResume.rawValue)).to(beGreaterThan(0))

// Reset transport for next test
transport.protocolMessagesSent.removeAll()

// Test with attachResume set to nil (default behavior)
let channelOptionsDefault = ARTRealtimeChannelOptions()
// Don't set attachResume, should be nil by default

let channelNameDefault = test.uniqueChannelName()
let channelWithDefaultResume = client.channels.get(channelNameDefault, options: channelOptionsDefault)

// First attach to set internal attachResume to true
waitUntil(timeout: testTimeout) { done in
channelWithDefaultResume.attach { error in
XCTAssertNil(error)
done()
}
}

// Verify it's attached and attachResume is true internally
XCTAssertEqual(channelWithDefaultResume.state, .attached)
XCTAssertTrue(channelWithDefaultResume.internal.attachResume)

// Now detach and reattach to test the default behavior
waitUntil(timeout: testTimeout) { done in
channelWithDefaultResume.detach { error in
XCTAssertNil(error)
done()
}
}

// Clear messages and reattach
transport.protocolMessagesSent.removeAll()

waitUntil(timeout: testTimeout) { done in
channelWithDefaultResume.attach { error in
XCTAssertNil(error)
done()
}
}

let attachMessagesDefault = transport.protocolMessagesSent.filter { $0.action == .attach }
XCTAssertEqual(attachMessagesDefault.count, 1)

guard let attachMessageDefault = attachMessagesDefault.first else {
fail("ATTACH message is missing"); return
}

// Should have the attach resume flag when using default behavior (since channel was previously attached)
expect(attachMessageDefault.flags & Int64(ARTProtocolMessageFlag.attachResume.rawValue)).to(beGreaterThan(0))

/*
* Note: To differentiate between messages received during normal operation vs. after resume,
* developers can listen to channel state changes and check the `resumed` property:
*
* channel.on(.attached) { stateChange in
* if stateChange.resumed {
* // Messages received shortly after this are likely from resume
* isResumedState = true
* } else {
* isResumedState = false
* }
* }
*
* channel.subscribe { message in
* if isResumedState {
* // This message was likely received as part of resume
* } else {
* // This message was received during normal operation
* }
* }
*/
}

// RTL5a
func test__049__Channel__detach__if_state_is_INITIALIZED_or_DETACHED_nothing_is_done() throws {
let test = Test()
Expand Down