@@ -3081,4 +3081,322 @@ describe('signals', () => {
30813081 destroy3 ( ) ;
30823082 } ;
30833083 } ) ;
3084+
3085+ test ( 'derived property on object depending on SvelteSet - immediate read after update' , ( ) => {
3086+ // Reproduction from issue #17263:
3087+ // An item has an `expanded` derived property that depends on expanded_ids.has(id)
3088+ // Reading item.expanded immediately after modifying expanded_ids should reflect the change
3089+ const log : any [ ] = [ ] ;
3090+
3091+ return ( ) => {
3092+ const expanded_ids = new SvelteSet < number > ( ) ;
3093+
3094+ // Create an "item" with a derived expanded property
3095+ function create_item ( id : number ) {
3096+ return {
3097+ id,
3098+ get expanded ( ) {
3099+ return expanded_ids . has ( id ) ;
3100+ }
3101+ } ;
3102+ }
3103+
3104+ const item = create_item ( 1 ) ;
3105+
3106+ // Expand function
3107+ function on_expand ( id : number ) {
3108+ expanded_ids . add ( id ) ;
3109+ }
3110+
3111+ // Collapse function
3112+ function on_collapse ( id : number ) {
3113+ expanded_ids . delete ( id ) ;
3114+ }
3115+
3116+ // Toggle function - this is where the bug manifests
3117+ function toggle_expansion ( ) {
3118+ if ( item . expanded ) {
3119+ on_collapse ( item . id ) ;
3120+ } else {
3121+ on_expand ( item . id ) ;
3122+ }
3123+ // Reading immediately after modification - this is the bug trigger
3124+ log . push ( item . expanded ) ;
3125+ }
3126+
3127+ // Create an effect that reads item.expanded
3128+ let destroy = effect_root ( ( ) => {
3129+ render_effect ( ( ) => {
3130+ // Just to establish reactivity
3131+ item . expanded ;
3132+ } ) ;
3133+ } ) ;
3134+
3135+ flushSync ( ) ;
3136+
3137+ // Initially not expanded
3138+ assert . equal ( item . expanded , false , 'initially not expanded' ) ;
3139+
3140+ // Toggle to expand
3141+ toggle_expansion ( ) ;
3142+ // Should now be true
3143+ assert . deepEqual ( log , [ true ] , 'should be expanded after toggle' ) ;
3144+
3145+ // Toggle to collapse
3146+ toggle_expansion ( ) ;
3147+ // Should now be false
3148+ assert . deepEqual ( log , [ true , false ] , 'should be collapsed after second toggle' ) ;
3149+
3150+ destroy ( ) ;
3151+ } ;
3152+ } ) ;
3153+
3154+ test ( 'derived depending on SvelteSet - item object with derived getter' , ( ) => {
3155+ // More direct reproduction of the issue pattern
3156+ const log : any [ ] = [ ] ;
3157+
3158+ return ( ) => {
3159+ const expanded_ids = new SvelteSet < number > ( ) ;
3160+
3161+ // Simulate the item structure from the reproduction
3162+ class Item {
3163+ id : number ;
3164+ constructor ( id : number ) {
3165+ this . id = id ;
3166+ }
3167+ get expanded ( ) {
3168+ return expanded_ids . has ( this . id ) ;
3169+ }
3170+ }
3171+
3172+ const item = new Item ( 1 ) ;
3173+
3174+ let destroy = effect_root ( ( ) => {
3175+ render_effect ( ( ) => {
3176+ log . push ( `effect: ${ item . expanded } ` ) ;
3177+ } ) ;
3178+ } ) ;
3179+
3180+ flushSync ( ) ;
3181+ assert . deepEqual ( log , [ 'effect: false' ] ) ;
3182+
3183+ // Add to set
3184+ expanded_ids . add ( 1 ) ;
3185+ // Immediately read the derived
3186+ const valueAfterAdd = item . expanded ;
3187+ log . push ( `immediate: ${ valueAfterAdd } ` ) ;
3188+
3189+ flushSync ( ) ;
3190+
3191+ // The immediate read should have returned true
3192+ assert . equal ( valueAfterAdd , true , 'immediate read after add should be true' ) ;
3193+
3194+ // Effect should have run
3195+ assert . ok ( log . includes ( 'effect: true' ) , 'effect should have seen true' ) ;
3196+
3197+ // Delete from set
3198+ expanded_ids . delete ( 1 ) ;
3199+ const valueAfterDelete = item . expanded ;
3200+ log . push ( `immediate: ${ valueAfterDelete } ` ) ;
3201+
3202+ flushSync ( ) ;
3203+
3204+ assert . equal ( valueAfterDelete , false , 'immediate read after delete should be false' ) ;
3205+
3206+ destroy ( ) ;
3207+ } ;
3208+ } ) ;
3209+
3210+ test ( 'derived on SvelteSet with object property - toggle pattern' , ( ) => {
3211+ // Exact pattern from the buggy reproduction
3212+ return ( ) => {
3213+ const expanded_ids = new SvelteSet < number > ( ) ;
3214+
3215+ const items = [
3216+ {
3217+ id : 1 ,
3218+ get expanded ( ) {
3219+ return expanded_ids . has ( this . id ) ;
3220+ }
3221+ } ,
3222+ {
3223+ id : 2 ,
3224+ get expanded ( ) {
3225+ return expanded_ids . has ( this . id ) ;
3226+ }
3227+ }
3228+ ] ;
3229+
3230+ function on_expand ( id : number ) {
3231+ expanded_ids . add ( id ) ;
3232+ }
3233+
3234+ function on_collapse ( id : number ) {
3235+ expanded_ids . delete ( id ) ;
3236+ }
3237+
3238+ // The buggy toggle function that reads item.expanded after modification
3239+ function buggy_toggle ( item : ( typeof items ) [ 0 ] ) {
3240+ if ( item . expanded ) {
3241+ on_collapse ( item . id ) ;
3242+ } else {
3243+ on_expand ( item . id ) ;
3244+ }
3245+ // This is the problematic pattern - reading immediately after modification
3246+ return item . expanded ;
3247+ }
3248+
3249+ let destroy = effect_root ( ( ) => {
3250+ render_effect ( ( ) => {
3251+ // Establish reactivity
3252+ items . forEach ( ( i ) => i . expanded ) ;
3253+ } ) ;
3254+ } ) ;
3255+
3256+ flushSync ( ) ;
3257+
3258+ // Toggle item 1 (expand)
3259+ const result1 = buggy_toggle ( items [ 0 ] ) ;
3260+ assert . equal ( result1 , true , 'after toggle expand, should read true' ) ;
3261+ assert . equal ( expanded_ids . has ( 1 ) , true , 'set should contain id 1' ) ;
3262+
3263+ // Toggle item 1 again (collapse)
3264+ const result2 = buggy_toggle ( items [ 0 ] ) ;
3265+ assert . equal ( result2 , false , 'after toggle collapse, should read false' ) ;
3266+ assert . equal ( expanded_ids . has ( 1 ) , false , 'set should not contain id 1' ) ;
3267+
3268+ // Toggle item 2 (expand)
3269+ const result3 = buggy_toggle ( items [ 1 ] ) ;
3270+ assert . equal ( result3 , true , 'after toggle expand item 2, should read true' ) ;
3271+
3272+ destroy ( ) ;
3273+ } ;
3274+ } ) ;
3275+
3276+ test ( 'derived passed as prop - item with expanded derived from SvelteSet' , ( ) => {
3277+ // Simulates the parent-child component pattern from issue #17263
3278+ // Parent creates item with expanded derived, passes to child
3279+ // Child reads item.expanded after modifying the set
3280+ return ( ) => {
3281+ const expanded_ids = new SvelteSet < number > ( ) ;
3282+
3283+ // Parent creates the item (like in App.svelte)
3284+ function create_item ( id : number ) {
3285+ const d = derived ( ( ) => expanded_ids . has ( id ) ) ;
3286+ return {
3287+ id,
3288+ get expanded ( ) {
3289+ return $ . get ( d ) ;
3290+ }
3291+ } ;
3292+ }
3293+
3294+ const item = create_item ( 1 ) ;
3295+
3296+ // Parent's expand/collapse functions
3297+ function on_expand ( id : number ) {
3298+ expanded_ids . add ( id ) ;
3299+ }
3300+ function on_collapse ( id : number ) {
3301+ expanded_ids . delete ( id ) ;
3302+ }
3303+
3304+ // Simulate child component receiving item as prop
3305+ let childDestroy : ( ) => void ;
3306+
3307+ // Parent effect (creates the item and passes to child)
3308+ let parentDestroy = effect_root ( ( ) => {
3309+ render_effect ( ( ) => {
3310+ // Parent might read item.expanded in template
3311+ item . expanded ;
3312+ } ) ;
3313+ } ) ;
3314+
3315+ flushSync ( ) ;
3316+
3317+ // Child effect - simulates child component
3318+ childDestroy = effect_root ( ( ) => {
3319+ render_effect ( ( ) => {
3320+ // Child reads item.expanded
3321+ item . expanded ;
3322+ } ) ;
3323+ } ) ;
3324+
3325+ flushSync ( ) ;
3326+
3327+ // Child's buggy toggle function
3328+ function buggy_on_toggle_expansion ( ) {
3329+ if ( item . expanded ) {
3330+ on_collapse ( item . id ) ;
3331+ } else {
3332+ on_expand ( item . id ) ;
3333+ }
3334+ // This triggers the bug - reading immediately after modification
3335+ return item . expanded ;
3336+ }
3337+
3338+ // Test the toggle
3339+ const result1 = buggy_on_toggle_expansion ( ) ;
3340+ assert . equal ( result1 , true , 'after expand, should read true' ) ;
3341+
3342+ const result2 = buggy_on_toggle_expansion ( ) ;
3343+ assert . equal ( result2 , false , 'after collapse, should read false' ) ;
3344+
3345+ const result3 = buggy_on_toggle_expansion ( ) ;
3346+ assert . equal ( result3 , true , 'after expand again, should read true' ) ;
3347+
3348+ childDestroy ( ) ;
3349+ parentDestroy ( ) ;
3350+ } ;
3351+ } ) ;
3352+
3353+ test ( 'derived with SvelteSet - disconnected then reconnected' , ( ) => {
3354+ // Test the scenario where the derived might become disconnected
3355+ return ( ) => {
3356+ const expanded_ids = new SvelteSet < number > ( ) ;
3357+
3358+ const d = derived ( ( ) => expanded_ids . has ( 1 ) ) ;
3359+
3360+ // First effect - connects the derived
3361+ let destroy1 = effect_root ( ( ) => {
3362+ render_effect ( ( ) => {
3363+ $ . get ( d ) ;
3364+ } ) ;
3365+ } ) ;
3366+
3367+ flushSync ( ) ;
3368+ assert . equal ( $ . get ( d ) , false ) ;
3369+
3370+ // Destroy the effect - disconnects the derived
3371+ destroy1 ( ) ;
3372+ flushSync ( ) ;
3373+
3374+ // Modify the set while derived is disconnected
3375+ expanded_ids . add ( 1 ) ;
3376+
3377+ // Create new effect - reconnects the derived
3378+ let destroy2 = effect_root ( ( ) => {
3379+ render_effect ( ( ) => {
3380+ $ . get ( d ) ;
3381+ } ) ;
3382+ } ) ;
3383+
3384+ flushSync ( ) ;
3385+
3386+ // The derived should reflect the updated value
3387+ assert . equal ( $ . get ( d ) , true , 'derived should see updated value after reconnection' ) ;
3388+
3389+ // Modify again
3390+ expanded_ids . delete ( 1 ) ;
3391+ const immediateValue = $ . get ( d ) ;
3392+ assert . equal ( immediateValue , false , 'immediate read should see deleted' ) ;
3393+
3394+ // Add back
3395+ expanded_ids . add ( 1 ) ;
3396+ const immediateValue2 = $ . get ( d ) ;
3397+ assert . equal ( immediateValue2 , true , 'immediate read should see added' ) ;
3398+
3399+ destroy2 ( ) ;
3400+ } ;
3401+ } ) ;
30843402} ) ;
0 commit comments