app/extend/context.js

'use strict';

const delegate = require('delegates');
const { assign } = require('utility');
const eggUtils = require('egg-core').utils;

const HELPER = Symbol('Context#helper');
const LOCALS = Symbol('Context#locals');
const LOCALS_LIST = Symbol('Context#localsList');
const COOKIES = Symbol('Context#cookies');
const CONTEXT_LOGGERS = Symbol('Context#logger');
const CONTEXT_HTTPCLIENT = Symbol('Context#httpclient');
const CONTEXT_ROUTER = Symbol('Context#router');

const proto = module.exports = {

  /**
   * Get the current visitor's cookies.
   */
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new this.app.ContextCookies(this, this.app.keys, this.app.config.cookies);
    }
    return this[COOKIES];
  },

  /**
   * Get a wrapper httpclient instance contain ctx in the hold request process
   *
   * @return {ContextHttpClient} the wrapper httpclient instance
   */
  get httpclient() {
    if (!this[CONTEXT_HTTPCLIENT]) {
      this[CONTEXT_HTTPCLIENT] = new this.app.ContextHttpClient(this);
    }
    return this[CONTEXT_HTTPCLIENT];
  },

  /**
   * Shortcut for httpclient.curl
   *
   * @function Context#curl
   * @param {String|Object} url - request url address.
   * @param {Object} [options] - options for request.
   * @return {Object} see {@link ContextHttpClient#curl}
   */
  curl(url, options) {
    return this.httpclient.curl(url, options);
  },

  /**
   * Alias to {@link Application#router}
   *
   * @member {Router} Context#router
   * @since 1.0.0
   * @example
   * ```js
   * this.router.pathFor('post', { id: 12 });
   * ```
   */
  get router() {
    if (!this[CONTEXT_ROUTER]) {
      this[CONTEXT_ROUTER] = this.app.router;
    }
    return this[CONTEXT_ROUTER];
  },

  /**
   * Set router to Context, only use on EggRouter
   * @param {EggRouter} val router instance
   */
  set router(val) {
    this[CONTEXT_ROUTER] = val;
  },

  /**
   * Get helper instance from {@link Application#Helper}
   *
   * @member {Helper} Context#helper
   * @since 1.0.0
   */
  get helper() {
    if (!this[HELPER]) {
      this[HELPER] = new this.app.Helper(this);
    }
    return this[HELPER];
  },

  /**
   * Wrap app.loggers with context infomation,
   * if a custom logger is defined by naming aLogger, then you can `ctx.getLogger('aLogger')`
   *
   * @param {String} name - logger name
   * @return {Logger} logger
   */
  getLogger(name) {
    let cache = this[CONTEXT_LOGGERS];
    if (!cache) {
      cache = this[CONTEXT_LOGGERS] = {};
    }

    // read from cache
    if (cache[name]) return cache[name];

    // get no exist logger
    const appLogger = this.app.getLogger(name);
    if (!appLogger) return null;

    // write to cache
    cache[name] = new this.app.ContextLogger(this, appLogger);
    return cache[name];
  },

  /**
   * Logger for Application, wrapping app.coreLogger with context infomation
   *
   * @member {ContextLogger} Context#logger
   * @since 1.0.0
   * @example
   * ```js
   * this.logger.info('some request data: %j', this.request.body);
   * this.logger.warn('WARNING!!!!');
   * ```
   */
  get logger() {
    return this.getLogger('logger');
  },

  /**
   * Logger for frameworks and plugins,
   * wrapping app.coreLogger with context infomation
   *
   * @member {ContextLogger} Context#coreLogger
   * @since 1.0.0
   */
  get coreLogger() {
    return this.getLogger('coreLogger');
  },

  /**
   * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables,
   * which will be used as data when view is rendering.
   * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`.
   *
   * when you set locals, only object is available
   *
   * ```js
   * this.locals = {
   *   a: 1
   * };
   * this.locals = {
   *   b: 1
   * };
   * this.locals.c = 1;
   * console.log(this.locals);
   * {
   *   a: 1,
   *   b: 1,
   *   c: 1,
   * };
   * ```
   *
   * `ctx.locals` has cache, it only merges `app.locals` once in one request.
   *
   * @member {Object} Context#locals
   */
  get locals() {
    if (!this[LOCALS]) {
      this[LOCALS] = assign({}, this.app.locals);
    }
    if (this[LOCALS_LIST] && this[LOCALS_LIST].length) {
      assign(this[LOCALS], this[LOCALS_LIST]);
      this[LOCALS_LIST] = null;
    }
    return this[LOCALS];
  },

  set locals(val) {
    if (!this[LOCALS_LIST]) {
      this[LOCALS_LIST] = [];
    }
    this[LOCALS_LIST].push(val);
  },

  /**
   * alias to {@link Context#locals}, compatible with koa that use this variable
   * @member {Object} state
   * @see Context#locals
   */
  get state() {
    return this.locals;
  },

  set state(val) {
    this.locals = val;
  },

  /**
   * Run async function in the background
   * @param {Function} scope - the first args is ctx
   * ```js
   * this.body = 'hi';
   *
   * this.runInBackground(async ctx => {
   *   await ctx.mysql.query(sql);
   *   await ctx.curl(url);
   * });
   * ```
   */
  runInBackground(scope) {
    // try to use custom function name first
    /* istanbul ignore next */
    const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true);
    scope._name = taskName;
    this._runInBackground(scope);
  },

  // let plugins or frameworks to reuse _runInBackground in some cases.
  // e.g.: https://github.com/eggjs/egg-mock/pull/78
  _runInBackground(scope) {
    const ctx = this;
    const start = Date.now();
    /* istanbul ignore next */
    const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true);
    // use app.toAsyncFunction to support both generator function and async function
    return ctx.app.toAsyncFunction(scope)(ctx)
      .then(() => {
        ctx.coreLogger.info('[egg:background] task:%s success (%dms)', taskName, Date.now() - start);
      })
      .catch(err => {
        // background task process log
        ctx.coreLogger.info('[egg:background] task:%s fail (%dms)', taskName, Date.now() - start);

        // emit error when promise catch, and set err.runInBackground flag
        err.runInBackground = true;
        ctx.app.emit('error', err, ctx);
      });
  },
};

/**
 * Context delegation.
 */

delegate(proto, 'request')
  /**
   * @member {Boolean} Context#acceptJSON
   * @see Request#acceptJSON
   * @since 1.0.0
   */
  .getter('acceptJSON')
  /**
   * @member {Array} Context#queries
   * @see Request#queries
   * @since 1.0.0
   */
  .getter('queries')
  /**
   * @member {Boolean} Context#accept
   * @see Request#accept
   * @since 1.0.0
   */
  .getter('accept')
  /**
   * @member {string} Context#ip
   * @see Request#ip
   * @since 1.0.0
   */
  .access('ip');

delegate(proto, 'response')
  /**
   * @member {Number} Context#realStatus
   * @see Response#realStatus
   * @since 1.0.0
   */
  .access('realStatus');