eventfulranges

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MPL-2.0 Imports: 8 Imported by: 0

README

eventfulranges

CI codecov Go Reference

An event-sourced CRDT for real-valued ranges. Ranges can be added and removed from any number of replicas, in any order, over any transport — and every replica that has seen the same operations converges to the same set.

Storage and transport are both swappable. A replica can keep its operations in a JSON Lines file, in memory, or in any backend you write against the store.Log interface. Replicas converge by exchanging operations over whatever transport you already have — goroutine channels, an in-process pub/sub bus, plain HTTP, or an event database such as KurrentDB (behind the kurrent build tag).

Quick start

set, _ := eventfulranges.Open(ctx, "./example", strategy.LWW) // ./example/ranges.stream.jsonl
_, _ = set.Add(ctx, 1, 10)   // [1,10]
_, _ = set.Remove(ctx, 3, 5) // cut a hole
for _, iv := range set.Ranges() {
    fmt.Println(iv) // [1,3) (5,10]
}

Replicas converge by exchanging operations, not by reconciling state:

// replica A and B each mutated independently ...
_ = a.ApplyAll(ctx, b.Ops())
_ = b.ApplyAll(ctx, a.Ops())
// a.Ranges() == b.Ranges()

Storage & transport

A replica is a store.Log plus a strategy. Three backends ship in the repo, and you can plug in your own:

set, _ := eventfulranges.Open(ctx, "./example", strategy.LWW)         // JSON Lines stream (default)
set, _ := eventfulranges.OpenStore(ctx, memory.New(), strategy.LWW)   // in memory
set, _ := eventfulranges.OpenStore(ctx, myBackend, strategy.LWW)      // your own store.Log

Open keeps the append-only event stream as JSON Lines (./example/ranges.stream.jsonl) and caches the materialized view in a sidecar snapshot (./example/ranges.snapshot.json). The stream is the source of truth; the snapshot only fast-forwards a restart.

Transport is yours to choose, too: convergence is just shipping Ops() and calling ApplyAll. See Demos for channels, a pub/sub bus, and HTTP, and KurrentDB for the event-database backend.

Strategies

Strategy Semantics
LWW Highest (timestamp, id) wins at each point
FWW Lowest (timestamp, id) wins at each point
AdditiveWins Union of all additions minus union of all removals
GrowOnly Union of all additions, removals ignored

Packages

Package Purpose
interval 1-D open/closed intervals with canonical set algebra
op The append-only operation (add / remove)
clock Hybrid logical clock and Lamport clock
strategy Conflict resolution: materializes ops to a set
engine Concurrency-safe log + view, snapshotting
store The EventStore interface (append/read/snapshot)
store/memory, store/jsonl In-memory and file backends
space n-dimensional generalization (half-open boxes)

The public facade is the root package eventfulranges.

Coordinates

Range endpoints are float64. Integer literals convert exactly while they fit a float64's 53-bit mantissa (|n| <= 2^53); fractional values are stored and compared verbatim, so no rounding error accumulates. There is no arbitrary-precision (math/big) coordinate type: endpoints beyond 2^53 round to the nearest representable value. int64 appears only as the operation timestamp, never as a coordinate.

Demos

go run ./demo/hello    # simplest use, no concurrency
go run ./demo/local    # goroutine replicas converge over channels
go run ./demo/pubsub   # replicas converge over an in-process pub/sub bus
go run ./demo/network  # two HTTP peers converge
go run ./demo/web      # interactive 3D visualizer, shared live over WebSockets

demo/hello opens an in-memory set and prints what a single add/remove leaves behind — the smallest possible program.

demo/local opens three in-memory replicas, lets each mutate its own copy from a goroutine, then floods every replica's Ops() to every other replica until they agree. The transport is Go channels; there is no network.

demo/pubsub is the same idea through a bus: each replica subscribes to a topic on a github.com/cskr/pubsub/v2 bus, mutates locally, and publishes its operations. Every replica applies every broadcast it receives, so they converge without talking to each other directly.

demo/network runs two replicas, each behind its own HTTP server. There is no CRDT-specific protocol — a peer exports its log as GET /ops (JSON) and folds someone else's log in with POST /ops. Each peer mutates its own copy, then the two exchange logs and converge; ports come from -ports 18080,18081.

demo/web serves an n-dimensional range-set visualizer (1–4 dimensions, with a rotatable translucent-box 3D view and copy/pasteable CSV). Everyone connected to the same instance shares one view: each add/remove is folded with additive-wins semantics and broadcast over a WebSocket, so concurrent edits converge regardless of order. Open http://localhost:8080/ui/.

One command starts it, and the other scripts cover the rest:

./scripts/demo-web.sh     # start the web visualizer (open the printed URL)
./scripts/build-web.sh    # (re)build the embedded UI (npm install + esbuild)
./scripts/itest-web.sh    # smoke test: unit tests + server serves the UI
./scripts/e2e-web.sh      # Playwright end-to-end tests

Each demo has a smoke test; run them with go test ./demo/....

Quality

Everything is checked by scripts/quality-gate.sh and on CI:

  • gofumpt formatting
  • golangci-lint (low-complexity and duplicate-code gates)
  • unit tests with the race detector and a 100% coverage gate
  • property-based tests (pgregory.net/rapid) checked against a biogo interval-tree oracle, plus Jepsen-style concurrent scenarios
  • fuzz smoke tests (Go native fuzzing)
  • mutation testing (gremlins, ≥80% efficacy)

Run it locally:

./scripts/test.sh        # fast: unit tests + coverage report
./scripts/quality-gate.sh # full: format, lint, tests, property, fuzz, mutation
./scripts/update-dependencies.sh # bump every module to its latest deps

KurrentDB

