element.version === currentVersion)) { versionNodes.unshift({ version: currentVersion, url: "#" }); } document.querySelector("#project-version").innerHTML = versionNodes.reduce( (acc, element) => { const status = currentVersion === element.version ? "selected disabled" : ""; return ` ${acc} `; }, `
dream_ets · v2.0.0
Dream Logo
Hex Package HexDocs MIT License Gleam

dream_ets

Type-safe ETS (Erlang Term Storage) for Gleam.

A standalone module providing a type-safe interface to Erlang’s ETS in-memory storage. Features a builder pattern for table configuration, type-safe operations, and comprehensive error handling. Built with the same quality standards as Dream, but completely independent—use it in any Gleam project.

Features

  • Type-safe - Keys and values are typed at compile time
  • Result-based errors - All operations return Result for explicit error handling
  • Builder pattern - Composable, fluent table configuration
  • Atomic operations - insert_new(), take() for race-free operations
  • Custom types - Store any type with custom encoders (JSON, tuples, etc.)
  • Pattern matching - Advanced ETS queries and match specifications
  • Table persistence - Save/load tables to disk
  • 100% tested - Comprehensive test coverage with verified examples
  • Zero dependencies - No Dream or framework requirements

Installation

gleam add dream_ets

Quick Start

Creating a Table

import dream_ets/config
import dream_ets/operations
import gleam/option
import gleam/result

pub fn create_string_table() -> Result(String, table.EtsError) {
  // Create a table using the builder pattern
  use cache <- result.try(
    config.new("user_cache")
    |> config.key_string()
    |> config.value_string()
    |> config.create(),
  )

  // Use it
  use _ <- result.try(operations.set(cache, "alice", "Alice"))
  use value <- result.try(operations.get(cache, "alice"))

  case value {
    option.Some(name) -> Ok(name)
    option.None -> Error(table.OperationFailed("Not found"))
  }
}

🧪 Tested source

Basic Operations

import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result

pub fn store_and_retrieve() -> Result(String, table.EtsError) {
  use cache <- result.try(helpers.new_string_table("cache"))

  // Store a value
  use _ <- result.try(operations.set(cache, "greeting", "Hello, World!"))

  // Retrieve it
  use value <- result.try(operations.get(cache, "greeting"))

  case value {
    option.Some(greeting) -> Ok(greeting)
    option.None -> Error(table.OperationFailed("Not found"))
  }
}

🧪 Tested source

Counter Tables

import dream_ets/helpers
import gleam/result

pub fn increment_page_views() -> Result(Int, table.EtsError) {
  use counter <- result.try(helpers.new_counter("page_views"))

  // Track multiple page views
  use _ <- result.try(helpers.increment(counter, "homepage"))
  use _ <- result.try(helpers.increment(counter, "homepage"))
  use count <- result.try(helpers.increment(counter, "homepage"))

  Ok(count)  // Returns 3
}

🧪 Tested source

Core Features

Custom Types with JSON

Store your own types using JSON encoding:

import dream_ets/config
import dream_ets/internal
import dream_ets/operations
import gleam/dynamic
import gleam/dynamic/decode
import gleam/json
import gleam/option
import gleam/result

pub type User {
  User(name: String, email: String)
}

fn encode_user(user: User) -> dynamic.Dynamic {
  json.object([
    #("name", json.string(user.name)),
    #("email", json.string(user.email)),
  ])
  |> json.to_string
  |> internal.to_dynamic
}

fn decode_user() -> decode.Decoder(User) {
  decode.string
  |> decode.then(fn(json_str) {
    case json.parse(json_str, user_from_json()) {
      Ok(user) -> decode.success(user)
      Error(_) -> decode.failure(User("", ""), "User")
    }
  })
}

fn user_from_json() -> decode.Decoder(User) {
  use name <- decode.field("name", decode.string)
  use email <- decode.field("email", decode.string)
  decode.success(User(name: name, email: email))
}

pub fn store_custom_type() -> Result(String, table.EtsError) {
  use users <- result.try(
    config.new("users")
    |> config.key_string()
    |> config.value(encode_user, decode_user())
    |> config.create(),
  )

  let user = User(name: "Alice", email: "alice@example.com")
  use _ <- result.try(operations.set(users, "alice", user))

  use retrieved <- result.try(operations.get(users, "alice"))

  case retrieved {
    option.Some(u) -> Ok(u.name <> " <" <> u.email <> ">")
    option.None -> Error(table.OperationFailed("User not found"))
  }
}

🧪 Tested source

Preventing Duplicates

Use insert_new() for atomic “check and insert” operations:

import dream_ets/helpers
import dream_ets/operations
import gleam/result

pub fn register_user() -> Result(Bool, table.EtsError) {
  use registrations <- result.try(helpers.new_string_table("registrations"))

  // Try to register username
  use registered <- result.try(operations.insert_new(
    registrations,
    "alice",
    "alice@example.com",
  ))

  case registered {
    True -> Ok(True)   // Username available
    False -> Ok(False) // Username already taken
  }
}

🧪 Tested source

Atomic Operations

import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result

pub fn atomic_take() -> Result(String, table.EtsError) {
  use queue <- result.try(helpers.new_string_table("jobs"))

  // Add a job
  use _ <- result.try(operations.set(queue, "job:123", "send_email"))

  // Take and remove atomically (no race conditions)
  use job <- result.try(operations.take(queue, "job:123"))

  case job {
    option.Some(task) -> Ok(task)
    option.None -> Error(table.OperationFailed("Job not found"))
  }
}

🧪 Tested source

Table Configuration

Optimize tables for your workload:

import dream_ets/config
import dream_ets/operations
import gleam/option
import gleam/result

pub fn configure_table() -> Result(String, table.EtsError) {
  // Create table with read concurrency enabled
  // Use this when multiple processes will read simultaneously
  use cache <- result.try(
    config.new("cache")
    |> config.read_concurrency(True)
    |> config.key_string()
    |> config.value_string()
    |> config.create(),
  )

  use _ <- result.try(operations.set(cache, "key", "value"))
  use value <- result.try(operations.get(cache, "key"))

  case value {
    option.Some(v) -> Ok(v)
    option.None -> Error(table.OperationFailed("Not found"))
  }
}

🧪 Tested source

Configuration Options:

  • read_concurrency(Bool) - Optimize for concurrent reads
  • write_concurrency(Bool) - Optimize for concurrent writes
  • compressed(Bool) - Compress data to save memory
  • table_type(Set | OrderedSet | Bag | DuplicateBag) - Key handling
  • access(Public | Protected | Private) - Process access control

Type Safety

import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result

pub fn type_safe_storage() -> Result(String, table.EtsError) {
  // String table enforces types at compile time
  use cache <- result.try(helpers.new_string_table("cache"))

  // ✅ This works
  use _ <- result.try(operations.set(cache, "key", "value"))

  // ❌ This would be a compile error:
  // operations.set(cache, 123, "value")
  // Error: Expected String, found Int

  use value <- result.try(operations.get(cache, "key"))

  case value {
    option.Some(v) -> Ok(v)
    option.None -> Error(table.OperationFailed("Not found"))
  }
}

🧪 Tested source

Persistence

import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result

pub fn save_to_disk() -> Result(String, table.EtsError) {
  use table <- result.try(helpers.new_string_table("data"))

  use _ <- result.try(operations.set(table, "key", "important data"))

  // Save to disk
  use _ <- result.try(operations.save_to_file(table, "/tmp/backup.ets"))

  // Verify it's still there
  use value <- result.try(operations.get(table, "key"))

  case value {
    option.Some(data) -> Ok(data)
    option.None -> Error(table.OperationFailed("Data lost"))
  }
}

🧪 Tested source

Complete API Reference

Table Creation

  • config.new(name) - Create table configuration
  • config.key_string(config) - Set string keys
  • config.value_string(config) - Set string values
  • config.key(config, encoder, decoder) - Set custom key encoding
  • config.value(config, encoder, decoder) - Set custom value encoding
  • config.counter(config) - Configure counter table (String keys, Int values)
  • config.create(config) - Create table from configuration
  • helpers.new_counter(name) - Convenience: create counter table
  • helpers.new_string_table(name) - Convenience: create string table

Basic Operations

  • operations.set(table, key, value) - Insert or update value
  • operations.get(table, key) - Retrieve value (returns Option)
  • operations.delete(table, key) - Remove key-value pair
  • operations.member(table, key) - Check if key exists (fast)
  • operations.delete_table(table) - Delete entire table
  • operations.delete_all_objects(table) - Clear all entries

Atomic Operations

  • operations.insert_new(table, key, value) - Insert only if key doesn’t exist (atomic)
  • operations.take(table, key) - Get and remove atomically (for queues)

Bulk Operations

Note: These now return Result to handle decode errors properly:

  • operations.keys(table) - Get all keys → Result(List(k), EtsError)
  • operations.values(table) - Get all values → Result(List(v), EtsError)
  • operations.to_list(table) - Get all pairs → Result(List(#(k, v)), EtsError)
  • operations.size(table) - Count entries → Result(Int, EtsError)

Counter Operations

  • helpers.increment(counter, key) - Increment by 1
  • helpers.increment_by(counter, key, amount) - Increment by amount
  • helpers.decrement(counter, key) - Decrement by 1
  • helpers.decrement_by(counter, key, amount) - Decrement by amount

Advanced (Low-level)

  • operations.update_element(table, key, pos, value) - Update tuple element
  • operations.match(table, pattern) - Pattern matching
  • operations.match_object(table, pattern) - Object matching
  • operations.select(table, match_spec) - SQL-like queries

Persistence

  • operations.save_to_file(table, filename) - Save table to disk
  • operations.load_from_file(filename) - Load table from disk

Error Handling

All operations return Result types. Common errors:

  • TableNotFound - Table was deleted
  • TableAlreadyExists - Tried to create duplicate table
  • InvalidKey / InvalidValue - Encoding/decoding failed
  • DecodeError(details) - Failed to decode data (corruption or encoder mismatch)
  • OperationFailed(message) - General operation failure

Example:

case operations.get(table, "user:123") {
  Ok(option.Some(user)) -> process_user(user)
  Ok(option.None) -> create_user()
  Error(table.DecodeError(err)) -> log_corruption(err)
  Error(table.TableNotFound) -> recreate_table()
  Error(other) -> handle_error(other)
}

Use Cases

Session Cache

use sessions <- result.try(
  config.new("sessions")
  |> config.key_string()
  |> config.value_string()
  |> config.create(),
)

use _ <- result.try(operations.set(sessions, "session:abc", "user:alice"))

Page View Analytics

use counter <- result.try(helpers.new_counter("analytics"))
use views <- result.try(helpers.increment(counter, "homepage"))

Distributed Locks

case operations.insert_new(locks, resource_id, owner_id) {
  Ok(True) -> Ok("Lock acquired")
  Ok(False) -> Error("Resource already locked")
  Error(err) -> Error("Lock system error")
}

Work Queue

case operations.take(queue, "next_job") {
  Ok(option.Some(job)) -> process_job(job)  // Atomically claimed
  Ok(option.None) -> wait_for_jobs()
  Error(err) -> handle_error(err)
}

Performance Notes

  • Basic ops (set, get, delete, member) - O(1) constant time
  • Bulk ops (keys, values, to_list, size) - O(n) iterates entire table
  • Concurrency - Enable read_concurrency for read-heavy workloads
  • Compression - Enable for large values to trade CPU for memory

Design Principles

This module follows the same quality standards as Dream:

  • Explicit over implicit - No hidden behavior, no magic
  • Result-based errors - All operations return Result to force error handling
  • No closures - All dependencies are explicit parameters
  • Simple over clever - Code should be obvious and boring
  • Type-safe - Leverage Gleam’s type system fully
  • Black-box testing - Test public interfaces, 100% coverage

All Examples Are Tested

Every code example in this README comes from test/snippets/ and is verified by our test suite. You can run them yourself:

cd modules/ets
gleam test

See test/snippets/ for complete, runnable examples.

About Dream

This module was originally built for the Dream web toolkit, but it’s completely standalone and can be used in any Gleam project. It follows Dream’s design principles and will be maintained as part of the Dream ecosystem.

License

MIT License - see LICENSE file for details.

Search Document