@plyaz/core 1.0.2 → 1.0.4
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 +69 -16
- package/dist/base/cache/index.d.ts.map +1 -1
- package/dist/base/cache/strategies/memory.d.ts.map +1 -1
- package/dist/base/cache/strategies/redis.d.ts.map +1 -1
- package/dist/domain/featureFlags/provider.d.ts +8 -0
- package/dist/domain/featureFlags/provider.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/factory.d.ts +30 -5
- package/dist/domain/featureFlags/providers/factory.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/file.d.ts +97 -1
- package/dist/domain/featureFlags/providers/file.d.ts.map +1 -1
- package/dist/domain/featureFlags/providers/memory.d.ts +12 -5
- package/dist/domain/featureFlags/providers/memory.d.ts.map +1 -1
- package/dist/engine/featureFlags/engine.d.ts +7 -0
- package/dist/engine/featureFlags/engine.d.ts.map +1 -1
- package/dist/index.cjs +507 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +482 -75
- package/dist/index.mjs.map +1 -1
- package/dist/utils/common/hash.d.ts.map +1 -1
- package/dist/utils/common/values.d.ts.map +1 -1
- package/dist/utils/featureFlags/context.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var config = require('@plyaz/config');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var util = require('util');
|
|
7
|
+
var url = require('url');
|
|
8
|
+
var yaml = require('yaml');
|
|
4
9
|
var common = require('@nestjs/common');
|
|
5
10
|
var React = require('react');
|
|
6
11
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
12
|
|
|
13
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
14
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
15
|
|
|
16
|
+
function _interopNamespace(e) {
|
|
17
|
+
if (e && e.__esModule) return e;
|
|
18
|
+
var n = Object.create(null);
|
|
19
|
+
if (e) {
|
|
20
|
+
Object.keys(e).forEach(function (k) {
|
|
21
|
+
if (k !== 'default') {
|
|
22
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
23
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function () { return e[k]; }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
n.default = e;
|
|
31
|
+
return Object.freeze(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
35
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
36
|
+
var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
|
|
10
37
|
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
11
38
|
|
|
12
39
|
// @plyaz package - Built with tsup
|
|
@@ -24,8 +51,6 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
24
51
|
};
|
|
25
52
|
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
26
53
|
var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
|
|
27
|
-
|
|
28
|
-
// src/utils/common/hash.ts
|
|
29
54
|
function hashString(str) {
|
|
30
55
|
const HASH_SHIFT = 5;
|
|
31
56
|
let hash = 0;
|
|
@@ -38,10 +63,10 @@ function hashString(str) {
|
|
|
38
63
|
}
|
|
39
64
|
__name(hashString, "hashString");
|
|
40
65
|
function isInRollout(identifier, percentage) {
|
|
41
|
-
if (percentage >=
|
|
66
|
+
if (percentage >= config.MATH_CONSTANTS.PERCENTAGE_MAX) return true;
|
|
42
67
|
if (percentage <= 0) return false;
|
|
43
68
|
const hash = hashString(identifier);
|
|
44
|
-
return hash %
|
|
69
|
+
return hash % config.MATH_CONSTANTS.PERCENTAGE_MAX < percentage;
|
|
45
70
|
}
|
|
46
71
|
__name(isInRollout, "isInRollout");
|
|
47
72
|
function createRolloutIdentifier(featureKey, userId) {
|
|
@@ -70,7 +95,7 @@ var HashUtils = {
|
|
|
70
95
|
* @param totalBuckets - Total number of buckets (default: 100)
|
|
71
96
|
* @returns true if identifier is in the bucket range
|
|
72
97
|
*/
|
|
73
|
-
isInBucketRange: /* @__PURE__ */ __name((identifier, startBucket, endBucket, totalBuckets =
|
|
98
|
+
isInBucketRange: /* @__PURE__ */ __name((identifier, startBucket, endBucket, totalBuckets = config.MATH_CONSTANTS.PERCENTAGE_MAX) => {
|
|
74
99
|
const bucket = hashString(identifier) % totalBuckets;
|
|
75
100
|
return bucket >= startBucket && bucket <= endBucket;
|
|
76
101
|
}, "isInBucketRange"),
|
|
@@ -85,8 +110,6 @@ var HashUtils = {
|
|
|
85
110
|
return hashString(str) % SAFE_INT;
|
|
86
111
|
}, "createSeed")
|
|
87
112
|
};
|
|
88
|
-
|
|
89
|
-
// src/utils/common/values.ts
|
|
90
113
|
function isStringFalsy(value) {
|
|
91
114
|
if (value === "") return true;
|
|
92
115
|
const lower = value.toLowerCase().trim();
|
|
@@ -152,7 +175,7 @@ var ValueUtils = {
|
|
|
152
175
|
*/
|
|
153
176
|
isValidPercentage: /* @__PURE__ */ __name((value) => {
|
|
154
177
|
if (typeof value !== "number") return false;
|
|
155
|
-
return !isNaN(value) && isFinite(value) && value >= 0 && value <=
|
|
178
|
+
return !isNaN(value) && isFinite(value) && value >= 0 && value <= config.MATH_CONSTANTS.PERCENTAGE_MAX;
|
|
156
179
|
}, "isValidPercentage"),
|
|
157
180
|
/**
|
|
158
181
|
* Clamps a number to a specific range.
|
|
@@ -186,9 +209,9 @@ var ValueUtils = {
|
|
|
186
209
|
* @param defaultValue - Default if path doesn't exist
|
|
187
210
|
* @returns Property value or default
|
|
188
211
|
*/
|
|
189
|
-
getNestedProperty: /* @__PURE__ */ __name((obj,
|
|
212
|
+
getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
|
|
190
213
|
if (!obj || typeof obj !== "object") return defaultValue;
|
|
191
|
-
const keys =
|
|
214
|
+
const keys = path2.split(".");
|
|
192
215
|
let current = obj;
|
|
193
216
|
for (const key of keys) {
|
|
194
217
|
if (current == null || typeof current !== "object") return defaultValue;
|
|
@@ -198,8 +221,6 @@ var ValueUtils = {
|
|
|
198
221
|
return current;
|
|
199
222
|
}, "getNestedProperty")
|
|
200
223
|
};
|
|
201
|
-
|
|
202
|
-
// src/utils/featureFlags/context.ts
|
|
203
224
|
var FeatureFlagContextBuilder = class _FeatureFlagContextBuilder {
|
|
204
225
|
static {
|
|
205
226
|
__name(this, "FeatureFlagContextBuilder");
|
|
@@ -395,7 +416,7 @@ var ContextUtils = {
|
|
|
395
416
|
if (context.platform && !["web", "mobile", "desktop"].includes(context.platform)) {
|
|
396
417
|
errors.push("Platform must be web, mobile, or desktop");
|
|
397
418
|
}
|
|
398
|
-
if (context.country && context.country.length !==
|
|
419
|
+
if (context.country && context.country.length !== config.ISO_STANDARDS.ISO_COUNTRY_CODE_LENGTH) {
|
|
399
420
|
errors.push("Country must be a 2-letter ISO country code");
|
|
400
421
|
}
|
|
401
422
|
return {
|
|
@@ -756,6 +777,16 @@ var FeatureFlagEngine = class {
|
|
|
756
777
|
this.overrides.delete(key);
|
|
757
778
|
this.log("Override removed:", key);
|
|
758
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Updates the default values for feature flags.
|
|
782
|
+
* This is useful when the FEATURES constant is updated at runtime.
|
|
783
|
+
*
|
|
784
|
+
* @param newDefaults - New default values
|
|
785
|
+
*/
|
|
786
|
+
updateDefaults(newDefaults) {
|
|
787
|
+
this.defaults = newDefaults;
|
|
788
|
+
this.log("Updated default feature values");
|
|
789
|
+
}
|
|
759
790
|
/**
|
|
760
791
|
* Clears all manual overrides.
|
|
761
792
|
*/
|
|
@@ -933,7 +964,7 @@ var FeatureFlagEngine = class {
|
|
|
933
964
|
value: rule.value,
|
|
934
965
|
isEnabled: isTruthy(rule.value),
|
|
935
966
|
reason: "rule_match",
|
|
936
|
-
|
|
967
|
+
matchedRuleId: rule.id,
|
|
937
968
|
evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
|
|
938
969
|
};
|
|
939
970
|
}
|
|
@@ -976,8 +1007,6 @@ var FeatureFlagEngine = class {
|
|
|
976
1007
|
}
|
|
977
1008
|
}
|
|
978
1009
|
};
|
|
979
|
-
|
|
980
|
-
// src/base/cache/strategies/memory.ts
|
|
981
1010
|
var MemoryCacheStrategy = class {
|
|
982
1011
|
static {
|
|
983
1012
|
__name(this, "MemoryCacheStrategy");
|
|
@@ -999,16 +1028,14 @@ var MemoryCacheStrategy = class {
|
|
|
999
1028
|
*
|
|
1000
1029
|
* @param config - Memory cache configuration
|
|
1001
1030
|
*/
|
|
1002
|
-
constructor(config = {}) {
|
|
1003
|
-
const DEFAULT_MAX_ENTRIES = 1e3;
|
|
1004
|
-
const DEFAULT_CLEANUP_INTERVAL = 6e4;
|
|
1031
|
+
constructor(config$1 = {}) {
|
|
1005
1032
|
const defaultConfig = {
|
|
1006
|
-
maxEntries:
|
|
1007
|
-
cleanupInterval:
|
|
1033
|
+
maxEntries: config.CACHE_MAX_SIZE_DEFAULT,
|
|
1034
|
+
cleanupInterval: config.CACHE_CLEANUP_INTERVAL_DEFAULT
|
|
1008
1035
|
};
|
|
1009
|
-
this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
|
|
1010
|
-
this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
|
|
1011
|
-
this.onEvict = config.onEvict;
|
|
1036
|
+
this.maxSize = config$1.maxSize ?? config$1.maxEntries ?? defaultConfig.maxEntries;
|
|
1037
|
+
this.cleanupInterval = config$1.cleanupInterval ?? defaultConfig.cleanupInterval;
|
|
1038
|
+
this.onEvict = config$1.onEvict;
|
|
1012
1039
|
this.startCleanup();
|
|
1013
1040
|
}
|
|
1014
1041
|
/**
|
|
@@ -1156,8 +1183,6 @@ var MemoryCacheStrategy = class {
|
|
|
1156
1183
|
}
|
|
1157
1184
|
}
|
|
1158
1185
|
};
|
|
1159
|
-
|
|
1160
|
-
// src/base/cache/strategies/redis.ts
|
|
1161
1186
|
var RedisCacheStrategy = class {
|
|
1162
1187
|
/**
|
|
1163
1188
|
* Creates a new Redis cache strategy.
|
|
@@ -1195,7 +1220,7 @@ var RedisCacheStrategy = class {
|
|
|
1195
1220
|
const redisKey = this.buildRedisKey(key);
|
|
1196
1221
|
const serializedEntry = JSON.stringify(entry);
|
|
1197
1222
|
const ttlMs = entry.expiresAt - Date.now();
|
|
1198
|
-
const ttlSeconds = Math.max(1, Math.ceil(ttlMs /
|
|
1223
|
+
const ttlSeconds = Math.max(1, Math.ceil(ttlMs / config.TIME_CONSTANTS.MILLISECONDS_PER_SECOND));
|
|
1199
1224
|
await this.client.set(redisKey, serializedEntry, "EX", ttlSeconds);
|
|
1200
1225
|
this.stats.setCount++;
|
|
1201
1226
|
}
|
|
@@ -1319,8 +1344,8 @@ var RedisCacheStrategy = class {
|
|
|
1319
1344
|
commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
|
|
1320
1345
|
enableOfflineQueue: defaultOptions.enableOfflineQueue
|
|
1321
1346
|
});
|
|
1322
|
-
await new Promise((
|
|
1323
|
-
client.on("ready",
|
|
1347
|
+
await new Promise((resolve2, reject) => {
|
|
1348
|
+
client.on("ready", resolve2);
|
|
1324
1349
|
client.on("error", reject);
|
|
1325
1350
|
});
|
|
1326
1351
|
return client;
|
|
@@ -1366,7 +1391,7 @@ var CacheManager = class {
|
|
|
1366
1391
|
const finalTtl = ttl ?? this.config.ttl;
|
|
1367
1392
|
const entry = {
|
|
1368
1393
|
data: value,
|
|
1369
|
-
expiresAt: Date.now() + finalTtl *
|
|
1394
|
+
expiresAt: Date.now() + finalTtl * config.TIME_CONSTANTS.MILLISECONDS_PER_SECOND,
|
|
1370
1395
|
createdAt: Date.now()
|
|
1371
1396
|
};
|
|
1372
1397
|
await this.strategy.set(key, entry);
|
|
@@ -1459,8 +1484,6 @@ var CacheManager = class {
|
|
|
1459
1484
|
await this.strategy.dispose?.();
|
|
1460
1485
|
}
|
|
1461
1486
|
};
|
|
1462
|
-
|
|
1463
|
-
// src/domain/featureFlags/provider.ts
|
|
1464
1487
|
var FeatureFlagProvider = class {
|
|
1465
1488
|
/**
|
|
1466
1489
|
* Creates a new feature flag provider.
|
|
@@ -1695,7 +1718,7 @@ var FeatureFlagProvider = class {
|
|
|
1695
1718
|
void this.refresh().catch((error) => {
|
|
1696
1719
|
this.log("Auto-refresh failed:", error);
|
|
1697
1720
|
});
|
|
1698
|
-
}, this.config.refreshInterval *
|
|
1721
|
+
}, this.config.refreshInterval * config.TIME_CONSTANTS.MILLISECONDS_PER_SECOND);
|
|
1699
1722
|
}
|
|
1700
1723
|
}
|
|
1701
1724
|
/**
|
|
@@ -1866,31 +1889,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
1866
1889
|
}
|
|
1867
1890
|
}
|
|
1868
1891
|
/**
|
|
1869
|
-
* Updates a flag
|
|
1892
|
+
* Updates a flag in memory.
|
|
1870
1893
|
*
|
|
1871
|
-
* @param
|
|
1872
|
-
* @param value - The new value
|
|
1873
|
-
* @param updateProps - Optional properties to update
|
|
1894
|
+
* @param flagOrKey - Either a complete flag object or a flag key
|
|
1895
|
+
* @param value - The new value (only used when first param is a key)
|
|
1896
|
+
* @param updateProps - Optional properties to update (only used when first param is a key)
|
|
1874
1897
|
*/
|
|
1875
|
-
updateFlag(
|
|
1876
|
-
|
|
1898
|
+
async updateFlag(flagOrKey, value, updateProps) {
|
|
1899
|
+
let key;
|
|
1900
|
+
let updatedFlag;
|
|
1901
|
+
if (typeof flagOrKey === "string") {
|
|
1902
|
+
key = flagOrKey;
|
|
1903
|
+
const existingFlag = this.flags.find((f) => f.key === key);
|
|
1904
|
+
if (!existingFlag) {
|
|
1905
|
+
this.log(`Flag with key ${key} not found in memory`);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (value === void 0) {
|
|
1909
|
+
this.log(`Value is required when updating flag by key`);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
updatedFlag = {
|
|
1913
|
+
...existingFlag,
|
|
1914
|
+
value,
|
|
1915
|
+
type: this.inferFlagType(value),
|
|
1916
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1917
|
+
updatedBy: "memory-runtime",
|
|
1918
|
+
...updateProps
|
|
1919
|
+
};
|
|
1920
|
+
} else {
|
|
1921
|
+
const flag = flagOrKey;
|
|
1922
|
+
key = flag.key;
|
|
1923
|
+
updatedFlag = {
|
|
1924
|
+
...flag,
|
|
1925
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1926
|
+
updatedBy: flag.updatedBy || "memory-runtime"
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
const flagIndex = this.flags.findIndex((f) => f.key === key);
|
|
1877
1930
|
if (flagIndex === -1) {
|
|
1878
1931
|
this.log(`Flag with key ${key} not found in memory`);
|
|
1879
1932
|
return;
|
|
1880
1933
|
}
|
|
1881
|
-
const updatedFlag = {
|
|
1882
|
-
...this.flags[flagIndex],
|
|
1883
|
-
value,
|
|
1884
|
-
type: this.inferFlagType(value),
|
|
1885
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1886
|
-
updatedBy: "memory-runtime",
|
|
1887
|
-
...updateProps
|
|
1888
|
-
};
|
|
1889
1934
|
this.flags[flagIndex] = updatedFlag;
|
|
1890
1935
|
this.engine.setFlags(this.flags);
|
|
1891
1936
|
void this.cacheManager.clear();
|
|
1892
1937
|
this.notifySubscribers();
|
|
1893
|
-
this.log(`Updated flag: ${key}
|
|
1938
|
+
this.log(`Updated flag: ${key}`);
|
|
1894
1939
|
}
|
|
1895
1940
|
/**
|
|
1896
1941
|
* Adds a new flag to memory at runtime.
|
|
@@ -2000,6 +2045,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2000
2045
|
getCurrentRules() {
|
|
2001
2046
|
return [...this.rules];
|
|
2002
2047
|
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Updates the features object and syncs all flags.
|
|
2050
|
+
* This allows updating the FEATURES constant at runtime.
|
|
2051
|
+
*
|
|
2052
|
+
* @param newFeatures - New features object to sync with
|
|
2053
|
+
*/
|
|
2054
|
+
async syncFeatures(newFeatures) {
|
|
2055
|
+
this.log("Syncing with new FEATURES values");
|
|
2056
|
+
this.features = newFeatures;
|
|
2057
|
+
await this.refresh();
|
|
2058
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features`);
|
|
2059
|
+
}
|
|
2003
2060
|
/**
|
|
2004
2061
|
* Resets the memory provider to its initial state.
|
|
2005
2062
|
*/
|
|
@@ -2033,13 +2090,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2033
2090
|
}
|
|
2034
2091
|
}
|
|
2035
2092
|
};
|
|
2036
|
-
|
|
2037
|
-
|
|
2093
|
+
var readFile2 = util.promisify(fs__namespace.readFile);
|
|
2094
|
+
var writeFile2 = util.promisify(fs__namespace.writeFile);
|
|
2095
|
+
var access2 = util.promisify(fs__namespace.access);
|
|
2096
|
+
var mkdir2 = util.promisify(fs__namespace.mkdir);
|
|
2038
2097
|
var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
2039
2098
|
static {
|
|
2040
2099
|
__name(this, "FileFeatureFlagProvider");
|
|
2041
2100
|
}
|
|
2042
2101
|
fileWatcher;
|
|
2102
|
+
lastFileContent;
|
|
2103
|
+
fileCheckInterval;
|
|
2104
|
+
rules = [];
|
|
2043
2105
|
/**
|
|
2044
2106
|
* Creates a new file feature flag provider.
|
|
2045
2107
|
*
|
|
@@ -2049,8 +2111,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2049
2111
|
constructor(config, features) {
|
|
2050
2112
|
super(config, features);
|
|
2051
2113
|
this.validateConfig();
|
|
2052
|
-
|
|
2053
|
-
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Initializes the provider and sets up file watching if enabled.
|
|
2117
|
+
*/
|
|
2118
|
+
async initialize() {
|
|
2119
|
+
await super.initialize();
|
|
2120
|
+
if (this.config.fileConfig?.shouldWatchForChanges) {
|
|
2121
|
+
this.setupFileWatcher();
|
|
2122
|
+
}
|
|
2054
2123
|
}
|
|
2055
2124
|
/**
|
|
2056
2125
|
* Fetches flags and rules from the configuration file.
|
|
@@ -2059,9 +2128,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2059
2128
|
* @returns Promise resolving to flags and rules from file
|
|
2060
2129
|
*/
|
|
2061
2130
|
async fetchData() {
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2131
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2132
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2133
|
+
try {
|
|
2134
|
+
await access2(resolvedPath, fs__namespace.constants.R_OK);
|
|
2135
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2136
|
+
this.lastFileContent = content;
|
|
2137
|
+
const data = await this.parseFileContent(content, format);
|
|
2138
|
+
this.validateFileData(data);
|
|
2139
|
+
this.rules = data.rules || [];
|
|
2140
|
+
return {
|
|
2141
|
+
flags: data.flags || [],
|
|
2142
|
+
rules: data.rules || []
|
|
2143
|
+
};
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
return this.handleFetchDataError(error, resolvedPath, format);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Parses file content based on format.
|
|
2150
|
+
*
|
|
2151
|
+
* @private
|
|
2152
|
+
*/
|
|
2153
|
+
async parseFileContent(content, format) {
|
|
2154
|
+
if (format === "json") {
|
|
2155
|
+
return this.parseJSON(content);
|
|
2156
|
+
} else if (format === "yaml") {
|
|
2157
|
+
return await this.parseYAML(content);
|
|
2158
|
+
}
|
|
2159
|
+
throw new Error(`Unsupported file format: ${format}`);
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Handles errors for fetchData, including file creation and fallback.
|
|
2163
|
+
*
|
|
2164
|
+
* @private
|
|
2165
|
+
*/
|
|
2166
|
+
async handleFetchDataError(error, resolvedPath, format) {
|
|
2167
|
+
const isFileNotFound = this.isFileNotFoundError(error);
|
|
2168
|
+
if (isFileNotFound && this.config.shouldFallbackToDefaults) {
|
|
2169
|
+
return await this.handleFileNotFound(resolvedPath, format);
|
|
2170
|
+
}
|
|
2171
|
+
this.log(`Error reading file ${resolvedPath}:`, error);
|
|
2172
|
+
if (this.config.shouldFallbackToDefaults) {
|
|
2173
|
+
return this.handleFallbackToDefaults();
|
|
2174
|
+
}
|
|
2175
|
+
throw error;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Type guard for NodeJS.ErrnoException.
|
|
2179
|
+
*/
|
|
2180
|
+
isFileNotFoundError(error) {
|
|
2181
|
+
return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Handles the case when the file is not found and fallback is enabled.
|
|
2185
|
+
*/
|
|
2186
|
+
async handleFileNotFound(resolvedPath, format) {
|
|
2187
|
+
this.log(`File not found at ${resolvedPath}, creating with default values`);
|
|
2188
|
+
try {
|
|
2189
|
+
await this.createDefaultFile(resolvedPath, format);
|
|
2190
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2191
|
+
this.lastFileContent = content;
|
|
2192
|
+
const data = await this.parseFileContent(content, format);
|
|
2193
|
+
return {
|
|
2194
|
+
flags: data.flags || [],
|
|
2195
|
+
rules: data.rules || []
|
|
2196
|
+
};
|
|
2197
|
+
} catch (createError) {
|
|
2198
|
+
this.log("Error creating default file:", createError);
|
|
2199
|
+
return {
|
|
2200
|
+
flags: this.createDefaultFlags(),
|
|
2201
|
+
rules: []
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Handles fallback to default flags and rules.
|
|
2207
|
+
*/
|
|
2208
|
+
handleFallbackToDefaults() {
|
|
2209
|
+
this.log("Falling back to default values");
|
|
2210
|
+
return {
|
|
2211
|
+
flags: this.createDefaultFlags(),
|
|
2212
|
+
rules: []
|
|
2213
|
+
};
|
|
2065
2214
|
}
|
|
2066
2215
|
/**
|
|
2067
2216
|
* Validates the file provider configuration.
|
|
@@ -2084,6 +2233,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2084
2233
|
throw new Error('File format must be either "json" or "yaml"');
|
|
2085
2234
|
}
|
|
2086
2235
|
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Resolves the file path, supporting relative and absolute paths.
|
|
2238
|
+
*
|
|
2239
|
+
* @private
|
|
2240
|
+
* @param filePath - The file path from configuration
|
|
2241
|
+
* @returns Resolved absolute file path
|
|
2242
|
+
*/
|
|
2243
|
+
resolveFilePath(filePath) {
|
|
2244
|
+
let pathToResolve = filePath;
|
|
2245
|
+
if (!pathToResolve) {
|
|
2246
|
+
const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2247
|
+
const __dirname = path__namespace.dirname(__filename);
|
|
2248
|
+
pathToResolve = path__namespace.join(__dirname, "../../../config/feature-provider.json");
|
|
2249
|
+
}
|
|
2250
|
+
if (path__namespace.isAbsolute(pathToResolve)) {
|
|
2251
|
+
return pathToResolve;
|
|
2252
|
+
}
|
|
2253
|
+
return path__namespace.resolve(process.cwd(), pathToResolve);
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Parses JSON content.
|
|
2257
|
+
*
|
|
2258
|
+
* @private
|
|
2259
|
+
* @param content - File content
|
|
2260
|
+
* @returns Parsed data
|
|
2261
|
+
*/
|
|
2262
|
+
parseJSON(content) {
|
|
2263
|
+
try {
|
|
2264
|
+
return JSON.parse(content);
|
|
2265
|
+
} catch (error) {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
`Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Parses YAML content.
|
|
2273
|
+
*
|
|
2274
|
+
* @private
|
|
2275
|
+
* @param content - File content
|
|
2276
|
+
* @returns Parsed data
|
|
2277
|
+
*/
|
|
2278
|
+
async parseYAML(content) {
|
|
2279
|
+
try {
|
|
2280
|
+
const data = yaml__namespace.parse(content);
|
|
2281
|
+
return data;
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Validates the structure of file data.
|
|
2290
|
+
*
|
|
2291
|
+
* @private
|
|
2292
|
+
* @param data - Parsed file data
|
|
2293
|
+
*/
|
|
2294
|
+
validateFileData(data) {
|
|
2295
|
+
if (!data || typeof data !== "object") {
|
|
2296
|
+
throw new Error("File must contain a valid object");
|
|
2297
|
+
}
|
|
2298
|
+
const fileData = data;
|
|
2299
|
+
this.checkFlagsArray(fileData.flags);
|
|
2300
|
+
this.checkRulesArray(fileData.rules);
|
|
2301
|
+
}
|
|
2302
|
+
checkFlagsArray(flags) {
|
|
2303
|
+
if (flags && !Array.isArray(flags)) {
|
|
2304
|
+
throw new Error('"flags" must be an array');
|
|
2305
|
+
}
|
|
2306
|
+
if (Array.isArray(flags)) {
|
|
2307
|
+
this.validateFlags(flags);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
checkRulesArray(rules) {
|
|
2311
|
+
if (rules && !Array.isArray(rules)) {
|
|
2312
|
+
throw new Error('"rules" must be an array');
|
|
2313
|
+
}
|
|
2314
|
+
if (Array.isArray(rules)) {
|
|
2315
|
+
this.validateRules(rules);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
validateFlags(flags) {
|
|
2319
|
+
flags.forEach((flag, index) => {
|
|
2320
|
+
if (!flag || typeof flag !== "object") {
|
|
2321
|
+
throw new Error(`Flag at index ${index} must be an object`);
|
|
2322
|
+
}
|
|
2323
|
+
const flagObj = flag;
|
|
2324
|
+
if (!flagObj.key || typeof flagObj.key !== "string") {
|
|
2325
|
+
throw new Error(`Flag at index ${index} must have a "key" property`);
|
|
2326
|
+
}
|
|
2327
|
+
if (flagObj.value === void 0) {
|
|
2328
|
+
throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
validateRules(rules) {
|
|
2333
|
+
rules.forEach((rule, index) => {
|
|
2334
|
+
if (!rule || typeof rule !== "object") {
|
|
2335
|
+
throw new Error(`Rule at index ${index} must be an object`);
|
|
2336
|
+
}
|
|
2337
|
+
const ruleObj = rule;
|
|
2338
|
+
if (!ruleObj.id || typeof ruleObj.id !== "string") {
|
|
2339
|
+
throw new Error(`Rule at index ${index} must have an "id" property`);
|
|
2340
|
+
}
|
|
2341
|
+
if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
|
|
2342
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
|
|
2343
|
+
}
|
|
2344
|
+
if (!Array.isArray(ruleObj.conditions)) {
|
|
2345
|
+
throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Creates a default configuration file with values from features.
|
|
2351
|
+
*
|
|
2352
|
+
* @private
|
|
2353
|
+
* @param filePath - Path where to create the file
|
|
2354
|
+
* @param format - File format (json or yaml)
|
|
2355
|
+
*/
|
|
2356
|
+
async createDefaultFile(filePath, format) {
|
|
2357
|
+
const dir = path__namespace.dirname(filePath);
|
|
2358
|
+
await mkdir2(dir, { recursive: true });
|
|
2359
|
+
const defaultData = {
|
|
2360
|
+
flags: this.createDefaultFlags(),
|
|
2361
|
+
rules: []
|
|
2362
|
+
};
|
|
2363
|
+
let content;
|
|
2364
|
+
if (format === "json") {
|
|
2365
|
+
content = JSON.stringify(defaultData, null, config.FORMAT_CONSTANTS.JSON_INDENT_SPACES);
|
|
2366
|
+
} else {
|
|
2367
|
+
content = yaml__namespace.stringify(defaultData);
|
|
2368
|
+
}
|
|
2369
|
+
await writeFile2(filePath, content, "utf-8");
|
|
2370
|
+
this.log(`Created default feature flag file at: ${filePath}`);
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Creates default flags from the features configuration.
|
|
2374
|
+
*
|
|
2375
|
+
* @private
|
|
2376
|
+
* @returns Array of default flags
|
|
2377
|
+
*/
|
|
2378
|
+
createDefaultFlags() {
|
|
2379
|
+
return Object.entries(this.features).map(([key, value]) => ({
|
|
2380
|
+
key,
|
|
2381
|
+
value,
|
|
2382
|
+
isEnabled: true,
|
|
2383
|
+
name: key,
|
|
2384
|
+
description: `Default flag for ${key}`,
|
|
2385
|
+
type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
|
|
2386
|
+
environment: "development",
|
|
2387
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2388
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2389
|
+
createdBy: "system",
|
|
2390
|
+
updatedBy: "system",
|
|
2391
|
+
metadata: {},
|
|
2392
|
+
tags: []
|
|
2393
|
+
}));
|
|
2394
|
+
}
|
|
2087
2395
|
/**
|
|
2088
2396
|
* Sets up file watching for hot reload if enabled.
|
|
2089
2397
|
*
|
|
@@ -2093,8 +2401,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2093
2401
|
if (!this.config.fileConfig?.shouldWatchForChanges) {
|
|
2094
2402
|
return;
|
|
2095
2403
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2404
|
+
const { filePath } = this.config.fileConfig;
|
|
2405
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2406
|
+
try {
|
|
2407
|
+
this.fileWatcher = fs__namespace.watch(resolvedPath, async (eventType) => {
|
|
2408
|
+
if (eventType === "change") {
|
|
2409
|
+
this.log(`File changed: ${resolvedPath}`);
|
|
2410
|
+
if (this.fileCheckInterval) {
|
|
2411
|
+
clearTimeout(this.fileCheckInterval);
|
|
2412
|
+
}
|
|
2413
|
+
this.fileCheckInterval = setTimeout(async () => {
|
|
2414
|
+
try {
|
|
2415
|
+
const content = await readFile2(resolvedPath, "utf-8");
|
|
2416
|
+
if (content !== this.lastFileContent) {
|
|
2417
|
+
this.log("File content changed, refreshing...");
|
|
2418
|
+
await this.refresh();
|
|
2419
|
+
}
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
this.log("Error reading changed file:", error);
|
|
2422
|
+
}
|
|
2423
|
+
}, this.config.fileConfig?.fileCheckInterval ?? config.FILE_CHECK_INTERVAL_DEFAULT);
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
this.log(`File watching enabled for: ${resolvedPath}`);
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
this.log(`Failed to set up file watching: ${error}`);
|
|
2429
|
+
}
|
|
2098
2430
|
}
|
|
2099
2431
|
/**
|
|
2100
2432
|
* Disposes of the file provider and stops file watching.
|
|
@@ -2102,10 +2434,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2102
2434
|
dispose() {
|
|
2103
2435
|
super.dispose();
|
|
2104
2436
|
if (this.fileWatcher) {
|
|
2105
|
-
|
|
2437
|
+
this.fileWatcher.close();
|
|
2106
2438
|
this.fileWatcher = void 0;
|
|
2107
2439
|
this.log("File watching stopped");
|
|
2108
2440
|
}
|
|
2441
|
+
if (this.fileCheckInterval) {
|
|
2442
|
+
clearTimeout(this.fileCheckInterval);
|
|
2443
|
+
this.fileCheckInterval = void 0;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Refreshes the provider by fetching latest data from the file.
|
|
2448
|
+
* Overrides base class to store rules.
|
|
2449
|
+
*
|
|
2450
|
+
* @returns Promise that resolves when refresh is complete
|
|
2451
|
+
*/
|
|
2452
|
+
async refresh() {
|
|
2453
|
+
await super.refresh();
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Updates the features object and writes to file.
|
|
2457
|
+
* This allows updating the FEATURES at runtime and persisting to file.
|
|
2458
|
+
*
|
|
2459
|
+
* @param newFeatures - New features object to sync with
|
|
2460
|
+
*/
|
|
2461
|
+
async syncFeatures(newFeatures) {
|
|
2462
|
+
this.log("Syncing with new FEATURES values and updating file");
|
|
2463
|
+
this.features = newFeatures;
|
|
2464
|
+
const fileData = {
|
|
2465
|
+
flags: this.createDefaultFlags(),
|
|
2466
|
+
rules: this.rules || []
|
|
2467
|
+
};
|
|
2468
|
+
const { filePath, format } = this.config.fileConfig;
|
|
2469
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
2470
|
+
try {
|
|
2471
|
+
let content;
|
|
2472
|
+
if (format === "json") {
|
|
2473
|
+
content = JSON.stringify(fileData, null, config.FORMAT_CONSTANTS.JSON_INDENT_SPACES);
|
|
2474
|
+
} else {
|
|
2475
|
+
content = yaml__namespace.stringify(fileData);
|
|
2476
|
+
}
|
|
2477
|
+
await writeFile2(resolvedPath, content, "utf-8");
|
|
2478
|
+
this.lastFileContent = content;
|
|
2479
|
+
this.engine.updateDefaults(newFeatures);
|
|
2480
|
+
await this.refresh();
|
|
2481
|
+
this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
this.log("Error syncing features to file:", error);
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2109
2488
|
}
|
|
2110
2489
|
/**
|
|
2111
2490
|
* Gets information about the file provider.
|
|
@@ -2113,18 +2492,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
|
|
|
2113
2492
|
* @returns File provider information
|
|
2114
2493
|
*/
|
|
2115
2494
|
getFileInfo() {
|
|
2495
|
+
const filePath = this.config.fileConfig?.filePath;
|
|
2496
|
+
const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
|
|
2497
|
+
let lastModified;
|
|
2498
|
+
if (resolvedPath) {
|
|
2499
|
+
try {
|
|
2500
|
+
const stats = fs__namespace.statSync(resolvedPath);
|
|
2501
|
+
lastModified = stats.mtime;
|
|
2502
|
+
} catch {
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2116
2505
|
return {
|
|
2117
|
-
filePath
|
|
2506
|
+
filePath,
|
|
2507
|
+
resolvedPath,
|
|
2118
2508
|
format: this.config.fileConfig?.format,
|
|
2119
2509
|
isWatchEnabled: Boolean(this.config.fileConfig?.shouldWatchForChanges),
|
|
2120
|
-
isImplemented:
|
|
2121
|
-
|
|
2122
|
-
"File reading and parsing logic",
|
|
2123
|
-
"YAML and JSON format support",
|
|
2124
|
-
"File watching for hot reload",
|
|
2125
|
-
"Standard file path configuration",
|
|
2126
|
-
"File validation and error handling"
|
|
2127
|
-
]
|
|
2510
|
+
isImplemented: true,
|
|
2511
|
+
lastModified
|
|
2128
2512
|
};
|
|
2129
2513
|
}
|
|
2130
2514
|
};
|
|
@@ -2437,8 +2821,6 @@ Examples:
|
|
|
2437
2821
|
};
|
|
2438
2822
|
}
|
|
2439
2823
|
};
|
|
2440
|
-
|
|
2441
|
-
// src/domain/featureFlags/providers/factory.ts
|
|
2442
2824
|
var PROVIDER_REGISTRY = {
|
|
2443
2825
|
memory: MemoryFeatureFlagProvider,
|
|
2444
2826
|
file: FileFeatureFlagProvider,
|
|
@@ -2541,25 +2923,69 @@ var FeatureFlagProviderFactory = class {
|
|
|
2541
2923
|
};
|
|
2542
2924
|
}
|
|
2543
2925
|
/**
|
|
2544
|
-
* Creates a default
|
|
2926
|
+
* Creates a default provider.
|
|
2927
|
+
* Uses file provider if features are not provided, memory provider otherwise.
|
|
2545
2928
|
*
|
|
2546
|
-
* @param features - Record of feature flag keys to their default values
|
|
2929
|
+
* @param features - Record of feature flag keys to their default values (optional)
|
|
2547
2930
|
* @param overrides - Optional configuration overrides
|
|
2548
|
-
* @returns
|
|
2931
|
+
* @returns Provider instance
|
|
2549
2932
|
*/
|
|
2550
2933
|
static createDefault(features, overrides) {
|
|
2934
|
+
const provider = features ? "memory" : "file";
|
|
2551
2935
|
const defaultConfig = {
|
|
2552
|
-
provider
|
|
2936
|
+
provider,
|
|
2553
2937
|
isCacheEnabled: true,
|
|
2554
|
-
cacheTtl:
|
|
2555
|
-
// 5 minutes
|
|
2938
|
+
cacheTtl: config.FEATURE_FLAG_CACHE_TTL_DEFAULT,
|
|
2556
2939
|
refreshInterval: 0,
|
|
2557
2940
|
// No auto-refresh
|
|
2558
2941
|
isLoggingEnabled: false,
|
|
2559
2942
|
shouldFallbackToDefaults: true,
|
|
2560
2943
|
...overrides
|
|
2561
2944
|
};
|
|
2562
|
-
|
|
2945
|
+
if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
|
|
2946
|
+
defaultConfig.fileConfig = {
|
|
2947
|
+
filePath: config.FEATURE_FLAG_FILE_PATHS.DEFAULT,
|
|
2948
|
+
format: "json",
|
|
2949
|
+
shouldWatchForChanges: false
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
return this.create(defaultConfig, features ?? {});
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Type guard to check if a provider supports feature syncing.
|
|
2956
|
+
*
|
|
2957
|
+
* @param provider - The provider instance to check
|
|
2958
|
+
* @returns True if provider has syncFeatures method
|
|
2959
|
+
*/
|
|
2960
|
+
static isSyncableProvider(provider) {
|
|
2961
|
+
return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Updates features on a provider if it supports the syncFeatures method.
|
|
2965
|
+
* This is useful for providers like MemoryProvider that can update their features at runtime.
|
|
2966
|
+
*
|
|
2967
|
+
* @param provider - The provider instance
|
|
2968
|
+
* @param newFeatures - New features to sync
|
|
2969
|
+
* @returns Promise that resolves when sync is complete
|
|
2970
|
+
* @throws Error if provider doesn't support feature syncing
|
|
2971
|
+
*/
|
|
2972
|
+
static async syncFeatures(provider, newFeatures) {
|
|
2973
|
+
if (this.isSyncableProvider(provider)) {
|
|
2974
|
+
await provider.syncFeatures(newFeatures);
|
|
2975
|
+
} else {
|
|
2976
|
+
throw new Error(
|
|
2977
|
+
`Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Checks if a provider supports feature syncing.
|
|
2983
|
+
*
|
|
2984
|
+
* @param provider - The provider instance to check
|
|
2985
|
+
* @returns True if provider supports syncFeatures method
|
|
2986
|
+
*/
|
|
2987
|
+
static supportsFeaturesSync(provider) {
|
|
2988
|
+
return this.isSyncableProvider(provider);
|
|
2563
2989
|
}
|
|
2564
2990
|
/**
|
|
2565
2991
|
* Validates provider configuration before instantiation.
|
|
@@ -2612,8 +3038,11 @@ var FeatureFlagProviderFactory = class {
|
|
|
2612
3038
|
validator();
|
|
2613
3039
|
}
|
|
2614
3040
|
static validateFileConfig(config) {
|
|
2615
|
-
if (
|
|
2616
|
-
|
|
3041
|
+
if (config.fileConfig) {
|
|
3042
|
+
const { format } = config.fileConfig;
|
|
3043
|
+
if (format && !["json", "yaml"].includes(format)) {
|
|
3044
|
+
throw new Error('File format must be either "json" or "yaml"');
|
|
3045
|
+
}
|
|
2617
3046
|
}
|
|
2618
3047
|
}
|
|
2619
3048
|
static validateRedisConfig(config) {
|