@plyaz/core 1.0.2 → 1.0.3

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
@@ -186,9 +213,9 @@ var ValueUtils = {
186
213
  * @param defaultValue - Default if path doesn't exist
187
214
  * @returns Property value or default
188
215
  */
189
- getNestedProperty: /* @__PURE__ */ __name((obj, path, defaultValue) => {
216
+ getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
190
217
  if (!obj || typeof obj !== "object") return defaultValue;
191
- const keys = path.split(".");
218
+ const keys = path2.split(".");
192
219
  let current = obj;
193
220
  for (const key of keys) {
194
221
  if (current == null || typeof current !== "object") return defaultValue;
@@ -756,6 +783,16 @@ var FeatureFlagEngine = class {
756
783
  this.overrides.delete(key);
757
784
  this.log("Override removed:", key);
758
785
  }
786
+ /**
787
+ * Updates the default values for feature flags.
788
+ * This is useful when the FEATURES constant is updated at runtime.
789
+ *
790
+ * @param newDefaults - New default values
791
+ */
792
+ updateDefaults(newDefaults) {
793
+ this.defaults = newDefaults;
794
+ this.log("Updated default feature values");
795
+ }
759
796
  /**
760
797
  * Clears all manual overrides.
761
798
  */
@@ -933,7 +970,7 @@ var FeatureFlagEngine = class {
933
970
  value: rule.value,
934
971
  isEnabled: isTruthy(rule.value),
935
972
  reason: "rule_match",
936
- ruleId: rule.id,
973
+ matchedRuleId: rule.id,
937
974
  evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
938
975
  };
939
976
  }
@@ -976,8 +1013,6 @@ var FeatureFlagEngine = class {
976
1013
  }
977
1014
  }
978
1015
  };
979
-
980
- // src/base/cache/strategies/memory.ts
981
1016
  var MemoryCacheStrategy = class {
982
1017
  static {
983
1018
  __name(this, "MemoryCacheStrategy");
@@ -999,16 +1034,14 @@ var MemoryCacheStrategy = class {
999
1034
  *
1000
1035
  * @param config - Memory cache configuration
1001
1036
  */
1002
- constructor(config = {}) {
1003
- const DEFAULT_MAX_ENTRIES = 1e3;
1004
- const DEFAULT_CLEANUP_INTERVAL = 6e4;
1037
+ constructor(config$1 = {}) {
1005
1038
  const defaultConfig = {
1006
- maxEntries: DEFAULT_MAX_ENTRIES,
1007
- cleanupInterval: DEFAULT_CLEANUP_INTERVAL
1039
+ maxEntries: config.CACHE_MAX_SIZE_DEFAULT,
1040
+ cleanupInterval: config.CACHE_CLEANUP_INTERVAL_DEFAULT
1008
1041
  };
1009
- this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
1010
- this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
1011
- this.onEvict = config.onEvict;
1042
+ this.maxSize = config$1.maxSize ?? config$1.maxEntries ?? defaultConfig.maxEntries;
1043
+ this.cleanupInterval = config$1.cleanupInterval ?? defaultConfig.cleanupInterval;
1044
+ this.onEvict = config$1.onEvict;
1012
1045
  this.startCleanup();
1013
1046
  }
1014
1047
  /**
@@ -1319,8 +1352,8 @@ var RedisCacheStrategy = class {
1319
1352
  commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
1320
1353
  enableOfflineQueue: defaultOptions.enableOfflineQueue
1321
1354
  });
1322
- await new Promise((resolve, reject) => {
1323
- client.on("ready", resolve);
1355
+ await new Promise((resolve2, reject) => {
1356
+ client.on("ready", resolve2);
1324
1357
  client.on("error", reject);
1325
1358
  });
1326
1359
  return client;
@@ -1866,31 +1899,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
1866
1899
  }
1867
1900
  }
1868
1901
  /**
1869
- * Updates a flag value in memory.
1902
+ * Updates a flag in memory.
1870
1903
  *
1871
- * @param key - The flag key to update
1872
- * @param value - The new value
1873
- * @param updateProps - Optional properties to update
1904
+ * @param flagOrKey - Either a complete flag object or a flag key
1905
+ * @param value - The new value (only used when first param is a key)
1906
+ * @param updateProps - Optional properties to update (only used when first param is a key)
1874
1907
  */
1875
- updateFlag(key, value, updateProps) {
1876
- const flagIndex = this.flags.findIndex((flag) => flag.key === key);
1908
+ async updateFlag(flagOrKey, value, updateProps) {
1909
+ let key;
1910
+ let updatedFlag;
1911
+ if (typeof flagOrKey === "string") {
1912
+ key = flagOrKey;
1913
+ const existingFlag = this.flags.find((f) => f.key === key);
1914
+ if (!existingFlag) {
1915
+ this.log(`Flag with key ${key} not found in memory`);
1916
+ return;
1917
+ }
1918
+ if (value === void 0) {
1919
+ this.log(`Value is required when updating flag by key`);
1920
+ return;
1921
+ }
1922
+ updatedFlag = {
1923
+ ...existingFlag,
1924
+ value,
1925
+ type: this.inferFlagType(value),
1926
+ updatedAt: /* @__PURE__ */ new Date(),
1927
+ updatedBy: "memory-runtime",
1928
+ ...updateProps
1929
+ };
1930
+ } else {
1931
+ const flag = flagOrKey;
1932
+ key = flag.key;
1933
+ updatedFlag = {
1934
+ ...flag,
1935
+ updatedAt: /* @__PURE__ */ new Date(),
1936
+ updatedBy: flag.updatedBy || "memory-runtime"
1937
+ };
1938
+ }
1939
+ const flagIndex = this.flags.findIndex((f) => f.key === key);
1877
1940
  if (flagIndex === -1) {
1878
1941
  this.log(`Flag with key ${key} not found in memory`);
1879
1942
  return;
1880
1943
  }
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
1944
  this.flags[flagIndex] = updatedFlag;
1890
1945
  this.engine.setFlags(this.flags);
1891
1946
  void this.cacheManager.clear();
1892
1947
  this.notifySubscribers();
1893
- this.log(`Updated flag: ${key} with new value:`, value);
1948
+ this.log(`Updated flag: ${key}`);
1894
1949
  }
1895
1950
  /**
1896
1951
  * Adds a new flag to memory at runtime.
@@ -2000,6 +2055,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
2000
2055
  getCurrentRules() {
2001
2056
  return [...this.rules];
2002
2057
  }
2058
+ /**
2059
+ * Updates the features object and syncs all flags.
2060
+ * This allows updating the FEATURES constant at runtime.
2061
+ *
2062
+ * @param newFeatures - New features object to sync with
2063
+ */
2064
+ async syncFeatures(newFeatures) {
2065
+ this.log("Syncing with new FEATURES values");
2066
+ this.features = newFeatures;
2067
+ await this.refresh();
2068
+ this.log(`Synced ${Object.keys(newFeatures).length} features`);
2069
+ }
2003
2070
  /**
2004
2071
  * Resets the memory provider to its initial state.
2005
2072
  */
@@ -2033,13 +2100,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
2033
2100
  }
2034
2101
  }
2035
2102
  };
2036
-
2037
- // src/domain/featureFlags/providers/file.ts
2103
+ var readFile2 = util.promisify(fs__namespace.readFile);
2104
+ var writeFile2 = util.promisify(fs__namespace.writeFile);
2105
+ var access2 = util.promisify(fs__namespace.access);
2106
+ var mkdir2 = util.promisify(fs__namespace.mkdir);
2038
2107
  var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2039
2108
  static {
2040
2109
  __name(this, "FileFeatureFlagProvider");
2041
2110
  }
2042
2111
  fileWatcher;
2112
+ lastFileContent;
2113
+ fileCheckInterval;
2114
+ rules = [];
2043
2115
  /**
2044
2116
  * Creates a new file feature flag provider.
2045
2117
  *
@@ -2049,8 +2121,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2049
2121
  constructor(config, features) {
2050
2122
  super(config, features);
2051
2123
  this.validateConfig();
2052
- this.setupFileWatcher();
2053
- throw new Error("File provider implementation coming soon");
2124
+ }
2125
+ /**
2126
+ * Initializes the provider and sets up file watching if enabled.
2127
+ */
2128
+ async initialize() {
2129
+ await super.initialize();
2130
+ if (this.config.fileConfig?.shouldWatchForChanges) {
2131
+ this.setupFileWatcher();
2132
+ }
2054
2133
  }
2055
2134
  /**
2056
2135
  * Fetches flags and rules from the configuration file.
@@ -2059,9 +2138,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2059
2138
  * @returns Promise resolving to flags and rules from file
2060
2139
  */
2061
2140
  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
- );
2141
+ const { filePath, format } = this.config.fileConfig;
2142
+ const resolvedPath = this.resolveFilePath(filePath);
2143
+ try {
2144
+ await access2(resolvedPath, fs__namespace.constants.R_OK);
2145
+ const content = await readFile2(resolvedPath, "utf-8");
2146
+ this.lastFileContent = content;
2147
+ const data = await this.parseFileContent(content, format);
2148
+ this.validateFileData(data);
2149
+ this.rules = data.rules || [];
2150
+ return {
2151
+ flags: data.flags || [],
2152
+ rules: data.rules || []
2153
+ };
2154
+ } catch (error) {
2155
+ return this.handleFetchDataError(error, resolvedPath, format);
2156
+ }
2157
+ }
2158
+ /**
2159
+ * Parses file content based on format.
2160
+ *
2161
+ * @private
2162
+ */
2163
+ async parseFileContent(content, format) {
2164
+ if (format === "json") {
2165
+ return this.parseJSON(content);
2166
+ } else if (format === "yaml") {
2167
+ return await this.parseYAML(content);
2168
+ }
2169
+ throw new Error(`Unsupported file format: ${format}`);
2170
+ }
2171
+ /**
2172
+ * Handles errors for fetchData, including file creation and fallback.
2173
+ *
2174
+ * @private
2175
+ */
2176
+ async handleFetchDataError(error, resolvedPath, format) {
2177
+ const isFileNotFound = this.isFileNotFoundError(error);
2178
+ if (isFileNotFound && this.config.shouldFallbackToDefaults) {
2179
+ return await this.handleFileNotFound(resolvedPath, format);
2180
+ }
2181
+ this.log(`Error reading file ${resolvedPath}:`, error);
2182
+ if (this.config.shouldFallbackToDefaults) {
2183
+ return this.handleFallbackToDefaults();
2184
+ }
2185
+ throw error;
2186
+ }
2187
+ /**
2188
+ * Type guard for NodeJS.ErrnoException.
2189
+ */
2190
+ isFileNotFoundError(error) {
2191
+ return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
2192
+ }
2193
+ /**
2194
+ * Handles the case when the file is not found and fallback is enabled.
2195
+ */
2196
+ async handleFileNotFound(resolvedPath, format) {
2197
+ this.log(`File not found at ${resolvedPath}, creating with default values`);
2198
+ try {
2199
+ await this.createDefaultFile(resolvedPath, format);
2200
+ const content = await readFile2(resolvedPath, "utf-8");
2201
+ this.lastFileContent = content;
2202
+ const data = await this.parseFileContent(content, format);
2203
+ return {
2204
+ flags: data.flags || [],
2205
+ rules: data.rules || []
2206
+ };
2207
+ } catch (createError) {
2208
+ this.log("Error creating default file:", createError);
2209
+ return {
2210
+ flags: this.createDefaultFlags(),
2211
+ rules: []
2212
+ };
2213
+ }
2214
+ }
2215
+ /**
2216
+ * Handles fallback to default flags and rules.
2217
+ */
2218
+ handleFallbackToDefaults() {
2219
+ this.log("Falling back to default values");
2220
+ return {
2221
+ flags: this.createDefaultFlags(),
2222
+ rules: []
2223
+ };
2065
2224
  }
2066
2225
  /**
2067
2226
  * Validates the file provider configuration.
@@ -2084,6 +2243,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2084
2243
  throw new Error('File format must be either "json" or "yaml"');
2085
2244
  }
2086
2245
  }
2246
+ /**
2247
+ * Resolves the file path, supporting relative and absolute paths.
2248
+ *
2249
+ * @private
2250
+ * @param filePath - The file path from configuration
2251
+ * @returns Resolved absolute file path
2252
+ */
2253
+ resolveFilePath(filePath) {
2254
+ let pathToResolve = filePath;
2255
+ if (!pathToResolve) {
2256
+ 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)));
2257
+ const __dirname = path__namespace.dirname(__filename);
2258
+ pathToResolve = path__namespace.join(__dirname, "../../../config/feature-provider.json");
2259
+ }
2260
+ if (path__namespace.isAbsolute(pathToResolve)) {
2261
+ return pathToResolve;
2262
+ }
2263
+ return path__namespace.resolve(process.cwd(), pathToResolve);
2264
+ }
2265
+ /**
2266
+ * Parses JSON content.
2267
+ *
2268
+ * @private
2269
+ * @param content - File content
2270
+ * @returns Parsed data
2271
+ */
2272
+ parseJSON(content) {
2273
+ try {
2274
+ return JSON.parse(content);
2275
+ } catch (error) {
2276
+ throw new Error(
2277
+ `Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
2278
+ );
2279
+ }
2280
+ }
2281
+ /**
2282
+ * Parses YAML content.
2283
+ *
2284
+ * @private
2285
+ * @param content - File content
2286
+ * @returns Parsed data
2287
+ */
2288
+ async parseYAML(content) {
2289
+ try {
2290
+ const data = yaml__namespace.parse(content);
2291
+ return data;
2292
+ } catch (error) {
2293
+ throw new Error(
2294
+ `Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
2295
+ );
2296
+ }
2297
+ }
2298
+ /**
2299
+ * Validates the structure of file data.
2300
+ *
2301
+ * @private
2302
+ * @param data - Parsed file data
2303
+ */
2304
+ validateFileData(data) {
2305
+ if (!data || typeof data !== "object") {
2306
+ throw new Error("File must contain a valid object");
2307
+ }
2308
+ const fileData = data;
2309
+ this.checkFlagsArray(fileData.flags);
2310
+ this.checkRulesArray(fileData.rules);
2311
+ }
2312
+ checkFlagsArray(flags) {
2313
+ if (flags && !Array.isArray(flags)) {
2314
+ throw new Error('"flags" must be an array');
2315
+ }
2316
+ if (Array.isArray(flags)) {
2317
+ this.validateFlags(flags);
2318
+ }
2319
+ }
2320
+ checkRulesArray(rules) {
2321
+ if (rules && !Array.isArray(rules)) {
2322
+ throw new Error('"rules" must be an array');
2323
+ }
2324
+ if (Array.isArray(rules)) {
2325
+ this.validateRules(rules);
2326
+ }
2327
+ }
2328
+ validateFlags(flags) {
2329
+ flags.forEach((flag, index) => {
2330
+ if (!flag || typeof flag !== "object") {
2331
+ throw new Error(`Flag at index ${index} must be an object`);
2332
+ }
2333
+ const flagObj = flag;
2334
+ if (!flagObj.key || typeof flagObj.key !== "string") {
2335
+ throw new Error(`Flag at index ${index} must have a "key" property`);
2336
+ }
2337
+ if (flagObj.value === void 0) {
2338
+ throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
2339
+ }
2340
+ });
2341
+ }
2342
+ validateRules(rules) {
2343
+ rules.forEach((rule, index) => {
2344
+ if (!rule || typeof rule !== "object") {
2345
+ throw new Error(`Rule at index ${index} must be an object`);
2346
+ }
2347
+ const ruleObj = rule;
2348
+ if (!ruleObj.id || typeof ruleObj.id !== "string") {
2349
+ throw new Error(`Rule at index ${index} must have an "id" property`);
2350
+ }
2351
+ if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
2352
+ throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
2353
+ }
2354
+ if (!Array.isArray(ruleObj.conditions)) {
2355
+ throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
2356
+ }
2357
+ });
2358
+ }
2359
+ /**
2360
+ * Creates a default configuration file with values from features.
2361
+ *
2362
+ * @private
2363
+ * @param filePath - Path where to create the file
2364
+ * @param format - File format (json or yaml)
2365
+ */
2366
+ async createDefaultFile(filePath, format) {
2367
+ const dir = path__namespace.dirname(filePath);
2368
+ await mkdir2(dir, { recursive: true });
2369
+ const defaultData = {
2370
+ flags: this.createDefaultFlags(),
2371
+ rules: []
2372
+ };
2373
+ let content;
2374
+ if (format === "json") {
2375
+ content = JSON.stringify(defaultData, null, 2);
2376
+ } else {
2377
+ content = yaml__namespace.stringify(defaultData);
2378
+ }
2379
+ await writeFile2(filePath, content, "utf-8");
2380
+ this.log(`Created default feature flag file at: ${filePath}`);
2381
+ }
2382
+ /**
2383
+ * Creates default flags from the features configuration.
2384
+ *
2385
+ * @private
2386
+ * @returns Array of default flags
2387
+ */
2388
+ createDefaultFlags() {
2389
+ return Object.entries(this.features).map(([key, value]) => ({
2390
+ key,
2391
+ value,
2392
+ isEnabled: true,
2393
+ name: key,
2394
+ description: `Default flag for ${key}`,
2395
+ type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
2396
+ environment: "development",
2397
+ createdAt: /* @__PURE__ */ new Date(),
2398
+ updatedAt: /* @__PURE__ */ new Date(),
2399
+ createdBy: "system",
2400
+ updatedBy: "system",
2401
+ metadata: {},
2402
+ tags: []
2403
+ }));
2404
+ }
2087
2405
  /**
2088
2406
  * Sets up file watching for hot reload if enabled.
2089
2407
  *
@@ -2093,8 +2411,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2093
2411
  if (!this.config.fileConfig?.shouldWatchForChanges) {
2094
2412
  return;
2095
2413
  }
2096
- this.log("File watching would be enabled for hot reload");
2097
- this.log("File path:", this.config.fileConfig.filePath);
2414
+ const { filePath } = this.config.fileConfig;
2415
+ const resolvedPath = this.resolveFilePath(filePath);
2416
+ try {
2417
+ this.fileWatcher = fs__namespace.watch(resolvedPath, async (eventType) => {
2418
+ if (eventType === "change") {
2419
+ this.log(`File changed: ${resolvedPath}`);
2420
+ if (this.fileCheckInterval) {
2421
+ clearTimeout(this.fileCheckInterval);
2422
+ }
2423
+ this.fileCheckInterval = setTimeout(async () => {
2424
+ try {
2425
+ const content = await readFile2(resolvedPath, "utf-8");
2426
+ if (content !== this.lastFileContent) {
2427
+ this.log("File content changed, refreshing...");
2428
+ await this.refresh();
2429
+ }
2430
+ } catch (error) {
2431
+ this.log("Error reading changed file:", error);
2432
+ }
2433
+ }, this.config.fileConfig?.fileCheckInterval ?? config.FILE_CHECK_INTERVAL_DEFAULT);
2434
+ }
2435
+ });
2436
+ this.log(`File watching enabled for: ${resolvedPath}`);
2437
+ } catch (error) {
2438
+ this.log(`Failed to set up file watching: ${error}`);
2439
+ }
2098
2440
  }
2099
2441
  /**
2100
2442
  * Disposes of the file provider and stops file watching.
@@ -2102,10 +2444,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2102
2444
  dispose() {
2103
2445
  super.dispose();
2104
2446
  if (this.fileWatcher) {
2105
- clearInterval(this.fileWatcher);
2447
+ this.fileWatcher.close();
2106
2448
  this.fileWatcher = void 0;
2107
2449
  this.log("File watching stopped");
2108
2450
  }
2451
+ if (this.fileCheckInterval) {
2452
+ clearTimeout(this.fileCheckInterval);
2453
+ this.fileCheckInterval = void 0;
2454
+ }
2455
+ }
2456
+ /**
2457
+ * Refreshes the provider by fetching latest data from the file.
2458
+ * Overrides base class to store rules.
2459
+ *
2460
+ * @returns Promise that resolves when refresh is complete
2461
+ */
2462
+ async refresh() {
2463
+ await super.refresh();
2464
+ }
2465
+ /**
2466
+ * Updates the features object and writes to file.
2467
+ * This allows updating the FEATURES at runtime and persisting to file.
2468
+ *
2469
+ * @param newFeatures - New features object to sync with
2470
+ */
2471
+ async syncFeatures(newFeatures) {
2472
+ this.log("Syncing with new FEATURES values and updating file");
2473
+ this.features = newFeatures;
2474
+ const fileData = {
2475
+ flags: this.createDefaultFlags(),
2476
+ rules: this.rules || []
2477
+ };
2478
+ const { filePath, format } = this.config.fileConfig;
2479
+ const resolvedPath = this.resolveFilePath(filePath);
2480
+ try {
2481
+ let content;
2482
+ if (format === "json") {
2483
+ content = JSON.stringify(fileData, null, 2);
2484
+ } else {
2485
+ content = yaml__namespace.stringify(fileData);
2486
+ }
2487
+ await writeFile2(resolvedPath, content, "utf-8");
2488
+ this.lastFileContent = content;
2489
+ this.engine.updateDefaults(newFeatures);
2490
+ await this.refresh();
2491
+ this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
2492
+ } catch (error) {
2493
+ this.log("Error syncing features to file:", error);
2494
+ throw new Error(
2495
+ `Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
2496
+ );
2497
+ }
2109
2498
  }
2110
2499
  /**
2111
2500
  * Gets information about the file provider.
@@ -2113,18 +2502,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2113
2502
  * @returns File provider information
2114
2503
  */
2115
2504
  getFileInfo() {
2505
+ const filePath = this.config.fileConfig?.filePath;
2506
+ const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
2507
+ let lastModified;
2508
+ if (resolvedPath) {
2509
+ try {
2510
+ const stats = fs__namespace.statSync(resolvedPath);
2511
+ lastModified = stats.mtime;
2512
+ } catch {
2513
+ }
2514
+ }
2116
2515
  return {
2117
- filePath: this.config.fileConfig?.filePath,
2516
+ filePath,
2517
+ resolvedPath,
2118
2518
  format: this.config.fileConfig?.format,
2119
2519
  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
- ]
2520
+ isImplemented: true,
2521
+ lastModified
2128
2522
  };
2129
2523
  }
2130
2524
  };
@@ -2437,8 +2831,6 @@ Examples:
2437
2831
  };
2438
2832
  }
2439
2833
  };
2440
-
2441
- // src/domain/featureFlags/providers/factory.ts
2442
2834
  var PROVIDER_REGISTRY = {
2443
2835
  memory: MemoryFeatureFlagProvider,
2444
2836
  file: FileFeatureFlagProvider,
@@ -2541,25 +2933,69 @@ var FeatureFlagProviderFactory = class {
2541
2933
  };
2542
2934
  }
2543
2935
  /**
2544
- * Creates a default memory provider for testing environments.
2936
+ * Creates a default provider.
2937
+ * Uses file provider if features are not provided, memory provider otherwise.
2545
2938
  *
2546
- * @param features - Record of feature flag keys to their default values
2939
+ * @param features - Record of feature flag keys to their default values (optional)
2547
2940
  * @param overrides - Optional configuration overrides
2548
- * @returns Memory provider instance
2941
+ * @returns Provider instance
2549
2942
  */
2550
2943
  static createDefault(features, overrides) {
2944
+ const provider = features ? "memory" : "file";
2551
2945
  const defaultConfig = {
2552
- provider: "memory",
2946
+ provider,
2553
2947
  isCacheEnabled: true,
2554
- cacheTtl: 300,
2555
- // 5 minutes
2948
+ cacheTtl: config.FEATURE_FLAG_CACHE_TTL_DEFAULT,
2556
2949
  refreshInterval: 0,
2557
2950
  // No auto-refresh
2558
2951
  isLoggingEnabled: false,
2559
2952
  shouldFallbackToDefaults: true,
2560
2953
  ...overrides
2561
2954
  };
2562
- return this.create(defaultConfig, features);
2955
+ if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
2956
+ defaultConfig.fileConfig = {
2957
+ filePath: config.FEATURE_FLAG_FILE_PATHS.DEFAULT,
2958
+ format: "json",
2959
+ shouldWatchForChanges: false
2960
+ };
2961
+ }
2962
+ return this.create(defaultConfig, features ?? {});
2963
+ }
2964
+ /**
2965
+ * Type guard to check if a provider supports feature syncing.
2966
+ *
2967
+ * @param provider - The provider instance to check
2968
+ * @returns True if provider has syncFeatures method
2969
+ */
2970
+ static isSyncableProvider(provider) {
2971
+ return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
2972
+ }
2973
+ /**
2974
+ * Updates features on a provider if it supports the syncFeatures method.
2975
+ * This is useful for providers like MemoryProvider that can update their features at runtime.
2976
+ *
2977
+ * @param provider - The provider instance
2978
+ * @param newFeatures - New features to sync
2979
+ * @returns Promise that resolves when sync is complete
2980
+ * @throws Error if provider doesn't support feature syncing
2981
+ */
2982
+ static async syncFeatures(provider, newFeatures) {
2983
+ if (this.isSyncableProvider(provider)) {
2984
+ await provider.syncFeatures(newFeatures);
2985
+ } else {
2986
+ throw new Error(
2987
+ `Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
2988
+ );
2989
+ }
2990
+ }
2991
+ /**
2992
+ * Checks if a provider supports feature syncing.
2993
+ *
2994
+ * @param provider - The provider instance to check
2995
+ * @returns True if provider supports syncFeatures method
2996
+ */
2997
+ static supportsFeaturesSync(provider) {
2998
+ return this.isSyncableProvider(provider);
2563
2999
  }
2564
3000
  /**
2565
3001
  * Validates provider configuration before instantiation.
@@ -2612,8 +3048,11 @@ var FeatureFlagProviderFactory = class {
2612
3048
  validator();
2613
3049
  }
2614
3050
  static validateFileConfig(config) {
2615
- if (!config.fileConfig) {
2616
- throw new Error("File configuration is required for file provider");
3051
+ if (config.fileConfig) {
3052
+ const { format } = config.fileConfig;
3053
+ if (format && !["json", "yaml"].includes(format)) {
3054
+ throw new Error('File format must be either "json" or "yaml"');
3055
+ }
2617
3056
  }
2618
3057
  }
2619
3058
  static validateRedisConfig(config) {