11import { writable , get } from 'svelte/store'
2+ import { tick } from 'svelte'
23
34import { gradient_type , gradient_space , gradient_interpolation , gradient_stops , gradient_angles } from './gradient'
45import { linear_angle , linear_named_angle } from './linear'
@@ -7,6 +8,13 @@ import { conic_angle, conic_position, conic_named_position } from './conic'
78
89import { buildGradientStrings } from '../utils/gradientString'
910
11+ // Pending mutations for batching rapid store updates via Svelte's tick()
12+ let pendingLayerUpdates : ( ( l : GradientLayer ) => void ) [ ] = [ ]
13+ // Track the target layer index when updates are queued to prevent applying to wrong layer
14+ let pendingLayerIndex : number | null = null
15+ // Track whether a tick-based flush is already scheduled
16+ let flushScheduled = false
17+
1018// Minimal layer shape mirroring the single-store shape
1119export type GradientLayer = {
1220 id : string
@@ -40,7 +48,7 @@ function snapshotFromStores(): GradientLayer {
4048 type : get ( gradient_type ) ,
4149 space : get ( gradient_space ) ,
4250 interpolation : get ( gradient_interpolation ) ,
43- stops : JSON . parse ( JSON . stringify ( get ( gradient_stops ) ) ) ,
51+ stops : structuredClone ( get ( gradient_stops ) ) ,
4452 linear : {
4553 named_angle : get ( linear_named_angle ) ,
4654 angle : get ( linear_angle ) ,
@@ -81,30 +89,78 @@ function applyLayerToStores(layer: GradientLayer) {
8189 conic_named_position . set ( layer . conic . named_position )
8290 conic_position . set ( { ...layer . conic . position } )
8391
84- gradient_stops . set ( JSON . parse ( JSON . stringify ( layer . stops ) ) )
92+ gradient_stops . set ( structuredClone ( layer . stops ) )
8593 }
8694 finally {
8795 // release in next microtask to let subscribers flush
8896 queueMicrotask ( ( ) => { isApplyingLayerToStores = false } )
8997 }
9098}
9199
92- function updateActiveLayer ( mutator : ( l : GradientLayer ) => void ) {
100+ // Flush pending layer updates - called via tick() to batch multiple store changes
101+ async function flushPendingUpdates ( ) {
102+ // Wait for Svelte to finish processing current reactive updates
103+ await tick ( )
104+
105+ // Capture state AFTER tick() resolves - this ensures all synchronous updates
106+ // that triggered before the await are included
107+ const targetIdx = pendingLayerIndex
108+ const updates = pendingLayerUpdates
93109 const list = get ( layers )
94- const idx = get ( active_layer_index ) ?? 0
95- if ( ! list . length || idx < 0 || idx >= list . length ) return
110+
111+ // Reset state to allow new batches to form
112+ pendingLayerUpdates = [ ]
113+ flushScheduled = false
114+ pendingLayerIndex = null
115+
116+ // Verify we have updates and target layer still exists and is valid
117+ if ( ! updates . length || targetIdx === null || ! list . length || targetIdx < 0 || targetIdx >= list . length ) {
118+ return
119+ }
120+
96121 const copy = [ ...list ]
97- const layer = { ...copy [ idx ] ,
98- linear : { ...copy [ idx ] . linear } ,
99- radial : { ...copy [ idx ] . radial , position : { ...copy [ idx ] . radial . position } } ,
100- conic : { ...copy [ idx ] . conic , position : { ...copy [ idx ] . conic . position } } ,
122+ const layer = { ...copy [ targetIdx ] ,
123+ linear : { ...copy [ targetIdx ] . linear } ,
124+ radial : { ...copy [ targetIdx ] . radial , position : { ...copy [ targetIdx ] . radial . position } } ,
125+ conic : { ...copy [ targetIdx ] . conic , position : { ...copy [ targetIdx ] . conic . position } } ,
101126 }
102- mutator ( layer )
127+
128+ // Apply all pending mutations in order
129+ for ( const m of updates ) {
130+ m ( layer )
131+ }
132+
103133 layer . cachedCss = buildGradientStrings ( layer )
104- copy [ idx ] = layer
134+ copy [ targetIdx ] = layer
105135 layers . set ( copy )
106136}
107137
138+ // Batch multiple rapid store updates into a single layer update to reduce renders
139+ function updateActiveLayer ( mutator : ( l : GradientLayer ) => void ) {
140+ const currentIdx = get ( active_layer_index ) ?? 0
141+
142+ // If layer changed since pending updates were queued, discard stale updates
143+ if ( pendingLayerIndex !== null && pendingLayerIndex !== currentIdx ) {
144+ pendingLayerUpdates = [ ]
145+ pendingLayerIndex = null
146+ }
147+
148+ // Track which layer these updates are for
149+ if ( pendingLayerIndex === null ) {
150+ pendingLayerIndex = currentIdx
151+ }
152+
153+ pendingLayerUpdates . push ( mutator )
154+
155+ // Schedule a flush via Svelte's tick() if not already scheduled
156+ // tick() returns a promise that resolves after pending state changes are applied
157+ // All synchronous store subscriptions will add their mutators before tick() resolves
158+ if ( ! flushScheduled ) {
159+ flushScheduled = true
160+ flushPendingUpdates ( )
161+ }
162+ }
163+
108164// Public API
109165export function addLayer ( { seed = 'duplicate' , position = 'top' as 'top' | 'bottom' } = { } ) {
110166 const base = seed === 'duplicate' ? snapshotFromStores ( ) : defaultLayer ( )
@@ -290,4 +346,5 @@ radial_position.subscribe(v => { if (!isApplyingLayerToStores) updateActiveLayer
290346conic_angle . subscribe ( v => { if ( ! isApplyingLayerToStores ) updateActiveLayer ( l => { l . conic . angle = v as any } ) } )
291347conic_named_position . subscribe ( v => { if ( ! isApplyingLayerToStores ) updateActiveLayer ( l => { l . conic . named_position = v } ) } )
292348conic_position . subscribe ( v => { if ( ! isApplyingLayerToStores ) updateActiveLayer ( l => { l . conic . position = { ...( v as any ) } } ) } )
293- gradient_stops . subscribe ( v => { if ( ! isApplyingLayerToStores ) updateActiveLayer ( l => { l . stops = JSON . parse ( JSON . stringify ( v ) ) } ) } )
349+ // Use structuredClone for a faster deep clone than JSON.parse/stringify
350+ gradient_stops . subscribe ( v => { if ( ! isApplyingLayerToStores ) updateActiveLayer ( l => { l . stops = structuredClone ( v ) } ) } )
0 commit comments