vertigo/driver_module/api/
api_import.rs

1use std::{collections::HashMap, future::Future, pin::Pin, rc::Rc};
2
3use crate::{
4    driver_module::{event_emitter::EventEmitter, js_value::JsValue},
5    fetch::request_builder::RequestBody,
6    get_driver,
7    struct_mut::ValueMut,
8    transaction, DropResource, FetchMethod, FetchResult, FutureBox, InstantType, JsJson,
9    JsJsonObjectBuilder, WebsocketConnection, WebsocketMessage,
10};
11
12use super::{
13    api_dom_access::DomAccess, arguments::Arguments, callbacks::CallbackStore,
14    panic_message::PanicMessage,
15};
16
17enum ConsoleLogLevel {
18    Debug,
19    Info,
20    Log,
21    Warn,
22    Error,
23}
24
25impl ConsoleLogLevel {
26    pub fn get_str(&self) -> &'static str {
27        match self {
28            Self::Debug => "debug",
29            Self::Info => "info",
30            Self::Log => "log",
31            Self::Warn => "warn",
32            Self::Error => "error",
33        }
34    }
35}
36
37#[derive(Clone)]
38pub struct ApiImport {
39    pub panic_message: PanicMessage,
40    pub fn_dom_access: fn(ptr: u32, size: u32) -> u32,
41
42    pub(crate) arguments: Arguments,
43    pub(crate) callback_store: CallbackStore,
44
45    pub on_fetch_start: EventEmitter<()>,
46    pub on_fetch_stop: EventEmitter<()>,
47}
48
49impl Default for ApiImport {
50    fn default() -> Self {
51        use super::external_api::api::safe_wrappers::{
52            safe_dom_access as fn_dom_access, safe_panic_message as panic_message,
53        };
54
55        let panic_message = PanicMessage::new(panic_message);
56
57        ApiImport {
58            panic_message,
59            fn_dom_access,
60            arguments: Arguments::default(),
61            callback_store: CallbackStore::new(),
62            on_fetch_start: EventEmitter::default(),
63            on_fetch_stop: EventEmitter::default(),
64        }
65    }
66}
67
68impl ApiImport {
69    pub fn show_panic_message(&self, message: String) {
70        self.panic_message.show(message);
71    }
72
73    fn console_4(&self, kind: ConsoleLogLevel, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
74        self.dom_access()
75            .root("window")
76            .get("console")
77            .call(
78                kind.get_str(),
79                vec![
80                    JsValue::str(arg1),
81                    JsValue::str(arg2),
82                    JsValue::str(arg3),
83                    JsValue::str(arg4),
84                ],
85            )
86            .exec();
87    }
88
89    pub fn console_debug_4(&self, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
90        self.console_4(ConsoleLogLevel::Debug, arg1, arg2, arg3, arg4)
91    }
92
93    pub fn console_log_4(&self, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
94        self.console_4(ConsoleLogLevel::Log, arg1, arg2, arg3, arg4)
95    }
96
97    pub fn console_info_4(&self, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
98        self.console_4(ConsoleLogLevel::Info, arg1, arg2, arg3, arg4)
99    }
100
101    pub fn console_warn_4(&self, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
102        self.console_4(ConsoleLogLevel::Warn, arg1, arg2, arg3, arg4)
103    }
104
105    pub fn console_error_4(&self, arg1: &str, arg2: &str, arg3: &str, arg4: &str) {
106        self.console_4(ConsoleLogLevel::Error, arg1, arg2, arg3, arg4)
107    }
108
109    pub fn cookie_get(&self, cname: &str) -> String {
110        let result = self
111            .dom_access()
112            .api()
113            .get("cookie")
114            .call("get", vec![JsValue::str(cname)])
115            .fetch();
116
117        if let JsValue::String(value) = result {
118            value
119        } else {
120            log::error!("cookie_get -> params decode error -> result={result:?}");
121            String::from("")
122        }
123    }
124
125    pub fn cookie_get_json(&self, cname: &str) -> JsJson {
126        if self.is_browser() {
127            let result = self
128                .dom_access()
129                .api()
130                .get("cookie")
131                .call("get_json", vec![JsValue::str(cname)])
132                .fetch();
133
134            if result != JsValue::Null {
135                if let JsValue::Json(value) = result {
136                    return value;
137                }
138                log::error!("cookie_get_json -> params decode error -> result={result:?}");
139            }
140        }
141        JsJson::Null
142    }
143
144    pub fn cookie_set(&self, cname: &str, cvalue: &str, expires_in: u64) {
145        if self.is_browser() {
146            self.dom_access()
147                .api()
148                .get("cookie")
149                .call(
150                    "set",
151                    vec![
152                        JsValue::str(cname),
153                        JsValue::str(cvalue),
154                        JsValue::U64(expires_in),
155                    ],
156                )
157                .exec();
158        } else {
159            log::warn!("Can't set cookie on server side");
160        }
161    }
162
163    pub fn cookie_set_json(&self, cname: &str, cvalue: JsJson, expires_in: u64) {
164        self.dom_access()
165            .api()
166            .get("cookie")
167            .call(
168                "set_json",
169                vec![
170                    JsValue::str(cname),
171                    JsValue::Json(cvalue),
172                    JsValue::U64(expires_in),
173                ],
174            )
175            .exec();
176    }
177
178    pub fn interval_set<F: Fn() + 'static>(&self, duration: u32, callback: F) -> DropResource {
179        let (callback_id, drop_callback) = self.callback_store.register(move |_| {
180            callback();
181            JsValue::Undefined
182        });
183
184        let result = self
185            .dom_access()
186            .api()
187            .get("interval")
188            .call(
189                "interval_set",
190                vec![JsValue::U32(duration), JsValue::U64(callback_id.as_u64())],
191            )
192            .fetch();
193
194        let timer_id = if let JsValue::I32(timer_id) = result {
195            timer_id
196        } else {
197            log::error!("interval_set -> expected i32 -> result={result:?}");
198            0
199        };
200
201        let api = self.clone();
202
203        DropResource::new(move || {
204            api.dom_access()
205                .api()
206                .get("interval")
207                .call("interval_clear", vec![JsValue::I32(timer_id)])
208                .exec();
209
210            drop_callback.off();
211        })
212    }
213
214    pub fn timeout_set<F: Fn() + 'static>(&self, duration: u32, callback: F) -> DropResource {
215        let (callback_id, drop_callback) = self.callback_store.register(move |_| {
216            callback();
217            JsValue::Undefined
218        });
219
220        let result = self
221            .dom_access()
222            .api()
223            .get("interval")
224            .call(
225                "timeout_set",
226                vec![JsValue::U32(duration), JsValue::U64(callback_id.as_u64())],
227            )
228            .fetch();
229
230        let timer_id = if let JsValue::I32(timer_id) = result {
231            timer_id
232        } else {
233            log::error!("timeout_set -> expected i32 -> result={result:?}");
234            0
235        };
236
237        let api = self.clone();
238
239        DropResource::new(move || {
240            api.dom_access()
241                .api()
242                .get("interval")
243                .call("interval_clear", vec![JsValue::I32(timer_id)])
244                .exec();
245
246            drop_callback.off();
247        })
248    }
249
250    pub fn set_timeout_and_detach<F: Fn() + 'static>(&self, duration: u32, callback: F) {
251        let drop_box: Rc<ValueMut<Option<DropResource>>> = Rc::new(ValueMut::new(None));
252
253        let callback_with_drop = {
254            let drop_box = drop_box.clone();
255
256            move || {
257                callback();
258                drop_box.set(None);
259            }
260        };
261
262        let drop = self.timeout_set(duration, callback_with_drop);
263        drop_box.set(Some(drop));
264    }
265
266    pub fn instant_now(&self) -> InstantType {
267        self.utc_now() as InstantType
268    }
269
270    pub fn utc_now(&self) -> i64 {
271        let result = self
272            .dom_access()
273            .root("window")
274            .get("Date")
275            .call("now", vec![])
276            .fetch();
277
278        match result {
279            JsValue::I64(time) => time,
280            JsValue::F64(time) => time as i64,
281            _ => {
282                self.panic_message
283                    .show(format!("api.utc_now -> incorrect result {result:?}"));
284                0_i64
285            }
286        }
287    }
288
289    pub fn timezone_offset(&self) -> i32 {
290        let result = self
291            .dom_access()
292            .api()
293            .call("getTimezoneOffset", vec![])
294            .fetch();
295
296        if let JsValue::I32(result) = result {
297            // Return in seconds to be compatible with chrono
298            // Opposite as JS returns the offset backwards
299            result * -60
300        } else {
301            self.panic_message.show(format!(
302                "api.timezone_offset -> incorrect result {result:?}"
303            ));
304            0
305        }
306    }
307
308    pub fn history_back(&self) {
309        self.dom_access()
310            .root("window")
311            .get("history")
312            .call("back", Vec::new())
313            .exec();
314    }
315
316    ///////////////////////////////////////////////////////////////////////////////////
317    // hash router
318    ///////////////////////////////////////////////////////////////////////////////////
319
320    pub fn get_hash_location(&self) -> String {
321        let result = self
322            .dom_access()
323            .api()
324            .get("hashRouter")
325            .call("get", Vec::new())
326            .fetch();
327
328        if let JsValue::String(value) = result {
329            value
330        } else {
331            log::error!("hashRouter -> params decode error -> result={result:?}");
332            String::from("")
333        }
334    }
335
336    pub fn push_hash_location(&self, new_hash: &str) {
337        self.dom_access()
338            .api()
339            .get("hashRouter")
340            .call("push", vec![JsValue::str(new_hash)])
341            .exec();
342    }
343
344    pub fn on_hash_change<F: Fn(String) + 'static>(&self, callback: F) -> DropResource {
345        let (callback_id, drop_callback) = self.callback_store.register(move |data| {
346            let new_hash = if let JsValue::String(new_hash) = data {
347                new_hash
348            } else {
349                log::error!("on_hash_route_change -> string was expected -> {data:?}");
350                String::from("")
351            };
352
353            transaction(|_| {
354                callback(new_hash);
355            });
356
357            JsValue::Undefined
358        });
359
360        self.dom_access()
361            .api()
362            .get("hashRouter")
363            .call("add", vec![JsValue::U64(callback_id.as_u64())])
364            .exec();
365
366        let api = self.clone();
367
368        DropResource::new(move || {
369            api.dom_access()
370                .api()
371                .get("hashRouter")
372                .call("remove", vec![JsValue::U64(callback_id.as_u64())])
373                .exec();
374
375            drop_callback.off();
376        })
377    }
378
379    ///////////////////////////////////////////////////////////////////////////////////
380    // history router
381    ///////////////////////////////////////////////////////////////////////////////////
382
383    pub fn get_history_location(&self) -> String {
384        let result = self
385            .dom_access()
386            .api()
387            .get("historyLocation")
388            .call("get", Vec::new())
389            .fetch();
390
391        if let JsValue::String(value) = result {
392            self.route_from_public(value)
393        } else {
394            log::error!("historyLocation -> params decode error -> result={result:?}");
395            String::from("")
396        }
397    }
398
399    pub fn push_history_location(&self, new_path: &str) {
400        self.dom_access()
401            .api()
402            .get("historyLocation")
403            .call("push", vec![JsValue::str(new_path)])
404            .exec();
405    }
406
407    pub fn replace_history_location(&self, new_hash: &str) {
408        self.dom_access()
409            .api()
410            .get("historyLocation")
411            .call("replace", vec![JsValue::str(new_hash)])
412            .exec();
413    }
414
415    pub fn on_history_change<F: Fn(String) + 'static>(&self, callback: F) -> DropResource {
416        let myself = self.clone();
417        let (callback_id, drop_callback) = self.callback_store.register(move |data| {
418            let new_local_path = if let JsValue::String(new_path) = data {
419                myself.route_from_public(new_path.clone())
420            } else {
421                    log::error!("on_history_change -> string was expected -> {data:?}");
422                String::from("")
423            };
424
425            transaction(|_| {
426                callback(new_local_path);
427            });
428
429            JsValue::Undefined
430        });
431
432        self.dom_access()
433            .api()
434            .get("historyLocation")
435            .call("add", vec![JsValue::U64(callback_id.as_u64())])
436            .exec();
437
438        let api = self.clone();
439
440        DropResource::new(move || {
441            api.dom_access()
442                .api()
443                .get("historyLocation")
444                .call("remove", vec![JsValue::U64(callback_id.as_u64())])
445                .exec();
446
447            drop_callback.off();
448        })
449    }
450
451    ///////////////////////////////////////////////////////////////////////////////////
452    ///////////////////////////////////////////////////////////////////////////////////
453
454    pub fn fetch(
455        &self,
456        method: FetchMethod,
457        url: String,
458        headers: Option<HashMap<String, String>>,
459        body: Option<RequestBody>,
460    ) -> Pin<Box<dyn Future<Output = FetchResult> + 'static>> {
461        let (sender, receiver) = FutureBox::new();
462
463        self.on_fetch_start.trigger(());
464
465        let on_fetch_stop = self.on_fetch_stop.clone();
466
467        let callback_id = self.callback_store.register_once(move |params| {
468            let params = params.convert(|mut params| {
469                let success = params.get_bool("success")?;
470                let status = params.get_u32("status")?;
471                let response = params.get_any("response")?;
472                params.expect_no_more()?;
473
474                if let JsValue::Json(json) = response {
475                    return Ok((success, status, RequestBody::Json(json)));
476                }
477
478                if let JsValue::String(text) = response {
479                    return Ok((success, status, RequestBody::Text(text)));
480                }
481
482                if let JsValue::Vec(buffer) = response {
483                    return Ok((success, status, RequestBody::Binary(buffer)));
484                }
485
486                let name = response.typename();
487                Err(format!(
488                    "Expected json or string or vec<u8>, received={name}"
489                ))
490            });
491
492            match params {
493                Ok((success, status, response)) => {
494                    get_driver().transaction(|_| {
495                        let response = match success {
496                            true => Ok((status, response)),
497                            false => Err(format!("{response:#?}")),
498                        };
499                        sender.publish(response);
500                        on_fetch_stop.trigger(());
501                    });
502                }
503                Err(error) => {
504                    log::error!("export_fetch_callback -> params decode error -> {error}");
505                    on_fetch_stop.trigger(());
506                }
507            }
508
509            JsValue::Undefined
510        });
511
512        let headers = {
513            let mut headers_builder = JsJsonObjectBuilder::default();
514
515            if let Some(headers) = headers {
516                for (key, value) in headers.into_iter() {
517                    headers_builder = headers_builder.insert(key, value);
518                }
519            }
520
521            headers_builder.get()
522        };
523
524        self.dom_access()
525            .api()
526            .get("fetch")
527            .call(
528                "fetch_send_request",
529                vec![
530                    JsValue::U64(callback_id.as_u64()),
531                    JsValue::String(method.to_str()),
532                    JsValue::String(url),
533                    JsValue::Json(headers),
534                    match body {
535                        Some(RequestBody::Text(body)) => JsValue::String(body),
536                        Some(RequestBody::Json(json)) => JsValue::Json(json),
537                        Some(RequestBody::Binary(bin)) => JsValue::Vec(bin),
538                        None => JsValue::Undefined,
539                    },
540                ],
541            )
542            .exec();
543
544        Box::pin(receiver)
545    }
546
547    #[must_use]
548    pub fn websocket<F: Fn(WebsocketMessage) + 'static>(
549        &self,
550        host: impl Into<String>,
551        callback: F,
552    ) -> DropResource {
553        let host: String = host.into();
554
555        let api = self.clone();
556
557        let (callback_id, drop_callback) =
558            self.callback_store
559                .register_with_id(move |callback_id, data| {
560                    if let JsValue::True = data {
561                        let connection = WebsocketConnection::new(api.clone(), callback_id);
562                        let connection = WebsocketMessage::Connection(connection);
563                        callback(connection);
564                        return JsValue::Undefined;
565                    }
566
567                    if let JsValue::String(message) = data {
568                        callback(WebsocketMessage::Message(message));
569                        return JsValue::Undefined;
570                    }
571
572                    if let JsValue::False = data {
573                        callback(WebsocketMessage::Close);
574                        return JsValue::Undefined;
575                    }
576
577                    log::error!("websocket - unsupported message type received");
578                    JsValue::Undefined
579                });
580
581        self.websocket_register_callback(host.as_str(), callback_id.as_u64());
582
583        DropResource::new({
584            let api = self.clone();
585
586            move || {
587                api.websocket_unregister_callback(callback_id.as_u64());
588                drop_callback.off();
589            }
590        })
591    }
592
593    fn websocket_register_callback(&self, host: &str, callback_id: u64) {
594        self.dom_access()
595            .api()
596            .get("websocket")
597            .call(
598                "websocket_register_callback",
599                vec![JsValue::String(host.to_string()), JsValue::U64(callback_id)],
600            )
601            .exec();
602    }
603
604    fn websocket_unregister_callback(&self, callback_id: u64) {
605        self.dom_access()
606            .api()
607            .get("websocket")
608            .call(
609                "websocket_unregister_callback",
610                vec![JsValue::U64(callback_id)],
611            )
612            .exec();
613    }
614
615    pub fn websocket_send_message(&self, callback_id: u64, message: &str) {
616        self.dom_access()
617            .api()
618            .get("websocket")
619            .call(
620                "websocket_send_message",
621                vec![
622                    JsValue::U64(callback_id),
623                    JsValue::String(message.to_string()),
624                ],
625            )
626            .exec();
627    }
628
629    pub fn dom_bulk_update(&self, value: JsJson) {
630        self.dom_access()
631            .api()
632            .get("dom")
633            .call("dom_bulk_update", vec![JsValue::Json(value)])
634            .exec();
635    }
636
637    pub fn dom_access(&self) -> DomAccess {
638        DomAccess::new(
639            self.panic_message,
640            self.arguments.clone(),
641            self.fn_dom_access,
642        )
643    }
644
645    pub fn get_random(&self, min: u32, max: u32) -> u32 {
646        let result = self
647            .dom_access()
648            .api()
649            .call("getRandom", vec![JsValue::U32(min), JsValue::U32(max)])
650            .fetch();
651
652        if let JsValue::I32(result) = result {
653            result as u32
654        } else {
655            self.panic_message
656                .show(format!("api.get_random -> incorrect result {result:?}"));
657            min
658        }
659    }
660
661    pub fn is_browser(&self) -> bool {
662        let result = self
663            .dom_access()
664            .api()
665            .call("isBrowser", Vec::new())
666            .fetch();
667
668        if let JsValue::True = result {
669            return true;
670        }
671
672        if let JsValue::False = result {
673            return false;
674        }
675
676        log::error!("logical value expected");
677        false
678    }
679
680    pub fn get_env(&self, name: String) -> Option<String> {
681        let result = self
682            .dom_access()
683            .api()
684            .call("get_env", vec![JsValue::String(name)])
685            .fetch();
686
687        if let JsValue::Null = result {
688            return None;
689        }
690
691        if let JsValue::String(value) = result {
692            return Some(value);
693        }
694
695        log::error!("get_env: string or null was expected");
696        None
697    }
698
699    pub fn route_from_public(&self, path: impl Into<String>) -> String {
700        let path: String = path.into();
701        if self.is_browser() {
702            // In the browser use env variable attached during SSR
703            let mount_point = self.get_env("vertigo-mount-point".to_string()).unwrap_or_else(|| "/".to_string());
704            if mount_point != "/" {
705                path.trim_start_matches(&mount_point).to_string()
706            } else {
707                path
708            }
709        } else {
710            // On the server no need to do anything
711            path
712        }
713    }
714
715    /// Synthetic command to respond with plain text, not DOM
716    pub fn plain_response(&self, body: String) {
717        if self.is_browser() {
718            return;
719        }
720
721        self.dom_access()
722            .synthetic("plain_response", JsValue::String(body))
723            .exec();
724    }
725
726    /// Synthetic command to respond with custom status code from SSR
727    pub fn set_status(&self, status: u16) {
728        if self.is_browser() {
729            return;
730        }
731
732        self.dom_access()
733            .synthetic("set_status", JsValue::U32(status as u32))
734            .exec();
735    }
736}