Skip to content

Commit 94e45f5

Browse files
committed
refactor: extract shared heartbeat interval-minute helpers
Deduplicate formatIntervalMinutes, parseIntervalMinutes, clampIntervalMinutes, and the derived minute constants + assertions that were copy-pasted between WorkspaceHeartbeatModal and HeartbeatSection into a single shared module at src/browser/utils/heartbeatIntervalMinutes.ts. Also adds intervalMinutesToMs() to replace the raw MS_PER_MINUTE multiplication that appeared in both callers.
1 parent 1ba44cf commit 94e45f5

3 files changed

Lines changed: 88 additions & 97 deletions

File tree

src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.tsx

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,22 @@ import { Input } from "@/browser/components/Input/Input";
1111
import { Switch } from "@/browser/components/Switch/Switch";
1212
import { useWorkspaceHeartbeat } from "@/browser/hooks/useWorkspaceHeartbeat";
1313
import assert from "@/common/utils/assert";
14+
import {
15+
clampIntervalMinutes,
16+
formatIntervalMinutes,
17+
HEARTBEAT_DEFAULT_INTERVAL_MINUTES,
18+
HEARTBEAT_MAX_INTERVAL_MINUTES,
19+
HEARTBEAT_MIN_INTERVAL_MINUTES,
20+
intervalMinutesToMs,
21+
parseIntervalMinutes,
22+
} from "@/browser/utils/heartbeatIntervalMinutes";
1423
import {
1524
HEARTBEAT_DEFAULT_CONTEXT_MODE,
1625
HEARTBEAT_DEFAULT_INTERVAL_MS,
1726
HEARTBEAT_DEFAULT_MESSAGE_BODY,
18-
HEARTBEAT_MAX_INTERVAL_MS,
19-
HEARTBEAT_MIN_INTERVAL_MS,
2027
type HeartbeatContextMode,
2128
} from "@/constants/heartbeat";
2229

