@inglorious/utils 5.0.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.0.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.2",
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 .",
@@ -36,24 +36,6 @@ export const eucledianDistance = (a, b) =>
36
36
  */
37
37
  export const manhattanDistance = (a, b) => abs(subtract(a.position, b.position))
38
38
 
39
- /**
40
- * Compares the cost of two nodes.
41
- *
42
- * @param {Node} a - The first node.
43
- * @param {Node} b - The second node.
44
- * @returns {number} - The difference in cost between the two nodes.
45
- */
46
- const compareCost = (a, b) => b.cost - a.cost
47
-
48
- /**
49
- * Compares the total cost of two nodes.
50
- *
51
- * @param {Node} a - The first node.
52
- * @param {Node} b - The second node.
53
- * @returns {number} - The difference in total cost between the two nodes.
54
- */
55
- const compareTotalCost = (a, b) => b.totalCost - a.totalCost
56
-
57
39
  /**
58
40
  * Finds the shortest path in a graph from a start node to an end node using a heuristic.
59
41
  *
@@ -148,3 +130,25 @@ function adaptGraph(graph) {
148
130
  function createFindNode(nodes) {
149
131
  return (id) => nodes.find((node) => node.id === id)
150
132
  }
133
+
134
+ /**
135
+ * Compares the cost of two nodes.
136
+ *
137
+ * @param {Node} a - The first node.
138
+ * @param {Node} b - The second node.
139
+ * @returns {number} - The difference in cost between the two nodes.
140
+ */
141
+ function compareCost(a, b) {
142
+ return b.cost - a.cost
143
+ }
144
+
145
+ /**
146
+ * Compares the total cost of two nodes.
147
+ *
148
+ * @param {Node} a - The first node.
149
+ * @param {Node} b - The second node.
150
+ * @returns {number} - The difference in total cost between the two nodes.
151
+ */
152
+ function compareTotalCost(a, b) {
153
+ return b.totalCost - a.totalCost
154
+ }
@@ -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.
@@ -8,57 +8,57 @@ const NEXT_KEY_OFFSET = 1
8
8
  const NEXT_LEVEL = 2
9
9
 
10
10
  /**
11
- * Creates a deep clone of a given value using the structured clone algorithm.
12
- * This is a modern and efficient way to deep-clone objects.
11
+ * Creates a deep clone of a given value.
13
12
  *
14
- * Note: `structuredClone` has some limitations. It cannot clone functions,
15
- * DOM nodes, property descriptors, getters, setters, or certain other
16
- * non-serializable objects. It will throw a `DataCloneError` in such cases.
13
+ * Plain objects, arrays and primitives -- what state is nearly always made of -- are
14
+ * copied by hand, because a general algorithm pays for being one: across thousands of
15
+ * small objects, `structuredClone` takes about twice as long and a JSON round trip
16
+ * longer still. Vectors stay vectors, which neither of those keeps: the tag that marks
17
+ * one is not enumerable, so anything that copies only what it can see drops it.
17
18
  *
18
- * For more details, see:
19
- * https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
19
+ * Anything else is handed to `structuredClone`, so a `Date` or a `Map` is still copied
20
+ * properly, and DOM nodes or class instances with non-cloneable internals still throw a
21
+ * `DataCloneError`. Functions are kept as they are rather than copied, since there is
22
+ * nothing in one to copy.
20
23
  *
21
24
  * This is similar to Lodash's `_.cloneDeep`.
22
25
  *
23
26
  * @template T
24
- * @param {T} obj - The value to clone.
27
+ * @param {T} value - The value to clone.
25
28
  * @returns {T} A deep clone of the input value.
26
29
  */
