notify

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package notify implements outbound transports for newly-detected signals. Notification is decoupled from detection: any code path that calls SignalRepository.Save through a NotifyingSignalRepository triggers the configured ports.Notifier without the engine, the detectors, or the pipeline knowing or caring how delivery happens.

Available transports:

  • WebhookNotifier — HMAC-signed POST per signal to one or more URLs
  • SSE — bounded in-memory broadcast for /v1/signals/stream clients
  • Multi — composite that fans out to several Notifiers

Delivery is at-most-once per consumer; consumers de-duplicate by Signal.ID. Persistence is the source of truth — push is a courtesy.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AckingNotifier added in v0.4.0

type AckingNotifier interface {
	NotifyAck(ctx context.Context, sig domain.Signal) error
}

AckingNotifier is the contract for downstream notifiers that can report whether a delivery succeeded. The standard webhook is best-effort; AckingWebhook (next sibling) reports per-call success so the outbox can decide whether to retry.

type Multi

type Multi []ports.Notifier

Multi composes several Notifiers into one. Failures in any single notifier do not block the others; each is invoked synchronously in slice order (transports are responsible for their own concurrency).

func (Multi) Notify

func (m Multi) Notify(ctx context.Context, sig domain.Signal)

Notify fans the signal out to every configured notifier.

type NotifyingSignalRepository

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

NotifyingSignalRepository wraps a ports.SignalRepository and calls the configured Notifier *after* a successful Save. Failures from the inner repository propagate normally; the notifier is only invoked on the persistence happy path. A nil notifier disables notification (the wrapper still delegates Save/List/Get/Count, so it can be installed unconditionally and the configuration decides whether anything actually fires).

func WrapSignals

func WrapSignals(inner ports.SignalRepository, notifier ports.Notifier) *NotifyingSignalRepository

WrapSignals returns a SignalRepository that calls notifier on each successful Save. Pass a nil notifier to disable push transparently.

func (*NotifyingSignalRepository) Count

Count delegates to the inner repository.

func (*NotifyingSignalRepository) Get

Get delegates to the inner repository.

func (*NotifyingSignalRepository) List

List delegates to the inner repository.

func (*NotifyingSignalRepository) Save

Save persists the signal through the inner repository, then — only if persistence succeeded and a notifier is configured — pushes the signal to consumers. Notifier panics are recovered so a buggy transport cannot crash the pipeline.

type Outbox added in v0.4.0

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

Outbox is a Notifier wrapper that retries failed deliveries on a background sweeper, providing at-least-once semantics within a process lifetime. Pending deliveries do NOT survive process restart — operators wanting durable at-least-once delivery wire their own persistent Notifier.

Retry policy: exponential backoff starting at MinBackoff, doubling up to MaxBackoff, with up to MaxAttempts before the delivery is dropped and metrics increment a "give-up" counter.

func NewOutbox added in v0.4.0

func NewOutbox(inner AckingNotifier, cfg OutboxConfig) (*Outbox, error)

NewOutbox constructs an Outbox. Callers must invoke Start to activate the sweeper goroutine; Stop drains in-flight retries.

func (*Outbox) Notify added in v0.4.0

func (o *Outbox) Notify(ctx context.Context, sig domain.Signal)

Notify is the ports.Notifier entry point: fire-and-forget. The underlying delivery is enqueued in the outbox; immediate delivery is attempted once, and failed deliveries are retried by the sweeper.

func (*Outbox) PendingCount added in v0.4.0

func (o *Outbox) PendingCount() int

PendingCount returns the number of deliveries waiting to retry. Useful for tests and metrics surface.

func (*Outbox) Start added in v0.4.0

func (o *Outbox) Start(ctx context.Context)

Start runs the sweeper until ctx is done or [Stop] is called.

func (*Outbox) Stop added in v0.4.0

func (o *Outbox) Stop()

Stop signals the sweeper to drain. Returns when the goroutine has exited.

type OutboxConfig added in v0.4.0

type OutboxConfig struct {
	MaxAttempts     int
	MinBackoff      time.Duration
	MaxBackoff      time.Duration
	PersistencePath string
}

OutboxConfig tunes the retrier. All fields default to sensible values when zero:

MaxAttempts default 5
MinBackoff  default 1s
MaxBackoff  default 30s

PersistencePath, when set, makes the outbox durable across process restarts: pending deliveries are snapshotted to the path after each enqueue/flush, and re-loaded at Start. The file format is JSON and the parent directory is created with 0o700 permissions.

type SSE

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

SSE is an in-memory broadcaster for Server-Sent Events. It implements ports.Notifier (push side) and exposes Subscribe / Unsubscribe so an HTTP handler can register browser/runtime clients for /v1/signals/stream. Each subscriber owns a buffered channel; a signal is delivered with a non-blocking send, so a slow client is silently dropped rather than back-pressuring the entire bus. SSE is therefore a best-effort live feed; consumers needing replay must combine the stream with a /v1/signals query keyed on the last-seen DetectedAt.

func NewSSE

func NewSSE(bufferSize int) *SSE

NewSSE returns a broadcaster with the given per-client buffer capacity. A buffer of 8-32 is typically right: enough to absorb a burst, small enough that genuinely stuck clients are dropped fast. A bufferSize of 0 forces every send to block until the receiver reads — so we floor at 1 silently to keep the broadcaster healthy.

func (*SSE) Notify

func (s *SSE) Notify(ctx context.Context, sig domain.Signal)

Notify fans the signal out to every matching subscriber via a non-blocking send. Slow consumers are dropped silently. Honours ctx cancellation to skip iteration when the broadcaster is being shut down.

func (*SSE) Subscribe

func (s *SSE) Subscribe(scopes []uuid.UUID, pattern string) (uuid.UUID, <-chan domain.Signal)

Subscribe registers a new client filtered by an allowlist of scope ids (nil = any scope, single-element slice = legacy per-scope stream) and pattern (empty = any pattern). It returns:

  • id used to unregister
  • read-only channel of domain.Signal matching the filter

The returned channel is closed by Unsubscribe; receivers should detect the close and exit their read loop. The signature is deliberately bare types so internal/api can declare a matching interface without importing this package — that breaks what would otherwise be a cycle (api -> notify -> api via the wire shape).

func (*SSE) SubscriberCount

func (s *SSE) SubscriberCount() int

SubscriberCount returns the number of currently-attached clients. Useful for /metrics and tests.

func (*SSE) Unsubscribe

func (s *SSE) Unsubscribe(id uuid.UUID)

Unsubscribe removes the client and closes its channel. Safe to call with an unknown id (no-op).

type Webhook

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

Webhook posts each newly-persisted signal as JSON to a configured set of URLs. It is a transport: it does not interpret the signal, does not decide whether to deliver, and never blocks the persistence path. Delivery is at-most-once per URL with a small best-effort retry on 5xx responses; consumers de-duplicate by Signal.ID and treat headers as authoritative.

Headers per request:

X-Chronos-Signature: sha256=<hex hmac-sha256 of body>  // when Secret is set
X-Chronos-Delivery:  <uuid v4>                          // unique per send attempt
X-Chronos-Event:     signal.detected

The signed payload is the same JSON shape as /v1/signals returns (api.SignalDTO), so a consumer can treat webhook bodies and pulled list responses identically.

func NewWebhook

func NewWebhook(cfg WebhookConfig, metrics *observability.Metrics, logger *slog.Logger) *Webhook

NewWebhook constructs a Webhook from configuration. The returned notifier is safe for concurrent use; pass it through ports.Notifier.

func (*Webhook) Close

func (w *Webhook) Close() error

Close blocks until every in-flight delivery finishes (or its per-request timeout expires). Useful from cmd/serve's graceful shutdown so we do not abandon pending pushes.

func (*Webhook) Notify

func (w *Webhook) Notify(ctx context.Context, sig domain.Signal)

Notify fans the signal out to every configured URL in parallel. Failures are logged and recorded as metrics but never propagate. Returns immediately; sends complete in background goroutines.

type WebhookConfig

type WebhookConfig struct {
	URLs    []string
	Secret  string
	Timeout time.Duration // default 5s when zero
	Retries int           // default 1 when negative
}

WebhookConfig describes a webhook fan-out target set. URLs is the only required field; an empty slice disables the transport. Secret is optional — when empty, no X-Chronos-Signature header is sent and receivers cannot authenticate the payload (acceptable for trusted network paths but not over the public internet).

Jump to

Keyboard shortcuts

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