components/player-manager/PlayerManager.js

import {
  isString,
} from '@ircam/sc-utils';

import ComoComponent from '../../core/ComoComponent.js';
import Player from './Player.js';

/**
 * The PlayerManager is responsible for the management of the players, which are
 * themselves defined as a link between a {@link ComoSource} and a {@link ComoScript}.
 *
 * The player manager gives access to the list of existing players, can create
 * full featured Player or mirrors of existing one (i.e. duplicate a player instance
 * on another node of the network)
 *
 * @see {@link Player}
 */
class PlayerManager extends ComoComponent {
  #ownedPlayers = new Set();
  #players; // collection of player states

  /**
   * @hideconstructor
   * @param {ComoNode} como
   * @param {String} name
   */
  constructor(como, name) {
    super(como, name);

    if (!this.como.sourceManager) {
      throw new Error(`Cannot construct PlayerManager: relies on 'como.sourceManager'`);
    }

    if (!this.como.scriptManager) {
      throw new Error(`Cannot construct PlayerManager: relies on 'como.scriptManager'`);
    }

    if (!this.como.sessionManager) {
      throw new Error(`Cannot construct PlayerManager: relies on 'como.sessionManager'`);
    }

    this.como.setRfcHandler(`${this.name}:createPlayer`, this.#createPlayer);
  }

  /**
   * The living collection of all players underlying shared states.
   *
   * @return {SharedStateCollection}
   */
  get players() {
    return this.#players;
  }

  /** @private */
  async start() {
    await super.start();

    this.#players = await this.como.stateManager.getCollection(`${this.name}:player`);
  }

  /**
   * Returns wether the player exists on the network
   *
   * @param {String} playerId
   * @returns {Boolean}
   */
  playerExists(playerId) {
    return !!this.players.find(player => player.get('id') === playerId);
  }

  /**
   * Create a player on a given {@link ComoNode}.
   *
   * @param {String} sourceId - Id of the source
   * @param {Object} [options]
   * @param {String} [options.nodeId=this.como.nodeId] - Optional id of the {@link ComoNode} where
   *  the player should be created, defaults to the node where the function is called
   * @param {String} [options.id=null] - Optional user-defined id to assign
   *  to the player. _Important: that for now this is the responsibility of the client code
   *  to ensure the ids remain unique across the network._
   * @returns {String} Id of the player
   */
  async createPlayer(sourceId, {
    nodeId = this.como.nodeId,
    id = null,
  }) {
    if (!this.como.sourceManager.sourceExists(sourceId)) {
      throw new Error(`Cannot execute "createPlayer" on PlayerManager: source with id ("${sourceId}") does not exists`);
    }

    return await this.como.requestRfc(nodeId, `${this.name}:createPlayer`, {
      sourceId,
      id,
    });
  }

  /** @private */
  #createPlayer = async ({ sourceId, id  }) => {
    const player = new Player(this.como, sourceId, id );
    await player.init();

    this.#ownedPlayers.add(player);

    return player.id;
  }

  /**
   * @todo - Implement
   * @private
   */
  async deletePlayer(playerId) {

  }

      // attach to exiting script shared state, if any, rather than creating a new instance
  /**
   * Get the full {@link Player} API access of a given player.
   *
   * If called on another {@link ComoNode}, ``getPlayerthis`` basically creates
   * a clone of the original player. Useful for example to duplicate the audio
   * of a node on another node.
   *
   * @param {String} playerId
   * @returns {Player|null}
   */
  async getPlayer(playerId) {
    if (!isString(playerId)) {
      throw new Error(`Cannot execute "getPlayer" on PlayerManager: argument 1 ("${playerId}") is not a string`);
    }

    const owned = Array.from(this.#ownedPlayers).find(player => player.id === playerId);

    if (owned) {
      return owned;
    }

    const notOwned = this.players.find(player => player.get('id') === playerId);

    if (notOwned) {
      const player = new Player(this.como, notOwned.get('sourceId'));
      await player.init(notOwned); // init with attached state from the collection
      return player;
    }

    return null;
  }

  /**
   * Get the underlying state of a player without the full {{@link Player} logic
   * and behavior.
   *
   * @param {String} playerId
   * @returns {Player|null}
   */
  async getPlayerState(playerId) {
    const player = this.players.find(player => player.get('id') === playerId);
    return player || null;
  }

  /**
   * Return the SharedState associated to the script assigned to the given player.
   *
   * Return null if the player is not associated to a script, or if the associated
   * script does not define a shared state.
   *
   * @param {String} playerId
   * @returns {SharedState|null}
   */
  async getScriptSharedState(playerId) {
    const player = this.players.find(player => player.get('id') === playerId);

    if (!player) {
      throw new Error(`Cannot execute "getScriptSharedState" on PlayerManager: player (${playerId}) does not exist`);
    }

    const scriptSharedStateClassName = player.get('scriptSharedStateClassName');
    const scriptSharedStateId = player.get('scriptSharedStateId');

    if (scriptSharedStateClassName !== null) {
      const scriptState = await this.como.stateManager.attach(
        scriptSharedStateClassName,
        scriptSharedStateId
      );

      return scriptState;
    }

    return null;
  }

}

export default PlayerManager;