Skip to content

Commit

Permalink
Switch from Date to Temporal (#2051)
Browse files Browse the repository at this point in the history
* useClock.ts

* schema.prisma migration.sql _index.tsx admin.shows.$show_.edit.tsx admin.shows.new.tsx show.ts admin.spec.ts global.setup.ts

* AudioControllerTest.tsx

* helpers.ts useShowInfo.spec.tsx

* useShowInfo.spec.tsx

* ShowInfo.ts useShowInfo.ts AudioController.tsx ShowPlaying.spec.tsx

* $show.[data.json].ts package.json package-lock.json

* eslint.config.js
  • Loading branch information
stephenwade authored Feb 23, 2025
1 parent 52d5619 commit 0130cc3
Show file tree
Hide file tree
Showing 20 changed files with 152 additions and 111 deletions.
8 changes: 3 additions & 5 deletions app/components/AudioController/AudioController.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable jsx-a11y/media-has-caption */

import { differenceInSeconds } from 'date-fns';
import type { AudioHTMLAttributes, FC, ReactNode, SyntheticEvent } from 'react';
import {
useCallback,
Expand Down Expand Up @@ -248,10 +247,9 @@ export const AudioController: FC<AudioControllerProps> = ({
showInfo.currentSet &&
change.currentSet.id !== showInfo.currentSet.id
) {
const setDifference = differenceInSeconds(
change.currentSet.start,
showInfo.currentSet.start,
);
const setDifference = showInfo.currentSet.start
.until(change.currentSet.start)
.total({ unit: 'seconds' });
delay += setDifference;
}

Expand Down
3 changes: 2 additions & 1 deletion app/hooks/useClock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Temporal } from 'temporal-polyfill';

function getCurrentSecond() {
return Math.floor(Date.now() / 1000);
return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
}

export function useClock(enabled = true): void {
Expand Down
36 changes: 25 additions & 11 deletions app/hooks/useShowInfo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addMilliseconds, addSeconds, isBefore, parseISO } from 'date-fns';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Temporal } from 'temporal-polyfill';

import type { loader as showDataLoader } from '~/routes/$show.[data.json]';
import type { ShowData } from '~/types/ShowData';
Expand All @@ -10,6 +10,10 @@ import { useFetcherIgnoreErrors } from './useFetcherIgnoreErrors';

type LoadedMetadataHandler = (args: { id: string; duration: number }) => void;

function isBefore(a: Temporal.Instant, b: Temporal.Instant): boolean {
return Temporal.Instant.compare(a, b) === -1;
}

export function useShowInfo(
loaderData: Pick<ShowData, 'slug' | 'serverDate' | 'sets'>,
{ ci = false, enableClock = true } = {},
Expand Down Expand Up @@ -46,48 +50,58 @@ export function useShowInfo(
const data = fetcher.data ?? loaderData;

const clientTimeSkewMs = useMemo(() => {
const serverDate = parseISO(data.serverDate);
const serverDate = Temporal.Instant.from(data.serverDate);

return Date.now() - serverDate.valueOf();
return (
Temporal.Now.instant().epochMilliseconds - serverDate.epochMilliseconds
);
}, [data.serverDate]);

const sets = useMemo(() => {
return data.sets
.map(function parseDates(set) {
const start = parseISO(set.start);
const start = Temporal.Instant.from(set.start);

const length = audioDurations[set.id] ?? set.duration;
const end = addSeconds(start, length);
const end = start.add({
// `.add()` requires integers
// seconds: length,
milliseconds: Math.round(length * 1000),
});

return { ...set, start, end };
})
.map(function adjustForClientTimeSkew(set) {
const start = addMilliseconds(set.start, clientTimeSkewMs);
const end = addMilliseconds(set.end, clientTimeSkewMs);
const start = set.start.add({ milliseconds: clientTimeSkewMs });
const end = set.end.add({ milliseconds: clientTimeSkewMs });

return { ...set, start, end };
});
}, [audioDurations, clientTimeSkewMs, data.sets]);

const currentSetIndex = sets.findIndex((set) =>
isBefore(Date.now(), set.end),
isBefore(Temporal.Now.instant(), set.end),
);
const currentSet = currentSetIndex === -1 ? undefined : sets[currentSetIndex];
const nextSet =
currentSetIndex === -1 ? undefined : sets[currentSetIndex + 1];

const timeInfo: TargetTimeInfo = currentSet
? isBefore(Date.now(), currentSet.start)
? isBefore(Temporal.Now.instant(), currentSet.start)
? {
status: 'WAITING_UNTIL_START',
secondsUntilSet: Math.ceil(
(currentSet.start.valueOf() - Date.now()) / 1000,
Temporal.Now.instant()
.until(currentSet.start)
.total({ unit: 'seconds' }),
),
}
: {
status: 'PLAYING',
currentTime: Math.floor(
(Date.now() - currentSet.start.valueOf()) / 1000,
currentSet.start
.until(Temporal.Now.instant())
.total({ unit: 'seconds' }),
),
}
: { status: 'ENDED' };
Expand Down
12 changes: 9 additions & 3 deletions app/routes/$show.[data.json].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { json, type LoaderFunction } from '@remix-run/node';
import { addSeconds, formatISO } from 'date-fns';
import { Temporal } from 'temporal-polyfill';

import { db } from '~/db.server/db';
import type { ShowData } from '~/types/ShowData';
Expand Down Expand Up @@ -28,10 +28,16 @@ export const loader = (async ({ params }) => {
id: set.id,
audioUrl: set.audioFile.url,
artist: set.artist,
start: addSeconds(show.startDate, set.offset).toISOString(),
start: Temporal.Instant.from(show.startDate)
.add({
// `.add()` requires integers
// seconds: set.offset,
milliseconds: Math.round(set.offset * 1000),
})
.toString(),
duration: set.audioFile.duration,
})),
serverDate: formatISO(new Date()),
serverDate: Temporal.Now.instant().toString(),
};

// Single Fetch doesn't work with Clerk
Expand Down
26 changes: 17 additions & 9 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import '~/styles/index.css';

import type { LoaderFunction } from '@remix-run/node';
import { redirect } from '@remix-run/node';
import { addSeconds } from 'date-fns';
import type { FC } from 'react';
import { Temporal, toTemporalInstant } from 'temporal-polyfill';
import { Temporal } from 'temporal-polyfill';

import { cache, INDEX_SHOW_SLUG_KEY } from '~/cache.server/cache';
import { db } from '~/db.server/db';
import { showIncludeData } from '~/types/ShowWithData';
import { validateShow } from '~/types/validateShow';

function isAfter(a: Temporal.Instant, b: Temporal.Instant): boolean {
return Temporal.Instant.compare(a, b) === 1;
}

/**
* Returns the slug of the earliest show that hasn't ended yet.
*/
Expand All @@ -26,18 +29,23 @@ async function calculateIndexShowSlug(): Promise<string | null> {
// `validateShow` ensures there's at least one set
const lastSet = show.sets.at(-1)!;

const startDate = Temporal.Instant.from(show.startDate);

return {
slug: show.slug,
startDate: toTemporalInstant.call(show.startDate),
endDate: addSeconds(
show.startDate,
lastSet.offset + lastSet.audioFile.duration,
),
startDate,
endDate: startDate.add({
// `.add()` requires integers
// seconds: lastSet.offset + lastSet.audioFile.duration,
milliseconds: Math.round(
(lastSet.offset + lastSet.audioFile.duration) * 1000,
),
}),
};
});

