Skip to content

feat(worker): serve trip plans from PID GTFS instead of Golemio - #56

Merged
i11v merged 4 commits into
mainfrom
claude/gtfs-trip-plans
Oct 17, 2026
Merged

i11v merged 4 commits into
mainfrom
claude/gtfs-trip-plans

Conversation

@i11v

@i11v i11v commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Why

Every GET /api/trips/:tripId miss spent the one Golemio token's budget (20 calls / 8 s) through the singleton gateway Durable Object. A cold iOS launch took ~8 s to load ~20 vehicle plans, and that cost grows with every user, which matters for the public release. Trip plans are static timetable data, and Golemio's trip endpoint is just a view of PID's public GTFS feed.

What

  • Build: build:index (same download, one unzip) now also writes every trip's plan to apps/web/public/data/trips/v1/<route>.json. There is one file per route prefix of the trip id: ~900 files, ~160 MB, about 6 s and 2.3 GB of RAM. Output is deterministic, so routes that didn't change upload nothing.
  • Format: TripBucketV1, tripBucketKey and tripFromBucket in packages/contract/src/trip-bucket.ts. The builder is in apps/worker/src/gtfs/buckets.ts; it applies the normalization toTrip did (time parsing, ASW ids, rounding).
  • Serve: the Worker reads the files through ASSETS. It keeps up to 3 parsed files and 512 plans per isolate and validates each plan against Trip before serving it. Trips never reach Golemio or the gateway DO. An id the deployed feed lacks returns 404 TripNotFound; an unreadable or malformed file returns 503 UpstreamUnavailable, which isn't cached, so the next request retries.
  • Removed: the Golemio trip-plan path (fetchTrip, the gateway's trip cache and getTrip RPC, PidTripResponse, toTrip and the Golemio trip fixture). Trips have one source of truth. The GTFS helpers moved to gtfs/buckets.ts, and the trip tests now build from GTFS rows.
  • Refresh: deploy.yml also runs daily at 03:30 UTC (after PID publishes the feed) and on manual dispatch.
  • scripts/lib/csv.ts now has a lazy iterCsv generator; parseCsv is unchanged.
  • Unrelated fix: vitest.integration.config.ts now points at apps/*. The move to apps/ left it matching no files.
  • docs/adr/0001-trip-plans-from-gtfs.md explains why I used static assets over R2, D1, KV or a Worker cron, and docs/DEPLOY.md is updated.

The HTTP contract is unchanged (contract:check passes, and wire-shape.json and the iOS fixtures didn't change). /api/trips/:id/vehicle and /api/vehicles still go to Golemio through the gateway.

Verification

  • Against production (2026-10-17): all 366 live vehicles' trip ids were in the feed. Plans built from it matched production /api/trips byte for byte on all 19 trips I compared. The other requests in that sample got 503 because I had used up production's Golemio budget.
  • Unit tests: bun run test passes (140 tests).
    • Bucket tests on GTFS rows carry over every former toTrip expectation: order, normalization, shape rounding and dedupe, time fallbacks.
    • Plan-cache tests cover a hit loading its file once, misses, a failed load reported as unavailable and retried, and malformed data reported as unavailable.
    • API tests check that trips never reach the gateway, that a miss is a 404 and that an unreadable file is a 503.
  • Integration: GOLEMIO_API_TOKEN=dummy bun run test:integration passes (4 tests). The new test gets a plan from local workerd with a dummy Golemio token, so the plan can only have come from the assets.
  • typecheck, lint, fmt:check, build:web and verify:pwa pass. No trip files end up in the PWA precache.

Notes

  • feat(worker): persist trip plans in the gateway's SQLite storage #52: closed. Persisting Golemio plans in the DO is no longer needed.
  • Behaviour change: an id missing from the deployed feed is now a 404, where Golemio would have been asked. Every live id I checked was in the feed, and the daily deploy keeps it current.
  • Size: deploy assets grow by ~160 MB, and each PR preview uploads them on its first deploy.

🤖 Generated with Claude Code

Every /api/trips/:tripId miss spent the shared Golemio budget (20 calls /
8 s) through the singleton gateway Durable Object, so a cold iOS launch
took ~8 s to load ~20 plans, and the cost grew with every user.

Trip plans are static GTFS data, and Golemio's trip endpoint is a view of
PID's public feed. build:index now also builds every trip's plan into
apps/web/public/data/trips/v1/<route>.json (one file per route prefix of
the trip id, ~900 files, ~160 MB) with the same normalization as toTrip.
The Worker reads them through the ASSETS binding, caches a few parsed
files and many plans per isolate, and asks the gateway only for ids the
deployed feed lacks. A daily scheduled deploy refreshes the data.

Checked against production: every live vehicle's trip id is in the feed,
and the built plans match /api/trips byte for byte. The HTTP contract is
unchanged; the bucket format lives in packages/contract. Also points the
integration suite at apps/*, which the move to apps/ left behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚀 Preview deployed: https://preview-56.tablo.run
fallback: https://tablo-pr-56.i11v.workers.dev

# Conflicts:
#	apps/worker/src/api.ts
#	apps/worker/test/api.test.ts
i11v and others added 2 commits October 18, 2026 00:17
Trip plans now have one source: the GTFS built into the deploy. The
Golemio route that served them (fetchTrip, the gateway's trip cache and
getTrip RPC, PidTripResponse, toTrip) only lingered as a fallback for
ids the deployed feed lacks, which the daily rebuild and the feed's two
weeks of lookahead make rare, so it goes.

/api/trips/:tripId now answers 404 for an id the feed lacks, and 503
when its bucket can't be read or holds malformed data, so clients retry
a data bug instead of treating it as a missing trip. The GTFS helpers
move from golemio/normalize.ts into gtfs/buckets.ts, and the trip-plan
tests build from GTFS rows instead of a Golemio fixture. The HTTP
contract is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/worker/src/golemio/schema.ts
@i11v
i11v merged commit e306982 into main Oct 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant