@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f

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.
Files changed (160) hide show
  1. package/dist-cjs/index.d.ts +1350 -80
  2. package/dist-cjs/index.js +5 -5
  3. package/dist-cjs/lib/ExecutionQueue.js +79 -0
  4. package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
  5. package/dist-cjs/lib/PerformanceTracker.js +43 -0
  6. package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
  7. package/dist-cjs/lib/array.js +3 -1
  8. package/dist-cjs/lib/array.js.map +2 -2
  9. package/dist-cjs/lib/bind.js.map +2 -2
  10. package/dist-cjs/lib/cache.js +27 -5
  11. package/dist-cjs/lib/cache.js.map +2 -2
  12. package/dist-cjs/lib/control.js +12 -0
  13. package/dist-cjs/lib/control.js.map +2 -2
  14. package/dist-cjs/lib/debounce.js.map +2 -2
  15. package/dist-cjs/lib/error.js.map +2 -2
  16. package/dist-cjs/lib/file.js +76 -11
  17. package/dist-cjs/lib/file.js.map +2 -2
  18. package/dist-cjs/lib/function.js.map +2 -2
  19. package/dist-cjs/lib/hash.js.map +2 -2
  20. package/dist-cjs/lib/id.js.map +2 -2
  21. package/dist-cjs/lib/iterable.js.map +2 -2
  22. package/dist-cjs/lib/json-value.js.map +1 -1
  23. package/dist-cjs/lib/media/apng.js.map +2 -2
  24. package/dist-cjs/lib/media/avif.js.map +2 -2
  25. package/dist-cjs/lib/media/gif.js.map +2 -2
  26. package/dist-cjs/lib/media/media.js +130 -4
  27. package/dist-cjs/lib/media/media.js.map +2 -2
  28. package/dist-cjs/lib/media/png.js +141 -0
  29. package/dist-cjs/lib/media/png.js.map +2 -2
  30. package/dist-cjs/lib/media/webp.js +1 -0
  31. package/dist-cjs/lib/media/webp.js.map +2 -2
  32. package/dist-cjs/lib/network.js.map +2 -2
  33. package/dist-cjs/lib/number.js.map +2 -2
  34. package/dist-cjs/lib/object.js +1 -1
  35. package/dist-cjs/lib/object.js.map +2 -2
  36. package/dist-cjs/lib/perf.js.map +2 -2
  37. package/dist-cjs/lib/reordering.js.map +2 -2
  38. package/dist-cjs/lib/retry.js.map +2 -2
  39. package/dist-cjs/lib/sort.js.map +2 -2
  40. package/dist-cjs/lib/storage.js.map +2 -2
  41. package/dist-cjs/lib/stringEnum.js.map +2 -2
  42. package/dist-cjs/lib/throttle.js.map +2 -2
  43. package/dist-cjs/lib/timers.js +103 -4
  44. package/dist-cjs/lib/timers.js.map +2 -2
  45. package/dist-cjs/lib/types.js.map +1 -1
  46. package/dist-cjs/lib/url.js.map +2 -2
  47. package/dist-cjs/lib/value.js.map +2 -2
  48. package/dist-cjs/lib/version.js.map +2 -2
  49. package/dist-cjs/lib/warn.js.map +2 -2
  50. package/dist-esm/index.d.mts +1350 -80
  51. package/dist-esm/index.mjs +1 -1
  52. package/dist-esm/lib/ExecutionQueue.mjs +79 -0
  53. package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
  54. package/dist-esm/lib/PerformanceTracker.mjs +43 -0
  55. package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
  56. package/dist-esm/lib/array.mjs +3 -1
  57. package/dist-esm/lib/array.mjs.map +2 -2
  58. package/dist-esm/lib/bind.mjs.map +2 -2
  59. package/dist-esm/lib/cache.mjs +27 -5
  60. package/dist-esm/lib/cache.mjs.map +2 -2
  61. package/dist-esm/lib/control.mjs +12 -0
  62. package/dist-esm/lib/control.mjs.map +2 -2
  63. package/dist-esm/lib/debounce.mjs.map +2 -2
  64. package/dist-esm/lib/error.mjs.map +2 -2
  65. package/dist-esm/lib/file.mjs +76 -11
  66. package/dist-esm/lib/file.mjs.map +2 -2
  67. package/dist-esm/lib/function.mjs.map +2 -2
  68. package/dist-esm/lib/hash.mjs.map +2 -2
  69. package/dist-esm/lib/id.mjs.map +2 -2
  70. package/dist-esm/lib/iterable.mjs.map +2 -2
  71. package/dist-esm/lib/media/apng.mjs.map +2 -2
  72. package/dist-esm/lib/media/avif.mjs.map +2 -2
  73. package/dist-esm/lib/media/gif.mjs.map +2 -2
  74. package/dist-esm/lib/media/media.mjs +130 -4
  75. package/dist-esm/lib/media/media.mjs.map +2 -2
  76. package/dist-esm/lib/media/png.mjs +141 -0
  77. package/dist-esm/lib/media/png.mjs.map +2 -2
  78. package/dist-esm/lib/media/webp.mjs +1 -0
  79. package/dist-esm/lib/media/webp.mjs.map +2 -2
  80. package/dist-esm/lib/network.mjs.map +2 -2
  81. package/dist-esm/lib/number.mjs.map +2 -2
  82. package/dist-esm/lib/object.mjs.map +2 -2
  83. package/dist-esm/lib/perf.mjs.map +2 -2
  84. package/dist-esm/lib/reordering.mjs.map +2 -2
  85. package/dist-esm/lib/retry.mjs.map +2 -2
  86. package/dist-esm/lib/sort.mjs.map +2 -2
  87. package/dist-esm/lib/storage.mjs.map +2 -2
  88. package/dist-esm/lib/stringEnum.mjs.map +2 -2
  89. package/dist-esm/lib/throttle.mjs.map +2 -2
  90. package/dist-esm/lib/timers.mjs +103 -4
  91. package/dist-esm/lib/timers.mjs.map +2 -2
  92. package/dist-esm/lib/url.mjs.map +2 -2
  93. package/dist-esm/lib/value.mjs.map +2 -2
  94. package/dist-esm/lib/version.mjs.map +2 -2
  95. package/dist-esm/lib/warn.mjs.map +2 -2
  96. package/package.json +1 -1
  97. package/src/lib/ExecutionQueue.test.ts +162 -20
  98. package/src/lib/ExecutionQueue.ts +110 -1
  99. package/src/lib/PerformanceTracker.test.ts +124 -0
  100. package/src/lib/PerformanceTracker.ts +63 -1
  101. package/src/lib/array.test.ts +263 -1
  102. package/src/lib/array.ts +183 -14
  103. package/src/lib/bind.test.ts +47 -0
  104. package/src/lib/bind.ts +69 -4
  105. package/src/lib/cache.test.ts +73 -0
  106. package/src/lib/cache.ts +47 -6
  107. package/src/lib/control.test.ts +50 -0
  108. package/src/lib/control.ts +198 -9
  109. package/src/lib/debounce.ts +28 -3
  110. package/src/lib/error.test.ts +60 -0
  111. package/src/lib/error.ts +27 -1
  112. package/src/lib/file.test.ts +49 -0
  113. package/src/lib/file.ts +117 -12
  114. package/src/lib/function.ts +11 -0
  115. package/src/lib/hash.test.ts +99 -0
  116. package/src/lib/hash.ts +69 -2
  117. package/src/lib/id.test.ts +32 -0
  118. package/src/lib/id.ts +53 -5
  119. package/src/lib/iterable.test.ts +25 -0
  120. package/src/lib/iterable.ts +4 -5
  121. package/src/lib/json-value.ts +71 -4
  122. package/src/lib/media/apng.test.ts +67 -0
  123. package/src/lib/media/apng.ts +38 -21
  124. package/src/lib/media/avif.test.ts +26 -0
  125. package/src/lib/media/avif.ts +34 -0
  126. package/src/lib/media/gif.test.ts +52 -0
  127. package/src/lib/media/gif.ts +25 -2
  128. package/src/lib/media/media.test.ts +58 -0
  129. package/src/lib/media/media.ts +220 -11
  130. package/src/lib/media/png.ts +162 -1
  131. package/src/lib/media/webp.test.ts +81 -0
  132. package/src/lib/media/webp.ts +33 -1
  133. package/src/lib/network.test.ts +38 -0
  134. package/src/lib/network.ts +6 -0
  135. package/src/lib/number.test.ts +74 -0
  136. package/src/lib/number.ts +29 -5
  137. package/src/lib/object.test.ts +236 -0
  138. package/src/lib/object.ts +194 -14
  139. package/src/lib/perf.ts +75 -3
  140. package/src/lib/reordering.test.ts +168 -0
  141. package/src/lib/reordering.ts +62 -4
  142. package/src/lib/retry.test.ts +77 -0
  143. package/src/lib/retry.ts +47 -1
  144. package/src/lib/sort.test.ts +36 -0
  145. package/src/lib/sort.ts +22 -1
  146. package/src/lib/storage.test.ts +130 -0
  147. package/src/lib/storage.tsx +54 -8
  148. package/src/lib/stringEnum.ts +20 -1
  149. package/src/lib/throttle.ts +46 -8
  150. package/src/lib/timers.test.ts +75 -0
  151. package/src/lib/timers.ts +124 -5
  152. package/src/lib/types.ts +126 -4
  153. package/src/lib/url.test.ts +44 -0
  154. package/src/lib/url.ts +40 -1
  155. package/src/lib/value.test.ts +102 -0
  156. package/src/lib/value.ts +67 -3
  157. package/src/lib/version.test.ts +494 -56
  158. package/src/lib/version.ts +36 -1
  159. package/src/lib/warn.test.ts +64 -0
  160. package/src/lib/warn.ts +43 -2
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isWebpAnimated } from './webp'
3
+
4
+ // Helper function to create a buffer with WebP signature
5
+ function createWebpBuffer(size: number, animated = false): ArrayBuffer {
6
+ const buffer = new ArrayBuffer(Math.max(size, 21))
7
+ const view = new Uint8Array(buffer)
8
+
9
+ // Set RIFF signature (bytes 0-3: 'RIFF')
10
+ view[0] = 0x52 // R
11
+ view[1] = 0x49 // I
12
+ view[2] = 0x46 // F
13
+ view[3] = 0x46 // F
14
+
15
+ // Set file size (bytes 4-7) - little endian
16
+ const fileSize = size - 8
17
+ view[4] = fileSize & 0xff
18
+ view[5] = (fileSize >> 8) & 0xff
19
+ view[6] = (fileSize >> 16) & 0xff
20
+ view[7] = (fileSize >> 24) & 0xff
21
+
22
+ // Set WebP signature (bytes 8-11: 'WEBP')
23
+ view[8] = 87 // W
24
+ view[9] = 69 // E
25
+ view[10] = 66 // B
26
+ view[11] = 80 // P
27
+
28
+ // Set VP8X chunk header for extended format (bytes 12-19)
29
+ view[12] = 86 // V
30
+ view[13] = 80 // P
31
+ view[14] = 56 // 8
32
+ view[15] = 88 // X
33
+
34
+ // Set VP8X chunk size (bytes 16-19) - 10 bytes
35
+ view[16] = 10
36
+ view[17] = 0
37
+ view[18] = 0
38
+ view[19] = 0
39
+
40
+ // Set VP8X flags (byte 20) - bit 1 is animation flag
41
+ if (animated) {
42
+ view[20] = 0b00000010 // Animation flag set
43
+ } else {
44
+ view[20] = 0b00000000 // No animation flag
45
+ }
46
+
47
+ return buffer
48
+ }
49
+
50
+ describe('isWebpAnimated', () => {
51
+ it('should return false for non-WebP data', () => {
52
+ const buffer = new ArrayBuffer(25)
53
+ const view = new Uint8Array(buffer)
54
+ view[8] = 80 // Not WebP signature
55
+ view[9] = 78
56
+ view[10] = 71
57
+ view[11] = 0
58
+
59
+ expect(isWebpAnimated(buffer)).toBe(false)
60
+ })
61
+
62
+ it('should return false for buffer too small for animation check', () => {
63
+ const buffer = new ArrayBuffer(10)
64
+ expect(isWebpAnimated(buffer)).toBe(false)
65
+ })
66
+
67
+ it('should return false for static WebP', () => {
68
+ const buffer = createWebpBuffer(100, false)
69
+ expect(isWebpAnimated(buffer)).toBe(false)
70
+ })
71
+
72
+ it('should return true for animated WebP', () => {
73
+ const buffer = createWebpBuffer(100, true)
74
+ expect(isWebpAnimated(buffer)).toBe(true)
75
+ })
76
+
77
+ it('should return false for empty buffer', () => {
78
+ const buffer = new ArrayBuffer(0)
79
+ expect(isWebpAnimated(buffer)).toBe(false)
80
+ })
81
+ })
@@ -2,7 +2,24 @@
2
2
  * MIT License: https://github.com/sindresorhus/is-webp/blob/main/license
3
3
  * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
4
4
  */
5
- function isWebp(view: Uint8Array) {
5
+
6
+ /**
7
+ * Determines whether a byte array represents a WebP image by checking the WebP file signature.
8
+ *
9
+ * @param view - The Uint8Array containing the potential WebP image data
10
+ * @returns True if the byte array is a valid WebP image, false otherwise
11
+ * @example
12
+ * ```ts
13
+ * // Check if file data is WebP format
14
+ * const file = new File([...], 'image.webp', { type: 'image/webp' })
15
+ * const buffer = await file.arrayBuffer()
16
+ * const view = new Uint8Array(buffer)
17
+ * const isWebPImage = isWebp(view)
18
+ * console.log(isWebPImage ? 'Valid WebP' : 'Not WebP')
19
+ * ```
20
+ * @internal
21
+ */
22
+ export function isWebp(view: Uint8Array) {
6
23
  if (!view || view.length < 12) {
7
24
  return false
8
25
  }
@@ -10,6 +27,21 @@ function isWebp(view: Uint8Array) {
10
27
  return view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80
11
28
  }
12
29
 
30
+ /**
31
+ * Determines whether a WebP image file contains animation data by checking the animation flag in the WebP VP8X chunk.
32
+ *
33
+ * @param buffer - The ArrayBuffer containing the WebP image data
34
+ * @returns True if the WebP image is animated, false otherwise
35
+ * @example
36
+ * ```ts
37
+ * // Check if a WebP file from user input is animated
38
+ * const file = new File([...], 'image.webp', { type: 'image/webp' })
39
+ * const buffer = await file.arrayBuffer()
40
+ * const animated = isWebpAnimated(buffer)
41
+ * console.log(animated ? 'Animated WebP' : 'Static WebP')
42
+ * ```
43
+ * @public
44
+ */
13
45
  export function isWebpAnimated(buffer: ArrayBuffer) {
14
46
  const view = new Uint8Array(buffer)
15
47
 
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { fetch, Image } from './network'
3
+
4
+ describe('fetch', () => {
5
+ it('should call window.fetch with referrerPolicy set to strict-origin-when-cross-origin', async () => {
6
+ const mockFetch = vi.spyOn(window, 'fetch').mockResolvedValue(new Response('test'))
7
+
8
+ await fetch('https://example.com')
9
+
10
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com', {
11
+ referrerPolicy: 'strict-origin-when-cross-origin',
12
+ })
13
+ mockFetch.mockRestore()
14
+ })
15
+
16
+ it('should override referrerPolicy if provided in init options', async () => {
17
+ const mockFetch = vi.spyOn(window, 'fetch').mockResolvedValue(new Response('test'))
18
+
19
+ const initOptions: RequestInit = {
20
+ referrerPolicy: 'no-referrer',
21
+ }
22
+
23
+ await fetch('https://example.com', initOptions)
24
+
25
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com', {
26
+ referrerPolicy: 'no-referrer',
27
+ })
28
+ mockFetch.mockRestore()
29
+ })
30
+ })
31
+
32
+ describe('Image', () => {
33
+ it('should create an image with referrerPolicy set to strict-origin-when-cross-origin', () => {
34
+ const img = Image()
35
+
36
+ expect(img.referrerPolicy).toBe('strict-origin-when-cross-origin')
37
+ })
38
+ })
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.
3
3
  *
