@ocavue/utils 1.3.0 → 1.4.0
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 +312 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -29
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/src/__snapshots__/e2e.test.ts.snap +2 -2
- package/src/checker.test.ts +22 -1
- package/src/checker.ts +7 -0
- package/src/counter.test.ts +197 -0
- package/src/counter.ts +166 -0
- package/src/default-map.test.ts +1 -193
- package/src/default-map.ts +149 -51
- package/src/index.ts +2 -1
- package/src/map-group-by.test.ts +23 -10
- package/src/map-group-by.ts +7 -15
- package/src/map-values.ts +1 -1
- package/src/object-entries.ts +13 -8
- package/src/object-group-by.test.ts +25 -10
- package/src/object-group-by.ts +7 -17
- package/src/once.test.ts +1 -30
- package/src/sleep.ts +8 -1
- package/src/once-stub-1.ts +0 -13
- package/src/once-stub-2.ts +0 -3
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 {
|
|
5
|
+
import { DefaultMap, DefaultWeakMap } from './default-map'
|
|
6
6
|
|
|
7
7
|
describe('DefaultMap', () => {
|
|
8
8
|
it('creates default values for missing keys', () => {
|
|
@@ -281,195 +281,3 @@ describe('DefaultWeakMap', () => {
|
|
|
281
281
|
expect(map.has(key)).toBe(true)
|
|
282
282
|
})
|
|
283
283
|
})
|
|
284
|
-
|
|
285
|
-
describe('Counter', () => {
|
|
286
|
-
it('initializes counts to 0', () => {
|
|
287
|
-
const counter = new Counter<string>()
|
|
288
|
-
|
|
289
|
-
expect(counter.get('key1')).toBe(0)
|
|
290
|
-
expect(counter.get('key2')).toBe(0)
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('increments counts', () => {
|
|
294
|
-
const counter = new Counter<string>()
|
|
295
|
-
|
|
296
|
-
counter.increment('key1')
|
|
297
|
-
expect(counter.get('key1')).toBe(1)
|
|
298
|
-
|
|
299
|
-
counter.increment('key1')
|
|
300
|
-
expect(counter.get('key1')).toBe(2)
|
|
301
|
-
|
|
302
|
-
counter.increment('key2')
|
|
303
|
-
expect(counter.get('key2')).toBe(1)
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
it('increments by custom amounts', () => {
|
|
307
|
-
const counter = new Counter<string>()
|
|
308
|
-
|
|
309
|
-
counter.increment('key1', 5)
|
|
310
|
-
expect(counter.get('key1')).toBe(5)
|
|
311
|
-
|
|
312
|
-
counter.increment('key1', 3)
|
|
313
|
-
expect(counter.get('key1')).toBe(8)
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
it('decrements counts', () => {
|
|
317
|
-
const counter = new Counter<string>()
|
|
318
|
-
|
|
319
|
-
counter.set('key1', 10)
|
|
320
|
-
counter.decrement('key1')
|
|
321
|
-
expect(counter.get('key1')).toBe(9)
|
|
322
|
-
|
|
323
|
-
counter.decrement('key1')
|
|
324
|
-
expect(counter.get('key1')).toBe(8)
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('decrements by custom amounts', () => {
|
|
328
|
-
const counter = new Counter<string>()
|
|
329
|
-
|
|
330
|
-
counter.set('key1', 10)
|
|
331
|
-
counter.decrement('key1', 3)
|
|
332
|
-
expect(counter.get('key1')).toBe(7)
|
|
333
|
-
|
|
334
|
-
counter.decrement('key1', 2)
|
|
335
|
-
expect(counter.get('key1')).toBe(5)
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('allows negative counts', () => {
|
|
339
|
-
const counter = new Counter<string>()
|
|
340
|
-
|
|
341
|
-
counter.decrement('key1')
|
|
342
|
-
expect(counter.get('key1')).toBe(-1)
|
|
343
|
-
|
|
344
|
-
counter.decrement('key1', 5)
|
|
345
|
-
expect(counter.get('key1')).toBe(-6)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
it('accepts initial entries', () => {
|
|
349
|
-
const initialEntries: [string, number][] = [
|
|
350
|
-
['a', 5],
|
|
351
|
-
['b', 10],
|
|
352
|
-
['c', 15],
|
|
353
|
-
]
|
|
354
|
-
const counter = new Counter<string>(initialEntries)
|
|
355
|
-
|
|
356
|
-
expect(counter.get('a')).toBe(5)
|
|
357
|
-
expect(counter.get('b')).toBe(10)
|
|
358
|
-
expect(counter.get('c')).toBe(15)
|
|
359
|
-
expect(counter.get('d')).toBe(0)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('works with all Map methods', () => {
|
|
363
|
-
const counter = new Counter<string>()
|
|
364
|
-
|
|
365
|
-
counter.increment('key1')
|
|
366
|
-
counter.increment('key2', 2)
|
|
367
|
-
|
|
368
|
-
expect(counter.size).toBe(2)
|
|
369
|
-
expect(counter.has('key1')).toBe(true)
|
|
370
|
-
expect([...counter.keys()]).toEqual(['key1', 'key2'])
|
|
371
|
-
expect([...counter.values()]).toEqual([1, 2])
|
|
372
|
-
})
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
describe('WeakCounter', () => {
|
|
376
|
-
it('initializes counts to 0', () => {
|
|
377
|
-
const counter = new WeakCounter<object>()
|
|
378
|
-
const key1 = {}
|
|
379
|
-
const key2 = {}
|
|
380
|
-
|
|
381
|
-
expect(counter.get(key1)).toBe(0)
|
|
382
|
-
expect(counter.get(key2)).toBe(0)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('increments counts', () => {
|
|
386
|
-
const counter = new WeakCounter<object>()
|
|
387
|
-
const key = {}
|
|
388
|
-
|
|
389
|
-
counter.increment(key)
|
|
390
|
-
expect(counter.get(key)).toBe(1)
|
|
391
|
-
|
|
392
|
-
counter.increment(key)
|
|
393
|
-
expect(counter.get(key)).toBe(2)
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
it('increments by custom amounts', () => {
|
|
397
|
-
const counter = new WeakCounter<object>()
|
|
398
|
-
const key = {}
|
|
399
|
-
|
|
400
|
-
counter.increment(key, 5)
|
|
401
|
-
expect(counter.get(key)).toBe(5)
|
|
402
|
-
|
|
403
|
-
counter.increment(key, 3)
|
|
404
|
-
expect(counter.get(key)).toBe(8)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it('decrements counts', () => {
|
|
408
|
-
const counter = new WeakCounter<object>()
|
|
409
|
-
const key = {}
|
|
410
|
-
|
|
411
|
-
counter.set(key, 10)
|
|
412
|
-
counter.decrement(key)
|
|
413
|
-
expect(counter.get(key)).toBe(9)
|
|
414
|
-
|
|
415
|
-
counter.decrement(key)
|
|
416
|
-
expect(counter.get(key)).toBe(8)
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
it('decrements by custom amounts', () => {
|
|
420
|
-
const counter = new WeakCounter<object>()
|
|
421
|
-
const key = {}
|
|
422
|
-
|
|
423
|
-
counter.set(key, 10)
|
|
424
|
-
counter.decrement(key, 3)
|
|
425
|
-
expect(counter.get(key)).toBe(7)
|
|
426
|
-
|
|
427
|
-
counter.decrement(key, 2)
|
|
428
|
-
expect(counter.get(key)).toBe(5)
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('allows negative counts', () => {
|
|
432
|
-
const counter = new WeakCounter<object>()
|
|
433
|
-
const key = {}
|
|
434
|
-
|
|
435
|
-
counter.decrement(key)
|
|
436
|
-
expect(counter.get(key)).toBe(-1)
|
|
437
|
-
|
|
438
|
-
counter.decrement(key, 5)
|
|
439
|
-
expect(counter.get(key)).toBe(-6)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
it('accepts initial entries', () => {
|
|
443
|
-
const key1 = {}
|
|
444
|
-
const key2 = {}
|
|
445
|
-
const key3 = {}
|
|
446
|
-
const initialEntries: [object, number][] = [
|
|
447
|
-
[key1, 5],
|
|
448
|
-
[key2, 10],
|
|
449
|
-
[key3, 15],
|
|
450
|
-
]
|
|
451
|
-
const counter = new WeakCounter<object>(initialEntries)
|
|
452
|
-
|
|
453
|
-
expect(counter.get(key1)).toBe(5)
|
|
454
|
-
expect(counter.get(key2)).toBe(10)
|
|
455
|
-
expect(counter.get(key3)).toBe(15)
|
|
456
|
-
|
|
457
|
-
const key4 = {}
|
|
458
|
-
expect(counter.get(key4)).toBe(0)
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
it('works with WeakMap methods', () => {
|
|
462
|
-
const counter = new WeakCounter<object>()
|
|
463
|
-
const key1 = {}
|
|
464
|
-
const key2 = {}
|
|
465
|
-
|
|
466
|
-
counter.increment(key1)
|
|
467
|
-
counter.increment(key2, 2)
|
|
468
|
-
|
|
469
|
-
expect(counter.has(key1)).toBe(true)
|
|
470
|
-
expect(counter.has(key2)).toBe(true)
|
|
471
|
-
|
|
472
|
-
counter.delete(key1)
|
|
473
|
-
expect(counter.has(key1)).toBe(false)
|
|
474
|
-
})
|
|
475
|
-
})
|
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
|
|
@@ -24,7 +86,93 @@ export class DefaultMap<K, V> extends Map<K, V> {
|
|
|
24
86
|
/**
|
|
25
87
|
* A weak map that automatically creates values for missing keys using a factory function.
|
|
26
88
|
*
|
|
27
|
-
* Similar to DefaultMap but uses WeakMap as the base, allowing garbage collection of keys.
|
|
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
|
+
* ```
|
|
28
176
|
*/
|
|
29
177
|
export class DefaultWeakMap<K extends WeakKey, V> extends WeakMap<K, V> {
|
|
30
178
|
private readonly defaultFactory: () => V
|
|
@@ -46,53 +194,3 @@ export class DefaultWeakMap<K extends WeakKey, V> extends WeakMap<K, V> {
|
|
|
46
194
|
return value
|
|
47
195
|
}
|
|
48
196
|
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A map that counts occurrences of keys.
|
|
52
|
-
*
|
|
53
|
-
* Similar to Python's [Counter](https://docs.python.org/3.13/library/collections.html#collections.Counter).
|
|
54
|
-
*/
|
|
55
|
-
export class Counter<K> extends DefaultMap<K, number> {
|
|
56
|
-
constructor(iterable?: Iterable<readonly [K, number]>) {
|
|
57
|
-
super(() => 0, iterable)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Increments the count for a key by a given amount (default 1).
|
|
62
|
-
*/
|
|
63
|
-
increment(key: K, amount = 1): void {
|
|
64
|
-
this.set(key, this.get(key) + amount)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Decrements the count for a key by a given amount (default 1).
|
|
69
|
-
*/
|
|
70
|
-
decrement(key: K, amount = 1): void {
|
|
71
|
-
this.set(key, this.get(key) - amount)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A weak map that counts occurrences of object keys.
|
|
77
|
-
*
|
|
78
|
-
* Similar to Counter but uses WeakMap as the base, allowing garbage collection of keys.
|
|
79
|
-
*/
|
|
80
|
-
export class WeakCounter<K extends WeakKey> extends DefaultWeakMap<K, number> {
|
|
81
|
-
constructor(entries?: readonly (readonly [K, number])[] | null) {
|
|
82
|
-
super(() => 0, entries)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Increments the count for a key by a given amount (default 1).
|
|
87
|
-
*/
|
|
88
|
-
increment(key: K, amount = 1): void {
|
|
89
|
-
this.set(key, this.get(key) + amount)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Decrements the count for a key by a given amount (default 1).
|
|
94
|
-
*/
|
|
95
|
-
decrement(key: K, amount = 1): void {
|
|
96
|
-
this.set(key, this.get(key) - amount)
|
|
97
|
-
}
|
|
98
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './checker'
|
|
2
|
-
export { Counter,
|
|
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,23 +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
|
-
const hasMapGroupBy: boolean = /* @__PURE__ */ (() => !!Map.groupBy)()
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 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.
|
|
37
25
|
*
|
|
38
26
|
* @public
|
|
39
27
|
*/
|
|
40
|
-
export
|
|
28
|
+
export function mapGroupBy<K, T>(
|
|
41
29
|
items: Iterable<T>,
|
|
42
30
|
keySelector: (item: T, index: number) => K,
|
|
43
|
-
)
|
|
31
|
+
): Map<K, T[]> {
|
|
32
|
+
return Map.groupBy
|
|
33
|
+
? Map.groupBy(items, keySelector)
|
|
34
|
+
: mapGroupByPolyfill(items, keySelector)
|
|
35
|
+
}
|
package/src/map-values.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function mapValues<ValueIn, ValueOut>(
|
|
|
35
35
|
object: Record<string, ValueIn>,
|
|
36
36
|
callback: (value: ValueIn, key: string) => ValueOut,
|
|
37
37
|
): Record<string, ValueOut> {
|
|
38
|
-
|
|
38
|
+
const result = {} as Record<string, ValueOut>
|
|
39
39
|
for (const [key, value] of Object.entries(object)) {
|
|
40
40
|
result[key] = callback(value, key)
|
|
41
41
|
}
|
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,25 +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
|
-
const hasObjectGroupBy: boolean = /* @__PURE__ */ (() => !!Object.groupBy)()
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 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.
|
|
37
25
|
*
|
|
38
26
|
* @public
|
|
39
27
|
*/
|
|
40
|
-
export
|
|
28
|
+
export function objectGroupBy<K extends PropertyKey, T>(
|
|
41
29
|
items: Iterable<T>,
|
|
42
30
|
keySelector: (item: T, index: number) => K,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
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
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
4
|
|
|
7
5
|
import { once } from './once'
|
|
8
6
|
|
|
@@ -25,31 +23,4 @@ describe('once', () => {
|
|
|
25
23
|
const getValue = once(() => value)
|
|
26
24
|
expect(getValue()).toBe(value)
|
|
27
25
|
})
|
|
28
|
-
|
|
29
|
-
it('can tree-shake', async () => {
|
|
30
|
-
const esbuild = await import('esbuild')
|
|
31
|
-
const cwd = import.meta.dirname
|
|
32
|
-
const input = path.join(cwd, 'once-stub-2.ts')
|
|
33
|
-
|
|
34
|
-
const result = await esbuild.build({
|
|
35
|
-
entryPoints: [input],
|
|
36
|
-
format: 'esm',
|
|
37
|
-
platform: 'neutral',
|
|
38
|
-
minify: true,
|
|
39
|
-
bundle: true,
|
|
40
|
-
write: false,
|
|
41
|
-
absWorkingDir: cwd,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const files = result.outputFiles.map((file) => {
|
|
45
|
-
return file.text
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
expect(files).toMatchInlineSnapshot(`
|
|
49
|
-
[
|
|
50
|
-
"function t(n){let o=!1,e;return()=>(o||(e=n(),o=!0,n=void 0),e)}function r(){console.log("fn1")}var u=t(r);function f(){console.log("fn2")}f();
|
|
51
|
-
",
|
|
52
|
-
]
|
|
53
|
-
`)
|
|
54
|
-
})
|
|
55
26
|
})
|
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))
|