eventbuf

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 2 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func TruncateText

func TruncateText(s string, max int) (string, bool)

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

func New[T any](max int) *Buffer[T]

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

func (b *Buffer[T]) Dropped() int

Dropped returns how many entries the bound has evicted since the buffer was created or last cleared.

func (*Buffer[T]) Len

func (b *Buffer[T]) Len() int

Len returns how many entries the buffer currently holds.

func (*Buffer[T]) Query

func (b *Buffer[T]) Query(q Query[T]) Result[T]

Query returns the entries matching q, oldest first, plus the accounting.

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

func (b *Buffer[T]) Upsert(key string, mutate func(cur T, existed bool) T) T

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

func NewSet[T any](perTarget, total int) *Set[T]

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]) Add

func (s *Set[T]) Add(targetID string, e T)

Add appends an entry for a target and enforces the total cap.

func (*Set[T]) Buffer

func (s *Set[T]) Buffer(targetID string) *Buffer[T]

Buffer returns the buffer for a target, creating it on first use.

func (*Set[T]) Clear

func (s *Set[T]) Clear(targetID string)

Clear empties one target's buffer.

func (*Set[T]) Forget

func (s *Set[T]) Forget(targetID string)

Forget discards a target's buffer entirely — for a tab that has closed, whose entries can never be read again.

func (*Set[T]) Query

func (s *Set[T]) Query(targetID string, q Query[T]) Result[T]

Query answers a query against one target's buffer.

func (*Set[T]) Stat

func (s *Set[T]) Stat(targetID string) (buffered, dropped int)

Stat returns a target's live counts without materializing its entries — for the per-message accounting a streaming (`--follow`) read carries.

func (*Set[T]) Subscribe

func (s *Set[T]) Subscribe(targetID string, fn func(T)) (stop func())

Subscribe registers fn for one target's entries (see Buffer.Subscribe).

func (*Set[T]) Targets

func (s *Set[T]) Targets() []string

Targets lists the targets this set holds a buffer for.

func (*Set[T]) Total

func (s *Set[T]) Total() int

Total returns how many entries are held across every target.

func (*Set[T]) Upsert

func (s *Set[T]) Upsert(targetID, key string, mutate func(cur T, existed bool) T) T

Upsert folds a correlated event into a target's entry for key (see Buffer.Upsert) and enforces the total cap.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL