events

package
v0.1.16 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package events implements the in-process pub/sub hub behind the per-resource SSE subscription endpoints (GET /api/{resource}/events, S45).

ADR-015: single-node by design. The engine runs as one process today, so the hub is an in-memory map with no external broker; when HA/multi-node ships, a broker (or Postgres LISTEN/NOTIFY) replaces the fan-out — the Publish call sites stay the same. Delivery is at-most-once with no replay/Last-Event-ID: a subscriber that connects after an event, or that is closed as a slow consumer, misses it (documented limitation; replay is future work).

Index

Constants

View Source
const DefaultMaxPerTenant = 1000

DefaultMaxPerTenant bounds concurrent subscribers per tenant when no explicit limit is configured (APPXIMO_MAX_SSE_PER_TENANT).

Variables

View Source
var ErrTenantLimit = errors.New("tenant SSE subscriber limit reached")

ErrTenantLimit is returned by Subscribe when the tenant has reached its concurrent-subscriber cap. The HTTP layer maps it to 429.

Functions

This section is empty.

Types

type Event

type Event struct {
	Type     string // "create" | "update" | "delete"
	Resource string
	ID       string
	Record   map[string]any // shared across subscribers — receivers must not mutate
}

Event is one resource change, broadcast to every subscriber of (tenant, resource). Record is the post-write row (RETURNING *) for create/update, and nil for delete (the row is gone; only the id is known — the DELETE path does not pay for a RETURNING just in case someone listens).

type Hub

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

Hub is the process-wide registry of subscribers, keyed tenant → resource → subscriber set. A nil *Hub is valid and inert: Publish/Count no-op and Subscribe fails, so tests and callers that don't wire SSE pass nil.

func NewHub

func NewHub(maxPerTenant int) *Hub

NewHub builds a Hub. maxPerTenant <= 0 falls back to DefaultMaxPerTenant.

func (*Hub) Count

func (h *Hub) Count(tenant string) int

Count returns the number of live subscribers for a tenant (all resources).

func (*Hub) Publish

func (h *Hub) Publish(tenant string, ev Event)

Publish broadcasts ev to every subscriber of (tenant, ev.Resource). It NEVER blocks the caller (the request path): the per-subscriber send is a non-blocking select, and a subscriber whose buffer is full is marked slow (its Slow channel is closed once) so its connection terminates. With zero subscribers the cost is one RLock + two map lookups.

func (*Hub) Subscribe

func (h *Hub) Subscribe(tenant, resource string, allowedFields []string, condField string, condValue any) (*Subscriber, error)

Subscribe registers a new subscriber for (tenant, resource) and returns it. Returns ErrTenantLimit when the tenant is at its concurrent cap.

func (*Hub) Unsubscribe

func (h *Hub) Unsubscribe(sub *Subscriber)

Unsubscribe removes sub from the hub. Idempotent; safe on a sub that was never registered. Every connection MUST call this on exit (defer) — it is what prevents subscriber/goroutine leaks.

type Subscriber

type Subscriber struct {
	// C delivers events. Never closed (avoids send-on-closed races); the
	// handler exits via Slow or its request context instead.
	C chan Event
	// Slow is closed (once) when the subscriber's buffer overflows. The handler
	// must send a final error event and terminate the connection.
	Slow chan struct{}

	// AllowedFields is the RBAC field allowlist captured at subscribe time
	// (empty = all fields). The SSE writer applies the same FilterFields the
	// GET handlers use, so a role never sees a field in an event that it
	// cannot see in a GET.
	AllowedFields []string
	// CondField/CondValue carry the row-level RBAC condition captured at
	// subscribe time ("" = none). Only eq is supported — the only operator the
	// policy engine emits today; events for non-matching rows are not
	// delivered (same row scoping as the list endpoint).
	CondField string
	CondValue any
	// contains filtered or unexported fields
}

Subscriber is one open SSE connection's mailbox. The HTTP handler owns the receive side: it selects on C (events), Slow (closed by the hub when this subscriber's buffer overflows), and the request context.

Jump to

Keyboard shortcuts

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