From 72a7c7e562db15f12919717fc661a53abb7065ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 4 Jan 2024 23:24:17 +0100 Subject: [PATCH 1/8] first pass at repl --- Cargo.lock | 178 ++++++++++++-- core/Cargo.toml | 2 + core/examples/cdp.rs | 528 ++++++++++++++++++++++++++++++++++++++++++ core/examples/repl.rs | 493 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 1186 insertions(+), 15 deletions(-) create mode 100644 core/examples/cdp.rs create mode 100644 core/examples/repl.rs diff --git a/Cargo.lock b/Cargo.lock index 8df00b155..470170cb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clipboard-win" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc" +dependencies = [ + "error-code", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -293,6 +302,8 @@ dependencies = [ "pin-project", "pretty_assertions", "rstest", + "rustyline", + "rustyline-derive", "serde", "serde_json", "serde_v8", @@ -435,15 +446,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "error-code" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" + [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -622,7 +650,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -759,7 +787,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -768,6 +796,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -845,7 +884,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1143,7 +1182,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1152,6 +1191,37 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rustyline" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "clipboard-win", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "107c3d5d7f370ac09efa62a78375f94d94b8a33c61d8c278b96683fb4dbf2d8d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1313,7 +1383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1849,7 +1919,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1948,6 +2018,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.11" @@ -1985,6 +2061,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.6.1" @@ -2064,7 +2146,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2073,13 +2164,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2088,42 +2194,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "yansi" version = "0.5.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 113029ac3..ae938064f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -53,6 +53,8 @@ pretty_assertions.workspace = true rstest.workspace = true fastrand.workspace = true unicycle = "0" +rustyline = { version = "13.0.0", default-features = false, features = ["with-file-history"] } +rustyline-derive = "=0.7.0" [[bench]] name = "ops_sync" diff --git a/core/examples/cdp.rs b/core/examples/cdp.rs new file mode 100644 index 000000000..659c94d45 --- /dev/null +++ b/core/examples/cdp.rs @@ -0,0 +1,528 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::serde::Deserialize; +use deno_core::serde::Deserializer; +use deno_core::serde::Serialize; +/// +use deno_core::serde_json; +use deno_core::serde_json::Value; + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AwaitPromiseArgs { + pub promise_object_id: RemoteObjectId, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_by_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_preview: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AwaitPromiseResponse { + pub result: RemoteObject, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallFunctionOnArgs { + pub function_declaration: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub silent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_by_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_gesture: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub await_promise: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_context_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_group: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub throw_on_side_effect: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallFunctionOnResponse { + pub result: RemoteObject, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompileScriptArgs { + pub expression: String, + #[serde(rename = "sourceURL")] + pub source_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_context_id: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompileScriptResponse { + pub script_id: Option, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluateArgs { + pub expression: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_group: Option, + #[serde( + rename = "includeCommandLineAPI", + skip_serializing_if = "Option::is_none" + )] + pub include_command_line_api: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub silent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub context_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_by_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_gesture: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub await_promise: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub throw_on_side_effect: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_breaks: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub repl_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "allowUnsafeEvalBlockedByCSP")] + pub allow_unsafe_eval_blocked_by_csp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unique_context_id: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluateResponse { + pub result: RemoteObject, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPropertiesArgs { + pub object_id: RemoteObjectId, + #[serde(skip_serializing_if = "Option::is_none")] + pub own_properties: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub accessor_properties_only: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub non_indexed_properties_only: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPropertiesResponse { + pub result: Vec, + pub internal_properties: Option>, + pub private_properties: Option>, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GlobalLexicalScopeNamesArgs { + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_context_id: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlobalLexicalScopeNamesResponse { + pub names: Vec, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct QueryObjectsArgs { + pub prototype_object_id: RemoteObjectId, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_group: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QueryObjectsResponse { + pub objects: RemoteObject, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseObjectArgs { + pub object_id: RemoteObjectId, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReleaseObjectGroupArgs { + pub object_group: String, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunScriptArgs { + pub script_id: ScriptId, + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_context_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_group: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub silent: Option, + #[serde( + rename = "includeCommandLineAPI", + skip_serializing_if = "Option::is_none" + )] + pub include_command_line_api: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub return_by_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub await_promise: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RunScriptResponse { + pub result: RemoteObject, + pub exception_details: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetAsyncCallStackDepthArgs { + pub max_depth: u64, +} + +// types + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteObject { + #[serde(rename = "type")] + pub kind: String, + pub subtype: Option, + pub class_name: Option, + #[serde(default, deserialize_with = "deserialize_some")] + pub value: Option, + pub unserializable_value: Option, + pub description: Option, + pub object_id: Option, + pub preview: Option, + pub custom_preview: Option, +} + +// Any value that is present is considered Some value, including null. +// ref: https://github.com/serde-rs/serde/issues/984#issuecomment-314143738 +fn deserialize_some<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + Deserialize::deserialize(deserializer).map(Some) +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ObjectPreview { + #[serde(rename = "type")] + pub kind: String, + pub subtype: Option, + pub description: Option, + pub overflow: bool, + pub properties: Vec, + pub entries: Option>, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PropertyPreview { + pub name: String, + #[serde(rename = "type")] + pub kind: String, + pub value: Option, + pub value_preview: Option, + pub subtype: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EntryPreview { + pub key: Option, + pub value: ObjectPreview, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomPreview { + pub header: String, + pub body_getter_id: RemoteObjectId, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExceptionDetails { + pub exception_id: u64, + pub text: String, + pub line_number: u64, + pub column_number: u64, + pub script_id: Option, + pub url: Option, + pub stack_trace: Option, + pub exception: Option, + pub execution_context_id: Option, + pub exception_meta_data: Option>, +} + +impl ExceptionDetails { + pub fn get_message_and_description(&self) -> (String, String) { + let description = self + .exception + .clone() + .and_then(|ex| ex.description) + .unwrap_or_else(|| "undefined".to_string()); + (self.text.to_string(), description) + } +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StackTrace { + pub description: Option, + pub call_frames: Vec, + pub parent: Option>, + pub parent_id: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallFrame { + pub function_name: String, + pub script_id: ScriptId, + pub url: String, + pub line_number: u64, + pub column_number: u64, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StackTraceId { + pub id: String, + pub debugger_id: Option, +} + +/// +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallArgument { + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unserializable_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object_id: Option, +} + +impl From<&RemoteObject> for CallArgument { + fn from(obj: &RemoteObject) -> Self { + Self { + value: obj.value.clone(), + unserializable_value: obj.unserializable_value.clone(), + object_id: obj.object_id.clone(), + } + } +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PropertyDescriptor { + pub name: String, + pub value: Option, + pub writable: Option, + pub get: Option, + pub set: Option, + pub configurable: bool, + pub enumerable: bool, + pub was_thrown: Option, + pub is_own: Option, + pub symbol: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InternalPropertyDescriptor { + pub name: String, + pub value: Option, +} + +/// +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrivatePropertyDescriptor { + pub name: String, + pub value: Option, + pub get: Option, + pub set: Option, +} + +/// +pub type RemoteObjectId = String; + +/// +pub type ExecutionContextId = u64; + +/// +pub type ScriptId = String; + +/// +pub type TimeDelta = u64; + +/// +pub type UnserializableValue = String; + +/// +pub type UniqueDebuggerId = String; + +/// +#[derive(Debug, Deserialize)] +pub struct SetScriptSourceResponse { + pub status: Status, +} + +#[derive(Debug, Deserialize)] +pub enum Status { + Ok, + CompileError, + BlockedByActiveGenerator, + BlockedByActiveFunction, + BlockedByTopLevelEsModuleChange, +} + +/// +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScriptParsed { + pub script_id: String, + pub url: String, +} + +/// +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CoverageRange { + /// Start character index. + #[serde(rename = "startOffset")] + pub start_char_offset: usize, + /// End character index. + #[serde(rename = "endOffset")] + pub end_char_offset: usize, + pub count: i64, +} + +/// +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FunctionCoverage { + pub function_name: String, + pub ranges: Vec, + pub is_block_coverage: bool, +} + +/// +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScriptCoverage { + pub script_id: String, + pub url: String, + pub functions: Vec, +} + +/// +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageArgs { + pub call_count: bool, + pub detailed: bool, + pub allow_triggered_updates: bool, +} + +/// +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageResponse { + pub timestamp: f64, +} + +/// +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TakePreciseCoverageResponse { + pub result: Vec, + pub timestamp: f64, +} + +#[derive(Debug, Deserialize)] +pub struct Notification { + pub method: String, + pub params: Value, +} +/// +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExceptionThrown { + pub timestamp: f64, + pub exception_details: ExceptionDetails, +} diff --git a/core/examples/repl.rs b/core/examples/repl.rs new file mode 100644 index 000000000..b4f047bbf --- /dev/null +++ b/core/examples/repl.rs @@ -0,0 +1,493 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use anyhow::Context as _; +use cdp::EvaluateResponse; +use deno_core::anyhow::anyhow; +use deno_core::anyhow::Error; +use deno_core::error::AnyError; +use deno_core::futures::channel::mpsc::UnboundedReceiver as FUnboundedReceiver; +use deno_core::serde_json; +use deno_core::serde_json::Value; +use deno_core::FsModuleLoader; +use deno_core::JsRuntime; +use deno_core::LocalInspectorSession; +use deno_core::PollEventLoopOptions; +use deno_core::RuntimeOptions; +use deno_unsync::spawn_blocking; +use futures::FutureExt; +use futures::StreamExt; +use parking_lot::Mutex; +use rustyline::completion::Completer; +use rustyline::error::ReadlineError; +use rustyline::highlight::Highlighter; +use rustyline::validate::ValidationContext; +use rustyline::validate::ValidationResult; +use rustyline::validate::Validator; +use rustyline::Cmd; +use rustyline::CompletionType; +use rustyline::Config; +use rustyline::Context; +use rustyline::Editor; +use rustyline::KeyCode; +use rustyline::KeyEvent; +use rustyline::Modifiers; +use rustyline::RepeatCount; +use rustyline_derive::Completer; +use rustyline_derive::Helper; +use rustyline_derive::Highlighter; +use rustyline_derive::Hinter; +use rustyline_derive::Validator; +use std::borrow::Cow; +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::Arc; +use tokio::sync::mpsc::channel; +use tokio::sync::mpsc::unbounded_channel; +use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::UnboundedSender; + +mod cdp; + +fn main() -> Result<(), Error> { + // let args: Vec = std::env::args().collect(); + // if args.len() < 2 { + // println!("Usage: target/examples/debug/fs_module_loader "); + // std::process::exit(1); + // } + // let main_url = &args[1]; + // println!("Run {main_url}"); + + let mut js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(Rc::new(FsModuleLoader)), + is_main: true, + ..Default::default() + }); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let future = async move { + let mut repl_session = ReplSession::initialize(js_runtime).await?; + let mut rustyline_channel = rustyline_channel(); + let helper = EditorHelper { + context_id: repl_session.context_id, + sync_sender: rustyline_channel.0, + }; + let editor = ReplEditor::new(helper)?; + + loop { + let line = read_line_and_poll_session( + &mut repl_session, + &mut rustyline_channel.1, + editor.clone(), + ) + .await; + match line { + Ok(line) => { + editor.set_should_exit_on_interrupt(false); + let output = repl_session.evaluate_line_and_get_output(&line).await; + + print!("{}", output); + } + Err(ReadlineError::Interrupted) => { + if editor.should_exit_on_interrupt() { + break; + } + editor.set_should_exit_on_interrupt(true); + println!("press ctrl+c again to exit"); + continue; + } + Err(ReadlineError::Eof) => { + break; + } + Err(err) => { + println!("Error: {err:?}"); + break; + } + }; + } + Ok(()) + }; + runtime.block_on(future) +} + +pub enum EvaluationOutput { + Value(String), + Error(String), +} + +impl std::fmt::Display for EvaluationOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvaluationOutput::Value(value) => f.write_str(value), + EvaluationOutput::Error(value) => f.write_str(value), + } + } +} + +pub fn result_to_evaluation_output( + r: Result, +) -> EvaluationOutput { + match r { + Ok(value) => value, + Err(err) => EvaluationOutput::Error(format!("error: {:#}", err)), + } +} + +struct ReplSession { + runtime: JsRuntime, + session: LocalInspectorSession, + context_id: u64, + notifications: Rc>>, +} + +impl ReplSession { + pub async fn initialize(mut runtime: JsRuntime) -> Result { + runtime.maybe_init_inspector(); + let mut session = runtime.inspector().borrow().create_local_session(); + + runtime + .with_event_loop_future( + session + .post_message::<()>("Runtime.enable", None) + .boxed_local(), + PollEventLoopOptions::default(), + ) + .await?; + + // Enabling the runtime domain will always send trigger one executionContextCreated for each + // context the inspector knows about so we grab the execution context from that since + // our inspector does not support a default context (0 is an invalid context id). + let context_id: u64; + let mut notification_rx = session.take_notification_rx(); + + loop { + let notification = notification_rx.next().await.unwrap(); + let method = notification.get("method").unwrap().as_str().unwrap(); + let params = notification.get("params").unwrap(); + if method == "Runtime.executionContextCreated" { + let context = params.get("context").unwrap(); + assert!(context + .get("auxData") + .unwrap() + .get("isDefault") + .unwrap() + .as_bool() + .unwrap()); + context_id = context.get("id").unwrap().as_u64().unwrap(); + break; + } + } + assert_ne!(context_id, 0); + + // TODO(bartlomieju): maybe execute some prelude here + + Ok(Self { + runtime, + session, + context_id, + notifications: Rc::new(RefCell::new(notification_rx)), + }) + } + + pub async fn post_message_with_event_loop( + &mut self, + method: &str, + params: Option, + ) -> Result { + self + .runtime + .with_event_loop_future( + self.session.post_message(method, params).boxed_local(), + PollEventLoopOptions { + // NOTE(bartlomieju): this is an important bit; we don't want to pump V8 + // message loop here, so that GC won't run. Otherwise, the resulting + // object might be GC'ed before we have a chance to inspect it. + pump_v8_message_loop: false, + ..Default::default() + }, + ) + .await + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + self + .runtime + .run_event_loop(PollEventLoopOptions { + wait_for_inspector: true, + pump_v8_message_loop: true, + }) + .await + } + + async fn evaluate_expression( + &mut self, + expression: &str, + ) -> Result { + let expr = format!("'use strict'; void 0;{expression}"); + + self + .post_message_with_event_loop( + "Runtime.evaluate", + Some(cdp::EvaluateArgs { + expression: expr, + object_group: None, + include_command_line_api: None, + silent: None, + context_id: Some(self.context_id), + return_by_value: None, + generate_preview: None, + user_gesture: None, + await_promise: None, + throw_on_side_effect: None, + timeout: None, + disable_breaks: None, + repl_mode: Some(true), + allow_unsafe_eval_blocked_by_csp: None, + unique_context_id: None, + }), + ) + .await + .and_then(|res| serde_json::from_value(res).map_err(|e| e.into())) + } + + async fn evaluate_line_and_get_output( + &mut self, + line: &str, + ) -> EvaluationOutput { + // Expressions like { "foo": "bar" } are interpreted as block expressions at the + // statement level rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + let wrapped_line = if line.trim_start().starts_with('{') + && !line.trim_end().ends_with(';') + { + format!("({})", &line) + } else { + line.to_string() + }; + + let evaluate_response = self.evaluate_expression(&wrapped_line).await; + + // If that fails, we retry it without wrapping in parens letting the error bubble up to the + // user if it is still an error. + let result = if wrapped_line != line + && (evaluate_response.is_err() + || evaluate_response + .as_ref() + .unwrap() + .exception_details + .is_some()) + { + self.evaluate_expression(line).await + } else { + evaluate_response + }; + + let output = match result { + Ok(evaluate_response) => { + let cdp::EvaluateResponse { + result, + exception_details, + } = evaluate_response; + + if let Some(exception_details) = exception_details { + let description = match exception_details.exception { + Some(exception) => { + if let Some(description) = exception.description { + description + } else if let Some(value) = exception.value { + value.to_string() + } else { + "undefined".to_string() + } + } + None => "Unknown exception".to_string(), + }; + EvaluationOutput::Error(format!( + "{} {}", + exception_details.text, description + )) + } else { + EvaluationOutput::Value(format!("{:#?}", result)) + } + } + Err(err) => EvaluationOutput::Error(err.to_string()), + }; + output + } +} + +async fn read_line_and_poll_session( + repl_session: &mut ReplSession, + message_handler: &mut RustylineSyncMessageHandler, + editor: ReplEditor, +) -> Result { + let mut line_fut = spawn_blocking(move || editor.readline()); + let mut poll_worker = true; + let notifications_rc = repl_session.notifications.clone(); + let mut notifications = notifications_rc.borrow_mut(); + + loop { + tokio::select! { + result = &mut line_fut => { + return result.unwrap(); + } + result = message_handler.recv() => { + match result { + Some(RustylineSyncMessage::PostMessage { method, params }) => { + let result = repl_session + .post_message_with_event_loop(&method, params) + .await; + message_handler.send(RustylineSyncResponse::PostMessage(result)).unwrap(); + }, + None => {}, // channel closed + } + + poll_worker = true; + } + message = notifications.next() => { + if let Some(message) = message { + let notification: cdp::Notification = serde_json::from_value(message).unwrap(); + if notification.method == "Runtime.exceptionThrown" { + let exception_thrown: cdp::ExceptionThrown = serde_json::from_value(notification.params).unwrap(); + let (message, description) = exception_thrown.exception_details.get_message_and_description(); + println!("{} {}", message, description); + } + } + } + _ = repl_session.run_event_loop(), if poll_worker => { + poll_worker = false; + } + } + } +} + +#[derive(Clone)] +struct ReplEditor { + inner: Arc>>, + should_exit_on_interrupt: Arc, +} + +impl ReplEditor { + pub fn new(helper: EditorHelper) -> Result { + let editor_config = Config::builder() + .completion_type(CompletionType::List) + .build(); + let mut editor = Editor::with_config(editor_config).unwrap(); + editor.set_helper(Some(helper)); + Ok(Self { + inner: Arc::new(Mutex::new(editor)), + should_exit_on_interrupt: Arc::new(AtomicBool::new(false)), + }) + } + + pub fn readline(&self) -> Result { + // TODO(bartlomieju): make prompt configurable + self.inner.lock().readline("> ") + } + + pub fn should_exit_on_interrupt(&self) -> bool { + self.should_exit_on_interrupt.load(Relaxed) + } + + pub fn set_should_exit_on_interrupt(&self, yes: bool) { + self.should_exit_on_interrupt.store(yes, Relaxed); + } +} + +#[derive(Helper, Hinter, Validator, Highlighter, Completer)] +struct EditorHelper { + pub context_id: u64, + pub sync_sender: RustylineSyncMessageSender, +} + +/// Rustyline uses synchronous methods in its interfaces, but we need to call +/// async methods. To get around this, we communicate with async code by using +/// a channel and blocking on the result. +pub fn rustyline_channel( +) -> (RustylineSyncMessageSender, RustylineSyncMessageHandler) { + let (message_tx, message_rx) = channel(1); + let (response_tx, response_rx) = unbounded_channel(); + + ( + RustylineSyncMessageSender { + message_tx, + response_rx: RefCell::new(response_rx), + }, + RustylineSyncMessageHandler { + response_tx, + message_rx, + }, + ) +} + +pub enum RustylineSyncMessage { + PostMessage { + method: String, + params: Option, + }, + // LspCompletions { + // line_text: String, + // position: usize, + // }, +} + +pub enum RustylineSyncResponse { + PostMessage(Result), + // LspCompletions(Vec), +} + +pub struct RustylineSyncMessageSender { + message_tx: Sender, + response_rx: RefCell>, +} + +impl RustylineSyncMessageSender { + pub fn post_message( + &self, + method: &str, + params: Option, + ) -> Result { + if let Err(err) = + self + .message_tx + .blocking_send(RustylineSyncMessage::PostMessage { + method: method.to_string(), + params: params + .map(|params| serde_json::to_value(params)) + .transpose()?, + }) + { + Err(anyhow!("{}", err)) + } else { + match self.response_rx.borrow_mut().blocking_recv().unwrap() { + RustylineSyncResponse::PostMessage(result) => result, + // RustylineSyncResponse::LspCompletions(_) => unreachable!(), + } + } + } +} + +pub struct RustylineSyncMessageHandler { + message_rx: Receiver, + response_tx: UnboundedSender, +} + +impl RustylineSyncMessageHandler { + pub async fn recv(&mut self) -> Option { + self.message_rx.recv().await + } + + pub fn send(&self, response: RustylineSyncResponse) -> Result<(), AnyError> { + self + .response_tx + .send(response) + .map_err(|err| anyhow!("{}", err)) + } +} From 72147623ac58281a1f067dc0aaf2ebb444c65126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 5 Jan 2024 00:27:54 +0100 Subject: [PATCH 2/8] down to one clippy error --- core/{examples => }/cdp.rs | 0 core/examples/repl.rs | 40 ++++++-------------------------------- core/lib.rs | 1 + 3 files changed, 7 insertions(+), 34 deletions(-) rename core/{examples => }/cdp.rs (100%) diff --git a/core/examples/cdp.rs b/core/cdp.rs similarity index 100% rename from core/examples/cdp.rs rename to core/cdp.rs diff --git a/core/examples/repl.rs b/core/examples/repl.rs index b4f047bbf..3e7d470d9 100644 --- a/core/examples/repl.rs +++ b/core/examples/repl.rs @@ -1,9 +1,8 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use anyhow::Context as _; -use cdp::EvaluateResponse; use deno_core::anyhow::anyhow; use deno_core::anyhow::Error; +use deno_core::cdp; use deno_core::error::AnyError; use deno_core::futures::channel::mpsc::UnboundedReceiver as FUnboundedReceiver; use deno_core::serde_json; @@ -17,29 +16,16 @@ use deno_unsync::spawn_blocking; use futures::FutureExt; use futures::StreamExt; use parking_lot::Mutex; -use rustyline::completion::Completer; use rustyline::error::ReadlineError; -use rustyline::highlight::Highlighter; -use rustyline::validate::ValidationContext; -use rustyline::validate::ValidationResult; -use rustyline::validate::Validator; -use rustyline::Cmd; use rustyline::CompletionType; use rustyline::Config; -use rustyline::Context; use rustyline::Editor; -use rustyline::KeyCode; -use rustyline::KeyEvent; -use rustyline::Modifiers; -use rustyline::RepeatCount; use rustyline_derive::Completer; use rustyline_derive::Helper; use rustyline_derive::Highlighter; use rustyline_derive::Hinter; use rustyline_derive::Validator; -use std::borrow::Cow; use std::cell::RefCell; -use std::path::PathBuf; use std::rc::Rc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering::Relaxed; @@ -51,18 +37,8 @@ use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; -mod cdp; - fn main() -> Result<(), Error> { - // let args: Vec = std::env::args().collect(); - // if args.len() < 2 { - // println!("Usage: target/examples/debug/fs_module_loader "); - // std::process::exit(1); - // } - // let main_url = &args[1]; - // println!("Run {main_url}"); - - let mut js_runtime = JsRuntime::new(RuntimeOptions { + let js_runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(FsModuleLoader)), is_main: true, ..Default::default() @@ -289,7 +265,7 @@ impl ReplSession { evaluate_response }; - let output = match result { + match result { Ok(evaluate_response) => { let cdp::EvaluateResponse { result, @@ -318,8 +294,7 @@ impl ReplSession { } } Err(err) => EvaluationOutput::Error(err.to_string()), - }; - output + } } } @@ -339,14 +314,11 @@ async fn read_line_and_poll_session( return result.unwrap(); } result = message_handler.recv() => { - match result { - Some(RustylineSyncMessage::PostMessage { method, params }) => { + if let Some(RustylineSyncMessage::PostMessage { method, params }) = result { let result = repl_session .post_message_with_event_loop(&method, params) .await; message_handler.send(RustylineSyncResponse::PostMessage(result)).unwrap(); - }, - None => {}, // channel closed } poll_worker = true; @@ -402,7 +374,7 @@ impl ReplEditor { } #[derive(Helper, Hinter, Validator, Highlighter, Completer)] -struct EditorHelper { +pub struct EditorHelper { pub context_id: u64, pub sync_sender: RustylineSyncMessageSender, } diff --git a/core/lib.rs b/core/lib.rs index cda4f05e6..78d022bb2 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -3,6 +3,7 @@ pub mod arena; mod async_cancel; mod async_cell; mod buffer_strategy; +pub mod cdp; pub mod error; mod error_codes; mod extensions; From 8aef56f48c94ba99c1ccde20263abc61c1f9aaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 5 Jan 2024 15:09:34 +0100 Subject: [PATCH 3/8] move around --- core/Cargo.toml | 4 + core/examples/{repl.rs => repl/mod.rs} | 185 +---------------------- core/examples/repl/session.rs | 197 +++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 182 deletions(-) rename core/examples/{repl.rs => repl/mod.rs} (59%) create mode 100644 core/examples/repl/session.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index ae938064f..4691d7f0b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -77,3 +77,7 @@ harness = false name = "arena" path = "benches/infra/arena.rs" harness = false + +[[example]] +name = "repl" +path = "examples/repl/mod.rs" \ No newline at end of file diff --git a/core/examples/repl.rs b/core/examples/repl/mod.rs similarity index 59% rename from core/examples/repl.rs rename to core/examples/repl/mod.rs index 3e7d470d9..8eabbfaf2 100644 --- a/core/examples/repl.rs +++ b/core/examples/repl/mod.rs @@ -37,6 +37,9 @@ use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; +mod session; +use session::ReplSession; + fn main() -> Result<(), Error> { let js_runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(FsModuleLoader)), @@ -116,188 +119,6 @@ pub fn result_to_evaluation_output( } } -struct ReplSession { - runtime: JsRuntime, - session: LocalInspectorSession, - context_id: u64, - notifications: Rc>>, -} - -impl ReplSession { - pub async fn initialize(mut runtime: JsRuntime) -> Result { - runtime.maybe_init_inspector(); - let mut session = runtime.inspector().borrow().create_local_session(); - - runtime - .with_event_loop_future( - session - .post_message::<()>("Runtime.enable", None) - .boxed_local(), - PollEventLoopOptions::default(), - ) - .await?; - - // Enabling the runtime domain will always send trigger one executionContextCreated for each - // context the inspector knows about so we grab the execution context from that since - // our inspector does not support a default context (0 is an invalid context id). - let context_id: u64; - let mut notification_rx = session.take_notification_rx(); - - loop { - let notification = notification_rx.next().await.unwrap(); - let method = notification.get("method").unwrap().as_str().unwrap(); - let params = notification.get("params").unwrap(); - if method == "Runtime.executionContextCreated" { - let context = params.get("context").unwrap(); - assert!(context - .get("auxData") - .unwrap() - .get("isDefault") - .unwrap() - .as_bool() - .unwrap()); - context_id = context.get("id").unwrap().as_u64().unwrap(); - break; - } - } - assert_ne!(context_id, 0); - - // TODO(bartlomieju): maybe execute some prelude here - - Ok(Self { - runtime, - session, - context_id, - notifications: Rc::new(RefCell::new(notification_rx)), - }) - } - - pub async fn post_message_with_event_loop( - &mut self, - method: &str, - params: Option, - ) -> Result { - self - .runtime - .with_event_loop_future( - self.session.post_message(method, params).boxed_local(), - PollEventLoopOptions { - // NOTE(bartlomieju): this is an important bit; we don't want to pump V8 - // message loop here, so that GC won't run. Otherwise, the resulting - // object might be GC'ed before we have a chance to inspect it. - pump_v8_message_loop: false, - ..Default::default() - }, - ) - .await - } - - pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { - self - .runtime - .run_event_loop(PollEventLoopOptions { - wait_for_inspector: true, - pump_v8_message_loop: true, - }) - .await - } - - async fn evaluate_expression( - &mut self, - expression: &str, - ) -> Result { - let expr = format!("'use strict'; void 0;{expression}"); - - self - .post_message_with_event_loop( - "Runtime.evaluate", - Some(cdp::EvaluateArgs { - expression: expr, - object_group: None, - include_command_line_api: None, - silent: None, - context_id: Some(self.context_id), - return_by_value: None, - generate_preview: None, - user_gesture: None, - await_promise: None, - throw_on_side_effect: None, - timeout: None, - disable_breaks: None, - repl_mode: Some(true), - allow_unsafe_eval_blocked_by_csp: None, - unique_context_id: None, - }), - ) - .await - .and_then(|res| serde_json::from_value(res).map_err(|e| e.into())) - } - - async fn evaluate_line_and_get_output( - &mut self, - line: &str, - ) -> EvaluationOutput { - // Expressions like { "foo": "bar" } are interpreted as block expressions at the - // statement level rather than an object literal so we interpret it as an expression statement - // to match the behavior found in a typical prompt including browser developer tools. - let wrapped_line = if line.trim_start().starts_with('{') - && !line.trim_end().ends_with(';') - { - format!("({})", &line) - } else { - line.to_string() - }; - - let evaluate_response = self.evaluate_expression(&wrapped_line).await; - - // If that fails, we retry it without wrapping in parens letting the error bubble up to the - // user if it is still an error. - let result = if wrapped_line != line - && (evaluate_response.is_err() - || evaluate_response - .as_ref() - .unwrap() - .exception_details - .is_some()) - { - self.evaluate_expression(line).await - } else { - evaluate_response - }; - - match result { - Ok(evaluate_response) => { - let cdp::EvaluateResponse { - result, - exception_details, - } = evaluate_response; - - if let Some(exception_details) = exception_details { - let description = match exception_details.exception { - Some(exception) => { - if let Some(description) = exception.description { - description - } else if let Some(value) = exception.value { - value.to_string() - } else { - "undefined".to_string() - } - } - None => "Unknown exception".to_string(), - }; - EvaluationOutput::Error(format!( - "{} {}", - exception_details.text, description - )) - } else { - EvaluationOutput::Value(format!("{:#?}", result)) - } - } - Err(err) => EvaluationOutput::Error(err.to_string()), - } - } -} - async fn read_line_and_poll_session( repl_session: &mut ReplSession, message_handler: &mut RustylineSyncMessageHandler, diff --git a/core/examples/repl/session.rs b/core/examples/repl/session.rs new file mode 100644 index 000000000..ffb0ad0cf --- /dev/null +++ b/core/examples/repl/session.rs @@ -0,0 +1,197 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_core::anyhow::Error; +use deno_core::cdp; +use deno_core::error::AnyError; +use deno_core::futures::channel::mpsc::UnboundedReceiver as FUnboundedReceiver; +use deno_core::serde_json; +use deno_core::serde_json::Value; +use deno_core::JsRuntime; +use deno_core::LocalInspectorSession; +use deno_core::PollEventLoopOptions; +use futures::FutureExt; +use futures::StreamExt; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct ReplSession { + runtime: JsRuntime, + session: LocalInspectorSession, + pub context_id: u64, + pub notifications: Rc>>, +} + +impl ReplSession { + pub async fn initialize(mut runtime: JsRuntime) -> Result { + runtime.maybe_init_inspector(); + let mut session = runtime.inspector().borrow().create_local_session(); + + runtime + .with_event_loop_future( + session + .post_message::<()>("Runtime.enable", None) + .boxed_local(), + PollEventLoopOptions::default(), + ) + .await?; + + // Enabling the runtime domain will always send trigger one executionContextCreated for each + // context the inspector knows about so we grab the execution context from that since + // our inspector does not support a default context (0 is an invalid context id). + let context_id: u64; + let mut notification_rx = session.take_notification_rx(); + + loop { + let notification = notification_rx.next().await.unwrap(); + let method = notification.get("method").unwrap().as_str().unwrap(); + let params = notification.get("params").unwrap(); + if method == "Runtime.executionContextCreated" { + let context = params.get("context").unwrap(); + assert!(context + .get("auxData") + .unwrap() + .get("isDefault") + .unwrap() + .as_bool() + .unwrap()); + context_id = context.get("id").unwrap().as_u64().unwrap(); + break; + } + } + assert_ne!(context_id, 0); + + // TODO(bartlomieju): maybe execute some prelude here + + Ok(Self { + runtime, + session, + context_id, + notifications: Rc::new(RefCell::new(notification_rx)), + }) + } + + pub async fn post_message_with_event_loop( + &mut self, + method: &str, + params: Option, + ) -> Result { + self + .runtime + .with_event_loop_future( + self.session.post_message(method, params).boxed_local(), + PollEventLoopOptions { + // NOTE(bartlomieju): this is an important bit; we don't want to pump V8 + // message loop here, so that GC won't run. Otherwise, the resulting + // object might be GC'ed before we have a chance to inspect it. + pump_v8_message_loop: false, + ..Default::default() + }, + ) + .await + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + self + .runtime + .run_event_loop(PollEventLoopOptions { + wait_for_inspector: true, + pump_v8_message_loop: true, + }) + .await + } + + async fn evaluate_expression( + &mut self, + expression: &str, + ) -> Result { + let expr = format!("'use strict'; void 0;{expression}"); + + self + .post_message_with_event_loop( + "Runtime.evaluate", + Some(cdp::EvaluateArgs { + expression: expr, + object_group: None, + include_command_line_api: None, + silent: None, + context_id: Some(self.context_id), + return_by_value: None, + generate_preview: None, + user_gesture: None, + await_promise: None, + throw_on_side_effect: None, + timeout: None, + disable_breaks: None, + repl_mode: Some(true), + allow_unsafe_eval_blocked_by_csp: None, + unique_context_id: None, + }), + ) + .await + .and_then(|res| serde_json::from_value(res).map_err(|e| e.into())) + } + + async fn evaluate_line_and_get_output( + &mut self, + line: &str, + ) -> EvaluationOutput { + // Expressions like { "foo": "bar" } are interpreted as block expressions at the + // statement level rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + let wrapped_line = if line.trim_start().starts_with('{') + && !line.trim_end().ends_with(';') + { + format!("({})", &line) + } else { + line.to_string() + }; + + let evaluate_response = self.evaluate_expression(&wrapped_line).await; + + // If that fails, we retry it without wrapping in parens letting the error bubble up to the + // user if it is still an error. + let result = if wrapped_line != line + && (evaluate_response.is_err() + || evaluate_response + .as_ref() + .unwrap() + .exception_details + .is_some()) + { + self.evaluate_expression(line).await + } else { + evaluate_response + }; + + match result { + Ok(evaluate_response) => { + let cdp::EvaluateResponse { + result, + exception_details, + } = evaluate_response; + + if let Some(exception_details) = exception_details { + let description = match exception_details.exception { + Some(exception) => { + if let Some(description) = exception.description { + description + } else if let Some(value) = exception.value { + value.to_string() + } else { + "undefined".to_string() + } + } + None => "Unknown exception".to_string(), + }; + EvaluationOutput::Error(format!( + "{} {}", + exception_details.text, description + )) + } else { + EvaluationOutput::Value(format!("{:#?}", result)) + } + } + Err(err) => EvaluationOutput::Error(err.to_string()), + } + } +} From 810b0307f255769a627dad713801e3786b7b30ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 6 Jan 2024 16:46:22 +0100 Subject: [PATCH 4/8] bring back latest updates from deno --- .gitignore | 1 + core/cdp.rs | 20 ++++++++++- core/examples/repl/mod.rs | 62 ++++++++++++++++++----------------- core/examples/repl/session.rs | 42 ++++++++++++++++-------- 4 files changed, 81 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index 9df3e845e..962f79e54 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ profile.json /tools/wpt/certs/serial* /ext/websocket/autobahn/reports +dprint.json \ No newline at end of file diff --git a/core/cdp.rs b/core/cdp.rs index 659c94d45..a69e60082 100644 --- a/core/cdp.rs +++ b/core/cdp.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +/// use deno_core::serde::Deserialize; use deno_core::serde::Deserializer; use deno_core::serde::Serialize; -/// use deno_core::serde_json; use deno_core::serde_json::Value; @@ -526,3 +526,21 @@ pub struct ExceptionThrown { pub timestamp: f64, pub exception_details: ExceptionDetails, } + +/// +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionContextCreated { + pub context: ExecutionContextDescription, +} + +/// +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionContextDescription { + pub id: ExecutionContextId, + pub origin: String, + pub name: String, + pub unique_id: String, + pub aux_data: Value, +} diff --git a/core/examples/repl/mod.rs b/core/examples/repl/mod.rs index 8eabbfaf2..edcc4aa9a 100644 --- a/core/examples/repl/mod.rs +++ b/core/examples/repl/mod.rs @@ -4,16 +4,12 @@ use deno_core::anyhow::anyhow; use deno_core::anyhow::Error; use deno_core::cdp; use deno_core::error::AnyError; -use deno_core::futures::channel::mpsc::UnboundedReceiver as FUnboundedReceiver; use deno_core::serde_json; use deno_core::serde_json::Value; use deno_core::FsModuleLoader; use deno_core::JsRuntime; -use deno_core::LocalInspectorSession; -use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; use deno_unsync::spawn_blocking; -use futures::FutureExt; use futures::StreamExt; use parking_lot::Mutex; use rustyline::error::ReadlineError; @@ -38,6 +34,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; mod session; +use session::EvaluationOutput; use session::ReplSession; fn main() -> Result<(), Error> { @@ -52,33 +49,52 @@ fn main() -> Result<(), Error> { .build()?; let future = async move { - let mut repl_session = ReplSession::initialize(js_runtime).await?; - let mut rustyline_channel = rustyline_channel(); + let session = ReplSession::initialize(js_runtime).await?; + let rustyline_channel = rustyline_channel(); let helper = EditorHelper { - context_id: repl_session.context_id, + context_id: session.context_id, sync_sender: rustyline_channel.0, }; let editor = ReplEditor::new(helper)?; + let mut repl = Repl { + session, + editor, + message_handler: rustyline_channel.1, + }; + repl.run().await?; + + Ok(()) + }; + runtime.block_on(future) +} + +struct Repl { + session: ReplSession, + editor: ReplEditor, + message_handler: RustylineSyncMessageHandler, +} +impl Repl { + async fn run(&mut self) -> Result<(), AnyError> { loop { let line = read_line_and_poll_session( - &mut repl_session, - &mut rustyline_channel.1, - editor.clone(), + &mut self.session, + &mut self.message_handler, + self.editor.clone(), ) .await; match line { Ok(line) => { - editor.set_should_exit_on_interrupt(false); - let output = repl_session.evaluate_line_and_get_output(&line).await; + self.editor.set_should_exit_on_interrupt(false); + let output = self.session.evaluate_line_and_get_output(&line).await; print!("{}", output); } Err(ReadlineError::Interrupted) => { - if editor.should_exit_on_interrupt() { + if self.editor.should_exit_on_interrupt() { break; } - editor.set_should_exit_on_interrupt(true); + self.editor.set_should_exit_on_interrupt(true); println!("press ctrl+c again to exit"); continue; } @@ -91,22 +107,8 @@ fn main() -> Result<(), Error> { } }; } - Ok(()) - }; - runtime.block_on(future) -} -pub enum EvaluationOutput { - Value(String), - Error(String), -} - -impl std::fmt::Display for EvaluationOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EvaluationOutput::Value(value) => f.write_str(value), - EvaluationOutput::Error(value) => f.write_str(value), - } + Ok(()) } } @@ -127,7 +129,7 @@ async fn read_line_and_poll_session( let mut line_fut = spawn_blocking(move || editor.readline()); let mut poll_worker = true; let notifications_rc = repl_session.notifications.clone(); - let mut notifications = notifications_rc.borrow_mut(); + let mut notifications = notifications_rc.lock().await; loop { tokio::select! { diff --git a/core/examples/repl/session.rs b/core/examples/repl/session.rs index ffb0ad0cf..d11ab8d5b 100644 --- a/core/examples/repl/session.rs +++ b/core/examples/repl/session.rs @@ -11,14 +11,14 @@ use deno_core::LocalInspectorSession; use deno_core::PollEventLoopOptions; use futures::FutureExt; use futures::StreamExt; -use std::cell::RefCell; -use std::rc::Rc; +use std::sync::Arc; +use tokio::sync::Mutex; pub struct ReplSession { runtime: JsRuntime, session: LocalInspectorSession, pub context_id: u64, - pub notifications: Rc>>, + pub notifications: Arc>>, } impl ReplSession { @@ -43,18 +43,20 @@ impl ReplSession { loop { let notification = notification_rx.next().await.unwrap(); - let method = notification.get("method").unwrap().as_str().unwrap(); - let params = notification.get("params").unwrap(); - if method == "Runtime.executionContextCreated" { - let context = params.get("context").unwrap(); - assert!(context - .get("auxData") - .unwrap() + let notification = + serde_json::from_value::(notification)?; + if notification.method == "Runtime.executionContextCreated" { + let execution_context_created = serde_json::from_value::< + cdp::ExecutionContextCreated, + >(notification.params)?; + assert!(execution_context_created + .context + .aux_data .get("isDefault") .unwrap() .as_bool() .unwrap()); - context_id = context.get("id").unwrap().as_u64().unwrap(); + context_id = execution_context_created.context.id; break; } } @@ -66,7 +68,7 @@ impl ReplSession { runtime, session, context_id, - notifications: Rc::new(RefCell::new(notification_rx)), + notifications: Arc::new(Mutex::new(notification_rx)), }) } @@ -131,7 +133,7 @@ impl ReplSession { .and_then(|res| serde_json::from_value(res).map_err(|e| e.into())) } - async fn evaluate_line_and_get_output( + pub async fn evaluate_line_and_get_output( &mut self, line: &str, ) -> EvaluationOutput { @@ -195,3 +197,17 @@ impl ReplSession { } } } + +pub enum EvaluationOutput { + Value(String), + Error(String), +} + +impl std::fmt::Display for EvaluationOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvaluationOutput::Value(value) => f.write_str(value), + EvaluationOutput::Error(value) => f.write_str(value), + } + } +} From 40c6158b34a50b1795c70973d9bfcc0279712f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 6 Jan 2024 16:48:50 +0100 Subject: [PATCH 5/8] some cleanup --- core/examples/repl/mod.rs | 9 ----- core/examples/repl/session.rs | 67 +++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/core/examples/repl/mod.rs b/core/examples/repl/mod.rs index edcc4aa9a..f83c2b5a1 100644 --- a/core/examples/repl/mod.rs +++ b/core/examples/repl/mod.rs @@ -112,15 +112,6 @@ impl Repl { } } -pub fn result_to_evaluation_output( - r: Result, -) -> EvaluationOutput { - match r { - Ok(value) => value, - Err(err) => EvaluationOutput::Error(format!("error: {:#}", err)), - } -} - async fn read_line_and_poll_session( repl_session: &mut ReplSession, message_handler: &mut RustylineSyncMessageHandler, diff --git a/core/examples/repl/session.rs b/core/examples/repl/session.rs index d11ab8d5b..45d9905aa 100644 --- a/core/examples/repl/session.rs +++ b/core/examples/repl/session.rs @@ -2,6 +2,7 @@ use deno_core::anyhow::Error; use deno_core::cdp; +use deno_core::cdp::EvaluateResponse; use deno_core::error::AnyError; use deno_core::futures::channel::mpsc::UnboundedReceiver as FUnboundedReceiver; use deno_core::serde_json; @@ -133,6 +134,41 @@ impl ReplSession { .and_then(|res| serde_json::from_value(res).map_err(|e| e.into())) } + fn evaluate_result_to_output( + result: Result, + ) -> EvaluationOutput { + match result { + Ok(evaluate_response) => { + let cdp::EvaluateResponse { + result, + exception_details, + } = evaluate_response; + + if let Some(exception_details) = exception_details { + let description = match exception_details.exception { + Some(exception) => { + if let Some(description) = exception.description { + description + } else if let Some(value) = exception.value { + value.to_string() + } else { + "undefined".to_string() + } + } + None => "Unknown exception".to_string(), + }; + EvaluationOutput::Error(format!( + "{} {}", + exception_details.text, description + )) + } else { + EvaluationOutput::Value(format!("{:#?}", result)) + } + } + Err(err) => EvaluationOutput::Error(err.to_string()), + } + } + pub async fn evaluate_line_and_get_output( &mut self, line: &str, @@ -165,36 +201,7 @@ impl ReplSession { evaluate_response }; - match result { - Ok(evaluate_response) => { - let cdp::EvaluateResponse { - result, - exception_details, - } = evaluate_response; - - if let Some(exception_details) = exception_details { - let description = match exception_details.exception { - Some(exception) => { - if let Some(description) = exception.description { - description - } else if let Some(value) = exception.value { - value.to_string() - } else { - "undefined".to_string() - } - } - None => "Unknown exception".to_string(), - }; - EvaluationOutput::Error(format!( - "{} {}", - exception_details.text, description - )) - } else { - EvaluationOutput::Value(format!("{:#?}", result)) - } - } - Err(err) => EvaluationOutput::Error(err.to_string()), - } + Self::evaluate_result_to_output(result) } } From 8ddca440a096a033afc73c773a05edbdd01db2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 6 Jan 2024 17:00:18 +0100 Subject: [PATCH 6/8] use tokio::main --- core/examples/repl/mod.rs | 40 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/core/examples/repl/mod.rs b/core/examples/repl/mod.rs index f83c2b5a1..9a79326b8 100644 --- a/core/examples/repl/mod.rs +++ b/core/examples/repl/mod.rs @@ -34,38 +34,30 @@ use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; mod session; -use session::EvaluationOutput; use session::ReplSession; -fn main() -> Result<(), Error> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Error> { let js_runtime = JsRuntime::new(RuntimeOptions { module_loader: Some(Rc::new(FsModuleLoader)), is_main: true, ..Default::default() }); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - let future = async move { - let session = ReplSession::initialize(js_runtime).await?; - let rustyline_channel = rustyline_channel(); - let helper = EditorHelper { - context_id: session.context_id, - sync_sender: rustyline_channel.0, - }; - let editor = ReplEditor::new(helper)?; - let mut repl = Repl { - session, - editor, - message_handler: rustyline_channel.1, - }; - repl.run().await?; - - Ok(()) + let session = ReplSession::initialize(js_runtime).await?; + let rustyline_channel = rustyline_channel(); + let helper = EditorHelper { + context_id: session.context_id, + sync_sender: rustyline_channel.0, + }; + let editor = ReplEditor::new(helper)?; + let mut repl = Repl { + session, + editor, + message_handler: rustyline_channel.1, }; - runtime.block_on(future) + repl.run().await?; + Ok(()) } struct Repl { @@ -88,7 +80,7 @@ impl Repl { self.editor.set_should_exit_on_interrupt(false); let output = self.session.evaluate_line_and_get_output(&line).await; - print!("{}", output); + println!("{}", output); } Err(ReadlineError::Interrupted) => { if self.editor.should_exit_on_interrupt() { From 0c9c95d86ed6d3e1d2c62d8910faf030a166880b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 6 Jan 2024 17:01:44 +0100 Subject: [PATCH 7/8] make prompt configurable --- core/examples/repl/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/examples/repl/mod.rs b/core/examples/repl/mod.rs index 9a79326b8..1633d76ea 100644 --- a/core/examples/repl/mod.rs +++ b/core/examples/repl/mod.rs @@ -50,7 +50,7 @@ async fn main() -> Result<(), Error> { context_id: session.context_id, sync_sender: rustyline_channel.0, }; - let editor = ReplEditor::new(helper)?; + let editor = ReplEditor::new(helper, Some(">>>".to_string()))?; let mut repl = Repl { session, editor, @@ -148,18 +148,23 @@ async fn read_line_and_poll_session( #[derive(Clone)] struct ReplEditor { + prompt: String, inner: Arc>>, should_exit_on_interrupt: Arc, } impl ReplEditor { - pub fn new(helper: EditorHelper) -> Result { + pub fn new( + helper: EditorHelper, + prompt: Option, + ) -> Result { let editor_config = Config::builder() .completion_type(CompletionType::List) .build(); let mut editor = Editor::with_config(editor_config).unwrap(); editor.set_helper(Some(helper)); Ok(Self { + prompt: prompt.unwrap_or_else(|| ">".to_string()), inner: Arc::new(Mutex::new(editor)), should_exit_on_interrupt: Arc::new(AtomicBool::new(false)), }) @@ -167,7 +172,7 @@ impl ReplEditor { pub fn readline(&self) -> Result { // TODO(bartlomieju): make prompt configurable - self.inner.lock().readline("> ") + self.inner.lock().readline(&self.prompt) } pub fn should_exit_on_interrupt(&self) -> bool { From 0ead3146456f197088b5b173b6ddcc84b0f78078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sun, 7 Jan 2024 23:40:19 +0100 Subject: [PATCH 8/8] rename --- core/examples/repl/session.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/examples/repl/session.rs b/core/examples/repl/session.rs index 45d9905aa..76f3289c9 100644 --- a/core/examples/repl/session.rs +++ b/core/examples/repl/session.rs @@ -17,7 +17,7 @@ use tokio::sync::Mutex; pub struct ReplSession { runtime: JsRuntime, - session: LocalInspectorSession, + inspector_session: LocalInspectorSession, pub context_id: u64, pub notifications: Arc>>, } @@ -25,11 +25,12 @@ pub struct ReplSession { impl ReplSession { pub async fn initialize(mut runtime: JsRuntime) -> Result { runtime.maybe_init_inspector(); - let mut session = runtime.inspector().borrow().create_local_session(); + let mut inspector_session = + runtime.inspector().borrow().create_local_session(); runtime .with_event_loop_future( - session + inspector_session .post_message::<()>("Runtime.enable", None) .boxed_local(), PollEventLoopOptions::default(), @@ -40,7 +41,7 @@ impl ReplSession { // context the inspector knows about so we grab the execution context from that since // our inspector does not support a default context (0 is an invalid context id). let context_id: u64; - let mut notification_rx = session.take_notification_rx(); + let mut notification_rx = inspector_session.take_notification_rx(); loop { let notification = notification_rx.next().await.unwrap(); @@ -67,7 +68,7 @@ impl ReplSession { Ok(Self { runtime, - session, + inspector_session, context_id, notifications: Arc::new(Mutex::new(notification_rx)), }) @@ -81,7 +82,10 @@ impl ReplSession { self .runtime .with_event_loop_future( - self.session.post_message(method, params).boxed_local(), + self + .inspector_session + .post_message(method, params) + .boxed_local(), PollEventLoopOptions { // NOTE(bartlomieju): this is an important bit; we don't want to pump V8 // message loop here, so that GC won't run. Otherwise, the resulting