Skip to content
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ This package allows you to configure various options for loading and managing co
### `ignoreServerIsOlderVersionError`
- **Type**: `boolean`
- **Optional**: `true`
- **Default**: `false`
- **Description**: Indicates whether to ignore the error when the server version is older than the requested version.
- **Environment Variable**: `CONFIG_IGNORE_SERVER_IS_OLDER_VERSION_ERROR`

Expand All @@ -128,6 +129,13 @@ This package allows you to configure various options for loading and managing co
- **Default**: `./config`
- **Description**: The path to the local configuration folder.

### `disableHotReload`
- **Type**: `boolean`
- **Optional**: `true`
- **Default**: `false`
- **Description**: Indicates whether hot-reloading should be disabled. If true, the SDK fetches the remote configuration exactly once upon startup and never starts the background polling loop.
- **Environment Variable**: `CONFIG_DISABLE_HOT_RELOAD`

### `pollIntervalMs`
- **Type**: `number`
- **Optional**: `true`
Expand All @@ -150,6 +158,7 @@ The following environment variables can be used to configure the options:
- `CONFIG_OFFLINE_MODE`: Sets the `offlineMode` option.
- `CONFIG_IGNORE_SERVER_IS_OLDER_VERSION_ERROR`: Sets the `ignoreServerIsOlderVersionError` option.
- `CONFIG_POLL_INTERVAL_MS`: Sets the `pollIntervalMs` option.
- `CONFIG_DISABLE_HOT_RELOAD`: Sets the `disableHotReload` option.

## Configuration Merging and Validation

Expand Down
12 changes: 7 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
debug('config called with options: %j', { ...options, schema: options.schema.$id });
const { schema: baseSchema, metricsRegistry, onChange, ...unvalidatedOptions } = options;
const initOptions = initializeOptions(unvalidatedOptions);
const { configName, offlineMode, version, ignoreServerIsOlderVersionError } = initOptions;
const { configName, offlineMode, version, ignoreServerIsOlderVersionError, disableHotReload } = initOptions;

// Load Local and Env Configs First (Independent of remote state)
const dereferencedSchema = await loadSchema(baseSchema);
Expand Down Expand Up @@ -79,7 +79,7 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
let validatedConfig: ReturnType<typeof mergeAndValidate>;

// Handle Remote Config and Polling
if (offlineMode !== true) {
if (!offlineMode) {
debug('handling fetching remote data');
// check if the server is using an older version of the schemas package
const capabilitiesResponse = await getServerCapabilities();
Expand All @@ -92,7 +92,7 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
satisfies: semverSatisfies,
});
}
if (ignoreServerIsOlderVersionError !== true && gt(LOCAL_SCHEMAS_PACKAGE_VERSION, capabilitiesResponse.schemasPackageVersion)) {
if (!ignoreServerIsOlderVersionError && gt(LOCAL_SCHEMAS_PACKAGE_VERSION, capabilitiesResponse.schemasPackageVersion)) {
debug(
'server is using an older version of the schemas package. local: %s, remote: %s',
LOCAL_SCHEMAS_PACKAGE_VERSION,
Expand Down Expand Up @@ -131,8 +131,10 @@ export async function config<T extends { [typeSymbol]: unknown; $id: string }>(
validatedConfig = mergeAndValidate(remoteConfig);

// Setup polling
changeDetector = new ChangeDetector(baseSchema.$id, initOptions, currentEtag, onChange);
changeDetector.start();
if (!disableHotReload) {
changeDetector = new ChangeDetector(baseSchema.$id, initOptions, currentEtag, onChange);
changeDetector.start();
}
} else {
// If offline, bypass remote and just merge local/env
validatedConfig = mergeAndValidate({});
Expand Down
1 change: 1 addition & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const configErrors = {
schemaVersionMismatchError: { code: 7, payload: {} as { remoteSchemaVersion: string; localSchemaVersion: string } },
promClientNotInstalledError: { code: 8, payload: {} as { message: string } },
serverVersionMismatchError: { code: 9, payload: {} as { remoteServerVersion: string; localServerVersion: string; satisfies: string } },
onChangeCallbackMissingError: { code: 10, payload: {} },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as const satisfies Record<string, { code: number; payload: any }>;

Expand Down
2 changes: 1 addition & 1 deletion src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function initializeMetrics(registry: Registry, schemaId: string, actualVe
name: configName,
request_version: version,
actual_version: actualVersion,
offline_mode: String(offlineMode ?? false),
offline_mode: String(offlineMode),
schemas_package_version: LOCAL_SCHEMAS_PACKAGE_VERSION,
package_version: PACKAGE_VERSION,
schema_id: schemaId,
Expand Down
5 changes: 5 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const defaultOptions: BaseOptions = {
configServerUrl: 'http://localhost:8080',
version: 'latest',
pollIntervalMs: 30000,
offlineMode: false,
ignoreServerIsOlderVersionError: false,
localConfigPath: './config',
disableHotReload: false,
terminatePod: true,
};

Expand All @@ -22,6 +26,7 @@ const envOptions: Partial<Record<keyof BaseOptions, string>> = {
offlineMode: process.env.CONFIG_OFFLINE_MODE,
ignoreServerIsOlderVersionError: process.env.CONFIG_IGNORE_SERVER_IS_OLDER_VERSION_ERROR,
pollIntervalMs: process.env.CONFIG_POLL_INTERVAL_MS,
disableHotReload: process.env.CONFIG_DISABLE_HOT_RELOAD,
terminatePod: process.env.CONFIG_TERMINATE_POD,
};

Expand Down
2 changes: 1 addition & 1 deletion src/rollout/ChangeDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class ChangeDetector {
if (this.timer) {
clearTimeout(this.timer);
}
const baseInterval = this.options.pollIntervalMs!;
const baseInterval = this.options.pollIntervalMs;
const jitter = baseInterval * JITTER_PERCENTAGE;
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const randomJitter = (Math.random() * 2 - 1) * jitter;
Expand Down
23 changes: 15 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,27 @@ export interface BaseOptions {
/**
* Indicates whether the configuration should be loaded in offline mode.
*/
offlineMode?: boolean;
offlineMode: boolean;
/**
* Indicates whether to ignore the error when the server version is older than the requested version.
*/
ignoreServerIsOlderVersionError?: boolean;
ignoreServerIsOlderVersionError: boolean;
/**
* The path to the local configuration folder.
* @default './config'
*/
localConfigPath?: string;
localConfigPath: string;
/**
* The polling interval in milliseconds.
* @default 30000
*/
pollIntervalMs?: number;
pollIntervalMs: number;
/**
* Indicates whether hot-reloading should be disabled.
* If true, the SDK fetches the remote configuration exactly once upon startup.
* @default false
*/
disableHotReload: boolean;
/**
* Indicates whether the pod will be terminated after an update.
* @default true
Expand Down Expand Up @@ -112,10 +118,11 @@ export const optionsSchema: JSONSchemaType<BaseOptions> = {
],
},
configServerUrl: { type: 'string' },
offlineMode: { type: 'boolean', nullable: true },
ignoreServerIsOlderVersionError: { type: 'boolean', nullable: true },
localConfigPath: { type: 'string', default: './config', nullable: true },
pollIntervalMs: { type: 'integer', default: 30000, nullable: true },
offlineMode: { type: 'boolean' },
ignoreServerIsOlderVersionError: { type: 'boolean' },
localConfigPath: { type: 'string', default: './config' },
pollIntervalMs: { type: 'integer', default: 30000 },
disableHotReload: { type: 'boolean', default: false },
terminatePod: { type: 'boolean', default: true },
},
};
Expand Down
2 changes: 2 additions & 0 deletions tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ describe('config', () => {
localConfigPath: './tests/config',
offlineMode: true,
pollIntervalMs: 3000,
disableHotReload: false,
ignoreServerIsOlderVersionError: false,
terminatePod: true,
});
});
Expand Down
28 changes: 28 additions & 0 deletions tests/rollout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,32 @@ describe('Continuous Polling (ChangeDetector)', () => {
expect(time).toBeLessThanOrEqual(maxWait);
});
});

it('should not start polling if disableHotReload is true', async () => {
// Arrange
const initialConfigData = createMockConfigData();

client
.intercept({ path: '/capabilities', method: 'GET' })
.reply(StatusCodes.OK, { serverVersion: '2.0.0', schemasPackageVersion: '99.9.9', pubSubEnabled: false });
client
.intercept({ path: `/config/name/1?shouldDereference=true&schemaId=${commonDbPartialV1.$id}`, method: 'GET' })
.reply(StatusCodes.OK, initialConfigData, { headers: { etag: 'etag-1' } });

// Act
await config({
configName: 'name',
version: 1,
schema: commonDbPartialV1,
localConfigPath: './tests/config',
onChange: onChangeMock,
disableHotReload: true,
});

// Advance time beyond the polling interval
await vi.advanceTimersByTimeAsync(DEFAULT_POLL_INTERVAL * (1 + JITTER_PERCENTAGE) + 1);

// Assert
expect(onChangeMock).not.toHaveBeenCalled();
});
});
Loading