Repository: denoland/deno_core
Pull request #512 (2024-01-29 19:27:52)

feat(testing): Add a worker subsystem to checkin
We can spawn a worker and send messages to it (and it can send messages back), and terminate it.


Patch:
diff --git a/core/cppgc.rs b/core/cppgc.rs
index ccdace1..a6fec26 100644
--- a/core/cppgc.rs
+++ b/core/cppgc.rs
@@ -18,7 +18,7 @@ const SLOT_OFFSET: i32 = 1;
 const FIELD_COUNT: usize = 2;
 
 pub fn make_cppgc_object<'a, T: 'static>(
-  scope: &'a mut v8::HandleScope,
+  scope: &mut v8::HandleScope<'a>,
   t: T,
 ) -> v8::Local<'a, v8::Object> {
   let templ = v8::ObjectTemplate::new(scope);
diff --git a/core/extension_set.rs b/core/extension_set.rs
index 03ecfa3..d1a5b28 100644
--- a/core/extension_set.rs
+++ b/core/extension_set.rs
@@ -16,12 +16,7 @@ use crate::OpMetricsFactoryFn;
 use crate::OpState;
 
 /// Contribute to the `OpState` from each extension.
-pub fn setup_op_state(
-  op_state: &mut OpState,
-  deno_core_ext: &mut Extension,
-  extensions: &mut [Extension],
-) {
-  deno_core_ext.take_state(op_state);
+pub fn setup_op_state(op_state: &mut OpState, extensions: &mut [Extension]) {
   for ext in extensions {
     ext.take_state(op_state);
   }
@@ -32,19 +27,19 @@ pub fn setup_op_state(
 // but not added to "ext:core/ops" virtual module.
 /// Collects ops from extensions & applies middleware
 pub fn init_ops(
-  deno_core_ext: &mut Extension,
+  deno_core_ops: &'static [OpDecl],
   extensions: &mut [Extension],
 ) -> Vec<OpDecl> {
   // In debug build verify there that inter-Extension dependencies
   // are setup correctly.
   #[cfg(debug_assertions)]
-  check_extensions_dependencies(deno_core_ext, extensions);
+  check_extensions_dependencies(extensions);
 
   let no_of_ops = extensions
     .iter()
     .map(|e| e.op_count())
     .fold(0, |ext_ops_count, count| count + ext_ops_count);
-  let mut ops = Vec::with_capacity(no_of_ops + deno_core_ext.op_count());
+  let mut ops = Vec::with_capacity(no_of_ops + deno_core_ops.len());
 
   // Collect all middlewares - deno_core extension must not have a middleware!
   let middlewares: Vec<Box<OpMiddlewareFn>> = extensions
@@ -56,11 +51,10 @@ pub fn init_ops(
   let macroware = move |d| middlewares.iter().fold(d, |d, m| m(d));
 
   // Collect ops from all extensions and apply a macroware to each of them.
-  let ext_ops = deno_core_ext.init_ops();
-  for ext_op in ext_ops {
+  for core_op in deno_core_ops {
     ops.push(OpDecl {
-      name: ext_op.name,
-      ..macroware(*ext_op)
+      name: core_op.name,
+      ..macroware(*core_op)
     });
   }
 
@@ -83,14 +77,10 @@ pub fn init_ops(
 
 /// This functions panics if any of the extensions is missing its dependencies.
 #[cfg(debug_assertions)]
-fn check_extensions_dependencies(
-  deno_core_ext: &Extension,
-  exts: &[Extension],
-) {
+fn check_extensions_dependencies(exts: &[Extension]) {
   for (index, ext) in exts.iter().enumerate() {
-    let mut previous_exts = vec![deno_core_ext];
-    previous_exts.extend_from_slice(&exts[..index].iter().collect::<Vec<_>>());
-    ext.check_dependencies(&previous_exts);
+    let previous_exts = &exts[..index];
+    ext.check_dependencies(previous_exts);
   }
 }
 
diff --git a/core/extensions.rs b/core/extensions.rs
index 22088a9..e5f43f7 100644
--- a/core/extensions.rs
+++ b/core/extensions.rs
@@ -554,7 +554,7 @@ impl Extension {
   /// Check if dependencies have been loaded, and errors if either:
   /// - The extension is depending on itself or an extension with the same name.
   /// - A dependency hasn't been loaded yet.
-  pub fn check_dependencies(&self, previous_exts: &[&Extension]) {
+  pub fn check_dependencies(&self, previous_exts: &[Extension]) {
     'dep_loop: for dep in self.deps {
       if dep == &self.name {
         panic!("Extension '{}' is either depending on itself or there is another extension with the same name", self.name);
diff --git a/core/modules/map.rs b/core/modules/map.rs
index a9c62c8..bd0413e 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -27,10 +27,12 @@ use crate::ModuleLoadResponse;
 use crate::ModuleSource;
 use crate::ModuleSpecifier;
 use anyhow::bail;
+use anyhow::Context as _;
 use anyhow::Error;
 use futures::future::FutureExt;
 use futures::stream::FuturesUnordered;
 use futures::stream::StreamFuture;
+use futures::task::noop_waker_ref;
 use futures::task::AtomicWaker;
 use futures::Future;
 use futures::StreamExt;
@@ -175,19 +177,15 @@ impl ModuleMap {
     }
   }
 
-  pub(crate) fn new_from_snapshotted_data(
-    loader: Rc<dyn ModuleLoader>,
-    exception_state: Rc<ExceptionState>,
-    import_meta_resolve_cb: ImportMetaResolveCallback,
+  pub(crate) fn update_with_snapshotted_data(
+    &self,
     scope: &mut v8::HandleScope,
     data: ModuleMapSnapshottedData,
-  ) -> Self {
-    let new = Self::new(loader, exception_state, import_meta_resolve_cb);
-    new
+  ) {
+    self
       .data
       .borrow_mut()
       .update_with_snapshotted_data(scope, data);
-    new
   }
 
   /// Get module id, following all aliases in case of module specifier
@@ -962,6 +960,41 @@ impl ModuleMap {
     receiver
   }
 
+  /// Helper function that allows to evaluate a module and ensure it's fully
+  /// evaluated without the need to poll the event loop.
+  ///
+  /// This is useful for evaluating internal modules that can't use Top-Level Await.
+  pub(crate) fn mod_evaluate_sync(
+    self: &Rc<Self>,
+    scope: &mut v8::HandleScope,
+    id: ModuleId,
+  ) -> Result<(), Error> {
+    let mut receiver = self.mod_evaluate(scope, id);
+
+    // After evaluate_pending_module, if the module isn't fully evaluated
+    // and the resolver solved, it means the module or one of its imports
+    // uses TLA.
+    match receiver.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
+      Poll::Ready(result) => {
+        result.with_context(|| {
+          let specifier = self.get_name_by_id(id).unwrap();
+          format!("Couldn't execute '{specifier}'")
+        })?;
+      }
+      Poll::Pending => {
+        // Find the TLA location and return it as an error
+        let messages = self.find_stalled_top_level_await(scope);
+        assert!(!messages.is_empty());
+        let msg = v8::Local::new(scope, &messages[0]);
+        let js_error = JsError::from_v8_message(scope, msg);
+        return Err(Error::from(js_error))
+          .with_context(|| "Top-level await is not allowed in extensions");
+      }
+    }
+
+    Ok(())
+  }
+
   fn dynamic_import_module_evaluate(
     &self,
     scope: &mut v8::HandleScope,
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index 293ca78..abfa217 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -9,6 +9,8 @@ use crate::op2;
 use crate::ops_builtin_types;
 use crate::ops_builtin_v8;
 use crate::JsBuffer;
+use crate::Op;
+use crate::OpDecl;
 use crate::OpState;
 use crate::Resource;
 use anyhow::Error;
@@ -19,97 +21,102 @@ use std::io::stdout;
 use std::io::Write;
 use std::rc::Rc;
 
-deno_core::extension!(
-  core,
-  ops = [
-    op_close,
-    op_try_close,
-    op_print,
-    op_resources,
-    op_wasm_streaming_feed,
-    op_wasm_streaming_set_url,
-    op_void_sync,
-    op_error_async,
-    op_error_async_deferred,
-    op_void_async,
-    op_void_async_deferred,
-    op_add,
-    op_add_async,
-    op_read,
-    op_read_all,
-    op_write,
-    op_read_sync,
-    op_write_sync,
-    op_write_all,
-    op_write_type_error,
-    op_shutdown,
-    op_format_file_name,
-    op_str_byte_length,
-    op_panic,
-    ops_builtin_types::op_is_any_array_buffer,
-    ops_builtin_types::op_is_arguments_object,
-    ops_builtin_types::op_is_array_buffer,
-    ops_builtin_types::op_is_array_buffer_view,
-    ops_builtin_types::op_is_async_function,
-    ops_builtin_types::op_is_big_int_object,
-    ops_builtin_types::op_is_boolean_object,
-    ops_builtin_types::op_is_boxed_primitive,
-    ops_builtin_types::op_is_data_view,
-    ops_builtin_types::op_is_date,
-    ops_builtin_types::op_is_generator_function,
-    ops_builtin_types::op_is_generator_object,
-    ops_builtin_types::op_is_map,
-    ops_builtin_types::op_is_map_iterator,
-    ops_builtin_types::op_is_module_namespace_object,
-    ops_builtin_types::op_is_native_error,
-    ops_builtin_types::op_is_number_object,
-    ops_builtin_types::op_is_promise,
-    ops_builtin_types::op_is_proxy,
-    ops_builtin_types::op_is_reg_exp,
-    ops_builtin_types::op_is_set,
-    ops_builtin_types::op_is_set_iterator,
-    ops_builtin_types::op_is_shared_array_buffer,
-    ops_builtin_types::op_is_string_object,
-    ops_builtin_types::op_is_symbol_object,
-    ops_builtin_types::op_is_typed_array,
-    ops_builtin_types::op_is_weak_map,
-    ops_builtin_types::op_is_weak_set,
-    ops_builtin_v8::op_set_handled_promise_rejection_handler,
-    ops_builtin_v8::op_timer_queue,
-    ops_builtin_v8::op_timer_cancel,
-    ops_builtin_v8::op_timer_ref,
-    ops_builtin_v8::op_timer_unref,
-    ops_builtin_v8::op_ref_op,
-    ops_builtin_v8::op_unref_op,
-    ops_builtin_v8::op_lazy_load_esm,
-    ops_builtin_v8::op_run_microtasks,
-    ops_builtin_v8::op_has_tick_scheduled,
-    ops_builtin_v8::op_set_has_tick_scheduled,
-    ops_builtin_v8::op_eval_context,
-    ops_builtin_v8::op_queue_microtask,
-    ops_builtin_v8::op_encode,
-    ops_builtin_v8::op_decode,
-    ops_builtin_v8::op_serialize,
-    ops_builtin_v8::op_deserialize,
-    ops_builtin_v8::op_set_promise_hooks,
-    ops_builtin_v8::op_get_promise_details,
-    ops_builtin_v8::op_get_proxy_details,
-    ops_builtin_v8::op_get_non_index_property_names,
-    ops_builtin_v8::op_get_constructor_name,
-    ops_builtin_v8::op_memory_usage,
-    ops_builtin_v8::op_set_wasm_streaming_callback,
-    ops_builtin_v8::op_abort_wasm_streaming,
-    ops_builtin_v8::op_destructure_error,
-    ops_builtin_v8::op_dispatch_exception,
-    ops_builtin_v8::op_op_names,
-    ops_builtin_v8::op_apply_source_map,
-    ops_builtin_v8::op_apply_source_map_filename,
-    ops_builtin_v8::op_current_user_call_site,
-    ops_builtin_v8::op_set_format_exception_callback,
-    ops_builtin_v8::op_event_loop_has_more_work,
-    ops_builtin_v8::op_arraybuffer_was_detached,
-  ],
-);
+macro_rules! builtin_ops {
+  ( $($op:ident $(:: $sub:ident)*),* ) => {
+    pub const BUILTIN_OPS: &'static [OpDecl] = &[
+      $( $op $(:: $sub)*::DECL, )*
+    ];
+  }
+}
+
+builtin_ops! {
+  op_close,
+  op_try_close,
+  op_print,
+  op_resources,
+  op_wasm_streaming_feed,
+  op_wasm_streaming_set_url,
+  op_void_sync,
+  op_error_async,
+  op_error_async_deferred,
+  op_void_async,
+  op_void_async_deferred,
+  op_add,
+  op_add_async,
+  op_read,
+  op_read_all,
+  op_write,
+  op_read_sync,
+  op_write_sync,
+  op_write_all,
+  op_write_type_error,
+  op_shutdown,
+  op_format_file_name,
+  op_str_byte_length,
+  op_panic,
+  ops_builtin_types::op_is_any_array_buffer,
+  ops_builtin_types::op_is_arguments_object,
+  ops_builtin_types::op_is_array_buffer,
+  ops_builtin_types::op_is_array_buffer_view,
+  ops_builtin_types::op_is_async_function,
+  ops_builtin_types::op_is_big_int_object,
+  ops_builtin_types::op_is_boolean_object,
+  ops_builtin_types::op_is_boxed_primitive,
+  ops_builtin_types::op_is_data_view,
+  ops_builtin_types::op_is_date,
+  ops_builtin_types::op_is_generator_function,
+  ops_builtin_types::op_is_generator_object,
+  ops_builtin_types::op_is_map,
+  ops_builtin_types::op_is_map_iterator,
+  ops_builtin_types::op_is_module_namespace_object,
+  ops_builtin_types::op_is_native_error,
+  ops_builtin_types::op_is_number_object,
+  ops_builtin_types::op_is_promise,
+  ops_builtin_types::op_is_proxy,
+  ops_builtin_types::op_is_reg_exp,
+  ops_builtin_types::op_is_set,
+  ops_builtin_types::op_is_set_iterator,
+  ops_builtin_types::op_is_shared_array_buffer,
+  ops_builtin_types::op_is_string_object,
+  ops_builtin_types::op_is_symbol_object,
+  ops_builtin_types::op_is_typed_array,
+  ops_builtin_types::op_is_weak_map,
+  ops_builtin_types::op_is_weak_set,
+  ops_builtin_v8::op_set_handled_promise_rejection_handler,
+  ops_builtin_v8::op_timer_queue,
+  ops_builtin_v8::op_timer_cancel,
+  ops_builtin_v8::op_timer_ref,
+  ops_builtin_v8::op_timer_unref,
+  ops_builtin_v8::op_ref_op,
+  ops_builtin_v8::op_unref_op,
+  ops_builtin_v8::op_lazy_load_esm,
+  ops_builtin_v8::op_run_microtasks,
+  ops_builtin_v8::op_has_tick_scheduled,
+  ops_builtin_v8::op_set_has_tick_scheduled,
+  ops_builtin_v8::op_eval_context,
+  ops_builtin_v8::op_queue_microtask,
+  ops_builtin_v8::op_encode,
+  ops_builtin_v8::op_decode,
+  ops_builtin_v8::op_serialize,
+  ops_builtin_v8::op_deserialize,
+  ops_builtin_v8::op_set_promise_hooks,
+  ops_builtin_v8::op_get_promise_details,
+  ops_builtin_v8::op_get_proxy_details,
+  ops_builtin_v8::op_get_non_index_property_names,
+  ops_builtin_v8::op_get_constructor_name,
+  ops_builtin_v8::op_memory_usage,
+  ops_builtin_v8::op_set_wasm_streaming_callback,
+  ops_builtin_v8::op_abort_wasm_streaming,
+  ops_builtin_v8::op_destructure_error,
+  ops_builtin_v8::op_dispatch_exception,
+  ops_builtin_v8::op_op_names,
+  ops_builtin_v8::op_apply_source_map,
+  ops_builtin_v8::op_apply_source_map_filename,
+  ops_builtin_v8::op_current_user_call_site,
+  ops_builtin_v8::op_set_format_exception_callback,
+  ops_builtin_v8::op_event_loop_has_more_work,
+  ops_builtin_v8::op_arraybuffer_was_detached
+}
 
 #[op2(fast)]
 pub fn op_panic(#[string] message: String) {
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index 5bdff5c..8a7e860 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -1,4 +1,5 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use anyhow::Context;
 use log::debug;
 use std::option::Option;
 use std::os::raw::c_void;
@@ -10,6 +11,7 @@ use super::jsruntime::CONTEXT_SETUP_SOURCES;
 use crate::error::has_call_site;
 use crate::error::is_instance_of_error;
 use crate::error::throw_type_error;
+use crate::error::AnyError;
 use crate::error::JsStackFrame;
 use crate::modules::get_requested_module_type_from_attributes;
 use crate::modules::parse_import_attributes;
@@ -221,42 +223,36 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
   global.set(scope, deno_str.into(), deno_obj.into());
 }
 
-/// If required, execute required JavaScript for `deno_core` to function and
-/// set up JavaScript bindings for ops.
-pub(crate) fn initialize_context<'s>(
+/// Execute `00_primordials.js` and `00_infra.js` that are required for ops
+/// to function properly
+pub(crate) fn initialize_primordials_and_infra(
+  scope: &mut v8::HandleScope,
+) -> Result<(), AnyError> {
+  for file_source in CONTEXT_SETUP_SOURCES {
+    let code = file_source.load().unwrap();
+    let source_str = code.v8_string(scope).unwrap();
+    let name = v8::String::new_external_onebyte_static(
+      scope,
+      file_source.specifier.as_bytes(),
+    )
+    .unwrap();
+    let origin = script_origin(scope, name);
+    // TODO(bartlomieju): these two calls will panic if there's any problem in the JS code
+    let script = v8::Script::compile(scope, source_str, Some(&origin))
+      .with_context(|| format!("Failed to parse {}", file_source.specifier))?;
+    script.run(scope).with_context(|| {
+      format!("Failed to execute {}", file_source.specifier)
+    })?;
+  }
+  Ok(())
+}
+
+/// Set up JavaScript bindings for ops.
+pub(crate) fn initialize_deno_core_ops_bindings<'s>(
   scope: &mut v8::HandleScope<'s>,
   context: v8::Local<'s, v8::Context>,
   op_ctxs: &[OpCtx],
-  init_mode: InitMode,
 ) {
-  // Execute `00_primordials.js` and `00_infra.js`
-  if init_mode == InitMode::New {
-    for file_source in CONTEXT_SETUP_SOURCES {
-      let code = file_source.load().unwrap();
-      let source_str = code.v8_string(scope).unwrap();
-      let name = v8::String::new_external_onebyte_static(
-        scope,
-        file_source.specifier.as_bytes(),
-      )
-      .unwrap();
-      let origin = script_origin(scope, name);
-      // TODO(bartlomieju): these two calls will panic if there's any problem in the JS code
-      let script =
-        v8::Script::compile(scope, source_str, Some(&origin)).unwrap();
-      script.run(scope).unwrap();
-    }
-  }
-
-  // Fast path - if all the ops have been registered already, bail out.
-  if matches!(
-    init_mode,
-    InitMode::FromSnapshot {
-      skip_op_registration: true
-    }
-  ) {
-    return;
-  }
-
   let global = context.global(scope);
 
   // Set up JavaScript bindings for the defined op - this will insert proper
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index ec426e0..7da3e5d 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -40,6 +40,7 @@ use crate::source_map::SourceMapCache;
 use crate::source_map::SourceMapGetter;
 use crate::Extension;
 use crate::ExtensionFileSource;
+use crate::ExtensionFileSourceCode;
 use crate::FastString;
 use crate::FeatureChecker;
 use crate::NoopModuleLoader;
@@ -47,10 +48,8 @@ use crate::OpMetricsEvent;
 use crate::OpState;
 use anyhow::anyhow;
 use anyhow::bail;
-use anyhow::Context as AnyhowContext;
 use anyhow::Error;
 use futures::future::poll_fn;
-use futures::task::noop_waker_ref;
 use futures::task::AtomicWaker;
 use futures::Future;
 use futures::FutureExt;
@@ -222,6 +221,16 @@ impl InitMode {
   pub fn is_from_snapshot(&self) -> bool {
     matches!(self, Self::FromSnapshot { .. })
   }
+
+  #[inline]
+  pub fn needs_ops_bindings(&self) -> bool {
+    !matches!(
+      self,
+      InitMode::FromSnapshot {
+        skip_op_registration: true
+      }
+    )
+  }
 }
 
 #[derive(Default)]
@@ -296,7 +305,10 @@ pub(crate) const NO_OF_BUILTIN_MODULES: usize = 2;
 pub struct JsRuntime {
   pub(crate) inner: InnerIsolateState,
   pub(crate) allocations: IsolateAllocations,
-  extensions: Vec<Extension>,
+  // Contains paths of source files that were executed in
+  // [`JsRuntime::init_extension_js`]. This field is populated only if a
+  // snapshot is being created.
+  files_loaded_from_fs_during_snapshot: Vec<&'static str>,
   init_mode: InitMode,
   // Marks if this is considered the top-level runtime. Used only by inspector.
   is_main_runtime: bool,
@@ -552,7 +564,12 @@ impl JsRuntime {
       cfg!(test),
       options.unsafe_expose_natives_and_gc(),
     );
-    JsRuntime::new_inner(options, false)
+    match JsRuntime::new_inner(options, false) {
+      Ok(runtime) => runtime,
+      Err(err) => {
+        panic!("Failed to initialize a JsRuntime: {:?}", err);
+      }
+    }
   }
 
   pub(crate) fn state_from(isolate: &v8::Isolate) -> Rc<JsRuntimeState> {
@@ -576,20 +593,19 @@ impl JsRuntime {
     EventLoopPendingState::new_from_scope(scope).is_pending()
   }
 
-  fn new_inner(mut options: RuntimeOptions, will_snapshot: bool) -> JsRuntime {
+  fn new_inner(
+    mut options: RuntimeOptions,
+    will_snapshot: bool,
+  ) -> Result<JsRuntime, Error> {
     let init_mode = InitMode::from_options(&options);
     let mut op_state = OpState::new(options.feature_checker.take());
 
     let mut extensions = std::mem::take(&mut options.extensions);
-    let mut deno_core_ext = crate::ops_builtin::core::init_ops();
-    let op_decls = extension_set::init_ops(&mut deno_core_ext, &mut extensions);
-    extension_set::setup_op_state(
-      &mut op_state,
-      &mut deno_core_ext,
-      &mut extensions,
-    );
+    let op_decls =
+      extension_set::init_ops(crate::ops_builtin::BUILTIN_OPS, &mut extensions);
+    extension_set::setup_op_state(&mut op_state, &mut extensions);
     let (
-      global_template_middlewares,
+      global_template_middleware,
       global_object_middlewares,
       additional_references,
     ) = extension_set::get_middlewares_and_external_refs(&mut extensions);
@@ -686,23 +702,23 @@ impl JsRuntime {
 
     // TODO(bartlomieju): this context creation and setup is really non-trivial.
     // Additionally it's unclear what is done when snapshotting/running from snapshot.
-    let (main_context, snapshotted_data) = {
+    let mut snapshotted_data = None;
+    let main_context = {
       let scope = &mut v8::HandleScope::new(&mut isolate);
 
       let context = create_context(
         scope,
-        &global_template_middlewares,
+        &global_template_middleware,
         &global_object_middlewares,
       );
 
       // Get module map data from the snapshot
-      let snapshotted_data = if init_mode.is_from_snapshot() {
-        Some(snapshot_util::get_snapshotted_data(scope, context))
-      } else {
-        None
-      };
+      if init_mode.is_from_snapshot() {
+        snapshotted_data =
+          Some(snapshot_util::get_snapshotted_data(scope, context));
+      }
 
-      (v8::Global::new(scope, context), snapshotted_data)
+      v8::Global::new(scope, context)
     };
 
     let mut context_scope: v8::HandleScope =
@@ -710,13 +726,19 @@ impl JsRuntime {
     let scope = &mut context_scope;
     let context = v8::Local::new(scope, &main_context);
 
-    bindings::initialize_deno_core_namespace(scope, context, init_mode);
-    bindings::initialize_context(
-      scope,
-      context,
-      &context_state.op_ctxs,
-      init_mode,
-    );
+    if init_mode == InitMode::New {
+      bindings::initialize_deno_core_namespace(scope, context, init_mode);
+      bindings::initialize_primordials_and_infra(scope)?;
+    }
+    // If we're creating a new runtime or there are new ops to register
+    // set up JavaScript bindings for them.
+    if init_mode.needs_ops_bindings() {
+      bindings::initialize_deno_core_ops_bindings(
+        scope,
+        context,
+        &context_state.op_ctxs,
+      );
+    }
 
     context.set_slot(scope, context_state.clone());
 
@@ -732,29 +754,24 @@ impl JsRuntime {
     let import_meta_resolve_cb = options
       .import_meta_resolve_callback
       .unwrap_or_else(|| Box::new(default_import_meta_resolve_cb));
-
     let exception_state = context_state.exception_state.clone();
-    let module_map = if let Some(snapshotted_data) = snapshotted_data {
+
+    let module_map = Rc::new(ModuleMap::new(
+      loader,
+      exception_state.clone(),
+      import_meta_resolve_cb,
+    ));
+
+    if let Some(snapshotted_data) = snapshotted_data {
       *exception_state.js_handled_promise_rejection_cb.borrow_mut() =
         snapshotted_data.js_handled_promise_rejection_cb;
       let module_map_snapshotted_data = ModuleMapSnapshottedData {
         module_map_data: snapshotted_data.module_map_data,
         module_handles: snapshotted_data.module_handles,
       };
-      Rc::new(ModuleMap::new_from_snapshotted_data(
-        loader,
-        exception_state,
-        import_meta_resolve_cb,
-        scope,
-        module_map_snapshotted_data,
-      ))
-    } else {
-      Rc::new(ModuleMap::new(
-        loader,
-        exception_state,
-        import_meta_resolve_cb,
-      ))
-    };
+      module_map
+        .update_with_snapshotted_data(scope, module_map_snapshotted_data);
+    }
 
     context.set_slot(scope, module_map.clone());
 
@@ -784,22 +801,19 @@ impl JsRuntime {
       },
       init_mode,
       allocations: IsolateAllocations::default(),
-      // TODO(bartlomieju): at this point extensions have only JS/ESM sources left;
-      // probably worth to add a dedicated struct to represent that.
-      extensions: vec![],
+      files_loaded_from_fs_during_snapshot: vec![],
       is_main_runtime: options.is_main,
     };
 
-    // TODO(mmastrac): We should thread errors back out of the runtime
-    js_runtime
-      .init_extension_js(&mut extensions)
-      .expect("Failed to evaluate extension JS");
+    let files_loaded_from_fs_during_snapshot =
+      js_runtime.init_extension_js(extensions)?;
 
-    // TODO(bartlomieju): clean this up - we shouldn't need to store extensions
-    // on the `js_runtime` as they are mutable.
-    js_runtime.extensions = extensions;
+    if will_snapshot {
+      js_runtime.files_loaded_from_fs_during_snapshot =
+        files_loaded_from_fs_during_snapshot;
+    }
 
-    js_runtime
+    Ok(js_runtime)
   }
 
   #[cfg(test)]
@@ -852,10 +866,8 @@ impl JsRuntime {
     }
   }
 
-  // TODO(bartlomieju): this method probably be public to the crate
-  /// Returns the extensions that this runtime is using (including internal ones).
-  pub fn extensions(&self) -> &Vec<Extension> {
-    &self.extensions
+  pub(crate) fn files_loaded_from_fs_during_snapshot(&self) -> &[&'static str] {
+    &self.files_loaded_from_fs_during_snapshot
   }
 
   #[inline]
@@ -906,68 +918,88 @@ impl JsRuntime {
       )
       .unwrap();
     module_map.instantiate_module(scope, mod_id).unwrap();
-    let mut receiver = module_map.mod_evaluate(scope, mod_id);
-    let Poll::Ready(result) =
-      receiver.poll_unpin(&mut Context::from_waker(noop_waker_ref()))
-    else {
-      unreachable!();
-    };
-    result.unwrap();
+    module_map.mod_evaluate_sync(scope, mod_id).unwrap();
+  }
+
+  /// Executes built-in scripts and ES modules - this code is required for
+  /// ops system to work properly, as well as providing necessary bindings
+  /// on the `Deno.core` namespace.
+  ///
+  /// This is not done in [`bindings::initialize_primordials_and_infra`] because
+  /// some of this code already relies on certain ops being available.
+  fn execute_builtin_sources(
+    &mut self,
+    realm: &JsRealm,
+    module_map: &Rc<ModuleMap>,
+    files_loaded: &mut Vec<&'static str>,
+  ) -> Result<(), Error> {
+    for file_source in &BUILTIN_SOURCES {
+      mark_as_loaded_from_fs_during_snapshot(files_loaded, &file_source.code);
+      realm.execute_script(
+        self.v8_isolate(),
+        file_source.specifier,
+        file_source.load()?,
+      )?;
+    }
+
+    for file_source in &BUILTIN_ES_MODULES {
+      mark_as_loaded_from_fs_during_snapshot(files_loaded, &file_source.code);
+      let mut scope = realm.handle_scope(self.v8_isolate());
+      module_map.lazy_load_es_module_from_code(
+        &mut scope,
+        file_source.specifier,
+        file_source.load()?,
+      )?;
+    }
+
+    Ok(())
   }
 
   /// Initializes JS of provided Extensions in the given realm.
   fn init_extension_js(
     &mut self,
-    extensions: &mut [Extension],
-  ) -> Result<(), Error> {
+    extensions: Vec<Extension>,
+  ) -> Result<Vec<&'static str>, Error> {
     // Initialization of JS happens in phases:
     // 1. Iterate through all extensions:
     //  a. Execute all extension "script" JS files
     //  b. Load all extension "module" JS files (but do not execute them yet)
     // 2. Iterate through all extensions:
     //  a. If an extension has a `esm_entry_point`, execute it.
+    let mut files_loaded = Vec::with_capacity(128);
     let realm = JsRealm::clone(&self.inner.main_realm);
     let context_global = realm.context();
     let module_map = realm.0.module_map();
+
+    // TODO(bartlomieju): hoist it out of this method.
+    // TODO(bartlomieju): this is somewhat duplicated in `bindings::initialize_context`,
+    // but for migration period we need to have ops available in both `Deno.core.ops`
+    // as well as have them available in "virtual ops module"
+    // if !matches!(
+    //   self.init_mode,
+    //   InitMode::FromSnapshot {
+    //     skip_op_registration: true
+    //   }
+    // ) {
+    if self.init_mode == InitMode::New {
+      self.execute_virtual_ops_module(context_global, module_map.clone());
+    }
+
+    // TODO(bartlomieju): hoist it out of this method.
+    if self.init_mode == InitMode::New {
+      self.execute_builtin_sources(&realm, &module_map, &mut files_loaded)?;
+    }
+
+    // TODO(bartlomieju): hoist it out of this method.
+    self.init_cbs(&realm);
+
     let loader = module_map.loader.borrow().clone();
-    let ext_loader = Rc::new(ExtModuleLoader::new(extensions));
+    let ext_loader = Rc::new(ExtModuleLoader::new(&extensions));
     *module_map.loader.borrow_mut() = ext_loader;
 
     let mut esm_entrypoints = vec![];
 
     futures::executor::block_on(async {
-      // TODO(bartlomieju): this is somewhat duplicated in `bindings::initialize_context`,
-      // but for migration period we need to have ops available in both `Deno.core.ops`
-      // as well as have them available in "virtual ops module"
-      // if !matches!(
-      //   self.init_mode,
-      //   InitMode::FromSnapshot {
-      //     skip_op_registration: true
-      //   }
-      // ) {
-      if self.init_mode == InitMode::New {
-        self.execute_virtual_ops_module(context_global, module_map.clone());
-      }
-
-      if self.init_mode == InitMode::New {
-        for file_source in &BUILTIN_SOURCES {
-          realm.execute_script(
-            self.v8_isolate(),
-            file_source.specifier,
-            file_source.load()?,
-          )?;
-        }
-        for file_source in &BUILTIN_ES_MODULES {
-          let mut scope = realm.handle_scope(self.v8_isolate());
-          module_map.lazy_load_es_module_from_code(
-            &mut scope,
-            file_source.specifier,
-            file_source.load()?,
-          )?;
-        }
-      }
-      self.init_cbs(&realm);
-
       for extension in extensions {
         // If the extension provides "lazy loaded ES modules" then store them
         // on the ModuleMap.
@@ -977,6 +1009,10 @@ impl JsRuntime {
         let maybe_esm_entry_point = extension.get_esm_entry_point();
 
         for file_source in extension.get_esm_sources() {
+          mark_as_loaded_from_fs_during_snapshot(
+            &mut files_loaded,
+            &file_source.code,
+          );
           realm
             .load_side_module(
               self.v8_isolate(),
@@ -991,6 +1027,10 @@ impl JsRuntime {
         }
 
         for file_source in extension.get_js_sources() {
+          mark_as_loaded_from_fs_during_snapshot(
+            &mut files_loaded,
+            &file_source.code,
+          );
           realm.execute_script(
             self.v8_isolate(),
             file_source.specifier,
@@ -1007,32 +1047,9 @@ impl JsRuntime {
           bail!("{} not present in the module map", specifier);
         };
 
-        let mut receiver = {
-          let isolate = self.v8_isolate();
-          let scope = &mut realm.handle_scope(isolate);
-
-          module_map.mod_evaluate(scope, mod_id)
-        };
-
-        // After evaluate_pending_module, if the module isn't fully evaluated
-        // and the resolver solved, it means the module or one of its imports
-        // uses TLA.
-        match receiver.poll_unpin(&mut Context::from_waker(noop_waker_ref())) {
-          Poll::Ready(result) => {
-            result
-              .with_context(|| format!("Couldn't execute '{specifier}'"))?;
-          }
-          Poll::Pending => {
-            // Find the TLA location and return it as an error
-            let scope = &mut realm.handle_scope(self.v8_isolate());
-            let messages = module_map.find_stalled_top_level_await(scope);
-            assert!(!messages.is_empty());
-            let msg = v8::Local::new(scope, &messages[0]);
-            let js_error = JsError::from_v8_message(scope, msg);
-            return Err(Error::from(js_error))
-              .with_context(|| "Top-level await is not allowed in extensions");
-          }
-        }
+        let isolate = self.v8_isolate();
+        let scope = &mut realm.handle_scope(isolate);
+        module_map.mod_evaluate_sync(scope, mod_id)?;
       }
 
       #[cfg(debug_assertions)]
@@ -1047,7 +1064,7 @@ impl JsRuntime {
     let module_map = realm.0.module_map();
     *module_map.loader.borrow_mut() = loader;
 
-    Ok(())
+    Ok(files_loaded)
   }
 
   pub fn eval<'s, T>(
@@ -1743,7 +1760,14 @@ impl JsRuntimeForSnapshot {
       true,
       options.unsafe_expose_natives_and_gc(),
     );
-    JsRuntimeForSnapshot(JsRuntime::new_inner(options, true))
+
+    let runtime = match JsRuntime::new_inner(options, true) {
+      Ok(r) => r,
+      Err(err) => {
+        panic!("Failed to initialize JsRuntime for snapshotting: {:?}", err);
+      }
+    };
+    JsRuntimeForSnapshot(runtime)
   }
 
   /// Takes a snapshot and consumes the runtime.
@@ -2163,3 +2187,12 @@ impl JsRuntime {
     Ok(dispatched_ops)
   }
 }
+
+fn mark_as_loaded_from_fs_during_snapshot(
+  files_loaded: &mut Vec<&'static str>,
+  source: &ExtensionFileSourceCode,
+) {
+  if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = source {
+    files_loaded.push(path);
+  }
+}
diff --git a/core/runtime/snapshot_util.rs b/core/runtime/snapshot_util.rs
index 06e312f..4ac64e6 100644
--- a/core/runtime/snapshot_util.rs
+++ b/core/runtime/snapshot_util.rs
@@ -4,10 +4,7 @@ use std::path::Path;
 use std::path::PathBuf;
 use std::time::Instant;
 
-use crate::runtime::jsruntime::BUILTIN_ES_MODULES;
-use crate::runtime::jsruntime::BUILTIN_SOURCES;
 use crate::Extension;
-use crate::ExtensionFileSourceCode;
 use crate::JsRuntimeForSnapshot;
 use crate::RuntimeOptions;
 use crate::Snapshot;
@@ -50,33 +47,11 @@ pub fn create_snapshot(
   );
   mark = Instant::now();
 
-  let mut files_loaded_during_snapshot = vec![];
-  for source in &BUILTIN_SOURCES {
-    if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) =
-      &source.code
-    {
-      files_loaded_during_snapshot.push(PathBuf::from(path));
-    }
-  }
-  for source in &BUILTIN_ES_MODULES {
-    if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) =
-      &source.code
-    {
-      files_loaded_during_snapshot.push(PathBuf::from(path));
-    }
-  }
-  for source in js_runtime
-    .extensions()
+  let files_loaded_during_snapshot = js_runtime
+    .files_loaded_from_fs_during_snapshot()
     .iter()
-    .flat_map(|e| vec![e.get_esm_sources(), e.get_js_sources()])
-    .flatten()
-  {
-    if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) =
-      &source.code
-    {
-      files_loaded_during_snapshot.push(PathBuf::from(path));
-    }
-  }
+    .map(PathBuf::from)
+    .collect::<Vec<_>>();
 
   if let Some(with_runtime_cb) = create_snapshot_options.with_runtime_cb {
     with_runtime_cb(&mut js_runtime);
diff --git a/dprint.json b/dprint.json
new file mode 100644
index 0000000..10e5b85
--- /dev/null
+++ b/dprint.json
@@ -0,0 +1,29 @@
+{
+  "typescript": {
+    "deno": true
+  },
+  "markdown": {
+    "deno": true
+  },
+  "json": {
+    "deno": true
+  },
+  "exec": {
+    "commands": [{
+      "command": "rustfmt --config imports_granularity=item",
+      "exts": ["rs"]
+    }]
+  },
+  "excludes": [
+    ".cargo_home",
+    ".git",
+    "target"
+  ],
+  "plugins": [
+    "https://plugins.dprint.dev/typescript-0.88.7.wasm",
+    "https://plugins.dprint.dev/json-0.19.1.wasm",
+    "https://plugins.dprint.dev/markdown-0.16.3.wasm",
+    "https://plugins.dprint.dev/toml-0.5.4.wasm",
+    "https://plugins.dprint.dev/exec-0.4.4.json@c207bf9b9a4ee1f0ecb75c594f774924baf62e8e53a2ce9d873816a408cecbf7"
+  ]
+}


Test patch:
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index 156f68a..36e453d 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -1010,7 +1010,7 @@ async fn test_dynamic_import_module_error_stack() {
 
 #[tokio::test]
 #[should_panic(
-  expected = r#"Failed to evaluate extension JS: Top-level await is not allowed in extensions
+  expected = r#"Failed to initialize a JsRuntime: Top-level await is not allowed in extensions
 
 Caused by:
     Top-level await promise never resolved
@@ -1057,7 +1057,7 @@ async fn tla_in_esm_extensions_panics() {
 #[cfg(debug_assertions)]
 #[tokio::test]
 #[should_panic(
-  expected = r#"Failed to evaluate extension JS: Error: This fails
+  expected = r#"Failed to initialize a JsRuntime: Error: This fails
     at a (mod:error:2:34)
     at mod:error:3:13"#
 )]
diff --git a/testing/checkin/runner/mod.rs b/testing/checkin/runner/mod.rs
index dc1eab4..f6267ed 100644
--- a/testing/checkin/runner/mod.rs
+++ b/testing/checkin/runner/mod.rs
@@ -16,12 +16,16 @@ use tokio::io::AsyncReadExt;
 
 use crate::checkin::runner::testing::TestData;
 
+use self::ops_worker::worker_create;
+use self::ops_worker::WorkerCloseWatcher;
+use self::ops_worker::WorkerHostSide;
 use self::testing::TestFunctions;
 
 mod ops;
 mod ops_async;
 mod ops_buffer;
 mod ops_io;
+mod ops_worker;
 mod testing;
 mod ts_module_loader;
 
@@ -44,6 +48,12 @@ deno_core::extension!(
     ops_async::op_async_throw_error_deferred,
     ops_buffer::op_v8slice_store,
     ops_buffer::op_v8slice_clone,
+    ops_worker::op_worker_spawn,
+    ops_worker::op_worker_send,
+    ops_worker::op_worker_recv,
+    ops_worker::op_worker_parent,
+    ops_worker::op_worker_await_close,
+    ops_worker::op_worker_terminate,
   ],
   esm_entry_point = "ext:checkin_runtime/__init.js",
   esm = [
@@ -54,15 +64,19 @@ deno_core::extension!(
     "console.ts" with_specifier "checkin:console",
     "testing.ts" with_specifier "checkin:testing",
     "timers.ts" with_specifier "checkin:timers",
+    "worker.ts" with_specifier "checkin:worker",
   ],
   state = |state| {
     state.put(TestFunctions::default());
     state.put(TestData::default());
-    state.put(Output::default());
   }
 );
 
-fn create_runtime() -> JsRuntime {
+fn create_runtime(
+  output: Output,
+  parent: Option<WorkerCloseWatcher>,
+) -> (JsRuntime, WorkerHostSide) {
+  let (worker, worker_host_side) = worker_create(parent);
   let mut extensions = vec![checkin_runtime::init_ops_and_esm()];
 
   for extension in &mut extensions {
@@ -88,13 +102,15 @@ fn create_runtime() -> JsRuntime {
   });
   let stats = runtime.runtime_activity_stats_factory();
   runtime.op_state().borrow_mut().put(stats);
-  runtime
+  runtime.op_state().borrow_mut().put(worker);
+  runtime.op_state().borrow_mut().put(output);
+  (runtime, worker_host_side)
 }
 
 /// Run a integration test within the `checkin` runtime. This executes a single file, imports and all,
 /// and compares its output with the `.out` file in the same directory.
 pub fn run_integration_test(test: &str) {
-  let runtime = create_runtime();
+  let (runtime, _) = create_runtime(Output::default(), None);
   let tokio = tokio::runtime::Builder::new_current_thread()
     .enable_all()
     .build()
@@ -117,21 +133,22 @@ async fn run_integration_test_task(
     .run_event_loop(PollEventLoopOptions::default())
     .await
   {
+    let state = runtime.op_state().clone();
+    let state = state.borrow();
+    let output: &Output = state.borrow();
     for line in e.to_string().split('\n') {
-      actual_output += "[ERR] ";
-      actual_output += line;
-      actual_output += "\n";
+      output.line(format!("[ERR] {line}"));
     }
   }
   f.await?;
-  let mut output: Output = runtime.op_state().borrow_mut().take();
-  output.lines.push(String::new());
+  let mut lines = runtime.op_state().borrow_mut().take::<Output>().take();
+  lines.push(String::new());
   let mut expected_output = String::new();
   File::open(test_dir.join(format!("{test}.out")))
     .await?
     .read_to_string(&mut expected_output)
     .await?;
-  actual_output += &output.lines.join("\n");
+  actual_output += &lines.join("\n");
   assert_eq!(actual_output, expected_output);
   Ok(())
 }
@@ -139,7 +156,7 @@ async fn run_integration_test_task(
 /// Run a unit test within the `checkin` runtime. This loads a file which registers a number of tests,
 /// then each test is run individually and failures are printed.
 pub fn run_unit_test(test: &str) {
-  let runtime = create_runtime();
+  let (runtime, _) = create_runtime(Output::default(), None);
   let tokio = tokio::runtime::Builder::new_current_thread()
     .enable_all()
     .build()
diff --git a/testing/checkin/runner/ops.rs b/testing/checkin/runner/ops.rs
index 5acd4f2..8c76fe1 100644
--- a/testing/checkin/runner/ops.rs
+++ b/testing/checkin/runner/ops.rs
@@ -23,7 +23,7 @@ pub fn op_log_debug(#[string] s: &str) {
 #[op2(fast)]
 pub fn op_log_info(#[state] output: &mut Output, #[string] s: String) {
   println!("{s}");
-  output.lines.push(s);
+  output.line(s);
 }
 
 #[op2]
diff --git a/testing/checkin/runner/ops_worker.rs b/testing/checkin/runner/ops_worker.rs
new file mode 100644
index 0000000..7c3fa47
--- /dev/null
+++ b/testing/checkin/runner/ops_worker.rs
@@ -0,0 +1,224 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use anyhow::anyhow;
+use anyhow::bail;
+use anyhow::Error;
+use deno_core::cppgc;
+use deno_core::op2;
+use deno_core::url::Url;
+use deno_core::v8;
+use deno_core::v8::IsolateHandle;
+use deno_core::JsRuntime;
+use deno_core::OpState;
+use deno_core::PollEventLoopOptions;
+use std::cell::RefCell;
+use std::future::poll_fn;
+use std::rc::Rc;
+use std::sync::mpsc::channel;
+use std::sync::Arc;
+use std::task::Poll;
+use tokio::sync::mpsc::unbounded_channel;
+use tokio::sync::mpsc::UnboundedReceiver;
+use tokio::sync::mpsc::UnboundedSender;
+use tokio::sync::watch;
+use tokio::sync::Mutex;
+
+use super::create_runtime;
+use super::testing::Output;
+
+/// Our cppgc object.
+pub struct WorkerControl {
+  worker_channel: WorkerChannel,
+  close_watcher: WorkerCloseWatcher,
+  handle: Option<IsolateHandle>,
+  shutdown_flag: Option<UnboundedSender<()>>,
+}
+
+pub struct WorkerChannel {
+  tx: UnboundedSender<String>,
+  rx: Mutex<UnboundedReceiver<String>>,
+}
+
+#[derive(Clone)]
+pub struct WorkerCloseWatcher {
+  watcher: Arc<Mutex<watch::Receiver<bool>>>,
+}
+
+pub struct Worker {
+  _close_send: watch::Sender<bool>,
+  pub(crate) close_watcher: WorkerCloseWatcher,
+  parent_channel: std::sync::Mutex<Option<WorkerChannel>>,
+  parent_close_watcher: std::sync::Mutex<Option<WorkerCloseWatcher>>,
+}
+
+pub struct WorkerHostSide {
+  worker_channel: WorkerChannel,
+  close_watcher: WorkerCloseWatcher,
+}
+
+pub fn worker_create(
+  parent: Option<WorkerCloseWatcher>,
+) -> (Worker, WorkerHostSide) {
+  let (tx1, rx1) = unbounded_channel();
+  let (tx2, rx2) = unbounded_channel();
+  let (_close_send, close_recv) = watch::channel(false);
+  let close_watcher = WorkerCloseWatcher {
+    watcher: Arc::new(Mutex::new(close_recv)),
+  };
+  let worker = Worker {
+    _close_send,
+    close_watcher: close_watcher.clone(),
+    parent_channel: parent
+      .as_ref()
+      .map(move |_| WorkerChannel {
+        tx: tx1,
+        rx: rx2.into(),
+      })
+      .into(),
+    parent_close_watcher: parent.into(),
+  };
+  let worker_host_side = WorkerHostSide {
+    close_watcher,
+    worker_channel: WorkerChannel {
+      tx: tx2,
+      rx: rx1.into(),
+    },
+  };
+  (worker, worker_host_side)
+}
+
+#[op2]
+pub fn op_worker_spawn<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  #[state] this_worker: &Worker,
+  #[state] output: &Output,
+  #[string] base_url: String,
+  #[string] main_script: String,
+) -> Result<v8::Local<'s, v8::Object>, Error> {
+  let output = output.clone();
+  let close_watcher = this_worker.close_watcher.clone();
+  let (init_send, init_recv) = channel();
+  let (shutdown_tx, shutdown_rx) = unbounded_channel();
+  std::thread::spawn(move || {
+    let (mut runtime, worker_host_side) =
+      create_runtime(output, Some(close_watcher));
+    init_send
+      .send(WorkerControl {
+        worker_channel: worker_host_side.worker_channel,
+        close_watcher: worker_host_side.close_watcher,
+        handle: Some(runtime.v8_isolate().thread_safe_handle()),
+        shutdown_flag: Some(shutdown_tx),
+      })
+      .map_err(|_| unreachable!())
+      .unwrap();
+    let tokio = tokio::runtime::Builder::new_current_thread()
+      .enable_all()
+      .build()
+      .expect("Failed to build a runtime");
+    tokio
+      .block_on(run_worker_task(runtime, base_url, main_script, shutdown_rx))
+      .expect("Failed to complete test");
+  });
+
+  // This is technically a blocking call
+  let worker = init_recv.recv()?;
+  Ok(cppgc::make_cppgc_object(scope, worker))
+}
+
+async fn run_worker_task(
+  mut runtime: JsRuntime,
+  base_url: String,
+  main_script: String,
+  mut shutdown_rx: UnboundedReceiver<()>,
+) -> Result<(), Error> {
+  let url = Url::try_from(base_url.as_str())?.join(&main_script)?;
+  let module = runtime.load_main_module(&url, None).await?;
+  let f = runtime.mod_evaluate(module);
+  // We need this structure for the shutdown code to ensure that the output is
+  // consistent whether the v8 termination signal is sent, or the shutdown_rx is
+  // triggered.
+  if let Err(e) = poll_fn(|cx| {
+    if shutdown_rx.poll_recv(cx).is_ready() {
+      // This matches the v8 error. We'll hit both, depending on timing.
+      return Poll::Ready(Err(anyhow!("Uncaught Error: execution terminated")));
+    }
+    runtime.poll_event_loop(cx, PollEventLoopOptions::default())
+  })
+  .await
+  {
+    let state = runtime.op_state().clone();
+    let state = state.borrow();
+    let output: &Output = state.borrow();
+    for line in e.to_string().split('\n') {
+      output.line(format!("[ERR] {line}"));
+    }
+    return Ok(());
+  }
+  _ = f.await;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_worker_send(
+  #[cppgc] worker: &WorkerControl,
+  #[string] message: String,
+) -> Result<(), Error> {
+  worker.worker_channel.tx.send(message)?;
+  Ok(())
+}
+
+#[op2(async)]
+#[string]
+pub async fn op_worker_recv(#[cppgc] worker: &WorkerControl) -> Option<String> {
+  worker.worker_channel.rx.lock().await.recv().await
+}
+
+#[op2]
+pub fn op_worker_parent<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  state: Rc<RefCell<OpState>>,
+) -> Result<v8::Local<'s, v8::Object>, Error> {
+  let state = state.borrow_mut();
+  let worker: &Worker = state.borrow();
+  let (Some(worker_channel), Some(close_watcher)) = (
+    worker.parent_channel.lock().unwrap().take(),
+    worker.parent_close_watcher.lock().unwrap().take(),
+  ) else {
+    bail!("No parent worker is available")
+  };
+  Ok(cppgc::make_cppgc_object(
+    scope,
+    WorkerControl {
+      worker_channel,
+      close_watcher,
+      handle: None,
+      shutdown_flag: None,
+    },
+  ))
+}
+
+#[op2(async)]
+pub async fn op_worker_await_close(#[cppgc] worker: &WorkerControl) {
+  loop {
+    if worker
+      .close_watcher
+      .watcher
+      .lock()
+      .await
+      .changed()
+      .await
+      .is_err()
+    {
+      break;
+    }
+  }
+}
+
+#[op2(fast)]
+pub fn op_worker_terminate(
+  #[cppgc] worker: &WorkerControl,
+  state: Rc<RefCell<OpState>>,
+) {
+  worker.handle.as_ref().unwrap().terminate_execution();
+  _ = worker.shutdown_flag.as_ref().unwrap().send(());
+  state.borrow_mut().waker.wake();
+}
diff --git a/testing/checkin/runner/testing.rs b/testing/checkin/runner/testing.rs
index 29640f5..258b21b 100644
--- a/testing/checkin/runner/testing.rs
+++ b/testing/checkin/runner/testing.rs
@@ -2,12 +2,23 @@
 use std::any::Any;
 use std::any::TypeId;
 use std::collections::HashMap;
+use std::sync::Arc;
+use std::sync::Mutex;
 
 use deno_core::v8;
 
-#[derive(Default)]
+#[derive(Clone, Default)]
 pub struct Output {
-  pub lines: Vec<String>,
+  pub lines: Arc<Mutex<Vec<String>>>,
+}
+impl Output {
+  pub fn line(&self, line: String) {
+    self.lines.lock().unwrap().push(line)
+  }
+
+  pub fn take(&self) -> Vec<String> {
+    std::mem::take(&mut self.lines.lock().unwrap())
+  }
 }
 
 #[derive(Default)]
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index 0e60401..5dbce97 100644
--- a/testing/checkin/runtime/__init.js
+++ b/testing/checkin/runtime/__init.js
@@ -4,6 +4,7 @@ import * as async from "checkin:async";
 import * as testing from "checkin:testing";
 import * as console from "checkin:console";
 import * as timers from "checkin:timers";
+import * as worker from "checkin:worker";
 testing;
 async;
 
@@ -12,6 +13,7 @@ globalThis.setTimeout = timers.setTimeout;
 globalThis.setInterval = timers.setInterval;
 globalThis.clearTimeout = timers.clearTimeout;
 globalThis.clearInterval = timers.clearInterval;
+globalThis.Worker = worker.Worker;
 Reflect.defineProperty(globalThis, "onunhandledrejection", {
   set: (cb) => {
     if (cb) {
diff --git a/testing/checkin/runtime/worker.ts b/testing/checkin/runtime/worker.ts
new file mode 100644
index 0000000..39cf537
--- /dev/null
+++ b/testing/checkin/runtime/worker.ts
@@ -0,0 +1,51 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import {
+  op_worker_await_close,
+  op_worker_parent,
+  op_worker_recv,
+  op_worker_send,
+  op_worker_spawn,
+  op_worker_terminate,
+} from "ext:core/ops";
+
+const privateConstructor = Symbol();
+let parentWorker = null;
+
+export class Worker {
+  // deno-lint-ignore no-explicit-any
+  #worker: any;
+
+  // deno-lint-ignore no-explicit-any
+  constructor(privateParent: symbol, worker: any);
+  constructor(baseUrl: string, url: string);
+  constructor(arg1: unknown, arg2: unknown) {
+    if (arg1 == privateConstructor) {
+      this.#worker = arg2;
+    } else {
+      this.#worker = op_worker_spawn(arg1, arg2);
+    }
+  }
+
+  sendMessage(message: string) {
+    op_worker_send(this.#worker, message);
+  }
+
+  async receiveMessage(): Promise<string | undefined> {
+    return await op_worker_recv(this.#worker);
+  }
+
+  terminate() {
+    op_worker_terminate(this.#worker);
+  }
+
+  get closed(): Promise<void> {
+    return op_worker_await_close(this.#worker);
+  }
+
+  static get parent(): Worker {
+    if (parentWorker === null) {
+      parentWorker = new Worker(privateConstructor, op_worker_parent());
+    }
+    return parentWorker;
+  }
+}
diff --git a/testing/integration/error_async_stack/error_async_stack.out b/testing/integration/error_async_stack/error_async_stack.out
index 2ecf7c7..806bbe1 100644
--- a/testing/integration/error_async_stack/error_async_stack.out
+++ b/testing/integration/error_async_stack/error_async_stack.out
@@ -1,8 +1,8 @@
-[ERR] Error: async
-[ERR]     at test:///integration/error_async_stack/error_async_stack.js:5:13
-[ERR]     at async test:///integration/error_async_stack/error_async_stack.js:4:5
-[ERR]     at async test:///integration/error_async_stack/error_async_stack.js:9:5
 Error: async
     at test:///integration/error_async_stack/error_async_stack.js:5:13
     at async test:///integration/error_async_stack/error_async_stack.js:4:5
     at async test:///integration/error_async_stack/error_async_stack.js:9:5
+[ERR] Error: async
+[ERR]     at test:///integration/error_async_stack/error_async_stack.js:5:13
+[ERR]     at async test:///integration/error_async_stack/error_async_stack.js:4:5
+[ERR]     at async test:///integration/error_async_stack/error_async_stack.js:9:5
diff --git a/testing/integration/worker_spawn/worker.ts b/testing/integration/worker_spawn/worker.ts
new file mode 100644
index 0000000..8da2edb
--- /dev/null
+++ b/testing/integration/worker_spawn/worker.ts
@@ -0,0 +1,5 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { Worker } from "checkin:worker";
+Worker.parent.sendMessage("hello from client");
+const message = await Worker.parent.receiveMessage();
+console.log(`worker got from main "${message}"`);
diff --git a/testing/integration/worker_spawn/worker_spawn.out b/testing/integration/worker_spawn/worker_spawn.out
new file mode 100644
index 0000000..a861f46
--- /dev/null
+++ b/testing/integration/worker_spawn/worker_spawn.out
@@ -0,0 +1,5 @@
+main started
+main worker booted
+main got from worker "hello from client"
+worker got from main "ok from main"
+main exiting
diff --git a/testing/integration/worker_spawn/worker_spawn.ts b/testing/integration/worker_spawn/worker_spawn.ts
new file mode 100644
index 0000000..13c2ea3
--- /dev/null
+++ b/testing/integration/worker_spawn/worker_spawn.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { Worker } from "checkin:worker";
+console.log("main started");
+const worker = new Worker(import.meta.url, "./worker.ts");
+console.log("main worker booted");
+const message = await worker.receiveMessage();
+console.log(`main got from worker "${message}"`);
+worker.sendMessage("ok from main");
+await worker.closed;
+console.log("main exiting");
diff --git a/testing/integration/worker_terminate/worker.ts b/testing/integration/worker_terminate/worker.ts
new file mode 100644
index 0000000..8da2edb
--- /dev/null
+++ b/testing/integration/worker_terminate/worker.ts
@@ -0,0 +1,5 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { Worker } from "checkin:worker";
+Worker.parent.sendMessage("hello from client");
+const message = await Worker.parent.receiveMessage();
+console.log(`worker got from main "${message}"`);
diff --git a/testing/integration/worker_terminate/worker_terminate.out b/testing/integration/worker_terminate/worker_terminate.out
new file mode 100644
index 0000000..c48897a
--- /dev/null
+++ b/testing/integration/worker_terminate/worker_terminate.out
@@ -0,0 +1,5 @@
+main started
+main worker booted
+main got from worker "hello from client"
+[ERR] Uncaught Error: execution terminated
+main exiting
diff --git a/testing/integration/worker_terminate/worker_terminate.ts b/testing/integration/worker_terminate/worker_terminate.ts
new file mode 100644
index 0000000..5093784
--- /dev/null
+++ b/testing/integration/worker_terminate/worker_terminate.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { Worker } from "checkin:worker";
+console.log("main started");
+const worker = new Worker(import.meta.url, "./worker.ts");
+console.log("main worker booted");
+const message = await worker.receiveMessage();
+console.log(`main got from worker "${message}"`);
+worker.terminate();
+await worker.closed;
+console.log("main exiting");
diff --git a/testing/lib.rs b/testing/lib.rs
index 9a149a0..20e742e 100644
--- a/testing/lib.rs
+++ b/testing/lib.rs
@@ -59,4 +59,6 @@ integration_test!(
   timer_ref,
   timer_ref_and_cancel,
   timer_many,
+  worker_spawn,
+  worker_terminate,
 );
diff --git a/testing/tsconfig.json b/testing/tsconfig.json
index 6b80ae4..95250fd 100644
--- a/testing/tsconfig.json
+++ b/testing/tsconfig.json
@@ -5,7 +5,8 @@
     "paths": {
       "checkin:async": ["checkin/runtime/async.ts"],
       "checkin:console": ["checkin/runtime/console.ts"],
-      "checkin:testing": ["checkin/runtime/testing.ts"]
+      "checkin:testing": ["checkin/runtime/testing.ts"],
+      "checkin:worker": ["checkin/runtime/worker.ts"]
     },
     "lib": ["ESNext", "DOM", "ES2023.Array"],
     "module": "ES2022",