Skip to content

Commit 13da597

Browse files
committed
fix(webapp,run-engine,core): treat an empty maxDelay as invalid and document the duration grammar
An empty string passed as maxDelay was falsy everywhere it was checked, so it read as no ceiling at all rather than as the invalid value it is. The server ceiling now also rejects zero and negative values at startup, and the duration grammar in the errors and JSDoc lists the hr suffix and compound forms that the parser has always accepted.
1 parent d7dd33b commit 13da597

6 files changed

Lines changed: 30 additions & 12 deletions

File tree

.changeset/debounce-max-duration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Debouncing with a `delay` longer than an hour now works. A hidden server-side li
66

77
That limit is gone. A debounce key with no `maxDelay` now keeps pushing its run back for as long as triggers keep arriving, which means it never executes while they do. Set `maxDelay` when the work has to happen eventually, and keep `delay` well below it, since the room available to push is the gap between the two.
88

9+
Triggers that set a `maxDelay` no longer than their `delay` are now rejected, as are unparseable `maxDelay` values. Both previously went through and left debouncing doing nothing.
10+
911
```ts
1012
await myTask.trigger(payload, {
1113
debounce: {

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ const EnvironmentSchema = z
10411041
* bound. Setting this applies a ceiling to every debounced run that does not carry its own
10421042
* `maxDelay`, and any `delay` at or above it stops runs from being pushed at all.
10431043
*/
1044-
RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS: z.coerce.number().int().optional(),
1044+
RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS: z.coerce.number().int().positive().optional(),
10451045

10461046
/**
10471047
* Bucket size in milliseconds used to quantize the newly computed `delayUntil`

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export class RunEngineTriggerTaskService {
140140
#validateDebounceMaxDelay(
141141
debounce: NonNullable<NonNullable<TriggerTaskRequestBody["options"]>["debounce"]>
142142
) {
143-
if (!debounce.maxDelay) {
143+
if (debounce.maxDelay === undefined) {
144144
return;
145145
}
146146

@@ -149,7 +149,8 @@ export class RunEngineTriggerTaskService {
149149
if (maxDelayMs === undefined) {
150150
throw new ServiceValidationError(
151151
`Invalid debounce maxDelay: ${debounce.maxDelay}. ` +
152-
`Supported formats: {number}s, {number}m, {number}h, {number}d, {number}w`
152+
`Supported formats: {number}s, {number}m, {number}h or {number}hr, {number}d, {number}w, ` +
153+
`optionally combined (for example "2h30m").`
153154
);
154155
}
155156

@@ -306,7 +307,8 @@ export class RunEngineTriggerTaskService {
306307
if (debounceDelayError || !debounceDelayUntil) {
307308
throw new ServiceValidationError(
308309
`Invalid debounce delay: ${body.options.debounce.delay}. ` +
309-
`Supported formats: {number}s, {number}m, {number}h, {number}d, {number}w`
310+
`Supported formats: {number}s, {number}m, {number}h or {number}hr, {number}d, ` +
311+
`{number}w, optionally combined (for example "2h30m").`
310312
);
311313
}
312314

apps/webapp/test/engine/triggerTask.debounce.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,17 @@ describe("RunEngineTriggerTaskService", () => {
536536
triggerWithDebounce({ key: "unparseable", delay: "10s", maxDelay: "soon" })
537537
).rejects.toThrow(/Invalid debounce maxDelay/);
538538

539+
await expect(
540+
triggerWithDebounce({ key: "empty", delay: "10s", maxDelay: "" })
541+
).rejects.toThrow(/Invalid debounce maxDelay/);
542+
543+
const compound = await triggerWithDebounce({
544+
key: "compound",
545+
delay: "2h30m",
546+
maxDelay: "1d",
547+
});
548+
expect(compound?.run.friendlyId).toBeDefined();
549+
539550
const withRoom = await triggerWithDebounce({
540551
key: "with-room",
541552
delay: "10s",

internal-packages/run-engine/src/engine/systems/debounceSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ return 0
579579
* which is itself unset by default. An unparseable `maxDelay` falls back to the ceiling.
580580
*/
581581
#resolveMaxDurationMs(debounce: DebounceOptions): number | undefined {
582-
if (!debounce.maxDelay) {
582+
if (debounce.maxDelay === undefined) {
583583
return this.maxDebounceDurationMs;
584584
}
585585

packages/core/src/v3/types/tasks.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -971,11 +971,14 @@ export type TriggerOptions = {
971971
* the new execution time stays inside `maxDelay`, so a `delay` at or above `maxDelay` leaves
972972
* no room to push and every trigger creates its own run.
973973
*
974-
* Supported formats: `{number}s` (seconds), `{number}m` (minutes), `{number}h` (hours),
975-
* `{number}d` (days), `{number}w` (weeks). Must be a duration, not a date. Minimum delay is
976-
* 1 second.
974+
* Must be a duration, not a date: the value is re-applied every time the run is pushed
975+
* back, so an absolute date cannot work and debouncing silently stops collapsing.
977976
*
978-
* @example "1s", "5s", "1m", "30m", "1h"
977+
* Supported formats: `{number}s` (seconds), `{number}m` (minutes), `{number}h` or
978+
* `{number}hr` (hours), `{number}d` (days), `{number}w` (weeks), optionally combined.
979+
* Minimum delay is 1 second.
980+
*
981+
* @example "1s", "5s", "1m", "30m", "1h", "2h30m"
979982
*/
980983
delay: string;
981984
/**
@@ -1001,10 +1004,10 @@ export type TriggerOptions = {
10011004
* `maxDelay` of `"5m"` keeps extending for just under 5 minutes from the first trigger,
10021005
* then runs.
10031006
*
1004-
* Supported formats: `{number}s` (seconds), `{number}m` (minutes), `{number}h` (hours),
1005-
* `{number}d` (days), `{number}w` (weeks).
1007+
* Supported formats: `{number}s` (seconds), `{number}m` (minutes), `{number}h` or
1008+
* `{number}hr` (hours), `{number}d` (days), `{number}w` (weeks), optionally combined.
10061009
*
1007-
* @example "30m", "2h", "1d"
1010+
* @example "30m", "2h", "1d", "2h30m"
10081011
*/
10091012
maxDelay?: string;
10101013
};

0 commit comments

Comments
 (0)