0

I'm having trouble using RecursiveIterator.

Basically, I need to iterate over a graph of objects to see what properties mays have been modified to be able to insert an history in a database...

So, I wrote the following Typescript code to play with in VS Code:

import RecursiveIterator = require("recursive-iterator");

const old = {
  vehicleId: 1,
  vehicleName: "Ol",
  modifiedProperties: [],
  device: {
    deviceId: 12,
    hardwareId: "12345",
    modifiedProperties: [],
  },
};

const n = {
  vehicleId: 1,
  vehicleName: "Ol2",
  modifiedProperties: ["vehicleName"],
  device: {
    deviceId: 12,
    hardwareId: "54321",
    modifiedProperties: ["hardwareId"],
  },
};

console.log("ouf");

var iteratorOnDeepClone = new RecursiveIterator(old, undefined, true);
var iteratorOnModifiedObject = new RecursiveIterator(n, undefined, true);

while (true) {
  let { value: valueOnModifiedObject, done: doneOnModifiedObject } =
    iteratorOnModifiedObject.next();
  let { value: valueOnDeepClone /*, done: doneOnDeepClone*/ } =
    iteratorOnDeepClone.next();

  if (valueOnDeepClone == undefined) {
    console.error("have a kitkat");
  }

  let {
    parent: parentOnDeepClone,
    node: nodeOnDeepClone,
    key: keyOnDeepClone,
    path: pathOnDeepClone,
    deep: deepOnDeepClone,
  } = valueOnDeepClone;
  let {
    parent: parentOnModifiedObject,
    node: nodeOnModifiedObject,
    key: keyOnModifiedObject,
    path: pathOnModifiedObject,
    deep: deepOnModifiedObject,
  } = valueOnModifiedObject;

  if (nodeOnModifiedObject.modifiedProperties != undefined) {
    for (var prop of nodeOnModifiedObject.modifiedProperties) {
      const oldValue: any = nodeOnDeepClone[prop];
      console.log(prop, oldValue);
    }
  }
  if (doneOnModifiedObject) {
    console.log("done!");
    break;
  }
}

Right now it fails (on line 41) because a some point the iteratorOnDeepClone.next() returned object is invalid:

Exception has occurred: TypeError: Cannot destructure property 'parent' of 'valueOnDeepClone' as it is undefined.
  at Object.<anonymous> (d:\temp\sandbox\out\iterator_sample.js:33:19)
    at Module._compile (internal/modules/cjs/loader.js:1137:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)
    at Module.load (internal/modules/cjs/loader.js:985:32)
    at Function.Module._load (internal/modules/cjs/loader.js:878:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
    at internal/main/run_main_module.js:17:47

package.json

{
  "name": "playground",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "recursive-iterator": "^3.3.0"
  },
  "devDependencies": {
    "ts-node": "^10.2.1",
    "tsconfig-paths": "^3.11.0",
    "typescript": "4.2.4"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "out",
    "sourceMap": true
  }
}

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/iterator_sample.ts",
            "preLaunchTask": "tsc: build - tsconfig.json",
            "outFiles": [
                "${workspaceFolder}/out/**/*.js"
            ]
        }
    ]
}

EDIT 1: Found he breaks and stopped the train. The problem lies with te modifiedProperties array that is considered as an object when not empty. So the iterators are desynchronized.

  • Please see [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) and [How do I create a runnable stack snippet?](https://meta.stackoverflow.com/questions/358992). It's extremely unlikely we need `launch.json`, `package.json`, or `tsconfig.json`. But you don't give us data we do need, such as a sample of the input where it fails. – Scott Sauyet Sep 30 '21 at 13:02
  • Possibly when `done` true on last element `value` is undefined. E.g. when there is no more values value is empty. – Aleksandr Smyshliaev Sep 30 '21 at 13:29
  • You also can try to destructure iterator without cycle - `[...iteratorOnModifiedObject]` – Aleksandr Smyshliaev Sep 30 '21 at 13:35
  • @ScottSauyet I'll look into that, thanks. –  Sep 30 '21 at 13:37

1 Answers1

2

I'd suggest that this is a problematic technique to find differences.

If you're trying to scan them in parallel, it's quite possible that you'll find differences that have only to do with the order that properties were added and not their values. Presumably that's not what you want.

User Mulan has an excellent answer explaining one good way to find differences, giving you a nested result object showing in excellent detail where things have changed. Here is a somewhat different technique, which flattens the objects using '.'-separated keys to delineate paths.

const compare = (a, b, first = transform (a), second = transform (b)) => 
  uniq (Object .keys (first) .concat (Object .keys (second)))
    .filter ((k) => first [k] !== second [k]) 
    .reduce ((a, k) => ((a [k] = {left: first [k], right: second [k]}), a), {})

const transform = (o) =>
  pathEntries (o) 
    .reduce ((a, [k, v]) => ((a[k .join ('.')] = v), a), {})

const pathEntries = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, x]) => pathEntries (x) .map (
          ([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v]
        )
      ) 
    : [[[], obj]]

const uniq = (xs) => 
  [... new Set (xs)]

const orig = {
  foo: 42,
  bar: {baz: 99, qux: {corge: [1, 2, 3]}},
  grault: 'abc',
}

const modified = {
  foo: 42,
  bar: {baz: 99, qux: {corge: [1, 2, 4]}},
  waldo: 'xyz'
}

console .log (compare (orig, modified))
.as-console-wrapper {max-height: 100% !important; top: 0}

This works by first turning an object like this:

const orig = {
  foo: 42,
  bar: {baz: 99, qux: {corge: [1, 2, 3]}},
  grault: 'abc',
}

into

[
  [["foo"], 42],
  [["bar", "baz"], 99],
  [["bar", "qux", "corge", 0] ,1],
  [["bar", "qux", "corge", 1], 2],
  [["bar", "qux", "corge", 2], 3],
  [["grault"], "abc"]
]

using the utility pathEntries function and then into

{
  "foo": 42,
  "bar.baz": 99,
  "bar.qux.corge.0": 1,
  "bar.qux.corge.1": 2,
  "bar.qux.corge.2": 3,
  "grault": "abc"
}

inside transform.

We do this for our first and second objects, then simply find the keys in the output object where they differ and report the differing values between the two objects in a single object structure.


Mulan's technique is more flexible, and captures the differences in a more structured manner. This one's flat output, though, may be nicer for storing in a database, depending on the style of db you have.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I like your approach. I was trying to stick with the recursive-iterator lib which was not a good idea. Asking for help was the right thing to do and I understand that I still have progress to make on asking questions the right way. Thank you very much. –  Sep 30 '21 at 14:26