Tamale.Space (tamale v0.1.0)

Copy Markdown

A versioned identity space: an ordered set of element ids, an append-only op log, and nothing else.

  • ids are stable and never reused — a deleted id stays dead, so a historical anchor can never resurrect onto a new element
  • every write is an Tamale.Op batch; a batch is atomic and bumps version by one, appending one log entry
  • the Space holds no domain data — element payloads (notes, phonemes, frames) live in Caller-side tables keyed by id

The log doubles as the tombstone record: anchors carry at_version and are transported along log[at_version..head] (Tamale.Transport), so truncate/2 below the oldest live anchor version replaces reference-counted GC. Single-writer assumption: one linear log, no branching. (Concurrent/offline editing would reintroduce tombstones — as a deliberate kernel extension, not a heuristic.)

Note: log is a plain list appended at the end (O(n) per batch). Fine at scaffold scale; a production Space can swap in a queue without changing semantics.

Summary

Types

One log entry: the version a batch produced, and its ops.

t()

Functions

Applies a batch atomically: each op validates against the running state; all land or none. On success bumps version and appends one log entry.

Applies a single op. Sugar for apply_batch(space, [op]).

Log entries with version > from_version, oldest first.

Whether id is currently active.

Creates a space. Genesis ids are active at version 0.

Raising variant of new/1, for tests and known-good genesis lists.

Drops log entries at or below oldest_live_version — the log-age equivalent of GC. Anchors older than that horizon can no longer be transported (log_from/2 reports :log_truncated).

Types

entry()

@type entry() :: {Tamale.version(), [Tamale.Op.t()]}

One log entry: the version a batch produced, and its ops.

t()

@type t() :: %Tamale.Space{
  base_version: Tamale.version(),
  ids: [Tamale.id()],
  log: [entry()],
  seen: MapSet.t(Tamale.id()),
  version: Tamale.version()
}

Functions

apply_batch(space, ops)

@spec apply_batch(t(), [Tamale.Op.t()]) :: {:ok, t()} | {:error, term()}

Applies a batch atomically: each op validates against the running state; all land or none. On success bumps version and appends one log entry.

apply_op(space, op)

@spec apply_op(t(), Tamale.Op.t()) :: {:ok, t()} | {:error, term()}

Applies a single op. Sugar for apply_batch(space, [op]).

log_from(space, from_version)

@spec log_from(t(), Tamale.version()) :: {:ok, [entry()]} | {:error, term()}

Log entries with version > from_version, oldest first.

Errors are explicit: {:future_version, v} if from_version is ahead of head, :log_truncated if the needed history was dropped by truncate/2.

member?(space, id)

@spec member?(t(), Tamale.id()) :: boolean()

Whether id is currently active.

new(ids \\ [])

@spec new([Tamale.id()]) :: {:ok, t()} | {:error, :duplicate_ids}

Creates a space. Genesis ids are active at version 0.

Returns {:error, :duplicate_ids} when genesis ids repeat — a Caller bug, reported rather than raised so Domain layers keep their no-raise rule.

new!(ids \\ [])

@spec new!([Tamale.id()]) :: t()

Raising variant of new/1, for tests and known-good genesis lists.

truncate(space, oldest_live_version)

@spec truncate(t(), Tamale.version()) :: t()

Drops log entries at or below oldest_live_version — the log-age equivalent of GC. Anchors older than that horizon can no longer be transported (log_from/2 reports :log_truncated).