diff --git a/specifications/features.md b/specifications/features.md index f5a9abfc3..8e453c48f 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -619,7 +619,8 @@ The threading and/or asynchronous model for each realtime library will vary by l - `(RTN16k)` When the library first connects to Ably after being instantiated with a `recover` client option, it should add an additional `recover` querystring param to the websocket request, set from the `connectionKey` component of the `recoveryKey`. Once the library has successfully connected to Ably, it should never again supply a `recover` querystring param. - `(RTN16g)` `Connection#createRecoveryKey` is a function that returns a string which incorporates the `connectionKey`, the current `msgSerial`, and a collection of pairs of channel `name` and current `channelSerial` for every currently attached channel. - `(RTN16g1)` It must be serialized in a way which is able to encode any unicode channel name. The SDK may assume that the recovery key will only be consumed by the same type of SDK, so this spec does not specify any particular serialization; however, the format should be forward-compatible through the same major version of the SDK. - - `(RTN16g2)` It should return `Null` when the SDK is in the `CLOSED`, `CLOSING`, `FAILED`, or `SUSPENDED` states, or when it does not have a `connectionKey` (for example, it has not yet become connected). + - `(RTN16g2)` This clause has been replaced by [`RTN16g3`](#RTN16g3) as of specification version 6.1.0. + - `(RTN16g3)` It should return `Null` when the SDK is in the `CLOSED`, `CLOSING`, or `FAILED` states, or when it does not have a `connectionKey` (for example, it has not yet become connected). - `(RTN16d)` The library may wish to test that after a connection has been successfully recovered, the `Connection#id` should be identical to the `id` of the connection that was recovered, and `Connection#key` should have been updated to the `ConnectionDetails#connectionKey` provided in the `CONNECTED` `ProtocolMessage`. - `(RTN16m)` This clause has been deleted. It was valid up to and including specification version `2.1`. - `(RTN16m1)` This clause has been deleted. It was valid up to and including specification version `2.1`. diff --git a/uts/docs/completion-status.md b/uts/docs/completion-status.md index f3e39469a..e58bb40f8 100644 --- a/uts/docs/completion-status.md +++ b/uts/docs/completion-status.md @@ -174,14 +174,14 @@ This matrix lists all spec items from the [Ably features spec](../../specificati | RTN5 | Concurrency test (50+ clients) | | | RTN6 | Successful connection definition | Information only| | RTN7 | ACK and NACK handling (RTN7a–RTN7e) | Yes — `realtime/unit/channels/channel_publish.md` covers RTN7a, RTN7b (via RTL6j tests), RTN7d, RTN7e | -| RTN8 | Connection#id attribute (RTN8a–RTN8c) | Yes — `realtime/unit/connection/connection_id_key_test.md` | -| RTN9 | Connection#key attribute (RTN9a–RTN9c) | Yes — `realtime/unit/connection/connection_id_key_test.md` | +| RTN8 | Connection#id attribute (RTN8a–RTN8d) | Yes — `realtime/unit/connection/connection_id_key_test.md` | +| RTN9 | Connection#key attribute (RTN9a–RTN9d) | Yes — `realtime/unit/connection/connection_id_key_test.md` | | RTN11 | Connect function (RTN11a–RTN11f) | Partial — `realtime/integration/connection_lifecycle_test.md` covers RTN11; `realtime/unit/connection/error_reason_test.md` covers RTN11d | | RTN12 | Close function (RTN12a–RTN12f) | Partial — `realtime/integration/connection_lifecycle_test.md` covers RTN12, RTN12a | | RTN13 | Ping function (RTN13a–RTN13e) | Yes — `realtime/unit/connection/connection_ping_test.md` | -| RTN14 | Connection opening failures (RTN14a–RTN14g) | Yes — `realtime/unit/connection/connection_open_failures_test.md`; `realtime/integration/connection/connection_failures_test.md` covers RTN14a, RTN14g; `realtime/integration/auth/token_renewal_test.md` covers RTN14b; `realtime/integration/proxy/connection_open_failures.md` covers RTN14a–RTN14d, RTN14g | -| RTN15 | Connection failures when CONNECTED (RTN15a–RTN15j) | Yes — `realtime/unit/connection/connection_failures_test.md`; `realtime/integration/proxy/connection_resume.md` covers RTN15a, RTN15b, RTN15c6, RTN15c7, RTN15g, RTN15g2, RTN15h1, RTN15h3, RTN15j | -| RTN16 | Connection recovery (RTN16a–RTN16m1) | Yes — `realtime/unit/connection/connection_recovery_test.md` covers RTN16d, RTN16f, RTN16f1, RTN16g, RTN16g1, RTN16g2, RTN16i, RTN16j, RTN16k, RTN16l; `realtime/integration/proxy/connection_resume.md` covers RTN16d, RTN16l; `realtime/unit/connection/error_reason_test.md` covers RTN16e | +| RTN14 | Connection opening failures (RTN14a–RTN14h) | Yes — `realtime/unit/connection/connection_open_failures_test.md`; `realtime/integration/connection/connection_failures_test.md` covers RTN14a, RTN14g; `realtime/integration/auth/token_renewal_test.md` covers RTN14b; `realtime/integration/proxy/connection_open_failures.md` covers RTN14a–RTN14d, RTN14g; `realtime/unit/connection/connection_failures_test.md` and `realtime/integration/proxy/connection_resume.md` cover RTN14h | +| RTN15 | Connection failures when CONNECTED (RTN15a–RTN15j) | Yes — `realtime/unit/connection/connection_failures_test.md`; `realtime/integration/proxy/connection_resume.md` covers RTN15a, RTN15b, RTN15c6, RTN15c7, RTN15h1, RTN15h3, RTN15j | +| RTN16 | Connection recovery (RTN16a–RTN16m1) | Yes — `realtime/unit/connection/connection_recovery_test.md` covers RTN16d, RTN16f, RTN16f1, RTN16g, RTN16g1, RTN16g3, RTN16i, RTN16j, RTN16k, RTN16l; `realtime/integration/proxy/connection_resume.md` covers RTN16d, RTN16l; `realtime/unit/connection/error_reason_test.md` covers RTN16e | | RTN17 | Domain selection and fallback (RTN17a–RTN17j) | Yes — `realtime/unit/connection/fallback_hosts_test.md` | | RTN19 | Transport state side effects (RTN19a–RTN19b) | Yes — `realtime/unit/channels/channel_publish.md` covers RTN19a, RTN19a2, RTN19b; `realtime/integration/proxy/connection_resume.md` covers RTN19a, RTN19a2 | | RTN20 | OS network change handling (RTN20a–RTN20c) | Yes — `realtime/unit/connection/network_change_test.md` | @@ -398,7 +398,7 @@ This matrix lists all spec items from the [Ably features spec](../../specificati | RTN15a | Unexpected disconnect triggers resume | Yes — `realtime/integration/proxy/connection_resume.md` | | RTN15b/c6 | Resume preserves connectionId | Yes — `realtime/integration/proxy/connection_resume.md` | | RTN15c7 | Failed resume gets new connectionId | Yes — `realtime/integration/proxy/connection_resume.md` | -| RTN15g/g2 | connectionStateTtl expiry clears resume state | Yes — `realtime/integration/proxy/connection_resume.md` | +| RTN14h | Reconnection still resumes after connectionStateTtl expiry | Yes — `realtime/integration/proxy/connection_resume.md` | | RTN15h1 | DISCONNECTED with token error, non-renewable → FAILED | Yes — `realtime/integration/proxy/connection_resume.md` | | RTN15h3 | DISCONNECTED with non-token error → reconnect | Yes — `realtime/integration/proxy/connection_resume.md` | | RTN15j | Fatal ERROR on established connection | Yes — `realtime/integration/proxy/connection_resume.md` | diff --git a/uts/realtime/integration/proxy/connection_resume.md b/uts/realtime/integration/proxy/connection_resume.md index 820cc990f..90fddc848 100644 --- a/uts/realtime/integration/proxy/connection_resume.md +++ b/uts/realtime/integration/proxy/connection_resume.md @@ -1,6 +1,6 @@ # Connection Resume and Recovery Proxy Integration Tests (RTN15, RTN16) -Spec points: `RTN15a`, `RTN15b`, `RTN15c6`, `RTN15c7`, `RTN15g`, `RTN15g2`, `RTN15h1`, `RTN15h3`, `RTN15j`, `RTN16d`, `RTN16l`, `RTN19a`, `RTN19a2` +Spec points: `RTN14h`, `RTN15a`, `RTN15b`, `RTN15c6`, `RTN15c7`, `RTN15h1`, `RTN15h3`, `RTN15j`, `RTN16d`, `RTN16l`, `RTN19a`, `RTN19a2` ## Test Type @@ -759,18 +759,17 @@ session.close() --- -## Test 22: RTN15g/g2 - connectionStateTtl expiry clears resume state +## Test 22: RTN14h - reconnection still resumes after connectionStateTtl expiry -**Test ID**: `realtime/proxy/RTN15g/ttl-expiry-clears-resume-0` +**Test ID**: `realtime/proxy/RTN14h/resume-after-ttl-expiry-0` | Spec | Requirement | |------|-------------| -| RTN15g | If disconnected for longer than connectionStateTtl, do not attempt resume; connect as a fresh connection | -| RTN15g2 | The staleness measure is whether the time since last activity exceeds connectionStateTtl + maxIdleInterval | +| RTN14h | Reconnection attempts in the SUSPENDED state should continue to attempt to resume, regardless of how long it has been since the client was last connected | -Tests that when the client has been disconnected for longer than connectionStateTtl + maxIdleInterval, the SDK does not attempt to resume. Instead it makes a fresh connection, resulting in a new connectionId. +As of specification version 6.1.0 this replaces RTN15g (deleted). The client no longer clears its connection state based on how long it has been disconnected: it always attempts to resume and lets the server decide whether continuity can be preserved. This test verifies that the reconnection attempt made after the connectionStateTtl has expired and the connection has become SUSPENDED still carries the `resume` query param. (The real server will typically have discarded the connection state by then, so the resume fails server-side and the client ends up with a new connectionId — but the client still attempts the resume.) -**Unit test counterpart:** `connection_failures_test.md` > RTN15g +**Unit test counterpart:** `connection_failures_test.md` > RTN14h ### Setup @@ -805,19 +804,19 @@ session = create_proxy_session( } }, "times": 1, - "comment": "RTN15g: Replace 1st CONNECTED to set short connectionStateTtl (2s) and known connectionId" + "comment": "RTN14h: Replace 1st CONNECTED to set short connectionStateTtl (2s) and known connectionId" }, { "match": { "type": "delay_after_ws_connect", "delayMs": 1000 }, "action": { "type": "close" }, "times": 1, - "comment": "RTN15g: Close connection after 1s — client enters DISCONNECTED with 2s TTL" + "comment": "RTN14h: Close connection after 1s — client enters DISCONNECTED with 2s TTL" }, { "match": { "type": "ws_connect", "count": 2 }, "action": { "type": "refuse_connection" }, "times": 1, - "comment": "RTN15g: Refuse 2nd ws_connect — keeps client disconnected until TTL expires and SUSPENDED fires" + "comment": "RTN14h: Refuse 2nd ws_connect — keeps client disconnected until TTL expires and SUSPENDED fires" } ] ) @@ -853,8 +852,9 @@ ASSERT client.connection.id == "proxy-ttl-test-id" # T=1s: proxy closes connection -> DISCONNECTED # T=1-3s: retry attempt is refused -> stays DISCONNECTED # T=3s: connectionStateTtl(2s) expires -> SUSPENDED -# T=4s: suspendedRetryTimeout(1s) fires -> fresh ws_connect (no resume) -# -> CONNECTED with new connectionId from real server +# T=4s: suspendedRetryTimeout(1s) fires -> ws_connect that still attempts a +# resume (RTN14h); the server has discarded the state, so it responds +# with a new connectionId -> CONNECTED AWAIT_STATE client.connection.state == ConnectionState.suspended WITH timeout: 15s @@ -867,7 +867,8 @@ AWAIT_STATE client.connection.state == ConnectionState.connected ### Assertions ```pseudo -# RTN15g: Connection ID changed (fresh connection, not resumed) +# The server discarded the connection state, so the resume failed server-side +# and the connection ID changed. ASSERT client.connection.id != "proxy-ttl-test-id" # Verify the proxy log shows at least 3 ws_connects @@ -878,9 +879,10 @@ ASSERT ws_connects.length >= 3 # First ws_connect: initial — no resume ASSERT ws_connects[0].queryParams["resume"] IS null -# Last ws_connect: fresh connection after TTL expiry — no resume +# RTN14h: the reconnection made after the TTL expired and the connection became +# suspended still attempts a resume, so it carries the resume query param. last_ws_connect = ws_connects[ws_connects.length - 1] -ASSERT last_ws_connect.queryParams["resume"] IS null +ASSERT last_ws_connect.queryParams["resume"] IS NOT null ``` ### Cleanup diff --git a/uts/realtime/unit/channels/channel_attach.md b/uts/realtime/unit/channels/channel_attach.md index b86021b11..1cbe5b286 100644 --- a/uts/realtime/unit/channels/channel_attach.md +++ b/uts/realtime/unit/channels/channel_attach.md @@ -644,7 +644,7 @@ CLOSE_CLIENT(client) **Spec requirement:** The ATTACH ProtocolMessage channelSerial field must be set to the RTL15b channelSerial. If the RTL15b channelSerial is not set, the field may be set to null or omitted. -Tests that channelSerial is included in ATTACH message when available. Uses setOptions (RTL16a) to trigger a reattach without going through DETACHED state, since RTL15b1 clears channelSerial on DETACHED. +Tests that channelSerial is included in ATTACH message when available. Uses setOptions (RTL16a) to trigger a reattach without going through DETACHED state, since RTL15b2 clears channelSerial on DETACHED. ### Setup ```pseudo @@ -678,7 +678,7 @@ AWAIT_STATE client.connection.state == ConnectionState.connected AWAIT channel.attach() # Trigger reattach via setOptions (RTL16a) — does NOT go through DETACHED, -# so channelSerial is preserved (RTL15b1 only clears on DETACHED/SUSPENDED/FAILED) +# so channelSerial is preserved (RTL15b2 only clears on DETACHED/FAILED) AWAIT channel.setOptions(ChannelOptions(modes: [subscribe])) ``` @@ -902,13 +902,13 @@ CLOSE_CLIENT(client) --- -## RTL4j - ATTACH_RESUME flag set for reattach +## RTL4j - ATTACH_RESUME flag no longer set -**Test ID**: `realtime/unit/RTL4j/attach-resume-flag-0` +**Test ID**: `realtime/unit/RTL4j/attach-resume-flag-not-set-0` -**Spec requirement:** If the attach is not a clean attach, the library should set the ATTACH_RESUME flag in the ATTACH message. Per RTL4j1, `attachResume` is cleared when the channel enters DETACHING or FAILED, so a detach+reattach IS a clean attach and should NOT have ATTACH_RESUME. A reattach while still attached (e.g. via setOptions) is NOT a clean attach and SHOULD have ATTACH_RESUME. +**Spec requirement:** As of specification version 6.1.0, RTL4j has been deleted: the server now makes the resumability decision, so the client need not set the (deprecated) ATTACH_RESUME flag (TR3f) on any ATTACH, including reattaches of a previously-attached channel. -Tests that ATTACH_RESUME flag is set on reattach while attached, but not on a clean attach. +Tests that the ATTACH_RESUME flag is set on neither a clean attach nor a reattach while attached. ### Setup ```pseudo @@ -947,9 +947,8 @@ AWAIT channel.setOptions(params: {rewind: "1"}) ### Assertions ```pseudo ASSERT length(captured_attach_messages) == 2 -# First attach should NOT have ATTACH_RESUME flag +# Neither ATTACH should have the ATTACH_RESUME flag set ASSERT (captured_attach_messages[0].flags AND 32) == 0 # ATTACH_RESUME = 32 -# Second attach (reattach while attached) SHOULD have ATTACH_RESUME flag -ASSERT (captured_attach_messages[1].flags AND 32) != 0 # ATTACH_RESUME = 32 +ASSERT (captured_attach_messages[1].flags AND 32) == 0 # ATTACH_RESUME = 32 CLOSE_CLIENT(client) ``` diff --git a/uts/realtime/unit/channels/channel_properties.md b/uts/realtime/unit/channels/channel_properties.md index 4181bd244..23bd8e0b6 100644 --- a/uts/realtime/unit/channels/channel_properties.md +++ b/uts/realtime/unit/channels/channel_properties.md @@ -1,6 +1,6 @@ # Channel Properties Tests -Spec points: `RTL15`, `RTL15b`, `RTL15b1`, `RTL15c` +Spec points: `RTL15`, `RTL15b`, `RTL15b1`, `RTL15b2`, `RTL15c` ## Test Type Unit test with mocked WebSocket @@ -448,7 +448,7 @@ AWAIT_STATE channel.state == ChannelState.attached ```pseudo # channelSerial should be from the new ATTACHED, not from DETACHED # The DETACHED action should not have updated channelSerial -# (RTL15b1 clears it on DETACHED/SUSPENDED/FAILED, then ATTACHED sets it fresh) +# (RTL15b2 clears it on DETACHED/FAILED, then ATTACHED sets it fresh) ASSERT attach_count == 2 ASSERT channel.properties.channelSerial == "serial-001" CLOSE_CLIENT(client) @@ -456,15 +456,15 @@ CLOSE_CLIENT(client) --- -## RTL15b1 - channelSerial cleared on DETACHED state +## RTL15b2 - channelSerial cleared on DETACHED state -**Test ID**: `realtime/unit/RTL15b1/serial-cleared-detached-0` +**Test ID**: `realtime/unit/RTL15b2/serial-cleared-detached-0` | Spec | Requirement | |------|-------------| -| RTL15b1 | If the channel enters the DETACHED, SUSPENDED, or FAILED state, it must clear its channelSerial | +| RTL15b2 | If the channel enters the DETACHED or FAILED state, it must clear its channelSerial (unlike previous spec versions, it must not clear it when entering SUSPENDED) | -Tests that `channelSerial` is cleared when the channel transitions to DETACHED. +Tests that `channelSerial` is cleared when the channel transitions to DETACHED. (As of specification version 6.1.0, RTL15b1 has been replaced by RTL15b2.) ### Setup ```pseudo @@ -515,17 +515,19 @@ CLOSE_CLIENT(client) --- -## RTL15b1 - channelSerial cleared on SUSPENDED state +## RTL15b2 - channelSerial retained in SUSPENDED state -**Test ID**: `realtime/unit/RTL15b1/serial-cleared-suspended-1` +**Test ID**: `realtime/unit/RTL15b2/serial-retained-suspended-1` -**Spec requirement:** If the channel enters the SUSPENDED state, it must clear its `channelSerial`. +**Spec requirement:** If the channel enters the SUSPENDED state, it must NOT clear its `channelSerial`. -Tests that `channelSerial` is cleared when the channel transitions to SUSPENDED (e.g. due to attach timeout). +As of specification version 6.1.0 (RTL15b2, replacing RTL15b1) the channelSerial is cleared only when entering DETACHED or FAILED. It is retained through SUSPENDED, so that it can be included on the subsequent ATTACH (RTL4c1) for the server's continuity decision. + +Tests that `channelSerial` is retained when the channel transitions to SUSPENDED (e.g. due to attach timeout). ### Setup ```pseudo -channel_name = "test-RTL15b1-suspended-${random_id()}" +channel_name = "test-RTL15b2-suspended-${random_id()}" attach_count = 0 mock_ws = MockWebSocket( @@ -578,23 +580,24 @@ AWAIT_STATE channel.state == ChannelState.suspended ### Assertions ```pseudo ASSERT channel.state == ChannelState.suspended -ASSERT channel.properties.channelSerial IS null +# RTL15b2: channelSerial is retained through SUSPENDED, not cleared +ASSERT channel.properties.channelSerial == "serial-001" CLOSE_CLIENT(client) ``` --- -## RTL15b1 - channelSerial cleared on FAILED state +## RTL15b2 - channelSerial cleared on FAILED state -**Test ID**: `realtime/unit/RTL15b1/serial-cleared-failed-2` +**Test ID**: `realtime/unit/RTL15b2/serial-cleared-failed-2` **Spec requirement:** If the channel enters the FAILED state, it must clear its `channelSerial`. -Tests that `channelSerial` is cleared when the channel transitions to FAILED (e.g. due to channel ERROR). +Tests that `channelSerial` is cleared when the channel transitions to FAILED (e.g. due to channel ERROR). (As of specification version 6.1.0, RTL15b1 has been replaced by RTL15b2.) ### Setup ```pseudo -channel_name = "test-RTL15b1-failed-${random_id()}" +channel_name = "test-RTL15b2-failed-${random_id()}" mock_ws = MockWebSocket( onConnectionAttempt: (conn) => conn.respond_with_success(CONNECTED_MESSAGE), diff --git a/uts/realtime/unit/connection/connection_failures_test.md b/uts/realtime/unit/connection/connection_failures_test.md index ab4bb1932..294e4b2f1 100644 --- a/uts/realtime/unit/connection/connection_failures_test.md +++ b/uts/realtime/unit/connection/connection_failures_test.md @@ -1,6 +1,6 @@ # Connection Failures When Connected Tests (RTN15) -Spec points: `RTN15`, `RTN15a`, `RTN15b`, `RTN15c`, `RTN15d`, `RTN15e`, `RTN15g`, `RTN15h`, `RTN15j` +Spec points: `RTN14h`, `RTN15`, `RTN15a`, `RTN15b`, `RTN15c`, `RTN15d`, `RTN15e`, `RTN15h`, `RTN15j` ## Test Type Unit test with mocked WebSocket client @@ -808,16 +808,15 @@ ASSERT client.connection.key == "key-1-updated" --- -## RTN15g - Connection state cleared after connectionStateTtl +## RTN14h - Reconnection attempts still resume after connectionStateTtl -**Test ID**: `realtime/unit/RTN15g/state-cleared-after-ttl-0` +**Test ID**: `realtime/unit/RTN14h/resume-after-ttl-0` -**Spec requirement:** If disconnected longer than connectionStateTtl, don't attempt resume. Clear local state and make fresh connection. +**Spec requirement:** Reconnection attempts in the SUSPENDED state should continue to attempt to resume, regardless of how long it has been since the client was last connected. -Tests that stale connections don't attempt resume. After disconnecting, reconnection -attempts fail repeatedly, causing the client to eventually transition to SUSPENDED -(once connectionStateTtl expires). When the client eventually reconnects from -SUSPENDED state, it makes a fresh connection without resume parameters. +Tests that the client keeps attempting to resume after being disconnected for longer than the connectionStateTtl. This replaces RTN15g (deleted in specification version 6.1.0): the client no longer discards its connection state based on how long it has been disconnected. It always attempts to resume and lets the server decide whether continuity can be preserved. + +After disconnecting, reconnection attempts fail repeatedly, causing the client to transition to SUSPENDED (once connectionStateTtl expires) and then keep retrying. Every reconnection attempt — including those made after the TTL expired and the connection became suspended — carries the original connectionKey in a `resume` query param. > **Note on verifying transient states:** Rather than trying to observe intermediate > states (e.g. DISCONNECTED, SUSPENDED) mid-test with `AWAIT_STATE`, we record all @@ -835,7 +834,7 @@ mock_ws = MockWebSocket( onConnectionAttempt: (conn) => { connection_attempt_count++ captured_connection_attempts.append(conn) - + IF connection_attempt_count == 1: # Initial connection succeeds conn.respond_with_success() @@ -849,23 +848,9 @@ mock_ws = MockWebSocket( connectionStateTtl: 5000 # 5 seconds TTL ) )) - ELSE IF connection_attempt_count < 6: - # Reconnection attempts 2-5 fail (connection refused) - # This keeps the client retrying while TTL expires - conn.respond_with_refused() ELSE: - # After TTL expires, fresh connection succeeds (no resume) - conn.respond_with_success() - conn.send_to_client(ProtocolMessage( - action: CONNECTED, - connectionId: "connection-2", # New ID - connectionKey: "key-2", - connectionDetails: ConnectionDetails( - connectionKey: "key-2", - maxIdleInterval: 15000, - connectionStateTtl: 120000 - ) - )) + # All reconnection attempts fail, so the connection stays suspended + conn.respond_with_refused() } ) install_mock(mock_ws) @@ -899,53 +884,32 @@ client.connection.on((change) => { client.connect() AWAIT_STATE client.connection.state == ConnectionState.connected -original_connection_id = client.connection.id -original_connection_key = client.connection.key - # Force disconnect - triggers immediate reconnect per RTN15a ws_connection = mock_ws.events.find(e => e.type == CONNECTION_SUCCESS).connection ws_connection.simulate_disconnect() # Reconnection attempts keep failing (connection refused). -# Advance time in increments to allow retries, TTL expiry, -# transition to SUSPENDED, and eventual successful reconnection. +# Advance time well past the connectionStateTtl so the connection becomes +# suspended and then keeps attempting to reconnect. # TTL is 5000ms, disconnectedRetryTimeout is 1000ms, # suspendedRetryTimeout is 2000ms. -LOOP up to 15 times: +LOOP 15 times: ADVANCE_TIME(2500) - IF client.connection.state == ConnectionState.connected: - BREAK - -# Wait for final successful reconnection -AWAIT_STATE client.connection.state == ConnectionState.connected - WITH timeout: 5 seconds ``` ### Assertions ```pseudo -# Verify the full state change sequence includes SUSPENDED -# (TTL expired while reconnection attempts were failing) -ASSERT state_changes CONTAINS_IN_ORDER [ - ConnectionState.connecting, - ConnectionState.connected, - ConnectionState.disconnected, - ConnectionState.suspended, - ConnectionState.connecting, - ConnectionState.connected -] - -# RTN15g: New connection (different ID, not resumed - TTL expired) -ASSERT client.connection.id == "connection-2" -ASSERT client.connection.id != original_connection_id - -# Fresh connection key -ASSERT client.connection.key == "key-2" -ASSERT client.connection.key != original_connection_key - -# Final reconnection URL did NOT include resume parameter -# (because TTL expired and connection state was cleared) -ASSERT "resume" NOT IN captured_connection_attempts.last.url.query_params +# Verify the connection became suspended +ASSERT state_changes CONTAINS ConnectionState.suspended + +# RTN14h: every reconnection attempt after the first should still attempt a +# resume, carrying the original connectionKey, even those made after the TTL +# expired and the connection became suspended. +reconnect_attempts = captured_connection_attempts[1:] +ASSERT reconnect_attempts.length > 0 +FOR EACH attempt IN reconnect_attempts: + ASSERT attempt.url.query_params["resume"] == "key-1" CLOSE_CLIENT(client) ``` diff --git a/uts/realtime/unit/connection/connection_id_key_test.md b/uts/realtime/unit/connection/connection_id_key_test.md index d8bd66966..235761d85 100644 --- a/uts/realtime/unit/connection/connection_id_key_test.md +++ b/uts/realtime/unit/connection/connection_id_key_test.md @@ -1,6 +1,6 @@ # Connection ID and Key Tests -Spec points: `RTN8`, `RTN8a`, `RTN8b`, `RTN8c`, `RTN9`, `RTN9a`, `RTN9b`, `RTN9c` +Spec points: `RTN8`, `RTN8a`, `RTN8b`, `RTN8c`, `RTN8d`, `RTN9`, `RTN9a`, `RTN9b`, `RTN9c`, `RTN9d` ## Test Type Unit test with mocked WebSocket client @@ -213,15 +213,15 @@ CLOSE_CLIENT(client2) --- -## RTN8c - Connection ID is null in terminal/non-connected states +## RTN8d - Connection ID is null in terminal/non-connected states -**Test ID**: `realtime/unit/RTN8c/id-null-after-closed-0` +**Test ID**: `realtime/unit/RTN8d/id-null-after-closed-0` | Spec | Requirement | |------|-------------| -| RTN8c | Is null when the SDK is in CLOSED, CLOSING, FAILED, or SUSPENDED states | +| RTN8d | Is null when the SDK is in the CLOSED, CLOSING, or FAILED states | -Tests that `connection.id` is cleared when the connection enters CLOSED or FAILED states. +Tests that `connection.id` is cleared when the connection enters CLOSED or FAILED states. (As of specification version 6.1.0, RTN8c has been replaced by RTN8d: the id is no longer cleared in the SUSPENDED state.) ### Setup ```pseudo @@ -257,15 +257,15 @@ CLOSE_CLIENT(client) --- -## RTN9c - Connection key is null in terminal/non-connected states +## RTN9d - Connection key is null in terminal/non-connected states -**Test ID**: `realtime/unit/RTN9c/key-null-after-closed-0` +**Test ID**: `realtime/unit/RTN9d/key-null-after-closed-0` | Spec | Requirement | |------|-------------| -| RTN9c | Is null when the SDK is in CLOSED, CLOSING, FAILED, or SUSPENDED states | +| RTN9d | Is null when the SDK is in the CLOSED, CLOSING, or FAILED states | -Tests that `connection.key` is cleared when the connection enters CLOSED or FAILED states. +Tests that `connection.key` is cleared when the connection enters CLOSED or FAILED states. (As of specification version 6.1.0, RTN9c has been replaced by RTN9d: the key is no longer cleared in the SUSPENDED state.) ### Setup ```pseudo @@ -301,9 +301,9 @@ CLOSE_CLIENT(client) --- -## RTN8c, RTN9c - ID and key null after FAILED +## RTN8d, RTN9d - ID and key null after FAILED -**Test ID**: `realtime/unit/RTN8c/id-key-null-after-failed-1` +**Test ID**: `realtime/unit/RTN8d/id-key-null-after-failed-1` **Spec requirement:** Connection ID and key are null in FAILED state. @@ -342,20 +342,34 @@ CLOSE_CLIENT(client) --- -## RTN8c, RTN9c - ID and key null after SUSPENDED +## RTN8d, RTN9d - ID and key retained in SUSPENDED -**Test ID**: `realtime/unit/RTN8c/id-key-null-after-suspended-2` +**Test ID**: `realtime/unit/RTN8d/id-key-retained-in-suspended-2` -**Spec requirement:** Connection ID and key are null in SUSPENDED state. +**Spec requirement:** Connection ID and key are retained in the SUSPENDED state. -Tests that both `connection.id` and `connection.key` are null when the connection transitions to SUSPENDED. +As of specification version 6.1.0 (RTN8d/RTN9d, replacing RTN8c/RTN9c) the connection id and key are cleared only in the terminal states (CLOSED, CLOSING, FAILED). They are retained through SUSPENDED, since the client always attempts to resume on reconnecting (RTN14h) and lets the server decide whether continuity can be preserved. + +Tests that both `connection.id` and `connection.key` are retained when the connection transitions to SUSPENDED. ### Setup ```pseudo enable_fake_timers() +# Connect successfully on the first attempt, then refuse all reconnection +# attempts so the connection ends up suspended. +attempt = 0 mock_ws = MockWebSocket( - onConnectionAttempt: (conn) => conn.respond_with_refused() + onConnectionAttempt: (conn) => { + IF attempt == 0 THEN + conn.respond_with_success( + CONNECTED_MESSAGE(connectionId: "conn-id-1", connectionKey: "conn-key-1") + ) + ELSE + conn.respond_with_refused() + END + attempt = attempt + 1 + } ) install_mock(mock_ws) @@ -371,6 +385,12 @@ client = Realtime(options: ClientOptions( ### Test Steps ```pseudo client.connect() +AWAIT_STATE client.connection.state == ConnectionState.connected +ASSERT client.connection.id == "conn-id-1" +ASSERT client.connection.key == "conn-key-1" + +# Drop the transport; subsequent attempts are refused, moving to DISCONNECTED +mock_ws.active_connection.simulate_disconnect() AWAIT_STATE client.connection.state == ConnectionState.disconnected # Advance past connectionStateTtl to reach SUSPENDED @@ -380,7 +400,8 @@ AWAIT_STATE client.connection.state == ConnectionState.suspended ### Assertions ```pseudo -ASSERT client.connection.id IS null -ASSERT client.connection.key IS null +# RTN8d, RTN9d: id and key are retained, not cleared +ASSERT client.connection.id == "conn-id-1" +ASSERT client.connection.key == "conn-key-1" CLOSE_CLIENT(client) ``` diff --git a/uts/realtime/unit/connection/connection_recovery_test.md b/uts/realtime/unit/connection/connection_recovery_test.md index cc429b6e5..3bcf7875a 100644 --- a/uts/realtime/unit/connection/connection_recovery_test.md +++ b/uts/realtime/unit/connection/connection_recovery_test.md @@ -1,6 +1,6 @@ # Connection Recovery Tests (RTN16) -Spec points: `RTN16d`, `RTN16f`, `RTN16f1`, `RTN16g`, `RTN16g1`, `RTN16g2`, `RTN16i`, `RTN16j`, `RTN16k`, `RTN16l` +Spec points: `RTN16d`, `RTN16f`, `RTN16f1`, `RTN16g`, `RTN16g1`, `RTN16g2`, `RTN16g3`, `RTN16i`, `RTN16j`, `RTN16k`, `RTN16l` ## Test Type Unit test with mocked WebSocket client @@ -119,13 +119,13 @@ CLOSE_CLIENT(client) --- -## RTN16g2 - createRecoveryKey returns null in inactive states and before first connect +## RTN16g3 - createRecoveryKey returns null in inactive states and before first connect -**Test ID**: `realtime/unit/RTN16g2/recovery-key-null-inactive-0` +**Test ID**: `realtime/unit/RTN16g3/recovery-key-null-inactive-0` -**Spec requirement:** `createRecoveryKey()` should return null when the SDK is in the CLOSED, CLOSING, FAILED, or SUSPENDED states, or when it does not have a connectionKey (e.g. before first connect). +**Spec requirement:** `createRecoveryKey()` should return null when the SDK is in the CLOSED, CLOSING, or FAILED states, or when it does not have a connectionKey (e.g. before first connect). As of specification version 6.1.0 (RTN16g3, replacing RTN16g2) the connectionKey is retained through the SUSPENDED state (RTN8d/RTN9d), since the client always attempts to resume on reconnecting; the connection therefore remains recoverable while suspended and `createRecoveryKey()` returns a valid key rather than null. -Tests that `createRecoveryKey()` returns null in all the specified states. +Tests that `createRecoveryKey()` returns null in the terminal/pre-connect states, and a valid key in the SUSPENDED state. ### Setup @@ -180,7 +180,8 @@ ASSERT client.connection.createRecoveryKey() IS null ```pseudo # All null cases verified inline above. -# For FAILED and SUSPENDED states, create separate clients to test: +# For the FAILED state (null) and the SUSPENDED state (retained key), create +# separate clients to test: # --- Test FAILED state --- mock_ws_failed = MockWebSocket( @@ -260,7 +261,9 @@ LOOP up to 10 times: BREAK AWAIT_STATE client_suspended.connection.state == ConnectionState.suspended -ASSERT client_suspended.connection.createRecoveryKey() IS null +# RTN8d/RTN9d: connectionKey is retained, so the connection is still +# recoverable while suspended and createRecoveryKey() returns a key. +ASSERT client_suspended.connection.createRecoveryKey() IS NOT null CLOSE_CLIENT(client_suspended) ```