eventfulranges

package module
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MPL-2.0 Imports: 17 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()

Use case: a shared calendar

The full program is in examples/calendar. A date is just a day number (float64), so a date range is a plain interval:

cal, _ := eventfulranges.OpenStore(ctx, memory.New(), strategy.AdditiveWins)
book, cancel := func(f, t string) { _, _ = cal.Add(ctx, days(f), days(t)) },
                func(f, t string) { _, _ = cal.Remove(ctx, days(f), days(t)) }

book("2026-07-01", "2026-07-10")   // Alice's vacation
book("2026-07-06", "2026-07-15")   // Bob's vacation (overlaps)
cancel("2026-07-08", "2026-07-10") // Alice cuts the trip short

cal.Contains(days("2026-07-01")) // true  — booked
cal.Contains(days("2026-07-08")) // false — cancelled
cal.Contains(days("2026-07-12")) // true  — Bob's still away
gantt
    title       Shared calendar — AdditiveWins
    dateFormat  YYYY-MM-DD
    axisFormat  %m-%d

    section Start (bookings)
    Alice books               :a, 2026-07-01, 10d
    Bob books                 :b, 2026-07-06, 10d

    section Operation
    Alice cancels             :crit, c, 2026-07-08, 3d

    section Result
    Busy (Alice, then Alice+Bob) :done,   r1, 2026-07-01, 7d
    Free                         :active, r2, 2026-07-08, 3d
    Busy (Bob)                   :done,   r3, 2026-07-11, 5d

AdditiveWins makes the busy set the union of all bookings minus all cancellations, so concurrent edits converge no matter the order.

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 is used only for bookkeeping — operation timestamps and log-version counters — never as a coordinate.

Demos

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

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

local
go run ./demo/local

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.

pubsub
go run ./demo/pubsub

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.

network
go run ./demo/network

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.

web
go run ./demo/web

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/....

web demo 2d

paint
go run ./demo/paint

demo/paint is an infinite, shared pixel whiteboard built on the library's n-dimensional range CRDT. Each stroke is one half-open add/remove box, so a filled rectangle of cells is a single operation. Browsers receive the operation log and materialize the view themselves — pure event sourcing — and concurrent strokes converge regardless of arrival order. The share link is the session URL, and the raw operation log is one click away as JSONL. Open http://localhost:8081/ui/.

./scripts/demo-paint.sh     # start the whiteboard (open the printed URL)
./scripts/build-paint.sh    # (re)build the embedded UI (npm install + esbuild)
./scripts/itest-paint.sh    # smoke test: Go tests + server serves the UI
./scripts/e2e-paint.sh      # vitest unit tests + Playwright end-to-end tests
  • tldraw — open-source infinite canvas, real-time collaboration
  • Excalidraw — infinite canvas, CRDT (Yjs) collaboration (source)
  • Miro — infinite canvas, real-time collaboration
  • FigJam — infinite canvas, real-time collaboration
  • InfiniPaint — collaborative canvas with no zoom limit
  • Endless Paper — single-user infinite canvas
  • Prezi — the zoomable-canvas presentation paradigm

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 BoxOption added in v0.0.3

type BoxOption = sengine.Option

BoxOption customizes a BoxSet.

func WithBoxCanonicalizer added in v0.0.3

func WithBoxCanonicalizer(c space.Canonicalizer) BoxOption

WithBoxCanonicalizer sets the final canonicalization applied to every materialized view.

func WithBoxClock added in v0.0.3

func WithBoxClock(c clock.Clock) BoxOption

WithBoxClock sets the clock used to stamp new box operations.

func WithBoxMetaMerge added in v0.0.3

func WithBoxMetaMerge(m meta.Merge) BoxOption

WithBoxMetaMerge sets the join used when boxes carrying metadata merge under AdditiveWins or GrowOnly. The default is the top-level key union in the meta package.

func WithBoxSnapshotEvery added in v0.0.3

func WithBoxSnapshotEvery(n int) BoxOption

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

type BoxSet added in v0.0.3

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

BoxSet is the high-level n-dimensional API over an n-D engine.

func OpenBoxStore added in v0.0.3

func OpenBoxStore(ctx context.Context, st sstore.Log, s sstrategy.Strategy, opts ...BoxOption) (*BoxSet, error)

OpenBoxStore opens a box set backed by the given event log.

func OpenBoxes added in v0.0.3

func OpenBoxes(ctx context.Context, dir string, s sstrategy.Strategy, opts ...BoxOption) (*BoxSet, error)

OpenBoxes opens a JSON Lines-backed box set stored under dir.

func (*BoxSet) Add added in v0.0.3

func (b *BoxSet) Add(ctx context.Context, min, max []float64) (sop.Op, error)

Add applies an addition over the half-open box [min, max) and returns the applied op.

func (*BoxSet) AddWithMeta added in v0.0.3

