diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8cdc90..4fd5585 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,6 +69,11 @@ jobs: with: node-version: 24 + - name: Install just + run: | + sudo apt-get update + sudo apt-get install -y just + # Cache deps/ directory — keyed on mix.lock - name: Cache Mix deps uses: actions/cache@v4 @@ -115,7 +120,8 @@ jobs: - name: Run Node Sync E2E tests if: ${{ matrix.main }} - run: mix test.node_sync_e2e + run: just e2e node-sync - name: Run Marmot E2E tests - run: mix test.marmot_e2e + if: ${{ matrix.main }} + run: just e2e marmot diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d7f1f2d..dbe2fe4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -68,6 +68,11 @@ jobs: with: node-version: 24 + - name: Install just + run: | + sudo apt-get update + sudo apt-get install -y just + - name: Cache Mix deps uses: actions/cache@v4 id: deps-cache @@ -113,10 +118,10 @@ jobs: run: mix test --color - name: Run Node Sync E2E - run: mix test.node_sync_e2e + run: just e2e node-sync - name: Run Marmot E2E - run: mix test.marmot_e2e + run: just e2e marmot - name: Check for unused locked deps run: | diff --git a/README.md b/README.md index 9dbc2bb..b9ef301 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ Parrhesia is a Nostr relay server written in Elixir/OTP. +**BETA CONDITION – BREAKING CHANGES MAY STILL HAPPEN!** + Supported storage backends: - PostgreSQL, which is the primary and production-oriented backend - in-memory storage, which is useful for tests, local experiments, and benchmarks -**BETA CONDITION – BREAKING CHANGES MAY STILL HAPPEN!** +Advanced Nostr features: - Advanced Querying: Full-text search (NIP-50) and COUNT queries (NIP-45). - Secure Messaging: First-class support for Marmot MLS-encrypted groups and NIP-17/44/59 gift-wrapped DMs. diff --git a/justfile b/justfile index 7f06cd9..08f3dcc 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,4 @@ set shell := ["bash", "-euo", "pipefail", "-c"] -set script-interpreter := ["bash", "-euo", "pipefail"] repo_root := justfile_directory() @@ -12,8 +11,9 @@ help topic="": @cd "{{repo_root}}" && ./scripts/just_help.sh "{{topic}}" # Raw e2e harness commands. -[script] -e2e subcommand="help" *args: +e2e subcommand *args: + #!/usr/bin/env bash + set -euo pipefail cd "{{repo_root}}" subcommand="{{subcommand}}" @@ -40,8 +40,9 @@ e2e subcommand="help" *args: fi # Benchmark flows (local/cloud/history + direct relay targets). -[script] -bench subcommand="help" *args: +bench subcommand *args: + #!/usr/bin/env bash + set -euo pipefail cd "{{repo_root}}" subcommand="{{subcommand}}" diff --git a/lib/parrhesia/storage/adapters/memory/store.ex b/lib/parrhesia/storage/adapters/memory/store.ex index 1a74d90..625a15b 100644 --- a/lib/parrhesia/storage/adapters/memory/store.ex +++ b/lib/parrhesia/storage/adapters/memory/store.ex @@ -22,7 +22,17 @@ defmodule Parrhesia.Storage.Adapters.Memory.Store do audit_logs: [] } - def ensure_started, do: start_store() + def start_link(opts \\ []) do + name = Keyword.get(opts, :name, @name) + Agent.start_link(&init_state/0, name: name) + end + + def ensure_started do + case Process.whereis(@name) do + pid when is_pid(pid) -> :ok + nil -> start_store() + end + end def put_event(event_id, event) when is_binary(event_id) and is_map(event) do :ok = ensure_started() @@ -159,28 +169,72 @@ defmodule Parrhesia.Storage.Adapters.Memory.Store do defp normalize_reduce_result(next_acc), do: next_acc def get(fun) do - :ok = ensure_started() - Agent.get(@name, fun) + with_store(fn pid -> Agent.get(pid, fun) end) end def update(fun) do - :ok = ensure_started() - Agent.update(@name, fun) + with_store(fn pid -> Agent.update(pid, fun) end) end def get_and_update(fun) do - :ok = ensure_started() - Agent.get_and_update(@name, fun) + with_store(fn pid -> Agent.get_and_update(pid, fun) end) end defp start_store do - case Agent.start_link(&init_state/0, name: @name) do - {:ok, _pid} -> :ok - {:error, {:already_started, _pid}} -> :ok - {:error, reason} -> {:error, reason} + case start_link() do + {:ok, _pid} -> + :ok + + {:error, {:already_started, pid}} -> + if Process.alive?(pid) do + :ok + else + wait_for_store_exit(pid) + end + + {:error, reason} -> + {:error, reason} end end + defp with_store(fun, attempts \\ 2) + + defp with_store(fun, attempts) when attempts > 0 do + :ok = ensure_started() + + case Process.whereis(@name) do + pid when is_pid(pid) -> + try do + fun.(pid) + catch + :exit, reason -> + if noproc_exit?(reason) and attempts > 1 do + with_store(fun, attempts - 1) + else + exit(reason) + end + end + + nil -> + with_store(fun, attempts - 1) + end + end + + defp with_store(_fun, 0), do: exit(:noproc) + + defp wait_for_store_exit(pid) do + ref = Process.monitor(pid) + + receive do + {:DOWN, ^ref, :process, ^pid, _reason} -> start_store() + after + 100 -> start_store() + end + end + + defp noproc_exit?({:noproc, _details}), do: true + defp noproc_exit?(_reason), do: false + defp init_state do ensure_tables_started() diff --git a/lib/parrhesia/storage/supervisor.ex b/lib/parrhesia/storage/supervisor.ex index 3e73332..adf4423 100644 --- a/lib/parrhesia/storage/supervisor.ex +++ b/lib/parrhesia/storage/supervisor.ex @@ -13,11 +13,19 @@ defmodule Parrhesia.Storage.Supervisor do @impl true def init(_init_arg) do - children = moderation_cache_children() ++ PostgresRepos.started_repos() + children = + memory_store_children() ++ moderation_cache_children() ++ PostgresRepos.started_repos() Supervisor.init(children, strategy: :one_for_one) end + defp memory_store_children do + case Application.get_env(:parrhesia, :storage, [])[:backend] do + :memory -> [Parrhesia.Storage.Adapters.Memory.Store] + _other -> [] + end + end + defp moderation_cache_children do if PostgresRepos.postgres_enabled?() and Application.get_env(:parrhesia, :moderation_cache_enabled, true) do diff --git a/test/parrhesia/storage/adapters/memory/adapter_test.exs b/test/parrhesia/storage/adapters/memory/adapter_test.exs index 27bbce7..e238ed8 100644 --- a/test/parrhesia/storage/adapters/memory/adapter_test.exs +++ b/test/parrhesia/storage/adapters/memory/adapter_test.exs @@ -6,6 +6,12 @@ defmodule Parrhesia.Storage.Adapters.Memory.AdapterTest do alias Parrhesia.Storage.Adapters.Memory.Events alias Parrhesia.Storage.Adapters.Memory.Groups alias Parrhesia.Storage.Adapters.Memory.Moderation + alias Parrhesia.Storage.Adapters.Memory.Store + + setup do + start_supervised!(Store) + :ok + end test "memory adapter supports basic behavior contract operations" do event_id = String.duplicate("a", 64)