@@ -2,17 +2,27 @@ use scru128::Scru128Id;
2
2
use std:: collections:: HashMap ;
3
3
use tracing:: instrument;
4
4
5
+ use serde:: { Deserialize , Serialize } ;
6
+
5
7
use crate :: error:: Error ;
6
8
use crate :: nu;
7
9
use crate :: nu:: commands;
8
10
use 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
+ }
10
19
11
20
#[ derive( Clone ) ]
12
21
struct Command {
13
22
id : Scru128Id ,
14
23
engine : nu:: Engine ,
15
24
definition : String ,
25
+ return_options : Option < ReturnOptions > ,
16
26
}
17
27
18
28
async fn handle_define (
@@ -59,7 +69,6 @@ pub async fn serve(
59
69
if frame. topic == "xs.threshold" {
60
70
break ;
61
71
}
62
-
63
72
if let Some ( name) = frame. topic . strip_suffix ( ".define" ) {
64
73
handle_define ( & frame, name, & base_engine, & store, & mut commands) . await ;
65
74
}
@@ -70,8 +79,23 @@ pub async fn serve(
70
79
if let Some ( name) = frame. topic . strip_suffix ( ".define" ) {
71
80
handle_define ( & frame, name, & base_engine, & store, & mut commands) . await ;
72
81
} 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
+ } ) ;
75
99
}
76
100
}
77
101
}
@@ -86,12 +110,12 @@ async fn register_command(
86
110
) -> Result < Command , Error > {
87
111
// Get definition from CAS
88
112
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 ) ?;
91
115
92
116
let mut engine = base_engine. clone ( ) ;
93
117
94
- // Add addtional commands, scoped to this command's context
118
+ // Add additional commands, scoped to this command's context
95
119
engine. add_commands ( vec ! [
96
120
Box :: new( commands:: cat_command:: CatCommand :: new(
97
121
store. clone( ) ,
@@ -103,10 +127,14 @@ async fn register_command(
103
127
) ) ,
104
128
] ) ?;
105
129
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
+
106
133
Ok ( Command {
107
134
id : frame. id ,
108
135
engine,
109
136
definition,
137
+ return_options,
110
138
} )
111
139
}
112
140
@@ -120,8 +148,9 @@ async fn register_command(
120
148
)
121
149
)
122
150
) ]
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 > {
124
152
let store = store. clone ( ) ;
153
+ let frame = frame. clone ( ) ;
125
154
126
155
tokio:: task:: spawn_blocking ( move || {
127
156
let base_meta = serde_json:: json!( {
@@ -139,19 +168,34 @@ async fn execute_command(command: Command, frame: Frame, store: &Store) -> Resul
139
168
) ,
140
169
) ] ) ?;
141
170
142
- let closure = parse_command_definition ( & mut engine, & command. definition ) ?;
171
+ let ( closure, _ ) = parse_command_definition ( & mut engine, & command. definition ) ?;
143
172
144
173
// Run command and process pipeline
145
174
match run_command ( & engine, closure, & frame) {
146
175
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
+
147
186
// Process each value as a .recv event
148
187
for value in pipeline_data {
149
188
let hash = store. cas_insert_sync ( value_to_json ( & value) . to_string ( ) ) ?;
150
189
let _ = store. append (
151
190
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
+ ) ,
153
196
frame. context_id ,
154
197
)
198
+ . maybe_ttl ( ttl. clone ( ) )
155
199
. hash ( hash)
156
200
. meta ( serde_json:: json!( {
157
201
"command_id" : command. id. to_string( ) ,
@@ -224,7 +268,7 @@ fn run_command(
224
268
fn parse_command_definition (
225
269
engine : & mut nu:: Engine ,
226
270
script : & str ,
227
- ) -> Result < nu_protocol:: engine:: Closure , Error > {
271
+ ) -> Result < ( nu_protocol:: engine:: Closure , Option < ReturnOptions > ) , Error > {
228
272
let mut working_set = nu_protocol:: engine:: StateWorkingSet :: new ( & engine. state ) ;
229
273
let block = nu_parser:: parse ( & mut working_set, None , script. as_bytes ( ) , false ) ;
230
274
@@ -240,12 +284,36 @@ fn parse_command_definition(
240
284
241
285
let config = result. into_value ( nu_protocol:: Span :: unknown ( ) ) ?;
242
286
287
+ // Get the process closure (required)
243
288
let process = config
244
289
. get_data_by_key ( "process" )
245
290
. ok_or ( "No 'process' field found in command configuration" ) ?
246
291
. into_closure ( ) ?;
247
292
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
+
248
316
engine. state . merge_env ( & mut stack) ?;
249
317
250
- Ok ( process)
318
+ Ok ( ( process, return_options ) )
251
319
}
0 commit comments