defmodule ExESDBGater.Repl.Console do @moduledoc """ Interactive test console for ExESDB Gater with comprehensive observer management. """ alias ExESDBGater.Repl alias ExESDBGater.Repl.{Observer, Producer, EventGenerator} alias ExESDBGater.API alias BCUtils.ColorFuncs, as: CF require Logger # Observer registry - stores observer metadata @observer_registry :observer_registry def start do # Initialize observer registry :ets.new(@observer_registry, [:named_table, :public, :set]) IO.puts("\n#{CF.bright_green_on_black()}🚀 ExESDB Interactive Test Console Started!#{CF.reset()}") IO.puts("#{CF.cyan_on_black()}Store: #{Repl.store()}#{CF.reset()}") IO.puts("#{CF.yellow_on_black()}Available streams: #{inspect(EventGenerator.streams())}#{CF.reset()}") main_menu() end defp main_menu do IO.puts("\n#{CF.bright_blue_on_black()}========== ExESDB Interactive Console ==========#{CF.reset()}") IO.puts("#{CF.green_on_black()}📊 Observers:#{CF.reset()}") IO.puts(" 1. Create Observer by Stream") IO.puts(" 2. Create Observer by Event Type") IO.puts(" 3. Create Observer by Event Payload") IO.puts(" 4. List Active Observers") IO.puts(" 5. Monitor Specific Observer") IO.puts(" 6. Stop Observer") IO.puts("\n#{CF.yellow_on_black()}🏭 Producers:#{CF.reset()}") IO.puts(" 7. Start Producers") IO.puts(" 8. Stop Producer") IO.puts(" 9. List Active Producers") IO.puts("\n#{CF.purple_on_black()}📡 Subscriptions:#{CF.reset()}") IO.puts(" 10. Create Subscription") IO.puts(" 11. List Subscriptions") IO.puts("\n#{CF.blue_on_black()}📈 Data Management:#{CF.reset()}") IO.puts(" 12. View Stream Data") IO.puts(" 13. Append Events Manually") IO.puts(" 14. Get Cluster Status") IO.puts("\n#{CF.red_on_black()}đŸšĒ Exit:#{CF.reset()}") IO.puts(" 15. Exit Console") choice = IO.gets("\n#{CF.bright_white_on_black()}👉 Select an option (1-15): #{CF.reset()}") |> String.trim() handle_choice(choice) main_menu() end # Observer Management defp handle_choice("1"), do: create_observer_by_stream() defp handle_choice("2"), do: create_observer_by_event_type() defp handle_choice("3"), do: create_observer_by_event_payload() defp handle_choice("4"), do: list_observers() defp handle_choice("5"), do: monitor_observer() defp handle_choice("6"), do: stop_observer() # Producer Management defp handle_choice("7"), do: start_producers() defp handle_choice("8"), do: stop_producer() defp handle_choice("9"), do: list_producers() # Subscription Management defp handle_choice("10"), do: create_subscription() defp handle_choice("11"), do: list_subscriptions() # Data Management defp handle_choice("12"), do: view_stream_data() defp handle_choice("13"), do: append_events_manually() defp handle_choice("14"), do: get_cluster_status() # Exit defp handle_choice("15"), do: exit_console() defp handle_choice(_), do: IO.puts("#{CF.red_on_black()}❌ Invalid choice. Please try again.#{CF.reset()}") # ========== Observer Functions ========== defp create_observer_by_stream do IO.puts("\n#{CF.bright_green_on_black()}📊 Creating Observer by Stream#{CF.reset()}") IO.puts("Available streams: #{inspect(EventGenerator.streams())}") IO.puts("Special streams: $all (all streams)") stream = IO.gets("Enter stream name (or $all): ") |> String.trim() observer_name = IO.gets("Enter observer name: ") |> String.trim() try do pid = Observer.start( store: Repl.store(), type: :by_stream, selector: stream, name: observer_name ) # Register observer register_observer(observer_name, :by_stream, stream, pid) IO.puts("#{CF.green_on_black()}✅ Observer '#{observer_name}' created for stream '#{stream}'#{CF.reset()}") rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to create observer: #{inspect(e)}#{CF.reset()}") end end defp create_observer_by_event_type do IO.puts("\n#{CF.bright_green_on_black()}📊 Creating Observer by Event Type#{CF.reset()}") IO.puts("Available event types:") EventGenerator.event_types() |> Enum.with_index(1) |> Enum.each(fn {type, i} -> IO.puts(" #{i}. #{type}") end) event_type = IO.gets("Enter event type: ") |> String.trim() observer_name = IO.gets("Enter observer name: ") |> String.trim() try do pid = Observer.start( store: Repl.store(), type: :by_event_type, selector: event_type, name: observer_name ) register_observer(observer_name, :by_event_type, event_type, pid) IO.puts("#{CF.green_on_black()}✅ Observer '#{observer_name}' created for event type '#{event_type}'#{CF.reset()}") rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to create observer: #{inspect(e)}#{CF.reset()}") end end defp create_observer_by_event_payload do IO.puts("\n#{CF.bright_green_on_black()}📊 Creating Observer by Event Payload#{CF.reset()}") IO.puts("Example patterns:") IO.puts(" %{operator: \"John\"} - Events with operator John") IO.puts(" %{temperature: 25} - Events with temperature 25") IO.puts(" %{intensity: _} - Events with any intensity field") observer_name = IO.gets("Enter observer name: ") |> String.trim() IO.puts("Enter payload pattern as Elixir map (e.g., %{operator: \"John\"})") pattern_str = IO.gets("Pattern: ") |> String.trim() try do {pattern, _} = Code.eval_string(pattern_str) pid = Observer.start( store: Repl.store(), type: :by_event_payload, selector: pattern, name: observer_name ) register_observer(observer_name, :by_event_payload, pattern, pid) IO.puts("#{CF.green_on_black()}✅ Observer '#{observer_name}' created for payload pattern #{inspect(pattern)}#{CF.reset()}") rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to create observer: #{inspect(e)}#{CF.reset()}") end end defp list_observers do IO.puts("\n#{CF.bright_blue_on_black()}📋 Active Observers#{CF.reset()}") observers = :ets.tab2list(@observer_registry) if observers == [] do IO.puts("#{CF.yellow_on_black()}âš ī¸ No active observers#{CF.reset()}") else IO.puts("#{CF.cyan_on_black()}#{String.pad_trailing("Name", 20)} #{String.pad_trailing("Type", 20)} #{String.pad_trailing("Selector", 30)} Status#{CF.reset()}") IO.puts(String.duplicate("-", 80)) observers |> Enum.each(fn {name, {type, selector, pid}} -> status = if Process.alive?(pid), do: "#{CF.green_on_black()}đŸŸĸ Running#{CF.reset()}", else: "#{CF.red_on_black()}🔴 Stopped#{CF.reset()}" IO.puts("#{String.pad_trailing(name, 20)} #{String.pad_trailing(to_string(type), 20)} #{String.pad_trailing(inspect(selector), 30)} #{status}") end) end end defp monitor_observer do list_observers() observer_name = IO.gets("\nEnter observer name to monitor: ") |> String.trim() case :ets.lookup(@observer_registry, observer_name) do [] -> IO.puts("#{CF.red_on_black()}❌ Observer '#{observer_name}' not found#{CF.reset()}") [{^observer_name, {type, selector, pid}}] -> if Process.alive?(pid) do IO.puts("\n#{CF.bright_yellow_on_black()}đŸ‘ī¸ Monitoring Observer: #{observer_name}#{CF.reset()}") IO.puts("#{CF.cyan_on_black()}Type: #{type}, Selector: #{inspect(selector)}#{CF.reset()}") IO.puts("#{CF.dim_white_on_black()}Press Ctrl+C to stop monitoring#{CF.reset()}") # Monitor the observer process monitor_process(pid, observer_name) else IO.puts("#{CF.red_on_black()}❌ Observer '#{observer_name}' is not running#{CF.reset()}") end end end defp stop_observer do list_observers() observer_name = IO.gets("\nEnter observer name to stop: ") |> String.trim() case :ets.lookup(@observer_registry, observer_name) do [] -> IO.puts("#{CF.red_on_black()}❌ Observer '#{observer_name}' not found#{CF.reset()}") [{^observer_name, {_type, _selector, pid}}] -> if Process.alive?(pid) do Process.exit(pid, :kill) :ets.delete(@observer_registry, observer_name) IO.puts("#{CF.green_on_black()}✅ Observer '#{observer_name}' stopped#{CF.reset()}") else :ets.delete(@observer_registry, observer_name) IO.puts("#{CF.yellow_on_black()}âš ī¸ Observer '#{observer_name}' was already stopped#{CF.reset()}") end end end # ========== Producer Functions ========== defp start_producers do IO.puts("\n#{CF.bright_green_on_black()}🏭 Starting Producers#{CF.reset()}") count = IO.gets("Number of producers (default 3): ") |> String.trim() count = if count == "", do: 3, else: String.to_integer(count) Repl.start_producers(count) IO.puts("#{CF.green_on_black()}✅ Started #{count} producers#{CF.reset()}") end defp stop_producer do IO.puts("\n#{CF.bright_red_on_black()}🛑 Stopping Producer#{CF.reset()}") stream_id = IO.gets("Enter stream ID to stop producer: ") |> String.trim() try do Repl.stop_producer_for_stream(stream_id) IO.puts("#{CF.green_on_black()}✅ Producer for '#{stream_id}' stopped#{CF.reset()}") rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to stop producer: #{inspect(e)}#{CF.reset()}") end end defp list_producers do IO.puts("\n#{CF.bright_blue_on_black()}📋 Active Producers#{CF.reset()}") # This would require extending the Producer module to track active producers IO.puts("#{CF.yellow_on_black()}â„šī¸ Producer listing requires extension of the Producer module#{CF.reset()}") end # ========== Subscription Functions ========== defp create_subscription do IO.puts("\n#{CF.bright_green_on_black()}📡 Creating Subscription#{CF.reset()}") subscription_name = IO.gets("Enter subscription name: ") |> String.trim() stream = IO.gets("Enter stream name: ") |> String.trim() start_from = IO.gets("Start from version (default 0): ") |> String.trim() start_from = if start_from == "", do: 0, else: String.to_integer(start_from) try do Repl.start_subscriber_for_stream(subscription_name, stream, start_from) IO.puts("#{CF.green_on_black()}✅ Subscription '#{subscription_name}' created#{CF.reset()}") rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to create subscription: #{inspect(e)}#{CF.reset()}") end end defp list_subscriptions do IO.puts("\n#{CF.bright_blue_on_black()}📋 Active Subscriptions#{CF.reset()}") case API.get_subscriptions(Repl.store()) do {:ok, subscriptions} -> if subscriptions == [] do IO.puts("#{CF.yellow_on_black()}âš ī¸ No active subscriptions#{CF.reset()}") else IO.puts("#{CF.cyan_on_black()}Subscriptions:#{CF.reset()}") subscriptions |> Enum.each(fn sub -> IO.puts(" â€ĸ #{inspect(sub, pretty: true)}") end) end {:error, reason} -> IO.puts("#{CF.red_on_black()}❌ Failed to get subscriptions: #{inspect(reason)}#{CF.reset()}") end end # ========== Data Management Functions ========== defp view_stream_data do IO.puts("\n#{CF.bright_blue_on_black()}📈 View Stream Data#{CF.reset()}") case API.get_streams(Repl.store()) do {:ok, streams} -> IO.puts("#{CF.cyan_on_black()}Available streams:#{CF.reset()}") streams |> Enum.with_index(1) |> Enum.each(fn {stream, i} -> IO.puts(" #{i}. #{stream}") end) stream = IO.gets("Enter stream name: ") |> String.trim() count = IO.gets("Number of events to fetch (default 10): ") |> String.trim() count = if count == "", do: 10, else: String.to_integer(count) case API.get_events(Repl.store(), stream, 1, count, :forward) do {:ok, events} -> IO.puts("\n#{CF.green_on_black()}📄 Events from '#{stream}':#{CF.reset()}") events |> Enum.each(fn event -> IO.puts("#{CF.yellow_on_black()}#{inspect(event, pretty: true)}#{CF.reset()}") end) {:error, reason} -> IO.puts("#{CF.red_on_black()}❌ Failed to get events: #{inspect(reason)}#{CF.reset()}") end {:error, reason} -> IO.puts("#{CF.red_on_black()}❌ Failed to get streams: #{inspect(reason)}#{CF.reset()}") end end defp append_events_manually do IO.puts("\n#{CF.bright_green_on_black()}📝 Append Events Manually#{CF.reset()}") stream = IO.gets("Enter stream name: ") |> String.trim() count = IO.gets("Number of events to append (default 5): ") |> String.trim() count = if count == "", do: 5, else: String.to_integer(count) case Repl.append(stream, count) do {:ok, events, event_count} -> IO.puts("#{CF.green_on_black()}✅ Appended #{event_count} events to '#{stream}'#{CF.reset()}") IO.puts("#{CF.cyan_on_black()}Latest events:#{CF.reset()}") events |> Enum.take(3) |> Enum.each(fn event -> IO.puts(" â€ĸ #{inspect(event, pretty: true)}") end) {:error, reason} -> IO.puts("#{CF.red_on_black()}❌ Failed to append events: #{inspect(reason)}#{CF.reset()}") end end defp get_cluster_status do IO.puts("\n#{CF.bright_blue_on_black()}🔗 Cluster Status#{CF.reset()}") try do gateway_pids = API.gateway_worker_pids() IO.puts("#{CF.green_on_black()}Active gateway workers: #{length(gateway_pids)}#{CF.reset()}") gateway_pids |> Enum.each(fn pid -> IO.puts(" â€ĸ #{inspect(pid)}") end) rescue e -> IO.puts("#{CF.red_on_black()}❌ Failed to get cluster status: #{inspect(e)}#{CF.reset()}") end end defp exit_console do IO.puts("\n#{CF.bright_red_on_black()}👋 Shutting down ExESDB Interactive Console...#{CF.reset()}") # Clean up observers observers = :ets.tab2list(@observer_registry) observers |> Enum.each(fn {name, {_type, _selector, pid}} -> if Process.alive?(pid) do Process.exit(pid, :kill) IO.puts("#{CF.yellow_on_black()}🛑 Stopped observer: #{name}#{CF.reset()}") end end) :ets.delete(@observer_registry) IO.puts("#{CF.green_on_black()}✅ Console shut down successfully!#{CF.reset()}") System.halt() end # ========== Helper Functions ========== defp register_observer(name, type, selector, pid) do :ets.insert(@observer_registry, {name, {type, selector, pid}}) end defp monitor_process(pid, name) do ref = Process.monitor(pid) receive do {:DOWN, ^ref, :process, ^pid, reason} -> IO.puts("\n#{CF.red_on_black()}💀 Observer '#{name}' stopped: #{inspect(reason)}#{CF.reset()}") :ets.delete(@observer_registry, name) after 100 -> # Check if process is still alive and continue monitoring if Process.alive?(pid) do monitor_process(pid, name) else IO.puts("\n#{CF.red_on_black()}💀 Observer '#{name}' is no longer running#{CF.reset()}") :ets.delete(@observer_registry, name) end end end end