@wasm-oj/server 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,8 @@ mod native;
15
15
  #[cfg(target_arch = "wasm32")]
16
16
  mod web;
17
17
  #[cfg(target_arch = "wasm32")]
18
+ pub(crate) mod web_interactive;
19
+ #[cfg(target_arch = "wasm32")]
18
20
  pub(crate) mod web_runtime;
19
21
 
20
22
  pub fn run(request: RunRequest) -> Result<RunResult, RunError> {
@@ -2,7 +2,9 @@ use crate::capabilities::attach_capability_denials;
2
2
  use crate::deterministic::{VirtualClock, attach_deterministic_imports};
3
3
  use crate::filesystem::{read_files_bounded, runtime_project_files};
4
4
  use crate::memory::LimitingTunables;
5
- use crate::meter::{CostPoints, METER_MODEL, instrument_wasm, meter_state, remaining_points};
5
+ use crate::meter::{
6
+ CostPoints, METER_MODEL, attach_safepoint, instrument_wasm, meter_state, remaining_points,
7
+ };
6
8
  use crate::module_imports::attach_imported_memory;
7
9
  use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
8
10
  use crate::output::{OutputBudget, OutputCapture};
@@ -105,13 +107,14 @@ fn run_in_runtime(request: RunRequest) -> Result<RunResult, RunError> {
105
107
  clock.clone(),
106
108
  request.startup_entropy_bytes,
107
109
  );
110
+ attach_safepoint(&mut store, &mut imports);
108
111
  attach_capability_denials(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
109
112
  let imported_memory =
110
113
  attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
111
114
 
112
115
  let instance = Instance::new(&mut store, &module, &imports)
113
116
  .map_err(|error| RunError::Compile(format!("failed to instantiate module: {error}")))?;
114
- let meter = meter_state(&mut store, &instance).map_err(RunError::Runtime)?;
117
+ let meter = meter_state(&instance).map_err(RunError::Runtime)?;
115
118
  let guest_memory = instance
116
119
  .exports
117
120
  .get_memory("memory")
@@ -2,13 +2,16 @@ use super::web_runtime::runtime_with_engine;
2
2
  use crate::capabilities::attach_capability_denials;
3
3
  use crate::deterministic::{VirtualClock, attach_deterministic_imports};
4
4
  use crate::filesystem::{read_files_bounded, runtime_project_files};
5
- use crate::meter::{CostPoints, METER_MODEL, instrument_wasm, meter_state, remaining_points};
5
+ use crate::meter::{
6
+ CostPoints, METER_MODEL, attach_safepoint, instrument_wasm, meter_state, remaining_points,
7
+ };
6
8
  use crate::module_imports::attach_imported_memory;
7
9
  use crate::module_policy::{DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit};
8
- use crate::output::{OutputBudget, OutputCapture};
10
+ use crate::output::{CappedOutput, OutputBudget, OutputCapture};
9
11
  use crate::{ExecutionMetrics, ExecutionTermination, RunError, RunRequest, RunResult};
10
12
  use std::io::Write;
11
13
  use std::sync::{Arc, Mutex};
14
+ use virtual_fs::VirtualFile;
12
15
  use wasmer::{Instance, Memory, Module, Store};
13
16
  use wasmer_wasix::{
14
17
  Pipe, WasiEnv, WasiError, WasiModuleInstanceHandles, WasiModuleTreeHandles, wasmer_wasix_types,
@@ -16,8 +19,29 @@ use wasmer_wasix::{
16
19
 
17
20
  pub fn run(
18
21
  request: RunRequest,
19
- mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
22
+ on_execution: impl FnMut(bool) -> Result<(), RunError>,
20
23
  ) -> Result<RunResult, RunError> {
24
+ let (mut stdin_writer, stdin_reader) = Pipe::channel();
25
+ stdin_writer
26
+ .write_all(&request.stdin)
27
+ .map_err(|error| RunError::Io(error.to_string()))?;
28
+ drop(stdin_writer);
29
+ execute(request, Box::new(stdin_reader), None, on_execution).map(|execution| execution.result)
30
+ }
31
+
32
+ pub(super) type StdoutPeer = Box<dyn FnOnce(CappedOutput) -> Box<dyn VirtualFile + Send + Sync>>;
33
+
34
+ pub(super) struct Execution {
35
+ pub result: RunResult,
36
+ pub exit_code: Option<i32>,
37
+ }
38
+
39
+ pub(super) fn execute(
40
+ request: RunRequest,
41
+ stdin: Box<dyn VirtualFile + Send + Sync>,
42
+ stdout_peer: Option<StdoutPeer>,
43
+ mut on_execution: impl FnMut(bool) -> Result<(), RunError>,
44
+ ) -> Result<Execution, RunError> {
21
45
  let limited = enforce_memory_limit(&request.wasm, request.resources.memory_limit_bytes)
22
46
  .map_err(RunError::Compile)?;
23
47
  let metered = instrument_wasm(&limited, request.resources.instruction_budget)
@@ -29,11 +53,6 @@ pub fn run(
29
53
  })?;
30
54
  let runtime = runtime_with_engine(store.engine().clone());
31
55
 
32
- let (mut stdin_writer, stdin_reader) = Pipe::channel();
33
- stdin_writer
34
- .write_all(&request.stdin)
35
- .map_err(|error| RunError::Io(error.to_string()))?;
36
- drop(stdin_writer);
37
56
  let output_limit = usize::try_from(request.resources.output_limit_bytes)
38
57
  .map_err(|_| RunError::InvalidRequest("output limit exceeds host range".to_string()))?;
39
58
  let output_budget = OutputBudget::new(output_limit);
@@ -51,8 +70,11 @@ pub fn run(
51
70
  .runtime(runtime)
52
71
  .args(request.args.clone())
53
72
  .envs(request.env.clone())
54
- .stdin(Box::new(stdin_reader))
55
- .stdout(Box::new(stdout_file))
73
+ .stdin(stdin)
74
+ .stdout(match stdout_peer {
75
+ Some(peer) => peer(stdout_file),
76
+ None => Box::new(stdout_file),
77
+ })
56
78
  .stderr(Box::new(stderr_file))
57
79
  .fs(filesystem.clone());
58
80
  builder
@@ -82,12 +104,13 @@ pub fn run(
82
104
  clock.clone(),
83
105
  request.startup_entropy_bytes,
84
106
  );
107
+ attach_safepoint(&mut store, &mut imports);
85
108
  attach_capability_denials(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
86
109
  let imported_memory =
87
110
  attach_imported_memory(&mut store, &module, &mut imports).map_err(RunError::Compile)?;
88
111
  let instance = Instance::new(&mut store, &module, &imports)
89
112
  .map_err(|error| RunError::Compile(format!("failed to instantiate module: {error}")))?;
90
- let meter = meter_state(&mut store, &instance).map_err(RunError::Runtime)?;
113
+ let meter = meter_state(&instance).map_err(RunError::Runtime)?;
91
114
  let guest_memory = instance
92
115
  .exports
93
116
  .get_memory("memory")
@@ -138,12 +161,14 @@ pub fn run(
138
161
  let logical_time_exceeded = clock.limit_exceeded()?;
139
162
 
140
163
  let mut code = 0;
164
+ let mut exit_code = None;
141
165
  let mut termination = ExecutionTermination::Exited;
142
166
  let mut trap_message = None;
143
167
  if let Err(error) = execution {
144
168
  if let Some(wasi_error) = crate::wasi_error(&error) {
145
169
  match wasi_error {
146
170
  WasiError::Exit(exit) => {
171
+ exit_code = Some(exit.raw());
147
172
  let errno: wasmer_wasix_types::wasi::Errno = (*exit).into();
148
173
  if errno != wasmer_wasix_types::wasi::Errno::Success {
149
174
  code = errno as i32;
@@ -211,7 +236,7 @@ pub fn run(
211
236
  CostPoints::Exhausted => request.resources.instruction_budget,
212
237
  };
213
238
  let filesystem_metrics = project_filesystem.metrics();
214
- Ok(RunResult {
239
+ let result = RunResult {
215
240
  code,
216
241
  metrics: ExecutionMetrics {
217
242
  cost,
@@ -231,5 +256,6 @@ pub fn run(
231
256
  trap_message,
232
257
  determinism: request.determinism,
233
258
  resources: request.resources,
234
- })
259
+ };
260
+ Ok(Execution { result, exit_code })
235
261
  }
@@ -0,0 +1,375 @@
1
+ use super::web::{Execution, execute};
2
+ use crate::output::CappedOutput;
3
+ use crate::{
4
+ DeterminismConfig, ExecutionTermination, InteractiveMetrics, InteractiveProcessResult,
5
+ InteractiveProgram, RunError, RunRequest,
6
+ };
7
+ use serde::{Deserialize, Serialize};
8
+ use std::io;
9
+ use std::pin::Pin;
10
+ use std::sync::Arc;
11
+ use std::task::{Context, Poll};
12
+ use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
13
+ use virtual_fs::{FsError, VirtualFile};
14
+ use wasm_bindgen::JsValue;
15
+
16
+ #[derive(Clone, Debug, Deserialize, Serialize)]
17
+ #[serde(deny_unknown_fields, rename_all = "camelCase")]
18
+ pub struct InteractiveSideRequest {
19
+ pub program: InteractiveProgram,
20
+ pub determinism: DeterminismConfig,
21
+ }
22
+
23
+ #[derive(Clone, Debug, Serialize)]
24
+ #[serde(rename_all = "camelCase")]
25
+ pub struct InteractiveSideResult {
26
+ pub process: InteractiveProcessResult,
27
+ #[serde(with = "serde_bytes")]
28
+ pub protocol: Vec<u8>,
29
+ }
30
+
31
+ /// Stream callbacks supplied by the side Worker. Reads and writes wait with
32
+ /// `Atomics.wait`, so they never return `Pending` to WASIX.
33
+ pub struct HostStreams {
34
+ read: js_sys::Function,
35
+ poll: js_sys::Function,
36
+ wait: js_sys::Function,
37
+ write: js_sys::Function,
38
+ close: js_sys::Function,
39
+ }
40
+
41
+ // SAFETY: the web build of runtime-core has no threads; these JS handles never
42
+ // leave the Worker thread that created them.
43
+ unsafe impl Send for HostStreams {}
44
+ // SAFETY: see `Send`.
45
+ unsafe impl Sync for HostStreams {}
46
+
47
+ impl HostStreams {
48
+ pub fn new(
49
+ read: js_sys::Function,
50
+ poll: js_sys::Function,
51
+ wait: js_sys::Function,
52
+ write: js_sys::Function,
53
+ close: js_sys::Function,
54
+ ) -> Self {
55
+ Self {
56
+ read,
57
+ poll,
58
+ wait,
59
+ write,
60
+ close,
61
+ }
62
+ }
63
+
64
+ fn read(&self, maximum: usize) -> io::Result<Vec<u8>> {
65
+ let chunk = self
66
+ .read
67
+ .call1(&JsValue::UNDEFINED, &JsValue::from(maximum as u32))
68
+ .map_err(host_error)?;
69
+ Ok(js_sys::Uint8Array::new(&chunk).to_vec())
70
+ }
71
+
72
+ fn poll(&self) -> io::Result<Option<usize>> {
73
+ let available = self
74
+ .poll
75
+ .call0(&JsValue::UNDEFINED)
76
+ .map_err(host_error)?
77
+ .as_f64()
78
+ .ok_or_else(|| io::Error::other("interactive input poll returned no size"))?;
79
+ Ok((available >= 0.0).then_some(available as usize))
80
+ }
81
+
82
+ fn wait(&self) -> io::Result<usize> {
83
+ let available = self
84
+ .wait
85
+ .call0(&JsValue::UNDEFINED)
86
+ .map_err(host_error)?
87
+ .as_f64()
88
+ .ok_or_else(|| io::Error::other("interactive input wait returned no size"))?;
89
+ Ok(available as usize)
90
+ }
91
+
92
+ fn write(&self, bytes: &[u8]) -> io::Result<usize> {
93
+ let written = self
94
+ .write
95
+ .call1(&JsValue::UNDEFINED, &js_sys::Uint8Array::from(bytes))
96
+ .map_err(host_error)?
97
+ .as_f64()
98
+ .ok_or_else(|| io::Error::other("interactive output write returned no size"))?;
99
+ if written < 0.0 {
100
+ return Err(io::ErrorKind::BrokenPipe.into());
101
+ }
102
+ Ok(written as usize)
103
+ }
104
+
105
+ /// Closes the input (fd 0) or output (fd 1) pipe end, so the peer sees
106
+ /// EOF or a broken pipe at once, as when native drops its pipe end.
107
+ fn close(&self, fd: u32) {
108
+ let _ = self.close.call1(&JsValue::UNDEFINED, &JsValue::from(fd));
109
+ }
110
+ }
111
+
112
+ fn host_error(error: JsValue) -> io::Error {
113
+ io::Error::other(format!("interactive stream failed: {error:?}"))
114
+ }
115
+
116
+ pub fn run_side(
117
+ request: InteractiveSideRequest,
118
+ streams: HostStreams,
119
+ on_execution: impl FnMut(bool) -> Result<(), RunError>,
120
+ ) -> Result<InteractiveSideResult, RunError> {
121
+ let program = request.program;
122
+ if program.wasm.is_empty() {
123
+ return Err(RunError::InvalidRequest(
124
+ "interactive Wasm must not be empty".to_string(),
125
+ ));
126
+ }
127
+ let run = RunRequest {
128
+ wasm: program.wasm,
129
+ args: program.args,
130
+ env: program.env,
131
+ stdin: Vec::new(),
132
+ files: program.files,
133
+ output_paths: Vec::new(),
134
+ cwd: program.cwd,
135
+ startup_entropy_bytes: program.startup_entropy_bytes,
136
+ determinism: request.determinism,
137
+ resources: program.resources,
138
+ };
139
+ super::validate(&run)?;
140
+ let streams = Arc::new(streams);
141
+ let input = StreamInput {
142
+ streams: streams.clone(),
143
+ };
144
+ let Execution { result, exit_code } = execute(
145
+ run,
146
+ Box::new(input),
147
+ Some(Box::new(move |capture| {
148
+ Box::new(StreamOutput { streams, capture })
149
+ })),
150
+ on_execution,
151
+ )?;
152
+ let code = match exit_code {
153
+ Some(code) if result.termination == ExecutionTermination::Exited => code,
154
+ _ => result.code,
155
+ };
156
+ Ok(InteractiveSideResult {
157
+ process: InteractiveProcessResult {
158
+ code,
159
+ stderr: result.stderr,
160
+ termination: result.termination,
161
+ metrics: InteractiveMetrics {
162
+ cost: result.metrics.cost,
163
+ operations: result.metrics.operations,
164
+ logical_time_ns: result.metrics.logical_time_ns,
165
+ filesystem_bytes: result.metrics.filesystem_bytes,
166
+ filesystem_entries: result.metrics.filesystem_entries,
167
+ protocol_bytes: result.metrics.stdout_bytes,
168
+ stderr_bytes: result.metrics.stderr_bytes,
169
+ },
170
+ },
171
+ protocol: result.stdout,
172
+ })
173
+ }
174
+
175
+ struct StreamInput {
176
+ streams: Arc<HostStreams>,
177
+ }
178
+
179
+ // WASIX drops the stdio handle when the last descriptor that refers to it is
180
+ // closed, which is when native drops its `PipeRx` or `PipeTx`.
181
+ impl Drop for StreamInput {
182
+ fn drop(&mut self) {
183
+ self.streams.close(0);
184
+ }
185
+ }
186
+
187
+ impl std::fmt::Debug for StreamInput {
188
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189
+ formatter.write_str("StreamInput")
190
+ }
191
+ }
192
+
193
+ impl AsyncRead for StreamInput {
194
+ fn poll_read(
195
+ self: Pin<&mut Self>,
196
+ _context: &mut Context<'_>,
197
+ buffer: &mut ReadBuf<'_>,
198
+ ) -> Poll<io::Result<()>> {
199
+ Poll::Ready(self.streams.read(buffer.remaining()).map(|chunk| {
200
+ buffer.put_slice(&chunk);
201
+ }))
202
+ }
203
+ }
204
+
205
+ impl AsyncWrite for StreamInput {
206
+ fn poll_write(
207
+ self: Pin<&mut Self>,
208
+ _context: &mut Context<'_>,
209
+ _buffer: &[u8],
210
+ ) -> Poll<io::Result<usize>> {
211
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
212
+ }
213
+
214
+ fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
215
+ Poll::Ready(Ok(()))
216
+ }
217
+
218
+ fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
219
+ Poll::Ready(Ok(()))
220
+ }
221
+ }
222
+
223
+ impl AsyncSeek for StreamInput {
224
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
225
+ Ok(())
226
+ }
227
+
228
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
229
+ Poll::Ready(Ok(0))
230
+ }
231
+ }
232
+
233
+ impl VirtualFile for StreamInput {
234
+ fn last_accessed(&self) -> u64 {
235
+ 0
236
+ }
237
+ fn last_modified(&self) -> u64 {
238
+ 0
239
+ }
240
+ fn created_time(&self) -> u64 {
241
+ 0
242
+ }
243
+ fn size(&self) -> u64 {
244
+ 0
245
+ }
246
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
247
+ Ok(())
248
+ }
249
+ fn unlink(&mut self) -> Result<(), FsError> {
250
+ Ok(())
251
+ }
252
+ fn get_special_fd(&self) -> Option<u32> {
253
+ Some(0)
254
+ }
255
+
256
+ fn poll_read_ready(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<usize>> {
257
+ match self.streams.poll() {
258
+ Ok(Some(available)) => Poll::Ready(Ok(available)),
259
+ Ok(None) if crate::deterministic::defer_stdin_readiness() => {
260
+ context.waker().wake_by_ref();
261
+ Poll::Pending
262
+ }
263
+ Ok(None) => Poll::Ready(self.streams.wait()),
264
+ Err(error) => Poll::Ready(Err(error)),
265
+ }
266
+ }
267
+
268
+ fn poll_write_ready(
269
+ self: Pin<&mut Self>,
270
+ _context: &mut Context<'_>,
271
+ ) -> Poll<io::Result<usize>> {
272
+ Poll::Ready(Err(io::ErrorKind::Unsupported.into()))
273
+ }
274
+ }
275
+
276
+ struct StreamOutput {
277
+ streams: Arc<HostStreams>,
278
+ capture: CappedOutput,
279
+ }
280
+
281
+ impl Drop for StreamOutput {
282
+ fn drop(&mut self) {
283
+ self.streams.close(1);
284
+ }
285
+ }
286
+
287
+ impl std::fmt::Debug for StreamOutput {
288
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289
+ formatter.write_str("StreamOutput")
290
+ }
291
+ }
292
+
293
+ impl AsyncWrite for StreamOutput {
294
+ /// Drops bytes written after the peer closed its stdin, as native does; they are already in
295
+ /// the transcript.
296
+ fn poll_write(
297
+ mut self: Pin<&mut Self>,
298
+ context: &mut Context<'_>,
299
+ buffer: &[u8],
300
+ ) -> Poll<io::Result<usize>> {
301
+ match Pin::new(&mut self.capture).poll_write(context, buffer) {
302
+ Poll::Ready(Ok(written)) => Poll::Ready(match self.streams.write(&buffer[..written]) {
303
+ Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(written),
304
+ result => result,
305
+ }),
306
+ result => result,
307
+ }
308
+ }
309
+
310
+ fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
311
+ Poll::Ready(Ok(()))
312
+ }
313
+
314
+ fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<()>> {
315
+ Poll::Ready(Ok(()))
316
+ }
317
+ }
318
+
319
+ impl AsyncRead for StreamOutput {
320
+ fn poll_read(
321
+ self: Pin<&mut Self>,
322
+ _context: &mut Context<'_>,
323
+ _buffer: &mut ReadBuf<'_>,
324
+ ) -> Poll<io::Result<()>> {
325
+ Poll::Ready(Ok(()))
326
+ }
327
+ }
328
+
329
+ impl AsyncSeek for StreamOutput {
330
+ fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
331
+ Ok(())
332
+ }
333
+
334
+ fn poll_complete(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<io::Result<u64>> {
335
+ Poll::Ready(Ok(0))
336
+ }
337
+ }
338
+
339
+ impl VirtualFile for StreamOutput {
340
+ fn last_accessed(&self) -> u64 {
341
+ 0
342
+ }
343
+ fn last_modified(&self) -> u64 {
344
+ 0
345
+ }
346
+ fn created_time(&self) -> u64 {
347
+ 0
348
+ }
349
+ fn size(&self) -> u64 {
350
+ 0
351
+ }
352
+ fn set_len(&mut self, _new_size: u64) -> Result<(), FsError> {
353
+ Ok(())
354
+ }
355
+ fn unlink(&mut self) -> Result<(), FsError> {
356
+ Ok(())
357
+ }
358
+ fn get_special_fd(&self) -> Option<u32> {
359
+ Some(1)
360
+ }
361
+
362
+ fn poll_read_ready(
363
+ self: Pin<&mut Self>,
364
+ _context: &mut Context<'_>,
365
+ ) -> Poll<io::Result<usize>> {
366
+ Poll::Ready(Ok(0))
367
+ }
368
+
369
+ fn poll_write_ready(
370
+ mut self: Pin<&mut Self>,
371
+ context: &mut Context<'_>,
372
+ ) -> Poll<io::Result<usize>> {
373
+ Pin::new(&mut self.capture).poll_write_ready(context)
374
+ }
375
+ }
@@ -1,6 +1,7 @@
1
+ use crate::run::web_interactive::{HostStreams, InteractiveSideRequest, run_side};
1
2
  use crate::{
2
3
  GoCompilerSession as CoreGoCompilerSession, GoCompilerSessionConfig, GoCompilerSessionRequest,
3
- InteractiveRequest, RunError, RunRequest, interactive_response, run_response_from_result,
4
+ RunError, RunFailure, RunRequest, run_response_from_result,
4
5
  };
5
6
  use serde::Serialize;
6
7
  use wasm_bindgen::prelude::*;
@@ -72,17 +73,62 @@ impl WebGoCompilerSession {
72
73
  }
73
74
  }
74
75
 
76
+ /// Runs one side of an interactive session in the calling Worker. `read`,
77
+ /// `wait` and `write` block on the session's shared ring buffers; `poll`
78
+ /// checks the input without blocking; `close(fd)` closes the input (0) or
79
+ /// output (1) end when the guest drops it.
75
80
  #[wasm_bindgen]
76
- pub async fn interact_wasm_oj(request: JsValue) -> Result<JsValue, JsValue> {
81
+ pub fn run_interactive_side(
82
+ request: JsValue,
83
+ read: js_sys::Function,
84
+ poll: js_sys::Function,
85
+ wait: js_sys::Function,
86
+ write: js_sys::Function,
87
+ close: js_sys::Function,
88
+ on_execution: js_sys::Function,
89
+ ) -> Result<JsValue, JsValue> {
77
90
  console_error_panic_hook::set_once();
78
- let request: InteractiveRequest = serde_wasm_bindgen::from_value(request)
79
- .map_err(|error| JsValue::from_str(&format!("invalid interactive request: {error}")))?;
80
- let response = interactive_response(request).await;
91
+ let request: InteractiveSideRequest =
92
+ serde_wasm_bindgen::from_value(request).map_err(|error| {
93
+ JsValue::from_str(&format!("invalid interactive side request: {error}"))
94
+ })?;
95
+ let response = match run_side(
96
+ request,
97
+ HostStreams::new(read, poll, wait, write, close),
98
+ |running| {
99
+ on_execution
100
+ .call1(&JsValue::UNDEFINED, &JsValue::from_bool(running))
101
+ .map(|_| ())
102
+ .map_err(|error| RunError::Runtime(format!("execution observer failed: {error:?}")))
103
+ },
104
+ ) {
105
+ Ok(result) => InteractiveSideResponse {
106
+ ok: true,
107
+ result: Some(result),
108
+ error: None,
109
+ },
110
+ Err(error) => InteractiveSideResponse {
111
+ ok: false,
112
+ result: None,
113
+ error: Some(RunFailure {
114
+ code: error.code(),
115
+ message: error.to_string(),
116
+ }),
117
+ },
118
+ };
81
119
  response
82
120
  .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
83
121
  .map_err(|error| {
84
122
  JsValue::from_str(&format!(
85
- "failed to serialize interactive response: {error}"
123
+ "failed to serialize interactive side response: {error}"
86
124
  ))
87
125
  })
88
126
  }
127
+
128
+ #[derive(Serialize)]
129
+ #[serde(rename_all = "camelCase")]
130
+ struct InteractiveSideResponse {
131
+ ok: bool,
132
+ result: Option<crate::run::web_interactive::InteractiveSideResult>,
133
+ error: Option<RunFailure>,
134
+ }