diff --git a/.gitignore b/.gitignore index 0ed698b6..57d34ffc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ .bloop metals.sbt target -project/target \ No newline at end of file +project/target diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..32cfc61d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.watcherExclude": { + "**/target": true + } +} \ No newline at end of file diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala index a3cfd456..a5f30ba9 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala @@ -8,24 +8,6 @@ object JavaScriptBackend extends Backend { val ext = ".js" - override def generate(aqua: AquaRes): Seq[Generated] = { - val funcs = NonEmptyChain.fromChain( - aqua.funcs - .map(JavaScriptFunc(_)) - ) - funcs - .map(fs => - Seq( - Generated( - ext, - JavaScriptFile.Header + "\n\n" + fs - .map(_.generate) - .toChain - .toList - .mkString("\n\n") - ) - ) - ) - .getOrElse(Seq.empty) - } + override def generate(res: AquaRes): Seq[Generated] = + if (res.isEmpty) Nil else Generated(ext, JavaScriptFile(res).generate) :: Nil } diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala new file mode 100644 index 00000000..7eebecd1 --- /dev/null +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala @@ -0,0 +1,53 @@ +package aqua.backend.js + +import aqua.backend.air.FuncAirGen +import aqua.model.transform.res.FuncRes +import aqua.types.* +import cats.syntax.show.* + +object JavaScriptCommon { + + // TODO: handle cases if there is already peer_ or config_ variable defined + def fixupArgName(arg: String): String = + if (arg == "peer" || arg == "config") { + arg + "_" + } else { + arg + } + + def callBackExprBody(at: ArrowType, callbackName: String): String = { + val arrowArgumentsToCallbackArgumentsList = + at.domain.toList.zipWithIndex + .map(_._2) + .map(idx => s"req.args[$idx]") + .concat(List("callParams")) + .mkString(", ") + + val callCallbackStatement = s"$callbackName(${arrowArgumentsToCallbackArgumentsList})" + + val callCallbackStatementAndReturn = + at.res.fold(s"${callCallbackStatement}; resp.result = {}")(_ => + s"resp.result = ${callCallbackStatement}" + ) + + val tetraplets = FuncRes + .arrowArgs(at) + .zipWithIndex + .map((x, idx) => { + s"${x.name}: req.tetraplets[${idx}]" + }) + .mkString(",") + + s""" + | const callParams = { + | ...req.particleContext, + | tetraplets: { + | ${tetraplets} + | }, + | }; + | resp.retCode = ResultCodes.success; + | ${callCallbackStatementAndReturn} + |""".stripMargin + } + +} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala index e39013e6..41b3c845 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala @@ -1,6 +1,23 @@ package aqua.backend.js import aqua.backend.Version +import aqua.model.transform.res.AquaRes + +case class JavaScriptFile(res: AquaRes) { + + import JavaScriptFile.Header + + def generate: String = + s"""${Header} + | + |// Services + |${res.services.map(JavaScriptService(_)).map(_.generate).toList.mkString("\n\n")} + | + |// Functions + |${res.funcs.map(JavaScriptFunc(_)).map(_.generate).toList.mkString("\n\n")} + |""".stripMargin + +} object JavaScriptFile { @@ -13,7 +30,13 @@ object JavaScriptFile { | * Aqua version: ${Version.version} | * | */ - |import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable.js'; + |import { FluencePeer } from '@fluencelabs/fluence'; + |import { + | ResultCodes, + | RequestFlow, + | RequestFlowBuilder, + | CallParams, + |} from '@fluencelabs/fluence/dist/internal/compilerSupport/v1.js'; |""".stripMargin } diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala index 25140b3c..cd2fd71f 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala @@ -7,117 +7,132 @@ import cats.syntax.show.* case class JavaScriptFunc(func: FuncRes) { - import JavaScriptFunc._ + import JavaScriptCommon._ import FuncRes._ import func._ - def argsJavaScript: String = - argNames.mkString(", ") - - // TODO: use common functions between TypeScript and JavaScript backends - private def returnCallback: String = returnType.fold("") { retType => - val respBody = retType match { - case OptionType(_) => - """ let [opt] = args; - | if (Array.isArray(opt)) { - | if (opt.length === 0) { resolve(null); } - | opt = opt[0]; - | } - | return resolve(opt);""".stripMargin - case pt: ProductType => - val unwrapOpts = pt.toList.zipWithIndex.collect { case (OptionType(_), i) => - s""" - | if(Array.isArray(opt[$i])) { - | if (opt[$i].length === 0) { opt[$i] = null; } - | else {opt[$i] = opt[$i][0]; } - | }""".stripMargin - }.mkString - - s""" let opt = args; - |$unwrapOpts - | return resolve(opt);""".stripMargin - case _ => - """ const [res] = args; - | resolve(res);""".stripMargin + private def returnCallback: String = + val respBody = func.returnType match { + case Some(x) => x match { + case OptionType(_) => + """ let [opt] = args; + | if (Array.isArray(opt)) { + | if (opt.length === 0) { resolve(null); } + | opt = opt[0]; + | } + | return resolve(opt);""".stripMargin + case pt: ProductType => + val unwrapOpts = pt.toList.zipWithIndex.collect { case (OptionType(_), i) => + s""" + | if(Array.isArray(opt[$i])) { + | if (opt[$i].length === 0) { opt[$i] = null; } + | else {opt[$i] = opt[$i][0]; } + | }""".stripMargin + }.mkString + s""" let opt = args; + |$unwrapOpts + | return resolve(opt);""".stripMargin + case _ => + """ const [res] = args; + | resolve(res);""".stripMargin + } + case None => "" } s"""h.onEvent('$callbackServiceId', '$respFuncName', (args) => { | $respBody |}); |""".stripMargin - } def generate: String = { val jsAir = FuncAirGen(func).generate - val setCallbacks = func.args.collect { + val setCallbacks = func.args.collect { // Product types are not handled case Arg(argName, OptionType(_)) => - s"""h.on('$dataServiceId', '$argName', () => {return $argName === null ? [] : [$argName];});""" + s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)} === null ? [] : [${fixupArgName(argName)}];});""" case Arg(argName, _: DataType) => - s"""h.on('$dataServiceId', '$argName', () => {return $argName;});""" + s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)};});""" case Arg(argName, at: ArrowType) => - val value = s"$argName(${argsCallToJs( - at - )})" - val expr = arrowToRes(at).fold(s"$value; return {}")(_ => s"return $value") - s"""h.on('$callbackServiceId', '$argName', (args) => {$expr;});""" + s""" + | h.use((req, resp, next) => { + | if(req.serviceId === '${conf.callbackService}' && req.fnName === '$argName') { + | ${callBackExprBody(at, argName)} + | } + | next(); + | }); + """.stripMargin } .mkString("\n") val returnVal = - returnType.headOption.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") + func.returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") val clientArgName = genArgName("client") val configArgName = genArgName("config") + val funcName = s"${func.funcName}" + + val argsLets = args.map(arg => s"let ${fixupArgName(arg.name)};").mkString("\n") + + val argsFormAssingment = args + .map(arg => fixupArgName(arg.name)) + .concat(List("config")) + .zipWithIndex + + // Argument unpacking has two forms: + // One starting from the first (by index) argument, + // One starting from zero + val argsAssignmentStartingFrom1 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix + 1}];").mkString("\n") + val argsAssignmentStartingFrom0 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix}];").mkString("\n") + s""" - |export async function ${func.funcName}(${clientArgName}${if (func.args.isEmpty) "" - else ", "}${argsJavaScript}, $configArgName) { - | let request; - | $configArgName = $configArgName || {}; - | const promise = new Promise((resolve, reject) => { - | var r = new RequestFlowBuilder() - | .disableInjections() - | .withRawScript( - | ` - |${jsAir.show} - | `, - | ) - | .configHandler((h) => { - | ${relayVarName.fold("") { r => - s"""h.on('$dataServiceId', '$r', () => { - | return client.relayPeerId; - | });""".stripMargin - }} + | export function ${func.funcName}(...args) { + | let peer; + | ${argsLets} + | let config; + | if (args[0] instanceof FluencePeer) { + | peer = args[0]; + | ${argsAssignmentStartingFrom1} + | } else { + | peer = FluencePeer.default; + | ${argsAssignmentStartingFrom0} + | } + | + | let request; + | const promise = new Promise((resolve, reject) => { + | const r = new RequestFlowBuilder() + | .disableInjections() + | .withRawScript( + | ` + | ${jsAir.show} + | `, + | ) + | .configHandler((h) => { + | ${conf.relayVarName.fold("") { r => + s"""h.on('${conf.getDataService}', '$r', () => { + | return peer.connectionInfo.connectedRelay ; + | });""".stripMargin }} | $setCallbacks | $returnCallback - | h.onEvent('$errorHandlerId', '$errorFuncName', (args) => { - | // assuming error is the single argument + | h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => { | const [err] = args; | reject(err); | }); | }) | .handleScriptError(reject) | .handleTimeout(() => { - | reject('Request timed out for ${funcName}'); + | reject('Request timed out for ${func.funcName}'); | }) - | if(${configArgName}.ttl) { + | if(${configArgName} && ${configArgName}.ttl) { | r.withTTL(${configArgName}.ttl) | } | request = r.build(); | }); - | await ${clientArgName}.initiateFlow(request); + | peer.internals.initiateFlow(request); | return ${returnVal}; |} """.stripMargin } } - -object JavaScriptFunc { - - def argsCallToJs(at: ArrowType): String = - FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]").mkString(", ") - -} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala new file mode 100644 index 00000000..5b177026 --- /dev/null +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala @@ -0,0 +1,74 @@ +package aqua.backend.js + +import aqua.backend.air.FuncAirGen +import aqua.model.transform.res.FuncRes +import aqua.types.* +import cats.syntax.show.* +import aqua.model.transform.res.ServiceRes + +case class JavaScriptService(srv: ServiceRes) { + + import JavaScriptCommon._ + + def fnHandler(arrow: ArrowType, memberName: String) = { + s""" + | if (req.fnName === '${memberName}') { + | ${callBackExprBody(arrow, "service." + memberName)} + | } + """.stripMargin + } + + def generate: String = + val fnHandlers = srv.members + .map{ case (name, arrow) => + fnHandler(arrow, name) + } + .mkString("\n\n") + + val registerName = s"register${srv.name}" + + val defaultServiceIdBranch = srv.defaultId.fold("")(x => + s""" + | else { + | serviceId = ${x} + |}""".stripMargin + ) + + s""" + | export function ${registerName}(...args) { + | let peer; + | let serviceId; + | let service; + | if (args[0] instanceof FluencePeer) { + | peer = args[0]; + | } else { + | peer = FluencePeer.default; + | } + | + | if (typeof args[0] === 'string') { + | serviceId = args[0]; + | } else if (typeof args[1] === 'string') { + | serviceId = args[1]; + | } ${defaultServiceIdBranch} + | + | if (!(args[0] instanceof FluencePeer) && typeof args[0] === 'object') { + | service = args[0]; + | } else if (typeof args[1] === 'object') { + | service = args[1]; + | } else { + | service = args[2]; + | } + | + | peer.internals.callServiceHandler.use((req, resp, next) => { + | if (req.serviceId !== serviceId) { + | next(); + | return; + | } + | + | ${fnHandlers} + | + | next(); + | }); + | } + """.stripMargin +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala new file mode 100644 index 00000000..e95a19c2 --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala @@ -0,0 +1,105 @@ +package aqua.backend.ts + +import aqua.backend.air.FuncAirGen +import aqua.model.transform.res.FuncRes +import aqua.types.* +import cats.syntax.show.* + +object TypeScriptCommon { + + def typeToTs(t: Type): String = t match { + case OptionType(t) => typeToTs(t) + " | null" + case ArrayType(t) => typeToTs(t) + "[]" + case StreamType(t) => typeToTs(t) + "[]" + case pt: ProductType => + "[" + pt.toList.map(typeToTs).mkString(", ") + "]" + case st: StructType => + s"{${st.fields.map(typeToTs).toNel.map(kv => kv._1 + ":" + kv._2).toList.mkString(";")}}" + case st: ScalarType if ScalarType.number(st) => "number" + case ScalarType.bool => "boolean" + case ScalarType.string => "string" + case lt: LiteralType if lt.oneOf.exists(ScalarType.number) => "number" + case lt: LiteralType if lt.oneOf(ScalarType.bool) => "boolean" + case lt: LiteralType if lt.oneOf(ScalarType.string) => "string" + case _: DataType => "any" + case at: ArrowType => fnDef(at) + } + + // TODO: handle cases if there is already peer_ or config_ variable defined + def fixupArgName(arg: String): String = + if(arg == "peer" || arg == "config") { + arg + "_" + } else { + arg + } + + def returnType(at: ArrowType): String = + at.res.fold("void")(typeToTs) + + def fnDef(at: ArrowType): String = + val args = argsToTs(at) + .concat(List(callParamsArg(at))) + .mkString(", ") + + val retType = returnType(at) + + s"(${args}) => ${retType}" + + def argsToTs(at: ArrowType): List[String] = + FuncRes + .arrowArgs(at) + .map(nt => nt.name + ": " + typeToTs(nt.`type`)) + + def callParamsArg(at: ArrowType): String = + val args = FuncRes.arrowArgs(at) + val generic = if (args.length > 0) { + val prep = args + .map(_.name) + .mkString("' | '") + + "'" + prep + "'" + } else { + "null" + } + s"callParams: CallParams<${generic}>" + + def argsCallToTs(at: ArrowType): List[String] = + FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]") + + def callBackExprBody(at: ArrowType, callbackName: String): String = { + val arrowArgumentsToCallbackArgumentsList = + at.domain.toList + .zipWithIndex + .map(_._2) + .map(idx => s"req.args[$idx]") + .concat(List("callParams")) + .mkString(", ") + + val callCallbackStatement = s"$callbackName(${arrowArgumentsToCallbackArgumentsList})" + + val callCallbackStatementAndReturn = + at.res.fold(s"${callCallbackStatement}; resp.result = {}")(_ => + s"resp.result = ${callCallbackStatement}" + ) + + val tetraplets = FuncRes + .arrowArgs(at) + .zipWithIndex + .map((x, idx) => { + s"${x.name}: req.tetraplets[${idx}]" + }) + .mkString(",") + + s""" + | const callParams = { + | ...req.particleContext, + | tetraplets: { + | ${tetraplets} + | }, + | }; + | resp.retCode = ResultCodes.success; + | ${callCallbackStatementAndReturn} + |""".stripMargin + } + +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala index 172289b3..3b177309 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala @@ -30,9 +30,13 @@ object TypeScriptFile { | * Aqua version: ${Version.version} | * | */ - |import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence'; - |import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable'; - |import { RequestFlow } from '@fluencelabs/fluence/dist/internal/RequestFlow'; + |import { FluencePeer } from '@fluencelabs/fluence'; + |import { + | ResultCodes, + | RequestFlow, + | RequestFlowBuilder, + | CallParams, + |} from '@fluencelabs/fluence/dist/internal/compilerSupport/v1'; |""".stripMargin } diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala index 7ddfce3b..dab85308 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala @@ -7,44 +7,42 @@ import cats.syntax.show.* case class TypeScriptFunc(func: FuncRes) { - import TypeScriptFunc._ + import TypeScriptCommon._ import FuncRes._ import func._ - def argsTypescript: String = - args.map(ad => s"${ad.name}: " + typeToTs(ad.`type`)).mkString(", ") - - private def returnCallback: String = returnType.fold("") { retType => - val respBody = retType match { - case OptionType(_) => - """ let [opt] = args; - | if (Array.isArray(opt)) { - | if (opt.length === 0) { resolve(null); } - | opt = opt[0]; - | } - | return resolve(opt);""".stripMargin - case pt: ProductType => - val unwrapOpts = pt.toList.zipWithIndex.collect { case (OptionType(_), i) => - s""" - | if(Array.isArray(opt[$i])) { - | if (opt[$i].length === 0) { opt[$i] = null; } - | else {opt[$i] = opt[$i][0]; } - | }""".stripMargin - }.mkString - - s""" let opt: any = args; - |$unwrapOpts - | return resolve(opt);""".stripMargin - case _ => - """ const [res] = args; - | resolve(res);""".stripMargin + private def returnCallback: String = + val respBody = func.returnType match { + case Some(x) => x match { + case OptionType(_) => + """ let [opt] = args; + | if (Array.isArray(opt)) { + | if (opt.length === 0) { resolve(null); } + | opt = opt[0]; + | } + | return resolve(opt);""".stripMargin + case pt: ProductType => + val unwrapOpts = pt.toList.zipWithIndex.collect { case (OptionType(_), i) => + s""" + | if(Array.isArray(opt[$i])) { + | if (opt[$i].length === 0) { opt[$i] = null; } + | else {opt[$i] = opt[$i][0]; } + | }""".stripMargin + }.mkString + s""" let opt: any = args; + |$unwrapOpts + | return resolve(opt);""".stripMargin + case _ => + """ const [res] = args; + | resolve(res);""".stripMargin + } + case None => "" } s"""h.onEvent('$callbackServiceId', '$respFuncName', (args) => { | $respBody |}); |""".stripMargin - } def generate: String = { @@ -55,100 +53,103 @@ case class TypeScriptFunc(func: FuncRes) { val setCallbacks = func.args.collect { // Product types are not handled case Arg(argName, OptionType(_)) => - s"""h.on('$dataServiceId', '$argName', () => {return $argName === null ? [] : [$argName];});""" + s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)} === null ? [] : [${fixupArgName(argName)}];});""" case Arg(argName, _: DataType) => - s"""h.on('$dataServiceId', '$argName', () => {return $argName;});""" + s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)};});""" case Arg(argName, at: ArrowType) => - val value = s"$argName(${argsCallToTs( - at - )})" - val expr = arrowToRes(at).fold(s"$value; return {}")(_ => s"return $value") - s"""h.on('$callbackServiceId', '$argName', (args) => {$expr;});""" + s""" + | h.use((req, resp, next) => { + | if(req.serviceId === '${conf.callbackService}' && req.fnName === '$argName') { + | ${callBackExprBody(at, argName)} + | } + | next(); + | }); + """.stripMargin } .mkString("\n") val returnVal = - returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") + func.returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") val clientArgName = genArgName("client") val configArgName = genArgName("config") val configType = "{ttl?: number}" + val funcName = s"${func.funcName}" + + val argsTypescript = args + .map(arg => s"${fixupArgName(arg.name)}: " + typeToTs(arg.`type`)) + .concat(List(s"config?: $configType")) + + // defines different types for overloaded service registration function. + var funcTypeOverload1 = argsTypescript.mkString(", ") + var funcTypeOverload2 = ("peer: FluencePeer" :: argsTypescript).mkString(", ") + + val argsLets = args.map(arg => s"let ${fixupArgName(arg.name)};").mkString("\n") + + val argsFormAssingment = args + .map(arg => fixupArgName(arg.name)) + .concat(List("config")) + .zipWithIndex + + // argument upnacking has two forms. + // One starting from the first (by index) argument, + // One starting from zero + val argsAssignmentStartingFrom1 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix + 1}];").mkString("\n") + val argsAssignmentStartingFrom0 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix}];").mkString("\n") + + val funcTypeRes = s"Promise<$retTypeTs>" + s""" - |export async function ${funcName}($clientArgName: FluenceClient${if (args.isEmpty) - "" - else ", "}${argsTypescript}, $configArgName?: $configType): Promise<$retTypeTs> { - | let request: RequestFlow; - | const promise = new Promise<$retTypeTs>((resolve, reject) => { - | const r = new RequestFlowBuilder() - | .disableInjections() - | .withRawScript( - | ` - |${tsAir.show} - | `, - | ) - | .configHandler((h) => { - | ${relayVarName.fold("") { r => - s"""h.on('$dataServiceId', '$r', () => { - | return $clientArgName.relayPeerId!; - | });""".stripMargin - }} + | export function ${func.funcName}(${funcTypeOverload1}) : ${funcTypeRes}; + | export function ${func.funcName}(${funcTypeOverload2}) : ${funcTypeRes}; + | export function ${func.funcName}(...args) { + | let peer: FluencePeer; + | ${argsLets} + | let config; + | if (args[0] instanceof FluencePeer) { + | peer = args[0]; + | ${argsAssignmentStartingFrom1} + | } else { + | peer = FluencePeer.default; + | ${argsAssignmentStartingFrom0} + | } + | + | let request: RequestFlow; + | const promise = new Promise<$retTypeTs>((resolve, reject) => { + | const r = new RequestFlowBuilder() + | .disableInjections() + | .withRawScript( + | ` + | ${tsAir.show} + | `, + | ) + | .configHandler((h) => { + | ${conf.relayVarName.fold("") { r => + s"""h.on('${conf.getDataService}', '$r', () => { + | return peer.connectionInfo.connectedRelay ; + | });""".stripMargin }} | $setCallbacks | $returnCallback - | h.onEvent('$errorHandlerId', '$errorFuncName', (args) => { - | // assuming error is the single argument + | h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => { | const [err] = args; | reject(err); | }); | }) | .handleScriptError(reject) | .handleTimeout(() => { - | reject('Request timed out for ${funcName}'); + | reject('Request timed out for ${func.funcName}'); | }) | if(${configArgName} && ${configArgName}.ttl) { | r.withTTL(${configArgName}.ttl) | } | request = r.build(); | }); - | await $clientArgName.initiateFlow(request!); + | peer.internals.initiateFlow(request!); | return ${returnVal}; |} """.stripMargin } } - -object TypeScriptFunc { - - def typeToTs(t: Type): String = t match { - case OptionType(t) => typeToTs(t) + " | null" - case ArrayType(t) => typeToTs(t) + "[]" - case StreamType(t) => typeToTs(t) + "[]" - case pt: ProductType => - "[" + pt.toList.map(typeToTs).mkString(", ") + "]" - case st: StructType => - s"{${st.fields.map(typeToTs).toNel.map(kv => kv._1 + ":" + kv._2).toList.mkString(";")}}" - case st: ScalarType if ScalarType.number(st) => "number" - case ScalarType.bool => "boolean" - case ScalarType.string => "string" - case lt: LiteralType if lt.oneOf.exists(ScalarType.number) => "number" - case lt: LiteralType if lt.oneOf(ScalarType.bool) => "boolean" - case lt: LiteralType if lt.oneOf(ScalarType.string) => "string" - case _: DataType => "any" - case at: ArrowType => - s"(${argsToTs(at)}) => ${FuncRes - .arrowToRes(at) - .fold("void")(typeToTs)}" - } - - def argsToTs(at: ArrowType): String = - FuncRes - .arrowArgs(at) - .map(nt => nt.name + ": " + typeToTs(nt.`type`)) - .mkString(", ") - - def argsCallToTs(at: ArrowType): String = - FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]").mkString(", ") - -} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala index 349cdc7e..1c770943 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala @@ -1,20 +1,125 @@ package aqua.backend.ts +import aqua.backend.air.FuncAirGen +import aqua.model.transform.res.FuncRes +import aqua.types.* +import cats.syntax.show.* import aqua.model.transform.res.ServiceRes case class TypeScriptService(srv: ServiceRes) { - import TypeScriptFunc.typeToTs + import TypeScriptCommon._ + + def fnHandler(arrow: ArrowType, memberName: String) = { + s""" + | if (req.fnName === '${memberName}') { + | ${callBackExprBody(arrow, "service." + memberName)} + | } + """.stripMargin + } def generate: String = + val fnHandlers = srv.members + .map{ case (name, arrow) => + fnHandler(arrow, name) + } + .mkString("\n\n") + + val fnDefs = srv.members + .map{ case (name, arrow) => + s"${name}: ${fnDef(arrow)};" + } + .mkString("\n") + + val serviceTypeName = s"${srv.name}Def"; + + val registerName = s"register${srv.name}" + + // defined arguments used in overloads below + val peerDecl = "peer: FluencePeer"; + val serviceIdDecl = "serviceId: string"; + val serviceDecl = s"service: ${serviceTypeName}" + + // Service registration functions has several overloads. + // Depending on whether the the service has the default id or not + // there would be different number of overloads + // This variable contain defines the list of lists where + // the outmost list describes the list of overloads + // and the innermost one defines the list of arguments in the overload + val registerServiceArgsSource = srv.defaultId.fold( + List( + List(serviceIdDecl, serviceDecl), + List(peerDecl, serviceIdDecl, serviceDecl) + ) + )(_ => + List( + List(serviceDecl), + List(serviceIdDecl, serviceDecl), + List(peerDecl, serviceDecl), + List(peerDecl, serviceIdDecl, serviceDecl), + ) + ) + + // Service registration functions has several overloads. + // Depending on whether the the service has the default id or not + // there would be different number of overloads + // This variable contain defines the list of lists where + // the outmost list describes the list of overloads + // and the innermost one defines the list of arguments in the overload + val registerServiceArgs = registerServiceArgsSource. + map(x => { + val args = x.mkString(", ") + s"export function ${registerName}(${args}): void;" + }) + .mkString("\n"); + + val defaultServiceIdBranch = srv.defaultId.fold("")(x => + s""" + | else { + | serviceId = ${x} + |}""".stripMargin + ) + s""" - |//${srv.name} - |//defaultId = ${srv.defaultId.getOrElse("undefined")} - | - |${srv.members.map { case (n, v) => - s"//${n}: ${typeToTs(v)}" - }.mkString("\n")} - |//END ${srv.name} - | - |""".stripMargin + | export interface ${serviceTypeName} { + | ${fnDefs} + | } + | + | ${registerServiceArgs} + | export function ${registerName}(...args) { + | let peer: FluencePeer; + | let serviceId; + | let service; + | if (args[0] instanceof FluencePeer) { + | peer = args[0]; + | } else { + | peer = FluencePeer.default; + | } + | + | if (typeof args[0] === 'string') { + | serviceId = args[0]; + | } else if (typeof args[1] === 'string') { + | serviceId = args[1]; + | } ${defaultServiceIdBranch} + | + | if (!(args[0] instanceof FluencePeer) && typeof args[0] === 'object') { + | service = args[0]; + | } else if (typeof args[1] === 'object') { + | service = args[1]; + | } else { + | service = args[2]; + | } + | + | peer.internals.callServiceHandler.use((req, resp, next) => { + | if (req.serviceId !== serviceId) { + | next(); + | return; + | } + | + | ${fnHandlers} + | + | next(); + | }); + | } + """.stripMargin }