benjohnde/delegable-proxy

View on GitHub
src/DelegableProxy.js

Summary

Maintainability
A
55 mins
Test Coverage
import { isInt } from './Helper'

/**
 * DelegableProxy, multi-level deepened proxy.
 * If any of the underlying object is modified, the primary delegate is invoked.
 */
export default class DelegableProxy {
  /**
   * @param {object} object An object to proxy add/mod/del via callback method.
   * @param {proxyCallback} delegate Callback which is invoked for some actions.
   * @param {boolean} shouldClone create a clean clone of the whole data structure
   */
  static wire(object, delegate, shouldClone) {
    if (object === null) {
      throw new Error('Why would one use Proxy with a null object?')
    }
    if (shouldClone) {
      const cloned = JSON.parse(JSON.stringify(object))
      return new DelegableProxy(cloned, delegate)
    }
    return new DelegableProxy(object, delegate)
  }

  /**
   * Small helper function to decouple from the derived object.
   * @param {object} ref mother object to invoke {proxyCallback}
   * @param {object} obj to wire
   * @param {integer} index [-1]
   * @param {boolean} isRootObject whether the sender is the root object or not
   */
  static relax(ref, obj, index) {
    const delegate = function(action, position) {
      ref.notifyDelegate(action, position, false)
    }
    return new DelegableProxy(obj, delegate, index)
  }

  constructor(object, delegate, index) {
    if (object === null) {
      throw new Error('Why would one use Proxy without a proper object to follow?')
    }
    if (typeof delegate !== 'function') {
      throw new Error('Why would one use Proxy without a proper delegate function?')
    }
    this.index = (index !== undefined) ? index : -1
    this.delegate = delegate
    this.handler = this.createHandler()
    this.wired = new WeakSet()
    this.ensureRecursiveWiring(object)
    return new Proxy(object, this.handler)
  }

  createHandler() {
    const self = this
    // return true to accept the changes
    return {
      deleteProperty: function(target, property) {
        self.notifyDelegate('del', self.formatProperty(property), true)
        return true
      },
      set: function(target, property, value, receiver) {
        const hasOldValue = target[property] !== undefined
        // if key does not exist but value is an object, wrap it!
        if (typeof value === 'object') {
          target[property] = DelegableProxy.relax(self, value)
        } else {
          target[property] = value
        }
        // array pushes always triggers this method twice
        if (property === 'length') {
          return true
        }
        // object changes (for instance added new method) should not be delegated
        if (property === '__proto__') {
          return true
        }
        // notify delegate
        const action = hasOldValue ? 'mod' : 'add'
        self.notifyDelegate(action, self.formatProperty(property), true)
        return true
      }
    }
  }

  ensureRecursiveWiring(object) {
    if (this.wired.has(object)) {
      console.log(object, 'is already wired, no further traverse')
      return
    }
    this.wired.add(object)
    // end condition
    const isObject = typeof object !== 'object'
    if (isObject) {
      return
    }
    // go deeper
    const self = this
    const keys = Object.keys(object)
    keys.forEach(k => {
      const o = object[k]
      // not eligible for Proxy
      if (typeof o !== 'object') {
        return
      }
      // if key is numeric, pass the current index for locating the root object later on
      if (isInt(k)) {
        const i = keys.indexOf(k)
        object[k] = DelegableProxy.relax(self, o, i)
        return
      }
      object[k] = DelegableProxy.relax(self, o)
    })
  }

  /**
   * As we only want to track indices of an array and not keys, short check.
   * @param {object} property
   * @returns -1 or index
   */
  formatProperty(property) {
    if (!isInt(property)) {
      return -1
    }
    return parseInt(property)
  }

  /**
   * Callback which is invoked for each add/mode/del.
   *
   * @callback proxyCallback
   * @param {string} action may be one of {add, del, mod}.
   * @param {number} position resultant, this is currently the index of the root object, which was altered.
   * @param {boolean} isRootObject whether the sender is the root object or not
   */
  notifyDelegate(action, position, isRootObject) {
    if (!isRootObject) {
      action = 'mod'
    }
    if (this.index < 0) {
      return this.delegate(action, position, isRootObject)
    }
    return this.delegate(action, this.index, isRootObject)
  }
}