ecosystem-uas.js

// used to append interfaces to omnicore sdk so that created app and AppStudio can use
import API from './ecosystem-base.js';
import {Logger} from './../function/log-helper.js';
import {InfoType, WarningType} from '../information/informationCode.js';
import {UAS, Success} from '../store/const.js';
import Store from '../store/store.js';
import {ErrorCode} from '../exception/exceptionDesc.js';

const factoryApiUasManagement = function (es) {
  let logModule = 'ecosystem-uas';
  /**
   * The API.UAS namespace provides a set of interfaces for User Administration Service (UAS) management.
   * @alias API.UAS
   * @namespace
   */
  es.UAS = new (function () {
    /**
     * Enum for all user grants
     * @readonly
     * @enum {string}
     * @memberof API.UAS
     */
    this.USERGRANTLIST = {
      UAS_CFG_WRITE: 'UAS_CFG_WRITE',
      UAS_BACKUP: 'UAS_BACKUP',
      UAS_CALIBRATE: 'UAS_CALIBRATE',
      UAS_CONTROLLER_PROPERTIES_WRITE: 'UAS_CONTROLLER_PROPERTIES_WRITE',
      UAS_EVENTLOG_CLEAR: 'UAS_EVENTLOG_CLEAR',
      UAS_FILE_ACCESS_READ: 'UAS_FILE_ACCESS_READ',
      UAS_FILE_ACCESS_READ_WRITE: 'UAS_FILE_ACCESS_READ_WRITE',
      UAS_IO_WRITE: 'UAS_IO_WRITE',
      UAS_REMOTE_WARMSTART: 'UAS_REMOTE_WARMSTART',
      UAS_RESTORE: 'UAS_RESTORE',
      UAS_RAPID_EDIT: 'UAS_RAPID_EDIT',
      UAS_RAPID_LOADPROGRAM: 'UAS_RAPID_LOADPROGRAM',
      UAS_RAPID_MODPOS: 'UAS_RAPID_MODPOS',
      UAS_RAPID_EXECUTE: 'UAS_RAPID_EXECUTE',
      UAS_RAPID_DEBUG: 'UAS_RAPID_DEBUG',
      UAS_SYSTEM_ADMINISTRATION: 'UAS_SYSTEM_ADMINISTRATION',
      UAS_SPEED_DECREASE: 'UAS_SPEED_DECREASE',
      UAS_RAPID_CURRVALUE: 'UAS_RAPID_CURRVALUE',
      UAS_REVOLUTION_COUNTER_UPDATE: 'UAS_REVOLUTION_COUNTER_UPDATE',
      UAS_SYSUPDATE: 'UAS_SYSUPDATE',
      UAS_REMOTE_LOGIN: 'UAS_REMOTE_LOGIN',
      UAS_NETWORK_SECURITY: 'UAS_NETWORK_SECURITY',
      UAS_REMOTE_MOUNT_FILE_ACCESS_READ_WRITE: 'UAS_REMOTE_MOUNT_FILE_ACCESS_READ_WRITE',
      UAS_REMOTE_START_STOP_IN_AUTO: 'UAS_REMOTE_START_STOP_IN_AUTO',
      UAS_FTP_READ: 'UAS_FTP_READ',
      UAS_FTP_WRITE: 'UAS_FTP_WRITE',
      UAS_LOCK_SAFETY_CONFIG: 'UAS_LOCK_SAFETY_CONFIG',
      UAS_SAFETY_SERVICES: 'UAS_SAFETY_SERVICES',
      UAS_SAFETY_SYNCHRONIZATION: 'UAS_SAFETY_SYNCHRONIZATION',
      UAS_KL_MODE_SELECTOR: 'UAS_KL_MODE_SELECTOR',
      UAS_SAFETY_COMMISSIONING_MODE: 'UAS_SAFETY_COMMISSIONING_MODE',
      UAS_REMOTE_MOUNT_FILE_ACCESS_READ: 'UAS_REMOTE_MOUNT_FILE_ACCESS_READ',
      UAS_APPL_KEY_FP_AUTO_LOGOFF: 'UAS_APPL_KEY_FP_AUTO_LOGOFF',
      UAS_UAS_ADMINISTRATION: 'UAS_UAS_ADMINISTRATION',
      UAS_DETACH_FLEXPENDANT: 'UAS_DETACH_FLEXPENDANT',
    };

    /**
     * Functions that have grant requirements.
     * @alias GRANTTYPES
     * @memberof API.UAS
     * @readonly
     * @enum {number}
     */
    this.GRANTTYPES = {
      loadModule: 1,
      deploy: 2,
      updatePosition: 3,
      ioWrite: 4,
      editRAPID: 5,
      executeProgram: 6,
    };

    /**
     * Functions that grant state.
     * @alias GRANTSTATES
     * @memberof API.UAS
     * @readonly
     * @enum {number}
     */
    this.GRANTSTATES = {
      NOT_GRANTED: 'NOT_GRANTED',
      GRANTED: 'GRANTED',
    };

    /**
     * Stable subscription object reference
     * @private
     */
    let uasSubscription = null;

    /**
     * Whether UAS subscription is active
     * @private
     */
    let uasSubscribed = false;

    /**
     * Prevent concurrent subscribe
     * @private
     */
    let uasSubscribePromise = null;

    /**
     * Prevent concurrent unsubscribe
     * @private
     */
    let uasUnsubscribePromise = null;

    /**
     * Whether Store has been initialized once
     * @private
     */
    let uasStoreInitialized = false;

    /**
     * One-time initialization lock
     * @private
     */
    let uasInitPromise = null;

    /**
     * Refresh lock for event-triggered updates
     * @private
     */
    let uasRefreshPromise = null;

    /**
     * Watchers: Map<grantKey, Set<callback>>
     * @private
     */
    const uasGrantWatchers = new Map();

    /**
     * Ensure UAS namespace exists in Store.
     *
     * @private
     * @returns {Object} Store namespace
     */
    const ensureUasNamespace = () => {
      let namespace = Store.getNamespace(UAS);
      if (!namespace) {
        Store.registerNamespace(UAS);
        namespace = Store.getNamespace(UAS);
      }
      return namespace;
    };

    /**
     * Read one grant state from Store.
     *
     * @private
     * @param {string} key
     * @returns {boolean|undefined}
     */
    const readGrantStateFromStore = (key) => {
      const mappingKey = Store.generateMappingKey(UAS, [key]);
      const mapping = ensureUasNamespace().getMapping(mappingKey);
      return mapping ? mapping.value : undefined;
    };

    /**
     * Write one grant state into Store.
     *
     * @private
     * @param {string} key
     * @param {boolean} value
     */
    const writeGrantStateToStore = (key, value) => {
      const namespace = ensureUasNamespace();
      const mappingKey = Store.generateMappingKey(UAS, [key]);
      namespace.setMapping(mappingKey, {state: Success, value: Boolean(value)});
    };

    /**
     * Build complete grant state map from current grants array.
     *
     * @private
     * @param {string[]} grants
     * @returns {Object<string, boolean>}
     */
    const buildGrantStateMap = (grants) => {
      const grantSet = new Set(Array.isArray(grants) ? grants : []);
      const stateMap = {};

      for (const grantKey of Object.values(this.USERGRANTLIST)) {
        stateMap[grantKey] = grantSet.has(grantKey);
      }

      return stateMap;
    };

    /**
     * Write the full state map into Store WITHOUT notifying watchers.
     * Used only for first-time initialization.
     *
     * @private
     * @param {Object<string, boolean>} stateMap
     */
    const initializeGrantStoreSilently = (stateMap) => {
      for (const grantKey of Object.values(this.USERGRANTLIST)) {
        writeGrantStateToStore(grantKey, Boolean(stateMap[grantKey]));
      }
      uasStoreInitialized = true;
    };

    /**
     * Notify watchers of one specific grant.
     *
     * @private
     * @param {string} key
     * @param {boolean} value
     */
    const notifyGrantWatchers = (key, value) => {
      const watchers = uasGrantWatchers.get(key);
      if (!watchers || watchers.size === 0) return;

      for (const callback of watchers) {
        try {
          callback(value);
        } catch (err) {
          Logger.w(
            WarningType.RobotOperation,
            `monitorUserGrant callback failed for ${key}: ${err && err.message ? err.message : err}`,
          );
        }
      }
    };

    /**
     * Refresh Store from controller after a UAS event,
     * then notify watchers only for grants whose boolean changed.
     *
     * @private
     * @returns {Promise<void>}
     */
    const refreshUasGrantStoreFromEvent = async () => {
      if (uasRefreshPromise) {
        return uasRefreshPromise;
      }

      uasRefreshPromise = (async () => {
        try {
          const grants = await this.getUserGrants();
          const nextStateMap = buildGrantStateMap(grants);

          for (const grantKey of Object.values(this.USERGRANTLIST)) {
            const prevValue = readGrantStateFromStore(grantKey);
            const nextValue = Boolean(nextStateMap[grantKey]);

            writeGrantStateToStore(grantKey, nextValue);

            if (prevValue !== nextValue) {
              notifyGrantWatchers(grantKey, nextValue);
            }
          }

          Logger.i(InfoType.RobotOperation, 'UAS grant store refreshed from event.');
        } catch (err) {
          Logger.w(
            WarningType.RobotOperation,
            `Failed to refresh UAS grant store from event: ${err && err.message ? err.message : err}`,
          );
          throw err;
        } finally {
          uasRefreshPromise = null;
        }
      })();

      return uasRefreshPromise;
    };

    /**
     * Ensure the UAS subscription object exists.
     *
     * @private
     */
    const ensureUasSubscriptionObject = () => {
      if (uasSubscription) return;

      uasSubscription = {
        getResourceString() {
          return '/rw/system/uas';
        },

        getTitle() {
          return 'UAS Subscription';
        },

        onchanged: async (newValue) => {
          // Logger.i(InfoType.RobotOperation, `UAS event received: ${JSON.stringify(newValue)}`);

          try {
            await refreshUasGrantStoreFromEvent();
          } catch (err) {
            Logger.w(
              WarningType.RobotOperation,
              `Failed to process UAS event: ${err && err.message ? err.message : err}`,
            );
          }
        },
      };
    };

    /**
     * Ensure UAS is fully initialized:
     * 1) start subscription
     * 2) fetch grants once
     * 3) write Store silently
     *
     * IMPORTANT:
     * - No watcher notification during initialization
     * - Concurrent calls share the same promise
     *
     * @private
     * @returns {Promise<void>}
     */
    const ensureUasInitialized = () => {
      if (uasStoreInitialized) {
        return Promise.resolve();
      }

      if (uasInitPromise) {
        return uasInitPromise;
      }

      uasInitPromise = (async () => {
        await this.subscribeRes();

        const grants = await this.getUserGrants();
        const stateMap = buildGrantStateMap(grants);

        initializeGrantStoreSilently(stateMap);

        Logger.i(InfoType.RobotOperation, 'UAS grant store initialized.');
      })().finally(() => {
        uasInitPromise = null;
      });

      return uasInitPromise;
    };

    // =========================
    // public api
    // =========================

    /**
     * Gets the user grants.
     * @alias getUserGrants
     * @memberof API.UAS
     * @returns {Promise<string[]>}
     * @example
     * await API.UAS.getUserGrants()
     */
    this.getUserGrants = async function () {
      // get logged user name
      let loginInfo = await RWS.UAS.getUser();
      let userName = loginInfo['name'];

      let userGrants = await API.RWS.UAS.getGrantsOfUser(userName);

      return userGrants;
    };

    /**
     * Checks whether the logged in user has function-required grants.
     * @alias hasSpecificGrants
     * @memberof API.UAS
     * @param {API.UAS.GRANTTYPES} [type] Funciton types.
     * @returns {Promise<boolean>}
     * @example
     * await API.UAS.hasSpecificGrants(API.UAS.GRANTTYPES.loadModule)
     */
    this.hasSpecificGrants = async function (type) {
      this.userGrants = await this.getUserGrants();
      // Logger.d(InfoType.RobotOperation, `The current user owned the grants: ${this.userGrants.toString()}`);

      let requiredGrants = [];

      switch (type) {
        case this.GRANTTYPES.loadModule:
          requiredGrants = [this.USERGRANTLIST.UAS_FILE_ACCESS_READ_WRITE, this.USERGRANTLIST.UAS_RAPID_LOADPROGRAM];
          break;
        case this.GRANTTYPES.deploy:
          requiredGrants = [this.USERGRANTLIST.UAS_FILE_ACCESS_READ_WRITE];
          break;
        case this.GRANTTYPES.updatePosition:
          requiredGrants = [this.USERGRANTLIST.UAS_RAPID_CURRVALUE];
          break;
        case this.GRANTTYPES.ioWrite:
          requiredGrants = [this.USERGRANTLIST.UAS_IO_WRITE];
          break;
        case this.GRANTTYPES.editRAPID:
          requiredGrants = [this.USERGRANTLIST.UAS_RAPID_EDIT];
          break;
        case this.GRANTTYPES.executeProgram:
          requiredGrants = [this.USERGRANTLIST.UAS_RAPID_EXECUTE];
          break;
        default:
          break;
      }
      return requiredGrants.every((grant) => this.userGrants.includes(grant));
    };

    this.isLoggedInAsLocalClient = async function () {
      let loginInfo = await RWS.UAS.getUser();
      return (loginInfo['name'] && loginInfo['locale'] && loginInfo['locale'] == 'local') || false;
    };

    /**
     * Start UAS subscription only.
     * Does not initialize Store by itself.
     *
     * @returns {Promise<void>}
     */
    this.subscribeRes = () => {
      if (uasSubscribed) {
        return Promise.resolve();
      }

      if (uasSubscribePromise) {
        return uasSubscribePromise;
      }

      ensureUasSubscriptionObject();

      uasSubscribePromise = RWS.Subscriptions.subscribe([uasSubscription])
        .then(() => {
          uasSubscribed = true;
          Logger.i(InfoType.RobotOperation, 'UAS subscription successfully registered.');
        })
        .catch((err) => {
          uasSubscribed = false;
          Logger.w(WarningType.RobotOperation, `UAS subscription failed: ${err && err.message ? err.message : err}`);
          return API.rejectWithStatus(`Subscription to ${uasSubscription.getTitle()} failed.`, err, {
            errorCode: ErrorCode.FailedToSubscribeResource,
          });
        })
        .finally(() => {
          uasSubscribePromise = null;
        });

      return uasSubscribePromise;
    };

    /**
     * Stop UAS subscription.
     *
     * @returns {Promise<void>}
     */
    this.unsubscribeRes = () => {
      if (uasUnsubscribePromise) {
        return uasUnsubscribePromise;
      }

      if (!uasSubscribed || !uasSubscription) {
        return Promise.resolve();
      }

      uasUnsubscribePromise = RWS.Subscriptions.unsubscribe([uasSubscription])
        .then(() => {
          uasSubscribed = false;
          Logger.i(InfoType.RobotOperation, 'UAS subscription successfully unsubscribed.');
        })
        .catch((err) => {
          Logger.w(WarningType.RobotOperation, `Failed to unsubscribe UAS: ${err && err.message ? err.message : err}`);
          return API.rejectWithStatus(`Unsubscription from ${uasSubscription.getTitle()} failed.`, err, {
            errorCode: ErrorCode.FailedToUnsubscribeResource,
          });
        })
        .finally(() => {
          uasUnsubscribePromise = null;
        });

      return uasUnsubscribePromise;
    };

    /**
     * Monitor one specific grant.
     *
     * Behavior:
     * 1) Ensure Store is initialized silently
     * 2) Register watcher
     * 3) Invoke ONLY this callback once with current cached value
     *
     * It does NOT trigger other callbacks.
     *
     * @param {string} key Grant name
     * @param {Function} callback Receives boolean
     * @returns {Promise<Function>} disposer
     */
    this.monitorUserGrant = async (key, callback) => {
      if (!Object.values(this.USERGRANTLIST).includes(key)) {
        throw new Error(`Unknown UAS grant key: ${String(key)}`);
      }

      if (typeof callback !== 'function') {
        throw new Error('monitorUserGrant requires callback to be a function.');
      }

      // Ensure subscription + store init once, silently
      await ensureUasInitialized();

      // Register watcher AFTER initialization
      if (!uasGrantWatchers.has(key)) {
        uasGrantWatchers.set(key, new Set());
      }
      uasGrantWatchers.get(key).add(callback);

      // Call only this callback once with current value
      const currentValue = Boolean(readGrantStateFromStore(key));
      callback(currentValue);

      // Return disposer for this callback
      return () => {
        this.unmonitorUserGrant(key, callback);
      };
    };

    /**
     * Remove one watcher callback for a grant.
     *
     * @param {string} key
     * @param {Function} [callback]
     */
    this.unmonitorUserGrant = (key, callback) => {
      const watchers = uasGrantWatchers.get(key);
      if (!watchers) return;

      if (typeof callback === 'function') {
        watchers.delete(callback);
      } else {
        watchers.clear();
      }

      if (watchers.size === 0) {
        uasGrantWatchers.delete(key);
      }
    };

    /**
     * Optional full cleanup helper.
     *
     * @returns {Promise<void>}
     */
    this.disposeUasMonitoring = async () => {
      try {
        await this.unsubscribeRes();
      } finally {
        uasGrantWatchers.clear();
        uasSubscription = null;
        uasSubscribed = false;
        uasSubscribePromise = null;
        uasUnsubscribePromise = null;
        uasStoreInitialized = false;
        uasInitPromise = null;
        uasRefreshPromise = null;
      }
    };
  })();

  es.constructUasManagement = true;
};

if (typeof API.constructUasManagement === 'undefined') {
  factoryApiUasManagement(API);
}

export default API;
export {factoryApiUasManagement};