All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

0.3.2 - 2026-02-28

Fixed

  • Horde compile-time detection now checks Horde.Registry instead of Horde — horde 0.10 has no root Horde module, so Code.ensure_loaded?(Horde) always returned false, preventing the Horde backend from being compiled even when the dependency was installed

0.3.1 - 2026-02-26

Fixed

0.3.0 - 2026-02-26

Fixed

  • DSL-configured shutdown_after and hibernate_after options were ignored at runtime — processes never shut down even when shutdown_after was set in the options block. The values were stored for introspection but never read during process startup. Server.init/1 and Server.start_link/1 now fall back to the module's DSL-configured values when options are not explicitly passed.

Added

  • Built-in state.id field on every object's State struct, automatically set to the object's ID at init time
    • Available in handlers, after_load, and handle_alarm callbacks
    • Not persisted to the database — it's runtime metadata, not domain state
    • Defaults to nil in test helpers like perform_handler/4 (set it yourself if your handler reads state.id)
  • Auto-generated DSL reference documentation via mix spark.cheat_sheets
  • mix docs alias now chains spark.cheat_sheetsdocsspark.replace_doc_links
  • CI check to verify DSL documentation is up-to-date (mix spark.cheat_sheets --check)

Changed

  • Breaking: field :id is now a reserved name and will raise a compile-time error. If you have an existing field :id in your state block, rename it (e.g., to :external_id or :resource_id) before upgrading.
  • State is now returned as a struct (%MyApp.Counter.State{count: 0}) instead of a plain atom-keyed map (%{count: 0})
    • The DSL automatically generates a nested State struct module from the declared fields and defaults
    • %{state | field: value} update syntax continues to work unchanged

    • State is persisted to the database as a plain JSON map (no __struct__ key)
    • Unknown keys in persisted state are silently dropped on load (forward-compatible with field removal)
    • get_persisted_state/3 in DurableObject.Testing now returns the module's State struct
    • Breaking: state[:field] bracket access no longer works — use state.field dot access instead

0.2.1 - 2026-02-03

Added

  • object_keys option to control how string keys within field values are converted when loading state from JSON
    • :strings (default) - leaves keys as strings
    • :atoms! - converts to existing atoms only (raises on unknown keys)
    • :atoms - creates atoms as needed (use with caution)
    • Configurable per-object in the DSL options block, or globally via config :durable_object, object_keys: :atoms!
    • DSL setting takes precedence over application config
  • DurableObject.Testing module with ergonomic test helpers
    • use DurableObject.Testing, repo: MyApp.Repo sets up Ecto sandbox and imports helpers
    • Unit testing: perform_handler/4 and perform_alarm_handler/3 for testing handler logic in isolation
    • Alarm assertions: assert_alarm_scheduled/4, refute_alarm_scheduled/4, all_scheduled_alarms/3
    • Alarm execution: fire_alarm/4 to bypass scheduler timing, drain_alarms/3 for alarm chains
    • State assertion: assert_persisted/4 for verifying persisted state
    • Async helper: assert_eventually/2 for polling conditions

Fixed

0.2.0 - 2026-01-30

Upgrading from 0.1.x

If you use the polling scheduler (DurableObject.Scheduler.Polling), the following changes are required. Users of the Oban scheduler are unaffected.

Required migration: Generate and run an upgrade migration before deploying:

mix durable_object.gen.migration
mix ecto.migrate

The task automatically detects your current migration version and generates the appropriate upgrade migration.

Idempotent handlers: The polling scheduler now uses at-least-once delivery. If a node crashes mid-handler, the alarm will be retried after claim_ttl expires (default: 60 seconds). Ensure your handle_alarm/3 callbacks are idempotent.

Added

  • mix durable_object.gen.migration task to generate upgrade migrations automatically
  • base option for DurableObject.Migration.up/1 and down/1 to support incremental upgrades
  • Crash recovery for polling scheduler alarms: if the server crashes or restarts while executing an alarm handler, the alarm is automatically retried
  • New claim_ttl option for polling scheduler (default: 60 seconds) - controls how long before a claimed alarm becomes eligible for retry. Lower values reduce recovery latency but increase risk of duplicate delivery if handlers are slow.
  • Migration version 3 adds claimed_at column to durable_object_alarms table

Changed

  • Polling scheduler now uses at-least-once semantics (handlers should be idempotent)
  • Alarms are claimed before firing and only deleted on success
  • Failed or interrupted alarm handlers will retry after the claim TTL expires

0.1.5 - 2026-01-28

Added

  • Lifecycle guide with Mermaid diagrams covering all phases from startup through shutdown
  • Mermaid diagram rendering support in ExDoc
  • usage-rules.md for LLM agent guidance via the usageRules ecosystem

Changed

  • Oban scheduler oban_instance option now defaults to Oban, matching the common case where apps use a single default Oban instance

Fixed

  • Documentation in DurableObject.Scheduler now uses correct option names (oban_instance and oban_queue) to match the implementation
  • Oban scheduler documentation now shows simple default configuration first, with customization options explained separately

0.1.4 - 2026-01-27

Fixed

  • Documentation now uses correct registry_mode config key instead of cluster
  • README migration example now uses latest migration version instead of hardcoding version: 1
  • Installer generates migrations using latest version for up/0
  • Polling scheduler documentation shows repo at top-level config (canonical location)

0.1.3 - 2026-01-27

Fixed

  • Oban scheduler schedule/4 now passes arguments to Oban.insert/2 in the correct order for named instances

0.1.2 - 2026-01-27

Fixed

0.1.1 - 2026-01-27

Fixed

  • Oban scheduler now uses correct config keys (oban_instance and oban_queue) to match what the installer generates

0.1.0 - 2026-01-27

Added

  • Initial release
  • Core Durable Object functionality with GenServer-backed instances
  • Spark DSL for declarative object definitions
    • state section for defining fields with types and defaults
    • handlers section for defining RPC methods
    • options section for lifecycle configuration
  • Automatic client API generation from handler definitions
  • Ecto-based persistence with JSON blob state storage
  • Versioned migrations for database schema (v1 creates tables, v2 removes unused locking columns)
  • Alarm scheduling with two backends:
  • Distribution support via Horde (optional)
  • Telemetry instrumentation for storage operations
  • Igniter-based installation task (mix igniter.install durable_object)
  • Object generator task (mix durable_object.gen.object)

Configuration Options

  • repo - Ecto repo for persistence
  • registry_mode - :local (default) or :horde for distribution
  • scheduler - Alarm scheduler backend
  • scheduler_opts - Backend-specific options