Documentation
¶
Overview ¶
Package eventbuf provides the bounded, concurrency-safe ring buffers that retain the CDP events a held connection receives, per target.
It is deliberately PURE: no CDP types, no context, no I/O. It knows nothing about console lines or network records — it is generic over the entry type so one implementation, and one test suite, serves every event-backed verb (RFC-0002's `console` and RFC-0003's `net`).
Callers own the entry shape and the filter predicate. This package owns the two things that make retention safe on a day-long session:
- the bounds — a ring of at most N entries per target, plus a total cap across targets, so an idle daemon cannot grow without limit;
- the accounting — a `dropped` counter that survives into the result envelope, so a caller can tell that it read too late instead of silently seeing a truncated history.
Two shapes of event are supported. Add appends an independent entry (a console line). Upsert folds a stream of correlated events into ONE entry by a caller-chosen key (a network request's start, response, and completion arrive as separate CDP events but are one record).
Index ¶
- func TruncateText(s string, max int) (string, bool)
- type Buffer
- func (b *Buffer[T]) Add(e T)
- func (b *Buffer[T]) Clear()
- func (b *Buffer[T]) Dropped() int
- func (b *Buffer[T]) Len() int
- func (b *Buffer[T]) Query(q Query[T]) Result[T]
- func (b *Buffer[T]) Subscribe(fn func(T)) (stop func())
- func (b *Buffer[T]) Upsert(key string, mutate func(cur T, existed bool) T) T
- type Query
- type Result
- type Set
- func (s *Set[T]) Add(targetID string, e T)
- func (s *Set[T]) Buffer(targetID string) *Buffer[T]
- func (s *Set[T]) Clear(targetID string)
- func (s *Set[T]) Forget(targetID string)
- func (s *Set[T]) Query(targetID string, q Query[T]) Result[T]
- func (s *Set[T]) Stat(targetID string) (buffered, dropped int)
- func (s *Set[T]) Subscribe(targetID string, fn func(T)) (stop func())
- func (s *Set[T]) Targets() []string
- func (s *Set[T]) Total() int
- func (s *Set[T]) Upsert(targetID, key string, mutate func(cur T, existed bool) T) T
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func TruncateText ¶
TruncateText caps s at max bytes, reporting whether it was cut.
It is here rather than in a caller because every event-backed verb needs the same bound (console message text, a network body) and the same subtlety: the cut lands on a rune boundary, so a truncated entry that WAS valid UTF-8 stays valid and still marshals into the envelope. max <= 0 means no cap.
It backs off at most utf8.UTFMax-1 bytes — the most a single rune can have on the wrong side of the cut — and deliberately no further. Re-validating the whole prefix after every byte, as this used to, is quadratic (a 64 KB body cost ~10 ms of scanning on the daemon's dispatch path) AND wrong: a payload whose invalid bytes start anywhere before the cut never converges, so it was trimmed to "". Truncation must not be able to destroy its input. Deciding whether the input is text at all is the CALLER's job, on the original bytes.
Types ¶
type Buffer ¶
type Buffer[T any] struct { // contains filtered or unexported fields }
Buffer is a bounded ring of entries for ONE target, safe for concurrent use.
The zero value is not usable; construct with New.
func New ¶
New returns a buffer holding at most max entries. A max of 0 (or less) is legal and holds nothing: every Add is counted as immediately dropped, which is the honest reading of "retain zero entries" rather than a silent no-op.
func (*Buffer[T]) Add ¶
func (b *Buffer[T]) Add(e T)
Add appends an independent entry, evicting the oldest when the ring is full.
func (*Buffer[T]) Clear ¶
func (b *Buffer[T]) Clear()
Clear empties the buffer and resets the dropped counter.
Resetting `dropped` is deliberate: an explicit clear starts a new observation window, and carrying an eviction count from before it would report "you read too late" about messages the caller threw away on purpose.
func (*Buffer[T]) Dropped ¶
Dropped returns how many entries the bound has evicted since the buffer was created or last cleared.
func (*Buffer[T]) Subscribe ¶
func (b *Buffer[T]) Subscribe(fn func(T)) (stop func())
Subscribe registers fn to receive every entry added or updated from now on, and returns the function that unregisters it.
fn runs while the buffer's lock is held, which is what keeps a subscriber's view in the same order the events arrived. It must therefore NEVER block and NEVER call back into this buffer or its Set — hand the entry to a buffered channel and do the work elsewhere.
func (*Buffer[T]) Upsert ¶
Upsert folds a correlated event into a single entry.
If key still names a live entry, mutate is called with it (existed = true) and the result replaces it in place — no eviction, no dropped++, and the entry keeps its original position in the ring, so a long-running request does not jump to the front of the history. Otherwise mutate is called with the zero value (existed = false) and the result is appended.
Out-of-order events are therefore tolerated by construction: whichever event arrives first creates the entry, and the rest merge into it.
type Query ¶
type Query[T any] struct { // Keep reports whether an entry matches. A nil Keep keeps everything. // Compose level / regex / since / status predicates here: the buffer stays // free of any knowledge of what an entry means. Keep func(T) bool // Limit keeps only the most recent Limit matches (0 = every match). Limit int // Clear empties the buffer after the query is answered, so the next read is // scoped to whatever the caller does next. Clear bool }
Query selects entries from a buffer. It is applied SERVER-SIDE — where the buffer lives, before the envelope is marshalled — so a chatty page cannot flood a caller's context.
type Result ¶
type Result[T any] struct { Entries []T // the matches, oldest first, at most Limit of them Count int // len(Entries) Buffered int // entries the buffer held when the query ran Dropped int // entries evicted by the bound since the buffer was last cleared Truncated bool // Limit cut the match list }
Result is a query's answer plus the accounting a caller needs to trust it.
type Set ¶
type Set[T any] struct { // contains filtered or unexported fields }
Set is the per-target collection of buffers, with a total cap across targets as the backstop.
Per-target rings alone bound each tab, but a session that touches many tabs would still grow linearly in tabs; the total cap makes the worst case a constant. When it is exceeded, the LARGEST buffer gives up its oldest entry — so the chatty tab pays, not the quiet one that happened to be added last.
The zero value is not usable; construct with NewSet.
func NewSet ¶
NewSet returns a Set whose per-target rings hold perTarget entries, capped at total entries across all targets (total <= 0 disables the total cap).
func (*Set[T]) Forget ¶
Forget discards a target's buffer entirely — for a tab that has closed, whose entries can never be read again.
func (*Set[T]) Stat ¶
Stat returns a target's live counts without materializing its entries — for the per-message accounting a streaming (`--follow`) read carries.