@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.mjs CHANGED
@@ -1,4 +1,9 @@
1
- import { FEATURES } from '@plyaz/config';
1
+ import { CACHE_MAX_SIZE_DEFAULT, CACHE_CLEANUP_INTERVAL_DEFAULT, FILE_CHECK_INTERVAL_DEFAULT, FEATURE_FLAG_FILE_PATHS, FEATURE_FLAG_CACHE_TTL_DEFAULT, 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';
@@ -180,9 +185,9 @@ var ValueUtils = {
180
185
  * @param defaultValue - Default if path doesn't exist
181
186
  * @returns Property value or default
182
187
  */
183
- getNestedProperty: /* @__PURE__ */ __name((obj, path, defaultValue) => {
188
+ getNestedProperty: /* @__PURE__ */ __name((obj, path2, defaultValue) => {
184
189
  if (!obj || typeof obj !== "object") return defaultValue;
185
- const keys = path.split(".");
190
+ const keys = path2.split(".");
186
191
  let current = obj;
187
192
  for (const key of keys) {
188
193
  if (current == null || typeof current !== "object") return defaultValue;
@@ -750,6 +755,16 @@ var FeatureFlagEngine = class {
750
755
  this.overrides.delete(key);
751
756
  this.log("Override removed:", key);
752
757
  }
758
+ /**
759
+ * Updates the default values for feature flags.
760
+ * This is useful when the FEATURES constant is updated at runtime.
761
+ *
762
+ * @param newDefaults - New default values
763
+ */
764
+ updateDefaults(newDefaults) {
765
+ this.defaults = newDefaults;
766
+ this.log("Updated default feature values");
767
+ }
753
768
  /**
754
769
  * Clears all manual overrides.
755
770
  */
@@ -927,7 +942,7 @@ var FeatureFlagEngine = class {
927
942
  value: rule.value,
928
943
  isEnabled: isTruthy(rule.value),
929
944
  reason: "rule_match",
930
- ruleId: rule.id,
945
+ matchedRuleId: rule.id,
931
946
  evaluatedAt: evaluatedAt ?? /* @__PURE__ */ new Date()
932
947
  };
933
948
  }
@@ -970,8 +985,6 @@ var FeatureFlagEngine = class {
970
985
  }
971
986
  }
972
987
  };
973
-
974
- // src/base/cache/strategies/memory.ts
975
988
  var MemoryCacheStrategy = class {
976
989
  static {
977
990
  __name(this, "MemoryCacheStrategy");
@@ -994,11 +1007,9 @@ var MemoryCacheStrategy = class {
994
1007
  * @param config - Memory cache configuration
995
1008
  */
996
1009
  constructor(config = {}) {
997
- const DEFAULT_MAX_ENTRIES = 1e3;
998
- const DEFAULT_CLEANUP_INTERVAL = 6e4;
999
1010
  const defaultConfig = {
1000
- maxEntries: DEFAULT_MAX_ENTRIES,
1001
- cleanupInterval: DEFAULT_CLEANUP_INTERVAL
1011
+ maxEntries: CACHE_MAX_SIZE_DEFAULT,
1012
+ cleanupInterval: CACHE_CLEANUP_INTERVAL_DEFAULT
1002
1013
  };
1003
1014
  this.maxSize = config.maxSize ?? config.maxEntries ?? defaultConfig.maxEntries;
1004
1015
  this.cleanupInterval = config.cleanupInterval ?? defaultConfig.cleanupInterval;
@@ -1313,8 +1324,8 @@ var RedisCacheStrategy = class {
1313
1324
  commandTimeout: this.config.commandTimeout ?? defaultOptions.commandTimeout,
1314
1325
  enableOfflineQueue: defaultOptions.enableOfflineQueue
1315
1326
  });
1316
- await new Promise((resolve, reject) => {
1317
- client.on("ready", resolve);
1327
+ await new Promise((resolve2, reject) => {
1328
+ client.on("ready", resolve2);
1318
1329
  client.on("error", reject);
1319
1330
  });
1320
1331
  return client;
@@ -1860,31 +1871,53 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
1860
1871
  }
1861
1872
  }
1862
1873
  /**
1863
- * Updates a flag value in memory.
1874
+ * Updates a flag in memory.
1864
1875
  *
1865
- * @param key - The flag key to update
1866
- * @param value - The new value
1867
- * @param updateProps - Optional properties to update
1876
+ * @param flagOrKey - Either a complete flag object or a flag key
1877
+ * @param value - The new value (only used when first param is a key)
1878
+ * @param updateProps - Optional properties to update (only used when first param is a key)
1868
1879
  */
1869
- updateFlag(key, value, updateProps) {
1870
- const flagIndex = this.flags.findIndex((flag) => flag.key === key);
1880
+ async updateFlag(flagOrKey, value, updateProps) {
1881
+ let key;
1882
+ let updatedFlag;
1883
+ if (typeof flagOrKey === "string") {
1884
+ key = flagOrKey;
1885
+ const existingFlag = this.flags.find((f) => f.key === key);
1886
+ if (!existingFlag) {
1887
+ this.log(`Flag with key ${key} not found in memory`);
1888
+ return;
1889
+ }
1890
+ if (value === void 0) {
1891
+ this.log(`Value is required when updating flag by key`);
1892
+ return;
1893
+ }
1894
+ updatedFlag = {
1895
+ ...existingFlag,
1896
+ value,
1897
+ type: this.inferFlagType(value),
1898
+ updatedAt: /* @__PURE__ */ new Date(),
1899
+ updatedBy: "memory-runtime",
1900
+ ...updateProps
1901
+ };
1902
+ } else {
1903
+ const flag = flagOrKey;
1904
+ key = flag.key;
1905
+ updatedFlag = {
1906
+ ...flag,
1907
+ updatedAt: /* @__PURE__ */ new Date(),
1908
+ updatedBy: flag.updatedBy || "memory-runtime"
1909
+ };
1910
+ }
1911
+ const flagIndex = this.flags.findIndex((f) => f.key === key);
1871
1912
  if (flagIndex === -1) {
1872
1913
  this.log(`Flag with key ${key} not found in memory`);
1873
1914
  return;
1874
1915
  }
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
1916
  this.flags[flagIndex] = updatedFlag;
1884
1917
  this.engine.setFlags(this.flags);
1885
1918
  void this.cacheManager.clear();
1886
1919
  this.notifySubscribers();
1887
- this.log(`Updated flag: ${key} with new value:`, value);
1920
+ this.log(`Updated flag: ${key}`);
1888
1921
  }
1889
1922
  /**
1890
1923
  * Adds a new flag to memory at runtime.
@@ -1994,6 +2027,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
1994
2027
  getCurrentRules() {
1995
2028
  return [...this.rules];
1996
2029
  }
2030
+ /**
2031
+ * Updates the features object and syncs all flags.
2032
+ * This allows updating the FEATURES constant at runtime.
2033
+ *
2034
+ * @param newFeatures - New features object to sync with
2035
+ */
2036
+ async syncFeatures(newFeatures) {
2037
+ this.log("Syncing with new FEATURES values");
2038
+ this.features = newFeatures;
2039
+ await this.refresh();
2040
+ this.log(`Synced ${Object.keys(newFeatures).length} features`);
2041
+ }
1997
2042
  /**
1998
2043
  * Resets the memory provider to its initial state.
1999
2044
  */
@@ -2027,13 +2072,18 @@ var MemoryFeatureFlagProvider = class extends FeatureFlagProvider {
2027
2072
  }
2028
2073
  }
2029
2074
  };
2030
-
2031
- // src/domain/featureFlags/providers/file.ts
2075
+ var readFile2 = promisify(fs.readFile);
2076
+ var writeFile2 = promisify(fs.writeFile);
2077
+ var access2 = promisify(fs.access);
2078
+ var mkdir2 = promisify(fs.mkdir);
2032
2079
  var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2033
2080
  static {
2034
2081
  __name(this, "FileFeatureFlagProvider");
2035
2082
  }
2036
2083
  fileWatcher;
2084
+ lastFileContent;
2085
+ fileCheckInterval;
2086
+ rules = [];
2037
2087
  /**
2038
2088
  * Creates a new file feature flag provider.
2039
2089
  *
@@ -2043,8 +2093,15 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2043
2093
  constructor(config, features) {
2044
2094
  super(config, features);
2045
2095
  this.validateConfig();
2046
- this.setupFileWatcher();
2047
- throw new Error("File provider implementation coming soon");
2096
+ }
2097
+ /**
2098
+ * Initializes the provider and sets up file watching if enabled.
2099
+ */
2100
+ async initialize() {
2101
+ await super.initialize();
2102
+ if (this.config.fileConfig?.shouldWatchForChanges) {
2103
+ this.setupFileWatcher();
2104
+ }
2048
2105
  }
2049
2106
  /**
2050
2107
  * Fetches flags and rules from the configuration file.
@@ -2053,9 +2110,89 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2053
2110
  * @returns Promise resolving to flags and rules from file
2054
2111
  */
2055
2112
  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
- );
2113
+ const { filePath, format } = this.config.fileConfig;
2114
+ const resolvedPath = this.resolveFilePath(filePath);
2115
+ try {
2116
+ await access2(resolvedPath, fs.constants.R_OK);
2117
+ const content = await readFile2(resolvedPath, "utf-8");
2118
+ this.lastFileContent = content;
2119
+ const data = await this.parseFileContent(content, format);
2120
+ this.validateFileData(data);
2121
+ this.rules = data.rules || [];
2122
+ return {
2123
+ flags: data.flags || [],
2124
+ rules: data.rules || []
2125
+ };
2126
+ } catch (error) {
2127
+ return this.handleFetchDataError(error, resolvedPath, format);
2128
+ }
2129
+ }
2130
+ /**
2131
+ * Parses file content based on format.
2132
+ *
2133
+ * @private
2134
+ */
2135
+ async parseFileContent(content, format) {
2136
+ if (format === "json") {
2137
+ return this.parseJSON(content);
2138
+ } else if (format === "yaml") {
2139
+ return await this.parseYAML(content);
2140
+ }
2141
+ throw new Error(`Unsupported file format: ${format}`);
2142
+ }
2143
+ /**
2144
+ * Handles errors for fetchData, including file creation and fallback.
2145
+ *
2146
+ * @private
2147
+ */
2148
+ async handleFetchDataError(error, resolvedPath, format) {
2149
+ const isFileNotFound = this.isFileNotFoundError(error);
2150
+ if (isFileNotFound && this.config.shouldFallbackToDefaults) {
2151
+ return await this.handleFileNotFound(resolvedPath, format);
2152
+ }
2153
+ this.log(`Error reading file ${resolvedPath}:`, error);
2154
+ if (this.config.shouldFallbackToDefaults) {
2155
+ return this.handleFallbackToDefaults();
2156
+ }
2157
+ throw error;
2158
+ }
2159
+ /**
2160
+ * Type guard for NodeJS.ErrnoException.
2161
+ */
2162
+ isFileNotFoundError(error) {
2163
+ return error instanceof Error && typeof error.code === "string" && error.code === "ENOENT";
2164
+ }
2165
+ /**
2166
+ * Handles the case when the file is not found and fallback is enabled.
2167
+ */
2168
+ async handleFileNotFound(resolvedPath, format) {
2169
+ this.log(`File not found at ${resolvedPath}, creating with default values`);
2170
+ try {
2171
+ await this.createDefaultFile(resolvedPath, format);
2172
+ const content = await readFile2(resolvedPath, "utf-8");
2173
+ this.lastFileContent = content;
2174
+ const data = await this.parseFileContent(content, format);
2175
+ return {
2176
+ flags: data.flags || [],
2177
+ rules: data.rules || []
2178
+ };
2179
+ } catch (createError) {
2180
+ this.log("Error creating default file:", createError);
2181
+ return {
2182
+ flags: this.createDefaultFlags(),
2183
+ rules: []
2184
+ };
2185
+ }
2186
+ }
2187
+ /**
2188
+ * Handles fallback to default flags and rules.
2189
+ */
2190
+ handleFallbackToDefaults() {
2191
+ this.log("Falling back to default values");
2192
+ return {
2193
+ flags: this.createDefaultFlags(),
2194
+ rules: []
2195
+ };
2059
2196
  }
2060
2197
  /**
2061
2198
  * Validates the file provider configuration.
@@ -2078,6 +2215,165 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2078
2215
  throw new Error('File format must be either "json" or "yaml"');
2079
2216
  }
2080
2217
  }
2218
+ /**
2219
+ * Resolves the file path, supporting relative and absolute paths.
2220
+ *
2221
+ * @private
2222
+ * @param filePath - The file path from configuration
2223
+ * @returns Resolved absolute file path
2224
+ */
2225
+ resolveFilePath(filePath) {
2226
+ let pathToResolve = filePath;
2227
+ if (!pathToResolve) {
2228
+ const __filename = fileURLToPath(import.meta.url);
2229
+ const __dirname = path.dirname(__filename);
2230
+ pathToResolve = path.join(__dirname, "../../../config/feature-provider.json");
2231
+ }
2232
+ if (path.isAbsolute(pathToResolve)) {
2233
+ return pathToResolve;
2234
+ }
2235
+ return path.resolve(process.cwd(), pathToResolve);
2236
+ }
2237
+ /**
2238
+ * Parses JSON content.
2239
+ *
2240
+ * @private
2241
+ * @param content - File content
2242
+ * @returns Parsed data
2243
+ */
2244
+ parseJSON(content) {
2245
+ try {
2246
+ return JSON.parse(content);
2247
+ } catch (error) {
2248
+ throw new Error(
2249
+ `Invalid JSON format: ${error instanceof Error ? error.message : String(error)}`
2250
+ );
2251
+ }
2252
+ }
2253
+ /**
2254
+ * Parses YAML content.
2255
+ *
2256
+ * @private
2257
+ * @param content - File content
2258
+ * @returns Parsed data
2259
+ */
2260
+ async parseYAML(content) {
2261
+ try {
2262
+ const data = yaml.parse(content);
2263
+ return data;
2264
+ } catch (error) {
2265
+ throw new Error(
2266
+ `Invalid YAML format: ${error instanceof Error ? error.message : String(error)}`
2267
+ );
2268
+ }
2269
+ }
2270
+ /**
2271
+ * Validates the structure of file data.
2272
+ *
2273
+ * @private
2274
+ * @param data - Parsed file data
2275
+ */
2276
+ validateFileData(data) {
2277
+ if (!data || typeof data !== "object") {
2278
+ throw new Error("File must contain a valid object");
2279
+ }
2280
+ const fileData = data;
2281
+ this.checkFlagsArray(fileData.flags);
2282
+ this.checkRulesArray(fileData.rules);
2283
+ }
2284
+ checkFlagsArray(flags) {
2285
+ if (flags && !Array.isArray(flags)) {
2286
+ throw new Error('"flags" must be an array');
2287
+ }
2288
+ if (Array.isArray(flags)) {
2289
+ this.validateFlags(flags);
2290
+ }
2291
+ }
2292
+ checkRulesArray(rules) {
2293
+ if (rules && !Array.isArray(rules)) {
2294
+ throw new Error('"rules" must be an array');
2295
+ }
2296
+ if (Array.isArray(rules)) {
2297
+ this.validateRules(rules);
2298
+ }
2299
+ }
2300
+ validateFlags(flags) {
2301
+ flags.forEach((flag, index) => {
2302
+ if (!flag || typeof flag !== "object") {
2303
+ throw new Error(`Flag at index ${index} must be an object`);
2304
+ }
2305
+ const flagObj = flag;
2306
+ if (!flagObj.key || typeof flagObj.key !== "string") {
2307
+ throw new Error(`Flag at index ${index} must have a "key" property`);
2308
+ }
2309
+ if (flagObj.value === void 0) {
2310
+ throw new Error(`Flag "${flagObj.key}" must have a "value" property`);
2311
+ }
2312
+ });
2313
+ }
2314
+ validateRules(rules) {
2315
+ rules.forEach((rule, index) => {
2316
+ if (!rule || typeof rule !== "object") {
2317
+ throw new Error(`Rule at index ${index} must be an object`);
2318
+ }
2319
+ const ruleObj = rule;
2320
+ if (!ruleObj.id || typeof ruleObj.id !== "string") {
2321
+ throw new Error(`Rule at index ${index} must have an "id" property`);
2322
+ }
2323
+ if (!ruleObj.flagKey || typeof ruleObj.flagKey !== "string") {
2324
+ throw new Error(`Rule "${ruleObj.id}" must have a "flagKey" property`);
2325
+ }
2326
+ if (!Array.isArray(ruleObj.conditions)) {
2327
+ throw new Error(`Rule "${ruleObj.id}" must have a "conditions" array`);
2328
+ }
2329
+ });
2330
+ }
2331
+ /**
2332
+ * Creates a default configuration file with values from features.
2333
+ *
2334
+ * @private
2335
+ * @param filePath - Path where to create the file
2336
+ * @param format - File format (json or yaml)
2337
+ */
2338
+ async createDefaultFile(filePath, format) {
2339
+ const dir = path.dirname(filePath);
2340
+ await mkdir2(dir, { recursive: true });
2341
+ const defaultData = {
2342
+ flags: this.createDefaultFlags(),
2343
+ rules: []
2344
+ };
2345
+ let content;
2346
+ if (format === "json") {
2347
+ content = JSON.stringify(defaultData, null, 2);
2348
+ } else {
2349
+ content = yaml.stringify(defaultData);
2350
+ }
2351
+ await writeFile2(filePath, content, "utf-8");
2352
+ this.log(`Created default feature flag file at: ${filePath}`);
2353
+ }
2354
+ /**
2355
+ * Creates default flags from the features configuration.
2356
+ *
2357
+ * @private
2358
+ * @returns Array of default flags
2359
+ */
2360
+ createDefaultFlags() {
2361
+ return Object.entries(this.features).map(([key, value]) => ({
2362
+ key,
2363
+ value,
2364
+ isEnabled: true,
2365
+ name: key,
2366
+ description: `Default flag for ${key}`,
2367
+ type: typeof value === "boolean" ? "boolean" : typeof value === "number" ? "number" : typeof value === "string" ? "string" : "json",
2368
+ environment: "development",
2369
+ createdAt: /* @__PURE__ */ new Date(),
2370
+ updatedAt: /* @__PURE__ */ new Date(),
2371
+ createdBy: "system",
2372
+ updatedBy: "system",
2373
+ metadata: {},
2374
+ tags: []
2375
+ }));
2376
+ }
2081
2377
  /**
2082
2378
  * Sets up file watching for hot reload if enabled.
2083
2379
  *
@@ -2087,8 +2383,32 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2087
2383
  if (!this.config.fileConfig?.shouldWatchForChanges) {
2088
2384
  return;
2089
2385
  }
2090
- this.log("File watching would be enabled for hot reload");
2091
- this.log("File path:", this.config.fileConfig.filePath);
2386
+ const { filePath } = this.config.fileConfig;
2387
+ const resolvedPath = this.resolveFilePath(filePath);
2388
+ try {
2389
+ this.fileWatcher = fs.watch(resolvedPath, async (eventType) => {
2390
+ if (eventType === "change") {
2391
+ this.log(`File changed: ${resolvedPath}`);
2392
+ if (this.fileCheckInterval) {
2393
+ clearTimeout(this.fileCheckInterval);
2394
+ }
2395
+ this.fileCheckInterval = setTimeout(async () => {
2396
+ try {
2397
+ const content = await readFile2(resolvedPath, "utf-8");
2398
+ if (content !== this.lastFileContent) {
2399
+ this.log("File content changed, refreshing...");
2400
+ await this.refresh();
2401
+ }
2402
+ } catch (error) {
2403
+ this.log("Error reading changed file:", error);
2404
+ }
2405
+ }, this.config.fileConfig?.fileCheckInterval ?? FILE_CHECK_INTERVAL_DEFAULT);
2406
+ }
2407
+ });
2408
+ this.log(`File watching enabled for: ${resolvedPath}`);
2409
+ } catch (error) {
2410
+ this.log(`Failed to set up file watching: ${error}`);
2411
+ }
2092
2412
  }
2093
2413
  /**
2094
2414
  * Disposes of the file provider and stops file watching.
@@ -2096,10 +2416,57 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2096
2416
  dispose() {
2097
2417
  super.dispose();
2098
2418
  if (this.fileWatcher) {
2099
- clearInterval(this.fileWatcher);
2419
+ this.fileWatcher.close();
2100
2420
  this.fileWatcher = void 0;
2101
2421
  this.log("File watching stopped");
2102
2422
  }
2423
+ if (this.fileCheckInterval) {
2424
+ clearTimeout(this.fileCheckInterval);
2425
+ this.fileCheckInterval = void 0;
2426
+ }
2427
+ }
2428
+ /**
2429
+ * Refreshes the provider by fetching latest data from the file.
2430
+ * Overrides base class to store rules.
2431
+ *
2432
+ * @returns Promise that resolves when refresh is complete
2433
+ */
2434
+ async refresh() {
2435
+ await super.refresh();
2436
+ }
2437
+ /**
2438
+ * Updates the features object and writes to file.
2439
+ * This allows updating the FEATURES at runtime and persisting to file.
2440
+ *
2441
+ * @param newFeatures - New features object to sync with
2442
+ */
2443
+ async syncFeatures(newFeatures) {
2444
+ this.log("Syncing with new FEATURES values and updating file");
2445
+ this.features = newFeatures;
2446
+ const fileData = {
2447
+ flags: this.createDefaultFlags(),
2448
+ rules: this.rules || []
2449
+ };
2450
+ const { filePath, format } = this.config.fileConfig;
2451
+ const resolvedPath = this.resolveFilePath(filePath);
2452
+ try {
2453
+ let content;
2454
+ if (format === "json") {
2455
+ content = JSON.stringify(fileData, null, 2);
2456
+ } else {
2457
+ content = yaml.stringify(fileData);
2458
+ }
2459
+ await writeFile2(resolvedPath, content, "utf-8");
2460
+ this.lastFileContent = content;
2461
+ this.engine.updateDefaults(newFeatures);
2462
+ await this.refresh();
2463
+ this.log(`Synced ${Object.keys(newFeatures).length} features to file: ${resolvedPath}`);
2464
+ } catch (error) {
2465
+ this.log("Error syncing features to file:", error);
2466
+ throw new Error(
2467
+ `Failed to sync features to file: ${error instanceof Error ? error.message : String(error)}`
2468
+ );
2469
+ }
2103
2470
  }
2104
2471
  /**
2105
2472
  * Gets information about the file provider.
@@ -2107,18 +2474,23 @@ var FileFeatureFlagProvider = class extends FeatureFlagProvider {
2107
2474
  * @returns File provider information
2108
2475
  */
2109
2476
  getFileInfo() {
2477
+ const filePath = this.config.fileConfig?.filePath;
2478
+ const resolvedPath = filePath ? this.resolveFilePath(filePath) : void 0;
2479
+ let lastModified;
2480
+ if (resolvedPath) {
2481
+ try {
2482
+ const stats = fs.statSync(resolvedPath);
2483
+ lastModified = stats.mtime;
2484
+ } catch {
2485
+ }
2486
+ }
2110
2487
  return {
2111
- filePath: this.config.fileConfig?.filePath,
2488
+ filePath,
2489
+ resolvedPath,
2112
2490
  format: this.config.fileConfig?.format,
2113
2491
  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
- ]
2492
+ isImplemented: true,
2493
+ lastModified
2122
2494
  };
2123
2495
  }
2124
2496
  };
@@ -2431,8 +2803,6 @@ Examples:
2431
2803
  };
2432
2804
  }
