-
Notifications
You must be signed in to change notification settings - Fork 1
Cache postal code → ward lookups instead of re-resolving each one #64
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||
| "use client"; | ||||||||||||||
|
|
||||||||||||||
| import { useState, type ReactNode } from "react"; | ||||||||||||||
| import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; | ||||||||||||||
| import type { WardView } from "@/lib/elections/election-data"; | ||||||||||||||
| import type { WardLookupResponse } from "@/lib/elections/ward-lookup"; | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -10,6 +10,22 @@ type State = | |||||||||||||
| | { status: "done"; result: WardLookupResponse } | ||||||||||||||
| | { status: "failed" }; | ||||||||||||||
|
|
||||||||||||||
| /** A complete postal code, however the visitor spaced or cased it. */ | ||||||||||||||
| const POSTAL_CODE = /^([A-Za-z]\d[A-Za-z])[\s-]*(\d[A-Za-z]\d)$/; | ||||||||||||||
|
|
||||||||||||||
| function normalize(typed: string): string | null { | ||||||||||||||
| const match = typed.trim().match(POSTAL_CODE); | ||||||||||||||
| return match ? `${match[1]} ${match[2]}`.toUpperCase() : null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Answers already fetched this session, keyed by normalized postal code. A | ||||||||||||||
| * lookup is a pure function of the code, so correcting a typo back to a code | ||||||||||||||
| * already tried — or re-submitting the same one — should cost nothing. Module | ||||||||||||||
| * scope so it survives the section unmounting. | ||||||||||||||
| */ | ||||||||||||||
| const cache = new Map<string, WardLookupResponse>(); | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Postal code → ward lookup for the wards section. The result is a best guess | ||||||||||||||
| * — postal centroids sit off-line near ward boundaries — so it reads as "looks | ||||||||||||||
|
|
@@ -32,12 +48,19 @@ export default function WardLookup({ | |||||||||||||
| }) { | ||||||||||||||
| const [postalCode, setPostalCode] = useState(""); | ||||||||||||||
| const [state, setState] = useState<State>({ status: "idle" }); | ||||||||||||||
| /** Which lookup is current, so a slow earlier reply can't overwrite a later one. */ | ||||||||||||||
| const latest = useRef(0); | ||||||||||||||
|
|
||||||||||||||
| const handleSubmit = async (event: React.FormEvent) => { | ||||||||||||||
| event.preventDefault(); | ||||||||||||||
| const typed = postalCode.trim(); | ||||||||||||||
| const lookup = useCallback(async (typed: string) => { | ||||||||||||||
| if (!typed) return; | ||||||||||||||
|
|
||||||||||||||
| const cached = cache.get(normalize(typed) ?? typed); | ||||||||||||||
| if (cached) { | ||||||||||||||
| setState({ status: "done", result: cached }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const request = ++latest.current; | ||||||||||||||
| setState({ status: "loading" }); | ||||||||||||||
| try { | ||||||||||||||
| // Sent exactly as typed — the API tolerates any spacing and casing, and | ||||||||||||||
|
|
@@ -46,11 +69,30 @@ export default function WardLookup({ | |||||||||||||
| `/api/elections/ward-lookup?postal_code=${encodeURIComponent(typed)}`, | ||||||||||||||
| ); | ||||||||||||||
| if (!res.ok) throw new Error(`ward-lookup ${res.status}`); | ||||||||||||||
| setState({ status: "done", result: await res.json() }); | ||||||||||||||
| const result: WardLookupResponse = await res.json(); | ||||||||||||||
| cache.set(normalize(typed) ?? typed, result); | ||||||||||||||
|
Comment on lines
+72
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the API returns
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/components/elections/WardLookup.tsx
Line: 72-73
Comment:
**Transient outages persist in session**
When the API returns `boundary_data_unavailable`, the unconditional cache write stores that transient outage in the module-scoped map, causing every retry for the postal code to keep showing "We can't look that up right now" for the remainder of the session even after the service recovers.
```suggestion
const result: WardLookupResponse = await res.json();
if (result.reason !== "boundary_data_unavailable") {
cache.set(normalize(typed) ?? typed, result);
}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||||||||||||||
| if (request === latest.current) setState({ status: "done", result }); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| console.error("[ward-lookup]", error); | ||||||||||||||
| setState({ status: "failed" }); | ||||||||||||||
| if (request === latest.current) setState({ status: "failed" }); | ||||||||||||||
| } | ||||||||||||||
| }, []); | ||||||||||||||
|
|
||||||||||||||
| // Look up as soon as the field holds a complete postal code, so the answer is | ||||||||||||||
| // usually on screen before the visitor reaches the button. Only complete | ||||||||||||||
| // codes fire — half-typed input would just spend requests on | ||||||||||||||
| // malformed_postal_code — and a short delay keeps a fast typist correcting the | ||||||||||||||
| // last character from sending two. | ||||||||||||||
| const complete = normalize(postalCode); | ||||||||||||||
| useEffect(() => { | ||||||||||||||
| if (!complete) return; | ||||||||||||||
| const timer = setTimeout(() => lookup(complete), 200); | ||||||||||||||
| return () => clearTimeout(timer); | ||||||||||||||
| }, [complete, lookup]); | ||||||||||||||
|
|
||||||||||||||
| const handleSubmit = (event: React.FormEvent) => { | ||||||||||||||
| event.preventDefault(); | ||||||||||||||
| lookup(postalCode.trim()); | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an uncached lookup remains in flight and the visitor enters a cached postal code, the cached branch returns without advancing
latest.current; the earlier request therefore still passes the sequence guard and overwrites the current result with another postal code's ward or an error.Prompt To Fix With AI