@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ocavue/utils",
3
3
  "type": "module",
4
- "version": "1.2.0",
4
+ "version": "1.3.1",
5
5
  "description": "A collection of utility functions for the browser and other environments",
6
6
  "author": "ocavue <ocavue@gmail.com>",
7
7
  "license": "MIT",
@@ -37,20 +37,22 @@
37
37
  "dist"
38
38
  ],
39
39
  "devDependencies": {
40
- "@ocavue/eslint-config": "^3.6.2",
41
- "@ocavue/tsconfig": "^0.6.1",
42
- "@size-limit/preset-small-lib": "^11.2.0",
40
+ "@ocavue/eslint-config": "^3.8.3",
41
+ "@ocavue/tsconfig": "^0.6.2",
42
+ "@size-limit/preset-small-lib": "^12.0.0",
43
43
  "@types/node": "^24.9.2",
44
- "@vitest/coverage-v8": "4.0.13",
45
- "eslint": "^9.38.0",
46
- "jsdom": "^27.2.0",
47
- "pkg-pr-new": "^0.0.60",
48
- "prettier": "^3.6.2",
49
- "size-limit": "^11.2.0",
50
- "tsdown": "^0.16.6",
44
+ "@vitest/coverage-v8": "^4.0.16",
45
+ "eslint": "^9.39.2",
46
+ "jsdom": "^27.4.0",
47
+ "knip": "^5.79.0",
48
+ "pkg-pr-new": "^0.0.62",
49
+ "prettier": "^3.7.4",
50
+ "size-limit": "^12.0.0",
51
+ "tinyexec": "^1.0.2",
52
+ "tinyglobby": "^0.2.15",
53
+ "tsdown": "^0.18.4",
51
54
  "typescript": "^5.9.3",
52
- "vite": "^7.2.4",
53
- "vitest": "^4.0.13"
55
+ "vitest": "^4.0.16"
54
56
  },
