- Implemented a service worker (sw.js) that handles installation, activation, and fetch events. - Added caching logic for public assets and a fallback for offline navigation. - Created unit tests (sw.test.ts) to validate service worker behavior, including asset caching, navigation handling, and activation logic. - Ensured that private API routes and third-party requests are excluded from caching.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import type { HabitService } from "../habits/service";
|
||
import type { Schedule } from "../habits/contracts";
|
||
|
||
export type TodayHabit = ReturnType<HabitService["day"]>;
|
||
export type TodayResponse = {
|
||
date: string;
|
||
timezone: string;
|
||
habits: TodayHabit[];
|
||
due: number;
|
||
completed: number;
|
||
};
|
||
|
||
export async function habitRequest<T>(
|
||
path: string,
|
||
options?: RequestInit,
|
||
): Promise<T> {
|
||
if (typeof navigator !== "undefined" && navigator.onLine === false)
|
||
throw new Error("You’re offline. Reconnect and try again. Changes have not been saved or queued.");
|
||
const response = await fetch(`/api${path}`, options);
|
||
if (!response.ok) {
|
||
if (response.status === 401)
|
||
throw new Error("Your session has expired. Sign in again to continue.");
|
||
const body = await response.json().catch(() => null);
|
||
throw new Error(
|
||
(body?.error || "Could not reach your workspace. Please try again.") + (response.status >= 500 && body?.requestId ? ` Reference: ${body.requestId}` : ""),
|
||
);
|
||
}
|
||
return response.status === 204
|
||
? (undefined as T)
|
||
: (response.json() as Promise<T>);
|
||
}
|
||
|
||
export function scheduleLabel(schedule?: Schedule) {
|
||
if (!schedule || schedule.type === "daily") return "Every day";
|
||
if (schedule.type === "weekdays")
|
||
return schedule.days
|
||
.map((day) => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day])
|
||
.join(", ");
|
||
if (schedule.type === "interval") return `Every ${schedule.every} days`;
|
||
return `Every ${schedule.every === 1 ? "week" : `${schedule.every} weeks`} · ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][schedule.weekday]}`;
|
||
}
|
||
|
||
export function formatTrackingDate(date: string) {
|
||
return new Date(`${date}T12:00:00Z`).toLocaleDateString("en", {
|
||
weekday: "long",
|
||
month: "long",
|
||
day: "numeric",
|
||
timeZone: "UTC",
|
||
});
|
||
}
|
||
|
||
/** Preserve unchanged row references when a background refresh returns new JSON objects. */
|
||
export function reconcileToday(current: TodayResponse | null, next: TodayResponse): TodayResponse {
|
||
if (!current || current.date !== next.date || current.timezone !== next.timezone) return next;
|
||
const previous = new Map(current.habits.map(habit => [habit.habitId, habit]));
|
||
const habits = next.habits.map(habit => {
|
||
const old = previous.get(habit.habitId);
|
||
return old && JSON.stringify(old) === JSON.stringify(habit) ? old : habit;
|
||
});
|
||
if (current.due === next.due && current.completed === next.completed && habits.length === current.habits.length && habits.every((habit, index) => habit === current.habits[index])) return current;
|
||
return { ...next, habits };
|
||
}
|