Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js-client)!: Segregation of responsibility between js-client packages [fixes DXJ-525] #378

Merged
merged 51 commits into from
Nov 19, 2023

Conversation

akim-bow
Copy link
Contributor

What have been done

  • Js-client doesn't care about aqua types anymore, interface simpler
  • added validation to builtins and public API
  • various bug fixes and optimizations

@akim-bow akim-bow requested review from folex and shamsartem November 14, 2023 14:03
Copy link

linear bot commented Nov 14, 2023

DXJ-525 Ensure validation for js-client services

Need to add validation for:
1. builtin services

  1. user services

@akim-bow akim-bow added the e2e Run e2e workflow label Nov 14, 2023
FluencePeer as FluencePeer$$
} from '@fluencelabs/js-client';

${converters}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea to hardcode the body of these functions in each compiled file
Don't understand the reason to do this
You already export v5_callFunction, v5_registerService and FluencePeer from '@fluencelabs/js-client' why not export these functions as well from '@fluencelabs/js-client' and use them normally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It went through hard refactoring process and end up in the aqua-to-js as i decided to completely remove any schema mentions from js-client. It uses schema to perform conversion. We have 3 options

  1. Make an exception for this case (easier)
  2. Minify and uglify this snippet to minimize it's impact on frontend (every function from every file would be bundled here)
  3. Support batch compilation in aqua-to-js package. As a result every batch compilation will generate a single converter.js file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to move it back to js-client


console.log("marine test finished, result: ", marine);
// TODO: Currently this behavior will timeout after TTL bcs this function don't call 'responseSrc'. Will be attended in the next updates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this matter can be safely moved to another issue? Is safe to release js-client without this fixed? If not - I would rather solve this issue before merging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it wasn't working before. For aqua function without return values (no responseSrv calls) it's impossible to determine when function call should finish. After your comment I reviewed that, fixed this particular test with hacks. But it still will break on some cases in parallel air. To reliably fix this we need to wait for LNG-286

return null;
} else if (schema.tag === "option") {
if (!Array.isArray(value)) {
throw new Error("Bad schema");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better error message could be useful, if you think a bit about when this error pottentially can happen. I would create an error text that explains what went wrong in general (e.g. Wasn't able to convert aqua to js) and then for each case some particulars that could be useful for debugging (e.g.schema generated by the aqua comiler expects an option and options are represented as js arrays but found type: ${typeof value}: ${value}) If aqua2ts recursive function also accepts the third argument which is context - then you can also store some context that can be useful to display in the error so it says how deep the error accured e.g. error happened at: someProperty.someNestedProperty.someNetedArray[0]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These errors are impossible actually. Only if you write your own wrong schema by hand and use it. But this is not the case. I think that no need to spend much efforts improving this. This made for satisfying ts, not for giving decent error messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These errors are tottally possible if something goes wrong with the compiler and for guys who write the compiler at least a little effort here would be useful to understand where the problem happened

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, probably we can do some runtime validation here

];
const registerServiceArgs =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(srvDef.defaultServiceId as DefaultServiceId).s_Some__f_value != null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks pretty hacky to be honest. Sometimes defaultServiceId is not a string and has some weird property in it? I would avoid this, should be possible somehow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno how to avoid :)
It's actual value that comes from aqua compiler

Copy link
Contributor Author

@akim-bow akim-bow Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can discuss this someday with aqua team. I created a task

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this comes from the compiler why not put this in types instead of just putting string there and asserting the type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because these types are generated from compiler team :)

Copy link
Contributor Author

@akim-bow akim-bow Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's 1 more question to ask how to split these types. I'm feeling ok to keep them in aqia-api.

Comment on lines 58 to 82
const argNames = [${argNames
.map((arg) => {
return `"${arg}"`;
})
.join(", ")}];
const argCount = argNames.length;
let peer = undefined;
if (args[0] instanceof FluencePeer$$) {
peer = args[0];
args = args.slice(1);
}


const callArgs = Object.fromEntries(args.slice(0, argCount).map((arg, i) => [argNames[i], arg]));

const params = ({
peer,
args: callArgs,
config: args[argCount]
});