./scripts/kurrent-up.sh          # docker compose up -d (needs Docker)
./scripts/itest-kurrent.sh       # integration tests (build tag kurrent)
./scripts/kurrent-down.sh

License

MPL-2.0

Documentation

Overview

Package eventfulranges is an event-sourced CRDT for real-valued ranges.

Operations (add or remove a range) are appended to an append-only log and merged under a conflict-resolution strategy such as last-write-wins or additive-wins. The default backend is a JSON Lines file, which needs no network; a KurrentDB backend is available behind the kurrent build tag.

Example
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/d-led/eventfulranges"
	"github.com/d-led/eventfulranges/strategy"
)

func main() {
	ctx := context.Background()
	dir, err := os.MkdirTemp("", "eventfulranges-example")
	if err != nil {
		panic(err)
	}
	defer func() { _ = os.RemoveAll(dir) }()

	rs, err := eventfulranges.Open(ctx, dir, strategy.LWW)
	if err != nil {
		panic(err)
	}

	_, _ = rs.Add(ctx, 1, 5)    // [1,5]
	_, _ = rs.Add(ctx, 3, 7)    // merges to [1,7]
	_, _ = rs.Remove(ctx, 2, 3) // cuts a hole

	for _, iv := range rs.Ranges() {
		fmt.Println(iv)
	}
}
Output:
[1,2)
(3,7]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option = engine.Option

Option customizes a RangeSet.

func WithClock

func WithClock(c clock.Clock) Option

WithClock sets the clock used to stamp new operations.

func WithSnapshotEvery

func WithSnapshotEvery(n int) Option

WithSnapshotEvery snapshots the view every n new operations; 0 disables automatic snapshots.

type RangeSet

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

RangeSet is the high-level API over an engine.

func Open

func Open(ctx context.Context, dir string, s strategy.Strategy, opts ...Option) (*RangeSet, error)

Open opens a JSON Lines-backed range set stored under dir.

func OpenStore

func OpenStore(ctx context.Context, st store.Log, s strategy.Strategy, opts ...Option) (*RangeSet, error)

OpenStore opens a range set backed by the given event log. Backends that also implement store.Snapshotter get snapshotting for free; stream-only backends simply skip it.

func (*RangeSet) Add

func (r *RangeSet) Add(ctx context.Context, start, end float64) (op.Op, error)

Add applies a closed addition [start, end] and returns the applied op.

func (*RangeSet) AddWithBounds

func (r *RangeSet) AddWithBounds(ctx context.Context, start, end float64, sb, eb interval.Bound) (op.Op, error)

AddWithBounds applies an addition with explicit boundary inclusivity.

func (*RangeSet) Apply

func (r *RangeSet) Apply(ctx context.Context, o op.Op) error

Apply applies a single operation.

func (*RangeSet) ApplyAll

func (r *RangeSet) ApplyAll(ctx context.Context, ops []op.Op) error

ApplyAll applies a batch of operations, ignoring duplicates by ID. It is the workhorse of anti-entropy: a replica can hand another replica's Ops() result straight back to ApplyAll.

func (*RangeSet) Contains

func (r *RangeSet) Contains(x float64) bool

Contains reports whether x belongs to the materialized set.

func (*RangeSet) Ops

func (r *RangeSet) Ops() []op.Op

Ops returns the known operations for anti-entropy exchange.

func (*RangeSet) Overlaps

func (r *RangeSet) Overlaps(iv interval.Interval) bool

Overlaps reports whether any materialized interval shares a point with iv.

func (*RangeSet) Ranges

func (r *RangeSet) Ranges() []interval.Interval

Ranges returns the current canonical interval view.

func (*RangeSet) Remove

func (r *RangeSet) Remove(ctx context.Context, start, end float64) (op.Op, error)

Remove applies a closed removal [start, end] and returns the applied op.

func (*RangeSet) RemoveWithBounds

func (r *RangeSet) RemoveWithBounds(ctx context.Context, start, end float64, sb, eb interval.Bound) (op.Op, error)

RemoveWithBounds applies a removal with explicit boundary inclusivity.

func (*RangeSet) Snapshot

func (r *RangeSet) Snapshot(ctx context.Context) error

Snapshot persists the current materialized view.

Directories

Path Synopsis
Package clock provides timestamps that order range operations across replicas.
Package clock provides timestamps that order range operations across replicas.
Package engine applies range operations to an append-only event log and materializes the converged view under a chosen conflict-resolution strategy.
Package engine applies range operations to an append-only event log and materializes the converged view under a chosen conflict-resolution strategy.
Package interval defines open and closed real-valued intervals together with canonical set operations over them.
Package interval defines open and closed real-valued intervals together with canonical set operations over them.
Package op defines the range operations that make up the CRDT event log.
Package op defines the range operations that make up the CRDT event log.
Package space generalizes the one-dimensional interval set to n dimensions.
Package space generalizes the one-dimensional interval set to n dimensions.
Package store defines the append-only event log that backs an engine.
Package store defines the append-only event log that backs an engine.
jsonl
Package jsonl persists the event log as JSON Lines with a sidecar snapshot file.
Package jsonl persists the event log as JSON Lines with a sidecar snapshot file.
kurrent
Package kurrent implements an EventStore backed by KurrentDB.
Package kurrent implements an EventStore backed by KurrentDB.
memory
Package memory provides an in-memory EventStore for tests and single-process use.
Package memory provides an in-memory EventStore for tests and single-process use.
Package strategy materializes a set of range operations into canonical non-overlapping intervals under a chosen conflict-resolution policy.
Package strategy materializes a set of range operations into canonical non-overlapping intervals under a chosen conflict-resolution policy.

Jump to

Keyboard shortcuts

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