@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 +5 -0
- package/README.md +31 -2
- package/package.json +1 -1
- package/src/main.js +35 -10
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
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
|
-
,
|
|
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
|
-
|
|
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
|