DevimalPlanet / object-traversal

Flexible and performant utility for traversing javascript objects.
MIT License
22 stars 1 forks source link
graph javascript search traversal typescript

object-traversal

License License npm downloads License License


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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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

Built with

TSDX
np
yarn 1.22.10