Documentation
¶
Overview ¶
Package timebox implements an event sourcing toolkit with pluggable persistence backends such as memory, Redis/Valkey, PostgreSQL, and Raft. It couples an append-only event log, snapshots, optimistic concurrency, and append-time indexing into a library that can be embedded into services
Typical usage looks like:
- Open backend persistence and create a Store from it
- Define Appliers that fold events into your aggregate state
- Optionally define an Indexer to project current status or label indexes
- Use an Executor to run Commands that raise events on an Aggregator
- Save snapshots explicitly or let the Executor refresh them while loading
- Query the Store directly for events and aggregate state
The examples/ directory contains a runnable order workflow that exercises the API in a small domain
Index ¶
- Constants
- Variables
- func Configure[T With[T]](defaults T, others ...T) T
- func GetEventValue[T any](e *Event) (T, error)
- func MakeDecodeAll[Value, Data any](codec Codec[Value, Data]) func([]Data) ([]Value, error)
- func MakeEncodeAll[Value, Data any](codec Codec[Value, Data]) func([]Value) ([]Data, error)
- func Raise[T, V any](ag *Aggregator[T], typ EventType, value V) error
- type AggregateID
- type Aggregator
- type AlwaysReady
- type AppendRequest
- type Applier
- type Appliers
- type ArchiveHandler
- type ArchiveRecord
- type Archiver
- type Backend
- type Codec
- type Command
- type Config
- type Event
- type EventCodec
- type EventType
- type EventsResult
- type Executor
- type Flusher
- type Handler
- type ID
- type Index
- type Indexer
- type LoadEventsRequest
- type LoadSnapshotRequest
- type Persistence
- type Queries
- type SnapshotRecord
- type SnapshotRequest
- type SnapshotResult
- type StatusEntry
- type Store
- func (s *Store) AppendEvents(id AggregateID, atSeq int64, evs []*Event) error
- func (s *Store) Archive(id AggregateID) error
- func (s *Store) Close() error
- func (s *Store) Config() *Config
- func (s *Store) ConsumeArchive(ctx context.Context, handler ArchiveHandler) error
- func (s *Store) GetEvents(id AggregateID, fromSeq int64) ([]*Event, error)
- func (s *Store) GetSnapshot(id AggregateID, target any) (*SnapshotResult, error)
- func (s *Store) PutSnapshot(id AggregateID, value any, sequence int64) error
- func (s *Store) Ready() <-chan struct{}
- func (s *Store) WaitReady(ctx context.Context) error
- type SuccessAction
- type VersionConflictError
- type With
Constants ¶
const ( // DefaultTrimEvents determines whether snapshots trim stored events DefaultTrimEvents = false // DefaultSnapshotRatio is the default EventsSize/SnapshotSize threshold DefaultSnapshotRatio = 1.0 // DefaultMaxRetries is the default optimistic concurrency retry count DefaultMaxRetries = 16 // DefaultCacheSize controls the projection LRU size DefaultCacheSize = 128 )
Variables ¶
var ( // JSONEvent encodes complete events as JSON JSONEvent EventCodec[[]byte] = jsonEventCodec{} // BinEvent encodes complete events using the internal binary format BinEvent interface { EventCodec[[]byte] AppendAll(buf []byte, evs []*Event) ([]byte, error) ReadAll(data []byte) ([]*Event, []byte, error) } = binEventCodec{} // EncodeJSONEvents encodes complete events as JSON bytes EncodeJSONEvents = MakeEncodeAll(JSONEvent) // DecodeJSONEvents decodes complete events from JSON bytes DecodeJSONEvents = MakeDecodeAll(JSONEvent) // EncodeBinEvents encodes complete events to the internal binary format EncodeBinEvents = MakeEncodeAll(BinEvent) // DecodeBinEvents decodes complete events from the internal binary format DecodeBinEvents = MakeDecodeAll(BinEvent) )
var ( // ErrInvalidMaxRetries indicates MaxRetries is below the allowed range ErrInvalidMaxRetries = errors.New("max retries must be > 0") // ErrInvalidCacheSize indicates CacheSize is below the allowed range ErrInvalidCacheSize = errors.New("cache size must be > 0") // ErrInvalidSnapshotRatio indicates SnapshotRatio is below allowed range ErrInvalidSnapshotRatio = errors.New("snapshot ratio must be >= 0") )
var ( // ErrUnexpectedResult indicates data returned in an unexpected shape ErrUnexpectedResult = errors.New("unexpected result") // ErrArchivingDisabled indicates archiving is not enabled ErrArchivingDisabled = errors.New("archiving not enabled for this store") // ErrArchiveRecordMalformed indicates an archive record was malformed ErrArchiveRecordMalformed = errors.New("archive record malformed") // ErrArchiveHandlerMissing indicates a consume call is missing a handler ErrArchiveHandlerMissing = errors.New("archive handler is required") )
var ( // ErrMaxRetriesExceeded indicates optimistic concurrency retries were // exhausted while attempting to persist events ErrMaxRetriesExceeded = errors.New("max retries exceeded") )
Functions ¶
func Configure ¶
func Configure[T With[T]](defaults T, others ...T) T
Configure overlays each supplied value on top of defaults in order
func GetEventValue ¶
GetEventValue unmarshals the event data into the requested type. It reuses a cached value when the requested type matches, and otherwise unmarshals without replacing the cached type. This is safe for concurrent access
func MakeDecodeAll ¶
MakeDecodeAll returns a batch decoder for the provided codec
func MakeEncodeAll ¶
MakeEncodeAll returns a batch encoder for the provided codec
Types ¶
type AggregateID ¶
type AggregateID []ID
AggregateID identifies an aggregate as a set of parts ("order", "123")
func NewAggregateID ¶
func NewAggregateID(parts ...ID) AggregateID
NewAggregateID builds an AggregateID from its parts
func (AggregateID) Equal ¶
func (id AggregateID) Equal(other AggregateID) bool
Equal compares two AggregateIDs for equality
type Aggregator ¶
type Aggregator[T any] struct { // contains filtered or unexported fields }
Aggregator maintains aggregate state for a command and tracks events raised through Raise. It is not safe for concurrent use
func (*Aggregator[_]) ID ¶
func (a *Aggregator[_]) ID() AggregateID
ID returns the aggregate's identifier components
func (*Aggregator[_]) NextSequence ¶
func (a *Aggregator[_]) NextSequence() int64
NextSequence returns the next sequence number that will be assigned to a new event
func (*Aggregator[T]) OnSuccess ¶
func (a *Aggregator[T]) OnSuccess(fn SuccessAction[T])
OnSuccess registers an action to run after Executor.Exec persists the raised events successfully
func (*Aggregator[T]) Value ¶
func (a *Aggregator[T]) Value() T
Value returns the aggregate's current state
type AlwaysReady ¶
type AlwaysReady struct{}
AlwaysReady can be embedded in a Persistence to provide a Ready() implementation for backends that are immediately available
func (AlwaysReady) Ready ¶
func (AlwaysReady) Ready() <-chan struct{}
Ready returns a pre-closed channel, indicating immediate readiness
type AppendRequest ¶
type AppendRequest struct {
StatusAt time.Time
*Store
Status *string
Labels map[string]string
ID AggregateID
Events []*Event
ExpectedSequence int64
}
AppendRequest contains primitive inputs required for an atomic append
type Applier ¶
Applier applies an event to an aggregate state, returning the new state
func MakeApplier ¶
MakeApplier wraps a strongly typed applier that receives the event payload value and returns an Applier that works with Event
type ArchiveHandler ¶
type ArchiveHandler func(context.Context, *ArchiveRecord) error
ArchiveHandler handles a single archive record
type ArchiveRecord ¶
type ArchiveRecord struct {
StreamID string
AggregateID AggregateID
SnapshotData json.RawMessage
Events []*Event
SnapshotSequence int64
}
ArchiveRecord stores stream metadata and aggregate artifacts
type Archiver ¶
type Archiver interface {
// Archive moves an aggregate's persisted artifacts into archive storage
Archive(id AggregateID) error
// ConsumeArchive blocks until one archive record is available or ctx
// is done
ConsumeArchive(ctx context.Context, handler ArchiveHandler) error
}
Archiver provides optional archive lifecycle support for Store
type Backend ¶
type Backend interface {
Persistence
Queries
}
Backend is used by Store to provide Persistence and Queries
type Codec ¶
type Codec[Value, Data any] interface { // Encode encodes a value to the codec data format Encode(Value) (Data, error) // Decode decodes codec data to a value Decode(Data) (Value, error) // Append appends an encoded value to a buffer Append(Data, Value) (Data, error) // Read reads a value from a buffer and returns the remainder Read(Data) (Value, Data, error) }
Codec encodes and decodes values
type Command ¶
type Command[T any] func(T, *Aggregator[T]) error
Command is user code that inspects state and raises events on an Aggregator. Returning an error aborts the operation
type Config ¶
type Config struct {
Indexer Indexer
SnapshotRatio float64
MaxRetries int
CacheSize int
TrimEvents bool
}
Config configures Store behavior
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a Config populated with sensible defaults
type Event ¶
type Event struct {
Timestamp time.Time `json:"timestamp"`
Type EventType `json:"type"`
AggregateID AggregateID `json:"aggregate_id"`
Data json.RawMessage `json:"data"`
Sequence int64 `json:"sequence"`
Raised bool `json:"-"`
// contains filtered or unexported fields
}
Event represents a single immutable event in the log, including its sequence, timestamp, and serialized payload
type EventCodec ¶
EventCodec encodes and decodes complete events
type EventsResult ¶
EventsResult contains raw persisted events and the sequence to assign to the first event in the slice
type Executor ¶
type Executor[T any] struct { // contains filtered or unexported fields }
Executor orchestrates loading aggregate state, executing commands, and persisting resulting events with optimistic retries
func NewExecutor ¶
func NewExecutor[T any]( store *Store, cons constructor[T], apps Appliers[T], onSuccess ...SuccessAction[T], ) *Executor[T]
NewExecutor constructs an Executor bound to a Store with the given appliers and state constructor
func (*Executor[T]) AppliesEvent ¶
AppliesEvent reports whether the executor has an applier for the event type
func (*Executor[T]) Exec ¶
Exec loads the aggregate state, executes the command, and persists raised events. It retries on version conflicts up to MaxRetries
func (*Executor[T]) SaveSnapshot ¶
SaveSnapshot forces an immediate snapshot save for the given Aggregate
type Handler ¶
Handler processes a single Event
func MakeDispatcher ¶
MakeDispatcher routes events to handlers keyed by EventType, ignoring unmatched event types
type Index ¶
type Index struct {
// Status represents the resultant aggregate status. nil means no
// status change, and "" clears any prior status
Status *string `json:"status,omitempty"`
// Labels updates current label values for the aggregate. nil means no
// label changes, and empty values remove the label
Labels map[string]string `json:"labels,omitempty"`
}
Index stores optional projection metadata derived from an event
type LoadEventsRequest ¶
LoadEventsRequest contains primitive inputs required for an event load
type LoadSnapshotRequest ¶
type LoadSnapshotRequest struct {
*Store
ID AggregateID
}
LoadSnapshotRequest contains primitive inputs required for a snapshot load
type Persistence ¶
type Persistence interface {
io.Closer
// NewStore creates a Store using this Persistence instance
NewStore(Config) (*Store, error)
// Ready reports when the persistence backend can serve requests
Ready() <-chan struct{}
// Append atomically appends events if the expected sequence still
// matches. It returns a VersionConflictError when the sequence check
// fails
Append(AppendRequest) error
// LoadEvents loads raw persisted events starting at fromSeq
LoadEvents(LoadEventsRequest) (*EventsResult, error)
// LoadSnapshot loads the raw snapshot and any trailing raw events
LoadSnapshot(LoadSnapshotRequest) (*SnapshotRecord, error)
// SaveSnapshot stores raw snapshot data using the supplied semantics
SaveSnapshot(SnapshotRequest) error
}
Persistence provides the low-level primitives Store uses to implement Store semantics
type Queries ¶
type Queries interface {
// ListAggregates lists aggregate IDs that match the provided prefix
ListAggregates(id AggregateID) ([]AggregateID, error)
// GetAggregateStatus loads the current indexed status for an aggregate
GetAggregateStatus(id AggregateID) (string, error)
// ListAggregatesByStatus lists aggregates currently indexed by status
ListAggregatesByStatus(status string) ([]StatusEntry, error)
// ListAggregatesByLabel lists aggregates currently indexed by
// label/value
ListAggregatesByLabel(label, value string) ([]AggregateID, error)
// ListLabelValues lists the distinct indexed values for a label
ListLabelValues(label string) ([]string, error)
}
Queries provides aggregate and index query operations
type SnapshotRecord ¶
type SnapshotRecord struct {
Data json.RawMessage
Events []*Event
Sequence int64
}
SnapshotRecord contains raw snapshot data and any raw trailing events
type SnapshotRequest ¶
SnapshotRequest contains primitive inputs required for a snapshot save
type SnapshotResult ¶
type SnapshotResult struct {
AdditionalEvents []*Event
NextSequence int64
SnapshotSize int
EventsSize int
}
SnapshotResult holds the loaded snapshot, the sequence at which it was taken, and any events that need to be applied after it
type StatusEntry ¶
StatusEntry holds an aggregate ID and the time it entered a status
type Store ¶
type Store struct {
Queries
// contains filtered or unexported fields
}
Store persists, queries, and snapshots aggregate events
func (*Store) AppendEvents ¶
AppendEvents atomically appends events for an aggregate if the expected sequence matches the current log sequence
func (*Store) ConsumeArchive ¶
func (s *Store) ConsumeArchive( ctx context.Context, handler ArchiveHandler, ) error
ConsumeArchive reads one archive record and invokes handler
func (*Store) GetSnapshot ¶
func (s *Store) GetSnapshot( id AggregateID, target any, ) (*SnapshotResult, error)
GetSnapshot loads the latest snapshot into target and returns any events stored after the snapshot sequence
func (*Store) PutSnapshot ¶
PutSnapshot saves a snapshot value and sequence if the provided sequence is newer than any stored snapshot
type SuccessAction ¶
SuccessAction receives the Aggregator's final value after Executor.Exec succeeds, as well as the Events persisted by that execution
type VersionConflictError ¶
VersionConflictError is returned when AppendEvents encounters a sequence mismatch. NewEvents contains the conflicting events
func (*VersionConflictError) Error ¶
func (e *VersionConflictError) Error() string
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
Package raft implements Timebox persistence semantics using Raft for the replicated commit path, a durable write-ahead log, and a local materialized read store
|
Package raft implements Timebox persistence semantics using Raft for the replicated commit path, a durable write-ahead log, and a local materialized read store |
