@@ -1357,6 +1357,75 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f
13571357 } ,
13581358 } ,
13591359
1360+ {
1361+ description : 'only one MAP_CREATE operation is applied for the same object ID' ,
1362+ action : async ( ctx ) => {
1363+ const { entryInstance, objectsHelper, channel } = ctx ;
1364+
1365+ // It's possible for multiple MAP_CREATE operations, with different serials, to be received
1366+ // for the same object ID. The object ID is derived from the operation's content, so they will
1367+ // have identical content. The client should only merge one of these operations into the object's data.
1368+
1369+ // create new map and set on root
1370+ const mapId = objectsHelper . fakeMapObjectId ( ) ;
1371+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1372+ channel,
1373+ serial : lexicoTimeserial ( 'aaa' , 0 , 0 ) ,
1374+ siteCode : 'aaa' ,
1375+ state : [
1376+ objectsHelper . mapCreateOp ( {
1377+ objectId : mapId ,
1378+ entries : {
1379+ foo : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'bar' } } ,
1380+ } ,
1381+ } ) ,
1382+ ] ,
1383+ } ) ;
1384+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1385+ channel,
1386+ serial : lexicoTimeserial ( 'aaa' , 1 , 0 ) ,
1387+ siteCode : 'aaa' ,
1388+ state : [ objectsHelper . mapSetOp ( { objectId : 'root' , key : mapId , data : { objectId : mapId } } ) ] ,
1389+ } ) ;
1390+
1391+ expect ( entryInstance . get ( mapId ) . size ( ) ) . to . equal ( 1 , 'Check map has 1 key after first MAP_CREATE' ) ;
1392+ expect ( entryInstance . get ( mapId ) . get ( 'foo' ) . value ( ) ) . to . equal (
1393+ 'bar' ,
1394+ 'Check map has correct "foo" value after first MAP_CREATE' ,
1395+ ) ;
1396+
1397+ // send another MAP_CREATE op for the same object ID, from a different site, with a later entry timeserial
1398+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1399+ channel,
1400+ serial : lexicoTimeserial ( 'ccc' , 0 , 0 ) ,
1401+ siteCode : 'ccc' ,
1402+ state : [
1403+ objectsHelper . mapCreateOp ( {
1404+ objectId : mapId ,
1405+ entries : {
1406+ foo : { timeserial : lexicoTimeserial ( 'ccc' , 0 , 0 ) , data : { string : 'bar' } } ,
1407+ } ,
1408+ } ) ,
1409+ ] ,
1410+ } ) ;
1411+
1412+ // verify the second CREATE was not applied by checking that a MAP_SET with an intermediate
1413+ // timeserial ('bbb') can still be applied. if the second CREATE had been wrongly applied,
1414+ // the entry's timeserial would be 'ccc' and this MAP_SET would be rejected.
1415+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1416+ channel,
1417+ serial : lexicoTimeserial ( 'bbb' , 0 , 0 ) ,
1418+ siteCode : 'bbb' ,
1419+ state : [ objectsHelper . mapSetOp ( { objectId : mapId , key : 'foo' , data : { string : 'updated' } } ) ] ,
1420+ } ) ;
1421+
1422+ expect ( entryInstance . get ( mapId ) . get ( 'foo' ) . value ( ) ) . to . equal (
1423+ 'updated' ,
1424+ 'Check MAP_SET was applied, proving second MAP_CREATE did not update entry timeserial' ,
1425+ ) ;
1426+ } ,
1427+ } ,
1428+
13601429 {
13611430 allTransportsAndProtocols : true ,
13621431 description : 'can apply MAP_SET with primitives object operation messages' ,
@@ -1849,6 +1918,52 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f
18491918 } ,
18501919 } ,
18511920
1921+ {
1922+ description : 'only one COUNTER_CREATE operation is applied for the same object ID' ,
1923+ action : async ( ctx ) => {
1924+ const { entryInstance, objectsHelper, channel } = ctx ;
1925+
1926+ // It's possible for multiple COUNTER_CREATE operations, with different serials, to be received
1927+ // for the same object ID. The object ID is derived from the operation's content, so they will
1928+ // have identical content. The client should only merge one of these operations into the object's data.
1929+
1930+ // create new counter and set on root
1931+ const counterId = objectsHelper . fakeCounterObjectId ( ) ;
1932+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1933+ channel,
1934+ serial : lexicoTimeserial ( 'aaa' , 0 , 0 ) ,
1935+ siteCode : 'aaa' ,
1936+ state : [ objectsHelper . counterCreateOp ( { objectId : counterId , count : 10 } ) ] ,
1937+ } ) ;
1938+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1939+ channel,
1940+ serial : lexicoTimeserial ( 'aaa' , 1 , 0 ) ,
1941+ siteCode : 'aaa' ,
1942+ state : [ objectsHelper . mapSetOp ( { objectId : 'root' , key : counterId , data : { objectId : counterId } } ) ] ,
1943+ } ) ;
1944+
1945+ expect ( entryInstance . get ( counterId ) . value ( ) ) . to . equal (
1946+ 10 ,
1947+ 'Check counter has value 10 after first COUNTER_CREATE' ,
1948+ ) ;
1949+
1950+ // send another COUNTER_CREATE op for the same object ID, from a different site
1951+ await objectsHelper . processObjectOperationMessageOnChannel ( {
1952+ channel,
1953+ serial : lexicoTimeserial ( 'bbb' , 0 , 0 ) ,
1954+ siteCode : 'bbb' ,
1955+ state : [ objectsHelper . counterCreateOp ( { objectId : counterId , count : 10 } ) ] ,
1956+ } ) ;
1957+
1958+ // verify the second CREATE was not applied - if it had been, the count would have been
1959+ // added again (10 + 10 = 20) due to how COUNTER_CREATE merges into existing state
1960+ expect ( entryInstance . get ( counterId ) . value ( ) ) . to . equal (
1961+ 10 ,
1962+ 'Check counter still has value 10 after second COUNTER_CREATE (not applied)' ,
1963+ ) ;
1964+ } ,
1965+ } ,
1966+
18521967 {
18531968 allTransportsAndProtocols : true ,
18541969 description : 'can apply COUNTER_INC object operation messages' ,
0 commit comments