@peter.naydenov/walk-async 3.0.6 → 3.1.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/Changelog.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
 
4
4
 
5
+ ### 3.1.0 (2026-07-07)
6
+ - [x] New option 'timeout'. Milliseconds. When set, the walk promise is rejected if callbacks do not resolve in time. Error message lists the breadcrumbs of the pending callbacks;
7
+
8
+
9
+
10
+ ### 3.0.7 (2026-07-07)
11
+ - [x] Fix: 'objectCallback' resolving the root object with a primitive value was crashing on null/undefined or producing broken results;
12
+ - [x] Fix: Own '__proto__' property was replacing the prototype of the copy instead of being copied as a regular property;
13
+
14
+
15
+
5
16
  ### 3.0.6 (2026-07-07)
6
17
  - [x] Fix: Top-level property named 'root' was flattened into the result or dropped;
7
18
 
package/README.md CHANGED
@@ -13,6 +13,7 @@ Creates an immutable copies of javascript data structures(objects, arrays or mix
13
13
  data // (required) Any JS object structure
14
14
  , objectCallback // (optional) Function executed on each object property
15
15
  , keyCallback // (optional) Function executed on each primitive property
16
+ , timeout // (optional) Milliseconds. Reject the promise if callbacks do not resolve in time
16
17
  })
17
18
  .then ( result => {
18
19
  // Result will become a exact deep copy of "data"
@@ -55,7 +56,8 @@ function keyCallbackFn ({ value, key, breadcrumbs, resolve, reject }) {
55
56
  // breadcrumbs: location of the property;
56
57
  // resolve: function that will resolve the callback promise. Provide the result as argument;
57
58
  // reject: function that can cancel the copy of that property;
58
- // Important: key callback should be resolved or rejected.
59
+ // Important: key callback should be resolved or rejected on every code path,
60
+ // otherwise the walk promise never settles. See section "Timeout".
59
61
  }
60
62
 
61
63
  const result = await walk ({ data, keyCallback : keyCallbackFn }); // It's the short way to provide only key-callback. Callback functions are optional.
@@ -74,7 +76,8 @@ function objectCallbackFn ({ value, key, breadcrumbs, resolve, reject }) {
74
76
  // breadcrumbs: location of the object;
75
77
  // resolve: function that will resolve the callback promise. Provide the result as argument;
76
78
  // reject: function that can cancel the copy of that property;
77
- // Important: Object callback should be resolved or rejected.
79
+ // Important: Object callback should be resolved or rejected on every code path,
80
+ // otherwise the walk promise never settles. See section "Timeout".
78
81
  }
79
82
 
80
83
  walk ({
@@ -95,6 +98,32 @@ Skip key-callbacks by not providing a keyCallback function.
95
98
  ```
96
99
 
97
100
 
101
+ ## Timeout
102
+
103
+ Every callback must call `resolve` or `reject` on every code path. If some path forgets to do it, the promise returned by `walk` will never settle — and by default there is no error and no hint which callback is the reason. The optional `timeout` property (milliseconds) turns that silent hang into a rejection with diagnostics:
104
+
105
+ ```js
106
+ function objectCallbackFn ({ value, key, breadcrumbs, resolve, reject }) {
107
+ if ( breadcrumbs === 'root/props' ) return // BUG: this path never resolves
108
+ resolve ( value )
109
+ }
110
+
111
+ walk ({ data, objectCallback: objectCallbackFn, timeout: 5000 })
112
+ .catch ( err => {
113
+ console.error ( err.message )
114
+ // walk-async: timed out after 5000ms; callbacks still pending:
115
+ // - objectCallback at 'root/props'
116
+ })
117
+ ```
118
+
119
+ The error message lists the breadcrumbs of every callback that was started but never resolved or rejected, so the broken code path can be found directly.
120
+
121
+ Notes:
122
+ - `timeout` is disabled by default. Without it the behavior is exactly as before;
123
+ - The limit applies to the whole walk, not to a single callback. Set it well above the worst-case duration of the legitimate async work inside the callbacks — it is a debugging safety net, not a scheduler;
124
+ - When callbacks finish in time, the timer is cleared and the result is delivered as usual.
125
+
126
+
98
127
  ## Installation
99
128
 
100
129
  Install for node.js projects by writing in your terminal:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peter.naydenov/walk-async",
3
- "version": "3.0.6",
3
+ "version": "3.1.0",
4
4
  "description": "Deep copy with fine control during the process. Can be used also as a deep forEach function",
5
5
  "main": ".dist/walk-async.umd.js",
6
6
  "type": "module",
package/src/main.js CHANGED
@@ -21,54 +21,92 @@ import askForPromise from "ask-for-promise"
21
21
  * @property {Object} data - The data structure to be copied.
22
22
  * @property {Function|null} objectCallback - A function called for every object in the data structure.
23
23
  * @property {Function|null} keyCallback - A function called for every property in the data structure.
24
+ * @property {number|null} [timeout] - Milliseconds. When set, the promise is rejected if callbacks do not resolve in time. Error lists the pending callbacks.
24
25
  */
25
26
 
26
27
 
27
28
 
28
29
 
29
30
  /**
30
- * Walk-async is a deep copy function that executes callback functions on every object and property.
31
- * Callbacks are async functions.
32
- *
31
+ * Walk-async is a deep copy function that executes callback functions on every object and property.
32
+ * Callbacks are async functions.
33
+ *
33
34
  * @param {Options} options - Object with following properties:
34
35
  * - data - The data structure to be copied.
35
36
  * - objectCallback - A function that will be called for every object in the data structure.
36
37
  * - keyCallback - A function that will be called for every property in the data structure.
38
+ * - timeout - Milliseconds. When set, the promise is rejected if callbacks do not resolve in time.
37
39
  * @param {...any} args - Any additional arguments will be passed to the callback functions.
38
- *
40
+ *
39
41
  * @returns {Promise} - A promise that resolves to the immutable copy of the data structure.
40
42
  */
41
- function walk ({
43
+ function walk ({
42
44
  data:origin
43
45
  , objectCallback = null
44
46
  , keyCallback = null
47
+ , timeout = null
45
48
  }, ...args ) {
46
- let
49
+ let
47
50
  type = findType ( origin )
48
51
  , result
49
52
  , extend = []
50
53
  , breadcrumbs = 'root'
51
- , cb = [ keyCallback, objectCallback ]
54
+ , pending = new Set () // Callbacks in flight. Read on timeout to report which ones never resolved.
55
+ , cb = [ keyCallback, objectCallback, pending ]
52
56
  , end = askForPromise ()
57
+ , rootTask = askForPromise ()
58
+ , IGNORE = Symbol ( 'ignore___' )
53
59
  ;
54
60
 
55
- switch ( type ) {
56
- case 'array' :
57
- result = []
58
- copyObject ( {root:origin}, result, extend, cb, breadcrumbs, true, ...args )
59
- .then ( () => goNext ( extend, result, end ))
60
- break
61
- case 'object' :
62
- result = {}
63
- copyObject ( {root:origin}, result, extend, cb, breadcrumbs, true, ...args )
64
- .then ( () => {
65
- goNext ( extend, result, end )
66
- })
67
- break
68
- case 'simple' :
69
- end.done ( origin )
70
- } // switch type
71
- return end.promise
61
+ if ( type !== 'simple' && objectCallback ) { // Root object callback. Executed before the result is allocated, so it can replace the root with anything.
62
+ pending.add ( `objectCallback at 'root'` )
63
+ objectCallback ({
64
+ resolve : rootTask.done
65
+ , reject : () => rootTask.done ( IGNORE )
66
+ , value : origin
67
+ , key : 'root'
68
+ , breadcrumbs
69
+ }, ...args )
70
+ }
71
+ else rootTask.done ( origin )
72
+
73
+ rootTask.onComplete ( item => {
74
+ pending.delete ( `objectCallback at 'root'` )
75
+ if ( item === IGNORE ) {
76
+ end.done ( ( type === 'array' ) ? [] : {} )
77
+ return
78
+ }
79
+ switch ( findType ( item ) ) {
80
+ case 'array' :
81
+ result = []
82
+ copyObject ( item, result, extend, cb, breadcrumbs, ...args )
83
+ .then ( () => goNext ( extend, result, end ))
84
+ break
85
+ case 'object' :
86
+ result = {}
87
+ copyObject ( item, result, extend, cb, breadcrumbs, ...args )
88
+ .then ( () => {
89
+ goNext ( extend, result, end )
90
+ })
91
+ break
92
+ case 'simple' :
93
+ end.done ( item )
94
+ } // switch type
95
+ })
96
+
97
+ if ( timeout == null ) return end.promise
98
+
99
+ const // Watchdog. 'end.timeout' races 'end.onComplete' against a timer, but leaves 'end.promise' untouched — so return a guard promise fed from the race instead.
100
+ EXPIRED = Symbol ( 'expired___' )
101
+ , guard = askForPromise ()
102
+ ;
103
+ end.timeout ( timeout, EXPIRED )
104
+ end.onComplete ( res => {
105
+ if ( res !== EXPIRED ) { guard.done ( res ); return }
106
+ const stuck = [...pending].map ( name => `\n - ${name}` ).join ( '' )
107
+ guard.cancel ( new Error ( `walk-async: timed out after ${timeout}ms; callbacks still pending:${stuck}` ) )
108
+ })
109
+ return guard.promise
72
110
  } // walk func.
73
111
 
74
112
 
@@ -93,7 +131,7 @@ function findType ( x ) {
93
131
 
94
132
 
95
133
  async function* generateList ( data, location, ex, cb, breadcrumbs, args ) {
96
- yield await copyObject ( data , location, ex, cb, breadcrumbs, false, ...args )
134
+ yield await copyObject ( data , location, ex, cb, breadcrumbs, ...args )
97
135
  } // generateList func.
98
136
 
99
137
 
@@ -107,12 +145,18 @@ function validateForInsertion ( k, result ) {
107
145
 
108
146
 
109
147
 
110
- // 'isWrapper' is true only for the call from 'walk' where 'origin' is the
111
- // synthetic {root:origin} wrapper. Detecting the wrapper by its position in the
112
- // call chain (instead of by key name) keeps data keys named 'root' safe.
113
- function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...args ) {
114
- let
115
- [ keyCallback, objectCallback ] = cb
148
+ // Plain assignment of a '__proto__' key triggers the inherited setter and
149
+ // replaces the prototype of 'target' instead of creating an own property.
150
+ function setKey ( target, k, value ) {
151
+ if ( k === '__proto__' ) Object.defineProperty ( target, k, { value, enumerable:true, writable:true, configurable:true })
152
+ else target[k] = value
153
+ } // setKey func.
154
+
155
+
156
+
157
+ function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
158
+ let
159
+ [ keyCallback, objectCallback, pending ] = cb
116
160
  , keys = Object.keys ( origin )
117
161
  , executeCallback = askForPromise ( keys )
118
162
  , finish = askForPromise ()
@@ -129,11 +173,13 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
129
173
  , resultIsArray = (findType (result) === 'array')
130
174
  , keyNumber = !isNaN ( k )
131
175
  , IGNORE = Symbol ( 'ignore___' )
132
- , isRoot = isWrapper // The wrapper has exactly one key: 'root'
133
- , br = isRoot ? 'root' : `${breadcrumbs}/${k}`
176
+ , br = `${breadcrumbs}/${k}`
177
+ , objectTag = `objectCallback at '${br}'`
178
+ , keyTag = `keyCallback at '${br}'`
134
179
  ;
135
180
 
136
181
  if ( hasObjectCallback ) {
182
+ pending.add ( objectTag )
137
183
  objectCallback ({
138
184
  resolve : objectCallbackTask.done
139
185
  , reject : () => objectCallbackTask.done ( IGNORE )
@@ -147,6 +193,7 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
147
193
  }
148
194
 
149
195
  objectCallbackTask.onComplete ( res => {
196
+ pending.delete ( objectTag )
150
197
  if ( res === '$$cancel' && !keyCallback ) { // deep copy, no callbacks
151
198
  keyCallbackTask.done ( '$$noUpdates' )
152
199
  return
@@ -164,9 +211,10 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
164
211
  keyCallbackTask.done ( '$$noUpdates' )
165
212
  return
166
213
  }
214
+ pending.add ( keyTag )
167
215
  keyCallback ({
168
216
  resolve : keyCallbackTask.done
169
- , reject : () => keyCallbackTask.done ( IGNORE )
217
+ , reject : () => keyCallbackTask.done ( IGNORE )
170
218
  , value : item
171
219
  , key : k
172
220
  , breadcrumbs : br
@@ -178,12 +226,8 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
178
226
  }) // objectCallbackTask complete
179
227
 
180
228
  keyCallbackTask.onComplete ( value => {
181
- if ( isRoot ) {
182
- extend.push ( generateList ( item, result, extend, cb, 'root', args ) )
183
- executeCallback.promises[i].done ( 'root' )
184
- return
185
- }
186
- if ( value == IGNORE ) {
229
+ pending.delete ( keyTag )
230
+ if ( value == IGNORE ) {
187
231
  executeCallback.promises[i].done ( 'ignore key' )
188
232
  return
189
233
  }
@@ -198,7 +242,7 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
198
242
  if ( type === 'simple' ) {
199
243
  const canInsert = validateForInsertion ( k, result )
200
244
  if ( canInsert ) result.push ( item )
201
- else result [k] = item
245
+ else setKey ( result, k, item )
202
246
  executeCallback.promises[i].done ('key')
203
247
  return
204
248
  }
@@ -210,14 +254,14 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, isWrapper, ...arg
210
254
  if ( type === 'object' ) {
211
255
  const newObject = {};
212
256
  if ( resultIsArray && keyNumber ) result.push ( newObject )
213
- else result[k] = newObject
257
+ else setKey ( result, k, newObject )
214
258
  extend.push ( generateList( item, newObject, extend, cb, br, args ) )
215
259
  executeCallback.promises[i].done ('object')
216
260
  }
217
261
  if ( type === 'array' ) {
218
262
  const newArray = [];
219
263
  if ( resultIsArray && keyNumber ) result.push ( newArray )
220
- else result[k] = newArray
264
+ else setKey ( result, k, newArray )
221
265
  extend.push ( generateList( item, newArray, extend, cb, br, args ) )
222
266
  executeCallback.promises[i].done ('array')
223
267
  }