@peter.naydenov/walk-async 6.1.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/src/main.js CHANGED
@@ -1,28 +1,31 @@
1
+ "use strict"
1
2
 
2
3
  /**
3
- * walk-async
4
+ * Walk-async
4
5
  *
5
- * Alternative of deep-copy that provides much better control during creation of immutable
6
- * copies of javascript data structures.
7
- * Library is using 'generator functions'. If support for old browsers is required,
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
- import askForPromise from "ask-for-promise"
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
- * Resolve the callback with the new value to store at the current key:
21
- * - resolving a primitive (or a built-in like `Date` / `Map` / `Set`) → stored as-is by reference;
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
- * Reject the callback. The current key is dropped from the result.
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
- * Arguments object received by both `keyCallback` and `objectCallback`.
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 - Slash-delimited path to the current key, starting with `root` (e.g. `"root/props/age"`).
47
- * @property {Resolve} resolve - Resolve the callback with the new value (see `Resolve` typedef).
48
- * @property {Reject} reject - Reject the callback to drop the current key (see `Reject` typedef).
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 (string, number, bigint, boolean,
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
- * Resolve with the new value to store, or reject to drop the key:
57
- * - resolve a primitive (or a built-in like `Date` / `Map` / `Set`) → stored as-is by reference;
58
- * - resolve a plain object or array → walk continues into it with the other callback applied to its children;
59
- * - reject → that key is dropped from the result.
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 {CallbackArgs} args
116
+ * @param {KeyCallbackArgs} args
63
117
  * @param {...*} rest - Any extra arguments passed to `walk()` are forwarded to the callback.
64
- * @returns {void}
118
+ * @returns {* | Promise<*>}
65
119
  */
66
120
 
67
121
  /**
68
122
  * Called once per object or array property, including the root.
69
- * The resolved value becomes the new value at that key:
70
- * - resolve an object or array → walk continues into it with the other callbacks applied to its children;
71
- * - resolve a primitive → it is stored at that key as-is, and walk does not descend into it (primitives have no children to walk);
72
- * - reject → the key is dropped from the result.
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 {CallbackArgs} args
135
+ * @param {ObjectCallbackArgs} args
76
136
  * @param {...*} rest
77
- * @returns {void}
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 copied.
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] - Optional. Milliseconds. When set, the promise is rejected if callbacks do not resolve in time. Error lists the pending callbacks.
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
- * Async sibling of `@peter.naydenov/walk`. Visits every member of a deep
93
- * JavaScript data structure once, in a single pass, and runs two optional
94
- * callbacks during the visit that can mask, filter, or substitute values
95
- * as the result is built.
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 required `data` property, two optional callback functions (`keyCallback`, `objectCallback`), and an optional `timeout`.
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<*>} - A promise that resolves to the immutable copy of `options.data` (with the callbacks' transformations applied).
170
+ * @returns {Promise<*>} - Resolves to the created result, or undefined when settings.copy is false.
106
171
  * @example
107
- * const result = await walk ({
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
- data:origin
115
- , objectCallback = null
116
- , keyCallback = null
117
- , timeout = null
118
- }, ...args ) {
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
- type = findType ( origin )
206
+ { data:origin, keyCallback, objectCallback, settings:config } = options
207
+ , type = findType ( origin )
121
208
  , result
122
209
  , extend = []
123
- , breadcrumbs = 'root'
124
- , pending = new Set () // Callbacks in flight. Read on timeout to report which ones never resolved.
125
- , cb = [ keyCallback, objectCallback, pending ]
126
- , end = askForPromise ()
127
- , rootTask = askForPromise ()
128
- , IGNORE = Symbol ( 'ignore___' )
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
- pending.add ( `objectCallback at 'root'` )
133
- objectCallback ({
134
- resolve : rootTask.done
135
- , reject : () => rootTask.done ( IGNORE )
136
- , value : origin
137
- , key : 'root'
138
- , breadcrumbs
139
- }, ...args )
140
- }
141
- else rootTask.done ( origin )
142
-
143
- rootTask.onComplete ( item => {
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
- switch ( findType ( item ) ) {
150
- case 'array' :
151
- result = []
152
- copyObject ( item, result, extend, cb, breadcrumbs, ...args )
153
- .then ( () => goNext ( extend, result, end ))
154
- break
155
- case 'object' :
156
- result = {}
157
- copyObject ( item, result, extend, cb, breadcrumbs, ...args )
158
- .then ( () => {
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
- function copyObject ( origin, result, extend, cb, breadcrumbs, ...args ) {
242
- let
243
- [ keyCallback, objectCallback, pending ] = cb
244
- , keys = Object.keys ( origin )
245
- , executeCallback = askForPromise ( keys )
246
- , finish = askForPromise ()
247
- ;
248
-
249
- keys.forEach ( (k,i) => {
250
- let
251
- type = findType(origin[k])
252
- , item = origin[k]
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
- objectCallbackTask.onComplete ( res => {
280
- pending.delete ( objectTag )
281
- if ( res === '$$cancel' && !keyCallback ) { // deep copy, no callbacks
282
- keyCallbackTask.done ( '$$noUpdates' )
283
- return
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