makeomatic/mservice

View on GitHub
packages/plugin-redis-core/src/utils.ts

Summary

Maintainability
A
1 hr
Test Coverage
import type { Microfleet } from '@microfleet/core-types'
import type * as _ from '@microfleet/plugin-logger'
import { strict as assert } from 'assert'
import { ArgumentError } from 'common-errors'
import _debug from 'debug'
import { readFile } from 'node:fs/promises'
import { glob } from 'glob'
import path from 'path'
import { ERROR_NOT_HEALTHY, ERROR_NOT_STARTED } from './constants'
import Redis from 'ioredis'

const debug = _debug('mservice:lua')

/**
 * Loads LUA script and defines it on the redis instance.
 * @param dir - Directory to scan for LUA scripts to load.
 * @param redis - Redis connector instance.
 */
export async function loadLuaScripts<T extends Redis.Redis | Redis.Cluster>(
  ctx: Microfleet,
  dir: string | string[],
  redis: T
): Promise<void> {
  // NOTE: this is a concious decision to use await serially
  // so that it's easier to debug
  // Operations that happen here are a one-off during script startup
  // process and gains from reading files in parallel IMO aren't
  // worth extra complexity

  const locations = Array.isArray(dir) ? dir : [dir]

  for (const location of locations) {
    if (!path.isAbsolute(location)) {
      throw new ArgumentError('config.scripts must be an absolute path')
    }

    debug('loading from %s', location)

    const scripts = await glob('*.lua', { cwd: location })
    for (const scriptName of scripts) {
      const lua = await readFile(`${location}/${scriptName}`, 'utf8')
      const name = path.basename(scriptName, '.lua')
      debug('attaching %s', name)
      // @ts-expect-error incomplete mapping
      if (typeof redis[name] === 'undefined') {
        // NOTICE: make sure that you pass number of keys as first arg when supplying function
        redis.defineCommand(name, { lua })
      } else {
        ctx.log.warn('script %s already defined', name)
      }
    }
  }
}

export function isStarted<T extends Redis.ClusterStatic | typeof Redis>(service: Microfleet, RedisType: T) {
  return (): boolean => (
    service.redis && (service.redis instanceof RedisType)
  )
}

export async function hasConnection(this: Microfleet, hasRedis: () => boolean): Promise<boolean> {
  assert(hasRedis(), ERROR_NOT_STARTED)

  const ping = await this.redis.ping()
  assert(ping, ERROR_NOT_HEALTHY)

  return true
}