2433
2805
  };
2434
-
2435
- // src/domain/featureFlags/providers/factory.ts
2436
2806
  var PROVIDER_REGISTRY = {
2437
2807
  memory: MemoryFeatureFlagProvider,
2438
2808
  file: FileFeatureFlagProvider,
@@ -2535,25 +2905,69 @@ var FeatureFlagProviderFactory = class {
2535
2905
  };
2536
2906
  }
2537
2907
  /**
2538
- * Creates a default memory provider for testing environments.
2908
+ * Creates a default provider.
2909
+ * Uses file provider if features are not provided, memory provider otherwise.
2539
2910
  *
2540
- * @param features - Record of feature flag keys to their default values
2911
+ * @param features - Record of feature flag keys to their default values (optional)
2541
2912
  * @param overrides - Optional configuration overrides
2542
- * @returns Memory provider instance
2913
+ * @returns Provider instance
2543
2914
  */
2544
2915
  static createDefault(features, overrides) {
2916
+ const provider = features ? "memory" : "file";
2545
2917
  const defaultConfig = {
2546
- provider: "memory",
2918
+ provider,
2547
2919
  isCacheEnabled: true,
2548
- cacheTtl: 300,
2549
- // 5 minutes
2920
+ cacheTtl: FEATURE_FLAG_CACHE_TTL_DEFAULT,
2550
2921
  refreshInterval: 0,
2551
2922
  // No auto-refresh
2552
2923
  isLoggingEnabled: false,
2553
2924
  shouldFallbackToDefaults: true,
2554
2925
  ...overrides
2555
2926
  };
2556
- return this.create(defaultConfig, features);
2927
+ if (defaultConfig.provider === "file" && !defaultConfig.fileConfig) {
2928
+ defaultConfig.fileConfig = {
2929
+ filePath: FEATURE_FLAG_FILE_PATHS.DEFAULT,
2930
+ format: "json",
2931
+ shouldWatchForChanges: false
2932
+ };
2933
+ }
2934
+ return this.create(defaultConfig, features ?? {});
2935
+ }
2936
+ /**
2937
+ * Type guard to check if a provider supports feature syncing.
2938
+ *
2939
+ * @param provider - The provider instance to check
2940
+ * @returns True if provider has syncFeatures method
2941
+ */
2942
+ static isSyncableProvider(provider) {
2943
+ return "syncFeatures" in provider && typeof provider.syncFeatures === "function";
2944
+ }
2945
+ /**
2946
+ * Updates features on a provider if it supports the syncFeatures method.
2947
+ * This is useful for providers like MemoryProvider that can update their features at runtime.
2948
+ *
2949
+ * @param provider - The provider instance
2950
+ * @param newFeatures - New features to sync
2951
+ * @returns Promise that resolves when sync is complete
2952
+ * @throws Error if provider doesn't support feature syncing
2953
+ */
2954
+ static async syncFeatures(provider, newFeatures) {
2955
+ if (this.isSyncableProvider(provider)) {
2956
+ await provider.syncFeatures(newFeatures);
2957
+ } else {
2958
+ throw new Error(
2959
+ `Provider type does not support feature syncing. Only providers with syncFeatures method (like MemoryProvider and FileProvider) support this operation.`
2960
+ );
2961
+ }
2962
+ }
2963
+ /**
2964
+ * Checks if a provider supports feature syncing.
2965
+ *
2966
+ * @param provider - The provider instance to check
2967
+ * @returns True if provider supports syncFeatures method
2968
+ */
2969
+ static supportsFeaturesSync(provider) {
2970
+ return this.isSyncableProvider(provider);
2557
2971
  }
2558
2972
  /**
2559
2973
  * Validates provider configuration before instantiation.
@@ -2606,8 +3020,11 @@ var FeatureFlagProviderFactory = class {
2606
3020
  validator();
2607
3021
  }
2608
3022
  static validateFileConfig(config) {
2609
- if (!config.fileConfig) {
2610
- throw new Error("File configuration is required for file provider");
3023
+ if (config.fileConfig) {
3024
+ const { format } = config.fileConfig;
3025
+ if (format && !["json", "yaml"].includes(format)) {
3026
+ throw new Error('File format must be either "json" or "yaml"');
3027
+ }
2611
3028
  }
2612
3029
  }
2613
3030
  static validateRedisConfig(config) {