Skip to content

Commit

Permalink
feat(compile): Add support for web workers in standalone mode
Browse files Browse the repository at this point in the history
This change enables the use of web workers in binaries produced by
`deno compile`. This requires the main module to be present in the
compiled module graph, which is why if it is not already, it must be
explicitly added as an additional root (see denoland#17663).
  • Loading branch information
andreubotella committed Mar 14, 2023
1 parent 1a4d647 commit becdde1
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 23 deletions.
115 changes: 92 additions & 23 deletions cli/standalone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use deno_core::anyhow::Context;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::futures::io::AllowStdIo;
use deno_core::futures::task::LocalFutureObj;
use deno_core::futures::AsyncReadExt;
use deno_core::futures::AsyncSeekExt;
use deno_core::futures::FutureExt;
Expand All @@ -26,12 +27,14 @@ use deno_core::ModuleLoader;
use deno_core::ModuleSpecifier;
use deno_core::ResolutionKind;
use deno_graph::source::Resolver;
use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel;
use deno_runtime::deno_web::BlobStore;
use deno_runtime::fmt_errors::format_js_error;
use deno_runtime::ops::worker_host::CreateWebWorkerCb;
use deno_runtime::ops::worker_host::WorkerEventCb;
use deno_runtime::permissions::Permissions;
use deno_runtime::permissions::PermissionsContainer;
use deno_runtime::permissions::PermissionsOptions;
use deno_runtime::web_worker::WebWorker;
use deno_runtime::web_worker::WebWorkerOptions;
use deno_runtime::worker::MainWorker;
use deno_runtime::worker::WorkerOptions;
use deno_runtime::BootstrapOptions;
Expand Down Expand Up @@ -125,9 +128,10 @@ fn u64_from_bytes(arr: &[u8]) -> Result<u64, AnyError> {
Ok(u64::from_be_bytes(*fixed_arr))
}

#[derive(Clone)]
struct EmbeddedModuleLoader {
eszip: eszip::EszipV2,
maybe_import_map_resolver: Option<CliGraphResolver>,
eszip: Arc<eszip::EszipV2>,
maybe_import_map_resolver: Option<Arc<CliGraphResolver>>,
}

impl ModuleLoader for EmbeddedModuleLoader {
Expand Down Expand Up @@ -222,6 +226,79 @@ fn metadata_to_flags(metadata: &Metadata) -> Flags {
}
}

fn web_worker_callback() -> Arc<WorkerEventCb> {
Arc::new(|worker| {
let fut = async move { Ok(worker) };
LocalFutureObj::new(Box::new(fut))
})
}

fn create_web_worker_callback(
ps: &ProcState,
module_loader: &Rc<EmbeddedModuleLoader>,
) -> Arc<CreateWebWorkerCb> {
let ps = ps.clone();
let module_loader = module_loader.as_ref().clone();
Arc::new(move |args| {
let module_loader = Rc::new(module_loader.clone());

let create_web_worker_cb = create_web_worker_callback(&ps, &module_loader);
let web_worker_cb = web_worker_callback();

let options = WebWorkerOptions {
bootstrap: BootstrapOptions {
args: ps.options.argv().clone(),
cpu_count: std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1),
debug_flag: ps.options.log_level().map_or(false, |l| l == Level::Debug),
enable_testing_features: false,
locale: deno_core::v8::icu::get_language_tag(),
location: Some(args.main_module.clone()),
no_color: !colors::use_color(),
is_tty: colors::is_tty(),
runtime_version: version::deno(),
ts_version: version::TYPESCRIPT.to_string(),
unstable: ps.options.unstable(),
user_agent: version::get_user_agent(),
inspect: ps.options.is_inspecting(),
},
extensions: ops::cli_exts(ps.clone()),
startup_snapshot: Some(crate::js::deno_isolate_init()),
unsafely_ignore_certificate_errors: ps
.options
.unsafely_ignore_certificate_errors()
.clone(),
root_cert_store: Some(ps.root_cert_store.clone()),
seed: ps.options.seed(),
module_loader,
npm_resolver: None, // not currently supported
create_web_worker_cb,
preload_module_cb: web_worker_cb.clone(),
pre_execute_module_cb: web_worker_cb,
format_js_error_fn: Some(Arc::new(format_js_error)),
source_map_getter: None,
worker_type: args.worker_type,
maybe_inspector_server: None,
get_error_class_fn: Some(&get_error_class_name),
blob_store: ps.blob_store.clone(),
broadcast_channel: ps.broadcast_channel.clone(),
shared_array_buffer_store: Some(ps.shared_array_buffer_store.clone()),
compiled_wasm_module_store: Some(ps.compiled_wasm_module_store.clone()),
cache_storage_dir: None,
stdio: Default::default(),
};

WebWorker::bootstrap_from_options(
args.name,
args.permissions,
args.main_module,
args.worker_id,
options,
)
})
}

