Skip to content

Conversation

ttypic
Copy link
Contributor

@ttypic ttypic commented Sep 3, 2025

Resolves #602

  • Introduced VCDiffDecoder abstract class and VCDiffPlugin implementation.
  • Enhanced delta message processing with proper support for VCDiff decoding.
  • Updated Options to accept a vcdiff_decoder.
  • Handle delta failures with recovery mechanisms (RTL18-RTL20 compliance).

Summary by CodeRabbit

  • New Features

    • Optional VCDiff delta decoding for Realtime channels; enable via channel options. Supports pluggable decoders with a default implementation. Install via the “vcdiff” extra (e.g., ably[vcdiff]).
    • Per-channel options API with the ability to update options and reattach only when required.
    • More robust message decoding with sequencing validation and automatic recovery on decode failures.
  • Tests

    • Added comprehensive tests covering VCDiff decoding, non-delta channels, failure recovery, and out-of-order handling.
  • Chores

    • Added optional dependency and extras for VCDiff; updated linter exclusions.

Copy link

coderabbitai bot commented Sep 3, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% 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-5456 feat: add VCDiff support for delta message decoding" is concise and accurately describes the primary change in this PR — adding VCDiff-based delta decoding support; it directly reflects the PR objectives and the code changes (decoder interface, default decoder, decode-path and realtime channel updates). The title is specific enough for a teammate scanning history to understand the main change without listing implementation details.
Linked Issues Check ✅ Passed The changes implement the linked-issue objectives: a VCDiffDecoder ABC is added (ably/types/options.py), a production decoder AblyVCDiffDecoder is provided (ably/vcdiff/default_vcdiff_decoder.py), and the decode pipeline is extended (EncodeDataMixin, Message, Presence, DecodingContext, RealtimeChannel) with recovery logic and tests; pyproject extras and tests support the external decoder dependency. These code-level changes enable the client to decode VCDiff delta-compressed messages and include RTL18-style failure handling, matching the stated goals from issues #602 and ECO-5456.
Out of Scope Changes Check ✅ Passed Most code changes are in-scope for adding VCDiff support, but a few non-functional project config edits appear outside the core objective: pyproject.toml adds an "experimental" package source pointing at test.pypi and setup.cfg expands flake8 excludes to include ably/sync and test/ably/sync; these affect packaging/test dependency resolution and linting rather than the decoding implementation. Test helpers and test modifications are expected and relevant to validating the feature.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ECO-5456/vcdiff-support

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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/620/features September 3, 2025 12:09 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from b15ae82 to 32ba990 Compare September 4, 2025 12:30
@ttypic ttypic marked this pull request as ready for review September 4, 2025 12:30
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 12:31 Inactive
@ttypic ttypic requested a review from sacOO7 September 4, 2025 12:34
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 12:35 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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/check.yml (1)

23-26: Upgrade checkout to actions/checkout@v4.

v2 runs on deprecated Node runtime and may break CI.

-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
🧹 Nitpick comments (14)
test/ably/utils.py (3)

186-198: Consider improving exception handling practices.

The current implementation catches all exceptions and re-raises a generic TimeoutError. While functional, this approach loses the original exception context which could be valuable for debugging.

Apply this diff to improve exception handling:

 async def assert_waiter(block: Callable[[], Awaitable[bool]], timeout: float = 10) -> None:
     """
     Polls a condition until it succeeds or times out.
     Args:
         block: A callable that returns a boolean indicating success
         timeout: Maximum time to wait in seconds (default: 10)
     Raises:
-        TimeoutError: If condition not met within timeout
+        asyncio.TimeoutError: If condition not met within timeout
     """
     try:
         await asyncio.wait_for(_poll_until_success(block), timeout=timeout)
     except asyncio.TimeoutError:
-        raise asyncio.TimeoutError(f"Condition not met within {timeout}s")
+        raise asyncio.TimeoutError(f"Condition not met within {timeout}s") from None

201-210: Consider adding debug logging for polling failures.

The current implementation silently ignores exceptions during polling, which can make debugging difficult. Consider logging failures while maintaining the polling behavior.

Apply this diff to add optional debug logging:

 async def _poll_until_success(block: Callable[[], Awaitable[bool]]) -> None:
     while True:
         try:
             success = await block()
             if success:
                 break
-        except Exception:
-            pass
+        except Exception as e:
+            # Log debug info but continue polling
+            import logging
+            logging.getLogger(__name__).debug(f"Polling condition failed: {e}")
 
         await asyncio.sleep(0.1)

213-235: Consider applying similar improvements to sync version.

The sync version has similar exception handling patterns. Consider applying the same improvements for consistency.

Apply this diff to improve the sync version:

 def assert_waiter_sync(block: Callable[[], bool], timeout: float = 10) -> None:
     """
     Blocking version of assert_waiter that polls a condition until it succeeds or times out.
     Args:
         block: A callable that returns a boolean indicating success
         timeout: Maximum time to wait in seconds (default: 10)
     Raises:
         TimeoutError: If condition not met within timeout
     """
     start_time = time.time()
 
     while True:
         try:
             success = block()
             if success:
                 break
-        except Exception:
-            pass
+        except Exception as e:
+            # Log debug info but continue polling
+            import logging
+            logging.getLogger(__name__).debug(f"Polling condition failed: {e}")
 
         if time.time() - start_time >= timeout:
-            raise TimeoutError(f"Condition not met within {timeout}s")
+            raise TimeoutError(f"Condition not met within {timeout}s") from None
 
         time.sleep(0.1)
ably/types/options.py (2)

24-26: Type-hint the new parameter for clarity

Annotate vcdiff_decoder to improve readability and editor support.

Apply:

+from typing import Optional
@@
-                 channel_retry_timeout=Defaults.channel_retry_timeout, add_request_ids=False, 
-                 vcdiff_decoder=None, **kwargs):
+                 channel_retry_timeout=Defaults.channel_retry_timeout, add_request_ids=False,
+                 vcdiff_decoder: Optional['VCDiffDecoder'] = None, **kwargs):

271-274: Expose a setter to allow runtime swap of the decoder

Optional quality-of-life improvement to support swapping the decoder without reconstructing Options.

Apply:

-    @property
-    def vcdiff_decoder(self):
-        return self.__vcdiff_decoder
+    @property
+    def vcdiff_decoder(self) -> Optional['VCDiffDecoder']:
+        return self.__vcdiff_decoder
+
+    @vcdiff_decoder.setter
+    def vcdiff_decoder(self, decoder: Optional['VCDiffDecoder']):
+        self.__vcdiff_decoder = decoder
ably/types/message.py (1)

193-194: Nit: trim exception message per lint hint (TRY003)

Shorten the message; the ID can go into cause or logs.

Apply:

-                raise AblyException(
-                    f"Delta message decode failure - previous message not available. Message id = {id}",
+                raise AblyException(
+                    "Delta message decode failure - previous message not available",
                     400,
                     40018,
                 )
ably/vcdiff_plugin.py (1)

46-50: Prefer log.exception for import failures

Captures traceback and reduces debugging time.

-        except ImportError as e:
-            log.error("vcdiff library not found. Install with: pip install ably[vcdiff]")
+        except ImportError as e:
+            log.exception("vcdiff library not found. Install with: pip install ably[vcdiff]")
             raise ImportError(
                 "VCDiff plugin requires vcdiff library. "
                 "Install with: pip install ably[vcdiff]"
             ) from e
ably/types/mixins.py (3)

71-74: Avoid one-line compound statements

Keeps style consistent and satisfies linters (E701).

-                data = bytearray(base64.b64decode(data)) if isinstance(data, bytes) \
-                    else bytearray(base64.b64decode(data.encode('utf-8')))
-                if not encoding_list: last_payload = data
+                data = bytearray(base64.b64decode(data)) if isinstance(data, bytes) \
+                    else bytearray(base64.b64decode(data.encode('utf-8')))
+                if not encoding_list:
+                    last_payload = data

123-123: Avoid one-line if

Minor readability/style improvement.

-        if context: context.base_payload = last_payload
+        if context:
+            context.base_payload = last_payload

23-28: Consider dataclass for DecodingContext

Reduces boilerplate and clarifies intent.

Example:

from dataclasses import dataclass
from typing import Optional, Any

@dataclass
class DecodingContext:
    base_payload: Optional[Any] = None
    last_message_id: Optional[str] = None
    vcdiff_decoder: Optional["VCDiffDecoder"] = None
ably/realtime/realtime_channel.py (2)

545-548: Consider optimizing the equality check for ChannelOptions.

The method correctly checks state and performs equality comparison. However, for better performance when options are identical objects, consider adding an identity check.

 def should_reattach_to_set_options(self, old_options: ChannelOptions, new_options: ChannelOptions) -> bool:
     if self.state != ChannelState.ATTACHING and self.state != ChannelState.ATTACHED:
         return False
+    # Fast path for identical objects
+    if old_options is new_options:
+        return False
     return old_options != new_options

607-612: Nitpick: Consider extracting error messages to constants.

The static analysis tool suggests avoiding long error messages inline. While not critical, extracting these to constants would improve maintainability.

# At module level
INVALID_OPTIONS_TYPE_ERROR = "options must be ChannelOptions instance or dictionary"
REATTACH_OPTIONS_ERROR = (
    'Channels.get() cannot be used to set channel options that would cause the channel to reattach. '
    'Please, use RealtimeChannel.setOptions() instead.'
)

# Then use in the method:
raise AblyException(INVALID_OPTIONS_TYPE_ERROR, 40000, 400)
ably/scripts/unasync.py (2)

242-243: Limit test-only token replacement to the test unasync pass.

assert_waiter exists in tests; mapping it during the source pass is unnecessary risk. Move this replacement into the “Test files” section alongside other test helpers.

Apply within this hunk:

-    _TOKEN_REPLACE["assert_waiter"] = "assert_waiter_sync"

Add near the test replacements (see next comment) to keep scope tight.


262-268: Add assert_waiterassert_waiter_sync mapping here (test pass).

Keeps all test helper rewrites together and avoids touching src code.

Apply near existing test token replacements:

     _TOKEN_REPLACE["asyncSetUp"] = "setUp"
     _TOKEN_REPLACE["asyncTearDown"] = "tearDown"
     _TOKEN_REPLACE["AsyncMock"] = "Mock"
+    _TOKEN_REPLACE["assert_waiter"] = "assert_waiter_sync"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 07d11a2 and d0ddba0.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/check.yml (1 hunks)
  • ably/__init__.py (1 hunks)
  • ably/realtime/realtime_channel.py (8 hunks)
  • ably/scripts/unasync.py (1 hunks)
  • ably/types/message.py (3 hunks)
  • ably/types/mixins.py (4 hunks)
  • ably/types/options.py (3 hunks)
  • ably/vcdiff_plugin.py (1 hunks)
  • pyproject.toml (3 hunks)
  • test/ably/realtime/realtimechannel_vcdiff_test.py (1 hunks)
  • test/ably/rest/restchannelpublish_test.py (2 hunks)
  • test/ably/utils.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
ably/vcdiff_plugin.py (4)
ably/types/options.py (2)
  • VCDiffDecoder (11-14)
  • decode (13-14)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/mixins.py (1)
  • decode (47-125)
test/ably/realtime/realtimechannel_vcdiff_test.py (2)
  • decode (20-23)
  • decode (29-30)
ably/types/options.py (3)
ably/types/mixins.py (1)
  • decode (47-125)
ably/vcdiff_plugin.py (1)
  • decode (52-77)
test/ably/realtime/realtimechannel_vcdiff_test.py (2)
  • decode (20-23)
  • decode (29-30)
ably/types/message.py (2)
ably/types/mixins.py (5)
  • EncodeDataMixin (30-129)
  • DeltaExtras (14-20)
  • decode (47-125)
  • encoding (36-37)
  • encoding (40-44)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/mixins.py (4)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/message.py (3)
  • extras (97-98)
  • data (69-70)
  • from_encoded (181-206)
ably/types/options.py (2)
  • vcdiff_decoder (272-273)
  • decode (13-14)
ably/vcdiff_plugin.py (1)
  • decode (52-77)
test/ably/realtime/realtimechannel_vcdiff_test.py (7)
ably/vcdiff_plugin.py (2)
  • VCDiffPlugin (27-77)
  • decode (52-77)
ably/realtime/realtime_channel.py (7)
  • ChannelOptions (25-99)
  • get (596-628)
  • attach (166-204)
  • name (552-554)
  • subscribe (285-337)
  • params (54-56)
  • params (59-63)
test/ably/testapp.py (3)
  • TestApp (37-115)
  • get_test_vars (41-70)
  • get_ably_realtime (80-83)
test/ably/utils.py (4)
  • BaseAsyncTestCase (43-58)
  • WaitableEvent (238-252)
  • finish (251-252)
  • wait (248-249)
ably/types/connectionstate.py (1)
  • ConnectionState (8-16)
ably/types/options.py (3)
  • VCDiffDecoder (11-14)
  • decode (13-14)
  • vcdiff_decoder (272-273)
ably/types/mixins.py (1)
  • decode (47-125)
ably/realtime/realtime_channel.py (4)
ably/rest/channel.py (8)
  • ably (143-144)
  • Channel (22-174)
  • Channels (177-229)
  • cipher (155-156)
  • get (182-193)
  • name (147-148)
  • options (159-160)
  • options (167-174)
ably/types/mixins.py (2)
  • DecodingContext (23-27)
  • from_encoded_array (128-129)
ably/util/eventemitter.py (1)
  • once_async (168-181)
ably/util/crypto.py (1)
  • CipherParams (16-42)
test/ably/rest/restchannelpublish_test.py (2)
ably/rest/channel.py (4)
  • ably (143-144)
  • publish (106-132)
  • get (182-193)
  • history (32-39)
test/ably/utils.py (2)
  • BaseAsyncTestCase (43-58)
  • assert_waiter (186-198)
🪛 Ruff (0.12.2)
ably/vcdiff_plugin.py

46-46: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


47-50: Avoid specifying long messages outside the exception class

(TRY003)


67-67: Avoid specifying long messages outside the exception class

(TRY003)


69-69: Avoid specifying long messages outside the exception class

(TRY003)


74-74: Consider moving this statement to an else block

(TRY300)


76-76: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


77-77: Avoid specifying long messages outside the exception class

(TRY003)

ably/types/message.py

193-194: Avoid specifying long messages outside the exception class

(TRY003)

ably/types/mixins.py

73-73: Multiple statements on one line (colon)

(E701)


77-77: Avoid specifying long messages outside the exception class

(TRY003)


81-81: Avoid specifying long messages outside the exception class

(TRY003)


102-102: Do not catch blind exception: Exception

(BLE001)


103-103: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


104-104: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


104-104: Avoid specifying long messages outside the exception class

(TRY003)


123-123: Multiple statements on one line (colon)

(E701)

test/ably/realtime/realtimechannel_vcdiff_test.py

29-29: Unused method argument: delta

(ARG002)


29-29: Unused method argument: base

(ARG002)


30-30: Create your own exception

(TRY002)


30-30: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Use raise without specifying exception name

Remove exception name

(TRY201)


86-86: Use of assert detected

(S101)


88-88: Use of assert detected

(S101)


117-117: Use raise without specifying exception name

Remove exception name

(TRY201)


127-127: Use of assert detected

(S101)


129-129: Use of assert detected

(S101)


150-150: Use of assert detected

(S101)


167-167: Use raise without specifying exception name

Remove exception name

(TRY201)


179-179: Use of assert detected

(S101)

ably/realtime/realtime_channel.py

41-41: Avoid specifying long messages outside the exception class

(TRY003)


62-62: Avoid specifying long messages outside the exception class

(TRY003)


94-94: Avoid specifying long messages outside the exception class

(TRY003)


440-440: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


612-612: Avoid specifying long messages outside the exception class

(TRY003)


620-625: Avoid specifying long messages outside the exception class

(TRY003)

test/ably/rest/restchannelpublish_test.py

405-405: Use of assert detected

(S101)


411-411: Function definition does not bind loop variable encoding

(B023)


412-412: Function definition does not bind loop variable encoding

(B023)


413-413: Function definition does not bind loop variable msg_data

(B023)


415-415: Function definition does not bind loop variable msg_data

(B023)


421-421: Use of assert detected

(S101)


426-426: Function definition does not bind loop variable expected_value

(B023)


426-426: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


426-426: Function definition does not bind loop variable expected_type

(B023)

test/ably/utils.py

198-198: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


198-198: Avoid specifying long messages outside the exception class

(TRY003)


207-208: try-except-pass detected, consider logging the exception

(S110)


207-207: Do not catch blind exception: Exception

(BLE001)


229-230: try-except-pass detected, consider logging the exception

(S110)


229-229: Do not catch blind exception: Exception

(BLE001)


233-233: Avoid specifying long messages outside the exception class

(TRY003)

🪛 actionlint (1.7.7)
.github/workflows/check.yml

27-27: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)


39-39: the runner of "actions/cache@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

⏰ 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). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.7)
🔇 Additional comments (25)
pyproject.toml (4)

59-59: LGTM - Optional dependency configuration.

The VCDiff dependency is correctly configured as optional with the experimental source. This allows users to install the VCDiff functionality when needed without making it a mandatory dependency.


64-64: LGTM - Extras configuration.

The extras configuration allows users to install VCDiff support via pip install ably[vcdiff], which is a clean way to provide optional functionality.


78-78: LGTM - Development dependency.

Adding VCDiff as a development dependency ensures that tests and development workflows can use the VCDiff functionality without requiring explicit installation steps.


30-33: Experimental package availability verified.

The vcdiff package versions 0.1.0a1 and 0.1.0a2 are present on TestPyPI, so the ^0.1.0a2 constraint from the experimental source is satisfied.

test/ably/utils.py (2)

1-9: LGTM - Import additions.

The new imports support the async waiter functionality and are appropriately scoped.


238-252: LGTM - WaitableEvent implementation.

The WaitableEvent class provides a clean synchronization primitive for async tests. The implementation is straightforward and appropriate for the test utility context.

test/ably/rest/restchannelpublish_test.py (2)

22-22: LGTM - Import update for assert_waiter.

The import correctly adds assert_waiter from the updated test utilities.


407-417: Excellent improvement replacing sleep with polling.

The replacement of fixed asyncio.sleep() calls with assert_waiter() based polling is a significant improvement. This makes the tests more reliable by waiting for actual conditions rather than arbitrary time delays, while also making them potentially faster when conditions are met early.

Also applies to: 423-428

ably/types/options.py (2)

11-14: Good abstraction for pluggable VCDiff decoders

Defining a small ABC with a single decode method keeps the contract clear. LGTM.


88-88: No action: vcdiff_decoder is correctly propagated
Options.vcdiff_decoder is passed into DecodingContext in ably/realtime/realtime_channel.py:143 and exercised by test/ably/realtime/realtimechannel_vcdiff_test.py.

ably/__init__.py (2)

8-8: Re-exporting VCDiffDecoder via top-level API looks good

Keeps the public surface cohesive. No issues.


11-11: Safe to import VCDiffPlugin at package init

Since the plugin imports the optional dependency only in __init__, top-level import won’t hard-require the extra. LGTM.

ably/types/message.py (2)

181-181: Signature extension preserves backwards compatibility

Adding context=None is safe; existing callers remain valid.


196-196: Passing context through to the decoder is correct

Ensures base payload propagation and delta decoding work as intended.

ably/vcdiff_plugin.py (1)

27-34: Overall plugin structure looks solid

Clear interface adherence, input validation, and export surface.

test/ably/realtime/realtimechannel_vcdiff_test.py (2)

51-89: Happy path coverage looks good

End-to-end delta flow validated; decoder call count assertion is valuable.


133-181: Recovery path assertion is on-point

Checks RTL18c behavior by validating reason.code and reattach events.

ably/realtime/realtime_channel.py (6)

25-100: LGTM! Well-structured ChannelOptions implementation.

The ChannelOptions class is well-designed with proper encapsulation, validation, and utility methods. The implementation follows Python best practices with proper type hints and documentation.


152-163: LGTM! Proper implementation of dynamic option updates.

The set_options method correctly handles dynamic channel option updates with appropriate state management and error propagation.


417-418: Verify attach serial handling for delta recovery.

The code correctly updates both attach_serial and channel_serial from the ATTACHED message. This is crucial for RTL18 recovery after delta decode failures.


572-582: LGTM! Correct implementation of RTL18 delta decode failure recovery.

The recovery procedure properly implements the RTL18 specification by logging the error, transitioning to ATTACHING state with the error reason, and triggering the pending state check.


127-144: No issues with VCDiff decoder setup. The initializer correctly passes options.vcdiff_decoder into DecodingContext, and existing tests cover both plugin-enabled and plugin-absent flows.


212-212: Params are correctly passed as a dict and serialized per spec; no changes required.

ably/scripts/unasync.py (1)

281-295: No action needed on assert_waiter_sync mapping. assert_waiter_sync is defined in test/ably/utils.py and, per the first-round unasync Rule, will be copied unchanged into test/ably/sync/utils.py, satisfying the tests’ import paths.

.github/workflows/check.yml (1)

45-47: Ensure VCDiff extra is installed if tests rely on it.

If the new tests require the optional VCDiff dependency, install it in CI.

-      run: poetry install -E crypto
+      run: poetry install -E crypto -E vcdiff

If multiple extras are expected, consider poetry install --all-extras.

@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from d0ddba0 to 4dd472d Compare September 4, 2025 13:16
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 13:17 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: 2

♻️ Duplicate comments (1)
.github/workflows/check.yml (1)

39-44: Upgrade cache action to v4 and key by OS + Python to avoid mixing virtualenvs.

Also add restore-keys for partial hits. This addresses the actionlint warning and prevents cross-Python contamination.

-    - uses: actions/cache@v3
+    - uses: actions/cache@v4
       name: Define a cache for the virtual environment based on the dependencies lock file
       with:
-        path: ./.venv
-        key: venv-${{ hashFiles('poetry.lock') }}
+        path: ./.venv
+        key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
+        restore-keys: |
+          venv-${{ runner.os }}-${{ matrix.python-version }}-
+          venv-${{ runner.os }}-
🧹 Nitpick comments (2)
.github/workflows/check.yml (2)

27-30: Good: upgraded to setup-python@v5. Add check-latest for fresher patch releases.

v5 resolves the Node 20/runtime warning. To reduce cache misses and ensure latest patch for each minor, enable check-latest.

     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
+        check-latest: true

32-33: Pin Poetry version for reproducibility.

Unpinned action is fine, but pinning Poetry itself avoids surprise breaks in lock/install behavior.

-    - name: Setup poetry
-      uses: abatilo/actions-poetry@v4
+    - name: Setup poetry
+      uses: abatilo/actions-poetry@v4
+      with:
+        poetry-version: '1.8.3'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 d0ddba0 and 4dd472d.

📒 Files selected for processing (1)
  • .github/workflows/check.yml (1 hunks)
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/check.yml

39-39: the runner of "actions/cache@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

⏰ 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). (7)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.7)

@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 4dd472d to 24bce80 Compare September 4, 2025 17:57
@ttypic ttypic changed the base branch from main to feat/channel-options September 4, 2025 17:57
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 17:57 Inactive
@ttypic ttypic force-pushed the feat/channel-options branch from 16f3ec3 to 5bec2a7 Compare September 4, 2025 18:06
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch 2 times, most recently from 0d16430 to 793d07a Compare September 4, 2025 18:13
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 18:14 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 793d07a to 72ae4d6 Compare September 4, 2025 18:19
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 18:20 Inactive
@ttypic ttypic force-pushed the feat/channel-options branch from 5bec2a7 to 3671ec8 Compare September 4, 2025 18:25
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 72ae4d6 to c8dbe6d Compare September 4, 2025 18:26
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 18:27 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from c8dbe6d to 36381ca Compare September 4, 2025 18:42
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 18:43 Inactive
@ttypic ttypic force-pushed the feat/channel-options branch 2 times, most recently from b0d7781 to 9032c63 Compare September 4, 2025 18:47
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 36381ca to bad7343 Compare September 4, 2025 18:49
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 4, 2025 18:50 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from bad7343 to 8074178 Compare September 4, 2025 18:54
@ttypic ttypic force-pushed the feat/channel-options branch from a06a6da to 85c8e70 Compare September 9, 2025 12:25
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from a0c096c to 833cc1a Compare September 9, 2025 12:28
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 9, 2025 12:29 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
ably/types/presence.py (1)

89-113: Use decoding context and validate delta 'from' to avoid wrong-base decodes

  • context arg is currently unused; pass it to decode.
  • Mirror Message.from_encoded delta 'from' guard to prevent corrupt decodes when base/id mismatch; raise AblyException 400/40018.

Apply within this method:

 @staticmethod
-def from_encoded(obj, cipher=None, context=None):
+def from_encoded(obj, cipher=None, context=None):
     id = obj.get('id')
     action = obj.get('action', PresenceAction.ENTER)
     client_id = obj.get('clientId')
     connection_id = obj.get('connectionId')
     data = obj.get('data')
     encoding = obj.get('encoding', '')
     timestamp = obj.get('timestamp')
     # member_key = obj.get('memberKey', None)
     extras = obj.get('extras', None)

     if timestamp is not None:
         timestamp = _dt_from_ms_epoch(timestamp)

-    decoded_data = PresenceMessage.decode(data, encoding, cipher)
+    # Ensure correct base sequencing for delta payloads (aligns with Message.from_encoded)
+    from ably.types.mixins import DeltaExtras  # local import to avoid cycles
+    from ably.util.exceptions import AblyException
+    delta_extra = DeltaExtras(extras)
+    if delta_extra.from_id:
+        last_id = getattr(context, 'last_message_id', None) if context else None
+        if last_id != delta_extra.from_id:
+            raise AblyException(
+                f"Delta message decode failure - previous message not available. Message id = {id}",
+                400,
+                40018,
+            )
+
+    decoded_data = PresenceMessage.decode(data, encoding, cipher, context)

