Skip to main content

worker/
dynamic_dispatch.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5use wasm_bindgen::{JsCast, JsValue};
6use worker_sys::DynamicDispatcher as DynamicDispatcherSys;
7
8use crate::{env::EnvBinding, Fetcher, Result};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
11pub struct DispatchGetOptions {
12    #[serde(flatten)]
13    pub extra: HashMap<String, JsonValue>,
14}
15
16/// A binding for dispatching events to Workers inside of a dispatch namespace by their name. This
17/// allows for your worker to directly invoke many workers by name instead of having multiple
18/// service worker bindings.
19///
20/// # Example:
21///
22/// ```no_run
23/// let dispatcher = env.dynamic_dispatcher("DISPATCHER")?;
24/// let fetcher = dispatcher.get("namespaced-worker-name")?;
25/// let resp = fetcher.fetch_request(req).await?;
26/// ```
27#[derive(Debug, Clone)]
28pub struct DynamicDispatcher(DynamicDispatcherSys);
29
30impl DynamicDispatcher {
31    /// Gets a [Fetcher] for a Worker inside of the dispatch namespace based of the name specified.
32    pub fn get(&self, name: impl Into<String>) -> Result<Fetcher> {
33        let fetcher_sys = self
34            .0
35            .get(name.into(), JsValue::undefined(), JsValue::undefined())?;
36        Ok(fetcher_sys.into())
37    }
38
39    /// Gets a [Fetcher] and passes `args` to the dispatch namespace `get(...)` call.
40    pub fn get_with_args<T: Serialize>(&self, name: impl Into<String>, args: T) -> Result<Fetcher> {
41        let args_js = to_js_value(&args)?;
42        let fetcher_sys = self.0.get(name.into(), args_js, JsValue::undefined())?;
43        Ok(fetcher_sys.into())
44    }
45
46    /// Gets a [Fetcher] and passes both `args` and `options` to the dispatch namespace `get(...)` call.
47    pub fn get_with_options<T: Serialize>(
48        &self,
49        name: impl Into<String>,
50        args: T,
51        options: DispatchGetOptions,
52    ) -> Result<Fetcher> {
53        let args_js = to_js_value(&args)?;
54        let options_js = match dispatch_options_json(options) {
55            Some(options) => to_js_value(&options)?,
56            None => JsValue::undefined(),
57        };
58        let fetcher_sys = self.0.get(name.into(), args_js, options_js)?;
59        Ok(fetcher_sys.into())
60    }
61}
62
63fn to_js_value<T: Serialize>(value: &T) -> Result<JsValue> {
64    value
65        .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
66        .map_err(Into::into)
67}
68
69fn dispatch_options_json(options: DispatchGetOptions) -> Option<JsonValue> {
70    if options.extra.is_empty() {
71        None
72    } else {
73        Some(JsonValue::Object(options.extra.into_iter().collect()))
74    }
75}
76
77impl EnvBinding for DynamicDispatcher {
78    const TYPE_NAME: &'static str = "Object";
79
80    fn get(val: JsValue) -> Result<Self> {
81        if !val.is_object() {
82            return Err("Binding cannot be cast to DynamicDispatcher from non-object value".into());
83        }
84
85        let has_get = js_sys::Reflect::has(&val, &JsValue::from("get"))?;
86        if !has_get {
87            return Err("Binding cannot be cast to DynamicDispatcher: missing `get` method".into());
88        }
89
90        Ok(Self(val.unchecked_into()))
91    }
92}
93
94impl JsCast for DynamicDispatcher {
95    fn instanceof(val: &JsValue) -> bool {
96        val.is_object()
97    }
98
99    fn unchecked_from_js(val: JsValue) -> Self {
100        DynamicDispatcher(val.unchecked_into())
101    }
102
103    fn unchecked_from_js_ref(val: &JsValue) -> &Self {
104        unsafe { &*(val as *const JsValue as *const Self) }
105    }
106}
107
108impl AsRef<JsValue> for DynamicDispatcher {
109    fn as_ref(&self) -> &wasm_bindgen::JsValue {
110        &self.0
111    }
112}
113
114impl From<JsValue> for DynamicDispatcher {
115    fn from(val: JsValue) -> Self {
116        DynamicDispatcher(val.unchecked_into())
117    }
118}
119
120impl From<DynamicDispatcher> for JsValue {
121    fn from(sec: DynamicDispatcher) -> Self {
122        sec.0.into()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use std::collections::HashMap;
129
130    use super::{dispatch_options_json, DispatchGetOptions};
131
132    #[test]
133    fn empty_options_serialize_to_none() {
134        assert!(dispatch_options_json(DispatchGetOptions::default()).is_none());
135    }
136
137    #[test]
138    fn dispatch_options_preserve_extra_fields() {
139        let mut extra = HashMap::new();
140        extra.insert("trace".to_string(), serde_json::json!(true));
141
142        let options = dispatch_options_json(DispatchGetOptions { extra })
143            .expect("options should be present");
144
145        assert_eq!(options["trace"], serde_json::json!(true));
146    }
147}