Documentation
¶
Overview ¶
Package source coordinates a windowed view over a paged remote source — the bookkeeping between "the user scrolled here" and "ask the server for that range", without ever doing the I/O itself.
It is shaped like pkg/poll: it owns no data and performs no requests. It tracks which window is held, which is in flight, and which query each request belongs to, and it emits RequestMsg when the rows on screen stop being rows it has. Your screen answers that with whatever HTTP, gRPC, or database call it likes, passing Query.Ctx so a superseded request is cancelled, then hands the result back through Deliver and pushes the rows into the component. Keeping the fetch in the screen is deliberate: every component in tuilib is synchronous, and a coordinator that owned a retry policy would drag it into places that have no business holding it.
It deliberately does not import pkg/table — nothing from tuilib but pkg/query, and internal/tick for its timers. The table reports what happened (ViewportChangedMsg, QueryChangedMsg) and the screen translates those into Viewport and SetQuery calls here, which keeps this package usable for any component that can say which rows are on screen — and keeps the dependency pointing one way.
The loop, in full:
func (s *Screen) Init() tea.Cmd { return s.src.Init() }
case table.ViewportChangedMsg:
cmds = append(cmds, s.src.Viewport(msg.FirstVisible, msg.LastVisible))
case table.QueryChangedMsg:
cmds = append(cmds, s.src.SetQuery(msg.Raw, msg.Terms, msg.Sort, msg.Desc))
case source.RequestMsg:
return s, s.fetch(msg.Query) // your call, under msg.Query.Ctx
case fetchedMsg:
ok, cmd := s.src.Deliver(msg.Page)
switch {
case !ok: // superseded; drop it
case msg.Page.Err != nil:
s.table.SetFailed(answered(msg.Query))
default:
s.table.SetWindow(msg.Rows, msg.Page.Offset, msg.Page.Total, answered(msg.Query))
}
return s, cmd
}
cmds = append(cmds, s.src.Update(msg)) // every message, like pkg/poll
Installing the window makes the component emit a fresh ViewportChangedMsg, which closes the loop: a short page that still doesn't cover the screen asks for the rest on its own.
Three things keep a slow source bearable. Every request carries its own context, cancelled the moment a newer request supersedes it — a new query, or a newer window of the same one — so at most one request per source is live on the server. Scrolling asks only once the viewport has been still for Options.ViewportDelay, so a held key or a scrollbar drag is one request, not one per page crossed. And while a committed query has no answer yet, scrolling asks for nothing at all: the rows on screen answer the previous query, and their positions mean nothing in the next.
The query history — each committed query answered, failed or cancelled — leaves as QueryAnsweredMsg, QueryFailedMsg and QueryCancelledMsg in the commands SetQuery and Deliver return. They are deliberately neutral, like pkg/runner's messages: pkg/app turns them into output-console records.
Index ¶
- Constants
- type Anchor
- type Anchored
- func (m Anchored) Anchor() Anchor
- func (m *Anchored) Cancel()
- func (m *Anchored) Deliver(p Page) (bool, tea.Cmd)
- func (m *Anchored) Find(term string, dir Dir) tea.Cmd
- func (c Anchored) ID() int64
- func (m *Anchored) Init() tea.Cmd
- func (m Anchored) More() (older, newer bool)
- func (m Anchored) PageSize() int
- func (m Anchored) Pending() bool
- func (m *Anchored) Poke() tea.Cmd
- func (m Anchored) PollsFailing() bool
- func (m *Anchored) Refresh() tea.Cmd
- func (m *Anchored) SetAnchor(a Anchor) tea.Cmd
- func (m *Anchored) SetGrowing(b bool) tea.Cmd
- func (m *Anchored) SetQuery(raw string, terms []query.Term, sort string, desc bool) tea.Cmd
- func (m *Anchored) Update(msg tea.Msg) tea.Cmd
- func (m *Anchored) Viewport(toOlder, toNewer int) tea.Cmd
- type AnchoredOptions
- type Dir
- type Mode
- type Model
- func (m *Model) Cancel()
- func (m *Model) Deliver(p Page) (bool, tea.Cmd)
- func (m Model) Exhausted() bool
- func (m *Model) Find(term string, dir Dir, from int) tea.Cmd
- func (m Model) Growing() bool
- func (m Model) Held() (start, count int)
- func (c Model) ID() int64
- func (m *Model) Init() tea.Cmd
- func (m Model) PageSize() int
- func (m Model) Pending() bool
- func (m *Model) Poke() tea.Cmd
- func (m Model) PollsFailing() bool
- func (m *Model) Refresh() tea.Cmd
- func (m *Model) SetGrowing(b bool) tea.Cmd
- func (m *Model) SetHeld(start, count int)
- func (m *Model) SetQuery(raw string, terms []query.Term, sort string, desc bool) tea.Cmd
- func (m Model) Total() int
- func (m *Model) Update(msg tea.Msg) tea.Cmd
- func (m *Model) Viewport(first, last int) tea.Cmd
- type Options
- type Page
- type Query
- type QueryAnsweredMsg
- type QueryCancelledMsg
- type QueryFailedMsg
- type QueryRecoveredMsg
- type RequestMsg
Constants ¶
const DefaultFollow = 2 * time.Second
DefaultFollow is the poll interval for Growing data when Options.Follow is zero.
const DefaultPageSize = 100
DefaultPageSize is the window size used when Options.PageSize is unset.
const DefaultViewportDelay = 150 * time.Millisecond
DefaultViewportDelay is how long the viewport must be still before a scroll asks for rows, when Options.ViewportDelay is zero.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Anchor ¶ added in v0.31.0
type Anchor struct {
// contains filtered or unexported fields
}
Anchor is where a view of Anchored data begins.
func Newest ¶ added in v0.31.0
func Newest() Anchor
Newest anchors at the newest item — the tail. The zero Anchor.
func (Anchor) Cursor ¶ added in v0.34.0
Cursor returns the cursor an At anchor names, and false for Newest and Oldest.
type Anchored ¶ added in v0.31.0
type Anchored struct {
// contains filtered or unexported fields
}
Anchored coordinates a view over Anchored data — items reachable only by walking forwards or backwards from an anchor, with no offsets and no reliable total: Elasticsearch search_after, a log API filtered by timestamp. It is Model's counterpart and shares its request plumbing, cancellation, viewport delay and query history, but its surface is edges rather than offsets.
The component holds a Span: items keyed by their cursors, grown at either edge and trimmed at the one furthest from the viewport. The source never stores or interprets a cursor. When it asks for more at an edge, the screen reads that edge's cursor from the component and encodes it for its API:
case source.RequestMsg:
older, newer := s.log.Edges()
return s, s.fetch(m.Query, older, newer) // m.Query.Dir says which
The request that starts a view has Query.FromAnchor set: page from the anchor, not from an edge — the span may still hold the previous query's items. Every other request extends the edge Query.Dir names.
One request is in flight at a time. The viewport reports how far it is from each edge; an edge with more beyond it is extended once the viewport comes within PageSize of it, nearest edge first.
func NewAnchored ¶ added in v0.31.0
func NewAnchored(opts AnchoredOptions) Anchored
NewAnchored constructs an Anchored source anchored at opts.Anchor. Nothing is requested until Init.
func (*Anchored) Cancel ¶ added in v0.31.0
func (m *Anchored) Cancel()
Cancel cancels whatever is in flight.
func (*Anchored) Deliver ¶ added in v0.31.0
Deliver records a fetched page — or a failure, in p.Err — and reports whether it was accepted. For an edge request, p.More says whether that edge has more beyond it. The command carries the query history and the next request, when the page left an edge still wanted.
func (*Anchored) Find ¶ added in v0.31.0
Find asks for the first item matching term beyond the edge in direction dir, within the committed filter. The screen fills the Cursor from the component's edge, as for any edge request. A reply with Page.Found and Page.Cursor is the hit: re-anchor with SetAnchor(At(cursor)).
func (Anchored) ID ¶ added in v0.31.0
func (c Anchored) ID() int64
ID identifies this source in the RequestMsg values it emits.
func (*Anchored) Poke ¶ added in v0.31.0
Poke makes a Growing source poll now — for a push channel saying there is news.
func (Anchored) PollsFailing ¶ added in v0.31.0
PollsFailing reports whether follow polls are failing.
func (*Anchored) Refresh ¶ added in v0.31.0
Refresh re-anchors at the current anchor under the same query.
func (*Anchored) SetAnchor ¶ added in v0.31.0
SetAnchor moves the view: the span is replaced by one starting at a. A search hit re-anchors with At(hit).
func (*Anchored) SetGrowing ¶ added in v0.31.0
SetGrowing says whether items are arriving at the newest end. While they are, the source polls every Follow: a fetch of what is newer than the newest edge while the viewport shows it (following), a Probe otherwise. Setting it false stops after one final read.
func (*Anchored) SetQuery ¶ added in v0.31.0
SetQuery installs a new filter and sort and re-anchors at the current anchor. Everything in flight is cancelled; a query abandoned before it was answered is reported as QueryCancelledMsg.
func (*Anchored) Update ¶ added in v0.31.0
Update handles the source's timers. Forward every message to it.
func (*Anchored) Viewport ¶ added in v0.31.0
Viewport reports how many items lie between the viewport and each edge of the span: toOlder above the first visible item, toNewer below the last. Feed it from the component's viewport message. An edge with more beyond it is extended once the viewport comes within PageSize of it, after Options.ViewportDelay.
type AnchoredOptions ¶ added in v0.31.0
type AnchoredOptions struct {
// PageSize is how many items one request asks for, and how close to an
// edge the viewport must come before that edge is extended. Defaults to
// DefaultPageSize.
PageSize int
// Name, ViewportDelay, Context and Follow mean what they do on Options.
Name string
ViewportDelay time.Duration
Context context.Context
Follow time.Duration
// Anchor is where the view starts. The zero value is Newest.
Anchor Anchor
}
AnchoredOptions configures an Anchored source.
type Mode ¶
type Mode int
Mode selects how the source is addressed.
const ( // ByOffset addresses rows by numeric offset ("?offset=200&limit=100"). // Windows can jump anywhere, so scrolling to the middle of a large set // fetches exactly the range on screen. Zero value. ByOffset Mode = iota // ByCursor addresses rows by an opaque continuation token. Only // forward, sequential paging is possible, so the screen accumulates // rows and installs them as one growing window at offset 0 — pass // Total -1 to SetWindow until the source runs out. ByCursor )
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is the coordinator. Embed as a value; drive it through the methods.
func (*Model) Cancel ¶ added in v0.31.0
func (m *Model) Cancel()
Cancel cancels whatever is in flight. A reply that arrives anyway is refused by Deliver.
func (*Model) Deliver ¶
Deliver records a fetched page — or a failed fetch, when p.Err is set — and reports whether it was accepted. A false return means the page answers a superseded request: drop it. The command carries the query history (QueryAnsweredMsg, QueryFailedMsg) and, when a query's first answer leaves part of the screen uncovered, the request for the rest.
func (Model) Exhausted ¶
Exhausted reports whether a ByCursor walk has run out of pages. Always false under ByOffset, where the total says the same thing.
func (*Model) Find ¶ added in v0.31.0
Find asks for the first item matching term after from (Newer) or before it (Older), within the committed filter. The reply's Page.Found and Page.Offset say where the match is; Deliver accepts it like any page but installs nothing, so the screen moves the component there.
func (Model) Growing ¶ added in v0.31.0
Growing reports whether the source is polling for new items.
func (Model) Held ¶
Held reports the window the source has supplied: its first row's logical index and how many rows it holds.
func (Model) ID ¶ added in v0.31.0
func (c Model) ID() int64
ID identifies this source in the RequestMsg values it emits.
func (*Model) Init ¶
Init returns the first request — the opening page of the current query. Batch it into your screen's Init, the way poll.Init is batched. It is needed because an empty component reports no viewport, so nothing else would ever ask for the first page.
func (*Model) Poke ¶ added in v0.31.0
Poke makes a Growing source poll now rather than at the next interval — for a push channel (a websocket) saying there is news. Push channels are hints: they can drop messages, so the data still comes from the poll.
func (Model) PollsFailing ¶ added in v0.31.0
PollsFailing reports whether follow polls are failing. The first failure was reported as QueryFailedMsg; the first success will be reported as QueryRecoveredMsg.
func (*Model) Refresh ¶
Refresh re-requests the window currently on screen without changing the query — the poll-driven "same view, fresh data" case, and the retry after a failure. Under ByCursor it restarts from the first page, since a cursor walk cannot be resumed from the middle.
func (*Model) SetGrowing ¶ added in v0.31.0
SetGrowing says whether the data is gaining items at its end — a job or flow run still running. While it is, the source polls every Options.Follow: while the viewport shows the newest item (the component is following) a poll fetches the tail, and otherwise it is a probe of one item that only learns the grown total. Setting it false stops polling after one final read, which catches items saved after the run ended.
func (*Model) SetHeld ¶ added in v0.31.0
SetHeld adopts the range the component actually holds, after it trimmed past its cap. Only meaningful with Options.MaxHeld.
func (*Model) SetQuery ¶
SetQuery installs a new filter and sort, discards the held window, and requests the first page of the new query. Everything in flight is cancelled, and Deliver refuses replies to the query that was just replaced, so a slow response to the previous filter cannot land under the new one. A query abandoned before it was answered is reported as QueryCancelledMsg.
It always requests, even when the arguments match the current query — a caller that has gone to the trouble of calling it wants a fetch.
func (Model) Total ¶
Total is the logical row count last reported, or -1 while unknown. Pass it straight to the component's window setter.
func (*Model) Update ¶ added in v0.31.0
Update handles the source's own timer. Forward every message to it, the way pkg/poll is forwarded; anything that isn't the source's returns nil.
func (*Model) Viewport ¶
Viewport reports the logical row range now on screen, inclusive. Feed it from the component's viewport message. When those rows are not ones the source has supplied, it asks for them once the viewport has been still for Options.ViewportDelay — so calling it on every scroll tick is fine.
type Options ¶
type Options struct {
// Name labels this source's query history, so a screen with several
// sources can tell their lines apart ("books: filter x → answered").
// Empty leaves the lines unlabelled.
Name string
// Mode selects offset or cursor addressing. Defaults to ByOffset.
Mode Mode
// PageSize is how many rows one request asks for, and the boundary
// windows align to. Defaults to DefaultPageSize.
PageSize int
// Prefetch is how many extra pages to pull beyond the range actually
// on screen. Zero (the default) fetches only what is needed, so the
// user sees placeholders briefly at each page boundary; one page of
// prefetch usually hides that at the cost of an extra request.
Prefetch int
// ViewportDelay is how long the viewport must be still before a
// scroll asks for rows. Zero means DefaultViewportDelay; negative
// means ask at once. Init, SetQuery and Refresh never wait.
ViewportDelay time.Duration
// Context is the parent of every request's context, so cancelling it
// cancels everything in flight. Defaults to context.Background().
Context context.Context
// MaxHeld keeps a contiguous range of up to this many items rather
// than one window: a page adjacent to what is held extends it, a
// disjoint one replaces it. Zero keeps one window, which is what a
// table wants; a timeline read back and forth wants a range. The
// component trims the range itself and reports it through SetHeld.
MaxHeld int
// Follow is how often a Growing source is polled (see SetGrowing).
// Zero means DefaultFollow; negative never polls.
Follow time.Duration
}
Options configures a Model.
type Page ¶
type Page struct {
// Gen must echo the Query.Gen that produced this page.
Gen int
// Offset is where these rows start (ByOffset). Ignored under ByCursor,
// where pages always extend the accumulated window.
Offset int
// Count is how many rows arrived. A zero count is remembered, so an
// offset the source has nothing for is not asked for twice.
Count int
// Total is the logical row count, or -1 when the source can't say.
Total int
// Next is the continuation token for the following page (ByCursor).
// Empty means the source is exhausted, which is also what finally
// establishes the total.
Next string
// Err is set when the fetch failed. Deliver it like any other page:
// Deliver decides whether anyone is still waiting for it.
Err error
// Found answers a Find: whether a match exists, at Offset (or Cursor,
// on Anchored).
Found bool
// Cursor is the match's cursor when a Find on Anchored data found one.
Cursor string
// More reports, on Anchored, whether the edge this page extended has
// more beyond it. False closes that edge.
More bool
}
Page reports what a fetch returned. Rows themselves never come here — they go straight from the screen into the component.
type Query ¶
type Query struct {
// Offset is the first row wanted (ByOffset only).
Offset int
// Limit is how many rows are wanted.
Limit int
// Cursor is the continuation token to resume from (ByCursor only).
// Empty on the first request of a query.
Cursor string
// Raw is the filter text the user committed, as typed.
Raw string
// Terms is Raw parsed. Scoped terms carry their resolved column Title,
// so building "?region=europe" needs no lookup.
Terms []query.Term
// Sort is the column title to order by, "" when unsorted.
Sort string
// Desc reverses the order.
Desc bool
// Gen identifies this request. Echo it back in Page.Gen; anything
// older is refused by Deliver, which is what stops an out-of-order
// reply from painting a window the user has already scrolled past.
Gen int
// Ctx is cancelled as soon as this request is superseded — by a new
// query, a newer window, Refresh, or Cancel — and once its page has
// been delivered. Pass it to the call that does the fetch.
Ctx context.Context
// Poll marks a follow poll: a request the source made on its own
// because the data is Growing (see Options.Follow). Fetch it like any
// other window.
Poll bool
// Probe marks a poll made while the user is not following (Anchored):
// fetch it, but only count what came back — the component shows how
// much is new without appending it.
Probe bool
// Inclusive, on Anchored, includes the item at Cursor itself: the
// first request of an At anchor, so the anchor appears exactly once.
Inclusive bool
// FromAnchor, on Anchored, marks the request that starts a view: page
// from the anchor — Cursor for At, the end for Newest or Oldest —
// rather than from the component's edge.
FromAnchor bool
// Find asks for the first item matching Term, starting after Offset
// (Dir Newer) or before it (Dir Older), within the committed filter.
// Limit is 1. Answer with Page.Found and the match's position in
// Page.Offset (Model) or Page.Cursor (Anchored).
Find bool
Term string
// Dir is the direction a Find — or, on Anchored, an edge request —
// walks.
Dir Dir
}
Query is one request's worth of parameters. Everything the source needs to answer is here; nothing about how to reach it is.
type QueryAnsweredMsg ¶ added in v0.31.0
QueryAnsweredMsg reports that a committed query got its first answer — or its first answer after failing. One per committed query, never one per page.
type QueryCancelledMsg ¶ added in v0.31.0
QueryCancelledMsg reports a committed query abandoned before it was answered, because By was committed in its place.
type QueryFailedMsg ¶ added in v0.31.0
QueryFailedMsg reports a fetch that failed while someone was still waiting for it. Window is true when the query already had an answer and only a page of it failed.
type QueryRecoveredMsg ¶ added in v0.31.0
QueryRecoveredMsg reports that follow polls are succeeding again after failing. QueryFailedMsg reported the start of the outage, once.
type RequestMsg ¶
RequestMsg asks the screen to fetch Query. Source names the model that asked, so a screen with several sources — or pkg/remote, binding one — answers only its own.