Outside this range, ensure imports are available if you prefer module-level imports:

from ably.util.exceptions import AblyException
from ably.types.mixins import EncodeDataMixin, DeltaExtras
ably/vcdiff_plugin.py (1)

1-83: Normalize AblyException HTTP status code for VCDiff failures
In ably/vcdiff_plugin.py (line 78) and ably/types/mixins.py (lines 82 & 105), change AblyException(..., 40018, 40018) to use status_code 400 and error_code 40018.

ably/realtime/realtime_channel.py (3)

39-41: Fix AblyException argument order (ChannelOptions.params validation)

Order should be (status_code=400, code=40000).

Apply:

-            raise AblyException("params must be a dictionary", 40000, 400)
+            raise AblyException("params must be a dictionary", 400, 40000)

77-85: Fix AblyException argument order (ChannelOptions.from_dict validation)

Order should be (400, 40000).

Apply:

-        if not isinstance(options_dict, dict):
-            raise AblyException("options must be a dictionary", 40000, 400)
+        if not isinstance(options_dict, dict):
+            raise AblyException("options must be a dictionary", 400, 40000)

241-258: Fix AblyException argument order in detach path

Two raises pass (code, status) instead of (status, code).

Apply:

-        elif self.state == ChannelState.FAILED:
-            raise AblyException("Unable to detach; channel state = failed", 90001, 400)
+        elif self.state == ChannelState.FAILED:
+            raise AblyException("Unable to detach; channel state = failed", 400, 90001)
...
-        elif new_state == ChannelState.ATTACHING:
-            raise AblyException("Detach request superseded by a subsequent attach request", 90000, 409)
+        elif new_state == ChannelState.ATTACHING:
+            raise AblyException("Detach request superseded by a subsequent attach request", 409, 90000)
♻️ Duplicate comments (10)
ably/types/message.py (1)

191-196: Guard against None context before accessing last_message_id

Avoid AttributeError when context is None; fail fast with 400/40018 per RTL18.

-        delta_extra = DeltaExtras(extras)
-        if delta_extra.from_id and delta_extra.from_id != context.last_message_id:
-            raise AblyException(f"Delta message decode failure - previous message not available. "
-                                f"Message id = {id}", 400, 40018)
+        delta_extra = DeltaExtras(extras)
+        if delta_extra.from_id:
+            last_id = getattr(context, 'last_message_id', None) if context else None
+            if last_id != delta_extra.from_id:
+                raise AblyException(
+                    f"Delta message decode failure - previous message not available. Message id = {id}",
+                    400,
+                    40018,
+                )
ably/vcdiff_plugin.py (1)

72-78: Use HTTP 400 for status_code and preserve traceback/cause on decode failures

Per Ably conventions, status_code must be 400 with code 40018. Also log with traceback and avoid duplicating the low-level error text in the public message; use cause to retain detail.

-        try:
-            # Use the vcdiff library to decode
-            result = self._vcdiff.decode(base, delta)
-            return result
-        except Exception as e:
-            log.error(f"VCDiff decode failed: {e}")
-            raise AblyException(f"VCDiff decode failure: {e}", 40018, 40018) from e
+        try:
+            # Use the vcdiff library to decode
+            return self._vcdiff.decode(base, delta)
+        except Exception as e:
+            log.exception("VCDiff decode failed")
+            raise AblyException("VCDiff decode failure", 400, 40018, cause=e) from e
ably/types/mixins.py (3)

103-106: Don’t catch blanket Exception; preserve AblyException and chain others

Use log.exception for stacktrace and raise with cause.
[ suggest_essential_refactor ]

Apply:

-                except Exception as e:
-                    log.error(f'VCDiff decode failed: {e}')
-                    raise AblyException('VCDiff decode failure', 40018, 40018)
+                except AblyException:
+                    raise
+                except Exception as e:
+                    log.exception('VCDiff decode failed')
+                    raise AblyException('VCDiff decode failure', 400, 40018, cause=e) from e

51-52: Only persist bytes-like payloads to context.base_payload

Avoid saving dict/list/str as base payload; track last bytes separately.
[ suggest_essential_refactor ]

Apply:

-        last_payload = data
+        last_bytes_payload = None
...
-                if not encoding_list:
-                    last_payload = data
+                if not encoding_list:
+                    last_bytes_payload = data
...
-                    data = bytearray(context.vcdiff_decoder.decode(delta_data, base_data))
-                    last_payload = data
+                    data = bytearray(context.vcdiff_decoder.decode(delta_data, base_data))
+                    last_bytes_payload = data
...
-        if context:
-            context.base_payload = last_payload
+        if context and last_bytes_payload is not None:
+            context.base_payload = last_bytes_payload

Also applies to: 71-75, 100-102, 124-126


76-82: Fix AblyException argument order and HTTP status

status_code must be an HTTP status (400); code is the Ably error code (40019/40018).

Apply:

-                if not context or not context.vcdiff_decoder:
-                    log.error('Message cannot be decoded as no VCDiff decoder available')
-                    raise AblyException('VCDiff decoder not available', 40019, 40019)
+                if not context or not context.vcdiff_decoder:
+                    log.error('Message cannot be decoded as no VCDiff decoder available')
+                    raise AblyException('VCDiff decoder not available', 400, 40019)
                 
-                if not context.base_payload:
-                    log.error('VCDiff decoding requires base payload')
-                    raise AblyException('VCDiff decode failure', 40018, 40018)
+                if not context.base_payload:
+                    log.error('VCDiff decoding requires base payload')
+                    raise AblyException('VCDiff decode failure', 400, 40018)
test/ably/realtime/realtimechannel_vcdiff_test.py (3)

1-10: Guard tests on optional vcdiff extra

Skip suite when vcdiff isn’t installed to avoid hard failures.
[ suggest_optional_refactor ]

Apply:

+import pytest
...
+pytest.importorskip("vcdiff", reason="Install with: pip install 'ably[vcdiff]'")

8-9: Use canonical ConnectionState import path

Import from types module, not realtime.connection.
[ suggest_nitpick ]

Apply:

-from ably.realtime.connection import ConnectionState
+from ably.types.connectionstate import ConnectionState

146-153: Subscribe using ChannelState enum, not string

Event emitter uses ChannelState values.
[ suggest_optional_refactor ]

Apply:

-            def on_attaching(state_change):
+            from ably.types.channelstate import ChannelState
+            def on_attaching(state_change):
                 attaching_events.append(state_change)
                 # RTL18c - Check error code
                 if state_change.reason and state_change.reason.code:
                     assert state_change.reason.code == 40018, "Check error code passed through per RTL18c"
 
-            channel.on('attaching', on_attaching)
+            channel.on(ChannelState.ATTACHING, on_attaching)
ably/realtime/realtime_channel.py (2)

421-430: Harden message decode error handling (preserve stack, avoid partial processing)

  • Use raw_messages var for logging.
  • On error: for 40018 start recovery and return immediately; for others log.exception and return.
    [ suggest_essential_refactor ]

Apply:

-            messages = []
-            try:
-                messages = Message.from_encoded_array(proto_msg.get('messages'), context=self.__decoding_context)
-                self.__decoding_context.last_message_id = messages[-1].id
-                self.__channel_serial = channel_serial
-            except AblyException as e:
-                if e.code == 40018:  # Delta decode failure - start recovery
-                    self._start_decode_failure_recovery(e)
-                else:
-                    log.error(f"Message processing error {e}. Skip messages {proto_msg.get('messages')}")
+            messages = []
+            raw_messages = proto_msg.get('messages', [])
+            try:
+                messages = Message.from_encoded_array(raw_messages, context=self.__decoding_context)
+                if messages:
+                    self.__decoding_context.last_message_id = messages[-1].id
+                self.__channel_serial = channel_serial
+            except AblyException as e:
+                if e.code == 40018:  # Delta decode failure - start recovery
+                    self._start_decode_failure_recovery(e)
+                    return
+                else:
+                    log.exception(f"Message processing error: {e}. Skipping {len(raw_messages)} messages")
+                    return

570-586: RTL18 recovery: log with traceback and reset decode context

Clear base_payload and last_message_id to avoid stale bases; keep existing flow.
[ suggest_essential_refactor ]

Apply:

     def _start_decode_failure_recovery(self, error: AblyException) -> None:
         """Start RTL18 decode failure recovery procedure"""

         if self.__decode_failure_recovery_in_progress:
             log.info('VCDiff recovery process already started, skipping')
             return

         self.__decode_failure_recovery_in_progress = True

