[AIT-38] Add support for partial sync of objects - protocol v6#2152
[AIT-38] Add support for partial sync of objects - protocol v6#2152VeskeR merged 3 commits intointegration/protocol-v6from
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaces the legacy Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 useentryPathObjectcreated 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');
dea317a to
7c87463
Compare
There was a problem hiding this comment.
🧹 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.objectand.mapare 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.
7c87463 to
a8afac7
Compare
|
I have removed spec references temporarily as I'm going to rewrite the spec next. Will re-add them once the spec is agreed |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
test/rest/http.test.js (1)
35-37: Optional: clarify the comment – it references the wrongDefaultsproperty.The comment says the assertion should not validate against
Defaults.version, butX-Ably-Versionis set fromDefaults.protocolVersion(an integer), notDefaults.version(the SDK semver string). The real reason to hardcode'6'is to avoid a circular assertion againstDefaults.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 anobjectIdis stored unconditionally, but subsequent messages assume type consistency.Line 56 stores the first
ObjectMessagefor a givenobjectIdregardless of whether it carries a validcounterormap(i.e., an unsupported type is also stored). Subsequent messages for the sameobjectIdare 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.
a8afac7 to
1ff0df1
Compare
I've approved the spec PR — please could you add these references here? |
1ff0df1 to
83e7d8f
Compare
Added spec references in https://github.com/ably/ably-js/compare/1ff0df106e844a91ab2c695f56466b3d3edff34a..83e7d8f206be609721bf1f017b0ce960e51d9fab |
76192dd to
8c2b980
Compare
lawrence-forooghian
left a comment
There was a problem hiding this comment.
LGTM, happy to approve once rebased and squashed
4e78cc9 to
ecbd145
Compare
4f71d20 to
f0902aa
Compare
d348a98 to
fdc33cb
Compare
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
See spec [1] and a DR [2] Resolves AIT-38 [1] ably/specification#413 [2] https://ably.atlassian.net/wiki/x/AQBxCQE
f0902aa to
d0bc431
Compare
[AIT-38] Add support for partial sync of objects - protocol v6
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
SyncObjectsDataPoolclass and simplifies a sync logic a bit by moving it toRealtimeObject.Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests