Skip to content

[AIT-38] Add support for partial sync of objects - protocol v6#2152

Merged
VeskeR merged 3 commits intointegration/protocol-v6from
AIT-38/partial-objects-sync
Feb 27, 2026
Merged

[AIT-38] Add support for partial sync of objects - protocol v6#2152
VeskeR merged 3 commits intointegration/protocol-v6from
AIT-38/partial-objects-sync

Conversation

@VeskeR
Copy link
Copy Markdown
Contributor

@VeskeR VeskeR commented Jan 23, 2026

This PR is based on #2159, please review that one first.

Spec for this ably/specification#413, DR https://ably.atlassian.net/wiki/x/AQBxCQE

Resolves AIT-38.

This also removes unnecessary SyncObjectsDataPool class and simplifies a sync logic a bit by moving it to RealtimeObject.

Summary by CodeRabbit

  • New Features

    • New synchronization pool implementation to accumulate and merge object sync messages during sync.
  • Refactor

    • Internal replacement of the previous sync data pool with the new pool implementation (no public API changes).
  • Bug Fixes

    • Unsupported object shapes are now logged and skipped instead of causing errors.
    • Improved merging for partial OBJECT_SYNC sequences, preserving map entries across messages.
  • Tests

    • Added OBJECT_SYNC tests covering multi-message builds, partial merges, and unknown-type resilience.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces the legacy SyncObjectsDataPool with a new SyncObjectsPool, moves pool logic to shape-based detection (counter vs map), adds map-state merging for partial OBJECT_SYNC messages, updates RealtimeObject to use the new pool, deletes the old module, and adds tests for multi-message sync scenarios.

Changes

Cohort / File(s) Summary
Module Report & Bundling
scripts/moduleReport.ts
Switched allowed LiveObjects bundle file reference from src/plugins/liveobjects/syncobjectsdatapool.ts to src/plugins/liveobjects/syncobjectspool.ts.
New Pool Implementation
src/plugins/liveobjects/syncobjectspool.ts
Added SyncObjectsPool class: accumulates ObjectMessage entries by objectId, validates shapes, merges partial map state via _mergeMapSyncState(), and exposes entries/size/isEmpty/clear/applyObjectSyncMessages.
Removed Legacy Pool
src/plugins/liveobjects/syncobjectsdatapool.ts
Deleted entire module and its public types/classes (SyncObjectsDataPool, LiveObjectDataEntry, LiveCounterDataEntry, LiveMapDataEntry, AnyDataEntry).
RealtimeObject Integration
src/plugins/liveobjects/realtimeobject.ts
Replaced SyncObjectsDataPool with SyncObjectsPool across imports, field names, initialization, clear/isEmpty/entries calls, and _applySync() logic; switched from type-driven creation to shape-based creation (check object?.counter vs object?.map) and guarded unsupported shapes.
Tests
test/realtime/liveobjects.test.js
Added OBJECT_SYNC tests covering multi-message object tree construction, resilience to unknown object types, and partial map entry merging across multiple sync messages (≈+188 lines).

Sequence Diagram

