Skip to content

Conversation

@maratal
Copy link
Collaborator

@maratal maratal commented Sep 11, 2025

Closes #2098

Summary by CodeRabbit

  • New Features

    • Endpoint-centric configuration and a new domain selection service; configurable connectivity-check URL.
  • Deprecations

    • environment, restHost and realtimeHost marked deprecated — migrate to endpoint.
  • Removals

    • Legacy fallback-host API removed; domain/fallbacks now driven by the new selector.
  • Bug Fixes

    • Improved domain resolution, fallback selection and connectivity-check behavior.
  • Tests

    • Added domain-selector tests and updated suites to endpoint/primary-domain semantics.
  • Chores

    • CI/test sandbox endpoints and scripts updated to non-production URLs.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Walkthrough

Replaces ARTFallbackHosts with a new ARTDomainSelector; introduces endpoint and connectivityCheckUrl options on ARTClientOptions; routes REST/Realtime/WebSocket host resolution and fallback logic through ARTDomainSelector; updates tests, project/module maps, CI, and scripts to endpoint/primaryDomain semantics.

Changes

Cohort / File(s) Summary
Domain selector & new class
Source/ARTDomainSelector.h, Source/ARTDomainSelector.m, Source/PrivateHeaders/Ably/ARTDomainSelector.h
New ARTDomainSelector class and private header: parses endpoint/environment/rest/realtime/fallback options and exposes primaryDomain, fallbackDomains, defaultFallbackDomains.
Client options & private API
Source/ARTClientOptions.m, Source/include/Ably/ARTClientOptions.h, Source/PrivateHeaders/Ably/ARTClientOptions+Private.h
Adds public endpoint and connectivityCheckUrl; integrates ARTDomainSelector; adds domain-derived accessors (primaryDomain, fallbackDomains), deprecates legacy environment/restHost/realtimeHost usage, adds setDefaultEndpoint:, and enforces mutual-exclusion with legacy options.
Defaults & connectivity
Source/ARTDefault.m, Source/PrivateHeaders/Ably/ARTDefault+Private.h, Source/include/Ably/ARTDefault.h
Introduces ARTDefault connectivity constants and connectivityCheckUrl; moves rest/realtime/fallback resolution toward DomainSelector-driven logic and exposes fallback APIs and setters.
Removed legacy fallback
Source/ARTFallbackHosts.m, Source/PrivateHeaders/Ably/ARTFallbackHosts.h
Removes ARTFallbackHosts implementation and header; fallback derivation migrated into ARTDomainSelector.
Runtime clients & transport updates
Source/ARTRealtime.m, Source/ARTRest.m, Source/ARTWebSocketTransport.m, Source/ARTConnection.m
Shift fallback resolution to options.domainSelector.fallbackDomains; simplify retry/fallback checks; ARTRest uses configurable connectivityCheckUrl and adds +deviceAccessQueue; WebSocket transport introduces internal host backing field initialized from domainSelector.primaryDomain; connection maxMessageSize uses ARTDefault constant.
Build & module updates
Ably.xcodeproj/project.pbxproj, Source/Ably.modulemap, Source/include/module.modulemap
Adds ARTDomainSelector.m/.h to project and module maps; removes ARTFallbackHosts references from project and module maps.
Tests & utilities
Test/AblyTests/**, Examples/Tests/TestsTests/TestsTests.swift, Test/AblyTests/Test Utilities/TestUtilities.swift, Test/AblyTests/Tests/ARTDomainSelectorTests.swift
Tests and helpers updated to use endpoint/primaryDomain; added comprehensive ARTDomainSelectorTests; updated many test expectations and deprecation annotations; sandbox endpoint set to nonprod:sandbox.
CI & scripts
.github/workflows/integration-test.yaml, Scripts/log-environment-information.sh
CI ABLY_ENV changed to nonprod:sandbox; scripts updated to reference sandbox.realtime.ably-nonprod.net.
Project file entries (repeat)
Ably.xcodeproj/project.pbxproj
Adds ARTDomainSelector.m/.h to build graph and removes ARTFallbackHosts.m/.h references.
sequenceDiagram
    participant Client as Client / Tests
    participant Options as ARTClientOptions
    participant Selector as ARTDomainSelector
    participant Rest as ARTRest
    participant Realtime as ARTRealtime

    Note over Client,Options: Client configures options (endpoint or legacy fields)
    Client->>Options: set endpoint / restHost / realtimeHost / fallbackHosts
    Options->>Selector: initWithEndpointClientOption(..., fallbackHostsUseDefault: ...)
    Selector-->>Options: primaryDomain / fallbackDomains

    Client->>Rest: REST request (host = selector.primaryDomain)
    Rest->>Selector: on failure check fallbackDomains
    alt fallback available
      Rest->>Rest: switch host -> next fallback and retry
      Rest->>Client: return retry result
    else no fallback
      Rest->>Client: report no-fallback error
    end

    Client->>Realtime: open realtime (host = selector.primaryDomain)
    Realtime->>Selector: on transport failure get fallbackDomains
    Realtime->>Realtime: reconnect using fallback hosts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • Areas needing careful review:
    • Source/ARTDomainSelector.m — parsing rules, routing-policy edge cases, fallback generation and priority.
    • Source/ARTClientOptions.m — mutual-exclusion enforcement, domainSelector lifecycle, copy semantics.
    • Source/ARTRest.m and Source/ARTRealtime.m — host switching, fallback iteration, connectivityCheckUrl behavior and device access queue.
    • Tests (Test/AblyTests/**) — broad expectation changes and new ARTDomainSelectorTests.
    • Project/module updates (Ably.xcodeproj/project.pbxproj, module maps) — ensure headers/sources are correctly registered.

Poem

🐰 I hopped through endpoints and sniffed the code,

Policy names and hosts in a tidy row.
Five fallback doors that spin when storms blow,
Old names tucked gently under new nets grown,
nonprod:sandbox — a cheerful hop back home.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'ECO-5551 Use endpoint as default connection option (ADR-119)' directly summarizes the main change: implementing endpoint as the default connection option per ADR-119, which aligns with the PR's primary objective.
Linked Issues check ✅ Passed The PR comprehensively implements all core coding requirements from #2098/ECO-5551: adds endpoint option accepting routing policy names and FQDNs, enforces incompatibility with legacy options, creates ARTDomainSelector for domain resolution per ADR-119 spec, maintains legacy option compatibility, and switches to ably.net domain by default.
Out of Scope Changes check ✅ Passed All changes are within scope of ADR-119 implementation. Workflow updates reflect endpoint changes, test updates validate new functionality, and infrastructure changes (modulemap, build files) support the new ARTDomainSelector class. Script update aligns with new domain names.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2098-ADR-119

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • ADR-119: Request failed with status code 404

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 11, 2025 21:03 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 11, 2025 21:05 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 11, 2025 21:12 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 11, 2025 21:16 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 13, 2025 15:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 13, 2025 15:59 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 13, 2025 16:49 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 13, 2025 16:55 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 13, 2025 20:30 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 13, 2025 20:34 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 13, 2025 20:50 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 13, 2025 20:55 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 13, 2025 21:18 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 13, 2025 21:21 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 20:45 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 14, 2025 20:49 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 21:55 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 21:59 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 14, 2025 22:03 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 22:29 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 14, 2025 22:33 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 22:51 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 14, 2025 22:55 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 14, 2025 23:33 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc September 14, 2025 23:38 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features September 15, 2025 18:53 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 27, 2025 15:35 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 27, 2025 15:38 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 27, 2025 15:43 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 27, 2025 15:46 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
Test/AblyTests/Tests/RestClientTests.swift (1)

177-181: Existing TODO’d expectations remain ineffective but are already tracked

The expect(publishTask.error).toEventually(beNil(), …) checks in test__015__…API_key and test__018__…token are still ineffective because publishTask.error starts as nil. This was already raised and is tracked in GitHub issue #2145, so leaving the TODOs in place in this PR is reasonable.

Also applies to: 201-207

Test/AblyTests/Tests/ARTDomainSelectorTests.swift (1)

400-411: Endpoint "nonprod:" producing .realtime.ably-nonprod.net still looks like an undesirable edge case

This test codifies that a nonprod: endpoint with no routing ID yields a primary domain of .realtime.ably-nonprod.net (leading dot, empty policy ID). That hostname is highly likely to be invalid or at least unusable, and it makes a simple typo ("nonprod:" vs "nonprod:sandbox") fail at runtime in a non-obvious way.

Consider tightening the parsing in ARTDomainSelector so that a nonprod-prefixed endpoint with an empty/whitespace-only suffix is treated as invalid or as “no nonprod routing ID” (e.g. falling back to the default primary domain or surfacing configuration as invalid). If you make that change, this test should be updated to assert the new, safer behavior instead of the leading-dot hostname.

Source/ARTDomainSelector.m (2)

66-88: Endpoint classification is too naive for IP/IPv6 literals

The hostname-vs-routing-policy decision only checks:

  • containsString:@"."
  • containsString:@"::"
  • equality with "localhost"

Anything else non-empty is treated as a routing policy ID. This will misclassify several valid endpoint forms, e.g.:

  • Fully expanded IPv6 literals without :: (2001:0db8:1:2:3:4:5:6).
  • Bracketed IPv6 with optional port ([::1]:8080).
  • Potential other IP literal syntaxes.

Those values will be turned into routing policies and combined with .realtime.ably.net, which is almost certainly not what the user intended.

Consider normalizing and validating IP literals explicitly (e.g. strip brackets/ports/zone IDs, then detect IPv4/IPv6 using a system API like inet_pton or a focused regex) before falling back to the routing-policy heuristics. That way:

  • All valid IPv4/IPv6 endpoints are consistently treated as hostnames.
  • Only “clean” nonprod/prod routing IDs go down the policyId path.

81-88: endpoint = "nonprod:" yields an empty policyId and leading-dot host

When endpointClientOption has the "nonprod:" prefix but nothing afterwards, policyId becomes the empty string. For the NonProductionRoutingPolicy branch this produces:

  • Primary domain: .realtime.ably-nonprod.net
  • Fallbacks: .a.fallback.ably-realtime-nonprod.com, etc.

Those hostnames are almost certainly invalid and arise from a plausible misconfiguration or typo.

It would be safer to treat an empty/whitespace-only suffix after "nonprod:" as “no routing ID” and either:

  • fall back to the default primary/fallback domains, or
  • surface an explicit configuration error.

If you change the behavior, remember to update the corresponding Swift test (test__028__ARTDomainSelector__should_handle_nonprod_with_empty_id) to assert the new semantics.

Source/ARTClientOptions.m (2)

90-99: setEndpoint still enforces REC1b1 even when clearing/propagating a nil endpoint (can break copy and legacy-only configs)

-setEndpoint: unconditionally raises if any legacy options are set, regardless of the value passed:

- (void)setEndpoint:(NSString *)endpoint {
    // REC1b1: endpoint cannot be used with deprecated options
    if (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost) {
        [NSException raise:...];
    }
    _endpoint = endpoint;
    _domainSelector = nil;
}

This means:

  • Copying an options instance that only uses environment/restHost/realtimeHost (with endpoint == nil) will still invoke setEndpoint:nil in copyWithZone:, and hasEnvironment/hasCustom*Host may already be true, causing copy to raise even though no endpoint is actually being configured.
  • Clearing an endpoint (setting it back to nil/empty) after legacy options have been set will also throw, even though the resulting configuration is valid (endpoint no longer in use).

REC1b1 only needs to be enforced when a non-empty endpoint is set. Suggest gating the check on endpoint itself:

- (void)setEndpoint:(NSString *)endpoint {
-    // REC1b1: endpoint cannot be used with deprecated options
-    if (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost) {
-        [NSException raise:NSInvalidArgumentException
-                    format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."];
-    }
-    _endpoint = endpoint;
-    // Reset domain selector when endpoint changes
-    _domainSelector = nil;
-}
+ (void)setEndpoint:(NSString *)endpoint {
+    // Only enforce REC1b1 when actually setting a non-empty endpoint
+    if (endpoint && [endpoint isNotEmptyString]) {
+        if (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost) {
+            [NSException raise:NSInvalidArgumentException
+                        format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."];
+        }
+    }
+    _endpoint = endpoint;
+    // Reset domain selector when endpoint changes (including clearing it)
+    _domainSelector = nil;
+}

This preserves the REC1b1 constraint for real endpoint usage while allowing copyWithZone: and “clear endpoint” flows to work for legacy-only configurations.


319-324: isProductionEnvironment now returns NO when environment is nil; likely diverges from historical “nil == production” semantics

Current implementation:

- (BOOL)isProductionEnvironment {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProductionEnvironment lowercaseString]];
#pragma clang diagnostic pop
}

When environment == nil (the default case for clients not explicitly configuring a non‑production environment), -[NSString lowercaseString] is sent to nil, and the expression evaluates to NO. Historically, “no environment configured” has been treated as production, so this inverts that meaning.

If call sites still rely on the traditional behavior (which the method name suggests), this should instead treat nil or "production" (case‑insensitive) as production, e.g.:

- (BOOL)isProductionEnvironment {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    NSString *env = [[self environment] lowercaseString];
    NSString *prod = [ARTDefaultProductionEnvironment lowercaseString];
    return (env == nil || [env isEqualToString:prod]);
#pragma clang diagnostic pop
}

This keeps legacy semantics while still allowing explicit non‑production environments to be detected.

🧹 Nitpick comments (2)
Source/ARTClientOptions.m (2)

193-215: Confirm DomainSelector’s host outputs are bare hostnames suitable for NSURLComponents.host (esp. for IPv6/FQDN/IP cases)

restUrlComponents and realtimeUrlForHost: now do:

components.host = self.primaryDomain;           // rest
...
components.host = host;                         // realtime

with primaryDomain and the host argument ultimately coming from ARTDomainSelector.

For NSURLComponents.host to remain valid across all supported endpoint forms (routing policy, FQDN, IPv4, IPv6, localhost), ARTDomainSelector needs to guarantee that:

  • Hosts are bare hostnames or IP literals, without embedded ports.
  • IPv6 literals, if supported, are in the form expected by NSURLComponents (no [] wrapping in the host property; ports handled via components.port).

Given ADR‑119 explicitly allows FQDN/IP/localhost endpoints, it would be good to double‑check DomainSelector’s outputs (and add tests) to ensure they are always safe to assign to NSURLComponents.host without additional normalization.


22-22: Global default endpoint is simple but relies on call order; consider whether it ever needs to change after first options instantiation

You now have:

NSString *ARTDefaultEndpoint = nil;

@implementation ARTClientOptions {
    NSString *_endpoint;
    ...
}

- (instancetype)initDefaults {
    ...
    _endpoint = ARTDefaultEndpoint;
    ...
    _connectivityCheckUrl = [ARTDefault connectivityCheckUrl];
}

+ (void)setDefaultEndpoint:(NSString *)endpoint {
    ARTDefaultEndpoint = endpoint;
}

This works fine if +setDefaultEndpoint: is only ever called once, before any ARTClientOptions are created (e.g., from a library initialization path or tests). If there’s any scenario where the default endpoint might be updated at runtime, already‑constructed ARTClientOptions instances will keep the old value because they snapshot ARTDefaultEndpoint in initDefaults.

If runtime changes are not required, this is acceptable as‑is; otherwise, consider either:

  • Having initDefaults read a source of truth that can change (e.g. ARTDefault), or
  • Documenting that setDefaultEndpoint: is a one‑time, pre‑instantiation configuration hook.

Also applies to: 32-38, 48-48, 80-80, 302-304

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f423081 and 624b1d1.

📒 Files selected for processing (25)
  • .github/workflows/integration-test.yaml (1 hunks)
  • Ably.xcodeproj/project.pbxproj (9 hunks)
  • Examples/Tests/TestsTests/TestsTests.swift (3 hunks)
  • Scripts/log-environment-information.sh (1 hunks)
  • Source/ARTClientOptions.m (8 hunks)
  • Source/ARTDefault.m (4 hunks)
  • Source/ARTDomainSelector.m (1 hunks)
  • Source/ARTFallbackHosts.m (0 hunks)
  • Source/ARTRealtime.m (3 hunks)
  • Source/ARTRest.m (7 hunks)
  • Source/ARTWebSocketTransport.m (5 hunks)
  • Source/Ably.modulemap (1 hunks)
  • Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (2 hunks)
  • Source/PrivateHeaders/Ably/ARTDefault+Private.h (2 hunks)
  • Source/PrivateHeaders/Ably/ARTDomainSelector.h (1 hunks)
  • Source/PrivateHeaders/Ably/ARTFallbackHosts.h (0 hunks)
  • Source/include/Ably/ARTClientOptions.h (2 hunks)
  • Source/include/Ably/ARTDefault.h (1 hunks)
  • Source/include/module.modulemap (1 hunks)
  • Test/AblyTests/Test Utilities/TestUtilities.swift (4 hunks)
  • Test/AblyTests/Tests/ARTDomainSelectorTests.swift (1 hunks)
  • Test/AblyTests/Tests/PushAdminTests.swift (1 hunks)
  • Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (22 hunks)
  • Test/AblyTests/Tests/RealtimeClientTests.swift (1 hunks)
  • Test/AblyTests/Tests/RestClientTests.swift (35 hunks)
💤 Files with no reviewable changes (2)
  • Source/ARTFallbackHosts.m
  • Source/PrivateHeaders/Ably/ARTFallbackHosts.h
🚧 Files skipped from review as they are similar to previous changes (8)
  • Scripts/log-environment-information.sh
  • Source/Ably.modulemap
  • Source/ARTWebSocketTransport.m
  • Source/include/Ably/ARTClientOptions.h
  • Source/PrivateHeaders/Ably/ARTDefault+Private.h
  • Test/AblyTests/Tests/PushAdminTests.swift
  • Source/include/module.modulemap
  • Source/ARTRealtime.m
🧰 Additional context used
🧬 Code graph analysis (3)
Test/AblyTests/Tests/RealtimeClientTests.swift (2)
Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (3)
  • deprecated (3843-3890)
  • deprecated (3991-4029)
  • deprecated (4074-4111)
Test/AblyTests/Tests/RestClientTests.swift (13)
  • deprecated (302-314)
  • deprecated (317-330)
  • deprecated (354-364)
  • deprecated (367-374)
  • deprecated (377-382)
  • deprecated (385-390)
  • deprecated (418-424)
  • deprecated (865-894)
  • deprecated (896-900)
  • deprecated (903-930)
  • deprecated (1014-1039)
  • deprecated (1098-1122)
  • deprecated (1226-1235)
Test/AblyTests/Tests/RestClientTests.swift (3)
Test/AblyTests/Test Utilities/TestUtilities.swift (7)
  • match (1516-1524)
  • publishTestMessage (484-486)
  • publishTestMessage (488-490)
  • getTestEndpoint (626-635)
  • at (1431-1433)
  • at (1440-1445)
  • extract (1526-1537)
Test/AblyTests/Test Utilities/Test.swift (1)
  • uniqueChannelName (15-32)
Test/AblyTests/Tests/RealtimeClientTests.swift (1)
  • deprecated (156-180)
Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (3)
Test/AblyTests/Test Utilities/TestUtilities.swift (8)
  • match (1516-1524)
  • at (1431-1433)
  • at (1440-1445)
  • data (934-936)
  • dispose (1639-1645)
  • splitDone (248-258)
  • clientOptions (144-159)
  • commonAppSetup (107-142)
Test/AblyTests/Tests/RealtimeClientTests.swift (1)
  • deprecated (156-180)
Test/AblyTests/Test Utilities/DataGatherer.swift (2)
  • waitForData (36-57)
  • waitForData (62-64)
🪛 Clang (14.0.6)
Source/PrivateHeaders/Ably/ARTDomainSelector.h

[error] 1-1: 'Foundation/Foundation.h' file not found

(clang-diagnostic-error)

🪛 SwiftLint (0.57.0)
Test/AblyTests/Tests/RestClientTests.swift

[Warning] 181-181: TODOs should be resolved (this check is useless bc publi...)

(todo)


[Warning] 206-206: TODOs should be resolved (this check is useless bc publi...)

(todo)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: check
  • GitHub Check: check
  • GitHub Check: check (iOS, test_iOS18_4)
  • GitHub Check: check (tvOS, test_tvOS18_4)
  • GitHub Check: check (macOS, test_macOS)
🔇 Additional comments (30)
.github/workflows/integration-test.yaml (1)

35-35: ABLY_ENV change is correctly applied; no other integration-test workflow files exist to replicate.

Verification confirms only one integration-test.yaml file exists in .github/workflows/, with the correct ABLY_ENV: nonprod:sandbox value at line 35. The environment variable naming aligns with ADR-119's convention for non-production sandbox routing policies. The comment on line 11 serves as a template reminder for future variants, but currently there are no other integration-test-*.yaml files requiring updates.

Ably.xcodeproj/project.pbxproj (2)

360-365: ARTDomainSelector file wiring looks correct and consistent.

  • PBXBuildFile entries for ARTDomainSelector.m/.h correctly point at the new PBXFileReference IDs.
  • File references place ARTDomainSelector.m under Source/Utilities and ARTDomainSelector.h under PrivateHeaders/Ably, matching existing private utilities.
  • The Utilities group now includes both .h and .m, so Xcode navigation and grouping remain coherent.

No issues here; this wiring is good to go.

Also applies to: 1224-1225, 1993-1994


2142-2142: DomainSelector is correctly hooked into all framework targets as a private utility.

  • ARTDomainSelector.h is added once to each Headers phase for iOS, macOS, and tvOS with ATTRIBUTES = (Private,), aligning with its intended internal role.
  • ARTDomainSelector.m is added once to each Sources phase for the same three targets, reusing the corresponding PBXBuildFile entries.
  • No duplicate or dangling references for these new build-file IDs.

Assuming DomainSelector is intentionally non‑public API, this target integration looks correct.

Also applies to: 2318-2318, 2527-2527, 2966-2966, 3110-3110, 3254-3254

Examples/Tests/TestsTests/TestsTests.swift (1)

21-21: Endpoint/nonprod and ably.com background URLs look consistent with ADR-119

Using https://sandbox.realtime.ably-nonprod.net:443/apps plus options.endpoint = "nonprod:sandbox" keeps sandbox provisioning and client connections aligned on the same nonprod routing policy, and switching the background URLSession pings to https://ably.com is purely cosmetic and safe here.

Also applies to: 50-50, 67-78

Test/AblyTests/Test Utilities/TestUtilities.swift (1)

119-119: Test utilities correctly pivoted to endpoint/domainSelector

  • Using https://\(options.domainSelector.primaryDomain):\(options.tlsPort)/apps keeps sandbox app provisioning in lockstep with whatever endpoint/domainSelector the tests are exercising.
  • Wiring clientOptions via options.endpoint = getTestEndpoint() and deriving that from ABLY_ENDPOINTABLY_ENV"nonprod:sandbox" cleanly centralizes test configuration.
  • Dropping the environment query item from the JWT endpoint and only sending encrypted matches the new endpoint-centric model.

Given this is test-only code, the existing infoDictionary! force unwrap is acceptable.

Also applies to: 146-158, 607-607, 626-635

Test/AblyTests/Tests/RealtimeClientTests.swift (1)

156-180: Deprecation wrapper around realtimeHost test is appropriate

Marking the realtimeHost-based test as deprecated (with a clear message) is a good way to preserve coverage for legacy behavior without surfacing compiler warnings while endpoint/domainSelector become primary.

Source/ARTRest.m (1)

19-20: DomainSelector-driven REST host/fallback behavior is wired correctly

  • Resetting currentFallbackHost/prioritizedHost on fallbackRetryExpiration and restoring the host via options.domainSelector.primaryDomain keeps RSC15f semantics while honoring endpoint/environment-derived defaults.
  • Using options.domainSelector.fallbackDomains as the source for ARTFallback and promoting a successful fallback to prioritizedHost maintains the expected REC2 fallback behavior.
  • Setting the Host header explicitly to the fallback host for retried requests (// RSC15j) is important to keep SNI/Host consistent with the URL when going through intermediaries.
  • currentHost now defers to domainSelector.primaryDomain, which matches the new endpoint-centric configuration.
  • internetIsUp: now respects a configurable connectivityCheckUrl with a sane default from ARTDefault, which is a straightforward, backward-compatible enhancement.
  • Exposing +deviceAccessQueue and reusing it in the device accessors centralizes synchronization around the shared ARTLocalDevice without changing behavior.

No issues spotted with these changes.

Also applies to: 375-383, 441-452, 460-463, 515-521, 677-689, 793-802

Source/PrivateHeaders/Ably/ARTDomainSelector.h (1)

1-40: ARTDomainSelector interface matches the endpoint/REC design

Explicitly requiring all domain-affecting client options in the initializer, disabling init, and exposing primaryDomain/fallbackDomains as read‑only properties gives a clear, constrained surface for domain resolution. This is a solid foundation for the endpoint‑centric behavior used elsewhere in the PR.

Test/AblyTests/Tests/RestClientTests.swift (1)

45-47: Endpoint/default-domain and fallback host tests are consistent with DomainSelector semantics

  • All expectations that were previously tied to ably.io have been updated to assert against main.realtime.ably.net and main.[a-e].fallback.ably-realtime.com, matching the new default primary domain and default fallback domain family.
  • The new REC1 tests (test__026a…027d) correctly capture:
    • endpoint + any deprecated option (environment/restHost/realtimeHost) being invalid,
    • endpoint as FQDN → primaryDomain == endpoint,
    • endpoint as nonprod routing policy ("nonprod:sandbox") → sandbox.realtime.ably-nonprod.net,
    • endpoint as production routing policy ("test") → test.realtime.ably.net,
    • environment/restHost/realtimeHost overriding the default primary domain as specified.
  • The new REC2 tests around options.domainSelector.fallbackDomains cover:
    • implicit default fallback list for the default primary domain,
    • empty fallback list when the primary domain is explicitly configured as a domain name,
    • nonprod routing policy endpoints (nonprod:sandbox) mapping to sandbox.[a-e].fallback.ably-realtime-nonprod.com,
    • environment and deprecated host options yielding the expected implicit fallback lists or empty lists.
  • Tests using ARTClientOptions.setDefaultEndpoint(getTestEndpoint()) and options.endpoint = … ensure both static and per-instance endpoint configuration surfaces are exercised.
  • RSC15h/RSC25/RSC15m/RSC15n host-fallback tests now drive against ARTDefault.fallbackHosts() and options.domainSelector.fallbackDomains, so they meaningfully validate the new selector‑based fallback behavior.

These changes give good coverage of the new endpoint/domainSelector model across default, custom, routing-policy, and deprecated-option scenarios.

Also applies to: 66-67, 112-112, 134-135, 392-403, 428-448, 450-476, 515-531, 552-575, 585-607, 685-694, 903-930, 932-957, 959-985, 987-1011, 1041-1067, 1070-1095, 1180-1183

Source/include/Ably/ARTDefault.h (1)

13-19: ARTDefault host/fallback accessors match new usage

Keeping +restHost, +realtimeHost, and the fallbackHosts APIs public is consistent with how tests and DomainSelector now consume defaults, and the minor signature tidy-up doesn’t change behavior.

Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (13)

44-56: Fallback host URL expectations now correctly align with ARTDomainSelector defaults

The updated comment and regex expectations for testUsesAlternativeHostOnResponse (default main.realtime.ably.net and main.[a-e].fallback.ably-realtime.com) match the new default primary domain and fallback domains exposed via ARTDomainSelector. The sequencing (primary then fallback) is still validated as before and looks consistent with REC2.


181-195: Default realtime connection host assertion matches new primary domain

test__017__Connection__url__should_connect_to_the_default_host now asserts url.host == "main.realtime.ably.net". This is consistent with ARTDomainSelector’s default policy (main.realtime.ably.net), so the change keeps the test aligned with the new endpoint-first behavior.


2178-2203: Using a non-routable IP via endpoint is a good way to exercise RTN14c timeout behavior

Setting options.endpoint = "10.255.255.1" (non-routable) for RTN14c ensures the connection attempt goes to a single, unreachable host classified as a hostname/IP, so no automatic fallbacks are used. The subsequent timing assertions against realtimeRequestTimeout remain valid and cleanly exercise the timeout path under the new endpoint semantics.


3843-3890: Deprecation annotations around realtimeHost tests are appropriate

Marking the realtimeHost-based host-fallback tests as @available(*, deprecated, ...) avoids compiler warnings while keeping coverage for legacy options. This is a sensible transition pattern while endpoint becomes the preferred configuration surface.


4031-4071: REC2a2 custom fallbackHosts test remains correct after domain changes

test__089__Connection__Host_Fallback__applies_when_an_array_of_ClientOptions_fallbackHosts_is_provided now expects the first connection to main.realtime.ably.net and subsequent attempts to the explicit custom fallback hosts [f-j].ably-realtime.com. This matches the new default primary domain while still verifying that custom fallbackHosts fully override generated fallbacks.


4073-4111: Deprecated fallbackHostsUseDefault path still correctly uses generated default fallbacks

test__089b__...fallbackHostsUseDefault... now asserts that with fallbackHostsUseDefault = true and a custom port, the client first connects to main.realtime.ably.net and then a main.[a-e].fallback.ably-realtime.com host. That matches ARTDomainSelector.defaultFallbackDomains and confirms the deprecated flag still routes through the REC2c1-style defaults.


4189-4260: Helper _test__091 and wrappers correctly validate primary vs fallback hosts for different endpoints

The new _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net(endpoint:test:) plus its three wrappers verify:

  • endpoint == nil → first URL uses main.realtime.ably.net, then a main.[a-e].fallback.ably-realtime.com host.
  • endpoint == "nonprod:sandbox" → first URL uses sandbox.realtime.ably-nonprod.net, then sandbox.[a-e].fallback.ably-realtime-nonprod.com.
  • endpoint == "test" → first URL uses test.realtime.ably.net, then test.[a-e].fallback.ably-realtime.com.

This matches the primary/fallback logic in ARTDomainSelector for default, nonprod, and production routing-policy endpoints and provides good regression coverage for REC2c1/3/4 under the new endpoint API.


4264-4353: Host retry ordering tests correctly reuse domainSelector.fallbackDomains for prod/nonprod endpoints

_test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available now:

  • Drives fallback generation via options.domainSelector.fallbackDomains for the current endpoint (nil, "nonprod:sandbox", or "test").
  • Uses the injected shuffleArrayInExpectedHostOrder to produce a deterministic “random” sequence.
  • Extracts hostnames from both websocket URLs and the connectivity-check HTTP requests, verifying that the actual retry sequence exactly matches the expected fallback host list for each endpoint variant.

The three wrapper tests for prod/sandbox/test ensure this behavior is exercised for all relevant domain families. The logic looks consistent with the fallback-domain construction in ARTDomainSelector.


4471-4512: Empty fallbackHosts array correctly disables fallback behavior

test__095__Connection__Host_Fallback__won_t_use_fallback_hosts_feature_if_an_empty_array_is_provided confirms:

  • With fallbackHosts = [], a hostUnreachable error still causes CONNECTING → DISCONNECTED → CONNECTING transitions.
  • Only two connection attempts occur, both to main.realtime.ably.net, and no generated fallback hosts are used.

This matches the intended semantics that an explicit empty array means “no fallbacks”, and it aligns with ARTDomainSelector.fallbackDomains returning the provided empty list verbatim.


4835-4903: Reachability now correctly targets the primary domain from domainSelector

In tests 109 and 110, the assertions that reachability.host == client.internal.options.domainSelector.primaryDomain ensure that:

  • OS reachability checks are aligned with whatever REC1 primary domain is selected (default or endpoint-based).
  • Simulated reachability changes drive the expected DISCONNECTED transitions in both CONNECTING and CONNECTED states.

This is a good update from older environment/host assumptions and keeps network-change behavior tied to the same domain-selection source used elsewhere.


4948-4953: RTN20c test correctly pins reconnection host against the original primary domain

In test__106_b__...restart_the_pending_connection_attempt..., capturing let primaryDomain = options.domainSelector.primaryDomain before overriding options.endpoint with a non-routable IP and then setting options.testOptions.reconnectionRealtimeHost = primaryDomain ensures:

  • The initial failed connection uses the “bad” endpoint, exercising the reachability path.
  • The subsequent reconnection attempt uses the original, valid primary domain.

This keeps reconnection semantics coherent with domain selection while still allowing the test to force a CONNECTING → DISCONNECTED → CONNECTING cycle via an unreachable endpoint.


4546-4556: Fallback HTTP requests remain tied to the selected fallback host

In test__096__Connection__Host_Fallback__client_is_connected_to_a_fallback_host_endpoint_should_do_HTTP_requests_to_the_same_data_centre, the updated regex checking //main.[a-e].fallback.ably-realtime.com for the websocket URL, and then asserting that subsequent REST time requests go to the same host, matches the new default fallback-domain schema and keeps the “same data centre” invariant intact.


4180-4185: Bad-request (400) path correctly kept on primary host after domain changes

test__090__Connection__Host_Fallback__should_not_use_an_alternative_host_when_the_client_receives_a_bad_request now asserts that both connection attempts target main.realtime.ably.net. This continues to verify that 400-class fake responses do not trigger fallback-host usage under the new domain scheme.

Source/ARTDomainSelector.m (1)

148-214: Fallback-domain generation logic matches REC2 and is internally consistent

The fallbackDomains implementation:

  • Honors explicit fallbackHostsClientOption (including an explicit empty array) before any generated defaults.
  • Respects fallbackHostsUseDefault by routing through defaultFallbackDomains.
  • Generates:
    • Default: main.[a–e].fallback.ably-realtime.com.
    • Nonprod routing policy: <policyId>.[a–e].fallback.ably-realtime-nonprod.com.
    • Prod routing policy and legacy env: <policyId>.[a–e].fallback.ably-realtime.com.
    • Hostname/legacy host: no automatic fallbacks.

defaultFallbackDomains also correctly returns the main.[a–e].fallback.ably-realtime.com set. This aligns with the new tests and REC2 requirements.

Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (1)

18-26: Exposing domainSelector on ARTClientOptions is a clean internal API choice

The new readonly domainSelector property and +setDefaultEndpoint: class method give internal code and tests a single, well-defined source of truth for REC1/REC2 domain behavior. This avoids duplicating domain/fallback state on ARTClientOptions itself and matches the direction suggested in earlier reviews. The realtimeUrlForHost: helper is also a sensible internal utility for constructing alternate-host URLs without mutating options.

Source/ARTClientOptions.m (2)

105-126: DomainSelector invalidation on all relevant options looks correct and keeps host resolution coherent

Resetting _domainSelector in setEndpoint:, setEnvironment:, setRestHost:, setRealtimeHost:, setFallbackHosts:, and setFallbackHostsUseDefault: ensures that any change to the inputs that drive domain selection will force a fresh ARTDomainSelector instance next time domainSelector, primaryDomain, or fallbackDomains are accessed.

Given how much logic now sits inside ARTDomainSelector, this pattern is important to avoid stale host/fallback state leaking between configurations. The current implementation (lazy creation + explicit resets on all the right setters) looks consistent and appropriate.

Also applies to: 130-187, 284-300


217-265: copyWithZone correctly propagates new endpoint/domain-related state, assuming setEndpoint is gated as above

The updated copyWithZone::

  • Copies endpoint and connectivityCheckUrl, so the clone preserves the new configuration surface.
  • Copies _fallbackHosts and _fallbackHostsUseDefault via ivars, intentionally bypassing the setters (which enforce mutual-exclusion rules) — this is appropriate for a faithful copy.
  • Reapplies legacy overrides (restHost, realtimeHost, environment) only when the source ivars are non‑nil, so defaults are not “invented” on the clone.

Once setEndpoint: is updated to only enforce REC1b1 for non‑empty endpoints (see earlier comment), this method should no longer raise when copying legacy‑only options and will produce an options object semantically equivalent to the original.

Source/ARTDefault.m (3)

4-7: DomainSelector integration for default hosts/fallbacks looks consistent with the new endpoint-centric model

ARTDefault now uses ARTDomainSelector in:

  • +fallbackHostsWithEnvironment: to derive fallback domains for a given environment.
  • +fallbackHosts to get the default fallback set.
  • +restHost / +realtimeHost to resolve the primary default domain.

This mirrors how ARTClientOptions delegates domain selection, which should keep host and fallback behavior consistent across “static default” and “per‑client options” code paths. The constructor arguments (nil endpoint, nil custom hosts, fallbackHostsUseDefault:false) look coherent for “library defaults” that are independent of any particular client options instance.

Also applies to: 25-55


9-12: Mutable global defaults for TTL and message sizes are wired up correctly

The introduction of:

  • ARTConnectionStateTtl
  • ARTMaxProductionMessageSize
  • ARTMaxSandboxMessageSize

combined with:

  • +connectionStateTtl and +setConnectionStateTtl:
  • +maxMessageSize with DEBUG/!DEBUG switching
  • +setMaxMessageSize:, +setMaxProductionMessageSize:, +setMaxSandboxMessageSize:

preserves the prior behavior while allowing tests (and consumers, if needed) to adjust these values at runtime. The use of @synchronized(self) in the setters is sufficient to avoid racy updates on these simple globals in typical SDK usage.

Also applies to: 68-90, 92-118


13-13: connectivityCheckUrl constant and helper are straightforward and align with the new ClientOptions default

Defining:

NSString *const ARTDefaultConnectivityCheckUrl = @"internet-up.ably-realtime.com/is-the-internet-up.txt";
...
+ (NSString *)connectivityCheckUrl {
    return [NSString stringWithFormat:@"https://%@", ARTDefaultConnectivityCheckUrl];
}

and then wiring this into ARTClientOptions via _connectivityCheckUrl = [ARTDefault connectivityCheckUrl]; gives a single, centralized source of truth for the connectivity check endpoint. The https:// prefix is handled here, and the host/path remain unchanged from the existing “is-the-internet-up” check, so this looks good.

Also applies to: 128-130

@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 27, 2025 16:25 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 27, 2025 16:28 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Source/ARTDomainSelector.m (1)

66-118: Endpoint policy parsing is clear, but IPv6/IP literal detection remains too naive

The REC1 policy parsing (endpoint → env → restHost → realtimeHost → default) is straightforward and matches the documented comments. However, the “hostname vs routing policy” heuristic:

if ([endpointClientOption containsString:@"."] ||
    [endpointClientOption containsString:@"::"] ||
    [endpointClientOption isEqualToString:@"localhost"])

will still misclassify many valid IP literals as routing-policy IDs (e.g. full IPv6 addresses without "::", bracketed IPv6 with ports, IPv4 addresses, or IPv6 with zone IDs). That means a user passing an IP literal as endpoint may silently get routed via REC1b3/b4 rather than REC1b2.

Consider normalizing and validating the endpoint as an IP literal before deciding it’s a routing policy, for example:

  • Strip brackets ([::1]), ports (:8080), and any zone suffix (%eth0).
  • Use inet_pton for IPv4/IPv6 detection.
  • Treat any successfully parsed IP literal as a hostname (REC1b2 path), not as a routing policy.

Also consider guarding against an empty policy ID after nonprod: (e.g. endpoint = "nonprod:") to avoid generating ".realtime.ably-nonprod.net".

🧹 Nitpick comments (5)
Source/ARTRealtime.m (1)

217-237: ARTDomainSelector import here is benign but currently unused

ARTRealtimeInternal relies on getClientOptions].domainSelector rather than directly on ARTDomainSelector symbols, so this import isn’t strictly necessary. It’s harmless, but could be removed later if you’re cleaning up includes.

Source/ARTClientOptions.m (4)

90-99: Be careful that setEndpoint: + copyWithZone: don’t throw when simply copying or clearing options

setEndpoint:, setEnvironment:, setRestHost:, and setRealtimeHost: correctly enforce the REC1b1 / REC1c1 mutual‑exclusion rules for endpoint vs legacy options, and they reset _domainSelector when inputs change, which is good.

However, because:

  • setEndpoint: unconditionally checks self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost, and
  • copyWithZone: always does options.endpoint = self.endpoint before copying legacy fields,

there are edge cases where you may raise NSInvalidArgumentException even though the effective configuration doesn’t introduce a new endpoint. Examples to consider:

  • Copying an options instance configured only via environment / custom hosts but with a nil or default endpoint.
  • Clearing an endpoint (setting it to nil or an empty string) on an instance that already has legacy host/environment configured.

To make these flows safer while still enforcing REC1b1, consider:

  • Gating the conflict check in setEndpoint: on the incoming argument being non‑nil/non‑empty, e.g.:
- (void)setEndpoint:(NSString *)endpoint {
-    // REC1b1: endpoint cannot be used with deprecated options
-    if (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost) {
+ (void)setEndpoint:(NSString *)endpoint {
+    // REC1b1: endpoint cannot be used with deprecated options
+    if (endpoint && [endpoint isNotEmptyString] &&
+        (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost)) {
         [NSException raise:NSInvalidArgumentException
                     format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."];
     }
     _endpoint = endpoint;
     _domainSelector = nil;
 }
  • Alternatively, bypassing the setter when copying (options->_endpoint = self->_endpoint;) if you’re confident the source object already satisfied the invariants.

This would still reject genuinely mixed configurations, but avoid surprising exceptions when copying or clearing options.

Also applies to: 130-187, 217-248


193-215: rest/realtime URL helpers now share primaryDomain – confirm this matches desired host split

restUrlComponents, restUrl, and realtimeUrl now all derive their host from primaryDomain (either directly or via realtimeUrlForHost:), and restHost/realtimeHost fall back to domainSelector.primaryDomain when no custom host is set. This is internally consistent; just double‑check that ARTDomainSelector.primaryDomain is indeed the correct canonical host for both REST and Realtime in the ADR‑119 world (i.e. you don’t still expect a distinct “rest” vs “realtime” hostname pair by default). If separate defaults are still intended, you may want distinct helpers or a small abstraction around “rest primary domain” vs “realtime primary domain”.


302-305: setDefaultEndpoint: as a process‑wide default is fine but worth documenting as global state

Having a global ARTDefaultEndpoint that influences subsequent initDefaults calls is a reasonable way to support test setups and app‑wide defaults. It might be worth documenting (in comments or higher‑level docs) that this is global mutable state affecting all future ARTClientOptions instances, so it’s not something applications should typically change at runtime outside of controlled test harnesses.


319-324: isProductionEnvironment semantics no longer treat “no environment” as production

isProductionEnvironment currently returns:

return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProductionEnvironment lowercaseString]];

Given environment is nil by default, this now reports NO when no environment is configured, whereas historically “no environment set” has been equivalent to production. If any callers use !isProductionEnvironment to branch non‑production behavior, this change could misclassify the default configuration.

To preserve the usual meaning while still handling explicit environment overrides, consider:

- (BOOL)isProductionEnvironment {
-    return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProductionEnvironment lowercaseString]];
-}
+ (BOOL)isProductionEnvironment {
+    NSString *env = [self.environment lowercaseString];
+    NSString *prod = [ARTDefaultProductionEnvironment lowercaseString];
+    // Treat a nil/unspecified environment as production.
+    return (env == nil || [env isEqualToString:prod]);
+}

(or an equivalent implementation using hasEnvironment).

That keeps behavior intuitive for both unset and explicitly “production” environments.

Also applies to: 326-331

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 624b1d1 and 4059131.

📒 Files selected for processing (25)
  • .github/workflows/integration-test.yaml (1 hunks)
  • Ably.xcodeproj/project.pbxproj (9 hunks)
  • Examples/Tests/TestsTests/TestsTests.swift (3 hunks)
  • Scripts/log-environment-information.sh (1 hunks)
  • Source/ARTClientOptions.m (8 hunks)
  • Source/ARTDefault.m (4 hunks)
  • Source/ARTDomainSelector.m (1 hunks)
  • Source/ARTFallbackHosts.m (0 hunks)
  • Source/ARTRealtime.m (3 hunks)
  • Source/ARTRest.m (7 hunks)
  • Source/ARTWebSocketTransport.m (5 hunks)
  • Source/Ably.modulemap (1 hunks)
  • Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (2 hunks)
  • Source/PrivateHeaders/Ably/ARTDefault+Private.h (2 hunks)
  • Source/PrivateHeaders/Ably/ARTDomainSelector.h (1 hunks)
  • Source/PrivateHeaders/Ably/ARTFallbackHosts.h (0 hunks)
  • Source/include/Ably/ARTClientOptions.h (2 hunks)
  • Source/include/Ably/ARTDefault.h (1 hunks)
  • Source/include/module.modulemap (1 hunks)
  • Test/AblyTests/Test Utilities/TestUtilities.swift (4 hunks)
  • Test/AblyTests/Tests/ARTDomainSelectorTests.swift (1 hunks)
  • Test/AblyTests/Tests/PushAdminTests.swift (1 hunks)
  • Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (22 hunks)
  • Test/AblyTests/Tests/RealtimeClientTests.swift (1 hunks)
  • Test/AblyTests/Tests/RestClientTests.swift (35 hunks)
💤 Files with no reviewable changes (2)
  • Source/ARTFallbackHosts.m
  • Source/PrivateHeaders/Ably/ARTFallbackHosts.h
🚧 Files skipped from review as they are similar to previous changes (6)
  • Scripts/log-environment-information.sh
  • Source/Ably.modulemap
  • Test/AblyTests/Tests/PushAdminTests.swift
  • Source/include/module.modulemap
  • Test/AblyTests/Tests/ARTDomainSelectorTests.swift
  • Examples/Tests/TestsTests/TestsTests.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Test/AblyTests/Tests/RestClientTests.swift (1)
Test/AblyTests/Test Utilities/TestUtilities.swift (5)
  • match (1516-1524)
  • getTestEndpoint (626-635)
  • at (1431-1433)
  • at (1440-1445)
  • extract (1526-1537)
🪛 Clang (14.0.6)
Source/PrivateHeaders/Ably/ARTDomainSelector.h

[error] 1-1: 'Foundation/Foundation.h' file not found

(clang-diagnostic-error)

🪛 SwiftLint (0.57.0)
Test/AblyTests/Tests/RestClientTests.swift

[Warning] 181-181: TODOs should be resolved (this check is useless bc publi...)

(todo)


[Warning] 206-206: TODOs should be resolved (this check is useless bc publi...)

(todo)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build
  • GitHub Check: check (tvOS, test_tvOS18_4)
  • GitHub Check: check (macOS, test_macOS)
  • GitHub Check: check (iOS, test_iOS18_4)
  • GitHub Check: check
  • GitHub Check: check
🔇 Additional comments (44)
Ably.xcodeproj/project.pbxproj (1)

360-365: ARTDomainSelector added cleanly across file references, groups, and all targets

ARTDomainSelector.m/.h have matching PBXFileReference and PBXBuildFile entries, are placed under the Utilities group, and are correctly wired into the iOS/macOS/tvOS Sources and Headers phases as Private headers with no dangling IDs. This wiring looks consistent and should compile fine.

Also applies to: 1224-1225, 1993-1994, 2142-2142, 2318-2318, 2527-2527, 2965-2967, 3110-3111, 3254-3255

Test/AblyTests/Test Utilities/TestUtilities.swift (3)

115-141: Using options.domainSelector.primaryDomain for app creation looks correct

Switching the test app creation URL to options.domainSelector.primaryDomain keeps tests aligned with the REC1/endpoint resolution path, and avoids coupling tests to legacy rest/realtime host behavior. No issues spotted here.


145-159: Endpoint-based test client options wiring is consistent

Setting options.endpoint = getTestEndpoint() is the right way to feed the new domain selection path in tests. The rest of the options setup (debug, key, token, dispatch queues) remains unchanged and compatible.


598-607: JWT helper no longer passes environment – confirm this is intentional

Dropping any environment-related query param and only sending encrypted (plus keyName/keySecret/etc.) changes how the echo server might infer routing. If the echo endpoint now relies purely on the API key and global routing config, this is fine; otherwise it might still expect an environment-like hint.

Can you confirm the echo JWT service used here is independent of environment/endpoint inputs and that no server-side logic relied on the old env param?

Source/ARTDomainSelector.m (3)

120-146: Primary-domain construction matches REC1 semantics

The switch in primaryDomain correctly yields:

  • Default: main.realtime.ably.net
  • Hostname: the raw hostname
  • Non-production routing policy: <policyId>.realtime.ably-nonprod.net
  • Production routing / legacy environment: <policyId>.realtime.ably.net
  • Legacy host: the raw hostname

This aligns with the policy comments and REC1* references; no issues found.


148-204: Fallback-domain generation is consistent with REC2 and legacy behaviors

The fallback selection order—explicit fallbackHostsClientOptionfallbackHostsUseDefault → policy-derived fallbacks—is sensible and matches the REC2a/b/c comments. For routing-policy and legacy-environment cases, generating <policyId>.[a–e].fallback.<tld> looks correct, and returning @[] for hostname/legacy-host is appropriate (no implicit fallbacks on arbitrary hostnames).


206-214: defaultFallbackDomains implementation & doc are now aligned

The helper now clearly documents and returns ["main.a.fallback.ably-realtime.com", ..., "main.e.fallback.ably-realtime.com"], matching the internal constants and REC2 default behavior. All good here.

Source/ARTDefault.m (4)

4-14: New default constants and connectivity URL are coherent

The introduction of ARTDefaultAPIVersion, ARTDefaultProductionEnvironment, connection-state TTL, production/sandbox message sizes, and ARTDefaultConnectivityCheckUrl consolidates mutable defaults into explicit globals. The connectivity check URL https://internet-up.ably-realtime.com/is-the-internet-up.txt matches the new non-sandbox infrastructure and is suitable as a single canonical check endpoint.


25-36: Using ARTDomainSelector from ARTDefault matches endpoint/env semantics

fallbackHostsWithEnvironment:, fallbackHosts, restHost, and realtimeHost now delegate to ARTDomainSelector with endpoint=nil and (optionally) environment, which:

  • Preserves legacy environment behavior via the LegacyEnvironment policy.
  • Makes the no-env case return the REC1 default primary domain and REC2 default fallbacks.

This keeps ARTDefault’s public API consistent with the new endpoint-first domain resolution.


68-118: Max message size & TTL setters map cleanly onto new globals

connectionStateTtl, maxMessageSize, maxSandboxMessageSize, maxProductionMessageSize and the corresponding setters correctly manipulate the new global variables, with DEBUG builds using sandbox limits and release builds using production limits. Synchronization via @synchronized(self) is sufficient for these rarely-mutated shared values.


128-130: connectivityCheckUrl helper is simple and sufficient

Returning a formatted https:// URL from ARTDefaultConnectivityCheckUrl is straightforward and matches how ARTRest.internetIsUp is expected to work. No issues here.

.github/workflows/integration-test.yaml (1)

32-36: CI ABLY_ENV value now matches endpoint-style nonprod routing policy

Updating ABLY_ENV to nonprod:sandbox is consistent with getTestEndpoint() (which uses ABLY_ENV when ABLY_ENDPOINT is unset) and with the new non-production routing-policy semantics from ADR-119. This should exercise the DomainSelector nonprod path in integration tests as intended.

Source/ARTWebSocketTransport.m (4)

69-71: Introducing _host ivar is a good decoupling step

Storing the host separately from options avoids the previous pattern where the transport mutated client options just to change its hostname. This makes later host overrides (e.g. fallbacks) more straightforward.


76-90: Initializing _host from options.domainSelector.primaryDomain is correct

Seeding _host with options.domainSelector.primaryDomain at construction ensures WebSocket transports always use the REC1-resolved primary domain by default, independent of legacy rest/realtimeHost options. Copying options afterward keeps other option values stable for the lifetime of the transport.


195-208: WebSocket URL now correctly respects the transport’s current host

Switching to:

NSURL *url = [urlComponents URLRelativeToURL:[options realtimeUrlForHost:self.host]];

means reconnects and fallbacks that call setHost: will correctly affect subsequent WebSocket URLs without mutating options. This aligns the transport’s behavior with the new DomainSelector-driven host model.


251-257: Host getter/setter now operate purely on transport-local state

setHost: and host now wrap the _host ivar instead of reading/writing client options. That keeps responsibility for the active host within the transport and avoids side effects on shared configuration.

Source/include/Ably/ARTDefault.h (1)

13-17: Public API additions for fallback hosts align with DomainSelector usage

Exposing +fallbackHosts and +fallbackHostsWithEnvironment: here matches the implementation in ARTDefault.m and gives SDK consumers a REC2-compliant way to inspect fallback behavior. Using the same restHost/realtimeHost signatures keeps the public surface stable.

Source/PrivateHeaders/Ably/ARTDefault+Private.h (1)

3-14: Private default and connectivity accessors are consistent with implementation

Declaring ARTDefaultProductionEnvironment, the max message size accessors, setConnectionStateTtl:, setMaxMessageSize:, and connectivityCheckUrl here matches the backing symbols and methods in ARTDefault.m. This provides the expected private hooks for tests and internal helpers without altering the public surface.

Source/ARTRealtime.m (3)

1497-1519: Fallback reconnection behavior remains consistent with new host source

reconnectWithFallback still operates on _fallbacks as before, and only the population of _fallbacks has changed elsewhere to use domainSelector.fallbackDomains. The reconnect sequence itself (internet check, prioritizedHost override, reconnect) is unchanged and remains compliant with RTN17’s requirements.


1521-1528: RSC15m check now correctly depends on the presence of DomainSelector fallbacks

Updating shouldRetryWithFallbackForError:options: to:

// RSC15m
return options.domainSelector.fallbackDomains.count > 0;

ensures fallback retries are only considered when the configured endpoint/env/hosts actually yield a non-empty fallback set. This nicely decouples RSC15m from legacy ARTFallbackHosts logic and aligns it with ADR-119’s endpoint semantics.


1671-1688: Realtime fallback initialization now uses domainSelector.fallbackDomains

When the transport fails with a retryable error and no _fallbacks exist, initializing:

NSArray *hosts = clientOptions.domainSelector.fallbackDomains;
_fallbacks = [[ARTFallback alloc] initWithFallbackHosts:hosts shuffleArray:clientOptions.testOptions.shuffleArray];

replaces ARTFallbackHosts-based lookup with DomainSelector-derived fallbacks. This maintains shuffle behavior and cleanly supports:

  • Default/routing-policy/env endpoints → generated fallback list.
  • Hostname/legacy host → empty fallback list, skipping fallback attempts.

Behavior for empty _fallbacks is already handled below, so this change looks correct.

Test/AblyTests/Tests/RealtimeClientTests.swift (1)

156-157: Appropriate deprecation annotation for testing deprecated API.

The @available(*, deprecated, ...) annotation correctly suppresses compiler warnings while testing the deprecated realtimeHost property. The message clearly documents why this test is marked deprecated and when the annotation should be removed.

Source/PrivateHeaders/Ably/ARTDomainSelector.h (1)

1-42: Well-designed domain selector interface.

The ARTDomainSelector interface is clean and well-documented:

  • Clear parameter documentation for the initializer
  • Appropriate use of nullable annotations
  • Correctly disables default init with NS_UNAVAILABLE
  • Read-only properties for primaryDomain and fallbackDomains align with the REC1/REC2 specifications

The static analysis error about Foundation/Foundation.h not being found is a false positive from running clang in isolation without the proper SDK paths.

Source/ARTRest.m (3)

376-382: Correct fallback retry expiration handling with domain selector.

The reset logic correctly clears currentFallbackHost and prioritizedHost, then switches back to domainSelector.primaryDomain when the fallback retry timeout expires. This aligns with RSC15f.


449-462: Fallback host resolution correctly migrated to ARTDomainSelector.

The fallback mechanism now sources hosts from domainSelector.fallbackDomains instead of the removed ARTFallbackHosts. The RSC15j spec reference for setting the Host header is correctly placed.


677-690: Connectivity check URL is now configurable per REC3a/REC3b.

The implementation correctly uses options.connectivityCheckUrl with a fallback to ARTDefault.connectivityCheckUrl. The inline spec references (REC3a, REC3b) help document the behavior.

Test/AblyTests/Tests/RestClientTests.swift (6)

45-46: Test URL patterns correctly updated for new domain scheme.

The fallback host tests now correctly expect main.realtime.ably.net for the primary domain and main.[a-e].fallback.ably-realtime.com for fallback domains, aligning with the ADR-119 implementation.


316-351: Comprehensive REC1b tests for endpoint option behavior.

These tests properly validate the endpoint option behavior:

  • REC1b1: Invalid to combine endpoint with deprecated options
  • REC1b2: FQDN endpoint becomes primary domain directly
  • REC1b3: Nonprod routing policy resolves correctly
  • REC1b4: Production routing policy resolves correctly

The deprecation annotations are appropriately applied where deprecated APIs are used.


1069-1095: REC2c4 test now correctly validates all three request URLs.

The assertion indices are now correct: at(0) for the primary domain, at(1) and at(2) for the two fallback requests. This addresses the issue from the previous review where both fallback assertions incorrectly used index 1.


1185-1253: Comprehensive REC2c tests for implicit fallback domain resolution.

These tests thoroughly validate the fallback domain resolution rules:

  • REC2c1: Default fallback domains (main.[a-e].fallback.ably-realtime.com)
  • REC2c2: Explicit domain → empty fallback set
  • REC2c3: Nonprod routing policy fallback domains
  • REC2c4: Production routing policy fallback domains
  • REC2c5: Deprecated environment option fallback domains
  • REC2c6: Deprecated restHost/realtimeHost → empty fallback set

181-181: TODO tracked in issue #2145.

The TODO comment about the ineffective assertion is already tracked in GitHub issue #2145, as noted in the past review comments.


206-206: TODO tracked in issue #2145.

Same as above - this TODO is covered by the existing GitHub issue.

Source/include/Ably/ARTClientOptions.h (3)

22-26: Well-documented endpoint property addition.

The endpoint property documentation clearly explains the three accepted value types (routing policy name, nonprod routing policy name, or FQDN) and links to the Platform Customization documentation.


30-38: Consistent deprecation messaging for legacy host properties.

Both restHost and realtimeHost are properly deprecated with clear messages directing users to the endpoint option. The @deprecated Doxygen tag in the doc comment and the DEPRECATED_MSG_ATTRIBUTE macro provide both documentation and compiler warnings.


61-64: connectivityCheckUrl is properly copied in -copyWithZone:.

Verification confirms that the connectivityCheckUrl property is correctly assigned at line 239 of Source/ARTClientOptions.m within the -copyWithZone: method implementation (options.connectivityCheckUrl = self.connectivityCheckUrl;). No action needed.

Test/AblyTests/Tests/RealtimeClientConnectionTests.swift (6)

24-56: Host and fallback expectations align with new endpoint semantics

The updated regex expectations for the default primary host (main.realtime.ably.net) and fallback pattern (main.[a-e].fallback.ably-realtime.com), along with the analogous checks in the RTN17h/REC2* tests, correctly reflect the ADR‑119 routing model and keep the assertions implementation‑agnostic via regex rather than hardcoding specific letters. This looks solid and gives good coverage of the new default/fallback behavior.

Also applies to: 4055-4071, 4101-4111, 4180-4185


2180-2203: Using endpoint with a non‑routable IP cleanly exercises timeout behavior

Switching this test to drive failure via options.endpoint = "10.255.255.1" rather than older host/environment knobs is a good fit with ADR‑119. It keeps the test focused on realtime request timeout behavior while validating the endpoint surface for IPs. No issues spotted here.


4189-4242: Coverage for primary host selection across prod/nonprod/test endpoints looks good

The shared helper _test__091__...every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net plus the three concrete tests (default, nonprod:sandbox, test) nicely validate:

  • Primary host is always hit first.
  • Nonprod routing policy names map to the expected *.realtime.ably-nonprod.net / *.fallback.ably-realtime-nonprod.com domains.
  • Plain policy names (e.g. "test") map to the *.realtime.ably.net / *.fallback.ably-realtime.com domains.

The use of regex and dropFirst(8) for nonprod: is a reasonable way to keep tests robust against minor formatting changes.

Also applies to: 4244-4260


4264-4353: Fallback retry ordering tests strongly exercise domain selector + REC3 behavior

_test__092__...retry_hosts_in_random_order_after_checkin (and the prod/sandbox/test variants) give thorough coverage of:

  • Using domainSelector.fallbackDomains as the source of truth for retry ordering.
  • The interaction between the deterministic shuffleArrayInExpectedHostOrder test hook and the observed retry sequence.
  • The “internet up” check-in semantics between fallback host changes.

These tests appear well‑constructed and should quickly catch regressions in fallback ordering or REC3 connectivity‑check wiring.


4471-4512: Empty fallbackHosts correctly disables fallback behavior

test__095__...won_t_use_fallback_hosts_feature_if_an_empty_array_is_provided clearly documents and verifies that an explicitly empty fallbackHosts array disables fallback usage, even when the error type would otherwise qualify. The DataGatherer‑based assertion on connection attempts and states is precise and matches the intent described in the comments.


4837-4867: Reachability tests now correctly couple to domainSelector.primaryDomain

Updating the RTN20* tests to assert that TestReachability.host equals client.internal.options.domainSelector.primaryDomain (rather than legacy realtimeHost/environment fields) gives good end‑to‑end validation that:

  • Reachability is monitoring the same primary domain that connections target under the endpoint/domain selector model.
  • OS‑driven connectivity changes transition the connection state machine as expected in both CONNECTING and CONNECTED scenarios, and in DISCONNECTED/SUSPENDED with reconnection.

This is a clean way to exercise the new domain selection pipeline via existing reachability hooks.

Also applies to: 4871-4903, 4919-4937, 4947-5013

Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (1)

7-8: Exposing domainSelector via the private header simplifies callers

Forward‑declaring ARTDomainSelector and exposing a readonly domainSelector plus realtimeUrlForHost: and +setDefaultEndpoint: on ARTClientOptions is a good cleanup compared to duplicating domain/fallback properties. It makes the intent behind domain selection clearer and keeps legacy host/environment handling encapsulated in ARTClientOptions.m. No issues from the interface perspective.

Also applies to: 18-26

Source/ARTClientOptions.m (2)

32-38: Endpoint and domainSelector initialization matches ADR‑119 defaults

The new ivars for _endpoint, _restHost, _realtimeHost, _environment, and _domainSelector, combined with initializing _endpoint from the process‑wide ARTDefaultEndpoint and _connectivityCheckUrl from ARTDefault, look consistent with the ADR‑119 goal of centralizing domain selection. Caching ARTDomainSelector and resetting it whenever inputs change is a sensible approach.

Also applies to: 48-54, 80-82


276-283: Domain selector invalidation on fallback changes is correct

The modifications to setFallbackHosts: and setFallbackHostsUseDefault: to reset _domainSelector ensure that any cached primary/fallback domains are recomputed when fallback configuration changes. This matches how you handle endpoint/environment/host changes and should keep the selector state consistent.

Also applies to: 284-301

@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 27, 2025 17:36 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 27, 2025 17:38 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 27, 2025 21:00 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 27, 2025 21:03 Inactive
@interface ARTDomainSelector ()

@property (nonatomic) ARTPrimaryDomainSelectionPolicy primaryDomainSelectionPolicy;
@property (nonatomic, nullable) NSString *policyId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you document these properties and explain for which primaryDomainSelectionPolicy values they are valid?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The policy that the library will use to determine its REC1 primary domain.
*/
typedef NS_ENUM(NSInteger, ARTPrimaryDomainSelectionPolicy) {
/// REC1a: The `endpoint` client option has not been specified.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each of these please can you document which of the policyId and hostname properties are relevant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going on in this commit "Fix detection of production environment"? Is it fixing something that was already broken or just a mistake in the implementation of the endpoint client option?

Also:

  1. why are we re-parsing the endpoint client option?
  2. when would the "environment" client option start with nonprod?
  3. what's even the purpose of this check? it seems like we are trying to impose a different outbound message size check for non-production environments (introduced in 9f4bb56?) but why? Realtime already has the ability to communicate a different maxMessageSize, why do we need to hardcode anything?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was broken and you are right, neither this check not the property itself is not needed (and was not needed). I've removed both, but let me know if you would like to see it in a separated PR (now I think the maxMessageSize test value is not needed in the SDK code itself either and should be set before each test that relies on it).

Copy link
Collaborator

@lawrence-forooghian lawrence-forooghian Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the change being in this PR given that it's in a separate commit, but doesn't it need to go before the endpoint commit? otherwise (and I haven't checked which) presumably one of the following is the case:

  1. you've "fixed" the non-prod check in the endpoint commit and then immediately removed it (seems unnecessary and as mentioned the previous "fix" for it didn't seem right), or
  2. you've not fixed the non-prod check in the endpoint commit in which case the endpoint commit is broken

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we still seem to have ARTMaxProductionMessageSize and ARTMaxSandboxMessageSize if I'm reading correctly?

@github-actions github-actions bot temporarily deployed to staging/pull/2109/features November 29, 2025 15:31 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/2109/jazzydoc November 29, 2025 15:33 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Source/ARTDefault.m (1)

24-53: Confirm DomainSelector configuration for fallbackHostsWithEnvironment: / restHost / realtimeHost

All three helpers construct a fresh ARTDomainSelector with endpointClientOption:nil, fallbackHostsClientOption:nil, the supplied (or nil) environmentClientOption, and fallbackHostsUseDefault:false, then use primaryDomain/fallbackDomains. That’s conceptually neat, but it subtly changes responsibility from ARTDefault/ARTFallbackHosts to ARTDomainSelector. Please confirm that:

  • fallbackDomains returns the expected legacy fallbacks for production and non‑prod environments with this argument set (especially with fallbackHostsUseDefault:false), and
  • restHost/realtimeHost returning the same primaryDomain matches your compatibility expectations for any caller that previously relied on distinct defaults.

If ARTDomainSelectorTests already cover these static helpers, that should be sufficient.

Source/ARTClientOptions.m (3)

22-38: Default endpoint & connectivity URL wiring are sensible; be aware of global state

Initialising _endpoint from the process‑wide ARTDefaultEndpoint and _connectivityCheckUrl from [ARTDefault connectivityCheckUrl] gives you a single place to adjust defaults (especially useful in tests). Just keep in mind that +setDefaultEndpoint: only affects instances created after the call; existing ARTClientOptions with their own endpoint or domainSelector won’t see that change. If this is intended purely for test setup, that behavior is fine, but it might be worth documenting that implicit lifecycle assumption where this helper is used.

Also applies to: 48-54, 80-81


130-191: Legacy environment / restHost / realtimeHost conflict checks are strict; consider allowing clears

The mutual‑exclusion rules between endpoint, environment, and the legacy host options in setEnvironment:, setRestHost:, and setRealtimeHost: are consistent with ADR‑119 and nicely symmetric. One side effect, though, is that these checks also run when the new value is nil (i.e. when a caller wants to clear an option), so you can’t currently “unset” environment while an endpoint is configured, or vice versa. That’s probably fine for production (options are usually set once), but it may be slightly awkward in tests or reconfiguration code. If you want to make clears cheaper, you could gate the conflict checks on non‑empty values, e.g. only run them when environment/host or endpoint are non‑empty strings.


284-305: Fallback hosts invalidation and default endpoint API are coherent; watch for test cross‑talk

Resetting _domainSelector in setFallbackHosts: and setFallbackHostsUseDefault: ensures primaryDomain/fallbackDomains always reflect the latest legacy fallback settings, and hasEnvironment correctly encapsulates the deprecated environment property. The simple +setDefaultEndpoint: setter for ARTDefaultEndpoint is fine for a process‑wide default, but because it’s a global mutable string, it’s easy for tests or embedding apps to accidentally leak configuration across call sites. If that becomes a problem, a follow‑up could introduce a small helper to save/restore the default endpoint around tests, or document a convention for doing so.

Also applies to: 319-324

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 799fece and 68b842f.

📒 Files selected for processing (5)
  • Source/ARTClientOptions.m (8 hunks)
  • Source/ARTConnection.m (1 hunks)
  • Source/ARTDefault.m (4 hunks)
  • Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (2 hunks)
  • Source/PrivateHeaders/Ably/ARTDefault+Private.h (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Source/PrivateHeaders/Ably/ARTDefault+Private.h
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: check (tvOS, test_tvOS18_4)
  • GitHub Check: check (iOS, test_iOS18_4)
  • GitHub Check: check (macOS, test_macOS)
  • GitHub Check: check
  • GitHub Check: build
  • GitHub Check: check
🔇 Additional comments (5)
Source/ARTConnection.m (1)

199-203: Confirm environment‑agnostic maxMessageSize default

When _maxMessageSize is unset, this now always returns [ARTDefault maxMessageSize], removing any prior prod vs sandbox branching. That looks consistent with the new endpoint/domain‑selector model, but it is a behavior change for clients relying on legacy sandbox defaults.

Please double‑check that ARTDefault.maxMessageSize matches the effective legacy defaults across all environments (prod, sandbox, nonprod), and consider adding a focused test to lock this behavior in.

Source/PrivateHeaders/Ably/ARTClientOptions+Private.h (1)

7-24: Private exposure of domainSelector/URL helpers is consistent and minimal

The added domainSelector property, setDefaultEndpoint: class method, and realtimeUrlForHost: helper line up cleanly with the implementation in ARTClientOptions.m and keep the internal surface focused while supporting the new endpoint/domain‑selection flow. No changes requested.

Source/ARTDefault.m (1)

6-13: Centralised defaults for API version, TTL, max message size, and connectivity URL look good

Extracting these values into module‑level constants and wiring connectionStateTtl, maxMessageSize, and connectivityCheckUrl through them makes the defaults clear and keeps the setters simple. The @synchronized writes on the scalar globals are adequate for the usual “tests tweak then restore” pattern. Please just double‑check that the concrete numbers for ARTConnectionStateTtl, ARTMaxProductionMessageSize/ARTMaxSandboxMessageSize, and the ARTDefaultConnectivityCheckUrl host/path are what you intend per ADR/spec/backend limits.

Also applies to: 67-81, 83-97, 107-109

Source/ARTClientOptions.m (2)

88-126: Endpoint / DomainSelector lazy initialisation and invalidation look correct

The setEndpoint: implementation enforcing REC1b1 against hasEnvironment / hasCustomRestHost / hasCustomRealtimeHost, coupled with resetting _domainSelector on change, and the lazy domainSelector that’s constructed from _endpoint, _fallbackHosts, _environment, _restHost, _realtimeHost, and _fallbackHostsUseDefault, all hang together well. primaryDomain/fallbackDomains simply forwarding into domainSelector makes the rest of the class read clearly. No changes requested here.


193-215: Host/URL builders now consistently use primaryDomain; confirm it is always a bare host

restUrlComponents using self.primaryDomain (rather than a legacy restHost resolver) and realtimeUrl delegating to realtimeUrlForHost:self.primaryDomain cleanly route both REST and Realtime through ARTDomainSelector. This looks correct as long as primaryDomain is always a hostname without scheme or port; if endpoint can ever be configured as a full URL or include a port, NSURLComponents.host would misinterpret it. From the surrounding code it appears endpoint is intended to be a routing policy/FQDN/IP, not a URL, but it would be good to ensure tests cover odd cases like IPv6 literals and non‑standard ports.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
Source/ARTDomainSelector.m (1)

70-98: Endpoint “hostname vs routing policy” detection is still too naïve (IPv6 and IP literals)

The endpointClientOption classification still only looks for ".", "::" or "localhost" to decide it’s a hostname:

if ([endpointClientOption containsString:@"."] ||
    [endpointClientOption containsString:@"::"] ||
    [endpointClientOption isEqualToString:@"localhost"]) {
    self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyHostname;
    self.hostname = endpointClientOption;
    return;
}

This will misclassify several legitimate endpoint forms:

  • IPv6 literals without ::, e.g. @"2001:db8:1:2:3:4:5:6" → treated as a routing policy ID and turned into 2001:db8:1:2:3:4:5:6.realtime.ably.net.
  • Bracketed IPv6 with ports, or IPv6 with zone IDs (e.g. [::1]:8080, fe80::1%en0), which don’t necessarily contain "::" in the raw string you want to interpret.
  • Any future IP/host formats that don’t contain "." but are still valid host tokens.

Given endpoint is intended to accept fully qualified hostnames and IP literals as well as routing policy IDs, this heuristic can lead to incorrect primary domains and bypass the “no automatic fallback for explicit host/IP” rule.

Consider:

  • Normalizing potential IP literals (strip brackets, ports, zone IDs), then using inet_pton for IPv4/IPv6 to decide “IP literal vs not”.
  • Treating “localhost” explicitly as you do now.
  • Falling back to a “contains '.' → hostname” rule when it’s not an IP literal.
  • Only then treating the residual cases as routing policy IDs (after the nonprod: prefix check).

This will avoid misclassifying valid IP endpoints as routing policies while keeping the current routing‑policy behavior intact.

🧹 Nitpick comments (1)
Source/ARTDomainSelector.m (1)

40-45: Minor docstring typos/wording for policyId and hostname

Two small nits:

  • Line 40: “primary and fallback domains names” → “primary and fallback domain names”.
  • Line 43: “Sores the value of the primary domain…” → “Stores the value of the primary domain…”.

Tidying these up will make the internal docs easier to read, especially since they explain which policies use policyId vs hostname.

-/// Prefix used for construction of the primary and fallback domains names (for `ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy`, `ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy` and `ARTPrimaryDomainSelectionPolicyLegacyEnvironment` values of ``primaryDomainSelectionPolicy``).
+/// Prefix used for construction of the primary and fallback domain names (for `ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy`, `ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy` and `ARTPrimaryDomainSelectionPolicyLegacyEnvironment` values of ``primaryDomainSelectionPolicy``).
@@
-/// Sores the value of the primary domain if assigned directly (for `ARTPrimaryDomainSelectionPolicyHostname` and `ARTPrimaryDomainSelectionPolicyLegacyHost` values of ``primaryDomainSelectionPolicy``).
+/// Stores the value of the primary domain if assigned directly (for `ARTPrimaryDomainSelectionPolicyHostname` and `ARTPrimaryDomainSelectionPolicyLegacyHost` values of ``primaryDomainSelectionPolicy``).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 68b842f and f9450ee.

📒 Files selected for processing (1)
  • Source/ARTDomainSelector.m (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: build
  • GitHub Check: check (macOS, test_macOS)
  • GitHub Check: check (iOS, test_iOS18_4)
  • GitHub Check: check (tvOS, test_tvOS18_4)
  • GitHub Check: check
  • GitHub Check: check
🔇 Additional comments (2)
Source/ARTDomainSelector.m (2)

124-145: Confirm LegacyEnvironment primary domain behavior vs ECO‑5551 requirement

For ARTPrimaryDomainSelectionPolicyLegacyEnvironment the primary domain is:

return [NSString stringWithFormat:@"%@.%@.%@",
        self.policyId, ARTDefaultRoutingSubdomain, ARTDefaultPrimaryTLD];

i.e. <environment>.realtime.ably.net.

ECO‑5551 / ADR‑119 state that when legacy options are explicitly configured, existing primary/fallback hostname behavior should be preserved. Switching primary for environment from the legacy domains to ably.net may be intentional, but if “preserve” is meant literally here, this path might need to stay on the legacy primary TLD instead.

Please double‑check this against ADR‑119 and the other language implementations (e.g. ably‑js / ably‑go) to ensure the LegacyEnvironment case matches the intended cross‑SDK behavior.


152-218: Fallback domain behavior and defaults look consistent with REC2

The fallback logic appears coherent and matches the described REC2 behavior:

  • Explicit fallbackHostsClientOption short‑circuits everything else.
  • Deprecated fallbackHostsUseDefault reuses defaultFallbackDomains.
  • Default policy and LegacyEnvironment/routing‑policy cases generate five ae fallbacks with the appropriate TLD:
    • Default: main.[a-e].fallback.ably-realtime.com.
    • Nonprod routing policy: <policy>.[a-e].fallback.ably-realtime-nonprod.com.
    • Production + legacy environment: <policy>.[a-e].fallback.ably-realtime.com.
  • Hostname/LegacyHost correctly return no automatic fallbacks.
  • defaultFallbackDomains’s comment now matches the actual return values.

This aligns with the earlier REC2 description and the “no fallbacks for explicit hosts” requirement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Use endpoint as default connection option (ADR-119)

3 participants