55
57
  "publishConfig": {
56
58
  "access": "public"
@@ -68,8 +70,13 @@
68
70
  "scripts": {
69
71
  "build": "tsdown",
70
72
  "dev": "tsdown --watch",
71
- "lint": "eslint .",
72
- "fix": "eslint --fix . && prettier --write .",
73
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:knip",
74
+ "lint:eslint": "eslint .",
75
+ "lint:prettier": "prettier --check .",
76
+ "lint:knip": "knip",
77
+ "fix": "pnpm run fix:eslint && pnpm run fix:prettier",
78
+ "fix:eslint": "eslint --fix .",
79
+ "fix:prettier": "prettier --write .",
73
80
  "ci:publish:snapshot": "pkg-pr-new publish --pnpm",
74
81
  "test": "vitest",
75
82
  "typecheck": "tsc -b"
@@ -0,0 +1,247 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`e2e > bundler outputs match snapshot 1`] = `
4
+ "
5
+ ################################################################################
6
+ esbuild/fn1.js
7
+ --------------------------------------------------------------------------------
8
+ var s = 2 ** 53 - 1
9
+ function e(t) {
10
+ let n = !1,
11
+ r
12
+ return () => (n || ((r = t()), (n = !0), (t = void 0)), r)
13
+ }
14
+ function o() {
15
+ console.log('function_1')
16
+ }
17
+ function u() {
18
+ console.log('function_2')
19
+ }
20
+ var l = e(u)
21
+ function i() {
22
+ console.log('function_3')
23
+ }
24
+ var p = e(i)
25
+ export { o as fn1 }
26
+
27
+
28
+ ################################################################################
29
+ esbuild/fn2.js
30
+ --------------------------------------------------------------------------------
31
+ var i = 2 ** 53 - 1
32
+ function e(t) {
33
+ let n = !1,
34
+ r
35
+ return () => (n || ((r = t()), (n = !0), (t = void 0)), r)
36
+ }
37
+ function o() {
38
+ console.log('function_2')
39
+ }
40
+ var c = e(o)
41
+ function u() {
42
+ console.log('function_3')
43
+ }
44
+ var l = e(u)
45
+ export { o as fn2 }
46
+
47
+
48
+ ################################################################################
49
+ esbuild/fn3.js
50
+ --------------------------------------------------------------------------------
51
+ var i = 2 ** 53 - 1
52
+ function e(t) {
53
+ let n = !1,
54
+ r
55
+ return () => (n || ((r = t()), (n = !0), (t = void 0)), r)
56
+ }
57
+ function u() {
58
+ console.log('function_2')
59
+ }
60
+ var c = e(u)
61
+ function o() {
62
+ console.log('function_3')
63
+ }
64
+ var l = e(o)
65
+ export { o as fn3 }
66
+
67
+
68
+ ################################################################################
69
+ esbuild/fn4.js
70
+ --------------------------------------------------------------------------------
71
+ var s = 2 ** 53 - 1
72
+ function e(t) {
73
+ let n = !1,
74
+ r
75
+ return () => (n || ((r = t()), (n = !0), (t = void 0)), r)
76
+ }
77
+ function o() {
78
+ console.log('function_2')
79
+ }
80
+ var l = e(o)
81
+ function u() {
82
+ console.log('function_3')
83
+ }
84
+ var p = e(u)
85
+ function i() {
86
+ console.log('function_4')
87
+ }
88
+ export { i as fn4 }
89
+
90
+
91
+ ################################################################################
92
+ rolldown/fn1.js
93
+ --------------------------------------------------------------------------------
94
+ function e() {
95
+ console.log(\`function_1\`)
96
+ }
97
+ export { e as fn1 }
98
+
99
+
100
+ ################################################################################
101
+ rolldown/fn2.js
102
+ --------------------------------------------------------------------------------
103
+ function e() {
104
+ console.log(\`function_2\`)
105
+ }
106
+ export { e as fn2 }
107
+
108
+
109
+ ################################################################################
110
+ rolldown/fn3.js
111
+ --------------------------------------------------------------------------------
112
+ function e() {
113
+ console.log(\`function_3\`)
114
+ }
115
+ export { e as fn3 }
116
+
117
+
118
+ ################################################################################
119
+ rolldown/fn4.js
120
+ --------------------------------------------------------------------------------
121
+ function e() {
122
+ console.log(\`function_4\`)
123
+ }
124
+ export { e as fn4 }
125
+
126
+
127
+ ################################################################################
128
+ rollup/fn1.js
129
+ --------------------------------------------------------------------------------
130
+ // A function that is not wrapped by \`once()\`
131
+ function fn1() {
132
+ console.log('function_1')
133
+ }
134
+
135
+ export { fn1 }
136
+
137
+
138
+ ################################################################################
139
+ rollup/fn2.js
140
+ --------------------------------------------------------------------------------
141
+ // A function that is wrapped by \`once()\`, but the wrapper is not exported
142
+ function fn2() {
143
+ console.log('function_2')
144
+ }
145
+
146
+ export { fn2 }
147
+
148
+
149
+ ################################################################################
150
+ rollup/fn3.js
151
+ --------------------------------------------------------------------------------
152
+ // A function that is wrapped by \`once()\`, and the wrapper is exported
153
+ function fn3() {
154
+ console.log('function_3')
155
+ }
156
+
157
+ export { fn3 }
158
+
159
+
160
+ ################################################################################
161
+ rollup/fn4.js
162
+ --------------------------------------------------------------------------------
163
+ // A function that is wrapped by \`once()\`, but the wrapper is marked as \`__PURE__\`
164
+ function fn4() {
165
+ console.log('function_4')
166
+ }
167
+
168
+ export { fn4 }
169
+
170
+
171
+ ################################################################################
172
+ rslib/fn1.js
173
+ --------------------------------------------------------------------------------
174
+ function n(n) {
175
+ let o = !1,
176
+ c
177
+ return () => (o || ((c = n()), (o = !0), (n = void 0)), c)
178
+ }
179
+ function o() {
180
+ console.log('function_1')
181
+ }
182
+ ;(n(function () {
183
+ console.log('function_2')
184
+ }),
185
+ n(function () {
186
+ console.log('function_3')
187
+ }))
188
+ export { o as fn1 }
189
+
190
+
191
+ ################################################################################
192
+ rslib/fn2.js
193
+ --------------------------------------------------------------------------------
194
+ function n(n) {
195
+ let o = !1,
196
+ t
197
+ return () => (o || ((t = n()), (o = !0), (n = void 0)), t)
198
+ }
199
+ function o() {
200
+ console.log('function_2')
201
+ }
202
+ ;(n(o),
203
+ n(function () {
204
+ console.log('function_3')
205
+ }))
206
+ export { o as fn2 }
207
+
208
+
209
+ ################################################################################
210
+ rslib/fn3.js
211
+ --------------------------------------------------------------------------------
212
+ function n(n) {
213
+ let o = !1,
214
+ t
215
+ return () => (o || ((t = n()), (o = !0), (n = void 0)), t)
216
+ }
217
+ function o() {
218
+ console.log('function_3')
219
+ }
220
+ ;(n(function () {
221
+ console.log('function_2')
222
+ }),
223
+ n(o))
224
+ export { o as fn3 }
225
+
226
+
227
+ ################################################################################
228
+ rslib/fn4.js
229
+ --------------------------------------------------------------------------------
230
+ function n(n) {
231
+ let o = !1,
232
+ c
233
+ return () => (o || ((c = n()), (o = !0), (n = void 0)), c)
234
+ }
235
+ function o() {
236
+ console.log('function_4')
237
+ }
238
+ ;(n(function () {
239
+ console.log('function_2')
240
+ }),
241
+ n(function () {
242
+ console.log('function_3')
243
+ }))
244
+ export { o as fn4 }
245
+
246
+ "
247
+ `;
@@ -0,0 +1,197 @@
1
+ // @vitest-environment node
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+
5
+ import { Counter, WeakCounter } from './counter'
6
+
7
+ describe('Counter', () => {
8
+ it('initializes counts to 0', () => {
9
+ const counter = new Counter<string>()
10
+
11
+ expect(counter.get('key1')).toBe(0)
12
+ expect(counter.get('key2')).toBe(0)
13
+ })
14
+
15
+ it('increments counts', () => {
16
+ const counter = new Counter<string>()
17
+
18
+ counter.increment('key1')
19
+ expect(counter.get('key1')).toBe(1)
20
+
21
+ counter.increment('key1')
22
+ expect(counter.get('key1')).toBe(2)
23
+
24
+ counter.increment('key2')
25
+ expect(counter.get('key2')).toBe(1)
26
+ })
27
+
28
+ it('increments by custom amounts', () => {
29
+ const counter = new Counter<string>()
30
+
31
+ counter.increment('key1', 5)
32
+ expect(counter.get('key1')).toBe(5)
33
+
34
+ counter.increment('key1', 3)
35
+ expect(counter.get('key1')).toBe(8)
36
+ })
37
+
38
+ it('decrements counts', () => {
39
+ const counter = new Counter<string>()
40
+
41
+ counter.set('key1', 10)
42
+ counter.decrement('key1')
43
+ expect(counter.get('key1')).toBe(9)
44
+
45
+ counter.decrement('key1')
46
+ expect(counter.get('key1')).toBe(8)
47
+ })
48
+
49
+ it('decrements by custom amounts', () => {
50
+ const counter = new Counter<string>()
51
+
52
+ counter.set('key1', 10)
53
+ counter.decrement('key1', 3)
54
+ expect(counter.get('key1')).toBe(7)
55
+
56
+ counter.decrement('key1', 2)
57
+ expect(counter.get('key1')).toBe(5)
58
+ })
59
+
60
+ it('allows negative counts', () => {
61
+ const counter = new Counter<string>()
62
+
63
+ counter.decrement('key1')
64
+ expect(counter.get('key1')).toBe(-1)
65
+
66
+ counter.decrement('key1', 5)
67
+ expect(counter.get('key1')).toBe(-6)
68
+ })
69
+
70
+ it('accepts initial entries', () => {
71
+ const initialEntries: [string, number][] = [
72
+ ['a', 5],
73
+ ['b', 10],
74
+ ['c', 15],
75
+ ]
76
+ const counter = new Counter<string>(initialEntries)
77
+
78
+ expect(counter.get('a')).toBe(5)
79
+ expect(counter.get('b')).toBe(10)
80
+ expect(counter.get('c')).toBe(15)
81
+ expect(counter.get('d')).toBe(0)
82
+ })
83
+
84
+ it('works with all Map methods', () => {
85
+ const counter = new Counter<string>()
86
+
87
+ counter.increment('key1')
88
+ counter.increment('key2', 2)
89
+
90
+ expect(counter.size).toBe(2)
91
+ expect(counter.has('key1')).toBe(true)
92
+ expect([...counter.keys()]).toEqual(['key1', 'key2'])
93
+ expect([...counter.values()]).toEqual([1, 2])
94
+ })
95
+ })
96
+
97
+ describe('WeakCounter', () => {
98
+ it('initializes counts to 0', () => {
99
+ const counter = new WeakCounter<object>()
100
+ const key1 = {}
101
+ const key2 = {}
102
+
103
+ expect(counter.get(key1)).toBe(0)
104
+ expect(counter.get(key2)).toBe(0)
105
+ })
106
+
107
+ it('increments counts', () => {
108
+ const counter = new WeakCounter<object>()
109
+ const key = {}
110
+
111
+ counter.increment(key)
112
+ expect(counter.get(key)).toBe(1)
113
+
114
+ counter.increment(key)
115
+ expect(counter.get(key)).toBe(2)
116
+ })
117
+
118
+ it('increments by custom amounts', () => {
119
+ const counter = new WeakCounter<object>()
120
+ const key = {}
121
+
122
+ counter.increment(key, 5)
123
+ expect(counter.get(key)).toBe(5)
124
+
125
+ counter.increment(key, 3)
126
+ expect(counter.get(key)).toBe(8)
127
+ })
128
+
129
+ it('decrements counts', () => {
130
+ const counter = new WeakCounter<object>()
131
+ const key = {}
132
+
133
+ counter.set(key, 10)
134
+ counter.decrement(key)
135
+ expect(counter.get(key)).toBe(9)
136
+
137
+ counter.decrement(key)
138
+ expect(counter.get(key)).toBe(8)
139
+ })
140
+
141
+ it('decrements by custom amounts', () => {
142
+ const counter = new WeakCounter<object>()
143
+ const key = {}
144
+
145
+ counter.set(key, 10)
146
+ counter.decrement(key, 3)
147
+ expect(counter.get(key)).toBe(7)
148
+
149
+ counter.decrement(key, 2)
150
+ expect(counter.get(key)).toBe(5)
151
+ })
152
+
153
+ it('allows negative counts', () => {
154
+ const counter = new WeakCounter<object>()
155
+ const key = {}
156
+
157
+ counter.decrement(key)
158
+ expect(counter.get(key)).toBe(-1)
159
+
160
+ counter.decrement(key, 5)
161
+ expect(counter.get(key)).toBe(-6)
162
+ })
163
+
164
+ it('accepts initial entries', () => {
165
+ const key1 = {}
166
+ const key2 = {}
167
+ const key3 = {}
168
+ const initialEntries: [object, number][] = [
169
+ [key1, 5],
170
+ [key2, 10],
171
+ [key3, 15],
172
+ ]
173
+ const counter = new WeakCounter<object>(initialEntries)
174
+
175
+ expect(counter.get(key1)).toBe(5)
176
+ expect(counter.get(key2)).toBe(10)
177
+ expect(counter.get(key3)).toBe(15)
178
+
179
+ const key4 = {}
180
+ expect(counter.get(key4)).toBe(0)
181
+ })
182
+
183
+ it('works with WeakMap methods', () => {
184
+ const counter = new WeakCounter<object>()
185
+ const key1 = {}
186
+ const key2 = {}
187
+
188
+ counter.increment(key1)
189
+ counter.increment(key2, 2)
190
+
191
+ expect(counter.has(key1)).toBe(true)
192
+ expect(counter.has(key2)).toBe(true)
193
+
194
+ counter.delete(key1)
195
+ expect(counter.has(key1)).toBe(false)
196
+ })
197
+ })
package/src/counter.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * A map that counts occurrences of keys.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // Count word occurrences
7
+ * const wordCounter = new Counter<string>()
8
+ * const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
9
+ *
10
+ * for (const word of words) {
11
+ * wordCounter.increment(word)
12
+ * }
13
+ *
14
+ * console.log(wordCounter.get('apple')) // 3
15
+ * console.log(wordCounter.get('banana')) // 2
16
+ * console.log(wordCounter.get('cherry')) // 1
17
+ * console.log(wordCounter.get('orange')) // 0 (defaults to 0)
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Initialize with existing counts
23
+ * const counter = new Counter<string>([
24
+ * ['red', 5],
25
+ * ['blue', 3],
26
+ * ['green', 7]
27
+ * ])
28
+ *
29
+ * counter.increment('red', 2) // red: 5 -> 7
30
+ * counter.decrement('blue') // blue: 3 -> 2
31
+ * counter.increment('yellow') // yellow: 0 -> 1
32
+ *
33
+ * console.log(counter.get('red')) // 7
34
+ * console.log(counter.get('blue')) // 2
35
+ * console.log(counter.get('yellow')) // 1
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Track event frequencies
41
+ * const eventCounter = new Counter<string>()
42
+ *
43
+ * eventCounter.increment('click', 5)
44
+ * eventCounter.increment('hover', 3)
45
+ * eventCounter.increment('click', 2)
46
+ *
47
+ * // Get most common events
48
+ * const events = [...eventCounter.entries()]
49
+ * .sort((a, b) => b[1] - a[1])
50
+ *
51
+ * console.log(events) // [['click', 7], ['hover', 3]]
52
+ * ```
53
+ */
54
+ export class Counter<K> extends Map<K, number> {
55
+ constructor(iterable?: Iterable<readonly [K, number]>) {
56
+ super(iterable)
57
+ }
58
+
59
+ override get(key: K): number {
60
+ return super.get(key) ?? 0
61
+ }
62
+
63
+ /**
64
+ * Increments the count for a key by a given amount (default 1).
65
+ */
66
+ increment(key: K, amount = 1): void {
67
+ this.set(key, this.get(key) + amount)
68
+ }
69
+
70
+ /**
71
+ * Decrements the count for a key by a given amount (default 1).
72
+ */
73
+ decrement(key: K, amount = 1): void {
74
+ this.set(key, this.get(key) - amount)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * A weak map that counts occurrences of object keys.
80
+ *
81
+ * Similar to {@link Counter} but uses WeakMap as the base, allowing garbage collection of keys.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Track reference counts for DOM elements
86
+ * const elementRefs = new WeakCounter<HTMLElement>()
87
+ *
88
+ * function addReference(element: HTMLElement) {
89
+ * elementRefs.increment(element)
90
+ * console.log(`References: ${elementRefs.get(element)}`)
91
+ * }
92
+ *
93
+ * function removeReference(element: HTMLElement) {
94
+ * elementRefs.decrement(element)
95
+ * console.log(`References: ${elementRefs.get(element)}`)
96
+ * }
97
+ *
98
+ * const div = document.createElement('div')
99
+ * addReference(div) // References: 1
100
+ * addReference(div) // References: 2
101
+ * removeReference(div) // References: 1
102
+ * ```
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Count object interactions without preventing garbage collection
107
+ * const objectInteractions = new WeakCounter<object>()
108
+ *
109
+ * function handleInteraction(obj: object, count = 1) {
110
+ * objectInteractions.increment(obj, count)
111
+ * }
112
+ *
113
+ * const user = { id: 1, name: 'Alice' }
114
+ * const session = { sessionId: 'abc123' }
115
+ *
116
+ * handleInteraction(user, 3)
117
+ * handleInteraction(session, 1)
118
+ * handleInteraction(user, 2)
119
+ *
120
+ * console.log(objectInteractions.get(user)) // 5
121
+ * console.log(objectInteractions.get(session)) // 1
122
+ * // When user and session are no longer referenced elsewhere,
123
+ * // they can be garbage collected along with their counts
124
+ * ```
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * // Initialize with existing counts
129
+ * const cache1 = {}
130
+ * const cache2 = {}
131
+ * const cache3 = {}
132
+ *
133
+ * const hitCounter = new WeakCounter<object>([
134
+ * [cache1, 10],
135
+ * [cache2, 5],
136
+ * [cache3, 15]
137
+ * ])
138
+ *
139
+ * hitCounter.increment(cache1, 3) // 10 -> 13
140
+ * console.log(hitCounter.get(cache1)) // 13
141
+ * console.log(hitCounter.get(cache2)) // 5
142
+ * ```
143
+ */
144
+ export class WeakCounter<K extends WeakKey> extends WeakMap<K, number> {
145
+ constructor(entries?: readonly (readonly [K, number])[] | null) {
146
+ super(entries)
147
+ }
148
+
149
+ override get(key: K): number {
150
+ return super.get(key) ?? 0
151
+ }
152
+
153
+ /**
154
+ * Increments the count for a key by a given amount (default 1).
155
+ */
156
+ increment(key: K, amount = 1): void {
157
+ this.set(key, this.get(key) + amount)
158
+ }
159
+
160
+ /**
161
+ * Decrements the count for a key by a given amount (default 1).
162
+ */
163
+ decrement(key: K, amount = 1): void {
164
+ this.set(key, this.get(key) - amount)
165
+ }
166
+ }