@@ -268,3 +268,348 @@ describe('reset()', () => {
268268 expect ( doc2 . clock ) . toBe ( doc2 . api . builder . clock ) ;
269269 } ) ;
270270} ) ;
271+
272+ describe ( 'deep clone state verification' , ( ) => {
273+ describe ( 'string sharing (memory efficiency)' , ( ) => {
274+ test ( 'StrChunk data strings are shared between original and clone' , ( ) => {
275+ const doc1 = Model . create ( ) ;
276+ doc1 . api . set ( { text : 'hello world' } ) ;
277+ doc1 . api . flush ( ) ;
278+ const doc2 = doc1 . clone ( ) ;
279+ const str1 = doc1 . api . str ( [ 'text' ] ) . node ;
280+ const str2 = doc2 . api . str ( [ 'text' ] ) . node ;
281+ expect ( str1 . view ( ) ) . toBe ( 'hello world' ) ;
282+ expect ( str2 . view ( ) ) . toBe ( 'hello world' ) ;
283+ const chunk1 = str1 . first ( ) ! ;
284+ const chunk2 = str2 . first ( ) ! ;
285+ expect ( chunk1 . data ) . toBe ( chunk2 . data ) ;
286+ } ) ;
287+
288+ test ( 'object keys are shared between original and clone' , ( ) => {
289+ const doc1 = Model . create ( ) ;
290+ doc1 . api . set ( { myLongKeyName : 123 , anotherKey : 'test' } ) ;
291+ doc1 . api . flush ( ) ;
292+ const doc2 = doc1 . clone ( ) ;
293+ const obj1 = doc1 . api . obj ( [ ] ) . node ;
294+ const obj2 = doc2 . api . obj ( [ ] ) . node ;
295+ const keys1 = Array . from ( obj1 . keys . keys ( ) ) . sort ( ) ;
296+ const keys2 = Array . from ( obj2 . keys . keys ( ) ) . sort ( ) ;
297+ expect ( keys1 ) . toEqual ( [ 'anotherKey' , 'myLongKeyName' ] ) ;
298+ expect ( keys2 ) . toEqual ( [ 'anotherKey' , 'myLongKeyName' ] ) ;
299+ } ) ;
300+ } ) ;
301+
302+ describe ( 'binary data sharing' , ( ) => {
303+ test ( 'BinChunk Uint8Array data is shared between original and clone' , ( ) => {
304+ const doc1 = Model . create ( ) ;
305+ const data = new Uint8Array ( [ 1 , 2 , 3 , 4 , 5 ] ) ;
306+ doc1 . api . set ( { bin : schema . bin ( data ) } ) ;
307+ doc1 . api . flush ( ) ;
308+ const doc2 = doc1 . clone ( ) ;
309+ const bin1 = doc1 . api . bin ( [ 'bin' ] ) . node ;
310+ const bin2 = doc2 . api . bin ( [ 'bin' ] ) . node ;
311+ expect ( bin1 . view ( ) ) . toEqual ( data ) ;
312+ expect ( bin2 . view ( ) ) . toEqual ( data ) ;
313+
314+ // Verify the underlying Uint8Array data is shared (same reference)
315+ const chunk1 = bin1 . first ( ) ! ;
316+ const chunk2 = bin2 . first ( ) ! ;
317+ expect ( chunk1 . data ) . toBe ( chunk2 . data ) ;
318+ } ) ;
319+ } ) ;
320+
321+ describe ( 'clock state' , ( ) => {
322+ test ( 'clone has same clock time' , ( ) => {
323+ const doc1 = Model . create ( ) ;
324+ doc1 . api . set ( { a : 1 , b : 2 } ) ;
325+ doc1 . api . flush ( ) ;
326+ const doc2 = doc1 . clone ( ) ;
327+ expect ( doc2 . clock . time ) . toBe ( doc1 . clock . time ) ;
328+ expect ( doc2 . clock . sid ) . toBe ( doc1 . clock . sid ) ;
329+ } ) ;
330+
331+ test ( 'fork has same clock time but different session ID' , ( ) => {
332+ const doc1 = Model . create ( ) ;
333+ doc1 . api . set ( { a : 1 , b : 2 } ) ;
334+ doc1 . api . flush ( ) ;
335+ const doc2 = doc1 . fork ( ) ;
336+ expect ( doc2 . clock . time ) . toBe ( doc1 . clock . time ) ;
337+ expect ( doc2 . clock . sid ) . not . toBe ( doc1 . clock . sid ) ;
338+ } ) ;
339+
340+ test ( 'clone preserves peer information in clock vector' , ( ) => {
341+ const doc1 = Model . create ( ) ;
342+ doc1 . api . set ( { a : 1 } ) ;
343+ doc1 . api . flush ( ) ;
344+ // Simulate receiving changes from another peer
345+ const doc2 = doc1 . fork ( ) ;
346+ doc2 . api . obj ( [ ] ) . set ( { b : 2 } ) ;
347+ const patch = doc2 . api . flush ( ) ;
348+ doc1 . applyPatch ( patch ) ;
349+ // Clone should preserve peer info
350+ const doc3 = doc1 . clone ( ) ;
351+ expect ( doc3 . clock . peers . size ) . toBe ( doc1 . clock . peers . size ) ;
352+ doc1 . clock . peers . forEach ( ( peerClock , sid ) => {
353+ const clonedPeerClock = doc3 . clock . peers . get ( sid ) ;
354+ expect ( clonedPeerClock ) . toBeDefined ( ) ;
355+ expect ( clonedPeerClock ! . time ) . toBe ( peerClock . time ) ;
356+ } ) ;
357+ } ) ;
358+ } ) ;
359+
360+ describe ( 'model tick' , ( ) => {
361+ test ( 'clone preserves model tick' , ( ) => {
362+ const doc1 = Model . create ( ) ;
363+ doc1 . api . set ( { a : 1 } ) ;
364+ doc1 . api . flush ( ) ;
365+ const doc2 = doc1 . fork ( ) ;
366+ expect ( doc1 . tick ) . toBe ( doc2 . tick ) ;
367+ doc2 . api . obj ( [ ] ) . set ( { b : 2 } ) ;
368+ doc1 . applyPatch ( doc2 . api . flush ( ) ) ;
369+ expect ( doc1 . tick ) . toBeGreaterThan ( 0 ) ;
370+ const doc3 = doc1 . clone ( ) ;
371+ expect ( doc3 . tick ) . toBe ( doc1 . tick ) ;
372+ } ) ;
373+ } ) ;
374+
375+ describe ( 'index completeness' , ( ) => {
376+ test ( 'clone has all nodes in index' , ( ) => {
377+ const doc1 = Model . create ( ) ;
378+ doc1 . api . set ( {
379+ str : 'hello' ,
380+ num : 42 ,
381+ bool : true ,
382+ nil : null ,
383+ arr : [ 1 , 2 , 3 ] ,
384+ obj : { nested : 'value' } ,
385+ } ) ;
386+ doc1 . api . flush ( ) ;
387+ const doc2 = doc1 . clone ( ) ;
388+ let count1 = 0 ;
389+ let count2 = 0 ;
390+ doc1 . index . forEach ( ( ) => count1 ++ ) ;
391+ doc2 . index . forEach ( ( ) => count2 ++ ) ;
392+ expect ( count2 ) . toBe ( count1 ) ;
393+ } ) ;
394+
395+ test ( 'clone has independent index' , ( ) => {
396+ const doc1 = Model . create ( ) ;
397+ doc1 . api . set ( { a : 1 } ) ;
398+ doc1 . api . flush ( ) ;
399+ const doc2 = doc1 . clone ( ) ;
400+ // Indexes should be different objects
401+ expect ( doc2 . index ) . not . toBe ( doc1 . index ) ;
402+ // Adding to doc1 should not affect doc2
403+ doc1 . api . obj ( [ ] ) . set ( { b : 2 } ) ;
404+ doc1 . api . flush ( ) ;
405+ let count1 = 0 ;
406+ let count2 = 0 ;
407+ doc1 . index . forEach ( ( ) => count1 ++ ) ;
408+ doc2 . index . forEach ( ( ) => count2 ++ ) ;
409+ expect ( count1 ) . toBeGreaterThan ( count2 ) ;
410+ } ) ;
411+ } ) ;
412+
413+ describe ( 'extensions' , ( ) => {
414+ test ( 'clone has cloned extensions' , ( ) => {
415+ const doc1 = Model . create ( ) ;
416+ doc1 . ext . register ( { } as any ) ;
417+ doc1 . api . set ( { a : 1 } ) ;
418+ doc1 . api . flush ( ) ;
419+ const doc2 = doc1 . clone ( ) ;
420+ expect ( doc2 . ext ) . not . toBe ( doc1 . ext ) ;
421+ expect ( doc2 . ext . size ( ) ) . toBe ( doc1 . ext . size ( ) ) ;
422+ } ) ;
423+ } ) ;
424+
425+ describe ( 'API independence' , ( ) => {
426+ test ( 'clone does not have API instance until accessed' , ( ) => {
427+ const doc1 = Model . create ( ) ;
428+ doc1 . api . set ( { a : 1 } ) ;
429+ doc1 . api . flush ( ) ;
430+ // Access api on doc1
431+ expect ( doc1 . api ) . toBeDefined ( ) ;
432+ const doc2 = doc1 . clone ( ) ;
433+ expect ( ( doc2 as any ) . _api ) . toBeUndefined ( ) ;
434+ // doc2 should have its own API when accessed
435+ expect ( doc2 . api ) . toBeDefined ( ) ;
436+ expect ( doc2 . api ) . not . toBe ( doc1 . api ) ;
437+ } ) ;
438+
439+ test ( 'node APIs are not shared between clones' , ( ) => {
440+ const doc1 = Model . create ( ) ;
441+ doc1 . api . set ( { str : 'hello' } ) ;
442+ doc1 . api . flush ( ) ;
443+ // Access node API on doc1
444+ const strApi1 = doc1 . api . str ( [ 'str' ] ) ;
445+ expect ( strApi1 ) . toBeDefined ( ) ;
446+ const doc2 = doc1 . clone ( ) ;
447+ // doc2's node API should be different
448+ const strApi2 = doc2 . api . str ( [ 'str' ] ) ;
449+ expect ( strApi2 ) . not . toBe ( strApi1 ) ;
450+ } ) ;
451+ } ) ;
452+
453+ describe ( 'RGA structure' , ( ) => {
454+ test ( 'StrNode clone preserves RGA structure with splits' , ( ) => {
455+ const doc1 = Model . create ( ) ;
456+ doc1 . api . set ( { text : 'abc' } ) ;
457+ doc1 . api . str ( [ 'text' ] ) . ins ( 3 , 'def' ) ;
458+ doc1 . api . str ( [ 'text' ] ) . del ( 1 , 2 ) ; // Creates tombstones
459+ doc1 . api . flush ( ) ;
460+
461+ const doc2 = doc1 . clone ( ) ;
462+
463+ // Verify views match
464+ expect ( doc2 . view ( ) ) . toEqual ( doc1 . view ( ) ) ;
465+
466+ // Verify chunk counts match
467+ const str1 = doc1 . api . str ( [ 'text' ] ) . node ;
468+ const str2 = doc2 . api . str ( [ 'text' ] ) . node ;
469+ expect ( str2 . count ) . toBe ( str1 . count ) ;
470+ expect ( str2 . length ( ) ) . toBe ( str1 . length ( ) ) ;
471+ } ) ;
472+
473+ test ( 'ArrNode clone preserves RGA structure' , ( ) => {
474+ const doc1 = Model . create ( ) ;
475+ doc1 . api . set ( { arr : [ 1 , 2 , 3 ] } ) ;
476+ doc1 . api . arr ( [ 'arr' ] ) . ins ( 3 , [ 4 , 5 ] ) ;
477+ doc1 . api . arr ( [ 'arr' ] ) . del ( 1 , 2 ) ; // Delete some elements
478+ doc1 . api . flush ( ) ;
479+
480+ const doc2 = doc1 . clone ( ) ;
481+
482+ // Verify views match
483+ expect ( doc2 . view ( ) ) . toEqual ( doc1 . view ( ) ) ;
484+
485+ // Verify chunk counts match
486+ const arr1 = doc1 . api . arr ( [ 'arr' ] ) . node ;
487+ const arr2 = doc2 . api . arr ( [ 'arr' ] ) . node ;
488+ expect ( arr2 . count ) . toBe ( arr1 . count ) ;
489+ expect ( arr2 . length ( ) ) . toBe ( arr1 . length ( ) ) ;
490+ } ) ;
491+
492+ test ( 'BinNode clone preserves RGA structure' , ( ) => {
493+ const doc1 = Model . create ( ) ;
494+ doc1 . api . set ( { bin : schema . bin ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) } ) ;
495+ doc1 . api . bin ( [ 'bin' ] ) . ins ( 3 , new Uint8Array ( [ 4 , 5 ] ) ) ;
496+ doc1 . api . bin ( [ 'bin' ] ) . del ( 1 , 2 ) ;
497+ doc1 . api . flush ( ) ;
498+
499+ const doc2 = doc1 . clone ( ) ;
500+
501+ // Verify views match
502+ const view1 = ( doc1 . view ( ) as any ) . bin ;
503+ const view2 = ( doc2 . view ( ) as any ) . bin ;
504+ expect ( view2 ) . toEqual ( view1 ) ;
505+
506+ // Verify chunk counts match
507+ const bin1 = doc1 . api . bin ( [ 'bin' ] ) . node ;
508+ const bin2 = doc2 . api . bin ( [ 'bin' ] ) . node ;
509+ expect ( bin2 . count ) . toBe ( bin1 . count ) ;
510+ expect ( bin2 . length ( ) ) . toBe ( bin1 . length ( ) ) ;
511+ } ) ;
512+ } ) ;
513+
514+ describe ( 'mutation isolation' , ( ) => {
515+ test ( 'modifying clone does not affect original' , ( ) => {
516+ const doc1 = Model . create ( ) ;
517+ doc1 . api . set ( { text : 'hello' } ) ;
518+ doc1 . api . flush ( ) ;
519+
520+ const doc2 = doc1 . clone ( ) ;
521+ doc2 . api . str ( [ 'text' ] ) . ins ( 5 , ' world' ) ;
522+ doc2 . api . flush ( ) ;
523+
524+ expect ( doc1 . view ( ) ) . toEqual ( { text : 'hello' } ) ;
525+ expect ( doc2 . view ( ) ) . toEqual ( { text : 'hello world' } ) ;
526+ } ) ;
527+
528+ test ( 'modifying original does not affect clone' , ( ) => {
529+ const doc1 = Model . create ( ) ;
530+ doc1 . api . set ( { text : 'hello' } ) ;
531+ doc1 . api . flush ( ) ;
532+
533+ const doc2 = doc1 . clone ( ) ;
534+
535+ doc1 . api . str ( [ 'text' ] ) . ins ( 5 , ' world' ) ;
536+ doc1 . api . flush ( ) ;
537+
538+ expect ( doc1 . view ( ) ) . toEqual ( { text : 'hello world' } ) ;
539+ expect ( doc2 . view ( ) ) . toEqual ( { text : 'hello' } ) ;
540+ } ) ;
541+
542+ test ( 'mutations to object in clone are isolated' , ( ) => {
543+ const doc1 = Model . create ( ) ;
544+ doc1 . api . set ( {
545+ obj : { a : 1 , b : 2 } ,
546+ } ) ;
547+ doc1 . api . flush ( ) ;
548+
549+ const doc2 = doc1 . clone ( ) ;
550+ doc2 . api . obj ( [ 'obj' ] ) . set ( { c : 3 } ) ;
551+ doc2 . api . obj ( [ 'obj' ] ) . del ( [ 'a' ] ) ;
552+ doc2 . api . flush ( ) ;
553+
554+ expect ( doc1 . view ( ) ) . toEqual ( { obj : { a : 1 , b : 2 } } ) ;
555+ expect ( doc2 . view ( ) ) . toEqual ( { obj : { b : 2 , c : 3 } } ) ;
556+ } ) ;
557+
558+ test ( 'mutations to array in clone are isolated' , ( ) => {
559+ const doc1 = Model . create ( ) ;
560+ doc1 . api . set ( { arr : [ 1 , 2 , 3 ] } ) ;
561+ doc1 . api . flush ( ) ;
562+
563+ const doc2 = doc1 . clone ( ) ;
564+ doc2 . api . arr ( [ 'arr' ] ) . ins ( 0 , [ 0 ] ) ;
565+ doc2 . api . arr ( [ 'arr' ] ) . del ( 3 , 1 ) ; // Delete index 3 (which is value 3)
566+ doc2 . api . flush ( ) ;
567+
568+ expect ( doc1 . view ( ) ) . toEqual ( { arr : [ 1 , 2 , 3 ] } ) ;
569+ expect ( doc2 . view ( ) ) . toEqual ( { arr : [ 0 , 1 , 2 ] } ) ;
570+ } ) ;
571+ } ) ;
572+
573+ describe ( 'complex documents' , ( ) => {
574+ test ( 'clone of deeply nested document' , ( ) => {
575+ const doc1 = Model . create ( ) ;
576+ doc1 . api . set ( {
577+ level1 : {
578+ level2 : {
579+ level3 : {
580+ value : 'deep' ,
581+ arr : [ 1 , [ 2 , [ 3 ] ] ] ,
582+ } ,
583+ } ,
584+ } ,
585+ } ) ;
586+ doc1 . api . flush ( ) ;
587+
588+ const doc2 = doc1 . clone ( ) ;
589+
590+ expect ( doc2 . view ( ) ) . toEqual ( doc1 . view ( ) ) ;
591+
592+ // Modify deep value in clone
593+ doc2 . api . obj ( [ 'level1' , 'level2' , 'level3' ] ) . set ( { value : 'modified' } ) ;
594+ doc2 . api . flush ( ) ;
595+
596+ expect ( ( doc1 . view ( ) as any ) . level1 . level2 . level3 . value ) . toBe ( 'deep' ) ;
597+ expect ( ( doc2 . view ( ) as any ) . level1 . level2 . level3 . value ) . toBe ( 'modified' ) ;
598+ } ) ;
599+
600+ test ( 'clone with vectors' , ( ) => {
601+ const doc1 = Model . create ( ) ;
602+ doc1 . api . set ( {
603+ vec : schema . vec ( schema . con ( 1 ) , schema . con ( 2 ) , schema . con ( 3 ) ) ,
604+ } ) ;
605+ doc1 . api . flush ( ) ;
606+
607+ const doc2 = doc1 . clone ( ) ;
608+
609+ const view1 = doc1 . view ( ) as any ;
610+ const view2 = doc2 . view ( ) as any ;
611+
612+ expect ( view2 . vec ) . toEqual ( view1 . vec ) ;
613+ } ) ;
614+ } ) ;
615+ } ) ;
0 commit comments