implement support for string form human durations in config

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-11-17 18:54:56 +01:00
parent 994506952c
commit d52d7f9935
19 changed files with 515 additions and 163 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config': minor
---
Make `readDurationFromConfig` support both ISO and ms formats as well, to make it easier to enter time as an end user
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch
'@backstage/plugin-notifications-backend-module-email': patch
'@backstage/plugin-events-backend-module-aws-sqs': patch
'@backstage/backend-plugin-api': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/backend-defaults': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-auth-backend': patch
---
Support ISO and ms string forms of durations in config too
+5 -5
View File
@@ -474,8 +474,8 @@ export interface Config {
cache?:
| {
store: 'memory';
/** An optional default TTL (in milliseconds). */
defaultTtl?: number | HumanDuration;
/** An optional default TTL (in milliseconds, if given as a number). */
defaultTtl?: number | HumanDuration | string;
}
| {
store: 'redis';
@@ -484,8 +484,8 @@ export interface Config {
* @visibility secret
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number | HumanDuration;
/** An optional default TTL (in milliseconds, if given as a number). */
defaultTtl?: number | HumanDuration | string;
/**
* Whether or not [useRedisSets](https://github.com/jaredwray/keyv/tree/main/packages/redis#useredissets) should be configured to this redis cache.
* Defaults to true if unspecified.
@@ -500,7 +500,7 @@ export interface Config {
*/
connection: string;
/** An optional default TTL (in milliseconds). */
defaultTtl?: number | HumanDuration;
defaultTtl?: number | HumanDuration | string;
};
cors?: {
@@ -185,6 +185,6 @@ describe('CacheManager integration', () => {
},
}),
),
).toThrow(/Invalid configuration backend.cache.defaultTtl/);
).toThrow(/Invalid duration 'hello' in config/);
});
});
@@ -24,6 +24,7 @@ import Keyv from 'keyv';
import { DefaultCacheClient } from './CacheClient';
import { CacheManagerOptions, ttlToMilliseconds } from './types';
import { durationToMilliseconds } from '@backstage/types';
import { readDurationFromConfig } from '@backstage/config';
type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;
@@ -75,17 +76,12 @@ export class CacheManager {
});
let defaultTtl: number | undefined;
if (defaultTtlConfig !== undefined && defaultTtlConfig !== null) {
if (defaultTtlConfig !== undefined) {
if (typeof defaultTtlConfig === 'number') {
defaultTtl = defaultTtlConfig;
} else if (
typeof defaultTtlConfig === 'object' &&
!Array.isArray(defaultTtlConfig)
) {
defaultTtl = durationToMilliseconds(defaultTtlConfig);
} else {
throw new Error(
`Invalid configuration backend.cache.defaultTtl: ${defaultTtlConfig}, expected milliseconds number or HumanDuration object`,
defaultTtl = durationToMilliseconds(
readDurationFromConfig(config, { key: 'backend.cache.defaultTtl' }),
);
}
}
@@ -354,19 +354,6 @@ export interface SchedulerService {
getScheduledTasks(): Promise<SchedulerServiceTaskDescriptor[]>;
}
function readDuration(config: Config, key: string): HumanDuration {
if (typeof config.get(key) === 'string') {
const value = config.getString(key);
const duration = Duration.fromISO(value);
if (!duration.isValid) {
throw new Error(`Invalid duration: ${value}`);
}
return duration.toObject();
}
return readDurationFromConfig(config, { key });
}
function readFrequency(
config: Config,
key: string,
@@ -382,7 +369,7 @@ function readFrequency(
return { trigger: 'manual' };
}
return readDuration(config, key);
return readDurationFromConfig(config, { key });
}
/**
@@ -396,10 +383,10 @@ export function readSchedulerServiceTaskScheduleDefinitionFromConfig(
config: Config,
): SchedulerServiceTaskScheduleDefinition {
const frequency = readFrequency(config, 'frequency');
const timeout = readDuration(config, 'timeout');
const timeout = readDurationFromConfig(config, { key: 'timeout' });
const initialDelay = config.has('initialDelay')
? readDuration(config, 'initialDelay')
? readDurationFromConfig(config, { key: 'initialDelay' })
: undefined;
const scope = config.getOptionalString('scope');
+2 -1
View File
@@ -37,7 +37,8 @@
},
"dependencies": {
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^"
"@backstage/types": "workspace:^",
"ms": "^2.1.3"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
@@ -15,114 +15,254 @@
*/
import {
readDurationFromConfig,
propsOfHumanDuration,
readDurationFromConfig,
} from './readDurationFromConfig';
import { ConfigReader } from './reader';
describe('readDurationFromConfig', () => {
it('reads all known keys', () => {
const config = new ConfigReader({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
describe('ISO form', () => {
it('parses the known forms', () => {
const config = new ConfigReader({
d1: 'P2DT6H',
d2: 'PT0.5S',
d3: 'PT3.1S',
d4: 'P1Y2M3W4DT5H6M7.8S',
});
expect(readDurationFromConfig(config, { key: 'd1' })).toEqual({
days: 2,
hours: 6,
});
expect(readDurationFromConfig(config, { key: 'd2' })).toEqual({
milliseconds: 500,
});
expect(readDurationFromConfig(config, { key: 'd3' })).toEqual({
seconds: 3,
milliseconds: 100,
});
expect(readDurationFromConfig(config, { key: 'd4' })).toEqual({
years: 1,
months: 2,
weeks: 3,
days: 4,
hours: 5,
minutes: 6,
seconds: 7,
milliseconds: 800,
});
});
expect(readDurationFromConfig(config)).toEqual({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
it('throws on errors', () => {
const config = new ConfigReader({
d1: 'P 1Y',
d2: 'P1L',
d3: 'P',
});
expect(() =>
readDurationFromConfig(config, { key: 'd1' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration 'P 1Y' in config at 'd1', Error: Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)"`,
);
expect(() =>
readDurationFromConfig(config, { key: 'd2' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration 'P1L' in config at 'd2', Error: Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)"`,
);
expect(() =>
readDurationFromConfig(config, { key: 'd3' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration 'P' in config at 'd3', Error: Invalid ISO format, no values given"`,
);
});
});
it('reads all known keys, for a subkey', () => {
const config = new ConfigReader({
sub: {
key: {
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
describe('ms form', () => {
it('parses the known units', () => {
// this is not exhaustive, but tests all supported units to ensure that
// our conversion to HumanDuration form works
const config = new ConfigReader({
d1: '1y',
d2: '2 years',
d3: '4w',
d4: '5h',
d5: '6 hrs',
d6: '7min',
d7: '9 minutes',
d8: '3.5 seconds',
d9: '25 ms',
d10: '1850ms',
});
expect(readDurationFromConfig(config, { key: 'd1' })).toEqual({
years: 1,
});
expect(readDurationFromConfig(config, { key: 'd2' })).toEqual({
years: 2,
});
expect(readDurationFromConfig(config, { key: 'd3' })).toEqual({
weeks: 4,
});
expect(readDurationFromConfig(config, { key: 'd4' })).toEqual({
hours: 5,
});
expect(readDurationFromConfig(config, { key: 'd5' })).toEqual({
hours: 6,
});
expect(readDurationFromConfig(config, { key: 'd6' })).toEqual({
minutes: 7,
});
expect(readDurationFromConfig(config, { key: 'd7' })).toEqual({
minutes: 9,
});
expect(readDurationFromConfig(config, { key: 'd8' })).toEqual({
seconds: 3,
milliseconds: 500,
});
expect(readDurationFromConfig(config, { key: 'd9' })).toEqual({
milliseconds: 25,
});
expect(readDurationFromConfig(config, { key: 'd10' })).toEqual({
seconds: 1,
milliseconds: 850,
});
});
it('throws on errors', () => {
const config = new ConfigReader({
d1: '1m 3s',
d2: '-3s',
d3: '',
});
expect(() =>
readDurationFromConfig(config, { key: 'd1' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration '1m 3s' in config at 'd1', Error: Not a valid duration string, try a number followed by a unit such as '1d' or '2 seconds'"`,
);
expect(() =>
readDurationFromConfig(config, { key: 'd2' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration '-3s' in config at 'd2', Error: Negative durations are not allowed"`,
);
expect(() =>
readDurationFromConfig(config, { key: 'd3' }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid type in config for key 'd3' in 'mock-config', got empty-string, wanted string"`,
);
});
});
describe('object form', () => {
it('reads all known keys', () => {
const config = new ConfigReader({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
});
expect(readDurationFromConfig(config)).toEqual({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
});
});
it('reads all known keys, for a subkey', () => {
const config = new ConfigReader({
sub: {
key: {
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
},
},
});
expect(readDurationFromConfig(config, { key: 'sub.key' })).toEqual({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
});
});
it('rejects wrong type of target, for a subkey', () => {
const config = new ConfigReader({
sub: { key: 7 },
});
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
"Failed to read duration from config, TypeError: Invalid type in config for key 'sub.key' in 'mock-config', got number, wanted object",
);
});
it('rejects no keys', () => {
const config = new ConfigReader({});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it('rejects no keys, for a subkey', () => {
const config = new ConfigReader({ sub: { key: {} } });
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
`Failed to read duration from config at 'sub.key', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it('rejects unknown keys', () => {
const config = new ConfigReader({
minutes: 3,
invalid: 'value',
});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it.each(propsOfHumanDuration)('rejects non-number %p', prop => {
const config = new ConfigReader({
[prop]: 'value',
});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Unable to convert config value for key '${prop}' in 'mock-config' to a number`,
);
});
it.each(propsOfHumanDuration)(
'rejects non-number %p, for a subkey',
prop => {
const config = new ConfigReader({
sub: {
key: {
[prop]: 'value',
},
},
});
expect(() =>
readDurationFromConfig(config, { key: 'sub.key' }),
).toThrow(
`Failed to read duration from config, Error: Unable to convert config value for key 'sub.key.${prop}' in 'mock-config' to a number`,
);
},
});
expect(readDurationFromConfig(config, { key: 'sub.key' })).toEqual({
milliseconds: 1,
seconds: 2,
minutes: 3,
hours: 4,
days: 5,
weeks: 6,
months: 7,
years: 8,
});
});
it('rejects wrong type of target, for a subkey', () => {
const config = new ConfigReader({
sub: { key: 7 },
});
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
"Failed to read duration from config, TypeError: Invalid type in config for key 'sub.key' in 'mock-config', got number, wanted object",
);
});
it('rejects no keys', () => {
const config = new ConfigReader({});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it('rejects no keys, for a subkey', () => {
const config = new ConfigReader({ sub: { key: {} } });
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
`Failed to read duration from config at 'sub.key', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it('rejects unknown keys', () => {
const config = new ConfigReader({
minutes: 3,
invalid: 'value',
});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`,
);
});
it.each(propsOfHumanDuration)('rejects non-number %p', prop => {
const config = new ConfigReader({
[prop]: 'value',
});
expect(() => readDurationFromConfig(config)).toThrow(
`Failed to read duration from config, Error: Unable to convert config value for key '${prop}' in 'mock-config' to a number`,
);
});
it.each(propsOfHumanDuration)('rejects non-number %p, for a subkey', prop => {
const config = new ConfigReader({
sub: {
key: {
[prop]: 'value',
},
},
});
expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow(
`Failed to read duration from config, Error: Unable to convert config value for key 'sub.key.${prop}' in 'mock-config' to a number`,
);
});
});
+214 -5
View File
@@ -15,8 +15,9 @@
*/
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { InputError, stringifyError } from '@backstage/errors';
import { HumanDuration } from '@backstage/types';
import ms from 'ms';
export const propsOfHumanDuration = [
'years',
@@ -30,18 +31,29 @@ export const propsOfHumanDuration = [
];
/**
* Reads a duration from a config object.
* Reads a duration from config.
*
* @public
* @remarks
*
* The supported formats are:
*
* - A string in the format of '1d', '2 seconds' etc. as supported by the `ms`
* library.
* - A standard ISO formatted duration string, e.g. 'P2DT6H' or 'PT1M'.
* - An object with individual units (in plural) as keys, e.g. `{ days: 2, hours: 6 }`.
*
* The string forms are naturally only supported if the `options.key` argument
* is passed, since a `Config` argument always represents an object by its
* nature.
*
* This does not support optionality; if you want to support optional durations,
* you need to first check the presence of the target with `config.has(...)` and
* then call this function.
*
* @param config - A configuration object
* @param key - If specified, read the duration from the given subkey
* under the config object
* @param key - If specified, read the duration from the given subkey under the
* config object
* @returns A duration object
*/
export function readDurationFromConfig(
@@ -49,6 +61,33 @@ export function readDurationFromConfig(
options?: {
key?: string;
},
): HumanDuration {
if (options?.key && typeof config.getOptional(options.key) === 'string') {
const value = config.getString(options.key).trim();
try {
return value.startsWith('P')
? parseIsoDuration(value)
: parseMsDuration(value);
} catch (error) {
throw new InputError(
`Invalid duration '${value}' in config at '${
options.key
}', ${stringifyError(error)}`,
);
}
}
return parseObjectDuration(config, options);
}
/**
* Parses the object form of durations.
*/
export function parseObjectDuration(
config: Config,
options?: {
key?: string;
},
): HumanDuration {
let root: Config;
let found = false;
@@ -94,8 +133,178 @@ export function readDurationFromConfig(
if (options?.key) {
prefix += ` at '${options.key}'`;
}
throw new InputError(`${prefix}, ${error}`);
throw new Error(`${prefix}, ${error}`);
}
return result as HumanDuration;
}
/**
* Parses friendly string durations like '1d', '2 seconds' etc using the ms
* library.
*/
export function parseMsDuration(input: string): HumanDuration {
if (/^\d+$/.exec(input)) {
// We explicitly disallow the only-digits form of the ms library, because
// from a configuration perspective it's just confusing to even be able to
// specify that
throw new Error(
`The value cannot be a plain number; try adding a unit like 'ms' or 'seconds'`,
);
}
let milliseconds = ms(input);
if (!Number.isFinite(milliseconds)) {
throw new Error(
`Not a valid duration string, try a number followed by a unit such as '1d' or '2 seconds'`,
);
} else if (milliseconds < 0) {
throw new Error('Negative durations are not allowed');
} else if (milliseconds === 0) {
return { milliseconds: 0 };
}
// As used by the ms library
const s = 1000;
const m = s * 60;
const h = m * 60;
const d = h * 24;
const w = d * 7;
const y = d * 365.25;
const result: HumanDuration = {};
if (milliseconds >= y) {
const years = Math.floor(milliseconds / y);
milliseconds -= years * y;
result.years = years;
}
if (milliseconds >= w) {
const weeks = Math.floor(milliseconds / w);
milliseconds -= weeks * w;
result.weeks = weeks;
}
if (milliseconds >= d) {
const days = Math.floor(milliseconds / d);
milliseconds -= days * d;
result.days = days;
}
if (milliseconds >= h) {
const hours = Math.floor(milliseconds / h);
milliseconds -= hours * h;
result.hours = hours;
}
if (milliseconds >= m) {
const minutes = Math.floor(milliseconds / m);
milliseconds -= minutes * m;
result.minutes = minutes;
}
if (milliseconds >= s) {
const seconds = Math.floor(milliseconds / s);
milliseconds -= seconds * s;
result.seconds = seconds;
}
if (milliseconds > 0) {
result.milliseconds = milliseconds;
}
return result;
}
/**
* Parses an ISO formatted duration string.
*
* Implementation taken from luxon's Duration.fromISO to not force that
* dependency on everyone.
*/
export function parseIsoDuration(input: string): HumanDuration {
const match =
/^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/.exec(
input,
);
if (!match) {
throw new Error(
`Invalid ISO format, expected a value similar to 'P2DT6H' (2 days 6 hours) or 'PT1M' (1 minute)`,
);
}
const [
s,
yearStr,
monthStr,
weekStr,
dayStr,
hourStr,
minuteStr,
secondStr,
millisecondsStr,
] = match;
const hasNegativePrefix = s[0] === '-';
const negativeSeconds = !!secondStr && secondStr[0] === '-';
const maybeNegate = (num: number | undefined, force = false) =>
num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num;
const parseFloating = (value: string) => {
if (typeof value === 'undefined' || value === null || value === '') {
return undefined;
}
return parseFloat(value);
};
const parseMillis = (fraction: string | undefined) => {
// Return undefined (instead of 0) in these cases, where fraction is not set
if (
typeof fraction === 'undefined' ||
fraction === null ||
fraction === ''
) {
return undefined;
}
const f = parseFloat(`0.${fraction}`) * 1000;
return Math.floor(f);
};
const years = maybeNegate(parseFloating(yearStr));
const months = maybeNegate(parseFloating(monthStr));
const weeks = maybeNegate(parseFloating(weekStr));
const days = maybeNegate(parseFloating(dayStr));
const hours = maybeNegate(parseFloating(hourStr));
const minutes = maybeNegate(parseFloating(minuteStr));
const seconds = maybeNegate(parseFloating(secondStr), secondStr === '-0');
const milliseconds = maybeNegate(
parseMillis(millisecondsStr),
negativeSeconds,
);
if (
years === undefined &&
months === undefined &&
weeks === undefined &&
days === undefined &&
hours === undefined &&
minutes === undefined &&
seconds === undefined &&
milliseconds === undefined
) {
throw new Error('Invalid ISO format, no values given');
}
return {
...(years ? { years } : {}),
...(months ? { months } : {}),
...(weeks ? { weeks } : {}),
...(days ? { days } : {}),
...(hours ? { hours } : {}),
...(minutes ? { minutes } : {}),
...(seconds ? { seconds } : {}),
...(milliseconds ? { milliseconds } : {}),
};
}
@@ -42,7 +42,7 @@ export interface Config {
/**
* The backstage token expiration.
*/
backstageTokenExpiration?: HumanDuration;
backstageTokenExpiration?: HumanDuration | string;
};
};
}
+1 -1
View File
@@ -143,7 +143,7 @@ export interface Config {
/**
* The backstage token expiration.
*/
backstageTokenExpiration?: HumanDuration;
backstageTokenExpiration?: HumanDuration | string;
};
/**
* Additional app origins to allow for authenticating
+2 -2
View File
@@ -157,9 +157,9 @@ export interface Config {
/** Defer stitching to be performed asynchronously */
mode: 'deferred';
/** Polling interval for tasks in seconds */
pollingInterval?: HumanDuration;
pollingInterval?: HumanDuration | string;
/** How long to wait for a stitch to complete before giving up in seconds */
stitchTimeout?: HumanDuration;
stitchTimeout?: HumanDuration | string;
};
/**
@@ -70,15 +70,12 @@ export function stitchingStrategyFromConfig(config: Config): StitchingStrategy {
const stitchTimeoutKey = 'catalog.stitchingStrategy.stitchTimeout';
const pollingInterval = config.has(pollingIntervalKey)
? readDurationFromConfig(config, {
key: pollingIntervalKey,
})
? readDurationFromConfig(config, { key: pollingIntervalKey })
: { seconds: 1 };
const stitchTimeout = config.has(stitchTimeoutKey)
? readDurationFromConfig(config, {
key: stitchTimeoutKey,
})
? readDurationFromConfig(config, { key: stitchTimeoutKey })
: { seconds: 60 };
return {
mode: 'deferred',
pollingInterval: pollingInterval,
+4 -4
View File
@@ -48,12 +48,12 @@ export interface Config {
/**
* (Optional) Visibility timeout for messages in flight.
*/
visibilityTimeout: HumanDuration;
visibilityTimeout: HumanDuration | string;
/**
* (Optional) Wait time when polling for available messages.
* Default: 20 seconds.
*/
waitTime: HumanDuration;
waitTime: HumanDuration | string;
};
/**
* (Optional) Timeout for the task execution which includes polling for messages
@@ -62,12 +62,12 @@ export interface Config {
*
* Must be greater than `queue.waitTime` + `waitTimeAfterEmptyReceive`.
*/
timeout: HumanDuration;
timeout: HumanDuration | string;
/**
* (Optional) Wait time before polling again if no message was received.
* Default: 1 minute.
*/
waitTimeAfterEmptyReceive: HumanDuration;
waitTimeAfterEmptyReceive: HumanDuration | string;
};
};
};
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { HumanDuration, JsonObject } from '@backstage/types';
import { Config, readDurationFromConfig } from '@backstage/config';
import { HumanDuration } from '@backstage/types';
import { Duration } from 'luxon';
const CONFIG_PREFIX_MODULE = 'events.modules.awsSqs.';
@@ -39,7 +39,7 @@ function readOptionalHumanDuration(
config: Config,
key: string,
): HumanDuration | undefined {
return config.getOptional<JsonObject>(key) as HumanDuration;
return config.has(key) ? readDurationFromConfig(config, { key }) : undefined;
}
function readOptionalDuration(
+2 -2
View File
@@ -99,7 +99,7 @@ export interface Config {
/**
* Throttle duration between email sending, defaults to 100ms
*/
throttleInterval?: HumanDuration;
throttleInterval?: HumanDuration | string;
/**
* Configuration for broadcast notifications
*/
@@ -120,7 +120,7 @@ export interface Config {
/**
* Email cache TTL, defaults to 1 hour
*/
ttl?: HumanDuration;
ttl?: HumanDuration | string;
};
filter?: {
/**
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
NotificationProcessor,
NotificationSendOptions,
@@ -75,14 +76,17 @@ export class NotificationsEmailProcessor implements NotificationProcessor {
this.replyTo = emailProcessorConfig.getOptionalString('replyTo');
this.concurrencyLimit =
emailProcessorConfig.getOptionalNumber('concurrencyLimit') ?? 2;
const throttleConfig =
emailProcessorConfig.getOptionalConfig('throttleInterval');
this.throttleInterval = throttleConfig
? durationToMilliseconds(readDurationFromConfig(throttleConfig))
this.throttleInterval = emailProcessorConfig.has('throttleInterval')
? durationToMilliseconds(
readDurationFromConfig(emailProcessorConfig, {
key: 'throttleInterval',
}),
)
: 100;
const cacheConfig = emailProcessorConfig.getOptionalConfig('cache.ttl');
this.cacheTtl = cacheConfig
? durationToMilliseconds(readDurationFromConfig(cacheConfig))
this.cacheTtl = emailProcessorConfig.has('cache.ttl')
? durationToMilliseconds(
readDurationFromConfig(emailProcessorConfig, { key: 'cache.ttl' }),
)
: 3_600_000;
this.frontendBaseUrl = config.getString('app.baseUrl');
this.allowlistEmailAddresses = emailProcessorConfig.getOptionalStringArray(
+4 -4
View File
@@ -26,6 +26,7 @@ export interface Config {
name?: string;
email?: string;
};
/**
* The commit message used when new components are created.
*/
@@ -64,16 +65,15 @@ export interface Config {
* be attempted to recover.
*
* If not specified, the default value is 5 seconds.
*
*/
EXPERIMENTAL_recoverTasksTimeout?: HumanDuration;
EXPERIMENTAL_recoverTasksTimeout?: HumanDuration | string;
/**
* Makes sure to auto-expire and clean up things that time out or for other reasons should not be left lingering.
*
* By default, the frequency is every 5 minutes.
*/
taskTimeoutJanitorFrequency?: HumanDuration;
taskTimeoutJanitorFrequency?: HumanDuration | string;
/**
* Sets the task's heartbeat timeout, when to consider a task to be staled.
@@ -82,6 +82,6 @@ export interface Config {
*
* Default value is 24 hours.
*/
taskTimeout?: HumanDuration;
taskTimeout?: HumanDuration | string;
};
}
+1
View File
@@ -4158,6 +4158,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
ms: ^2.1.3
languageName: unknown
linkType: soft