From 0f6a5c5fc24e8dc9125c5c536c8547a86ca87b15 Mon Sep 17 00:00:00 2001 From: Colin Ihrig Date: Tue, 28 Jun 2022 10:49:30 -0400 Subject: [PATCH] feat(web): add beforeunload event (#14830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the 'beforeunload' event. Co-authored-by: Bartek IwaƄczuk --- cli/lsp/testing/execution.rs | 6 ++++ cli/main.rs | 38 +++++++++++++++++++++---- cli/standalone.rs | 9 +++++- cli/tests/integration/run_tests.rs | 6 ++++ cli/tests/testdata/before_unload.js | 21 ++++++++++++++ cli/tests/testdata/before_unload.js.out | 8 ++++++ cli/tools/bench.rs | 6 ++++ cli/tools/test.rs | 7 +++++ core/01_core.js | 1 + core/ops_builtin_v8.rs | 24 ++++++++++++++++ core/runtime.rs | 32 ++++++++++++++++++--- runtime/js/99_main.js | 1 + runtime/worker.rs | 18 ++++++++++++ 13 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 cli/tests/testdata/before_unload.js create mode 100644 cli/tests/testdata/before_unload.js.out diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs index 6b4e947a0aed96..83f74e5edcb9ae 100644 --- a/cli/lsp/testing/execution.rs +++ b/cli/lsp/testing/execution.rs @@ -226,6 +226,12 @@ async fn test_specifier( worker.js_runtime.resolve_value(test_result).await?; + loop { + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + worker.run_event_loop(false).await?; + } worker.dispatch_unload_event(&located_script_name!())?; } diff --git a/cli/main.rs b/cli/main.rs index e74ed8518b3e53..de44add172f8a6 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -639,7 +639,13 @@ async fn eval_command( } worker.execute_main_module(&main_module).await?; worker.dispatch_load_event(&located_script_name!())?; - worker.run_event_loop(false).await?; + loop { + worker.run_event_loop(false).await?; + + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + } worker.dispatch_unload_event(&located_script_name!())?; Ok(0) } @@ -975,7 +981,12 @@ async fn run_from_stdin(flags: Flags) -> Result { } worker.execute_main_module(&main_module).await?; worker.dispatch_load_event(&located_script_name!())?; - worker.run_event_loop(false).await?; + loop { + worker.run_event_loop(false).await?; + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + } worker.dispatch_unload_event(&located_script_name!())?; Ok(worker.get_exit_code()) } @@ -1014,7 +1025,15 @@ async fn run_with_watch(flags: Flags, script: String) -> Result { self.worker.dispatch_load_event(&located_script_name!())?; self.pending_unload = true; - let result = self.worker.run_event_loop(false).await; + let result = loop { + let result = self.worker.run_event_loop(false).await; + if !self + .worker + .dispatch_beforeunload_event(&located_script_name!())? + { + break result; + } + }; self.pending_unload = false; if let Err(err) = result { @@ -1162,9 +1181,16 @@ async fn run_command( } worker.dispatch_load_event(&located_script_name!())?; - worker - .run_event_loop(maybe_coverage_collector.is_none()) - .await?; + + loop { + worker + .run_event_loop(maybe_coverage_collector.is_none()) + .await?; + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + } + worker.dispatch_unload_event(&located_script_name!())?; if let Some(coverage_collector) = maybe_coverage_collector.as_mut() { diff --git a/cli/standalone.rs b/cli/standalone.rs index d66eb7694a5bf9..50baf70ea6c374 100644 --- a/cli/standalone.rs +++ b/cli/standalone.rs @@ -316,7 +316,14 @@ pub async fn run( ); worker.execute_main_module(main_module).await?; worker.dispatch_load_event(&located_script_name!())?; - worker.run_event_loop(true).await?; + + loop { + worker.run_event_loop(false).await?; + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + } + worker.dispatch_unload_event(&located_script_name!())?; std::process::exit(0); } diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index bd27cd8ddb0e53..1cd1db0ef6b801 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -249,6 +249,12 @@ itest!(webstorage_serialization { output: "webstorage/serialization.ts.out", }); +// tests the beforeunload event +itest!(beforeunload_event { + args: "run before_unload.js", + output: "before_unload.js.out", +}); + // tests to ensure that when `--location` is set, all code shares the same // localStorage cache based on the origin of the location URL. #[test] diff --git a/cli/tests/testdata/before_unload.js b/cli/tests/testdata/before_unload.js new file mode 100644 index 00000000000000..2572e512b91662 --- /dev/null +++ b/cli/tests/testdata/before_unload.js @@ -0,0 +1,21 @@ +let count = 0; + +console.log("0"); + +globalThis.addEventListener("beforeunload", (e) => { + console.log("GOT EVENT"); + if (count === 0 || count === 1) { + e.preventDefault(); + setTimeout(() => { + console.log("3"); + }, 100); + } + + count++; +}); + +console.log("1"); + +setTimeout(() => { + console.log("2"); +}, 100); diff --git a/cli/tests/testdata/before_unload.js.out b/cli/tests/testdata/before_unload.js.out new file mode 100644 index 00000000000000..f1f2ab49a5729f --- /dev/null +++ b/cli/tests/testdata/before_unload.js.out @@ -0,0 +1,8 @@ +0 +1 +2 +GOT EVENT +3 +GOT EVENT +3 +GOT EVENT diff --git a/cli/tools/bench.rs b/cli/tools/bench.rs index 3c40b4e9518d18..3a40a4e970ddc9 100644 --- a/cli/tools/bench.rs +++ b/cli/tools/bench.rs @@ -404,6 +404,12 @@ async fn bench_specifier( worker.js_runtime.resolve_value(bench_result).await?; + loop { + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + worker.run_event_loop(false).await?; + } worker.dispatch_unload_event(&located_script_name!())?; Ok(()) diff --git a/cli/tools/test.rs b/cli/tools/test.rs index ac146fdbf76d75..71374d94ec6452 100644 --- a/cli/tools/test.rs +++ b/cli/tools/test.rs @@ -814,6 +814,13 @@ async fn test_specifier( worker.js_runtime.resolve_value(test_result).await?; + loop { + if !worker.dispatch_beforeunload_event(&located_script_name!())? { + break; + } + worker.run_event_loop(false).await?; + } + worker.dispatch_unload_event(&located_script_name!())?; if let Some(coverage_collector) = maybe_coverage_collector.as_mut() { diff --git a/core/01_core.js b/core/01_core.js index 9337c023142ffd..156300327418c5 100644 --- a/core/01_core.js +++ b/core/01_core.js @@ -267,6 +267,7 @@ destructureError: opSync.bind(null, "op_destructure_error"), terminate: opSync.bind(null, "op_terminate"), opNames: opSync.bind(null, "op_op_names"), + eventLoopHasMoreWork: opSync.bind(null, "op_event_loop_has_more_work"), }); ObjectAssign(globalThis.__bootstrap, { core }); diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index 7f0c58212b3ed1..4bc80faa5568f9 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -47,6 +47,7 @@ pub(crate) fn init_builtins_v8() -> Vec { op_op_names::decl(), op_apply_source_map::decl(), op_set_format_exception_callback::decl(), + op_event_loop_has_more_work::decl(), ] } @@ -786,3 +787,26 @@ fn op_set_format_exception_callback<'a>( let old = old.map(|v| v8::Local::new(scope, v)); Ok(old.map(|v| from_v8(scope, v.into()).unwrap())) } + +#[op(v8)] +fn op_event_loop_has_more_work(scope: &mut v8::HandleScope) -> bool { + let state_rc = JsRuntime::state(scope); + let module_map_rc = JsRuntime::module_map(scope); + let state = state_rc.borrow_mut(); + let module_map = module_map_rc.borrow(); + + let has_pending_refed_ops = state.pending_ops.len() > state.unrefed_ops.len(); + let has_pending_dyn_imports = module_map.has_pending_dynamic_imports(); + let has_pending_dyn_module_evaluation = + !state.pending_dyn_mod_evaluate.is_empty(); + let has_pending_module_evaluation = state.pending_mod_evaluate.is_some(); + let has_pending_background_tasks = scope.has_pending_background_tasks(); + let has_tick_scheduled = state.has_tick_scheduled; + + has_pending_refed_ops + || has_pending_dyn_imports + || has_pending_dyn_module_evaluation + || has_pending_module_evaluation + || has_pending_background_tasks + || has_tick_scheduled +} diff --git a/core/runtime.rs b/core/runtime.rs index 23fe73013323d7..8b150e4e953266 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -90,14 +90,14 @@ pub struct JsRuntime { event_loop_middlewares: Vec>, } -struct DynImportModEvaluate { +pub(crate) struct DynImportModEvaluate { load_id: ModuleLoadId, module_id: ModuleId, promise: v8::Global, module: v8::Global, } -struct ModEvaluate { +pub(crate) struct ModEvaluate { promise: v8::Global, sender: oneshot::Sender>, } @@ -158,8 +158,8 @@ pub(crate) struct JsRuntimeState { pub(crate) js_wasm_streaming_cb: Option>, pub(crate) pending_promise_exceptions: HashMap, v8::Global>, - pending_dyn_mod_evaluate: Vec, - pending_mod_evaluate: Option, + pub(crate) pending_dyn_mod_evaluate: Vec, + pub(crate) pending_mod_evaluate: Option, /// A counter used to delay our dynamic import deadlock detection by one spin /// of the event loop. dyn_module_evaluate_idle_counter: u32, @@ -1021,6 +1021,30 @@ Pending dynamic modules:\n".to_string(); Poll::Pending } + + pub fn event_loop_has_work(&mut self) -> bool { + let state_rc = Self::state(self.v8_isolate()); + let module_map_rc = Self::module_map(self.v8_isolate()); + let state = state_rc.borrow_mut(); + let module_map = module_map_rc.borrow(); + + let has_pending_refed_ops = + state.pending_ops.len() > state.unrefed_ops.len(); + let has_pending_dyn_imports = module_map.has_pending_dynamic_imports(); + let has_pending_dyn_module_evaluation = + !state.pending_dyn_mod_evaluate.is_empty(); + let has_pending_module_evaluation = state.pending_mod_evaluate.is_some(); + let has_pending_background_tasks = + self.v8_isolate().has_pending_background_tasks(); + let has_tick_scheduled = state.has_tick_scheduled; + + has_pending_refed_ops + || has_pending_dyn_imports + || has_pending_dyn_module_evaluation + || has_pending_module_evaluation + || has_pending_background_tasks + || has_tick_scheduled + } } extern "C" fn near_heap_limit_callback( diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 1d046d1618d7d8..c13faa93626136 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -583,6 +583,7 @@ delete Intl.v8BreakIterator; defineEventHandler(window, "error"); defineEventHandler(window, "load"); + defineEventHandler(window, "beforeunload"); defineEventHandler(window, "unload"); const isUnloadDispatched = SymbolFor("isUnloadDispatched"); // Stores the flag for checking whether unload is dispatched or not. diff --git a/runtime/worker.rs b/runtime/worker.rs index acb50dc30552b0..fd937559f33d73 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -371,6 +371,24 @@ impl MainWorker { "dispatchEvent(new Event('unload'))", ) } + + /// Dispatches "beforeunload" event to the JavaScript runtime. Returns a boolean + /// indicating if the event was prevented and thus event loop should continue + /// running. + pub fn dispatch_beforeunload_event( + &mut self, + script_name: &str, + ) -> Result { + let value = self.js_runtime.execute_script( + script_name, + // NOTE(@bartlomieju): not using `globalThis` here, because user might delete + // it. Instead we're using global `dispatchEvent` function which will + // used a saved reference to global scope. + "dispatchEvent(new Event('beforeunload', { cancelable: true }));", + )?; + let local_value = value.open(&mut self.js_runtime.handle_scope()); + Ok(local_value.is_false()) + } } #[cfg(test)]