diff --git a/README.md b/README.md index 73edeb5..177a965 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` @@ -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 diff --git a/src/config.ts b/src/config.ts index 2e68865..bca0bcb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -43,7 +43,7 @@ export async function config( 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); @@ -79,7 +79,7 @@ export async function config( let validatedConfig: ReturnType; // 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(); @@ -92,7 +92,7 @@ export async function config( 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, @@ -131,8 +131,10 @@ export async function config( 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({}); diff --git a/src/errors.ts b/src/errors.ts index bcb1d37..d0732a9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -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; diff --git a/src/metrics.ts b/src/metrics.ts index d45b686..cc8547b 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -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, diff --git a/src/options.ts b/src/options.ts index a40893b..4fda7ca 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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, }; @@ -22,6 +26,7 @@ const envOptions: Partial> = { 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, }; diff --git a/src/rollout/ChangeDetector.ts b/src/rollout/ChangeDetector.ts index 55c850e..8cc3fd3 100644 --- a/src/rollout/ChangeDetector.ts +++ b/src/rollout/ChangeDetector.ts @@ -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; diff --git a/src/types.ts b/src/types.ts index 014da30..ea3163d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 @@ -112,10 +118,11 @@ export const optionsSchema: JSONSchemaType = { ], }, 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 }, }, }; diff --git a/tests/config.spec.ts b/tests/config.spec.ts index 4e6447c..753bab8 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -215,6 +215,8 @@ describe('config', () => { localConfigPath: './tests/config', offlineMode: true, pollIntervalMs: 3000, + disableHotReload: false, + ignoreServerIsOlderVersionError: false, terminatePod: true, }); }); diff --git a/tests/rollout.spec.ts b/tests/rollout.spec.ts index 4439b6c..04c0ff3 100644 --- a/tests/rollout.spec.ts +++ b/tests/rollout.spec.ts @@ -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(); + }); });