Skip to content

refactor(site): expose time values in render functions as centralized, pure state #17587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
refactor: slight cleanup
  • Loading branch information
Parkreiner committed Apr 29, 2025
commit 1bc946355fc03f2bd59493d49dde13aeb4d80c6d
35 changes: 16 additions & 19 deletions site/src/hooks/useTimeSync.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/**
* @todo Things that still need to be done before this can be called done:
*
* 1. Finish up the interval reconciliation method
* 2. Update the class to respect the resyncOnNewSubscription option
* 3. Add tests
* 3. Make it so that if you provide an explicit type parameter to the hook
* call, you must provide a runtime select function
* 4. Add tests
*/
import {
createContext,
Expand All @@ -15,18 +18,18 @@ import {
type PropsWithChildren,
} from "react";

export const MAX_REFRESH_ONE_SECOND = 1_000;
export const MAX_REFRESH_ONE_MINUTE = 60 * 1_000;
export const MAX_REFRESH_ONE_HOUR = 60 * 60 * 1_000;
export const MAX_REFRESH_ONE_DAY = 24 * 60 * 60 * 1_000;
export const IDEAL_REFRESH_ONE_SECOND = 1_000;
export const IDEAL_REFRESH_ONE_MINUTE = 60 * 1_000;
export const IDEAL_REFRESH_ONE_HOUR = 60 * 60 * 1_000;
export const IDEAL_REFRESH_ONE_DAY = 24 * 60 * 60 * 1_000;

type SetInterval = (fn: () => void, intervalMs: number) => number;
type ClearInterval = (id: number | undefined) => void;

type TimeSyncInitOptions = Readonly<{
/**
* Configures whether adding a new subscription will immediately create a new
* time snapshot and use it to update all other subscriptions.
* Configures whether adding a new subscription will immediately create a
* new time snapshot and use it to update all other subscriptions.
*/
resyncOnNewSubscription: boolean;

Expand Down Expand Up @@ -216,15 +219,6 @@ export class TimeSync implements TimeSyncApi {

const timeSyncContext = createContext<TimeSync | null>(null);

function useTimeSyncContext(): TimeSync {
const timeSync = useContext(timeSyncContext);
if (timeSync === null) {
throw new Error("Cannot call useTimeSync outside of a TimeSyncProvider");
}

return timeSync;
}

type TimeSyncProviderProps = Readonly<
PropsWithChildren<{
options?: Partial<TimeSyncInitOptions>;
Expand Down Expand Up @@ -297,7 +291,10 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
// accessibility, but it also gives us a globally unique ID associated with
// whichever component instance is consuming this hook
const hookId = useId();
const timeSync = useTimeSyncContext();
const timeSync = useContext(timeSyncContext);
if (timeSync === null) {
throw new Error("Cannot call useTimeSync outside of a TimeSyncProvider");
}

// We need to define this callback using useCallback instead of
// useEffectEvent because we want the memoization to be invaliated when the
Expand All @@ -322,8 +319,8 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
// memoized – useSyncExternalStore treats it similar to a useEffectEvent
// callback, except that unlike with useEffectEvent, it's render-safe.
const getNewState = (): T => {
const latestDate = timeSync.getLatestDatetimeSnapshot() as T & Date;
const selected = (select?.(latestDate) ?? latestDate) as T;
const newSnapshot = timeSync.getLatestDatetimeSnapshot() as T & Date;
const selected = (select?.(newSnapshot) ?? newSnapshot) as T;
return selected;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { DateRange as DailyPicker, type DateRangeValue } from "./DateRange";
import { type InsightsInterval, IntervalMenu } from "./IntervalMenu";
import { WeekPicker, numberOfWeeksOptions } from "./WeekPicker";
import { lastWeeks } from "./utils";
import { MAX_REFRESH_ONE_DAY, useTimeSync } from "hooks/useTimeSync";
import { IDEAL_REFRESH_ONE_DAY, useTimeSync } from "hooks/useTimeSync";

const DEFAULT_NUMBER_OF_WEEKS = numberOfWeeksOptions[0];

Expand All @@ -71,7 +71,7 @@ export default function TemplateInsightsPage() {
const [searchParams, setSearchParams] = useSearchParams();

const insightsInterval = useTimeSync<InsightsInterval>({
idealRefreshIntervalMs: MAX_REFRESH_ONE_DAY,
idealRefreshIntervalMs: IDEAL_REFRESH_ONE_DAY,
select: (newDatetime) => {
const templateCreateDate = new Date(template.created_at);
const hasFiveWeeksOrMore = addWeeks(templateCreateDate, 5) < newDatetime;
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/WorkspacePage/AppStatuses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
} from "api/typesGenerated";
import { useProxy } from "contexts/ProxyContext";
import { formatDistance, formatDistanceToNow } from "date-fns";
import { MAX_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
import { IDEAL_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
import type { FC } from "react";
import { createAppLinkHref } from "utils/apps";

Expand Down Expand Up @@ -154,7 +154,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
referenceDate,
}) => {
const comparisonDate = useTimeSync({
idealRefreshIntervalMs: MAX_REFRESH_ONE_MINUTE,
idealRefreshIntervalMs: IDEAL_REFRESH_ONE_MINUTE,
select: (dateState) => referenceDate ?? dateState,
});
const theme = useTheme();
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/WorkspacesPage/LastUsed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Stack } from "components/Stack/Stack";
import { StatusIndicatorDot } from "components/StatusIndicator/StatusIndicator";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { MAX_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
import { IDEAL_REFRESH_ONE_MINUTE, useTimeSync } from "hooks/useTimeSync";
import type { FC } from "react";

dayjs.extend(relativeTime);
Expand All @@ -15,7 +15,7 @@ export const LastUsed: FC<LastUsedProps> = ({ lastUsedAt }) => {
* @todo Verify that this is equivalent
*/
const [circle, message] = useTimeSync({
idealRefreshIntervalMs: MAX_REFRESH_ONE_MINUTE,
idealRefreshIntervalMs: IDEAL_REFRESH_ONE_MINUTE,
select: (date) => {
const t = dayjs(lastUsedAt);
const deltaMsg = t.from(dayjs(date));
Expand Down
Loading