-        # RTL18a: Log error with code 40018
-        log.error(f'VCDiff decode failure: {error}')
+        # RTL18a: Log error with code 40018
+        log.exception(f'VCDiff decode failure: {error}')
+
+        # Reset delta decode context to avoid applying stale base after recovery starts
+        self.__decoding_context.base_payload = None
+        self.__decoding_context.last_message_id = None

         # RTL18b: Message is already discarded by not processing it

         # RTL18c: Send ATTACH with previous channel serial and transition to ATTACHING
         self._notify_state(ChannelState.ATTACHING, reason=error)
         self._check_pending_state()
🧹 Nitpick comments (4)
ably/types/presence.py (1)

170-174: Consider threading context through presence response handler (optional)

If presence history ever carries deltas, expose an optional context in make_presence_response_handler and pass it into from_encoded_array.

Proposed shape (outside this hunk):

def make_presence_response_handler(cipher, context=None):
    def encrypted_presence_response_handler(response):
        messages = response.to_native()
        return PresenceMessage.from_encoded_array(messages, cipher=cipher, context=context)
    return encrypted_presence_response_handler
ably/types/message.py (1)

234-238: Optional: allow passing decoding context in message response handler

REST usually isn’t delta-encoded, but for consistency and future-proofing, accept an optional context and pass it through.

-def make_message_response_handler(cipher):
-    def encrypted_message_response_handler(response):
+def make_message_response_handler(cipher, context=None):
+    def encrypted_message_response_handler(response):
         messages = response.to_native()
-        return Message.from_encoded_array(messages, cipher=cipher)
+        return Message.from_encoded_array(messages, cipher=cipher, context=context)
     return encrypted_message_response_handler
ably/vcdiff_plugin.py (2)

47-51: Log import failure with traceback; message can be shorter

Use log.exception to capture stack trace; keep guidance concise.

-        except ImportError as e:
-            log.error("vcdiff library not found. Install with: pip install ably[vcdiff]")
+        except ImportError as e:
+            log.exception("vcdiff library not found")
             raise ImportError(
-                "VCDiff plugin requires vcdiff library. "
-                "Install with: pip install ably[vcdiff]"
+                "VCDiff plugin requires the optional vcdiff dependency; install via: pip install 'ably[vcdiff]'"
             ) from e

1-18: Keep module header succinct (optional)

Trim usage block or move to docs to reduce noise in the runtime module.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • 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 a0c096c and 833cc1a.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • ably/__init__.py (1 hunks)
  • ably/realtime/realtime_channel.py (5 hunks)
  • ably/types/message.py (3 hunks)
  • ably/types/mixins.py (4 hunks)
  • ably/types/options.py (3 hunks)
  • ably/types/presence.py (1 hunks)
  • ably/vcdiff_plugin.py (1 hunks)
  • pyproject.toml (3 hunks)
  • test/ably/realtime/realtimechannel_vcdiff_test.py (1 hunks)
  • test/ably/rest/restauth_test.py (3 hunks)
  • test/ably/utils.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/ably/rest/restauth_test.py
  • test/ably/utils.py
  • ably/types/options.py
  • ably/init.py
  • pyproject.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-08T15:37:26.765Z
Learnt from: lmars
PR: ably/ably-python#571
File: ably/util/exceptions.py:74-74
Timestamp: 2024-10-08T15:37:26.765Z
Learning: In `AblyException.decode_error_response`, exceptions are caught and re-thrown as an `AblyException` in `AblyException.raise_for_response`, so there's no need to raise an `AblyException` within `decode_error_response`.

Applied to files:

  • ably/types/mixins.py
🧬 Code graph analysis (6)
ably/types/message.py (3)
ably/types/mixins.py (5)
  • EncodeDataMixin (30-131)
  • DeltaExtras (14-20)
  • decode (47-127)
  • encoding (36-37)
  • encoding (40-44)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/vcdiff_plugin.py (1)
  • decode (53-78)
ably/types/mixins.py (4)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/message.py (3)
  • extras (97-98)
  • data (69-70)
  • from_encoded (181-206)
ably/types/options.py (2)
  • vcdiff_decoder (272-273)
  • decode (13-14)
ably/vcdiff_plugin.py (1)
  • decode (53-78)
ably/realtime/realtime_channel.py (4)
ably/types/message.py (3)
  • Message (24-231)
  • id (77-78)
  • id (81-82)
ably/types/mixins.py (2)
  • DecodingContext (23-27)
  • from_encoded_array (130-131)
ably/types/options.py (1)
  • vcdiff_decoder (272-273)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/presence.py (1)
ably/types/message.py (1)
  • from_encoded (181-206)
ably/vcdiff_plugin.py (4)
ably/types/options.py (2)
  • VCDiffDecoder (11-14)
  • decode (13-14)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/mixins.py (1)
  • decode (47-127)
test/ably/realtime/realtimechannel_vcdiff_test.py (2)
  • decode (20-23)
  • decode (29-30)
test/ably/realtime/realtimechannel_vcdiff_test.py (7)
ably/vcdiff_plugin.py (2)
  • VCDiffPlugin (28-78)
  • decode (53-78)
ably/realtime/realtime_channel.py (6)
  • ChannelOptions (24-85)
  • get (601-633)
  • params (48-50)
  • params (566-568)
  • attach (154-192)
  • name (545-547)
test/ably/testapp.py (3)
  • TestApp (37-115)
  • get_test_vars (41-70)
  • get_ably_realtime (80-83)
test/ably/utils.py (4)
  • BaseAsyncTestCase (43-58)
  • WaitableEvent (238-252)
  • finish (251-252)
  • wait (248-249)
ably/types/options.py (3)
  • VCDiffDecoder (11-14)
  • decode (13-14)
  • vcdiff_decoder (272-273)
ably/types/mixins.py (1)
  • decode (47-127)
ably/util/eventemitter.py (2)
  • once_async (168-181)
  • on (44-86)
🪛 Ruff (0.12.2)
ably/types/message.py

193-194: Avoid specifying long messages outside the exception class

(TRY003)

ably/types/mixins.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


82-82: Avoid specifying long messages outside the exception class

(TRY003)


103-103: Do not catch blind exception: Exception

(BLE001)


104-104: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


105-105: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


105-105: Avoid specifying long messages outside the exception class

(TRY003)

ably/realtime/realtime_channel.py

430-430: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

ably/types/presence.py

89-89: Unused static method argument: context

(ARG004)

ably/vcdiff_plugin.py

47-47: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


48-51: Avoid specifying long messages outside the exception class

(TRY003)


68-68: Avoid specifying long messages outside the exception class

(TRY003)


70-70: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Consider moving this statement to an else block

(TRY300)


77-77: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


78-78: Avoid specifying long messages outside the exception class

(TRY003)

test/ably/realtime/realtimechannel_vcdiff_test.py

29-29: Unused method argument: delta

(ARG002)


29-29: Unused method argument: base

(ARG002)


30-30: Create your own exception

(TRY002)


30-30: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Use raise without specifying exception name

Remove exception name

(TRY201)


117-117: Use raise without specifying exception name

Remove exception name

(TRY201)


167-167: Use raise without specifying exception name

Remove exception name

(TRY201)

⏰ 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). (7)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.9)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.8)
🔇 Additional comments (10)
ably/vcdiff_plugin.py (3)

67-71: Type validation is fine

Explicit bytes checks for delta and base look good.


28-36: Docstring aligns with behavior

Interface and error semantics are clear.


81-82: all export is correct

Public surface is explicit.

ably/types/mixins.py (3)

11-21: LGTM: ENC_VCDIFF constant and DeltaExtras helper look correct

Naming and extraction of delta.from are consistent with usage in Message.from_encoded.


23-28: LGTM: DecodingContext fields are appropriate

Carries base payload, last message id, and decoder reference as needed.


130-131: LGTM: Context propagation through from_encoded_array

Passing context into each element decode is required for deltas.

test/ably/realtime/realtimechannel_vcdiff_test.py (2)

73-76: Re-raise original exception with traceback

Use bare raise inside except to preserve traceback.
[ suggest_nitpick ]
Apply:

-                except Exception as e:
-                    waitable_event.finish()
-                    raise e
+                except Exception:
+                    waitable_event.finish()
+                    raise

Also applies to: 115-118, 165-168


35-37: Unused variable: valid_key_format

Remove if not used.
[ suggest_nitpick ]
Apply:

-        self.valid_key_format = "api:key"
ably/realtime/realtime_channel.py (2)

128-132: Nit: simplify vcdiff_decoder assignment

The ternary is redundant.
[ suggest_nitpick ]
Apply:

