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

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:

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

Basic Operations

Atomic Operations

Bulk Operations

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

Counter Operations

Advanced (Low-level)

Persistence

Error Handling

All operations return Result types. Common errors:

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

Design Principles

This module follows the same quality standards as Dream:

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