use core::{
fmt,
future::{ready, Future},
};
use http_kit::http_error;
use skyzen_core::Extractor;
use crate::StatusCode;
#[derive(Debug)]
pub struct WorkerContextError(String);
impl WorkerContextError {
#[cfg(target_arch = "wasm32")]
fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl fmt::Display for WorkerContextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "the runtime refused post-response work: {}", self.0)
}
}
impl std::error::Error for WorkerContextError {}
impl http_kit::HttpError for WorkerContextError {
fn status(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
http_error!(
pub WorkerContextNotConfigured,
StatusCode::INTERNAL_SERVER_ERROR,
"Execution context not available. On Workers it is threaded in by `#[skyzen::main]`; \
natively it is provided by the built-in runtime, not by an embedding host."
);
#[derive(Clone, Debug)]
pub struct WorkerContext(Inner);
impl Extractor for WorkerContext {
type Error = WorkerContextNotConfigured;
fn extract(
request: &mut crate::Request,
) -> impl Future<Output = Result<Self, Self::Error>> + Send {
ready(
request
.extensions()
.get::<Self>()
.cloned()
.ok_or_else(WorkerContextNotConfigured::new),
)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug)]
pub struct ShutdownGuard(pub async_channel::Sender<core::convert::Infallible>);
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone)]
struct Inner {
executor: std::sync::Arc<executor_core::AnyExecutor>,
guard: ShutdownGuard,
}
#[cfg(not(target_arch = "wasm32"))]
impl fmt::Debug for Inner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WorkerContext").finish_non_exhaustive()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl WorkerContext {
#[must_use]
pub const fn new(
executor: std::sync::Arc<executor_core::AnyExecutor>,
guard: ShutdownGuard,
) -> Self {
Self(Inner { executor, guard })
}
pub fn wait_until<F>(&self, future: F) -> Result<(), WorkerContextError>
where
F: Future<Output = ()> + Send + 'static,
{
use executor_core::Executor as _;
let guard = self.0.guard.0.clone();
self.0
.executor
.spawn(async move {
let _guard = guard;
future.await;
})
.detach();
Ok(())
}
}
#[cfg(target_arch = "wasm32")]
#[derive(Clone)]
struct Inner(wasm_bindgen::JsValue);
#[cfg(target_arch = "wasm32")]
unsafe impl Send for Inner {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for Inner {}
#[cfg(target_arch = "wasm32")]
impl fmt::Debug for Inner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WorkerContext").finish_non_exhaustive()
}
}
#[cfg(target_arch = "wasm32")]
impl WorkerContext {
#[must_use]
pub const fn new(context: super::wasm::ExecutionContext) -> Self {
Self(Inner(context))
}
#[must_use]
pub const fn as_js(&self) -> &wasm_bindgen::JsValue {
&self.0 .0
}
pub fn wait_until<F>(&self, future: F) -> Result<(), WorkerContextError>
where
F: Future<Output = ()> + 'static,
{
let promise = wasm_bindgen_futures::future_to_promise(async move {
future.await;
Ok(wasm_bindgen::JsValue::UNDEFINED)
});
self.call_method("waitUntil", Some(&promise.into()))
}
pub fn props<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, WorkerContextError> {
use wasm_bindgen::JsValue;
let props = js_sys::Reflect::get(&self.0 .0, &JsValue::from_str("props"))
.map_err(|error| WorkerContextError::new(format!("{error:?}")))?;
if props.is_undefined() || props.is_null() {
return Ok(None);
}
serde_wasm_bindgen::from_value(props)
.map(Some)
.map_err(|error| {
WorkerContextError::new(format!(
"the calling Worker's `ctx.props` did not deserialize: {error}"
))
})
}
pub fn pass_through_on_exception(&self) -> Result<(), WorkerContextError> {
self.call_method("passThroughOnException", None)
}
fn call_method(
&self,
name: &str,
argument: Option<&wasm_bindgen::JsValue>,
) -> Result<(), WorkerContextError> {
use wasm_bindgen::{JsCast as _, JsValue};
let context = &self.0 .0;
let method = js_sys::Reflect::get(context, &JsValue::from_str(name))
.map_err(|error| WorkerContextError::new(format!("{error:?}")))?;
let method = method.dyn_into::<js_sys::Function>().map_err(|_| {
WorkerContextError::new(format!(
"the execution context has no `{name}` method; is this a WinterCG fetch handler?"
))
})?;
let called = argument.map_or_else(
|| method.call0(context),
|argument| method.call1(context, argument),
);
called
.map(|_| ())
.map_err(|error| WorkerContextError::new(format!("{error:?}")))
}
}