Skip to main content

loom-elixir

Elixir language expertise for idiomatic, production-quality code.

Quellinformationen

Repository
cosmix/loom
Letzte Quellaktivität
25. September 2026 um 06:00
Erkannte Sprache von SKILL.md
Englisch
Sterne
61
Forks
4

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
loom-elixir
description
Elixir language expertise for idiomatic, production-quality code.
triggers
["elixir","ex","exs","mix","hex","otp","erlang","beam","genserver","supervisor","phoenix","liveview","ecto","exunit","credo","dialyzer","oban","plug","mox"]
# Elixir Language Expertise ## Overview Idiomatic, production-grade Elixir on the BEAM: mix, pattern matching, processes and OTP (GenServer, supervisors), Phoenix and Ecto, and ExUnit. Assumes competence; the value is in what is easy to get wrong (atom exhaustion, a single GenServer serializing the hot path, unbounded mailboxes, compile-time config baked into releases, LiveView's double mount, Ecto's absent lazy loading) and in writing tests that loom's `mix-test` adapter can select one at a time. ## Tooling **mix** builds, tests and manages dependencies; Hex is the package registry. | Task | Command | | --- | --- | | New project | `mix new app --sup` (with a supervision tree), `mix phx.new app` (Phoenix) | | Add a dependency | add it to `deps/0` in `mix.exs` (mix has no `add` command), then `mix deps.get` to resolve and write `mix.lock` | | Update one dependency | `mix deps.update jason` | | Remove unused lock entries | `mix deps.unlock --unused`; CI check: `mix deps.unlock --check-unused` | | Outdated / retired packages | `mix hex.outdated`, `mix hex.audit`; `mix deps.audit` with `mix_audit` for CVEs | | Compile strictly | `mix compile --warnings-as-errors` | | Find recompilation chains | `mix xref graph --label compile-connected` | | Format | `mix format` (config in `.formatter.exs`); CI: `mix format --check-formatted` | | Lint | `mix credo --strict` | | Types | `mix dialyzer` (dialyxir); recent compilers also emit gradual type-check warnings | | Security (Phoenix) | `mix sobelow` | Commit `mix.lock`. Dependencies with the `only: [:dev, :test]` and `runtime: false` options stay out of releases. **The gate:** ```bash mix format --check-formatted mix compile --warnings-as-errors mix credo --strict mix test --warnings-as-errors mix dialyzer # when the project uses dialyxir ``` ## Pattern Matching and Control Flow - `=` matches; it binds on success and raises `MatchError` on failure. Pin an existing value with `^`: `^expected = actual`. - Prefer multiple function clauses with guards over `if`/`cond` chains inside one body; clause order is match order. - Return tagged tuples (`{:ok, value}`, `{:error, reason}`) from functions that can fail; the bang variant (`File.read!`) raises instead. Offer both when callers need both. - `with` chains the happy path. Without `else`, the first non-matching value is returned as is. With `else`, every non-matching shape must have a clause, or `with` raises `WithClauseError`. - Map patterns match a subset of keys: `%{} = value` matches any map, so test emptiness with `map_size(m) == 0`. A struct pattern `%User{}` also asserts the struct type. - Binary patterns parse protocols and file headers without manual slicing. ```elixir defmodule Spool.Writer do @spec write(Path.t(), String.t(), iodata()) :: :ok | {:error, :symlink | File.posix()} def write(root, job_id, payload) when is_binary(job_id) do with {:ok, %File.Stat{type: :directory}} <- File.lstat(root), :ok <- File.write(Path.join(root, job_id), payload, [:exclusive]) do :ok else {:ok, %File.Stat{type: :symlink}} -> {:error, :symlink} {:ok, %File.Stat{}} -> {:error, :enotdir} {:error, reason} -> {:error, reason} end end end <<magic::binary-size(4), version::unsigned-big-16, rest::binary>> = header ``` **Atoms are never garbage-collected** and the atom table has a fixed limit (1,048,576 by default); filling it crashes the VM. Never call `String.to_atom/1` or `:erlang.binary_to_term/1` on external input. Use `String.to_existing_atom/1` or an explicit map from strings to atoms. **Structs and data:** `@enforce_keys` for required fields; `Keyword.validate!/2` for option lists; `get_in`/`put_in`/`update_in` for nested updates; `Map.update!/3` when the key must exist. ## Processes and OTP - Processes share nothing and communicate by message; each is cheap (a few KB). Every message is copied into the receiver's heap, except binaries over 64 bytes, which are reference-counted. - **GenServer** state lives in one process, so every call is serialized. Use a GenServer for state, concurrency control or lifecycle, and plain modules for code organization. A single GenServer on the hot path becomes the bottleneck; serve read-heavy shared state from ETS (`read_concurrency: true`) or `:persistent_term` (rarely changed: every update triggers a global GC pass). - Do slow initialization in `handle_continue/2` so `start_link` returns and the supervisor is not blocked. - `GenServer.call` times out after 5_000 ms by default and exits the caller. `cast` has no backpressure: a fast producer grows the receiver's mailbox without bound. Prefer `call` when the sender can outpace the receiver. - Defining any `handle_info/2` clause replaces the default, which logs unexpected messages; add a catch-all clause, or a stray message crashes the process with `FunctionClauseError`. - `terminate/2` runs on shutdown only if the process traps exits (`Process.flag(:trap_exit, true)`); never rely on it for durable cleanup. - Links propagate crashes; monitors deliver a `:DOWN` message. Link what should die together; monitor what you only observe. - Name dynamic processes through `Registry` (`{:via, Registry, {Spool.Registry, key}}`); never generate atoms for names. ```elixir defmodule Spool.Queue do use GenServer require Logger def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__) def push(job), do: GenServer.call(__MODULE__, {:push, job}) def depth, do: GenServer.call(__MODULE__, :depth) @impl true def init(opts), do: {:ok, %{jobs: :queue.new(), dir: Keyword.fetch!(opts, :dir)}, {:continue, :load}} @impl true def handle_continue(:load, state), do: {:noreply, %{state | jobs: load_pending(state.dir)}} @impl true def handle_call({:push, job}, _from, state), do: {:reply, :ok, %{state | jobs: :queue.in(job, state.jobs)}} def handle_call(:depth, _from, state), do: {:reply, :queue.len(state.jobs), state} @impl true def handle_info(msg, state) do Logger.warning("unexpected message: #{inspect(msg)}") {:noreply, state} end end ``` **Supervisors.** Strategies: `:one_for_one` (restart the failed child), `:rest_for_one` (restart it and every child started after it), `:one_for_all`. Children start in list order and stop in reverse. The default restart intensity is 3 restarts in 5 seconds; past it the supervisor exits and its parent decides. Use `restart: :transient` for children that may finish normally, `DynamicSupervisor` for children started on demand. ```elixir children = [ {Registry, keys: :unique, name: Spool.Registry}, {Task.Supervisor, name: Spool.TaskSupervisor}, {Spool.Queue, dir: Application.fetch_env!(:spool, :dir)} ] Supervisor.start_link(children, strategy: :rest_for_one, name: Spool.Supervisor) ``` **Tasks.** `Task.async_stream(items, &fetch/1, max_concurrency: 8, timeout: 10_000)` bounds concurrency; `Task.Supervisor.async_nolink/2` runs work that may crash without taking the caller down. A bare `Task.async` links to the caller, so its crash kills the caller too. ## Phoenix Essentials - **Contexts** (`Accounts`, `Billing`) own their schemas and `Repo` calls; controllers and LiveViews call contexts, never `Repo` directly. - **Plug:** `conn` is immutable; each plug returns a new one. A plug that sends a response must `halt/1`, or later plugs keep running. - **LiveView** `mount/3` runs twice: once for the disconnected HTTP render, once over the WebSocket. Subscribe to PubSub and start timers only when `connected?(socket)`. Use `stream/3` for large collections so the socket does not hold them, and `assign_async/3` or `start_async/3` for slow loads. - **Verified routes** (`~p"/orders/#{order}"`) are checked at compile time. - **Config:** `config/config.exs` and `config/prod.exs` are evaluated at build time and baked into the release; read secrets and environment in `config/runtime.exs` with `System.fetch_env!/1`. Inside modules, `Application.compile_env/3` for build-time values (mix tracks them), `Application.get_env/3` at runtime. - **Security:** HEEx escapes output; `raw/1` bypasses it. Forms carry CSRF tokens by default; keep `:protect_from_forgery` and `:put_secure_browser_headers` in the browser pipeline. ```elixir def mount(_params, _session, socket) do if connected?(socket), do: Phoenix.PubSub.subscribe(Shop.PubSub, "orders") {:ok, stream(socket, :orders, Orders.list_recent())} end def handle_info({:order_created, order}, socket) do {:noreply, stream_insert(socket, :orders, order, at: 0)} end ``` ## Ecto Essentials - **Changesets:** the field list passed to `cast/4` is the mass-assignment allowlist. `unique_constraint/2` turns the database's unique-index violation into a changeset error and needs that index to exist. - **No lazy loading:** an association that was not preloaded is `%Ecto.Association.NotLoaded{}`. Preload in the query or with `Repo.preload/2`; preloading inside a loop is N+1. - **Queries** compose, and `^` interpolates a bound parameter, so pinned values cannot inject SQL. `fragment/1` takes `?` placeholders and a literal string. - **Transactions:** `Repo.transaction/2` or `Ecto.Multi` for named steps whose failure you want to inspect. - **Bulk operations:** `Repo.insert_all/3` and `update_all/3` skip changesets, validations and automatic timestamps. - **Migrations:** `create index(..., concurrently: true)` needs `@disable_ddl_transaction true` and `@disable_migration_lock true`. ```elixir def changeset(user, attrs) do user |> cast(attrs, [:email, :name]) # role is never cast from user input |> validate_required([:email]) |> update_change(:email, &String.downcase/1) |> unique_constraint(:email) end Order |> where([o], o.customer_id == ^customer_id) |> preload(:lines) |> Repo.all() ``` ## Background Jobs and Releases - **Oban** stores jobs in Postgres. Insert the job in the same transaction as the data it processes (`Ecto.Multi` plus `Oban.insert/3`), so a rollback never leaves an orphan job. - `perform/1` returns `:ok`, `{:ok, value}`, `{:error, reason}` (retried up to `max_attempts`), `{:cancel, reason}` or `{:snooze, seconds}`. A raise also counts as a failed attempt. - Job args round-trip through JSON: a worker receives string keys (`%{"order_id" => id}`), so match on strings, and pass ids instead of structs. - Workers must be idempotent: every failure is retried. Add `unique: [period: 60]` where duplicates are possible. - Test with `Oban.Testing` (`perform_job/2`, `assert_enqueued/1`) and `testing: :manual` in `config/test.exs`. ```elixir defmodule Shop.Workers.SendConfirmation do use Oban.Worker, queue: :mailers, max_attempts: 5 @impl Oban.Worker def perform(%Oban.Job{args: %{"order_id" => order_id}}) do with {:ok, order} <- Shop.Orders.fetch(order_id) do Shop.Mailer.deliver_confirmation(order) end end end ``` **Releases:** `MIX_ENV=prod mix release` builds a self-contained release; `config/runtime.exs` runs at every boot. Run migrations from a release module (`bin/shop eval "Shop.Release.migrate()"`), since mix is absent in a release. ## Testing ```elixir # test/spool/writer_test.exs defmodule Spool.WriterTest do use ExUnit.Case, async: true @moduletag :tmp_dir describe "write/3" do test "writes the payload into the spool directory", %{tmp_dir: dir} do assert :ok = Spool.Writer.write(dir, "job-1", "payload") assert File.read!(Path.join(dir, "job-1")) == "payload" end test "rejects a symlinked spool directory", %{tmp_dir: dir} do target = Path.join(dir, "target") link = Path.join(dir, "spool") File.mkdir_p!(target) File.ln_s!(target, link) assert {:error, :symlink} = Spool.Writer.write(link, "job-1", "payload") refute File.exists?(Path.join(target, "job-1")) end end end ``` - `test/test_helper.exs` calls `ExUnit.start()`; test files end in `_test.exs`. - `async: true` runs the module concurrently with other async modules. Leave it off when tests touch global state: application env, named processes, fixed file-system paths, `:persistent_term`. - `setup` returns a map or keyword list merged into the context; `setup_all` runs once per module; `on_exit/1` registers cleanup. The `:tmp_dir` tag gives each test a fresh directory in the context. - `start_supervised!/1` starts a process under the test supervisor and stops it when the test ends; prefer it over `start_link` in tests. - `assert_receive msg, timeout` (default 100 ms) and `refute_receive` replace `Process.sleep` for asynchronous assertions. - `assert_raise/2`, `capture_log/1`, `capture_io/1`; `doctest Spool.Writer` runs the `iex>` examples in `@doc`. - **Mox:** mocks only behaviours. `Mox.defmock(Spool.StoreMock, for: Spool.Store)`, `expect/4` in the test, `setup :verify_on_exit!`, and `Mox.allow/3` when another process calls the mock.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen