aboutsummaryrefslogtreecommitdiff
path: root/internal/readings/offline.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 10:45:28 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 10:45:28 +0200
commitf110c338718a6eea3693fb6afb4f7fcc5b77003b (patch)
tree52b6adb382445a0762f63fb862361d7fbd8637f2 /internal/readings/offline.go
parentfd3eda0da228d2417b8b7f9007533e214d7a5e80 (diff)
parentb24702cc9b8d7f2e408bb92032e8f7fa2aaa836b (diff)
downloadlectio-f110c338718a6eea3693fb6afb4f7fcc5b77003b.tar.gz
lectio-f110c338718a6eea3693fb6afb4f7fcc5b77003b.zip
merge: make the calendar engine stop repeating whole-year work per day
A seven-day view resolved the year seven times. Each readings.Load stacked the calendar data layers and re-read the Bible book table -- both date-independent -- and the EF path rebuilt the year's entire transfer plan and its occupancy scan on every single day. Three logic-neutral fixes: hoist the date-independent setup out of the Days loop; memoise the EF transfer plan on (year, Selection, content hash of the merged sanctoral); and build the occupancy index once alongside it rather than scanning per candidate. EF's seven-day view goes 74ms to 16ms, its multiple over OF from ~13x to ~3x. OF is unchanged, as it never touched the EF paths. Output is byte-identical throughout, proven by SHA-256 over a 492-case sweep spanning both forms, both languages, four corpora, the Triduum, a Requiem day and the three Joseph/Annunciation collision years. The caches are keyed on content, not identity, so a user overlay invalidates them -- asserted by a test that varies the overlay and requires the answer to change, itself mutation-proved by dropping the content hash and watching it fail.
Diffstat (limited to 'internal/readings/offline.go')
-rw-r--r--internal/readings/offline.go49
1 files changed, 43 insertions, 6 deletions
diff --git a/internal/readings/offline.go b/internal/readings/offline.go
index b2c16df..546d3b0 100644
--- a/internal/readings/offline.go
+++ b/internal/readings/offline.go
@@ -13,6 +13,33 @@ import (
"github.com/lukaszkasprzak/lectio/internal/naming"
)
+// Prepared holds the date-independent setup offlineLoad otherwise redoes on
+// every call: the stacked calendar layers (caldata.Stack) and the book table
+// (bible.LoadBookTable). Both depend only on cfg -- never on the date -- so a
+// caller resolving many dates against the same cfg (mobile.Days's 7-day loop,
+// an eventual month view) should build one Prepared with Prepare and reuse it
+// via LoadWith for every date, instead of paying Stack's INI parsing and
+// LoadBookTable's file read + parse once per date. See Prepare and LoadWith.
+type Prepared struct {
+ layers []calendar.Layer
+ tbl *bible.BookTable
+}
+
+// Prepare builds a Prepared for cfg: the layer stack for cfg.Selection().Form
+// stacked with cfg.Use (caldata.Stack), and the book table for the user's
+// books.ini override, if any (bible.LoadBookTable). Both calls already
+// tolerate their own failure (Stack falls back to the embedded calendar on a
+// bad user layer; LoadBookTable falls back to the embedded book table on a
+// bad user override) exactly as offlineLoad always has -- Prepare changes
+// only when this work happens, never what it computes or how it degrades.
+func Prepare(cfg config.Config) Prepared {
+ sel := cfg.Selection()
+ dir, _ := config.CalendarsDir()
+ layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error
+ tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored
+ return Prepared{layers: layers, tbl: tbl}
+}
+
// offlineLoad resolves a day's readings entirely from the embedded calendar
// engine and lectionary data -- no network. It returns the same source-agnostic
// liturgy.Section / liturgy.DayInfo the CLI/TUI/web already render, so the daily
@@ -20,18 +47,28 @@ import (
// lectio's English-canonical authored form; the render localises each one to
// the chosen corpus's Psalter and the user's sigla dialect (see
// render.GatherVersion, bible.OFRef).
+//
+// offlineLoad is Prepare(cfg) followed by offlineLoadWith -- a single call's
+// worth of convenience for Load, which has no date to amortize Prepare's cost
+// over. A caller with several dates should call Prepare once and use
+// offlineLoadWith/LoadWith directly instead (see Prepared's doc comment).
func offlineLoad(cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) {
+ return offlineLoadWith(Prepare(cfg), cfg, date)
+}
+
+// offlineLoadWith is offlineLoad, given an already-built Prepared instead of
+// building its own. Computing the day itself (calendar.Compute, the readings
+// it resolves) still happens once per call, exactly as before -- only the
+// layer stack and book table are reused.
+func offlineLoadWith(p Prepared, cfg config.Config, date string) ([]liturgy.Section, liturgy.DayInfo, error) {
d, err := time.Parse("2006-01-02", date)
if err != nil {
return nil, liturgy.DayInfo{}, fmt.Errorf("bad date %q (want YYYY-MM-DD)", date)
}
sel := cfg.Selection()
- dir, _ := config.CalendarsDir()
- layers, _ := caldata.Stack(sel.Form, dir, cfg.Use) // Stack falls back to embedded data on error
- day := calendar.Compute(d.UTC(), sel, layers)
- rs := caldata.Readings(sel, layers, d.UTC(), day)
- tbl, _ := bible.LoadBookTable(config.UserBooksINI()) // nil on error -> citations shown as authored
- return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), tbl), dayInfo(cfg, day), nil
+ day := calendar.Compute(d.UTC(), sel, p.layers)
+ rs := caldata.Readings(sel, p.layers, d.UTC(), day)
+ return sectionsFor(rs, sel.Form, cfg.UILanguage, cfg.SiglaLang(), p.tbl), dayInfo(cfg, day), nil
}
// citationForms renders a reading's authored (English) citation into its