@bcts/dcbor 1.0.0-alpha.8 → 1.0.0-beta.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/walk.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ *
2
6
  * Tree traversal system for CBOR data structures.
3
7
  *
4
8
  * This module provides a visitor pattern implementation for traversing
@@ -125,16 +129,18 @@ export const asKeyValue = (element: WalkElement): [Cbor, Cbor] | undefined => {
125
129
  };
126
130
 
127
131
  /**
128
- * Visitor function type with state threading.
132
+ * Visitor function type.
129
133
  *
130
- * @template State - The type of state passed through the traversal
134
+ * @template State - The type of state passed into each visit
131
135
  * @param element - The element being visited
132
136
  * @param level - The depth level in the tree (0 = root)
133
137
  * @param edge - Information about the edge leading to this element
134
- * @param state - Current state value
138
+ * @param state - The state value cloned from the parent visit
135
139
  * @returns Tuple of [newState, stopDescent] where:
136
- * - newState: The updated state to pass to subsequent visits
137
- * - stopDescent: If true, don't descend into children of this element
140
+ * - newState: The state to pass into descendants of this element. Each
141
+ * descendant receives an independent clone of `newState`; sibling
142
+ * subtrees do *not* see each other's mutations.
143
+ * - stopDescent: If true, do not descend into children of this element.
138
144
  */
