From 43842e44351d93434db7c5d54477dfec6e900793 Mon Sep 17 00:00:00 2001 From: Aex Date: Sat, 23 May 2020 18:27:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +++ LICENSE | 21 +++++++ README.md | 7 ++- package.json | 34 ++++++++++ src/cache.ts | 146 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 5 ++ src/redis.ts | 19 ++++++ src/typing.ts | 18 ++++++ tsconfig.json | 12 ++++ yarn.lock | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 package.json create mode 100644 src/cache.ts create mode 100644 src/index.ts create mode 100644 src/redis.ts create mode 100644 src/typing.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38b2f23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea/ +/node_modules/ +/dist/ + +.DS_Store +npm-debug.log +yarn-error.log \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9911caf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 COA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5acf919..958f79d 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# coa-redis \ No newline at end of file +# coa-redis + +[![GitHub license](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](LICENSE) +[![npm version](https://img.shields.io/npm/v/coa-redis.svg?style=flat-square)](https://www.npmjs.org/package/coa-redis) +[![npm downloads](https://img.shields.io/npm/dm/coa-redis.svg?style=flat-square)](http://npm-stat.com/charts.html?package=coa-redis) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/coajs/coa-redis/pulls) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e360ff7 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "coa-redis", + "version": "1.0.4", + "description": "redis component for coa", + "keywords": [ + "coajs", + "coa", + "redis" + ], + "license": "MIT", + "author": "Aex", + "homepage": "https://github.com/coajs/coa-redis", + "repository": { + "type": "git", + "url": "https://github.com/coajs/coa-redis.git" + }, + "scripts": { + "dev": "tsc -w", + "build": "rm -rf dist && tsc && cp package.json dist", + "npm-publish": "yarn build && yarn version --patch && cp package.json README.md dist && cd dist && npm publish" + }, + "dependencies": { + "@types/ioredis": "^4.16.2", + "coa-echo": "^1.0.8", + "coa-env": "^1.0.2", + "coa-error": "^1.0.2", + "coa-helper": "^1.0.5", + "ioredis": "^4.17.1" + }, + "devDependencies": { + "@types/node": "^14.0.5", + "typescript": "^3.9.3" + } +} diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..b4853af --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,146 @@ +import { env } from 'coa-env' +import { die } from 'coa-error' +import { _ } from 'coa-helper' +import redis from './redis' +import { Dic } from './typing' + +const ms_ttl = 30 * 24 * 3600 * 1000 +const prefix = env.redis.prefix.toString() || 'coa-d0' + +declare global { + type CacheDelete = [string, string[]] +} + +export default new class { + + redis = redis + + // 设置 + async set (nsp: string, id: string, value: any, ms: number = ms_ttl) { + ms > 0 || die.hint('cache hash ms 必须大于0') + const expire = _.now() + ms + const data = this.encode(value, expire) + return await redis.io.hset(this.key(nsp), id, data) + } + + // 批量设置 + async mSet (nsp: string, values: Dic, ms: number = ms_ttl) { + ms > 0 || die.error('cache hash ms 必须大于0') + _.keys(values).length > 0 || die.error('cache hash values值的数量 必须大于0') + const expire = Date.now() + ms + const data = {} as Dic + _.forEach(values, (v, k) => data[k] = this.encode(v, expire)) + return await redis.io.hmset(this.key(nsp), data) + } + + // 获取 + async get (nsp: string, id: string) { + const ret = await redis.io.hget(this.key(nsp), id) || '' + return this.decode(ret, _.now()) + } + + // 批量获取 + async mGet (nsp: string, ids: string[]) { + const ret = await redis.io.hmget(this.key(nsp), ...ids) + const result = {} as Dic + const time = _.now() + _.forEach(ids, (id, i) => result[id] = this.decode(ret[i], time)) + return result + } + + // 获取 + async warp (nsp: string, id: string, worker: () => Promise, ms = ms_ttl) { + let result = await this.get(nsp, id) + if (result === undefined) { + result = await worker() + ms > 0 && await this.set(nsp, id, result, ms) + } + return result as T + } + + // 获取 + async mWarp (nsp: string, ids: string[], worker: (ids: string[]) => Promise, ms = ms_ttl) { + const result = await this.mGet(nsp, ids) + + const newIds = [] as string[] + _.forEach(ids, id => { + if (result[id] === undefined) newIds.push(id) + }) + + if (newIds.length) { + const newResult = await worker(newIds) as any + _.forEach(newIds, id => { + if (!newResult[id]) newResult[id] = null + }) + ms > 0 && await this.mSet(nsp, newResult, ms) + _.extend(result, newResult) + } + + return result + } + + // 删除 + async delete (nsp: string, ids: string[] = []) { + if (ids.length) + return await redis.io.hdel(this.key(nsp), ...ids) + else + return await redis.io.del(this.key(nsp)) + } + + // 删除 + async mDelete (deleteIds: CacheDelete[]) { + if (deleteIds.length === 0) + return 0 + else if (deleteIds.length === 1) + return await this.delete(...deleteIds[0]) + + const pipeline = redis.io.pipeline() + deleteIds.forEach(([nsp, ids]) => { + ids.length ? pipeline.hdel(this.key(nsp), ...ids) : pipeline.del(this.key(nsp)) + }) + return await pipeline.exec() + } + + // 清除无效的缓存 + async clearUseless () { + const now = _.now() + const keys1 = await redis.io.keys(this.key('*')) + for (const i1 in keys1) { + const key1 = keys1[i1] + const keys2 = await redis.io.hkeys(key1) as string[] + for (const i2 in keys2) { + const key2 = keys2[i2] + const value = await redis.io.hget(key1, key2) || '' + const expire = _.toInteger(value.substr(1, 13)) + if (expire < now) await redis.io.hdel(key1, key2) + } + } + } + + // 清楚指定命名空间的缓存 + async clear (nsp: string = '') { + const keys = await redis.io.keys(this.key(nsp + '*')) + return keys.length ? await redis.io.del(...keys) : 0 + } + + // 设置nsp + public key (nsp: string) { + return prefix + ':' + nsp + } + + private encode (value: any, expire: number) { + if (value === undefined) value = null + return JSON.stringify([expire, value]) + } + + private decode (value: string | null, time: number) { + if (!value) return undefined + try { + const data = JSON.parse(value) + const expire = data[0] || 0 + return expire < time ? undefined : data[1] + } catch (e) { + return undefined + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..506ad67 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +import cache from './cache' +import redis from './redis' +import { RedisEnv } from './typing' + +export { redis, cache, RedisEnv } \ No newline at end of file diff --git a/src/redis.ts b/src/redis.ts new file mode 100644 index 0000000..cc6cfd4 --- /dev/null +++ b/src/redis.ts @@ -0,0 +1,19 @@ +import { echo } from 'coa-echo' +import { env } from 'coa-env' +import * as Redis from 'ioredis' + +const redis = new Redis({ + port: env.redis.port, + host: env.redis.host, + password: env.redis.password, + db: env.redis.db, + lazyConnect: true, +}) + +env.redis.trace && redis.monitor((err, monitor) => { + monitor.on('monitor', (time, args, source, database) => { + echo.grey('* Redis: [%s] %s', database, args) + }) +}) + +export default { io: redis } \ No newline at end of file diff --git a/src/typing.ts b/src/typing.ts new file mode 100644 index 0000000..65b006d --- /dev/null +++ b/src/typing.ts @@ -0,0 +1,18 @@ +export interface Dic { + [key: string]: T +} + +export interface RedisEnv { + host: string, + port: number, + db: number, + password: string, + prefix: string, + trace: boolean +} + +declare module 'coa-env' { + interface Env { + redis: RedisEnv + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..37c5106 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "module": "commonjs", + "target": "esnext", + "outDir": "dist", + "declaration": true + }, + "include": [ + "src" + ] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..4ef286b --- /dev/null +++ b/yarn.lock @@ -0,0 +1,170 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/ioredis@^4.16.2": + version "4.16.2" + resolved "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.16.2.tgz#192c487a003463a6881dbc29faaa168902ef851a" + integrity sha512-b1wSWCLx5ygzDZ2d1BYBzsTzUcPEOcJH8d5O6AJGGJXjE6C+HrlA61sw5GQNqtqTPVo2Hpc4rPA30lI0T6QSLQ== + dependencies: + "@types/node" "*" + +"@types/lodash@^4.14.152": + version "4.14.152" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" + integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== + +"@types/node@*", "@types/node@^14.0.5": + version "14.0.5" + resolved "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" + integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== + +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + +coa-echo@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/coa-echo/-/coa-echo-1.0.8.tgz#0347b947d37d68d03b9058f822dc0f65499210a8" + integrity sha512-0ap+N+merkETH4QzP81GTUv96t7SWfwiYrhhxW+kmR6Te7GculaklomRsmgBN//5lIif5Gd/OCt8l8r7igrfMA== + +coa-env@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/coa-env/-/coa-env-1.0.2.tgz#fe8de04b88a53503b92c904bbb7de49aa72f704e" + integrity sha512-H6Ozw3WpB+cCxzNmvZgx2sHf/e0fMxanCKqT7z9tzLRm/A3MSa9hYbHf9N03yTCOFCrFid4seELsN9s0yQnMJg== + dependencies: + extend2 "^1.0.0" + +coa-error@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/coa-error/-/coa-error-1.0.2.tgz#dd492625079f8da7ba25fd9b3b6f2208a18c6515" + integrity sha512-EzusOoiCcWhP36JenBjKVqTNrJ6W7IwPeg9PtO9a+ESJ7S0gGnwkscAtT3y0zQlHJwDdMDNx8IhZVVRA4rllqg== + dependencies: + coa-echo "^1.0.8" + +coa-helper@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/coa-helper/-/coa-helper-1.0.5.tgz#e7c2599677411f8014d682bff420f929f54924e0" + integrity sha512-92cJn/904pKeQdTZjkh15DVW/b3RufroA+4owxOVdY404NMmizXIBrCPSRCeE1l3PW2ws5qLVWZ2/RTu5N9BAQ== + dependencies: + "@types/lodash" "^4.14.152" + axios "^0.19.2" + dayjs "^1.8.27" + hashids "^2.2.1" + lodash "^4.17.15" + +dayjs@^1.8.27: + version "1.8.27" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.8.27.tgz#a8ae63ee990af28c05c430f0e160ae835a0fbbf8" + integrity sha512-Jpa2acjWIeOkg8KURUHICk0EqnEFSSF5eMEscsOgyJ92ZukXwmpmRkPSUka7KHSfbj5eKH30ieosYip+ky9emQ== + +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +denque@^1.1.0: + version "1.4.1" + resolved "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + +extend2@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/extend2/-/extend2-1.0.0.tgz#0425a989b4dac2a486a32257f5140103756a7a3c" + integrity sha1-BCWpibTawqSGoyJX9RQBA3Vqejw= + +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + +hashids@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/hashids/-/hashids-2.2.1.tgz#ad0c600f0083aa0df7451dfd184e53db34f71289" + integrity sha512-+hQeKWwpSDiWFeu/3jKUvwboE4Z035gR6FnpscbHPOEEjCbgv2px9/Mlb3O0nOTRyZOw4MMFRYfVL3zctOV6OQ== + +ioredis@^4.17.1: + version "4.17.1" + resolved "https://registry.npmjs.org/ioredis/-/ioredis-4.17.1.tgz#06ef3d3b2cb96b7e6bc90a7b8839a33e743843ad" + integrity sha512-kfxkN/YO1dnyaoAGyNdH3my4A1eoGDy4QOfqn6o86fo4dTboxyxYVW0S0v/d3MkwCWlvSWhlwq6IJMY9BlWs6w== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.1.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + redis-commands "1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.0.1" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +redis-commands@1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +standard-as-callback@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" + integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== + +typescript@^3.9.3: + version "3.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" + integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==