redis-abstraction 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # redis-abstraction
2
+ A Redis client pool with abstraction to different Redis libraries.
3
+
4
+ This package helps to create redis connections in a connection pool fashion, ideally redis is single threaded so multiple connection or a connection pool for simple commands may not make sense, but there are some conditions in which connection pool makes sense like blocking commands, pub-sub commands etc apart form these cases there are also some fringe cases where if you have a simple command with a lot of data to be serialized on wire/network here multiple connections make sense as redis execution is single threaded but its I/O stack is multi so another command which has relatively small data to be serialized on wire gets stuck behind this big network transfer command ahead of it, It can take advantage of another connection where there is not que before-hand and get executed first in such a case connection pools make sense. This package also supports cluster mode connections.
5
+
6
+ Working of this package is simple it exposes an interface [i-redis-client-pool](https://github.com/LRagji/redis-abstraction/blob/main/source/i-redis-client-pool.ts) which has following methods
7
+ 1. `acquire(token: string): Promise<void>` : Responsibile to acquire a connection to redis server in-reference to the unique token provided.
8
+ 2. `run(token: string, commandArgs: string[]): Promise<any>` : Responsible to run a redis command in-reference to the unique token acquired before.
9
+ 3. `release(token: string): Promise<void>` : Responsible to release the acquired connection back into connection pool in-reference to the unique token acquired before.
10
+
11
+ There are some more supporting methods as given below:
12
+ * `generateUniqueToken(prefix: string): string;` : Generates a unique token for a given prefix, this can be then used to acquire and release connections from pool.
13
+ * `pipeline(token: string, commands: string[][], transaction: boolean): Promise<any>` : Executes a set of commands together either in transaction or just close to one another.
14
+ * `script(token: string, filename: string, keys: string[], args: string[]): Promise<any>` : Registers and executes a lua script.
15
+
16
+ Currently this package only supports ioredis as underneath client library, but in future it may expand to other libraries as well.
17
+
18
+ ## Getting Started
19
+
20
+ 1. Install using `npm -i redis-abstraction`
21
+ 2. Require in your project. `const { IORedisClientPool } = require('redis-abstraction');` or `import { IORedisClientPool } from 'redis-abstraction';`
22
+ 3. Run redis on local docker if required. `docker run --name streamz -p 6379:6379 -itd --rm redis:latest`
23
+ 4. All done, Start using it!!.
24
+
25
+ ## Examples/Code snippets
26
+
27
+ 1. Please find example code in [examples](https://github.com/LRagji/redis-abstraction/blob/main/examples) folder
28
+ 2. Please find example code usage in [unit tests](https://github.com/LRagji/redis-abstraction/blob/main/tests/specs-ioredis-client-pool.ts)
29
+
30
+
31
+ ### Non Cluster Initialization
32
+
33
+ ```javascript
34
+ //Define the redis connection string
35
+ const singleNodeRedisConnectionString = 'rediss://redis.my-service.com';
36
+ //Create a injector function for creating redis connection instance.
37
+ const connectionInjector = () => IORedisClientPool.IORedisClientClusterFactory([singleNodeRedisConnectionString]);
38
+ //Initialize the pool
39
+ const pool = new IORedisClientPool(connectionInjector);
40
+
41
+ //Pass it around in the application.
42
+ main(pool)
43
+ .finally(async () => {
44
+ //Remember to call shutdown which closes all connections in pool, else node.js process will not exit.
45
+ await pool.shutdown()
46
+ })
47
+ ```
48
+ ### Clustered Initialization
49
+
50
+ ```javascript
51
+ //Define the redis connection string
52
+ const clusteredRedisConnectionStringPrimary = 'rediss://redis.my-service.com';
53
+ const clusteredRedisConnectionStringSecondary = 'rediss://redis.my-service.com' || clusteredRedisConnectionStringPrimary; //Secondary is optional if not present pass in primary connection.
54
+ //Create a injector function for creating redis connection instance.
55
+ const connectionInjector = () => IORedisClientPool.IORedisClientClusterFactory([clusteredRedisConnectionStringPrimary,clusteredRedisConnectionStringSecondary]);//Passing more than one connection string indicates its a cluster setup.
56
+ //Initialize the pool
57
+ const pool = new IORedisClientPool(connectionInjector);
58
+
59
+ //Pass it around in the application.
60
+ main(pool)
61
+ .finally(async () => {
62
+ //Remember to call shutdown which closes all connections in pool, else node.js process will not exit.
63
+ await pool.shutdown()
64
+ })
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ```javascript
70
+ //Generates a unique token
71
+ const token = pool.generateUniqueToken('Test');
72
+ try {
73
+ //Acquire idle connection from the pool or create a fresh one if entire pool connections are busy
74
+ await pool.acquire(token);
75
+ //Execute the command on the acquired connection
76
+ await pool.run(token, ['set', 'key', 'value']);
77
+ }
78
+ finally {
79
+ //Release of connection is important as it makes it available for others to acquire.
80
+ await pool.release(token);
81
+ }
82
+ ```
83
+
84
+ ## Built with
85
+
86
+ 1. Authors :heart: for Open Source.
87
+
88
+
89
+ ## Contributions
90
+
91
+ 1. New ideas/techniques are welcomed.
92
+ 2. Raise a Pull Request.
93
+
94
+ ## License
95
+
96
+ This project is contrubution to public domain and completely free for use, view [LICENSE.md](/license.md) file for details.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Interface used to abstract a redis client/connection for this package
3
+ */
4
+ export interface IRedisClientPool {
5
+ /**
6
+ * This method acquires a redis client instance from a pool of redis clients with token as an identifier/handle.
7
+ * @param token A unique string used to acquire a redis client instance against. Treat this as redis client handle.
8
+ */
9
+ acquire(token: string): Promise<void>;
10
+ /**
11
+ * This method releases the acquired redis client back into the pool.
12
+ * @param token A unique string used when acquiring client via {@link acquire} method
13
+ */
14
+ release(token: string): Promise<void>;
15
+ /**
16
+ * Signals a dispose method to the pool stating no more clients will be needed, donot call any methods post calling shutdown.
17
+ */
18
+ shutdown(): Promise<void>;
19
+ /**
20
+ * Executes a single command on acquired connection.
21
+ * @param token token string which was used to acquire.
22
+ * @param commandArgs Array of strings including commands and arguments Eg:["set","key","value"]
23
+ * @returns Promise of any type.
24
+ */
25
+ run(token: string, commandArgs: string[]): Promise<any>;
26
+ /**
27
+ * This method is used to execute a set of commands in one go sequentially on redis side.
28
+ * @param token token string which was used to acquire.
29
+ * @param commands Array of array of strings including multiple commands and arguments that needs to be executed in one trip to the server sequentially. Eg:[["set","key","value"],["get","key"]]
30
+ * @param transaction Boolean value to indicate if to run the commands as transaction on redis or just run them close to each other.
31
+ * @returns Promise of all results in the commands(any type).
32
+ */
33
+ pipeline(token: string, commands: string[][], transaction: boolean): Promise<any>;
34
+ /**
35
+ * This method is used to execute a lua script on redis connection.
36
+ * @param token token string which was used to acquire.
37
+ * @param filePath Full file path of the lua script to be executed Eg: path.join(__dirname, "script.lua")
38
+ * @param keys Array of strings, Keys to be passsed to the script.
39
+ * @param args Array of strings, Arguments to be passed to the script.
40
+ */
41
+ script(token: string, filePath: string, keys: string[], args: string[]): Promise<any>;
42
+ /**
43
+ * This method should provide unique token for a given prefix.
44
+ * @param prefix An identity string for token to prepend.
45
+ */
46
+ generateUniqueToken(prefix: string): string;
47
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { IRedisClientPool } from './i-redis-client-pool';
2
+ export { IORedisClientPool } from './ioredis-client-pool';
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IORedisClientPool = void 0;
4
+ var ioredis_client_pool_1 = require("./ioredis-client-pool");
5
+ Object.defineProperty(exports, "IORedisClientPool", { enumerable: true, get: function () { return ioredis_client_pool_1.IORedisClientPool; } });
@@ -0,0 +1,33 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { IRedisClientPool } from "./i-redis-client-pool";
4
+ import crypto from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import Redis, { Cluster } from 'ioredis';
7
+ export declare type RedisConnection = Cluster | Redis;
8
+ export declare class IORedisClientPool implements IRedisClientPool {
9
+ private readonly nodeFSModule;
10
+ private readonly nodeCryptoModule;
11
+ private poolRedisClients;
12
+ private activeRedisClients;
13
+ private filenameToCommand;
14
+ private redisConnectionCreator;
15
+ private idlePoolSize;
16
+ private totalConnectionCounter;
17
+ constructor(redisConnectionCreator: () => RedisConnection, idlePoolSize?: number, nodeFSModule?: typeof fs, nodeCryptoModule?: typeof crypto);
18
+ generateUniqueToken(prefix: string): string;
19
+ shutdown(): Promise<void>;
20
+ acquire(token: string): Promise<void>;
21
+ release(token: string): Promise<void>;
22
+ run(token: string, commandArgs: any): Promise<unknown>;
23
+ pipeline(token: string, commands: string[][], transaction?: boolean): Promise<unknown[] | undefined>;
24
+ script(token: string, filePath: string, keys: string[], args: any[]): Promise<any>;
25
+ info(): {
26
+ "Idle Size": number;
27
+ "Current Active": number;
28
+ "Pooled Connection": number;
29
+ "Peak Connections": number;
30
+ };
31
+ private MD5Hash;
32
+ static IORedisClientClusterFactory(connectionDetails: string[], instanceInjection?: <T>(c: new (...args: any) => T, args: any[]) => T): RedisConnection;
33
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.IORedisClientPool = void 0;
39
+ const node_crypto_1 = __importDefault(require("node:crypto"));
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const ioredis_1 = __importStar(require("ioredis"));
42
+ const utils_1 = require("ioredis/built/utils");
43
+ function createInstance(c, args = []) { return new c(...args); }
44
+ class IORedisClientPool {
45
+ constructor(redisConnectionCreator, idlePoolSize = 6, nodeFSModule = node_fs_1.default, nodeCryptoModule = node_crypto_1.default) {
46
+ this.nodeFSModule = nodeFSModule;
47
+ this.nodeCryptoModule = nodeCryptoModule;
48
+ this.filenameToCommand = new Map();
49
+ this.totalConnectionCounter = 0;
50
+ this.poolRedisClients = Array.from({ length: idlePoolSize }, (_) => redisConnectionCreator());
51
+ this.totalConnectionCounter += idlePoolSize;
52
+ this.activeRedisClients = new Map();
53
+ this.redisConnectionCreator = redisConnectionCreator;
54
+ this.idlePoolSize = idlePoolSize;
55
+ }
56
+ generateUniqueToken(prefix) {
57
+ return `${prefix}-${this.nodeCryptoModule.randomUUID()}`;
58
+ }
59
+ shutdown() {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ const waitHandles = [...this.poolRedisClients, ...Array.from(this.activeRedisClients.values())]
62
+ .map((_) => __awaiter(this, void 0, void 0, function* () { yield _.quit(); _.disconnect(); }));
63
+ yield Promise.allSettled(waitHandles);
64
+ this.poolRedisClients = [];
65
+ this.activeRedisClients.clear();
66
+ this.totalConnectionCounter = 0;
67
+ });
68
+ }
69
+ acquire(token) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ if (!this.activeRedisClients.has(token)) {
72
+ const availableClient = this.poolRedisClients.pop() || (() => { this.totalConnectionCounter += 1; return this.redisConnectionCreator(); })();
73
+ this.activeRedisClients.set(token, availableClient);
74
+ }
75
+ });
76
+ }
77
+ release(token) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ const releasedClient = this.activeRedisClients.get(token);
80
+ if (releasedClient == undefined) {
81
+ return;
82
+ }
83
+ this.activeRedisClients.delete(token);
84
+ if (this.poolRedisClients.length < this.idlePoolSize) {
85
+ this.poolRedisClients.push(releasedClient);
86
+ }
87
+ else {
88
+ yield releasedClient.quit();
89
+ releasedClient.disconnect();
90
+ }
91
+ });
92
+ }
93
+ run(token, commandArgs) {
94
+ return __awaiter(this, void 0, void 0, function* () {
95
+ const redisClient = this.activeRedisClients.get(token);
96
+ if (redisClient == undefined) {
97
+ throw new Error("Please acquire a client with proper token");
98
+ }
99
+ return yield redisClient.call(commandArgs.shift(), ...commandArgs);
100
+ });
101
+ }
102
+ pipeline(token, commands, transaction = true) {
103
+ return __awaiter(this, void 0, void 0, function* () {
104
+ const redisClient = this.activeRedisClients.get(token);
105
+ if (redisClient == undefined) {
106
+ throw new Error("Please acquire a client with proper token");
107
+ }
108
+ const result = transaction === true ? yield redisClient.multi(commands).exec() : yield redisClient.pipeline(commands).exec();
109
+ return result === null || result === void 0 ? void 0 : result.map(r => {
110
+ let err = r[0];
111
+ if (err != null) {
112
+ throw err;
113
+ }
114
+ return r[1];
115
+ });
116
+ });
117
+ }
118
+ script(token, filePath, keys, args) {
119
+ return __awaiter(this, void 0, void 0, function* () {
120
+ const redisClient = this.activeRedisClients.get(token);
121
+ if (redisClient == undefined) {
122
+ throw new Error("Please acquire a client with proper token");
123
+ }
124
+ let command = this.filenameToCommand.get(filePath);
125
+ // @ts-ignore
126
+ if (command == null || redisClient[command] == null) {
127
+ const contents = yield this.nodeFSModule.promises.readFile(filePath, { encoding: "utf-8" });
128
+ command = this.MD5Hash(contents);
129
+ redisClient.defineCommand(command, { lua: contents });
130
+ this.filenameToCommand.set(filePath, command);
131
+ }
132
+ // @ts-ignore
133
+ return yield redisClient[command](keys.length, keys, args);
134
+ });
135
+ }
136
+ info() {
137
+ const returnObj = {
138
+ "Idle Size": this.idlePoolSize,
139
+ "Current Active": this.activeRedisClients.size,
140
+ "Pooled Connection": this.poolRedisClients.length,
141
+ "Peak Connections": this.totalConnectionCounter
142
+ };
143
+ this.totalConnectionCounter = 0;
144
+ return returnObj;
145
+ }
146
+ MD5Hash(value) {
147
+ return this.nodeCryptoModule.createHash('md5').update(value).digest('hex');
148
+ }
149
+ static IORedisClientClusterFactory(connectionDetails, instanceInjection = createInstance) {
150
+ const distinctConnections = new Set(connectionDetails);
151
+ if (distinctConnections.size === 0) {
152
+ throw new Error("Inncorrect or Invalid Connection details, cannot be empty");
153
+ }
154
+ if (connectionDetails.length > distinctConnections.size || distinctConnections.size > 1) {
155
+ const parsedRedisURl = (0, utils_1.parseURL)(connectionDetails[0]); //Assuming all have same password(they should have finally its a cluster)
156
+ const awsElasticCacheOptions = {
157
+ dnsLookup: (address, callback) => callback(null, address),
158
+ redisOptions: {
159
+ tls: connectionDetails[0].startsWith("rediss:") == true ? {} : undefined,
160
+ password: parsedRedisURl.password,
161
+ maxRedirections: 32
162
+ },
163
+ };
164
+ return instanceInjection(ioredis_1.Cluster, [Array.from(distinctConnections.values()), awsElasticCacheOptions]);
165
+ }
166
+ else {
167
+ return instanceInjection(ioredis_1.default, [connectionDetails[0]]);
168
+ }
169
+ }
170
+ }
171
+ exports.IORedisClientPool = IORedisClientPool;
package/license.md ADDED
@@ -0,0 +1,9 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
4
+
5
+ In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
6
+
7
+ 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 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.
8
+
9
+ For more information, please refer to https://unlicense.org
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "redis-abstraction",
3
+ "version": "0.0.2",
4
+ "description": "A Redis client pool with abstraction to different Redis libraries.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "clean-build": "run-script-os",
8
+ "copy-files": "run-script-os",
9
+ "copy-files:linux": "cp ./README.md ./package.json ./package-lock.json ./license.md ./dist/",
10
+ "copy-files:macos": "cp ./README.md ./package.json ./package-lock.json ./license.md ./dist/",
11
+ "copy-files:windows": "for %I in (.\\README.md .\\package.json .\\package-lock.json .\\license.md) do copy %I .\\dist\\",
12
+ "clean-build:macos": "rm -rf ./dist/",
13
+ "clean-build:linux": "rm -rf ./dist/",
14
+ "clean-build:windows": "rmdir /s /q .\\dist\\",
15
+ "test-run": "nyc --reporter=html --reporter=text mocha -r ts-node/register ./tests/**/*.ts",
16
+ "test": "npm run build && npm run test-run",
17
+ "build": "(npm run clean-build || node -v) && tsc && npm run copy-files && npm run docs",
18
+ "redisstop": "docker stop TestCentralStore && ping 127.0.0.1 -c 3",
19
+ "redisstart": "(npm run redisstop || docker -v ) && docker run --name TestCentralStore -v ${PWD}:\"/var/lib/luatest\" -p 6379:6379 -itd --rm redis:latest",
20
+ "redisView": "docker run -v redisinsight:/db -p 8001:8001 -itd --rm redislabs/redisinsight:latest",
21
+ "push": "npm whoami && npm version patch && npm test && cd ./dist && npm publish && cd .. && git push --tags",
22
+ "docs": "typedoc"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/LRagji/redis-abstraction.git"
27
+ },
28
+ "author": "Laukik",
29
+ "license": "SEE LICENSE IN license.md",
30
+ "bugs": {
31
+ "url": "https://github.com/LRagji/redis-abstraction/issues"
32
+ },
33
+ "homepage": "https://github.com/LRagji/redis-abstraction#readme",
34
+ "keywords": [
35
+ "cluster",
36
+ "redis",
37
+ "client",
38
+ "abstraction",
39
+ "Laukik"
40
+ ],
41
+ "devDependencies": {
42
+ "@types/mocha": "^9.1.1",
43
+ "@types/sinon": "^10.0.15",
44
+ "cross-env": "^7.0.3",
45
+ "mocha": "^9.2.2",
46
+ "nyc": "^15.1.0",
47
+ "run-script-os": "^1.1.6",
48
+ "sinon": "^15.2.0",
49
+ "ts-node": "^10.7.0",
50
+ "typedoc": "^0.22.14",
51
+ "typescript": "^4.6.3"
52
+ },
53
+ "dependencies": {
54
+ "ioredis": "^5.3.2"
55
+ }
56
+ }