const result = await callFunction$$({
script: ${scriptConstName},
...params,
});
Copy link
Contributor

@shamsartem shamsartem Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part also feels strange to hard-code and duplicate for each compiled function
I believe there is no reason for compiled wrappers to contain any duplicated code
They should literally contatin only the functions to call with hardcoded air and schemas passed down to them
If you want to move this internally from one package to another for some reason - this is fine

Copy link
Contributor

@shamsartem shamsartem Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are able to do ts inference from schemas some day then it would be probably enough to just compile aqua and save the result as JSON

{
 "services": {
   "service1": {
     "schema": ...
   },
  ...
 },
 "functions": {
   "function1": {
     "schema": ...
     "air": ...
   }
 }
}

and then on ts side

import someAqua from "compiled-aqua.json"
import { aquaToJs } from "@fluencelabs/js-client"

// service1 and function1 types are infered from compiled-aqua.json
const { registerService1, function1 } = aquaToJs(someAqua)

so nothing has to be duplicated at all

or e.g additionally generate simple ts files that would just do something like this:

import { aquaToJs } from '@fluencelabs/js-client'
const someCompiledAqua = { ... }
export default aquaToJs(someCompiledAqua)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all dublicated code

Comment on lines 71 to 72
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.path.length === 1 && typeof issue.path[0] === "number") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be combined into one if

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some reason no, ts won't infer the type correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this case can be fixed as you suggested. I thought that it was about very similar case in conversion.ts file

Comment on lines +84 to +85
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} argument(s). Got ${ctx.data.length}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to have a variable where you access this any and disable esling only once

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not possible to do this somewhere above these conditions. This prop available only when condition is met

