@inglorious/utils 3.1.0 → 3.3.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/utils",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "A set of general-purpose utility functions designed with functional programming principles in mind.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { v } from "../v.js"
|
|
2
|
+
import { isArray } from "./array.js"
|
|
3
|
+
|
|
1
4
|
const INITIAL_LEVEL = 0
|
|
2
5
|
const NEXT_LEVEL = 2
|
|
3
6
|
|
|
@@ -8,7 +11,46 @@ const NEXT_LEVEL = 2
|
|
|
8
11
|
* @returns {Object} A deep clone of the input object.
|
|
9
12
|
*/
|
|
10
13
|
export function clone(obj) {
|
|
11
|
-
return
|
|
14
|
+
return deserialize(serialize(obj))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deserializes an object, converting plain object representations back into
|
|
19
|
+
* their original types, such as vector-like objects. This is the inverse
|
|
20
|
+
* operation of `serialize`.
|
|
21
|
+
*
|
|
22
|
+
* - Recursively deserializes nested objects.
|
|
23
|
+
* - Converts objects with a `_type: "vector"` property and `coords` array
|
|
24
|
+
* back into a vector-like object using the `v` factory function.
|
|
25
|
+
* - Copies all other property values as-is.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} str - The JSON string to deserialize.
|
|
28
|
+
* @returns {Object} The deserialized object.
|
|
29
|
+
*/
|
|
30
|
+
export function deserialize(str) {
|
|
31
|
+
const data = JSON.parse(str)
|
|
32
|
+
|
|
33
|
+
return revive(data)
|
|
34
|
+
|
|
35
|
+
function revive(value) {
|
|
36
|
+
if (isArray(value)) {
|
|
37
|
+
return value.map(revive)
|
|
38
|
+
}
|
|
39
|
+
if (isObject(value)) {
|
|
40
|
+
if (value._type === "vector" && value.coords) {
|
|
41
|
+
return v(...value.coords)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const deserialized = {}
|
|
45
|
+
for (const key in value) {
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
47
|
+
deserialized[key] = revive(value[key])
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return deserialized
|
|
51
|
+
}
|
|
52
|
+
return value
|
|
53
|
+
}
|
|
12
54
|
}
|
|
13
55
|
|
|
14
56
|
/**
|
|
@@ -73,7 +115,7 @@ export function map(obj, callback) {
|
|
|
73
115
|
*
|
|
74
116
|
* **Important:** Unlike Immer, which uses structural sharing via proxies for
|
|
75
117
|
* high performance, this implementation performs a full deep clone of the base
|
|
76
|
-
* state on every call using `
|
|
118
|
+
* state on every call using `deserialize(serialize())`. This can be
|
|
77
119
|
* inefficient for large or complex states. It is intended for simple use cases
|
|
78
120
|
* where the convenience of the API outweighs the performance cost.
|
|
79
121
|
*
|
|
@@ -102,6 +144,47 @@ export function produce(baseState, recipe, ...args) {
|
|
|
102
144
|
return result === undefined ? draft : result
|
|
103
145
|
}
|
|
104
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Serializes an object, converting special types like vectors into a plain
|
|
149
|
+
* object representation. This is useful for processes like saving state to a
|
|
150
|
+
* file or sending it over a network.
|
|
151
|
+
*
|
|
152
|
+
* - Recursively serializes nested objects.
|
|
153
|
+
* - Converts objects with a `__isVector__` property into a serializable
|
|
154
|
+
* format: `{ _type: "vector", coords: [...] }`.
|
|
155
|
+
* - Copies all other property values as-is.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} obj - The object to serialize.
|
|
158
|
+
* @returns {string} The serialized JSON string.
|
|
159
|
+
*/
|
|
160
|
+
export function serialize(obj) {
|
|
161
|
+
function replacer(key, value) {
|
|
162
|
+
// Handle top-level vector
|
|
163
|
+
if (value?.__isVector__) {
|
|
164
|
+
return {
|
|
165
|
+
_type: "vector",
|
|
166
|
+
coords: Array.from(value),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isObject(value)) {
|
|
171
|
+
const serialized = {}
|
|
172
|
+
|
|
173
|
+
for (const k in value) {
|
|
174
|
+
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
175
|
+
serialized[k] = replacer(undefined, value[k])
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return serialized
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return value
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return JSON.stringify(obj, replacer)
|
|
186
|
+
}
|
|
187
|
+
|
|
105
188
|
/**
|
|
106
189
|
* Converts an object or array to a formatted string representation.
|
|
107
190
|
*
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { expect, test } from "vitest"
|
|
2
2
|
|
|
3
|
+
import { v } from "../v.js"
|
|
3
4
|
import {
|
|
4
5
|
clone,
|
|
6
|
+
deserialize,
|
|
5
7
|
filter,
|
|
6
8
|
find,
|
|
7
9
|
isObject,
|
|
8
10
|
map,
|
|
9
11
|
produce,
|
|
12
|
+
serialize,
|
|
10
13
|
toString,
|
|
11
14
|
} from "./object.js"
|
|
12
15
|
|
|
@@ -112,7 +115,7 @@ test("it should produce a new state without mutating the original", () => {
|
|
|
112
115
|
g: 6,
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
const originalBaseState =
|
|
118
|
+
const originalBaseState = deserialize(serialize(baseState))
|
|
116
119
|
|
|
117
120
|
const nextState = produce(baseState, recipe)
|
|
118
121
|
|
|
@@ -216,3 +219,117 @@ test("it should return a string representation of a nested object", () => {
|
|
|
216
219
|
g: 6
|
|
217
220
|
}`)
|
|
218
221
|
})
|
|
222
|
+
|
|
223
|
+
test("it should serialize a basic object with primitive values", () => {
|
|
224
|
+
const obj = { a: 1, b: "hello", c: true }
|
|
225
|
+
const expected = `{"a":1,"b":"hello","c":true}`
|
|
226
|
+
|
|
227
|
+
expect(serialize(obj)).toBe(expected)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test("it should serialize an object with nested objects", () => {
|
|
231
|
+
const obj = { a: 1, b: { c: "nested", d: { e: false } } }
|
|
232
|
+
const expected = `{"a":1,"b":{"c":"nested","d":{"e":false}}}`
|
|
233
|
+
|
|
234
|
+
expect(serialize(obj)).toBe(expected)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("it should serialize an object containing a vector-like object", () => {
|
|
238
|
+
const obj = { position: v(1, 2, 3) }
|
|
239
|
+
const expected = `{"position":{"_type":"vector","coords":[1,2,3]}}`
|
|
240
|
+
|
|
241
|
+
expect(serialize(obj)).toBe(expected)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("it should serialize an object with nested vector-like objects", () => {
|
|
245
|
+
const obj = {
|
|
246
|
+
entity: {
|
|
247
|
+
id: "player",
|
|
248
|
+
pos: v(10, 20),
|
|
249
|
+
vel: v(1, 0),
|
|
250
|
+
stats: { health: 100 },
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
const expected = `{"entity":{"id":"player","pos":{"_type":"vector","coords":[10,20]},"vel":{"_type":"vector","coords":[1,0]},"stats":{"health":100}}}`
|
|
254
|
+
|
|
255
|
+
expect(serialize(obj)).toBe(expected)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test("it should serialize an empty object", () => {
|
|
259
|
+
const obj = {}
|
|
260
|
+
const expected = `{}`
|
|
261
|
+
|
|
262
|
+
expect(serialize(obj)).toBe(expected)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("it should serialize an object with null and undefined values (undefined should be omitted)", () => {
|
|
266
|
+
const obj = { a: null, b: undefined, c: 1 }
|
|
267
|
+
const expected = `{"a":null,"c":1}` // JSON.stringify omits undefined properties
|
|
268
|
+
|
|
269
|
+
expect(serialize(obj)).toBe(expected)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test("it should deserialize a basic object with primitive values", () => {
|
|
273
|
+
const serialized = `{"a":1,"b":"hello","c":true}`
|
|
274
|
+
const expected = { a: 1, b: "hello", c: true }
|
|
275
|
+
|
|
276
|
+
expect(deserialize(serialized)).toStrictEqual(expected)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("it should deserialize an object with nested objects", () => {
|
|
280
|
+
const serialized = `{"a":1,"b":{"c":"nested","d":{"e":false}}}`
|
|
281
|
+
const expected = { a: 1, b: { c: "nested", d: { e: false } } }
|
|
282
|
+
|
|
283
|
+
expect(deserialize(serialized)).toStrictEqual(expected)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test("it should deserialize an object containing a vector representation", () => {
|
|
287
|
+
const serialized = `{"position":{"_type":"vector","coords":[1,2,3]}}`
|
|
288
|
+
const expected = { position: v(1, 2, 3) }
|
|
289
|
+
|
|
290
|
+
const result = deserialize(serialized)
|
|
291
|
+
|
|
292
|
+
expect(result).toStrictEqual(expected)
|
|
293
|
+
expect(result.position.__isVector__).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("it should deserialize an object with nested vector representations", () => {
|
|
297
|
+
const serialized = `{"entity":{"id":"player","pos":{"_type":"vector","coords":[10,20]},"vel":{"_type":"vector","coords":[1,0]},"stats":{"health":100}}}`
|
|
298
|
+
const expected = {
|
|
299
|
+
entity: {
|
|
300
|
+
id: "player",
|
|
301
|
+
pos: v(10, 20),
|
|
302
|
+
vel: v(1, 0),
|
|
303
|
+
stats: { health: 100 },
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
const result = deserialize(serialized)
|
|
307
|
+
|
|
308
|
+
expect(result).toStrictEqual(expected)
|
|
309
|
+
expect(result.entity.pos.__isVector__).toBe(true)
|
|
310
|
+
expect(result.entity.vel.__isVector__).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test("it should deserialize an empty object", () => {
|
|
314
|
+
const serialized = `{}`
|
|
315
|
+
const expected = {}
|
|
316
|
+
|
|
317
|
+
expect(deserialize(serialized)).toStrictEqual(expected)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test("it should correctly round-trip a complex object through serialize and deserialize", () => {
|
|
321
|
+
const original = {
|
|
322
|
+
id: "game1",
|
|
323
|
+
player: { name: "Alice", score: 100, position: v(10, 20) },
|
|
324
|
+
enemies: [{ type: "goblin", pos: v(5, 5) }],
|
|
325
|
+
settings: { volume: 0.8, mute: false },
|
|
326
|
+
}
|
|
327
|
+
const jsonString = serialize(original)
|
|
328
|
+
const deserialized = deserialize(jsonString)
|
|
329
|
+
|
|
330
|
+
expect(deserialized).toStrictEqual(original)
|
|
331
|
+
expect(deserialized.player.position.__isVector__).toBe(true)
|
|
332
|
+
expect(deserialized.enemies[0].pos.__isVector__).toBe(true)
|
|
333
|
+
// Ensure that non-vector objects are still distinct references if they were originally
|
|
334
|
+
expect(deserialized.player).not.toBe(original.player)
|
|
335
|
+
})
|
package/src/math/vector.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* @typedef {import('./types').Vector3} Vector3
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { isArray } from "../data-structures/array.js"
|
|
8
7
|
import { v } from "../v.js"
|
|
9
8
|
import {
|
|
10
9
|
abs as nAbs,
|
|
@@ -128,17 +127,13 @@ export function fromAngle(angle) {
|
|
|
128
127
|
|
|
129
128
|
/**
|
|
130
129
|
* Checks if a value is a vector.
|
|
131
|
-
*
|
|
132
|
-
*
|
|
130
|
+
* This is determined by checking for the `__isVector__` property, which is
|
|
131
|
+
* added by the `v()` factory function for efficient type checking.
|
|
133
132
|
* @param {*} value - The value to check.
|
|
134
133
|
* @returns {boolean} True if the value is a vector, false otherwise.
|
|
135
134
|
*/
|
|
136
135
|
export function isVector(value) {
|
|
137
|
-
return
|
|
138
|
-
value?.__isVector__ ||
|
|
139
|
-
(isArray(value) &&
|
|
140
|
-
value.every((coordinate) => typeof coordinate === "number"))
|
|
141
|
-
)
|
|
136
|
+
return !!value?.__isVector__
|
|
142
137
|
}
|
|
143
138
|
|
|
144
139
|
/**
|
package/src/math/vector.test.js
CHANGED
|
@@ -126,8 +126,7 @@ test("it should compute the magnitude of a vector (aka length)", () => {
|
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
test("it should check if a value is a vector", () => {
|
|
129
|
-
expect(isVector([1, 2, 3])).toBe(
|
|
130
|
-
expect(isVector([])).toBe(true)
|
|
129
|
+
expect(isVector([1, 2, 3])).toBe(false)
|
|
131
130
|
expect(isVector(v(1, 2, 3))).toBe(true)
|
|
132
131
|
})
|
|
133
132
|
|