@sohanemon/utils 5.2.5 → 5.2.6

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,45 @@
1
+ /**
2
+ * Deeply cleans any value by converting all `null` values to `undefined`,
3
+ * and merges in a fallback object for default values.
4
+ *
5
+ * Features:
6
+ * - Circular reference detection to prevent infinite loops
7
+ * - Proper handling of special objects (Date, Map, Set, RegExp, etc.)
8
+ * - Strict type safety with improved generics
9
+ * - Better error messages and validation
10
+ * - Support for nested structures with Symbol properties
11
+ * - Optional maximum recursion depth to prevent stack overflow
12
+ * - Configurable null-to-undefined conversion
13
+ *
14
+ * @param data - Any input data (object, array, primitive)
15
+ * @param fallback - Optional fallback values to merge with
16
+ * @param options - Configuration options
17
+ * @param options.maxDepth - Maximum recursion depth (default: 100)
18
+ * @param options.throwOnCircular - Whether to throw on circular refs (default: false)
19
+ * @param options.convertNullToUndefined - Convert null to undefined (default: true)
20
+ * @returns Same type as input, but with all nulls replaced by undefined
21
+ *
22
+ * @throws {TypeError} If data or fallback are invalid types when strict validation is enabled
23
+ * @throws {RangeError} If circular reference detected and throwOnCircular is true
24
+ *
25
+ * @example
26
+ * // Basic usage
27
+ * hydrate({ a: null, b: 'test' }) // { a: undefined, b: 'test' }
28
+ *
29
+ * @example
30
+ * // With fallback values
31
+ * hydrate({ a: null }, { a: 'default' }) // { a: 'default' }
32
+ *
33
+ * @example
34
+ * // Keep nulls as-is
35
+ * hydrate({ a: null }, undefined, { convertNullToUndefined: false }) // { a: null }
36
+ */
37
+ export declare function hydrate<T>(data: T, fallback?: Partial<T>, options?: {
38
+ maxDepth?: number;
39
+ throwOnCircular?: boolean;
40
+ convertNullToUndefined?: boolean;
41
+ }): T;
42
+ /**
43
+ * Type guard utility for checking if a value is an object with a specific shape
44
+ */
45
+ export declare function isWithFallbackCompatible<T extends object>(value: unknown): value is T;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Deeply cleans any value by converting all `null` values to `undefined`,
3
+ * and merges in a fallback object for default values.
4
+ *
5
+ * Features:
6
+ * - Circular reference detection to prevent infinite loops
7
+ * - Proper handling of special objects (Date, Map, Set, RegExp, etc.)
8
+ * - Strict type safety with improved generics
9
+ * - Better error messages and validation
10
+ * - Support for nested structures with Symbol properties
11
+ * - Optional maximum recursion depth to prevent stack overflow
12
+ * - Configurable null-to-undefined conversion
13
+ *
14
+ * @param data - Any input data (object, array, primitive)
15
+ * @param fallback - Optional fallback values to merge with
16
+ * @param options - Configuration options
17
+ * @param options.maxDepth - Maximum recursion depth (default: 100)
18
+ * @param options.throwOnCircular - Whether to throw on circular refs (default: false)
19
+ * @param options.convertNullToUndefined - Convert null to undefined (default: true)
20
+ * @returns Same type as input, but with all nulls replaced by undefined
21
+ *
22
+ * @throws {TypeError} If data or fallback are invalid types when strict validation is enabled
23
+ * @throws {RangeError} If circular reference detected and throwOnCircular is true
24
+ *
25
+ * @example
26
+ * // Basic usage
27
+ * hydrate({ a: null, b: 'test' }) // { a: undefined, b: 'test' }
28
+ *
29
+ * @example
30
+ * // With fallback values
31
+ * hydrate({ a: null }, { a: 'default' }) // { a: 'default' }
32
+ *
33
+ * @example
34
+ * // Keep nulls as-is
35
+ * hydrate({ a: null }, undefined, { convertNullToUndefined: false }) // { a: null }
36
+ */
37
+ export function hydrate(data, fallback, options) {
38
+ const maxDepth = options?.maxDepth ?? 100;
39
+ const throwOnCircular = options?.throwOnCircular ?? false;
40
+ const convertNullToUndefined = options?.convertNullToUndefined ?? true;
41
+ // Use WeakSet for O(1) circular reference detection
42
+ const visited = new WeakSet();
43
+ return processValue(data, fallback, 0, visited);
44
+ function processValue(value, fallbackValue, depth, visited) {
45
+ // Check recursion depth
46
+ if (depth > maxDepth) {
47
+ console.warn(`[hydrate] Maximum recursion depth (${maxDepth}) exceeded. Returning value as-is.`);
48
+ return value ?? fallbackValue;
49
+ }
50
+ if (value === null) {
51
+ return convertNullToUndefined ? undefined : (fallbackValue ?? null);
52
+ }
53
+ // Handle undefined - use fallback or return undefined
54
+ if (value === undefined) {
55
+ return fallbackValue ?? undefined;
56
+ }
57
+ // Handle primitives: string, number, boolean, symbol, bigint
58
+ const type = typeof value;
59
+ if (type !== 'object') {
60
+ return value;
61
+ }
62
+ if (visited.has(value)) {
63
+ if (throwOnCircular) {
64
+ throw new RangeError('[hydrate] Circular reference detected');
65
+ }
66
+ // Return the value as-is to break the cycle
67
+ return value;
68
+ }
69
+ // Mark as visited to detect circular references
70
+ visited.add(value);
71
+ if (isSpecialObject(value)) {
72
+ return handleSpecialObject(value, fallbackValue);
73
+ }
74
+ // Handle arrays
75
+ if (Array.isArray(value)) {
76
+ const fallbackArray = Array.isArray(fallbackValue)
77
+ ? fallbackValue
78
+ : undefined;
79
+ return value.map((item, index) => processValue(item, fallbackArray?.[index], depth + 1, visited));
80
+ }
81
+ // Handle plain objects
82
+ if (isPlainObject(value)) {
83
+ const fallbackObj = isPlainObject(fallbackValue)
84
+ ? fallbackValue
85
+ : {};
86
+ const result = { ...fallbackObj };
87
+ // Process all enumerable properties including symbols
88
+ const keys = [
89
+ ...Object.keys(value),
90
+ ...Object.getOwnPropertySymbols(value),
91
+ ];
92
+ for (const k of keys) {
93
+ const propValue = value[k];
94
+ const fallbackProp = fallbackObj[k];
95
+ result[k] = processValue(propValue, fallbackProp, depth + 1, visited);
96
+ }
97
+ return result;
98
+ }
99
+ // For other objects, return as-is (instances, etc.)
100
+ return value ?? fallbackValue;
101
+ }
102
+ }
103
+ /**
104
+ * Checks if a value is a plain object (not a special type)
105
+ */
106
+ function isPlainObject(value) {
107
+ if (typeof value !== 'object' || value === null) {
108
+ return false;
109
+ }
110
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
111
+ return false;
112
+ }
113
+ // Objects with null prototype are still plain objects
114
+ const proto = Object.getPrototypeOf(value);
115
+ return proto === null || proto === Object.prototype;
116
+ }
117
+ /**
118
+ * Checks if a value is a special object type that needs custom handling
119
+ */
120
+ function isSpecialObject(value) {
121
+ if (typeof value !== 'object' || value === null) {
122
+ return false;
123
+ }
124
+ const stringTag = Object.prototype.toString.call(value);
125
+ const specialTypes = [
126
+ '[object Date]',
127
+ '[object RegExp]',
128
+ '[object Map]',
129
+ '[object Set]',
130
+ '[object WeakMap]',
131
+ '[object WeakSet]',
132
+ '[object Promise]',
133
+ '[object Error]',
134
+ '[object ArrayBuffer]',
135
+ ];
136
+ return specialTypes.includes(stringTag);
137
+ }
138
+ /**
139
+ * Handles special object types that shouldn't be deeply cloned
140
+ */
141
+ function handleSpecialObject(value, fallbackValue) {
142
+ // For special types, return the original value or fallback
143
+ // These shouldn't be deeply cloned as they have internal state
144
+ return value ?? fallbackValue;
145
+ }
146
+ /**
147
+ * Type guard utility for checking if a value is an object with a specific shape
148
+ */
149
+ export function isWithFallbackCompatible(value) {
150
+ return (typeof value === 'object' &&
151
+ (value === null ||
152
+ Array.isArray(value) ||
153
+ (typeof value === 'object' &&
154
+ Object.prototype.toString.call(value) === '[object Object]')));
155
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './cookie';
2
+ export * from './hydrate';
2
3
  export * from './object';
3
4
  export * from './poll';
4
5
  export * from './schedule';
@@ -1,4 +1,5 @@
1
1
  export * from './cookie';
2
+ export * from './hydrate';
2
3
  export * from './object';
3
4
  export * from './poll';
4
5
  export * from './schedule';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sohanemon/utils",
3
- "version": "5.2.5",
3
+ "version": "5.2.6",
4
4
  "author": "Sohan Emon <sohanemon@outlook.com>",
5
5
  "description": "",
6
6
  "type": "module",