sequenceDiagram
    participant RO as RealtimeObject
    participant Pool as SyncObjectsPool
    participant Map as LiveMap
    participant Counter as LiveCounter

    RO->>Pool: applyObjectSyncMessages(objectMessages)
    activate Pool

    loop each ObjectMessage
        Pool->>Pool: validate object exists
        alt first message for objectId
            Pool->>Pool: store ObjectMessage
        else existing entry
            alt object has counter
                Pool->>Pool: log error, skip partial counter
            else object has map
                Pool->>Pool: _mergeMapSyncState(existing, new)
            end
        end
    end
    deactivate Pool

    RO->>Pool: entries()
    loop each [objectId, objectMessage]
        alt objectMessage.object?.counter
            RO->>Counter: LiveCounter.fromObjectState(objectMessage)
        else objectMessage.object?.map
            RO->>Map: LiveMap.fromObjectState(objectMessage)
        else unknown shape
            RO->>RO: log skip
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through syncs both near and far,
Collected objects, stitched each map and star,
Counters kept whole, maps merged piece by piece,
Old pool retired — the new one brings us peace,
A rabbit cheers for tidy sync release!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'AIT-38 Add support for partial sync of objects - protocol v6' clearly and concisely describes the main change: adding support for partial object synchronization in protocol v6, which aligns with the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AIT-38/partial-objects-sync

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/2152/bundle-report January 23, 2026 12:00 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features January 23, 2026 12:00 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/typedoc January 23, 2026 12:00 Inactive
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/plugins/liveobjects/realtimeobject.ts`:
- Around line 372-381: The code uses objectState.objectId as a Map key without
validation which can cause messages to pile up under undefined; inside the loop
in realtimeobject.ts where objectState and objectId are extracted (symbols:
objectState, objectId, this._syncObjectsPool), add a guard that checks if
objectId is null/undefined and if so skip storing/merging that message and emit
a warning/error (use the existing logger on the class, e.g., this.logger.warn or
console.warn) before continue; ensure no entry is created in _syncObjectsPool
for missing objectId and include the offending message or minimal context in the
log to aid debugging.
🧹 Nitpick comments (1)
test/realtime/liveobjects.test.js (1)

1182-1242: Consider asserting on a post-sync root to avoid timing races.
The assertions currently use entryPathObject created before the partial sync; if OBJECT_SYNC application ever becomes async, this can become flaky. Grabbing the root after the final sync message guarantees you’re asserting on the fresh state.

♻️ Suggested tweak
-            expect(entryPathObject.get('stringKey').value()).to.equal('hello', 'Check root has correct string value');
-            expect(entryPathObject.get('counter').value()).to.equal(15, 'Check counter has correct aggregated value');
-            expect(entryPathObject.get('map').get('foo').value()).to.equal('bar', 'Check map has initial entries');
-            expect(entryPathObject.get('map').get('baz').value()).to.equal('qux', 'Check map has materialised entries');
+            const root = await channel.object.get(); // ensure OBJECT_SYNC has fully applied
+            expect(root.get('stringKey').value()).to.equal('hello', 'Check root has correct string value');
+            expect(root.get('counter').value()).to.equal(15, 'Check counter has correct aggregated value');
+            expect(root.get('map').get('foo').value()).to.equal('bar', 'Check map has initial entries');
+            expect(root.get('map').get('baz').value()).to.equal('qux', 'Check map has materialised entries');

Comment thread src/plugins/liveobjects/realtimeobject.ts Outdated
Copy link
Copy Markdown
Collaborator

@lawrence-forooghian lawrence-forooghian left a comment

Choose a reason for hiding this comment

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

LGTM, some minor comments. I'd suggest we finalise the spec (have left some comments there) and update the spec point references before merging.

Comment thread src/plugins/liveobjects/realtimeobject.ts Outdated
Comment thread src/plugins/liveobjects/syncobjectsdatapool.ts Outdated
Comment thread src/plugins/liveobjects/realtimeobject.ts Outdated
Comment thread test/realtime/liveobjects.test.js Outdated
Comment thread test/realtime/liveobjects.test.js Outdated
@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from dea317a to 7c87463 Compare February 20, 2026 10:29
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features February 20, 2026 10:30 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/bundle-report February 20, 2026 10:30 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/typedoc February 20, 2026 10:30 Inactive
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/plugins/liveobjects/realtimeobject.ts (1)

411-423: Consider extracting typed locals to avoid repeated non-null assertions.

The ! assertions on Lines 412, 413, 415, 416, 422 are safe because callers guarantee .object and .map are set, but they repeat 5 times and obscure that guarantee for future readers. Extracting to typed locals at the top makes the precondition explicit once.

Proposed refactor
  private _mergeMapSyncState(existingEntry: ObjectMessage, newObjectMessage: ObjectMessage): void {
-   const existingObjectState = existingEntry.object!;
-   const newObjectState = newObjectMessage.object!;
+   const existingMap = existingEntry.object!.map!;
+   const newMap = newObjectMessage.object!.map;

-   if (!existingObjectState.map!.entries) {
-     existingObjectState.map!.entries = {};
+   if (!existingMap.entries) {
+     existingMap.entries = {};
    }

-   if (newObjectState.map?.entries) {
-     // During partial sync, no two messages contain the same map key,
-     // so entries can be merged directly without conflict checking.
-     Object.assign(existingObjectState.map!.entries, newObjectState.map.entries);
+   if (newMap?.entries) {
+     Object.assign(existingMap.entries, newMap.entries);
    }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/liveobjects/realtimeobject.ts` around lines 411 - 423, In