pub async fn run(
eszip: eszip::EszipV2,
metadata: Metadata,
Expand All @@ -232,13 +309,11 @@ pub async fn run(
let permissions = PermissionsContainer::new(Permissions::from_options(
&metadata.permissions,
)?);
let blob_store = BlobStore::default();
let broadcast_channel = InMemoryBroadcastChannel::default();
let module_loader = Rc::new(EmbeddedModuleLoader {
eszip,
eszip: Arc::new(eszip),
maybe_import_map_resolver: metadata.maybe_import_map.map(
|(base, source)| {
CliGraphResolver::new(
Arc::new(CliGraphResolver::new(
None,
Some(Arc::new(
parse_from_json(&base, &source).unwrap().import_map,
Expand All @@ -247,21 +322,15 @@ pub async fn run(
ps.npm_api.clone(),
ps.npm_resolution.clone(),
ps.package_json_deps_installer.clone(),
)
))
},
),
});
let create_web_worker_cb = Arc::new(|_| {
todo!("Workers are currently not supported in standalone binaries");
});
let web_worker_cb = Arc::new(|_| {
todo!("Workers are currently not supported in standalone binaries");
});
let create_web_worker_cb = create_web_worker_callback(&ps, &module_loader);
let web_worker_cb = web_worker_callback();

v8_set_flags(construct_v8_flags(&metadata.v8_flags, vec![]));

let root_cert_store = ps.root_cert_store.clone();

let options = WorkerOptions {
bootstrap: BootstrapOptions {
args: metadata.argv,
Expand All @@ -280,11 +349,11 @@ pub async fn run(
user_agent: version::get_user_agent(),
inspect: ps.options.is_inspecting(),
},
extensions: ops::cli_exts(ps),
extensions: ops::cli_exts(ps.clone()),
startup_snapshot: Some(crate::js::deno_isolate_init()),
unsafely_ignore_certificate_errors: metadata
.unsafely_ignore_certificate_errors,
root_cert_store: Some(root_cert_store),
root_cert_store: Some(ps.root_cert_store.clone()),
seed: metadata.seed,
source_map_getter: None,
format_js_error_fn: Some(Arc::new(format_js_error)),
Expand All @@ -299,10 +368,10 @@ pub async fn run(
get_error_class_fn: Some(&get_error_class_name),
cache_storage_dir: None,
origin_storage_dir: None,
blob_store,
broadcast_channel,
shared_array_buffer_store: None,
compiled_wasm_module_store: None,
blob_store: ps.blob_store.clone(),
broadcast_channel: ps.broadcast_channel.clone(),
shared_array_buffer_store: Some(ps.shared_array_buffer_store.clone()),
compiled_wasm_module_store: Some(ps.compiled_wasm_module_store.clone()),
stdio: Default::default(),
};
let mut worker = MainWorker::bootstrap_from_options(
Expand Down
85 changes: 85 additions & 0 deletions cli/tests/integration/compile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,91 @@ fn check_local_by_default2() {
));
}

#[test]
fn workers_basic() {
let _guard = util::http_server();
let dir = TempDir::new();
let exe = if cfg!(windows) {
dir.path().join("basic.exe")
} else {
dir.path().join("basic")
};
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("compile")
.arg("--no-check")
.arg("--output")
.arg(&exe)
.arg(util::testdata_path().join("./compile/workers/basic.ts"))
.output()
.unwrap();
assert!(output.status.success());

let output = Command::new(&exe).output().unwrap();
assert!(output.status.success());
let expected = std::fs::read_to_string(
util::testdata_path().join("./compile/workers/basic.out"),
)
.unwrap();
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}

#[test]
fn workers_not_in_module_map() {
let _guard = util::http_server();
let dir = TempDir::new();
let exe = if cfg!(windows) {
dir.path().join("not_in_module_map.exe")
} else {
dir.path().join("not_in_module_map")
};
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("compile")
.arg("--output")
.arg(&exe)
.arg(util::testdata_path().join("./compile/workers/not_in_module_map.ts"))
.output()
.unwrap();
assert!(output.status.success());

let output = Command::new(&exe).env("NO_COLOR", "").output().unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.starts_with(concat!(
"error: Uncaught (in worker \"\") Module not found\n",
"error: Uncaught (in promise) Error: Unhandled error in child worker.\n"
)));
}

#[test]
fn workers_side_modules() {
let _guard = util::http_server();
let dir = TempDir::new();
let exe = if cfg!(windows) {
dir.path().join("side_modules.exe")
} else {
dir.path().join("side_modules")
};
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("compile")
.arg("--side-module")
.arg(util::testdata_path().join("./compile/workers/worker.ts"))
.arg("--output")
.arg(&exe)
.arg(util::testdata_path().join("./compile/workers/not_in_module_map.ts"))
.output()
.unwrap();
assert!(output.status.success());

let output = Command::new(&exe).env("NO_COLOR", "").output().unwrap();
assert!(output.status.success());
let expected_stdout =
concat!("Hello from worker!\n", "Received 42\n", "Closing\n");
assert_eq!(&String::from_utf8(output.stdout).unwrap(), expected_stdout);
}

#[test]
fn dynamic_import() {
let _guard = util::http_server();
Expand Down
5 changes: 5 additions & 0 deletions cli/tests/testdata/compile/workers/basic.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
worker.js imported from main thread
Starting worker
Hello from worker!
Received 42
Closing
11 changes: 11 additions & 0 deletions cli/tests/testdata/compile/workers/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "./worker.ts";

console.log("Starting worker");
const worker = new Worker(
new URL("./worker.ts", import.meta.url),
{ type: "module" },
);

setTimeout(() => {
worker.postMessage(42);
}, 500);
11 changes: 11 additions & 0 deletions cli/tests/testdata/compile/workers/not_in_module_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This time ./worker.ts is not in the module map, so the worker
// initialization will fail unless worker.js is passed as a side module.

const worker = new Worker(
new URL("./worker.ts", import.meta.url),
{ type: "module" },
);

setTimeout(() => {
worker.postMessage(42);
}, 500);
14 changes: 14 additions & 0 deletions cli/tests/testdata/compile/workers/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference no-default-lib="true" />
/// <reference lib="deno.worker" />

if (import.meta.main) {
console.log("Hello from worker!");

addEventListener("message", (evt) => {
console.log(`Received ${evt.data}`);
console.log("Closing");
self.close();
});
} else {
console.log("worker.js imported from main thread");
}

0 comments on commit becdde1

Please sign in to comment.