func (b *BoxSet) AddWithMeta(ctx context.Context, min, max []float64, m json.RawMessage) (sop.Op, error)

AddWithMeta applies an addition over the half-open box [min, max) carrying JSON-object metadata, and returns the applied op.

func (*BoxSet) Apply added in v0.0.3

func (b *BoxSet) Apply(ctx context.Context, o sop.Op) error

Apply applies a single operation.

func (*BoxSet) ApplyAll added in v0.0.3

func (b *BoxSet) ApplyAll(ctx context.Context, ops []sop.Op) error

ApplyAll applies a batch of operations, ignoring duplicates by ID.

func (*BoxSet) Boxes added in v0.0.3

func (b *BoxSet) Boxes() []space.Box

Boxes returns the current canonical box cover.

func (*BoxSet) Compact added in v0.0.3

func (b *BoxSet) Compact(ctx context.Context) error

Compact rewrites the store as a snapshot of the current view, collapsing the stream to its smallest form.

func (*BoxSet) Contains added in v0.0.3

func (b *BoxSet) Contains(p []float64) bool

Contains reports whether the point belongs to the materialized set.

func (*BoxSet) Crossed added in v0.0.3

func (b *BoxSet) Crossed(p space.Path) []space.Box

Crossed returns the materialized boxes the path crosses with positive length, in canonical cover order.

func (*BoxSet) Ops added in v0.0.3

func (b *BoxSet) Ops() []sop.Op

Ops returns the known operations for anti-entropy exchange.

func (*BoxSet) Overlaps added in v0.0.3

func (b *BoxSet) Overlaps(box space.Box) bool

Overlaps reports whether any materialized box shares a point with box.

func (*BoxSet) Remove added in v0.0.3

func (b *BoxSet) Remove(ctx context.Context, min, max []float64) (sop.Op, error)

Remove applies a removal over the half-open box [min, max) and returns the applied op.

func (*BoxSet) RemoveWithMeta added in v0.0.3

func (b *BoxSet) RemoveWithMeta(ctx context.Context, min, max []float64, m json.RawMessage) (sop.Op, error)

RemoveWithMeta applies a removal over the half-open box [min, max) carrying JSON-object metadata, and returns the applied op.

func (*BoxSet) Retract added in v0.0.3

func (b *BoxSet) Retract(ctx context.Context, refID string) (sop.Op, error)

Retract cancels the operation named refID, undoing that one edit without touching any other operation. Retraction is single-level: a Retract cannot itself be retracted.

func (*BoxSet) RetractWithID added in v0.0.4

func (b *BoxSet) RetractWithID(ctx context.Context, id, refID string) (sop.Op, error)

RetractWithID cancels the operation named refID exactly like Retract, but records the retraction itself under id when it is non-empty. A caller that assigns its own operation identifiers can therefore acknowledge the retraction by ID, just like any other operation.

func (*BoxSet) Snapshot added in v0.0.3

func (b *BoxSet) Snapshot(ctx context.Context) error

Snapshot persists the current materialized view.

func (*BoxSet) Traverse added in v0.0.3

func (b *BoxSet) Traverse(p space.Path) []space.PathSegment

Traverse partitions the path's parameter interval into covered and gap segments as it enters and leaves the materialized set.

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) Compact added in v0.0.3

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

Compact rewrites the store as a snapshot of the current view, collapsing the stream to its smallest form.

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 meta merges JSON-object metadata attached to ranges.
Package meta merges JSON-object metadata attached to ranges.
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.
engine
Package engine applies box operations to an append-only event log and materializes the converged n-dimensional view under a chosen conflict-resolution strategy.
Package engine applies box operations to an append-only event log and materializes the converged n-dimensional view under a chosen conflict-resolution strategy.
op
Package op defines the box operations that make up the n-dimensional CRDT event log.
Package op defines the box operations that make up the n-dimensional CRDT event log.
store
Package store defines the append-only event log that backs an n-dimensional engine.
Package store defines the append-only event log that backs an n-dimensional engine.
store/jsonl
Package jsonl persists the n-dimensional event log as a single JSON Lines stream: operation records are appended in order and materialized snapshots are embedded as records in the same file, so the stream can be compacted.
Package jsonl persists the n-dimensional event log as a single JSON Lines stream: operation records are appended in order and materialized snapshots are embedded as records in the same file, so the stream can be compacted.
store/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.
strategy
Package strategy materializes a set of box operations into a canonical cover of boxes under a chosen conflict-resolution policy.
Package strategy materializes a set of box operations into a canonical cover of boxes under a chosen conflict-resolution policy.
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 a single JSON Lines stream: operation records are appended in order and materialized snapshots are embedded as records in the same file, so the stream can be compacted.
Package jsonl persists the event log as a single JSON Lines stream: operation records are appended in order and materialized snapshots are embedded as records in the same file, so the stream can be compacted.
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