Change ObjectOperation.initialValue to be a JSON encoded string#2065
Change ObjectOperation.initialValue to be a JSON encoded string#2065
ObjectOperation.initialValue to be a JSON encoded string#2065Conversation
WalkthroughInitial-value handling for live objects is moved from binary buffers to JSON strings. Signatures and helpers across Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant LiveObject as LiveCounter/LiveMap
participant ObjectMessage
participant ObjectId
Client->>LiveObject: request create (e.g. count or entries)
LiveObject->>ObjectMessage: build operation (createInitialValueOperation)
LiveObject->>ObjectMessage: createInitialValueJSONString(operation, client)
ObjectMessage-->>LiveObject: initialValueJSONString
LiveObject->>ObjectId: fromInitialValue(platform, type, initialValueJSONString, nonce, ts)
ObjectId-->>LiveObject: objectId
LiveObject-->>Client: ObjectMessage { initialValue: initialValueJSONString, objectId, ... }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.38.6)test/realtime/objects.test.jsTip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
c241fbf to
a37559d
Compare
There isn't a spec PR for this yet, but it's needed in order to implement the write spec [1]. Asked about it in [2]. It's implemented in JS in [3]. Got Cursor to do this. [1] ably/specification#353 [2] https://github.com/ably/specification/pull/353/files#r2228017382 [3] ably/ably-js#2065
There isn't a spec PR for this yet, but it's needed in order to implement the write spec [1]. Asked about it in [2]. It's implemented in JS in [3]. Got Cursor to do this. [1] ably/specification#353 [2] https://github.com/ably/specification/pull/353/files#r2228017382 [3] ably/ably-js#2065
There isn't a spec PR for this yet, but it's needed in order to implement the write spec [1]. Asked about it in [2]. It's implemented in JS in [3]. Got Cursor to do this. [1] ably/specification#353 [2] https://github.com/ably/specification/pull/353/files#r2228017382 [3] ably/ably-js#2065
a37559d to
ad89800
Compare
This updates ably-js SDK to use simpler `initialValue` API added in [1], based on the LiveObjects DR [2] [1] ably/realtime#7549 [2] https://ably.atlassian.net/wiki/x/GID-9Q
ad89800 to
c5c8076
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/plugins/objects/objectmessage.ts (3)
146-155: initialValue should be treated as write-only; strip on decode and document construction pathGood addition and clear spec notes. Two follow-ups recommended:
- Runtime: prevent leaking
initialValueback to app code if it ever arrives from the server (spec says “must not be read by the client”).- Docs: nudge SDK users and internal callsites to always build this via
createInitialValueJSONString(...)to ensure proper canonicalization and bytes handling.Apply these small changes:
/** * The initial value of the object, represented as a JSON string. * Used along with the nonce and timestamp to create the object ID. * * This field must be set by the client for MAP_CREATE and COUNTER_CREATE operations. * The server uses it to verify the object ID, and after verification, the JSON string * is decoded into the initial value for new Map or Counter objects. * - * This field must not be read by the client if received from the server. + * This field must not be read by the client if received from the server. + * + * Important: Construct this string via {@link createInitialValueJSONString} to ensure + * consistent JSON encoding (including proper bytes/base64 handling). */And (outside this hunk) strip it on decode before returning to user code:
// place just before `return result;` in WireObjectMessage.decode() if (result.operation && 'initialValue' in result.operation) { delete (result.operation as any).initialValue; }I can open a follow-up PR to add a unit test that asserts
initialValueis absent on decoded messages.
241-246: Trivial pass-through helper — consider inlining (optional)
encodeObjectDatasimply forwards toencodeFn. You could inline the call sites for less indirection, unless you prefer the semantic name for clarity.
247-276: createInitialValueJSONString: defensively exclude stale nested initialValue and sanitize inputGreat approach: encode as a wire operation first, then stringify for JSON. Two low-risk hardening tweaks:
- Don’t accidentally embed a stale
operation.initialValueinside itself if a caller reuses anObjectOperationobject.- (Optional) Validate the action is one of the *_CREATE actions to catch misuse early.
Apply this minimal diff:
export function createInitialValueJSONString( operation: Partial<ObjectOperation<ObjectData>>, client: BaseClient, ): string { + // Avoid embedding a stale `initialValue` inside itself if present on the input + // (e.g. when reusing a previously prepared operation object). + const { initialValue: _ignored, ...opWithoutInitialValue } = operation as any; // the object operation may contain user-provided data that requires encoding. // for example, buffers must be encoded since the initial value will be represented as a JSON string. // we can use ObjectMessage methods to encode the object operation. const msg = ObjectMessage.fromValues( - { operation: operation as ObjectOperation<ObjectData> }, + { operation: opWithoutInitialValue as ObjectOperation<ObjectData> }, client.Utils, client.MessageEncoding, ); const wireMsg = msg.encode(client); // get the encoded operation that is safe to be sent over the wire as a JSON string. const { operation: encodedOperation } = wireMsg.encodeForWire(client.Utils.Format.json); // finally, initialValue is the JSON string representation of the encoded operation. return JSON.stringify(encodedOperation); }If you want to enforce the *_CREATE-only precondition, we can add a lightweight guard that logs via
client.Loggerwhenactionis neitherMAP_CREATEnorCOUNTER_CREATE.I can add unit tests that cover: (1) buffers inside map entries are base64 in the output JSON, (2) nested
initialValueis not included, (3) non-create actions are rejected/logged.
📜 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.
📒 Files selected for processing (5)
src/plugins/objects/livecounter.ts(3 hunks)src/plugins/objects/livemap.ts(3 hunks)src/plugins/objects/objectid.ts(1 hunks)src/plugins/objects/objectmessage.ts(8 hunks)test/realtime/objects.test.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/plugins/objects/livemap.ts
- test/realtime/objects.test.js
- src/plugins/objects/objectid.ts
- src/plugins/objects/livecounter.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: VeskeR
PR: ably/ably-js#1924
File: src/plugins/liveobjects/statemessage.ts:220-224
Timestamp: 2024-11-22T17:15:25.075Z
Learning: In `src/plugins/liveobjects/statemessage.ts`, using `JSON.parse(JSON.stringify(...))` for deep copying is acceptable when buffer values are preserved in subsequent code.
⏰ 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: test-browser (webkit)
- GitHub Check: test-browser (firefox)
- GitHub Check: test-node (16.x)
- GitHub Check: test-node (18.x)
- GitHub Check: test-browser (chromium)
- GitHub Check: test-npm-package
- GitHub Check: test-node (20.x)
🔇 Additional comments (6)
src/plugins/objects/objectmessage.ts (6)
7-7: Broadened encoder input type — LGTMAllowing
EncodeObjectDataFunctionto accept eitherObjectDataorWireObjectDatasimplifies reuse across pre-wire and wire-encoding paths.
187-193: encode(...) signature update — LGTMSwitching to a single
EncodeObjectDataFunctionkeeps the API narrow and avoids the need for a separate “initial value” encoder.
223-226: encodeMapEntries signature simplification — LGTMConsistent use of
EncodeObjectDataFunctionacross helpers improves readability.
389-410: ObjectMessage.encode: bytes/JSON encoding path — LGTMEncoding covers Bufferlike, primitives, and JSON objects correctly, deferring bytes Base64 to wire encoding. This aligns with our previous learning about deep copying with JSON while preserving original buffers for encoding.
551-552: toJSON delegates to instance encodeForWire — LGTMNice containment; removing private fields via destructuring is clean.
473-486: No remaining static calls or encoding metadata usages foundBoth verification steps confirmed that:
- There are no static invocations of
WireObjectMessage.encodeForWireanywhere in the codebase.- No consumers reference an
encodingfield on object payloads, so dropping the encoding metadata is safe.All concerns have been addressed.
This updates ably-js SDK to use simpler
initialValueAPI added to realtime in [1], based on the LiveObjects DR [2][1] https://github.com/ably/realtime/pull/7549
[2] https://ably.atlassian.net/wiki/x/GID-9Q
Summary by CodeRabbit