27
- export function clone(obj) {
28
- return deserialize(serialize(obj))
29
- }
30
+ export function clone(value) {
31
+ if (value?.__isVector__) {
32
+ return v(...value)
33
+ }
30
34
 
31
- /**
32
- * Recursively merges properties from a source object into a target object.
33
- * This is a helper function for `merge` and `extend`.
34
- *
35
- * @param {Object} target - The target object to merge into.
36
- * @param {Object} source - The source object to merge from.
37
- * @param {Function} [merger] - An optional function to customize merging behavior for specific keys.
38
- * @returns {Object} - The modified target object.
39
- */
40
- function deepMerge(target, source, merger) {
41
- for (const [key, value] of Object.entries(source)) {
42
- if (isFunction(merger)) {
43
- const mergedValue = merger(target[key], value)
44
- if (mergedValue !== undefined) {
45
- target[key] = mergedValue
46
- continue
47
- }
48
- }
35
+ if (isArray(value)) {
36
+ const copy = new Array(value.length)
49
37
 
50
- if (isArray(value)) {
51
- target[key] = value
52
- } else if (isObject(value)) {
53
- if (!isObject(target[key])) {
54
- target[key] = {}
55
- }
56
- target[key] = deepMerge(target[key], value, merger)
57
- } else {
58
- target[key] = value
38
+ for (let index = 0; index < value.length; index++) {
39
+ copy[index] = clone(value[index])
59
40
  }
41
+
42
+ return copy
60
43
  }
61
- return target
44
+
45
+ if (value === null || typeof value !== "object") {
46
+ return value
47
+ }
48
+
49
+ const prototype = Object.getPrototypeOf(value)
50
+
51
+ if (prototype !== Object.prototype && prototype !== null) {
52
+ return structuredClone(value)
53
+ }
54
+
55
+ const copy = {}
56
+
57
+ for (const key in value) {
58
+ copy[key] = clone(value[key])
59
+ }
60
+
61
+ return copy
62
62
  }
63
63
 
64
64
  /**
@@ -242,9 +242,6 @@ export function get(obj, path, defaultValue = undefined) {
242
242
  export function isDeepEqual(left, right, seen = new WeakMap()) {
243
243
  if (Object.is(left, right)) return true
244
244
 
245
- // `Object.is` has already said no to these two being the same object, and neither can be
246
- // walked: a function is equal to itself and nothing else, and a key present in one and
247
- // not the other is a difference even when both read as undefined.
248
245
  if (typeof left !== "object" || typeof right !== "object") return false
249
246
  if (left === null || right === null) return false
250
247
 
@@ -326,7 +323,7 @@ export function mergeWith(merger, target, ...sources) {
326
323
  *
327
324
  * **Important:** Unlike Immer, which uses structural sharing via proxies for
328
325
  * high performance, this implementation performs a full deep clone of the base
329
- * state on every call using `deserialize(serialize())`. This can be
326
+ * state on every call using `clone`. This can be
330
327
  * inefficient for large or complex states. It is intended for simple use cases
331
328
  * where the convenience of the API outweighs the performance cost.
332
329
  *
@@ -370,7 +367,6 @@ export function produce(baseState, recipe, ...args) {
370
367
  */
371
368
  export function serialize(obj) {
372
369
  function replacer(key, value) {
373
- // Handle top-stage vector
374
370
  if (value?.__isVector__) {
375
371
  return {
376
372
  _type: "vector",
@@ -427,9 +423,7 @@ export function set(obj, path, value) {
427
423
  const key = keys[i]
428
424
  const nextKey = keys[i + NEXT_KEY_OFFSET]
429
425
 
430
- // Create missing intermediate objects
431
426
  if (!(key in current) || typeof current[key] !== "object") {
432
- // Create array if next key is a number, object otherwise
433
427
  current[key] = /^\d+$/.test(nextKey) ? [] : {}
434
428
  }
435
429
 
@@ -480,3 +474,36 @@ ${" ".repeat(indentationLevel)}}`
480
474
 
481
475
  return obj
482
476
  }
477
+
478
+ /**
479
+ * Recursively merges properties from a source object into a target object.
480
+ * This is a helper function for `merge` and `extend`.
481
+ *
482
+ * @param {Object} target - The target object to merge into.
483
+ * @param {Object} source - The source object to merge from.
484
+ * @param {Function} [merger] - An optional function to customize merging behavior for specific keys.
485
+ * @returns {Object} - The modified target object.
486
+ */
487
+ function deepMerge(target, source, merger) {
488
+ for (const [key, value] of Object.entries(source)) {
489
+ if (isFunction(merger)) {
490
+ const mergedValue = merger(target[key], value)
491
+ if (mergedValue !== undefined) {
492
+ target[key] = mergedValue
493
+ continue
494
+ }
495
+ }
496
+
497
+ if (isArray(value)) {
498
+ target[key] = value
499
+ } else if (isObject(value)) {
500
+ if (!isObject(target[key])) {
501
+ target[key] = {}
502
+ }
503
+ target[key] = deepMerge(target[key], value, merger)
504
+ } else {
505
+ target[key] = value
506
+ }
507
+ }
508
+ return target
509
+ }
@@ -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
+ }
@@ -71,9 +71,10 @@ export type DefaultsResult<T extends object, D extends object> = Omit<
71
71
  }
72
72
 
73
73
  /**
74
- * Creates a deep clone of a value.
74
+ * Creates a deep clone of a value. Plain data is copied by hand and vectors stay
75
+ * vectors; anything else goes through `structuredClone`.
75
76
  */
76
- export function clone<T>(obj: T): T
77
+ export function clone<T>(value: T): T
77
78
 
78
79
  /**
79
80
  * Assigns default properties to a target in place.
@@ -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