1package paged23import (4 "context"5 "slices"67 "src.rest/cat/012/internal/fileio"8 "src.rest/cat/012/internal/sheet"9)1011// Pages are the rows a source's tab shows, in its view's order (a sort12// and a filter, pushed down to SQL or streamed: fileio.SourceView), read13// a page at a time as the tab scrolls, and at most keepPages of them14// kept, so memory stays bounded whatever the source's size. Building a15// sorted or filtered view reads the whole source once; until it's built,16// and while a page is on its way, the rows it would show aren't known.17type Pages struct {18 h fileio.Source19 gen int20 order sheet.SourceOrder2122 view fileio.SourceView23 building bool24 err string2526 pages map[int64]*page // by page number27 used int64 // counts page reads, for dropping the least used28 want map[int64]bool // pages asked for or on their way29 fresh []*PageJob30}3132// page is PageRows rows of the view: each one's number in the source33// and its values.34type page struct {35 nums []int6436 rows [][]sheet.LiveCell37 used int6438}3940// PageRows is how many rows a page holds; keepPages is how many are41// kept.42const (43 PageRows = 12844 keepPages = 4845)4647// NewPages are the pages of the view of h, the gen'th opening of its48// source, ordered as o says.49func NewPages(h fileio.Source, gen int, o *sheet.SourceOrder) *Pages {50 p := &Pages{h: h, gen: gen, pages: map[int64]*page{}, want: map[int64]bool{}}51 if o != nil {52 p.order = *o53 }54 p.building = true55 p.fresh = append(p.fresh, &PageJob{p: p, build: true})56 return p57}5859// Same reports whether the pages are of h's gen'th opening, ordered as o60// says.61func (p *Pages) Same(h fileio.Source, gen int, o *sheet.SourceOrder) bool {62 var ord sheet.SourceOrder63 if o != nil {64 ord = *o65 }66 return p.h == h && p.gen == gen && sameOrder(p.order, ord)67}6869// Of reports whether the pages are of h's gen'th opening, however70// ordered.71func (p *Pages) Of(h fileio.Source, gen int) bool { return p.h == h && p.gen == gen }7273func sameOrder(a, b sheet.SourceOrder) bool {74 return slices.Equal(a.Sort, b.Sort) && slices.Equal(a.Filter, b.Filter)75}7677// Close lets go of the view.78func (p *Pages) Close() {79 if p.view != nil {80 p.view.Close()81 }82}8384// Rows counts the rows the view shows, with false while it's built.85func (p *Pages) Rows() (int64, bool) {86 if p.view == nil {87 return 0, false88 }89 return p.view.Rows(), true90}9192// Building reports whether the view is being built; Err says why it93// couldn't be.94func (p *Pages) Building() bool { return p.building }95func (p *Pages) Err() string { return p.err }9697// Row is the view's row i: its number in the source and its values,98// with false while its page isn't read, which Want asks for.99func (p *Pages) Row(i int64) (int64, []sheet.LiveCell, bool) {100 pg := p.pages[i/PageRows]101 if pg == nil || int(i%PageRows) >= len(pg.rows) {102 return 0, nil, false103 }104 p.used++105 pg.used = p.used106 return pg.nums[i%PageRows], pg.rows[i%PageRows], true107}108109// Want asks for the pages holding rows from through to of the view, to110// be read unless they are.111func (p *Pages) Want(from, to int64) {112 if p.view == nil {113 return114 }115 last := p.view.Rows() - 1116 for n := max(from, 0) / PageRows; n <= min(to, last)/PageRows; n++ {117 if p.pages[n] != nil || p.want[n] {118 continue119 }120 p.want[n] = true121 p.fresh = append(p.fresh, &PageJob{p: p, n: n, view: p.view})122 }123}124125// Jobs hands out the reads asked for since the last call, to run in the126// background.127func (p *Pages) Jobs() []*PageJob {128 out := p.fresh129 p.fresh = nil130 return out131}132133// Store keeps what a job read, back on the owner's goroutine, and134// reports whether it changed what shows.135func (p *Pages) Store(j *PageJob) bool {136 if j.p != p {137 return false138 }139 if j.build {140 p.building = false141 p.view, p.err = j.view, ""142 if j.err != nil {143 p.err = describe(j.err)144 }145 return true146 }147 if j.view != p.view {148 delete(p.want, j.n)149 return false150 }151 if j.err != nil {152 // Still wanted, so not asked for again until the view is153 // made again: a page that can't be read would be read over154 // and over.155 p.err = describe(j.err)156 return true157 }158 delete(p.want, j.n)159 p.used++ // as the page most lately used, so it isn't the one let go160 p.pages[j.n] = &page{nums: j.nums, rows: j.rows, used: p.used}161 p.drop()162 return true163}164165// drop lets the least used pages go past keepPages.166func (p *Pages) drop() {167 for len(p.pages) > keepPages {168 least, at := int64(-1), int64(0)169 for n, pg := range p.pages {170 if least < 0 || pg.used < at {171 least, at = n, pg.used172 }173 }174 delete(p.pages, least)175 }176}177178// PageJob builds a view or reads a page of it, on any goroutine.179type PageJob struct {180 p *Pages181 build bool182 n int64183 view fileio.SourceView184 nums []int64185 rows [][]sheet.LiveCell186 err error187}188189// Run does the job.190func (j *PageJob) Run(ctx context.Context) {191 if j.build {192 fo := fileio.SourceOrder{}193 for _, s := range j.p.order.Sort {194 fo.Sort = append(fo.Sort, fileio.SourceSort{Col: s.Col, Desc: s.Desc})195 }196 for _, f := range j.p.order.Filter {197 fo.Filter = append(fo.Filter, fileio.SourceFilter{Col: f.Col, Cond: f.Cond})198 }199 j.view, j.err = j.p.h.View(ctx, fo)200 return201 }202 j.nums, j.rows, j.err = j.view.Page(ctx, j.n*PageRows, PageRows)203}