Planet4.Me docs
Open the app

#Sessions

A trip is a burst of check-offs, and the pause after it means you left. Sessions are created implicitly by the first check-off — never by asking "which store are you at" — and closed lazily on read: on mount and on every return to visibility, an abandoned session is closed. No timer and no scheduled function, so it works offline and costs nothing.

A session opens without waiting for a location fix. getCurrentPosition's timeout covers acquiring a position, not a permission prompt nobody has answered, so awaiting it could block the very tap that checks an item off. The fix is requested alongside and lands on the session whenever it arrives, or never — which is an ordinary outcome, not an error.

It opens without waiting for a read, either. ensureSession is synchronous: it answers from the open-session listener's last snapshot (sessionRef), or from the trip this device just opened and the listener has not echoed yet (opened), and otherwise opens a new one with a client-generated id. It never performs a getDocs, because on a connection that is up but not answering — the half-open state a phone in a supermarket lives in — a read waits on the server for ten seconds or so before falling back to the cache, and what it was holding up was the row moving. The check-off's writes now go out in the same tick as the tap, which is what src/data/items.ts promised all along. The opened ref is also what keeps two quick taps on one trip: both would otherwise see no open session and start their own. The listener normally catches up within the same tick, since Firestore raises local writes to listeners before the network sees them.

closedAt is backdated to the session's last activity rather than set to whenever somebody reopened the app, because the trip ended when the check-offs stopped, and a store visit recorded three days late would corrupt the trip cadence that lead times are derived from.

Items that were on the list and never checked off are recorded on the closed session as evidence for out-of-stock detection, and leftBehind (src/lib/sessions/leftBehind.ts, layer 1) reads it: the closed trips to a regular's own shop — pinned by hand, or learned from its purchases (stapleShop); a regular under Anywhere has no shop to count trips to — newest first, and how many in a row from the newest carried the regular in uncheckedStapleIds. A trip that bought it, or did not list it, ends the run; a trip elsewhere is not evidence about this shop and neither breaks nor extends it; an open trip has decided nothing. Two in a row (LEFT_BEHIND_TRIPS) earns one muted line on the Regulars row and under the Shop section of the regular's page — "Not found at Kroger on the last 2 trips there." (LeftBehindLine, the one sentence both render) — and fewer earns silence. It is a derivation over the recent-sessions listener, so nothing is stored and a re-attributed trip changes the answer at once. See DECISIONS, "A trip that came home without it is a sentence, not a verdict".

useCheckOff is the single place a check-off happens, because it is three things at once: the item leaves the list, a trip opens if none is running, and — if the item resolves to a staple — a purchase enters the stream. A repeat within six hours is treated as the same purchase, so two people in one shop record one.