forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it, vi } from 'vitest'
2import * as fc from 'fast-check'
3import { mapWithConcurrency } from '../../../../shared/utils/async'
4
5describe('mapWithConcurrency', () => {
6 it('processes all items and returns results in order', async () => {
7 const items = [1, 2, 3, 4, 5]
8 const results = await mapWithConcurrency(items, async x => x * 2)
9
10 expect(results).toEqual([2, 4, 6, 8, 10])
11 })
12
13 it('respects concurrency limit', async () => {
14 let concurrent = 0
15 let maxConcurrent = 0
16
17 const items = Array.from({ length: 10 }, (_, i) => i)
18
19 await mapWithConcurrency(
20 items,
21 async () => {
22 concurrent++
23 maxConcurrent = Math.max(maxConcurrent, concurrent)
24 await new Promise(resolve => setTimeout(resolve, 10))
25 concurrent--
26 },
27 3,
28 )
29
30 expect(maxConcurrent).toBe(3)
31 })
32
33 it('handles empty array', async () => {
34 const results = await mapWithConcurrency([], async x => x)
35 expect(results).toEqual([])
36 })
37
38 it('handles single item', async () => {
39 const results = await mapWithConcurrency([42], async x => x * 2)
40 expect(results).toEqual([84])
41 })
42
43 it('passes index to callback', async () => {
44 const items = ['a', 'b', 'c']
45 const results = await mapWithConcurrency(items, async (item, index) => `${item}${index}`)
46
47 expect(results).toEqual(['a0', 'b1', 'c2'])
48 })
49
50 it('propagates errors', async () => {
51 const items = [1, 2, 3]
52 const fn = vi.fn(async (x: number) => {
53 if (x === 2) throw new Error('test error')
54 return x
55 })
56
57 await expect(mapWithConcurrency(items, fn)).rejects.toThrow('test error')
58 })
59
60 it('uses default concurrency of 10', async () => {
61 let concurrent = 0
62 let maxConcurrent = 0
63
64 const items = Array.from({ length: 20 }, (_, i) => i)
65
66 await mapWithConcurrency(items, async () => {
67 concurrent++
68 maxConcurrent = Math.max(maxConcurrent, concurrent)
69 await new Promise(resolve => setTimeout(resolve, 5))
70 concurrent--
71 })
72
73 expect(maxConcurrent).toBe(10)
74 })
75
76 it('handles fewer items than concurrency limit', async () => {
77 let concurrent = 0
78 let maxConcurrent = 0
79
80 const items = [1, 2, 3]
81
82 await mapWithConcurrency(
83 items,
84 async () => {
85 concurrent++
86 maxConcurrent = Math.max(maxConcurrent, concurrent)
87 await new Promise(resolve => setTimeout(resolve, 10))
88 concurrent--
89 },
90 10,
91 )
92
93 // Should only have 3 concurrent since we only have 3 items
94 expect(maxConcurrent).toBe(3)
95 })
96
97 it('waits for all tasks to succeed and return them in order whatever their count and the concurrency', async () => {
98 await fc.assert(
99 fc.asyncProperty(
100 fc.array(fc.anything()),
101 fc.integer({ min: 1 }),
102 fc.scheduler(),
103 async (items, concurrency, s) => {
104 const fn = s.scheduleFunction(async item => item)
105 const results = await s.waitFor(mapWithConcurrency(items, fn, concurrency))
106 expect(results).toEqual(items)
107 },
108 ),
109 )
110 })
111
112 it('not run more than concurrency tasks in parallel', async () => {
113 await fc.assert(
114 fc.asyncProperty(
115 fc.array(fc.anything()),
116 fc.integer({ min: 1 }),
117 fc.scheduler(),
118 async (items, concurrency, s) => {
119 let tooManyRunningTasksEncountered = false
120 let currentlyRunning = 0
121 const fn = async (item: (typeof items)[number]) => {
122 currentlyRunning++
123 if (currentlyRunning > concurrency) {
124 tooManyRunningTasksEncountered = true
125 }
126 const task = s.schedule(Promise.resolve(item))
127 task.then(() => currentlyRunning--) // this task always succeeds by construct
128 return task
129 }
130 await s.waitFor(mapWithConcurrency(items, fn, concurrency))
131 expect(tooManyRunningTasksEncountered).toBe(false)
132 },
133 ),
134 )
135 })
136})