index.js

'use strict'

const { AWS, log } = require('./util')
const { sleep, exists } = require('@nuskin/uncle-buck')
const dynamodb = new AWS.DynamoDB.DocumentClient()
const sqs = new AWS.SQS()
const TABLE_COUNTER = 'ConnectionLifeguardCounter'
const TABLE_UUID = 'ConnectionLifeguardUuid'
const QURL = `https://sqs.${process.env.AWS_REGION}.amazonaws.com/${process.env.ACCOUNT_ID}/ConnectionLifeguard`

const { customAlphabet } = require('nanoid')
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 22)

/**
 * @typedef {object} approvalResponse
 * @property {boolean} approved Is `true` when the request was approved.  Else it is `false`.
 * @property {number} count Current count of the pool.
 * @property {string} uuid The unique identifier created when the request is approved.
 */

/**
 * @typedef {object} doneResponse
 * @property {number} count the current count after decrementing
 */


/**
 * getExpireAt produces epochSeconds equaling now + 2 minutes
 * @returns {number} epochSeconds the number of seconds elapsed since 1970-01-01T00:00:00Z
 */
const getExpireAt = () => {
    let now = new Date()
    return parseInt(now.setMinutes(now.getMinutes() + 2) / 1000)
}

/**
 * insertUuid inserts a record into the uuid table.
 * @param {string} uuid 
 * @param {string} pool 
 */
const insertUuid = async (uuid, pool) => {
    log.debug(`calling insertUuid(${uuid},${pool})`)

    const params = {
        TableName: TABLE_UUID,
        Item: {
            uuid,
            pool,
            expireAt: getExpireAt()
        }
    }

    log.debug({params})

    try {
        const result = await dynamodb.put(params).promise()
        await sender(uuid,pool)
        log.debug({result})
    } catch (err) {
        log.info({err})
    }
}

/**
 * deleteUuid removes the uuid record from the uuid table.
 * @param {string} uuid 
 */
const deleteUuid = async (uuid) => {
    log.debug(`calling deleteUuid(${uuid})`)

    const params = {
        TableName: TABLE_UUID,
        Key: { uuid },
        ReturnValues: 'ALL_OLD'
    }

    log.debug({params})

    try {
        const deleteUuidResult = await dynamodb.delete(params).promise()
        log.debug({deleteUuidResult})
    } catch (err) {
        log.info({err})
    }
}

/**
 * decrement will decrement the counter by 1 for the specified `pool`
 * @param {String} pool 
 * @returns {doneResponse} response contains decremented count.
 */
const decrement = async (pool) => {
    log.debug(`calling decrement(${pool})`)
    let decrementResult = { count: 0 }
    
    try {
        const params = {
            TableName: TABLE_COUNTER,
            Key: { pool },
            UpdateExpression: 'SET #count = #count - :decr',
            ConditionExpression: '#count > :MIN',
            ExpressionAttributeNames: {'#count' : 'count'},
            ExpressionAttributeValues: {
                ':decr': 1,
                ':MIN': 0
            },
            ReturnValues: 'UPDATED_NEW'
        }
        log.debug({params})

        const record = await dynamodb.update(params).promise()
        log.debug({record})

        if(exists(record, 'Attributes')){
            decrementResult.count = record.Attributes.count
        }
    } catch (error) {
        log.info({error})
    }

    log.debug({decrementResult})
    return decrementResult
}

/**
 * done is used to notify ConnectionLifeguard that you no longer need a database connection. 
 * done attempts to remove the uuid from the uuid table, then decrements the pool counter.
 * @param {String} pool The name of the pool your request was for.
 * @param {String} uuid The uuid that was given to you when your request was approved.
 * @returns {doneResponse} response object is returned with decremented count.
 */
const done = async (pool, uuid) => {
    log.debug(`calling done(${pool},${uuid})`)
    await deleteUuid(uuid)
    return await decrement(pool)
}

/**
 * create creates a new entry for the record passed in.
 * @param {String} pool The record entry to create
 * @returns {approvalResponse} response object containing your request data.
 */
const requestApproval = async (pool) => {
    log.debug(`calling requestApproval(${pool})`)
    let attempts = 0
    let result = {
        approved: false
    }

    do {
        try {
            attempts++
            const params = {
                TableName: TABLE_COUNTER,
                Key: { pool },
                UpdateExpression: 'SET #count = :incr + #count',
                ConditionExpression: '#count < :MAX',
                ExpressionAttributeNames: {'#count' : 'count'},
                ExpressionAttributeValues: {
                    ':incr': 1,
                    ':MAX': 100
                },
                ReturnValues: 'UPDATED_NEW'
            }
            log.debug({params})

            const record = await dynamodb.update(params).promise()
            log.debug({record})
            if(exists(record, 'Attributes')){
                result.count = record.Attributes.count
                result.approved = true
                result.uuid = nanoid() // also put in ddb
                await insertUuid(result.uuid, pool)
            }
        } catch (err) {
            log.error({attempts, err})
            await sleep(100)
        }
    } while (!result.approved && attempts < 10)

    log.debug({result})
    return result
}

/**
 * This places a message in a queue that is delayed by a specified number of seconds.
 * Once the message becomes visible it is consumed by a lambda that will remove the uuid
 * record and decrement the pool counter.
 * @param {string} uuid 
 * @param {string} pool 
 */
const sender = async (uuid, pool) => {
    log.info(`calling sender(${uuid},${pool})`)

    // SQS message parameters
    const params = {
        MessageBody: JSON.stringify({uuid,pool}),
        QueueUrl: QURL
    }

    const sqsResponse = await sqs.sendMessage(params).promise()
    log.info({sqsResponse})
}

module.exports = {
    done,
    requestApproval
}