diff --git a/src/plugins/liveobjects/livemap.ts b/src/plugins/liveobjects/livemap.ts index a76f63f22..3b67d41cb 100644 --- a/src/plugins/liveobjects/livemap.ts +++ b/src/plugins/liveobjects/livemap.ts @@ -9,10 +9,12 @@ import { Primitive, Value, } from '../../../liveobjects'; +import { DefaultInstance } from './instance'; import { LiveCounter } from './livecounter'; import { LiveCounterValueType } from './livecountervaluetype'; import { LiveMapValueType } from './livemapvaluetype'; import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject'; +import { DefaultPathObject } from './pathobject'; import { getObjectDataPrimitive, MapRemove, @@ -186,6 +188,19 @@ export class LiveMap = Record> ) { throw new client.ErrorInfo('Map value data type is unsupported', 40013, 400); // OD4a } + + // RTLMV4c1 - live objects obtained from the channel, and the public objects that wrap them, + // are not valid map values. Without this check they would fall through to the JSON encoding + // branch and fail with a confusing serialization error (or leak internal state to the wire). + // To assign an object to a key, a new object must be created via the LiveMap.create() or + // LiveCounter.create() value types instead. + if (value instanceof LiveObject || value instanceof DefaultPathObject || value instanceof DefaultInstance) { + throw new client.ErrorInfo( + 'Map value data type is unsupported: a reference to an existing object cannot be assigned as a map value; use LiveMap.create() or LiveCounter.create() to create a new object', + 40013, + 400, + ); + } } /** diff --git a/test/realtime/liveobjects.test.js b/test/realtime/liveobjects.test.js index 21f62a8f3..cc13ee45f 100644 --- a/test/realtime/liveobjects.test.js +++ b/test/realtime/liveobjects.test.js @@ -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'); }, },