nwmatcher-cache.js 5.4 KB
/*
 * Copyright (C) 2007-2018 Diego Perini
 * All rights reserved.
 *
 * Caching/memoization module for NWMatcher
 *
 * Added capabilities:
 *
 * - Mutation Events are feature tested and used safely
 * - handle caching different document types HTML/XML/SVG
 * - store result sets for different selectors / contexts
 * - simultaneously control mutation on multiple documents
 *
 */

(function(global) {

  // export the public API for CommonJS implementations,
  // for headless JS engines or for standard web browsers
  var Dom =
    // as CommonJS/NodeJS module
    typeof exports == 'object' ? exports :
    // create or extend NW namespace
    ((global.NW || (global.NW = { })) &&
    (global.NW.Dom || (global.NW.Dom = { }))),

  Contexts = { },
  Results = { },

  isEnabled = false,
  isExpired = true,
  isPaused = false,

  context = global.document,
  root = context.documentElement,

  // timing pauses
  now = 0,

  // last time cache initialization was called
  lastCalled = 0,

  // minimum time allowed between calls to the cache initialization
  minCacheRest = 15, //ms

  mutationTest =
    function(type, callback) {
      var isSupported = false,
      root = document.documentElement,
      div = document.createElement('div'),
      handler = function() { isSupported = true; };
      root.insertBefore(div, root.firstChild);
      div.addEventListener(type, handler, true);
      if (callback && callback.call) callback(div);
      div.removeEventListener(type, handler, true);
      root.removeChild(div);
      return isSupported;
    },

  // check for Mutation Events, DOMAttrModified should be
  // enough to ensure DOMNodeInserted/DOMNodeRemoved exist
  HACKED_MUTATION_EVENTS = false,

  NATIVE_MUTATION_EVENTS = root.addEventListener ?
    mutationTest('DOMAttrModified', function(e) { e.setAttribute('id', 'nw'); }) : false,

  loadResults =
    function(selector, from, doc, root) {
      if (isEnabled && !isPaused) {
        if (!isExpired) {
          if (Results[selector] && Contexts[selector] === from) {
            return Results[selector];
          }
        } else {
          // pause caching while we are getting
          // hammered by dom mutations (jdalton)
          now = (new Date).getTime();
          if ((now - lastCalled) < minCacheRest) {
            isPaused = isExpired = true;
            setTimeout(function() { isPaused = false; }, minCacheRest);
          } else setCache(true, doc);
          lastCalled = now;
        }
      }
      return undefined;
    },

  saveResults =
    function(selector, from, doc, data) {
      Contexts[selector] = from;
      Results[selector] = data;
      return;
    },

  /*-------------------------------- CACHING ---------------------------------*/

  // invoked by mutation events to expire cached parts
  mutationWrapper =
    function(event) {
      var d = event.target.ownerDocument || event.target;
      stopMutation(d);
      expireCache(d);
    },

  // append mutation events
  startMutation =
    function(d) {
      if (!d.isCaching && d.addEventListener) {
        // FireFox/Opera/Safari/KHTML have support for Mutation Events
        d.addEventListener('DOMAttrModified', mutationWrapper, true);
        d.addEventListener('DOMNodeInserted', mutationWrapper, true);
        d.addEventListener('DOMNodeRemoved',  mutationWrapper, true);
        d.isCaching = true;
      }
    },

  // remove mutation events
  stopMutation =
    function(d) {
      if (d.isCaching && d.removeEventListener) {
        d.removeEventListener('DOMAttrModified', mutationWrapper, true);
        d.removeEventListener('DOMNodeInserted', mutationWrapper, true);
        d.removeEventListener('DOMNodeRemoved',  mutationWrapper, true);
        d.isCaching = false;
      }
    },

  // enable/disable context caching system
  // @d optional document context (iframe, xml document)
  // script loading context will be used as default context
  setCache =
    function(enable, d) {
      if (!!enable) {
        isExpired = false;
        startMutation(d);
      } else {
        isExpired = true;
        stopMutation(d);
      }
      isEnabled = !!enable;
    },

  // expire complete cache
  // can be invoked by Mutation Events or
  // programmatically by other code/scripts
  // document context is mandatory no checks
  expireCache =
    function(d) {
      isExpired = true;
      Contexts = { };
      Results = { };
    };

  if (!NATIVE_MUTATION_EVENTS && root.addEventListener && Element && Element.prototype) {
    if (mutationTest('DOMNodeInserted', function(e) { e.appendChild(document.createElement('div')); }) &&
        mutationTest('DOMNodeRemoved', function(e) { e.removeChild(e.appendChild(document.createElement('div'))); })) {
      HACKED_MUTATION_EVENTS = true;
      Element.prototype._setAttribute = Element.prototype.setAttribute;
      Element.prototype.setAttribute =
        function(name, val) {
          this._setAttribute(name, val);
          mutationWrapper({
            target: this,
            type: 'DOMAttrModified',
            attrName: name,
            attrValue: val });
        };
    }
  }

  isEnabled = NATIVE_MUTATION_EVENTS || HACKED_MUTATION_EVENTS;

  /*------------------------------- PUBLIC API -------------------------------*/

  // save results into cache
  Dom.saveResults = saveResults;

  // load results from cache
  Dom.loadResults = loadResults;

  // expire DOM tree cache
  Dom.expireCache = expireCache;

  // enable/disable cache
  Dom.setCache = setCache;

})(this);