lodash#isEqualWith JavaScript Examples

The following examples show how to use lodash#isEqualWith. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Cloud.js    From lens-extension-cc with MIT License 4 votes vote down vote up
/**
   * @constructor
   * @param {string|Object} urlOrSpec Either the URL for the mgmt cluster (must not end
   *  with a slash ('/')), or an object that matches the `specTs` Typeset and contains
   *  multiple properties to initialize the Cloud (including the URL).
   * @param {IpcMain} [ipcMain] Reference to the IpcMain singleton instance on the MAIN
   *  thread, if this Cloud instance is being created on the MAIN thread; `undefined`
   *  otherwise.
   */
  constructor(urlOrSpec, ipcMain) {
    super();

    const cloudUrl =
      typeof urlOrSpec === 'string' ? urlOrSpec : urlOrSpec.cloudUrl;

    DEV_ENV && rtv.verify({ cloudUrl }, { cloudUrl: Cloud.specTs.cloudUrl });

    // NOTE: `spec` is validated after all properties are declared via `this.update(spec)`
    //  only if it was given

    let _name = null;
    let _syncAll = false;
    let _namespaces = {}; // map of name to CloudNamespace for internal efficiency
    let _syncedProjects = []; // cached from _namespaces for performance
    let _ignoredProjects = []; // cached from _namespaces for performance
    let _token = null;
    let _expiresIn = null;
    let _tokenValidTill = null;
    let _refreshToken = null;
    let _refreshExpiresIn = null;
    let _refreshTokenValidTill = null;
    let _username = null;
    let _config = null;
    let _connectError = null;
    let _connecting = false;
    let _loaded = false;
    let _fetching = false;

    // true if this Cloud's properties are currently being updated via `Cloud.update()`
    //  resulting from a change in the CloudStore
    let _updatingFromStore = false;

    /**
     * @member {string} name The "friendly name" given to the mgmt cluster by
     * the user when they will add new mgmt clusters to the extension.
     */
    Object.defineProperty(this, 'name', {
      enumerable: true,
      get() {
        return _name;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify({ token: newValue }, { token: Cloud.specTs.name });
        if (_name !== newValue) {
          _name = newValue;
          this.dispatchEvent(CLOUD_EVENTS.PROP_CHANGE, {
            isFromStore: _updatingFromStore,
          });
        }
      },
    });

    /**
     * @member {boolean} syncAll True if the user chooses to sync all namespaces in
     *  a mgmt cluster, or false if they pick individual namespaces.
     */
    Object.defineProperty(this, 'syncAll', {
      enumerable: true,
      get() {
        return _syncAll;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify({ token: newValue }, { token: Cloud.specTs.syncAll });
        if (_syncAll !== newValue) {
          _syncAll = newValue;
          this.dispatchEvent(CLOUD_EVENTS.SYNC_CHANGE, {
            isFromStore: _updatingFromStore,
          });
        }
      },
    });

    /**
     * @readonly
     * @member {Array<string>} syncedProjects A simple list of namespace __names__ in
     *  the mgmt cluster that are being synced.
     */
    Object.defineProperty(this, 'syncedProjects', {
      enumerable: true,
      get() {
        return _syncedProjects;
      },
    });

    /**
     * @readonly
     * @member {Array<string>} ignoredProjects A simple list of namespace __names__ in
     *  the mgmt cluster that __not__ being synced.
     */
    Object.defineProperty(this, 'ignoredProjects', {
      enumerable: true,
      get() {
        return _ignoredProjects;
      },
    });

    /**
     * @readonly
     * @member {Array<string>} allProjects Full list of all known namespace __names__.
     *  `syncedProjects` + `ignoredProjects` in one list.
     */
    Object.defineProperty(this, 'allProjects', {
      enumerable: true,
      get() {
        return [..._syncedProjects, ..._ignoredProjects];
      },
    });

    /**
     * @readonly
     * @member {Array<CloudNamespace>} namespaces All known namespaces in this Cloud.
     */
    Object.defineProperty(this, 'namespaces', {
      enumerable: true,
      get() {
        return Object.values(_namespaces);
      },
    });

    /**
     * @readonly
     * @member {Array<CloudNamespace>} syncedNamespaces Only known __synced__ namespaces
     *  in this Cloud.
     */
    Object.defineProperty(this, 'syncedNamespaces', {
      enumerable: true,
      get() {
        return this.namespaces.filter((ns) => ns.synced);
      },
    });

    /**
     * @member {boolean} loaded True if this Cloud's data has been successfully fetched
     *  at least once. Once true, doesn't change again. Use `#fetching` instead for
     *  periodic data fetch state.
     */
    Object.defineProperty(this, 'loaded', {
      enumerable: true,
      get() {
        return _loaded;
      },
      set(newValue) {
        if (_loaded !== !!newValue) {
          _loaded = !!newValue;
          this.dispatchEvent(CLOUD_EVENTS.LOADED_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          ipcMain?.notifyCloudLoadedChange(this.cloudUrl, _loaded);
        }
      },
    });

    /**
     * @member {boolean} fetching True if this Cloud's data is actively being fetched
     *  (e.g. by the SyncManager).
     */
    Object.defineProperty(this, 'fetching', {
      enumerable: true,
      get() {
        return _fetching;
      },
      set(newValue) {
        if (_fetching !== !!newValue) {
          _fetching = !!newValue;
          this.dispatchEvent(CLOUD_EVENTS.FETCHING_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          ipcMain?.notifyCloudFetchingChange(this.cloudUrl, _fetching);
        }
      },
    });

    /** @member {boolean} connecting True if this Cloud is currently trying to connect to MCC. */
    Object.defineProperty(this, 'connecting', {
      enumerable: true,
      get() {
        return _connecting;
      },
      set(newValue) {
        if (_connecting !== !!newValue) {
          _connecting = !!newValue;
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          ipcMain?.notifyCloudStatusChange(this.cloudUrl, {
            connecting: _connecting,
            connectError: this.connectError,
          });
        }
      },
    });

    /** @member {string} connectError */
    Object.defineProperty(this, 'connectError', {
      enumerable: true,
      get() {
        return _connectError;
      },
      set(newValue) {
        const validValue =
          (newValue instanceof Error ? newValue.message : newValue) || null;
        DEV_ENV &&
          rtv.verify(
            { validValue },
            { validValue: [rtv.EXPECTED, rtv.STRING] }
          );
        if (validValue !== _connectError) {
          _connectError = validValue || null;
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          ipcMain?.notifyCloudStatusChange(this.cloudUrl, {
            connecting: this.connecting,
            connectError: _connectError,
          });
        }
      },
    });

    /**
     * @readonly
     * @member {string} status One of the `CONNECTION_STATUSES` enum values.
     */
    Object.defineProperty(this, 'status', {
      enumerable: true,
      get() {
        if (this.connecting) {
          return CONNECTION_STATUSES.CONNECTING;
        }

        // we're not truly connected to the Cloud unless there's no error, we have
        //  a config (can't make API calls without it), we have an API token, and'
        //  we have the ability to refresh it when it expires
        if (!this.connectError && this.token && this.refreshTokenValid) {
          // if we're on MAIN and all we're missing is the config, claim we're "connecting"
          //  because we most likely just restored this Cloud from disk after opening Lens,
          //  and we just haven't attempted to fetch the config yet as part of a data fetch
          // if we're on RENDERER (where we never connect other than for preview purposes),
          //  then we simply can't consider this
          if (ipcMain && !this.config) {
            return CONNECTION_STATUSES.CONNECTING;
          }

          return CONNECTION_STATUSES.CONNECTED;
        }

        return CONNECTION_STATUSES.DISCONNECTED;
      },
    });

    /** @member {Object} config Mgmt cluster config object. */
    Object.defineProperty(this, 'config', {
      enumerable: true,
      get() {
        return _config;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { config: newValue },
            {
              config: [rtv.EXPECTED, rtv.CLASS_OBJECT, { ctor: CloudConfig }],
            }
          );
        if (newValue !== _config) {
          _config = newValue || null;
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // affects status
        }
      },
    });

    /** @member {string|null} token */
    Object.defineProperty(this, 'token', {
      enumerable: true,
      get() {
        return _token;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify({ token: newValue }, { token: Cloud.specTs.id_token });
        if (_token !== newValue) {
          _token = newValue || null; // normalize empty to null
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {number|null} expiresIn __Seconds__ until expiry from time acquired. */
    Object.defineProperty(this, 'expiresIn', {
      enumerable: true,
      get() {
        return _expiresIn;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { expiresIn: newValue },
            { expiresIn: Cloud.specTs.expires_in }
          );
        if (_expiresIn !== newValue) {
          _expiresIn = newValue;
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {Date|null} tokenValidTill */
    Object.defineProperty(this, 'tokenValidTill', {
      enumerable: true,
      get() {
        return _tokenValidTill;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { tokenValidTill: newValue },
            { tokenValidTill: [rtv.EXPECTED, rtv.DATE] }
          );
        if (_tokenValidTill !== newValue) {
          _tokenValidTill = newValue;
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {string|null} refreshToken */
    Object.defineProperty(this, 'refreshToken', {
      enumerable: true,
      get() {
        return _refreshToken;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { refreshToken: newValue },
            { refreshToken: Cloud.specTs.refresh_token }
          );
        if (_refreshToken !== newValue) {
          _refreshToken = newValue || null; // normalize empty to null
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {number|null} refreshExpiresIn __Seconds__ until expiry from time acquired. */
    Object.defineProperty(this, 'refreshExpiresIn', {
      enumerable: true,
      get() {
        return _refreshExpiresIn;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { refreshExpiresIn: newValue },
            { refreshExpiresIn: Cloud.specTs.refresh_expires_in }
          );
        if (_refreshExpiresIn !== newValue) {
          _refreshExpiresIn = newValue;
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {Date|null} refreshTokenValidTill */
    Object.defineProperty(this, 'refreshTokenValidTill', {
      enumerable: true,
      get() {
        return _refreshTokenValidTill;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { refreshTokenValidTill: newValue },
            { refreshTokenValidTill: [rtv.EXPECTED, rtv.DATE] }
          );
        if (_refreshTokenValidTill !== newValue) {
          _refreshTokenValidTill = newValue;
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          });
          this.dispatchEvent(CLOUD_EVENTS.STATUS_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // tokens affect status
        }
      },
    });

    /** @member {string|null} username */
    Object.defineProperty(this, 'username', {
      enumerable: true,
      get() {
        return _username;
      },
      set(newValue) {
        DEV_ENV &&
          rtv.verify(
            { username: newValue },
            { username: Cloud.specTs.username }
          );
        if (_username !== newValue) {
          _username = newValue || null; // normalize empty to null
          this.dispatchEvent(CLOUD_EVENTS.TOKEN_CHANGE, {
            isFromStore: _updatingFromStore,
          }); // related to tokens but not status
        }
      },
    });

    /**
     * @member {string|null} cloudUrl
     */
    Object.defineProperty(this, 'cloudUrl', {
      enumerable: true,
      get() {
        return cloudUrl;
      },
    });

    /**
     * Replaces the Cloud's list of namespaces given an entirely new set. The `syncAll` flag
     *  is __ignored__.
     * @method
     * @param {Array<CloudNamespace|Object>} newNamespaces New namespaces, typically from disk
     *  as plain objects with the `cloudNamespaceTs` shape.
     */
    Object.defineProperty(this, 'replaceNamespaces', {
      // non-enumerable since normally, methods are hidden on the prototype, but in this case,
      //  since we require the private context of the constructor, we have to define it on
      //  the instance itself
      enumerable: false,
      value: function (newNamespaces) {
        DEV_ENV &&
          rtv.verify(
            { newNamespaces },
            { newNamespaces: Cloud.specTs.namespaces }
          );

        // reset since we're getting new set of namespaces
        _namespaces = {};
        _syncedProjects = [];
        _ignoredProjects = [];

        newNamespaces.forEach((ns) => {
          const newCns = new CloudNamespace({
            cloudUrl: this.cloudUrl,
            name: ns,
          });

          if (newCns.synced) {
            _syncedProjects.push(newCns.name);
          } else {
            _ignoredProjects.push(newCns.name);
          }

          _namespaces[newCns.name] = newCns;
        });

        this.dispatchEvent(CLOUD_EVENTS.SYNC_CHANGE, {
          isFromStore: _updatingFromStore,
        });
      },
    });

    /**
     * Updates the Cloud's list of namespaces given all known namespaces and the Cloud's
     *  `syncAll` flag (i.e. add any new namespaces to its synced or ignored list, remove
     *  any old ones, and update ones that have had count changes).
     * @method
     * @param {Array<Namespace>} fetchedNamespaces All existing/known namespaces from the
     *  latest data fetch.
     */
    Object.defineProperty(this, 'updateNamespaces', {
      // non-enumerable since normally, methods are hidden on the prototype, but in this case,
      //  since we require the private context of the constructor, we have to define it on
      //  the instance itself
      enumerable: false,
      value: function (fetchedNamespaces) {
        DEV_ENV &&
          rtv.verify(
            { fetchedNamespaces },
            { fetchedNamespaces: [[rtv.CLASS_OBJECT, { $: Namespace }]] }
          );

        const oldNamespaces = { ..._namespaces };
        const syncedList = [];
        const ignoredList = [];
        let changed = false;

        _namespaces = {}; // reset since we're getting new set of known namespaces

        fetchedNamespaces.forEach((ns) => {
          let synced = false;

          if (this.syncAll) {
            if (this.ignoredProjects.includes(ns.name)) {
              ignoredList.push(ns.name); // keep ignoring it (explicitly ignored)
            } else {
              synced = true;
              syncedList.push(ns.name); // sync it (newly discovered)
            }
          } else {
            if (this.syncedProjects.includes(ns.name)) {
              synced = true;
              syncedList.push(ns.name); // keep syncing it
            } else {
              ignoredList.push(ns.name); // ignore it (newly discovered)
            }
          }

          const newCns = new CloudNamespace({
            cloudUrl: this.cloudUrl,
            name: ns,
            synced,
          });

          // DEEP-compare existing to new so we only notify of SYNC_CHANGE if something
          //  about the namespace has really changed (especially in the case where we
          //  were already syncing it); this will also work if we don't know about
          //  this namespace yet
          if (
            isEqualWith(oldNamespaces[ns.name], newCns, compareCloudNamespaces)
          ) {
            // no changes: use old/existing one since it's deep-equal
            _namespaces[ns.name] = oldNamespaces[ns.name];
          } else {
            changed = true;
            _namespaces[ns.name] = newCns;
          }
        });

        // check to see if any old namespaces have been removed
        changed ||= Object.keys(oldNamespaces).some(
          (name) => !_namespaces[name]
        );

        _syncedProjects = syncedList;
        _ignoredProjects = ignoredList;

        if (changed) {
          this.dispatchEvent(CLOUD_EVENTS.SYNC_CHANGE, {
            isFromStore: _updatingFromStore,
          });
        }
      },
    });

    /**
     * Updates the Cloud's synced and ignored namespace __names__.
     * @method
     * @param {Array<string>} syncedList A list of namespace __names__ in the mgmt cluster that
     *  should be synced.
     * @param {Array<string>} ignoredList A list of namespace __names__ in the mgmt cluster that
     *  should not be synced.
     * @throws {Error} If either list contains a namespace __name__ that is not already in
     *  the list of all known namespaces in this Cloud.
     */
    Object.defineProperty(this, 'updateSyncedProjects', {
      // non-enumerable since normally, methods are hidden on the prototype, but in this case,
      //  since we bind to the private context of the constructor, we have to define it on
      //  the instance itself
      enumerable: false,
      value: function (syncedList, ignoredList) {
        DEV_ENV &&
          rtv.verify(
            { syncedList, ignoredList },
            {
              syncedList: [[rtv.STRING]],
              ignoredList: [[rtv.STRING]],
            }
          );

        let changed = false;

        syncedList.forEach((name) => {
          if (!_namespaces[name]) {
            throw new Error(
              `Cannot add unknown namespace ${logValue(
                name
              )} to synced set in cloud=${logValue(this.cloudUrl)}`
            );
          }

          if (!_namespaces[name].synced) {
            // wasn't synced before and now will be
            _namespaces[name].synced = true;
            changed = true;
          }
        });

        ignoredList.forEach((name) => {
          if (!_namespaces[name]) {
            throw new Error(
              `Cannot add unknown namespace ${logValue(
                name
              )} to ignored set in cloud=${logValue(this.cloudUrl)}`
            );
          }

          if (_namespaces[name].synced) {
            // was synced before and now will be ignored
            _namespaces[name].synced = false;
            changed = true;
          }
        });

        _syncedProjects = syncedList;
        _ignoredProjects = ignoredList;

        if (changed) {
          this.dispatchEvent(CLOUD_EVENTS.SYNC_CHANGE, {
            isFromStore: _updatingFromStore,
          });
        }
      },
    });

    /**
     * Updates this Cloud given a JSON model matching `Cloud.specTs`, typically originating
     *  from disk. This is basically like calling the constructor, just that it updates this
     *  instance's properties (if changes have occurred) rather than creating a new instance.
     *  Change events are triggered as needed.
     * @method
     * @param {Object} spec JSON object. Must match the `Cloud.specTs` shape.
     * @param {boolean} [isFromStore] Set to true if this update is emanating from disk. This
     *  is important, as any change events fired as a result will have its `info.isFromStore`
     *  flag set to `true` to help prevent circular/endless store updates from Mobx reactions
     *  on both MAIN and RENDERER threads.
     * @returns {Cloud} This instance, for chaining/convenience.
     * @throws {Error} If `spec.cloudUrl` is different from this Cloud's `cloudUrl`.
     *  Don't re-used Clouds. Destroy them and create new ones instead.
     */
    Object.defineProperty(this, 'update', {
      // non-enumerable since normally, methods are hidden on the prototype, but in this case,
      //  since we bind to the private context of the constructor, we have to define it on
      //  the instance itself
      enumerable: false,
      value: function (spec, isFromStore = false) {
        DEV_ENV && rtv.verify({ spec }, { spec: Cloud.specTs });

        if (spec.cloudUrl !== this.cloudUrl) {
          throw new Error(
            `cloudUrl cannot be changed; spec.cloudUrl=${logValue(
              spec.cloudUrl
            )}, cloud=${logValue(this.cloudUrl)}`
          );
        }

        _updatingFromStore = !!isFromStore;

        this.name = spec.name;
        this.syncAll = spec.syncAll;
        this.replaceNamespaces(spec.namespaces);
        this.username = spec.username;

        if (spec.id_token && spec.refresh_token) {
          this.updateTokens(spec);
        } else {
          this.resetTokens();
        }

        _updatingFromStore = false;

        return this;
      },
    });

    //// initialize

    if (typeof urlOrSpec !== 'string') {
      this.update(urlOrSpec);
    }

    // since we assign properties when initializing, and this may cause some events
    //  to get dispatched, make sure we start with a clean slate for any listeners
    //  that get added to this new instance we just constructed
    this.emptyEventQueue();
  }