@rohal12/spindle 0.51.2 → 0.51.4

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.
@@ -0,0 +1,109 @@
1
+ import { isDraft } from 'immer';
2
+
3
+ /** Traverse dot-path segments on an object and return the nested value. */
4
+ export function getByPath(
5
+ obj: Record<string, unknown>,
6
+ segments: readonly string[],
7
+ ): unknown {
8
+ let current: unknown = obj;
9
+ for (const seg of segments) {
10
+ if (current == null || typeof current !== 'object') return undefined;
11
+ current = (current as Record<string, unknown>)[seg];
12
+ }
13
+ return current;
14
+ }
15
+
16
+ /**
17
+ * Shallow copy that keeps the prototype, so a registered class instance
18
+ * stays an instance of its class (with the same own keys deepClone copies).
19
+ */
20
+ function shallowCopy(value: object): Record<string, unknown> {
21
+ let copy: object;
22
+ if (Array.isArray(value)) copy = [];
23
+ else if (value instanceof Map) copy = new Map(value);
24
+ else if (value instanceof Set) copy = new Set(value);
25
+ else if (value instanceof Date) copy = new Date(value.getTime());
26
+ else copy = Object.create(Object.getPrototypeOf(value)) as object;
27
+ return Object.assign(copy, value) as Record<string, unknown>;
28
+ }
29
+
30
+ export interface SetByPathOptions {
31
+ /**
32
+ * Create a plain object for a missing or non-object intermediate instead
33
+ * of throwing.
34
+ */
35
+ createMissing?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Set a value at dot-path `segments` below `root`.
40
+ *
41
+ * When `root` is an Immer draft, objects Immer does not draft (registered
42
+ * class instances and everything below them) are copied on the way down and
43
+ * the copy is assigned to its parent, so the write produces new references
44
+ * up to the root. Writing into such an object directly would mutate the
45
+ * instance shared with the previous state and with history, and subscribers
46
+ * would see no change. Outside a draft (a private working copy) the path is
47
+ * written in place.
48
+ */
49
+ export function setByPath(
50
+ root: Record<string, unknown>,
51
+ segments: readonly string[],
52
+ value: unknown,
53
+ options: SetByPathOptions = {},
54
+ ): void {
55
+ const parent = walkToParent(root, segments, options.createMissing ?? false);
56
+ parent[segments[segments.length - 1]!] = value;
57
+ }
58
+
59
+ /**
60
+ * Delete the property at dot-path `segments` below `root`, copying
61
+ * undrafted objects on the way down like setByPath(). Does nothing when an
62
+ * intermediate is missing or not an object, or the property is absent.
63
+ */
64
+ export function deleteByPath(
65
+ root: Record<string, unknown>,
66
+ segments: readonly string[],
67
+ ): void {
68
+ const last = segments[segments.length - 1]!;
69
+ // Check first, so a no-op delete copies nothing.
70
+ const holder = getByPath(root, segments.slice(0, -1));
71
+ if (holder == null || typeof holder !== 'object' || !(last in holder)) {
72
+ return;
73
+ }
74
+ const parent = walkToParent(root, segments, false);
75
+ delete parent[last];
76
+ }
77
+
78
+ /**
79
+ * Walk to the object holding the last of `segments`, copying undrafted
80
+ * objects when `root` is an Immer draft (see setByPath). A missing or
81
+ * non-object intermediate is replaced by a plain object when
82
+ * `createMissing` is set, and throws a TypeError otherwise.
83
+ */
84
+ function walkToParent(
85
+ root: Record<string, unknown>,
86
+ segments: readonly string[],
87
+ createMissing: boolean,
88
+ ): Record<string, unknown> {
89
+ const copyOnWrite = isDraft(root);
90
+ let current: Record<string, unknown> = root;
91
+ for (let i = 0; i < segments.length - 1; i++) {
92
+ const seg = segments[i]!;
93
+ let next = current[seg];
94
+ if (next == null || typeof next !== 'object') {
95
+ if (!createMissing) {
96
+ throw new TypeError(
97
+ `spindle: Cannot set property "${segments[i + 1]}" on ${next === null ? 'null' : typeof next} (at "${segments.slice(0, i + 1).join('.')}")`,
98
+ );
99
+ }
100
+ next = {};
101
+ current[seg] = next;
102
+ } else if (copyOnWrite && !isDraft(next)) {
103
+ next = shallowCopy(next);
104
+ current[seg] = next;
105
+ }
106
+ current = next as Record<string, unknown>;
107
+ }
108
+ return current;
109
+ }
@@ -0,0 +1,77 @@
1
+ import { getClassName } from '../class-registry';
2
+
3
+ type Constructor = new (...args: any[]) => any;
4
+
5
+ /**
6
+ * Content-derived string key for a value, used to remount components when
7
+ * the value's contents change (e.g. {for} iterations, #45).
8
+ *
9
+ * JSON-compatible values (strings, finite numbers, booleans, null, arrays and
10
+ * plain objects of those) produce exactly their `JSON.stringify` output. Other
11
+ * values get distinct, unambiguous markers so their contents are reflected
12
+ * too: Map and Set entries (nested at any depth), Date, RegExp, BigInt,
13
+ * undefined, NaN/±Infinity, symbols, functions and registered class
14
+ * instances. Cyclic references become a back-reference marker instead of
15
+ * throwing, and the function never throws.
16
+ */
17
+ export function stableKey(value: unknown): string {
18
+ try {
19
+ return keyOf(value, []);
20
+ } catch {
21
+ return '<unkeyable>';
22
+ }
23
+ }
24
+
25
+ function keyOf(val: unknown, ancestors: object[]): string {
26
+ switch (typeof val) {
27
+ case 'string':
28
+ return JSON.stringify(val);
29
+ case 'number':
30
+ return Number.isFinite(val) ? JSON.stringify(val) : String(val);
31
+ case 'boolean':
32
+ return String(val);
33
+ case 'bigint':
34
+ return `${val}n`;
35
+ case 'undefined':
36
+ return 'undefined';
37
+ case 'symbol':
38
+ return val.toString();
39
+ case 'function':
40
+ return `<function ${JSON.stringify(val.name)}>`;
41
+ }
42
+ if (val === null) return 'null';
43
+
44
+ const obj = val as object;
45
+ const depth = ancestors.indexOf(obj);
46
+ if (depth !== -1) return `<cycle ${ancestors.length - depth}>`;
47
+
48
+ if (val instanceof Date) return `Date(${val.getTime()})`;
49
+ if (val instanceof RegExp) return `RegExp(${String(val)})`;
50
+
51
+ ancestors.push(obj);
52
+ try {
53
+ if (Array.isArray(val)) {
54
+ return `[${val.map((v) => keyOf(v, ancestors)).join(',')}]`;
55
+ }
56
+ if (val instanceof Map) {
57
+ const entries = [...val].map(
58
+ ([k, v]) => `[${keyOf(k, ancestors)},${keyOf(v, ancestors)}]`,
59
+ );
60
+ return `Map[${entries.join(',')}]`;
61
+ }
62
+ if (val instanceof Set) {
63
+ return `Set[${[...val].map((v) => keyOf(v, ancestors)).join(',')}]`;
64
+ }
65
+
66
+ const record = obj as Record<string, unknown>;
67
+ const body = Object.keys(record)
68
+ .map((k) => `${JSON.stringify(k)}:${keyOf(record[k], ancestors)}`)
69
+ .join(',');
70
+ const className = getClassName(obj.constructor as Constructor);
71
+ return className === undefined
72
+ ? `{${body}}`
73
+ : `Class(${JSON.stringify(className)}){${body}}`;
74
+ } finally {
75
+ ancestors.pop();
76
+ }
77
+ }