@@ -367,3 +367,118 @@ function runBaseTest(name, autoFreeze, useListener) {
367367 } )
368368 } )
369369}
370+
371+ // Bug reproduction: DraftSet leakage when placed in new object and set into Map
372+ // This bug occurs when a plain object containing a draft Set is set into a DraftMap
373+ describe ( "RTK-5159: DraftSet leakage in Map" , ( ) => {
374+ const { produce} = new Immer ( { autoFreeze : true } )
375+
376+ test ( "DraftSet should not leak when placed in new object and set into Map" , ( ) => {
377+ const baseState = {
378+ map : new Map ( [ [ "key1" , { users : new Set ( ) } ] ] )
379+ }
380+
381+ // First produce: add a user
382+ const state1 = produce ( baseState , draft => {
383+ const entry = draft . map . get ( "key1" )
384+ entry . users . add ( { name : "user1" } )
385+ } )
386+
387+ // Second produce: create new object with existing Set and set into Map
388+ const state2 = produce ( state1 , draft => {
389+ const existingUsers = draft . map . get ( "key1" ) ?. users ?? new Set ( )
390+ const newEntry = { users : existingUsers } // New plain object with existing draft Set
391+ draft . map . set ( "key1" , newEntry )
392+ newEntry . users . add ( { name : "user2" } )
393+ } )
394+
395+ // Should not throw "Cannot use a proxy that has been revoked"
396+ const users = state2 . map . get ( "key1" ) . users
397+
398+ expect ( users ) . toBeInstanceOf ( Set )
399+ expect ( [ ...users ] . map ( u => u . name ) ) . toEqual ( [ "user1" , "user2" ] )
400+ } )
401+
402+ test ( "DraftSet in new object set into Map should finalize correctly" , ( ) => {
403+ const baseState = {
404+ map : new Map ( [ [ "key1" , { items : new Set ( [ 1 , 2 , 3 ] ) } ] ] )
405+ }
406+
407+ const result = produce ( baseState , draft => {
408+ const existingItems = draft . map . get ( "key1" ) . items
409+ // Create a new plain object containing the draft Set
410+ const newEntry = { items : existingItems , extra : "data" }
411+ draft . map . set ( "key1" , newEntry )
412+ newEntry . items . add ( 4 )
413+ } )
414+
415+ // Verify the result is properly finalized (not a draft/proxy)
416+ const items = result . map . get ( "key1" ) . items
417+ expect ( items ) . toBeInstanceOf ( Set )
418+ expect ( [ ...items ] ) . toEqual ( [ 1 , 2 , 3 , 4 ] )
419+ expect ( result . map . get ( "key1" ) . extra ) . toBe ( "data" )
420+ } )
421+
422+ // Test for DraftSet.add() with non-draft object containing nested draft
423+ // This covers the handleCrossReference() call in DraftSet.add()
424+ test ( "DraftSet.add() should handle non-draft object containing nested draft" , ( ) => {
425+ const baseState = {
426+ items : [ { id : 1 , name : "item1" } ] ,
427+ itemSet : new Set ( )
428+ }
429+
430+ const result = produce ( baseState , draft => {
431+ // Get a draft of an existing item
432+ const draftItem = draft . items [ 0 ]
433+ // Create a new plain object that contains the draft
434+ const wrapper = { item : draftItem , extra : "wrapper data" }
435+ // Add the non-draft wrapper (containing a draft) to the Set
436+ draft . itemSet . add ( wrapper )
437+ // Modify the draft item after adding
438+ draftItem . name = "modified"
439+ } )
440+
441+ // The wrapper should be finalized correctly
442+ const addedItems = [ ...result . itemSet ]
443+ expect ( addedItems ) . toHaveLength ( 1 )
444+ expect ( addedItems [ 0 ] . item . id ) . toBe ( 1 )
445+ expect ( addedItems [ 0 ] . item . name ) . toBe ( "modified" )
446+ expect ( addedItems [ 0 ] . extra ) . toBe ( "wrapper data" )
447+ // Verify it's not a proxy
448+ expect ( ( ) => JSON . stringify ( result ) ) . not . toThrow ( )
449+ } )
450+
451+ test ( "DraftSet.add() should handle deeply nested drafts in plain objects" , ( ) => {
452+ const baseState = {
453+ nested : {
454+ deep : {
455+ value : "original"
456+ }
457+ } ,
458+ mySet : new Set ( )
459+ }
460+
461+ const result = produce ( baseState , draft => {
462+ // Get a draft of a deeply nested object
463+ const deepDraft = draft . nested . deep
464+ // Create a plain object hierarchy containing the draft
465+ const wrapper = {
466+ level1 : {
467+ level2 : {
468+ draftRef : deepDraft
469+ }
470+ }
471+ }
472+ // Add to Set
473+ draft . mySet . add ( wrapper )
474+ // Modify the draft
475+ deepDraft . value = "modified"
476+ } )
477+
478+ const items = [ ...result . mySet ]
479+ expect ( items ) . toHaveLength ( 1 )
480+ expect ( items [ 0 ] . level1 . level2 . draftRef . value ) . toBe ( "modified" )
481+ // Should not throw when accessing
482+ expect ( ( ) => JSON . stringify ( result ) ) . not . toThrow ( )
483+ } )
484+ } )
0 commit comments