{"id":24804954,"url":"https://github.com/rill-project/rill","last_synced_at":"2026-05-06T05:31:23.705Z","repository":{"id":57544111,"uuid":"162836935","full_name":"rill-project/rill","owner":"rill-project","description":"Event-Sourced microservices framework","archived":false,"fork":false,"pushed_at":"2019-01-09T19:14:35.000Z","size":153,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-03-15T02:14:06.545Z","etag":null,"topics":["event-sourcing","microservices","mnesia","postgres"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rill-project.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-22T19:17:53.000Z","updated_at":"2024-03-15T02:14:06.546Z","dependencies_parsed_at":"2022-09-16T23:01:35.978Z","dependency_job_id":null,"html_url":"https://github.com/rill-project/rill","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rill-project%2Frill","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rill-project%2Frill/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rill-project%2Frill/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rill-project%2Frill/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rill-project","download_url":"https://codeload.github.com/rill-project/rill/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245422921,"owners_count":20612725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-sourcing","microservices","mnesia","postgres"],"created_at":"2025-01-30T07:16:04.714Z","updated_at":"2026-05-06T05:31:23.674Z","avatar_url":"https://github.com/rill-project.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Rill\n\nTranslation of [Eventide Framework](https://eventide-project.org/) in Elixir.\nPlease refer to their [documentation](http://docs.eventide-project.org/)\nfor learning purposes or finding out arguments of some functions.\n\n## Installation\n\nInstall via Hex:\n\n```elixir\ndef deps do\n  [\n    {:rill, \"\u003e= 0.0.0\"}\n  ]\nend\n```\n\n### Memory\n\nTo use the in-memory MessageStore, `Rill.MessageStore.Memory.Server` needs to be started and `Session` needs to be configured with the server pid (or name).\n\n### Mnesia (memory)\n\nTo use the Mnesia MessageStore, `Rill.MessageStore.Mnesia.start()` needs to be\ncalled once and `Session` needs to be configured with a namespace (any string\nworks). Optionally the session can be initialized using `rand/0`, which returns\na tuple with `{%Session{}, uuid}` (uuid is a `String`)\n\n### Ecto.Postgres\n\nTo use the Ecto.Postgres MessageStore, `ecto` and `ecto_sql`, packages are\nneeded.\n\nA Ruby installation is needed.\n\nFollowing the steps for [Eventide Postgres Setup](http://docs.eventide-project.org/setup/postgres.html#eventide-for-postgres-setup),\nup to [Create the Message Store Database](http://docs.eventide-project.org/setup/postgres.html#create-the-message-store-database)\n(included), will ensure the database is correctly created.\n\nA Repo module needs to be created, following these guidelines:\n\n```elixir\ndefmodule MyRepo do\n  use Ecto.Repo, otp_app: :your_app\n\n  @doc \"\"\"\n  Dynamically loads the repository url from the\n  DATABASE_URL environment variable.\n  \"\"\"\n  def init(_, opts) do\n    # This part is entirely optional\n    {:ok, Keyword.put(opts, :url, System.get_env(\"DATABASE_URL\"))}\n  end\nend\n```\n\nThe `Repo` module needs to be started and supplied to `Session` during\nconfiguration.\n\n## Getting Started\n\n### Memory\n\n```elixir\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\n{:ok, pid} = Rill.MessageStore.Memory.Server.start_link()\nsession = Rill.MessageStore.Memory.Session.new(pid)\nmessage = %Renamed{name: \"foo\"}\n\nRill.MessageStore.write(session, message, \"person\")\n```\n\n### Mnesia (memory)\n\n```elixir\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\nRill.MessageStore.Mnesia.start()\nnamespace = Rill.Identifier.UUID.Random.get()\nsession = Rill.MessageStore.Mnesia.Session.new(namespace)\n# Alternatively:\n# session = Rill.MessageStore.Mnesia.Session.rand()\nmessage = %Renamed{name: \"foo\"}\n\nRill.MessageStore.write(session, message, \"person\")\n```\n\n### Postgres\n\n```elixir\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\n{:ok, _pid} = MyRepo.start_link([name: MyRepo])\nsession = Rill.MessageStore.Ecto.Postgres.Session.new(MyRepo)\nmessage = %Renamed{name: \"foo\"}\n\nRill.MessageStore.write(session, message, \"person\")\n```\n\n## Framework at a Glance\n\n```elixir\n\ndefmodule Person do\n  defstruct [:name, :age]\nend\n\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\ndefmodule Person.Projection do\n  use Rill, :projection\n\n  @impl Rill.EntityProjection\n  def apply(%Renamed{} = renamed, person) do\n    Map.put(person, :name, renamed.name)\n  end\nend\n\ndefmodule Repo do\n  use Ecto.Repo,\n    otp_app: :my_app,\n    adapter: Ecto.Adapters.Postgres\n\n  def init(_, opts) do\n    {:ok, Keyword.put(opts, :url, System.get_env(\"DATABASE_URL\"))}\n  end\nend\n\ndefmodule Store do\n  use Rill, [\n    :store,\n    entity: %Person{},\n    category: \"person\",\n    projection: Person.Projection\n  ]\nend\n\ndefmodule Handler do\n  use Rill, :handler\n\n  @impl Rill.Messaging.Handler\n  def handle(%Renamed{} = renamed, _session) do\n    IO.inspect(renamed)\n  end\nend\n\ndefmodule Run do\n  def run do\n    {:ok, pid1} = Repo.start_link(name: Repo)\n\n    session = Rill.MessageStore.Ecto.Postgres.Session.new(Repo)\n\n    renamed = %Renamed{name: \"Joe\"}\n    Rill.MessageStore.write(session, renamed, \"person-123\")\n    \n    [person, version] = Store.get(session, \"123\", include: [:version])\n    person.name # =\u003e \"Joe\"\n    version # =\u003e 0\n\n    Supervisor.start_link(\n      [\n        {Rill.Consumer,\n         [\n           handlers: [Handler],\n           stream_name: \"person\",\n           identifier: \"personIdentifier\",\n           session: session,\n           poll_interval_milliseconds: 10000,\n           batch_size: 1\n         ]}\n      ],\n      strategy: :one_for_one\n    )\n\n    :timer.sleep(1500)\n    # IO.inspect will output `renamed` content\n  end\nend\n```\n\n## Features\n\n### Read from MessageStore\n\nReading all messages for a given stream name, can be accomplished with:\n\n```elixir\nRill.MessageStore.read(session, \"streamName\") # Returns a stream\n```\n\nA utility is provided for the following common pattern (used in tests):\n\n- Read a message\n- Pass the message to a handler\n- Repeat the previous 2 steps N times\n\n```elixir\n# Handles all messages from \"streamName\", 5 times\nRill.MessageStore.Reader.handle(session, \"streamName\", HandlerModule, 5)\n```\n\nThe `handle` function is provided as utility for testing, it's not meant to\nbe used in production code. Notice that `handle` returns the `Session`, so it\ncan be piped.\n\n### Define a Message\n\n```elixir\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\nmessage = %Renamed{name: \"foo\"}\n```\n\n### Write to MessageStore\n\n#### Simple write\n\n```elixir\nRill.MessageStore.write(session, message, \"streamName\")\n```\n\n#### Write with expected version\n\n```elixir\nRill.MessageStore.write(session, message, \"streamName\", expected_version: version)\n```\n\n#### Write initial message\n\nRaises exception if 1+ messages are present already on the stream\n\n```elixir\nRill.MessageStore.write_initial(session, message, \"streamName\")\n```\n\n#### Write one message\n\nLike `write_initial` but instead of raising, returns `nil`. If a\nmessage is written, a non-negative integer is returned instead\n\n```elixir\nRill.MessageStore.write_once(session, message, \"streamName\")\n```\n\n#### Write batch of messages to the same stream\n\n```elixir\nRill.MessageStore.write(session, [msg1, msg2], \"streamName\", expected_version: version)\n```\n\n### Define a Projection\n\n```elixir\ndefmodule Person do\n  defstruct [name: \"\", age: 0]\nend\n\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\ndefmodule Person.Projection do\n  use Rill, :projection\n\n  @impl Rill.EntityProjection\n  # Pattern matching on the first argument it's REQUIRED to determine the\n  # struct that needs to be used to decode the message coming from the\n  # MessageStore\n  def apply(%Renamed{} = renamed, person) do\n    Map.put(person, :name, renamed.name)\n  end\nend\n\n# A projection is not usually called directly\n\n# Simulates a message coming from the database\nmessage_data = %Rill.MessageStore.MessageData.Read{\n  data: %{name: \"Joe\"},\n  type: \"Renamed\"\n}\nperson = %Person{name: \"Fran\", age: 29}\nperson = Person.Projection.apply(person, message_data)\n\nperson.name # =\u003e \"Joe\"\n```\n\n### Create a Session\n\nA `Rill.Session` is just a struct, but there are some utilities available.\n\n#### Memory\n\n```elixir\n{:ok, pid} = Rill.MessageStore.Memory.Server.start_link()\nsession = Rill.MessageStore.Memory.Session.new(pid)\n```\n\n#### Mnesia (memory)\n\n```elixir\nRill.MessageStore.Mnesia.start()\nnamespace = Rill.Identifier.UUID.Random.get()\nsession = Rill.MessageStore.Mnesia.Session.new(namespace)\n```\n\nAlternatively:\n\n```elixir\nRill.MessageStore.Mnesia.start()\nsession = Rill.MessageStore.Mnesia.Session.rand()\n```\n\n#### Postgres\n\n```elixir\n{:ok, _} = Repo.start_link([name: Repo])\nsession = Rill.MessageStore.Ecto.Postgres.Session.new(repo)\n```\n\n### Define a Store\n\n```elixir\ndefmodule Store do\n  use Rill, [\n    :store\n    # Initial value for the entity, can be omitted and defaults to `nil`\n    entity: %Person{},\n    # Stream name category\n    category: \"person\",\n    # Projection module\n    projection: Person.Projection\n  ]\nend\n\n[person] = Store.fetch(\"123\")\n\n# Including the current version\n[person, version] = Store.fetch(\"123\", include: :version) # or [:version]\n```\n\n### Define a Handler\n\n```elixir\ndefmodule Renamed do\n  use Rill, :message\n  defstruct([:name])\nend\n\ndefmodule Handler do\n  use Rill, :handler\n\n  @impl Rill.Messaging.Handler\n  # Pattern matching on the first argument it's REQUIRED to determine the\n  # struct that needs to be used to decode the message coming from the\n  # MessageStore\n  def handle(%Renamed{} = renamed, _session) do\n    IO.inspect(renamed)\n    IO.puts(\"hello\")\n  end\nend\n\n# A handler is not usually called directly\n\n# Simulates a message coming from the database\nmessage_data = %Rill.MessageStore.MessageData.Read{\n  data: %{name: \"Joe\"},\n  type: \"Renamed\"\n}\n\nHandler.handle(session, message_data)\n```\n\nWhen a handler is defined using `use Rill, :handler`, the following utilities\nare provided:\n\n- `try_version` macro, which rescue any\n  `Rill.MessageStore.ExpectedVersion.Error` and returns `nil`\n- `MessageStore` is available (automatically aliased)\n- `Message` is available (automatically aliased)\n- The `stream_name/1`, `/2` and `/3` are imported automatically from\n  `Rill.MessageStore.StreamName`\n\n### Start a Consumer\n\n```elixir\nRill.Consumer.start_link([\n  # The following arguments are all required\n  handlers: [Handler],\n  stream_name: \"person\",\n  # Must be supplied to uniquely identify this consumer\n  identifier: \"personIdentifier\",\n  session: session\n  # Optionally can pass a `condition` argument, handled by the underlying\n  # MessageStore adapter\n])\n```\n\nAlternatively, a `Consumer` can be started from a `Supervisor`:\n\n```elixir\nSupervisor.start_link(\n  [\n    {Rill.Consumer,\n     [\n       handlers: [Handler],\n       stream_name: \"person\",\n       identifier: \"personIdentifier\",\n       session: session\n     ]}\n  ],\n  strategy: :one_for_one\n)\n```\n\n## TODO\n\n- [x] Logging\n- [ ] Tests\n- [ ] Test utilities\n- [ ] Position Store\n- [ ] Store Cache\n- [ ] Snapshotting\n- [ ] View Data\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frill-project%2Frill","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frill-project%2Frill","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frill-project%2Frill/lists"}