events

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package events implements the in-process publish/subscribe broker behind dbbat's live event stream.

The broker is deliberately transport-agnostic: it knows about topics, sequence numbers and backpressure, and nothing about WebSockets, SSE, gin or HTTP. That separation is the mitigation for the transport trade-off recorded in docs/approvals.md — the stream currently ships over a WebSocket, but the envelope below would ride an SSE response body unchanged, so a later swap is confined to internal/api/stream.go.

Two invariants matter more than anything else here:

  • Publishing is non-blocking and never returns an error. Events originate on the proxy hot path, and a slow or broken stream subscriber must never be able to stall — let alone break — a live database connection.
  • Authorization is *not* the broker's job. Subscribers carry an authorizer supplied by the API layer, re-evaluated on every send, because sql_text routinely carries PII and the stream must never be a wider read path than GET /api/v1/queries. That re-check runs in the *transport's* write loop, never on the publish path: the API's authorizer does store round-trips, and calling it inline from Publish would let a slow database stall a proxy session holding a query.

Index

Constants

View Source
const (
	// TopicApprovalsPending carries every approval-pending query across all
	// connections. Low volume by construction, and exempt from drop-on-overflow.
	TopicApprovalsPending = "approvals/pending"
	// TopicConnections carries connection open/close events. Admin only.
	TopicConnections = "connections"
	// TopicConnectionPrefix is the prefix of the per-connection query topic:
	// connection/<uid>/queries.
	TopicConnectionPrefix = "connection/"
	// TopicConnectionSuffix closes the per-connection query topic.
	TopicConnectionSuffix = "/queries"
)

Topic names and prefixes. The namespace is the extension point; nothing else about the transport is query-specific.

View Source
const (
	// EventQuery is a query observed on a connection.
	EventQuery = "query"
	// EventApprovalPending is a query parked awaiting a human decision.
	EventApprovalPending = "approval_pending"
	// EventApprovalResolved is the resolution of a hold — approved, denied or
	// abandoned — carrying who resolved it.
	EventApprovalResolved = "approval_resolved"
	// EventConnection is a connection open/close.
	EventConnection = "connection"
)

Event types carried in the envelope's `type` field.

View Source
const DefaultBuffer = 256

DefaultBuffer is the per-subscriber send-buffer depth. Deep enough to ride out a GC pause or a slow browser tab, shallow enough that a genuinely stuck subscriber is detected in bounded memory.

View Source
const MaxTopicLength = 128

MaxTopicLength bounds a topic name. The longest legitimate one is connection/<uuid>/queries; anything materially longer is a client trying to make the server remember something.

Variables

This section is empty.

Functions

func ConnectionQueriesTopic

func ConnectionQueriesTopic(connectionUID string) string

ConnectionQueriesTopic builds the per-connection query topic name.

func ConnectionUIDFromTopic

func ConnectionUIDFromTopic(topic string) (string, bool)

ConnectionUIDFromTopic extracts the connection uid from a connection/<uid>/queries topic, reporting whether the topic had that shape.

func ValidTopic

func ValidTopic(topic string) bool

ValidTopic reports whether a topic name is one this server can ever serve. It is a pure string check with no I/O, so the transport can reject junk before it reaches anything that allocates or hits the database.

Types

type Authorizer

type Authorizer func(topic string) bool

Authorizer decides whether a subscriber may currently receive a topic. It is evaluated at subscribe time *and* again immediately before each send, so a user whose role or ownership changed mid-stream stops receiving at once rather than at the next reconnect.

It may be expensive (the API implementation reads the store), so the broker never calls it while publishing — see Authorized.

type Broker

type Broker struct {
	// contains filtered or unexported fields
}

Broker is the in-process fan-out hub.

func New

func New() *Broker

New creates an empty broker.

func (*Broker) Publish

func (b *Broker) Publish(topic, eventType string, data map[string]any)

Publish delivers an event to every local subscriber of the topic and hands it to the forwarder for other replicas. Never blocks, never errors: a nil broker is a no-op so proxy code can call it unconditionally.

func (*Broker) PublishLocal

func (b *Broker) PublishLocal(topic, eventType string, data map[string]any)

PublishLocal delivers an event to local subscribers only, without re-forwarding. This is the entry point for events arriving *from* another replica — forwarding them again would loop.

func (*Broker) SetForwarder

func (b *Broker) SetForwarder(f Forwarder)

SetForwarder installs (or clears, with nil) the cross-replica forwarder.

func (*Broker) Subscribe

func (b *Broker) Subscribe(authorize Authorizer, buffer int) *Subscriber

Subscribe registers a new subscriber. The returned *Subscriber must be closed by the caller (typically with defer) to release broker resources.

func (*Broker) SubscriberCount

func (b *Broker) SubscriberCount() int

SubscriberCount reports how many subscribers are attached. Test/telemetry helper.

type Event

type Event struct {
	Topic string         `json:"topic"`
	Seq   uint64         `json:"seq"`
	Type  string         `json:"type"`
	Data  map[string]any `json:"data"`
	At    time.Time      `json:"at"`
}

Event is one published message. Seq is assigned by the broker at publish time and is monotonic per broker instance (not across replicas — clients use it to detect local gaps, not to order globally).

type Forwarder

type Forwarder func(ev Event)

Forwarder is called for every locally-originated publish so the caller can fan the event out to other replicas (dbbat runs multiple pods; the session holding a query is on replica A while the admin's socket is on replica B). It must not block; the broker calls it inline on the publish path.

type Subscriber

type Subscriber struct {
	// contains filtered or unexported fields
}

Subscriber is one client's view of the broker: a topic set plus a bounded delivery channel.

func (*Subscriber) Authorized

func (s *Subscriber) Authorized(topic string) bool

Authorized re-evaluates the subscriber's access to a topic. The transport calls it immediately before writing each event — that is the actual moment of send, and it is a goroutine the session does not wait on, so an expensive authorizer costs latency on one client's stream and nothing else.

func (*Subscriber) Close

func (s *Subscriber) Close()

Close detaches the subscriber from the broker and closes its channel. Idempotent.

func (*Subscriber) Dropped

func (s *Subscriber) Dropped() int64

Dropped returns and resets the number of events dropped due to overflow. The transport turns a non-zero value into {"type":"lagged","dropped":N} so the client knows to refetch from REST rather than assume continuity.

func (*Subscriber) Events

func (s *Subscriber) Events() <-chan Event

Events is the delivery channel. It is closed when the subscriber is closed.

func (*Subscriber) Subscribe

func (s *Subscriber) Subscribe(topic string) bool

Subscribe adds a topic after re-checking authorization. Returns false when the subscriber may not read that topic.

func (*Subscriber) TakePriority

func (s *Subscriber) TakePriority() []Event

TakePriority drains any drop-exempt events that overflowed the main channel. The transport calls this alongside each channel read so pending-approval events are delivered even when the client is behind on everything else.

func (*Subscriber) Topics

func (s *Subscriber) Topics() []string

Topics returns the currently subscribed topic names.

func (*Subscriber) Unsubscribe

func (s *Subscriber) Unsubscribe(topic string)

Unsubscribe removes a topic.

Jump to

Keyboard shortcuts

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