diff --git a/.github/workflows/GH-820-graceperiod.yml b/.github/workflows/GH-820-graceperiod.yml index 24957940..21e9b781 100644 --- a/.github/workflows/GH-820-graceperiod.yml +++ b/.github/workflows/GH-820-graceperiod.yml @@ -51,7 +51,7 @@ jobs: - uses: ./ with: retry-method: 'equal_intervals' - wait-seconds-before-first-polling: '0' + wait-seconds-before-first-polling: '1' min-interval-seconds: '5' attempt-limits: '100' wait-list: | @@ -77,7 +77,7 @@ jobs: - uses: ./ with: retry-method: 'equal_intervals' - wait-seconds-before-first-polling: '0' + wait-seconds-before-first-polling: '1' min-interval-seconds: '5' attempt-limits: '100' wait-list: | diff --git a/CHANGELOG.md b/CHANGELOG.md index b635088e..1bf099e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This file only records notable changes. Not synchronized with all releases and t - main - not yet released - Add `startupGracePeriod` option in wait-list: [#820](https://github.com/kachick/wait-other-jobs/issues/820) + - Restrict `wait-seconds-before-first-polling` if it is too short as zero or shorter than `startupGracePeriod` - v3.2.0 - Add `eventName` option in wait-list: [#771](https://github.com/kachick/wait-other-jobs/issues/771) - v3.1.0 diff --git a/dist/index.js b/dist/index.js index de99a321..4f7dfce5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -31042,6 +31042,12 @@ var MyDurationLike = z2.object({ nanoseconds: z2.number().optional() }).strict().readonly(); var Durationable = z2.union([z2.string().duration(), MyDurationLike]).transform((item) => getDuration(item)); +var Duration = z2.instanceof(mr.Duration).refine( + (d2) => mr.Duration.compare(d2, { seconds: 0 }) > 0, + { + message: "Too short interval for pollings" + } +); var defaultGrace = mr.Duration.from({ seconds: 10 }); function isDurationLike(my) { for (const [_2, value] of Object.entries(my)) { @@ -31053,7 +31059,7 @@ function isDurationLike(my) { } function getDuration(durationable) { if (typeof durationable === "string" || isDurationLike(durationable)) { - return mr.Duration.from(durationable); + return Duration.parse(mr.Duration.from(durationable)); } throw new Error("unexpected value is specified in durations"); } @@ -31079,26 +31085,26 @@ var retryMethods = z2.enum(["exponential_backoff", "equal_intervals"]); var Options = z2.object({ waitList: WaitList, skipList: SkipList, - waitSecondsBeforeFirstPolling: z2.number().min(0), - minIntervalSeconds: z2.number().min(1), + initialDuration: Duration, + leastInterval: Duration, retryMethod: retryMethods, attemptLimits: z2.number().min(1), isEarlyExit: z2.boolean(), shouldSkipSameWorkflow: z2.boolean(), isDryRun: z2.boolean() -}).readonly().refine( +}).strict().readonly().refine( ({ waitList, skipList }) => !(waitList.length > 0 && skipList.length > 0), { message: "Do not specify both wait-list and skip-list", path: ["waitList", "skipList"] } ).refine( - ({ waitSecondsBeforeFirstPolling, waitList }) => waitList.every( + ({ initialDuration, waitList }) => waitList.every( (item) => !(mr.Duration.compare( - { seconds: waitSecondsBeforeFirstPolling }, + initialDuration, item.startupGracePeriod ) > 0 && mr.Duration.compare(item.startupGracePeriod, defaultGrace) !== 0) ), { message: "A shorter startupGracePeriod waiting for the first poll does not make sense", - path: ["waitSecondsBeforeFirstPolling", "waitList"] + path: ["initialDuration", "waitList"] } ); @@ -31142,8 +31148,8 @@ function parseInput() { const shouldSkipSameWorkflow = (0, import_core.getBooleanInput)("skip-same-workflow", { required: true, trimWhitespace: true }); const isDryRun = (0, import_core.getBooleanInput)("dry-run", { required: true, trimWhitespace: true }); const options = Options.parse({ - waitSecondsBeforeFirstPolling, - minIntervalSeconds, + initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }), + leastInterval: Durationable.parse({ seconds: minIntervalSeconds }), retryMethod, attemptLimits, waitList: JSON.parse((0, import_core.getInput)("wait-list", { required: true })), @@ -32360,6 +32366,17 @@ function groupBy(items, callback) { } // src/report.ts +function readableDuration(duration) { + const { hours, minutes, seconds } = duration.round({ largestUnit: "hours" }); + const eachUnit = [`${seconds} seconds`]; + if (minutes > 0) { + eachUnit.unshift(`${minutes} minutes`); + } + if (hours > 0) { + eachUnit.unshift(`${hours} hours`); + } + return `about ${eachUnit.join(" ")}`; +} function summarize(check, trigger) { const { checkRun: run2, checkSuite: suite, workflow, workflowRun } = check; const isCompleted = run2.status === "COMPLETED"; @@ -32482,40 +32499,34 @@ function generateReport(summaries, trigger, elapsed, { waitList, skipList, shoul // src/wait.ts import { setTimeout as setTimeout2 } from "timers/promises"; -var wait = setTimeout2; +var waitPrimitive = setTimeout2; +function wait(interval) { + return waitPrimitive(interval.total("milliseconds")); +} function getRandomInt(min, max) { const flooredMin = Math.ceil(min); return Math.floor(Math.random() * (Math.floor(max) - flooredMin) + flooredMin); } -function readableDuration(milliseconds) { - const msecToSec = 1e3; - const secToMin = 60; - const seconds = milliseconds / msecToSec; - const minutes = seconds / secToMin; - const { unit, value, precision } = minutes >= 1 ? { unit: "minutes", value: minutes, precision: 1 } : { unit: "seconds", value: seconds, precision: 0 }; - const adjustor = 10 ** precision; - return `about ${(Math.round(value * adjustor) / adjustor).toFixed( - precision - )} ${unit}`; -} var MIN_JITTER_MILLISECONDS = 1e3; var MAX_JITTER_MILLISECONDS = 7e3; -function calcExponentialBackoffAndJitter(minIntervalSeconds, attempts) { +function calcExponentialBackoffAndJitter(leastInterval, attempts) { const jitterMilliseconds = getRandomInt(MIN_JITTER_MILLISECONDS, MAX_JITTER_MILLISECONDS); - return minIntervalSeconds * 2 ** (attempts - 1) * 1e3 + jitterMilliseconds; + return mr.Duration.from({ + milliseconds: leastInterval.total("milliseconds") * 2 ** (attempts - 1) + jitterMilliseconds + }); } -function getIdleMilliseconds(method, minIntervalSeconds, attempts) { +function getInterval(method, leastInterval, attempts) { switch (method) { case "exponential_backoff": return calcExponentialBackoffAndJitter( - minIntervalSeconds, + leastInterval, attempts ); case "equal_intervals": - return minIntervalSeconds * 1e3; + return leastInterval; default: { const _exhaustiveCheck = method; - return minIntervalSeconds * 1e3; + return leastInterval; } } } @@ -32568,16 +32579,15 @@ async function run() { break; } if (attempts === 1) { - const initialMsec = options.waitSecondsBeforeFirstPolling * 1e3; - (0, import_core3.info)(`Wait ${readableDuration(initialMsec)} before first polling.`); - await wait(initialMsec); + (0, import_core3.info)(`Wait ${readableDuration(options.initialDuration)} before first polling.`); + await wait(options.initialDuration); } else { - const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts); - (0, import_core3.info)(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`); - await wait(msec); + const interval = getInterval(options.retryMethod, options.leastInterval, attempts); + (0, import_core3.info)(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`); + await wait(interval); } const elapsed = mr.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) }); - (0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()}(${elapsed.toString()}) ~`); + (0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`); const checks = await fetchChecks(githubToken, trigger); if ((0, import_core3.isDebug)()) { (0, import_core3.debug)(JSON.stringify({ label: "rawdata", checks, elapsed }, null, 2)); diff --git a/src/input.ts b/src/input.ts index 75c166e5..ac5e0305 100644 --- a/src/input.ts +++ b/src/input.ts @@ -1,7 +1,7 @@ import { debug, getInput, getBooleanInput, setSecret, isDebug, error } from '@actions/core'; import { context } from '@actions/github'; -import { Options, Trigger } from './schema.ts'; +import { Durationable, Options, Trigger } from './schema.ts'; export function parseInput(): { trigger: Trigger; options: Options; githubToken: string } { const { @@ -45,8 +45,8 @@ export function parseInput(): { trigger: Trigger; options: Options; githubToken: const isDryRun = getBooleanInput('dry-run', { required: true, trimWhitespace: true }); const options = Options.parse({ - waitSecondsBeforeFirstPolling, - minIntervalSeconds, + initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }), + leastInterval: Durationable.parse({ seconds: minIntervalSeconds }), retryMethod, attemptLimits, waitList: JSON.parse(getInput('wait-list', { required: true })), diff --git a/src/main.ts b/src/main.ts index 107e80fc..eb0c57c5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,8 +24,8 @@ function colorize(severity: Severity, message: string): string { import { parseInput } from './input.ts'; import { fetchChecks } from './github-api.ts'; -import { Severity, generateReport, getSummaries } from './report.ts'; -import { readableDuration, wait, getIdleMilliseconds } from './wait.ts'; +import { Severity, generateReport, getSummaries, readableDuration } from './report.ts'; +import { getInterval, wait } from './wait.ts'; import { Temporal } from 'temporal-polyfill'; async function run(): Promise { @@ -58,18 +58,17 @@ async function run(): Promise { } if (attempts === 1) { - const initialMsec = options.waitSecondsBeforeFirstPolling * 1000; - info(`Wait ${readableDuration(initialMsec)} before first polling.`); - await wait(initialMsec); + info(`Wait ${readableDuration(options.initialDuration)} before first polling.`); + await wait(options.initialDuration); } else { - const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts); - info(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`); - await wait(msec); + const interval = getInterval(options.retryMethod, options.leastInterval, attempts); + info(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`); + await wait(interval); } // Put getting elapsed time before of fetchChecks to keep accuracy of the purpose const elapsed = Temporal.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) }); - startGroup(`Polling ${attempts}: ${(new Date()).toISOString()}(${elapsed.toString()}) ~`); + startGroup(`Polling ${attempts}: ${(new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`); const checks = await fetchChecks(githubToken, trigger); if (isDebug()) { debug(JSON.stringify({ label: 'rawdata', checks, elapsed }, null, 2)); diff --git a/src/report.test.ts b/src/report.test.ts index b0371391..f09b8b28 100644 --- a/src/report.test.ts +++ b/src/report.test.ts @@ -1,11 +1,20 @@ import test from 'node:test'; import assert from 'node:assert'; import { checks8679817057, checks92810686811WaitSuccessPolling1 } from './snapshot.ts'; -import { Report, Summary, generateReport, getSummaries } from './report.ts'; +import { Report, Summary, generateReport, getSummaries, readableDuration } from './report.ts'; import { omit } from './util.ts'; import { Temporal } from 'temporal-polyfill'; import { jsonEqual } from './assert.ts'; +test('readableDuration', () => { + assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 454356 })), 'about 7 minutes 34 seconds'); + assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 32100 })), 'about 32 seconds'); + assert.strictEqual( + readableDuration(Temporal.Duration.from({ hours: 4, minutes: 100, seconds: 79 })), + 'about 5 hours 41 minutes 19 seconds', + ); +}); + const exampleSummary = Object.freeze( { isAcceptable: false, diff --git a/src/report.ts b/src/report.ts index 9a5ea0b9..732f1493 100644 --- a/src/report.ts +++ b/src/report.ts @@ -4,6 +4,18 @@ import { join, relative } from 'path'; import { Temporal } from 'temporal-polyfill'; import { groupBy } from './util.ts'; +export function readableDuration(duration: Temporal.Duration): string { + const { hours, minutes, seconds } = duration.round({ largestUnit: 'hours' }); + const eachUnit = [`${seconds} seconds`]; + if (minutes > 0) { + eachUnit.unshift(`${minutes} minutes`); + } + if (hours > 0) { + eachUnit.unshift(`${hours} hours`); + } + return `about ${eachUnit.join(' ')}`; +} + export interface Summary { isAcceptable: boolean; isCompleted: boolean; diff --git a/src/schema.test.ts b/src/schema.test.ts index b4b057c8..ac076d87 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -1,5 +1,5 @@ import test from 'node:test'; -import { deepStrictEqual, throws } from 'node:assert'; +import { throws } from 'node:assert'; import { Durationable, Options } from './schema.ts'; import { Temporal } from 'temporal-polyfill'; import { durationEqual, optionsEqual } from './assert.ts'; @@ -9,21 +9,21 @@ const defaultOptions = Object.freeze({ attemptLimits: 1000, waitList: [], skipList: [], - waitSecondsBeforeFirstPolling: 10, - minIntervalSeconds: 15, + initialDuration: Temporal.Duration.from({ seconds: 10 }), + leastInterval: Temporal.Duration.from({ seconds: 15 }), retryMethod: 'equal_intervals', shouldSkipSameWorkflow: false, isDryRun: false, }); test('Options keep given values', () => { - deepStrictEqual({ + optionsEqual({ isEarlyExit: true, attemptLimits: 1000, waitList: [], skipList: [], - waitSecondsBeforeFirstPolling: 10, - minIntervalSeconds: 15, + initialDuration: Temporal.Duration.from({ seconds: 10 }), + leastInterval: Temporal.Duration.from({ seconds: 15 }), retryMethod: 'equal_intervals', shouldSkipSameWorkflow: false, isDryRun: false, @@ -45,9 +45,9 @@ test('Options set some default values it cannot be defined in action.yml', () => }); test('Options reject invalid values', () => { - throws(() => Options.parse({ ...defaultOptions, minIntervalSeconds: 0 }), { + throws(() => Options.parse({ ...defaultOptions, leastInterval: Temporal.Duration.from({ seconds: 0 }) }), { name: 'ZodError', - message: /too_small/, + message: /Too short interval for pollings/, }); throws(() => Options.parse({ ...defaultOptions, attemptLimits: 0 }), { @@ -182,7 +182,7 @@ test('wait-list have startupGracePeriod', async (t) => { () => Options.parse({ ...defaultOptions, - waitSecondsBeforeFirstPolling: 41, + initialDuration: Temporal.Duration.from({ seconds: 41 }), waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 40 } }], }), { @@ -196,12 +196,12 @@ test('wait-list have startupGracePeriod', async (t) => { optionsEqual( Options.parse({ ...defaultOptions, - waitSecondsBeforeFirstPolling: 42, + initialDuration: Temporal.Duration.from({ seconds: 42 }), waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 10 } }], }), { ...defaultOptions, - waitSecondsBeforeFirstPolling: 42, + initialDuration: Temporal.Duration.from({ seconds: 42 }), waitList: [{ workflowFile: 'ci.yml', optional: false, @@ -213,12 +213,12 @@ test('wait-list have startupGracePeriod', async (t) => { optionsEqual( Options.parse({ ...defaultOptions, - waitSecondsBeforeFirstPolling: 42, + initialDuration: Temporal.Duration.from({ seconds: 42 }), waitList: [{ workflowFile: 'ci.yml' }], }), { ...defaultOptions, - waitSecondsBeforeFirstPolling: 42, + initialDuration: Temporal.Duration.from({ seconds: 42 }), waitList: [{ workflowFile: 'ci.yml', optional: false, diff --git a/src/schema.ts b/src/schema.ts index 57467d23..8857ad79 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -37,6 +37,13 @@ type MyDurationLike = z.infer; // IETF does not define duration formats in their RFCs, but in RFC 3399 refers ISO 8601 duration formats. // https://www.ietf.org/rfc/rfc3339.txt export const Durationable = z.union([z.string().duration(), MyDurationLike]).transform((item) => getDuration(item)); +export const Duration = z.instanceof(Temporal.Duration).refine( + (d) => Temporal.Duration.compare(d, { seconds: 0 }) > 0, + { + message: 'Too short interval for pollings', + }, +); +type Duration = z.infer; const defaultGrace = Temporal.Duration.from({ seconds: 10 }); // workaround for https://github.com/colinhacks/zod/issues/635 @@ -51,9 +58,9 @@ function isDurationLike(my: MyDurationLike): my is DurationLike { } // workaround for https://github.com/colinhacks/zod/issues/635 -export function getDuration(durationable: string | MyDurationLike): Temporal.Duration { +export function getDuration(durationable: string | MyDurationLike): Duration { if (typeof durationable === 'string' || isDurationLike(durationable)) { - return Temporal.Duration.from(durationable); + return Duration.parse(Temporal.Duration.from(durationable)); } throw new Error('unexpected value is specified in durations'); @@ -89,29 +96,29 @@ export type RetryMethod = z.infer; export const Options = z.object({ waitList: WaitList, skipList: SkipList, - waitSecondsBeforeFirstPolling: z.number().min(0), - minIntervalSeconds: z.number().min(1), + initialDuration: Duration, + leastInterval: Duration, retryMethod: retryMethods, attemptLimits: z.number().min(1), isEarlyExit: z.boolean(), shouldSkipSameWorkflow: z.boolean(), isDryRun: z.boolean(), -}).readonly().refine( +}).strict().readonly().refine( ({ waitList, skipList }) => !(waitList.length > 0 && skipList.length > 0), { message: 'Do not specify both wait-list and skip-list', path: ['waitList', 'skipList'] }, ).refine( - ({ waitSecondsBeforeFirstPolling, waitList }) => + ({ initialDuration, waitList }) => waitList.every( (item) => !(Temporal.Duration.compare( - { seconds: waitSecondsBeforeFirstPolling }, + initialDuration, item.startupGracePeriod, ) > 0 && Temporal.Duration.compare(item.startupGracePeriod, defaultGrace) !== 0), ), { message: 'A shorter startupGracePeriod waiting for the first poll does not make sense', - path: ['waitSecondsBeforeFirstPolling', 'waitList'], + path: ['initialDuration', 'waitList'], }, ); diff --git a/src/wait.test.ts b/src/wait.test.ts index 9ea0d6bf..c7622fcc 100644 --- a/src/wait.test.ts +++ b/src/wait.test.ts @@ -1,17 +1,17 @@ import { wait, calcExponentialBackoffAndJitter, - readableDuration, MIN_JITTER_MILLISECONDS, MAX_JITTER_MILLISECONDS, - getIdleMilliseconds, + getInterval, } from './wait.ts'; import test from 'node:test'; import assert from 'node:assert'; +import { Temporal } from 'temporal-polyfill'; test('wait 100 ms', async () => { performance.mark('start'); - await wait(100); + await wait(Temporal.Duration.from({ milliseconds: 100 })); performance.mark('end'); // The void typing looks like a wrong definition of @types/node const measure: unknown = performance.measure('Wait duration', 'start', 'end'); @@ -24,30 +24,34 @@ test('wait 100 ms', async () => { }); test('interval will be like a cheap exponential backoff', () => { - const minIntervalSeconds = 100; + const leastInterval = Temporal.Duration.from({ seconds: 100 }); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 1) >= (100000 + MIN_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 1) < (100000 + MAX_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 2) >= (200000 + MIN_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 2) < (200000 + MAX_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 3) >= (400000 + MIN_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 3) < (400000 + MAX_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 4) >= (800000 + MIN_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 4) < (800000 + MAX_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 5) >= (1600000 + MIN_JITTER_MILLISECONDS)); - assert(calcExponentialBackoffAndJitter(minIntervalSeconds, 5) < (1600000 + MAX_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 1).total('milliseconds') >= (100000 + MIN_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 1).total('milliseconds') < (100000 + MAX_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 2).total('milliseconds') >= (200000 + MIN_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 2).total('milliseconds') < (200000 + MAX_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 3).total('milliseconds') >= (400000 + MIN_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 3).total('milliseconds') < (400000 + MAX_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 4).total('milliseconds') >= (800000 + MIN_JITTER_MILLISECONDS)); + assert(calcExponentialBackoffAndJitter(leastInterval, 4).total('milliseconds') < (800000 + MAX_JITTER_MILLISECONDS)); + assert( + calcExponentialBackoffAndJitter(leastInterval, 5).total('milliseconds') >= (1600000 + MIN_JITTER_MILLISECONDS), + ); + assert(calcExponentialBackoffAndJitter(leastInterval, 5).total('milliseconds') < (1600000 + MAX_JITTER_MILLISECONDS)); }); -test('readableDuration', () => { - assert.strictEqual(readableDuration(454356), 'about 7.6 minutes'); - assert.strictEqual(readableDuration(32100), 'about 32 seconds'); -}); - -test('getIdleMilliseconds returns different value with the given method', () => { - const minIntervalSeconds = 100; +test('getInterval returns different value with the given method', () => { + const leastInterval = Temporal.Duration.from({ seconds: 100 }); - assert(getIdleMilliseconds('exponential_backoff', minIntervalSeconds, 5) >= (1600000 + MIN_JITTER_MILLISECONDS)); - assert(getIdleMilliseconds('exponential_backoff', minIntervalSeconds, 5) < (1600000 + MAX_JITTER_MILLISECONDS)); + assert( + getInterval('exponential_backoff', leastInterval, 5).total('milliseconds') >= (1600000 + MIN_JITTER_MILLISECONDS), + ); + assert( + getInterval('exponential_backoff', leastInterval, 5).total('milliseconds') < (1600000 + MAX_JITTER_MILLISECONDS), + ); - assert.strictEqual(getIdleMilliseconds('equal_intervals', minIntervalSeconds, 5), minIntervalSeconds * 1000); + assert.strictEqual( + Temporal.Duration.compare(getInterval('equal_intervals', leastInterval, 5), leastInterval), + 0, + ); }); diff --git a/src/wait.ts b/src/wait.ts index b950a856..3466a2a5 100644 --- a/src/wait.ts +++ b/src/wait.ts @@ -1,8 +1,12 @@ import { setTimeout } from 'timers/promises'; import { RetryMethod } from './schema.ts'; +import { Temporal } from 'temporal-polyfill'; // Just aliasing to avoid misusing setTimeout between ES method and timers/promises version. -export const wait = setTimeout; +export const waitPrimitive = setTimeout; +export function wait(interval: Temporal.Duration) { + return waitPrimitive(interval.total('milliseconds')); +} // Taken from MDN // The maximum is exclusive and the minimum is inclusive @@ -11,47 +15,35 @@ function getRandomInt(min: number, max: number) { return Math.floor((Math.random() * (Math.floor(max) - flooredMin)) + flooredMin); } -// 454356 milliseconds => 7.5725999999999996 minutes => about 7.6 minutes -export function readableDuration(milliseconds: number): string { - const msecToSec = 1000; - const secToMin = 60; - - const seconds = milliseconds / msecToSec; - const minutes = seconds / secToMin; - const { unit, value, precision }: { unit: string; value: number; precision: number } = minutes >= 1 - ? { unit: 'minutes', value: minutes, precision: 1 } - : { unit: 'seconds', value: seconds, precision: 0 }; - const adjustor = 10 ** precision; - return `about ${ - (Math.round(value * adjustor) / adjustor).toFixed( - precision, - ) - } ${unit}`; -} - export const MIN_JITTER_MILLISECONDS = 1000; export const MAX_JITTER_MILLISECONDS = 7000; export function calcExponentialBackoffAndJitter( - minIntervalSeconds: number, + leastInterval: Temporal.Duration, attempts: number, -): number { +): Temporal.Duration { const jitterMilliseconds = getRandomInt(MIN_JITTER_MILLISECONDS, MAX_JITTER_MILLISECONDS); - return ((minIntervalSeconds * (2 ** (attempts - 1))) * 1000) + jitterMilliseconds; + return Temporal.Duration.from({ + milliseconds: (leastInterval.total('milliseconds') * (2 ** (attempts - 1))) + jitterMilliseconds, + }); } -export function getIdleMilliseconds(method: RetryMethod, minIntervalSeconds: number, attempts: number): number { +export function getInterval( + method: RetryMethod, + leastInterval: Temporal.Duration, + attempts: number, +): Temporal.Duration { switch (method) { case ('exponential_backoff'): return calcExponentialBackoffAndJitter( - minIntervalSeconds, + leastInterval, attempts, ); case ('equal_intervals'): - return minIntervalSeconds * 1000; + return leastInterval; default: { const _exhaustiveCheck: never = method; - return minIntervalSeconds * 1000; + return leastInterval; } } }