Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Key starting points:

## Current Status

Phase 8 governance work is complete, but the repo is still closing feature gaps in earlier phases. The current implementation is strongest in resource storage, compilation, runtime wiring, observability, and governance, while real Ash-backed binding execution and full external renderer integration remain open in reopened Phase 1, 3, 4, 5, and 7 workstreams.
Phase 8 governance work is complete, and the runtime stack now includes real Ash-backed binding execution, authorization, and LiveView reactivity. The main remaining open workstreams are the legacy resource DSL items in Phase 1 and the optional external renderer package integration tracked in Phase 7.

## Development Notes

Expand Down
4 changes: 2 additions & 2 deletions guides/user/UG-0003-data-binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ context = %{user_id: "user-1", params: %{}, assigns: %{}}
AshUI.Runtime.BidirectionalBinding.write_binding(binding, "Updated Name", socket, context)
```

The current implementation returns mock update results, but the call shape is the one LiveView integration already uses.
Successful writes now return the real Ash update result metadata, including the updated record and resolved field value.

## Event Handling in LiveView

Expand Down Expand Up @@ -190,7 +190,7 @@ Your `source` is not a map in the expected shape.

### Empty or placeholder values

The runtime currently resolves resource data through placeholder loaders in some paths. Verify the binding shape first before assuming the renderer is broken.
The runtime now resolves binding data through real Ash reads. If you see empty values, check authorization, record identifiers, and transformation defaults before assuming the renderer is broken.

### Writes fail with forbidden errors

Expand Down
1 change: 1 addition & 0 deletions lib/ash_ui/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule AshUI.Application do
AshUI.Authorization.Runtime.init_cache()

children = [
{Phoenix.PubSub, name: AshUI.PubSub},
AshUI.Telemetry,
AshUI.Repo,
AshUI.Rendering.Registry
Expand Down
19 changes: 11 additions & 8 deletions lib/ash_ui/liveview/update_integration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule AshUI.LiveView.UpdateIntegration do
require Logger

alias AshUI.LiveView.Integration
alias AshUI.Notifications
alias AshUI.Runtime.BindingEvaluator
alias AshUI.Runtime.ResourceAccess

Expand Down Expand Up @@ -105,7 +106,8 @@ defmodule AshUI.LiveView.UpdateIntegration do

This should be called from LiveView's `handle_info/2` callback.
"""
@spec handle_resource_change(map(), Phoenix.LiveView.Socket.t()) :: update_result()
@spec handle_resource_change(map() | Ash.Notifier.Notification.t(), Phoenix.LiveView.Socket.t()) ::
update_result()
def handle_resource_change(notification, socket) do
bindings = socket.assigns[:ash_ui_bindings] || %{}

Expand Down Expand Up @@ -136,7 +138,11 @@ defmodule AshUI.LiveView.UpdateIntegration do
@doc """
Handles subscription messages from Ash.Notifier.
"""
@spec handle_notification(tuple(), Phoenix.LiveView.Socket.t()) :: update_result()
@spec handle_notification(term(), Phoenix.LiveView.Socket.t()) :: update_result()
def handle_notification(%Ash.Notifier.Notification{} = notification, socket) do
handle_resource_change(notification, socket)
end

def handle_notification({:created, resource}, socket) do
handle_resource_change(%{type: :created, resource: resource, timestamp: DateTime.utc_now()}, socket)
end
Expand Down Expand Up @@ -220,13 +226,9 @@ defmodule AshUI.LiveView.UpdateIntegration do
"#{inspect(resource)}_#{action}_#{:erlang.phash2(filter)}"
end

defp subscribe_to_resource(_resource, _subscription) do
# Ash.Notifier integration is still an external dependency.
# We track subscriptions per LiveView session so the reactivity pipeline
# behaves correctly once real notifications are delivered.
:ok
end
defp subscribe_to_resource(resource, _subscription), do: Notifications.subscribe(resource)

defp unsubscribe_from_resource(%{resource: resource}), do: Notifications.unsubscribe(resource)
defp unsubscribe_from_resource(_subscription), do: :ok

defp binding_resources(bindings, socket) do
Expand Down Expand Up @@ -449,6 +451,7 @@ defmodule AshUI.LiveView.UpdateIntegration do
end)
end

defp get_notification_resource(%Ash.Notifier.Notification{resource: resource}), do: resource
defp get_notification_resource(%{resource: %{__struct__: resource}}), do: resource
defp get_notification_resource(%{resource: resource}) when is_atom(resource), do: resource
defp get_notification_resource(_), do: nil
Expand Down
66 changes: 66 additions & 0 deletions lib/ash_ui/notifications.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule AshUI.Notifications do
@moduledoc """
PubSub bridge for Ash resource notifications used by LiveView reactivity.

Resources publish Ash notifications through `Ash.Notifier.PubSub` to the
local `AshUI.PubSub` server, and runtime integrations subscribe to the
per-resource change topics exposed here.
"""

@doc """
Broadcasts a notification payload on the given topic and event.
"""
@spec broadcast(String.t(), String.t(), term()) :: :ok | {:error, term()}
def broadcast(topic, _event, payload) do
Phoenix.PubSub.broadcast(AshUI.PubSub, topic, payload)
end

@doc """
Subscribes the current process to change notifications for a resource.
"""
@spec subscribe(module()) :: :ok
def subscribe(resource) do
case resource_topic(resource) do
{:ok, topic} -> Phoenix.PubSub.subscribe(AshUI.PubSub, topic)
{:error, _reason} -> :ok
end
end

@doc """
Unsubscribes the current process from change notifications for a resource.
"""
@spec unsubscribe(module()) :: :ok
def unsubscribe(resource) do
case resource_topic(resource) do
{:ok, topic} -> Phoenix.PubSub.unsubscribe(AshUI.PubSub, topic)
{:error, _reason} -> :ok
end
end

@doc """
Returns the resource change topic for an Ash resource module.
"""
@spec resource_topic(module()) :: {:ok, String.t()} | {:error, :unsupported_resource}
def resource_topic(resource) when is_atom(resource) do
resource
|> resource_path()
|> case do
nil -> {:error, :unsupported_resource}
path -> {:ok, "ash_ui:resource:#{path}:changes"}
end
end

def resource_topic(_resource), do: {:error, :unsupported_resource}

defp resource_path(resource) do
resource
|> Atom.to_string()
|> String.trim_leading("Elixir.")
|> case do
"" -> nil
name -> String.replace(name, ".", ":")
end
rescue
_ -> nil
end
end
12 changes: 12 additions & 0 deletions lib/ash_ui/resources/binding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ defmodule AshUI.Resources.Binding do
Bindings connect UI elements to Ash resource data.
"""

@resource_topic_prefix "ash_ui:resource:AshUI:Resources:Binding"

use Ash.Resource,
domain: AshUI.Domain,
authorizers: [Ash.Policy.Authorizer],
notifiers: [Ash.Notifier.PubSub],
data_layer: AshPostgres.DataLayer

postgres do
table "ui_bindings"
repo AshUI.Repo
end

pub_sub do
module AshUI.Notifications
prefix @resource_topic_prefix

publish :create, "changes"
publish :update, "changes"
publish :destroy, "changes"
end

attributes do
uuid_primary_key :id
attribute :source, :map, allow_nil?: false, default: %{}
Expand Down
12 changes: 12 additions & 0 deletions lib/ash_ui/resources/element.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ defmodule AshUI.Resources.Element do
Elements are atomic UI components (widgets) like buttons, inputs, text, etc.
"""

@resource_topic_prefix "ash_ui:resource:AshUI:Resources:Element"

use Ash.Resource,
domain: AshUI.Domain,
authorizers: [Ash.Policy.Authorizer],
notifiers: [Ash.Notifier.PubSub],
data_layer: AshPostgres.DataLayer

postgres do
table "ui_elements"
repo AshUI.Repo
end

pub_sub do
module AshUI.Notifications
prefix @resource_topic_prefix

publish :create, "changes"
publish :update, "changes"
publish :destroy, "changes"
end

attributes do
uuid_primary_key :id
attribute :type, :atom, allow_nil?: false
Expand Down
12 changes: 12 additions & 0 deletions lib/ash_ui/resources/screen.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@ defmodule AshUI.Resources.Screen do
Ash Resource for storing unified-ui screen definitions.
"""

@resource_topic_prefix "ash_ui:resource:AshUI:Resources:Screen"

use Ash.Resource,
domain: AshUI.Domain,
authorizers: [Ash.Policy.Authorizer],
notifiers: [Ash.Notifier.PubSub],
data_layer: AshPostgres.DataLayer

postgres do
table "ui_screens"
repo AshUI.Repo
end

pub_sub do
module AshUI.Notifications
prefix @resource_topic_prefix

publish :create, "changes"
publish :update, "changes"
publish :destroy, "changes"
end

attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false
Expand Down
2 changes: 1 addition & 1 deletion specs/contracts/binding_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Bindings MUST emit telemetry events for evaluation, update, and error flows.

## Implementation Note

The repository currently exposes the binding APIs and telemetry surface described here, but some read, write, list, and action paths are still backed by placeholder implementations. This contract describes the intended real Ash-backed behavior that the reopened Phase 3 and Phase 4 work will complete.
The repository now exposes the binding APIs and telemetry surface described here through real Ash-backed read, write, list, and action paths. This contract describes the behavior that the runtime and LiveView integration enforce today.

## Traceability

Expand Down
2 changes: 1 addition & 1 deletion specs/planning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ The plan aligns to:

## Status Note

The phase files are historical planning documents, not a guarantee that every checked item is production-backed today. After the RFC-0002 re-baseline, some earlier phases have been reopened to reflect remaining gaps around resource-level authorization, real Ash-backed binding execution, and external renderer integration.
The phase files are historical planning documents, not a guarantee that every checked item is production-backed today. After the RFC-0002 re-baseline, the remaining open implementation gaps are concentrated in the legacy resource DSL work from Phase 1 and the optional external renderer packages tracked in Phase 7.
46 changes: 23 additions & 23 deletions specs/planning/phase-03-data-binding-and-signal-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ Back to index: [README](./README.md)
- Bidirectional bindings support read and write operations
- Action bindings trigger Ash actions on UI events

[ ] 3 Phase 3 - Data Binding and Signal Mapping
[X] 3 Phase 3 - Data Binding and Signal Mapping
Implement reactive data binding from Ash resources to UI elements through unified-ui signal format.

Status note: the runtime APIs, data structures, and much of the surrounding test coverage exist, but several source-resolution, write, list, and action paths are still backed by placeholders instead of real Ash calls.
Status note: runtime bindings now resolve reads, writes, list loading, and actions through real Ash-backed helpers with authorization-aware access and end-to-end coverage.

[X] 3.1 Section - Binding Evaluation
Implement runtime evaluation of bindings against Ash resource data.
Expand All @@ -31,13 +31,13 @@ Back to index: [README](./README.md)
[X] 3.1.1.3 Subtask - Return `{:ok, value}` or `{:error, reason}`
[X] 3.1.1.4 Subtask - Cache evaluated values for performance

[ ] 3.1.2 Task - Implement source resolution against real Ash resources
[X] 3.1.2 Task - Implement source resolution against real Ash resources
Resolve structured binding sources to Ash resource attributes and relationships.

[X] 3.1.2.1 Subtask - Parse structured binding sources
[ ] 3.1.2.2 Subtask - Load resource using real Ash reads with proper authorization
[ ] 3.1.2.3 Subtask - Extract attribute value from loaded resource
[ ] 3.1.2.4 Subtask - Handle relationship traversal against loaded data
[X] 3.1.2.2 Subtask - Load resource using real Ash reads with proper authorization
[X] 3.1.2.3 Subtask - Extract attribute value from loaded resource
[X] 3.1.2.4 Subtask - Handle relationship traversal against loaded data

[X] 3.1.3 Task - Implement transformation application
Apply transformation rules to resolved values.
Expand All @@ -47,7 +47,7 @@ Back to index: [README](./README.md)
[X] 3.1.3.3 Subtask - Apply `default` transformations when source is nil
[X] 3.1.3.4 Subtask - Apply `validate` transformations and return errors

[ ] 3.2 Section - Bidirectional Value Bindings
[X] 3.2 Section - Bidirectional Value Bindings
Implement two-way data binding for `:value` type bindings.

[X] 3.2.1 Task - Implement read direction
Expand All @@ -58,51 +58,51 @@ Back to index: [README](./README.md)
[X] 3.2.1.3 Subtask - Update LiveView assigns on value change
[X] 3.2.1.4 Subtask - Handle loading and error states

[ ] 3.2.2 Task - Implement write direction
[X] 3.2.2 Task - Implement write direction
Flow data from UI elements to Ash resources.

[X] 3.2.2.1 Subtask - Capture user input events from LiveView
[X] 3.2.2.2 Subtask - Validate input data before writing
[ ] 3.2.2.3 Subtask - Call real Ash update actions with new value
[ ] 3.2.2.4 Subtask - Handle update errors and display to user
[X] 3.2.2.3 Subtask - Call real Ash update actions with new value
[X] 3.2.2.4 Subtask - Handle update errors and display to user

[ ] 3.2.3 Task - Implement conflict resolution
[X] 3.2.3 Task - Implement conflict resolution
Handle concurrent updates to shared data.

[X] 3.2.3.1 Subtask - Detect stale data with optimistic locking
[X] 3.2.3.2 Subtask - Retry on conflict with backoff
[X] 3.2.3.3 Subtask - Present conflict UI to user for resolution
[X] 3.2.3.4 Subtask - Emit conflict telemetry events

[ ] 3.3 Section - List Bindings
[X] 3.3 Section - List Bindings
Implement collection binding for `:list` type bindings.

[ ] 3.3.1 Task - Implement collection loading
[X] 3.3.1 Task - Implement collection loading
Load and bind collections of resources to UI elements.

[X] 3.3.1.1 Subtask - Resolve collection source path
[ ] 3.3.1.2 Subtask - Use real Ash reads to load collection
[ ] 3.3.1.3 Subtask - Apply pagination and filtering
[X] 3.3.1.2 Subtask - Use real Ash reads to load collection
[X] 3.3.1.3 Subtask - Apply pagination and filtering
[X] 3.3.1.4 Subtask - Handle empty collections

[ ] 3.3.2 Task - Implement collection reactivity
[X] 3.3.2 Task - Implement collection reactivity
Update UI when collection data changes.

[X] 3.3.2.1 Subtask - Subscribe to collection changes
[X] 3.3.2.2 Subtask - Re-render list on collection modification
[X] 3.3.2.3 Subtask - Handle insert, update, delete operations
[X] 3.3.2.4 Subtask - Maintain scroll position during updates

[ ] 3.4 Section - Action Bindings
[X] 3.4 Section - Action Bindings
Implement event-driven binding for `:action` type bindings.

[ ] 3.4.1 Task - Implement action execution
[X] 3.4.1 Task - Implement action execution
Execute Ash actions in response to UI events.

[X] 3.4.1.1 Subtask - Parse action source
[ ] 3.4.1.2 Subtask - Call real Ash actions with event data
[ ] 3.4.1.3 Subtask - Check authorization before execution
[ ] 3.4.1.4 Subtask - Return action result to UI
[X] 3.4.1.2 Subtask - Call real Ash actions with event data
[X] 3.4.1.3 Subtask - Check authorization before execution
[X] 3.4.1.4 Subtask - Return action result to UI

[X] 3.4.2 Task - Implement action event wiring
Connect UI events to action bindings.
Expand All @@ -123,15 +123,15 @@ Back to index: [README](./README.md)
[X] 3.5.1.3 Subtask - Implement signal creation helpers
[X] 3.5.1.4 Subtask - Add signal validation

[ ] 3.5.2 Task - Convert to Jido.Signal format
[X] 3.5.2 Task - Convert to Jido.Signal format
Ensure signals are compatible with unified signal transport.

[X] 3.5.2.1 Subtask - Wrap Ash signals in Jido.Signal structure
[X] 3.5.2.2 Subtask - Use CloudEvents-compatible event format
[X] 3.5.2.3 Subtask - Include required CloudEvents fields (id, source, type)
[X] 3.5.2.4 Subtask - Add signal metadata for tracing

[ ] 3.6 Section - Phase 3 Integration Tests
[X] 3.6 Section - Phase 3 Integration Tests
Validate binding evaluation and reactivity end-to-end.

[X] 3.6.1 Task - Value binding integration scenarios
Expand Down
Loading
Loading