scenes_context.js

const debug = require('debug')('opengram:scenes:context')
const Composer = require('../composer')

const noop = () => Promise.resolve()
const now = () => Math.floor(Date.now() / 1000)

/**
 * @class
 * @memberof Scenes
 */
class SceneContext {
  constructor (ctx, scenes, options) {
    /** @type {OpengramContext} */
    this.ctx = ctx
    /** @type {Map<string, Scenes.WizardScene|Scenes.BaseScene>} */
    this.scenes = scenes
    /** @type {boolean} */
    this.leaving = false
    /** @type {StageOptions} */
    this.options = options
  }

  /**
   * Getter returns current scene session object
   *
   * @return {object}
  */
  get session () {
    const sessionName = this.options.sessionName
    let session = this.ctx[sessionName].__scenes || {}
    if (session.expires < now()) {
      session = {}
    }
    this.ctx[sessionName].__scenes = session
    return session
  }

  /**
   * Getter returns state of current scene
   *
   * @return {object}
   */
  get state () {
    this.session.state = this.session.state || {}
    return this.session.state
  }

  /**
   * Setter sets state of current scene
   *
   * @param {object} value New state value
   */
  set state (value) {
    this.session.state = { ...value }
  }

  /**
   * @return {undefined|Scenes.WizardScene|Scenes.BaseScene}
   */
  get current () {
    const sceneId = this.session.current || this.options.default
    return sceneId === undefined || !this.scenes.has(sceneId)
      ? undefined
      : this.scenes.get(sceneId)
  }

  /**
   * Resets scenes data
   *
   * @return {void}
   */
  reset () {
    const sessionName = this.options.sessionName
    delete this.ctx[sessionName].__scenes
  }

  /**
   * Enter to scene by name
   *
   * Use `initialState` to pass some initial data of `ctx.scene.state`
   *
   * @param {string} sceneId Scene name
   * @param {object} [initialState] Scene initial state
   * @param {boolean} [silent] If true, enters to given scene without calling `enter`handler, and without calling `leave` handler for current scene (if user currently in scene)
   * @throws {Error}
   * @return {Promise}
   */
  async enter (sceneId, initialState, silent) {
    if (!sceneId || !this.scenes.has(sceneId)) {
      throw new Error(`Can't find scene: ${sceneId}`)
    }

    if (!silent) {
      await this.leave()
    }

    debug('Entering scene', sceneId, initialState, silent)
    this.session.current = sceneId
    this.state = initialState
    const ttl = this.current.ttl || this.options.ttl

    if (ttl) {
      this.session.expires = now() + ttl
    }

    if (!this.current || silent) {
      return
    }

    const handler =
      'enterMiddleware' in this.current &&
      typeof this.current.enterMiddleware === 'function'
        ? this.current.enterMiddleware()
        : this.current.middleware()
    return await handler(this.ctx, noop)
  }

  /**
   * Used for re-entering to current scene without destroying `ctx.scene.state`
   *
   * @throws {Error}
   * @return {Promise}
   */
  reenter () {
    return this.enter(this.session.current, this.state)
  }

  /**
   * Used to exit the current scene
   *
   * @return {Promise<void>}
   */
  async leave () {
    if (this.leaving) return
    debug('Leaving scene')
    try {
      this.leaving = true

      if (this.current === undefined) {
        return
      }

      const handler =
        'leaveMiddleware' in this.current &&
        typeof this.current.leaveMiddleware === 'function'
          ? this.current.leaveMiddleware()
          : Composer.passThru()

      await handler(this.ctx, noop)

      return this.reset()
    } finally {
      this.leaving = false
    }
  }
}

module.exports = SceneContext