@peter.naydenov/walk-async 6.0.0 → 7.0.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 +34 -2
- package/Migration.guide.md +274 -0
- package/README.md +501 -311
- package/dist/walk-async.cjs +1 -1
- package/dist/walk-async.esm.mjs +1 -1
- package/dist/walk-async.umd.js +1 -1
- package/package.json +19 -8
- package/src/copyObject.js +145 -0
- package/src/createCallbackArgs.js +15 -0
- package/src/findType.js +28 -0
- package/src/finish.js +38 -0
- package/src/ignore.js +24 -0
- package/src/main.js +230 -285
- package/src/pass.js +36 -0
- package/src/runCallback.js +55 -0
- package/src/setKey.js +12 -0
- package/src/validateForInsertion.js +12 -0
- package/types/copyObject.d.ts +8 -0
- package/types/copyObject.d.ts.map +1 -0
- package/types/createCallbackArgs.d.ts +32 -0
- package/types/createCallbackArgs.d.ts.map +1 -0
- package/types/findType.d.ts +3 -0
- package/types/findType.d.ts.map +1 -0
- package/types/finish.d.ts +25 -0
- package/types/finish.d.ts.map +1 -0
- package/types/ignore.d.ts +10 -0
- package/types/ignore.d.ts.map +1 -0
- package/types/main.d.ts +147 -61
- package/types/main.d.ts.map +1 -1
- package/types/pass.d.ts +23 -0
- package/types/pass.d.ts.map +1 -0
- package/types/runCallback.d.ts +3 -0
- package/types/runCallback.d.ts.map +1 -0
- package/types/setKey.d.ts +3 -0
- package/types/setKey.d.ts.map +1 -0
- package/types/validateForInsertion.d.ts +3 -0
- package/types/validateForInsertion.d.ts.map +1 -0
- package/rollup.config.js +0 -41
- package/tsconfig.json +0 -11
- package/types/src/main.d.ts +0 -35
- package/types/src/main.d.ts.map +0 -1
- package/vitest.config.js +0 -8
package/src/main.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
+
"use strict"
|
|
1
2
|
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Walk-async
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* add a polyfill for 'generators'.
|
|
6
|
+
* Walk through javascript data structures with callbacks for transforming values,
|
|
7
|
+
* controlling traversal, and optionally building a copy.
|
|
8
|
+
* Nested containers are processed iteratively through a work queue.
|
|
9
9
|
*
|
|
10
10
|
* History notes:
|
|
11
11
|
* - Walk-async was born on September 18th, 2022
|
|
12
12
|
* - Converted to ES module on January 1st, 2024
|
|
13
|
-
*
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
import findType from "./findType.js";
|
|
18
|
+
import copyObject from "./copyObject.js";
|
|
19
|
+
import createCallbackArgs from "./createCallbackArgs.js";
|
|
20
|
+
import PASS, { isPass } from "./pass.js";
|
|
21
|
+
import IGNORE, { isIgnore } from "./ignore.js";
|
|
22
|
+
import runCallback from "./runCallback.js";
|
|
23
|
+
import { isFinish } from "./finish.js";
|
|
24
|
+
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* - resolving a plain object or array → walk continues into it with the other callback applied to its children;
|
|
23
|
-
*
|
|
24
|
-
* You must call `resolve` or `reject` on every code path — otherwise the walk promise never settles
|
|
25
|
-
* (use the `timeout` option to turn that silent hang into a rejection with diagnostics).
|
|
27
|
+
* Explicitly settle a callback with its current value or a replacement.
|
|
28
|
+
* Promises supplied to resolve are awaited before traversal continues.
|
|
26
29
|
*
|
|
27
30
|
* @callback Resolve
|
|
28
31
|
* @param {*} value
|
|
@@ -30,7 +33,8 @@ import askForPromise from "ask-for-promise"
|
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
36
|
+
* Drop the current key or entire branch. Retained for v6 callbacks.
|
|
37
|
+
* To reject the walk itself, throw or return a rejected promise.
|
|
34
38
|
*
|
|
35
39
|
* @callback Reject
|
|
36
40
|
* @param {*} [reason]
|
|
@@ -38,333 +42,274 @@ import askForPromise from "ask-for-promise"
|
|
|
38
42
|
*/
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
45
|
+
* Private sentinel returned by IGNORE().
|
|
46
|
+
*
|
|
47
|
+
* @typedef {symbol} IgnoreToken
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Call and return the result to drop the current key or entire branch.
|
|
52
|
+
*
|
|
53
|
+
* @callback IgnoreFunction
|
|
54
|
+
* @returns {IgnoreToken}
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Call with no arguments to keep the current value, or pass a replacement.
|
|
59
|
+
* Supplied replacement promises are awaited before traversal continues.
|
|
60
|
+
* Return the instruction from `objectCallback` to skip `keyCallback` on
|
|
61
|
+
* its immediate properties. Nested objects and arrays continue normally.
|
|
62
|
+
*
|
|
63
|
+
* @callback PassFunction
|
|
64
|
+
* @param {*} [value]
|
|
65
|
+
* @returns {import('./pass.js').PassInstruction}
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stop the walk, optionally including a final value or branch.
|
|
70
|
+
* Supplied replacement promises are awaited before traversal continues.
|
|
71
|
+
*
|
|
72
|
+
* @callback FinishFunction
|
|
73
|
+
* @param {*} [value]
|
|
74
|
+
* @returns {import('./finish.js').FinishInstruction}
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Shared arguments received by `keyCallback` and `objectCallback`.
|
|
42
79
|
*
|
|
43
80
|
* @typedef {object} CallbackArgs
|
|
44
81
|
* @property {*} value - The current value being processed.
|
|
45
82
|
* @property {string} key - Property key as a string.
|
|
46
|
-
* @property {string} breadcrumbs
|
|
47
|
-
* @property {
|
|
48
|
-
* @property {
|
|
83
|
+
* @property {string} [breadcrumbs] - Slash-delimited path including the current key. Absent when `settings.breadcrumbs` is false.
|
|
84
|
+
* @property {ReadonlyArray<string>} [parentPath] - Read-only path to the parent, excluding the current key. Absent when `settings.parentPath` is false.
|
|
85
|
+
* @property {Resolve} resolve - Reading this field opts into explicit settlement with resolve(value) or reject().
|
|
86
|
+
* @property {Reject} reject - Drop the current key or branch; does not reject the walk promise.
|
|
87
|
+
* @property {FinishFunction} FINISH - Return FINISH() to omit the current value and stop, or FINISH(value) to include a final value or branch.
|
|
88
|
+
* @property {IgnoreFunction} IGNORE - Return IGNORE() from the callback to drop the current key from the result.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {CallbackArgs & { isFinished: boolean }} KeyCallbackArgs
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @typedef {CallbackArgs & { PASS: PassFunction }} ObjectCallbackArgs
|
|
49
97
|
*/
|
|
50
98
|
|
|
51
99
|
/**
|
|
52
|
-
* Called once per primitive property
|
|
100
|
+
* Called once per primitive property unless its immediate parent returns
|
|
101
|
+
* `PASS()` or `PASS(value)` from `objectCallback`. Types: string, number, bigint, boolean,
|
|
53
102
|
* symbol, null, undefined, function, Date, RegExp, Map, Set, WeakMap,
|
|
54
|
-
* WeakSet, ArrayBuffer, DataView, typed arrays, DOM nodes
|
|
103
|
+
* WeakSet, ArrayBuffer, DataView, typed arrays, DOM nodes.
|
|
55
104
|
*
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
* -
|
|
59
|
-
* -
|
|
105
|
+
* Return the new value to store, or `IGNORE()` to drop the key:
|
|
106
|
+
* - return a primitive (or a built-in like `Date`/`Map`/`Set`) → stored as-is by reference;
|
|
107
|
+
* - return a plain object or array → walk continues into it with the other callback applied to its children;
|
|
108
|
+
* - return `IGNORE()` → that key is dropped from the result;
|
|
109
|
+
* - return `FINISH()` → omit the current key and stop the entire walk;
|
|
110
|
+
* - return `FINISH(value)` → include the supplied value directly and stop, without descending into it.
|
|
111
|
+
* isFinished is true for keys in a final branch selected by objectCallback.
|
|
112
|
+
* With settings.copy false, no values are stored; returned containers
|
|
113
|
+
* still control what gets visited.
|
|
60
114
|
*
|
|
61
115
|
* @callback KeyCallback
|
|
62
|
-
* @param {
|
|
116
|
+
* @param {KeyCallbackArgs} args
|
|
63
117
|
* @param {...*} rest - Any extra arguments passed to `walk()` are forwarded to the callback.
|
|
64
|
-
* @returns {
|
|
118
|
+
* @returns {* | Promise<*>}
|
|
65
119
|
*/
|
|
66
120
|
|
|
67
121
|
/**
|
|
68
122
|
* Called once per object or array property, including the root.
|
|
69
|
-
* The
|
|
70
|
-
* -
|
|
71
|
-
* -
|
|
72
|
-
* -
|
|
123
|
+
* The returned value becomes the new value at that key:
|
|
124
|
+
* - return an object or array → walk continues into it with the other callbacks;
|
|
125
|
+
* - return a primitive → passed to `keyCallback` when active, otherwise stored as-is;
|
|
126
|
+
* - return `IGNORE()` → the key is dropped from the result.
|
|
127
|
+
* - return `PASS()` → keep the current object/array without its immediate key callbacks; nested callbacks continue normally.
|
|
128
|
+
* - return `PASS(value)` → use the replacement with the same rule; simple replacements are stored directly.
|
|
129
|
+
* - return `FINISH()` → omit the branch and stop the entire walk.
|
|
130
|
+
* - return `FINISH(value)` → process only the final branch with keyCallback and isFinished true, suppressing later object callbacks; without keyCallback, include it directly.
|
|
131
|
+
* With settings.copy false, returned values control traversal without
|
|
132
|
+
* being stored in a result or assigned to the source.
|
|
73
133
|
*
|
|
74
134
|
* @callback ObjectCallback
|
|
75
|
-
* @param {
|
|
135
|
+
* @param {ObjectCallbackArgs} args
|
|
76
136
|
* @param {...*} rest
|
|
77
|
-
* @returns {
|
|
137
|
+
* @returns {* | Promise<*>}
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @typedef {object} Settings
|
|
142
|
+
* @property {boolean} [copy] - Defaults to true. Only false disables result creation; callbacks still run and walk resolves undefined.
|
|
143
|
+
* @property {boolean} [breadcrumbs] - Defaults to true. Only false disables breadcrumbs preparation.
|
|
144
|
+
* @property {boolean} [parentPath] - Defaults to true. Only false disables parent-path preparation.
|
|
145
|
+
* @property {boolean} [detectCycles] - Defaults to true. Only false disables circular-reference checks and their bookkeeping; visited data and replacements must then be acyclic or callbacks must prune cyclic branches.
|
|
78
146
|
*/
|
|
79
147
|
|
|
80
148
|
/**
|
|
81
149
|
* @typedef {object} Options
|
|
82
|
-
* @property {*} data - Required. Any JS data structure that will be
|
|
150
|
+
* @property {*} data - Required. Any JS data structure that will be walked.
|
|
83
151
|
* @property {KeyCallback} [keyCallback] - Optional. Executed on each primitive property.
|
|
84
152
|
* @property {ObjectCallback} [objectCallback] - Optional. Executed on each object/array property, including the root.
|
|
85
|
-
* @property {number} [timeout]
|
|
153
|
+
* @property {number} [timeout] - Optional milliseconds. Reject the walk with pending-callback diagnostics when time expires.
|
|
154
|
+
* @property {Settings} [settings] - Optional. Disable copying, unused callback metadata, or cycle detection with false.
|
|
86
155
|
*/
|
|
87
156
|
|
|
88
157
|
|
|
89
158
|
/**
|
|
90
159
|
* Walk-async
|
|
91
160
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* The deep copy of the original data is mostly a side-effect: the real
|
|
98
|
-
* power is the ability to do all your modifications in one pass, with full
|
|
99
|
-
* freedom inside a single callback — now with async work, awaits, and
|
|
100
|
-
* timeouts supported.
|
|
161
|
+
* Walks through a deep JavaScript data structure, building a copy by default.
|
|
162
|
+
* Two optional callbacks can mask, filter, substitute, or collect values.
|
|
163
|
+
* Callback results are awaited in traversal order; thrown errors and rejected promises reject the walk.
|
|
164
|
+
* Set settings.copy to false to walk without building a result.
|
|
165
|
+
* By default, circular references point to the ancestor copy without visiting its contents again.
|
|
101
166
|
*
|
|
102
167
|
* @function walk
|
|
103
|
-
* @param {Options} options - Required. Object with
|
|
168
|
+
* @param {Options} options - Required. Object with `data`, optional callbacks, and optional `settings`.
|
|
104
169
|
* @param {...*} args - Optional. Additional arguments forwarded to both callbacks.
|
|
105
|
-
* @returns {Promise<*>} -
|
|
170
|
+
* @returns {Promise<*>} - Resolves to the created result, or undefined when settings.copy is false.
|
|
106
171
|
* @example
|
|
107
|
-
*
|
|
172
|
+
* let result = await walk ({
|
|
108
173
|
* data: someData,
|
|
109
174
|
* keyCallback: keyCallbackFn,
|
|
110
175
|
* objectCallback: objectCallbackFn
|
|
111
176
|
* })
|
|
177
|
+
*
|
|
178
|
+
* // Note: objectCallback is executed before keyCallback.
|
|
179
|
+
* // If you modify an object with objectCallback, keyCallback will be
|
|
180
|
+
* // executed on the result of objectCallback.
|
|
112
181
|
*/
|
|
113
|
-
function walk ({
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
182
|
+
async function walk (options,...args) {
|
|
183
|
+
const pending = new Set ();
|
|
184
|
+
const task = traverse ( options, pending, ...args );
|
|
185
|
+
if ( options.timeout == null ) return task
|
|
186
|
+
|
|
187
|
+
let timer;
|
|
188
|
+
const guard = new Promise ( ( resolve, reject ) => {
|
|
189
|
+
timer = setTimeout ( () => {
|
|
190
|
+
const stuck = [...pending].map ( name => `\n - ${name}` ).join ( '' );
|
|
191
|
+
reject ( new Error ( `walk-async: timed out after ${options.timeout}ms; callbacks still pending:${stuck}` ) )
|
|
192
|
+
}, options.timeout )
|
|
193
|
+
})
|
|
194
|
+
try {
|
|
195
|
+
return await Promise.race ([ task, guard ])
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
clearTimeout ( timer )
|
|
199
|
+
}
|
|
200
|
+
} // walk func.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async function traverse ( options, pending, ...args ) {
|
|
119
205
|
let
|
|
120
|
-
|
|
206
|
+
{ data:origin, keyCallback, objectCallback, settings:config } = options
|
|
207
|
+
, type = findType ( origin )
|
|
121
208
|
, result
|
|
122
209
|
, extend = []
|
|
123
|
-
,
|
|
124
|
-
,
|
|
125
|
-
,
|
|
126
|
-
,
|
|
127
|
-
|
|
128
|
-
|
|
210
|
+
, ancestors
|
|
211
|
+
, parent
|
|
212
|
+
, hasCallbacks = !!( keyCallback || objectCallback )
|
|
213
|
+
, settings = {
|
|
214
|
+
copy : config?.copy !== false
|
|
215
|
+
, breadcrumbs : hasCallbacks && config?.breadcrumbs !== false
|
|
216
|
+
, parentPath : hasCallbacks && config?.parentPath !== false
|
|
217
|
+
, detectCycles : config?.detectCycles !== false
|
|
218
|
+
}
|
|
219
|
+
, breadcrumbs = settings.breadcrumbs ? 'root' : undefined
|
|
220
|
+
, parentPath = settings.parentPath && type !== 'simple' ? Object.freeze ([ 'root' ]) : undefined
|
|
221
|
+
, pass = false
|
|
222
|
+
, control = { finished:false, isFinished:false, restart:false }
|
|
223
|
+
, cb = [ keyCallback, objectCallback ]
|
|
129
224
|
;
|
|
130
225
|
|
|
131
226
|
if ( type !== 'simple' && objectCallback ) { // Root object callback. Executed before the result is allocated, so it can replace the root with anything.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
pending.delete ( `objectCallback at 'root'` )
|
|
145
|
-
if ( item === IGNORE ) {
|
|
146
|
-
end.done ( ( type === 'array' ) ? [] : {} )
|
|
147
|
-
return
|
|
227
|
+
const rootParent = settings.parentPath ? Object.freeze ( [] ) : undefined
|
|
228
|
+
const callbackArgs = createCallbackArgs ( origin, 'root', IGNORE, breadcrumbs, rootParent, settings )
|
|
229
|
+
callbackArgs.PASS = PASS
|
|
230
|
+
const replacement = await runCallback ( objectCallback, callbackArgs, pending, 'objectCallback', ...args )
|
|
231
|
+
if ( isFinish ( replacement ) ) {
|
|
232
|
+
if ( !replacement.hasValue ) {
|
|
233
|
+
if ( !settings.copy ) return
|
|
234
|
+
return ( type === 'array' ) ? [] : {}
|
|
235
|
+
}
|
|
236
|
+
if ( !keyCallback ) return settings.copy ? replacement.value : undefined
|
|
237
|
+
control.isFinished = true
|
|
238
|
+
origin = replacement.value
|
|
148
239
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
goNext ( extend, result, end )
|
|
160
|
-
})
|
|
161
|
-
break
|
|
162
|
-
case 'simple' :
|
|
163
|
-
end.done ( item )
|
|
164
|
-
} // switch type
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
if ( timeout == null ) return end.promise
|
|
168
|
-
|
|
169
|
-
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.
|
|
170
|
-
EXPIRED = Symbol ( 'expired___' )
|
|
171
|
-
, guard = askForPromise ()
|
|
172
|
-
;
|
|
173
|
-
end.timeout ( timeout, EXPIRED )
|
|
174
|
-
end.onComplete ( res => {
|
|
175
|
-
if ( res !== EXPIRED ) { guard.done ( res ); return }
|
|
176
|
-
const stuck = [...pending].map ( name => `\n - ${name}` ).join ( '' )
|
|
177
|
-
guard.cancel ( new Error ( `walk-async: timed out after ${timeout}ms; callbacks still pending:${stuck}` ) )
|
|
178
|
-
})
|
|
179
|
-
return guard.promise
|
|
180
|
-
} // walk func.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
async function goNext ( extend, result, end ) {
|
|
185
|
-
for await ( const plus of extend ) {
|
|
186
|
-
await plus.next ()
|
|
240
|
+
else if ( isIgnore ( replacement ) ) {
|
|
241
|
+
if ( !settings.copy ) return
|
|
242
|
+
return ( type === 'array' ) ? [] : {}
|
|
243
|
+
}
|
|
244
|
+
else if ( isPass ( replacement ) ) {
|
|
245
|
+
pass = true
|
|
246
|
+
if ( replacement.hasValue ) origin = replacement.value
|
|
247
|
+
}
|
|
248
|
+
else origin = replacement
|
|
249
|
+
type = findType ( origin )
|
|
187
250
|
}
|
|
188
|
-
end.done ( result )
|
|
189
|
-
} // goNext func.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
function findType ( x ) {
|
|
194
|
-
if ( x == null ) return 'simple' // null and undefined
|
|
195
|
-
if ( x.nodeType ) return 'simple' // DOM node
|
|
196
|
-
if ( x instanceof Array ) return 'array'
|
|
197
|
-
if ( typeof x === 'object' ) {
|
|
198
|
-
// Built-in object types whose data lives outside the own-enumerable-string-key
|
|
199
|
-
// model that walk uses. Treated as 'simple' so the value is preserved by
|
|
200
|
-
// reference (same contract as functions and DOM nodes).
|
|
201
|
-
if ( x instanceof Date ) return 'simple'
|
|
202
|
-
if ( x instanceof RegExp ) return 'simple'
|
|
203
|
-
if ( x instanceof Map ) return 'simple'
|
|
204
|
-
if ( x instanceof Set ) return 'simple'
|
|
205
|
-
if ( x instanceof WeakMap ) return 'simple'
|
|
206
|
-
if ( x instanceof WeakSet ) return 'simple'
|
|
207
|
-
if ( x instanceof ArrayBuffer ) return 'simple'
|
|
208
|
-
if ( x instanceof DataView ) return 'simple'
|
|
209
|
-
if ( ArrayBuffer.isView ( x ) ) return 'simple' // Typed arrays (Uint8Array, Float32Array, ...)
|
|
210
|
-
return 'object'
|
|
211
|
-
}
|
|
212
|
-
return 'simple' // number, bigint, string, boolean, symbol, function
|
|
213
|
-
} // findType func.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
async function* generateList ( data, location, ex, cb, breadcrumbs, args ) {
|
|
218
|
-
yield await copyObject ( data , location, ex, cb, breadcrumbs, ...args )
|
|
219
|
-
} // generateList func.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
function validateForInsertion ( k, result ) {
|
|
223
|
-
const inArray = result instanceof Array;
|
|
224
|
-
if ( !inArray ) return false
|
|
225
|
-
const isNumber = !isNaN ( k );
|
|
226
|
-
if ( isNumber ) return true
|
|
227
|
-
else return false
|
|
228
|
-
} // insertInArray func.
|
|
229
251
|
|
|
252
|
+
switch ( type ) {
|
|
253
|
+
case 'array' :
|
|
254
|
+
result = settings.copy ? [] : undefined
|
|
255
|
+
parent = await copyObject ( origin, result, extend, cb, breadcrumbs, settings, control, parentPath, pass, undefined, ancestors, pending, ...args )
|
|
256
|
+
break
|
|
257
|
+
case 'object' :
|
|
258
|
+
result = settings.copy ? {} : undefined
|
|
259
|
+
parent = await copyObject ( origin, result, extend, cb, breadcrumbs, settings, control, parentPath, pass, undefined, ancestors, pending, ...args )
|
|
260
|
+
break
|
|
261
|
+
case 'simple' :
|
|
262
|
+
return settings.copy ? origin : undefined
|
|
263
|
+
} // switch type
|
|
264
|
+
|
|
265
|
+
for ( let index = 0; index < extend.length && !control.finished; ) {
|
|
266
|
+
const job = extend[index];
|
|
267
|
+
extend[index] = undefined
|
|
268
|
+
control.restart = false
|
|
269
|
+
if ( settings.detectCycles ) ancestors = moveParents ( parent, job.parent, ancestors )
|
|
270
|
+
const path = settings.parentPath ? Object.freeze ([ ...job.parentPath, job.key ]) : undefined;
|
|
271
|
+
parent = await copyObject ( job.data, job.location, extend, cb, job.breadcrumbs, settings, control, path, job.pass, job.parent, ancestors, pending, ...args )
|
|
272
|
+
index = control.restart ? 0 : index + 1
|
|
273
|
+
}
|
|
274
|
+
return result
|
|
275
|
+
} // traverse func.
|
|
230
276
|
|
|
231
277
|
|
|
232
|
-
// Plain assignment of a '__proto__' key triggers the inherited setter and
|
|
233
|
-
// replaces the prototype of 'target' instead of creating an own property.
|
|
234
|
-
function setKey ( target, k, value ) {
|
|
235
|
-
if ( k === '__proto__' ) Object.defineProperty ( target, k, { value, enumerable:true, writable:true, configurable:true })
|
|
236
|
-
else target[k] = value
|
|
237
|
-
} // setKey func.
|
|
238
278
|
|
|
279
|
+
function moveParents ( current, target, ancestors ) {
|
|
280
|
+
// Shallow branches need only a short parent-chain scan. Queue order
|
|
281
|
+
// moves through increasing depths, so the lookup is built once.
|
|
282
|
+
if ( !ancestors ) {
|
|
283
|
+
if ( target.depth < 32 ) return
|
|
284
|
+
ancestors = new WeakMap()
|
|
285
|
+
for ( let parent = target; parent; parent = parent.parent ) {
|
|
286
|
+
ancestors.set ( parent.data, parent )
|
|
287
|
+
}
|
|
288
|
+
return ancestors
|
|
289
|
+
}
|
|
239
290
|
|
|
291
|
+
const pending = [];
|
|
240
292
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
, hasObjectCallback = ( type !== 'simple' && objectCallback != null )
|
|
254
|
-
, objectCallbackTask = askForPromise ()
|
|
255
|
-
, keyCallbackTask = askForPromise ()
|
|
256
|
-
, finishWithCallbacks = askForPromise ()
|
|
257
|
-
, resultIsArray = (findType (result) === 'array')
|
|
258
|
-
, keyNumber = !isNaN ( k )
|
|
259
|
-
, IGNORE = Symbol ( 'ignore___' )
|
|
260
|
-
, br = `${breadcrumbs}/${k}`
|
|
261
|
-
, objectTag = `objectCallback at '${br}'`
|
|
262
|
-
, keyTag = `keyCallback at '${br}'`
|
|
263
|
-
;
|
|
264
|
-
|
|
265
|
-
if ( hasObjectCallback ) {
|
|
266
|
-
pending.add ( objectTag )
|
|
267
|
-
objectCallback ({
|
|
268
|
-
resolve : objectCallbackTask.done
|
|
269
|
-
, reject : () => objectCallbackTask.done ( IGNORE )
|
|
270
|
-
, value : item
|
|
271
|
-
, key : k
|
|
272
|
-
, breadcrumbs : br
|
|
273
|
-
}, ...args )
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
objectCallbackTask.done ( '$$cancel' )
|
|
277
|
-
}
|
|
293
|
+
// Keep only the target branch's ancestors in the lookup. Both chains
|
|
294
|
+
// share the root, so they meet before either chain runs out.
|
|
295
|
+
while ( current !== target ) {
|
|
296
|
+
if ( current.depth >= target.depth ) {
|
|
297
|
+
ancestors.delete ( current.data )
|
|
298
|
+
current = current.parent
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
pending.push ( target )
|
|
302
|
+
target = target.parent
|
|
303
|
+
}
|
|
304
|
+
}
|
|
278
305
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if ( res !== '$$cancel' ) {
|
|
286
|
-
item = res
|
|
287
|
-
type = findType ( item )
|
|
288
|
-
}
|
|
289
|
-
if ( item == IGNORE ) {
|
|
290
|
-
// Object callback rejected this key. Skip the rest of
|
|
291
|
-
// the per-key chain; resolve the remaining tasks so
|
|
292
|
-
// nothing leaks.
|
|
293
|
-
executeCallback.promises[i].done ( 'ignore object' )
|
|
294
|
-
keyCallbackTask.done ( '$$cancel' )
|
|
295
|
-
finishWithCallbacks.done ()
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
if ( type === 'simple' ) {
|
|
299
|
-
if ( !keyCallback ) {
|
|
300
|
-
keyCallbackTask.done ( '$$noUpdates' )
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
pending.add ( keyTag )
|
|
304
|
-
keyCallback ({
|
|
305
|
-
resolve : keyCallbackTask.done
|
|
306
|
-
, reject : () => keyCallbackTask.done ( IGNORE )
|
|
307
|
-
, value : item
|
|
308
|
-
, key : k
|
|
309
|
-
, breadcrumbs : br
|
|
310
|
-
}, ...args );
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
keyCallbackTask.done ( '$$cancel' )
|
|
314
|
-
}
|
|
315
|
-
}) // objectCallbackTask complete
|
|
316
|
-
|
|
317
|
-
keyCallbackTask.onComplete ( value => {
|
|
318
|
-
pending.delete ( keyTag )
|
|
319
|
-
if ( value == IGNORE ) {
|
|
320
|
-
// Key callback rejected this key. Finish the
|
|
321
|
-
// per-key chain so finishWithCallbacks doesn't leak.
|
|
322
|
-
executeCallback.promises[i].done ( 'ignore key' )
|
|
323
|
-
finishWithCallbacks.done ()
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
if ( value === '$$cancel' ) {
|
|
327
|
-
finishWithCallbacks.done ()
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
if ( value !== '$$noUpdates' ) {
|
|
331
|
-
item = value
|
|
332
|
-
type = findType ( item )
|
|
333
|
-
}
|
|
334
|
-
if ( type === 'simple' ) {
|
|
335
|
-
const canInsert = validateForInsertion ( k, result )
|
|
336
|
-
if ( canInsert ) result.push ( item )
|
|
337
|
-
else setKey ( result, k, item )
|
|
338
|
-
executeCallback.promises[i].done ('key')
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
finishWithCallbacks.done ()
|
|
343
|
-
}) // keyCallbackTask complete
|
|
344
|
-
|
|
345
|
-
finishWithCallbacks.onComplete ( () => {
|
|
346
|
-
if ( type === 'object' ) {
|
|
347
|
-
const newObject = {};
|
|
348
|
-
if ( resultIsArray && keyNumber ) result.push ( newObject )
|
|
349
|
-
else setKey ( result, k, newObject )
|
|
350
|
-
extend.push ( generateList( item, newObject, extend, cb, br, args ) )
|
|
351
|
-
executeCallback.promises[i].done ('object')
|
|
352
|
-
}
|
|
353
|
-
if ( type === 'array' ) {
|
|
354
|
-
const newArray = [];
|
|
355
|
-
if ( resultIsArray && keyNumber ) result.push ( newArray )
|
|
356
|
-
else setKey ( result, k, newArray )
|
|
357
|
-
extend.push ( generateList( item, newArray, extend, cb, br, args ) )
|
|
358
|
-
executeCallback.promises[i].done ('array')
|
|
359
|
-
}
|
|
360
|
-
})
|
|
361
|
-
}) // forEach k
|
|
362
|
-
executeCallback.onComplete ( r => finish.done () )
|
|
363
|
-
return finish.promise
|
|
364
|
-
} // copyObject func.
|
|
306
|
+
for ( let index = pending.length - 1; index >= 0; index-- ) {
|
|
307
|
+
const parent = pending[index];
|
|
308
|
+
ancestors.set ( parent.data, parent )
|
|
309
|
+
}
|
|
310
|
+
return ancestors
|
|
311
|
+
} // moveParents func.
|
|
365
312
|
|
|
366
313
|
|
|
367
314
|
|
|
368
315
|
export default walk
|
|
369
|
-
|
|
370
|
-
|
package/src/pass.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const instructions = new WeakSet ();
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Private instruction created by the PASS helper.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} PassInstruction
|
|
10
|
+
* @property {*} value
|
|
11
|
+
* @property {boolean} hasValue
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Use the current value, or a supplied replacement, without its
|
|
16
|
+
* immediate key callbacks. Nested object callbacks continue normally.
|
|
17
|
+
*
|
|
18
|
+
* @param {*} [value] - Replacement value. Omit to keep the current value.
|
|
19
|
+
* @returns {PassInstruction}
|
|
20
|
+
*/
|
|
21
|
+
function PASS ( value ) {
|
|
22
|
+
const instruction = Object.freeze ({ value, hasValue:arguments.length > 0 });
|
|
23
|
+
instructions.add ( instruction )
|
|
24
|
+
return instruction
|
|
25
|
+
} // PASS func.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function isPass ( value ) {
|
|
30
|
+
return instructions.has ( value )
|
|
31
|
+
} // isPass func.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export { isPass }
|
|
36
|
+
export default PASS
|