mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 16:10:01 +00:00
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.
921 lines
32 KiB
Rust
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)
|
|
}
|
|
}
|