commands/command.js

// *****************************************************************************
// Copyright 2013-2023 Aerospike, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// *****************************************************************************

'use strict'

const AerospikeError = require('../error')

// Command is an abstract template (aka "mix-in") for concrete command
// subclasses, that execute a specific database command method on the native
// Aerospike add-on. A concrete command subclass should be defined like this,
// where "getAsync" is the native command method to call:
//
//   class GetCommand extends Command("getAsync") {
//       // ...
//   }

/**
 * @class Command
 * @classdesc Database command.
 * @type Class
 */
module.exports = asCommand => class Command {
  /** @private */
  constructor (client, args, callback) {
    /**
     * Client instance used to execute this command.
     *
     * @name Command#client
     * @type {Client}
     */
    this.client = client

    /** @private */
    this.args = args

    if (callback) {
      /** @private */
      this.callback = callback
    }

    /**
     * Whether debug stacktraces are enabled.
     *
     * @name Command#captureStackTraces
     * @type {boolean}
     * @see {@link Client#captureStackTraces}
     */
    this.captureStackTraces = client.captureStackTraces

    /**
     * The record key for which the command was issued. (Single-key commands only.)
     *
     * @name Command#key
     * @type {?Key}
     */
    this.key = undefined

    this.ensureConnected = true
  }

  /** @private */
  captureStackTrace () {
    if (this.captureStackTraces) {
      AerospikeError.captureStackTrace(this, this.captureStackTrace)
      // this.startTime = process.hrtime()
    }
  }

  /** @private */
  connected () {
    return this.client.isConnected(false)
  }

  /** @private */
  convertError (error) {
    return AerospikeError.fromASError(error, this)
  }

  /** @private */
  convertResult (arg1, arg2, arg3) {
    return arg1
  }

  /** @private */
  convertResponse (err, arg1, arg2, arg3) {
    const error = this.convertError(err)
    const result = this.convertResult(arg1, arg2, arg3)
    return [error, result]
  }

  /** @private */
  execute () {
    if (this.ensureConnected && !this.connected()) {
      return this.sendError('Not connected.')
    }

    this.captureStackTrace()

    if (this.expectsPromise()) {
      return this.executeAndReturnPromise()
    } else {
      return this.executeWithCallback(this.callback.bind(this))
    }
  }

  /** @private */
  executeWithCallback (callback) {
    // C client will call the callback function synchronously under certain error
    // conditions; if we detect a synchronous callback we need to schedule the JS
    // callback to be called asynchronously anyway.
    let sync = true
    this.process((error, result) => {
      if (sync) {
        process.nextTick(callback, error, result)
      } else {
        return callback(error, result)
      }
    })
    sync = false // if we get here before the cb was called the cb is async
  }

  /** @private */
  executeAndReturnPromise () {
    return new Promise((resolve, reject) => {
      this.process((error, result) => {
        if (error) {
          reject(error)
        } else {
          resolve(result)
        }
      })
    })
  }

  /** @private */
  expectsPromise () {
    return !this.callback
  }

  /** @private */
  asCommand () {
    return asCommand
  }

  /** @private */
  process (cb) {
    const asCallback = (err, arg1, arg2, arg3) => {
      const tmp = this.convertResponse(err, arg1, arg2, arg3)
      const error = tmp[0]
      const result = tmp[1]
      return cb(error, result)
    }
    const asArgs = this.args.concat([asCallback])
    this.client.asExec(this.asCommand(), asArgs)
  }

  /** @private */
  sendError (message) {
    const error = new AerospikeError(message, this)
    if (this.expectsPromise()) {
      return Promise.reject(error)
    } else {
      process.nextTick(this.callback, error)
    }
  }
}