# av.js

    const _ = require('underscore');
    const uuid = require('uuid/v4');
    const userAgent = require('./ua');
    const { inherits, parseDate } = require('./utils');
    const Promise = require('./promise');

    const AV = global.AV || {};

    // All internal configuration items
    AV._config = {
      serverURLs: {},
      useMasterKey: false,
      production: null,
      realtime: null,
      requestTimeout: null,
    };

    // configs shared by all AV instances
    AV._sharedConfig = {
      userAgent,
      liveQueryRealtime: null,
    };

    /**
     * Contains all AV API classes and functions.
     * @namespace AV
     */

    /**
     * Returns prefix for localStorage keys used by this instance of AV.
     * @param {String} path The relative suffix to append to it.
     *     null or undefined is treated as the empty string.
     * @return {String} The full key name.
     * @private
     */
    AV._getAVPath = function(path) {
      if (!AV.applicationId) {
        throw new Error('You need to call AV.initialize before using AV.');
      }
      if (!path) {
        path = '';
      }
      if (!_.isString(path)) {
        throw new Error("Tried to get a localStorage path that wasn't a String.");
      }
      if (path[0] === '/') {
        path = path.substring(1);
      }
      return 'AV/' + AV.applicationId + '/' + path;
    };

    /**
     * Returns the unique string for this app on this machine.
     * Gets reset when localStorage is cleared.
     * @private
     */
    AV._installationId = null;
    AV._getInstallationId = () => {
      // See if it's cached in RAM.
      if (AV._installationId) {
        return Promise.resolve(AV._installationId);
      }

      // Try to get it from localStorage.
      const path = AV._getAVPath('installationId');
      return AV.localStorage.getItemAsync(path).then(_installationId => {
        AV._installationId = _installationId;
        if (!AV._installationId) {
          // It wasn't in localStorage, so create a new one.
          AV._installationId = _installationId = uuid();
          return AV.localStorage
            .setItemAsync(path, _installationId)
            .then(() => _installationId);
        }
        return _installationId;
      });
    };

    AV._subscriptionId = null;
    AV._refreshSubscriptionId = (path = AV._getAVPath('subscriptionId')) => {
      const subscriptionId = (AV._subscriptionId = uuid());
      return AV.localStorage
        .setItemAsync(path, subscriptionId)
        .then(() => subscriptionId);
    };
    AV._getSubscriptionId = () => {
      // See if it's cached in RAM.
      if (AV._subscriptionId) {
        return Promise.resolve(AV._subscriptionId);
      }

      // Try to get it from localStorage.
      const path = AV._getAVPath('subscriptionId');
      return AV.localStorage.getItemAsync(path).then(_subscriptionId => {
        AV._subscriptionId = _subscriptionId;
        if (!AV._subscriptionId) {
          // It wasn't in localStorage, so create a new one.
          _subscriptionId = AV._refreshSubscriptionId(path);
        }
        return _subscriptionId;
      });
    };

    AV._parseDate = parseDate;

    // A self-propagating extend function.
    AV._extend = function(protoProps, classProps) {
      var child = inherits(this, protoProps, classProps);
      child.extend = this.extend;
      return child;
    };

    /**
     * Converts a value in a AV Object into the appropriate representation.
     * This is the JS equivalent of Java's AV.maybeReferenceAndEncode(Object)
     * if seenObjects is falsey. Otherwise any AV.Objects not in
     * seenObjects will be fully embedded rather than encoded
     * as a pointer.  This array will be used to prevent going into an infinite
     * loop because we have circular references.  If <seenObjects>
     * is set, then none of the AV Objects that are serialized can be dirty.
     * @private
     */
    AV._encode = function(value, seenObjects, disallowObjects, full = true) {
      if (value instanceof AV.Object) {
        if (disallowObjects) {
          throw new Error('AV.Objects not allowed here');
        }
        if (!seenObjects || _.include(seenObjects, value) || !value._hasData) {
          return value._toPointer();
        }
        return value._toFullJSON(seenObjects.concat(value), full);
      }
      if (value instanceof AV.ACL) {
        return value.toJSON();
      }
      if (_.isDate(value)) {
        return full ? { __type: 'Date', iso: value.toJSON() } : value.toJSON();
      }
      if (value instanceof AV.GeoPoint) {
        return value.toJSON();
      }
      if (_.isArray(value)) {
        return _.map(value, function(x) {
          return AV._encode(x, seenObjects, disallowObjects, full);
        });
      }
      if (_.isRegExp(value)) {
        return value.source;
      }
      if (value instanceof AV.Relation) {
        return value.toJSON();
      }
      if (value instanceof AV.Op) {
        return value.toJSON();
      }
      if (value instanceof AV.File) {
        if (!value.url() && !value.id) {
          throw new Error('Tried to save an object containing an unsaved file.');
        }
        return value._toFullJSON(seenObjects, full);
      }
      if (_.isObject(value)) {
        return _.mapObject(value, (v, k) =>
          AV._encode(v, seenObjects, disallowObjects, full)
        );
      }
      return value;
    };

    /**
     * The inverse function of AV._encode.
     * @private
     */
    AV._decode = function(value, key) {
      if (!_.isObject(value) || _.isDate(value)) {
        return value;
      }
      if (_.isArray(value)) {
        return _.map(value, v => AV._decode(v));
      }
      if (value instanceof AV.Object) {
        return value;
      }
      if (value instanceof AV.File) {
        return value;
      }
      if (value instanceof AV.Op) {
        return value;
      }
      if (value instanceof AV.GeoPoint) {
        return value;
      }
      if (value instanceof AV.ACL) {
        return value;
      }
      if (key === 'ACL') {
        return new AV.ACL(value);
      }
      if (value.__op) {
        return AV.Op._decode(value);
      }
      var className;
      if (value.__type === 'Pointer') {
        className = value.className;
        var pointer = AV.Object._create(className);
        if (Object.keys(value).length > 3) {
          const v = _.clone(value);
          delete v.__type;
          delete v.className;
          pointer._finishFetch(v, true);
        } else {
          pointer._finishFetch({ objectId: value.objectId }, false);
        }
        return pointer;
      }
      if (value.__type === 'Object') {
        // It's an Object included in a query result.
        className = value.className;
        const v = _.clone(value);
        delete v.__type;
        delete v.className;
        var object = AV.Object._create(className);
        object._finishFetch(v, true);
        return object;
      }
      if (value.__type === 'Date') {
        return AV._parseDate(value.iso);
      }
      if (value.__type === 'GeoPoint') {
        return new AV.GeoPoint({
          latitude: value.latitude,
          longitude: value.longitude,
        });
      }
      if (value.__type === 'Relation') {
        if (!key) throw new Error('key missing decoding a Relation');
        var relation = new AV.Relation(null, key);
        relation.targetClassName = value.className;
        return relation;
      }
      if (value.__type === 'File') {
        var file = new AV.File(value.name);
        const v = _.clone(value);
        delete v.__type;
        file._finishFetch(v);
        return file;
      }
      return _.mapObject(value, AV._decode);
    };

    /**
     * The inverse function of {@link AV.Object#toFullJSON}.
     * @since 2.0.0
     * @method
     * @param {Object}
     * return {AV.Object|AV.File|any}
     */
    AV.parseJSON = AV._decode;

    AV._encodeObjectOrArray = function(value) {
      var encodeAVObject = function(object) {
        if (object && object._toFullJSON) {
          object = object._toFullJSON([]);
        }

        return _.mapObject(object, function(value) {
          return AV._encode(value, []);
        });
      };

      if (_.isArray(value)) {
        return value.map(function(object) {
          return encodeAVObject(object);
        });
      } else {
        return encodeAVObject(value);
      }
    };

    AV._arrayEach = _.each;

    /**
     * Does a deep traversal of every item in object, calling func on every one.
     * @param {Object} object The object or array to traverse deeply.
     * @param {Function} func The function to call for every item. It will
     *     be passed the item as an argument. If it returns a truthy value, that
     *     value will replace the item in its parent container.
     * @returns {} the result of calling func on the top-level object itself.
     * @private
     */
    AV._traverse = function(object, func, seen) {
      if (object instanceof AV.Object) {
        seen = seen || [];
        if (_.indexOf(seen, object) >= 0) {
          // We've already visited this object in this call.
          return;
        }
        seen.push(object);
        AV._traverse(object.attributes, func, seen);
        return func(object);
      }
      if (object instanceof AV.Relation || object instanceof AV.File) {
        // Nothing needs to be done, but we don't want to recurse into the
        // object's parent infinitely, so we catch this case.
        return func(object);
      }
      if (_.isArray(object)) {
        _.each(object, function(child, index) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[index] = newChild;
          }
        });
        return func(object);
      }
      if (_.isObject(object)) {
        AV._each(object, function(child, key) {
          var newChild = AV._traverse(child, func, seen);
          if (newChild) {
            object[key] = newChild;
          }
        });
        return func(object);
      }
      return func(object);
    };

    /**
     * This is like _.each, except:
     * * it doesn't work for so-called array-like objects,
     * * it does work for dictionaries with a "length" attribute.
     * @private
     */
    AV._objectEach = AV._each = function(obj, callback) {
      if (_.isObject(obj)) {
        _.each(_.keys(obj), function(key) {
          callback(obj[key], key);
        });
      } else {
        _.each(obj, callback);
      }
    };

    module.exports = AV;