@peter.naydenov/walk-async 3.0.7 → 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,11 @@
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
+
5
10
  ### 3.0.7 (2026-07-07)
6
11
  - [x] Fix: 'objectCallback' resolving the root object with a primitive value was crashing on null/undefined or producing broken results;
7
12
  - [x] Fix: Own '__proto__' property was replacing the prototype of the copy instead of being copied as a regular property;
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.7",
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,40 +21,45 @@ 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
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 ()
53
57
  , rootTask = askForPromise ()
54
58
  , IGNORE = Symbol ( 'ignore___' )
55
59
  ;
56
60
 
57
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'` )
58
63
  objectCallback ({
59
64
  resolve : rootTask.done
60
65
  , reject : () => rootTask.done ( IGNORE )
@@ -66,6 +71,7 @@ function walk ({
66
71
  else rootTask.done ( origin )
67
72
 
68
73
  rootTask.onComplete ( item => {
74
+ pending.delete ( `objectCallback at 'root'` )
69
75
  if ( item === IGNORE ) {
70
76
  end.done ( ( type === 'array' ) ? [] : {} )
71
77
  return
@@ -87,7 +93,20 @@ function walk ({
87
93
  end.done ( item )
88
94
  } // switch type
89
95
  })
90
- return end.promise
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
91
110
  } // walk func.
92
111
 
93
112
 
@@ -136,8 +155,8 @@ function setKey ( target, k, value ) {
136
155
 
137
156
 
138
157
  function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
139
- let
140
- [ keyCallback, objectCallback ] = cb
158
+ let
159
+ [ keyCallback, objectCallback, pending ] = cb
141
160
  , keys = Object.keys ( origin )
142
161
  , executeCallback = askForPromise ( keys )
143
162
  , finish = askForPromise ()
@@ -155,9 +174,12 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
155
174
  , keyNumber = !isNaN ( k )
156
175
  , IGNORE = Symbol ( 'ignore___' )
157
176
  , br = `${breadcrumbs}/${k}`
177
+ , objectTag = `objectCallback at '${br}'`
178
+ , keyTag = `keyCallback at '${br}'`
158
179
  ;
159
180
 
160
181
  if ( hasObjectCallback ) {
182
+ pending.add ( objectTag )
161
183
  objectCallback ({
162
184
  resolve : objectCallbackTask.done
163
185
  , reject : () => objectCallbackTask.done ( IGNORE )
@@ -171,6 +193,7 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
171
193
  }
172
194
 
173
195
  objectCallbackTask.onComplete ( res => {
196
+ pending.delete ( objectTag )
174
197
  if ( res === '$$cancel' && !keyCallback ) { // deep copy, no callbacks
175
198
  keyCallbackTask.done ( '$$noUpdates' )
176
199
  return
@@ -188,9 +211,10 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
188
211
  keyCallbackTask.done ( '$$noUpdates' )
189
212
  return
190
213
  }
214
+ pending.add ( keyTag )
191
215
  keyCallback ({
192
216
  resolve : keyCallbackTask.done
193
- , reject : () => keyCallbackTask.done ( IGNORE )
217
+ , reject : () => keyCallbackTask.done ( IGNORE )
194
218
  , value : item
195
219
  , key : k
196
220
  , breadcrumbs : br
@@ -202,6 +226,7 @@ function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
202
226
  }) // objectCallbackTask complete
203
227
 
204
228
  keyCallbackTask.onComplete ( value => {
229
+ pending.delete ( keyTag )
205
230
  if ( value == IGNORE ) {
206
231
  executeCallback.promises[i].done ( 'ignore key' )
207
232
  return