wasm_framework/
lib.rs

1#![deny(missing_docs)]
2//! Base support for wasm service using Confluence Workers
3//!
4use async_trait::async_trait;
5use js_sys::{Function, Reflect};
6use service_logging::{log, LogEntry, LogQueue, Logger, Severity};
7use std::cell::RefCell;
8use std::fmt;
9use wasm_bindgen::JsValue;
10
11mod error;
12pub use error::Error;
13mod method;
14pub use method::Method;
15mod request;
16pub use request::Request;
17mod response;
18pub use response::{Body, Response};
19mod media_type;
20pub use media_type::media_type;
21
22/// re-export url::Url
23pub use url::Url;
24
25mod context;
26pub use context::Context;
27mod httpdate;
28pub(crate) mod js_values;
29pub use httpdate::HttpDate;
30
31/// Logging support for deferred tasks
32#[derive(Debug)]
33pub struct RunContext {
34    /// queue of deferred messages
35    pub log_queue: RefCell<LogQueue>,
36}
37
38// workers are single-threaded
39unsafe impl Sync for RunContext {}
40
41impl RunContext {
42    /// log message (used by log! macro)
43    pub fn log(&self, entry: LogEntry) {
44        self.log_queue.borrow_mut().log(entry);
45        /*
46        let mut guard = match self.log_queue.lock() {
47            Ok(guard) => guard,
48            Err(_poisoned) => {
49                // lock shouldn't be poisoned because we don't have panics in production wasm,
50                // so this case shouldn't occur
51                return;
52            }
53        };
54        guard.log(entry);
55         */
56    }
57}
58
59/// Runnable trait for deferred tasks
60/// Deferred tasks are often useful for logging and analytics.
61/// ```rust
62/// use std::{rc::Rc,sync::Mutex};;
63/// use async_trait::async_trait;
64/// use service_logging::{log,Logger,LogQueue,Severity};
65/// use wasm_service::{Runnable,RunContext};
66///
67/// struct Data { s: String }
68/// #[async_trait]
69/// impl Runnable for Data {
70///     async fn run(&self, ctx: &RunContext) {
71///         log!(ctx, Severity::Info, msg: format!("Deferred with data: {}", self.s ));
72///     }
73/// }
74/// ```
75#[async_trait]
76pub trait Runnable {
77    /// Execute a deferred task. The task may append
78    /// logs to `lq` using the [`log`] macro. Logs generated
79    /// are sent to the log service after all deferred tasks have run.
80    ///
81    /// Note that if there is a failure sending logs to the logging service,
82    /// those log messages (and the error from the send failure) will be unreported.
83    async fn run(&self, ctx: &RunContext);
84}
85
86/// Generic page error return - doesn't require ctx
87#[derive(Clone, Debug)]
88pub struct HandlerReturn {
89    /// status code (default: 200)
90    pub status: u16,
91    /// body text
92    pub text: String,
93}
94
95impl fmt::Display for HandlerReturn {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "({},{})", self.status, self.text)
98    }
99}
100
101/// Generate handler return "error"
102pub fn handler_return(status: u16, text: &str) -> HandlerReturn {
103    HandlerReturn {
104        status,
105        text: text.to_string(),
106    }
107}
108
109impl Default for HandlerReturn {
110    fn default() -> Self {
111        Self {
112            status: 200,
113            text: String::default(),
114        }
115    }
116}
117
118/// Trait that defines app/service's request handler and router
119/// See [rustwasm-service-template](https://github.com/stevelr/rustwasm-service-template/blob/master/src/lib.rs)
120///   for a more complete example
121///
122///```rust
123/// use service_logging::{Severity::Verbose,log,Logger};
124/// use wasm_service::{Context,Handler,HandlerReturn,Request};
125/// use async_trait::async_trait;
126/// struct MyHandler {}
127/// #[async_trait(?Send)]
128/// impl Handler for MyHandler {
129///     /// Process incoming Request
130///     async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn> {
131///         // log all incoming requests
132///         log!(ctx, Verbose, method: req.method(), url: req.url());
133///         match (req.method(), req.url().path()) {
134///             (GET, "/hello") => {
135///                 ctx.response().content_type("text/plain; charset=UTF-8").unwrap()
136///                               .text("Hello world!");
137///             }
138///             _ => {
139///                 ctx.response().status(404).text("Not Found");
140///             }
141///         }
142///         Ok(())
143///     }
144/// }
145///```
146#[async_trait(?Send)]
147pub trait Handler {
148    /// Implementation of application request handler
149    async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn>;
150}
151
152/// Configuration parameters for service
153/// Parameter E is your crate's error type
154pub struct ServiceConfig {
155    /// Logger
156    pub logger: Box<dyn Logger>,
157
158    /// Request handler
159    pub handlers: Vec<Box<dyn Handler>>,
160
161    /// how to handle internal errors. This function should modify ctx.response()
162    /// with results, which, for example, could include rendering a page or sending
163    /// a redirect. The default implementation returns status 200 with a short text message.
164    pub internal_error_handler: fn(req: &Request, ctx: &mut Context),
165
166    /// how to handle Not Found (404) responses.  This function should modify ctx.response()
167    /// with results, which, for example, could include rendering a page or sending
168    /// a redirect. The default implementation returns status 404 with a short text message.
169    pub not_found_handler: fn(req: &Request, ctx: &mut Context),
170}
171
172impl Default for ServiceConfig {
173    /// Default construction of ServiceConfig does no logging and handles no requests.
174    fn default() -> ServiceConfig {
175        ServiceConfig {
176            logger: service_logging::silent_logger(),
177            handlers: Vec::new(),
178            internal_error_handler: default_internal_error_handler,
179            not_found_handler: default_not_found_handler,
180        }
181    }
182}
183
184struct DeferredData {
185    tasks: Vec<Box<dyn Runnable + std::panic::UnwindSafe>>,
186    logs: Vec<LogEntry>,
187    logger: Box<dyn Logger>,
188}
189
190/// Entrypoint for wasm-service. Converts parameters from javascript into [Request],
191/// invokes app-specific [Handler](trait.Handler.html), and converts [`Response`] to javascript.
192/// Also sends logs to [Logger](https://docs.rs/service-logging/0.3/service_logging/trait.Logger.html) and runs deferred tasks.
193pub async fn service_request(req: JsValue, config: ServiceConfig) -> Result<JsValue, JsValue> {
194    let mut is_err = false;
195    let map = js_sys::Map::from(req);
196    let req = Request::from_js(&map)?;
197    let mut ctx = Context::default();
198    let mut handler_result = Ok(());
199    for handler in config.handlers.iter() {
200        handler_result = handler.handle(&req, &mut ctx).await;
201        if ctx.is_internal_error().is_some() {
202            (config.internal_error_handler)(&req, &mut ctx);
203            is_err = true;
204            break;
205        }
206        // if handler set response, or returned HandlerReturn (which is a response), stop iter
207        if handler_result.is_err() || !ctx.response().is_unset() {
208            break;
209        }
210    }
211    if let Err(result) = handler_result {
212        // Convert HandlerReturn to status/body
213        ctx.response().status(result.status).text(result.text);
214    } else if ctx.response().is_unset() {
215        // If NO handler set a response, it's content not found
216        // the not-found handler might return a static page or redirect
217        (config.not_found_handler)(&req, &mut ctx);
218    }
219    let response = ctx.take_response();
220    if response.get_status() < 200 || response.get_status() > 307 {
221        is_err = true;
222    }
223    let severity = if response.get_status() == 404 {
224        Severity::Warning
225    } else if is_err {
226        Severity::Error
227    } else {
228        Severity::Info
229    };
230    log!(ctx, severity, _:"service", method: req.method(), url: req.url(), status: response.get_status());
231    if is_err {
232        // if any error occurred, send logs now; fast path (on success) defers logging
233        // also, if there was an error, don't execute deferred tasks
234        let _ = config
235            .logger
236            .send("http", ctx.take_logs())
237            .await
238            .map_err(|e| {
239                ctx.response()
240                    .header("X-service-log-err-ret", e.to_string())
241                    .unwrap()
242            });
243    } else {
244        // From incoming request, extract 'event' object, and get ref to its 'waitUntil' function
245        let js_event =
246            js_sys::Object::from(check_defined(map.get(&"event".into()), "missing event")?);
247        let wait_func = Function::from(
248            Reflect::get(&js_event, &JsValue::from_str("waitUntil"))
249                .map_err(|_| "event without waitUntil")?,
250        );
251        // this should always return OK (event has waitUntil property) unless api is broken.
252        let promise = deferred_promise(Box::new(DeferredData {
253            tasks: ctx.take_tasks(),
254            logs: ctx.take_logs(),
255            logger: config.logger,
256        }));
257        let _ = wait_func.call1(&js_event, &promise); // todo: handle result
258    }
259    Ok(response.into_js())
260}
261
262/// Default implementation of internal error handler
263/// Sets status to 200 and returns a short error message
264fn default_internal_error_handler(req: &Request, ctx: &mut Context) {
265    let error = ctx.is_internal_error();
266    log!(ctx, Severity::Error, _:"InternalError", url: req.url(),
267        error: error.map(|e| e.to_string()).unwrap_or_else(|| String::from("none")));
268    ctx.response()
269        .status(200)
270        .content_type(mime::TEXT_PLAIN_UTF_8)
271        .unwrap()
272        .text("Sorry, an internal error has occurred. It has been logged.");
273}
274
275/// Default implementation of not-found handler.
276/// Sets status to 404 and returns a short message "Not Found"
277pub fn default_not_found_handler(req: &Request, ctx: &mut Context) {
278    log!(ctx, Severity::Info, _:"NotFound", url: req.url());
279    ctx.response()
280        .status(404)
281        .content_type(mime::TEXT_PLAIN_UTF_8)
282        .unwrap()
283        .text("Not Found");
284}
285
286/// Future task that will run deferred. Includes deferred logs plus user-defined tasks.
287/// This function contains a rust async wrapped in a Javascript Promise that will be passed
288/// to the event.waitUntil function, so it gets processed after response is returned.
289fn deferred_promise(args: Box<DeferredData>) -> js_sys::Promise {
290    wasm_bindgen_futures::future_to_promise(async move {
291        // send first set of logs
292        if let Err(e) = args.logger.send("http", args.logs).await {
293            log_log_error(e);
294        }
295        // run each deferred task
296        // let log_queue = Mutex::new(LogQueue::default());
297        let log_queue = RefCell::new(LogQueue::default());
298        let run_ctx = RunContext { log_queue };
299        for t in args.tasks.iter() {
300            t.run(&run_ctx).await;
301        }
302
303        // if any logs were generated during processing of deferred tasks, send those
304        let logs = run_ctx.log_queue.borrow_mut().take();
305        if let Err(e) = args.logger.send("http", logs).await {
306            log_log_error(e);
307        }
308        // all done, return nothing
309        Ok(JsValue::undefined())
310    })
311}
312
313/// Returns javascript value, or Err if undefined
314fn check_defined(v: JsValue, msg: &str) -> Result<JsValue, JsValue> {
315    if v.is_undefined() {
316        return Err(JsValue::from_str(msg));
317    }
318    Ok(v)
319}
320
321/// logging fallback: if we can't send to external logger,
322/// log to "console" so it can be seen in worker logs
323fn log_log_error(e: Box<dyn std::error::Error>) {
324    web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
325        "Error sending logs: {:?}",
326        e
327    )))
328}