list.js

/**
 * Redis LIST type wrapper
 *
 * Implemented methods:
 *
 * - length (**LLEN**)
 * - shift (**LPOP**)
 * - pop (**RPOP**)
 * - push (**RPUSHX**)
 * - unshift (**LPUSHX**)
 * - slice (**LRANGE**)
 * - insertAfter (**LINSERT AFTER**)
 * - insertBefore (**LINSERT BEFORE**)
 * - getElementAt (**LINDEX**)
 * - setElementAt (**LSET**)
 * - trim (**LTRIM**)
 *
 *
 * Not implemented:
 *
 * - **BLPOP**
 * - **BRPOP**
 * - **LPUSH** (**LPUSHX** used instead)
 * - **RPUSH** (**RPUSHX** used instead)
 * - **RPOPLPUSH**
 */

'use strict';

const Wrapper = require('./wrapper');

/**
 * Class to handle Redis LIST type.
 *
 * Behaves the same way as Array does so use it just like you would use Array, with some exceptions of course
 *
 * @example
 * const client = require('redis').createClient();
 * const types  = require('redis-type')(client);
 *
 * const list = new types.List('my_list', true); // last flag for JSON=true
 *
 * (async () => {
 *
 *   await list.push({a: 1}, {a: 2}, {a: 3});
 *
 *   console.log(await list.slice()); // get list contents
 *
 *   const lastEl = await list.pop();
 *
 *   console.log(await list.slice()); // [{a: 1}, {a: 2}];
 *
 *   await list.setElementAt(0, lastEl);
 *
 *   console.log(await list.slice()); // [{a: 3}, {a: 2}];
 *
 * })();
 *
 * @extends Wrapper
 */
class List extends Wrapper {

    /**
     * Get length of a list
     *
     * - Redis command: [LLEN]{@link https://redis.io/commands/llen}
     * - JavaScript analogy: [Array.length]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length}
     *
     * @example
     * (async () => {
     *
     *   let len = await list.length();
     *
     * })()
     *
     * @return {Promise<Number>} Length of the list
     */
    length() {
        return this.prom('llen')();
    }

    /**
     * Remove and get first element of the list
     *
     * - Redis command: [LPOP]{@link https://redis.io/commands/lpop}
     * - JavaScript analogy: [Array.prototype.shift]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift}
     *
     * @example
     * (async () => {
     *
     *   await list.push(1, 2, 3);       // fill list with values
     *   let first = await list.shift(); // gonna be 1 and list will contain [2, 3]
     *
     * })()
     *
     * @return {Promise<Object|String>} Shifted value (Object when useJSON=true or String)
     */
    shift() {
        return this.useJSON
            ? this.prom('lpop')().then(this.json.parse)
            : this.prom('lpop')();
    }

    /**
     * Remove and get last element
     *
     * - Redis command: [RPOP]{@link https://redis.io/commands/rpop}
     * - JavaScript analogy: [Array.prototype.pop]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop}
     *
     * @example
     * (async () => {
     *
     *   await list.push(1, 2, 3);    // fill list with values
     *   let last = await list.pop(); // gonna be 3 and list will contain [1, 2]
     *
     * })()
     *
     * @return {Promise<Object|String>} Popped value (Object when useJSON=true or String)
     */
    pop() {
        return this.useJSON
            ? this.prom('rpop')().then(this.json.parse)
            : this.prom('rpop')();
    }

    /**
     * Prepend list with element(s)
     *
     * - Redis command: [LPUSH]{@link https://redis.io/commands/lpush}
     * - JavaScript analogy: [Array.prototype.unshift]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift}
     *
     * @example
     * (async () => {
     *
     *   await list.unshift(1);    // list [1]
     *   await list.unshift(3, 2); // list [3, 2, 1] - just like an Array in JS
     *
     * })()
     *
     * @param  {String}  ...els Element(s) to push in the beginning
     * @return {Promise}
     */
    unshift(...els) {
        return this.useJSON
            ? this.prom('lpush').apply(this, els.reverse().map(this.json.toJSON))
            : this.prom('lpush').apply(this, els.reverse());
    }

    /**
     * Push element(s) to the end of the list
     *
     * - Redis command: [RPUSH]{@link https://redis.io/commands/rpush}
     * - JavaScript analogy: [Array.prototype.push]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push}
     *
     * @example
     * (async () => {
     *
     *   await list.push(1, 2, 3, 4); // return value: 4
     *   await list.push(1);          // return value: 5
     *
     * })();
     *
     * @param  {String}         ...els Element(s) to push
     * @return {Promise<Number>}       Resulting length of the list
     */
    push(...els) {
        return this.useJSON
            ? this.prom('rpush').apply(this, els.map(this.json.toJSON))
            : this.prom('rpush').apply(this, els);
    }


