fix(planner): stop an empty reservation event from breaking a trip for good

reservation:created and reservation:updated come in two shapes. Normally they
carry the reservation; the accommodation cascade sends them empty, as a
"something changed, go and look" ping that the event contract allows for.

The applier read the id off both. With no reservations loaded yet the
existence check never ran its callback, so the cast slipped through and the
store became [undefined]. From there every render that walked the list threw
"Cannot read properties of undefined (reading 'endpoints')" into the route
error boundary, and since the store is rebuilt from the same events, the trip
stayed unreachable in that browser. The reporter could only get out of it by
clearing localStorage.

Both appliers now decline an event with nothing on it, and the Dexie writer
does too rather than leaning on a swallowed catch. The predicate that walks
the list is total as well: it guarded the property but not the element, and
it is handed straight to Array.filter over a list it does not own.

Declining the empty ping would have left a hotel booked by one member
invisible to the others until they reloaded, so the ping is honoured the way
day:reordered already honours its own — fetch the authoritative list.

The server keeps sending what it sends. Making the cascade carry the full
reservation would mean moving the REST, MCP and RPC paths together with
their pinned tests, and would still leave the client fragile against the
next emitter that does this.

Closes #1979
This commit is contained in:
Maurice
2026-08-20 17:17:52 +02:00
committed by Maurice
parent 8f171453d3
commit 68bf5316ad
3 changed files with 142 additions and 9 deletions
+10 -3
View File
@@ -1,8 +1,15 @@
import type { Reservation } from '../types'
/** A reservation is routable on the map once it has at least two ordered endpoints (from/to/stop). */
export function isRoutableReservation(r: Pick<Reservation, 'endpoints'>): boolean {
return (r.endpoints || []).length >= 2
/**
* A reservation is routable on the map once it has at least two ordered
* endpoints (from/to/stop).
*
* Total in its argument, because it is handed straight to Array.filter over a
* list this module does not own. It used to guard the property but not the
* element, so one undefined entry took the whole trip planner down (#1979).
*/
export function isRoutableReservation(r: Pick<Reservation, 'endpoints'> | null | undefined): boolean {
return (r?.endpoints || []).length >= 2
}
export interface RouteVisibilityOptions {