11import deepEqual from 'deep-equal' ;
22
33import type * as API from '../../../ably' ;
4- import { LiveObject , LiveObjectData , LiveObjectUpdate , LiveObjectUpdateNoop } from './liveobject' ;
4+ import { BufferedOperation , LiveObject , LiveObjectData , LiveObjectUpdate , LiveObjectUpdateNoop } from './liveobject' ;
55import { LiveObjects } from './liveobjects' ;
66import {
77 MapSemantics ,
@@ -77,10 +77,12 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
7777
7878 /**
7979 * Returns the value associated with the specified key in the underlying Map object.
80- * If no element is associated with the specified key, undefined is returned.
81- * If the value that is associated to the provided key is an objectId string of another Live Object,
82- * then you will get a reference to that Live Object if it exists in the local pool, or undefined otherwise.
83- * If the value is not an objectId, then you will get that value.
80+ *
81+ * - If no entry is associated with the specified key, `undefined` is returned.
82+ * - If map entry is tombstoned (deleted), `undefined` is returned.
83+ * - If the value associated with the provided key is an objectId string of another Live Object, a reference to that Live Object
84+ * is returned, provided it exists in the local pool and is valid. Otherwise, `undefined` is returned.
85+ * - If the value is not an objectId, then that value is returned.
8486 */
8587 // force the key to be of type string as we only allow strings as key in a map
8688 get < TKey extends keyof T & string > ( key : TKey ) : T [ TKey ] {
@@ -94,14 +96,26 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
9496 return undefined as T [ TKey ] ;
9597 }
9698
97- // data exists for non-tombstoned elements
99+ // data always exists for non-tombstoned elements
98100 const data = element . data ! ;
99101
100102 if ( 'value' in data ) {
103+ // map entry has a primitive type value, just return it as is.
101104 return data . value as T [ TKey ] ;
102- } else {
103- return this . _liveObjects . getPool ( ) . get ( data . objectId ) as T [ TKey ] ;
104105 }
106+
107+ // map entry points to another object, get it from the pool
108+ const refObject : LiveObject | undefined = this . _liveObjects . getPool ( ) . get ( data . objectId ) ;
109+ if ( ! refObject ) {
110+ return undefined as T [ TKey ] ;
111+ }
112+
113+ if ( ! refObject . isValid ( ) ) {
114+ // non-valid objects must not be surfaced to the end users
115+ return undefined as T [ TKey ] ;
116+ }
117+
118+ return refObject as API . LiveObject as T [ TKey ] ;
105119 }
106120
107121 size ( ) : number {
@@ -112,6 +126,13 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
112126 continue ;
113127 }
114128
129+ // data always exists for non-tombstoned elements
130+ const data = value . data ! ;
131+ if ( 'objectId' in data && ! this . _liveObjects . getPool ( ) . get ( data . objectId ) ?. isValid ( ) ) {
132+ // should not count non-valid objects
133+ continue ;
134+ }
135+
115136 size ++ ;
116137 }
117138
@@ -145,6 +166,16 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
145166 // as it's important to mark that the op was processed by the object
146167 this . _siteTimeserials [ opSiteCode ] = opOriginTimeserial ;
147168
169+ if ( msg . isMapSetWithObjectIdReference ( ) && ! this . _liveObjects . getPool ( ) . get ( op . mapOp ?. data ?. objectId ! ) ?. isValid ( ) ) {
170+ // invalid objects must not be surfaced to the end users, so we cannot apply this MAP_SET operation on the map yet,
171+ // as it will set the key to point to the invalid object. we also can't just update the key on a map right now, as
172+ // that would require us to send an update event for the key, and the user will end up with a key on map which got
173+ // updated to return undefined, which is undesired. instead we should buffer the MAP_SET operation until referenced
174+ // object becomes valid
175+ this . _handleMapSetWithInvalidObjectReference ( op . mapOp ! , opOriginTimeserial ) ;
176+ return ;
177+ }
178+
148179 let update : LiveMapUpdate | LiveObjectUpdateNoop ;
149180 switch ( op . action ) {
150181 case StateOperationAction . MAP_CREATE :
@@ -231,7 +262,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
231262
232263 const previousDataRef = this . _dataRef ;
233264 // override all relevant data for this object with data from the state object
234- this . _createOperationIsMerged = false ;
265+ this . _setCreateOperationIsMerged ( false ) ;
235266 this . _dataRef = this . _liveMapDataFromMapEntries ( stateObject . map ?. entries ?? { } ) ;
236267 // should default to empty map if site timeserials do not exist on the state object, so that any future operation can be applied to this object
237268 this . _siteTimeserials = stateObject . siteTimeserials ?? { } ;
@@ -331,7 +362,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
331362 Object . assign ( aggregatedUpdate . update , update . update ) ;
332363 } ) ;
333364
334- this . _createOperationIsMerged = true ;
365+ this . _setCreateOperationIsMerged ( true ) ;
335366
336367 return aggregatedUpdate ;
337368 }
@@ -344,6 +375,38 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
344375 ) ;
345376 }
346377
378+ private _handleMapSetWithInvalidObjectReference ( op : StateMapOp , opOriginTimeserial : string | undefined ) : void {
379+ const refObjectId = op ?. data ?. objectId ! ;
380+ // ensure referenced object always exist so we can subscribe to it becoming valid
381+ this . _liveObjects . getPool ( ) . createZeroValueObjectIfNotExists ( refObjectId ) ;
382+
383+ // wait until the referenced object becomes valid, then apply MAP_SET operation,
384+ // as it will now point to the existing valid object
385+ const { off } = this . _liveObjects
386+ . getPool ( )
387+ . get ( refObjectId ) !
388+ . onceValid ( ( ) => {
389+ try {
390+ const update = this . _applyMapSet ( op , opOriginTimeserial ) ;
391+ this . notifyUpdated ( update ) ;
392+ } catch ( error ) {
393+ this . _client . Logger . logAction (
394+ this . _client . logger ,
395+ this . _client . Logger . LOG_ERROR ,
396+ `LiveMap._handleMapSetWithInvalidObjectReference()` ,
397+ `error applying buffered MAP_SET operation: ${ this . _client . Utils . inspectError ( error ) } ` ,
398+ ) ;
399+ } finally {
400+ this . _bufferedOperations . delete ( bufferedOperation ) ;
401+ }
402+ } ) ;
403+
404+ const bufferedOperation : BufferedOperation = {
405+ cancel : ( ) => off ( ) ,
406+ } ;
407+ this . _bufferedOperations . add ( bufferedOperation ) ;
408+ }
409+
347410 private _applyMapCreate ( op : StateOperation ) : LiveMapUpdate | LiveObjectUpdateNoop {
348411 if ( this . _createOperationIsMerged ) {
349412 // There can't be two different create operation for the same object id, because the object id
@@ -395,11 +458,6 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
395458 let liveData : StateData ;
396459 if ( ! Utils . isNil ( op . data . objectId ) ) {
397460 liveData = { objectId : op . data . objectId } as ObjectIdStateData ;
398- // this MAP_SET op is setting a key to point to another object via its object id,
399- // but it is possible that we don't have the corresponding object in the pool yet (for example, we haven't seen the *_CREATE op for it).
400- // we don't want to return undefined from this map's .get() method even if we don't have the object,
401- // so instead we create a zero-value object for that object id if it not exists.
402- this . _liveObjects . getPool ( ) . createZeroValueObjectIfNotExists ( op . data . objectId ) ;
403461 } else {
404462 liveData = { encoding : op . data . encoding , value : op . data . value } as ValueStateData ;
405463 }
0 commit comments