-        vcdiff_decoder = self.__realtime.options.vcdiff_decoder if self.__realtime.options.vcdiff_decoder else None
-        self.__decoding_context = DecodingContext(vcdiff_decoder=vcdiff_decoder)
+        self.__decoding_context = DecodingContext(vcdiff_decoder=self.__realtime.options.vcdiff_decoder)

472-474: LGTM: recovery flag reset when leaving ATTACHING

Prevents recovery lockout on future failures.

@ttypic ttypic force-pushed the feat/channel-options branch 2 times, most recently from a5e34dc to 204138f Compare September 10, 2025 11:38
- Introduced `VCDiffDecoder` abstract class and `VCDiffPlugin` implementation.
- Enhanced delta message processing with proper support for VCDiff decoding.
- Updated `Options` to accept a `vcdiff_decoder`.
- Handle delta failures with recovery mechanisms (RTL18-RTL20 compliance).
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 64e1e1c to d893fb7 Compare September 10, 2025 11:49
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 10, 2025 11:50 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from d893fb7 to 3293f3a Compare September 10, 2025 11:52
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 10, 2025 11:53 Inactive
@ttypic ttypic force-pushed the ECO-5456/vcdiff-support branch from 3293f3a to 0966233 Compare September 10, 2025 12:27
@github-actions github-actions bot temporarily deployed to staging/pull/620/features September 10, 2025 12:28 Inactive
Base automatically changed from feat/channel-options to main September 10, 2025 12:41
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
ably/realtime/realtime_channel.py (4)

39-41: Fix AblyException argument order (HTTP status, then Ably code)

Should be (400, 40000). Current (40000, 400) misclassifies errors.

-            raise AblyException("params must be a dictionary", 40000, 400)
+            raise AblyException("params must be a dictionary", 400, 40000)

247-247: Same issue: wrong AblyException argument order

Detach error should use HTTP 400, code 90001.

-            raise AblyException("Unable to detach; channel state = failed", 90001, 400)
+            raise AblyException("Unable to detach; channel state = failed", 400, 90001)

261-261: Same issue: use 409 status, Ably code 90000

-            raise AblyException("Detach request superseded by a subsequent attach request", 90000, 409)
+            raise AblyException("Detach request superseded by a subsequent attach request", 409, 90000)

605-631: Channels.get: accept dict options safely; fix reattach check and setter

Passing a dict currently breaks (to_dict() call). Convert dicts via ChannelOptions.from_dict.

-    def get(self, name: str, options: Optional[ChannelOptions] = None) -> RealtimeChannel:
+    def get(self, name: str, options: Optional[ChannelOptions] = None) -> RealtimeChannel:
@@
-        if name not in self.__all:
-            channel = self.__all[name] = RealtimeChannel(self.__ably, name, options)
+        if name not in self.__all:
+            if isinstance(options, dict):
+                options = ChannelOptions.from_dict(options)
+            channel = self.__all[name] = RealtimeChannel(self.__ably, name, options)
         else:
             channel = self.__all[name]
             # Update options if channel is not attached or currently attaching
-            if options and channel.should_reattach_to_set_options(options):
+            if isinstance(options, dict):
+                options = ChannelOptions.from_dict(options)
+            if options and channel.should_reattach_to_set_options(options):
                 raise AblyException(
                     'Channels.get() cannot be used to set channel options that would cause the channel to '
                     'reattach. Please, use RealtimeChannel.setOptions() instead.',
                     400,
                     40000
                 )
             elif options:
                 channel.set_options_without_reattach(options)
♻️ Duplicate comments (9)
pyproject.toml (1)

30-33: TestPyPI source in published metadata — clarify install story or remove

Including an explicit TestPyPI source may confuse downstream consumers; pip won’t use Poetry sources. If vcdiff-decoder isn’t on PyPI, pip install ably[vcdiff] will fail. Either document the exact install (with --extra-index-url) or drop the source and the extra until it’s on PyPI.

Run to verify availability on PyPI:

#!/bin/bash
set -euo pipefail
for name in vcdiff-decoder vcdiff_decoder; do
  echo "Checking $name on PyPI…"
  if curl -fsSL "https://pypi.org/pypi/${name}/json" >/dev/null; then
    echo "Found $name on PyPI"
  else
    echo "NOT found: $name on PyPI"
  fi
done
ably/types/message.py (1)

191-196: Guard against None or stale context to avoid AttributeError and comply with RTL18

Accessing context.last_message_id unguarded will crash on delta messages when context is None. Fail with 400/40018 per spec.

-        delta_extra = DeltaExtras(extras)
-        if delta_extra.from_id and delta_extra.from_id != context.last_message_id:
-            raise AblyException(f"Delta message decode failure - previous message not available. "
-                                f"Message id = {id}", 400, 40018)
+        delta_extra = DeltaExtras(extras)
+        if delta_extra.from_id:
+            last_id = getattr(context, 'last_message_id', None) if context else None
+            if last_id != delta_extra.from_id:
+                raise AblyException(
+                    f"Delta message decode failure - previous message not available. Message id = {id}",
+                    400,
+                    40018,
+                )
ably/vcdiff/ably_vcdiff_decoder.py (1)

72-79: Normalize error: use HTTP 400 status, Ably code 40018; chain the cause

Current code raises 40018 as both status and code.

-        except Exception as e:
-            log.error(f"VCDiff decode failed: {e}")
-            raise AblyException(f"VCDiff decode failure: {e}", 40018, 40018) from e
+        except AblyException:
+            raise
+        except Exception as e:
+            log.exception("VCDiff decode failed")
+            raise AblyException("VCDiff decode failure", 400, 40018, cause=e) from e
ably/types/mixins.py (2)

76-83: Fix AblyException status/code ordering for decoder availability and missing base

Use HTTP 400 as status; keep Ably codes 40019 (no decoder) and 40018 (decode failure).

-                if not context or not context.vcdiff_decoder:
-                    log.error('Message cannot be decoded as no VCDiff decoder available')
-                    raise AblyException('VCDiff decoder not available', 40019, 40019)
+                if not context or not context.vcdiff_decoder:
+                    log.error('Message cannot be decoded as no VCDiff decoder available')
+                    raise AblyException('VCDiff decoder not available', 400, 40019)
                 
-                if not context.base_payload:
-                    log.error('VCDiff decoding requires base payload')
-                    raise AblyException('VCDiff decode failure', 40018, 40018)
+                if not context.base_payload:
+                    log.error('VCDiff decoding requires base payload')
+                    raise AblyException('VCDiff decode failure', 400, 40018)

103-106: Don’t catch blanket Exception; preserve AblyException and chain others

Improves diagnostics and satisfies linters.

-                except Exception as e:
-                    log.error(f'VCDiff decode failed: {e}')
-                    raise AblyException('VCDiff decode failure', 40018, 40018)
+                except AblyException:
+                    raise
+                except Exception as e:
+                    log.exception('VCDiff decode failed')
+                    raise AblyException('VCDiff decode failure', 400, 40018, cause=e) from e
test/ably/realtime/realtimechannel_vcdiff_test.py (2)

1-10: Skip module if optional decoder isn’t installed; fix imports

Guard tests so environments without the extra don’t hard-fail; import ConnectionState from canonical location.

+import pytest
 import asyncio
 import json
 
-from ably import AblyVCDiffDecoder
+from ably import AblyVCDiffDecoder
 from ably.realtime.realtime_channel import ChannelOptions
 from test.ably.testapp import TestApp
 from test.ably.utils import BaseAsyncTestCase, WaitableEvent
-from ably.realtime.connection import ConnectionState
+from ably.types.connectionstate import ConnectionState
 from ably.types.options import VCDiffDecoder
+from ably.types.channelstate import ChannelState
+
+# Skip if optional decoder isn’t present
+pytest.importorskip("vcdiff_decoder", reason="vcdiff extra not installed")

146-153: Subscribe using enum value, not string

Channel emits ChannelState enums.

-            channel.on('attaching', on_attaching)
+            channel.on(ChannelState.ATTACHING, on_attaching)
ably/realtime/realtime_channel.py (2)

575-592: RTL18: log with traceback and reset decode context before reattach

Prevents stale bases on recovery.

     def _start_decode_failure_recovery(self, error: AblyException) -> None:
         """Start RTL18 decode failure recovery procedure"""
@@
-        # RTL18a: Log error with code 40018
-        log.error(f'VCDiff decode failure: {error}')
+        # RTL18a: Log error with code 40018
+        log.exception(f'VCDiff decode failure: {error}')
+
+        # Reset delta decode context to avoid stale bases after recovery starts
+        self.__decoding_context.base_payload = None
+        self.__decoding_context.last_message_id = None
@@
-        self._notify_state(ChannelState.ATTACHING, reason=error)
+        self._notify_state(ChannelState.ATTACHING, reason=error)
         self._check_pending_state()