const upcomingShows = showsWithEndDate.filter(
({ endDate }) => endDate > new Date(),
const upcomingShows = showsWithEndDate.filter(({ endDate }) =>
isAfter(endDate, Temporal.Now.instant()),
);

upcomingShows.sort((a, b) =>
Expand Down
21 changes: 9 additions & 12 deletions app/routes/admin.shows.$show_.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { json, redirect } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import type { FC } from 'react';
import { validationError } from 'remix-validated-form';
import { Temporal, toTemporalInstant } from 'temporal-polyfill';
import { Temporal } from 'temporal-polyfill';

import { redirectToLogin } from '~/auth/redirect-to-login.server';
import { cache, INDEX_SHOW_SLUG_KEY } from '~/cache.server/cache';
Expand Down Expand Up @@ -41,8 +41,7 @@ export const loader = (async (args) => {
if (!show) throw notFound();

const startDate = show.startDate
? toTemporalInstant
.call(show.startDate)
? Temporal.Instant.from(show.startDate)
.toZonedDateTimeISO(show.timeZone)
.toPlainDateTime()
.toString({ smallestUnit: 'second' })
Expand Down Expand Up @@ -76,21 +75,19 @@ export const action = (async (args) => {
const { data, error } = await validator.validate(form);
if (error) return validationError(error);

const { sets, ...rest } = replaceUndefinedsWithNull(data);
const { startDate, sets, ...rest } = replaceUndefinedsWithNull(data);

const startDate = rest.startDate
? new Date(
Temporal.PlainDateTime.from(rest.startDate).toZonedDateTime(
rest.timeZone,
).epochMilliseconds,
)
: null;
const startInstant = startDate
? Temporal.PlainDateTime.from(startDate)
.toZonedDateTime(rest.timeZone)
.toInstant()
: undefined;

await db.show.update({
where: { id },
data: {
...rest,
startDate,
startDate: startInstant?.toString(),
sets: {
deleteMany: {
id: { notIn: sets.map((set) => set.id).filter(isDefined) },
Expand Down
16 changes: 7 additions & 9 deletions app/routes/admin.shows.new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,18 @@ export const action = (async (args) => {
const { data, error } = await validator.validate(form);
if (error) return validationError(error);

const { sets, ...rest } = data;
const { startDate, sets, ...rest } = data;

const startDate = rest.startDate
? new Date(
Temporal.PlainDateTime.from(rest.startDate).toZonedDateTime(
rest.timeZone,
).epochMilliseconds,
)
: null;
const startInstant = startDate
? Temporal.PlainDateTime.from(startDate)
.toZonedDateTime(rest.timeZone)
.toInstant()
: undefined;

const show = await db.show.create({
data: {
...rest,
startDate,
startDate: startInstant?.toString(),
sets: { create: sets },
},
});
Expand Down
6 changes: 4 additions & 2 deletions app/types/ShowInfo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Temporal } from 'temporal-polyfill';

import type { SetData } from './ShowData';

type SetInfo = Omit<SetData, 'start'> & {
start: Date;
end: Date;
start: Temporal.Instant;
end: Temporal.Instant;
};

interface SetsInfo {
Expand Down
13 changes: 13 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ export default tseslint.config(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...reactHooks.configs.recommended.rules,

'no-restricted-globals': [
'error',
{ name: 'Date', message: 'Use `Temporal` instead.' },
],
'@typescript-eslint/no-restricted-types': [
'error',
{
types: {
Date: 'Use `Temporal` instead.',
},
},
],

'no-plusplus': 'error',
'object-shorthand': 'warn',
quotes: ['warn', 'single', { avoidEscape: true }],
Expand Down
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@remix-run/react": "2.15.3",
"@remix-run/serve": "2.15.3",
"@remix-validated-form/with-zod": "2.0.7",
"date-fns": "3.6.0",
"nanoid": "5.1.2",
"node-cache": "5.1.2",
"react": "18.3.1",
Expand Down
10 changes: 5 additions & 5 deletions playwright/ct-tests/AudioController/AudioControllerTest.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { createRemixStub } from '@remix-run/testing';
import { addSeconds } from 'date-fns';
import type { FC } from 'react';
import { useState } from 'react';
import { Temporal } from 'temporal-polyfill';
import { useDeepCompareMemo } from 'use-deep-compare';

import type { AudioMetadata } from '~/components/AudioController';
Expand Down Expand Up @@ -31,7 +31,7 @@ function getMockData({
alternate = false,
empty = false,
}: GetMockDataProps): Pick<ShowData, 'slug' | 'serverDate' | 'sets'> {
const now = new Date();
const now = Temporal.Now.instant();

const sets: ShowData['sets'] = empty
? []
Expand All @@ -40,21 +40,21 @@ function getMockData({
id: alternate ? ID_3 : ID_1,
audioUrl: `${AUDIO_FILE_URL}?${alternate ? 3 : 1}`,
artist: `Artist ${alternate ? 3 : 1}`,
start: addSeconds(now, 0 - offsetSec).toISOString(),
start: now.add({ seconds: 0 - offsetSec }).toString(),
duration: AUDIO_FILE_LENGTH,
},
{
id: alternate ? ID_4 : ID_2,
audioUrl: `${AUDIO_FILE_URL}?${alternate ? 4 : 2}`,
artist: `Artist ${alternate ? 4 : 2}`,
start: addSeconds(now, 100 - offsetSec).toISOString(),
start: now.add({ seconds: 100 - offsetSec }).toString(),
duration: AUDIO_FILE_LENGTH,
},
];

return {
slug: 'test',
serverDate: now.toISOString(),
serverDate: now.toString(),
sets,
};
}
Expand Down
Loading

0 comments on commit 0130cc3

Please sign in to comment.