_mergeMapSyncState, remove repeated non-null assertions by extracting typed
local variables for existingEntry.object and newObjectMessage.object and their
.map properties at the top (e.g., const existingObject = existingEntry.object;
const existingMap = existingObject.map; const newObject =
newObjectMessage.object; const newMap = newObject.map), ensure you initialize
existingMap.entries when missing and then use existingMap.entries and
newMap.entries in the subsequent logic (including the Object.assign call) so the
function no longer repeats the `!` assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/plugins/liveobjects/realtimeobject.ts`:
- Around line 372-381: The code currently reads objectId = objectState.objectId
and uses it as a Map key (this._syncObjectsPool.set) without validating it; add
an early guard after extracting objectState (the same place where you check
!objectMessage.object) to ensure objectId is present and valid (e.g., if
(!objectId) { log/warn and continue; }) so malformed messages do not insert
under undefined; update the handling around objectState/objectId in the same
block to skip and optionally log the offending objectMessage when objectId is
missing.

---

Nitpick comments:
In `@src/plugins/liveobjects/realtimeobject.ts`:
- Around line 411-423: In _mergeMapSyncState, remove repeated non-null
assertions by extracting typed local variables for existingEntry.object and
newObjectMessage.object and their .map properties at the top (e.g., const
existingObject = existingEntry.object; const existingMap = existingObject.map;
const newObject = newObjectMessage.object; const newMap = newObject.map), ensure
you initialize existingMap.entries when missing and then use existingMap.entries
and newMap.entries in the subsequent logic (including the Object.assign call) so
the function no longer repeats the `!` assertions.

@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from 7c87463 to a8afac7 Compare February 20, 2026 11:47
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features February 20, 2026 11:48 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/bundle-report February 20, 2026 11:48 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/typedoc February 20, 2026 11:48 Inactive
@VeskeR
Copy link
Copy Markdown
Contributor Author

VeskeR commented Feb 20, 2026

I have removed spec references temporarily as I'm going to rewrite the spec next. Will re-add them once the spec is agreed

Copy link
Copy Markdown

@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

🧹 Nitpick comments (2)
test/rest/http.test.js (1)

35-37: Optional: clarify the comment – it references the wrong Defaults property.

The comment says the assertion should not validate against Defaults.version, but X-Ably-Version is set from Defaults.protocolVersion (an integer), not Defaults.version (the SDK semver string). The real reason to hardcode '6' is to avoid a circular assertion against Defaults.protocolVersion.toString() itself.

✏️ Suggested comment clarification
-        // This test should not directly validate version against Defaults.version, as
-        // ultimately the version header has been derived from that value.
+        // Hardcoded rather than using Defaults.protocolVersion.toString() to avoid a
+        // circular assertion — the header value is derived directly from that constant.
         expect(headers['X-Ably-Version']).to.equal('6', 'Verify current version number');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/rest/http.test.js` around lines 35 - 37, Update the test comment to
reference Defaults.protocolVersion (an integer) instead of Defaults.version and
clarify why the header is hardcoded: the assertion compares
headers['X-Ably-Version'] to '6' to avoid a circular check against
Defaults.protocolVersion.toString(); modify the comment around the expect(...)
line (the assertion for headers['X-Ably-Version']) to state that the value
originates from Defaults.protocolVersion and is intentionally hardcoded in the
test to prevent validating against the source of the header itself.
src/plugins/liveobjects/syncobjectspool.ts (1)

54-82: First message for an objectId is stored unconditionally, but subsequent messages assume type consistency.

Line 56 stores the first ObjectMessage for a given objectId regardless of whether it carries a valid counter or map (i.e., an unsupported type is also stored). Subsequent messages for the same objectId are then routed by type—counters are rejected (Line 60-69), maps are merged (Line 72-74), and unsupported types are skipped (Line 77-82). This means the routing for the second message depends on the second message's type, not the first's, which could lead to inconsistent merging (e.g., first message is a counter, second is a map → merges into a counter entry).

Consider validating the type on the first store as well, or at least validating type consistency between existing and new entries before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/liveobjects/syncobjectspool.ts` around lines 54 - 82, The code
stores the first ObjectMessage into this._pool unconditionally which allows a
later message of a different type to be merged into an incompatible entry;
update SyncObjectsPool.applyObjectSyncMessages() to enforce type validation:
when inserting the initial entry (the branch that currently does
this._pool.set(objectId, objectMessage)) only accept/store messages that contain
a known type (objectState.counter or objectState.map), otherwise log and skip,
and additionally before calling _mergeMapSyncState or rejecting as a counter,
compare the existingEntry's type (inspect existingEntry.objectState.map/counter)
against the incoming objectMessage.objectState and if they differ log a clear
error and skip the message to avoid cross-type merges.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/liveobjects/syncobjectspool.ts`:
- Around line 89-110: The _mergeMapSyncState function assumes
existingEntry.object.map exists; add a guard so if existingObjectState.map is
undefined (i.e., the existing entry is a non-map), initialize it to a map
envelope with an empty entries object before touching entries; then proceed to
merge newObjectState.map.entries as before (use existingObjectState.map = {
entries: {} } or equivalent to preserve type expectations), ensuring you only
call Object.assign when newObjectState.map?.entries is present.

---

Duplicate comments:
In `@src/plugins/liveobjects/syncobjectspool.ts`:
- Around line 50-57: The code uses objectState.objectId as a Map key without
validating it, which can insert undefined/empty keys into this._pool; add a
guard after const objectId = objectState.objectId to validate it (non-null,
non-undefined, non-empty string/invalid type) and skip processing this
objectMessage when invalid (e.g., log/warn and continue) so you never call
this._pool.get/set with a bogus key; update the block around objectId,
existingEntry and this._pool to perform this validation before accessing the
Map.

