sardines-built-in-services
Version:
A built-in service provider for sardines.js
224 lines (197 loc) • 6.37 kB
text/typescript
/*
* Created on 2020-02-18
* Author: Robin <robin@naturewake.com>
*/
import * as redis from 'redis'
// import { ReplyError } from 'redis'
// import { promisify } from 'util'
import { StorageBase } from './base'
// Server settings
export interface RedisServerSettings {
host?: string
port?: number
password?: string
path?: string
url?: string
string_numbers?: boolean
return_buffers?: boolean
detect_buffers?: boolean
enable_offline_queue?: boolean
retry_unfulfilled_commands?: boolean
family?: string
disable_resubscribing?: boolean
retry_strategy?: any
db?: number
[key:string]: any
}
const defaultServerSettings:RedisServerSettings = {
host: '127.0.0.1',
port: 6379,
return_buffers: false,
detect_buffers: false,
family: 'IPv4',
retry_strategy: null,
db: 0
}
const mergeDefaultServerSettings = (serverSettings: RedisServerSettings):RedisServerSettings => {
const newSettings = Object.assign({}, defaultServerSettings, serverSettings)
const redundentKeys = ['host', 'port']
if (typeof serverSettings.host === 'undefined') {
for (const key of redundentKeys) {
delete newSettings[key]
}
}
return newSettings
}
// Operation options
export enum RedisDataType {
string = 'string',
number = 'number',
boolean = 'boolean',
object = 'object'
}
export interface RedisOperationOptions {
dataType?: RedisDataType
expire?: number
}
export const defaultRedisOperationOptions = {
dataType: RedisDataType.string,
expire: -1
}
const mergeDefaultRedisOperationOptions = (options?: RedisOperationOptions): RedisOperationOptions => {
const newOptions = Object.assign({}, defaultRedisOperationOptions, options)
return newOptions
}
// the class
export class RedisCache extends StorageBase{
protected serverSettings: any
protected client: any
protected max_reconnect_retry_time: number = 1000 * 60 * 60
protected max_reconnect_attempts: number = 10
protected max_reconnect_interval: number = 5000
constructor(serverSettings: RedisServerSettings) {
super()
this.serverSettings = mergeDefaultServerSettings(serverSettings)
}
get connected() {
return this.client && this.client.connected
}
async connect(serverSettings: RedisServerSettings|null = null): Promise<any> {
if (this.connected) return
if (serverSettings) {
this.serverSettings = mergeDefaultServerSettings(serverSettings)
}
// Set default retry_strategy
if (!this.serverSettings.retry_strategy && this.serverSettings.retry_strategy !== false) {
const that = this
this.serverSettings.retry_strategy = function(options:any) {
if (options.error && options.error.code === "ECONNREFUSED") {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error("The server refused the connection");
}
if (options.total_retry_time > that.max_reconnect_retry_time) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error("Retry time exhausted");
}
if (options.attempt > that.max_reconnect_attempts) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.min(options.attempt * 100, that.max_reconnect_interval);
}
}
// connect to redis server
this.client = redis.createClient(this.serverSettings)
// When connecting to a Redis server that requires authentication, the AUTH command must be sent as the first command after connecting.
return new Promise((resolve, reject) => {
if (this.serverSettings.password) {
this.client.auth(this.serverSettings.password, (err: any) => {
if (err) {
console.error('redis auth error:', err)
reject(err)
}
})
}
if (typeof this.serverSettings.db !== 'undefined') {
try {
this.client.select(this.serverSettings.db, (err:any) => {
if (err) {
console.error('redis select db error:', err)
reject(err)
}
})
} catch (e) {
reject(e)
}
}
this.client.on('error', (err:any) => {
console.error('redis connection error:', err)
reject(err)
})
this.client.on('ready', () => {
resolve(this.client.connected)
})
})
}
async get(key:string, options?: RedisOperationOptions): Promise<any> {
const theOptions = mergeDefaultRedisOperationOptions(options)
return new Promise((resolve, reject) => {
let handler = this.client.get
if (theOptions.dataType === RedisDataType.object) {
handler = this.client.hgetall
}
const args = [key, (err:any, res:any) => {
if (err) reject(err)
switch(theOptions.dataType) {
case RedisDataType.number:
resolve((res === null) ? Number.NaN : Number(res))
break
case RedisDataType.boolean:
resolve((res === 'true'?true:false))
break
default:
resolve(res)
break
}
}]
handler.apply(this.client, args)
})
}
async del(key: string): Promise<any> {
return new Promise((resolve, reject) => {
this.client.del(key, (err:any, res:any) => {
if (err) reject(err)
resolve(res)
})
})
}
async set(key:string, obj: any, options?: RedisOperationOptions): Promise<any> {
const theOptions = mergeDefaultRedisOperationOptions(options)
return new Promise((resolve, reject) => {
const args: any[] = [key]
let handler = this.client.set
if (theOptions.dataType === RedisDataType.object
|| (typeof obj === 'object' && Object.keys(obj).length > 0)) {
for (let k of Object.keys(obj)) {
args.push(k)
args.push(obj[k])
}
handler = this.client.hmset
} else {
args.push(obj)
}
if (typeof theOptions.expire !== 'undefined' && theOptions.expire > 0) {
args.push('EX')
args.push(theOptions.expire)
}
args.push((err: any, res: any) => {
if (err) reject(err)
resolve(res)
})
handler.apply(this.client, args)
})
}
}