Elixir ATProtocol ingestion and sync library.

chore: update dependencies

ovyerus.com 107ccc42 21c04ab7

verified
+269 -9
+260
AGENTS.md
··· 1 + # AGENTS.md 2 + 3 + This file provides guidance for agentic coding assistants working with the 4 + Drinkup codebase. 5 + 6 + ## Project Overview 7 + 8 + Drinkup is an Elixir library for consuming events from an ATProtocol relay 9 + (firehose/`com.atproto.sync.subscribeRepos`). It uses OTP principles with 10 + GenStatem for managing WebSocket connections and Task.Supervisor for concurrent 11 + event processing. 12 + 13 + ## Build, Lint, and Test Commands 14 + 15 + ### Running Tests 16 + 17 + ```bash 18 + # Run all tests 19 + mix test 20 + 21 + # Run a single test file 22 + mix test test/drinkup_test.exs 23 + 24 + # Run a specific test by line number 25 + mix test test/drinkup_test.exs:5 26 + 27 + # Run tests with coverage 28 + mix test --cover 29 + 30 + # Run tests matching a pattern 31 + mix test --only [tag_name] 32 + ``` 33 + 34 + ### Formatting and Linting 35 + 36 + ```bash 37 + # Format code (uses .formatter.exs config) 38 + mix format 39 + 40 + # Check if code is formatted 41 + mix format --check-formatted 42 + 43 + # Run Credo for static code analysis 44 + mix credo 45 + 46 + # Run Credo strictly 47 + mix credo --strict 48 + ``` 49 + 50 + ### Compilation and Documentation 51 + 52 + ```bash 53 + # Compile the project 54 + mix compile 55 + 56 + # Clean build artifacts 57 + mix clean 58 + 59 + # Generate documentation 60 + mix docs 61 + 62 + # Run Dialyzer for type checking (if configured) 63 + mix dialyzer 64 + ``` 65 + 66 + ## Code Style Guidelines 67 + 68 + ### Module Structure 69 + 70 + - Use `defmodule` with clear, descriptive names following `Drinkup.<Component>` 71 + namespace 72 + - Place module documentation (`@moduledoc`) immediately after `defmodule` 73 + - Group related functionality within nested modules (e.g., 74 + `Drinkup.Event.Commit.RepoOp`) 75 + - Order module contents: module attributes, types, public functions, private 76 + functions 77 + 78 + ### Imports and Aliases 79 + 80 + - Use `require` for macros (e.g., `require Logger`) 81 + - Use `alias` to shorten module names, prefer explicit aliases over `import` 82 + - Group in order: `require`, `alias`, `import` 83 + - Example: 84 + ```elixir 85 + require Logger 86 + alias Drinkup.{Event, Options} 87 + ``` 88 + 89 + ### Type Specifications 90 + 91 + - Use TypedStruct for structs with typed fields (dependency: 92 + `{:typedstruct, "~> 0.5"}`) 93 + - Define `@type` specs for complex types, unions, and public APIs 94 + - Use `@spec` for all public functions 95 + - Use `enforce: true` for required TypedStruct fields 96 + - Example: 97 + 98 + ```elixir 99 + use TypedStruct 100 + 101 + typedstruct enforce: true do 102 + field :consumer, module() 103 + field :name, atom(), default: Drinkup 104 + field :cursor, pos_integer() | nil, enforce: false 105 + end 106 + ``` 107 + 108 + ### Naming Conventions 109 + 110 + - Modules: PascalCase (`Drinkup.Event.Commit`) 111 + - Functions: snake_case (`handle_event/1`, `from/1`) 112 + - Variables: snake_case (`repo_op`, `last_seq`) 113 + - Private functions: prefix with `defp`, mark with `@spec` if complex 114 + - Atoms: lowercase with underscores (`:ok`, `:connect_timeout`) 115 + - Behaviours: use `@behaviour` (not `@behavior`) 116 + 117 + ### Function Definitions 118 + 119 + - Pattern match in function heads when possible 120 + - Use guard clauses for simple type/value checks 121 + - Prefer multiple function heads over large case statements 122 + - Example: 123 + ```elixir 124 + def valid_seq?(nil, seq) when is_integer(seq), do: true 125 + def valid_seq?(last_seq, nil) when is_integer(last_seq), do: true 126 + def valid_seq?(last_seq, seq) when is_integer(last_seq) and is_integer(seq), 127 + do: seq > last_seq 128 + def valid_seq?(_last_seq, _seq), do: false 129 + ``` 130 + 131 + ### Error Handling 132 + 133 + - Use `try/rescue` for expected errors, catch and log appropriately 134 + - Use Logger for errors: 135 + `Logger.error("Message: #{Exception.format(:error, e, __STACKTRACE__)}")` 136 + - Return tagged tuples: `{:ok, result}` or `{:error, reason}` 137 + - Use `with` for chaining operations that may fail 138 + - Example from Socket module: 139 + ```elixir 140 + with {:ok, header, next} <- CAR.DagCbor.decode(frame), 141 + {:ok, payload, _} <- CAR.DagCbor.decode(next), 142 + {%{"op" => @op_regular}, _} <- {header, payload} do 143 + # happy path 144 + else 145 + {:error, reason} -> Logger.warning("Failed to decode: #{inspect(reason)}") 146 + end 147 + ``` 148 + 149 + ### OTP and Concurrency Patterns 150 + 151 + - Use `child_spec/1` for custom supervisor specifications 152 + - Prefer `GenServer` for stateful processes, `:gen_statem` for state machines 153 + - Use `Task.Supervisor` for concurrent, fire-and-forget work (see 154 + `Event.dispatch/2`) 155 + - Register processes via Registry for named lookups 156 + - Define proper restart strategies (`:permanent`, `:transient`, `:temporary`) 157 + 158 + ### Comments 159 + 160 + - Avoid obvious comments; prefer self-documenting code 161 + - Use `# TODO:` for future improvements (see existing TODOs in codebase) 162 + - Use `# DEPRECATED` for deprecated fields (see Commit struct) 163 + - Document complex algorithms or non-obvious business logic 164 + - Use module-level `@moduledoc` and function-level `@doc` for public APIs 165 + 166 + ### Formatting 167 + 168 + - Use `mix format` (configured in `.formatter.exs`) 169 + - Import deps for formatting: `import_deps: [:typedstruct]` 170 + - Line length: default Elixir formatter settings 171 + - Use 2-space indentation (enforced by formatter) 172 + 173 + ### Testing 174 + 175 + - Use ExUnit for tests (files in `test/` with `_test.exs` suffix) 176 + - Use `use ExUnit.Case` in test modules 177 + - Use `doctest Module` for testing documentation examples 178 + - Tag tests for selective running: `@tag :integration` 179 + - Use descriptive test names: `test "validates sequence numbers correctly"` 180 + 181 + ## Project-Specific Patterns 182 + 183 + ### Consumer Behaviour Pattern 184 + 185 + - Implement `@behaviour Drinkup.Consumer` with `handle_event/1` callback 186 + - Use pattern matching to handle different event types 187 + - Return any value; errors are caught by Task.Supervisor wrapper 188 + 189 + ### RecordConsumer Macro Pattern 190 + 191 + - Use `use Drinkup.RecordConsumer` with `collections:` opt for filtering 192 + - Override `handle_create/1`, `handle_update/1`, `handle_delete/1` as needed 193 + - Collections can be exact strings or Regex patterns: `~r/app\.bsky\.graph\..+/` 194 + 195 + ### WebSocket State Machine 196 + 197 + - Socket module uses `:gen_statem` with states: `:disconnected`, 198 + `:connecting_http`, `:connecting_ws`, `:connected` 199 + - State functions match on events: `state_name(:enter, from, data)` or 200 + `state_name(:info, msg, data)` 201 + - Use `{:next_event, :internal, event}` for internal state transitions 202 + 203 + ## Dependencies 204 + 205 + - `{:gun, "~> 2.2"}` - HTTP/WebSocket client 206 + - `{:car, "~> 0.1.0"}` - CAR (Content Addressable aRchive) format 207 + - `{:cbor, "~> 1.0.0"}` - CBOR encoding/decoding 208 + - `{:typedstruct, "~> 0.5"}` - Typed structs 209 + - `{:credo, "~> 1.7"}` - Static analysis (dev/test only) 210 + 211 + ## Common Tasks 212 + 213 + ### Adding a New Event Type 214 + 215 + 1. Create `lib/event/your_event.ex` with TypedStruct definition 216 + 2. Add `from/1` function to parse payload 217 + 3. Add pattern match in `Drinkup.Event.from/2` 218 + 4. Add to `@type t()` union in `Drinkup.Event` 219 + 5. Update `CHANGELOG.md` under `[Unreleased]` section with the new feature 220 + 221 + ### Debugging Connection Issues 222 + 223 + - Check `:gun` connection logs in Socket module 224 + - Verify sequence tracking with `Event.valid_seq?/2` 225 + - Monitor state transitions: `:disconnected` → `:connecting_http` → 226 + `:connecting_ws` → `:connected` 227 + 228 + ## Changelog Management 229 + 230 + **IMPORTANT**: After completing any feature or fixing a bug from a previous 231 + release, you MUST update `CHANGELOG.md`. 232 + 233 + ### Changelog Format 234 + 235 + - Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format 236 + - Uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 237 + - Group changes under appropriate sections: `Added`, `Changed`, `Deprecated`, 238 + `Removed`, `Fixed`, `Security` 239 + 240 + ### When to Update 241 + 242 + - **New features**: Add under `## [Unreleased]` → `### Added` 243 + - **Bug fixes**: Add under `## [Unreleased]` → `### Fixed` 244 + - **Breaking changes**: Add under `## [Unreleased]` → `### Breaking Changes` 245 + - **Deprecations**: Add under `## [Unreleased]` → `### Deprecated` 246 + - **Security fixes**: Add under `## [Unreleased]` → `### Security` 247 + 248 + ### Example Entry 249 + 250 + ```markdown 251 + ## [Unreleased] 252 + 253 + ### Added 254 + 255 + - Support for `#handle` event type in firehose consumer 256 + 257 + ### Fixed 258 + 259 + - Sequence validation now correctly handles nil cursor on initial connection 260 + ```
+3 -3
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1748026106, 6 - "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=", 5 + "lastModified": 1767640445, 6 + "narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=", 7 7 "owner": "nixos", 8 8 "repo": "nixpkgs", 9 - "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c", 9 + "rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5", 10 10 "type": "github" 11 11 }, 12 12 "original": {
+6 -6
mix.lock
··· 2 2 "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 3 "car": {:hex, :car, "0.1.1", "a5bc4c5c1be96eab437634b3c0ccad1fe17b5e3d68c22a4031241ae1345aebd4", [:mix], [{:cbor, "~> 1.0.0", [hex: :cbor, repo: "hexpm", optional: false]}, {:typedstruct, "~> 0.5", [hex: :typedstruct, repo: "hexpm", optional: false]}, {:varint, "~> 1.4", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "f895dda8123d04dd336db5a2bf0d0b47f4559cd5383f83fcca0700c1b45bfb6a"}, 4 4 "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, 5 - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, 6 - "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, 7 - "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 5 + "certifi": {:hex, :certifi, "2.16.0", "a4edfc1d2da3424d478a3271133bf28e0ec5e6fd8c009aab5a4ae980cb165ce9", [:rebar3], [], "hexpm", "8a64f6669d85e9cc0e5086fcf29a5b13de57a13efa23d3582874b9a19303f184"}, 6 + "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, 7 + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, 8 8 "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 9 - "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 10 - "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 9 + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, 10 + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 11 11 "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, 12 12 "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 13 13 "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 14 "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 15 15 "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 16 16 "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 17 - "typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"}, 17 + "typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"}, 18 18 "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, 19 19 }