Documentation
¶
Overview ¶
Package durable provides a single-writer WebSocket relay reference with a persistent operation log, bounded replay, and reconnect support.
It is intentionally separate from extensions: extensions is a bounded live relay, while durable owns a bbolt-backed transport log. It remains a reference for one active process and one persistent volume, not a clustered replication service. Applications must still supply TLS, authentication, authorization, concrete CRDT state/frontier checkpoints, durable outboxes, membership, and tombstone-GC policy.
Index ¶
- Constants
- Variables
- func DecodeChange(data []byte, maxMessageBytes, maxActorBytes int) (replica.Dot, []byte, error)
- func EncodeChange(change replica.Change) ([]byte, error)
- type AppendResult
- type Authenticate
- type Authorize
- type AuthorizeSubscription
- type ClientConfig
- type Config
- type Event
- type Group
- type GroupConfig
- type Handler
- type Log
- type MerkleBoundary
- type MerkleIndex
- type MerkleLeaf
- type MerkleLog
- type MerkleSnapshot
- type Peer
- type ReconnectClient
- type RevalidateSubscription
- type StateVectorLog
- type Store
- func (store *Store) Append(groupID string, change replica.Change) (AppendResult, error)
- func (store *Store) CatchUp(groupID string, vector replica.Frontier, maxEvents, maxBytes uint64, ...) ([]Event, uint64, error)
- func (store *Store) Close() error
- func (store *Store) Closed() bool
- func (store *Store) MerkleEnabled() bool
- func (store *Store) MerkleEvents(groupID string, identities []crdt.Tag, maxEvents, maxBytes uint64, ...) ([]Event, error)
- func (store *Store) MerkleSnapshot(groupID string, maxLeaves, maxBytes uint64, manifest replica.Manifest, ...) (MerkleSnapshot, error)
- func (store *Store) Replay(groupID string, after, maxEvents, maxBytes uint64, manifest replica.Manifest, ...) ([]Event, uint64, error)
- type StoreConfig
- type Validate
Constants ¶
const MerkleSubprotocol = "crdt-durable-v3"
MerkleSubprotocol identifies the optional HLC/Merkle anti-entropy protocol. It does not send a replica state vector. Each committed durable event has a relay-generated HLC identity, and a Merkle root commits to its canonical event contents. Roots only detect divergence; they do not authenticate a peer, acknowledge persistence, or authorize tombstone compaction.
const StateVectorSubprotocol = "crdt-durable-v2"
StateVectorSubprotocol identifies the optional state-vector catch-up protocol. A v2 peer proves only its own installed Dot prefixes; the vector is neither authentication, a receipt, nor permission to compact tombstones.
const Subprotocol = "crdt-durable-v1"
Subprotocol identifies the stable cursor-resume durable relay protocol. It is independent from CRDT frame versions and extensions.Subprotocol.
Variables ¶
var ( // ErrInvalidConfig reports a missing or unsafe durable relay configuration. ErrInvalidConfig = errors.New("crdt durable: invalid configuration") ErrUnauthorized = errors.New("crdt durable: unauthorized") // ErrConflictingDot reports a retry that reuses an existing Dot with a // different canonical payload. The existing binding remains authoritative. ErrConflictingDot = errors.New("crdt durable: conflicting dot") // ErrStoreFull reports that retaining another event would exceed the // configured operation-log budget. The server does not evict history. ErrStoreFull = errors.New("crdt durable: operation log limit reached") // the configured bounded replay window. Callers must bootstrap from a // validated application checkpoint instead of accepting a partial replay. ErrReplayUnavailable = errors.New("crdt durable: replay unavailable") // calculate a complete bounded suffix for a durable delivery frontier. // Callers must fall back to a valid cursor replay or bootstrap from a // validated application checkpoint; accepting a partial catch-up is unsafe. ErrStateVectorUnavailable = errors.New("crdt durable: state-vector catch-up unavailable") // produce a complete bounded HLC/Merkle inventory. The caller must // bootstrap from a validated application checkpoint rather than accepting a // partial repair. ErrAntiEntropyUnavailable = errors.New("crdt durable: HLC/Merkle anti-entropy unavailable") // ErrMerkleDiverged reports a local inventory that contains a leaf absent // from the authoritative relay view or the same HLC identity with a // different digest. Repair must bootstrap or be investigated; it must not // silently drop either history. ErrMerkleDiverged = errors.New("crdt durable: HLC/Merkle histories diverged") // ErrCorruptStore reports damaged or internally inconsistent durable data. // The relay fails closed rather than guessing which operation to omit. ErrCorruptStore = errors.New("crdt durable: corrupt store") // ErrClosed reports use of a closed client or store. ErrClosed = errors.New("crdt durable: closed") // ErrQueueFull reports a bounded peer or client queue that cannot accept // another message without unbounded memory growth. ErrQueueFull = errors.New("crdt durable: queue full") )
Functions ¶
func DecodeChange ¶
DecodeChange decodes a bounded durable-log envelope. Storage providers must construct a replica.Change with the expected manifest and policy before returning this data to a relay.
Types ¶
type AppendResult ¶
AppendResult records the outcome of one idempotent log append. A duplicate Dot is safe only when the store verified that its canonical payload is identical to the existing binding.
type Authenticate ¶
Authenticate authenticates a request before the WebSocket upgrade.
type Authorize ¶
Authorize binds a proposed CRDT change to the authenticated peer and exact manifest. At minimum it must prevent a peer from publishing another actor.
type AuthorizeSubscription ¶
AuthorizeSubscription controls replay/live-event access independently from write authorization.
type ClientConfig ¶
type ClientConfig struct {
Header http.Header
HTTPClient *http.Client
Policy crdt.ProtocolPolicy
MaxMessageBytes int
MaxActorBytes int
MaxQueuedChanges int
MaxStateVectorEntries int
MaxMerkleLeaves int
MaxMerkleBytes int
HandshakeTimeout time.Duration
WriteTimeout time.Duration
PingInterval time.Duration
PingTimeout time.Duration
MinReconnectBackoff time.Duration
MaxReconnectBackoff time.Duration
Cursor uint64
// StateVector returns the contiguous frontier from the same durable
// application checkpoint as the CRDT state. When set, the client requests
// the v2 bounded missing-Dot catch-up protocol on every reconnect.
StateVector func() replica.Frontier
// OnCatchUp persists the state/frontier after all v2 catch-up events and
// records highWater as the durable cursor in the same transaction. It is
// required with StateVector because a skipped log event is never proof that
// its payload was installed.
OnCatchUp func(highWater uint64) error
// MerkleRoot returns the root reconstructed from the same durable local
// event inventory as the concrete CRDT checkpoint. When all three Merkle
// callbacks are supplied, the client requests v3 without sending a state
// vector.
MerkleRoot func() [32]byte
// ReconcileMerkle compares the complete, bounded remote inventory against
// that durable local inventory and returns the sorted HLC identities absent
// locally. It must reject an unexpected local-only or differently-digested
// leaf instead of silently accepting a divergent history.
ReconcileMerkle func([]MerkleLeaf) ([]crdt.Tag, error)
// OnMerkleCatchUp atomically records the completed checkpoint boundary
// after all requested events have been installed and MerkleRoot equals the
// remote root. It is required for a v3 client.
OnMerkleCatchUp func(MerkleBoundary) error
OnEvent func(Event) error
}
ClientConfig configures a reconnecting durable WebSocket client. OnEvent must durably install the concrete CRDT state and delivery frontier before it returns nil. Its transaction must also record event.Sequence as the resume cursor and settle any matching application outbox row.
type Config ¶
type Config struct {
Store Log
Groups []*Group
Authenticate Authenticate
Authorize Authorize
AuthorizeSubscription AuthorizeSubscription
// RevalidateSubscription is optional. When provided, it runs before every
// heartbeat so the host can close revoked or expired long-lived sessions.
RevalidateSubscription RevalidateSubscription
OriginPatterns []string
MaxMessageBytes int
MaxActorBytes int
MaxQueuedEvents int
MaxQueuedBytes int
MaxReplayEvents int
MaxReplayBytes int
MaxStateVectorEntries int
MaxMerkleLeaves int
MaxMerkleBytes int
HandshakeTimeout time.Duration
WriteTimeout time.Duration
PingInterval time.Duration
PingTimeout time.Duration
// Telemetry receives bounded, payload-free operational events for
// handshake, replay, and append outcomes. A nil Reporter is the default
// and adds no reporting work to relay paths.
Telemetry *telemetry.Reporter
}
Config configures an authenticated durable WebSocket relay. Store and all authorization callbacks are required; the handler never starts a listener.
type Event ¶
type Event struct {
Sequence uint64
// HLC is the relay-persisted HLC identity used by MerkleSubprotocol. It is
// zero for v1/v2 stores and events. It is independent from tags contained
// inside an application CRDT delta.
HLC crdt.Tag
Change replica.Change
}
Event is one committed transport-log entry. Sequence is strictly increasing within its group and is the only valid durable replay cursor.
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
Group owns the manifest, validation boundary, and live subscribers for one durable operation log. It does not own concrete application CRDT state.
func NewGroup ¶
func NewGroup(config GroupConfig) (*Group, error)
NewGroup validates one immutable-by-convention manifest and requires a state-independent concrete CRDT validator.
type GroupConfig ¶
type GroupConfig struct {
Manifest replica.Manifest
Policy crdt.ProtocolPolicy
Validate Validate
}
GroupConfig defines one manifest-bound durable transport group.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler is safe to mount into an application-owned HTTP server. It exposes only GET /ws and requires Subprotocol on every accepted connection.
func NewHandler ¶
NewHandler validates a complete, bounded durable-relay configuration.
type Log ¶
type Log interface {
Append(groupID string, change replica.Change) (AppendResult, error)
Replay(groupID string, after, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, uint64, error)
Closed() bool
}
Log is the durable-relay storage contract. Implementations must make Append atomic: a new event, its group-local sequence, the Dot-to-canonical-payload binding, and capacity accounting either all become durable or none do.
Replay must return a contiguous complete suffix or ErrReplayUnavailable; returning a prefix silently would let a receiver advance its cursor past missing CRDT changes. Implementations validate stored bytes against the manifest and policy supplied by the relay before returning them.
The relay never closes a Log. Its owner controls connection lifetime so a shared PostgreSQL, MySQL, SQL Server, SQLite, or Redis client pool can serve more than one handler.
type MerkleBoundary ¶ added in v1.0.34
MerkleBoundary is the durable checkpoint boundary emitted after a v3 repair. The application persists it with its concrete CRDT state, HLC state, local Merkle inventory, and cursor before the client accepts live traffic.
type MerkleIndex ¶ added in v1.0.34
type MerkleIndex struct {
// contains filtered or unexported fields
}
MerkleIndex is a small in-process helper for an application's durable event inventory. It mirrors only HLC identities and canonical event digests; it never stores CRDT payloads. Applications must persist the equivalent index with their concrete CRDT checkpoint before reporting Root to a v3 client.
Put is idempotent for the same immutable event and rejects a same-HLC, different-digest conflict. Reconcile refuses local-only history instead of turning a Merkle mismatch into accidental data loss.
func NewMerkleIndex ¶ added in v1.0.34
func NewMerkleIndex() *MerkleIndex
NewMerkleIndex constructs an empty HLC/Merkle inventory.
func (*MerkleIndex) Put ¶ added in v1.0.34
func (index *MerkleIndex) Put(event Event) error
Put records one durably accepted v3 event. Call it only after the same application transaction has persisted the concrete CRDT state and event identity; the helper itself is not a persistence layer.
func (*MerkleIndex) Reconcile ¶ added in v1.0.34
func (index *MerkleIndex) Reconcile(remote []MerkleLeaf) ([]crdt.Tag, error)
Reconcile compares a complete remote inventory with this local inventory and returns the sorted relay HLC identities missing locally. A local-only or different-digest leaf is a fail-closed divergence, not a deletion request.
type MerkleLeaf ¶ added in v1.0.34
MerkleLeaf identifies one immutable event in a Merkle anti-entropy inventory. Digest is SHA-256 over the canonical HLC plus durable change envelope; HLC is the leaf key and is allocated atomically with that event. Neither field is an authorization credential.
type MerkleLog ¶ added in v1.0.34
type MerkleLog interface {
Log
MerkleEnabled() bool
MerkleSnapshot(groupID string, maxLeaves, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) (MerkleSnapshot, error)
MerkleEvents(groupID string, identities []crdt.Tag, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, error)
}
MerkleLog is an optional Log capability for bounded no-state-vector anti-entropy. A caller first compares a MerkleSnapshot root, then, only on divergence, compares its complete bounded leaf inventory and requests the HLC identities it lacks. Snapshot and Events must fail closed when either requested set cannot be complete within the supplied limits.
MerkleEnabled permits a storage implementation to keep v1/v2 support while requiring explicit HLC persistence before it advertises v3.
type MerkleSnapshot ¶ added in v1.0.34
type MerkleSnapshot struct {
Root [sha256.Size]byte
HighWater uint64
HLC crdt.Tag
Leaves []MerkleLeaf
}
MerkleSnapshot is one immutable relay-log view. HighWater and HLC form the replay-to-live boundary; Leaves is a complete bounded inventory matching Root. Empty logs have a zero HLC and the canonical empty Merkle root.
type Peer ¶
type Peer struct {
ID string
}
Peer is an authenticated application identity. ID must be stable and must never be copied from a client-controlled CRDT actor identifier.
type ReconnectClient ¶
type ReconnectClient struct {
// contains filtered or unexported fields
}
ReconnectClient reconnects with an application-provided durable cursor. Its in-memory queue is deliberately bounded and is not a replacement for an application outbox; persist an outgoing change before calling Publish.
func NewReconnectClient ¶
func NewReconnectClient(endpoint string, manifest replica.Manifest, config ClientConfig) (*ReconnectClient, error)
NewReconnectClient validates configuration without making a network call.
func (*ReconnectClient) Cursor ¶
func (client *ReconnectClient) Cursor() uint64
Cursor reports the highest event whose OnEvent callback succeeded during the current process. On restart, supply the cursor loaded from the same durable application transaction as the CRDT state/frontier.
func (*ReconnectClient) Err ¶
func (client *ReconnectClient) Err() error
Err returns the last transient session error observed by Run. A successful handshake clears it; callers still own logging and operational policy.
type RevalidateSubscription ¶
RevalidateSubscription is invoked periodically for an established subscription. Hosts use it to apply session expiry or revocation policy to long-lived connections; a failure closes only that connection.
type StateVectorLog ¶
type StateVectorLog interface {
Log
CatchUp(groupID string, vector replica.Frontier, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, uint64, error)
}
StateVectorLog is an optional Log capability for bounded recovery from a replica.Frontier. CatchUp must return every event whose Dot is not covered by vector, ordered by durable sequence, or return an error. It must never return a convenient partial result.
The base Log contract intentionally remains cursor-based so existing Redis, PostgreSQL, MySQL, SQL Server, SQLite, and host implementations continue to work with v1 clients.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a bbolt-backed, single-writer operation log. bbolt enforces an exclusive file lock; deployments must still schedule only one active relay process for a data file.
func OpenStore ¶
func OpenStore(path string, config StoreConfig) (*Store, error)
OpenStore opens or creates a durable operation log at path with mode 0600. The parent directory must already be owned and protected by the host.
func (*Store) Append ¶
Append transactionally binds a Dot to its canonical envelope and allocates the next group-local sequence for new data. The caller must validate the concrete CRDT delta before invoking Append.
func (*Store) CatchUp ¶
func (store *Store) CatchUp(groupID string, vector replica.Frontier, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, uint64, error)
CatchUp returns the complete bounded set of events not covered by vector. The vector represents only contiguous locally installed Dot prefixes. A vector ahead of retained log data, or a required suffix beyond the caller's explicit limits, fails closed rather than silently declaring a client synced.
func (*Store) Closed ¶
Closed reports whether Close has completed or is in progress. It allows a Handler to fail closed without taking ownership of the store's lifetime.
func (*Store) MerkleEnabled ¶ added in v1.0.34
MerkleEnabled reports whether this store was explicitly configured to persist a relay HLC with each event. A disabled store remains a valid v1/v2 Log but must not advertise the v3 anti-entropy protocol.
func (*Store) MerkleEvents ¶ added in v1.0.34
func (store *Store) MerkleEvents(groupID string, identities []crdt.Tag, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, error)
MerkleEvents returns the complete requested event set in canonical HLC order. A request for an unknown HLC identity fails closed: partial repair would let a client attest to a root it cannot actually reconstruct.
func (*Store) MerkleSnapshot ¶ added in v1.0.34
func (store *Store) MerkleSnapshot(groupID string, maxLeaves, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) (MerkleSnapshot, error)
MerkleSnapshot returns a complete bounded HLC/Merkle view of one durable group. It verifies every retained event before it contributes to the root; an absent HLC tag or index is unsafe because a receiver could otherwise accept a root that does not name all replayable events.
func (*Store) Replay ¶
func (store *Store) Replay(groupID string, after, maxEvents, maxBytes uint64, manifest replica.Manifest, policy crdt.ProtocolPolicy, maxMessageBytes, maxActorBytes int) ([]Event, uint64, error)
Replay atomically reads every event after after in sequence order. It fails rather than returning a prefix when the caller's explicit replay budget cannot cover the entire missed suffix.
type StoreConfig ¶
type StoreConfig struct {
MaxEvents uint64
MaxBytes uint64
OpenTimeout time.Duration
// HLCReplicaID enables the no-state-vector HLC/Merkle anti-entropy
// capability. The store persists this relay-local clock in the same bbolt
// transaction as every newly committed event. Empty preserves the legacy
// cursor/state-vector-only behavior.
HLCReplicaID string
}
StoreConfig bounds retained canonical event data per replication group. Both limits are required: durable replay must apply an explicit overload policy rather than retaining unbounded history.