@@ -18,6 +18,7 @@ import {
1818 createProxy ,
1919 ArchType ,
2020 ImmerScope ,
21+ isArrayIndex ,
2122 handleCrossReference
2223} from "../internal"
2324
@@ -38,6 +39,7 @@ export interface ProxyArrayState extends ProxyBaseState {
3839 base_ : AnyArray
3940 copy_ : AnyArray | null
4041 draft_ : Drafted < AnyArray , ProxyArrayState >
42+ operationMethod ?: ArrayOperationMethod
4143}
4244
4345type ProxyState = ProxyObjectState | ProxyArrayState
@@ -103,6 +105,16 @@ export const objectTraps: ProxyHandler<ProxyState> = {
103105 get ( state , prop ) {
104106 if ( prop === DRAFT_STATE ) return state
105107
108+ // Intercept array methods so that we can override
109+ // behavior and skip proxy creation for perf
110+ if (
111+ state . type_ === ArchType . Array &&
112+ typeof prop === "string" &&
113+ isArrayOperationMethod ( prop )
114+ ) {
115+ return createMethodInterceptor ( state , prop )
116+ }
117+
106118 const source = latest ( state )
107119 if ( ! has ( source , prop , state . type_ ) ) {
108120 // non-existing or non-own property...
@@ -112,6 +124,19 @@ export const objectTraps: ProxyHandler<ProxyState> = {
112124 if ( state . finalized_ || ! isDraftable ( value ) ) {
113125 return value
114126 }
127+
128+ // During mutating array operations, defer proxy creation for array elements
129+ // This optimization avoids creating unnecessary proxies during sort/reverse
130+ if (
131+ state . type_ === ArchType . Array &&
132+ ( state as ProxyArrayState ) . operationMethod &&
133+ isMutatingArrayMethod ( ( state as ProxyArrayState ) . operationMethod ! ) &&
134+ isArrayIndex ( prop )
135+ ) {
136+ // Return raw value during mutating operations, create proxy only if modified
137+ return value
138+ }
139+
115140 // Check for existing draft in modified state.
116141 // Assigned values are never drafted. This catches any drafts we created, too.
117142 if ( value === peek ( state . base_ , prop ) ) {
@@ -245,6 +270,265 @@ arrayTraps.set = function(state, prop, value) {
245270 return objectTraps . set ! . call ( this , state [ 0 ] , prop , value , state [ 0 ] )
246271}
247272
273+ // Type-safe union of mutating array method names
274+ type MutatingArrayMethod =
275+ | "push"
276+ | "pop"
277+ | "shift"
278+ | "unshift"
279+ | "splice"
280+ | "reverse"
281+ | "sort"
282+
283+ // Type-safe union of non-mutating array method names
284+ type NonMutatingArrayMethod =
285+ | "filter"
286+ | "slice"
287+ | "concat"
288+ | "flat"
289+ | "find"
290+ | "findIndex"
291+ | "findLast"
292+ | "findLastIndex"
293+ | "some"
294+ | "every"
295+ | "reduce"
296+ | "reduceRight"
297+ | "indexOf"
298+ | "lastIndexOf"
299+ | "includes"
300+ | "join"
301+ | "toString"
302+ | "toLocaleString"
303+
304+ // Union of all array operation methods
305+ type ArrayOperationMethod = MutatingArrayMethod | NonMutatingArrayMethod
306+
307+ const SHIFTING_METHODS = new Set < MutatingArrayMethod > ( [ "shift" , "unshift" ] )
308+
309+ const QUEUE_METHODS = new Set < MutatingArrayMethod > ( [ "push" , "pop" ] )
310+
311+ const RESULT_RETURNING_METHODS = new Set < MutatingArrayMethod > ( [
312+ ...QUEUE_METHODS ,
313+ ...SHIFTING_METHODS
314+ ] )
315+
316+ const REORDERING_METHODS = new Set < MutatingArrayMethod > ( [ "reverse" , "sort" ] )
317+
318+ // Optimized method detection using array-based lookup
319+ const MUTATING_METHODS = new Set < MutatingArrayMethod > ( [
320+ ...RESULT_RETURNING_METHODS ,
321+ ...REORDERING_METHODS ,
322+ "splice"
323+ ] )
324+
325+ const FIND_METHODS = new Set < NonMutatingArrayMethod > ( [ "find" , "findLast" ] )
326+
327+ const NON_MUTATING_METHODS = new Set < NonMutatingArrayMethod > ( [
328+ "filter" ,
329+ "slice" ,
330+ "concat" ,
331+ "flat" ,
332+ ...FIND_METHODS ,
333+ "findIndex" ,
334+ "findLastIndex" ,
335+ "some" ,
336+ "every" ,
337+ "indexOf" ,
338+ "lastIndexOf" ,
339+ "includes" ,
340+ "join" ,
341+ "toString" ,
342+ "toLocaleString"
343+ ] )
344+
345+ // Type guard for method detection
346+ export function isMutatingArrayMethod (
347+ method : string
348+ ) : method is MutatingArrayMethod {
349+ return MUTATING_METHODS . has ( method as any )
350+ }
351+
352+ export function isNonMutatingArrayMethod (
353+ method : string
354+ ) : method is NonMutatingArrayMethod {
355+ return NON_MUTATING_METHODS . has ( method as any )
356+ }
357+
358+ export function isArrayOperationMethod (
359+ method : string
360+ ) : method is ArrayOperationMethod {
361+ return isMutatingArrayMethod ( method ) || isNonMutatingArrayMethod ( method )
362+ }
363+
364+ function enterOperation ( state : ProxyArrayState , method : ArrayOperationMethod ) {
365+ state . operationMethod = method
366+ }
367+
368+ function exitOperation ( state : ProxyArrayState ) {
369+ state . operationMethod = undefined
370+ }
371+
372+ // Shared utility functions for array method handlers
373+ function executeArrayMethod < T > (
374+ state : ProxyArrayState ,
375+ operation : ( ) => T ,
376+ markLength = true
377+ ) : T {
378+ prepareCopy ( state )
379+ const result = operation ( )
380+ markChanged ( state )
381+ if ( markLength ) state . assigned_ ! . set ( "length" , true )
382+ return result
383+ }
384+
385+ function markAllIndicesReassigned ( state : ProxyArrayState ) {
386+ for ( let i = 0 ; i < state . copy_ ! . length ; i ++ ) {
387+ state . assigned_ ! . set ( i . toString ( ) , true )
388+ }
389+ }
390+
391+ function normalizeSliceIndex ( index : number , length : number ) : number {
392+ if ( index < 0 ) {
393+ return Math . max ( length + index , 0 )
394+ }
395+ return Math . min ( index , length )
396+ }
397+
398+ // Consolidated handler functions
399+ function handleSimpleOperation (
400+ state : ProxyArrayState ,
401+ method : string ,
402+ args : any [ ]
403+ ) {
404+ return executeArrayMethod ( state , ( ) => {
405+ const result = ( state . copy_ ! as any ) [ method ] ( ...args )
406+
407+ // Handle index reassignment for shifting methods
408+ if ( SHIFTING_METHODS . has ( method as MutatingArrayMethod ) ) {
409+ markAllIndicesReassigned ( state )
410+ }
411+
412+ // Return appropriate value based on method
413+ return RESULT_RETURNING_METHODS . has ( method as MutatingArrayMethod )
414+ ? result
415+ : state . draft_
416+ } )
417+ }
418+
419+ function handleReorderingOperation (
420+ state : ProxyArrayState ,
421+ method : string ,
422+ args : any [ ]
423+ ) {
424+ return executeArrayMethod (
425+ state ,
426+ ( ) => {
427+ ; ( state . copy_ ! as any ) [ method ] ( ...args )
428+ markAllIndicesReassigned ( state )
429+ return state . draft_
430+ } ,
431+ false
432+ ) // Don't mark length as changed
433+ }
434+
435+ export function createMethodInterceptor (
436+ state : ProxyArrayState ,
437+ method : ArrayOperationMethod
438+ ) {
439+ return function interceptedMethod ( ...args : any [ ] ) {
440+ // Enter operation mode
441+ enterOperation ( state , method )
442+
443+ try {
444+ // Check if this is a mutating method
445+ if ( isMutatingArrayMethod ( method ) ) {
446+ // Direct method dispatch - no configuration lookup needed
447+ if ( RESULT_RETURNING_METHODS . has ( method ) ) {
448+ return handleSimpleOperation ( state , method , args )
449+ }
450+ if ( REORDERING_METHODS . has ( method ) ) {
451+ return handleReorderingOperation ( state , method , args )
452+ }
453+
454+ if ( method === "splice" ) {
455+ const res = executeArrayMethod ( state , ( ) =>
456+ state . copy_ ! . splice ( ...( args as [ number , number , ...any [ ] ] ) )
457+ )
458+ markAllIndicesReassigned ( state )
459+ return res
460+ }
461+ } else {
462+ // Handle non-mutating methods
463+ return handleNonMutatingOperation ( state , method , args )
464+ }
465+ } finally {
466+ // Always exit operation mode
467+ exitOperation ( state )
468+ }
469+ }
470+ }
471+
472+ function handleNonMutatingOperation (
473+ state : ProxyArrayState ,
474+ method : NonMutatingArrayMethod ,
475+ args : any [ ]
476+ ) {
477+ const source = latest ( state )
478+
479+ // Methods that return arrays with selected items - need to return drafts
480+ if ( method === "filter" ) {
481+ const predicate = args [ 0 ]
482+ const result : any [ ] = [ ]
483+
484+ // First pass: call predicate on base values to determine which items pass
485+ for ( let i = 0 ; i < source . length ; i ++ ) {
486+ if ( predicate ( source [ i ] , i , source ) ) {
487+ // Only create draft for items that passed the predicate
488+ result . push ( state . draft_ [ i ] )
489+ }
490+ }
491+
492+ return result
493+ }
494+
495+ if ( FIND_METHODS . has ( method ) ) {
496+ const predicate = args [ 0 ]
497+ const isForward = method === "find"
498+ const step = isForward ? 1 : - 1
499+ const start = isForward ? 0 : source . length - 1
500+
501+ for ( let i = start ; i >= 0 && i < source . length ; i += step ) {
502+ if ( predicate ( source [ i ] , i , source ) ) {
503+ return state . draft_ [ i ]
504+ }
505+ }
506+ return undefined
507+ }
508+
509+ if ( method === "slice" ) {
510+ const rawStart = args [ 0 ] ?? 0
511+ const rawEnd = args [ 1 ] ?? source . length
512+
513+ // Normalize negative indices
514+ const start = normalizeSliceIndex ( rawStart , source . length )
515+ const end = normalizeSliceIndex ( rawEnd , source . length )
516+
517+ const result : any [ ] = [ ]
518+
519+ // Return drafts for items in the slice range
520+ for ( let i = start ; i < end ; i ++ ) {
521+ result . push ( state . draft_ [ i ] )
522+ }
523+
524+ return result
525+ }
526+
527+ // For other methods (indexOf, includes, join, etc.), call on base array
528+ // These don't need drafts since they don't expose items for mutation
529+ return source [ method as keyof typeof Array . prototype ] ( ...args )
530+ }
531+
248532// Access a property without creating an Immer draft.
249533function peek ( draft : Drafted , prop : PropertyKey ) {
250534 const state = draft [ DRAFT_STATE ]
0 commit comments