Skip to content

About

No description, website, or topics provided.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

ash_remote

An Elixir client for the ash_typescript RPC protocol. A backend publishes a versioned Ash.Info.Manifest JSON artifact describing its RPC-exposed resources, types, actions, and filter/sort capabilities. mix ash_remote.gen consumes that manifest and generates standalone Ash resources — data parts mirrored, actions as stubs — backed by AshRemote.DataLayer, which speaks /rpc/run and /rpc/validate. Generated resources depend only on ash + ash_remote and compile in any Ash app.

How it works

backend (ash + Ash.Info.Manifest)                    client (ash + ash_remote)
  AshRemote.Server.manifest_json/1 ──► manifest.json ──► mix ash_remote.gen ──► generated resources
  (served at GET /manifest.json)                                                  │
   /rpc/run, /rpc/validate  ◄──────────────── AshRemote.DataLayer ◄───────────────┘

The manifest comes from AshRemote.Server.manifest_json/1 (served by AshRemote.Server.Router at GET /manifest.json), which enriches Ash core's serializer output with the action names, relationship attributes, and validations the generator needs — the raw mix ash.manifest.dump output alone is not sufficient.

AshRemote.Gen translates that manifest into AshCloner.Definitions. AshCloner then creates or updates the client modules, including output paths, missing entities, and drift handling. All cloning and merge behavior lives in the in-repo packages/ash_cloner package.

  • The client's Ash.Query/changeset is encoded into an RPC body (AshRemote.Encode.{Fields,Filter,Sort,Pagination}), sent via AshRemote.Transport (default Req), and the response decoded back into resource structs. Calculation and aggregate loads fold into the field selection; relationship loads currently use Ash's batched follow-up reads (one request per relationship).
  • The server is authoritative: generated action stubs carry no changes/validations; the backend does the real casting, authorization, and persistence.

Usage

Generate resources from a manifest:

mix ash_remote.gen --manifest path/to/manifest.json --namespace MyApp.Remote

Configure the base URL (resolved lazily at call time, so one codebase works per env):

# config/runtime.exs
config :ash_remote, base_url: System.fetch_env!("REMOTE_BASE_URL")

Then call the generated resources like any Ash resource:

MyApp.Remote.Todo
|> Ash.Query.filter(completed == false)
|> Ash.Query.load([:comment_count, :is_overdue, user: [:name]])
|> Ash.read!()

Validations declared on backend resources are mirrored onto the generated client resources when they're expressible as data (builtin validations with literal options, including where-guarded ones) — so forms and changesets fail fast client-side, while the server re-validates every write. Changes and lifecycle hooks are never mirrored; they run only on the server.

Debug the wire traffic by logging every RPC request (URL, resource/action, outcome, duration, request/response bodies):

# config/dev.exs
config :ash_remote, debug_requests: true
[debug] ash_remote: POST http://127.0.0.1:4010/rpc/run TodoServer.Todo.read → ok (4.2ms)
request:  %{"action" => "read", ...}
response: %{"data" => [...], "success" => true}

Realtime

Beyond request/response, ash_remote can replicate server-side Ash notifications to connected clients over Phoenix Channels: when a mutation happens on the server, each subscribed client re-emits an equivalent local %Ash.Notifier.Notification{} on its proxy resource — so client resources can declare ordinary notifiers (Ash.Notifier.PubSub → LiveView, …) and they fire as if the mutation were local.

Model: notifications are invalidation hints; reads are truth. Delivery is at-most-once with no replay — a reconnect gap loses events, and the :resubscribed lifecycle event is the documented "refetch now" signal.

Server

# resource: attach the notifier
use Ash.Resource, notifiers: [AshRemote.Server.Notifier]

# domain rpc block: name the broadcaster and (optionally) gate publications
rpc do
  pub_sub MyAppWeb.Endpoint      # anything exporting broadcast/3
  resource MyApp.Todo do
    expose :create                # exposing publishes by default
    publish :internal_touch       # opt an unexposed action in
    no_publish :create            # opt out — always wins
  end
end

Published set = (exposed ∪ publish) ∖ no_publish (mutation actions only). Mount AshRemote.Server.Socket in your endpoint (socket "/ash_remote/socket", …); channel join defaults to deny — implement authorize_subscription/4.

Client

# generated resource carries `realtime? true`; add it to your supervision tree:
{AshRemote.Realtime, otp_app: :my_app}

AshRemote.Realtime discovers realtime? resources, opens one Slipstream socket per base_url, auto-joins a topic each, and re-emits every pushed change locally (echoes of the client's own writes are suppressed by default). Requires the optional :phoenix/:phoenix_pubsub (server) and :slipstream (client) deps.

Authorization

Two layers, both enforced:

  • RPC and socket connect resolve an actor the same way you already do in Ash — an upstream auth plug sets it via Ash.PlugHelpers (RPC), or the host socket's connect/3 assigns :ash_remote_actor (realtime). Both integrate directly with ash_authentication.
  • Per-record: the actor's read-policy filter is computed once at join and every broadcast is checked against it in-memory before it is pushed (with a single authorized re-read by pk as fallback), so a subscription never reveals a row the actor could not have read (resources without policies skip the check).

To authenticate, pass the actor — a token in the actor's metadata (Ash.read!(RemoteTodo, actor: signed_in_user)) is auto-forwarded as a Bearer header, or set explicit context: %{ash_remote: %{headers: %{…}}}. See example/todo_server + example/todo_client for a full ash_authentication demo (owner-only vs public todos, live).

Composing with ash_multi_datalayer

AshRemote.DataLayer supports pk-based upsert, making it a valid asynchronous replication target for ash_multi_datalayer's LocalOutbox strategy (local-first writes, background flush). AshRemote.MultiDatalayer ships the glue for the inbound direction: ChangeNotifier forwards realtime pushes into the layered resource's orchestrator, and LifecycleGuard turns socket lifecycle events (resubscribe, join denial) into refresh/reconcile signals. Requires the optional :ash_multi_datalayer dep; see the AshRemote.MultiDatalayer moduledoc and example/README.md for a full offline-first demo.

Limitations (v1)

  • Bulk writes default notify?: false → not replicated unless you opt in.
  • Generic (:action) actions and previous-values topics are not published.
  • A notifier's load/2 on a client resource triggers a remote read.

Layout

Path What
lib/ash_remote/transport/ AshRemote.Transport behaviour + Req implementation
lib/ash_remote/realtime.ex, realtime/ client realtime supervisor, Slipstream connection, inbound replication
lib/ash_remote/server/{notifier,socket,channel}.ex server broadcast notifier + Phoenix socket/channel (join + per-record auth)
lib/ash_remote/topics.ex, decoder.ex shared topic naming + wire-record decoding
lib/ash_remote/multi_datalayer.ex, multi_datalayer/ optional glue for ash_multi_datalayer (inbound change notifier + lifecycle guard)
lib/ash_remote/protocol.ex pure request-body build / response parse
lib/ash_remote/error.ex wire errors → Ash.Error.*
lib/ash_remote/encode/ Ash.Query → wire (fields, filter, sort, pagination)
lib/ash_remote/data_layer.ex AshRemote.DataLayer (implements Ash.DataLayer and folds in the remote do ... end section)
lib/ash_remote/resource/section.ex shared remote do ... end Spark DSL section
lib/ash_remote/manifest/ manifest loader + normalized structs
lib/ash_remote/gen/ manifest → resource source
lib/ash_remote/server.ex, server/ server-side RPC core + AshRemote.Server.Router plug (ported from ash_typescript; mount it in a backend)
lib/mix/tasks/ash_remote.gen.ex the Igniter generator task
test/support/backend/ a reference backend that mounts AshRemote.Server.Router

Tests

mix test

The suite includes the full end-to-end path (test/ash_remote/e2e_test.exs): dump a manifest from the reference backend → generate client resources → compile them → drive CRUD + relationship/calculation/aggregate loads over real HTTP against the running backend. See DECISIONS.md for design decisions.

About

No description, website, or topics provided.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages