Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { clearTrips, readTripQuery, readTrips, removeTrip, toggleTrip, tripQuery
import type { Journey, Lang, Suggestion } from "./lib/transit/types"
import coordinatesFile from "../data/en/coordinates.json"
import { accessByCode } from "./lib/status/pack"
import { loadLastPoll } from "./lib/status/lastPoll"
import { loadStatus, loadStatusHistory, statusClock } from "./lib/status/load"
import { locationCopy, readLocation, rememberedLocation, resetLocationMemory, type LocationResult } from "./lib/status/location"
import { nearestBoardable, nearestStation, type Coordinate } from "./lib/status/nearest"
Expand Down Expand Up @@ -166,6 +167,10 @@ export function App() {
const [helpNotice, setHelpNotice] = useState<string | null>(null)
const [trips, setTrips] = useState<SavedTrip[]>(readStoredTrips)
const [savedMark, setSavedMark] = useState<{ n: number; fromCode: string; toCode: string } | null>(null)
const [statusPolledAt, setStatusPolledAt] = useState<string | null>(null)
useEffect(() => {
void loadLastPoll().then(setStatusPolledAt)
}, [])
useEffect(() => {
if (tab !== "saved" || savedMark === null) return
const id = window.setTimeout(() => setSavedMark(null), 1100)
Expand Down Expand Up @@ -838,6 +843,7 @@ export function App() {
here={here}
locating={locating}
locateNote={locateNote}
polledAt={statusPolledAt}
onLocate={() =>
readHere(
() => undefined,
Expand Down
23 changes: 22 additions & 1 deletion src/lib/status/lastPoll.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"
import { afterEach, describe, expect, it, vi } from "vitest"
import { readLastPollPayload } from "./lastPoll"

describe("last poll payload", () => {
Expand All @@ -12,3 +12,24 @@ describe("last poll payload", () => {
expect(readLastPollPayload(null)).toBeNull()
})
})

describe("loadLastPoll session cache", () => {
afterEach(() => {
vi.unstubAllGlobals()
vi.resetModules()
})

it("hits the network only once per module session", async () => {
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ polledAt: "2026-10-13T14:00:00.000" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
)
vi.stubGlobal("fetch", fetchMock)
const mod = await import("./lastPoll")
await expect(mod.loadLastPoll()).resolves.toBe("2026-10-13T14:00:00.000")
await expect(mod.loadLastPoll()).resolves.toBe("2026-10-13T14:00:00.000")
expect(fetchMock).toHaveBeenCalledTimes(1)
})
})
8 changes: 8 additions & 0 deletions src/lib/status/lastPoll.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** Trial: last successful Status bake dispatch from the Cloudflare ping Worker. */
export const STATUS_PING_URL = "https://detro-status-ping.rushforlinux.workers.dev"

let sessionPoll: Promise<string | null> | null = null

export function readLastPollPayload(raw: unknown): string | null {
if (!raw || typeof raw !== "object") return null
const polledAt = (raw as { polledAt?: unknown }).polledAt
Expand All @@ -20,3 +22,9 @@ export async function fetchLastPoll(signal?: AbortSignal): Promise<string | null
return null
}
}

/** One network read per app session (tab navigation must not refetch). */
export function loadLastPoll(): Promise<string | null> {
if (!sessionPoll) sessionPoll = fetchLastPoll()
return sessionPoll
}
17 changes: 3 additions & 14 deletions src/ui/StatusView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from "react"
import { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent } from "react"
import { ArrowRight, LocateFixed, X } from "lucide-react"
import type { Copy } from "../i18n/copy"
import { lineIndexFor, searchCatalog, type CatalogStation } from "../lib/dmrc/snapshot"
import { fetchLastPoll } from "../lib/status/lastPoll"
import { statusStamp12, statusTime12 } from "../lib/status/load"
import { nearestOpen } from "../lib/status/gates"
import { nearestStation, type Coordinate } from "../lib/status/nearest"
Expand All @@ -29,6 +28,7 @@ export function StatusView({
onLocate,
onPlan,
onClear,
polledAt = null,
initialFrom = null,
initialDest = null,
}: {
Expand All @@ -45,6 +45,7 @@ export function StatusView({
onLocate: () => void
onPlan: (from: Stop, to: Stop) => void
onClear: () => void
polledAt?: string | null
initialFrom?: string | null
initialDest?: string | null
}) {
Expand All @@ -56,7 +57,6 @@ export function StatusView({
const [looked, setLooked] = useState<Stop | null>(() => named(initialFrom, names) ?? named(initialDest, names))
const [openLine, setOpenLine] = useState<string | null>(null)
const [focus, setFocus] = useState<string | null>(null)
const [polledAt, setPolledAt] = useState<string | null>(null)
const closedCount = Object.keys(pack.stations).length
const openCount = Math.max(0, names.size - closedCount)
const clock = pack.fetchedAt ? statusTime12(pack.fetchedAt) : ""
Expand Down Expand Up @@ -92,17 +92,6 @@ export function StatusView({
const suggestions = useMemo(() => (query.trim() ? searchCatalog(stations, "en", query).slice(0, 8) : []), [query, stations])
const listOpen = searchOpen && query.trim().length > 0

useEffect(() => {
let alive = true
// Do not abort on Strict Mode remount — a cancelled first request looks like a failure in DevTools.
fetchLastPoll().then((value) => {
if (alive) setPolledAt(value)
})
return () => {
alive = false
}
}, [])

function lookAt(stop: Stop, lineCode?: string | null) {
setLooked(stop)
setQuery("")
Expand Down
Loading