---

Nitpick comments:
In `@src/plugins/liveobjects/syncobjectspool.ts`:
- Around line 54-82: The code stores the first ObjectMessage into this._pool
unconditionally which allows a later message of a different type to be merged
into an incompatible entry; update SyncObjectsPool.applyObjectSyncMessages() to
enforce type validation: when inserting the initial entry (the branch that
currently does this._pool.set(objectId, objectMessage)) only accept/store
messages that contain a known type (objectState.counter or objectState.map),
otherwise log and skip, and additionally before calling _mergeMapSyncState or
rejecting as a counter, compare the existingEntry's type (inspect
existingEntry.objectState.map/counter) against the incoming
objectMessage.objectState and if they differ log a clear error and skip the
message to avoid cross-type merges.

In `@test/rest/http.test.js`:
- Around line 35-37: Update the test comment to reference
Defaults.protocolVersion (an integer) instead of Defaults.version and clarify
why the header is hardcoded: the assertion compares headers['X-Ably-Version'] to
'6' to avoid a circular check against Defaults.protocolVersion.toString();
modify the comment around the expect(...) line (the assertion for
headers['X-Ably-Version']) to state that the value originates from
Defaults.protocolVersion and is intentionally hardcoded in the test to prevent
validating against the source of the header itself.

Comment thread src/plugins/liveobjects/syncobjectspool.ts Outdated
@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from a8afac7 to 1ff0df1 Compare February 20, 2026 12:57
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features February 20, 2026 12:58 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/typedoc February 20, 2026 12:58 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/bundle-report February 20, 2026 12:58 Inactive
@VeskeR VeskeR changed the base branch from main to AIT-315/protocol-v6-object-message February 20, 2026 12:58
@lawrence-forooghian
Copy link
Copy Markdown
Collaborator

I have removed spec references temporarily as I'm going to rewrite the spec next. Will re-add them once the spec is agreed

I've approved the spec PR — please could you add these references here?

@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from 1ff0df1 to 83e7d8f Compare February 23, 2026 16:30
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features February 23, 2026 16:31 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/bundle-report February 23, 2026 16:32 Inactive
@VeskeR
Copy link
Copy Markdown
Contributor Author

VeskeR commented Feb 23, 2026

I've approved the spec PR — please could you add these references here?

Added spec references in https://github.com/ably/ably-js/compare/1ff0df106e844a91ab2c695f56466b3d3edff34a..83e7d8f206be609721bf1f017b0ce960e51d9fab

@VeskeR VeskeR force-pushed the AIT-315/protocol-v6-object-message branch 3 times, most recently from 76192dd to 8c2b980 Compare February 25, 2026 14:42
Copy link
Copy Markdown
Collaborator

@lawrence-forooghian lawrence-forooghian left a comment

Choose a reason for hiding this comment

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

LGTM, happy to approve once rebased and squashed

@VeskeR VeskeR force-pushed the AIT-315/protocol-v6-object-message branch 3 times, most recently from 4e78cc9 to ecbd145 Compare February 26, 2026 14:56
@VeskeR VeskeR changed the title [AIT-38] Protocol v6 and support for partial sync of objects [AIT-38] Add support for partial sync of objects - protocol v6 Feb 26, 2026
@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from 4f71d20 to f0902aa Compare February 26, 2026 21:58
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/features February 26, 2026 21:59 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/bundle-report February 26, 2026 21:59 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2152/typedoc February 26, 2026 21:59 Inactive
@VeskeR
Copy link
Copy Markdown
Contributor Author

VeskeR commented Feb 26, 2026

LGTM, happy to approve once rebased and squashed

Rebased on top of #2159 (currently d348a98) and squashed. Please approve if you're happy, I'm going to merge once #2159 lands

@VeskeR VeskeR force-pushed the AIT-315/protocol-v6-object-message branch from d348a98 to fdc33cb Compare February 27, 2026 17:54
Base automatically changed from AIT-315/protocol-v6-object-message to integration/protocol-v6 February 27, 2026 20:26
Add:
- check client is ably build the final object tree across multiple sync
  messages
- check sync sequence does not break when receiving an unknown object
  type
@VeskeR VeskeR force-pushed the AIT-38/partial-objects-sync branch from f0902aa to d0bc431 Compare February 27, 2026 20:28
@VeskeR VeskeR merged commit 1be8271 into integration/protocol-v6 Feb 27, 2026
16 of 26 checks passed
@VeskeR VeskeR deleted the AIT-38/partial-objects-sync branch February 27, 2026 22:19
VeskeR added a commit that referenced this pull request Mar 12, 2026
[AIT-38] Add support for partial sync of objects - protocol v6
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.

2 participants