Source: persistenceManager.js

/**
 * Copyright (c) 2017, Oracle and/or its affiliates.
 * All rights reserved.
 */

define(['./impl/PersistenceXMLHttpRequest', './impl/PersistenceSyncManager', './impl/offlineCacheManager', './impl/logger', './impl/fetch'],
  function (PersistenceXMLHttpRequest, PersistenceSyncManager, offlineCacheManager, logger) {
    'use strict';

    /**
     * @export
     * @class PersistenceManager
     * @hideconstructor
     * @classdesc PersistenceManager is used for offline support by acting as
     * the interface between the application, webservice, and the the Offline Persistence Toolkit.
     * The PersistenceManger is where the endpoints are registered/unregistered for Offline Persistence Toolkit.
     * In order to use PersistenceManager, a default story factory must be registered using
     * {@link PersistenceStoreManager#registerDefaultStoreFactory|registerDefaultStoreFactory} before
     * initializing PersistenceManager. PersistenceManager is not needed when Offline Persistence Toolkit
     * is operating in {@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API |ServiceWorker}
     * Mode. Please see {@link README|Concept section} for more infomation.
     * <br />
     * <br />
     * PersistenceManager should be {@link PersistenceManager#init|initialized} prior to using any of the other methods.
     */
    function PersistenceManager() {
      Object.defineProperty(this, '_registrations', {
        value: [],
        writable: true
      });
      Object.defineProperty(this, '_eventListeners', {
        value: [],
        writable: true
      });
      Object.defineProperty(this, '_forceOffline', {
        value: false,
        writable: true
      });
      Object.defineProperty(this, '_isOffline', {
        value: false,
        writable: true
      });
      Object.defineProperty(this, '_cache', {
        value: null,
        writable: true
      });
      Object.defineProperty(this, '_persistenceSyncManager', {
        value: new PersistenceSyncManager(this.isOnline.bind(this), this.browserFetch.bind(this), this.getCache.bind(this))
      });
    };

    /**
     * Initializes the PersistenceManager
     * and will return a promise once the PersistenceManager is ready.
     * A default story factory must be registered using
     * {@link PersistenceStoreManager#registerDefaultStoreFactory|registerDefaultStoreFactory}
     * before initializing PersistenceManager. This method is normally called when the applciation
     * is first started or when a {@link PersistenceRegistration} needs to be deregistered.
     * @method
     * @name init
     * @memberof PersistenceManager
     * @export
     * @instance
     * @return {Promise} returns a Promise when resolved that this
     *                   PersistenceManager is fully initialized.
     *
     * @example
     * PersistenceManager.init().then(function(){
     *  // Insert Your Code Here
     *  // sample code
     *  PersistenceManager.register()
     * });
     */
    PersistenceManager.prototype.init = function () {
      logger.log("Offline Persistence Toolkit PersistenceManager: Initilizing");
      _replaceBrowserApis(this);
      _addBrowserEventListeners(this);
      _openOfflineCache(this);

      return Promise.resolve();
    };

    /**
     * Force the PersistenceManager to go offline regardless of the browsers actual network status.
     * This function should be used if the user wants the application to function in offline mode.
     * When {@link PersistenceManager#isOnline|PersistenceManager.isOnline()} is called, it will
     * return false is forceOffline is set to true.
     * @method
     * @name forceOffline
     * @memberof PersistenceManager
     * @export
     * @instance
     * @param {boolean} offline If true, sets the PersistenceManager offline
     * @example <caption>Toggle Force Offline to true</caption>
     * PersistenceManager.forceOffline(true)
     * @example <caption>Toggle Force Offline to false</caption>
     * PersistenceManager.forceOffline(false)
     */
    PersistenceManager.prototype.forceOffline = function (offline) {
      logger.log("Offline Persistence Toolkit PersistenceManager: forceOffline is called with value: " + offline);
      this._forceOffline = offline;
    };

    /**
     * PersistenceManager.getCache returns the Offline Persistence Toolkit implementation of the standard Cache API.
     * In additional to functionalities provided by the standard Cache API, this OfflineCache also interacts with
     * shredding methods for more fine-grain caching and allows for a clear() function to remove the clear the cache.
     * @method
     * @name getCache
     * @memberof PersistenceManager
     * @export
     * @instance
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Cache | CacheAPI MDN}
     * @return {OfflineCache} returns the cache store for the Persistence Framework. Implements the Cache API.
     * @example
     * var PersisteneCache = PersistenceManager.getCache()
     * PersistenceCache.[someMethod]
     * @example
     * PersistenceManager.getCache().[someMethod]
     */
    PersistenceManager.prototype.getCache = function () {
      return this._cache;
    };

    /**
     * Checks if the browser is online. PersistenceManager can be forced into offline mode by setting
     * {@link forceOffline} to true.
     * <ul>
     * <li>Returns true if the browser AND PersistenceManager is online.</li>
     * <li>Returns false if the browser OR PersistenceManager is offline.</li>
     * </ul>
     * Note: To determine if the browser is online, the function will use
     * navigator.onLine whose behavior is browser specific. If being used
     * in a hybrid mobile application, please install the
     * {@link https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-network-information/|Cordova Network Information Plugin}.
     * Installing the plugin will enable this function to return accurate
     * browser online status.
     *
     * @method
     * @name isOnline
     * @memberof PersistenceManager
     * @export
     * @instance
     * @return {boolean} Returns true if online, false if not.
     */
    PersistenceManager.prototype.isOnline = function () {
      var online = navigator.onLine;

      if (navigator.network &&
        navigator.network.connection &&
        navigator.network.connection.type == Connection.NONE) {
        online = false;
        logger.log("Offline Persistence Toolkit PersistenceManager: Cordova network info plugin is returning online value: " + online);
      }
      return online && !this._isOffline && !this._forceOffline;
    };

    /**
     * Registers a URL endpoint for persistence storage. The scope value can also be a regular expression.
     * If not options are passed in, PersistenceManager.register acts similarly to {@link PersistenceManager#getRegistrations|getRegistrations}
     * and will return a array of all registrations objects or null if none exist.
     * @method
     * @name register
     * @memberof PersistenceManager
     * @export
     * @instance
     * @param {{scope: string}=} options Options to control registration
     * <ul>
     * <li>options.scope The URI which should be persisted.</li>
     * <li>options.scope Can also be a regular expression.</li>
     * </ul>
     * @return {Promise<PersistenceRegistration|Array<PersistenceRegistration>|null>} Returns a Promise which resolves to a
     * {@link PersistenceRegistration} object for the specified options.
     * If options is null, returns an array of all current {@link PersistenceRegistration} objects
     * or null if there are none.
     *
     * @example <caption>String Scope</caption>
     * PersistenceManager.register({scope:'/location'}).then(function(registration){
     * // add event listeners for scope '/location'
     * });
     * PersistenceManager.register({scope:'oracle.com'}).then(function(registration){
     * // add event listeners for scope 'oracle.com'
     * });
     *
     * @example <caption>Regular Expression Scope</caption>
     * PersistenceManager.register({scope:'/emp/g'}).then(function(registration){
     * // add event listeners for scope '/emp/'
     * });
     *
     * @example <caption>No Options</caption>
     * PersistenceManager.register().then(function(registrationArray){
     * // Returns an array of registered objects
     * });
     * PersistenceManager.register().then(function(registration){
     * // registration is null if there are no registration objects
     * });
     */
    PersistenceManager.prototype.register = function (options) {
      options = options || {};
      var registration = new PersistenceRegistration(options['scope'], this);
      this._registrations.push(registration);

      return Promise.resolve(registration);
    };

    /**
     * Return the {@link PersistenceRegistration}  object for the given URL.
     * @method
     * @name getRegistration
     * @memberof PersistenceManager
     * @export
     * @instance
     * @param {string} url Url
     * @return {Promise<PersistenceRegistration|undefined>} Returns a Promise which resolves to the
     * {@link PersistenceRegistration} object for the URL or undefined if nothing is found.
     *
     * @example <caption>Suppose a registration was registered for '/location'</caption>
     * PersistenceManager.getRegistration('https://www.oracle.com/location/stores').then(function(registration){
     * // registration will be the registration object for '/location'
     * });
     * @example <caption>No PersistenceRegistration is registered</caption>
     * PersistenceManager.getRegistration('https://www.oracle.com/doesNotExist').then(function(registration){
     * // registration will be undefined
     * });
     */
    PersistenceManager.prototype.getRegistration = function (url) {
      var i;
      var registration;
      var registrationCount = this._registrations.length;

      for (i = 0; i < registrationCount; i++) {
        registration = this._registrations[i];

        if (url.match(registration.scope)) {
          return Promise.resolve(registration);
        }
      }
      return Promise.resolve();
    };

    /**
     * Return an array of {@link PersistenceRegistration} objects. Similar to {@link PersistenceManager#register|PersistenceManager.register()}
     * when no options are passed in.
     * @method
     * @name getRegistrations
     * @memberof PersistenceManager
     * @export
     * @instance
     * @return {Promise<Array<PersistenceRegistration>>} Returns a Promise which resolves to an array of
     * {@link PersistenceRegistration} objects.
     * @example
     * PersistenceManager.getRegistrations().then(function(registrationArray){
     * // Your Code Here
     * });
     */
    PersistenceManager.prototype.getRegistrations = function () {
      return Promise.resolve(this._registrations.slice());
    };

    /**
     * getSyncManager returns the {@link PersistenceSyncManager|Sync Manager} which is used to support synchronization capabilities for requests which were
     * made when offline. The PersistenceSyncManager supports operations such as retrieving the sync log, invoking the {@link PersistenceSyncManager#sync|sync()} API
     * to replay the requests which were made while offline, inserting/removing requests from the sync log, adding
     * event listeners for sync operations, and performing undo/redo operations for the shredded local data which
     * were made as a result of those requests.
     * @method
     * @name getSyncManager
     * @memberof PersistenceManager
     * @export
     * @instance
     * @return {PersistenceSyncManager} Returns the Sync Manager
     * @example
     * var syncManager = PersistenceManager.getSyncManager()
     * syncManager.[someMethod]
     * @example
     * PersistenceManager.getSyncManager().[someMethod]
     */
    PersistenceManager.prototype.getSyncManager = function () {
      return this._persistenceSyncManager;
    };

    /**
     * Call fetch API
     * without going through the persistence framework. This is the
     * unproxied browser provided fetch API.
     * @method
     * @name browserFetch
     * @memberof PersistenceManager
     * @export
     * @instance
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | FetchAPI MDN}
     * @param {String|Request} request A USVString containing the direct URL of the
     * resource you want to fetch or a Request object.
     * @return {Promise<Response>} Resolves to the Response when complete
     * @example
     * PersistenceManager.browserFetch('https://www.oracle.com/').then(function(response){
     * // Your Code Here
     * });
     */
    PersistenceManager.prototype.browserFetch = function (request) {
      var self = this;
      logger.log("Offline Persistence Toolkit PersistenceManager: browserFetch() for Request with url: " + request.url);
      // only do special processing in browser context. In service worker context
      // just call regular fetch.
      var requestObj = request
      if (_isBrowserContext()) {
        // store the last Request object on the PersistenceManager so that we
        // can detect if a fetch polyfill is causing a XHR request to our
        // XHR adapter
        Object.defineProperty(this, '_browserFetchRequest', {
          value: request,
          writable: true
        });
        return new Promise(function (resolve, reject) {
          logger.log("Offline Persistence Toolkit PersistenceManager: Calling browser fetch function for Request with url: " + request.url);
          if (request._browserRequest) {
            requestObj = request._browserRequest;
          }
          self._browserFetchFunc.call(window, requestObj).then(function (response) {
            resolve(response);
          }, function (error) {
            reject(error);
          });
          self._browserFetchRequest = null;
        });
      } else {
        if (request._browserRequest) {
          requestObj = request._browserRequest;
        }
        return fetch(requestObj);
      }
    };

    function _addBrowserEventListeners(persistenceManager) {
      var self = persistenceManager;
      // add listeners for browser online
      // Don't do it for Service Workers
      if (_isBrowserContext() &&
        !self._addedBrowserEventListeners) {
        logger.log("Offline Persistence Toolkit PersistenceManager: Adding browser event listeners");
        window.addEventListener('offline', function (e) {
          self._isOffline = true;
        }, false);

        window.addEventListener('online', function (e) {
          self._isOffline = false;
        }, false);
        self._addedBrowserEventListeners = true;
      }
    };

    function _isBrowserContext() {
      return (typeof window != 'undefined') && (window != null);
    };

    function _dispatchEvent(persistenceManager, eventType, event) {
      var i;
      var j;
      var returnValue;
      var registration;
      var respondWithPromise = null;
      var registrations = persistenceManager._registrations;
      var registrationCount = registrations != null ? registrations.length : 0;
      var eventListenerCount;
      for (i = 0; i < registrationCount; i++) {
        registration = registrations[i];

        if (event.request.url.match(registration['scope']) != null) {
          eventListenerCount = registration._eventListeners.length;

          for (j = 0; j < eventListenerCount; j++) {
            if (registration._eventListeners[j]['type'] == eventType) {
              if (eventType == 'fetch') {
                if (respondWithPromise === null &&
                  event._setPromiseCallbacks instanceof Function) {
                  respondWithPromise = new Promise(function (resolve, reject) {
                    event._setPromiseCallbacks(resolve, reject);
                  });
                }
                logger.log("Offline Persistence Toolkit PersistenceManager: Calling fetch event listener");
                registration._eventListeners[j]['listener'](event);
              } else {
                logger.log("Offline Persistence Toolkit PersistenceManager: Calling event listener");
                returnValue = registration._eventListeners[j]['listener'](event);

                if (returnValue === false) {
                  // event cancelled
                  return false;
                }
              }
            }
          }

          if (respondWithPromise != null) {
            return respondWithPromise;
          }
        }
      }
      return true;
    };

    function _openOfflineCache(persistenceManager) {
      var self = persistenceManager;
      self._cache = offlineCacheManager.open('systemCache');
    };

    function _replaceBrowserApis(persistenceManager) {
      // save off the browser fetch and XHR only in browser context.
      // also add listeners for browser online
      // Don't do it for Service Workers
      if (_needsRequestReplaced() &&
        !persistenceManager._browserRequestConstructor &&
        !persistenceManager._persistenceRequestConstructor) {
        logger.log("Offline Persistence Toolkit PersistenceManager: Replacing Safari Browser APIs");
        // using self to refer to both the "window" and the "self" context
        // of serviceworker
        Object.defineProperty(persistenceManager, '_browserRequestConstructor', {
          value: self.Request,
          writable: false
        });
        Object.defineProperty(persistenceManager, '_persistenceRequestConstructor', {
          value: persistenceRequest(persistenceManager),
          writable: false
        });
        self['Request'] = persistenceManager._persistenceRequestConstructor;
        if (!_isBrowserContext() &&
          !persistenceManager._browserFetchFunc) {
          // replace serviceWorker's fetch with this wrapper to unwrap safari request in sw
          Object.defineProperty(persistenceManager, '_browserFetchFunc', {
            value: self.fetch,
            writable: false
          });
          self['fetch'] = serviceWorkerFetch(persistenceManager)
        }
      }

      if (_isBrowserContext() &&
        !persistenceManager._browserFetchFunc &&
        !persistenceManager._browserXMLHttpRequest) {
        // browser context
        // _browserFetchFunc is always non-null because we polyfill it
        logger.log("Offline Persistence Toolkit PersistenceManager: Replacing browser APIs");
        Object.defineProperty(persistenceManager, '_browserFetchFunc', {
          value: window.fetch,
          writable: false
        });
        Object.defineProperty(persistenceManager, '_browserXMLHttpRequest', {
          value: window.XMLHttpRequest,
          writable: false
        });
        // replace the browser fetch and XHR with our own
        window['fetch'] = persistenceFetch(persistenceManager);
        window['XMLHttpRequest'] = function () {
          if (persistenceManager._browserFetchRequest != null) {
            // this means we got invoked by a fetch polyfill. That means
            // we can just use the browser XHR since the browser doesn't
            // support the Fetch API anyway
            return new persistenceManager._browserXMLHttpRequest();
          }
          return new PersistenceXMLHttpRequest(persistenceManager._browserXMLHttpRequest);
        };
      }
    };

    function _unregister(persistenceManager, registration) {
      var self = persistenceManager
      var regIdx = self._registrations.indexOf(registration);

      if (regIdx > -1) {
        self._registrations.splice(regIdx, 1);
        return true;
      }
      return false;
    };

    /**
     * @export
     * @class PersistenceRegistration
     * @classdesc PersistenceRegistration constructor. This constructor is created when
     * {@link PersistenceManager#register|PersistenceManager.register API } is envoked.
     * This API is used to add eventlisteners to the registered scope or remove an existing
     * registration object from the {@link PersistenceManager}.
     * @constructor
     * @param {string} scope The URI which should be persisted
     * @param {Object} PersistenceManager
     */
    function PersistenceRegistration(scope, persistenceManager) {
      Object.defineProperty(this, 'scope', {
        value: scope,
        enumerable: true
      });
      Object.defineProperty(this, '_persistenceManager', {
        value: persistenceManager
      });
      Object.defineProperty(this, '_eventListeners', {
        value: [],
        writable: true
      });
    }
    ;

    /**
     * The scope for the registration. This is the unique identifier for each registration.
     * @export
     * @instance
     * @memberof PersistenceRegistration
     *
     * @type {string}
     */

    /**
     * Add an event listener for how an event will be handled. Currently, the only supported event is the "fetch" event.
     * @method
     * @name addEventListener
     * @memberof PersistenceRegistration
     * @export
     * @instance
     * @param {string} type A string representing the event type to listen for. Supported value is "fetch"
     * which will trigger the function with a FetchEvent once a fetch occurs.
     * @param {Function} listener The object that receives a notification when an event of the specified type occurs
     * @example
     * // Using Default Response Proxy
     * var ResponseProxy = defaultResponseProxy.getResponseProxy(
     *    {
     *       fetchStrategy: fetchStrategies.getCacheFirstStrategy()
     * });
     * PersistenceManager.register({scope:'/employee'}).then(function(registration){
     *    PersistenceRegistration.addEventListener('fetch',ResponseProxy.getFetchEventListener());
     * });
     * @example
     * // Using a custom event listener
     *
     * //using {@link DefaultResponseProxy#getResponseProxy|defaultResponseProxy.getResponseProxy} to set up a shreading method
     * var defaultItemResponseProxy = defaultResponseProxy.getResponseProxy(
     *   {
     *     jsonProcessor:
     *       {
     *         shredder: simpleJsonShredding.getShredder('item', 'ID'),
     *         unshredder: simpleJsonShredding.getUnshredder()
     *       },
     *     queryHandler: queryHandlers.getSimpleQueryHandler('item', 'limit')
     *   });
     * persistenceManager.register({
     *   scope: '/item'
     * }).then(function (registration) {
     *   registration.addEventListener('fetch', function (event) {
     *     // Using the event's respondWith method, return a new promise
     *     // that will resolve into the response from the persistence framework
     *     event.respondWith(new Promise(function (resolve, reject) {
     *       defaultItemResponseProxy.processRequest(event.request).then(function (response) {
     *         if (response != null) {
     *           // Check to see if the response from the cache is stale
     *           if ((new Date(response.headers.get('x-oracle-jscpt-cache-expiration-date'))).getTime() < (new Date()).getTime()) {
     *             // handle response
     *           } else {
     *             // handle stale response
     *           }
     *         }
     *         resolve(response);
     *       });
     *     }));
     *   })});
     */
    PersistenceRegistration.prototype.addEventListener = function (type, listener) {
      this._eventListeners.push({'type': type.toLowerCase(),
                                'listener': listener});
    };

    /**
     * Unregisters the registration. Will finish any ongoing operations before it is unregistered.
     * @method
     * @name unregister
     * @memberof PersistenceRegistration
     * @export
     * @instance
     * @return {Promise<Boolean>} Promise resolves with a boolean indicating whether the scope has been unregistered or not
     * <ul>
     * <li>true if successfully unregistered</li>
     * <li> false if unsuccessful</li>
     * </ul>
     * @example
     * PersistenceManager.getRegistration('https://www.oracle.com/location').then(function(registration){
     *     registration.unregister().then(function(registrationStatus){
     *      // registrationStatus is boolean based on if it was succuessful or not
     *    })
     * })
     */
    PersistenceRegistration.prototype.unregister = function () {
      return Promise.resolve(_unregister(this._persistenceManager, this));
    };

    // this is the persistence version of fetch which we replace the browser
    // version with. We only do this in browser context, not in service worker context.
    // The replacement is done in persistenceManager.
    function persistenceFetch(persistenceManager) {
      function PersistenceFetchEvent(request) {
        Object.defineProperty(this, 'isReload', {
          value: false,
          enumerable: true
        });
        // clientId is not applicable to a non-ServiceWorker context
        Object.defineProperty(this, 'clientId', {
          value: null,
          enumerable: true
        });
        // client is not applicable to a non-ServiceWorker context
        Object.defineProperty(this, 'client', {
          value: null,
          enumerable: true
        });
        Object.defineProperty(this, 'request', {
          value: request,
          enumerable: true
        });
        Object.defineProperty(this, '_resolveCallback', {
          value: null,
          writable: true
        });
        Object.defineProperty(this, '_rejectCallback', {
          value: null,
          writable: true
        });
      };

      PersistenceFetchEvent.prototype.respondWith = function (any) {
        var self = this;
        if (any instanceof Promise) {
          any.then(function (response) {
            self._resolveCallback(response);
          }, function (err) {
            self._rejectCallback(err);
          });
        } else if (typeof (any) == 'function') {
          var response = any();
          self._resolveCallback(response);
        }
      };

      PersistenceFetchEvent.prototype._setPromiseCallbacks = function (resolveCallback, rejectCallback) {
        this._resolveCallback = resolveCallback;
        this._rejectCallback = rejectCallback;
      };

      return function (input, init) {
        var request;
        // input can either be a Request object or something
        // which can be passed to the Request constructor
        if (Request.prototype.isPrototypeOf(input) && !init) {
          request = input
        } else {
          request = new Request(input, init)
        }

        // check if it's an endpoint we care about
        return persistenceManager.getRegistration(request.url).then(function (registration) {
          if (registration != null) {
            // create a Fetch Event
            var fetchEvent = new PersistenceFetchEvent(request);
            var promise = _dispatchEvent(persistenceManager, 'fetch', fetchEvent);
            // if a promise is returned than a fetch listener(s) handled the event
            if (promise != null &&
              promise instanceof Promise) {
              return promise;
            }
          }
          // if the endpoint is not registered or the Fetch Event Listener
          // did not return a Promise then just do a regular browser fetch
          return persistenceManager.browserFetch(request);
        });
      };
    };

    function persistenceRequest(persistenceManager) {
      function PersistenceRequest(input, init) {
        var self = this;

        // create two variables to house the input and init vars
        var requestInput = input;
        var requestInit = init;
        logger.log("Offline Persistence Toolkit persistenceRequest: Create New Request");
        // Check if the input is a Request object
        if (input._input){
          logger.log("Offline Persistence Toolkit persistenceRequest: Input is a PersistenceRequest");
          // we replace the user inputs with a copy of the previous request object's input and init vars
          requestInput = input._input;
          requestInit = Object.assign({}, input._init);
          // if there are any init's for for this request, then those must also be carried over to
          // the requestInit overwriting any previous entries
          for (var key in init) {
            if (init.hasOwnProperty(key)) {
              requestInit[key]= init[key];
            }
          }
          // the headers and body must be checked for formData instance
          // if it has both exist, then the headers.get("Content-Type") must be replace
          // to preserve formData Boundary
          if (input.headers &&
            requestInit &&
            requestInit.body &&
            requestInit.body instanceof FormData) {
            // check to see if the header exist before adding, if it does only replace content-type
            if (requestInit.headers){
              var contentType = input.headers.get("Content-Type")
              requestInit.headers.set("Content-Type",contentType );
            } else {
              // else replace whole header
              requestInit.headers = input.headers;
            }
          }
        }

        this._browserRequest = new persistenceManager._browserRequestConstructor(requestInput, requestInit);
        this._input = requestInput;
        this._init = requestInit;
        var requestDefineProperty = function (requestProperty) {
          var propDescriptors = Object.getOwnPropertyDescriptor(self._browserRequest, requestProperty);

          if (propDescriptors &&
            (propDescriptors.writable ||
              propDescriptors.set)) {
            Object.defineProperty(self, requestProperty, {
              get: function () {
                return self._browserRequest[requestProperty];
              },
              set: function (value) {
                self._browserRequest[requestProperty] = value;
              },
              enumerable: true
            });
          } else {
            Object.defineProperty(self, requestProperty, {
              get: function () {
                return self._browserRequest[requestProperty];
              },
              enumerable: true
            });
          }
        }
        var property;
        for (property in this._browserRequest) {
          // cannot use typeof on the "body" property as it will result in a not implmented error
          if (property == "body" || typeof this._browserRequest[property] != "function") {
            requestDefineProperty(property);
          }
        }
        var boundary = this.headers.get("Content-Type");
        if (boundary != null &&
          boundary.indexOf("boundary=") > -1 &&
          boundary.indexOf("form-data") > -1) {
          boundary = boundary.split("boundary=")
          this._boundary = "--" + boundary[boundary.length - 1];
        }

        this.arrayBuffer = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called arrayBuffer()");
          var self = this;
          try {
            if (self._init &&
              self._init.body) {
              if (!(self._init.body instanceof FormData)) {
                return self._browserRequest.arrayBuffer();
              } else {
                return _formDataToArrayBuffer(self._init.body, self._boundary)
              }
            }
            return self._browserRequest.arrayBuffer();
          } catch (e) {
            return Promise.reject(e);
          }
        }

        this.blob = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called blob()");
          var self = this;
          try {
            if (self._init &&
              self._init.body) {
              if (!(self._init.body instanceof FormData)) {
                return self._browserRequest.blob();
              } else {
                return _formDataToString(self._init.body, self._boundary).then(function (formDataText) {
                  var formDataBlob = new Blob([formDataText],
                    { type: self.headers.get("Content-Type") });
                  return formDataBlob;
                })
              }
            }
            return self._browserRequest.blob();
          } catch (e) {
            return Promise.reject(e);
          }
        }

        this.formData = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called formData()");
          var self = this;
          try {
            if (self._init &&
              self._init.body) {
              if (!(self._init.body instanceof FormData)) {
                return self._browserRequest.formData();
              } else {
                return Promise.resolve(self._init.body);
              }
            }
            return self._browserRequest.formData();
          } catch (e) {
            return Promise.reject(e);
          }
        }

        this.json = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called json()");
          var self = this;
          try {
            if (self._init &&
              self._init.body) {
              if (!(self._init.body instanceof FormData)) {
                return self._browserRequest.json();
              } else {
                return Promise.reject(new SyntaxError("Unexpected number in JSON at position 1"));
              }
            }
            return self._browserRequest.json();
          } catch (e) {
            return Promise.reject(e);
          }
        }

        this.text = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called text()");
          var self = this;
          try {
            if (self._init &&
              self._init.body) {
              if (!(self._init.body instanceof FormData)) {
                return self._browserRequest.text();
              } else {
                return _formDataToString(self._init.body, self._boundary);
              }
            }
            return self._browserRequest.text();
          } catch (e) {
            return Promise.reject(e);
          }
        }

        this.clone = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest: Called clone()");
          var self = this;
          if (self.headers &&
            self._init &&
            self._init.body &&
            self._init.body instanceof FormData) {
            self._init.headers = self.headers;
          }
          var clonedRequest = new PersistenceRequest(self._input, self._init);
          clonedRequest._browserRequest = self._browserRequest.clone();
          return clonedRequest;
        }
        this.toString = function () {
          logger.log("Offline Persistence Toolkit persistenceRequest:requestToString()");
          if (this._input.url){
            return this._input.url;
          } else {
            return this._input;
          }
        }
      };
      return PersistenceRequest;
    };
    // this is the minimal wrapper version of fetch which we replace the serviceworker
    // version with. We only do this in Safari's serviceworker context to unwrap our
    // wrapped requests so that there are no need to call request._browserRequest
    // The replacement is done in persistenceManager.
    function serviceWorkerFetch(persistenceManager) {
      function serviceWorkerFetchEvent(request) {
        Object.defineProperty(this, 'isReload', {
          value: false,
          enumerable: true
        });
        // clientId is not applicable to a non-ServiceWorker context
        Object.defineProperty(this, 'clientId', {
          value: null,
          enumerable: true
        });
        // client is not applicable to a non-ServiceWorker context
        Object.defineProperty(this, 'client', {
          value: null,
          enumerable: true
        });
        Object.defineProperty(this, 'request', {
          value: request,
          enumerable: true
        });
        Object.defineProperty(this, '_resolveCallback', {
          value: null,
          writable: true
        });
        Object.defineProperty(this, '_rejectCallback', {
          value: null,
          writable: true
        });
      };

      serviceWorkerFetchEvent.prototype.respondWith = function (any) {
        var self = this;
        if (any instanceof Promise) {
          any.then(function (response) {
            self._resolveCallback(response);
          }, function (err) {
            self._rejectCallback(err);
          });
        } else if (typeof (any) == 'function') {
          var response = any();
          self._resolveCallback(response);
        }
      };

      serviceWorkerFetchEvent.prototype._setPromiseCallbacks = function (resolveCallback, rejectCallback) {
        this._resolveCallback = resolveCallback;
        this._rejectCallback = rejectCallback;
      };

      return function (input, init) {
        var request;
        // input can either be a Request object or something
        // which can be passed to the Request constructor
        if (Request.prototype.isPrototypeOf(input) && !init) {
          request = input;
        } else {
          request = new Request(input, init);
        }
        logger.log("Offline Persistence Toolkit serviceWorkerFetch:"+request.url);
        if (request._browserRequest) {
          request = request._browserRequest;
        }
        return new Promise(function (resolve, reject) {
          persistenceManager._browserFetchFunc.call(self, request).then(function (response) {
            resolve(response);
          }, function (error) {
            reject(error);
          });
        })
      };
    }

    function _needsRequestReplaced() {
      // check if it is a safari userAgent and capturing the version
      // this check covers desktops and new versions of ipads
      // user agent is different for older ipads and all iphones
      var isSafari = navigator.userAgent.match(/^(?:(?!chrome|android|iphone|ipad).)*\/([0-9\.]*) safari.*$/i);
      // if there is a match, there is a value otherwise its null
      if (isSafari) {
        var test = isSafari[1].split('.')
        // Check if safari at least v14.1 since this is when the version with formdata is updated.
        if (test[0] >14) {
          return false;
        } else if (test[0] == 14 && test[1] != 0) {
          return false;
        } else {
          return true;
        }
      } else {
        // iOS webkit engine is bundled with the iphone OS version
        // ALL browsers on iOS devices are webkit based
        // ie if broken in safari, will be broken in chrome on iOS device
        var isiOS = navigator.userAgent.match(/(?:iPad|iPhone).*?os ([0-9\_]*)/i);
        // check for iOS version, request + formdata support added in iOS 14_3 + based on browserstack
        if (isiOS) {
          var test = isiOS[1].split('_')
          if (test[0] >14) {
            return false;
          } else if (test[0] == 14 && test[1] >= 3) {
            return false;
          }else {
            return true;
          }
        }
      }
      return false;
    };

    function _formDataToPromiseString(value, key, boundary) {
      return new Promise(function (resolve, reject) {
        var entryText;
        var itemType = value.constructor.name;
        switch (itemType) {
          case "File":
            // File objects must be encoded using FileReader API since on
            // Safari blobs are basically stubs with no conversion operations
            var reader = new FileReader();
            reader.onload = function (evt) {
              entryText = '\r\n' +
                'Content-Disposition: form-data; name="' +
                key.toString() +
                '"; filename="' + value.name +
                '"\r\n' +
                'Content-Type: ' + value.type +
                '\r\n\r\n' +
                evt.target.result +
                '\r\n' +
                boundary;
              resolve(entryText)
            };
            reader.onerror = function () {
              reader.abort();
              reject(new DOMException("Problem parsing input file."));
            };
            reader.readAsText(value);
            break;
          case "String":
            entryText = '\r\n' +
              'Content-Disposition: form-data; name="' +
              key +
              '"\r\n\r\n' +
              value +
              '\r\n' +
              boundary;
            resolve(entryText);
            break;
          default:
            // If a user appends their own objects into the formData
            // using formData.set("key",{value:value}) , when it is transformed
            // into text, it results in the object appearing as [object Object]
            entryText = '\r\n' +
              'Content-Disposition: form-data; name="' +
              key.toString() +
              '"\r\n\r\n' +
              value.toString() +
              '\r\n' +
              boundary;
            resolve(entryText);
            break;
        }
      })
    }

    function _formDataToString(formData, boundary) {
      return new Promise(function (resolve, reject) {
        var promiseArray = [];
        var formDataText = boundary;
        // since fileReader is a promise based API, all formData entries
        // are converted to promise which return string versions of the entry
        // the promise.all then concatinates the strings together.
        formData.forEach(function (value, key) {
          promiseArray.push(_formDataToPromiseString(value, key, boundary));
        })
        Promise.all(promiseArray).then(function (entryText) {
          entryText.forEach(function (entry) {
            formDataText += entry;
          })
          formDataText += "--";
          resolve(formDataText);
        }).catch(function (err) {
          reject(err);
        })
      })
    }

    function _concatBuffers(arrayOfBuffers){
      // helper function to join arraybuffers together
      var result = new Uint8Array(0);
      arrayOfBuffers.forEach(function(arrayBuffer){
        var tmp = new Uint8Array(result.byteLength + arrayBuffer.byteLength);
        tmp.set(new Uint8Array(result), 0);
        tmp.set(new Uint8Array(arrayBuffer), result.byteLength);
        result = tmp;
      })
      return result.buffer;
    }
    function _formDataToPromiseArryBuffer(value, key, boundary) {
      return new Promise(function (resolve, reject) {
        var enc = new TextEncoder();
        var endingBuffer =  enc.encode('\r\n' +boundary).buffer; //endingBuffer does not change
        var itemType = value.constructor.name;
        switch (itemType) {
          case "File":
            // File objects must be encoded using FileReader API since on
            // Safari blobs are basically stubs with no conversion operations
            var reader = new FileReader();
            reader.onload = function (evt) {
              var startingBuffer = enc.encode('\r\n' + 'Content-Disposition: form-data; name="' + key.toString() + '"; filename="' + value.name +'"\r\n' +'Content-Type: ' + value.type +'\r\n\r\n').buffer;
              var fileBuffer = evt.target.result;
              var result = _concatBuffers([startingBuffer,fileBuffer,endingBuffer]);
              resolve(result)
            };
            reader.onerror = function () {
              reader.abort();
              reject(new DOMException("Problem parsing input file."));
            };
            reader.readAsArrayBuffer(value);
            break;
          case "String":
            var startingBuffer = enc.encode('\r\n' + 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n').buffer;
            var stringBuffer = enc.encode(value).buffer;
            var result = _concatBuffers([startingBuffer,stringBuffer,endingBuffer]);
            resolve(result);
            break;
          default:
            // If a user appends their own objects into the formData
            // using formData.set("key",{value:value}) , when it is transformed
            // into text, it results in the object appearing as [object Object]
            var startingBuffer = enc.encode('\r\n' + 'Content-Disposition: form-data; name="' + key.toString() + '"\r\n\r\n').buffer;
            var stringBuffer = enc.encode(value.toString()).buffer;
            var result = _concatBuffers([startingBuffer,stringBuffer,endingBuffer]);
            resolve(result);
            break;
        }
      })
    }
    function _formDataToArrayBuffer(formData, boundary) {
      return new Promise(function (resolve, reject) {
        var promiseArray = [];
        var enc = new TextEncoder()
        var formDataArrayBuffer = enc.encode(boundary).buffer;
        var finalString = enc.encode('--').buffer;
        // since fileReader is a promise based API, all formData entriesÍ
        // are converted to promise which return arrayBuffer versions of the entry
        // the promise.all then concatinates the strings together.
        formData.forEach(function (value, key) {
          promiseArray.push(_formDataToPromiseArryBuffer(value, key, boundary));
        })
        Promise.all(promiseArray).then(function (entryBuffer) {
          entryBuffer.forEach(function (entry) {
            formDataArrayBuffer = _concatBuffers([formDataArrayBuffer,entry]);
          })
          var formDataArrayBufferFinal = _concatBuffers([formDataArrayBuffer,finalString]);
          resolve(formDataArrayBufferFinal);
        }).catch(function (err) {
          reject(err);
        })
      })
    }
    return new PersistenceManager();
    /**
     * @export
     * @class PersistenceSyncManager
     * @classdesc The PersistenceSyncManager should be used support synchronization
     * capabilities for requests which were made when offline. The PersistenceSyncManager
     * instance can be obtained via the {@link PersistenceManager#getSyncManager|persistenceManager.getSyncManager() API}. The
     * PersistenceSyncManager supports operations such as retrieving the sync log,
     * invoking the sync() API to replay the requests which were made while offline,
     * inserting/removing requests from the sync log, adding event listeners for
     * sync operations, and performing undo/redo operations for the shredded local
     * data which were made as a result of those requests.
     * @param {Function} isOnline The persistenceManager.isOnline() function
     * @param {Function} browserFetch The persistenceManager.browserFetch() function
     * @param {Function} cache The persistenceManager.getCache() function
     */

    /**
     * Add an event listener. The listener should always return a Promise which should resolve to either null or an object with the action field.
     * <br>
     * <p>
     * For the beforeSyncRequest event, the resolved value of the Promise returned by the listener should be one of the items given below:
     * <p>
     * <table>
     * <thead>
     * <tr>
     * <th>Resolved Value</th>
     * <th>Behavior</th>
     * </tr>
     * </thead>
     * <tbody>
     * <tr>
     * <td>null</td>
     * <td>Continue replaying the request</td>
     * </tr>
     * <tr>
     * <td>{action: 'replay', request: Request obj}</td>
     * <td>Replay the provided Request obj</td>
     * </tr>
     * <tr>
     * <td>{action: 'skip'}</td>
     * <td>Skip replaying the request</td>
     * </tr>
     * <tr>
     * <td>{action: 'stop'}</td>
     * <td>Stop the sync process</td>
     * </tr>
     * <tr>
     * <td>{action: 'continue'}</td>
     * <td>Continue replaying the request</td>
     * </tr>
     * </tbody>
     * </table>
     * <br>
     * For the syncRequest event, the resolved value of the Promise returned by the listener should be one of the items given below:
     * <p>
     * <table>
     * <thead>
     * <tr>
     * <th>Resolved Value</th>
     * <th>Behavior</th>
     * </tr>
     * </thead>
     * <tbody>
     * <tr>
     * <td>null</td>
     * <td>Continue processing the sync log</td>
     * </tr>
     * <tr>
     * <td>{action: 'stop'}</td>
     * <td>Stop the sync process</td>
     * </tr>
     * <tr>
     * <td>{action: 'continue'}</td>
     * <td>Continue processing the sync log</td>
     * </tr>
     * </tbody>
     * </table>
     * <br>
     * @method
     * @name addEventListener
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {string} type A string representing the event type to listen for. Such as "beforeSyncRequest", "syncRequest", and "afterSyncRequest"
     * @param {Function} listener The function that receives a notification when an event of the specified type occurs. The function should return a Promise.
     * @param {string=} scope optional scope of the Requests. If not specified, will trigger for all Requests.
     * @example
     * var afterRequestListener = function (event) {
     *   var statusCode = event.response.status;
     *   if (statusCode == 200) {
     *     return new Promise(function (resolve, reject) {
     *       // Handle Response Here
     *       resolve({action: 'continue'});
     *     });
     *   }
     *   return Promise.resolve({action: 'continue'});
     * }
     * PersistenceSyncManager.addEventListener('syncRequest',afterRequestListener,'/sync');
     */

    /**
     * Remove the event listener.
     * @method
     * @name removeEventListener
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {string} type A string representing the event type to listen for.
     * @param {Function} listener The function that receives a notification when an event of the specified type occurs. The function should return a Promise.
     * @param {string=} scope optional scope of the Requests. If not specified, will trigger for all Requests.
     */

    /**
     * Returns a Promise which resolves to all the Requests in the Sync Log returned as an Array sorted by
     * the created date of the Request
     * @method
     * @name getSyncLog
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @return {Promise<Array<Request>>} Returns a Promise which resolves to all the Requests in the Sync Log returned as an Array of compound objects
     * which have the following structure:
     * <ul>
     * <li>requestId An internally generated unique id for the Request.</li>
     * <li>request The Request object.</li>
     * <li>undo The undo function which returns a Promise to undo the changes
     * made to the local shredded data store for the Request. The Promise will
     * resolve to true if there is undo data and false if there wasn't any.</li>
     * <li>redo The redo function which returns a Promise to redo the changes
     * made to the local shredded data store for the Request. The Promise will
     * resolve to true if there is redo data and false if there wasn't any.</li>
     * </ul>
     * @example
     * PersistenceSyncManger.getSyncLog().then(function(allRequests){
     * // Your Code Here
     * });
     */

    /**
     * Insert a Request into the Sync Log. The position in the Sync Log the Request will be inserted at
     * is determined by the Request created date.
     * @method
     * @name insertRequest
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {Request} request Request object
     * @param {{undoRedoDataArray: Array}=} options Options
     * <ul>
     * <li>options.undoRedoDataArray optionally specify undo/redo data if this request was shredded. Should be an Array
     * whose entries should have the structure:
     * <ul>
     * <li>operation The operation performed on the local store, e.g. upsert or remove</li>
     * <li>storeName The local store name</li>
     * <li>undoRedoData An Array of compounds object with the following structure containing the undo/redo data
     * <ul>
     * <li>key The key for the shredded data row.</li>
     * <li>undo The undo data for the shredded data row.</li>
     * <li>redo The redo data for the shredded data row.</li>
     * </ul>
     * </li>
     * </ul>
     * </li>
     * </ul>
     * @return {Promise<String>} Returns a Promise which resolves with the Request Id when complete
     * @example
     * PersistenceSyncManager.insertRequest(request).then(function(requestID){
     * // Your Code Here
     * });
     * @example
     * PersistenceSyncManager.insertRequest(request,{undoRedoDataArray:['upsert','localStore',[key,true,false]]}).then(function(requestID){
     * // Your Code Here
     * });
     */

    /**
     * Delete a Request from the Sync Log
     * @method
     * @name removeRequest
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {string} requestId The unique id for the Request
     * @return {Promise<Request>} Returns a Promise which resolves to the removed Request when complete
     * @example
     * PersistenceSyncManager.removeRequest('uniqueid').then(function(removedRequest){
     * // your code here
     * });
     */

    /**
     * Update a Request from the Sync Log. This function effectivaly replaces the
     * Request in the sync log with the provided Request.
     * @method
     * @name updateRequest
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {string} requestId The unique id for the Request
     * @param {Request} request Request object
     * @return {Promise<Request>} Returns a Promise which resolves to the replaced Request when complete
     * @example
     * PersistenceSyncManager.updateRequest('uniqueID',request).then(function(oldRequest){
     * // your code here
     * });
     */

    /**
     * Synchronize the log with the server. By default sync will first send an OPTIONS request before each request URL
     * to determine if the server is reachable. This OPTIONS request will be timed out after 60s and the sync will fail
     * with a HTTP response 504 error. If the OPTIONS request does not time out then sync will progress.
     * @method
     * @name sync
     * @memberof PersistenceSyncManager
     * @export
     * @instance
     * @param {Object=} options Options
     * <ul>
     * <li>options.preflightOptionsRequest - <String,Regex> 'enabled' or 'disabled' or url regex. 'enabled' is the default. 'disabled' will disable sending an OPTIONS request for all URLs. Specifying an URL pattern will enable the OPTIONS request only for those URLs.</li>
     * <li>options.preflightOptionsRequestTimeout - <Number> The timeout for the OPTIONS request. Default is 60s.</li>
     * </ul>
     * @return {Promise} Returns a Promise which resolves when complete
     * @example
     * PersistenceSyncManager.sync().then(function(){
     * // Your code here
     * });
     * @example
     * PersistenceSyncManager.sync({preflightOptionRequest:/employee/g,preflightOptionsRequestTimeout:100})
     */

    /**
     * @export
     * @class OfflineCache
     * @classdesc Offline Persistence Toolkit implementation of the standard
     * {@link https://developer.mozilla.org/en-US/docs/Web/API/Cache|Cache API}.
     * In additional to functionalities provided by the standard Cache API, this
     * OfflineCache also interacts with shredding methods for more fine-grain
     * caching. In addition to the Cache API, the OfflineCache also supports
     * the clear() function to clear the cache.
     * @constructor
     * @param {String} name name of the cache
     * @param {Object} persistencestore instance for cache storage
     */

    /**
     * Retrieve the name of the cache object.
     * @method
     * @name getName
     * @memberof OfflineCache
     * @instance
     * @return {string} Returns the name of the cache object.
     * @example
     * // Assume the cache name is "employee"
     * OfflineCache.getName() // returns "employee"
     */

    /**
     * Takes a request, retrieves it and adds the resulting response
     * object to the cache.
     * @method
     * @name add
     * @memberof OfflineCache
     * @instance
     * @param {Request} request The request object to fetch for response
     *                          and be cached.
     * @return {Promise} returns a Promise that is resolved when the reponse
     *                           is retrieved and request/response is cached.
     * @example
     * OfflineCache.add(request).then(function(){
     * // Your Code Here
     * });
     */

    /**
     * Clear the cache.
     * @method
     * @name clear
     * @memberof OfflineCache
     * @instance
     * @return {Promise<Boolean>} Returns a promise that resolves to true when the cache is cleared.
     * Will resolve to false if not all cache items were able to be cleared.
     * @example
     * OfflineCache.clear().then(function(isCleared){
     * // isCleared is a Boolean value
     * // Your Code here
     * });
     */

    /**
     * Find the first response in this Cache object that match the request with the options.
     * @method
     * @name match
     * @memberof OfflineCache
     * @instance
     * @param {Request} a request object to match against
     * @param {{ignoreSearch: boolean, ignoreMethod: boolean, ignoreVary: boolean}=} options Options to control the matching operation
     * <ul>
     * <li>options.ignoreSearch - A Boolean that specifies whether to ignore
     *                          the query string in the url.  For example,
     *                          if set to true the ?value=bar part of
     *                          http://foo.com/?value=bar would be ignored
     *                          when performing a match. It defaults to false.</li>
     * <li>options.ignoreMethod - A Boolean that, when set to true, prevents
     *                          matching operations from validating the
     *                          Request http method (normally only GET and
     *                          HEAD are allowed.) It defaults to false.</li>
     * <li>options.ignoreVary - A Boolean that when set to true tells the
     *                          matching operation not to perform VARY header
     *                          matching — i.e. if the URL matches you will get
     *                          a match regardless of whether the Response
     *                          object has a VARY header. It defaults to false.</li>
     * </ul>
     * @return {Promise<Response|undefined>} Returns a Promise that resolves to the Response associated with the
     *                           first matching request in the Cache object. If no match is
     *                           found, the Promise resolves to undefined.
     * @example
     * OfflineCache.match(request).then(function(response){
     * // Your code here
     * });
     * @example
     * OfflineCache.match(request,{ignoreSearch:true}).then(function(response){
     * // Your code here
     * });
     */

    /**
     * Finds all responses whose request matches the passed-in request with the specified
     * options.
     * @method
     * @name matchAll
     * @memberof OfflineCache
     * @instance
     * @param {Request} request The request object to match against
     * @param {{ignoreSearch: boolean, ignoreMethod: boolean, ignoreVary: boolean}=} options Options to control the matching operation
     * <ul>
     * <li>options.ignoreSearch A Boolean that specifies whether to ignore
     *                          the query string in the url.  For example,
     *                          if set to true the ?value=bar part of
     *                          http://foo.com/?value=bar would be ignored
     *                          when performing a match. It defaults to false.</li>
     * <li>options.ignoreMethod A Boolean that, when set to true, prevents
     *                          matching operations from validating the
     *                          Request http method (normally only GET and
     *                          HEAD are allowed.) It defaults to false.</li>
     * <li>options.ignoreVary A Boolean that when set to true tells the
     *                          matching operation not to perform VARY header
     *                          matching — i.e. if the URL matches you will get
     *                          a match regardless of whether the Response
     *                          object has a VARY header. It defaults to false.</li>
     * </ul>
     * @return {Promise<Array<Response>>} Returns a Promise that resolves to an array of response objects
     *                            whose request matches the passed-in request.
     * @example
     * OfflineCache.matchAll(request).then(function(responseArray){
     * // Your code here
     * });
     */

    /**
     * Add the request/response pair into the cache.
     * @method
     * @name put
     * @memberof OfflineCache
     * @instance
     * @param {Request} request Request object of the pair
     * @param {Response} response Response object of the pair
     * @return {Promise} Returns a promise when the request/response pair is cached.
     * @example
     * OfflineCache.put(request,response).then(function(){
     * // response has been cached
     * // your code here
     * });
     */

    /**
     * Delete the all the entries in the cache that matches the passed-in request with
     * the specified options.
     * @method
     * @name delete
     * @memberof OfflineCache
     * @instance
     * @param {Request} request The request object to match against
     * @param {{ignoreSearch: boolean, ignoreMethod: boolean, ignoreVary: boolean}=} options Options to control the matching operation
     * <ul>
     * <li>options.ignoreSearch A Boolean that specifies whether to ignore
     *                          the query string in the url.  For example,
     *                          if set to true the ?value=bar part of
     *                          http://foo.com/?value=bar would be ignored
     *                          when performing a match. It defaults to false.</li>
     * <li>options.ignoreMethod A Boolean that, when set to true, prevents
     *                          matching operations from validating the
     *                          Request http method (normally only GET and
     *                          HEAD are allowed.) It defaults to false.</li>
     * <li>options.ignoreVary A Boolean that when set to true tells the
     *                          matching operation not to perform VARY header
     *                          matching — i.e. if the URL matches you will get
     *                          a match regardless of whether the Response
     *                          object has a VARY header. It defaults to false.</li>
     * </ul>
     * @return {Promse<Boolean>} Finds the Cache entry whose key is the request, and if found,
     *                  deletes the Cache entry and returns a Promise that resolves to
     *                  true. If no Cache entry is found, it returns a Promise that
     *                  resolves to false.
     * @example
     * OfflineCache.delete(request).then(function(result){
     * // result is a boolean that based on if the .delete was able to to find and remove a cached entry.
     * });
     */

    /**
     * Retrieves all the keys in this cache.
     * @method
     * @name keys
     * @memberof OfflineCache
     * @instance
     * @param {Request} [request] The request object to match against
     * @param {{ignoreSearch: boolean, ignoreMethod: boolean, ignoreVary: boolean}=} options Options to control the matching operation
     * <ul>
     * <li>options.ignoreSearch A Boolean that specifies whether to ignore
     *                          the query string in the url.  For example,
     *                          if set to true the ?value=bar part of
     *                          http://foo.com/?value=bar would be ignored
     *                          when performing a match. It defaults to false.</li>
     * <li>options.ignoreMethod A Boolean that, when set to true, prevents
     *                          matching operations from validating the
     *                          Request http method (normally only GET and
     *                          HEAD are allowed.) It defaults to false.</li>
     * <li>options.ignoreVary A Boolean that when set to true tells the
     *                          matching operation not to perform VARY header
     *                          matching — i.e. if the URL matches you will get
     *                          a match regardless of whether the Response
     *                          object has a VARY header. It defaults to false.</li>
     * </ul>
     * @return {Promise<Array>} Returns a promise that resolves to an array of Cache keys.
     * @example
     * // with a request object passed in, it will return all keys related that that request
     * OfflineCache.keys(request).then(function(keyArray){
     * //keyArray will only contain key that match the request
     * });
     * @example
     * // without a request object, it will return all keys in the cache
     * OfflineCache.keys().then(function(allKeys){
     * // allKeys contains every key stored in the cache
     * });
     */

    /**
     * Checks if a match to this request with the specified options exist in the
     * cache or not. This is an optimization over match because we don't need to
     * query out the shredded data to fill the response body.
     * @method
     * @name hasMatch
     * @memberof OfflineCache
     * @instance
     * @param {Request} request The request object to match against
     * @param {{ignoreSearch: boolean, ignoreMethod: boolean, ignoreVary: boolean}=} options Options to control the matching operation
     * <ul>
     * <li>options.ignoreSearch A Boolean that specifies whether to ignore
     *                          the query string in the url.  For example,
     *                          if set to true the ?value=bar part of
     *                          http://foo.com/?value=bar would be ignored
     *                          when performing a match. It defaults to false.</li>
     * <li>options.ignoreMethod A Boolean that, when set to true, prevents
     *                          matching operations from validating the
     *                          Request http method (normally only GET and
     *                          HEAD are allowed.) It defaults to false.</li>
     * <li>options.ignoreVary A Boolean that when set to true tells the
     *                          matching operation not to perform VARY header
     *                          matching — i.e. if the URL matches you will get
     *                          a match regardless of whether the Response
     *                          object has a VARY header. It defaults to false.</li>
     * </ul>
     * @return {Promise<Boolean>} Returns a promise that resolves to true if a match exist
     *                   while false otherwise.
     * @example
     * OfflineCache.hasMatch(request).then(function(matchExists){
     * //your code here
     * });
     */

  });