425-434: Robust message batch handling; don’t lose traceback; skip batch on error

Use a local raw_messages, update last_message_id only when non-empty, and log exceptions.

-            messages = []
-            try:
-                messages = Message.from_encoded_array(proto_msg.get('messages'), context=self.__decoding_context)
-                self.__decoding_context.last_message_id = messages[-1].id
-                self.__channel_serial = channel_serial
-            except AblyException as e:
-                if e.code == 40018:  # Delta decode failure - start recovery
-                    self._start_decode_failure_recovery(e)
-                else:
-                    log.error(f"Message processing error {e}. Skip messages {proto_msg.get('messages')}")
+            messages = []
+            raw_messages = proto_msg.get('messages', [])
+            try:
+                messages = Message.from_encoded_array(raw_messages, context=self.__decoding_context)
+                if messages:
+                    self.__decoding_context.last_message_id = messages[-1].id
+                self.__channel_serial = channel_serial
+            except AblyException as e:
+                if e.code == 40018:  # Delta decode failure - start recovery
+                    self._start_decode_failure_recovery(e)
+                    return  # Skip processing during recovery
+                else:
+                    log.exception(f"Message processing error: {e}. Skipping {len(raw_messages)} messages")
+                    return
🧹 Nitpick comments (7)
pyproject.toml (3)

59-59: Optional dependency naming looks fine; keep prerelease allowance explicit

Using ^0.1.0a1 is fine for Poetry. Ensure docs state that this is a pre-release and how to enable it if users pin via other tools.


64-64: Extras wiring good, contingent on PyPI availability

vcdiff = ["vcdiff-decoder"] is correct for extras. It still depends on the package being on PyPI (see earlier comment).


66-86: Fix deprecated section: move dev-dependencies to Poetry groups

GA warns: [tool.poetry.dev-dependencies] is deprecated. Rename to [tool.poetry.group.dev.dependencies]. No behavior change.

-[tool.poetry.dev-dependencies]
+[tool.poetry.group.dev.dependencies]
 pytest = "^7.1"
 mock = "^4.0.3"
 ...
 vcdiff-decoder = { version = "^0.1.0a1" }
ably/types/presence.py (1)

89-105: Pass context through to decode; removes unused-arg and enables delta if ever needed

Currently context is accepted but not used. Forward it to decode.

-        decoded_data = PresenceMessage.decode(data, encoding, cipher)
+        decoded_data = PresenceMessage.decode(data, encoding, cipher, context)
ably/vcdiff/ably_vcdiff_decoder.py (1)

47-51: Prefer logging.exception for import failure; tighten guidance text

Preserve traceback and keep a single clear user action.

-        except ImportError as e:
-            log.error("vcdiff library not found. Install with: pip install ably[vcdiff]")
-            raise ImportError(
-                "VCDiff plugin requires vcdiff library. "
-                "Install with: pip install ably[vcdiff]"
-            ) from e
+        except ImportError as e:
+            log.exception("vcdiff-decoder not found")
+            raise ImportError("Install optional decoder with: pip install 'ably[vcdiff]'") from e
test/ably/realtime/realtimechannel_vcdiff_test.py (2)

73-76: Re-raise without shadowing to keep traceback

Use bare raise in handlers.

-                except Exception as e:
-                    waitable_event.finish()
-                    raise e
+                except Exception:
+                    waitable_event.finish()
+                    raise

Also applies to: 115-117, 165-167


59-61: Redundant attach before subscribe

subscribe() awaits attach() (RTL7c). You can drop the explicit await channel.attach() to reduce chatter.

Also applies to: 101-104

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • 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 833cc1a and 0966233.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • ably/__init__.py (1 hunks)
  • ably/realtime/realtime_channel.py (5 hunks)
  • ably/types/message.py (3 hunks)
  • ably/types/mixins.py (4 hunks)
  • ably/types/options.py (3 hunks)
  • ably/types/presence.py (1 hunks)
  • ably/vcdiff/ably_vcdiff_decoder.py (1 hunks)
  • pyproject.toml (3 hunks)
  • test/ably/realtime/realtimechannel_vcdiff_test.py (1 hunks)
  • test/ably/rest/restauth_test.py (3 hunks)
  • test/ably/utils.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/ably/utils.py
  • ably/init.py
  • ably/types/options.py
  • test/ably/rest/restauth_test.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-01T18:02:27.016Z
Learnt from: lmars
PR: ably/ably-python#571
File: ably/util/exceptions.py:74-74
Timestamp: 2024-10-01T18:02:27.016Z
Learning: In `AblyException.decode_error_response`, exceptions are caught and re-thrown as an `AblyException` in `AblyException.raise_for_response`, so there's no need to raise an `AblyException` within `decode_error_response`.

Applied to files:

  • ably/types/mixins.py
🧬 Code graph analysis (6)
ably/vcdiff/ably_vcdiff_decoder.py (3)
ably/types/options.py (3)
  • VCDiffDecoder (11-14)
  • vcdiff_decoder (272-273)
  • decode (13-14)
ably/util/exceptions.py (1)
  • AblyException (9-84)
test/ably/realtime/realtimechannel_vcdiff_test.py (2)
  • decode (20-23)
  • decode (29-30)
ably/types/message.py (2)
ably/types/mixins.py (5)
  • EncodeDataMixin (30-131)
  • DeltaExtras (14-20)
  • decode (47-127)
  • encoding (36-37)
  • encoding (40-44)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/realtime/realtime_channel.py (5)
ably/transport/websockettransport.py (1)
  • ProtocolMessageAction (29-41)
ably/types/message.py (3)
  • Message (24-231)
  • id (77-78)
  • id (81-82)
ably/types/mixins.py (2)
  • DecodingContext (23-27)
  • from_encoded_array (130-131)
ably/types/options.py (1)
  • vcdiff_decoder (272-273)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/mixins.py (3)
ably/util/exceptions.py (1)
  • AblyException (9-84)
ably/types/message.py (3)
  • extras (97-98)
  • data (69-70)
  • from_encoded (181-206)
ably/types/options.py (2)
  • vcdiff_decoder (272-273)
  • decode (13-14)
ably/types/presence.py (3)
ably/types/message.py (1)
  • from_encoded (181-206)
ably/realtime/realtime_channel.py (1)
  • cipher (43-45)
ably/rest/channel.py (1)
  • cipher (155-156)
test/ably/realtime/realtimechannel_vcdiff_test.py (8)
ably/vcdiff/ably_vcdiff_decoder.py (2)
  • AblyVCDiffDecoder (28-78)
  • decode (53-78)
ably/realtime/realtime_channel.py (7)
  • ChannelOptions (24-85)
  • get (606-631)
  • params (48-50)
  • params (571-573)
  • attach (158-196)
  • name (550-552)
  • subscribe (277-329)
test/ably/testapp.py (3)
  • TestApp (37-115)
  • get_test_vars (41-70)
  • get_ably_realtime (80-83)
test/ably/utils.py (4)
  • BaseAsyncTestCase (43-58)
  • WaitableEvent (238-252)
  • finish (251-252)
  • wait (248-249)
ably/types/connectionstate.py (1)
  • ConnectionState (8-16)
ably/types/options.py (3)
  • VCDiffDecoder (11-14)
  • vcdiff_decoder (272-273)
  • decode (13-14)
ably/types/mixins.py (1)
  • decode (47-127)
ably/util/eventemitter.py (2)
  • once_async (168-181)
  • on (44-86)
🪛 Ruff (0.12.2)
ably/vcdiff/ably_vcdiff_decoder.py

47-47: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


48-51: Avoid specifying long messages outside the exception class

(TRY003)


68-68: Avoid specifying long messages outside the exception class

(TRY003)


70-70: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Consider moving this statement to an else block

(TRY300)


77-77: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


78-78: Avoid specifying long messages outside the exception class

(TRY003)

ably/types/message.py

193-194: Avoid specifying long messages outside the exception class

(TRY003)

ably/realtime/realtime_channel.py

434-434: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

ably/types/mixins.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


82-82: Avoid specifying long messages outside the exception class

(TRY003)


103-103: Do not catch blind exception: Exception

(BLE001)


104-104: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


105-105: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


105-105: Avoid specifying long messages outside the exception class

(TRY003)

ably/types/presence.py

89-89: Unused static method argument: context

(ARG004)

test/ably/realtime/realtimechannel_vcdiff_test.py

29-29: Unused method argument: delta

(ARG002)


29-29: Unused method argument: base

(ARG002)


30-30: Create your own exception

(TRY002)


30-30: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Use raise without specifying exception name

Remove exception name

(TRY201)


117-117: Use raise without specifying exception name

Remove exception name

(TRY201)


