@inglorious/utils 5.1.0 → 5.2.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/README.md CHANGED
@@ -16,7 +16,7 @@ This package provides a collection of pure, composable helper functions to strea
16
16
  ## Installation
17
17
 
18
18
  ```bash
19
- npm install @inglorious/utils
19
+ pnpm add @inglorious/utils
20
20
  ```
21
21
 
22
22
  ---
@@ -118,6 +118,21 @@ Vectors are mutable arrays, so copy a shared vector rather than storing it where
118
118
  to one, such as an entity position. Copy it with `v(...ZERO_VECTOR)`: a plain spread
119
119
  (`[...ZERO_VECTOR]`) gives you a new array but drops the tag that `isVector` looks for.
120
120
 
121
+ ### Saving and loading JSON
122
+
123
+ Keep a value in `localStorage` between visits, such as a game's high scores or an app's
124
+ saved state. Neither function throws: absent or blocked storage, and a value that cannot be
125
+ parsed, read as the fallback, and a write that did not happen answers `false`.
126
+
127
+ ```javascript
128
+ import { readJSON, writeJSON } from "@inglorious/utils/storage.js"
129
+
130
+ const scores = readJSON("high-scores", [])
131
+ writeJSON("high-scores", [...scores, 1200]) // => true
132
+ ```
133
+
134
+ Pass another `Storage` as the last argument, such as `sessionStorage` or a fake in tests.
135
+
121
136
  ---
122
137
 
123
138
  ## API
@@ -139,6 +154,10 @@ know whether a helper lives under `data-structures` or `math`:
139
154
  - **`@inglorious/utils/quaternion`**: `quaternion` and `combine`.
140
155
  - **`@inglorious/utils/vector`**: Single-vector helpers (`magnitude`, `normalize`, `rotate`, ...) alongside multi-vector helpers (`cross`, `dot`, `sum`, ...).
141
156
 
157
+ One module stands on its own:
158
+
159
+ - **`@inglorious/utils/storage`**: `readJSON` and `writeJSON`, for keeping JSON in `localStorage`.
160
+
142
161
  `divide`, `mod`, `multiply` and `power` accept either a scalar or one or more vectors:
143
162
  a scalar is applied to every component, while vectors are combined component-wise.
144
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/utils",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "A set of general-purpose utility functions designed with functional programming principles in mind.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -118,6 +118,14 @@
118
118
  "types": "./types/math/vector.d.ts",
119
119
  "import": "./src/math/vector.js"
120
120
  },
121
+ "./storage.js": {
122
+ "types": "./types/storage.d.ts",
123
+ "import": "./src/storage.js"
124
+ },
125
+ "./storage": {
126
+ "types": "./types/storage.d.ts",
127
+ "import": "./src/storage.js"
128
+ },
121
129
  "./v.js": {
122
130
  "types": "./types/v.d.ts",
123
131
  "import": "./src/v.js"
@@ -137,13 +145,13 @@
137
145
  },
138
146
  "sideEffects": false,
139
147
  "devDependencies": {
140
- "@inglorious/eslint-config": "1.1.4",
148
+ "@inglorious/eslint-config": "1.2.0",
141
149
  "prettier": "^3.9.6",
142
150
  "vite": "^8.2.2",
143
151
  "vitest": "^5.0.0"
144
152
  },
145
153
  "engines": {
146
- "node": ">= 22"
154
+ "node": ">= 26"
147
155
  },
148
156
  "scripts": {
149
157
  "format": "prettier --write .",
@@ -7,7 +7,6 @@
7
7
  */
8
8
  const DEFAULT_COMPARATOR = (a, b) => b - a
9
9
 
10
- // Threshold constants for comparison results
11
10
  const LESS_THAN_ZERO = 0
12
11
  const GREATER_THAN_ZERO = 0
13
12
 
@@ -1,5 +1,3 @@
1
- // @see https://stackfull.dev/heaps-in-javascript
2
-
3
1
  /**
4
2
  * Default comparator for a max-heap.
5
3
  * @param {*} a - First element.
@@ -8,17 +6,16 @@
8
6
  */
9
7
  const DEFAULT_COMPARATOR = (a, b) => b - a
10
8
 
11
- // Constants for heap operations
12
- const ROOT_INDEX = 0 // Index of the root element in the heap.
13
- const SINGLE_ELEMENT = 1 // Heap size when it contains a single element.
14
- const LEFT_CHILD_MULTIPLIER = 2 // Multiplier to calculate the left child index.
15
- const RIGHT_CHILD_MULTIPLIER = 2 // Multiplier to calculate the right child index.
16
- const PARENT_DIVISOR = 2 // Divisor to calculate the parent index.
17
- const LEFT_CHILD_OFFSET = 1 // Offset to calculate the left child index.
18
- const RIGHT_CHILD_OFFSET = 2 // Offset to calculate the right child index.
19
- const LAST_ELEMENT_OFFSET = 1 // Offset to get the last element in the heap.
20
- const INDEX_ADJUSTMENT = 1 // Adjustment for zero-based indexing.
21
- const COMPARATOR_THRESHOLD = 0 // Threshold for comparator results.
9
+ const ROOT_INDEX = 0
10
+ const SINGLE_ELEMENT = 1
11
+ const LEFT_CHILD_MULTIPLIER = 2
12
+ const RIGHT_CHILD_MULTIPLIER = 2
13
+ const PARENT_DIVISOR = 2
14
+ const LEFT_CHILD_OFFSET = 1
15
+ const RIGHT_CHILD_OFFSET = 2
16
+ const LAST_ELEMENT_OFFSET = 1
17
+ const INDEX_ADJUSTMENT = 1
18
+ const COMPARATOR_THRESHOLD = 0
22
19
 
23
20
  /**
24
21
  * Checks if a heap contains a specific item.
@@ -28,8 +28,6 @@ const NEXT_LEVEL = 2
28
28
  * @returns {T} A deep clone of the input value.
29
29
  */
30
30
  export function clone(value) {
31
- // A vector holds numbers and nothing else, so it needs no copying below the top -- and
32
- // building it in one go is noticeably cheaper than copying it and tagging the copy.
33
31
  if (value?.__isVector__) {
34
32
  return v(...value)
35
33
  }
@@ -244,9 +242,6 @@ export function get(obj, path, defaultValue = undefined) {
244
242
  export function isDeepEqual(left, right, seen = new WeakMap()) {
245
243
  if (Object.is(left, right)) return true
246
244
 
247
- // `Object.is` has already said no to these two being the same object, and neither can be
248
- // walked: a function is equal to itself and nothing else, and a key present in one and
249
- // not the other is a difference even when both read as undefined.
250
245
  if (typeof left !== "object" || typeof right !== "object") return false
251
246
  if (left === null || right === null) return false
252
247
 
@@ -372,7 +367,6 @@ export function produce(baseState, recipe, ...args) {
372
367
  */
373
368
  export function serialize(obj) {
374
369
  function replacer(key, value) {
375
- // Handle top-stage vector
376
370
  if (value?.__isVector__) {
377
371
  return {
378
372
  _type: "vector",
@@ -429,9 +423,7 @@ export function set(obj, path, value) {
429
423
  const key = keys[i]
430
424
  const nextKey = keys[i + NEXT_KEY_OFFSET]
431
425
 
432
- // Create missing intermediate objects
433
426
  if (!(key in current) || typeof current[key] !== "object") {
434
- // Create array if next key is a number, object otherwise
435
427
  current[key] = /^\d+$/.test(nextKey) ? [] : {}
436
428
  }
437
429
 
@@ -12,8 +12,6 @@ export function toCamelCase(input) {
12
12
  return [firstChar.toLowerCase(), ...rest].join("")
13
13
  }
14
14
 
15
- // How long a row or column is by one, which is the whole of what the offsets below mean.
16
- // The grid's own edges are counted by the same one.
17
15
  const ONE = 1
18
16
  const NONE = 0
19
17
  const FIRST = 0
@@ -56,9 +54,6 @@ export function editDistance(a, b) {
56
54
  return grid[a.length][b.length]
57
55
  }
58
56
 
59
- // How far a string may be from the one asked about and still be worth suggesting. A
60
- // mistyped name is nearly always one or two edits away from the right one; anything
61
- // further off is a different string rather than a wrong one.
62
57
  const NEAR_ENOUGH = 3
63
58
 
64
59
  /**
@@ -1,5 +1,3 @@
1
- // @see https://jonathanwhiting.com/tutorial/collision/
2
-
3
1
  import { isBetween } from "./number.js"
4
2
  import { intersectsRectangle } from "./rectangle.js"
5
3
 
package/src/math/line.js CHANGED
@@ -20,7 +20,6 @@ export function distanceFromPoint(line, point) {
20
20
  return abs(a * x + b * z + c) / hypothenuse(a, b)
21
21
  }
22
22
 
23
- // @see https://math.stackexchange.com/questions/275529/check-if-line-intersects-with-circles-perimeter
24
23
  /**
25
24
  * Determines whether a line intersects with the perimeter of a circle.
26
25
  * @param {Line} line - The line represented by the equation ax + bz + c = 0, where `line` is [a, b, c].
@@ -1,6 +1,6 @@
1
- const DEFAULT_PRECISION = 1 // Default precision used for snapping numbers.
2
- const DEFAULT_TOLERANCE = 0.1 // Default tolerance used for comparing numbers.
3
- const SQUARE_ROOT = 0.5 // Exponent used for calculating square roots.
1
+ const DEFAULT_PRECISION = 1
2
+ const DEFAULT_TOLERANCE = 0.1
3
+ const SQUARE_ROOT = 0.5
4
4
 
5
5
  /**
6
6
  * Returns the absolute value of a number.
@@ -8,9 +8,9 @@ import { cos, sin } from "./trigonometry.js"
8
8
  import { cross, dot, multiply, sum } from "./vector.js"
9
9
 
10
10
  const DEFAULT_ANGLE = 0
11
- const HALF = 2 // Constant representing the divisor for halving an angle.
11
+ const HALF = 2
12
12
  // eslint-disable-next-line no-magic-numbers
13
- const Y_AXIS = v(0, 1, 0) // Default axis of rotation (Y-axis).
13
+ const Y_AXIS = v(0, 1, 0)
14
14
 
15
15
  /**
16
16
  * Combines two quaternions using the Hamilton product.
@@ -74,7 +74,6 @@ export function intersectsRectangle(rectangle1, rectangle2) {
74
74
  const back2 = z2 - halfD2
75
75
  const front2 = z2 + halfD2
76
76
 
77
- // AABB intersection test
78
77
  return (
79
78
  left1 <= right2 &&
80
79
  right1 >= left2 &&
package/src/math/rng.js CHANGED
@@ -31,10 +31,6 @@ export function random(...args) {
31
31
  return Math.random()
32
32
  }
33
33
 
34
- // A range with a fractional bound cannot be stepped through as integers, so it
35
- // is read as a continuous range. Requiring both bounds to be floats used to
36
- // send `random(0.5, 1)` down the integer path, where it could only ever answer
37
- // 0 or 1.
38
34
  if (
39
35
  args.length > 1 &&
40
36
  (!Number.isInteger(args[0]) || !Number.isInteger(args[1]))
@@ -1,7 +1,7 @@
1
1
  import { mod } from "./number.js"
2
2
 
3
- const HALF_CIRCLE_IN_DEGRESS = 180 // Half-circle in degrees (180°)
4
- const FULL_CIRCLE = 2 // Full circle multiplier for radians (2π)
3
+ const HALF_CIRCLE_IN_DEGRESS = 180
4
+ const FULL_CIRCLE = 2
5
5
 
6
6
  /**
7
7
  * Calculates the arctangent of the quotient of its arguments.
@@ -499,9 +499,7 @@ export function toSpherical(vector) {
499
499
  return zero()
500
500
  }
501
501
 
502
- // In a Y-up system, inclination is the angle with the Y axis.
503
502
  const inclination = Math.acos(vector[Y] / r)
504
- // Azimuth is the angle in the XZ plane, which `angle()` calculates.
505
503
  const azimuth = angle(vector)
506
504
 
507
505
  return v(r, inclination, azimuth)
@@ -666,7 +664,6 @@ function rotateWithQuaternion(vector, angle) {
666
664
  return vector
667
665
  }
668
666
 
669
- // @see https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Performance_comparisons
670
667
  const [w, ...r] = quaternion(angle)
671
668
  const result = sum(
672
669
  vector,
@@ -5,9 +5,9 @@
5
5
  import { clamp, multiply, sum, zero } from "../math/vector.js"
6
6
  import { applyFriction } from "./friction.js"
7
7
 
8
- const DEFAULT_DT = 1 // Default time delta for the applyAcceleration function.
9
- const NO_FRICTION = 0 // No friction constant.
10
- const HALF_ACCELERATION = 0.5 // Half of the acceleration factor used in position calculation.
8
+ const DEFAULT_DT = 1
9
+ const NO_FRICTION = 0
10
+ const HALF_ACCELERATION = 0.5
11
11
 
12
12
  /**
13
13
  * Applies acceleration to an object using Euler's integration method.
@@ -36,15 +36,12 @@ export function applyAcceleration(params, dt = DEFAULT_DT) {
36
36
  friction = NO_FRICTION,
37
37
  } = params
38
38
 
39
- // Clamp acceleration to the maximum allowed range
40
39
  acceleration = clamp(acceleration, -maxAcceleration, maxAcceleration)
41
40
 
42
- // Update velocity with acceleration and clamp to the maximum allowed speed
43
41
  velocity = sum(velocity, multiply(acceleration, dt))
44
42
  velocity = clamp(velocity, -maxSpeed, maxSpeed)
45
43
  velocity = applyFriction({ velocity, friction }, dt)
46
44
 
47
- // Update position with velocity and acceleration
48
45
  position = sum(
49
46
  position,
50
47
  multiply(velocity, dt),
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { magnitude, setMagnitude, zero } from "../math/vector.js"
6
6
 
7
- const DEFAULT_DT = 1 // Default time delta for the applyFriction function.
8
- const NO_FRICTION = 0 // No friction constant.
7
+ const DEFAULT_DT = 1
8
+ const NO_FRICTION = 0
9
9
 
10
10
  /**
11
11
  * Applies friction to a given velocity vector.
@@ -6,11 +6,11 @@
6
6
 
7
7
  import { zero } from "../math/vector.js"
8
8
 
9
- const DEFAULT_DT = 1 // Default delta time for the applyGravity function.
10
- const DEFAULT_JUMP = 0 // Default maximum jump height.
11
- const DEFAULT_LEAP = 0 // Default maximum leap distance.
12
- const DEFAULT_SPEED = 0 // Default maximum speed.
13
- const NO_VELOCITY = 0 // No initial velocity.
9
+ const DEFAULT_DT = 1
10
+ const DEFAULT_JUMP = 0
11
+ const DEFAULT_LEAP = 0
12
+ const DEFAULT_SPEED = 0
13
+ const NO_VELOCITY = 0
14
14
 
15
15
  /**
16
16
  * Applies gravity to an object based on its current velocity and position.
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable no-magic-numbers */
2
2
 
3
- const DEFAULT_MAX_JUMP = 0 // Default maximum jump height.
4
- const DEFAULT_MAX_LEAP = 0 // Default maximum leap distance.
5
- const DEFAULT_MAX_SPEED = 0 // Default maximum speed.
3
+ const DEFAULT_MAX_JUMP = 0
4
+ const DEFAULT_MAX_LEAP = 0
5
+ const DEFAULT_MAX_SPEED = 0
6
6
 
7
7
  /**
8
8
  * Calculates the vertical velocity (vy) of a jump
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { clamp, multiply, sum, zero } from "../math/vector.js"
6
6
 
7
- const DEFAULT_DT = 1 // Default time delta for the applyVelocity function.
8
- const DEFAULT_MAX_SPEED = 0 // Default maximum speed for velocity clamping.
7
+ const DEFAULT_DT = 1
8
+ const DEFAULT_MAX_SPEED = 0
9
9
 
10
10
  /**
11
11
  * Applies velocity to a position based on the given parameters and options.
package/src/storage.js ADDED
@@ -0,0 +1,52 @@
1
+ const DEFAULT_STORAGE = () => globalThis.localStorage
2
+
3
+ /**
4
+ * Reads a value back as JSON.
5
+ *
6
+ * Storage that is absent -- a server render, a private window, a test -- reads as the
7
+ * fallback rather than throwing, so a caller does not have to know whether it is running
8
+ * somewhere that has any. A value that cannot be parsed is treated the same way: what is
9
+ * stored is not this function's to defend, and refusing to start over it would be worse
10
+ * than starting again.
11
+ *
12
+ * @param {string} key - Where the value is kept.
13
+ * @param {*} fallback - What to answer when there is nothing usable to read.
14
+ * @param {Storage} [storage] - Where to read from. Defaults to the browser's own.
15
+ * @returns {*} The stored value, or the fallback.
16
+ */
17
+ export function readJSON(key, fallback, storage) {
18
+ try {
19
+ const raw = store(storage)?.getItem(key)
20
+ if (raw === null || raw === undefined) return fallback
21
+ return JSON.parse(raw)
22
+ } catch {
23
+ return fallback
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Writes a value down as JSON, and answers whether it took.
29
+ *
30
+ * Storage can be full, absent, or refusing writes for its own reasons, and none of those
31
+ * is worth throwing over: a game that cannot remember a score can still be played. The
32
+ * answer is returned so that a caller which does care -- a test, mostly -- can say so.
33
+ *
34
+ * @param {string} key - Where to keep the value.
35
+ * @param {*} value - The value to keep.
36
+ * @param {Storage} [storage] - Where to write to. Defaults to the browser's own.
37
+ * @returns {boolean} `true` if the value was written.
38
+ */
39
+ export function writeJSON(key, value, storage) {
40
+ try {
41
+ const target = store(storage)
42
+ if (!target) return false
43
+ target.setItem(key, JSON.stringify(value))
44
+ return true
45
+ } catch {
46
+ return false
47
+ }
48
+ }
49
+
50
+ function store(storage) {
51
+ return storage ?? DEFAULT_STORAGE()
52
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reads a value back as JSON. Absent storage, a missing key or a value that
3
+ * cannot be parsed all read as the fallback, rather than throwing.
4
+ *
5
+ * @param key - Where the value is kept.
6
+ * @param fallback - What to answer when there is nothing usable to read.
7
+ * @param storage - Where to read from. Defaults to the browser's own.
8
+ * @returns The stored value, or the fallback.
9
+ */
10
+ export function readJSON<T>(
11
+ key: string,
12
+ fallback: T,
13
+ storage?: Storage | null,
14
+ ): T
15
+
16
+ /**
17
+ * Writes a value down as JSON, and answers whether it took. Absent, full or
18
+ * refusing storage answers `false` rather than throwing.
19
+ *
20
+ * @param key - Where to keep the value.
21
+ * @param value - The value to keep.
22
+ * @param storage - Where to write to. Defaults to the browser's own.
23
+ * @returns `true` if the value was written.
24
+ */
25
+ export function writeJSON(
26
+ key: string,
27
+ value: unknown,
28
+ storage?: Storage | null,
29
+ ): boolean