P0lip/nimma

Unexpected multiple traversals

simlu opened this issue · 1 comments

simlu commented

So in the readme it states:

[...] whereas Nimma, for the most time, doesn't really care whether you supply it with 10s or 100s of paths. [...]

Unfortunately multiple paths mean multiple accesses when using Nimma. This is a huge deal when many input paths are given or slow getters are used.

This should definitely be mentioned in the Readme. It is probably the biggest and most fundamental difference to object-scan.

Click to show Test Case
import Nimma from 'nimma';
import objectScan from 'object-scan';

class AccessLogger {
  constructor(obj) {
    const logs = {};
    const prepare = (o) => {
      const entries = Object.entries(o);
      const result = {};
      for (let i = 0; i < entries.length; i += 1) {
        const [k, v] = entries[i];
        const value = prepare(v);
        result[k] = value;
        Object.defineProperty(result, k, {
          get() {
            if (!(k in logs)) {
              logs[k] = 0;
            }
            logs[k] += 1;
            return value;
          }
        });
      }
      return result;
    };
    this.logs = logs;
    this.obj = prepare(obj);
  }

  reset() {
    const logs = { ...this.logs };
    Object.keys(this.logs).forEach((k) => {
      delete this.logs[k];
    });
    return logs;
  }
}

const accessLogger = new AccessLogger({
  a: {
    b: {
      c: {},
      d: {},
      e: {}
    }
  }
});
const { obj } = accessLogger;

const keysNimma = [
  '$.a.b.c',
  '$..*',
  '$.a.b.d',
  '$.a.b.e',
  '$.a.*.e',
  '$..e',
  '$.*.*',
  '$..b.c',
  '$.a.b'
];
const keysObjectScan = keysNimma
  .map((k) => (k.startsWith('$..') ? `**${k.slice(2)}` : k.slice(2)))

const testNimma = () => {
  const callbacks = [];
  new Nimma(keysNimma).query(obj, keysNimma.reduce((p, k) => ({
    ...p,
    [k]({ value, path }) {
      callbacks.push([path.join('.'), k, value]);
    }
  }), {}));
  return { callbacks, access: accessLogger.reset() };
};

const testObjectScan = () => {
  const callbacks = objectScan(keysObjectScan, {
    joined: true,
    filterFn: ({ matchedBy, key, value, context }) => {
      matchedBy.forEach((m) => {
        context.push([key, m, value]);
      });
    }
  })(obj, []);
  return { callbacks, access: accessLogger.reset() };
};

console.log(testNimma());
/*
{
  callbacks: [
    [ 'a.b.c', '$.a.b.c', {} ],
    [ 'a.b.d', '$.a.b.d', {} ],
    [ 'a.b.e', '$.a.b.e', {} ],
    [ 'a.b', '$.a.b', [Object] ],
    [ 'a', '$..*', [Object] ],
    [ 'a.b', '$..*', [Object] ],
    [ 'a.b', '$.*.*', [Object] ],
    [ 'a.b.c', '$..*', {} ],
    [ 'a.b.c', '$..b.c', {} ],
    [ 'a.b.d', '$..*', {} ],
    [ 'a.b.e', '$..*', {} ],
    [ 'a.b.e', '$.a.*.e', {} ],
    [ 'a.b.e', '$..e', {} ]
  ],
  access: { a: 10, b: 9, c: 3, d: 3, e: 3 }
}
*/
console.log(testObjectScan());
/*
{
  callbacks: [
    [ 'a.b.e', 'a.b.e', {} ],
    [ 'a.b.e', 'a.*.e', {} ],
    [ 'a.b.e', '**.*', {} ],
    [ 'a.b.e', '**.e', {} ],
    [ 'a.b.d', 'a.b.d', {} ],
    [ 'a.b.d', '**.*', {} ],
    [ 'a.b.c', 'a.b.c', {} ],
    [ 'a.b.c', '**.*', {} ],
    [ 'a.b.c', '**.b.c', {} ],
    [ 'a.b', 'a.b', [Object] ],
    [ 'a.b', '**.*', [Object] ],
    [ 'a.b', '*.*', [Object] ],
    [ 'a', '**.*', [Object] ]
  ],
  access: { a: 2, b: 2, e: 2, d: 2, c: 2 }
}
 */
simlu commented

I've added a test suite now that highlights the difference:
https://github.com/blackflux/object-scan/pull/1753/files