Files
moli/moli-renderer-v8/src/runtime/page.rs
ldm0 ca9514fc4d refactor(navigation): admit document configuration with its commit
Capture target configuration inside the exact navigation admission and carry it in the renderer commit command. Apply it before consuming the prepared Document so a separate configuration acknowledgement cannot let an older snapshot overwrite an update accepted before admission.

Use the same configured commit for buffered, error-document and compatibility navigation, and remove the protocol update-then-commit adapter. Cover additions and removals of queued preloads for new and replacement Pages, plus author-visible bindings, permissions and worlds.

Validation: cargo fmt --all; full workspace Clippy with warnings denied; cargo nextest run --no-fail-fast (20919 passed, 19 skipped); 12 CLI CDP scenarios and Playwright routing smoke passed.
2026-10-29 17:39:23 +08:00

921 lines
32 KiB
Rust

use std::{
sync::atomic::AtomicU64,
sync::{Arc, Weak},
};
use anyhow::{Result, anyhow};
#[cfg(test)]
use tokio::sync::oneshot;
use url::Url;
use crate::{
local_executor::JsLocalExecutor, network::ResourceRequestClient,
render_runtime::RenderRuntimeOwner, types::ScriptExecutionReport,
};
use super::{
DocumentStartScript, ExternalRawDocumentBodyStream, PageVmInitStage,
RendererBrowserContextRuntime, RendererBrowserContextRuntimeOwner,
RendererBrowserContextRuntimeOwnerAccess, RendererDocumentCommitPermit,
RendererDocumentIsolateAccountingDiagnostics, RendererInspectorSessionRestoreSnapshot,
RendererOwnerCommand, RendererOwnerHandle, RendererOwnerReply, RendererPageCreationArtifacts,
RendererPageCreationDiagnostics, RendererPageHandle, RendererPageReservationToken,
RendererPageState, RendererPageStateRevision, RendererPendingDownloadActivation,
RendererPerformanceMetricSnapshot, RendererReservedServiceWorkerClient,
};
/// Optional document bootstrap inputs shared by HTML and streaming creation.
/// Applied before document-start scripts; live updates use Page commands.
#[derive(Default)]
pub struct RendererDocumentOptions {
pub indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
pub storage_bucket_store: Option<crate::context_bootstrap::SharedStorageBucketStore>,
pub document_start_scripts: Vec<DocumentStartScript>,
pub runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
pub extra_http_headers: moli_fetch::RequestHeaders,
pub document_settings: moli_page_types::DocumentSettings,
pub network_offline: bool,
pub blocked_url_patterns: Vec<String>,
pub fetch_subresource_interception_enabled: bool,
pub fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
pub runtime_inspector_session_restore_snapshots: Vec<RendererInspectorSessionRestoreSnapshot>,
pub root_frame_id: Option<String>,
pub main_document_commit: Option<crate::RendererMainDocumentCommit>,
}
pub(crate) struct PageVmStateCapture {
pub(crate) snapshot_revision: RendererPageStateRevision,
pub(crate) final_url: Url,
pub(crate) document_title: String,
pub(crate) document_activity: moli_page_types::DocumentActivity,
pub(crate) report: Arc<ScriptExecutionReport>,
pub(crate) navigation_response: Option<PageVmNavigationResponse>,
pub(crate) idle_override: Option<crate::protocol_types::EmulatedIdleOverride>,
pub(crate) service_worker_client_id: u64,
pub(crate) document_isolate_identity: usize,
pub(crate) dedicated_worker_running_worker_isolate_count: usize,
pub(crate) performance_metric_snapshot: RendererPerformanceMetricSnapshot,
}
#[derive(Debug, Clone)]
pub(crate) struct PageVmNavigationResponse {
pub(crate) requested_url: Url,
pub(crate) redirected: bool,
pub(crate) redirect_count: usize,
pub(crate) redirect_chain: Vec<crate::protocol_types::NavigationRedirect>,
pub(crate) status: u16,
pub(crate) headers: Vec<(String, Vec<u8>)>,
}
#[derive(Debug, Clone)]
pub struct JsRuntime {
inner: Arc<JsRuntimeInner>,
}
/// Thread-affine owner returned by the standalone JS runtime constructor.
/// Explicit browser-context callers keep their owner at the outer root and use
/// `JsRuntime` as a renderer handle only.
#[derive(Debug)]
pub struct JsRuntimeOwner {
runtime: Option<JsRuntime>,
_browser_context_owner: RendererBrowserContextRuntimeOwner,
}
#[derive(Debug)]
struct JsRuntimeInner {
renderer_owner: RendererOwnerHandle,
browser_context_runtime: RendererBrowserContextRuntime,
_render_runtime: RenderRuntimeOwner,
}
/// A renderer-owner lifetime capability. Keeping its recursive actor/channel
/// graph behind this interface also bounds Send/Sync proofs in protocol crates.
pub(in crate::runtime) trait RendererRuntimeLease:
Send + Sync + std::fmt::Debug
{
fn into_runtime(self: Arc<Self>) -> JsRuntime;
fn cancel_page_producers(&self);
fn cancel_pending_auxiliary_page(&self, reservation: RendererPageReservationToken);
fn release_captured_document_environment(&self, id: u64);
}
impl RendererRuntimeLease for JsRuntimeInner {
fn release_captured_document_environment(&self, id: u64) {
let _ = self.renderer_owner.enqueue_command_with_reply(
RendererOwnerCommand::ReleaseCapturedDocumentEnvironment { id },
);
}
fn into_runtime(self: Arc<Self>) -> JsRuntime {
JsRuntime { inner: self }
}
fn cancel_page_producers(&self) {
cancel_js_runtime_page_producers(self);
}
fn cancel_pending_auxiliary_page(&self, reservation: RendererPageReservationToken) {
let _ = self.renderer_owner.enqueue_command_with_reply(
RendererOwnerCommand::CancelPendingAuxiliaryPage { reservation },
);
}
}
#[derive(Clone)]
pub(crate) struct RendererProducerShutdownHandle {
renderer_owner_id: u64,
runtime: Weak<dyn RendererRuntimeLease>,
}
impl std::fmt::Debug for RendererProducerShutdownHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RendererProducerShutdownHandle")
.field("renderer_owner_id", &self.renderer_owner_id)
.finish_non_exhaustive()
}
}
impl RendererProducerShutdownHandle {
pub(in crate::runtime) fn upgrade_runtime(&self) -> Option<Arc<dyn RendererRuntimeLease>> {
self.runtime.upgrade()
}
pub(crate) fn renderer_owner_id(&self) -> u64 {
self.renderer_owner_id
}
pub(crate) fn cancel_page_producers(&self) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.cancel_page_producers();
}
}
pub(crate) fn is_live(&self) -> bool {
self.runtime.strong_count() != 0
}
}
fn cancel_js_runtime_page_producers(inner: &JsRuntimeInner) {
inner.renderer_owner.terminate_for_context_owner_shutdown();
}
impl Default for JsRuntimeOwner {
fn default() -> Self {
JsRuntime::initialize()
}
}
impl JsRuntime {
pub fn initialize() -> JsRuntimeOwner {
let browser_context_owner = RendererBrowserContextRuntime::new();
let runtime = Self::initialize_with_browser_context_owner_access(
&browser_context_owner.owner_access(),
)
.expect("standalone browser context owner must accept its JS runtime");
JsRuntimeOwner {
runtime: Some(runtime),
_browser_context_owner: browser_context_owner,
}
}
/// Creates a JS runtime and atomically registers its producer shutdown
/// capability with the matching browser-context owner root.
pub fn initialize_with_browser_context_owner_access(
browser_context_owner_access: &RendererBrowserContextRuntimeOwnerAccess,
) -> Result<Self> {
let runtime = Self::initialize_with_browser_context_runtime_inner(
browser_context_owner_access.runtime(),
);
browser_context_owner_access
.register_renderer_producer(&runtime)
.map_err(|error| anyhow!("browser context producer owner unavailable: {error}"))?;
Ok(runtime)
}
pub fn initialize_with_page_vm_document_isolate_for_diagnostics() -> JsRuntimeOwner {
Self::initialize()
}
fn initialize_with_browser_context_runtime_inner(
browser_context_runtime: RendererBrowserContextRuntime,
) -> Self {
moli_v8_init::ensure_v8_initialized_with_flags(
Some(crate::v8_platform::initialization_flags()),
crate::v8_platform::create_platform,
);
let local_executor = JsLocalExecutor::new();
let next_page_id = Arc::new(AtomicU64::new(1));
let (renderer_owner, render_runtime) = RendererOwnerHandle::new(
local_executor.clone(),
next_page_id.clone(),
browser_context_runtime.clone(),
);
let runtime = Self {
inner: Arc::new(JsRuntimeInner {
renderer_owner,
browser_context_runtime,
_render_runtime: render_runtime,
}),
};
assert!(
runtime
.inner
.renderer_owner
.state
.runtime_handle
.set(runtime.producer_shutdown_handle())
.is_ok()
);
runtime
}
pub fn browser_context_runtime(&self) -> RendererBrowserContextRuntime {
self.inner.browser_context_runtime.clone()
}
/// Cancels Page/DedicatedWorker producers before an outer owner root joins
/// browser resource runtimes.
pub fn terminate_resource_producers_for_owner_shutdown(&self) {
cancel_js_runtime_page_producers(&self.inner);
self.inner
.browser_context_runtime
.terminate_resource_producers_for_owner_shutdown();
}
pub(crate) fn producer_shutdown_handle(&self) -> RendererProducerShutdownHandle {
let runtime: Arc<dyn RendererRuntimeLease> = self.inner.clone();
RendererProducerShutdownHandle {
renderer_owner_id: self.renderer_owner_id_for_diagnostics(),
runtime: Arc::downgrade(&runtime),
}
}
#[cfg(test)]
pub(crate) fn install_owner_command_dispatch_gate_for_testing(
&self,
) -> (
crossbeam_channel::Receiver<()>,
crossbeam_channel::Sender<()>,
) {
self.inner
.renderer_owner
.install_command_dispatch_gate_for_testing()
}
#[cfg(test)]
pub(crate) fn publish_next_command_output_before_owner_settlement_for_testing(&self) {
self.inner
.renderer_owner
.publish_next_command_output_before_settlement_for_testing();
}
#[cfg(test)]
pub(crate) fn close_owner_command_admission_for_testing(&self) {
self.inner
.renderer_owner
.close_command_admission_for_testing();
}
#[cfg(test)]
pub(crate) fn renderer_page_count_for_testing(&self) -> usize {
self.inner.renderer_owner.len()
}
#[cfg(test)]
pub(crate) fn start_minimal_html_page_for_reservation_testing(
&self,
page_reservation: RendererPageReservationToken,
loader: &ResourceRequestClient,
) -> Result<PendingHtmlPage> {
let url = Url::parse("https://reservation.test/")
.expect("static reservation test URL should parse");
self.start_create_html_page_from_response(
page_reservation,
url.clone(),
url,
None,
false,
0,
200,
vec![("content-type".to_owned(), b"text/html".to_vec())],
loader,
crate::RendererWebStorageHandles::ephemeral(),
"<!doctype html>".to_owned(),
None,
crate::RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
crate::RendererDocumentOptions {
..Default::default()
},
)
}
#[cfg(test)]
pub(crate) fn enqueue_owner_command_probe_for_testing(
&self,
) -> Result<oneshot::Receiver<Result<RendererOwnerReply>>> {
self.inner
.renderer_owner
.enqueue_command_with_reply(RendererOwnerCommand::TestingDeferredPageVmDropPendingCount)
}
#[cfg(test)]
pub(crate) fn try_attach_page_slot_for_testing(
&self,
) -> (
Result<()>,
super::page_context_cancel::RendererPageContextCancelReceiver,
) {
let page_id = self.inner.renderer_owner.allocate_page_id();
let (cancel_tx, cancel_rx) =
super::page_context_cancel::renderer_page_context_cancel_channel();
let slot = super::RendererPageSlotHandle::new(
Arc::downgrade(&self.inner.renderer_owner.state),
super::RendererPageEntry::removed(page_id),
cancel_tx,
Default::default(),
);
let result = self
.inner
.renderer_owner
.state
.page_table
.insert_new_slot(page_id, slot)
.map(|_| ());
(result, cancel_rx)
}
pub fn document_isolate_model_for_diagnostics(&self) -> &'static str {
"related-pages"
}
pub fn document_isolate_accounting_for_diagnostics(
&self,
) -> RendererDocumentIsolateAccountingDiagnostics {
crate::script_vm::renderer_document_isolate_accounting_diagnostics()
}
pub fn renderer_owner_id_for_diagnostics(&self) -> u64 {
self.inner.renderer_owner.state.owner_local_host_id.as_u64()
}
pub fn shares_renderer_owner_with(&self, other: &Self) -> bool {
Arc::ptr_eq(
&self.inner.renderer_owner.state,
&other.inner.renderer_owner.state,
)
}
pub fn renderer_owner_handle(&self) -> RendererOwnerHandle {
self.inner.renderer_owner.clone()
}
pub fn set_renderer_output_transport_sender(
&self,
sender: crate::runtime::RendererOutputTransportSender,
) {
self.inner
.renderer_owner
.set_renderer_output_transport_sender(sender);
}
#[allow(clippy::too_many_arguments)]
pub async fn create_html_page_from_response(
&self,
requested_url: Url,
final_url: Url,
navigation_initiator_url: Option<Url>,
navigation_redirected: bool,
navigation_redirect_count: usize,
response_status: u16,
response_headers: Vec<(String, Vec<u8>)>,
loader: &ResourceRequestClient,
web_storage: crate::RendererWebStorageHandles,
html: String,
options: RendererDocumentOptions,
) -> Result<(
RendererPageHandle,
Arc<RendererPageState>,
RendererPageCreationDiagnostics,
RendererPageCreationArtifacts,
Option<RendererPendingDownloadActivation>,
)> {
let request = self
.inner
.renderer_owner
.build_create_html_page_request_with_env(
self.reserve_page_for_creation(),
requested_url,
navigation_initiator_url,
navigation_redirected,
navigation_redirect_count,
response_status,
response_headers,
loader,
web_storage,
final_url,
html,
PageVmInitStage::Load,
options,
);
let reply = self
.inner
.renderer_owner
.dispatch_command(RendererOwnerCommand::CreateHtmlPage(request))
.await?;
self.inner
.renderer_owner
.materialize_page_created_reply_parts(reply)
}
#[allow(clippy::too_many_arguments)]
pub fn start_create_html_page_from_response(
&self,
page_reservation: RendererPageReservationToken,
requested_url: Url,
final_url: Url,
navigation_initiator_url: Option<Url>,
navigation_redirected: bool,
navigation_redirect_count: usize,
response_status: u16,
response_headers: Vec<(String, Vec<u8>)>,
loader: &ResourceRequestClient,
web_storage: crate::RendererWebStorageHandles,
html: String,
top_level_storage_key: Option<moli_storage_key::MoliStorageKey>,
top_level_navigation_dispatch: crate::RendererTopLevelNavigationDispatch,
options: RendererDocumentOptions,
) -> Result<PendingHtmlPage> {
let mut request = self
.inner
.renderer_owner
.build_create_html_page_request_with_env(
page_reservation,
requested_url,
navigation_initiator_url,
navigation_redirected,
navigation_redirect_count,
response_status,
response_headers,
loader,
web_storage,
final_url,
html,
PageVmInitStage::Load,
options,
);
request.top_level_storage_key = top_level_storage_key;
request.top_level_navigation_dispatch = top_level_navigation_dispatch;
let reply_rx = self
.inner
.renderer_owner
.enqueue_command_with_reply(RendererOwnerCommand::CreateHtmlPage(request))?;
Ok(PendingHtmlPage {
runtime: self.clone(),
reply_rx,
})
}
/// Reserves the exact owner-local identity of a Page before its creation
/// command can enter the renderer queue.
///
/// Protocol coordinators that must route pre-install output bind this
/// identity to their target first, then pass it to
/// [`Self::start_create_html_page_from_response`].
pub fn reserve_page_for_creation(&self) -> RendererPageReservationToken {
self.inner.renderer_owner.allocate_page_reservation_token()
}
#[allow(clippy::too_many_arguments)]
pub async fn create_streaming_raw_page_from_external_body(
&self,
requested_url: Url,
final_url: Url,
navigation_initiator_url: Option<Url>,
navigation_redirected: bool,
navigation_redirect_count: usize,
navigation_redirect_chain: Vec<crate::protocol_types::NavigationRedirect>,
response_status: u16,
response_headers: Vec<(String, Vec<u8>)>,
loader: &ResourceRequestClient,
web_storage: crate::RendererWebStorageHandles,
raw_body: ExternalRawDocumentBodyStream,
wpt_extensions_enabled: bool,
stage: PageVmInitStage,
reply_boundary: crate::RendererReplyBoundary,
top_level_navigation_dispatch: crate::RendererTopLevelNavigationDispatch,
navigation_reply_policy: crate::RendererNavigationReplyPolicy,
reserved_service_worker_client: Option<RendererReservedServiceWorkerClient>,
lifecycle_decider: Option<crate::RendererLifecycleDecider>,
options: RendererDocumentOptions,
) -> Result<(
RendererPageHandle,
Arc<RendererPageState>,
RendererPageCreationDiagnostics,
RendererPageCreationArtifacts,
Option<RendererPendingDownloadActivation>,
)> {
let prepared = self
.prepare_streaming_raw_document_from_external_body(
self.reserve_page_for_creation(),
requested_url,
final_url,
navigation_initiator_url,
navigation_redirected,
navigation_redirect_count,
navigation_redirect_chain,
response_status,
response_headers,
loader,
web_storage,
raw_body,
wpt_extensions_enabled,
stage,
reply_boundary,
top_level_navigation_dispatch,
navigation_reply_policy,
reserved_service_worker_client,
lifecycle_decider,
options,
)
.await?;
let permit = prepared.issue_commit_permit();
prepared.commit(permit).await
}
/// Moves a streaming response and all document bootstrap inputs onto the
/// renderer owner lane without starting the parser or author scripts.
#[allow(clippy::too_many_arguments)]
pub async fn prepare_streaming_raw_document_from_external_body(
&self,
page_target: impl Into<super::RendererDocumentPreparationTarget>,
requested_url: Url,
final_url: Url,
navigation_initiator_url: Option<Url>,
navigation_redirected: bool,
navigation_redirect_count: usize,
navigation_redirect_chain: Vec<crate::protocol_types::NavigationRedirect>,
response_status: u16,
response_headers: Vec<(String, Vec<u8>)>,
loader: &ResourceRequestClient,
web_storage: crate::RendererWebStorageHandles,
raw_body: ExternalRawDocumentBodyStream,
wpt_extensions_enabled: bool,
stage: PageVmInitStage,
reply_boundary: crate::RendererReplyBoundary,
top_level_navigation_dispatch: crate::RendererTopLevelNavigationDispatch,
navigation_reply_policy: crate::RendererNavigationReplyPolicy,
reserved_service_worker_client: Option<RendererReservedServiceWorkerClient>,
lifecycle_decider: Option<crate::RendererLifecycleDecider>,
options: RendererDocumentOptions,
) -> Result<PreparedRendererDocument> {
let mut request = self
.inner
.renderer_owner
.build_create_streaming_raw_page_request(
requested_url,
final_url,
navigation_initiator_url,
navigation_redirected,
navigation_redirect_count,
navigation_redirect_chain,
response_status,
response_headers,
loader,
web_storage,
raw_body,
stage,
options,
);
request.wpt_extensions_enabled = wpt_extensions_enabled;
request.reply_boundary = reply_boundary;
request.top_level_navigation_dispatch = top_level_navigation_dispatch;
request.navigation_reply_policy = navigation_reply_policy;
request.reserved_service_worker_client = reserved_service_worker_client;
request.lifecycle_decider = lifecycle_decider;
// This is the provisional-load boundary. Wake the old document before
// dispatching owner work: queuing the wake itself would deadlock behind
// a paused script. Downloads and intercepted responses never get here.
let (page_reservation, replacement) = page_target
.into()
.begin_and_reserve(&self.inner.renderer_owner)
.await?;
request.document_replacement = replacement.clone();
// Own cancellation before awaiting the prepare reply, so abandoning
// the future also retires a queued or already stored preparation.
let preparation = RendererDocumentPreparation {
runtime: self.clone(),
token: page_reservation,
cancel_on_drop: true,
_document_replacement: replacement,
};
let reply = self
.inner
.renderer_owner
.dispatch_command(RendererOwnerCommand::PrepareStreamingRawDocument {
token: page_reservation,
request,
})
.await?;
match reply {
RendererOwnerReply::PreparedRendererDocumentStored {
renderer_devtools_agent_token,
} => Ok(PreparedRendererDocument {
preparation,
renderer_devtools_agent_token,
}),
_ => Err(anyhow!(
"renderer owner returned non-prepare reply for prepared document request"
)),
}
}
}
impl JsRuntimeOwner {
pub fn handle(&self) -> JsRuntime {
self.runtime
.as_ref()
.expect("standalone JS runtime owner was already shut down")
.clone()
}
}
impl std::ops::Deref for JsRuntimeOwner {
type Target = JsRuntime;
fn deref(&self) -> &Self::Target {
self.runtime
.as_ref()
.expect("standalone JS runtime owner was already shut down")
}
}
impl Drop for JsRuntimeOwner {
fn drop(&mut self) {
if let Some(runtime) = self.runtime.take() {
runtime.terminate_resource_producers_for_owner_shutdown();
// Join the renderer before the browser-context owner broadcasts
// and joins its network runtimes. Leaving this to implicit field
// drop would reverse that structured-concurrency order.
drop(runtime);
}
self._browser_context_owner.shutdown_and_join();
}
}
/// An opaque handle to owner-local streaming document inputs held before the
/// renderer commit barrier.
///
/// Dropping this handle schedules cancellation. Only a permit issued for this
/// exact handle can consume the owner-local residence and start bootstrap.
pub struct PreparedRendererDocument {
preparation: RendererDocumentPreparation,
renderer_devtools_agent_token: super::RendererDevToolsAgentToken,
}
/// One synchronously enqueued commit and its exact renderer reply. Dropping
/// the wait cancels preparation; it cannot prove that commit never started.
pub struct PendingRendererPageReplacement {
preparation: RendererDocumentPreparation,
reply_rx: tokio::sync::oneshot::Receiver<Result<RendererOwnerReply>>,
}
impl PendingRendererPageReplacement {
pub async fn wait(
mut self,
) -> std::result::Result<
super::RendererPageReplacementCommit,
super::RendererPageReplacementError,
> {
use super::RendererPageReplacementError;
let reply = self
.reply_rx
.await
.map_err(|_| anyhow!("render runtime reply channel closed"))
.and_then(|reply| reply)
.map_err(RendererPageReplacementError::from_dispatch_error)?;
self.preparation.cancel_on_drop = false;
match reply {
RendererOwnerReply::PageReplacementCommitted(replacement) => Ok(*replacement),
_ => Err(RendererPageReplacementError::document_unavailable(anyhow!(
"renderer owner returned a non-replacement commit reply"
))),
}
}
}
struct RendererDocumentPreparation {
runtime: JsRuntime,
token: RendererPageReservationToken,
cancel_on_drop: bool,
_document_replacement:
Option<Arc<super::document_replacement::RendererDocumentReplacementScope>>,
}
impl PreparedRendererDocument {
pub fn token(&self) -> RendererPageReservationToken {
self.preparation.token
}
pub fn renderer_devtools_agent_token(&self) -> super::RendererDevToolsAgentToken {
self.renderer_devtools_agent_token
}
pub fn issue_commit_permit(&self) -> RendererDocumentCommitPermit {
RendererDocumentCommitPermit::new(self.token())
}
/// Replaces the live target configuration consumed when the first
/// execution contexts are created.
pub async fn update_commit_configuration(
&self,
configuration: super::RendererPreparedDocumentCommitConfiguration,
) -> Result<()> {
let reply = self
.preparation
.runtime
.inner
.renderer_owner
.dispatch_command(
RendererOwnerCommand::UpdatePreparedRendererDocumentCommitConfiguration {
token: self.token(),
configuration,
},
)
.await?;
match reply {
RendererOwnerReply::PreparedRendererDocumentCommitConfigurationUpdated => Ok(()),
_ => Err(anyhow!(
"renderer owner returned non-update reply for prepared document configuration"
)),
}
}
pub fn start_commit(
mut self,
permit: RendererDocumentCommitPermit,
configuration: Option<super::RendererPreparedDocumentCommitConfiguration>,
) -> Result<
impl std::future::Future<
Output = Result<(
RendererPageHandle,
Arc<RendererPageState>,
RendererPageCreationDiagnostics,
RendererPageCreationArtifacts,
Option<RendererPendingDownloadActivation>,
)>,
> + 'static,
> {
anyhow::ensure!(
permit.prepared_document() == self.token(),
"renderer document commit permit does not belong to this prepared document"
);
let reply_rx = self
.preparation
.runtime
.inner
.renderer_owner
.enqueue_command_with_reply(RendererOwnerCommand::CommitPreparedRendererDocument {
permit,
configuration: configuration.map(Box::new),
})?;
Ok(async move {
let reply = reply_rx
.await
.map_err(|_| anyhow!("render runtime reply channel closed"))??;
self.preparation.cancel_on_drop = false;
self.preparation
.runtime
.inner
.renderer_owner
.materialize_page_created_reply_parts(reply)
})
}
pub async fn commit(
self,
permit: RendererDocumentCommitPermit,
) -> Result<(
RendererPageHandle,
Arc<RendererPageState>,
RendererPageCreationDiagnostics,
RendererPageCreationArtifacts,
Option<RendererPendingDownloadActivation>,
)> {
self.start_commit(permit, None)?.await
}
/// Commits a response into the Page that issued this replacement reservation.
/// The reply must be adopted by that Page's existing handle.
pub fn start_commit_page_replacement(
self,
permit: RendererDocumentCommitPermit,
configuration: Option<super::RendererPreparedDocumentCommitConfiguration>,
) -> std::result::Result<PendingRendererPageReplacement, super::RendererPageReplacementError>
{
use super::RendererPageReplacementError;
if permit.prepared_document() != self.token() {
return Err(RendererPageReplacementError::document_preserved(anyhow!(
"renderer document commit permit does not belong to this prepared document"
)));
}
if self.token().replacement.is_none() {
return Err(RendererPageReplacementError::document_preserved(anyhow!(
"a fresh Page reservation cannot replace an existing Page"
)));
}
let reply_rx = self
.preparation
.runtime
.inner
.renderer_owner
.enqueue_command_with_reply(RendererOwnerCommand::CommitPreparedPageReplacement {
permit,
configuration: configuration.map(Box::new),
})
.map_err(RendererPageReplacementError::from_dispatch_error)?;
Ok(PendingRendererPageReplacement {
preparation: self.preparation,
reply_rx,
})
}
pub async fn commit_page_replacement(
self,
permit: RendererDocumentCommitPermit,
) -> std::result::Result<
super::RendererPageReplacementCommit,
super::RendererPageReplacementError,
> {
self.start_commit_page_replacement(permit, None)?
.wait()
.await
}
pub async fn cancel(mut self) -> Result<()> {
let reply = self
.preparation
.runtime
.inner
.renderer_owner
.dispatch_command(RendererOwnerCommand::CancelPreparedRendererDocument {
token: self.token(),
})
.await?;
match reply {
RendererOwnerReply::PreparedRendererDocumentCanceled => {
self.preparation.cancel_on_drop = false;
Ok(())
}
_ => Err(anyhow!(
"renderer owner returned non-cancel reply for prepared document request"
)),
}
}
}
impl Drop for RendererDocumentPreparation {
fn drop(&mut self) {
if !self.cancel_on_drop {
return;
}
let _ = self
.runtime
.inner
.renderer_owner
.enqueue_command_with_reply(RendererOwnerCommand::CancelPreparedRendererDocument {
token: self.token,
});
}
}
/// Handle to an in-flight `CreateHtmlPage` renderer command kicked off via
/// [`JsRuntime::start_create_html_page_from_response`]. The renderer thread
/// is already (or about to be) processing the work; the caller `await`s
/// [`Self::await_ready`] when the resulting page is needed.
pub struct PendingHtmlPage {
runtime: JsRuntime,
reply_rx: tokio::sync::oneshot::Receiver<Result<RendererOwnerReply>>,
}
impl PendingHtmlPage {
pub async fn await_ready(
self,
) -> Result<(
RendererPageHandle,
Arc<RendererPageState>,
RendererPageCreationDiagnostics,
RendererPageCreationArtifacts,
Option<RendererPendingDownloadActivation>,
)> {
let reply = self.reply_rx.await.map_err(|_| {
anyhow!("renderer reply channel closed before pending html page ready")
})??;
self.runtime
.inner
.renderer_owner
.materialize_page_created_reply_parts(reply)
}
}