Skip to content

Commit 116ee88

Browse files
authored
refactor: add subscribe and enabled methods to ConfigFilePersister (#1670)
1 parent 413db4b commit 116ee88

File tree

1 file changed

+68
-20
lines changed

1 file changed

+68
-20
lines changed

packages/unraid-shared/src/services/config-file.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ import type { Subscription } from "rxjs";
1111
import { ConfigFileHandler } from "../util/config-file-handler.js";
1212
import { 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

Comments
 (0)