Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ members = [
"examples/context",
"examples/cookie",
"examples/datastar",
"examples/deferred-view",
"examples/error",
"examples/font",
"examples/hello-world",
Expand Down
2 changes: 2 additions & 0 deletions crates/topcoat-asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod config;
mod error;
mod manifest;
mod options;
mod resource;
#[cfg(feature = "router")]
mod router;
#[cfg(feature = "serve")]
Expand All @@ -26,6 +27,7 @@ pub use config::*;
pub use error::*;
pub use manifest::*;
pub use options::*;
pub use resource::*;
#[cfg(feature = "router")]
pub use router::*;
#[cfg(feature = "serve")]
Expand Down
57 changes: 57 additions & 0 deletions crates/topcoat-asset/src/resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use topcoat_core::{
context::Cx,
error::Result,
response_event::{ClientResource, ClientResourceKind},
};

use crate::{Asset, asset_config};

/// An asset the browser should load while the response is streaming.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AssetResource {
/// A stylesheet loaded with a `<link rel="stylesheet">` element.
Stylesheet(Asset),
/// A JavaScript module loaded with a `<script type="module">` element.
Module(Asset),
}

impl Asset {
/// Loads this asset as a stylesheet while the response is streaming.
#[must_use]
pub const fn stylesheet(self) -> AssetResource {
AssetResource::Stylesheet(self)
}

/// Loads this asset as a JavaScript module while the response is streaming.
#[must_use]
pub const fn module(self) -> AssetResource {
AssetResource::Module(self)
}
}

/// Adds streaming asset requirements to a request context.
pub trait CxAssetExt {
/// Starts loading `resource` in the browser as soon as this requirement is
/// flushed to the response. Repeated requirements are deduplicated.
///
/// # Errors
///
/// Returns an error if the same internal resource key is used for
/// incompatible requirements.
fn require_asset(&self, resource: AssetResource) -> Result<()>;
}

impl CxAssetExt for Cx {
fn require_asset(&self, resource: AssetResource) -> Result<()> {
let (asset, kind, kind_key) = match resource {
AssetResource::Stylesheet(asset) => {
(asset, ClientResourceKind::Stylesheet, "stylesheet")
}
AssetResource::Module(asset) => (asset, ClientResourceKind::Module, "module"),
};
let key = format!("asset:{kind_key}:{:016x}", asset.id().as_u64());
let url = asset_config(self).resolve(asset);

self.require_client_resource(ClientResource { key, kind, url })
}
}
3 changes: 3 additions & 0 deletions crates/topcoat-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ build = []
anyhow.workspace = true
anymap3.workspace = true
boxcar.workspace = true
getrandom.workspace = true
hashbrown.workspace = true
http.workspace = true
pin-project-lite.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["sync"] }

Expand Down
79 changes: 69 additions & 10 deletions crates/topcoat-core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::{any::Any, ops::Deref, sync::Arc};
pub use context_map::*;
pub use id::*;

pub use crate::memoize::MemoizeAsRef;
use crate::{abort::AbortStore, memoize::MemoizeCache};
use crate::{abort::AbortStore, memoize::MemoizeCache, response_event::ResponseEvents};
pub use crate::{memoize::MemoizeAsRef, response_event::JsonKey};

/// The request context.
///
Expand All @@ -20,9 +20,10 @@ use crate::{abort::AbortStore, memoize::MemoizeCache};
pub struct Cx {
id: CxId,
app_context: Arc<ContextMap>,
request_context: ContextMap,
memoize_cache: MemoizeCache,
abort_store: AbortStore,
request_context: Arc<ContextMap>,
memoize_cache: Arc<MemoizeCache>,
abort_store: Arc<AbortStore>,
pub(crate) response_events: Arc<ResponseEvents>,
}

impl Cx {
Expand All @@ -31,17 +32,61 @@ impl Cx {
Self {
id: CxId::new(),
app_context,
request_context,
memoize_cache: MemoizeCache::new(),
abort_store: AbortStore::new(),
request_context: Arc::new(request_context),
memoize_cache: Arc::new(MemoizeCache::new()),
abort_store: Arc::new(AbortStore::new()),
response_events: Arc::new(ResponseEvents::new()),
}
}

/// Returns this context's unique [`CxId`].
#[inline]
#[must_use]
pub fn id(&self) -> CxId {
self.id
}

/// Returns an owned handle to this request context.
///
/// The handle keeps request and app context values alive while a response
/// stream is still producing deferred views.
#[must_use]
pub fn handle(&self) -> CxHandle {
CxHandle(Self {
id: self.id,
app_context: Arc::clone(&self.app_context),
request_context: Arc::clone(&self.request_context),
memoize_cache: Arc::clone(&self.memoize_cache),
abort_store: Arc::clone(&self.abort_store),
response_events: Arc::clone(&self.response_events),
})
}
}

/// An owned handle to a request [`Cx`].
///
/// It dereferences to `Cx`, so context helpers accept `&handle`.
#[derive(Debug)]
pub struct CxHandle(Cx);

impl Clone for CxHandle {
fn clone(&self) -> Self {
self.0.handle()
}
}

impl AsRef<Cx> for CxHandle {
fn as_ref(&self) -> &Cx {
&self.0
}
}

impl Deref for CxHandle {
type Target = Cx;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// Assembles the request context for an in-flight request.
Expand Down Expand Up @@ -72,11 +117,17 @@ impl CxBuilder {
///
/// A type can hold only one value at a time, so registering a type that is
/// already present replaces it and hands back the displaced value.
///
/// # Panics
///
/// Panics if the request context was shared before the builder finished.
pub fn insert<T>(&mut self, value: T) -> Option<T>
where
T: Any + Send + Sync,
{
self.cx.request_context.insert(value)
Arc::get_mut(&mut self.cx.request_context)
.expect("request context was shared before it was built")
.insert(value)
}

/// Returns `true` if a value of type `T` has been registered on the request
Expand All @@ -101,12 +152,18 @@ impl CxBuilder {

/// Returns a mutable reference to the request context value of type `T`, or
/// `None` if no such value has been registered.
///
/// # Panics
///
/// Panics if the request context was shared before the builder finished.
#[must_use]
pub fn get_mut<T>(&mut self) -> Option<&mut T>
where
T: Any + Send + Sync,
{
self.cx.request_context.get_mut::<T>()
Arc::get_mut(&mut self.cx.request_context)
.expect("request context was shared before it was built")
.get_mut::<T>()
}

/// Consumes the builder, returning the finished [`Cx`].
Expand Down Expand Up @@ -170,12 +227,14 @@ impl CxTestBuilder {

#[inline]
#[doc(hidden)]
#[must_use]
pub fn memoize_cache(cx: &Cx) -> &MemoizeCache {
&cx.memoize_cache
}

#[inline]
#[doc(hidden)]
#[must_use]
pub fn abort_store(cx: &Cx) -> &AbortStore {
&cx.abort_store
}
2 changes: 2 additions & 0 deletions crates/topcoat-core/src/context/context_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ where
/// }
/// ```
#[track_caller]
#[must_use]
pub fn app_context<T>(cx: &Cx) -> &T
where
T: Any + Send + Sync,
Expand Down Expand Up @@ -131,6 +132,7 @@ where
/// }
/// ```
#[track_caller]
#[must_use]
pub fn request_context<T>(cx: &Cx) -> &T
where
T: Any + Send + Sync,
Expand Down
2 changes: 2 additions & 0 deletions crates/topcoat-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ pub mod error;
pub mod fnv1a;
pub mod internal;
pub mod memoize;
#[doc(hidden)]
pub mod response_event;
Loading