@@ -804,6 +804,52 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f
804804 } ,
805805 } ,
806806
807+ {
808+ description : 'OBJECT_SYNC does not break when receiving an unknown object type' ,
809+ action : async ( ctx ) => {
810+ const { channel, objectsHelper } = ctx ;
811+
812+ // first message: unknown object type (no counter or map field set)
813+ await objectsHelper . processObjectStateMessageOnChannel ( {
814+ channel,
815+ syncSerial : 'serial:cursor' ,
816+ state : [
817+ {
818+ object : {
819+ objectId : 'unknown:object123' ,
820+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
821+ tombstone : false ,
822+ // intentionally not setting counter or map fields
823+ } ,
824+ } ,
825+ ] ,
826+ } ) ;
827+
828+ // second message: root with a key, ends sync sequence
829+ await objectsHelper . processObjectStateMessageOnChannel ( {
830+ channel,
831+ syncSerial : 'serial:' ,
832+ state : [
833+ objectsHelper . mapObject ( {
834+ objectId : 'root' ,
835+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
836+ initialEntries : {
837+ foo : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'bar' } } ,
838+ } ,
839+ } ) ,
840+ ] ,
841+ } ) ;
842+
843+ const root = await channel . object . get ( ) ;
844+
845+ // verify root has the expected key - SDK should not break due to unknown object type
846+ expect ( root . get ( 'foo' ) . value ( ) ) . to . equal (
847+ 'bar' ,
848+ 'Check root has correct value after unknown object type in sync' ,
849+ ) ;
850+ } ,
851+ } ,
852+
807853 {
808854 description : 'OBJECT_SYNC sequence with "tombstone=true" for an object creates tombstoned object' ,
809855 action : async ( ctx ) => {
@@ -1132,6 +1178,161 @@ define(['ably', 'shared_helper', 'chai', 'liveobjects', 'liveobjects_helper'], f
11321178 ) . to . be . true ;
11331179 } ,
11341180 } ,
1181+
1182+ {
1183+ description : 'partial OBJECT_SYNC builds object tree across multiple messages' ,
1184+ action : async ( ctx ) => {
1185+ const { channel, objectsHelper, entryPathObject } = ctx ;
1186+
1187+ const counterId = objectsHelper . fakeCounterObjectId ( ) ;
1188+ const mapId = objectsHelper . fakeMapObjectId ( ) ;
1189+
1190+ // send three separate OBJECT_SYNC messages: one for root, one for counter, one for map
1191+ await objectsHelper . processObjectStateMessageOnChannel ( {
1192+ channel,
1193+ syncSerial : 'serial:cursor1' ,
1194+ state : [
1195+ objectsHelper . mapObject ( {
1196+ objectId : 'root' ,
1197+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1198+ initialEntries : {
1199+ stringKey : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'hello' } } ,
1200+ counter : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { objectId : counterId } } ,
1201+ map : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { objectId : mapId } } ,
1202+ } ,
1203+ } ) ,
1204+ ] ,
1205+ } ) ;
1206+
1207+ await objectsHelper . processObjectStateMessageOnChannel ( {
1208+ channel,
1209+ syncSerial : 'serial:cursor2' ,
1210+ state : [
1211+ objectsHelper . counterObject ( {
1212+ objectId : counterId ,
1213+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1214+ initialCount : 10 ,
1215+ materialisedCount : 5 ,
1216+ } ) ,
1217+ ] ,
1218+ } ) ;
1219+
1220+ await objectsHelper . processObjectStateMessageOnChannel ( {
1221+ channel,
1222+ syncSerial : 'serial:' , // end sync sequence
1223+ state : [
1224+ objectsHelper . mapObject ( {
1225+ objectId : mapId ,
1226+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1227+ initialEntries : {
1228+ foo : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'bar' } } ,
1229+ } ,
1230+ materialisedEntries : {
1231+ baz : { timeserial : lexicoTimeserial ( 'bbb' , 0 , 0 ) , data : { string : 'qux' } } ,
1232+ } ,
1233+ } ) ,
1234+ ] ,
1235+ } ) ;
1236+
1237+ expect ( entryPathObject . get ( 'stringKey' ) . value ( ) ) . to . equal ( 'hello' , 'Check root has correct string value' ) ;
1238+ expect ( entryPathObject . get ( 'counter' ) . value ( ) ) . to . equal ( 15 , 'Check counter has correct aggregated value' ) ;
1239+ expect ( entryPathObject . get ( 'map' ) . get ( 'foo' ) . value ( ) ) . to . equal ( 'bar' , 'Check map has initial entries' ) ;
1240+ expect ( entryPathObject . get ( 'map' ) . get ( 'baz' ) . value ( ) ) . to . equal ( 'qux' , 'Check map has materialised entries' ) ;
1241+ } ,
1242+ } ,
1243+
1244+ {
1245+ description : 'partial OBJECT_SYNC merges map entries across multiple messages for the same objectId' ,
1246+ action : async ( ctx ) => {
1247+ const { channel, objectsHelper, entryPathObject } = ctx ;
1248+
1249+ const mapId = objectsHelper . fakeMapObjectId ( ) ;
1250+
1251+ await objectsHelper . processObjectStateMessageOnChannel ( {
1252+ channel,
1253+ syncSerial : 'serial:cursor1' ,
1254+ state : [
1255+ objectsHelper . mapObject ( {
1256+ objectId : 'root' ,
1257+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1258+ initialEntries : {
1259+ map : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { objectId : mapId } } ,
1260+ } ,
1261+ } ) ,
1262+ ] ,
1263+ } ) ;
1264+
1265+ await objectsHelper . processObjectStateMessageOnChannel ( {
1266+ channel,
1267+ syncSerial : 'serial:cursor2' ,
1268+ state : [
1269+ objectsHelper . mapObject ( {
1270+ objectId : mapId ,
1271+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1272+ // initialEntries are the same across all partial messages
1273+ initialEntries : {
1274+ initialKey : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'initial' } } ,
1275+ } ,
1276+ // materialisedEntries are merged across partial messages
1277+ materialisedEntries : {
1278+ key1 : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { number : 1 } } ,
1279+ key2 : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'two' } } ,
1280+ } ,
1281+ } ) ,
1282+ ] ,
1283+ } ) ;
1284+
1285+ await objectsHelper . processObjectStateMessageOnChannel ( {
1286+ channel,
1287+ syncSerial : 'serial:cursor3' ,
1288+ state : [
1289+ objectsHelper . mapObject ( {
1290+ objectId : mapId ,
1291+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1292+ initialEntries : {
1293+ initialKey : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'initial' } } ,
1294+ } ,
1295+ materialisedEntries : {
1296+ key3 : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { number : 3 } } ,
1297+ key4 : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { boolean : true } } ,
1298+ } ,
1299+ } ) ,
1300+ ] ,
1301+ } ) ;
1302+
1303+ await objectsHelper . processObjectStateMessageOnChannel ( {
1304+ channel,
1305+ syncSerial : 'serial:' , // end sync sequence
1306+ state : [
1307+ objectsHelper . mapObject ( {
1308+ objectId : mapId ,
1309+ siteTimeserials : { aaa : lexicoTimeserial ( 'aaa' , 0 , 0 ) } ,
1310+ initialEntries : {
1311+ initialKey : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'initial' } } ,
1312+ } ,
1313+ materialisedEntries : {
1314+ key5 : { timeserial : lexicoTimeserial ( 'aaa' , 0 , 0 ) , data : { string : 'five' } } ,
1315+ } ,
1316+ } ) ,
1317+ ] ,
1318+ } ) ;
1319+
1320+ const map = entryPathObject . get ( 'map' ) ;
1321+
1322+ // verify initial entries are applied
1323+ expect ( map . get ( 'initialKey' ) . value ( ) ) . to . equal (
1324+ 'initial' ,
1325+ 'Check keys from the create operations are present' ,
1326+ ) ;
1327+
1328+ // verify all materialised entries were merged
1329+ expect ( map . get ( 'key1' ) . value ( ) ) . to . equal ( 1 , 'Check key1 from first partial sync' ) ;
1330+ expect ( map . get ( 'key2' ) . value ( ) ) . to . equal ( 'two' , 'Check key2 from first partial sync' ) ;
1331+ expect ( map . get ( 'key3' ) . value ( ) ) . to . equal ( 3 , 'Check key3 from second partial sync' ) ;
1332+ expect ( map . get ( 'key4' ) . value ( ) ) . to . equal ( true , 'Check key4 from second partial sync' ) ;
1333+ expect ( map . get ( 'key5' ) . value ( ) ) . to . equal ( 'five' , 'Check key5 from third partial sync' ) ;
1334+ } ,
1335+ } ,
11351336 ] ;
11361337
11371338 const applyOperationsScenarios = [
0 commit comments