Grove.Tree.DocumentStorage (Grove v0.1.1)

View Source

High-level document persistence using snapshots and event logs.

Implements the Eg-walker persistence pattern:

  • Snapshots capture full tree state at critical versions
  • Event logs store operations since the last snapshot
  • Loading replays events on top of snapshot for fast startup

Storage Keys

  • {:snapshot, doc_id, clock} - Tree snapshots at critical versions
  • {:events, doc_id, batch_id} - Event batches since last snapshot
  • {:meta, doc_id} - Document metadata (latest snapshot clock, etc.)

Usage

# Initialize storage
{:ok, storage} = DocumentStorage.new(storage_module, storage_ref, "doc_123")

# Save tree (auto-creates snapshots at critical versions)
{:ok, storage} = DocumentStorage.save(storage, tree)

# Load tree (snapshot + event replay)
{:ok, tree, storage} = DocumentStorage.load(storage, "replica_1")

# Force snapshot and truncate old events
{:ok, storage} = DocumentStorage.compact(storage)

Performance

With 100K events and periodic snapshots:

  • Without snapshot: ~10s to replay all events
  • With snapshot: <100ms to load snapshot + replay recent events

Summary

Functions

Forces a snapshot and removes old event batches.

Returns the number of events since the last snapshot.

Loads a tree from storage using snapshot + event replay.

Creates a new DocumentStorage for the given document.

Saves the tree state, potentially creating a snapshot.

Returns whether a snapshot should be created.

Types

t()

@type t() :: %Grove.Tree.DocumentStorage{
  document_id: String.t(),
  last_snapshot_clock: non_neg_integer() | nil,
  pending_events: [Grove.Tree.Event.t()],
  storage_module: module(),
  storage_ref: term()
}

Functions

compact(storage, tree)

@spec compact(t(), Grove.Tree.t()) :: {:ok, t()} | {:error, term()}

Forces a snapshot and removes old event batches.

This compacts storage by:

  1. Creating a snapshot at current state (even if not critical)
  2. Removing event batches before the snapshot
  3. Keeping only the new snapshot

Examples

{:ok, storage} = DocumentStorage.compact(storage, tree)

events_since_snapshot_count(storage, tree)

@spec events_since_snapshot_count(t(), Grove.Tree.t()) :: non_neg_integer()

Returns the number of events since the last snapshot.

load(storage, replica_id)

@spec load(t(), String.t()) :: {:ok, Grove.Tree.t() | nil, t()} | {:error, term()}

Loads a tree from storage using snapshot + event replay.

Examples

{:ok, tree, storage} = DocumentStorage.load(storage, "replica_1")
{:ok, nil, storage} = DocumentStorage.load(storage, "replica_1")  # No document

new(storage_module, storage_ref, document_id)

@spec new(module(), term(), String.t()) :: {:ok, t()} | {:error, term()}

Creates a new DocumentStorage for the given document.

Examples

{:ok, storage} = DocumentStorage.new(Grove.Storage.DETS, dets_ref, "doc_123")

save(storage, tree)

@spec save(t(), Grove.Tree.t()) :: {:ok, t()} | {:error, term()}

Saves the tree state, potentially creating a snapshot.

Creates a snapshot if:

  • Tree is at a critical version (frontier size == 1)
  • Events since last snapshot exceed threshold

Otherwise, saves new events to the event log.

Examples

{:ok, storage} = DocumentStorage.save(storage, tree)

should_snapshot?(tree, events_since)

@spec should_snapshot?(Grove.Tree.t(), [Grove.Tree.Event.t()]) :: boolean()

Returns whether a snapshot should be created.