-
Notifications
You must be signed in to change notification settings - Fork 60
fix(liveobjects): reject live object references as map values #2246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4060,7 +4060,7 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f | |
| { | ||
| description: 'LiveMap.set throws on invalid input', | ||
| action: async (ctx) => { | ||
| const { objectsHelper, channelName, entryInstance } = ctx; | ||
| const { objectsHelper, channelName, entryInstance, entryPathObject } = ctx; | ||
|
|
||
| const mapCreatedPromise = waitForMapKeyUpdate(entryInstance, 'map'); | ||
| await objectsHelper.createAndSetOnMap(channelName, { | ||
|
|
@@ -4086,6 +4086,12 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f | |
| await expectToThrowAsync(async () => map.set('key', null), 'Map value data type is unsupported'); | ||
| await expectToThrowAsync(async () => map.set('key', BigInt(1)), 'Map value data type is unsupported'); | ||
| await expectToThrowAsync(async () => map.set('key', Symbol()), 'Map value data type is unsupported'); | ||
|
|
||
| // RTLMV4c1 - references to existing objects (Instance or PathObject) are not valid | ||
| // map values; only LiveMap.create()/LiveCounter.create() value types can assign objects | ||
| await expectToThrowAsync(async () => map.set('key', map), 'Map value data type is unsupported'); | ||
| await expectToThrowAsync(async () => map.set('key', entryInstance), 'Map value data type is unsupported'); | ||
| await expectToThrowAsync(async () => map.set('key', entryPathObject), 'Map value data type is unsupported'); | ||
|
Comment on lines
+4090
to
+4094
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert RTLMV4c1 error code/status in the new rejection checks. These new cases currently verify only the message. Since this PR contract requires Suggested test tightening- await expectToThrowAsync(async () => map.set('key', map), 'Map value data type is unsupported');
- await expectToThrowAsync(async () => map.set('key', entryInstance), 'Map value data type is unsupported');
- await expectToThrowAsync(async () => map.set('key', entryPathObject), 'Map value data type is unsupported');
+ await expectToThrowAsync(async () => map.set('key', map), 'Map value data type is unsupported', {
+ withCode: 40013,
+ withStatusCode: 400,
+ });
+ await expectToThrowAsync(async () => map.set('key', entryInstance), 'Map value data type is unsupported', {
+ withCode: 40013,
+ withStatusCode: 400,
+ });
+ await expectToThrowAsync(async () => map.set('key', entryPathObject), 'Map value data type is unsupported', {
+ withCode: 40013,
+ withStatusCode: 400,
+ });🤖 Prompt for AI Agents |
||
| }, | ||
| }, | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai, go through the PR description and understand the problem. I'm still not sure, how come above if check
if ( value === null || (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && typeof value !== 'object') ) {, didn't return early error till it reached here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sacOO7Good question — this comes down to howtypeofworks in JavaScript.The first check:
only filters out
null, and non-primitive/non-object types likeundefined,function,symbol, orbigint. It's a coarse-grained check based on JavaScript'stypeofoperator, which has only a handful of possible return values ('string','number','boolean','object','function','undefined','symbol','bigint').Crucially,
typeofreturns'object'for every non-null object reference — plain object literals, arrays,Map/Setinstances, and also class instances likeLiveObject,DefaultPathObject, andDefaultInstance. There's no way to tell them apart usingtypeofalone; it can't distinguish "a plain JSON-like object" from "an instance of a specific class."So when you did
map.set('b', map.get('a')), the returned value was anInstance/PathObjectwrapper (or internally aLiveObject) — itstypeofis'object', so it sailed right past the first check. It then fell through to the JSON-encoding branch (primitiveToObjectData), which is where it either hit the circular-structureTypeErrorat publish time, or silently serialized internal state.That's exactly why this PR adds a second, more specific check using
instanceof:instanceofwalks the prototype chain to check whether the value was actually constructed by one of these specific classes, whichtypeofsimply cannot do. The two checks are complementary: the first rejects primitives/null/unsupported types, and the second (new) one narrows further within the'object'bucket to reject specific disallowed object types (live object references), while still allowing plain JSON-serializable objects/arrays through.🐇💭