@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 +11 -0
- package/README.md +31 -2
- package/package.json +1 -1
- package/src/main.js +87 -43
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
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
|
-
,
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
,
|
|
133
|
-
,
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|