{"id":44302786,"url":"https://github.com/ismasan/sourced","last_synced_at":"2026-02-11T02:53:24.507Z","repository":{"id":47543430,"uuid":"155718763","full_name":"ismasan/sourced","owner":"ismasan","description":"Event Sourcing / CQRS toolkit for Ruby. Eventual consistency and concurrency built-in.","archived":false,"fork":false,"pushed_at":"2026-02-02T16:04:07.000Z","size":770,"stargazers_count":37,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-03T05:12:16.169Z","etag":null,"topics":["cqrs","event-driven","eventsourcing","services"],"latest_commit_sha":null,"homepage":"https://ismasan.github.io/sourced/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ismasan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-11-01T13:24:09.000Z","updated_at":"2026-02-02T16:04:40.000Z","dependencies_parsed_at":"2024-12-09T17:21:05.530Z","dependency_job_id":"fa95bd70-2ffa-42d4-b26e-d8da0558ae12","html_url":"https://github.com/ismasan/sourced","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ismasan/sourced","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ismasan%2Fsourced","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ismasan%2Fsourced/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ismasan%2Fsourced/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ismasan%2Fsourced/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ismasan","download_url":"https://codeload.github.com/ismasan/sourced/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ismasan%2Fsourced/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29326078,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-11T02:08:56.257Z","status":"ssl_error","status_checked_at":"2026-02-11T02:08:51.338Z","response_time":97,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["cqrs","event-driven","eventsourcing","services"],"created_at":"2026-02-11T02:53:21.906Z","updated_at":"2026-02-11T02:53:24.501Z","avatar_url":"https://github.com/ismasan.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sourced\n\n**WORK IN PROGRESS**\n\nEvent Sourcing / CQRS library for Ruby.\nThere's many ES gems available already. The objectives here are:\n* Cohesive and toy-like DX.\n* Eventual consistency by default. Actor-like execution model.\n* Low-level APIs for durable messaging.\n* Supports the [Decide, Evolve, React pattern](https://ismaelcelis.com/posts/decide-evolve-react-pattern-in-ruby/)\n* Control concurrency by modeling.\n* Simple to operate: it should be as simple to run as most Ruby queuing systems.\n* Explore ES as a programming model for Ruby apps.\n\nA small demo app [here](https://github.com/ismasan/sourced_todo).\n\n## The programming model\n\nIf you're unfamiliar with Event Sourcing, you can read this first: [Event Sourcing from the ground up, with Ruby examples](https://ismaelcelis.com/posts/event-sourcing-ruby-examples)\nFor a high-level overview of the mental model, [read this](https://ismaelcelis.com/posts/2025-04-give-it-time/). Or the video version, [here](https://www.youtube.com/watch?v=EgUwnzUJHMA).\n\nThe entire behaviour of an event-sourced app is described via **commands**, **events** and **reactions**.\n\n\u003cimg width=\"1024\" height=\"469\" alt=\"sourced-arch-diagram\" src=\"https://github.com/user-attachments/assets/ed916471-525f-4743-bc9a-10a2b6d9f8e9\" /\u003e\n\n\n* **Commands** are _intents_ to effect some change in the state of the system. Ex. `Add cart item`, `Place order`, `Update email`, etc.\n* **Events** are produced after handling a command and they describe _facts_ or state changes in the system. Ex. `Item added to cart`, `order placed`, `email updated`. Events are stored and you can use them to build views (\"projections\"), caches and reports to support UIs, or other artifacts.\n* **Reactions** are blocks of code that run _after_ an event has been processed and can dispatch new commands in a workflow or automation.\n* **State** is whatever object you need to hold the current state of a part of the system. It's usually derived from past events, and it's just enough to interrogate the state of the system and make the next decision.\n\n## Actors\n\n### Overview\n\nActors are classes that encapsulate the full life-cycle of a concept in your domain, backed by an event stream. This includes loading state from past events and handling commands for a part of your system. They can also define reactions to their own events, or events emitted by other actors. This is a simple shopping cart actor.\n\n```ruby\nclass Cart \u003c Sourced::Actor\n  # Define what cart state looks like.\n  # This is the initial state which will be updated by applying events.\n  # The state holds whatever data is relevant to decide how to handle a command.\n  # It can be any object you need. A custom class instance, a Hash, an Array, etc.\n  CartState = Struct.new(:id, :status, :items) do\n    def total = items.sum { |it| it.price * it.quantity }\n  end\n    \n  CartItem = Struct.new(:product_id, :price, :quantity)\n    \n  # This factory is called to initialise a blank cart.\n  state do |id|\n    CartState.new(id:, status: 'open', items: [])\n  end\n  \n  # Define a command and its handling logic.\n  # The command handler will be passed the current state of the cart,\n  # and the command instance itself.\n  # Its main job is to validate business rules and decide whether new events\n  # can be emitted to update the state\n  command :add_item, product_id: String, price: Integer, quantity: Integer do |cart, cmd|\n    # Validate that this command can run\n    raise \"cart is not open!\" unless cart.status == 'open'\n    # Produce a new event with the same attributes as the command\n    event :item_added, cmd.payload\n  end\n  \n  # Define an event handler that will \"evolve\" the state of the cart by adding an item to it.\n  # These handlers are also used to \"hydrate\" the initial state from Sourced's storage backend\n  # when first handling a command\n  event :item_added, product_id: String, price: Integer, quantity: Integer do |cart, event|\n    cart.items \u003c\u003c CartItem.new(**event.payload.to_h)\n  end\n  \n  # Optionally, define how this actor reacts to the event above.\n  # .reaction blocks can dispatch new commands that will be routed to their handlers.\n  # This allows you to build workflows.\n  reaction :item_added do |event|\n    # Evaluate whether we should dispatch the next command.\n    # Here we could fetch some external data or query that might be needed\n    # to populate the new commands.\n    # Here we dispatch a command to the same stream_id present in the event\n    dispatch(:send_admin_email, product_id: event.payload.product_id)\n  end\n  \n  # Handle the :send_admin_email dispatched by the reaction above\n  command :send_admin_email, product_id: String do |cart, cmd|\n    # maybe produce new events\n  end\nend\n```\n\nUsing the `Cart` actor in an IRB console. This will use Sourced's in-memory backend by default.\n\n```ruby\ncart = Cart.new(id: 'test-cart')\ncart.state.total # =\u003e 0\n# Instantiate a command and handle it\ncmd = Cart::AddItem.build('test-cart', product_id: 'p123', price: 1000, quantity: 2)\nevents = cart.decide(cmd)\n# =\u003e [Cart::ItemAdded.new(...)]\ncmd.valid? # true\n# Inspect state\ncart.state.total # 2000\ncart.items.items.size # 1\n# Inspect that events were stored\ncart.seq # 2 the sequence number or \"version\" in storage. Ie. how many commands / events exist for this cart\n# Append new messages to the backend\nSourced.config.backend.append_to_stream('test-cart', events)\n# Load events for cart\nevents = Sourced.history_for(cart)\n# =\u003e an array with instances of [Cart::AddItem, Cart::ItemAdded]\nevents.map(\u0026:type) # ['cart.add_item', 'cart.item_added']\n```\n\nTry loading a new cart instance from recorded events\n\n```ruby\ncart2, events = Sourced.load(Cart, 'test-cart')\ncart2.seq # 2\ncart2.state.total # 2000\ncart2.state.items.size # 1\n```\n\n### Registering actors\n\nInvoking commands directly on an actor instance works in an IRB console or a synchronous-only web handler, but for actors to be available to background workers, and to react to other actor's events, you need to register them.\n\n```ruby\nSourced.register(Cart)\n```\n\nThis achieves two things:\n\n1. Messages can be routed to this actor by background processes, using `Sourced.dispatch(message)`.\n2. The actor can _react_ to other events in the system (more on event choreography later), via its low-level `.handle(event)` [Reactor Interface](#the-reactor-interface).\n\nThese two properties are what enables asynchronous, eventually-consistent systems in Sourced.\n\n### Expanded message syntax\n\nCommands and event structs can also be defined separately as `Sourced::Command` and `Sourced::Event` sub-classes.\n\nThese definitions include a message _type_ (for storage) and payload attributes schema, if any.\n\n```ruby\nmodule Carts\n  # A command to add an item to the cart\n  # Commands may come from HTML forms, so we use Types::Lax to coerce attributes\n  AddItem = Sourced::Command.define('carts.add_item') do\n    attribute :product_id, Types::Lax::Integer\n    attribute :quantity, Types::Lax::Integer.default(1)\n    attribute :price, Types::Lax::Integer.default(0)\n  end\n  \n  # An event to track items added to the cart\n  # Events are only produced by valid commands, so we don't \n  # need validations or coercions\n  ItemAdded = Sourced::Event.define('carts.item_added') do\n    attribute :product_id, Integer\n    attribute :quantity, Integer\n    attribute :price, Integer\n  end\n  \n  ## Now define command and event handlers in a Actor\n  class Cart \u003c Sourced::Actor\n    # Initial state, etc...\n    \n    command AddItem do |cart, cmd|\n      # logic here\n      event ItemAdded, cmd.payload\n    end\n    \n    event ItemAdded do |cart, event|\n      cart.items \u003c\u003c CartItem.new(**event.payload.to_h)\n    end\n  end\nend\n```\n\n### `.command` block\n\nThe class-level `.command` block defines a _command handler_. Its job is to take a command (from a user, an automation, etc), validate it, and apply state changes by publishing new events.\n\n\u003cimg width=\"615\" height=\"168\" alt=\"sourced-command-handler\" src=\"https://github.com/user-attachments/assets/4db26fa1-6671-4611-b994-b3e864cd88b4\" /\u003e\n\n\n```ruby\ncommand AddItem do |cart, cmd|\n  # logic here...\n  # apply and publish one or more new events\n  # using instance-level #event(event_type, **payload)\n  event ItemAdded, product_id: cmd.payload.product_id\nend\n```\n\n\n\n### `.event` block\n\nThe class-level `.event` block registers an _event handler_ used to _evolve_ the actor's internal state.\n\nThese blocks are used both to load the initial state when handling a command, and to apply new events to the state in command handlers.\n\n\u003cimg width=\"573\" height=\"146\" alt=\"sourced-evolve-handler\" src=\"https://github.com/user-attachments/assets/174fb8d0-e2ef-41f3-8f43-c94b766529ec\" /\u003e\n\n\n```ruby\nevent ItemAdded do |cart, event|\n  cart.items \u003c\u003c CartItem.new(**event.payload.to_h)\nend\n```\n\nThese handlers are pure: given the same state and event, they should always update the state in the same exact way. They should never reach out to the outside (API calls, current time, etc), and they should never run validations. They work on events already committed to history, which by definition are assumed to be valid.\n\n### `.before_evolve` block\n\nThe class-level `.before_evolve` block registers a callback that runs **before** each registered event handler during state evolution. This is useful for common logic that should run before all event handlers, such as updating timestamps or recording metadata.\n\n```ruby\nclass CartListings \u003c Sourced::Projector::StateStored\n  state do |id|\n    { id:, items: [], updated_at: nil, seq: 0 }\n  end\n\n  # This block runs before any .event handler\n  before_evolve do |state, event|\n    state[:updated_at] = event.created_at\n    state[:seq] = event.seq\n  end\n\n  event Cart::ItemAdded do |state, event|\n    state[:items] \u003c\u003c event.payload.to_h\n  end\n\n  event Cart::Placed do |state, event|\n    state[:status] = :placed\n  end\nend\n```\n\nThe `before_evolve` callback only runs for events that have a registered handler via the `.event` macro. If an event is not handled by this class, the callback is skipped for that event.\n\n### `.reaction` block\n\nThe class-level `.reaction` block registers an event handler that _reacts_ to events already published by this or other Actors.\n\n`.reaction` blocks can dispatch the next command in a workflow with the instance-level `#dispatch` helper.\n\n\u003cimg width=\"504\" height=\"109\" alt=\"sourced-react-handler\" src=\"https://github.com/user-attachments/assets/b181ebdd-4bc7-4692-a2ab-910c1a829ec4\" /\u003e\n\n\n```ruby\nreaction ItemAdded do |cart, event|\n  # dispatch the next command to the event's stream_id\n  dispatch(\n    CheckInventory, \n    product_id: event.payload.product_id,\n    quantity: event.payload.quantity\n  )\nend\n```\n\nYou can also dispatch commanda to _other_ streams. For example for starting concurrent workflows.\n\n```ruby\n# dispatch a command to a new custom-made stream_id\ndispatch(CheckInventory, event.payload).to(\"cart-#{Time.now.to_i}\")\n\n# Or use Sourced.new_stream_id\ndispatch(CheckInventory, event.payload).to(Sourced.new_stream_id)\n\n# Or start a new stream and dispatch commands to another actor\ndispatch(:notify, message: 'hello!').to(NotifierActor)\n```\n\n#### `.reaction` block with actor state\n\n `.reaction`  blocks receive the actor state, which is derived by applying past events to it (same as when handling commands).\n\n```ruby\n# Define an event handler to evolve state\nevent ItemAdded do |state, event|\n  state[:item_count] += 1\nend\n\n# Now react to it and check state\nreaction ItemAdded do |state, event|\n  if state[:item_count] \u003e 30\n    dispatch NotifyBigCart\n  end\nend\n```\n\n#### `.reaction` with state for all events\n\nIf the event name or class is omitted, the `.reaction` macro registers reaction handlers for all events already registered for the actor with the `.event` macro, minus events that have specific reaction handlers defined.\n\n```ruby\n# wildcard reaction for all evolved events\nreaction do |state, event|\n  if state[:item_count] \u003e 30\n    dispatch NotifyBigCart\n  end\nend\n```\n\n#### `.reaction` for multiple events\n\n```ruby\nreaction ItemAdded, InventoryChecked do |state, event|\n  # etc\nend\n```\n\nIt also works with symbols, for messages that have been defined as symbols (ex `event :item_added`)\n\n```ruby\nreaction :item_added, InventoryChecked do |state, event|\n  # etc\nend\n```\n\n## Causation and correlation\n\nWhen a command produces events, or when an event makes a reactor dispatch a new command, the cause-and-effect relationship between these messages is tracked by Sourced in the form of `correlation_id` and `causation_id` properties in each message's metadata.\n\n\u003cimg width=\"878\" height=\"326\" alt=\"sourced-causation-correlation\" src=\"https://github.com/user-attachments/assets/88d86b65-50ff-4222-8941-406826fab243\" /\u003e\n\n\nThis helps the system keep a full audit trail of the cause-and-effect behaviour of the entire system.\n\n\u003cimg width=\"877\" height=\"629\" alt=\"CleanShot 2025-11-11 at 23 59 40\" src=\"https://github.com/user-attachments/assets/38765370-2e80-46d8-bf30-1651208d5cf9\" /\u003e\n\n## Background vs. foreground execution\n\nTODO\n\n## Projectors\n\nProjectors react to events published by actors and update views, search indices, caches, or other representations of current state useful to the app. They can both react to events as they happen in the system, and also \"catch up\" to past events. Sourced keeps track of where in the global event stream each projector is.\n\nFrom the outside-in, projectors are classes that implement the _Reactor interface_.\n\nSourced ships with two ready-to-use projectors, but you can also build your own.\n\n### State-stored projector\n\nA state-stored projector fetches initial state from storage somewhere (DB, files, API), and then after reacting to events and updating state, it can save it back to the same or different storage.\n\n```ruby\nclass CartListings \u003c Sourced::Projector::StateStored\n  # Fetch listing record from DB, or new one.\n  state do |id|\n    CartListing.find_or_initialize(id)\n  end\n\n  # Evolve listing record from events\n  event Carts::ItemAdded do |listing, event|\n    listing.total += event.payload.price\n  end\n\n  # Sync listing record back to DB\n  sync do |state:, events:, replaying:|\n    state.save!\n  end\nend\n```\n\n### Event-sourced projector\n\nAn event-sourced projector fetches initial state from past events in the event store, and then after reacting to events and updating state, it can save it to a DB table, a file, etc.\n\n```ruby\nclass CartListings \u003c Sourced::Projector::EventSourced\n  # Initial in-memory state\n  state do |id|\n    { id:, total: 0 }\n  end\n\n  # Evolve listing record from events\n  event Carts::ItemAdded do |listing, event|\n    listing[:total] += event.payload.price\n  end\n\n  # Sync listing record to a file\n  sync do |state:, events:, replaying:|\n    File.write(\"/listings/#{state[:id]}.json\", JSON.dump(state)) \n  end\nend\n```\n\n### Registering projectors\n\nLike any other _reactor_, projectors need to be registered for background workers to route events to them.\n\n```ruby\n# In your app's configuration\nSourced.register(CartListings)\n```\n\n### Reacting to events and scheduling the next command from projectors\n\nSourced projectors can define `.reaction` handlers that will be called after evolving state via their `.event` handlers, in the same transaction.\n\nThis can be useful to implement TODO List patterns where a projector persists projected data, and then reacts to the data update using the data to schedule the next command in a workflow.\n\n![CleanShot 2025-05-30 at 18 43 01](https://github.com/user-attachments/assets/ef8a61b7-6b99-49a1-9767-af94b9c2c4e2)\n\n\n```ruby\nclass ReadyOrders \u003c Sourced::Projector::StateStored\n  # Fetch listing record from DB, or new one.\n  state do |id|\n    OrderListing.find_or_initialize(id)\n  end\n\n  event Orders::ItemAdded do |listing, event|\n    listing.line_items \u003c\u003c event.payload\n  end\n  \n  # Evolve listing record from events\n  event Orders::PaymentConfirmed do |listing, event|\n    listing.payment_confirmed = true\n  end\n\n  event Orders::BuildConfirmed do |listing, event|\n    listing.build_confirmed = true\n  end\n  \n  # Sync listing record back to DB\n  sync do |state:, events:, replaying:|\n    state.save!\n  end\n  \n  # If a listing has both the build and payment confirmed,\n  # automate dispatching the next command in the workflow\n  reaction do |listing, event|\n    if listing.payment_confirmed? \u0026\u0026 listing.build_confirmed?\n      dispatch Orders::Release, **listing.attributes\n    end\n  end\nend\n```\n\nProjectors can also define `.reaction event_class do |state, event|` to react to specific events, or `reaction event1, event2` to react to more than one event with the same block.\n\n### Skipping projector reactions when replaying events\n\nWhen a projector's offsets are reset (so that it starts re-processing events and re- building projections), Sourced skips invoking a projector's `.reaction` handlers. This is because building projections should be deterministic, and rebuilding them should not trigger side-effects such as automations (we don't want to call 3rd party APIs, send emails, or just dispatch the same commands over and over when rebuilding projections).\n\nTo do this, Sourced keeps track of each consumer groups' highest acknowledged event sequence. When a consumer group is reset and starts re-processing past events, this sequence number is compared with each event's sequence, which tells us whether the event has been processed before.\n\n## Concurrency model\n\nConcurrency in Sourced is achieved by explicitely _modeling it in_.\n\nSourced workers process messages by acquiring locks on `[reactor group ID][stream ID]`. For example `\"CartActor:cart-123\"`\n\nThis means that all events for a given reactor/stream are processed in order, but events for different streams can be processed concurrently. You can define workflows where some work is done concurrently by modeling them as a collaboration of streams.\n\n### Single-stream sequential execution\n\nIn the following (simplified!) example, a Holiday Booking workflow is modelled as a single stream (\"Actor\"). The infrastructure makes sure these steps are run sequentially.\n\n\u003cimg width=\"1583\" height=\"292\" alt=\"sourced-concurrency-single-lane\" src=\"https://github.com/user-attachments/assets/025529de-906c-41b4-8f21-0b7759b6e394\" /\u003e\n\nThe Actor glues its steps together by reacting to events emitted by the previous step, and dispatching the next command.\n\n```ruby\nclass HolidayBooking \u003c Sourced::Actor\n  # State and details omitted...\n  \n  command :start_booking do |state, cmd|\n    event :booking_started\n  end\n  \n  reaction :booking_started do |event|\n    dispatch :book_flight\n  end\n  \n  command :book_flight do |state, cmd|\n    event :flght_booked\n  end\n  \n  reaction :flight_booked do |event|\n    dispatch :book_hotel\n  end\n  \n  command :book_hotel do |state, cmd|\n    event :hotel_booked\n  end\n  \n  # Define event handlers if you haven't...\n  event :booking_started, # ..etc\n  event :flight_booked, # ..etc\nend\n```\n\n### Multi-stream concurrent execution\n\nIn this other example, the same workflow is split into separate streams/actors, so that Flight and Hotel bookings can run concurrently from each other. When completed, they each notify the parent Holiday actor, so the whole process coalesces into a sequential operation again.\n\n\u003cimg width=\"1787\" alt=\"sourced-concurrency-multi-lane\" src=\"https://github.com/user-attachments/assets/444445ff-b837-4c19-8c28-1b47eada7a41\" /\u003e\n\n```ruby\n# An actor dispatches a message to different stream\n# messages for different streams are processed concurrently\nreaction BookingStarted do |state, event|\n  dispatch(BookHotel).to(\"#{event.stream_id}-hotel\")\nend\n```\n\n### Units of work\n\n\u003cimg width=\"1249\" height=\"652\" alt=\"CleanShot 2025-11-15 at 14 38 05\" src=\"https://github.com/user-attachments/assets/a3631dd5-08b9-4381-8082-ce5cdc8958ed\" /\u003e\n\nThe diagram shows the units of work in an example Sourced workflow. The operations within each of the red boxes are protected by a combination of transactions and locking strategies on the consumer group + stream ID, so they are isolated from other concurrent processing. They can be said to be **immediately consistent**. \nThe data-flow _between_ these boxes is propagated asynchronously by Sourced's infrastructure so, relative to each other, the entire system is **eventually consistent**.\n\nThese transactional boundaries are guarded by the same locks that enforce the concurrency model, so that for example the same message can't be processed twice by the same Reactor (workflow, projector, etc). \n\n## Durable workflows\n\nThere's a `Sourced::DurableWorkflow` class that can be subclassed to define Reactors with a synchronous-looking API. This is *work in progress*.\n\n```ruby\nclass BookHoliday \u003c Sourced::DurableWorkflow\n  # This method can be called like a regular method\n  # The methods inside also have blocking semantics\n  # but they're in fact event-sourced, and will be\n  # retried on failure until the booking completes.\n  # Methods that were succesful will be idempotent on retry\n  def execute(flight_info, hotel_info)\n    flight = book_flight(flight_info)\n    hotel = book_hotel(hotel_info)\n    confirm_booking(flight, hotel)\n  end\n  \n  # The .durable macro turns a regular method\n  # into an event-sourced workflow\n  durable def book_flight(info)\n    FlightsAPI.book(info)\n  end\n  \n  durable def book_hotel(info)\n    HotelsAPI.book(info)\n  end\n  \n  durable def confirm_booking(flight, hotel)\n    # etc,\n  end\nend\n```\n\nThese executions will be handed off to the runtime to be run by one or more workers, while preserving ordering. You can optionally wait for a result.\n\n```ruby\nresult = BookHoliday.execute(flight_info, hotel_info).wait.output\n# Confirmed booking, or whatever error result your code returns\n```\n\nEvents for the full execution are recorded to the backend.\n\u003cimg width=\"1016\" height=\"1298\" alt=\"CleanShot 2025-11-13 at 13 48 27@2x\" src=\"https://github.com/user-attachments/assets/a591a1a4-88e6-435e-bb27-cb4990aaf91f\" /\u003e\n\nDurable workflows must be registered with the runtime, like any other Reactor.\n\n```ruby\nSourced.register BookHoliday\n```\n\n## Handler DSL\n\nThe `Sourced::Handler` mixin provides a lighter-weight DSL for simple reactors.\n\n```ruby\nclass OrderTelemetry\n  include Sourced::Handler\n  \n  # Handle these Order events\n  # and log them\n  on Order::Started do |event|\n    Logger.info ['order started', event.stream_id]\n    []\n  end\n  \n  on Order::Placed do |event|\n    Logger.info ['order placed', event.stream_id]\n    []\n  end\nend\n\n# Register it\nSourced.register OrderTelemetry\n```\n\nHandlers can optionally define the `:history` argument. The runtime will provide the full message history for the stream ID being handled.\n\n```ruby\non Order::Placed do |event, history:|\n  total = history\n    .filter { |e| Order::ProductAdded === e }\n    .reduce(0) { |n, e| n + e.payload.price }\n  \n  if total \u003e 10000\n    return [Order::AddDiscount.build(event.stream_id, amount: 100)]\n  end\n  \n  []\nend\n```\n\nIt also supports multiple event types, for generic handling.\n\n```ruby\non Order::Placed, Order::Complete do |event|\n  Logger.info \"received event #{event.inspect}\"\n  []\nend\n```\n\n## Command methods for Actors\n\nThe optional `Sourced::CommandMethods` mixin allows invoking an Actor's commands as regular methods.\n\n`CommandMethods` automatically generates instance methods from command definitions,\nallowing you to invoke commands in two ways:\n\n1. **In-memory version** (e.g., `actor.start(name: 'Joe')`)\n   - Validates the command and executes the decision handler\n   - Returns a tuple of [cmd, new_events]\n   - Does NOT persist events to backend\n2. **Durable version** (e.g., `actor.start!(name: 'Joe')`)\n   - Same as in-memory, but also appends events to backend\n   - Raises `FailedToAppendMessagesError` if backend fails\n\nInclude the module in an Actor and define commands normally:\n\n```ruby\nclass MyActor \u003c Sourced::Actor\n  include Sourced::CommandMethods\n\n  command :create_item, name: String do |state, cmd|\n    event :item_created, cmd.payload\n  end\nend\n\nactor = MyActor.new(id: 'actor-123')\ncmd, events = actor.create_item(name: 'Widget')  # In-memory\ncmd, events = actor.create_item!(name: 'Widget') # Persists to backend\n```\n\n\n\n## Orchestration and choreography\n\n### Orchestration\n\nOrchestration is when the flow control of a multi-collaborator workflow is centralised into a single entity. This can be achieved by having one Actor coordinate the communication by reacting to events and sending commands to other actors.\n\n```ruby\nclass HolidayBooking \u003c Sourced::Actor\n  state do |id|\n    BookingState.new(id)\n  end\n  \n  command StartBooking do |booking, cmd|\n    # validations, etc\n    event BookingStarted, cmd.payload\n  end\n  \n  event BookingStarted\n  \n  # React to BookingStarted and start sub-workflows\n  reaction BookingStarted do |booking, event|\n    dispatch(HotelBooking::Start)\n  end\n  \n  # React to events emitted by sub-workflows\n  reaction HotelBooking::Started do |booking, event|\n    dispatch(ConfirmHotelBooking, event.payload)\n  end\n  \n  command ConfirmHotelBooking do |booking, cmd|\n    unless booking.hotel.booked?\n      event HotelBookingConfirmed, cmd.payload\n    end\n  end\n  \n  event HotelBookingConfirmed do |booking, event|\n    # update booking state\n    booking.confirm_hotel(event.payload)\n  end\nend\n```\n\nThis is a verbose step-by-step choreography, but it can be made more succint by ommiting the mirroring of commands/events, if needed (or by using the [Reactor Interface](#the-reactor-interface) directly).\n\n*TODO*: a way for Actors to initialise their internal state with event attributes other than the `stream_id`. For example, events may carry a `booking_id` for the overall workflow.\n\n### Choreography\n\nChoreography is when each component reacts to other components' events without centralised control. The overall workflow \"emerges\" from this collaboration.\n\n```ruby\nclass HotelBooking \u003c Sourced::Actor\n  # The HotelBooking defines its own\n  # reactions to booking events\n  reaction HolidayBooking::StartBooking do |state, event|\n    # dispatch a command to itself to start its own life-cycle\n    dispatch Start, event.payload\n  end\n  \n  command Start do |state, cmd|\n    # validations, etc\n    # other Actors in the choreography\n    # can choose to react to events emitted here\n    event Started, cmd.payload\n  end\n  \n  event Started do |state, event|\n    # update state, etc\n  end\nend\n```\n\n## Appending and reading messages\n\n### Appending messages without optimistic locking\n\nUse `Backend#append_next_to_stream` to append messages to a stream, with no questions asked.\n\n```ruby\nmessage = ProductAdded.build('order-123', product_id: 123, price: 100)\nSourced.config.backend.append_next_to_stream('order-123', [message])\n\n# Shortcut:\nSourced.dispatch(message)\n```\n\n### Appending messages with optimistic locking\n\nUsing `Backend#append_to_stream`, the backend expects the new messages `seq` property (sequence number) to be greater than the last message in storage for the same stream. This is to catch concurrent writes where a different client or thread may append to the stream while your code was preparing for it.\n\n```ruby\n# Your code must make sure to increment sequence numbers\npast_events = Sourced.config.backend.read_stream('order-123')\nlast_known_seq = past_events.last\u0026.seq # ex. 10\n# Instantiate new messages and make sure to increment their sequences\nmessage = ProductAdded.new(\n  stream_id: 'order-123', \n  seq: last_known_seq + 1, # \u003c== incremented sequence\n  payload: { product_id: 123, price: 100 }\n)\n\n# This will raise an exception if there's already a message\n# for this stream with this sequence number in storage.\nSourced.backend.append_to_stream('order-123', [message])\n```\n\n`Sourced::Actor` classes do this incrementing automatically when they produce new messages.\n\n### Scheduling messages in the future\n\nYou can append messages to a separate log, with a schedule time. Sourced workers will periodically poll this log and move these messages into the main log at the right time.\n\n```ruby\nmessage = ProductAdded.build('order-123', product_id: 123, price: 100)\nSourced.config.backend.schedule_messages([message], at: Time.now + 20)\n```\n\nActor reactions can use the `#dispatch` and `#at` helpers to schedule commands to run at a future time.\n\n```ruby\nreaction ProductAdded do |order, event|\n  dispatch(NotifyNewProduct).at(Time.now + 20)\nend\n```\n\n## Replaying messages\n\nYou can use the backend API to reset offsets for a specific consumer group, which will cause workers to start replaying messages for that group.\n\n```ruby\nSourced.config.backend.reset_consumer_group(ReadyOrder)\n```\n\nSee [below](#stopping-and-starting-consumer-groups) for other consumer lifecycle methods.\t\n\n## The Reactor Interface\n\nAll built-in Reactors (Actors, Projections) build on the low-level Reactor Interface\n\n```ruby\nclass MyReactor\n  extend Sourced::Consumer\n  \n  # The runtime will poll and hand over messages of this type\n  # to this class' .handle() method\n  def self.handled_messages = [Order::Started, Order::Placed]\n  \n  # The runtime invokes this method when it finds a new message\n  # of type present in the list above\n  def self.handle(new_message)\n    # Process message here.\n    # This method can return an Array or one or more of the following\n    actions = []\n    \n    # Just aknowledge new_message\n    actions \u003c\u003c Sourced::Actions::OK\n    \n    # Append these new messages to the event store\n    # Sourced will automatically increment the stream's sequence number\n    # (ie. no optimistic locking)\n    started = Order::Started.build(new_message.stream_id)\n    actions \u003c\u003c Sourced::Actions::AppendNext.new([started])\n    \n    # Append these new messages to the event store.\n    # The messages are expected to have a :seq incremented after new_message.seq\n    # Messages will fail to append if other messages have been appended\n    # with overlapping sequence numbers (optimistic locking)\n    started = Order::Started.new(stream_id: new_message.stream_id, seq: new_message.seq + 1)\n    actions \u003c\u003c Sourced::Actions::AppendAfter.new(new_message.stream_id, [started])\n    \n    # Tell the runtime to retry this message\n    # This is a low-level action and Sourced already uses it when handling exceptions\n    # and retries\n    actions \u003c\u003c Sourded::Actions::RETRY\n    \n    actions\n  end\nend\n```\n\nYou can implement your own low-level reactors following the interface above. Then register them as normal.\n\n```ruby\nSourced.register MyReactor\n```\n\n### Reactors that require message history\n\nReactors that declare the `:history` argument will also be provided the full message history for the stream being handled.\n\nThis is how event-sourced Actors are implemented.\n\n```ruby\ndef self.handle(new_message, history:)\n  # evolve state from history,\n  # handle command, return new events, etc\n  []\nend\n```\n\n### `:replaying` flag.\n\nYour `.handle` method can also declare a `:replaying` boolean, which tells the reactor whether the stream is replaying events, or handling new messages. Reactors use this to run or omit side-effects (for example, replaying Projectors don't run `reaction` blocks).\n\n```ruby\ndef self.handle(new_message, history:, replaying:)\n  if replaying\n    # Omit side-effects\n  else\n    # Trigger side-effects\n  end\nend\n```\n\n## Testing\n\nThere's a couple of experimental RSpec helpers that allow testing Sourced reactors in GIVEN, WHEN, THEN style.\n\n*GIVEN* existing events A, B, C\nWHEN new command D is sent\nTHEN I expect new events E and F\n\n### Single reactor\n\nUse `with_reactor` to unit-test the life-cycle of a single reactor.\n\n```ruby\nrequire 'sourced/testing/rspec'\n\nRSpec.describe Order do\n  include Sourced::Testing::RSpec\n\n  it 'adds product to order' do\n    with_reactor(Order, 'order-123')\n      .when(Order::AddProduct, product_id: 1, price: 100)\n      .then(Order::ProductAdded.build('order-123', product_id: 1, price: 100))\n  end\n\n  it 'is a noop if product already in order' do\n    with_reactor(Order, 'order-123')\n      .given(Order::ProductAdded, product_id: 1, price: 100)\n      .when(Order::AddProduct, product_id: 1, price: 100)\n      .then([])\n  end\nend\n```\n\n`#then` can also take a block, which will be given the low level `Sourced::Actions` objects returned by your `.handle()` interface.\n\nYou can use this block to test reactors that trigger side effects.\n\n```ruby\nwith_reactor(Webhooks, 'webhook-1')\n  .when(Webooks::Dispatch, name: 'Joe')\n  .then do |actions|\n    expect(api_request).to have_been_requested\n  end\n```\n\nYou can mix argument and block assertions with `.then()`\n\n```ruby\nwith_reactor(Webhooks, 'webhook-1')\n  .when(Webooks::Dispatch, name: 'Joe')\n  .then do |_|\n    expect(api_request).to have_been_requested\n  end\n  .then(Webhooks::Dispatched, reference: 'webhook-abc')\n```\n\nFor reactors that have `sync` blocks for side-effects (ex. Projectors), use `#then!` to trigger those side-effects and assert their results.\n\n```ruby\nwith_reactor(PlacedOrders, 'order-123')\n  .given(Order::Started)\n  .given(Order::ProductAdded, product_id: 1, price: 100, units: 2)\n  .given(Order::Placed)\n  .then! do |_|\n    expect(OrderRecord.find('order-123').total).to eq(200)\n  end\n```\n\n### Multiple reactors (A.K.A \"Sagas\")\n\nUse `with_reactors` to test the collaboration of multiple reactors sending and picking up eachother's messages.\n\n```ruby\nit 'tests collaboration of reactors' do\n  order_stream = 'actor-1'\n  payment_stream = 'actor-1-payment'\n  telemetry_stream = Testing::Telemetry::STREAM_ID\n\n  # With these reactors\n  with_reactors(Order, Payment, Telemetry)\n    # GIVEN that these events exist in history\n    .given(Order::Started.build(order_stream, name: 'foo'))\n    # WHEN I dispatch this new command\n    .when(Order::StartPayment.build(order_stream))\n    # Then I expect\n    .then do |stage|\n      # The different reactors collaborated and\n      # left this message trail behind\n      # Backend#messages is only available in the TestBackend\n      expect(stage.backend.messages).to match_sourced_messages([\n        Order::Started.build(order_stream, name: 'foo'), \n        Order::StartPayment.build(order_stream), \n        Order::PaymentStarted.build(order_stream), \n        Telemetry::Logged.build(telemetry_stream, source_stream: order_stream),\n        Payment::Process.build(payment_stream), \n        Payment::Processed.build(payment_stream),\n        Telemetry::Logged.build(telemetry_stream, source_stream: payment_stream),\n      ])\n    end\nend\n```\n\n`with_reactors` sets up its own in-memory backend, so you can test multi-reactor workflows in terms of what messages they produce without database or network requests, and there's no need for database setup or tear-down. Just test the behaviour!\n\nThe `.then` block can take an optional second argument, which will be passed as only the _new_ messages produced by the reactors, appended after any messages setup with `given`.\n\n```ruby\n.then do |stage, new_messages|\n  expect(new_messages).to match_sourced_messages([...])\nend\n```\n\n\n\n## Setup\n\nYou'll need the `pg` and `sequel` gems.\n\n```ruby\ngem 'sourced', github: 'ismasan/sourced'\ngem 'pg'\ngem 'sequel'\n```\n\nCreate a Postgres database.\nFor now Sourced uses the Sequel gem. In future there'll be an ActiveRecord adapter with migrations support.\n\nConfigure and migrate the database.\n\n```ruby\nSourced.configure do |config|\n  config.backend = Sequel.connect(ENV.fetch('DATABASE_URL'))\n\n  # Worker and housekeeping options (shown with defaults)\n  config.worker_count = 2                       # Number of worker fibers\n  config.housekeeping_count = 1                 # Number of housekeeper fibers\n  config.housekeeping_interval = 3              # Seconds between scheduling cycles\n  config.housekeeping_heartbeat_interval = 5    # Seconds between worker heartbeats\n  config.housekeeping_claim_ttl_seconds = 120   # Seconds before stale claims are reaped\nend\n\nSourced.config.backend.install unless Sourced.config.backend.installed?\n```\n\nThese options are used by both `Sourced::Supervisor` and the Falcon integration. When running workers alongside a web server (Falcon, or any other Async-compatible server), these control how many worker and housekeeper fibers are spawned per OS process.\n\n### Generating Sequel migrations\n\nIf your app already uses Sequel's migrator, you can copy Sourced's migration into your migrations directory instead of using `backend.install`.\n\n```ruby\nbackend = Sourced.config.backend\nbackend.copy_migration_to(\"db/migrations\")\n# =\u003e writes db/migrations/001_create_sourced_tables.rb\n```\n\nOr use a block to control the file name (e.g. timestamped migrations):\n\n```ruby\nbackend.copy_migration_to do\n  \"db/migrations/#{Time.now.strftime('%Y%m%d%H%M%S')}_create_sourced_tables.rb\"\nend\n```\n\nThe generated file is a standard `Sequel.migration { change { ... } }` that works with `Sequel::Migrator`. It respects the `prefix` and `schema` options passed when configuring the backend:\n\n```ruby\nSourced.configure do |config|\n  db = Sequel.connect(ENV.fetch('DATABASE_URL'))\n  config.backend = Sourced::Backends::SequelBackend.new(db, prefix: 'myapp', schema: 'events')\nend\n\n# Migration will create tables like events.myapp_messages, events.myapp_streams, etc.\nSourced.config.backend.copy_migration_to(\"db/migrations\")\n```\n\nRegister your Actors and Reactors.\n\n```ruby\nSourced.register(Leads::Actor)\nSourced.register(Leads::Listings)\nSourced.register(Webooks::Dispatcher)\n```\n\n### Running workers as a separate process\n\nWhen using a web server that doesn't share Sourced's Async event loop (e.g. Puma), or in non-web applications, run workers as a standalone process using `Sourced::Supervisor`:\n\n```ruby\n# worker.rb\nrequire_relative 'config/environment'\n# start workers with 10 worker fibers or threads per OS process\n# depending on Sourced.config.executor (:async, :thread, or custom)\nSourced::Supervisor.start(count: 10)\n```\n\nThis requires managing two processes in deployment: one for your web server, one for workers.\n\n### Running workers with Falcon\n\nIf you use [Falcon](https://github.com/socketry/falcon) as your web server, you can run Sourced workers in the same process. Both Falcon and Sourced use the [Async](https://github.com/socketry/async) gem, so workers run as lightweight fibers alongside web requests — no separate worker process needed.\n\nThis requires `Sourced.config.executor = :async` (the default). Do not change it to `:thread` when using Falcon, as workers must run as fibers to share Falcon's event loop.\n\nAdd a `./falcon.rb` file to the root of your app, which requieres `sourced/falcon`  (no hard dependency on Falcon in sourced.gemspec):\n\n```ruby\n# falcon.rb\n#!/usr/bin/env falcon-host\nrequire 'bundler/setup'\nrequire 'sourced/falcon'\nrequire_relative 'config/environment' # \u003c= YOUR app setup, Sourced.configure, register reactors, etc.\n\nservice \"my-app\" do\n  include Sourced::Falcon::Environment\n  include Falcon::Environment::Rackup    # loads config.ru\n\n  # -- Falcon / Async options --\n  url \"http://[::]:9292\"                 # Server bind URL (default: \"http://[::]:9292\")\n  count 2                                # Number of OS processes to fork (default: Etc.nprocessors)\n  timeout 30                             # Connection timeout in seconds (default: nil)\n  verbose false                          # Enable verbose logging (default: false)\n  cache true                             # Enable HTTP response caching (default: false)\n\n  # Sourced worker options default to Sourced.config values.\n  # Override per-service if needed:\n  # sourced_worker_count 4\n  # sourced_housekeeping_count 1\n  # sourced_housekeeping_interval 3\n  # sourced_housekeeping_heartbeat_interval 5\n  # sourced_housekeeping_claim_ttl_seconds 120\nend\n```\n\nRun with:\n\n```\nfalcon host\n```\n\nTotal Sourced workers = `count * sourced_worker_count`. For example, `count 2` and `sourced_worker_count 4` gives 8 worker fibers across 2 OS processes, all competing for events via database locks (same as running multiple Supervisors).\n\nSet `config.worker_count = 0` to run Falcon as a web-only process with no Sourced workers. This is useful if you want to run workers separately via `Sourced::Supervisor` while still using Falcon for HTTP, or if you explicitely don't want workers adding unnecessary pressure on the database.\n\nOn shutdown (`Ctrl-C` / `SIGTERM`), Falcon signals workers to stop. Their poll loops exit gracefully with no stale claims.\n\n## Custom attribute types and coercions.\n\nDefine a module to hold your attribute types using [Plumb](https://github.com/ismasan/plumb)\n\n```ruby\nmodule Types\n  include Plumb::Types\n  \n  # Your own types here.\n  CorporateEmail = Email[/@apple\\.com^/]\nend\n```\n\nThen you can use any [built-in Plumb types](https://github.com/ismasan/plumb?tab=readme-ov-file#built-in-types), as well as your own, when defining command or event structs (or any other data structures for your app).\n\n```ruby\nUpdateEmail = Sourced::Command.define('accounts.update_email') do\n  attribute :email, Types::CorporateEmail\nend\n```\n\n## Error handling\n\nSourced workflows are eventually-consistent by default. This means that commands and events are handled in background processes, and any exceptions raised can't be immediatly surfaced back to the user (and, there might not be a user anyway!).\n\nMost \"domain errors\" in command handlers should be handled by the developer and recorded as domain events, so that the domain can react and/or compensate for them.\n\nTo handle true _exceptions_ (code or data bugs, network or IO exceptions) Sourced provides a default error strategy that will \"stop\" the affected consumer group (the Postgres backend will log the exception and offending message in the `consumer_groups` table).\n\nYou can configure the error strategy with retries and exponential backoff, as well as `on_retry` and `on_stop` callbacks.\n\n```ruby\nSourced.configure do |config|\n  # config.backend = Sequel.connect(ENV.fetch('DATABASE_URL'))\n  config.error_strategy do |s|\n    s.retry(\n      # Retry up to 3 times\n      times: 3,\n      # Wait 5 seconds before retrying\n      after: 5, \n      # Custom backoff: given after=5, retries in 5, 10 and 15 seconds before stopping\n      backoff: -\u003e(retry_after, retry_count) { retry_after * retry_count }\n    )\n    \n    # Trigger this callback on each retry\n    s.on_retry do |n, exception, message, later|\n      LOGGER.info(\"Retrying #{n} times\")\n    end\n\n    # Finally, trigger this callback\n    # after all retries have failed and the consumer group is stopped.\n    s.on_stop do |exception, message|\n      Sentry.capture_exception(exception)\n    end\n  end\nend\n```\n\n### Custom error strategy\n\nYou can also configure your own error strategy. It must respond to `#call(exception, message, group)`\n\n```ruby\nCUSTOM_STRATEGY = proc do |exception, message, group|\n  case exception\n  when Faraday::Error\n    group.retry(Time.now + 10)\n  else\n    group.stop(exception)\n  end\nend\n\nSourced.configure do |config|\n  # Configure backend, etc\n  config.error_strategy = CUSTOM_STRATEGY\nend\n```\n\n## Stopping and starting consumer groups.\n\n`Sourced.config.backend` provides an API for stopping and starting consumer groups. For example to resume groups that were stopped by raised exceptions, after the error has been corrected.\n\n```ruby\nSourced.config.backend.stop_consumer_group('Carts::Listings')\nSourced.config.backend.start_consumer_group('Carts::Listings')\n```\n\n## Rails integration\n\nSoon.\n\n## Sourced vs. ActiveJob\n\nActiveJob is a great way to handle background jobs in Rails. It's simple and easy to use. However, it's not designed for event sourcing.\nActiveJob backends (and other job queues) are optimised for parallel processing of jobs, this means that multiple jobs for the same business entity may be processed in parallel without any ordering guarantees.\n\n\u003cimg width=\"832\" height=\"493\" alt=\"sourced-job-queue-diagram\" src=\"https://github.com/user-attachments/assets/c51b03be-8794-4954-968a-87ecdd97d2f7\" /\u003e\n\nSourced's concurrency model is designed to process events for the same entity in order, while allowing for parallel processing of events for different entities.\n\n\u003cimg width=\"802\" height=\"552\" alt=\"sourced-ordered-streams-diagram\" src=\"https://github.com/user-attachments/assets/ddfbff4b-11bb-4e0c-93e9-e0851c4721d9\" /\u003e\n\n## Gotchas\n\nCurrently `Sourced` is focused on eventual consistency and background processing\nof commands and events through background workers. Eventually a synchronous mode\nwill be added for simpler use-cases.\n\nThis can be confusing if you expect your reactions to run automatically and\nsynchronously when you issue commands.\n\nThis can be a gotcha if you're using the `Sourced::CommandMethods` mixin which\npersists events but does not call your reactor right away. If you need to you\nshould explicitly call `#react` after issuing commands.\n\n```ruby\n```\n```ruby\nchat = Sourced.load(Chat, 'chat-123')\n# Would persist but not call reactions\n_cmd, events = chat.send_message!(content: query)\n# Have to react manually\ncommands = chat.react(events)\n# now dispatch these commands again?\n```\n\n\n## Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n    $ bundle add sourced\n\n**Note**: this gem is under active development, so you probably want to install from Github:\nIn your Gemfile:\n\n    $ gem 'sourced', github: 'ismasan/sourced'\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/ismasan/sourced.\t\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fismasan%2Fsourced","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fismasan%2Fsourced","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fismasan%2Fsourced/lists"}