Skip to content

Flexible and performant utility for traversing javascript objects.

License

Notifications You must be signed in to change notification settings

DevimalPlanet/object-traversal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

object-traversal


Flexible and performant utility for traversing javascript objects.

Installation

npm i object-traversal

âś” Features

  1. Performance
    • Traverses over 20 million nodes per second. (2020 MacBook Air)
    • Around 10 times faster than popular alternatives. (npm run benchmark)
  2. Configurable
    • Tweak traversalOpts for even more speed, traversal order, maxDepth and more.
  3. Zero dependencies
    • Works on both NodeJS and the browser.
  4. Big test coverage
  5. Typescript

Docs

Usage

import { traverse } from 'object-traversal';

traverse(object, callback, opts?);

object

Any instance of javascript object, cyclic or otherwise.

callback

A function that will be called once for each node in the provided root object, including the root object itself.

The callback function has the following signature:

// Callback function signature
export type TraversalCallback = (context: TraversalCallbackContext) => any;

// Callback context
export type TraversalCallbackContext = {
  parent: ArbitraryObject | null; // parent is null when callback is being called on the root `object`
  key: string | null; // key is null when callback is being called on the root `object`
  value: any;
  meta: {
    nodePath?: string | null;
    visitedNodes: WeakSet<ArbitraryObject>;
    depth: number;
  };
};

opts

An optional configuration object. See below for the available options and their default values.

export type TraversalOpts = {
  /**
   * Default: 'depth-first'
   */
  traversalType?: 'depth-first' | 'breadth-first';

  /**
   * Traversal stops when the traversed node count reaches this value.
   *
   * Default: Number.Infinity
   */
  maxNodes?: number;

  /**
   * If set to `true`, prevents infinite loops by not re-visiting repeated nodes.
   *
   * Default: true
   */
  cycleHandling?: boolean;

  /**
   * The maximum depth that must be traversed.
   *
   * Root object has depth 0.
   *
   * Default: Number.Infinity
   */
  maxDepth?: number;

  /**
   * If true, traversal will stop as soon as the callback returns a truthy value.
   *
   * This is useful for search use cases, where you typically want to skip traversing the remaining nodes once the target is found.
   *
   * Default: false
   */
  haltOnTruthy?: boolean;

  /**
   * The string to be used as separator for the `meta.nodePath` segments.
   *
   * Set to null if you wish to turn off `meta.nodePath` to increase traversal speed.
   *
   * Default: '.'
   */
  pathSeparator?: string | null;
};

Examples

Double all numbers in-place

Click to expand
exampleObject = {
  name: 'Hello World!',
  age: 1,
  accounts: 2,
  friends: 3,
};
function double({ parent, key, value, meta }) {
  if (typeof value === 'number') {
    parent[key] = value * 2;
  }
}

traverse(exampleObject, double);

console.log(exampleObject);
// {
//   name: 'Hello World!',
//   age: 2,
//   accounts: { checking: 4, savings: 6 },
//   friends: 8
// }

Find deep nested values by criteria

Click to expand
network = {
  name: 'Person1',
  age: 52,
  friends: [
    {
      name: 'Person2',
      age: 25,
      friends: [],
    },
    {
      name: 'Person3',
      age: 42,
      friends: [
        {
          name: 'Person4',
          age: 18,
          friends: [
            {
              name: 'Person5',
              age: 33,
              friends: [],
            },
          ],
        },
      ],
    },
  ],
};
const numbersOver25 = [];

function collectOver25({ parent, key, value, meta }) {
  if (key === 'age' && value > 25) {
    numbersOver25.push(value);
  }
}

traverse(network, collectOver25);

console.log(numbersOver25);
// [ 52, 42, 33 ]

Find paths by criteria

Click to expand
network = {
  name: 'Alice Doe',
  age: 52,
  friends: [
    {
      name: 'John Doe',
      age: 25,
      friends: [],
    },
    {
      name: 'Bob Doe',
      age: 42,
      friends: [
        {
          name: 'John Smith',
          age: 18,
          friends: [
            {
              name: 'Charlie Doe',
              age: 33,
              friends: [],
            },
          ],
        },
      ],
    },
  ],
};
const pathsToPeopleNamedJohn = [];

function callback({ parent, key, value, meta }) {
  if (value.name && value.name.startsWith('John')) {
    pathsToPeopleNamedJohn.push(meta.nodePath);
  }
}

traverse(network, callback);

console.log(pathsToPeopleNamedJohn);
// [ 'friends.0', 'friends.1.friends.0' ]

Get node by path

Click to expand
network = {
  name: 'Alice Doe',
  age: 52,
  friends: [
    {
      name: 'John Doe',
      age: 25,
      friends: [],
    },
    {
      name: 'Bob Doe',
      age: 42,
      friends: [
        {
          name: 'John Smith',
          age: 18,
          friends: [
            {
              name: 'Charlie Doe',
              age: 33,
              friends: [],
            },
          ],
        },
      ],
    },
  ],
};
import { getNodeByPath } from 'object-traversal';

const firstFriend = getNodeByPath(network, 'friends.0');
console.log(firstFriend);
// { name: 'John Doe', age: 25, friends: [] }

Breadth-first traversal

Click to expand
network = {
  name: 'Person 1',
  age: 52,
  friends: [
    {
      name: 'Person 2',
      age: 42,
      friends: [
        {
          name: 'Person 4',
          age: 18,
          friends: [],
        },
      ],
    },
    {
      name: 'Person 3',
      age: 25,
      friends: [],
    },
  ],
};
let names = [];

function getName({ parent, key, value, meta }) {
  if (value.name) {
    names.push(value.name);
  }
}

traverse(network, getName, { traversalType: 'breadth-first' });

console.log(names);
// [ 'Person 1', 'Person 2', 'Person 3', 'Person 4' ]

Roadmap

  • Configurable BFS/DFS
  • Max depth
  • Configurable path separator
  • Utility for consuming paths
  • Toggleable cycle handler
  • Iterator support
  • Sequential promise support
  • Multi threading & further speed enhancements
  • Streaming research
  • More granular cycleHandling: 'revisit', 'norevisit', and 'off'

Built with

TSDX
np
yarn 1.22.10