%% @doc Event publishing abstraction for neuroevolution. %% %% This module provides a pluggable event system that can use different %% backends for event distribution: %% %% - `neuroevolution_events_local' - Local pg-based pubsub (default) %% - `neuroevolution_events_macula' - Distributed via Macula mesh (future) %% %% == Event Types == %% %% Commands (requests for work): %% - `{evaluate_request, #{request_id, realm, individual_id, network, options}}' %% - `{evaluate_batch_request, #{request_id, realm, individuals, options}}' %% %% Events (facts that happened): %% - `{evaluated, #{request_id, individual_id, metrics, evaluator_node}}' %% - `{generation_started, #{realm, generation, population_size, timestamp}}' %% - `{generation_completed, #{realm, generation, best_fitness, avg_fitness, ...}}' %% - `{training_started, #{realm, config}}' %% - `{training_stopped, #{realm, generation, reason}}' %% %% == Topic Design == %% %% Topics follow the pattern: `neuro..' %% - `neuro.default.evaluate' - Evaluation requests %% - `neuro.default.evaluated' - Evaluation results %% - `neuro.default.events' - Training lifecycle events %% %% @author Macula.io %% @copyright 2025 Macula.io -module(neuroevolution_events). -export([ publish/2, subscribe/1, subscribe/2, unsubscribe/1, set_backend/1, get_backend/0, %% Topic helpers evaluate_topic/1, evaluated_topic/1, events_topic/1 ]). %% Behaviour callbacks that backends must implement -callback publish(Topic :: binary(), Event :: term()) -> ok. -callback subscribe(Topic :: binary(), Pid :: pid()) -> ok. -callback unsubscribe(Topic :: binary(), Pid :: pid()) -> ok. %%% ============================================================================ %%% API Functions %%% ============================================================================ %% @doc Publish an event to a topic. %% %% The event will be delivered to all subscribers of the topic. %% Event format is typically `{EventType, EventData}' where EventData is a map. -spec publish(Topic, Event) -> ok when Topic :: binary(), Event :: term(). publish(Topic, Event) -> Backend = get_backend(), Backend:publish(Topic, Event). %% @doc Subscribe the calling process to a topic. -spec subscribe(Topic) -> ok when Topic :: binary(). subscribe(Topic) -> subscribe(Topic, self()). %% @doc Subscribe a specific process to a topic. -spec subscribe(Topic, Pid) -> ok when Topic :: binary(), Pid :: pid(). subscribe(Topic, Pid) -> Backend = get_backend(), Backend:subscribe(Topic, Pid). %% @doc Unsubscribe the calling process from a topic. -spec unsubscribe(Topic) -> ok when Topic :: binary(). unsubscribe(Topic) -> Backend = get_backend(), Backend:unsubscribe(Topic, self()). %% @doc Set the event backend module. %% %% The backend module must implement the neuroevolution_events behaviour. %% Default is `neuroevolution_events_local'. -spec set_backend(Module) -> ok when Module :: module(). set_backend(Module) -> persistent_term:put({?MODULE, backend}, Module), ok. %% @doc Get the current event backend module. -spec get_backend() -> module(). get_backend() -> persistent_term:get({?MODULE, backend}, neuroevolution_events_local). %%% ============================================================================ %%% Helper Functions for Topic Construction %%% ============================================================================ %% @doc Get the evaluation request topic for a realm. -spec evaluate_topic(Realm) -> binary() when Realm :: binary(). evaluate_topic(Realm) -> <<"neuro.", Realm/binary, ".evaluate">>. %% @doc Get the evaluation results topic for a realm. -spec evaluated_topic(Realm) -> binary() when Realm :: binary(). evaluated_topic(Realm) -> <<"neuro.", Realm/binary, ".evaluated">>. %% @doc Get the training events topic for a realm. -spec events_topic(Realm) -> binary() when Realm :: binary(). events_topic(Realm) -> <<"neuro.", Realm/binary, ".events">>.