diff --git a/federation-1/harmonizer/js-src/composition.ts b/federation-1/harmonizer/js-src/composition.ts index e2af6f476..22b085777 100644 --- a/federation-1/harmonizer/js-src/composition.ts +++ b/federation-1/harmonizer/js-src/composition.ts @@ -73,10 +73,5 @@ function getPosition(token: Token): Position { //@ts-ignore function parseTypedefs(source: string) { - try { - return parse(source); - } catch (err) { - // Return the error in a way that we know how to handle it. - done({ Err: [{ message: err.toString() }] }); - } + return parse(source); } diff --git a/federation-1/harmonizer/js-src/do_compose.ts b/federation-1/harmonizer/js-src/do_compose.ts index b79e019d1..62e334fb6 100644 --- a/federation-1/harmonizer/js-src/do_compose.ts +++ b/federation-1/harmonizer/js-src/do_compose.ts @@ -7,17 +7,14 @@ import type { CompositionResult } from "./types"; * They'll be stripped in the emitting of this file as JS, of course. */ declare let composition_bridge: { composition: typeof composition }; - -declare let done: (compositionResult: CompositionResult) => void; declare let serviceList: { sdl: string; name: string; url: string }[]; +let result: CompositionResult; try { - /** - * @type {{ errors: Error[], supergraphSdl?: undefined } | { errors?: undefined, supergraphSdl: string; }} - */ - const composed = composition_bridge.composition(serviceList); - - done(composed); + result = composition_bridge.composition(serviceList); } catch (err) { - done({ Err: [err] }); + result = { Err: [err] }; } +// The JsRuntime::execute_script Rust function will return this top-level value, +// because it is the final completion value of the current script. +result; diff --git a/federation-1/harmonizer/js-src/runtime.js b/federation-1/harmonizer/js-src/runtime.js index 4bfbaa0fb..9cdfc9ca1 100644 --- a/federation-1/harmonizer/js-src/runtime.js +++ b/federation-1/harmonizer/js-src/runtime.js @@ -7,10 +7,6 @@ function print(value) { Deno.core.print(`${value.toString()}\n`); } -function done(result) { - Deno.core.ops.op_composition_result(result); -} - // We build some of the preliminary objects that our esbuilt package is // expecting to be present in the environment. // 'process' is a Node.js ism. We rely on process.env.NODE_ENV, in diff --git a/federation-1/harmonizer/src/js_types.rs b/federation-1/harmonizer/src/js_types.rs index 7246b9b47..df9ed3500 100644 --- a/federation-1/harmonizer/src/js_types.rs +++ b/federation-1/harmonizer/src/js_types.rs @@ -31,17 +31,6 @@ pub(crate) struct CompositionError { nodes: Option>, } -impl CompositionError { - pub(crate) fn generic(message: String) -> Self { - Self { - message: Some(message), - extensions: None, - code: None, - nodes: None, - } - } -} - impl Display for CompositionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let code = if let Some(extensions) = &self.extensions { diff --git a/federation-1/harmonizer/src/lib.rs b/federation-1/harmonizer/src/lib.rs index efdf3cf55..f3d5b2b03 100644 --- a/federation-1/harmonizer/src/lib.rs +++ b/federation-1/harmonizer/src/lib.rs @@ -29,9 +29,7 @@ composition implementation while we work toward something else. #![forbid(unsafe_code)] #![deny(missing_debug_implementations, nonstandard_style)] #![warn(missing_docs, future_incompatible, unreachable_pub, rust_2018_idioms)] -use deno_core::{error::AnyError, op, Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot}; -use std::borrow::Cow; -use std::sync::mpsc::{channel, Sender}; +use deno_core::{JsRuntime, RuntimeOptions, Snapshot}; mod js_types; @@ -47,22 +45,9 @@ pub fn harmonize(subgraph_definitions: Vec) -> BuildResult { // The snapshot is created in the build_harmonizer.rs script and included in our binary image let buffer = include_bytes!(concat!(env!("OUT_DIR"), "/composition.snap")); - // We'll use this channel to get the results - let (tx, rx) = channel::>(); - - let my_ext = Extension { - name: env!("CARGO_PKG_NAME"), - ops: Cow::Borrowed(&[op_composition_result::DECL]), - op_state_fn: Some(Box::new(move |state| { - state.put(tx); - })), - ..Default::default() - }; - // Use our snapshot to provision our new runtime let options = RuntimeOptions { startup_snapshot: Some(Snapshot::Static(buffer)), - extensions: vec![my_ext], ..Default::default() }; let mut runtime = JsRuntime::new(options); @@ -83,52 +68,50 @@ pub fn harmonize(subgraph_definitions: Vec) -> BuildResult { .expect("unable to evaluate service list in JavaScript runtime"); // run the unmodified do_compose.js file, which expects `serviceList` to be set - runtime - .execute_script( - "do_compose.js", - deno_core::FastString::Static(include_str!("../bundled/do_compose.js")), - ) - .expect("unable to invoke composition in JavaScript runtime"); - - // wait for a message from `op_composition_result` - rx.recv().expect("channel remains open") -} - -#[op] -fn op_composition_result( - state: &mut OpState, - value: serde_json::Value, -) -> Result { - // the JavaScript object can contain an array of errors - let deserialized_result: Result>, serde_json::Error> = - serde_json::from_value(value); - - let build_result: Result> = match deserialized_result { - Ok(build_result) => build_result, - Err(e) => Err(vec![CompositionError::generic(format!( - "Something went wrong, this is a bug: {e}" - ))]), - }; - - let build_output = build_result - .map(BuildOutput::new) - .map_err(|composition_errors| { - // we then embed that array of errors into the `BuildErrors` type which is implemented - // as a single error with each of the underlying errors listed as causes. - composition_errors - .iter() - .map(|err| BuildError::from(err.clone())) - .collect::() - }); - - let sender = state - .borrow::>>() - .clone(); - // send the build result - sender.send(build_output).expect("channel must be open"); - - // Don't return anything to JS since its value is unused - Ok(serde_json::json!(null)) + match runtime.execute_script( + "do_compose", + deno_core::FastString::Static(include_str!("../bundled/do_compose.js")), + ) { + Ok(execute_result) => { + let scope = &mut runtime.handle_scope(); + let local = deno_core::v8::Local::new(scope, execute_result); + match deno_core::serde_v8::from_v8::>>( + scope, local, + ) { + Ok(Ok(output)) => Ok(BuildOutput::new(output)), + Ok(Err(errors)) => { + let mut build_errors = BuildErrors::new(); + for error in errors { + build_errors.push(error.into()); + } + Err(build_errors) + } + Err(e) => { + let mut errors = BuildErrors::new(); + errors.push(BuildError::composition_error( + None, + Some(format!("Unable to deserialize composition result: {}", e)), + None, + None, + )); + Err(errors) + } + } + } + Err(e) => { + let mut errors = BuildErrors::new(); + errors.push(BuildError::composition_error( + None, + Some(format!( + "Error invoking composition in JavaScript runtime: {}", + e, + )), + None, + None, + )); + Err(errors) + } + } } #[cfg(test)] diff --git a/federation-2/harmonizer/js-src/composition.ts b/federation-2/harmonizer/js-src/composition.ts index 7a1a46c55..001feb819 100644 --- a/federation-2/harmonizer/js-src/composition.ts +++ b/federation-2/harmonizer/js-src/composition.ts @@ -148,15 +148,14 @@ function parseTypedefs(source: string, subgraphName: string) { ]; } - // Return the error in a way that we know how to handle it. - done({ - Err: [ - { - code: ERRORS.INVALID_GRAPHQL.code, - message: "[" + subgraphName + "] - " + err.toString(), - nodes: nodeTokens, - }, - ], - }); + const error: CompositionError = { + code: ERRORS.INVALID_GRAPHQL.code, + message: "[" + subgraphName + "] - " + err.toString(), + nodes: nodeTokens, + omittedNodesCount: 0, + }; + + // This error will be caught by the try-catch block in do_compose.ts. + throw error; } } diff --git a/federation-2/harmonizer/js-src/do_compose.ts b/federation-2/harmonizer/js-src/do_compose.ts index 9e40e447e..beb5bfd87 100644 --- a/federation-2/harmonizer/js-src/do_compose.ts +++ b/federation-2/harmonizer/js-src/do_compose.ts @@ -8,17 +8,22 @@ import type { CompositionResult } from "./types"; */ declare let composition_bridge: { composition: typeof composition }; -declare let done: (compositionResult: CompositionResult) => void; declare let serviceList: { sdl: string; name: string; url?: string }[]; declare let nodesLimit: number | null; +let result: CompositionResult; try { - // /** - // * @type {{ errors: Error[], supergraphSdl?: undefined, hints: undefined } | { errors?: undefined, supergraphSdl: string, hints: string }} - // */ - const composed = composition_bridge.composition(serviceList, nodesLimit); - - done(composed); -} catch (err) { - done({ Err: [{ message: err.toString() }] }); + result = composition_bridge.composition(serviceList, nodesLimit); +} catch (e) { + result = e && { + Err: [ + { + ...e, + message: e.toString(), + }, + ], + }; } +// The JsRuntime::execute_script Rust function will return this top-level value, +// because it is the final completion value of the current script. +result; diff --git a/federation-2/harmonizer/js-src/runtime.js b/federation-2/harmonizer/js-src/runtime.js index 827fd55a0..bed88076d 100644 --- a/federation-2/harmonizer/js-src/runtime.js +++ b/federation-2/harmonizer/js-src/runtime.js @@ -13,10 +13,6 @@ function print(value) { Deno.core.print(`${value.toString()}\n`); } -function done(result) { - Deno.core.ops["op_composition_result"](result); -} - // We build some of the preliminary objects that our esbuilt package is // expecting to be present in the environment. // 'process' is a Node.js ism. We rely on process.env.NODE_ENV, in diff --git a/federation-2/harmonizer/src/js_types.rs b/federation-2/harmonizer/src/js_types.rs index af5848064..0abb9fb55 100644 --- a/federation-2/harmonizer/src/js_types.rs +++ b/federation-2/harmonizer/src/js_types.rs @@ -34,18 +34,6 @@ pub(crate) struct CompositionError { omitted_nodes_count: Option, } -impl CompositionError { - pub(crate) fn generic(message: String) -> Self { - Self { - message: Some(message), - extensions: None, - code: None, - nodes: None, - omitted_nodes_count: Some(u32::default()), - } - } -} - impl Display for CompositionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let code = if let Some(extensions) = &self.extensions { diff --git a/federation-2/harmonizer/src/lib.rs b/federation-2/harmonizer/src/lib.rs index d0bab7edb..61abee2d1 100644 --- a/federation-2/harmonizer/src/lib.rs +++ b/federation-2/harmonizer/src/lib.rs @@ -29,9 +29,7 @@ composition implementation while we work toward something else. #![forbid(unsafe_code)] #![deny(missing_debug_implementations, nonstandard_style)] #![warn(missing_docs, future_incompatible, unreachable_pub, rust_2018_idioms)] -use deno_core::{op, Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot}; -use std::borrow::Cow; -use std::sync::mpsc::{channel, Sender}; +use deno_core::{JsRuntime, RuntimeOptions, Snapshot}; mod js_types; @@ -57,22 +55,9 @@ pub fn harmonize_limit( // The snapshot is created in the build_harmonizer.rs script and included in our binary image let buffer = include_bytes!(concat!(env!("OUT_DIR"), "/composition.snap")); - // We'll use this channel to get the results - let (tx, rx) = channel::>(); - - let my_ext = Extension { - name: env!("CARGO_PKG_NAME"), - ops: Cow::Borrowed(&[op_composition_result::DECL]), - op_state_fn: Some(Box::new(move |state| { - state.put(tx); - })), - ..Default::default() - }; - // Use our snapshot to provision our new runtime let options = RuntimeOptions { startup_snapshot: Some(Snapshot::Static(buffer)), - extensions: vec![my_ext], ..Default::default() }; let mut runtime = JsRuntime::new(options); @@ -109,44 +94,50 @@ pub fn harmonize_limit( .expect("unable to evaluate nodes limit in JavaScript runtime"); // run the unmodified do_compose.js file, which expects `serviceList` to be set - runtime - .execute_script( - "do_compose", - deno_core::FastString::Static(include_str!("../bundled/do_compose.js")), - ) - .expect("unable to invoke composition in JavaScript runtime"); - - // wait for a message from `op_composition_result` - rx.recv().expect("channel remains open") -} - -#[op] -fn op_composition_result(state: &mut OpState, value: serde_json::Value) { - // the JavaScript object can contain an array of errors - let deserialized_result: Result>, serde_json::Error> = - serde_json::from_value(value); - - let build_result: Result> = match deserialized_result { - Ok(build_result) => build_result, - Err(e) => Err(vec![CompositionError::generic(format!( - "Something went wrong, this is a bug: {e}" - ))]), - }; - - let build_result: BuildResult = build_result.map_err(|composition_errors| { - // we then embed that array of errors into the `BuildErrors` type which is implemented - // as a single error with each of the underlying errors listed as causes. - composition_errors - .iter() - .map(|err| BuildError::from(err.clone())) - .collect::() - }); - - let sender = state - .borrow::>>() - .clone(); - // send the build result - sender.send(build_result).expect("channel must be open"); + match runtime.execute_script( + "do_compose", + deno_core::FastString::Static(include_str!("../bundled/do_compose.js")), + ) { + Ok(execute_result) => { + let scope = &mut runtime.handle_scope(); + let local = deno_core::v8::Local::new(scope, execute_result); + match deno_core::serde_v8::from_v8::>>( + scope, local, + ) { + Ok(Ok(output)) => Ok(output), + Ok(Err(errors)) => { + let mut build_errors = BuildErrors::new(); + for error in errors { + build_errors.push(error.into()); + } + Err(build_errors) + } + Err(e) => { + let mut errors = BuildErrors::new(); + errors.push(BuildError::composition_error( + None, + Some(format!("Unable to deserialize composition result: {}", e)), + None, + None, + )); + Err(errors) + } + } + } + Err(e) => { + let mut errors = BuildErrors::new(); + errors.push(BuildError::composition_error( + None, + Some(format!( + "Error invoking composition in JavaScript runtime: {}", + e + )), + None, + None, + )); + Err(errors) + } + } } #[cfg(test)] diff --git a/federation-2/router-bridge/js-src/do_api_schema.ts b/federation-2/router-bridge/js-src/do_api_schema.ts index 7c6a4fcf7..4d5d17f60 100644 --- a/federation-2/router-bridge/js-src/do_api_schema.ts +++ b/federation-2/router-bridge/js-src/do_api_schema.ts @@ -8,14 +8,17 @@ import type { OperationResult } from "./types"; */ declare let bridge: { apiSchema: typeof apiSchema }; -declare let done: (operationResult: OperationResult) => void; declare let sdl: string; declare let graphqlValidation: boolean | undefined; const result = bridge.apiSchema(sdl, { graphqlValidation }); +let opResult: OperationResult; if (result.errors?.length > 0) { - done({ Err: result.errors }); + opResult = { Err: result.errors }; } else { - done({ Ok: result.data }); + opResult = { Ok: result.data }; } +// The JsRuntime::execute_script Rust function will return this top-level value, +// because it is the final completion value of the current script. +opResult; diff --git a/federation-2/router-bridge/js-src/do_introspect.ts b/federation-2/router-bridge/js-src/do_introspect.ts index 344193b84..153eab8bc 100644 --- a/federation-2/router-bridge/js-src/do_introspect.ts +++ b/federation-2/router-bridge/js-src/do_introspect.ts @@ -8,22 +8,22 @@ import type { OperationResult, QueryPlannerConfigExt } from "./types"; */ declare let bridge: { batchIntrospect: typeof batchIntrospect }; -declare let done: (operationResult: OperationResult) => void; declare let sdl: string; declare let queries: string[]; declare let config: QueryPlannerConfigExt; +let opResult: OperationResult; if (!sdl) { - done({ + opResult = { Err: [{ message: "Error in JS-Rust-land: SDL is empty." }], - }); -} - -try { - const introspected = bridge.batchIntrospect(sdl, queries, config); - done({ Ok: introspected }); -} catch (err) { - done({ - Err: err, - }); + }; +} else { + try { + opResult = { Ok: bridge.batchIntrospect(sdl, queries, config) }; + } catch (err) { + opResult = { Err: err }; + } } +// The JsRuntime::execute_script Rust function will return this top-level value, +// because it is the final completion value of the current script. +opResult; diff --git a/federation-2/router-bridge/js-src/runtime.js b/federation-2/router-bridge/js-src/runtime.js index 73b4d4847..3938bdbd8 100644 --- a/federation-2/router-bridge/js-src/runtime.js +++ b/federation-2/router-bridge/js-src/runtime.js @@ -13,10 +13,6 @@ function print(value) { Deno.core.print(`${value.toString()}\n`); } -function done(result) { - Deno.core.ops.deno_result(result); -} - crypto = { getRandomValues: (arg) => { Deno.core.ops.op_crypto_get_random_values(arg); diff --git a/federation-2/router-bridge/src/js.rs b/federation-2/router-bridge/src/js.rs index a4a8d0581..582e46915 100644 --- a/federation-2/router-bridge/src/js.rs +++ b/federation-2/router-bridge/src/js.rs @@ -1,15 +1,8 @@ use crate::error::Error; /// Wraps creating the Deno Js runtime collecting parameters and executing a script. -use deno_core::{ - anyhow::{anyhow, Error as AnyError}, - op, Extension, JsRuntime, Op, OpState, RuntimeOptions, Snapshot, -}; +use deno_core::{Extension, JsRuntime, RuntimeOptions, Snapshot}; use serde::de::DeserializeOwned; use serde::Serialize; -use std::{ - borrow::Cow, - sync::mpsc::{channel, Sender}, -}; // A reasonable default starting limit for our deno heap. const APOLLO_ROUTER_BRIDGE_EXPERIMENTAL_V8_INITIAL_HEAP_SIZE_DEFAULT: &str = "256"; @@ -44,26 +37,17 @@ impl Js { Ok(self) } - pub(crate) fn execute( + pub(crate) fn execute( &self, name: &'static str, source: &'static str, - ) -> Result { - // We'll use this channel to get the results - let (tx, rx) = channel::>(); - - let happy_tx = tx.clone(); - - let my_ext = Extension { + ) -> Result { + let noop_ext = Extension { name: env!("CARGO_PKG_NAME"), - ops: Cow::Borrowed(&[deno_result::::DECL]), - op_state_fn: Some(Box::new(move |state: &mut OpState| { - state.put(happy_tx); - })), ..Default::default() }; - let mut runtime = self.build_js_runtime(my_ext); + let mut runtime = self.build_js_runtime(noop_ext); for parameter in self.parameters.iter() { runtime @@ -74,20 +58,23 @@ impl Js { .expect("unable to evaluate service list in JavaScript runtime"); } - // We are sending the error through the channel already - let _ = runtime - .execute_script(name, deno_core::FastString::Static(source)) - .map_err(|e| { + match runtime.execute_script(name, deno_core::FastString::Static(source)) { + Ok(execute_result) => { + let scope = &mut runtime.handle_scope(); + let local = deno_core::v8::Local::new(scope, execute_result); + match deno_core::serde_v8::from_v8::(scope, local) { + Ok(result) => Ok(result), + Err(e) => Err(Error::DenoRuntime(format!( + "unable to deserialize result of `{name}` in JavaScript runtime \n error: \n {e:?}" + ))), + } + } + Err(e) => { let message = format!("unable to invoke `{name}` in JavaScript runtime \n error: \n {e:?}"); - - tx.send(Err(Error::DenoRuntime(message))) - .expect("channel must be open"); - - e - }); - - rx.recv().expect("channel remains open") + Err(Error::DenoRuntime(message)) + } + } } pub(crate) fn build_js_runtime(&self, my_ext: Extension) -> JsRuntime { @@ -197,15 +184,3 @@ impl Js { js_runtime } } - -#[op] -fn deno_result(state: &mut OpState, payload: Response) -> Result<(), AnyError> -where - Response: DeserializeOwned + 'static, -{ - // we're cloning here because we don't wanna keep the borrow across an await point - let sender = state.borrow::>>().clone(); - sender - .send(Ok(payload)) - .map_err(|e| anyhow!("couldn't send response {e}")) -} diff --git a/xtask/src/tools/cargo.rs b/xtask/src/tools/cargo.rs index f494471b7..ae9050258 100644 --- a/xtask/src/tools/cargo.rs +++ b/xtask/src/tools/cargo.rs @@ -7,7 +7,6 @@ use crate::tools::Runner; use crate::utils::{self, CommandOutput}; use crate::Result; -use std::convert::TryInto; use std::fs; pub(crate) struct CargoRunner { diff --git a/xtask/src/tools/git.rs b/xtask/src/tools/git.rs index 48b54ff18..0f49354e1 100644 --- a/xtask/src/tools/git.rs +++ b/xtask/src/tools/git.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, env, str::FromStr}; +use std::{env, str::FromStr}; use crate::{packages::PackageTag, tools::Runner, utils::CommandOutput};