diff options
-rw-r--r-- | core/bindings.rs | 81 | ||||
-rw-r--r-- | core/lib.deno_core.d.ts | 21 | ||||
-rw-r--r-- | core/runtime.rs | 163 |
3 files changed, 232 insertions, 33 deletions
diff --git a/core/bindings.rs b/core/bindings.rs index 63c7ea4b9..f5eb1705e 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -39,6 +39,18 @@ lazy_static::lazy_static! { function: set_macrotask_callback.map_fn_to() }, v8::ExternalReference { + function: set_nexttick_callback.map_fn_to() + }, + v8::ExternalReference { + function: run_microtasks.map_fn_to() + }, + v8::ExternalReference { + function: has_tick_scheduled.map_fn_to() + }, + v8::ExternalReference { + function: set_has_tick_scheduled.map_fn_to() + }, + v8::ExternalReference { function: eval_context.map_fn_to() }, v8::ExternalReference { @@ -145,6 +157,20 @@ pub fn initialize_context<'s>( "setMacrotaskCallback", set_macrotask_callback, ); + set_func( + scope, + core_val, + "setNextTickCallback", + set_nexttick_callback, + ); + set_func(scope, core_val, "runMicrotasks", run_microtasks); + set_func(scope, core_val, "hasTickScheduled", has_tick_scheduled); + set_func( + scope, + core_val, + "setHasTickScheduled", + set_has_tick_scheduled, + ); set_func(scope, core_val, "evalContext", eval_context); set_func(scope, core_val, "encode", encode); set_func(scope, core_val, "decode", decode); @@ -438,7 +464,36 @@ fn opcall_async<'s>( } } -fn set_macrotask_callback( +fn has_tick_scheduled( + scope: &mut v8::HandleScope, + _args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + let state_rc = JsRuntime::state(scope); + let state = state_rc.borrow(); + rv.set(to_v8(scope, state.has_tick_scheduled).unwrap()); +} + +fn set_has_tick_scheduled( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + + state.has_tick_scheduled = args.get(0).is_true(); +} + +fn run_microtasks( + scope: &mut v8::HandleScope, + _args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + scope.perform_microtask_checkpoint(); +} + +fn set_nexttick_callback( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, _rv: v8::ReturnValue, @@ -451,17 +506,23 @@ fn set_macrotask_callback( Err(err) => return throw_type_error(scope, err.to_string()), }; - let slot = match &mut state.js_macrotask_cb { - slot @ None => slot, - _ => { - return throw_type_error( - scope, - "Deno.core.setMacrotaskCallback() already called", - ); - } + state.js_nexttick_cbs.push(v8::Global::new(scope, cb)); +} + +fn set_macrotask_callback( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + let state_rc = JsRuntime::state(scope); + let mut state = state_rc.borrow_mut(); + + let cb = match v8::Local::<v8::Function>::try_from(args.get(0)) { + Ok(cb) => cb, + Err(err) => return throw_type_error(scope, err.to_string()), }; - slot.replace(v8::Global::new(scope, cb)); + state.js_macrotask_cbs.push(v8::Global::new(scope, cb)); } fn eval_context( diff --git a/core/lib.deno_core.d.ts b/core/lib.deno_core.d.ts index 4a5d6433b..f33f6164a 100644 --- a/core/lib.deno_core.d.ts +++ b/core/lib.deno_core.d.ts @@ -86,5 +86,26 @@ declare namespace Deno { function setWasmStreamingCallback( cb: (source: any, rid: number) => void, ): void; + + /** + * Set a callback that will be called after resolving ops and before resolving + * macrotasks. + */ + function setNextTickCallback( + cb: () => void, + ): void; + + /** Check if there's a scheduled "next tick". */ + function hasNextTickScheduled(): bool; + + /** Set a value telling the runtime if there are "next ticks" scheduled */ + function setHasNextTickScheduled(value: bool): void; + + /** + * Set a callback that will be called after resolving ops and "next ticks". + */ + function setMacrotaskCallback( + cb: () => bool, + ): void; } } diff --git a/core/runtime.rs b/core/runtime.rs index c7831f88f..5d8e9231c 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -158,7 +158,9 @@ pub(crate) struct JsRuntimeState { pub global_context: Option<v8::Global<v8::Context>>, pub(crate) js_recv_cb: Option<v8::Global<v8::Function>>, pub(crate) js_sync_cb: Option<v8::Global<v8::Function>>, - pub(crate) js_macrotask_cb: Option<v8::Global<v8::Function>>, + pub(crate) js_macrotask_cbs: Vec<v8::Global<v8::Function>>, + pub(crate) js_nexttick_cbs: Vec<v8::Global<v8::Function>>, + pub(crate) has_tick_scheduled: bool, pub(crate) js_wasm_streaming_cb: Option<v8::Global<v8::Function>>, pub(crate) pending_promise_exceptions: HashMap<v8::Global<v8::Promise>, v8::Global<v8::Value>>, @@ -363,7 +365,9 @@ impl JsRuntime { dyn_module_evaluate_idle_counter: 0, js_recv_cb: None, js_sync_cb: None, - js_macrotask_cb: None, + js_macrotask_cbs: vec![], + js_nexttick_cbs: vec![], + has_tick_scheduled: false, js_wasm_streaming_cb: None, js_error_create_fn, pending_ops: FuturesUnordered::new(), @@ -773,6 +777,7 @@ impl JsRuntime { // Ops { self.resolve_async_ops(cx)?; + self.drain_nexttick()?; self.drain_macrotasks()?; self.check_promise_exceptions()?; } @@ -1549,34 +1554,73 @@ impl JsRuntime { } fn drain_macrotasks(&mut self) -> Result<(), Error> { - let js_macrotask_cb_handle = - match &Self::state(self.v8_isolate()).borrow().js_macrotask_cb { - Some(handle) => handle.clone(), - None => return Ok(()), - }; + let state = Self::state(self.v8_isolate()); + + if state.borrow().js_macrotask_cbs.is_empty() { + return Ok(()); + } + let js_macrotask_cb_handles = state.borrow().js_macrotask_cbs.clone(); let scope = &mut self.handle_scope(); - let js_macrotask_cb = js_macrotask_cb_handle.open(scope); - // Repeatedly invoke macrotask callback until it returns true (done), - // such that ready microtasks would be automatically run before - // next macrotask is processed. - let tc_scope = &mut v8::TryCatch::new(scope); - let this = v8::undefined(tc_scope).into(); - loop { - let is_done = js_macrotask_cb.call(tc_scope, this, &[]); + for js_macrotask_cb_handle in js_macrotask_cb_handles { + let js_macrotask_cb = js_macrotask_cb_handle.open(scope); - if let Some(exception) = tc_scope.exception() { - return exception_to_err_result(tc_scope, exception, false); - } + // Repeatedly invoke macrotask callback until it returns true (done), + // such that ready microtasks would be automatically run before + // next macrotask is processed. + let tc_scope = &mut v8::TryCatch::new(scope); + let this = v8::undefined(tc_scope).into(); + loop { + let is_done = js_macrotask_cb.call(tc_scope, this, &[]); + + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false); + } + + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(()); + } - if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - break; + let is_done = is_done.unwrap(); + if is_done.is_true() { + break; + } } + } + + Ok(()) + } - let is_done = is_done.unwrap(); - if is_done.is_true() { - break; + fn drain_nexttick(&mut self) -> Result<(), Error> { + let state = Self::state(self.v8_isolate()); + + if state.borrow().js_nexttick_cbs.is_empty() { + return Ok(()); + } + + if !state.borrow().has_tick_scheduled { + let scope = &mut self.handle_scope(); + scope.perform_microtask_checkpoint(); + } + + // TODO(bartlomieju): Node also checks for absence of "rejection_to_warn" + if !state.borrow().has_tick_scheduled { + return Ok(()); + } + + let js_nexttick_cb_handles = state.borrow().js_nexttick_cbs.clone(); + let scope = &mut self.handle_scope(); + + for js_nexttick_cb_handle in js_nexttick_cb_handles { + let js_nexttick_cb = js_nexttick_cb_handle.open(scope); + + let tc_scope = &mut v8::TryCatch::new(scope); + let this = v8::undefined(tc_scope).into(); + js_nexttick_cb.call(tc_scope, this, &[]); + + if let Some(exception) = tc_scope.exception() { + return exception_to_err_result(tc_scope, exception, false); } } @@ -2347,4 +2391,77 @@ assertEquals(1, notify_return_value); .unwrap(); runtime.run_event_loop(false).await.unwrap(); } + + #[tokio::test] + async fn test_set_macrotask_callback_set_next_tick_callback() { + async fn op_async_sleep( + _op_state: Rc<RefCell<OpState>>, + _: (), + _: (), + ) -> Result<(), Error> { + // Future must be Poll::Pending on first call + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + Ok(()) + } + + let extension = Extension::builder() + .ops(vec![("op_async_sleep", op_async(op_async_sleep))]) + .build(); + + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![extension], + ..Default::default() + }); + + runtime + .execute_script( + "macrotasks_and_nextticks.js", + r#" + (async function () { + const results = []; + Deno.core.setMacrotaskCallback(() => { + results.push("macrotask"); + return true; + }); + Deno.core.setNextTickCallback(() => { + results.push("nextTick"); + Deno.core.setHasTickScheduled(false); + }); + + Deno.core.setHasTickScheduled(true); + await Deno.core.opAsync('op_async_sleep'); + if (results[0] != "nextTick") { + throw new Error(`expected nextTick, got: ${results[0]}`); + } + if (results[1] != "macrotask") { + throw new Error(`expected macrotask, got: ${results[1]}`); + } + })(); + "#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap(); + } + + #[tokio::test] + async fn test_set_macrotask_callback_set_next_tick_callback_multiple() { + let mut runtime = JsRuntime::new(Default::default()); + + runtime + .execute_script( + "multiple_macrotasks_and_nextticks.js", + r#" + Deno.core.setMacrotaskCallback(() => { return true; }); + Deno.core.setMacrotaskCallback(() => { return true; }); + Deno.core.setNextTickCallback(() => {}); + Deno.core.setNextTickCallback(() => {}); + "#, + ) + .unwrap(); + let isolate = runtime.v8_isolate(); + let state_rc = JsRuntime::state(isolate); + let state = state_rc.borrow(); + assert_eq!(state.js_macrotask_cbs.len(), 2); + assert_eq!(state.js_nexttick_cbs.len(), 2); + } } |