-module(factos). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/factos.gleam"). -export([event_type/1, event_type_name/1, tag/1, tag_value/1, empty_metadata/0, metadata/1, metadata_entries/1, 'query'/1, query_item/2, decider/3, view/2, reactor/1, compute_events/3, compute_state/3, evolve_recorded/3, project/2, project_from/3, react/2, react_all/2, merge_reactors/2, merge_views/2, decide_context/3, matches_query/2, highest_position/2]). -export_type([event_type/0, tag/0, metadata/0, 'query'/0, query_item/0, sequence_position/0, append_condition/0, decider/4, view/2, reactor/2, revision/0, decoded/1, recorded/1, context/2, loaded_stream/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Store-independent event-sourcing domain primitives.\n" "\n" " Factos keeps the domain model in the application. The core module models\n" " facts, command contexts, pure decision components, and pure views. Concrete\n" " storage concerns live in backend packages such as `factos_sqlight` and\n" " `factos_kurrentdb_erlang`.\n" "\n" " This package follows a context-first reading of Event Sourcing: accepted\n" " facts are the authoritative state of the system, and the facts relevant to a\n" " command are considered before new facts are accepted. Aggregates, stream-per-\n" " object storage, CQRS, projections, and message brokers are implementation\n" " choices rather than prerequisites.\n" "\n" " The central flow is:\n" "\n" " 1. Select a command context with `Query`.\n" " 2. Fold the matching recorded events into a temporary decision state.\n" " 3. Run a pure `Decider`.\n" " 4. Append the produced facts only if the context is still stable.\n" "\n" " Backends implement the storage-specific parts of that flow. This module keeps\n" " the shared types and pure computations small and portable.\n" ). -opaque event_type() :: {event_type, binary()}. -opaque tag() :: {tag, binary()}. -opaque metadata() :: {metadata, list({binary(), binary()})}. -type 'query'() :: all_events | {'query', list(query_item())}. -type query_item() :: {query_item, list(event_type()), list(tag())}. -type sequence_position() :: no_position | {sequence_position, integer()}. -type append_condition() :: no_append_condition | {fail_if_events_match, 'query'(), sequence_position()}. -type decider(DKT, DKU, DKV, DKW) :: {decider, DKU, fun((DKU, DKT) -> {ok, list(DKV)} | {error, DKW}), fun((DKU, DKV) -> DKU)}. -type view(DKX, DKY) :: {view, DKX, fun((DKX, DKY) -> DKX)}. -type reactor(DKZ, DLA) :: {reactor, fun((recorded(DKZ)) -> list(DLA))}. -type revision() :: no_events | {current_revision, integer()}. -type decoded(DLB) :: {decoded, DLB, event_type(), integer(), list(tag()), metadata()}. -type recorded(DLC) :: {recorded, binary(), binary(), integer(), sequence_position(), event_type(), integer(), list(tag()), metadata(), DLC}. -type context(DLD, DLE) :: {context, 'query'(), DLE, list(recorded(DLD)), sequence_position(), append_condition()}. -type loaded_stream(DLF, DLG) :: {loaded_stream, binary(), DLG, list(recorded(DLF)), revision()}. -file("src/factos.gleam", 207). ?DOC(" Wrap an event type name.\n"). -spec event_type(binary()) -> event_type(). event_type(Name) -> {event_type, Name}. -file("src/factos.gleam", 212). ?DOC(" Unwrap an event type name.\n"). -spec event_type_name(event_type()) -> binary(). event_type_name(Event_type) -> {event_type, Name} = Event_type, Name. -file("src/factos.gleam", 218). ?DOC(" Wrap a tag value.\n"). -spec tag(binary()) -> tag(). tag(Value) -> {tag, Value}. -file("src/factos.gleam", 223). ?DOC(" Unwrap a tag value.\n"). -spec tag_value(tag()) -> binary(). tag_value(Tag) -> {tag, Value} = Tag, Value. -file("src/factos.gleam", 229). ?DOC(" No event metadata.\n"). -spec empty_metadata() -> metadata(). empty_metadata() -> {metadata, []}. -file("src/factos.gleam", 234). ?DOC(" Build event metadata from key/value pairs.\n"). -spec metadata(list({binary(), binary()})) -> metadata(). metadata(Entries) -> {metadata, Entries}. -file("src/factos.gleam", 239). ?DOC(" Unwrap event metadata entries.\n"). -spec metadata_entries(metadata()) -> list({binary(), binary()}). metadata_entries(Metadata) -> {metadata, Entries} = Metadata, Entries. -file("src/factos.gleam", 248). ?DOC( " Build a query from query items.\n" "\n" " An empty list becomes `AllEvents`; otherwise the query contains the supplied\n" " items. Query items are OR-combined by `matches_query`.\n" ). -spec 'query'(list(query_item())) -> 'query'(). 'query'(Items) -> case Items of [] -> all_events; [_ | _] -> {'query', Items} end. -file("src/factos.gleam", 259). ?DOC( " Build one command-context query branch.\n" "\n" " Event types are OR-combined. Tags are AND-combined. Empty lists act as wildcards\n" " for that part of the item.\n" ). -spec query_item(list(event_type()), list(tag())) -> query_item(). query_item(Types, Tags) -> {query_item, Types, Tags}. -file("src/factos.gleam", 271). ?DOC( " Build a pure command-side decider.\n" "\n" " The supplied functions remain owned by the application domain. Factos only\n" " stores them together so backends and tests can run the same read-decide-append\n" " flow consistently.\n" ). -spec decider( DLM, fun((DLM, DLN) -> {ok, list(DLO)} | {error, DLQ}), fun((DLM, DLO) -> DLM) ) -> decider(DLN, DLM, DLO, DLQ). decider(Initial, Decide, Evolve) -> {decider, Initial, Decide, Evolve}. -file("src/factos.gleam", 283). ?DOC( " Build a pure projection view.\n" "\n" " A view folds events into read-side state. It does not prescribe where that\n" " state is stored or how events are delivered.\n" ). -spec view(DLX, fun((DLX, DLY) -> DLX)) -> view(DLX, DLY). view(Initial, Evolve) -> {view, Initial, Evolve}. -file("src/factos.gleam", 295). ?DOC( " Build a pure event reactor.\n" "\n" " Reactors are the side-effect planning counterpart to views: they consume\n" " committed recorded events and return application-owned effect values without\n" " executing IO.\n" ). -spec reactor(fun((recorded(DMB)) -> list(DMD))) -> reactor(DMB, DMD). reactor(React) -> {reactor, React}. -file("src/factos.gleam", 451). -spec fold_events(DPN, list(DPO), fun((DPN, DPO) -> DPN)) -> DPN. fold_events(Initial, Events, Evolve) -> gleam@list:fold( Events, Initial, fun(State, Event) -> Evolve(State, Event) end ). -file("src/factos.gleam", 305). ?DOC( " Fold events with a decider and decide which new events a command produces.\n" "\n" " This is useful for unit tests and for in-memory command handling. It does not\n" " perform any append or consistency check.\n" ). -spec compute_events(decider(DMH, any(), DMJ, DMK), list(DMJ), DMH) -> {ok, list(DMJ)} | {error, DMK}. compute_events(Decider, Events, Command) -> {decider, Initial, Decide, Evolve} = Decider, Decide(fold_events(Initial, Events, Evolve), Command). -file("src/factos.gleam", 318). ?DOC( " Decide from an optional current state and return the state after produced events.\n" "\n" " If `current` is `None`, the decider's initial state is used. The function first\n" " runs the decider, then folds the produced events into the decision state.\n" ). -spec compute_state( decider(DMT, DMU, any(), DMW), gleam@option:option(DMU), DMT ) -> {ok, DMU} | {error, DMW}. compute_state(Decider, Current, Command) -> {decider, Initial, Decide, Evolve} = Decider, State@1 = case Current of {some, State} -> State; none -> Initial end, gleam@result:'try'( Decide(State@1, Command), fun(Events) -> {ok, fold_events(State@1, Events, Evolve)} end ). -file("src/factos.gleam", 337). ?DOC( " Fold recorded events into state using a domain evolution function.\n" "\n" " Backends use this after decoding stored events. Only the domain event payload is\n" " passed to `evolve`; storage metadata is ignored for state computation.\n" ). -spec evolve_recorded(DNE, list(recorded(DNF)), fun((DNE, DNF) -> DNE)) -> DNE. evolve_recorded(Initial, Events, Evolve) -> gleam@list:fold( Events, Initial, fun(State, Recorded) -> Evolve(State, erlang:element(10, Recorded)) end ). -file("src/factos.gleam", 347). ?DOC(" Project events from a view's initial state.\n"). -spec project(view(DNI, DNJ), list(DNJ)) -> DNI. project(View, Events) -> {view, Initial, Evolve} = View, fold_events(Initial, Events, Evolve). -file("src/factos.gleam", 356). ?DOC(" Project events starting from an already materialized view state.\n"). -spec project_from(view(DNN, DNO), DNN, list(DNO)) -> DNN. project_from(View, State, Events) -> {view, _, Evolve} = View, fold_events(State, Events, Evolve). -file("src/factos.gleam", 366). ?DOC(" Produce effect values for one committed recorded event.\n"). -spec react(reactor(DNS, DNT), recorded(DNS)) -> list(DNT). react(Reactor, Event) -> {reactor, React} = Reactor, React(Event). -file("src/factos.gleam", 375). ?DOC(" Produce effect values for committed recorded events, preserving event order.\n"). -spec react_all(reactor(DNY, DNZ), list(recorded(DNY))) -> list(DNZ). react_all(Reactor, Events) -> gleam@list:flat_map(Events, fun(Event) -> react(Reactor, Event) end). -file("src/factos.gleam", 387). ?DOC( " Merge two reactors that consume the same event type.\n" "\n" " The resulting reactor runs both reactors for each event and concatenates their\n" " produced effects in argument order.\n" ). -spec merge_reactors(reactor(DOF, DOG), reactor(DOF, DOG)) -> reactor(DOF, DOG). merge_reactors(First, Second) -> {reactor, fun(Event) -> lists:append(react(First, Event), react(Second, Event)) end}. -file("src/factos.gleam", 399). ?DOC( " Merge two views that consume the same event type.\n" "\n" " The resulting view keeps both states in a tuple and evolves both for every\n" " event. This is a convenience for composing small pure projections.\n" ). -spec merge_views(view(DON, DOO), view(DOR, DOO)) -> view({DON, DOR}, DOO). merge_views(First, Second) -> {view, First_initial, First_evolve} = First, {view, Second_initial, Second_evolve} = Second, {view, {First_initial, Second_initial}, fun(State, Event) -> {First_state, Second_state} = State, {First_evolve(First_state, Event), Second_evolve(Second_state, Event)} end}. -file("src/factos.gleam", 415). ?DOC( " Run a command against a previously read context.\n" "\n" " The returned tuple preserves the original context alongside the newly produced\n" " events so a backend can append them with `context.append_condition`.\n" ). -spec decide_context(context(DOW, DOX), DPA, decider(DPA, DOX, DOW, DPB)) -> {ok, {context(DOW, DOX), list(DOW)}} | {error, DPB}. decide_context(Context, Command, Decider) -> gleam@result:'try'( (erlang:element(3, Decider))(erlang:element(3, Context), Command), fun(Events) -> {ok, {Context, Events}} end ). -file("src/factos.gleam", 475). -spec matches_tags(list(tag()), list(tag())) -> boolean(). matches_tags(Event_tags, Required_tags) -> case Required_tags of [] -> true; [_ | _] -> gleam@list:all( Required_tags, fun(Required) -> gleam@list:any(Event_tags, fun(Tag) -> Tag =:= Required end) end ) end. -file("src/factos.gleam", 465). -spec matches_types(event_type(), list(event_type())) -> boolean(). matches_types(Event_type, Types) -> case Types of [] -> true; [_ | _] -> gleam@list:any(Types, fun(Required) -> Event_type =:= Required end) end. -file("src/factos.gleam", 460). -spec matches_item(recorded(any()), query_item()) -> boolean(). matches_item(Recorded, Item) -> {query_item, Types, Tags} = Item, matches_types(erlang:element(6, Recorded), Types) andalso matches_tags( erlang:element(8, Recorded), Tags ). -file("src/factos.gleam", 425). ?DOC(" Test whether a recorded event belongs to a query-defined context.\n"). -spec matches_query(recorded(any()), 'query'()) -> boolean(). matches_query(Recorded, Query) -> case Query of all_events -> true; {'query', Items} -> gleam@list:any( Items, fun(_capture) -> matches_item(Recorded, _capture) end ) end. -file("src/factos.gleam", 436). ?DOC( " Return the later of two sequence positions.\n" "\n" " `NoPosition` acts as absence of an observed position. If both positions are\n" " concrete, the larger integer is returned.\n" ). -spec highest_position(sequence_position(), sequence_position()) -> sequence_position(). highest_position(Left, Right) -> case {Left, Right} of {no_position, Position} -> Position; {Position@1, no_position} -> Position@1; {{sequence_position, Left@1}, {sequence_position, Right@1}} -> case Left@1 >= Right@1 of true -> {sequence_position, Left@1}; false -> {sequence_position, Right@1} end end.