@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/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 >= 100) return true;
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 % 100 < percentage;
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 = 100) => {
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 <= 100;
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, path, defaultValue) => {
212
+ getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
190
213
  if (!obj || typeof obj !== "object") return defaultValue;
191
- const keys = path.split(".");
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 !== 2) {
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
- ruleId: rule.id,
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: DEFAULT_MAX_ENTRIES,
1007
- cleanupInterval: DEFAULT_CLEANUP_INTERVAL
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 / 1e3));
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((resolve, reject) => {
1323
- client.on("ready", resolve);
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 * 1e3,
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 * 1e3);
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 value in memory.
1892
+ * Updates a flag in memory.
1870
1893
  *
1871
- * @param key - The flag key to update
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(key, value, updateProps) {
1876
- const flagIndex = this.flags.findIndex((flag) => flag.key === key);
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} with new value:`, value);
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
- // src/domain/featureFlags/providers/file.ts
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
- this.setupFileWatcher();
2053
- throw new Error("File provider implementation coming soon");
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
- throw new Error(
2063
- 'File Provider is not yet fully implemented. This requires dedicated file paths to be configured in @plyaz/core.\n\nRequired Implementation:\n1. Define standard file paths for feature flags\n2. Implement file reading and parsing logic\n3. Add YAML and JSON format support\n4. Set up file watching for hot reload\n5. Add file validation and error handling\n\nRecommended File Structure:\n@plyaz/core/config/\n├── feature-flags.json\n├── feature-flags.yaml\n├── rules/\n│ ├── targeting-rules.json\n│ └── rollout-rules.json\n└── environments/\n ├── development.json\n ├── staging.json\n └── production.json\n\nExample Configuration:\n{\n provider: "file",\n fileConfig: {\n filePath: "@plyaz/core/config/feature-flags.json",\n format: "json",\n shouldWatchForChanges: true\n },\n isCacheEnabled: true,\n cacheTtl: 60\n}\n\nNote: Default provider should be memory for testing environments.\nFile provider is intended for configuration-driven deployments.\n\nMigration Note: This provider will be moved to @plyaz/core/src/domain/featureFlags/providers/'
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
- this.log("File watching would be enabled for hot reload");
2097
- this.log("File path:", this.config.fileConfig.filePath);
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
- clearInterval(this.fileWatcher);
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: this.config.fileConfig?.filePath,
2506
+ filePath,
2507
+ resolvedPath,
2118
2508
  format: this.config.fileConfig?.format,
2119
2509
  isWatchEnabled: Boolean(this.config.fileConfig?.shouldWatchForChanges),
2120
- isImplemented: false,
2121
- requiredImplementation: [
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 memory provider for testing environments.
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 Memory provider instance
2931
+ * @returns Provider instance
2549
2932
  */
2550
2933
  static createDefault(features, overrides) {
2934
+ const provider = features ? "memory" : "file";
2551
2935
  const defaultConfig = {
2552
- provider: "memory",
2936
+ provider,
2553
2937
  isCacheEnabled: true,
2554
- cacheTtl: 300,
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
- return this.create(defaultConfig, features);
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 (!config.fileConfig) {
2616
- throw new Error("File configuration is required for file provider");
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) {