diff --git a/packages/core/aqua-to-js/src/generate/__test__/generate.spec.ts b/packages/core/aqua-to-js/src/generate/__test__/generate.spec.ts index 1deadbe8..5de31ffe 100644 --- a/packages/core/aqua-to-js/src/generate/__test__/generate.spec.ts +++ b/packages/core/aqua-to-js/src/generate/__test__/generate.spec.ts @@ -45,7 +45,7 @@ describe("Aqua to js/ts compiler", () => { }; }); - it("matches js snapshot", async () => { + it("matches js snapshots", async () => { const jsResult = generateSources(res, "js", pkg); const jsTypes = generateTypes(res, pkg); @@ -58,7 +58,7 @@ describe("Aqua to js/ts compiler", () => { ); }); - it("matches ts snapshot", async () => { + it("matches ts snapshots", async () => { const tsResult = generateSources(res, "ts", pkg); await expect(tsResult).toMatchFileSnapshot( diff --git a/packages/core/aqua-to-js/src/generate/header.ts b/packages/core/aqua-to-js/src/generate/header.ts index ce4cfc17..17ce9763 100644 --- a/packages/core/aqua-to-js/src/generate/header.ts +++ b/packages/core/aqua-to-js/src/generate/header.ts @@ -39,6 +39,7 @@ ${ : "" } +// Making aliases to reduce chance of accidental name collision import { v5_callFunction as callFunction$$, v5_registerService as registerService$$, diff --git a/packages/core/aqua-to-js/src/generate/interfaces.ts b/packages/core/aqua-to-js/src/generate/interfaces.ts index ef330d10..3a8c400a 100644 --- a/packages/core/aqua-to-js/src/generate/interfaces.ts +++ b/packages/core/aqua-to-js/src/generate/interfaces.ts @@ -119,19 +119,24 @@ export class TSTypeGenerator implements TypeGenerator { const serviceDecl = `service: ${srvName}Def`; const serviceIdDecl = `serviceId: string`; + const functionOverloadsWithDefaultServiceId = [ + [serviceDecl], + [serviceIdDecl, serviceDecl], + [peerDecl, serviceDecl], + [peerDecl, serviceIdDecl, serviceDecl], + ]; + + const functionOverloadsWithoutDefaultServiceId = [ + [serviceIdDecl, serviceDecl], + [peerDecl, serviceIdDecl, serviceDecl], + ]; + const registerServiceArgs = + // This wrong type comes from aqua team. We need to discuss fix with them // eslint-disable-next-line @typescript-eslint/consistent-type-assertions (srvDef.defaultServiceId as DefaultServiceId).s_Some__f_value != null - ? [ - [serviceDecl], - [serviceIdDecl, serviceDecl], - [peerDecl, serviceDecl], - [peerDecl, serviceIdDecl, serviceDecl], - ] - : [ - [serviceIdDecl, serviceDecl], - [peerDecl, serviceIdDecl, serviceDecl], - ]; + ? functionOverloadsWithDefaultServiceId + : functionOverloadsWithoutDefaultServiceId; return [ interfaces, diff --git a/packages/core/aqua-to-js/src/generate/service.ts b/packages/core/aqua-to-js/src/generate/service.ts index c06963a0..c58d3fd1 100644 --- a/packages/core/aqua-to-js/src/generate/service.ts +++ b/packages/core/aqua-to-js/src/generate/service.ts @@ -20,6 +20,7 @@ import { recursiveRenameLaquaProps } from "../utils.js"; import { TypeGenerator } from "./interfaces.js"; +// Actual value of defaultServiceId which comes from aqua-api export interface DefaultServiceId { s_Some__f_value?: string; } diff --git a/packages/core/aqua-to-js/src/generate/validators.ts b/packages/core/aqua-to-js/src/generate/validators.ts index 78c3d6d2..5e917a98 100644 --- a/packages/core/aqua-to-js/src/generate/validators.ts +++ b/packages/core/aqua-to-js/src/generate/validators.ts @@ -18,12 +18,39 @@ import { ArrowWithoutCallbacks, FunctionCallDef, JSONValue, + ScalarType, SimpleTypes, UnlabeledProductType, } from "@fluencelabs/interfaces"; import { typeToTs } from "../common.js"; +const numberTypes = [ + "u8", + "u16", + "u32", + "u64", + "i8", + "i16", + "i32", + "i64", + "f32", + "f64", +]; + +function isScalar(schema: ScalarType, arg: JSONValue) { + if (numberTypes.includes(schema.name)) { + return typeof arg === "number"; + } else if (schema.name === "bool") { + return typeof arg === "boolean"; + } else if (schema.name === "string") { + return typeof arg === "string"; + } else { + // Should not be possible + return false; + } +} + export function validateFunctionCall( schema: FunctionCallDef, ...args: JSONValue[] @@ -47,12 +74,12 @@ export function validateFunctionCall( export function validateFunctionCallArg( schema: SimpleTypes | UnlabeledProductType | ArrowWithoutCallbacks, arg: JSONValue, - argIndex: number, + argPosition: number, ) { if (!isTypeMatchesSchema(schema, arg)) { const expectedType = typeToTs(schema); throw new Error( - `Argument ${argIndex} doesn't match schema. Expected type: ${expectedType}`, + `Argument ${argPosition} doesn't match schema. Expected type: ${expectedType}`, ); } } @@ -66,29 +93,7 @@ export function isTypeMatchesSchema( } else if (schema.tag === "option") { return arg === null || isTypeMatchesSchema(schema.type, arg); } else if (schema.tag === "scalar") { - if ( - [ - "u8", - "u16", - "u32", - "u64", - "i8", - "i16", - "i32", - "i64", - "f32", - "f64", - ].includes(schema.name) - ) { - return typeof arg === "number"; - } else if (schema.name === "bool") { - return typeof arg === "boolean"; - } else if (schema.name === "string") { - return typeof arg === "string"; - } else { - // Should not be possible - return false; - } + return isScalar(schema, arg); } else if (schema.tag === "array") { return ( Array.isArray(arg) && diff --git a/packages/core/aqua-to-js/src/utils.ts b/packages/core/aqua-to-js/src/utils.ts index 7527f7c2..6ab0b53f 100644 --- a/packages/core/aqua-to-js/src/utils.ts +++ b/packages/core/aqua-to-js/src/utils.ts @@ -33,6 +33,7 @@ const packageJsonSchema = z.object({ name: z.string(), version: z.string(), devDependencies: z.object({ + // This version used in header file ["@fluencelabs/aqua-api"]: z.string(), }), }); diff --git a/packages/core/js-client/src/api.ts b/packages/core/js-client/src/api.ts index 77702849..2544b522 100644 --- a/packages/core/js-client/src/api.ts +++ b/packages/core/js-client/src/api.ts @@ -33,6 +33,17 @@ import { FluencePeer } from "./jsPeer/FluencePeer.js"; import { callAquaFunction, Fluence, registerService } from "./index.js"; +const isAquaConfig = ( + config: JSONValue | ServiceImpl[string] | undefined, +): config is CallAquaFunctionConfig => { + return ( + typeof config === "object" && + config !== null && + !Array.isArray(config) && + ["undefined", "number"].includes(typeof config["ttl"]) + ); +}; + /** * Convenience function to support Aqua `func` generation backend * The compiler only need to generate a call the function and provide the corresponding definitions and the air script @@ -47,9 +58,11 @@ export const v5_callFunction = async ( script: string, ): Promise => { const argNames = Object.keys(def.arrow); - const argCount = argNames.length; + const schemaArgCount = argNames.length; - const functionArgs: Record = + type FunctionArg = SimpleTypes | ArrowWithoutCallbacks; + + const schemaFunctionArgs: Record = def.arrow.domain.tag === "nil" ? {} : def.arrow.domain.fields; let peer: FluencePeer | undefined; @@ -61,26 +74,26 @@ export const v5_callFunction = async ( peer = Fluence.defaultClient; } - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const config = - argCount < args.length - ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - (args.pop() as CallAquaFunctionConfig | undefined) - : undefined; - if (peer == null) { throw new Error( "Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?", ); } + // if args more than expected in schema (schemaArgCount) then last arg is config + const config = schemaArgCount < args.length ? args.pop() : undefined; + + if (!isAquaConfig(config)) { + throw new Error("Config should be object type"); + } + const callArgs = Object.fromEntries( - args.slice(0, argCount).map((arg, i) => { - const argSchema = functionArgs[argNames[i]]; + args.slice(0, schemaArgCount).map((arg, i) => { + const argSchema = schemaFunctionArgs[argNames[i]]; if (argSchema.tag === "arrow") { if (typeof arg !== "function") { - throw new Error("Argument and schema doesn't match"); + throw new Error("Argument and schema don't match"); } const wrappedFunction = wrapFunction(arg, argSchema); @@ -89,7 +102,7 @@ export const v5_callFunction = async ( } if (typeof arg === "function") { - throw new Error("Argument and schema doesn't match"); + throw new Error("Argument and schema don't match"); } return [argNames[i], ts2aqua(arg, argSchema)]; @@ -111,13 +124,13 @@ export const v5_callFunction = async ( fireAndForget: returnTypeVoid, }); - const valueSchema = + const returnSchema = def.arrow.codomain.tag === "unlabeledProduct" && def.arrow.codomain.items.length === 1 ? def.arrow.codomain.items[0] : def.arrow.codomain; - return aqua2ts(result, valueSchema); + return aqua2ts(result, returnSchema); }; /** @@ -145,7 +158,13 @@ export const v5_registerService = (args: unknown[], def: ServiceDef): void => { ); } - if (typeof args[0] === "string") { + if (args.length === 2) { + if (typeof args[0] !== "string") { + throw new Error( + `Service ID should be of type string. ${typeof args[0]} provided.`, + ); + } + serviceId = args[0]; } @@ -153,8 +172,10 @@ export const v5_registerService = (args: unknown[], def: ServiceDef): void => { throw new Error("Service ID is not provided"); } + // Schema for every function in service const serviceSchema = def.functions.tag === "nil" ? {} : def.functions.fields; + // Wrapping service impl to convert their args ts -> aqua and backwards const wrappedServiceImpl = Object.fromEntries( Object.entries(serviceImpl).map(([name, func]) => { return [name, wrapFunction(func, serviceSchema[name])]; diff --git a/packages/core/js-client/src/clientPeer/__test__/client.spec.ts b/packages/core/js-client/src/clientPeer/__test__/client.spec.ts index fc347a83..dc6c548b 100644 --- a/packages/core/js-client/src/clientPeer/__test__/client.spec.ts +++ b/packages/core/js-client/src/clientPeer/__test__/client.spec.ts @@ -25,8 +25,10 @@ import { checkConnection } from "../checkConnection.js"; import { nodes, RELAY } from "./connection.js"; +const ONE_SECOND = 1000; + describe("FluenceClient usage test suite", () => { - it("Should resolve at TTL when fire and forget behavior is used", async () => { + it("Should stop particle processing after TTL is reached", async () => { await withClient(RELAY, { defaultTtlMs: 600 }, async (peer) => { const script = ` (seq @@ -36,7 +38,7 @@ describe("FluenceClient usage test suite", () => { const particle = await peer.internals.createNewParticle(script); - const now = Date.now(); + const start = Date.now(); const promise = new Promise((resolve, reject) => { registerHandlersHelper(peer, particle, { @@ -51,7 +53,11 @@ describe("FluenceClient usage test suite", () => { }); await expect(promise).rejects.toThrow(ExpirationError); - expect(Date.now() - 500).toBeGreaterThanOrEqual(now); + + expect( + Date.now() - 500, + "Particle processing didn't stop after TTL is reached", + ).toBeGreaterThanOrEqual(start); }); }); @@ -209,13 +215,17 @@ describe("FluenceClient usage test suite", () => { ); }); - it("With connection options: defaultTTL", async () => { - await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => { - const isConnected = await checkConnection(peer); + it( + "With connection options: defaultTTL", + async () => { + await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => { + const isConnected = await checkConnection(peer); - expect(isConnected).toBeFalsy(); - }); - }, 1000); + expect(isConnected).toBeFalsy(); + }); + }, + ONE_SECOND, + ); }); it.skip("Should throw correct error when the client tries to send a particle not to the relay", async () => { @@ -247,15 +257,11 @@ describe("FluenceClient usage test suite", () => { particle, () => {}, (error: Error) => { - if (error instanceof SendError) { - reject(error.message); - } + reject(error); }, ); }); - await promise; - await expect(promise).rejects.toMatch( "Particle is expected to be sent to only the single peer (relay which client is connected to)", ); diff --git a/packages/core/js-client/src/compilerSupport/callFunction.ts b/packages/core/js-client/src/compilerSupport/callFunction.ts index a062b326..7042fea8 100644 --- a/packages/core/js-client/src/compilerSupport/callFunction.ts +++ b/packages/core/js-client/src/compilerSupport/callFunction.ts @@ -69,6 +69,7 @@ export const callAquaFunction = async ({ const particle = await peer.internals.createNewParticle(script, config.ttl); return new Promise((resolve, reject) => { + // Registering function args as a services for (const [name, argVal] of Object.entries(args)) { let service: ServiceDescription; diff --git a/packages/core/js-client/src/compilerSupport/conversions.ts b/packages/core/js-client/src/compilerSupport/conversions.ts index cfb6cf5c..63c44783 100644 --- a/packages/core/js-client/src/compilerSupport/conversions.ts +++ b/packages/core/js-client/src/compilerSupport/conversions.ts @@ -147,12 +147,12 @@ export const wrapFunction = ( const result = await value(...tsArgs, context); - const valueSchema = + const resultSchema = schema.codomain.tag === "unlabeledProduct" && schema.codomain.items.length === 1 ? schema.codomain.items[0] : schema.codomain; - return ts2aqua(result, valueSchema); + return ts2aqua(result, resultSchema); }; }; diff --git a/packages/core/js-client/src/ephemeral/client.ts b/packages/core/js-client/src/ephemeral/client.ts index c4d45773..e95e0bdf 100644 --- a/packages/core/js-client/src/ephemeral/client.ts +++ b/packages/core/js-client/src/ephemeral/client.ts @@ -37,6 +37,7 @@ export class EphemeralNetworkClient extends FluencePeer { ) { const workerLoader = new WorkerLoader(); + // TODO: use js-client-isomorphic const controlModuleLoader = new WasmLoaderFromNpm( "@fluencelabs/marine-js", "marine-js.wasm", diff --git a/packages/core/js-client/src/ephemeral/network.ts b/packages/core/js-client/src/ephemeral/network.ts index ba8cf710..88514ed5 100644 --- a/packages/core/js-client/src/ephemeral/network.ts +++ b/packages/core/js-client/src/ephemeral/network.ts @@ -232,6 +232,7 @@ export class EphemeralNetwork { // shared worker for all the peers this.workerLoader = new WorkerLoaderFromFs("../../marine/worker-script"); + // TODO: use js-client-isomorphic this.controlModuleLoader = new WasmLoaderFromNpm( "@fluencelabs/marine-js", "marine-js.wasm", diff --git a/packages/core/js-client/src/jsPeer/FluencePeer.ts b/packages/core/js-client/src/jsPeer/FluencePeer.ts index c14a6a46..3764e7bd 100644 --- a/packages/core/js-client/src/jsPeer/FluencePeer.ts +++ b/packages/core/js-client/src/jsPeer/FluencePeer.ts @@ -538,6 +538,17 @@ export abstract class FluencePeer { "id %s. send successful", newParticle.id, ); + + if ( + this.jsServiceHost.getHandler( + "callbackSrv", + "response", + item.particle.id, + ) == null + ) { + // try to finish script if fire-and-forget enabled + item.onSuccess({}); + } }) .catch((e: unknown) => { log_particle.error( @@ -623,10 +634,9 @@ export abstract class FluencePeer { "callbackSrv", "response", item.particle.id, - ) == null && - item.result.nextPeerPks.length === 0 + ) == null ) { - // try to finish script + // try to finish script if fire-and-forget enabled item.onSuccess({}); } } diff --git a/packages/core/js-client/src/util/testUtils.ts b/packages/core/js-client/src/util/testUtils.ts index da7967ff..5ce57f11 100644 --- a/packages/core/js-client/src/util/testUtils.ts +++ b/packages/core/js-client/src/util/testUtils.ts @@ -148,6 +148,7 @@ export class TestPeer extends FluencePeer { constructor(keyPair: KeyPair, connection: IConnection) { const workerLoader = new WorkerLoader(); + // TODO: use js-client-isomorphic const controlModuleLoader = new WasmLoaderFromNpm( "@fluencelabs/marine-js", "marine-js.wasm", @@ -193,6 +194,7 @@ export const withClient = async ( ) => { const workerLoader = new WorkerLoader(); + // TODO: use js-client-isomorphic const controlModuleLoader = new WasmLoaderFromNpm( "@fluencelabs/marine-js", "marine-js.wasm",