dispatch

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package dispatch receives newline-delimited envelope JSON over HTTP, runs the full receive ladder per line, and answers with newline-delimited ack JSON. Send posts messages and collects the replies. See docs/history/dispatch.md for the contract.

The ladder runs, per line, in fixed order and fails fast: Decode, VerifySignature, Room.Accepts, resolve, handle, then NewAck, Confirm, and Encode build the reply. workflow.EmitMessageDelivered and workflow.EmitMessageAcked are best-effort diagnostics outside this ladder, called after their point in the sequence with their error return ignored; they never fail a line.

MessageDeliveredEvent fires after VerifySignature and before Room.Accepts. It means "signature verified," not "room-admitted": a delivered event can still precede an admission rejection.

Index

Constants

View Source
const DefaultMaxBodyBytes int64 = 1 << 20

DefaultMaxBodyBytes caps one NDJSON request body when Options.MaxBodyBytes is zero. It bounds the memory one request can commit before any line runs the receive ladder.

View Source
const DefaultReplayCapacity = 100000

DefaultReplayCapacity caps the entry count of the ledger New builds internally when Options.Ledger is nil. A caller-supplied Ledger is not bounded by this constant; the caller owns its Store's capacity. The cap makes replay protection a bounded window: an evicted key is processed again if it arrives later. It bounds the records that hold no live claim, not the records that do.

View Source
const DefaultReplayLease = 30 * time.Second

DefaultReplayLease bounds one line's replay claim when Options.ReplayLease is zero. This is not a crash-detection timeout: size it above Handler.Handle's expected p99 latency, or a slow handler's own replay can re-run work before its first claim completes. See the Scope section warning in docs/history/dispatch.md.

Variables

View Source
var (
	// ErrInvalidOptions reports an Options field that fails Validate:
	// a blank ID, a nil Room, a nil Resolve, a negative MaxBodyBytes,
	// or a negative or sub-one-second ReplayLease or ReplayCapacity.
	// The wrapped message names the field and the rule it broke.
	ErrInvalidOptions = errors.New("dispatch: invalid options")
	ErrBadMethod      = errors.New("dispatch: POST required")
	ErrBadRequest     = errors.New("dispatch: request body read failed")
	// ErrReplay reports a message the ledger already admitted: a
	// completed, failed, or blocked key, or a key still claimed by an
	// in-flight duplicate. Endpoint.Handler answers this with a
	// "dispatch: replay:" error line instead of running resolve or handle again.
	ErrReplay = errors.New("dispatch: replay: message already processed")
)

Sentinel errors for New and Endpoint.Handler; test with errors.Is.

Functions

This section is empty.

Types

type Endpoint

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

Endpoint receives NDJSON envelope messages and answers with NDJSON acks. Build one with New.

func New

func New(opts Options) (*Endpoint, error)

New validates opts, builds a Bus when opts.Bus is nil, and returns the wired Endpoint. Emit accepts an event name with no subscriber, so the endpoint needs no fixture subscriptions on the bus.

func (*Endpoint) Handler

func (e *Endpoint) Handler() http.Handler

Handler serves POST requests with NDJSON bodies. A non-POST method answers 405 with ErrBadMethod. An unreadable body answers 400 with ErrBadRequest, as does a body past Options.MaxBodyBytes. Each request line runs the receive ladder and contributes one reply line, an ack or a JSON error object; the stream stays open across a per-line failure.

type Handler

type Handler interface {
	Handle(ctx context.Context, m envelope.Message) (string, error)
}

Handler resolves one received message into a restatement. New's caller supplies a Resolve func that looks one up per message. Implementations receive an already-verified, already-admitted message.

type Options

type Options struct {
	// ID is this endpoint's identity; it becomes each ack's From.
	ID string
	// Room gates admission: only a signed message from a member,
	// addressed only to members, and naming this room, is admitted.
	Room *room.Room
	// Resolve looks up the Handler that owns an admitted message.
	Resolve func(ctx context.Context, m envelope.Message) (Handler, error)
	// Bus receives MessageDeliveredEvent and MessageAckedEvent. Built
	// when nil; no handler is subscribed. Callers add handlers through
	// Bus().Subscribe.
	Bus *events.Bus
	// MaxBodyBytes caps one request body. Zero resolves to
	// DefaultMaxBodyBytes; a negative value fails Validate. A body
	// past the cap answers 400 with ErrBadRequest.
	MaxBodyBytes int64
	// Ledger provides replay protection over the receive ladder. Built
	// as a bounded in-memory ledger, sharing Bus for its events, when
	// nil.
	Ledger *ledger.Ledger
	// ReplayLease bounds one line's replay claim. Zero resolves to
	// DefaultReplayLease. A negative value, or a value under one
	// second, fails Validate. Size this above Handler.Handle's
	// expected p99 latency; see the warning in the Scope section of
	// docs/history/dispatch.md.
	ReplayLease time.Duration
	// ReplayCapacity caps the entry count of the ledger New builds
	// internally when Ledger is nil. Zero resolves to
	// DefaultReplayCapacity. A negative value fails Validate. Ignored
	// when Ledger is set; the caller-supplied Ledger owns its own
	// Store's capacity.
	//
	// Replay protection is a bounded window, not a permanent
	// guarantee: a key evicted under this cap is processed again if it
	// arrives later, and the endpoint answers a fresh ack. The cap
	// bounds the records that hold no live claim. A record claimed by
	// an in-flight Handle call is never evicted, so a hard memory
	// bound needs a request-rate limit in front of the endpoint.
	ReplayCapacity int
}

Options configures New. ID, Room, and Resolve are required.

func (Options) Validate

func (o Options) Validate() error

Validate checks ID, Room, Resolve, MaxBodyBytes, ReplayLease, and ReplayCapacity, in that order, and returns the first violation wrapped in ErrInvalidOptions. A nonzero ReplayLease under one second fails Validate: this is a sanity floor against a unit-confusion bug, not a floor tied to any handler's real latency.

type SendResult

type SendResult struct {
	Ack envelope.Ack
	Err error // set when the server answered an error line
}

SendResult is one reply line's outcome, in request order.

func Send

func Send(ctx context.Context, url string, msgs []envelope.Message) ([]SendResult, error)

Send posts msgs as one NDJSON request to url and returns one SendResult per reply line, in the order the server answered. An error line from the server surfaces as that entry's Err; a decode failure on a reply line surfaces the same way.

Jump to

Keyboard shortcuts

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