From af7f130ee8c124e2b0f0db473751645e9d115d72 Mon Sep 17 00:00:00 2001 From: Nicolas Zelaya Date: Fri, 3 Nov 2023 12:35:14 -0300 Subject: [PATCH 1/9] Fixing keybuilder to match spec --- src/storages/KeyBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index dc9581b5..c3124ae9 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -21,7 +21,7 @@ export class KeyBuilder { } buildFlagSetKey(flagSet: string) { - return `${this.prefix}.flagset.${flagSet}`; + return `${this.prefix}.flagSet.${flagSet}`; } buildSplitKey(splitName: string) { From 5912dc898434f1258030fbee85a451489a04b3ca Mon Sep 17 00:00:00 2001 From: Nicolas Zelaya Date: Fri, 3 Nov 2023 12:36:35 -0300 Subject: [PATCH 2/9] Implement getNamesByFlagSets for Redis --- src/storages/inRedis/SplitsCacheInRedis.ts | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 310c014d..f83ee21a 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -10,7 +10,7 @@ import { ISet, _Set } from '../../utils/lang/sets'; /** * Discard errors for an answer of multiple operations. */ -function processPipelineAnswer(results: Array<[Error | null, string]>): string[] { +function processPipelineAnswer(results: Array<[Error | null, string]>): (string | string[])[] { return results.reduce((accum: string[], errValuePair: [Error | null, string]) => { if (errValuePair[0] === null) accum.push(errValuePair[1]); return accum; @@ -195,9 +195,29 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * or rejected if wrapper operation fails. * @todo this is a no-op method to be implemented */ - getNamesByFlagSets(): Promise> { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - return new Promise(flagSets => new _Set([])); + getNamesByFlagSets(flagSets: string[]): Promise> { + const toReturn: ISet = new _Set([]); + const listOfKeys: string[] = []; + + flagSets && flagSets.forEach(flagSet => { + listOfKeys.push(this.keys.buildFlagSetKey(flagSet)); + }); + + if (listOfKeys.length > 0) { + + return this.redis.pipeline(listOfKeys.map(k => ['smembers', k])).exec() + .then(processPipelineAnswer) + .then((setsRaw) => { + this.log.error(setsRaw); + setsRaw.forEach((setContent) => { + (setContent as string[]).forEach(flagName => toReturn.add(flagName)); + }); + + return toReturn; + }); + } else { + return new Promise(() => toReturn); + } } /** From 6268360ab044e250665b89bead81b2ca58d0819c Mon Sep 17 00:00:00 2001 From: Nicolas Zelaya Date: Mon, 6 Nov 2023 09:19:35 -0300 Subject: [PATCH 3/9] Handling pipeline method properly in RedisAdapter --- src/storages/inRedis/RedisAdapter.ts | 71 +++++++++++++++++++--------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 7beb2988..423566f5 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -8,7 +8,8 @@ import { timeout } from '../../utils/promise/timeout'; const LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim', 'hset']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset']; +const METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline']; // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere. const DEFAULT_OPTIONS = { @@ -56,6 +57,7 @@ export class RedisAdapter extends ioredis { this.once('ready', () => { const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0; this.log.info(LOG_PREFIX + `Redis connection established. Queued commands: ${commandsCount}.`); + this._notReadyCommandsQueue && this._notReadyCommandsQueue.forEach(queued => { this.log.info(LOG_PREFIX + `Executing queued ${queued.name} command.`); queued.command().then(queued.resolve).catch(queued.reject); @@ -71,49 +73,72 @@ export class RedisAdapter extends ioredis { _setTimeoutWrappers() { const instance: Record = this; - METHODS_TO_PROMISE_WRAP.forEach(method => { - const originalMethod = instance[method]; + const wrapWithQueueOrExecute = (method: string, commandWrapper: Function) => { + if (instance._notReadyCommandsQueue) { + return new Promise((resolve, reject) => { + instance._notReadyCommandsQueue.unshift({ + resolve, + reject, + command: commandWrapper, + name: method.toUpperCase() + }); + }); + } else { + return commandWrapper(); + } + }; - instance[method] = function () { + const wrapCommand = (originalMethod: Function, methodName: string) => { + return function () { const params = arguments; - function commandWrapper() { - instance.log.debug(LOG_PREFIX + `Executing ${method}.`); - // Return original method - const result = originalMethod.apply(instance, params); + const commandWrapper = () => { + instance.log.debug(`${LOG_PREFIX} Executing ${methodName}.`); + const result = originalMethod.apply(this, params); if (thenable(result)) { // For handling pending commands on disconnect, add to the set and remove once finished. // On sync commands there's no need, only thenables. instance._runningCommands.add(result); - const cleanUpRunningCommandsCb = function () { + const cleanUpRunningCommandsCb = () => { instance._runningCommands.delete(result); }; // Both success and error remove from queue. result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb); return timeout(instance._options.operationTimeout, result).catch(err => { - instance.log.error(LOG_PREFIX + `${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); + instance.log.error(`${LOG_PREFIX}${methodName} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); // Handling is not the adapter responsibility. throw err; }); } return result; - } + }; - if (instance._notReadyCommandsQueue) { - return new Promise((res, rej) => { - instance._notReadyCommandsQueue.unshift({ - resolve: res, - reject: rej, - command: commandWrapper, - name: method.toUpperCase() - }); - }); - } else { - return commandWrapper(); - } + return wrapWithQueueOrExecute(methodName, commandWrapper); + }; + }; + + // Wrap regular async methods to track timeouts and queue when Redis is not yet executing commands. + METHODS_TO_PROMISE_WRAP.forEach(methodName => { + const originalFn = instance[methodName]; + instance[methodName] = wrapCommand(originalFn, methodName); + }); + + // Special handling for pipeline~like methods. We need to wrap the async trigger, which is exec, but return the Pipeline right away. + METHODS_TO_PROMISE_WRAP_EXEC.forEach(methodName => { + const originalFn = instance[methodName]; + // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable. + instance[methodName] = function () { + instance.log.debug(`${LOG_PREFIX} Creating ${methodName}.`); + + const res = originalFn.apply(instance, arguments); + const originalExec = res.exec; + + res.exec = wrapCommand(originalExec, methodName + '.exec').bind(res); + + return res; }; }); } From 965df61ed2df298f27325f4d553e7d2de614f531 Mon Sep 17 00:00:00 2001 From: Nicolas Zelaya Date: Mon, 6 Nov 2023 14:39:32 -0300 Subject: [PATCH 4/9] RedisAdapter typing shadowed this --- src/storages/inRedis/RedisAdapter.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 423566f5..5a616c11 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -1,4 +1,4 @@ -import ioredis from 'ioredis'; +import ioredis, { Pipeline } from 'ioredis'; import { ILogger } from '../../logger/types'; import { merge, isString } from '../../utils/lang'; import { _Set, setToArray, ISet } from '../../utils/lang/sets'; @@ -89,12 +89,15 @@ export class RedisAdapter extends ioredis { }; const wrapCommand = (originalMethod: Function, methodName: string) => { + // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one) + // or it can be the instance of a Pipeline object. return function () { const params = arguments; + const caller: (Pipeline | RedisAdapter) = this; // Need to change this crap to have TypeScript stop complaining const commandWrapper = () => { instance.log.debug(`${LOG_PREFIX} Executing ${methodName}.`); - const result = originalMethod.apply(this, params); + const result = originalMethod.apply(caller, params); if (thenable(result)) { // For handling pending commands on disconnect, add to the set and remove once finished. From 8057a7c0f8a3b26184427464a9f7a04465a16489 Mon Sep 17 00:00:00 2001 From: Nicolas Zelaya Date: Tue, 7 Nov 2023 11:13:45 -0300 Subject: [PATCH 5/9] fix after merge --- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 83c06472..06f61074 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -5,7 +5,7 @@ import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISplit } from '../../dtos/types'; import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync'; -import { ISet } from '../../utils/lang/sets'; +import { _Set, ISet } from '../../utils/lang/sets'; /** * Discard errors for an answer of multiple operations. From 1ce4acbc4fb405b9fc4853d0a903d385b6a7056f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 29 Nov 2023 23:13:54 -0300 Subject: [PATCH 6/9] Fix typescript issue in RedisAdapter --- src/storages/inRedis/RedisAdapter.ts | 4 ++-- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index dcdadb4a..81a2b4fd 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -91,9 +91,9 @@ export class RedisAdapter extends ioredis { const wrapCommand = (originalMethod: Function, methodName: string) => { // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one) // or it can be the instance of a Pipeline object. - return function () { + return function (this: RedisAdapter | Pipeline) { const params = arguments; - const caller: (Pipeline | RedisAdapter) = this; // Need to change this crap to have TypeScript stop complaining + const caller: (Pipeline | RedisAdapter) = this; const commandWrapper = () => { instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 70abd74b..a9370ea3 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -10,7 +10,7 @@ import type { RedisAdapter } from './RedisAdapter'; /** * Discard errors for an answer of multiple operations. */ -function processPipelineAnswer(results: Array<[Error | null, string]>): (string | string[])[] { +function processPipelineAnswer(results: Array<[Error | null, string]>): string[] { return results.reduce((accum: string[], errValuePair: [Error | null, string]) => { if (errValuePair[0] === null) accum.push(errValuePair[1]); return accum; From 58ca122ddcd159c2124bfbfdaea7fec75f354f74 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 30 Nov 2023 15:59:12 -0300 Subject: [PATCH 7/9] Polishing --- src/storages/inRedis/RedisAdapter.ts | 36 ++++++++++++---------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 1c4b3ba7..6d738606 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -73,29 +73,14 @@ export class RedisAdapter extends ioredis { _setTimeoutWrappers() { const instance: Record = this; - const wrapWithQueueOrExecute = (method: string, commandWrapper: Function) => { - if (instance._notReadyCommandsQueue) { - return new Promise((resolve, reject) => { - instance._notReadyCommandsQueue.unshift({ - resolve, - reject, - command: commandWrapper, - name: method.toUpperCase() - }); - }); - } else { - return commandWrapper(); - } - }; - const wrapCommand = (originalMethod: Function, methodName: string) => { // The value of "this" in this function should be the instance actually executing the method. It might be the instance referred (the base one) // or it can be the instance of a Pipeline object. return function (this: RedisAdapter | Pipeline) { const params = arguments; - const caller: (Pipeline | RedisAdapter) = this; + const caller = this; - const commandWrapper = () => { + function commandWrapper() { instance.log.debug(`${LOG_PREFIX}Executing ${methodName}.`); const result = originalMethod.apply(caller, params); @@ -117,9 +102,20 @@ export class RedisAdapter extends ioredis { } return result; - }; + } - return wrapWithQueueOrExecute(methodName, commandWrapper); + if (instance._notReadyCommandsQueue) { + return new Promise((resolve, reject) => { + instance._notReadyCommandsQueue.unshift({ + resolve, + reject, + command: commandWrapper, + name: methodName.toUpperCase() + }); + }); + } else { + return commandWrapper(); + } }; }; @@ -134,8 +130,6 @@ export class RedisAdapter extends ioredis { const originalFn = instance[methodName]; // "First level wrapper" to handle the sync execution and wrap async, queueing later if applicable. instance[methodName] = function () { - instance.log.debug(`${LOG_PREFIX} Creating ${methodName}.`); - const res = originalFn.apply(instance, arguments); const originalExec = res.exec; From e44196ce8a79ded95219d350270f9805a6f1cdc8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 30 Nov 2023 17:25:21 -0300 Subject: [PATCH 8/9] Unit test --- package-lock.json | 4 +- package.json | 2 +- .../inRedis/__tests__/RedisAdapter.spec.ts | 42 ++++++++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f131008e..416f9ff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.2", + "version": "1.12.1-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.2", + "version": "1.12.1-rc.3", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index df3097c7..b4bd3540 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.2", + "version": "1.12.1-rc.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index b982d06a..a8ef69da 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -12,13 +12,27 @@ const LOG_PREFIX = 'storage:redis-adapter: '; // The list of methods we're wrapping on a promise (for timeout) on the adapter. const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'expire', 'mget', 'lrange', 'ltrim', 'hset', 'hincrby', 'popNRaw']; +const METHODS_TO_PROMISE_WRAP_EXEC = ['pipeline']; +const pipelineExecMock = jest.fn(() => Promise.resolve('exec')); const ioredisMock = reduce([...METHODS_TO_PROMISE_WRAP, 'disconnect'], (acc, methodName) => { acc[methodName] = jest.fn(() => Promise.resolve(methodName)); return acc; +}, reduce(METHODS_TO_PROMISE_WRAP_EXEC, (acc, methodName) => { + acc[methodName] = jest.fn(() => { + const pipelineAlikeMock = Object.assign(reduce(METHODS_TO_PROMISE_WRAP, (acc, methodName) => { + acc[methodName] = jest.fn(() => pipelineAlikeMock); + return acc; + }, {}), { + exec: pipelineExecMock + }); + + return pipelineAlikeMock; + }); + return acc; }, { once: jest.fn() -}) as { once: jest.Mock }; +}) as { once: jest.Mock }); let constructorParams: any = false; @@ -247,14 +261,16 @@ describe('STORAGE Redis Adapter', () => { url: 'redis://localhost:6379/0' }); - forEach(METHODS_TO_PROMISE_WRAP, methodName => { + forEach([...METHODS_TO_PROMISE_WRAP, ...METHODS_TO_PROMISE_WRAP_EXEC], methodName => { expect(instance[methodName]).not.toBe(ioredisMock[methodName]); // Method "${methodName}" from ioredis library should be wrapped. expect(ioredisMock[methodName]).not.toBeCalled(); // Checking that the method was not called yet. const startingQueueLength = instance._notReadyCommandsQueue.length; // We do have the commands queue on this state, so a call for this methods will queue the command. - const wrapperResult = instance[methodName](methodName); + const wrapperResult = METHODS_TO_PROMISE_WRAP_EXEC.includes(methodName) ? + instance[methodName](methodName).exec() : + instance[methodName](methodName); expect(wrapperResult instanceof Promise).toBe(true); // The result is a promise since we are queueing commands on this state. expect(instance._notReadyCommandsQueue.length).toBe(startingQueueLength + 1); // The queue should have one more item. @@ -263,19 +279,24 @@ describe('STORAGE Redis Adapter', () => { expect(typeof queuedCommand.resolve).toBe('function'); // The queued item should have the correct form. expect(typeof queuedCommand.reject).toBe('function'); // The queued item should have the correct form. expect(typeof queuedCommand.command).toBe('function'); // The queued item should have the correct form. - expect(queuedCommand.name).toBe(methodName.toUpperCase()); // The queued item should have the correct form. + expect(queuedCommand.name).toBe((METHODS_TO_PROMISE_WRAP_EXEC.includes(methodName) ? methodName + '.exec' : methodName).toUpperCase()); // The queued item should have the correct form. }); instance._notReadyCommandsQueue = false; // Remove the queue. loggerMock.error.resetHistory; - forEach(METHODS_TO_PROMISE_WRAP, (methodName, index) => { + forEach([...METHODS_TO_PROMISE_WRAP, ...METHODS_TO_PROMISE_WRAP_EXEC], (methodName, index) => { // We do NOT have the commands queue on this state, so a call for this methods will execute the command. - expect(ioredisMock[methodName]).not.toBeCalled(); // Control assertion - Original method (${methodName}) was not called yet + if (METHODS_TO_PROMISE_WRAP.includes(methodName)) expect(ioredisMock[methodName]).not.toBeCalled(); // Control assertion - Original method (${methodName}) was not called yet + else expect(pipelineExecMock).not.toBeCalled(); // Control assertion - Original Pipeline exec method was not called yet const previousTimeoutCalls = timeout.mock.calls.length; let previousRunningCommandsSize = instance._runningCommands.size; - instance[methodName](methodName).catch(() => { }); // Swallow exception so it's not spread to logs. + + (METHODS_TO_PROMISE_WRAP_EXEC.includes(methodName) ? + instance[methodName](methodName).exec() : + instance[methodName](methodName) + ).catch(() => { }); // Swallow exception so it's not spread to logs. expect(ioredisMock[methodName]).toBeCalled(); // Original method (${methodName}) is called right away (through wrapper) when we are not queueing anymore. expect(instance._runningCommands.size).toBe(previousRunningCommandsSize + 1); // If the result of the operation was a thenable it will add the item to the running commands queue. @@ -290,7 +311,7 @@ describe('STORAGE Redis Adapter', () => { commandTimeoutResolver.rej('test'); setTimeout(() => { // Allow the promises to tick. expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes with error, it's promise is removed from the instance._runningCommands queue. - expect(loggerMock.error.mock.calls[index]).toEqual([`${LOG_PREFIX}${methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. + expect(loggerMock.error.mock.calls[index]).toEqual([`${LOG_PREFIX}${METHODS_TO_PROMISE_WRAP_EXEC.includes(methodName) ? methodName + '.exec' : methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. }, 0); }); @@ -306,9 +327,10 @@ describe('STORAGE Redis Adapter', () => { instance._notReadyCommandsQueue = false; // Connection is "ready" - forEach(METHODS_TO_PROMISE_WRAP, methodName => { + forEach([...METHODS_TO_PROMISE_WRAP, ...METHODS_TO_PROMISE_WRAP_EXEC], methodName => { // Just call the wrapped method, we don't care about all the paths tested on the previous case, just how it behaves when the command is resolved. - instance[methodName](methodName); + if (METHODS_TO_PROMISE_WRAP_EXEC.includes(methodName)) instance[methodName](methodName).exec(); + else instance[methodName](methodName); // Get the original promise (the one passed to timeout) const commandTimeoutResolver = timeoutPromiseResolvers[0]; From b49ae03713ae3958608f545fa629e5d8f0e94abf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 1 Dec 2023 13:29:43 -0300 Subject: [PATCH 9/9] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 416f9ff2..d19598ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.3", + "version": "1.12.1-rc.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.3", + "version": "1.12.1-rc.4", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index b4bd3540..d666fba7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.12.1-rc.3", + "version": "1.12.1-rc.4", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js",