139
145
  export type Visitor<State> = (
140
146
  element: WalkElement,
@@ -143,57 +149,47 @@ export type Visitor<State> = (
143
149
  state: State,
144
150
  ) => [State, boolean];
145
151
 
152
+ /**
153
+ * Clone helper used to give each descendant subtree an independent copy of
154
+ * the post-visit state — mirrors Rust `State: Clone` + `state.clone()` per
155
+ * child in `walk.rs`. Falls back to the value as-is for primitives (which
156
+ * don't need cloning) and uses `structuredClone` for objects.
157
+ */
158
+ const cloneState = <S>(s: S): S => {
159
+ if (s === null) return s;
160
+ const t = typeof s;
161
+ if (t !== "object" && t !== "function") return s;
162
+ // `structuredClone` is a host-provided global available in modern Node
163
+ // (≥ 17) and every modern browser; declare it inline so eslint's
164
+ // `no-undef` is satisfied without a project-wide globals declaration.
165
+ return (globalThis as { structuredClone(v: unknown): unknown }).structuredClone(s) as S;
166
+ };
167
+
146
168
  /**
147
169
  * Walk a CBOR tree, visiting each element with a visitor function.
148
170
  *
149
171
  * The visitor function is called for each element in the tree, in depth-first order.
150
- * State is threaded through the traversal, allowing accumulation of results.
172
+ * State semantics mirror Rust's `walk_internal`:
173
+ *
174
+ * - The visitor's returned `newState` propagates **down** to descendants of
175
+ * the just-visited node only.
176
+ * - Sibling subtrees each receive an independent clone of the parent's
177
+ * post-visit state, so accumulating mutations in one subtree never leak
178
+ * into a sibling.
179
+ * - State changes do not propagate **up**: the public `walk` returns `void`.
151
180
  *
152
181
  * For maps, the visitor is called with:
153
182
  * 1. A 'keyvalue' element containing both key and value
154
183
  * 2. The key individually (if descent wasn't stopped)
155
184
  * 3. The value individually (if descent wasn't stopped)
156
185
  *
157
- * @template State - The type of state to thread through the traversal
186
+ * @template State - The type of state to pass into each visit
158
187
  * @param cbor - The CBOR value to traverse
159
188
  * @param initialState - Initial state value
160
189
  * @param visitor - Function to call for each element
161
- * @returns Final state after traversal
162
- *
163
- * @example
164
- * ```typescript
165
- * // Count all text strings in a structure
166
- * interface CountState { count: number }
167
- *
168
- * const structure = cbor({ name: 'Alice', tags: ['urgent', 'draft'] });
169
- * const result = walk(structure, { count: 0 }, (element, level, edge, state) => {
170
- * if (element.type === 'single' && element.cbor.type === MajorType.Text) {
171
- * return [{ count: state.count + 1 }, false];
172
- * }
173
- * return [state, false];
174
- * });
175
- * console.log(result.count); // 3 (name, urgent, draft)
176
- * ```
177
- *
178
- * @example
179
- * ```typescript
180
- * // Find first occurrence and stop
181
- * const structure = cbor([1, 2, 3, 'found', 5, 6]);
182
- * let found = false;
183
- *
184
- * walk(structure, null, (element, level, edge) => {
185
- * if (element.type === 'single' &&
186
- * element.cbor.type === MajorType.Text &&
187
- * element.cbor.value === 'found') {
188
- * found = true;
189
- * return [null, true]; // Stop descending
190
- * }
191
- * return [null, false];
192
- * });
193
- * ```
194
190
  */
195
- export const walk = <State>(cbor: Cbor, initialState: State, visitor: Visitor<State>): State => {
196
- return walkInternal(cbor, 0, { type: EdgeType.None }, initialState, visitor);
191
+ export const walk = <State>(cbor: Cbor, initialState: State, visitor: Visitor<State>): void => {
192
+ walkInternal(cbor, 0, { type: EdgeType.None }, initialState, visitor);
197
193
  };
198
194
 
199
195
  /**
@@ -207,62 +203,43 @@ function walkInternal<State>(
207
203
  edge: EdgeTypeVariant,
208
204
  state: State,
209
205
  visitor: Visitor<State>,
210
- skipVisit = false,
211
- ): State {
212
- let currentState = state;
213
- let stopDescent = false;
214
-
215
- // Visit the current element (unless skipVisit is true)
216
- if (!skipVisit) {
217
- const element: WalkElement = { type: "single", cbor };
218
- const [newState, stop] = visitor(element, level, edge, currentState);
219
- currentState = newState;
220
- stopDescent = stop;
221
-
222
- // If visitor says to stop descending, return immediately
223
- if (stopDescent) {
224
- return currentState;
225
- }
226
- }
227
-
228
- // Recursively visit children based on CBOR type
229
- // Only container types (Array, Map, Tagged) need special handling; leaf nodes use default
206
+ ): void {
207
+ // Visit the current element.
208
+ const element: WalkElement = { type: "single", cbor };
209
+ const [postVisitState, stop] = visitor(element, level, edge, state);
210
+ if (stop) return;
211
+
212
+ // Recursively visit children based on CBOR type. Each child receives an
213
+ // independent clone of `postVisitState`, matching Rust `state.clone()`.
230
214
  // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
231
215
  switch (cbor.type) {
232
216
  case MajorType.Array:
233
- currentState = walkArray(cbor, level, currentState, visitor);
217
+ walkArray(cbor, level, postVisitState, visitor);
234
218
  break;
235
-
236
219
  case MajorType.Map:
237
- currentState = walkMap(cbor, level, currentState, visitor);
220
+ walkMap(cbor, level, postVisitState, visitor);
238
221
  break;
239
-
240
222
  case MajorType.Tagged:
241
- currentState = walkTagged(cbor, level, currentState, visitor);
223
+ walkTagged(cbor, level, postVisitState, visitor);
242
224
  break;
243
-
244
- // Leaf nodes: Unsigned, Negative, Bytes, Text, Simple
245
225
  default:
246
- // No children to visit
226
+ // Leaf nodes (Unsigned, Negative, Bytes, Text, Simple) have no children.
247
227
  break;
248
228
  }
249
-
250
- return currentState;
251
229
  }
252
230
 
253
231
  /**
254
- * Walk an array's elements.
232
+ * Walk an array's elements. Each element is visited with an independent
233
+ * clone of `parentState`.
255
234
  *
256
235
  * @internal
257
236
  */
258
237
  function walkArray<State>(
259
238
  cbor: CborArrayType,
260
239
  level: number,
261
- state: State,
240
+ parentState: State,
262
241
  visitor: Visitor<State>,
263
- ): State {
264
- let currentState = state;
265
-
242
+ ): void {
266
243
  for (let index = 0; index < cbor.value.length; index++) {
267
244
  const item = cbor.value[index];
268
245
  if (item === undefined) {
@@ -271,246 +248,65 @@ function walkArray<State>(
271
248
  message: `Array element at index ${index} is undefined`,
272
249
  });
273
250
  }
274
- currentState = walkInternal(
251
+ walkInternal(
275
252
  item,
276
253
  level + 1,
277
254
  { type: EdgeType.ArrayElement, index },
278
- currentState,
255
+ cloneState(parentState),
279
256
  visitor,
280
257
  );
281
258
  }
282
-
283
- return currentState;
284
259
  }
285
260
 
286
261
  /**
287
262
  * Walk a map's key-value pairs.
288
263
  *
289
- * For each entry:
290
- * 1. Visit the key-value pair as a semantic unit
291
- * 2. If not stopped, visit the key individually
292
- * 3. If not stopped, visit the value individually
264
+ * Each kv pair receives a clone of `parentState`. If descent isn't stopped,
265
+ * the key and value subtrees receive independent clones of the kv-visit's
266
+ * post-visit state.
293
267
  *
294
268
  * @internal
295
269
  */
296
270
  function walkMap<State>(
297
271
  cbor: CborMapType,
298
272
  level: number,
299
- state: State,
273
+ parentState: State,
300
274
  visitor: Visitor<State>,
301
- ): State {
302
- let currentState = state;
303
-
275
+ ): void {
304
276
  for (const entry of cbor.value.entriesArray) {
305
277
  const { key, value } = entry;
306
278
 
307
- // First, visit the key-value pair as a semantic unit
308
279
  const kvElement: WalkElement = { type: "keyvalue", key, value };
309
- const [kvState, kvStop] = visitor(
280
+ const [kvPostState, kvStop] = visitor(
310
281
  kvElement,
311
282
  level + 1,
312
283
  { type: EdgeType.MapKeyValue },
313
- currentState,
284
+ cloneState(parentState),
314
285
  );
315
- currentState = kvState;
286
+ if (kvStop) continue;
316
287
 
317
- // If not stopped, visit key and value individually
318
- if (!kvStop) {
319
- currentState = walkInternal(key, level + 1, { type: EdgeType.MapKey }, currentState, visitor);
320
-
321
- currentState = walkInternal(
322
- value,
323
- level + 1,
324
- { type: EdgeType.MapValue },
325
- currentState,
326
- visitor,
327
- );
328
- }
288
+ walkInternal(key, level + 1, { type: EdgeType.MapKey }, cloneState(kvPostState), visitor);
289
+ walkInternal(value, level + 1, { type: EdgeType.MapValue }, cloneState(kvPostState), visitor);
329
290
  }
330
-
331
- return currentState;
332
291
  }
333
292
 
334
293
  /**
335
- * Walk a tagged value's content.
294
+ * Walk a tagged value's content. The content visit receives a clone of
295
+ * `parentState`.
336
296
  *
337
297
  * @internal
338
298
  */
339
299
  function walkTagged<State>(
340
300
  cbor: CborTaggedType,
341
301
  level: number,
342
- state: State,
302
+ parentState: State,
343
303
  visitor: Visitor<State>,
344
- ): State {
345
- return walkInternal(cbor.value, level + 1, { type: EdgeType.TaggedContent }, state, visitor);
304
+ ): void {
305
+ walkInternal(
306
+ cbor.value,
307
+ level + 1,
308
+ { type: EdgeType.TaggedContent },
309
+ cloneState(parentState),
310
+ visitor,
311
+ );
346
312
  }
347
-
348
- /**
349
- * Helper: Count all elements in a CBOR tree.
350
- *
351
- * @param cbor - The CBOR value to count
352
- * @returns Total number of elements visited
353
- *
354
- * @example
355
- * ```typescript
356
- * const structure = cbor([1, 2, [3, 4]]);
357
- * const count = countElements(structure);
358
- * console.log(count); // 6 (array, 1, 2, inner array, 3, 4)
359
- * ```
360
- */
361
- export const countElements = (cbor: Cbor): number => {
362
- interface CountState {
363
- count: number;
364
- }
365
-
366
- const result = walk<CountState>(cbor, { count: 0 }, (_element, _level, _edge, state) => {
367
- return [{ count: state.count + 1 }, false];
368
- });
369
-
370
- return result.count;
371
- };
372
-
373
- /**
374
- * Helper: Collect all elements at a specific depth level.
375
- *
376
- * @param cbor - The CBOR value to traverse
377
- * @param targetLevel - The depth level to collect (0 = root)
378
- * @returns Array of CBOR values at the target level
379
- *
380
- * @example
381
- * ```typescript
382
- * const structure = cbor([[1, 2], [3, 4]]);
383
- * const level1 = collectAtLevel(structure, 1);
384
- * // Returns: [[1, 2], [3, 4]]
385
- * const level2 = collectAtLevel(structure, 2);
386
- * // Returns: [1, 2, 3, 4]
387
- * ```
388
- */
389
- export const collectAtLevel = (cbor: Cbor, targetLevel: number): Cbor[] => {
390
- interface CollectState {
391
- items: Cbor[];
392
- }
393
-
394
- const result = walk<CollectState>(cbor, { items: [] }, (element, level, _edge, state) => {
395
- if (level === targetLevel && element.type === "single") {
396
- return [{ items: [...state.items, element.cbor] }, false];
397
- }
398
- return [state, false];
399
- });
400
-
401
- return result.items;
402
- };
403
-
404
- /**
405
- * Helper: Find first element matching a predicate.
406
- *
407
- * @template T - Type of extracted value
408
- * @param cbor - The CBOR value to search
409
- * @param predicate - Function to test each element
410
- * @returns First matching element, or undefined if not found
411
- *
412
- * @example
413
- * ```typescript
414
- * const structure = cbor({ users: [
415
- * { name: 'Alice', age: 30 },
416
- * { name: 'Bob', age: 25 }
417
- * ]});
418
- *
419
- * const bob = findFirst(structure, (element) => {
420
- * if (element.type === 'single' &&
421
- * element.cbor.type === MajorType.Text &&
422
- * element.cbor.value === 'Bob') {
423
- * return true;
424
- * }
425
- * return false;
426
- * });
427
- * ```
428
- */
429
- export const findFirst = (
430
- cbor: Cbor,
431
- predicate: (element: WalkElement) => boolean,
432
- ): Cbor | undefined => {
433
- interface FindState {
434
- found?: Cbor;
435
- }
436
-
437
- const result = walk<FindState>(cbor, {}, (element, _level, _edge, state) => {
438
- if (state.found !== undefined) {
439
- // Already found, stop descending
440
- return [state, true];
441
- }
442
-
443
- if (predicate(element)) {
444
- if (element.type === "single") {
445
- return [{ found: element.cbor }, true]; // Stop after finding
446
- }
447
- // Matched but not a single element, stop anyway
448
- return [state, true];
449
- }
450
-
451
- return [state, false];
452
- });
453
-
454
- return result.found;
455
- };
456
-
457
- /**
458
- * Helper: Collect all text strings in a CBOR tree.
459
- *
460
- * @param cbor - The CBOR value to traverse
461
- * @returns Array of all text string values found
462
- *
463
- * @example
464
- * ```typescript
465
- * const doc = cbor({
466
- * title: 'Document',
467
- * tags: ['urgent', 'draft'],
468
- * author: { name: 'Alice' }
469
- * });
470
- *
471
- * const texts = collectAllText(doc);
472
- * // Returns: ['Document', 'urgent', 'draft', 'Alice']
473
- * ```
474
- */
475
- export const collectAllText = (cbor: Cbor): string[] => {
476
- interface TextState {
477
- texts: string[];
478
- }
479
-
480
- const result = walk<TextState>(cbor, { texts: [] }, (element, _level, _edge, state) => {
481
- if (element.type === "single" && element.cbor.type === MajorType.Text) {
482
- return [{ texts: [...state.texts, element.cbor.value] }, false];
483
- }
484
- return [state, false];
485
- });
486
-
487
- return result.texts;
488
- };
489
-
490
- /**
491
- * Helper: Get the maximum depth of a CBOR tree.
492
- *
493
- * @param cbor - The CBOR value to measure
494
- * @returns Maximum depth (0 for leaf values, 1+ for containers)
495
- *
496
- * @example
497
- * ```typescript
498
- * const flat = cbor([1, 2, 3]);
499
- * console.log(maxDepth(flat)); // 1
500
- *
501
- * const nested = cbor([[[1]]]);
502
- * console.log(maxDepth(nested)); // 3
503
- * ```
504
- */
505
- export const maxDepth = (cbor: Cbor): number => {
506
- interface DepthState {
507
- maxDepth: number;
508
- }
509
-
510
- const result = walk<DepthState>(cbor, { maxDepth: 0 }, (_element, level, _edge, state) => {
511
- const newMaxDepth = Math.max(state.maxDepth, level);
512
- return [{ maxDepth: newMaxDepth }, false];
513
- });
514
-
515
- return result.maxDepth;
516
- };