req: CallServiceData,
): [z.infer<T>, null] | [null, string] => {
const result = schema.safeParse(req.args, {
errorMap: (issue, ctx) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider having another function that just returns a string and for this function to only do
return { message: getErrorMsg(issue, ctx) } so you don't repeat { message: part everywhere. Up to you

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestion. I don't want to refactor this rn because not sure about error messages completely. Probably i will do message reporting in other way, e.g. add a dedicated lib.

Comment on lines 312 to 317
concat: withSchema(z.array(z.array(z.unknown())))((args) => {
// concat accepts only 'never' type
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const arr = args as never[][];
return success([].concat(...arr));
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do?

Suggested change
concat: withSchema(z.array(z.array(z.unknown())))((args) => {
// concat accepts only 'never' type
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const arr = args as never[][];
return success([].concat(...arr));
}),
concat: withSchema(z.array(z.array(jsonSchema)))((args) => {
return success(args.flat());
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will add slow runtime check which is completely useless in this scenario

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about .flat() with typecast and explanation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = "".concat(...(req.args as string[]));
concat_strings: withSchema(z.array(z.string()))((args) => {
const res = "".concat(...args);
return success(res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just

Suggested change
return success(res);
return success(args.join(''));

throw new SchemaValidationError(path, schema, "string", arg);
}
} else {
throw new SchemaValidationError(path, schema, "never", arg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe would be more useful

Suggested change
throw new SchemaValidationError(path, schema, "never", arg);
throw new SchemaValidationError(path, schema, schema.name, arg);

Copy link
Contributor

@shamsartem shamsartem Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe even more useful would be to throw a more actioonable error that would explain that this ${schema.name} is unsupported please update js-client

@akim-bow
Copy link
Contributor Author

CLI PR with passing tests
fluencelabs/cli#630

@akim-bow akim-bow merged commit f4a550d into master Nov 19, 2023
7 of 8 checks passed
@akim-bow akim-bow deleted the DXJ-525 branch November 19, 2023 02:04
@fluencebot fluencebot mentioned this pull request Nov 19, 2023
@@ -95,7 +95,7 @@ async function checkConsistency(file, versionsMap) {

for (const [name, versionInDep] of versionsMap) {
const check = (x, version) => {
if (version.includes("*")) {
if (version.includes("*") || version.includes("^")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file bumps version in ci to support e2e testing. Here i added support for workspace symbol.

@@ -20,10 +20,12 @@
"base64-js": "1.5.1"
},
"devDependencies": {
"@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use normal version here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will replace them later after releases in aqua and registry. Just don't want to block on them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a todos on these

"@fluencelabs/trust-graph": "3.1.2"
"@fluencelabs/aqua-to-js": "workspace:*",
"@fluencelabs/js-client": "workspace:*",
"@fluencelabs/registry": "0.8.8-1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this version also looks interesting. I wonder why

@@ -18,12 +18,14 @@
"ts-pattern": "5.0.5"
},
"devDependencies": {
"@fluencelabs/aqua-api": "0.12.0",
"@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here as well

@@ -52,8 +53,7 @@ export default async function aquaToJs<T extends OutputType>(
sources: generateSources(res, "js", packageJson),
types: generateTypes(res, packageJson),
}
: // TODO: probably there is a way to remove this type assert
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
: // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid casting here if you just use overloads here as I suggested long ago when this function first appeared

type JsOutput = {
  sources: string;
  types: string;
};

type TsOutput = {
  sources: string;
};

type NothingToGenerate = null;

export default async function aquaToJs(
  res: CompilationResult,
  outputType: "js",
): Promise<JsOutput | NothingToGenerate>;
export default async function aquaToJs(
  res: CompilationResult,
  outputType: "ts",
): Promise<TsOutput | NothingToGenerate>;

export default async function aquaToJs(
  res: CompilationResult,
  outputType: "js" | "ts",
): Promise<JsOutput | TsOutput | NothingToGenerate> {
  if (
    Object.keys(res.services).length === 0 &&
    Object.keys(res.functions).length === 0
  ) {
    return null;
  }

  const packageJson = await getPackageJsonContent();

  if (outputType === "js") {
    return {
      sources: generateSources(res, "js", packageJson),
      types: generateTypes(res, packageJson),
    };
  }

  return {
    sources: generateSources(res, "ts", packageJson),
  };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Comment on lines +127 to +131
/**
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
* The options allows to specify the timeout for that message in milliseconds.
* If not specified the default timeout will be used
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting to know if these doc comments still appear in IDE in autocomplete as they previously did when zod was not used. If they are lost - I would think about somehow restoring them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, comments should be preserved. I checked

Comment on lines +17 to +21
export class ExpirationError extends Error {}

export class InterpreterError extends Error {}

export class SendError extends Error {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain what do these solve exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add comments.
I separated errors to improve assertion with instanceof operator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these errors exposed to the user so he can use instanceof? Or you only use instanceof yourself somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They used in tests right now. Helps to validate specific error. It's just replacement of the old bad API with 10 different statuses. I will add more errors to cover every case and open them for user. Right now if user wants to check error type, he can use error message field

${"op"} | ${"noop"} | ${[1, 2]} | ${0} | ${{}}
${"op"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]}
${"op"} | ${"array_length"} | ${[[1, 2, 3]]} | ${0} | ${3}
${"op"} | ${"array_length"} | ${[]} | ${1} | ${"array_length accepts exactly one argument, found: 0"}
${"op"} | ${"array_length"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these errors happen, can you as a developer understand that error happened in e.g. array_length function? Is there a stack trace? If for developers it's possbile to understand this then probably these errors are ok. If not - then previous erros where deffinetly better

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added wrappers for printing service and function errors

Comment on lines +46 to +52
error: [];
signature: [number[]];
success: true;
}
| {
error: string;
signature: null;
error: [string];
signature: [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just curious why this change was required

Copy link
Contributor Author

@akim-bow akim-bow Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After we removed schema from js-client internals, there is no aqua->js casting right before service calls. So now internal services should work with aqua types directly.

akim-bow added a commit that referenced this pull request Nov 22, 2023
* Review fixes

* remove logs

* Fixes

* Todo to remove prefix later

* Refactor service signatures

* Fixes

* Update lock file

* Fix lockfile

* Update deps

* More fixes and renames

* Fix compiler

* Peer refactoring and cutting onConnectionChange API

* Revert deleted API
@fluencebot fluencebot mentioned this pull request Nov 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
e2e Run e2e workflow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants