@@ -10,6 +10,7 @@ defmodule Statifier.Actions.SendAction do
1010 - Interface with external systems via Event I/O Processors
1111 """
1212
13+ alias Predicator.Duration
1314 alias Statifier . { Evaluator , Event , StateChart }
1415 alias Statifier.Logging.LogManager
1516 require LogManager
@@ -80,18 +81,25 @@ defmodule Statifier.Actions.SendAction do
8081 """
8182 @ spec execute ( Statifier.StateChart . t ( ) , t ( ) ) :: Statifier.StateChart . t ( )
8283 def execute ( state_chart , % __MODULE__ { } = send_action ) do
83- # Phase 1: Only support immediate internal sends
84- { :ok , event_name , target_uri , _delay } = evaluate_send_parameters ( send_action , state_chart )
85-
86- if target_uri == "#_internal" do
87- execute_internal_send ( event_name , send_action , state_chart )
88- else
89- # Phase 1: Log unsupported external targets
90- LogManager . info ( state_chart , "External send targets not yet supported" , % {
91- action_type: "send_action" ,
92- target: target_uri ,
93- event_name: event_name
94- } )
84+ { :ok , event_name , target_uri , delay_ms } = evaluate_send_parameters ( send_action , state_chart )
85+
86+ cond do
87+ target_uri == "#_internal" and delay_ms == 0 ->
88+ # Immediate internal send - execute now
89+ execute_internal_send ( event_name , send_action , state_chart )
90+
91+ target_uri == "#_internal" and delay_ms > 0 ->
92+ # Delayed internal send - requires StateMachine context
93+ execute_delayed_send ( event_name , send_action , state_chart , delay_ms )
94+
95+ true ->
96+ # External targets not yet supported
97+ LogManager . info ( state_chart , "External send targets not yet supported" , % {
98+ action_type: "send_action" ,
99+ target: target_uri ,
100+ event_name: event_name ,
101+ delay_ms: delay_ms
102+ } )
95103 end
96104 end
97105
@@ -131,13 +139,55 @@ defmodule Statifier.Actions.SendAction do
131139 end
132140
133141 defp evaluate_delay ( send_action , state_chart ) do
134- state_chart
135- |> evaluate_attribute_with_expr (
136- send_action . delay ,
137- send_action . compiled_delay_expr ,
138- send_action . delay_expr ,
139- "0s"
140- )
142+ delay_string =
143+ state_chart
144+ |> evaluate_attribute_with_expr (
145+ send_action . delay ,
146+ send_action . compiled_delay_expr ,
147+ send_action . delay_expr ,
148+ "0s"
149+ )
150+
151+ # Parse delay string to milliseconds using Predicator's duration parsing
152+ case parse_delay_to_milliseconds ( delay_string ) do
153+ { :ok , milliseconds } ->
154+ milliseconds
155+
156+ { :error , reason } ->
157+ LogManager . warn ( state_chart , "Invalid delay expression, defaulting to 0ms" , % {
158+ action_type: "send_action" ,
159+ delay_string: delay_string ,
160+ error: inspect ( reason )
161+ } )
162+
163+ 0
164+ end
165+ end
166+
167+ # Parse delay string to milliseconds using Predicator's duration support
168+ defp parse_delay_to_milliseconds ( delay_string ) when is_binary ( delay_string ) do
169+ case Predicator . evaluate ( delay_string ) do
170+ { :ok , % { } = duration_map } ->
171+ # Duration map returned - convert to milliseconds
172+ { :ok , Duration . to_milliseconds ( duration_map ) }
173+
174+ { :ok , numeric_value } when is_number ( numeric_value ) ->
175+ # Numeric value - assume milliseconds
176+ { :ok , round ( numeric_value ) }
177+
178+ { :ok , string_value } when is_binary ( string_value ) ->
179+ # Try evaluating as duration string again (might be nested evaluation)
180+ case Predicator . evaluate ( string_value ) do
181+ { :ok , % { } = duration_map } -> { :ok , Duration . to_milliseconds ( duration_map ) }
182+ { :ok , numeric_value } when is_number ( numeric_value ) -> { :ok , round ( numeric_value ) }
183+ error -> error
184+ end
185+
186+ error ->
187+ error
188+ end
189+ rescue
190+ error -> { :error , error }
141191 end
142192
143193 # Common helper for evaluating attributes that can be static or expressions
@@ -173,6 +223,60 @@ defmodule Statifier.Actions.SendAction do
173223 end
174224 end
175225
226+ # Execute delayed send - requires StateMachine context
227+ defp execute_delayed_send ( event_name , send_action , state_chart , delay_ms ) do
228+ # Check if we're running in StateMachine context via StateChart field
229+ case state_chart . state_machine_pid do
230+ pid when is_pid ( pid ) ->
231+ # Generate send ID for tracking
232+ send_id = generate_send_id ( send_action )
233+
234+ # Build event data
235+ event_data = build_event_data ( send_action , state_chart )
236+
237+ # Create the delayed event
238+ delayed_event = % Event {
239+ name: event_name ,
240+ data: event_data ,
241+ origin: :internal
242+ }
243+
244+ # Schedule the delayed send through StateMachine (async to avoid deadlock)
245+ GenServer . cast ( pid , { :schedule_delayed_send , send_id , delayed_event , delay_ms } )
246+
247+ LogManager . info ( state_chart , "Scheduled delayed send" , % {
248+ action_type: "send_action" ,
249+ event_name: event_name ,
250+ delay_ms: delay_ms ,
251+ send_id: send_id
252+ } )
253+
254+ state_chart
255+
256+ nil ->
257+ # Not in StateMachine context - warn and execute immediately
258+ warned_state_chart =
259+ LogManager . warn (
260+ state_chart ,
261+ "Delayed send requires StateMachine context, executing immediately" ,
262+ % {
263+ action_type: "send_action" ,
264+ event_name: event_name ,
265+ delay_ms: delay_ms
266+ }
267+ )
268+
269+ execute_internal_send ( event_name , send_action , warned_state_chart )
270+ end
271+ end
272+
273+ # Generate unique send ID using UXID or use provided ID
274+ defp generate_send_id ( % __MODULE__ { id: id } ) when not is_nil ( id ) , do: id
275+
276+ defp generate_send_id ( % __MODULE__ { } ) do
277+ UXID . generate! ( prefix: "send" , size: :s )
278+ end
279+
176280 defp execute_internal_send ( event_name , send_action , state_chart ) do
177281 # Build event data from namelist, params, and content
178282 event_data = build_event_data ( send_action , state_chart )
0 commit comments