Skip to content

Commit db42927

Browse files
CopilotJasonMore
andcommitted
Add insertArrayAt method to prevent stack overflow with large arrays
Co-authored-by: JasonMore <383719+JasonMore@users.noreply.github.com>
1 parent 5b7ba36 commit db42927

File tree

3 files changed

+93
-5
lines changed

3 files changed

+93
-5
lines changed

src/__tests__/indexed-set.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,80 @@ describe('IndexedSet', () => {
4242
})
4343
})
4444

45+
describe('insertArrayAt', () => {
46+
it('inserts an array of elements at the specified index', () => {
47+
const set = new IndexedSet<string>()
48+
set.insertArrayAt(0, ['a', 'b', 'c'])
49+
50+
expect(set.get(0)).toBe('a')
51+
expect(set.get(1)).toBe('b')
52+
expect(set.get(2)).toBe('c')
53+
expect(set.size).toBe(3)
54+
})
55+
56+
it('inserts elements in the middle', () => {
57+
const set = new IndexedSet<string>()
58+
set.insertArrayAt(0, ['a', 'c'])
59+
set.insertArrayAt(1, ['b'])
60+
61+
expect(set.get(0)).toBe('a')
62+
expect(set.get(1)).toBe('b')
63+
expect(set.get(2)).toBe('c')
64+
})
65+
66+
it('deduplicates elements', () => {
67+
const set = new IndexedSet<string>()
68+
set.insertArrayAt(0, ['a', 'b'])
69+
set.insertArrayAt(2, ['b', 'c']) // 'b' should be ignored
70+
71+
expect(set.size).toBe(3)
72+
expect(set.get(0)).toBe('a')
73+
expect(set.get(1)).toBe('b')
74+
expect(set.get(2)).toBe('c')
75+
})
76+
77+
it('handles empty arrays', () => {
78+
const set = new IndexedSet<string>()
79+
set.insertArrayAt(0, ['a', 'b'])
80+
set.insertArrayAt(0, []) // empty array
81+
82+
expect(set.size).toBe(2)
83+
})
84+
85+
it('handles large arrays (500k elements) without stack overflow', () => {
86+
const set = new IndexedSet<number>()
87+
// Use 500k elements to ensure we test beyond the typical call stack limit
88+
// which causes "Maximum call stack size exceeded" when using spread with splice
89+
const largeArray = Array.from({length: 500000}, (_, i) => i)
90+
91+
// Use insertArrayAt for large arrays to avoid spread operator stack overflow
92+
expect(() => {
93+
set.insertArrayAt(0, largeArray)
94+
}).not.toThrow()
95+
96+
expect(set.size).toBe(500000)
97+
expect(set.get(0)).toBe(0)
98+
expect(set.get(499999)).toBe(499999)
99+
expect(set.has(250000)).toBe(true)
100+
})
101+
102+
it('handles inserting large arrays at middle positions', () => {
103+
const set = new IndexedSet<number>()
104+
set.insertAt(0, -2, -1)
105+
const largeArray = Array.from({length: 500000}, (_, i) => i)
106+
107+
// Use insertArrayAt for large arrays to avoid spread operator stack overflow
108+
expect(() => {
109+
set.insertArrayAt(1, largeArray)
110+
}).not.toThrow()
111+
112+
expect(set.size).toBe(500002)
113+
expect(set.get(0)).toBe(-2)
114+
expect(set.get(1)).toBe(0)
115+
expect(set.get(500001)).toBe(-1)
116+
})
117+
})
118+
45119
describe('delete', () => {
46120
it('removes an element and returns true', () => {
47121
const set = new IndexedSet<string>()

src/focus-zone.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
483483
}
484484
// Insert all elements atomically.
485485
const insertionIndex = findInsertionIndex(filteredElements)
486-
focusableElements.insertAt(insertionIndex, ...filteredElements)
486+
focusableElements.insertArrayAt(insertionIndex, filteredElements)
487487
for (const element of filteredElements) {
488488
// Set tabindex="-1" on all tabbable elements, but save the original
489489
// value in case we need to disable the behavior
@@ -514,7 +514,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
514514
function reinitializeWithFreshElements() {
515515
const freshElements = [...iterateFocusableElements(container, iterateFocusableElementsOptions)]
516516
focusableElements.clear()
517-
focusableElements.insertAt(0, ...freshElements)
517+
focusableElements.insertArrayAt(0, freshElements)
518518
for (const element of freshElements) {
519519
if (!savedTabIndex.has(element)) {
520520
savedTabIndex.set(element, element.getAttribute('tabindex'))

src/utils/indexed-set.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,32 @@ export class IndexedSet<T> {
1010
private _itemSet = new Set<T>()
1111

1212
/**
13-
* Insert elements at a specific index. If index is omitted, appends to end.
13+
* Insert an array of elements at a specific index.
14+
* Use this method for large arrays to avoid stack overflow issues with spread operators.
1415
*/
15-
insertAt(index: number, ...elements: T[]): void {
16+
insertArrayAt(index: number, elements: T[]): void {
1617
const newElements = elements.filter(e => !this._itemSet.has(e))
1718
if (newElements.length === 0) return
1819

19-
this._items.splice(index, 0, ...newElements)
20+
// Avoid using spread with splice to prevent stack overflow with large arrays.
21+
// Use Array.prototype.concat which handles large arrays without spreading.
22+
const before = this._items.slice(0, index)
23+
const after = this._items.slice(index)
24+
this._items = before.concat(newElements, after)
25+
2026
for (const element of newElements) {
2127
this._itemSet.add(element)
2228
}
2329
}
2430

31+
/**
32+
* Insert elements at a specific index. If index is omitted, appends to end.
33+
* Note: For large arrays, use insertArrayAt() instead to avoid stack overflow.
34+
*/
35+
insertAt(index: number, ...elements: T[]): void {
36+
this.insertArrayAt(index, elements)
37+
}
38+
2539
/**
2640
* Remove an element by reference. Returns true if element was found and removed.
2741
*/

0 commit comments

Comments
 (0)