@inglorious/utils 3.1.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/utils",
3
- "version": "3.1.0",
3
+ "version": "3.2.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 JSON.parse(JSON.stringify(obj))
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 `JSON.parse(JSON.stringify())`. This can be
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 = JSON.parse(JSON.stringify(baseState))
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
+ })