@@ -56,6 +56,7 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
5656 implements PublicLiveMap < T >
5757{
5858 declare readonly [ __livetype ] : 'LiveMap' ; // type-only, unique symbol to satisfy branded interfaces, no JS emitted
59+ private _clearTimeserial ?: string ;
5960
6061 constructor (
6162 realtimeObject : RealtimeObject ,
@@ -358,6 +359,10 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
358359 update = this . _applyObjectDelete ( msg ) ;
359360 break ;
360361
362+ case ObjectOperationAction . MAP_CLEAR :
363+ update = this . _applyMapClear ( msg ) ;
364+ break ;
365+
361366 default :
362367 throw new this . _client . ErrorInfo (
363368 `Invalid ${ op . action } op for LiveMap objectId=${ this . getObjectId ( ) } ` ,
@@ -440,6 +445,7 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
440445 } else {
441446 // otherwise override data for this object with data from the object state
442447 this . _createOperationIsMerged = false ; // RTLM6b
448+ this . _clearTimeserial = objectState . map ?. clearTimeserial ;
443449 this . _dataRef = this . _liveMapDataFromMapEntries ( objectState . map ?. entries ?? { } ) ; // RTLM6c
444450 // RTLM6d
445451 if ( ! this . _client . Utils . isNil ( objectState . createOp ) ) {
@@ -737,9 +743,20 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
737743 ) : LiveMapUpdate < T > | LiveObjectUpdateNoop {
738744 const { ErrorInfo, Utils } = this . _client ;
739745
746+ // Check operation's serial against clearTimeserial first
747+ if ( this . _clearTimeserial && ( ! opSerial || this . _clearTimeserial >= opSerial ) ) {
748+ this . _client . Logger . logAction (
749+ this . _client . logger ,
750+ this . _client . Logger . LOG_MICRO ,
751+ 'LiveMap._applyMapSet()' ,
752+ `skipping update for key="${ op . key } ": op serial ${ opSerial } < clear serial ${ this . _clearTimeserial } ; objectId=${ this . getObjectId ( ) } ` ,
753+ ) ;
754+ return { noop : true } ;
755+ }
756+
740757 const existingEntry = this . _dataRef . data . get ( op . key ) ;
741758 // RTLM7a
742- if ( existingEntry && ! this . _canApplyMapOperation ( existingEntry . timeserial , opSerial ) ) {
759+ if ( existingEntry && ! this . _canApplyMapEntryOperation ( existingEntry . timeserial , opSerial ) ) {
743760 // RTLM7a1 - the operation's serial <= the entry's serial, ignore the operation.
744761 this . _client . Logger . logAction (
745762 this . _client . logger ,
@@ -823,9 +840,11 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
823840 opTimestamp : number | undefined ,
824841 msg : ObjectMessage ,
825842 ) : LiveMapUpdate < T > | LiveObjectUpdateNoop {
843+ // No need to check against clearTimeserial - a remove after a clear is valid
844+
826845 const existingEntry = this . _dataRef . data . get ( op . key ) ;
827846 // RTLM8a
828- if ( existingEntry && ! this . _canApplyMapOperation ( existingEntry . timeserial , opSerial ) ) {
847+ if ( existingEntry && ! this . _canApplyMapEntryOperation ( existingEntry . timeserial , opSerial ) ) {
829848 // RTLM8a1 - the operation's serial <= the entry's serial, ignore the operation.
830849 this . _client . Logger . logAction (
831850 this . _client . logger ,
@@ -886,12 +905,91 @@ export class LiveMap<T extends Record<string, Value> = Record<string, Value>>
886905 return update ;
887906 }
888907
908+ private _applyMapClear ( objectMessage : ObjectMessage ) : LiveMapUpdate < T > | LiveObjectUpdateNoop {
909+ const opSerial = objectMessage . serial ! ;
910+
911+ if ( this . _clearTimeserial == null || opSerial > this . _clearTimeserial ) {
912+ this . _client . Logger . logAction (
913+ this . _client . logger ,
914+ this . _client . Logger . LOG_MICRO ,
915+ 'LiveMap._applyMapClear()' ,
916+ `updating clearTimeserial; previous=${ this . _clearTimeserial } , new=${ opSerial } ; objectId=${ this . getObjectId ( ) } ` ,
917+ ) ;
918+ this . _clearTimeserial = opSerial ;
919+ } else {
920+ this . _client . Logger . logAction (
921+ this . _client . logger ,
922+ this . _client . Logger . LOG_MICRO ,
923+ 'LiveMap._applyMapClear()' ,
924+ `skipping MAP_CLEAR: op serial ${ opSerial } <= current clear serial ${ this . _clearTimeserial } ; objectId=${ this . getObjectId ( ) } ` ,
925+ ) ;
926+ return { noop : true } ;
927+ }
928+
929+ let tombstonedAt : number ;
930+ if ( objectMessage . serialTimestamp != null ) {
931+ tombstonedAt = objectMessage . serialTimestamp ;
932+ } else {
933+ this . _client . Logger . logAction (
934+ this . _client . logger ,
935+ this . _client . Logger . LOG_MINOR ,
936+ 'LiveMap._applyMapClear()' ,
937+ `map has been cleared but no "serialTimestamp" found in the message, using local clock instead; objectId=${ this . getObjectId ( ) } ` ,
938+ ) ;
939+ tombstonedAt = Date . now ( ) ; // best-effort estimate since no timestamp provided by the server
940+ }
941+
942+ const update : LiveMapUpdate < T > = {
943+ update : { } ,
944+ objectMessage,
945+ _type : 'LiveMapUpdate' ,
946+ } ;
947+
948+ for ( const [ key , entry ] of this . _dataRef . data . entries ( ) ) {
949+ const entrySerial = entry . timeserial ;
950+ if ( entrySerial == null || this . _clearTimeserial >= entrySerial ) {
951+ this . _client . Logger . logAction (
952+ this . _client . logger ,
953+ this . _client . Logger . LOG_MICRO ,
954+ 'LiveMap._applyMapClear()' ,
955+ `clearing entry; key="${ key } ", entry serial=${ entrySerial } , clear serial=${ this . _clearTimeserial } , objectId=${ this . getObjectId ( ) } ` ,
956+ ) ;
957+
958+ // Handle parent reference removal for object references
959+ if ( entry . data && 'objectId' in entry . data ) {
960+ // Remove parent reference from the object that was being referenced
961+ const referencedObject = this . _realtimeObject . getPool ( ) . get ( entry . data . objectId ) ;
962+ if ( referencedObject ) {
963+ referencedObject . removeParentReference ( this , key ) ;
964+ }
965+ }
966+
967+ entry . tombstone = true ;
968+ entry . tombstonedAt = tombstonedAt ;
969+ entry . timeserial = this . _clearTimeserial ;
970+ entry . data = undefined ;
971+
972+ const typedKey : keyof T & string = key ;
973+ update . update [ typedKey ] = 'removed' ;
974+ } else {
975+ this . _client . Logger . logAction (
976+ this . _client . logger ,
977+ this . _client . Logger . LOG_MICRO ,
978+ 'LiveMap._applyMapClear()' ,
979+ `skipping clearing entry; key="${ key } ", entry serial=${ entrySerial } , clear serial=${ this . _clearTimeserial } , objectId=${ this . getObjectId ( ) } ` ,
980+ ) ;
981+ }
982+ }
983+
984+ return update ;
985+ }
986+
889987 /**
890988 * Returns true if the serials of the given operation and entry indicate that
891989 * the operation should be applied to the entry, following the CRDT semantics of this LiveMap.
892990 * @spec RTLM9
893991 */
894- private _canApplyMapOperation ( mapEntrySerial : string | undefined , opSerial : string | undefined ) : boolean {
992+ private _canApplyMapEntryOperation ( mapEntrySerial : string | undefined , opSerial : string | undefined ) : boolean {
895993 // for LWW CRDT semantics (the only supported LiveMap semantic) an operation
896994 // should only be applied if its serial is strictly greater ("after") than an entry's serial.
897995
0 commit comments