66/* eslint-disable prefer-destructuring */
77/* eslint-disable no-param-reassign */
88/* eslint-disable no-lone-blocks */
9+ /* eslint-disable no-extend-native */
910import {
1011 produce ,
1112 enableMapSet ,
@@ -20,7 +21,7 @@ import {
2021 finishDraft ,
2122 immerable ,
2223} from 'immer' ;
23- import { create , apply , current } from '../src' ;
24+ import { create , apply , current , isDraft } from '../src' ;
2425import { deepClone } from '../src/utils' ;
2526
2627enableMapSet ( ) ;
@@ -877,3 +878,187 @@ test('replays patches for a shared draft array', () => {
877878 checkImmerPatches ( state , fn ) ;
878879 checkMutativePatches ( state , fn ) ;
879880} ) ;
881+
882+ // Immer's current() copies a plain Set and then iterates the copy while
883+ // set() appends the current value of every element to that same copy, so a
884+ // Set holding drafts or unfrozen draftable objects grows forever. The guard
885+ // below turns that runaway growth into an error instead of a hang.
886+ function withSetAddLimit < T > ( limit : number , fn : ( ) => T ) : T {
887+ const originalAdd = Set . prototype . add ;
888+ let calls = 0 ;
889+ Set . prototype . add = function add ( this : Set < unknown > , value : unknown ) {
890+ calls += 1 ;
891+ if ( calls > limit ) {
892+ throw new Error ( `Set.prototype.add was called more than ${ limit } times` ) ;
893+ }
894+ return originalAdd . call ( this , value ) ;
895+ } as typeof Set . prototype . add ;
896+ try {
897+ return fn ( ) ;
898+ } finally {
899+ Set . prototype . add = originalAdd ;
900+ }
901+ }
902+
903+ test ( 'current() on a plain Set that holds drafts' , ( ) => {
904+ const makeBase = ( ) : any => ( { a : { child : { x : 1 } } } ) ;
905+ {
906+ // ! it should return a snapshot; instead current() never finishes
907+ expect ( ( ) =>
908+ withSetAddLimit ( 1000 , ( ) =>
909+ produce ( makeBase ( ) , ( draft : any ) => {
910+ draft . a . child . x = 2 ;
911+ draft . a . s = new Set ( [ draft . a . child ] ) ;
912+ immerCurrent ( draft . a ) ;
913+ } )
914+ )
915+ ) . toThrow ( 'Set.prototype.add was called more than 1000 times' ) ;
916+ }
917+ {
918+ let snapshot : any ;
919+ create ( makeBase ( ) , ( draft : any ) => {
920+ draft . a . child . x = 2 ;
921+ draft . a . s = new Set ( [ draft . a . child ] ) ;
922+ snapshot = current ( draft . a ) ;
923+ } ) ;
924+ expect ( snapshot . s . size ) . toBe ( 1 ) ;
925+ const [ item ] = [ ...snapshot . s ] ;
926+ expect ( isDraft ( item ) ) . toBe ( false ) ;
927+ expect ( item ) . toEqual ( { x : 2 } ) ;
928+ }
929+ } ) ;
930+
931+ test ( 'current() on a plain Set of unfrozen objects' , ( ) => {
932+ {
933+ // ! it should return a snapshot; instead current() never finishes
934+ expect ( ( ) =>
935+ withSetAddLimit ( 1000 , ( ) =>
936+ produce ( { a : { t : 0 } } as any , ( draft : any ) => {
937+ draft . a . s = new Set ( [ { q : 1 } ] ) ;
938+ draft . a . t = 1 ;
939+ immerCurrent ( draft . a ) ;
940+ } )
941+ )
942+ ) . toThrow ( 'Set.prototype.add was called more than 1000 times' ) ;
943+ }
944+ {
945+ const inner = new Set ( [ { q : 1 } ] ) ;
946+ let snapshot : any ;
947+ create ( { a : { t : 0 } } as any , ( draft : any ) => {
948+ draft . a . s = inner ;
949+ draft . a . t = 1 ;
950+ snapshot = current ( draft . a ) ;
951+ } ) ;
952+ expect ( snapshot . s ) . toBe ( inner ) ;
953+ expect ( snapshot . s . size ) . toBe ( 1 ) ;
954+ }
955+ } ) ;
956+
957+ const isRevokedProxy = ( value : unknown ) => {
958+ try {
959+ Object . getPrototypeOf ( value ) ;
960+ return false ;
961+ } catch {
962+ return true ;
963+ }
964+ } ;
965+
966+ test ( 'a plain Set that holds a draft is finalized' , ( ) => {
967+ type State = { a : { child : { x : number } ; s ?: Set < unknown > } } ;
968+ const makeBase = ( ) : State => ( { a : { child : { x : 1 } } } ) ;
969+ const recipe = ( draft : State ) => {
970+ draft . a . child . x = 2 ;
971+ draft . a . s = new Set ( [ draft . a . child ] ) ;
972+ } ;
973+ {
974+ enablePatches ( ) ;
975+ // Immer adds the finalized child to the Set but leaves the revoked draft
976+ // proxy in it, and the add patch carries that Set.
977+ // ! the Set should hold only the finalized child and the patches should replay
978+ const base = makeBase ( ) ;
979+ const [ state , patches ] = produceWithPatches ( base , recipe ) ;
980+ expect ( state . a . s ! . size ) . toBe ( 2 ) ;
981+ expect ( Array . from ( state . a . s ! ) . filter ( isRevokedProxy ) ) . toHaveLength ( 1 ) ;
982+ expect ( state . a . s ! . has ( state . a . child ) ) . toBe ( true ) ;
983+ expect ( ( ) => applyPatches ( base , patches ) ) . toThrow ( / r e v o k e d / ) ;
984+ }
985+ {
986+ const base = makeBase ( ) ;
987+ const [ state , patches , inversePatches ] = create ( base , recipe , {
988+ enablePatches : true ,
989+ } ) ;
990+ expect ( state . a . s ! . size ) . toBe ( 1 ) ;
991+ expect ( state . a . s ! . has ( state . a . child ) ) . toBe ( true ) ;
992+ expect ( state . a . child ) . toEqual ( { x : 2 } ) ;
993+ expect ( apply ( base , patches ) ) . toEqual ( state ) ;
994+ expect ( apply ( state , inversePatches ) ) . toEqual ( base ) ;
995+ }
996+ } ) ;
997+
998+ test ( 'assigning a draft from an inner scope to an outer draft' , ( ) => {
999+ const makeBases = ( ) => ( {
1000+ outerBase : { slot : null as any , k : 0 } ,
1001+ innerBase : { item : { v : 1 } } ,
1002+ } ) ;
1003+ {
1004+ // The inner produce revokes its proxies when it returns, so the outer
1005+ // draft keeps a revoked proxy in `slot`, with or without patches.
1006+ // ! it should finalize the inner draft into the outer state
1007+ const plain = makeBases ( ) ;
1008+ expect ( ( ) =>
1009+ produce ( plain . outerBase , ( outer : any ) => {
1010+ produce ( plain . innerBase , ( inner : any ) => {
1011+ inner . item . v = 2 ;
1012+ outer . slot = inner . item ;
1013+ outer . k = 1 ;
1014+ } ) ;
1015+ } )
1016+ ) . toThrow ( / r e v o k e d / ) ;
1017+ enablePatches ( ) ;
1018+ const withPatches = makeBases ( ) ;
1019+ expect ( ( ) =>
1020+ produceWithPatches ( withPatches . outerBase , ( outer : any ) => {
1021+ produceWithPatches ( withPatches . innerBase , ( inner : any ) => {
1022+ inner . item . v = 2 ;
1023+ outer . slot = inner . item ;
1024+ outer . k = 1 ;
1025+ } ) ;
1026+ } )
1027+ ) . toThrow ( / r e v o k e d / ) ;
1028+ }
1029+ {
1030+ const { outerBase, innerBase } = makeBases ( ) ;
1031+ let innerResult : any ;
1032+ const [ outer , outerPatches , outerInversePatches ] = create (
1033+ outerBase ,
1034+ ( o : any ) => {
1035+ innerResult = create (
1036+ innerBase ,
1037+ ( i : any ) => {
1038+ i . item . v = 2 ;
1039+ o . slot = i . item ;
1040+ o . k = 1 ;
1041+ } ,
1042+ { enablePatches : true }
1043+ ) ;
1044+ } ,
1045+ { enablePatches : true }
1046+ ) ;
1047+ const [ inner , innerPatches , innerInversePatches ] = innerResult ;
1048+ expect ( outer ) . toEqual ( { slot : { v : 2 } , k : 1 } ) ;
1049+ expect ( inner ) . toEqual ( { item : { v : 2 } } ) ;
1050+ expect ( isDraft ( outer . slot ) ) . toBe ( false ) ;
1051+ // Each scope records only its own changes.
1052+ expect ( outerPatches . map ( ( patch : any ) => patch . path ) ) . toEqual ( [
1053+ [ 'slot' ] ,
1054+ [ 'k' ] ,
1055+ ] ) ;
1056+ expect ( innerPatches . map ( ( patch : any ) => patch . path ) ) . toEqual ( [
1057+ [ 'item' , 'v' ] ,
1058+ ] ) ;
1059+ expect ( apply ( outerBase , outerPatches ) ) . toEqual ( outer ) ;
1060+ expect ( apply ( outer , outerInversePatches ) ) . toEqual ( outerBase ) ;
1061+ expect ( apply ( innerBase , innerPatches ) ) . toEqual ( inner ) ;
1062+ expect ( apply ( inner , innerInversePatches ) ) . toEqual ( innerBase ) ;
1063+ }
1064+ } ) ;
0 commit comments