167-167: Use raise without specifying exception name

Remove exception name

(TRY201)

🪛 GitHub Actions: .github/workflows/check.yml
pyproject.toml

[warning] 1-1: The 'poetry.dev-dependencies' section is deprecated and will be removed in a future version. Use 'poetry.group.dev.dependencies' instead.

🔇 Additional comments (1)
ably/realtime/realtime_channel.py (1)

416-416: Use logging.warning instead of deprecated warn

[Suggest_nitpick]

-                log.warn("RealtimeChannel._on_message(): ATTACHED received while not attaching")
+                log.warning("RealtimeChannel._on_message(): ATTACHED received while not attaching")

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

Gone through all the files.
Apart from review comments and red CI, code looks good.

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 (3)
test/ably/realtime/realtimechannel_vcdiff_test.py (3)

1-10: Skip suite when optional extra is missing.

Guard tests so CI without ably[vcdiff] doesn’t hard-fail.

 import asyncio
 import json
 
 from ably import AblyVCDiffDecoder
 from ably.realtime.realtime_channel import ChannelOptions
 from test.ably.testapp import TestApp
 from test.ably.utils import BaseAsyncTestCase, WaitableEvent
-from ably.realtime.connection import ConnectionState
+from ably.types.connectionstate import ConnectionState
 from ably.types.options import VCDiffDecoder
+import pytest
+
+# Skip if the optional decoder backend is not installed
+pytest.importorskip("vcdiff_decoder", reason="vcdiff extra not installed")

146-153: Subscribe to enum state, not a string literal.

Channel emits ChannelState values; string "attaching" won’t match.

-            def on_attaching(state_change):
+            from ably.types.channelstate import ChannelState  # move to top-level if preferred
+            def on_attaching(state_change):
                 attaching_events.append(state_change)
                 # RTL18c - Check error code
                 if state_change.reason and state_change.reason.code:
                     assert state_change.reason.code == 40018, "Check error code passed through per RTL18c"
 
-            channel.on('attaching', on_attaching)
+            channel.on(ChannelState.ATTACHING, on_attaching)

214-214: Use ChannelState enum here as well.

-            channel.on('attaching', on_attaching)
+            from ably.types.channelstate import ChannelState  # ensure imported at top
+            channel.on(ChannelState.ATTACHING, on_attaching)
🧹 Nitpick comments (6)
setup.cfg (1)

11-11: Confirm widened Flake8 exclude (could hide defects).

Excluding ably/sync and test/ably/sync removes those paths from linting. If ably/sync contains runtime code, this may mask issues. If it’s intentional (generated/legacy), add a brief rationale comment above to avoid future confusion; otherwise prefer narrowing or using extend-exclude just for tests.

-# Exclude virtual environment check
-exclude = .venv,venv,env,.env,.git,__pycache__,.pytest_cache,build,dist,*.egg-info,ably/sync,test/ably/sync
+# Exclude virtual envs, build artifacts, and (intentional) sync dirs (non-linted/legacy)
+exclude = .venv,venv,env,.env,.git,__pycache__,.pytest_cache,build,dist,*.egg-info,ably/sync,test/ably/sync
ably/vcdiff/default_vcdiff_decoder.py (2)

7-18: Fix example imports in docstring.

AblyVCDiffDecoder is re-exported at ably, and AblyRealtime is not under ably.vcdiff. Update the example to reflect actual imports.

-Usage:
-    from ably.vcdiff import AblyVCDiffDecoder, AblyRealtime
+Usage:
+    from ably import AblyVCDiffDecoder, AblyRealtime

47-51: Use logging.exception and name the optional dependency correctly.

This captures the traceback and clarifies the missing package.

-            log.error("vcdiff library not found. Install with: pip install ably[vcdiff]")
+            log.exception("vcdiff-decoder not found. Install with: pip install ably[vcdiff]")
             raise ImportError(
-                "VCDiff plugin requires vcdiff library. "
+                "VCDiff plugin requires the vcdiff-decoder library. "
                 "Install with: pip install ably[vcdiff]"
             ) from e
test/ably/realtime/realtimechannel_vcdiff_test.py (3)

73-76: Use bare raise to preserve original traceback.

Re-raising the caught exception by name rewrites the traceback.

-                except Exception as e:
-                    waitable_event.finish()
-                    raise e
+                except Exception:
+                    waitable_event.finish()
+                    raise
-                except Exception as e:
-                    waitable_event.finish()
-                    raise e
+                except Exception:
+                    waitable_event.finish()
+                    raise
-                except Exception as e:
-                    waitable_event.finish()
-                    raise e
+                except Exception:
+                    waitable_event.finish()
+                    raise

Also applies to: 115-118, 165-168


36-37: Remove unused test variable.

valid_key_format is never used.

-        self.valid_key_format = "api:key"
-

59-61: Avoid redundant attach (subscribe already awaits attach).

Keeps tests shorter and avoids double state transitions.

-            await channel.attach()
+            # subscribe() will attach if needed
-            await channel.attach()
+            # subscribe() will attach if needed
-            await channel.attach()
+            # subscribe() will attach if needed

Also applies to: 101-104, 192-194

📜 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 0966233 and fd3b028.

📒 Files selected for processing (5)
  • ably/__init__.py (1 hunks)
  • ably/types/options.py (3 hunks)
  • ably/vcdiff/default_vcdiff_decoder.py (1 hunks)
  • setup.cfg (1 hunks)
  • test/ably/realtime/realtimechannel_vcdiff_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ably/types/options.py
  • ably/init.py
🧰 Additional context used
🧬 Code graph analysis (2)
ably/vcdiff/default_vcdiff_decoder.py (3)
ably/types/options.py (3)
  • VCDiffDecoder (11-24)
  • vcdiff_decoder (282-283)
  • decode (23-24)
ably/util/exceptions.py (1)
  • AblyException (9-84)
test/ably/realtime/realtimechannel_vcdiff_test.py (2)
  • decode (20-23)
  • decode (29-30)
test/ably/realtime/realtimechannel_vcdiff_test.py (7)
ably/vcdiff/default_vcdiff_decoder.py (2)
  • AblyVCDiffDecoder (28-78)
  • decode (53-78)
ably/realtime/realtime_channel.py (7)
  • ChannelOptions (24-85)
  • get (606-631)
  • params (48-50)
  • params (571-573)
  • attach (158-196)
  • name (550-552)
  • subscribe (277-329)
test/ably/testapp.py (3)
  • TestApp (37-115)
  • get_test_vars (41-70)
  • get_ably_realtime (80-83)
test/ably/utils.py (4)
  • BaseAsyncTestCase (43-58)
  • WaitableEvent (238-252)
  • finish (251-252)
  • wait (248-249)
ably/types/connectionstate.py (1)
  • ConnectionState (8-16)
ably/types/options.py (3)
  • VCDiffDecoder (11-24)
  • vcdiff_decoder (282-283)
  • decode (23-24)
ably/types/mixins.py (1)
  • decode (47-127)
🪛 Ruff (0.12.2)
ably/vcdiff/default_vcdiff_decoder.py

47-47: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


48-51: Avoid specifying long messages outside the exception class

(TRY003)


68-68: Avoid specifying long messages outside the exception class

(TRY003)


70-70: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Consider moving this statement to an else block

(TRY300)


77-77: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


78-78: Avoid specifying long messages outside the exception class

(TRY003)

test/ably/realtime/realtimechannel_vcdiff_test.py

29-29: Unused method argument: delta

(ARG002)


29-29: Unused method argument: base

(ARG002)


30-30: Create your own exception

(TRY002)


30-30: Avoid specifying long messages outside the exception class

(TRY003)


75-75: Use raise without specifying exception name

Remove exception name

(TRY201)


117-117: Use raise without specifying exception name

Remove exception name

(TRY201)


167-167: Use raise without specifying exception name

Remove exception name

(TRY201)

⏰ 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). (7)
  • GitHub Check: check (3.13)
  • GitHub Check: check (3.12)
  • GitHub Check: check (3.7)
  • GitHub Check: check (3.8)
  • GitHub Check: check (3.11)
  • GitHub Check: check (3.10)
  • GitHub Check: check (3.9)
🔇 Additional comments (1)
ably/vcdiff/default_vcdiff_decoder.py (1)

73-75: Confirm vcdiff-decoder call signature (base, delta).

Some libraries expect (delta, base). Please confirm to avoid silent corruption.

@ttypic ttypic requested a review from sacOO7 September 15, 2025 12:33
Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

Lgtm

@ttypic ttypic merged commit 181ae3d into main Sep 16, 2025
10 checks passed
@ttypic ttypic deleted the ECO-5456/vcdiff-support branch September 16, 2025 09:02
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.

vcdiff support
2 participants