23-
const MS_PER_MINUTE = 60_000;
24-
const HEARTBEAT_MIN_INTERVAL_MINUTES = HEARTBEAT_MIN_INTERVAL_MS / MS_PER_MINUTE;
25-
const HEARTBEAT_MAX_INTERVAL_MINUTES = HEARTBEAT_MAX_INTERVAL_MS / MS_PER_MINUTE;
26-
const HEARTBEAT_DEFAULT_INTERVAL_MINUTES = HEARTBEAT_DEFAULT_INTERVAL_MS / MS_PER_MINUTE;
27-
28-
assert(
29-
Number.isInteger(HEARTBEAT_MIN_INTERVAL_MINUTES),
30-
"Workspace heartbeat minimum interval must be a whole number of minutes"
31-
);
32-
assert(
33-
Number.isInteger(HEARTBEAT_MAX_INTERVAL_MINUTES),
34-
"Workspace heartbeat maximum interval must be a whole number of minutes"
35-
);
36-
assert(
37-
Number.isInteger(HEARTBEAT_DEFAULT_INTERVAL_MINUTES),
38-
"Workspace heartbeat default interval must be a whole number of minutes"
39-
);
40-
4130
const HEARTBEAT_CONTEXT_MODE_OPTIONS: Array<{
4231
value: HeartbeatContextMode;
4332
label: string;
@@ -74,33 +63,6 @@ interface WorkspaceHeartbeatModalProps {
7463
onOpenChange: (open: boolean) => void;
7564
}
7665

77-
function formatIntervalMinutes(intervalMs: number): string {
78-
if (!Number.isFinite(intervalMs)) {
79-
return String(HEARTBEAT_DEFAULT_INTERVAL_MINUTES);
80-
}
81-
82-
const roundedMinutes = Math.round(intervalMs / MS_PER_MINUTE);
83-
return String(clampIntervalMinutes(roundedMinutes));
84-
}
85-
86-
function parseIntervalMinutes(value: string): number | null {
87-
const trimmedValue = value.trim();
88-
if (trimmedValue.length === 0 || !/^\d+$/.test(trimmedValue)) {
89-
return null;
90-
}
91-
92-
const minutes = Number.parseInt(trimmedValue, 10);
93-
return Number.isInteger(minutes) ? minutes : null;
94-
}
95-
96-
function clampIntervalMinutes(minutes: number): number {
97-
assert(Number.isInteger(minutes), "Workspace heartbeat minutes must be a whole number");
98-
return Math.min(
99-
HEARTBEAT_MAX_INTERVAL_MINUTES,
100-
Math.max(HEARTBEAT_MIN_INTERVAL_MINUTES, minutes)
101-
);
102-
}
103-
10466
function getValidationErrorMessage(value: string): string | null {
10567
const minutes = parseIntervalMinutes(value);
10668
if (minutes == null) {
@@ -221,7 +183,7 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
221183

222184
const didSave = await save({
223185
enabled: draftEnabled,
224-
intervalMs: parsedMinutes * MS_PER_MINUTE,
186+
intervalMs: intervalMinutesToMs(parsedMinutes),
225187
contextMode: draftContextMode,
226188
// Read directly from the textarea on save so the final keystroke is preserved even if the
227189
// click lands before React finishes flushing the last state update.

src/browser/features/Settings/Sections/HeartbeatSection.tsx

Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,19 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
22

33
import { Input } from "@/browser/components/Input/Input";
44
import { useAPI } from "@/browser/contexts/API";
5-
import assert from "@/common/utils/assert";
5+
import {
6+
clampIntervalMinutes,
7+
formatIntervalMinutes,
8+
HEARTBEAT_MAX_INTERVAL_MINUTES,
9+
HEARTBEAT_MIN_INTERVAL_MINUTES,
10+
intervalMinutesToMs,
11+
parseIntervalMinutes,
12+
} from "@/browser/utils/heartbeatIntervalMinutes";
613
import {
714
HEARTBEAT_DEFAULT_INTERVAL_MS,
815
HEARTBEAT_DEFAULT_MESSAGE_BODY,
9-
HEARTBEAT_MAX_INTERVAL_MS,
10-
HEARTBEAT_MIN_INTERVAL_MS,
1116
} from "@/constants/heartbeat";
1217

13-
const MS_PER_MINUTE = 60_000;
14-
const HEARTBEAT_MIN_INTERVAL_MINUTES = HEARTBEAT_MIN_INTERVAL_MS / MS_PER_MINUTE;
15-
const HEARTBEAT_MAX_INTERVAL_MINUTES = HEARTBEAT_MAX_INTERVAL_MS / MS_PER_MINUTE;
16-
const HEARTBEAT_DEFAULT_INTERVAL_MINUTES = HEARTBEAT_DEFAULT_INTERVAL_MS / MS_PER_MINUTE;
17-
18-
assert(
19-
Number.isInteger(HEARTBEAT_MIN_INTERVAL_MINUTES),
20-
"Heartbeat minimum interval must be a whole number of minutes"
21-
);
22-
assert(
23-
Number.isInteger(HEARTBEAT_MAX_INTERVAL_MINUTES),
24-
"Heartbeat maximum interval must be a whole number of minutes"
25-
);
26-
assert(
27-
Number.isInteger(HEARTBEAT_DEFAULT_INTERVAL_MINUTES),
28-
"Heartbeat default interval must be a whole number of minutes"
29-
);
30-
31-
function formatIntervalMinutes(intervalMs: number | undefined): string {
32-
if (intervalMs == null || !Number.isFinite(intervalMs)) {
33-
return String(HEARTBEAT_DEFAULT_INTERVAL_MINUTES);
34-
}
35-
36-
const roundedMinutes = Math.round(intervalMs / MS_PER_MINUTE);
37-
return String(clampIntervalMinutes(roundedMinutes));
38-
}
39-
40-
function parseIntervalMinutes(value: string): number | null {
41-
const trimmedValue = value.trim();
42-
if (trimmedValue.length === 0 || !/^\d+$/.test(trimmedValue)) {
43-
return null;
44-
}
45-
46-
const minutes = Number.parseInt(trimmedValue, 10);
47-
return Number.isInteger(minutes) ? minutes : null;
48-
}
49-
50-
function clampIntervalMinutes(minutes: number): number {
51-
assert(Number.isInteger(minutes), "Heartbeat minutes must be a whole number");
52-
return Math.min(
53-
HEARTBEAT_MAX_INTERVAL_MINUTES,
54-
Math.max(HEARTBEAT_MIN_INTERVAL_MINUTES, minutes)
55-
);
56-
}
57-
5818
export function HeartbeatSection() {
5919
const { api } = useAPI();
6020
const [heartbeatDefaultPrompt, setHeartbeatDefaultPrompt] = useState("");
@@ -192,7 +152,7 @@ export function HeartbeatSection() {
192152
})
193153
.then(() =>
194154
api.config.updateHeartbeatDefaultIntervalMs({
195-
intervalMs: clampedMinutes * MS_PER_MINUTE,
155+
intervalMs: intervalMinutesToMs(clampedMinutes),
196156
})
197157
)
198158
.then(() => {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import assert from "@/common/utils/assert";
2+
import {
3+
HEARTBEAT_DEFAULT_INTERVAL_MS,
4+
HEARTBEAT_MAX_INTERVAL_MS,
5+
HEARTBEAT_MIN_INTERVAL_MS,
6+
} from "@/constants/heartbeat";
7+
8+
const MS_PER_MINUTE = 60_000;
9+
10+
export const HEARTBEAT_MIN_INTERVAL_MINUTES = HEARTBEAT_MIN_INTERVAL_MS / MS_PER_MINUTE;
11+
export const HEARTBEAT_MAX_INTERVAL_MINUTES = HEARTBEAT_MAX_INTERVAL_MS / MS_PER_MINUTE;
12+
export const HEARTBEAT_DEFAULT_INTERVAL_MINUTES = HEARTBEAT_DEFAULT_INTERVAL_MS / MS_PER_MINUTE;
13+
14+
assert(
15+
Number.isInteger(HEARTBEAT_MIN_INTERVAL_MINUTES),
16+
"Heartbeat minimum interval must be a whole number of minutes"
17+
);
18+
assert(
19+
Number.isInteger(HEARTBEAT_MAX_INTERVAL_MINUTES),
20+
"Heartbeat maximum interval must be a whole number of minutes"
21+
);
22+
assert(
23+
Number.isInteger(HEARTBEAT_DEFAULT_INTERVAL_MINUTES),
24+
"Heartbeat default interval must be a whole number of minutes"
25+
);
26+
27+
/**
28+
* Convert a stored interval (milliseconds) to a display string in minutes.
29+
* Falls back to the default when the value is missing or non-finite.
30+
*/
31+
export function formatIntervalMinutes(intervalMs: number | undefined): string {
32+
if (intervalMs == null || !Number.isFinite(intervalMs)) {
33+
return String(HEARTBEAT_DEFAULT_INTERVAL_MINUTES);
34+
}
35+
36+
const roundedMinutes = Math.round(intervalMs / MS_PER_MINUTE);
37+
return String(clampIntervalMinutes(roundedMinutes));
38+
}
39+
40+
/**
41+
* Parse a user-entered string into whole minutes, or `null` if it isn't a valid integer.
42+
*/
43+
export function parseIntervalMinutes(value: string): number | null {
44+
const trimmedValue = value.trim();
45+
if (trimmedValue.length === 0 || !/^\d+$/.test(trimmedValue)) {
46+
return null;
47+
}
48+
49+
const minutes = Number.parseInt(trimmedValue, 10);
50+
return Number.isInteger(minutes) ? minutes : null;
51+
}
52+
53+
/**
54+
* Clamp whole minutes to the allowed heartbeat range.
55+
*/
56+
export function clampIntervalMinutes(minutes: number): number {
57+
assert(Number.isInteger(minutes), "Heartbeat minutes must be a whole number");
58+
return Math.min(
59+
HEARTBEAT_MAX_INTERVAL_MINUTES,
60+
Math.max(HEARTBEAT_MIN_INTERVAL_MINUTES, minutes)
61+
);
62+
}
63+
64+
/**
65+
* Convert whole minutes back to milliseconds for storage.
66+
*/
67+
export function intervalMinutesToMs(minutes: number): number {
68+
return minutes * MS_PER_MINUTE;
69+
}

0 commit comments

Comments
 (0)