4
+ * @param input - A Request object or string containing the URL to fetch
5
+ * @param init - Optional request initialization options
6
+ * @returns Promise that resolves to the Response object
4
7
  * @internal
5
8
  */
6
9
  export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
@@ -16,6 +19,9 @@ export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promi
16
19
  * Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file
17
20
  * but the main concern here is the referrerPolicy and setting it correctly.
18
21
  *
22
+ * @param width - Optional width for the image element
23
+ * @param height - Optional height for the image element
24
+ * @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'
19
25
  * @internal
20
26
  */
21
27
  export const Image = (width?: number, height?: number) => {
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { invLerp, lerp, modulate, rng } from './number'
3
+
4
+ describe('lerp', () => {
5
+ test('should interpolate between two values correctly', () => {
6
+ expect(lerp(0, 100, 0.5)).toBe(50)
7
+ expect(lerp(10, 20, 0.25)).toBe(12.5)
8
+ expect(lerp(-10, -5, 0.5)).toBe(-7.5)
9
+ })
10
+
11
+ test('should extrapolate when t is outside 0-1 range', () => {
12
+ expect(lerp(0, 10, 1.5)).toBe(15)
13
+ expect(lerp(0, 10, -0.5)).toBe(-5)
14
+ })
15
+ })
16
+
17
+ describe('invLerp', () => {
18
+ test('should find normalized position correctly', () => {
19
+ expect(invLerp(0, 100, 25)).toBe(0.25)
20
+ expect(invLerp(10, 20, 15)).toBe(0.5)
21
+ expect(invLerp(-10, -5, -7.5)).toBe(0.5)
22
+ })
23
+
24
+ test('should handle values outside the range', () => {
25
+ expect(invLerp(0, 10, 15)).toBe(1.5)
26
+ expect(invLerp(0, 10, -5)).toBe(-0.5)
27
+ })
28
+ })
29
+
30
+ describe('rng', () => {
31
+ test('should generate deterministic results for same seed', () => {
32
+ const random1 = rng('same-seed')
33
+ const random2 = rng('same-seed')
34
+
35
+ const values1 = [random1(), random1(), random1()]
36
+ const values2 = [random2(), random2(), random2()]
37
+
38
+ expect(values1).toEqual(values2)
39
+ })
40
+
41
+ test('should generate different results for different seeds', () => {
42
+ const random1 = rng('seed-1')
43
+ const random2 = rng('seed-2')
44
+
45
+ const values1 = [random1(), random1(), random1()]
46
+ const values2 = [random2(), random2(), random2()]
47
+
48
+ expect(values1).not.toEqual(values2)
49
+ })
50
+ })
51
+
52
+ describe('modulate', () => {
53
+ test('should map value between ranges correctly', () => {
54
+ expect(modulate(0, [0, 1], [0, 100])).toBe(0)
55
+ expect(modulate(0.5, [0, 1], [0, 100])).toBe(50)
56
+ expect(modulate(1, [0, 1], [0, 100])).toBe(100)
57
+ expect(modulate(30, [10, 50], [100, 200])).toBe(150)
58
+ })
59
+
60
+ test('should extrapolate without clamping by default', () => {
61
+ expect(modulate(2, [0, 1], [0, 100])).toBe(200)
62
+ expect(modulate(-1, [0, 1], [0, 100])).toBe(-100)
63
+ })
64
+
65
+ test('should clamp results when clamp is true', () => {
66
+ expect(modulate(2, [0, 1], [0, 100], true)).toBe(100)
67
+ expect(modulate(-1, [0, 1], [0, 100], true)).toBe(0)
68
+ })
69
+
70
+ test('should handle clamping with reversed target range', () => {
71
+ expect(modulate(2, [0, 1], [100, 0], true)).toBe(0)
72
+ expect(modulate(-1, [0, 1], [100, 0], true)).toBe(100)
73
+ })
74
+ })
package/src/lib/number.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Linear interpolate between two values.
3
3
  *
4
+ * @param a - The start value
5
+ * @param b - The end value
6
+ * @param t - The interpolation factor (0-1)
7
+ * @returns The interpolated value
4
8
  * @example
5
- *
6
9
  * ```ts
7
- * const A = lerp(0, 1, 0.5)
10
+ * const halfway = lerp(0, 100, 0.5) // 50
11
+ * const quarter = lerp(10, 20, 0.25) // 12.5
8
12
  * ```
9
- *
10
13
  * @public
11
14
  */
12
15
  export function lerp(a: number, b: number, t: number) {
@@ -14,9 +17,18 @@ export function lerp(a: number, b: number, t: number) {
14
17
  }
15
18
 
16
19
  /**
17
- * Inverse lerp between two values. Given a value `n` in the range [a, b], returns a number between
20
+ * Inverse lerp between two values. Given a value `t` in the range [a, b], returns a number between
18
21
  * 0 and 1.
19
22
  *
23
+ * @param a - The start value of the range
24
+ * @param b - The end value of the range
25
+ * @param t - The value within the range [a, b]
26
+ * @returns The normalized position (0-1) of t within the range [a, b]
27
+ * @example
28
+ * ```ts
29
+ * const position = invLerp(0, 100, 25) // 0.25
30
+ * const normalized = invLerp(10, 20, 15) // 0.5
31
+ * ```
20
32
  * @public
21
33
  */
22
34
  export function invLerp(a: number, b: number, t: number) {
@@ -25,10 +37,22 @@ export function invLerp(a: number, b: number, t: number) {
25
37
 
26
38
  /**
27
39
  * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift). The
28
- * result will always be betweeen -1 and 1.
40
+ * result will always be between -1 and 1.
29
41
  *
30
42
  * Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
31
43
  *
44
+ * @param seed - The seed string for deterministic random generation (defaults to empty string)
45
+ * @returns A function that will return a random number between -1 and 1 each time it is called
46
+ * @example
47
+ * ```ts
48
+ * const random = rng('my-seed')
49
+ * const num1 = random() // Always the same for this seed
50
+ * const num2 = random() // Next number in sequence
51
+ *
52
+ * // Different seed produces different sequence
53
+ * const otherRandom = rng('other-seed')
54
+ * const different = otherRandom() // Different value
55
+ * ```
32
56
  * @public
33
57
  */
34
58
  export function rng(seed = '') {
@@ -0,0 +1,236 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ areObjectsShallowEqual,
4
+ filterEntries,
5
+ getChangedKeys,
6
+ groupBy,
7
+ hasOwnProperty,
8
+ isEqualAllowingForFloatingPointErrors,
9
+ mapObjectMapValues,
10
+ objectMapEntries,
11
+ objectMapEntriesIterable,
12
+ objectMapFromEntries,
13
+ objectMapKeys,
14
+ objectMapValues,
15
+ omit,
16
+ } from './object'
17
+
18
+ describe('hasOwnProperty', () => {
19
+ it('should work with objects that override hasOwnProperty', () => {
20
+ const obj = {
21
+ name: 'Alice',
22
+ hasOwnProperty: () => false,
23
+ }
24
+ expect(hasOwnProperty(obj, 'name')).toBe(true)
25
+ })
26
+ })
27
+
28
+ describe('objectMapKeys', () => {
29
+ it('should return typed keys preserving order', () => {
30
+ const obj = { z: 1, a: 2, m: 3 }
31
+ const keys = objectMapKeys(obj)
32
+ expect(keys).toEqual(['z', 'a', 'm'])
33
+ })
34
+ })
35
+
36
+ describe('objectMapValues', () => {
37
+ it('should return typed values preserving order', () => {
38
+ const obj = { first: 'a', second: 'b', third: 'c' }
39
+ const values = objectMapValues(obj)
40
+ expect(values).toEqual(['a', 'b', 'c'])
41
+ })
42
+ })
43
+
44
+ describe('objectMapEntries', () => {
45
+ it('should return typed entries preserving order', () => {
46
+ const obj = { z: 1, a: 2 }
47
+ const entries = objectMapEntries(obj)
48
+ expect(entries).toEqual([
49
+ ['z', 1],
50
+ ['a', 2],
51
+ ])
52
+ })
53
+ })
54
+
55
+ describe('objectMapEntriesIterable', () => {
56
+ it('should skip inherited properties and work as iterator', () => {
57
+ const parent = { inherited: 'value' }
58
+ const child = Object.create(parent)
59
+ child.own = 'ownValue'
60
+
61
+ const entries = Array.from(objectMapEntriesIterable(child))
62
+ expect(entries).toEqual([['own', 'ownValue']])
63
+ })
64
+
65
+ it('should work in for...of loops', () => {
66
+ const obj = { x: 10, y: 20 }
67
+ const collected: Array<[string, number]> = []
68
+
69
+ for (const [key, value] of objectMapEntriesIterable(obj)) {
70
+ collected.push([key, value])
71
+ }
72
+
73
+ expect(collected).toEqual([
74
+ ['x', 10],
75
+ ['y', 20],
76
+ ])
77
+ })
78
+ })
79
+
80
+ describe('objectMapFromEntries', () => {
81
+ it('should create typed object from entries', () => {
82
+ const entries: Array<['name' | 'age', string | number]> = [
83
+ ['name', 'Alice'],
84
+ ['age', 30],
85
+ ]
86
+ const obj = objectMapFromEntries(entries)
87
+ expect(obj).toEqual({ name: 'Alice', age: 30 })
88
+ })
89
+
90
+ it('should handle duplicate keys (last wins)', () => {
91
+ const entries: Array<['key', string]> = [
92
+ ['key', 'first'],
93
+ ['key', 'second'],
94
+ ]
95
+ const obj = objectMapFromEntries(entries)
96
+ expect(obj).toEqual({ key: 'second' })
97
+ })
98
+ })
99
+
100
+ describe('filterEntries', () => {
101
+ it('should filter entries and optimize unchanged objects', () => {
102
+ const scores = { alice: 85, bob: 92, charlie: 78 }
103
+ const passing = filterEntries(scores, (name, score) => score >= 80)
104
+ expect(passing).toEqual({ alice: 85, bob: 92 })
105
+
106
+ // Optimization: return same reference when no changes
107
+ const unchanged = filterEntries(scores, () => true)
108
+ expect(unchanged).toBe(scores)
109
+ })
110
+
111
+ it('should pass key and value to predicate', () => {
112
+ const obj = { prefix_a: 1, other_b: 2, prefix_c: 3 }
113
+ const result = filterEntries(obj, (key, value) => key.startsWith('prefix_') && value > 1)
114
+ expect(result).toEqual({ prefix_c: 3 })
115
+ })
116
+ })
117
+
118
+ describe('mapObjectMapValues', () => {
119
+ it('should map values with key and value access', () => {
120
+ const obj = { a: 1, b: 2, c: 3 }
121
+ const result = mapObjectMapValues(obj, (key, value) => `${key}:${value}`)
122
+ expect(result).toEqual({ a: 'a:1', b: 'b:2', c: 'c:3' })
123
+ })
124
+
125
+ it('should skip inherited properties', () => {
126
+ const parent = { inherited: 'value' }
127
+ const child = Object.create(parent)
128
+ child.own = 'ownValue'
129
+
130
+ const result = mapObjectMapValues(child, (k, v) => (v as string).toUpperCase())
131
+ expect(result).toEqual({ own: 'OWNVALUE' })
132
+ })
133
+ })
134
+
135
+ describe('areObjectsShallowEqual', () => {
136
+ it('should compare shallow equality correctly', () => {
137
+ const a = { x: 1, y: 2 }
138
+ const b = { x: 1, y: 2 }
139
+ const c = { x: 1, y: 3 }
140
+ const d = { x: 1, z: 2 }
141
+
142
+ expect(areObjectsShallowEqual(a, a)).toBe(true) // Same reference
143
+ expect(areObjectsShallowEqual(a, b)).toBe(true) // Same values
144
+ expect(areObjectsShallowEqual(a, c)).toBe(false) // Different values
145
+ expect(areObjectsShallowEqual(a, d as any)).toBe(false) // Different keys
146
+ })
147
+
148
+ it('should use Object.is for value comparison', () => {
149
+ expect(areObjectsShallowEqual({ x: NaN }, { x: NaN })).toBe(true)
150
+ expect(areObjectsShallowEqual({ x: -0 }, { x: +0 })).toBe(false)
151
+ })
152
+ })
153
+
154
+ describe('groupBy', () => {
155
+ it('should group items by key selector function', () => {
156
+ const people = [
157
+ { name: 'Alice', age: 25 },
158
+ { name: 'Bob', age: 30 },
159
+ { name: 'Charlie', age: 25 },
160
+ ]
161
+ const byAge = groupBy(people, (person) => `age-${person.age}`)
162
+ expect(byAge).toEqual({
163
+ 'age-25': [
164
+ { name: 'Alice', age: 25 },
165
+ { name: 'Charlie', age: 25 },
166
+ ],
167
+ 'age-30': [{ name: 'Bob', age: 30 }],
168
+ })
169
+ })
170
+
171
+ it('should accumulate duplicate keys', () => {
172
+ const items = ['apple', 'apricot', 'banana']
173
+ const result = groupBy(items, (item) => item.charAt(0))
174
+ expect(result).toEqual({ a: ['apple', 'apricot'], b: ['banana'] })
175
+ })
176
+ })
177
+
178
+ describe('omit', () => {
179
+ it('should create new object without specified keys', () => {
180
+ const user = { id: '123', name: 'Alice', password: 'secret', email: 'alice@example.com' }
181
+ const publicUser = omit(user, ['password'])
182
+ expect(publicUser).toEqual({ id: '123', name: 'Alice', email: 'alice@example.com' })
183
+ expect(publicUser).not.toBe(user)
184
+ })
185
+
186
+ it('should handle multiple keys and non-existent keys', () => {
187
+ const obj = { a: 1, b: 2, c: 3 }
188
+ const result = omit(obj, ['b', 'nonexistent'])
189
+ expect(result).toEqual({ a: 1, c: 3 })
190
+ })
191
+ })
192
+
193
+ describe('getChangedKeys', () => {
194
+ it('should identify changed keys using Object.is', () => {
195
+ const before = { name: 'Alice', age: 25, city: 'NYC' }
196
+ const after = { name: 'Alice', age: 26, city: 'NYC' }
197
+ expect(getChangedKeys(before, after)).toEqual(['age'])
198
+ })
199
+
200
+ it('should use Object.is comparison (NaN and zero handling)', () => {
201
+ const before = { nan: NaN, zero: -0, normal: 1 }
202
+ const after = { nan: NaN, zero: +0, normal: 2 }
203
+ expect(getChangedKeys(before, after)).toEqual(['zero', 'normal'])
204
+ })
205
+
206
+ it('should only check keys from first object', () => {
207
+ const before = { a: 1, b: 2 }
208
+ const after = { a: 1, b: 2, c: 3 }
209
+ expect(getChangedKeys(before, after)).toEqual([])
210
+ })
211
+ })
212
+
213
+ describe('isEqualAllowingForFloatingPointErrors', () => {
214
+ it('should handle floating point precision errors', () => {
215
+ const a = { x: 0.1 + 0.2 } // 0.30000000000000004
216
+ const b = { x: 0.3 }
217
+ expect(isEqualAllowingForFloatingPointErrors(a, b)).toBe(true)
218
+ })
219
+
220
+ it('should work with custom threshold and reject large differences', () => {
221
+ const a = { x: 0.1 }
222
+ const b = { x: 0.15 }
223
+ expect(isEqualAllowingForFloatingPointErrors(a, b, 0.1)).toBe(true)
224
+ expect(isEqualAllowingForFloatingPointErrors(a, b, 0.01)).toBe(false)
225
+ expect(isEqualAllowingForFloatingPointErrors({ x: 0.1 }, { x: 0.2 })).toBe(false)
226
+ })
227
+
228
+ it('should handle deep objects and arrays with mixed types', () => {
229
+ const a = { user: 'Alice', score: 0.1 + 0.2, values: [1.0000001, 'test'] }
230
+ const b = { user: 'Alice', score: 0.3, values: [1.0000002, 'test'] }
231
+ expect(isEqualAllowingForFloatingPointErrors(a, b)).toBe(true)
232
+
233
+ // Different structure should fail
234
+ expect(isEqualAllowingForFloatingPointErrors({ x: 0.3 }, { y: 0.3 })).toBe(false)
235
+ })
236
+ })