    /**
     * Get range of elements with BEGIN-END arguments
     *
     * - Redis command: [LRANGE]{@link https://redis.io/commands/lrange}
     * - JavaScript analogy: [Array.prototype.slice]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice}
     *
     * Supports just the same interface as Array does.
     *
     * @example
     * (async () => {
     *
     *   await list.push(1, 2, 3, 4, 5);
     *
     *   let one   = await list.slice();    // gonna be [1, 2, 3, 4, 5]
     *   let two   = await list.slice(1);   // gonna be [2, 3, 4, 5]
     *   let three = await list.slice(-2);  // gonna be [4, 5]
     *   let four  = await list.slice(1, 3) // gonna be [2, 3]
     *
     * })()
     *
     * @param  {Number}         [begin=0]  Start point index
     * @param  {Number}         [end=null] End point index
     * @return {Promise<Array>}            Resulting slice of List for given params
     */
    slice(begin = 0, end = null) {

        switch (true) {
            case (end === null && begin < 0):
                end = -1;
                break;
            case (end === null && begin === 0):
                begin = 0; end = -1;
                break;
            case (end === null):
                end = -1;
                break;
            case (end !== null):
                end -= 1;
        }

        return this.useJSON
            ? this.prom('lrange')(begin, end).then(this.json.parseArray)
            : this.prom('lrange')(begin, end);
    }

    /**
     * Insert element after the element with given value (as String)
     *
     * - Redis command: [LINSERT AFTER]{@link https://redis.io/commands/linsert}
     * - JavaScript analogy: none
     *
     * @example
     * (async () => {
     *
     *   await list.push('a', 'c', 'd');
     *   await list.insertAfter('a', 'b'); // returned value: 4, list: [a, b, c, d]
     *
     * })()
     *
     * @param  {Number}          key Key to insert after
     * @param  {String}          el  Element to insert
     * @return {Promise<Number>}     Resulting list length
     */
    insertAfter(key, el) {
        return this.useJSON
            ? this.prom('linsert')('after', key, this.json.toJSON(el))
            : this.prom('linsert')('after', key, el);
    }

    /**
     * Insert elenent before the elemnt with given index
     *
     * - Redis command: [LINSERT BEFORE]{@link https://redis.io/commands/linsert}
     * - JavaScript analogy: none
     *
     * @example
     * (async () => {
     *
     *   await list.push('a', 'c', 'd');
     *   await list.insertBefore('c', 'b'); // returned value: 4, list: [a, b, c, d]
     *
     * })()
     *
     * @param  {Number}          key Key to insert after
     * @param  {String}          el  Element to insert
     * @return {Promise<Number>}     Resulting list length
     */
    insertBefore(key, el) {
        return this.useJSON
            ? this.prom('linsert')('before', key, this.json.toJSON(el))
            : this.prom('linsert')('before', key, el);
    }

    /**
     * Get element at the given index
     *
     * - Redis command: [LINDEX]{@link https://redis.io/commands/lindex}
     * - JavaScript analogy: none | or Array[index]
     *
     * @example
     * (async () => {
     *
     *   await list.push('a', 'b', 'c', 'd');
     *   let el1 = await list.getElementAt(0);   // value: 'a'
     *   let el2 = await list.getElementAt(-1);  // value: 'd'
     *   let el3 = await list.getElementAt(100); // value: null
     *
     * })()
     *
     * @param  {Number}                 index Index to get element at
     * @return {Promise<?String|Object>}      Value under given index or null
     */
    getElementAt(index) {
        return this.useJSON
            ? this.prom('lindex')(index).then(this.json.parse)
            : this.prom('lindex')(index);
    }

    /**
     * Set element at given index
     *
     * - Redis command: [LSET]{@link https://redis.io/commands/lset}
     * - JavaScript analogy: none | or Array[index] = value
     *
     * @example
     * (async () => {
     *
     *   await list.push('a', 'b', 'c', 'd');
     *   await list.setElementAt(0, 'd');     // OK, list is [d, b, c, d]
     *   await list.setElementAt(1, 'd');     // OK, list is [d, d, c, d]
     *   await list.setElementAt(2, 'd');     // OK, list is [d, d, d, d]
     *
     *   await list.setElementAt(100, 'd').catch((e) => console.log(e)); // err: index out of range
     *
     * })()
     *
     * @param  {Number}          index Index to set el at
     * @param  {String}          value Value to set at given index
     * @return {Promise<String>}       OK String
     *
     * @throws {Error} When index is out of range (AKA greater than list.length())
     */
    setElementAt(index, value) {
        return this.useJSON
            ? this.prom('lset')(index, this.json.toJSON(value))
            : this.prom('lset')(index, value);
    }

    /**
     * Trim list in the given begin-end range
     *
     * - Redis command: [LTRIM]{@link https://redis.io/commands/ltrim}
     * - JavaScript analogy: none; maybe Array.prototype.slice()
     *
     * Indexes you specify are included (0, 0) - one element, (0, 1) - 2 elements
     *
     * @example
     * (async () => {
     *
     *   await list.push(1, 2, 3, 4);
     *   await list.trim(0, -1);        // list [1,2,3,4] - the same
     *   await list.trim(-3, -1);       // list [2,3,4]   - from the end
     *   await list.trim(0, 1);         // list [2,3]     - from the start
     *
     * })()
     *
     * @param  {Number}          begin Start index
     * @param  {Number}          end   End index
     * @return {Promise<String>}       OK String when success
     */
    trim(begin, end) {
        return this.prom('ltrim')(begin, end);
    }
}

module.exports = exports = List;