@@ -11,6 +11,19 @@ import type { Subscription } from "rxjs";
1111import { ConfigFileHandler } from "../util/config-file-handler.js" ;
1212import { ConfigDefinition } from "../util/config-definition.js" ;
1313
14+ export type ConfigSubscription = {
15+ /**
16+ * Called when the config changes.
17+ * To prevent race conditions, a config is not provided to the callback.
18+ */
19+ next ?: ( ) => Promise < void > ;
20+
21+ /**
22+ * Called when an error occurs within the subscriber.
23+ */
24+ error ?: ( error : unknown ) => Promise < void > ;
25+ } ;
26+
1427/**
1528 * Abstract base class for persisting configuration objects to JSON files.
1629 *
@@ -44,7 +57,7 @@ export abstract class ConfigFilePersister<T extends object>
4457
4558 /**
4659 * Creates a new ConfigFilePersister instance.
47- *
60+ *
4861 * @param configService The NestJS ConfigService instance for reactive config management
4962 */
5063 constructor ( protected readonly configService : ConfigService ) {
@@ -66,9 +79,18 @@ export abstract class ConfigFilePersister<T extends object>
6679 */
6780 abstract configKey ( ) : string ;
6881
82+ /**
83+ * Support feature flagging or dynamic toggling of config persistence.
84+ *
85+ * @returns Whether the config is enabled. Defaults to true.
86+ */
87+ enabled ( ) : boolean {
88+ return true ;
89+ }
90+
6991 /**
7092 * Returns a `structuredClone` of the current config object.
71- *
93+ *
7294 * @param assertExists - Whether to throw an error if the config does not exist. Defaults to true.
7395 * @returns The current config object, or the default config if assertExists is false & no config exists
7496 */
@@ -90,7 +112,7 @@ export abstract class ConfigFilePersister<T extends object>
90112
91113 /**
92114 * Replaces the current config with a new one. Will trigger a persistence attempt.
93- *
115+ *
94116 * @param config - The new config object
95117 */
96118 replaceConfig ( config : T ) {
@@ -101,7 +123,7 @@ export abstract class ConfigFilePersister<T extends object>
101123 /**
102124 * Returns the absolute path to the configuration file.
103125 * Combines `PATHS_CONFIG_MODULES` environment variable with the filename.
104- *
126+ *
105127 * @throws Error if `PATHS_CONFIG_MODULES` environment variable is not set
106128 */
107129 configPath ( ) : string {
@@ -132,46 +154,72 @@ export abstract class ConfigFilePersister<T extends object>
132154 * Loads config from disk and sets up reactive change subscription.
133155 */
134156 async onModuleInit ( ) {
157+ if ( ! this . enabled ( ) ) return ;
135158 this . logger . verbose ( `Config path: ${ this . configPath ( ) } ` ) ;
136159 await this . loadOrMigrateConfig ( ) ;
137160
138- this . configObserver = this . configService . changes$
139- . pipe ( bufferTime ( 25 ) )
140- . subscribe ( {
141- next : async ( changes ) => {
142- const configChanged = changes . some ( ( { path } ) =>
143- path ?. startsWith ( this . configKey ( ) )
144- ) ;
145- if ( configChanged ) {
146- await this . persist ( ) ;
147- }
148- } ,
149- error : ( err ) => {
150- this . logger . error ( "Error receiving config changes:" , err ) ;
151- } ,
152- } ) ;
161+ this . configObserver = this . subscribe ( {
162+ next : async ( ) => {
163+ await this . persist ( ) ;
164+ } ,
165+ error : async ( err ) => {
166+ this . logger . error ( err , "Error receiving config changes" ) ;
167+ } ,
168+ } ) ;
153169 }
154170
155171 /**
156172 * Persists configuration to disk with change detection optimization.
157- *
173+ *
158174 * @param config - The config object to persist (defaults to current config from service)
159175 * @returns `true` if persisted to disk, `false` if skipped or failed
160176 */
161177 async persist (
162178 config = this . configService . get ( this . configKey ( ) )
163179 ) : Promise < boolean > {
180+ if ( ! this . enabled ( ) ) {
181+ this . logger . verbose ( `Config is disabled, skipping persistence` ) ;
182+ return false ;
183+ }
164184 if ( ! config ) {
165185 this . logger . warn ( `Cannot persist undefined config` ) ;
166186 return false ;
167187 }
168188 return await this . fileHandler . writeConfigFile ( config ) ;
169189 }
170190
191+ /**
192+ * Subscribe to config changes. Changes are buffered for 25ms to prevent race conditions.
193+ *
194+ * When enabled() returns false, the `next` callback will not be called.
195+ *
196+ * @param subscription - The subscription to add
197+ * @returns rxjs Subscription
198+ */
199+ subscribe ( subscription : ConfigSubscription ) {
200+ return this . configService . changes$ . pipe ( bufferTime ( 25 ) ) . subscribe ( {
201+ next : async ( changes ) => {
202+ if ( ! subscription . next ) return ;
203+ const configChanged = changes . some ( ( { path } ) =>
204+ path ?. startsWith ( this . configKey ( ) )
205+ ) ;
206+ if ( configChanged && this . enabled ( ) ) {
207+ await subscription . next ( ) ;
208+ }
209+ } ,
210+ error : async ( err ) => {
211+ if ( subscription . error ) {
212+ await subscription . error ( err ) ;
213+ }
214+ } ,
215+ } ) ;
216+ }
217+
171218 /**
172219 * Load or migrate configuration and set it in ConfigService.
173220 */
174221 private async loadOrMigrateConfig ( ) {
222+ if ( ! this . enabled ( ) ) return ;
175223 const config = await this . fileHandler . loadConfig ( ) ;
176224 this . configService . set ( this . configKey ( ) , config ) ;
177225 return this . persist ( config ) ;
0 commit comments