@@ -2,17 +2,27 @@ use scru128::Scru128Id;
22use std:: collections:: HashMap ;
33use tracing:: instrument;
44
5+ use serde:: { Deserialize , Serialize } ;
6+
57use crate :: error:: Error ;
68use crate :: nu;
79use crate :: nu:: commands;
810use crate :: nu:: util:: value_to_json;
9- use crate :: store:: { FollowOption , Frame , ReadOptions , Store } ;
11+ use crate :: store:: { FollowOption , Frame , ReadOptions , Store , TTL } ;
12+
13+ // TODO: DRY with handlers
14+ #[ derive( Clone , Debug , Serialize , Deserialize , Default ) ]
15+ pub struct ReturnOptions {
16+ pub suffix : Option < String > ,
17+ pub ttl : Option < TTL > ,
18+ }
1019
1120#[ derive( Clone ) ]
1221struct Command {
1322 id : Scru128Id ,
1423 engine : nu:: Engine ,
1524 definition : String ,
25+ return_options : Option < ReturnOptions > ,
1626}
1727
1828async fn handle_define (
@@ -59,7 +69,6 @@ pub async fn serve(
5969 if frame. topic == "xs.threshold" {
6070 break ;
6171 }
62-
6372 if let Some ( name) = frame. topic . strip_suffix ( ".define" ) {
6473 handle_define ( & frame, name, & base_engine, & store, & mut commands) . await ;
6574 }
@@ -70,8 +79,23 @@ pub async fn serve(
7079 if let Some ( name) = frame. topic . strip_suffix ( ".define" ) {
7180 handle_define ( & frame, name, & base_engine, & store, & mut commands) . await ;
7281 } else if let Some ( name) = frame. topic . strip_suffix ( ".call" ) {
73- if let Some ( command) = commands. get ( name) {
74- execute_command ( command. clone ( ) , frame, & store) . await ?;
82+ let name = name. to_owned ( ) ;
83+ if let Some ( command) = commands. get ( & name) {
84+ let store = store. clone ( ) ;
85+ let frame = frame. clone ( ) ;
86+ let command = command. clone ( ) ;
87+ tokio:: spawn ( async move {
88+ if let Err ( e) = execute_command ( command, & frame, & store) . await {
89+ tracing:: error!( "Failed to execute command '{}': {:?}" , name, e) ;
90+ let _ = store. append (
91+ Frame :: builder ( format ! ( "{}.error" , name) , frame. context_id )
92+ . meta ( serde_json:: json!( {
93+ "error" : e. to_string( ) ,
94+ } ) )
95+ . build ( ) ,
96+ ) ;
97+ }
98+ } ) ;
7599 }
76100 }
77101 }
@@ -86,12 +110,12 @@ async fn register_command(
86110) -> Result < Command , Error > {
87111 // Get definition from CAS
88112 let hash = frame. hash . as_ref ( ) . ok_or ( "Missing hash field" ) ?;
89- let definition = store. cas_read ( hash) . await ?;
90- let definition = String :: from_utf8 ( definition ) ?;
113+ let definition_bytes = store. cas_read ( hash) . await ?;
114+ let definition = String :: from_utf8 ( definition_bytes ) ?;
91115
92116 let mut engine = base_engine. clone ( ) ;
93117
94- // Add addtional commands, scoped to this command's context
118+ // Add additional commands, scoped to this command's context
95119 engine. add_commands ( vec ! [
96120 Box :: new( commands:: cat_command:: CatCommand :: new(
97121 store. clone( ) ,
@@ -103,10 +127,14 @@ async fn register_command(
103127 ) ) ,
104128 ] ) ?;
105129
130+ // Parse the command configuration to extract return_options (ignore the process closure here)
131+ let ( _closure, return_options) = parse_command_definition ( & mut engine, & definition) ?;
132+
106133 Ok ( Command {
107134 id : frame. id ,
108135 engine,
109136 definition,
137+ return_options,
110138 } )
111139}
112140
@@ -120,8 +148,9 @@ async fn register_command(
120148 )
121149 )
122150) ]
123- async fn execute_command ( command : Command , frame : Frame , store : & Store ) -> Result < ( ) , Error > {
151+ async fn execute_command ( command : Command , frame : & Frame , store : & Store ) -> Result < ( ) , Error > {
124152 let store = store. clone ( ) ;
153+ let frame = frame. clone ( ) ;
125154
126155 tokio:: task:: spawn_blocking ( move || {
127156 let base_meta = serde_json:: json!( {
@@ -139,19 +168,34 @@ async fn execute_command(command: Command, frame: Frame, store: &Store) -> Resul
139168 ) ,
140169 ) ] ) ?;
141170
142- let closure = parse_command_definition ( & mut engine, & command. definition ) ?;
171+ let ( closure, _ ) = parse_command_definition ( & mut engine, & command. definition ) ?;
143172
144173 // Run command and process pipeline
145174 match run_command ( & engine, closure, & frame) {
146175 Ok ( pipeline_data) => {
176+ let recv_suffix = command
177+ . return_options
178+ . as_ref ( )
179+ . and_then ( |opts| opts. suffix . as_deref ( ) )
180+ . unwrap_or ( ".recv" ) ;
181+ let ttl = command
182+ . return_options
183+ . as_ref ( )
184+ . and_then ( |opts| opts. ttl . clone ( ) ) ;
185+
147186 // Process each value as a .recv event
148187 for value in pipeline_data {
149188 let hash = store. cas_insert_sync ( value_to_json ( & value) . to_string ( ) ) ?;
150189 let _ = store. append (
151190 Frame :: builder (
152- format ! ( "{}.recv" , frame. topic. strip_suffix( ".call" ) . unwrap( ) ) ,
191+ format ! (
192+ "{}{}" ,
193+ frame. topic. strip_suffix( ".call" ) . unwrap( ) ,
194+ recv_suffix
195+ ) ,
153196 frame. context_id ,
154197 )
198+ . maybe_ttl ( ttl. clone ( ) )
155199 . hash ( hash)
156200 . meta ( serde_json:: json!( {
157201 "command_id" : command. id. to_string( ) ,
@@ -224,7 +268,7 @@ fn run_command(
224268fn parse_command_definition (
225269 engine : & mut nu:: Engine ,
226270 script : & str ,
227- ) -> Result < nu_protocol:: engine:: Closure , Error > {
271+ ) -> Result < ( nu_protocol:: engine:: Closure , Option < ReturnOptions > ) , Error > {
228272 let mut working_set = nu_protocol:: engine:: StateWorkingSet :: new ( & engine. state ) ;
229273 let block = nu_parser:: parse ( & mut working_set, None , script. as_bytes ( ) , false ) ;
230274
@@ -240,12 +284,36 @@ fn parse_command_definition(
240284
241285 let config = result. into_value ( nu_protocol:: Span :: unknown ( ) ) ?;
242286
287+ // Get the process closure (required)
243288 let process = config
244289 . get_data_by_key ( "process" )
245290 . ok_or ( "No 'process' field found in command configuration" ) ?
246291 . into_closure ( ) ?;
247292
293+ // Optionally parse return_options (using the same approach as in handlers)
294+ let return_options = if let Some ( return_config) = config. get_data_by_key ( "return_options" ) {
295+ let record = return_config
296+ . as_record ( )
297+ . map_err ( |_| "return must be a record" ) ?;
298+
299+ let suffix = record
300+ . get ( "suffix" )
301+ . map ( |v| v. as_str ( ) . map_err ( |_| "suffix must be a string" ) )
302+ . transpose ( ) ?
303+ . map ( String :: from) ;
304+
305+ let ttl = record
306+ . get ( "ttl" )
307+ . map ( |v| serde_json:: from_str ( & value_to_json ( v) . to_string ( ) ) )
308+ . transpose ( )
309+ . map_err ( |e| format ! ( "invalid TTL: {}" , e) ) ?;
310+
311+ Some ( ReturnOptions { suffix, ttl } )
312+ } else {
313+ None
314+ } ;
315+
248316 engine. state . merge_env ( & mut stack) ?;
249317
250- Ok ( process)
318+ Ok ( ( process, return_options ) )
251319}
0 commit comments