@ocavue/utils 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +326 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -312
- package/dist/index.js.map +1 -1
- package/package.json +22 -15
- package/src/__snapshots__/e2e.test.ts.snap +247 -0
- package/src/counter.test.ts +197 -0
- package/src/counter.ts +166 -0
- package/src/default-map.test.ts +98 -1
- package/src/default-map.ts +174 -0
- package/src/e2e.test.ts +36 -0
- package/src/get-id.test.ts +20 -1
- package/src/get-id.ts +15 -1
- package/src/index.ts +2 -1
- package/src/map-group-by.test.ts +23 -10
- package/src/map-group-by.ts +7 -13
- package/src/object-entries.ts +13 -8
- package/src/object-group-by.test.ts +25 -10
- package/src/object-group-by.ts +7 -15
- package/src/once.test.ts +1 -1
- package/src/sleep.ts +8 -1
package/src/default-map.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, it, expect } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { DefaultMap } from './default-map'
|
|
5
|
+
import { DefaultMap, DefaultWeakMap } from './default-map'
|
|
6
6
|
|
|
7
7
|
describe('DefaultMap', () => {
|
|
8
8
|
it('creates default values for missing keys', () => {
|
|
@@ -184,3 +184,100 @@ describe('DefaultMap', () => {
|
|
|
184
184
|
expect(map.get('group2').get('item1')).toBe(0)
|
|
185
185
|
})
|
|
186
186
|
})
|
|
187
|
+
|
|
188
|
+
describe('DefaultWeakMap', () => {
|
|
189
|
+
it('creates default values for missing keys', () => {
|
|
190
|
+
const map = new DefaultWeakMap<object, number>(() => 0)
|
|
191
|
+
const key1 = {}
|
|
192
|
+
const key2 = {}
|
|
193
|
+
|
|
194
|
+
expect(map.get(key1)).toBe(0)
|
|
195
|
+
expect(map.get(key2)).toBe(0)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('returns existing values for set keys', () => {
|
|
199
|
+
const map = new DefaultWeakMap<object, number>(() => 0)
|
|
200
|
+
const key = {}
|
|
201
|
+
|
|
202
|
+
map.set(key, 42)
|
|
203
|
+
expect(map.get(key)).toBe(42)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('stores the default value when accessing missing key', () => {
|
|
207
|
+
const map = new DefaultWeakMap<object, number>(() => 5)
|
|
208
|
+
const key = {}
|
|
209
|
+
|
|
210
|
+
const value = map.get(key)
|
|
211
|
+
expect(value).toBe(5)
|
|
212
|
+
expect(map.has(key)).toBe(true)
|
|
213
|
+
expect(map.get(key)).toBe(5)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('works with array factory', () => {
|
|
217
|
+
const map = new DefaultWeakMap<object, string[]>(() => [])
|
|
218
|
+
const key1 = {}
|
|
219
|
+
const key2 = {}
|
|
220
|
+
|
|
221
|
+
map.get(key1).push('item1')
|
|
222
|
+
map.get(key1).push('item2')
|
|
223
|
+
map.get(key2).push('item3')
|
|
224
|
+
|
|
225
|
+
expect(map.get(key1)).toEqual(['item1', 'item2'])
|
|
226
|
+
expect(map.get(key2)).toEqual(['item3'])
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('accepts initial entries', () => {
|
|
230
|
+
const key1 = {}
|
|
231
|
+
const key2 = {}
|
|
232
|
+
const key3 = {}
|
|
233
|
+
const initialEntries: [object, number][] = [
|
|
234
|
+
[key1, 1],
|
|
235
|
+
[key2, 2],
|
|
236
|
+
[key3, 3],
|
|
237
|
+
]
|
|
238
|
+
const map = new DefaultWeakMap<object, number>(() => 0, initialEntries)
|
|
239
|
+
|
|
240
|
+
expect(map.get(key1)).toBe(1)
|
|
241
|
+
expect(map.get(key2)).toBe(2)
|
|
242
|
+
expect(map.get(key3)).toBe(3)
|
|
243
|
+
|
|
244
|
+
const key4 = {}
|
|
245
|
+
expect(map.get(key4)).toBe(0)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('calls factory function only when key is missing', () => {
|
|
249
|
+
let callCount = 0
|
|
250
|
+
const map = new DefaultWeakMap<object, number>(() => {
|
|
251
|
+
callCount++
|
|
252
|
+
return 42
|
|
253
|
+
})
|
|
254
|
+
const existing = {}
|
|
255
|
+
const newKey = {}
|
|
256
|
+
|
|
257
|
+
map.set(existing, 100)
|
|
258
|
+
|
|
259
|
+
map.get(existing)
|
|
260
|
+
expect(callCount).toBe(0)
|
|
261
|
+
|
|
262
|
+
map.get(newKey)
|
|
263
|
+
expect(callCount).toBe(1)
|
|
264
|
+
|
|
265
|
+
map.get(newKey)
|
|
266
|
+
expect(callCount).toBe(1)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('works with delete method', () => {
|
|
270
|
+
const map = new DefaultWeakMap<object, number>(() => 10)
|
|
271
|
+
const key = {}
|
|
272
|
+
|
|
273
|
+
map.get(key)
|
|
274
|
+
expect(map.has(key)).toBe(true)
|
|
275
|
+
|
|
276
|
+
map.delete(key)
|
|
277
|
+
expect(map.has(key)).toBe(false)
|
|
278
|
+
|
|
279
|
+
const value = map.get(key)
|
|
280
|
+
expect(value).toBe(10)
|
|
281
|
+
expect(map.has(key)).toBe(true)
|
|
282
|
+
})
|
|
283
|
+
})
|
package/src/default-map.ts
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
* A map that automatically creates values for missing keys using a factory function.
|
|
3
3
|
*
|
|
4
4
|
* Similar to Python's [defaultdict](https://docs.python.org/3.13/library/collections.html#collections.defaultdict).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Group items by category using arrays
|
|
9
|
+
* const groupByCategory = new DefaultMap<string, string[]>(() => [])
|
|
10
|
+
*
|
|
11
|
+
* groupByCategory.get('fruits').push('apple', 'banana')
|
|
12
|
+
* groupByCategory.get('vegetables').push('carrot')
|
|
13
|
+
* groupByCategory.get('fruits').push('orange')
|
|
14
|
+
*
|
|
15
|
+
* console.log(groupByCategory.get('fruits')) // ['apple', 'banana', 'orange']
|
|
16
|
+
* console.log(groupByCategory.get('vegetables')) // ['carrot']
|
|
17
|
+
* console.log(groupByCategory.get('dairy')) // [] (auto-created)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Build a graph with adjacency lists
|
|
23
|
+
* const graph = new DefaultMap<string, Set<string>>(() => new Set())
|
|
24
|
+
*
|
|
25
|
+
* graph.get('A').add('B').add('C')
|
|
26
|
+
* graph.get('B').add('C').add('D')
|
|
27
|
+
* graph.get('C').add('D')
|
|
28
|
+
*
|
|
29
|
+
* console.log([...graph.get('A')]) // ['B', 'C']
|
|
30
|
+
* console.log([...graph.get('B')]) // ['C', 'D']
|
|
31
|
+
* console.log([...graph.get('E')]) // [] (auto-created empty set)
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Initialize with existing entries
|
|
37
|
+
* const scores = new DefaultMap<string, number>(
|
|
38
|
+
* () => 0,
|
|
39
|
+
* [
|
|
40
|
+
* ['Alice', 100],
|
|
41
|
+
* ['Bob', 85]
|
|
42
|
+
* ]
|
|
43
|
+
* )
|
|
44
|
+
*
|
|
45
|
+
* scores.set('Alice', scores.get('Alice') + 10) // 100 -> 110
|
|
46
|
+
* console.log(scores.get('Alice')) // 110
|
|
47
|
+
* console.log(scores.get('Bob')) // 85
|
|
48
|
+
* console.log(scores.get('Charlie')) // 0 (auto-created)
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // Nested DefaultMaps for 2D data structures
|
|
54
|
+
* const matrix = new DefaultMap<number, DefaultMap<number, number>>(
|
|
55
|
+
* () => new DefaultMap<number, number>(() => 0)
|
|
56
|
+
* )
|
|
57
|
+
*
|
|
58
|
+
* matrix.get(0).set(0, 1)
|
|
59
|
+
* matrix.get(0).set(1, 2)
|
|
60
|
+
* matrix.get(1).set(1, 3)
|
|
61
|
+
*
|
|
62
|
+
* console.log(matrix.get(0).get(0)) // 1
|
|
63
|
+
* console.log(matrix.get(0).get(1)) // 2
|
|
64
|
+
* console.log(matrix.get(1).get(0)) // 0 (auto-created)
|
|
65
|
+
* console.log(matrix.get(2).get(3)) // 0 (both auto-created)
|
|
66
|
+
* ```
|
|
5
67
|
*/
|
|
6
68
|
export class DefaultMap<K, V> extends Map<K, V> {
|
|
7
69
|
private readonly defaultFactory: () => V
|
|
@@ -20,3 +82,115 @@ export class DefaultMap<K, V> extends Map<K, V> {
|
|
|
20
82
|
return value
|
|
21
83
|
}
|
|
22
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A weak map that automatically creates values for missing keys using a factory function.
|
|
88
|
+
*
|
|
89
|
+
* Similar to {@link DefaultMap} but uses WeakMap as the base, allowing garbage collection of keys.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // Store metadata for DOM elements without preventing garbage collection
|
|
94
|
+
* const elementMetadata = new DefaultWeakMap<HTMLElement, { clicks: number; hovers: number }>(
|
|
95
|
+
* () => ({ clicks: 0, hovers: 0 })
|
|
96
|
+
* )
|
|
97
|
+
*
|
|
98
|
+
* const button = document.querySelector('button')!
|
|
99
|
+
* const div = document.querySelector('div')!
|
|
100
|
+
*
|
|
101
|
+
* elementMetadata.get(button).clicks++
|
|
102
|
+
* elementMetadata.get(button).clicks++
|
|
103
|
+
* elementMetadata.get(div).hovers++
|
|
104
|
+
*
|
|
105
|
+
* console.log(elementMetadata.get(button)) // { clicks: 2, hovers: 0 }
|
|
106
|
+
* console.log(elementMetadata.get(div)) // { clicks: 0, hovers: 1 }
|
|
107
|
+
* // When elements are removed from DOM and not referenced,
|
|
108
|
+
* // their metadata can be garbage collected
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* // Cache computed properties for objects
|
|
114
|
+
* const computedCache = new DefaultWeakMap<object, Map<string, any>>(
|
|
115
|
+
* () => new Map()
|
|
116
|
+
* )
|
|
117
|
+
*
|
|
118
|
+
* function getOrCompute(obj: object, key: string, compute: () => any) {
|
|
119
|
+
* const cache = computedCache.get(obj)
|
|
120
|
+
* if (!cache.has(key)) {
|
|
121
|
+
* cache.set(key, compute())
|
|
122
|
+
* }
|
|
123
|
+
* return cache.get(key)
|
|
124
|
+
* }
|
|
125
|
+
*
|
|
126
|
+
* const user = { name: 'Alice', age: 30 }
|
|
127
|
+
* const displayName = getOrCompute(user, 'displayName', () => user.name.toUpperCase())
|
|
128
|
+
* const birthYear = getOrCompute(user, 'birthYear', () => new Date().getFullYear() - user.age)
|
|
129
|
+
*
|
|
130
|
+
* console.log(displayName) // 'ALICE'
|
|
131
|
+
* console.log(birthYear) // 1994 (or current year - 30)
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* // Initialize with existing entries
|
|
137
|
+
* const obj1 = {}
|
|
138
|
+
* const obj2 = {}
|
|
139
|
+
*
|
|
140
|
+
* const objectData = new DefaultWeakMap<object, string[]>(
|
|
141
|
+
* () => [],
|
|
142
|
+
* [
|
|
143
|
+
* [obj1, ['tag1', 'tag2']],
|
|
144
|
+
* [obj2, ['tag3']]
|
|
145
|
+
* ]
|
|
146
|
+
* )
|
|
147
|
+
*
|
|
148
|
+
* objectData.get(obj1).push('tag4')
|
|
149
|
+
* console.log(objectData.get(obj1)) // ['tag1', 'tag2', 'tag4']
|
|
150
|
+
* console.log(objectData.get(obj2)) // ['tag3']
|
|
151
|
+
*
|
|
152
|
+
* const obj3 = {}
|
|
153
|
+
* console.log(objectData.get(obj3)) // [] (auto-created)
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* // Track event listeners per element using both DefaultWeakMap and DefaultMap
|
|
159
|
+
* const eventListeners = new DefaultWeakMap<EventTarget, DefaultMap<string, Function[]>>(
|
|
160
|
+
* () => new DefaultMap<string, Function[]>(() => [])
|
|
161
|
+
* )
|
|
162
|
+
*
|
|
163
|
+
* function addListener(target: EventTarget, event: string, handler: Function) {
|
|
164
|
+
* eventListeners.get(target).get(event).push(handler)
|
|
165
|
+
* }
|
|
166
|
+
*
|
|
167
|
+
* const element = document.createElement('button')
|
|
168
|
+
* addListener(element, 'click', () => console.log('clicked'))
|
|
169
|
+
* addListener(element, 'click', () => console.log('also clicked'))
|
|
170
|
+
* addListener(element, 'hover', () => console.log('hovered'))
|
|
171
|
+
*
|
|
172
|
+
* console.log(eventListeners.get(element).get('click').length) // 2
|
|
173
|
+
* console.log(eventListeners.get(element).get('hover').length) // 1
|
|
174
|
+
* // No need for has() checks or null assertions - everything auto-initializes!
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export class DefaultWeakMap<K extends WeakKey, V> extends WeakMap<K, V> {
|
|
178
|
+
private readonly defaultFactory: () => V
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
defaultFactory: () => V,
|
|
182
|
+
entries?: readonly (readonly [K, V])[] | null,
|
|
183
|
+
) {
|
|
184
|
+
super(entries)
|
|
185
|
+
this.defaultFactory = defaultFactory
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override get(key: K): V {
|
|
189
|
+
if (this.has(key)) {
|
|
190
|
+
return super.get(key)!
|
|
191
|
+
}
|
|
192
|
+
const value = this.defaultFactory()
|
|
193
|
+
this.set(key, value)
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/e2e.test.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { x } from 'tinyexec'
|
|
7
|
+
import { glob } from 'tinyglobby'
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
9
|
+
|
|
10
|
+
const ROOT_DIR = path.join(import.meta.dirname, '..')
|
|
11
|
+
const E2E_OUT_DIR = path.join(ROOT_DIR, 'e2e', 'dist')
|
|
12
|
+
|
|
13
|
+
describe('e2e', () => {
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
await x('pnpm', ['-w', 'build'], {
|
|
16
|
+
nodeOptions: { cwd: ROOT_DIR, stdio: 'inherit' },
|
|
17
|
+
throwOnError: true,
|
|
18
|
+
})
|
|
19
|
+
await x('pnpm', ['--filter', 'e2e', 'run', 'build'], {
|
|
20
|
+
nodeOptions: { cwd: ROOT_DIR, stdio: 'inherit' },
|
|
21
|
+
throwOnError: true,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('bundler outputs match snapshot', async () => {
|
|
26
|
+
const files = await glob('**/*', { cwd: E2E_OUT_DIR, onlyFiles: true })
|
|
27
|
+
const output = ['']
|
|
28
|
+
|
|
29
|
+
for (const file of files.sort()) {
|
|
30
|
+
const content = fs.readFileSync(path.join(E2E_OUT_DIR, file), 'utf-8')
|
|
31
|
+
output.push('#'.repeat(80), file, '-'.repeat(80), content, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(output.join('\n')).toMatchSnapshot()
|
|
35
|
+
})
|
|
36
|
+
})
|
package/src/get-id.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { getId } from './get-id'
|
|
3
|
+
import { getId, setMaxSafeInteger } from './get-id'
|
|
4
4
|
|
|
5
5
|
describe('getId', () => {
|
|
6
6
|
it('returns a positive number', () => {
|
|
@@ -11,4 +11,23 @@ describe('getId', () => {
|
|
|
11
11
|
it('returns different values on consecutive calls', () => {
|
|
12
12
|
expect(getId()).not.toBe(getId())
|
|
13
13
|
})
|
|
14
|
+
|
|
15
|
+
it('never exceeds the configured maximum', () => {
|
|
16
|
+
const max = 5
|
|
17
|
+
setMaxSafeInteger(max)
|
|
18
|
+
let prevId = -1
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
for (let i = 0; i < max * 3; i += 1) {
|
|
22
|
+
const id = getId()
|
|
23
|
+
expect(id).toBeLessThan(max)
|
|
24
|
+
expect(id).toBeGreaterThanOrEqual(1)
|
|
25
|
+
expect(id).not.toBe(prevId)
|
|
26
|
+
|
|
27
|
+
prevId = id
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
setMaxSafeInteger(Number.MAX_SAFE_INTEGER)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
14
33
|
})
|
package/src/get-id.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
let id = 0
|
|
2
2
|
|
|
3
|
+
let maxSafeInteger = Number.MAX_SAFE_INTEGER
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sets the maximum safe integer for the id generator. Only for testing purposes.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export function setMaxSafeInteger(max: number) {
|
|
11
|
+
maxSafeInteger = max
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* Generates a unique positive integer.
|
|
5
16
|
*/
|
|
6
17
|
export function getId(): number {
|
|
7
|
-
id
|
|
18
|
+
id++
|
|
19
|
+
if (id >= maxSafeInteger) {
|
|
20
|
+
id = 1
|
|
21
|
+
}
|
|
8
22
|
return id
|
|
9
23
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './checker'
|
|
2
|
-
export {
|
|
2
|
+
export { Counter, WeakCounter } from './counter'
|
|
3
|
+
export { DefaultMap, DefaultWeakMap } from './default-map'
|
|
3
4
|
export * from './dom'
|
|
4
5
|
export { formatBytes } from './format-bytes'
|
|
5
6
|
export { getId } from './get-id'
|
package/src/map-group-by.test.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import { describe, it,
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
mapGroupBy,
|
|
5
|
-
mapGroupByPolyfill,
|
|
6
|
-
mapGroupByNative,
|
|
7
|
-
} from './map-group-by'
|
|
3
|
+
import { mapGroupBy, mapGroupByPolyfill } from './map-group-by'
|
|
8
4
|
|
|
9
5
|
const testCases = [
|
|
10
6
|
{ name: 'mapGroupBy', fn: mapGroupBy },
|
|
11
7
|
{ name: 'mapGroupByPolyfill', fn: mapGroupByPolyfill },
|
|
12
8
|
]
|
|
13
9
|
|
|
14
|
-
if (!!Map.groupBy) {
|
|
15
|
-
testCases.push({ name: 'mapGroupByNative', fn: mapGroupByNative })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
describe.each(testCases)('$name', ({ fn }) => {
|
|
19
11
|
it('groups items by key', () => {
|
|
20
12
|
const items = [1, 2, 3, 4, 5, 6]
|
|
@@ -76,3 +68,24 @@ describe.each(testCases)('$name', ({ fn }) => {
|
|
|
76
68
|
expect(result.get(1)).toEqual([1, 3, 5])
|
|
77
69
|
})
|
|
78
70
|
})
|
|
71
|
+
|
|
72
|
+
describe('mapGroupBy', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
if ('groupBy' in Map) {
|
|
75
|
+
// @ts-expect-error - spy
|
|
76
|
+
vi.spyOn(Map, 'groupBy', 'get').mockReturnValueOnce(undefined)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
vi.restoreAllMocks()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('falls back to polyfill when Map.groupBy is not available', () => {
|
|
85
|
+
const items = [1, 2, 3, 4, 5, 6]
|
|
86
|
+
const result = mapGroupBy(items, (item) => item % 2)
|
|
87
|
+
|
|
88
|
+
expect(result.get(0)).toEqual([2, 4, 6])
|
|
89
|
+
expect(result.get(1)).toEqual([1, 3, 5])
|
|
90
|
+
})
|
|
91
|
+
})
|
package/src/map-group-by.ts
CHANGED
|
@@ -21,21 +21,15 @@ export function mapGroupByPolyfill<K, T>(
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
export function mapGroupByNative<K, T>(
|
|
27
|
-
items: Iterable<T>,
|
|
28
|
-
keySelector: (item: T, index: number) => K,
|
|
29
|
-
): Map<K, T[]> {
|
|
30
|
-
return Map.groupBy(items, keySelector)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A polyfill for the `Map.groupBy` static method.
|
|
24
|
+
* A polyfill for the [`Map.groupBy()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy) static method.
|
|
35
25
|
*
|
|
36
26
|
* @public
|
|
37
27
|
*/
|
|
38
|
-
export
|
|
28
|
+
export function mapGroupBy<K, T>(
|
|
39
29
|
items: Iterable<T>,
|
|
40
30
|
keySelector: (item: T, index: number) => K,
|
|
41
|
-
)
|
|
31
|
+
): Map<K, T[]> {
|
|
32
|
+
return Map.groupBy
|
|
33
|
+
? Map.groupBy(items, keySelector)
|
|
34
|
+
: mapGroupByPolyfill(items, keySelector)
|
|
35
|
+
}
|
package/src/object-entries.ts
CHANGED
|
@@ -17,13 +17,16 @@ export type ObjectEntries<T extends Record<string, any>> = {
|
|
|
17
17
|
}[keyof T]
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* A type-safe wrapper around
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* A type-safe wrapper around
|
|
21
|
+
* [`Object.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries)
|
|
22
|
+
* that preserves the exact types of object keys and values. Unlike the standard
|
|
23
|
+
* `Object.entries()` which returns `[string, any][]`, this function returns an
|
|
24
|
+
* array of tuples where each tuple is precisely typed according to the input
|
|
25
|
+
* object's structure.
|
|
24
26
|
*
|
|
25
|
-
* This is particularly useful when working with objects that have known, fixed
|
|
26
|
-
* and you want to maintain type safety when iterating over
|
|
27
|
+
* This is particularly useful when working with objects that have known, fixed
|
|
28
|
+
* property types and you want to maintain type safety when iterating over
|
|
29
|
+
* entries.
|
|
27
30
|
*
|
|
28
31
|
* @example
|
|
29
32
|
* ```typescript
|
|
@@ -53,6 +56,8 @@ export type ObjectEntries<T extends Record<string, any>> = {
|
|
|
53
56
|
*
|
|
54
57
|
* @public
|
|
55
58
|
*/
|
|
56
|
-
export
|
|
59
|
+
export function objectEntries<T extends Record<string, any>>(
|
|
57
60
|
obj: T,
|
|
58
|
-
)
|
|
61
|
+
): ObjectEntries<T>[] {
|
|
62
|
+
return Object.entries(obj)
|
|
63
|
+
}
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import { describe, it,
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
objectGroupBy,
|
|
5
|
-
objectGroupByPolyfill,
|
|
6
|
-
objectGroupByNative,
|
|
7
|
-
} from './object-group-by'
|
|
3
|
+
import { objectGroupBy, objectGroupByPolyfill } from './object-group-by'
|
|
8
4
|
|
|
9
5
|
const testCases = [
|
|
10
6
|
{ name: 'objectGroupBy', fn: objectGroupBy },
|
|
11
7
|
{ name: 'objectGroupByPolyfill', fn: objectGroupByPolyfill },
|
|
12
8
|
]
|
|
13
9
|
|
|
14
|
-
if (!!Object.groupBy) {
|
|
15
|
-
testCases.push({ name: 'objectGroupByNative', fn: objectGroupByNative })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
describe.each(testCases)('$name', ({ fn }) => {
|
|
19
11
|
it('groups items by key', () => {
|
|
20
12
|
const items = [1, 2, 3, 4, 5, 6]
|
|
@@ -84,3 +76,26 @@ describe.each(testCases)('$name', ({ fn }) => {
|
|
|
84
76
|
expect(result.odd).toEqual([1, 3, 5])
|
|
85
77
|
})
|
|
86
78
|
})
|
|
79
|
+
|
|
80
|
+
describe('objectGroupBy', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
if ('groupBy' in Object) {
|
|
83
|
+
// @ts-expect-error - spy
|
|
84
|
+
vi.spyOn(Object, 'groupBy', 'get').mockReturnValueOnce(undefined)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
vi.restoreAllMocks()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('falls back to polyfill when Object.groupBy is not available', () => {
|
|
93
|
+
const items = [1, 2, 3, 4, 5, 6]
|
|
94
|
+
const result = objectGroupBy(items, (item) =>
|
|
95
|
+
item % 2 === 0 ? 'even' : 'odd',
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
expect(result.even).toEqual([2, 4, 6])
|
|
99
|
+
expect(result.odd).toEqual([1, 3, 5])
|
|
100
|
+
})
|
|
101
|
+
})
|
package/src/object-group-by.ts
CHANGED
|
@@ -21,23 +21,15 @@ export function objectGroupByPolyfill<K extends PropertyKey, T>(
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
export function objectGroupByNative<K extends PropertyKey, T>(
|
|
27
|
-
items: Iterable<T>,
|
|
28
|
-
keySelector: (item: T, index: number) => K,
|
|
29
|
-
): Partial<Record<K, T[]>> {
|
|
30
|
-
return Object.groupBy(items, keySelector)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A polyfill for the `Object.groupBy` static method.
|
|
24
|
+
* A polyfill for the [`Object.groupBy()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy) static method.
|
|
35
25
|
*
|
|
36
26
|
* @public
|
|
37
27
|
*/
|
|
38
|
-
export
|
|
28
|
+
export function objectGroupBy<K extends PropertyKey, T>(
|
|
39
29
|
items: Iterable<T>,
|
|
40
30
|
keySelector: (item: T, index: number) => K,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
): Partial<Record<K, T[]>> {
|
|
32
|
+
return Object.groupBy
|
|
33
|
+
? Object.groupBy(items, keySelector)
|
|
34
|
+
: objectGroupByPolyfill(items, keySelector)
|
|
35
|
+
}
|
package/src/once.test.ts
CHANGED
package/src/sleep.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Returns a Promise that resolves after a specified number of milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* @param ms - The number of milliseconds to wait.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```js
|
|
8
|
+
* await sleep(1000) // Wait 1 second
|
|
9
|
+
* ```
|
|
3
10
|
*/
|
|
4
11
|
export function sleep(ms: number): Promise<void> {
|
|
5
12
|
return new Promise((resolve) => setTimeout(resolve, ms))
|