@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.mjs CHANGED
@@ -1,4 +1,9 @@
1
- import { FEATURES } from '@plyaz/config';
1
+ import { CACHE_MAX_SIZE_DEFAULT, CACHE_CLEANUP_INTERVAL_DEFAULT, TIME_CONSTANTS, FORMAT_CONSTANTS, FILE_CHECK_INTERVAL_DEFAULT, FEATURE_FLAG_FILE_PATHS, FEATURE_FLAG_CACHE_TTL_DEFAULT, MATH_CONSTANTS, ISO_STANDARDS, FEATURES } from '@plyaz/config';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { promisify } from 'util';
5
+ import { fileURLToPath } from 'url';
6
+ import * as yaml from 'yaml';
2
7
  import { Post, Param, Body, Put, Delete, Get, Query, Controller, Injectable, Global, Module, HttpException, HttpStatus, Logger } from '@nestjs/common';
3
8
  import React, { createContext, useState, useRef, useCallback, useEffect, useContext, useMemo } from 'react';
4
9
  import { jsx } from 'react/jsx-runtime';
@@ -18,8 +23,6 @@ var __decorateClass = (decorators, target, key, kind) => {
18
23
  };
19
24
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
20
25
  var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
21
-
22
- // src/utils/common/hash.ts
23
26
  function hashString(str) {
24
27
  const HASH_SHIFT = 5;
25
28
  let hash = 0;
@@ -32,10 +35,10 @@ function hashString(str) {
32
35
  }
33
36
  __name(hashString, "hashString");
34
37
  function isInRollout(identifier, percentage) {
35
- if (percentage >= 100) return true;
38
+ if (percentage >= MATH_CONSTANTS.PERCENTAGE_MAX) return true;
36
39
  if (percentage <= 0) return false;
37
40
  const hash = hashString(identifier);
38
- return hash % 100 < percentage;
41
+ return hash % MATH_CONSTANTS.PERCENTAGE_MAX < percentage;
39
42
  }
40
43
  __name(isInRollout, "isInRollout");
41
44
  function createRolloutIdentifier(featureKey, userId) {
@@ -64,7 +67,7 @@ var HashUtils = {
64
67
  * @param totalBuckets - Total number of buckets (default: 100)
65
68
  * @returns true if identifier is in the bucket range
66
69
  */
67
- isInBucketRange: /* @__PURE__ */ __name((identifier, startBucket, endBucket, totalBuckets = 100) => {
70
+ isInBucketRange: /* @__PURE__ */ __name((identifier, startBucket, endBucket, totalBuckets = MATH_CONSTANTS.PERCENTAGE_MAX) => {
68
71
  const bucket = hashString(identifier) % totalBuckets;
69
72
  return bucket >= startBucket && bucket <= endBucket;
70
73
  }, "isInBucketRange"),
@@ -79,8 +82,6 @@ var HashUtils = {
79
82
  return hashString(str) % SAFE_INT;
80
83
  }, "createSeed")
81
84
  };
82
-
83
- // src/utils/common/values.ts
84
85
  function isStringFalsy(value) {
85
86
  if (value === "") return true;
86
87
  const lower = value.toLowerCase().trim();
@@ -146,7 +147,7 @@ var ValueUtils = {
146
147
  */
147
148
  isValidPercentage: /* @__PURE__ */ __name((value) => {
148
149
  if (typeof value !== "number") return false;
149
- return !isNaN(value) && isFinite(value) && value >= 0 && value <= 100;
150
+ return !isNaN(value) && isFinite(value) && value >= 0 && value <= MATH_CONSTANTS.PERCENTAGE_MAX;
150
151
  }, "isValidPercentage"),
151
152
  /**
152
153
  * Clamps a number to a specific range.
@@ -180,9 +181,9 @@ var ValueUtils = {
180
181
  * @param defaultValue - Default if path doesn't exist
181
182
  * @returns Property value or default
182
183
  */
183
- getNestedProperty: /* @__PURE__ */ __name((obj, path, defaultValue) => {
184
+ getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
184
185
  if (!obj || typeof obj !== "object") return defaultValue;
185
- const keys = path.split(".");
186
+ const keys = path2.split(".");
186
187
  let current = obj;
187
188
  for (const key of keys) {
188
189
  if (current == null || typeof current !== "object") return defaultValue;
@@ -192,8 +193,6 @@ var ValueUtils = {
192
193
  return current;
193
194
  }, "getNestedProperty")
194
195
  };
195
-
196
- // src/utils/featureFlags/context.ts
197
196
  var FeatureFlagContextBuilder = class _FeatureFlagContextBuilder {
198
197
  static {
199
198
  __name(this, "FeatureFlagContextBuilder");
@@ -389,7 +388,7 @@ var ContextUtils = {
389
388
  if (context.platform && !["web", "mobile", "desktop"].includes(context.platform)) {
390
389
  errors.push("Platform must be web, mobile, or desktop");
391
390
  }
392
- if (context.country && context.country.length !== 2) {
391
+ if (context.country && context.country.length !== ISO_STANDARDS.ISO_COUNTRY_CODE_LENGTH) {
393
392
  errors.push("Country must be a 2-letter ISO country code");
394
393
  }
395
394
  return {
@@ -750,6 +749,16 @@ var FeatureFlagEngine = class {
750
749
  this.overrides.delete(key);
751
750
  this.log("Override removed:", key);
752
751
  }
752
+ /**
753
+ * Updates the default values for feature flags.
754
+ * This is useful when the FEATURES constant is updated at runtime.
755
+ *
756
+ * @param newDefaults - New default values
757
+ */
758
+ updateDefaults(newDefaults) {
759
+ this.defaults = newDefaults;
760
+ this.log("Updated default feature values");
761
+ }
753
762
  /**
754
763
  * Clears all manual overrides.
755
764
  */
@@ -927,7 +936,7 @@ var FeatureFlagEngine = class {
927
936
  value: rule.value,
928
937
  isEnabled: isTruthy(rule.value),
929
938
  reason: "rule_match",
930
- ruleId: rule.id,
939
+ matchedRuleId: rule.id,
931
940
  evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
932
941
  };
933
942
  }
@@ -970,8 +979,6 @@ var FeatureFlagEngine = class {
970
979
  }
971
980
  }
972
981
  };
973
-
974
- // src/base/cache/strategies/memory.ts
975
982
  var MemoryCacheStrategy = class {
976
983
  static {
977
984
  __name(this, "MemoryCacheStrategy");
@@ -994,11 +1001,9 @@ var MemoryCacheStrategy = class {
994
1001
  * @param config - Memory cache configuration
995
1002
  */
996
1003
  constructor(config = {}) {
997
- const DEFAULT_MAX_ENTRIES = 1e3;
998
- const DEFAULT_CLEANUP_INTERVAL = 6e4;
999
1004
  const defaultConfig = {
1000
- maxEntries: DEFAULT_MAX_ENTRIES,
1001
- cleanupInterval: DEFAULT_CLEANUP_INTERVAL
1005
+ maxEntries: CACHE_MAX_SIZE_DEFAULT,
1006
+ cleanupInterval: CACHE_CLEANUP_INTERVAL_DEFAULT
1002
1007
  };
1003
1008
  this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
1004
1009
  this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
@@ -1150,8 +1155,6 @@ var MemoryCacheStrategy = class {
1150
1155
  }
1151
1156
  }
1152
1157
  };
1153
-
1154
- // src/base/cache/strategies/redis.ts
1155
1158
  var RedisCacheStrategy = class {
1156
1159
  /**
1157
1160
  * Creates a new Redis cache strategy.
@@ -1189,7 +1192,7 @@ var RedisCacheStrategy = class {
1189
1192
  const redisKey = this.buildRedisKey(key);
1190
1193
  const serializedEntry = JSON.stringify(entry);
1191
1194
  const ttlMs = entry.expiresAt - Date.now();
1192
- const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
1195
+ const ttlSeconds = Math.max(1, Math.ceil(ttlMs / TIME_CONSTANTS.MILLISECONDS_PER_SECOND));
1193
1196
  await this.client.set(redisKey, serializedEntry, "EX", ttlSeconds);
1194
1197
  this.stats.setCount++;
1195
1198
  }
@@ -1313,8 +1316,8 @@ var RedisCacheStrategy = class {
1313
1316
  commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
1314
1317
  enableOfflineQueue: defaultOptions.enableOfflineQueue
1315
1318
  });
1316
- await new Promise((resolve, reject) => {
1317
- client.on("ready", resolve);
1319
+ await new Promise((resolve2, reject) => {
1320
+ client.on("ready", resolve2);
1318
1321
  client.on("error", reject);
1319
1322
  });
1320
1323
  return client;
@@ -1360,7 +1363,7 @@ var CacheManager = class {
1360
1363
  const finalTtl = ttl ?? this.config.ttl;
1361
1364
  const entry = {
1362
1365
  data: value,
1363
- expiresAt: Date.now() + finalTtl * 1e3,
1366
+ expiresAt: Date.now() + finalTtl * TIME_CONSTANTS.MILLISECONDS_PER_SECOND,
1364
1367
  createdAt: Date.now()
1365
1368
  };
1366
1369
  await this.strategy.set(key, entry);
@@ -1453,8 +1456,6 @@ var CacheManager = class {
1453
1456
  await this.strategy.dispose?.();
1454
1457
  }
1455
1458
  };
1456
-
1457
- // src/domain/featureFlags/provider.ts
1458
1459
  var FeatureFlagProvider = class {
1459
1460
  /**
1460
1461
  * Creates a new feature flag provider.
@@ -1689,7 +1690,7 @@ var FeatureFlagProvider = class {
1689
1690
  void this.refresh().catch((error) => {
1690
1691
  this.log("Auto-refresh failed:", error);
1691
1692
  });
1692
- }, this.config.refreshInterval * 1e3);
1693
+ }, this.config.refreshInterval * TIME_CONSTANTS.MILLISECONDS_PER_SECOND);
1693
1694
  }
1694
1695
  }
1695
1696
  /**
@@ -1860,31 +1861,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
1860
1861
  }
1861
1862
  }
1862
1863
  /**
1863
- * Updates a flag value in memory.
1864
+ * Updates a flag in memory.
1864
1865
  *
1865
- * @param key - The flag key to update
1866
- * @param value - The new value
1867
- * @param updateProps - Optional properties to update
1866
+ * @param flagOrKey - Either a complete flag object or a flag key
1867
+ * @param value - The new value (only used when first param is a key)
1868
+ * @param updateProps - Optional properties to update (only used when first param is a key)
1868
1869
  */
1869
- updateFlag(key, value, updateProps) {
1870
- const flagIndex = this.flags.findIndex((flag) => flag.key === key);
1870
+ async updateFlag(flagOrKey, value, updateProps) {
1871
+ let key;
1872
+ let updatedFlag;
1873
+ if (typeof flagOrKey === "string") {
1874
+ key = flagOrKey;
1875
+ const existingFlag = this.flags.find((f) => f.key === key);
1876
+ if (!existingFlag) {
1877
+ this.log(`Flag with key ${key} not found in memory`);
1878
+ return;
1879
+ }
1880
+ if (value === void 0) {
1881
+ this.log(`Value is required when updating flag by key`);
1882
+ return;
1883
+ }
1884
+ updatedFlag = {
1885
+ ...existingFlag,
1886
+ value,
1887
+ type: this.inferFlagType(value),
1888
+ updatedAt: /* @__PURE__ */ new Date(),
1889
+ updatedBy: "memory-runtime",
1890
+ ...updateProps
1891
+ };
1892
+ } else {
1893
+ const flag = flagOrKey;
1894
+ key = flag.key;
1895
+ updatedFlag = {
1896
+ ...flag,
1897
+ updatedAt: /* @__PURE__ */ new Date(),
1898
+ updatedBy: flag.updatedBy || "memory-runtime"
1899
+ };
1900
+ }
1901
+ const flagIndex = this.flags.findIndex((f) => f.key === key);
1871
1902
  if (flagIndex === -1) {
1872
1903
  this.log(`Flag with key ${key} not found in memory`);
1873
1904
  return;
1874
1905
  }
1875
- const updatedFlag = {
1876
- ...this.flags[flagIndex],
1877
- value,
1878
- type: this.inferFlagType(value),
1879
- updatedAt: /* @__PURE__ */ new Date(),
1880
- updatedBy: "memory-runtime",
1881
- ...updateProps
1882
- };
1883
1906
  this.flags[flagIndex] = updatedFlag;
1884
1907
  this.engine.setFlags(this.flags);
1885
1908
  void this.cacheManager.clear();
1886
1909
  this.notifySubscribers();
1887
- this.log(`Updated flag: ${key} with new value:`, value);
1910
+ this.log(`Updated flag: ${key}`);
1888
1911
  }
1889
1912
  /**
1890
1913
  * Adds a new flag to memory at runtime.
@@ -1994,6 +2017,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
1994
2017
  getCurrentRules() {
1995
2018
  return [...this.rules];
1996
2019
  }
2020
+ /**
2021
+ * Updates the features object and syncs all flags.
2022
+ * This allows updating the FEATURES constant at runtime.
2023
+ *
2024
+ * @param newFeatures - New features object to sync with
2025
+ */
2026
+ async syncFeatures(newFeatures) {
2027
+ this.log("Syncing with new FEATURES values");
2028
+ this.features = newFeatures;
2029
+ await this.refresh();
2030
+ this.log(`Synced ${Object.keys(newFeatures).length} features`);
2031
+ }
1997
2032
  /**
1998
2033
  * Resets the memory provider to its initial state.
1999
2034
  */
@@ -2027,13 +2062,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
2027
2062
  }
2028
2063
  }
2029
2064
  };
2030
-
2031
- // src/domain/featureFlags/providers/file.ts
2065
+ var readFile2 = promisify(fs.readFile);
2066
+ var writeFile2 = promisify(fs.writeFile);
2067
+ var access2 = promisify(fs.access);
2068
+ var mkdir2 = promisify(fs.mkdir);
2032
2069
  var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2033
2070
  static {
2034
2071
  __name(this, "FileFeatureFlagProvider");
2035
2072
  }
2036
2073
  fileWatcher;
2074
+ lastFileContent;
2075
+ fileCheckInterval;
2076
+ rules = [];
2037
2077
  /**
2038
2078
  * Creates a new file feature flag provider.
2039
2079
  *
@@ -2043,8 +2083,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2043
2083
  constructor(config, features) {
2044
2084
  super(config, features);
2045
2085
  this.validateConfig();
2046
- this.setupFileWatcher();
2047
- throw new Error("File provider implementation coming soon");
2086
+ }
2087
+ /**
2088
+ * Initializes the provider and sets up file watching if enabled.
2089
+ */
2090
+ async initialize() {
2091
+ await super.initialize();
2092
+ if (this.config.fileConfig?.shouldWatchForChanges) {
2093
+ this.setupFileWatcher();
2094
+ }
2048
2095
  }
2049
2096
  /**
2050
2097
  * Fetches flags and rules from the configuration file.
@@ -2053,9 +2100,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2053
2100
  * @returns Promise resolving to flags and rules from file
2054
2101
  */
2055
2102
  async fetchData() {
2056
- throw new Error(
2057
- '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/'
2058
- );
2103
+ const { filePath, format } = this.config.fileConfig;
2104
+ const resolvedPath = this.resolveFilePath(filePath);
2105
+ try {
2106
+ await access2(resolvedPath, fs.constants.R_OK);
2107
+ const content = await readFile2(resolvedPath, "utf-8");
2108
+ this.lastFileContent = content;
2109
+ const data = await this.parseFileContent(content, format);
2110
+ this.validateFileData(data);
2111
+ this.rules = data.rules || [];
2112
+ return {
2113
+ flags: data.flags || [],
2114
+ rules: data.rules || []
2115
+ };
2116
+ } catch (error) {
2117
+ return this.handleFetchDataError(error, resolvedPath, format);
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Parses file content based on format.
2122
+ *
2123
+ * @private
2124
+ */
2125
+ async parseFileContent(content, format) {
2126
+ if (format === "json") {
2127
+ return this.parseJSON(content);
2128
+ } else if (format === "yaml") {
2129
+ return await this.parseYAML(content);
2130
+ }
2131
+ throw new Error(`Unsupported file format: ${format}`);
2132
+ }
2133
+ /**
2134
+ * Handles errors for fetchData, including file creation and fallback.
2135
+ *
2136
+ * @private
2137
+ */
2138
+ async handleFetchDataError(error, resolvedPath, format) {
2139
+ const isFileNotFound = this.isFileNotFoundError(error);
2140
+ if (isFileNotFound && this.config.shouldFallbackToDefaults) {
2141
+ return await this.handleFileNotFound(resolvedPath, format);
2142
+ }
2143
+ this.log(`Error reading file ${resolvedPath}:`, error);
2144
+ if (this.config.shouldFallbackToDefaults) {
2145
+ return this.handleFallbackToDefaults();
2146
+ }
2147
+ throw error;
2148
+ }
2149
+ /**
2150
+ * Type guard for NodeJS.ErrnoException.
2151
+ */
2152
+ isFileNotFoundError(error) {
2153
+ return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
2154
+ }
2155
+ /**
2156
+ * Handles the case when the file is not found and fallback is enabled.
2157
+ */
2158
+ async handleFileNotFound(resolvedPath, format) {
2159
+ this.log(`File not found at ${resolvedPath}, creating with default values`);
2160
+ try {
2161
+ await this.createDefaultFile(resolvedPath, format);
2162
+ const content = await readFile2(resolvedPath, "utf-8");
2163
+ this.lastFileContent = content;
2164
+ const data = await this.parseFileContent(content, format);
2165
+ return {
2166
+ flags: data.flags || [],
2167
+ rules: data.rules || []
2168
+ };
2169
+ } catch (createError) {
2170
+ this.log("Error creating default file:", createError);
2171
+ return {
2172
+ flags: this.createDefaultFlags(),
2173
+ rules: []
2174
+ };
2175
+ }
2176
+ }
2177
+ /**
2178
+ * Handles fallback to default flags and rules.
2179
+ */
2180
+ handleFallbackToDefaults() {
2181
+ this.log("Falling back to default values");
2182
+ return {
2183
+ flags: this.createDefaultFlags(),
2184
+ rules: []
2185
+ };
2059
2186
  }
2060
2187
  /**
2061
2188
  * Validates the file provider configuration.
@@ -2078,6 +2205,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2078
2205
  throw new Error('File format must be either "json" or "yaml"');
2079
2206
  }
2080
2207
  }
2208
+ /**
2209
+ * Resolves the file path, supporting relative and absolute paths.
2210
+ *
2211
+ * @private
2212
+ * @param filePath - The file path from configuration
2213
+ * @returns Resolved absolute file path
2214
+ */
2215
+ resolveFilePath(filePath) {
2216
+ let pathToResolve = filePath;
2217
+ if (!pathToResolve) {
2218
+ const __filename = fileURLToPath(import.meta.url);
2219
+ const __dirname = path.dirname(__filename);
2220
+ pathToResolve = path.join(__dirname, "../../../config/feature-provider.json");
2221
+ }
2222
+ if (path.isAbsolute(pathToResolve)) {
2223
+ return pathToResolve;
2224
+ }
2225
+ return path.resolve(process.cwd(), pathToResolve);
2226
+ }
2227
+ /**
2228
+ * Parses JSON content.
2229
+ *
2230
+ * @private
2231
+ * @param content - File content
2232
+ * @returns Parsed data
2233
+ */
2234
+ parseJSON(content) {
2235
+ try {
2236
+ return JSON.parse(content);
2237
+ } catch (error) {
2238
+ throw new Error(
2239
+ `Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
2240
+ );
2241
+ }
2242
+ }
2243
+ /**
2244
+ * Parses YAML content.
2245
+ *
2246
+ * @private
2247
+ * @param content - File content
2248
+ * @returns Parsed data
2249
+ */
2250
+ async parseYAML(content) {
2251
+ try {
2252
+ const data = yaml.parse(content);
2253
+ return data;
2254
+ } catch (error) {
2255
+ throw new Error(
2256
+ `Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
2257
+ );
2258
+ }
2259
+ }
2260
+ /**
2261
+ * Validates the structure of file data.
2262
+ *
2263
+ * @private
2264
+ * @param data - Parsed file data
2265
+ */
2266
+ validateFileData(data) {
2267
+ if (!data || typeof data !== "object") {
2268
+ throw new Error("File must contain a valid object");
2269
+ }
2270
+ const fileData = data;
2271
+ this.checkFlagsArray(fileData.flags);
2272
+ this.checkRulesArray(fileData.rules);
2273
+ }
2274
+ checkFlagsArray(flags) {
2275
+ if (flags && !Array.isArray(flags)) {
2276
+ throw new Error('"flags" must be an array');
2277
+ }
2278
+ if (Array.isArray(flags)) {
2279
+ this.validateFlags(flags);
2280
+ }
2281
+ }
2282
+ checkRulesArray(rules) {
2283
+ if (rules && !Array.isArray(rules)) {
2284
+ throw new Error('"rules" must be an array');
2285
+ }
2286
+ if (Array.isArray(rules)) {
2287
+ this.validateRules(rules);
2288
+ }
2289
+ }
2290
+ validateFlags(flags) {
2291
+ flags.forEach((flag, index) => {
2292
+ if (!flag || typeof flag !== "object") {
2293
+ throw new Error(`Flag at index ${index} must be an object`);
2294
+ }
2295
+ const flagObj = flag;
2296
+ if (!flagObj.key || typeof flagObj.key !== "string") {
2297
+ throw new Error(`Flag at index ${index} must have a "key" property`);
2298
+ }
2299
+ if (flagObj.value === void 0) {
2300
+ throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
2301
+ }
2302
+ });
2303
+ }
2304
+ validateRules(rules) {
2305
+ rules.forEach((rule, index) => {
2306
+ if (!rule || typeof rule !== "object") {
2307
+ throw new Error(`Rule at index ${index} must be an object`);
2308
+ }
2309
+ const ruleObj = rule;
2310
+ if (!ruleObj.id || typeof ruleObj.id !== "string") {
2311
+ throw new Error(`Rule at index ${index} must have an "id" property`);
2312
+ }
2313
+ if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
2314
+ throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
2315
+ }
2316
+ if (!Array.isArray(ruleObj.conditions)) {
2317
+ throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
2318
+ }
2319
+ });
2320
+ }
2321
+ /**
2322
+ * Creates a default configuration file with values from features.
2323
+ *
2324
+ * @private
2325
+ * @param filePath - Path where to create the file
2326
+ * @param format - File format (json or yaml)
2327
+ */
2328
+ async createDefaultFile(filePath, format) {
2329
+ const dir = path.dirname(filePath);
2330
+ await mkdir2(dir, { recursive: true });
2331
+ const defaultData = {
2332
+ flags: this.createDefaultFlags(),
2333
+ rules: []
2334
+ };
2335
+ let content;
2336
+ if (format === "json") {
2337
+ content = JSON.stringify(defaultData, null, FORMAT_CONSTANTS.JSON_INDENT_SPACES);
2338
+ } else {
2339
+ content = yaml.stringify(defaultData);
2340
+ }
2341
+ await writeFile2(filePath, content, "utf-8");
2342
+ this.log(`Created default feature flag file at: ${filePath}`);
2343
+ }
2344
+ /**
2345
+ * Creates default flags from the features configuration.
2346
+ *
2347
+ * @private
2348
+ * @returns Array of default flags
2349
+ */
2350
+ createDefaultFlags() {
2351
+ return Object.entries(this.features).map(([key, value]) => ({
2352
+ key,
2353
+ value,
2354
+ isEnabled: true,
2355
+ name: key,
2356
+ description: `Default flag for ${key}`,
2357
+ type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
2358
+ environment: "development",
2359
+ createdAt: /* @__PURE__ */ new Date(),
2360
+ updatedAt: /* @__PURE__ */ new Date(),
2361
+ createdBy: "system",
2362
+ updatedBy: "system",
2363
+ metadata: {},
2364
+ tags: []
2365
+ }));
2366
+ }
2081
2367
  /**
2082
2368
  * Sets up file watching for hot reload if enabled.
2083
2369
  *
@@ -2087,8 +2373,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2087
2373
  if (!this.config.fileConfig?.shouldWatchForChanges) {
2088
2374
  return;
2089
2375
  }
2090
- this.log("File watching would be enabled for hot reload");
2091
- this.log("File path:", this.config.fileConfig.filePath);
2376
+ const { filePath } = this.config.fileConfig;
2377
+ const resolvedPath = this.resolveFilePath(filePath);
2378
+ try {
2379
+ this.fileWatcher = fs.watch(resolvedPath, async (eventType) => {
2380
+ if (eventType === "change") {
2381
+ this.log(`File changed: ${resolvedPath}`);
2382
+ if (this.fileCheckInterval) {
2383
+ clearTimeout(this.fileCheckInterval);
2384
+ }
2385
+ this.fileCheckInterval = setTimeout(async () => {
2386
+ try {
2387
+ const content = await readFile2(resolvedPath, "utf-8");
2388
+ if (content !== this.lastFileContent) {
2389
+ this.log("File content changed, refreshing...");
2390
+ await this.refresh();
2391
+ }
2392
+ } catch (error) {
2393
+ this.log("Error reading changed file:", error);
2394
+ }
2395
+ }, this.config.fileConfig?.fileCheckInterval ?? FILE_CHECK_INTERVAL_DEFAULT);
2396
+ }
2397
+ });
2398
+ this.log(`File watching enabled for: ${resolvedPath}`);
2399
+ } catch (error) {
2400
+ this.log(`Failed to set up file watching: ${error}`);
2401
+ }
2092
2402
  }
2093
2403
  /**
2094
2404
  * Disposes of the file provider and stops file watching.
@@ -2096,10 +2406,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2096
2406
  dispose() {
2097
2407
  super.dispose();
2098
2408
  if (this.fileWatcher) {
2099
- clearInterval(this.fileWatcher);
2409
+ this.fileWatcher.close();
2100
2410
  this.fileWatcher = void 0;
2101
2411
  this.log("File watching stopped");
2102
2412
  }
2413
+ if (this.fileCheckInterval) {
2414
+ clearTimeout(this.fileCheckInterval);
2415
+ this.fileCheckInterval = void 0;
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Refreshes the provider by fetching latest data from the file.
2420
+ * Overrides base class to store rules.
2421
+ *
2422
+ * @returns Promise that resolves when refresh is complete
2423
+ */
2424
+ async refresh() {
2425
+ await super.refresh();
2426
+ }
2427
+ /**
2428
+ * Updates the features object and writes to file.
2429
+ * This allows updating the FEATURES at runtime and persisting to file.
2430
+ *
2431
+ * @param newFeatures - New features object to sync with
2432
+ */
2433
+ async syncFeatures(newFeatures) {
2434
+ this.log("Syncing with new FEATURES values and updating file");
2435
+ this.features = newFeatures;
2436
+ const fileData = {
2437
+ flags: this.createDefaultFlags(),
2438
+ rules: this.rules || []
2439
+ };
2440
+ const { filePath, format } = this.config.fileConfig;
2441
+ const resolvedPath = this.resolveFilePath(filePath);
2442
+ try {
2443
+ let content;
2444
+ if (format === "json") {
2445
+ content = JSON.stringify(fileData, null, FORMAT_CONSTANTS.JSON_INDENT_SPACES);
2446
+ } else {
2447
+ content = yaml.stringify(fileData);
2448
+ }
2449
+ await writeFile2(resolvedPath, content, "utf-8");
2450
+ this.lastFileContent = content;
2451
+ this.engine.updateDefaults(newFeatures);
2452
+ await this.refresh();
2453
+ this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
2454
+ } catch (error) {
2455
+ this.log("Error syncing features to file:", error);
2456
+ throw new Error(
2457
+ `Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
2458
+ );
2459
+ }
2103
2460
  }
2104
2461
  /**
2105
2462
  * Gets information about the file provider.
@@ -2107,18 +2464,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2107
2464
  * @returns File provider information
2108
2465
  */
2109
2466
  getFileInfo() {
2467
+ const filePath = this.config.fileConfig?.filePath;
2468
+ const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
2469
+ let lastModified;
2470
+ if (resolvedPath) {
2471
+ try {
2472
+ const stats = fs.statSync(resolvedPath);
2473
+ lastModified = stats.mtime;
2474
+ } catch {
2475
+ }
2476
+ }
2110
2477
  return {
2111
- filePath: this.config.fileConfig?.filePath,
2478
+ filePath,
2479
+ resolvedPath,
2112
2480
  format: this.config.fileConfig?.format,
2113
2481
  isWatchEnabled: Boolean(this.config.fileConfig?.shouldWatchForChanges),
2114
- isImplemented: false,
2115
- requiredImplementation: [
2116
- "File reading and parsing logic",
2117
- "YAML and JSON format support",
2118
- "File watching for hot reload",
2119
- "Standard file path configuration",
2120
- "File validation and error handling"
2121
- ]
2482
+ isImplemented: true,
2483
+ lastModified
2122
2484
  };
2123
2485
  }
2124
2486
  };
@@ -2431,8 +2793,6 @@ Examples:
2431
2793
  };
2432
2794
  }
2433
2795
  };
2434
-
2435
- // src/domain/featureFlags/providers/factory.ts
2436
2796
  var PROVIDER_REGISTRY = {
2437
2797
  memory: MemoryFeatureFlagProvider,
2438
2798
  file: FileFeatureFlagProvider,
@@ -2535,25 +2895,69 @@ var FeatureFlagProviderFactory = class {
2535
2895
  };
2536
2896
  }
2537
2897
  /**
2538
- * Creates a default memory provider for testing environments.
2898
+ * Creates a default provider.
2899
+ * Uses file provider if features are not provided, memory provider otherwise.
2539
2900
  *
2540
- * @param features - Record of feature flag keys to their default values
2901
+ * @param features - Record of feature flag keys to their default values (optional)
2541
2902
  * @param overrides - Optional configuration overrides
2542
- * @returns Memory provider instance
2903
+ * @returns Provider instance
2543
2904
  */
2544
2905
  static createDefault(features, overrides) {
2906
+ const provider = features ? "memory" : "file";
2545
2907
  const defaultConfig = {
2546
- provider: "memory",
2908
+ provider,
2547
2909
  isCacheEnabled: true,
2548
- cacheTtl: 300,
2549
- // 5 minutes
2910
+ cacheTtl: FEATURE_FLAG_CACHE_TTL_DEFAULT,
2550
2911
  refreshInterval: 0,
2551
2912
  // No auto-refresh
2552
2913
  isLoggingEnabled: false,
2553
2914
  shouldFallbackToDefaults: true,
2554
2915
  ...overrides
2555
2916
  };
2556
- return this.create(defaultConfig, features);
2917
+ if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
2918
+ defaultConfig.fileConfig = {
2919
+ filePath: FEATURE_FLAG_FILE_PATHS.DEFAULT,
2920
+ format: "json",
2921
+ shouldWatchForChanges: false
2922
+ };
2923
+ }
2924
+ return this.create(defaultConfig, features ?? {});
2925
+ }
2926
+ /**
2927
+ * Type guard to check if a provider supports feature syncing.
2928
+ *
2929
+ * @param provider - The provider instance to check
2930
+ * @returns True if provider has syncFeatures method
2931
+ */
2932
+ static isSyncableProvider(provider) {
2933
+ return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
2934
+ }
2935
+ /**
2936
+ * Updates features on a provider if it supports the syncFeatures method.
2937
+ * This is useful for providers like MemoryProvider that can update their features at runtime.
2938
+ *
2939
+ * @param provider - The provider instance
2940
+ * @param newFeatures - New features to sync
2941
+ * @returns Promise that resolves when sync is complete
2942
+ * @throws Error if provider doesn't support feature syncing
2943
+ */
2944
+ static async syncFeatures(provider, newFeatures) {
2945
+ if (this.isSyncableProvider(provider)) {
2946
+ await provider.syncFeatures(newFeatures);
2947
+ } else {
2948
+ throw new Error(
2949
+ `Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
2950
+ );
2951
+ }
2952
+ }
2953
+ /**
2954
+ * Checks if a provider supports feature syncing.
2955
+ *
2956
+ * @param provider - The provider instance to check
2957
+ * @returns True if provider supports syncFeatures method
2958
+ */
2959
+ static supportsFeaturesSync(provider) {
2960
+ return this.isSyncableProvider(provider);
2557
2961
  }
2558
2962
  /**
2559
2963
  * Validates provider configuration before instantiation.
@@ -2606,8 +3010,11 @@ var FeatureFlagProviderFactory = class {
2606
3010
  validator();
2607
3011
  }
2608
3012
  static validateFileConfig(config) {
2609
- if (!config.fileConfig) {
2610
- throw new Error("File configuration is required for file provider");
3013
+ if (config.fileConfig) {
3014
+ const { format } = config.fileConfig;
3015
+ if (format && !["json", "yaml"].includes(format)) {
3016
+ throw new Error('File format must be either "json" or "yaml"');
3017
+ }
2611
3018
  }
2612
3019
  }
2613
3020
  static validateRedisConfig(config) {