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

Schema validation in js-client
This commit is contained in:
Akim 2023-11-19 09:04:10 +07:00 committed by GitHub
parent 638da47bc2
commit f4a550dd22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 2998 additions and 11303 deletions

View File

@ -43,7 +43,7 @@ jobs:
uses: fluencelabs/aqua/.github/workflows/tests.yml@main uses: fluencelabs/aqua/.github/workflows/tests.yml@main
with: with:
js-client-snapshots: "${{ needs.js-client.outputs.js-client-snapshots }}" js-client-snapshots: "${{ needs.js-client.outputs.js-client-snapshots }}"
nox-image: "fluencelabs/nox:unstable_minimal" nox-image: "fluencelabs/nox:0.4.2"
flox: flox:
needs: needs:
- js-client - js-client

View File

@ -6,7 +6,7 @@ on:
nox-image: nox-image:
description: "nox image tag" description: "nox image tag"
type: string type: string
default: "fluencelabs/nox:0.4.0" default: "fluencelabs/nox:0.4.2"
avm-version: avm-version:
description: "@fluencelabs/avm version" description: "@fluencelabs/avm version"
type: string type: string

View File

@ -2,11 +2,13 @@
.eslintcache .eslintcache
pnpm-lock.yaml pnpm-lock.yaml
**/node_modules node_modules
**/dist dist
**/build build
**/public public
**/CHANGELOG.md **/CHANGELOG.md
packages/core/js-client-isomorphic/src/versions.ts packages/core/js-client-isomorphic/src/versions.ts
__snapshots__
packages/@tests/aqua/src/_aqua/**

2
ci.cjs
View File

@ -95,7 +95,7 @@ async function checkConsistency(file, versionsMap) {
for (const [name, versionInDep] of versionsMap) { for (const [name, versionInDep] of versionsMap) {
const check = (x, version) => { const check = (x, version) => {
if (version.includes("*")) { if (version.includes("*") || version.includes("^")) {
return; return;
} }

View File

@ -0,0 +1,51 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { writeFile } from "fs/promises";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { compileFromPath } from "@fluencelabs/aqua-api";
import aquaToJs from "@fluencelabs/aqua-to-js";
const files = ["smoke_test", "finalize_particle"];
for (const file of files) {
const cr = await compileFromPath({
filePath: join(
dirname(fileURLToPath(import.meta.url)),
"_aqua",
file + ".aqua",
),
targetType: "air",
imports: [fileURLToPath(new URL("./node_modules", import.meta.url))],
});
if (cr.errors.length > 0) {
throw new Error(cr.errors.join("\n"));
}
const res = await aquaToJs(cr, "ts");
if (res == null) {
throw new Error("AquaToJs gave null value after compilation");
}
await writeFile(
fileURLToPath(new URL(join("src", "_aqua", file + ".ts"), import.meta.url)),
res.sources,
);
}

View File

@ -11,7 +11,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"compile-aqua": "fluence aqua -i ./_aqua -o ./src/_aqua" "compile-aqua": "node --loader ts-node/esm compile-aqua.ts"
}, },
"repository": "https://github.com/fluencelabs/fluence-js", "repository": "https://github.com/fluencelabs/fluence-js",
"author": "Fluence Labs", "author": "Fluence Labs",
@ -20,10 +20,12 @@
"base64-js": "1.5.1" "base64-js": "1.5.1"
}, },
"devDependencies": { "devDependencies": {
"@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
"@fluencelabs/aqua-lib": "0.6.0", "@fluencelabs/aqua-lib": "0.6.0",
"@fluencelabs/cli": "0.7.2", "@fluencelabs/aqua-to-js": "workspace:*",
"@fluencelabs/js-client": "workspace:^", "@fluencelabs/js-client": "workspace:*",
"@fluencelabs/registry": "0.8.2", "@fluencelabs/registry": "0.8.8-1",
"@fluencelabs/trust-graph": "3.1.2" "@fluencelabs/trust-graph": "3.1.2",
"ts-node": "10.9.1"
} }
} }

View File

@ -2,71 +2,68 @@
// @ts-nocheck // @ts-nocheck
/** /**
* *
* This file is auto-generated. Do not edit manually: changes may be erased. * This file is generated using:
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/. * @fluencelabs/aqua-api version: 0.12.4-main-cee4448-2196-1
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues * @fluencelabs/aqua-to-js version: 0.2.0
* Aqua version: 0.12.0 * If you find any bugs in generated AIR, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* If you find any bugs in generated JS/TS, please write an issue on GitHub: https://github.com/fluencelabs/js-client/issues
* *
*/ */
import type { import type { IFluenceClient as IFluenceClient$$, ParticleContext as ParticleContext$$ } from '@fluencelabs/js-client';
IFluenceClient as IFluenceClient$$,
CallParams as CallParams$$, // Making aliases to reduce chance of accidental name collision
} from "@fluencelabs/js-client"; import {
import { v5_callFunction as callFunction$$,
v5_callFunction as callFunction$$, v5_registerService as registerService$$
v5_registerService as registerService$$, } from '@fluencelabs/js-client';
} from "@fluencelabs/js-client";
// Services
// Functions // Functions
export const test_script = ` export const test_script = `
(seq (xor
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) (seq
(xor (seq
(xor (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(call -relay- ("op" "noop") []) (xor
(fail %last_error%) (call -relay- ("op" "noop") [])
) (fail :error:)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 0]) )
) )
) (call %init_peer_id% ("callbackSrv" "response") [])
`; )
(call %init_peer_id% ("errorHandlingSrv" "error") [:error: 0])
)
`;
export function test(config?: { ttl?: number }): Promise<void>; export type TestParams = [config?: {ttl?: number}] | [peer: IFluenceClient$$, config?: {ttl?: number}];
export function test( export type TestResult = Promise<void>;
peer: IFluenceClient$$,
config?: { ttl?: number },
): Promise<void>;
export function test(...args: any) { export function test(...args: TestParams): TestResult {
return callFunction$$( return callFunction$$(
args, args,
{ {
functionName: "test", "functionName": "test",
arrow: { "arrow": {
tag: "arrow", "domain": {
domain: { "fields": {},
tag: "labeledProduct", "tag": "labeledProduct"
fields: {},
}, },
codomain: { "codomain": {
tag: "nil", "tag": "nil"
}, },
}, "tag": "arrow"
names: {
relay: "-relay-",
getDataSrv: "getDataSrv",
callbackSrv: "callbackSrv",
responseSrv: "callbackSrv",
responseFnName: "response",
errorHandlingSrv: "errorHandlingSrv",
errorFnName: "error",
},
}, },
test_script, "names": {
); "relay": "-relay-",
"getDataSrv": "getDataSrv",
"callbackSrv": "callbackSrv",
"responseSrv": "callbackSrv",
"responseFnName": "response",
"errorHandlingSrv": "errorHandlingSrv",
"errorFnName": "error"
}
},
test_script
);
} }
/* eslint-enable */

File diff suppressed because it is too large Load Diff

View File

@ -90,11 +90,11 @@ export const runTest = async (): Promise<TestResult> => {
console.log("running marine test..."); console.log("running marine test...");
const marine = await marineTest(wasm); const marine = await marineTest(wasm);
console.log("marine test finished, result: ", marine);
console.log("running particle test..."); console.log("running particle test...");
await particleTest();
console.log("marine test finished, result: ", marine); await particleTest();
const returnVal = { const returnVal = {
hello, hello,

View File

@ -19,7 +19,7 @@
"author": "Fluence Labs", "author": "Fluence Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fluencelabs/js-client": "workspace:*", "@fluencelabs/js-client-isomorphic": "workspace:*",
"@test/test-utils": "workspace:*" "@test/test-utils": "workspace:*"
}, },
"devDependencies": { "devDependencies": {

View File

@ -77,7 +77,6 @@ const getRelayTime = () => {
return callAquaFunction({ return callAquaFunction({
args, args,
def,
script, script,
config, config,
peer: Fluence.defaultClient, peer: Fluence.defaultClient,

View File

@ -61,21 +61,16 @@ export const startContentServer = (
source: "/js-client.min.js", source: "/js-client.min.js",
destination: "/source/index.min.js", destination: "/source/index.min.js",
}, },
// TODO:
// something like this
// {
// source: "/@fluencelabs/:name(\\w+)@:version([\\d.]+)/:path*",
// destination: "/deps/@fluencelabs/:name/:path",
// }
// not supported for some reason. Need to manually iterate over all possible paths
{ {
source: "/@fluencelabs/:name([\\w-]+)@:version([\\d.]+)/dist/:asset", source: "/@fluencelabs/:name([\\w-]+)@:version([\\d.]+)/dist/:asset",
destination: "/node_modules/@fluencelabs/:name/dist/:asset", destination:
"/node_modules/@fluencelabs/js-client-isomorphic/node_modules/@fluencelabs/:name/dist/:asset",
}, },
{ {
source: source:
"/@fluencelabs/:name([\\w-]+)@:version([\\d.]+)/dist/:prefix/:asset", "/@fluencelabs/:name([\\w-]+)@:version([\\d.]+)/dist/:prefix/:asset",
destination: "/node_modules/@fluencelabs/:name/dist/:prefix/:asset", destination:
"/node_modules/@fluencelabs/js-client-isomorphic/node_modules/@fluencelabs/:name/dist/:prefix/:asset",
}, },
], ],
headers: [ headers: [

View File

@ -1,3 +1,3 @@
{ {
"ignorePatterns": ["src/**/__snapshots__/**/*"] "ignorePatterns": ["src/**/__snapshots__/**/*", "src/**/*.js"]
} }

View File

@ -18,12 +18,14 @@
"ts-pattern": "5.0.5" "ts-pattern": "5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@fluencelabs/aqua-api": "0.12.0", "@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
"@fluencelabs/aqua-lib": "0.7.3", "@fluencelabs/aqua-lib": "0.7.3",
"@fluencelabs/interfaces": "workspace:*", "@fluencelabs/interfaces": "workspace:*",
"@fluencelabs/js-client": "workspace:^",
"@fluencelabs/registry": "0.8.7", "@fluencelabs/registry": "0.8.7",
"@fluencelabs/spell": "0.5.20", "@fluencelabs/spell": "0.5.20",
"@fluencelabs/trust-graph": "0.4.7", "@fluencelabs/trust-graph": "0.4.7",
"vitest": "0.34.6" "vitest": "0.34.6",
"zod": "3.22.4"
} }
} }

View File

@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { ArrowWithoutCallbacks, NonArrowType } from "@fluencelabs/interfaces"; import { ArrowType, NonArrowType } from "@fluencelabs/interfaces";
import { match, P } from "ts-pattern"; import { match, P } from "ts-pattern";
import { getFuncArgs } from "./utils.js"; import { getFuncArgs } from "./utils.js";
export function genTypeName( export function genTypeName(
t: NonArrowType | ArrowWithoutCallbacks, t: NonArrowType | ArrowType,
name: string, name: string,
): readonly [string | undefined, string] { ): readonly [string | undefined, string] {
const genType = typeToTs(t); const genType = typeToTs(t);
@ -46,7 +46,7 @@ export function genTypeName(
}); });
} }
export function typeToTs(t: NonArrowType | ArrowWithoutCallbacks): string { export function typeToTs(t: NonArrowType | ArrowType): string {
return match(t) return match(t)
.with({ tag: "nil" }, () => { .with({ tag: "nil" }, () => {
return "null"; return "null";
@ -120,16 +120,7 @@ export function typeToTs(t: NonArrowType | ArrowWithoutCallbacks): string {
return [name, typeToTs(type)]; return [name, typeToTs(type)];
}); });
const generic = args.push(["callParams", `ParticleContext$$`]);
args.length === 0
? "null"
: args
.map(([name]) => {
return `'${name}'`;
})
.join(" | ");
args.push(["callParams", `CallParams$$<${generic}>`]);
const funcArgs = args const funcArgs = args
.map(([name, type]) => { .map(([name, type]) => {

View File

@ -0,0 +1,20 @@
/* eslint-disable */
// @ts-nocheck
/**
*
* This file is generated using:
* @fluencelabs/aqua-api version: 0.0.0
* @fluencelabs/aqua-to-js version: 0.0.0
* If you find any bugs in generated AIR, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* If you find any bugs in generated JS/TS, please write an issue on GitHub: https://github.com/fluencelabs/js-client/issues
*
*/
import type { IFluenceClient as IFluenceClient$$, ParticleContext as ParticleContext$$ } from '@fluencelabs/js-client';
// Making aliases to reduce chance of accidental name collision
import {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$
} from '@fluencelabs/js-client';

View File

@ -0,0 +1,20 @@
/* eslint-disable */
// @ts-nocheck
/**
*
* This file is generated using:
* @fluencelabs/aqua-api version: 0.0.0
* @fluencelabs/aqua-to-js version: 0.0.0
* If you find any bugs in generated AIR, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* If you find any bugs in generated JS/TS, please write an issue on GitHub: https://github.com/fluencelabs/js-client/issues
*
*/
// Making aliases to reduce chance of accidental name collision
import {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$
} from '@fluencelabs/js-client';

View File

@ -0,0 +1,20 @@
/* eslint-disable */
// @ts-nocheck
/**
*
* This file is generated using:
* @fluencelabs/aqua-api version: 0.0.0
* @fluencelabs/aqua-to-js version: 0.0.0
* If you find any bugs in generated AIR, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* If you find any bugs in generated JS/TS, please write an issue on GitHub: https://github.com/fluencelabs/js-client/issues
*
*/
import type { IFluenceClient as IFluenceClient$$, ParticleContext as ParticleContext$$ } from '@fluencelabs/js-client';
// Making aliases to reduce chance of accidental name collision
import {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$
} from '@fluencelabs/js-client';

View File

@ -14,46 +14,55 @@
* limitations under the License. * limitations under the License.
*/ */
import url from "url"; import { fileURLToPath } from "url";
import { compileFromPath } from "@fluencelabs/aqua-api"; import { compileFromPath } from "@fluencelabs/aqua-api";
import { describe, expect, it } from "vitest"; import { beforeAll, describe, expect, it } from "vitest";
import { getPackageJsonContent, PackageJson } from "../../utils.js"; import { getPackageJsonContent, PackageJson } from "../../utils.js";
import { generateTypes, generateSources } from "../index.js"; import { generateTypes, generateSources } from "../index.js";
import { CompilationResult } from "../interfaces.js";
let res: Omit<CompilationResult, "funcCall">;
let pkg: PackageJson;
describe("Aqua to js/ts compiler", () => { describe("Aqua to js/ts compiler", () => {
it("compiles smoke tests successfully", async () => { beforeAll(async () => {
const res = await compileFromPath({ res = await compileFromPath({
filePath: url.fileURLToPath( filePath: fileURLToPath(
new URL("./sources/smoke_test.aqua", import.meta.url), new URL("./sources/smoke_test.aqua", import.meta.url),
), ),
imports: ["./node_modules"], imports: ["./node_modules"],
targetType: "air", targetType: "air",
}); });
const pkg: PackageJson = { pkg = {
...(await getPackageJsonContent()), ...(await getPackageJsonContent()),
version: "0.0.0", version: "0.0.0",
devDependencies: { devDependencies: {
"@fluencelabs/aqua-api": "0.0.0", "@fluencelabs/aqua-api": "0.0.0",
}, },
}; };
});
// TODO: see https://github.com/fluencelabs/js-client/pull/366#discussion_r1370567711 it("matches js snapshots", async () => {
// @ts-expect-error don't use compileFromPath directly here
const jsResult = generateSources(res, "js", pkg); const jsResult = generateSources(res, "js", pkg);
// TODO: see https://github.com/fluencelabs/js-client/pull/366#discussion_r1370567711
// @ts-expect-error don't use compileFromPath directly here
const jsTypes = generateTypes(res, pkg); const jsTypes = generateTypes(res, pkg);
expect(jsResult).toMatchSnapshot(); await expect(jsResult).toMatchFileSnapshot(
expect(jsTypes).toMatchSnapshot(); "./__snapshots__/generate.snap.js",
);
// TODO: see https://github.com/fluencelabs/js-client/pull/366#discussion_r1370567711 await expect(jsTypes).toMatchFileSnapshot(
// @ts-expect-error don't use compileFromPath directly here "./__snapshots__/generate.snap.d.ts",
);
});
it("matches ts snapshots", async () => {
const tsResult = generateSources(res, "ts", pkg); const tsResult = generateSources(res, "ts", pkg);
expect(tsResult).toMatchSnapshot(); await expect(tsResult).toMatchFileSnapshot(
"./__snapshots__/generate.snap.ts",
);
}); });
}); });

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { recursiveRenameLaquaProps } from "../utils.js"; import { capitalize, recursiveRenameLaquaProps } from "../utils.js";
import { AquaFunction, TypeGenerator } from "./interfaces.js"; import { AquaFunction, TypeGenerator } from "./interfaces.js";
@ -40,8 +40,11 @@ ${func.script}\`;
${typeGenerator.funcType(func)} ${typeGenerator.funcType(func)}
export function ${func.funcDef.functionName}(${typeGenerator.type( export function ${func.funcDef.functionName}(${typeGenerator.type(
"...args", "...args",
"any[]", `${capitalize(func.funcDef.functionName)}Params`,
)}) { )})${typeGenerator.type(
"",
`${capitalize(func.funcDef.functionName)}Result`,
)} {
return callFunction$$( return callFunction$$(
args, args,
${JSON.stringify(recursiveRenameLaquaProps(funcDef), null, 4)}, ${JSON.stringify(recursiveRenameLaquaProps(funcDef), null, 4)},

View File

@ -35,12 +35,13 @@ export default function generateHeader(
*/ */
${ ${
outputType === "ts" outputType === "ts"
? "import type { IFluenceClient as IFluenceClient$$, CallParams as CallParams$$ } from '@fluencelabs/js-client';" ? "import type { IFluenceClient as IFluenceClient$$, ParticleContext as ParticleContext$$ } from '@fluencelabs/js-client';"
: "" : ""
} }
// Making aliases to reduce chance of accidental name collision
import { import {
v5_callFunction as callFunction$$, v5_callFunction as callFunction$$,
v5_registerService as registerService$$, v5_registerService as registerService$$
} from '@fluencelabs/js-client';`; } from '@fluencelabs/js-client';`;
} }

View File

@ -20,6 +20,8 @@ import { genTypeName, typeToTs } from "../common.js";
import { CLIENT } from "../constants.js"; import { CLIENT } from "../constants.js";
import { capitalize, getFuncArgs } from "../utils.js"; import { capitalize, getFuncArgs } from "../utils.js";
import { DefaultServiceId } from "./service.js";
export interface TypeGenerator { export interface TypeGenerator {
type(field: string, type: string): string; type(field: string, type: string): string;
generic(field: string, type: string): string; generic(field: string, type: string): string;
@ -54,7 +56,7 @@ export class TSTypeGenerator implements TypeGenerator {
args.push([undefined, `config?: {ttl?: number}`]); args.push([undefined, `config?: {ttl?: number}`]);
const argsDefs = args.map(([, def]) => { const argsDefs = args.map(([, def]) => {
return " " + def; return def;
}); });
const argsDesc = args const argsDesc = args
@ -66,28 +68,30 @@ export class TSTypeGenerator implements TypeGenerator {
}); });
const functionOverloads = [ const functionOverloads = [
argsDefs.join(",\n"), argsDefs.join(", "),
[` peer: ${CLIENT}`, ...argsDefs].join(",\n"), [`peer: ${CLIENT}`, ...argsDefs].join(", "),
]; ];
const [resTypeDesc, resType] = genTypeName( const [resTypeDesc, resType] = genTypeName(
funcDef.arrow.codomain, funcDef.arrow.codomain,
capitalize(funcDef.functionName) + "Result", capitalize(funcDef.functionName) + "ResultType",
); );
const functionOverloadArgsType = functionOverloads
.map((overload) => {
return `[${overload}]`;
})
.join(" | ");
return [ return [
argsDesc.join("\n"), argsDesc.join("\n"),
resTypeDesc ?? "", resTypeDesc ?? "",
functionOverloads `export type ${capitalize(
.flatMap((fo) => { funcDef.functionName,
return [ )}Params = ${functionOverloadArgsType};`,
`export function ${funcDef.functionName}(`, `export type ${capitalize(
fo, funcDef.functionName,
`): Promise<${resType}>;`, )}Result = Promise<${resType}>;\n`,
"",
];
})
.join("\n"),
] ]
.filter((s) => { .filter((s) => {
return s !== ""; return s !== "";
@ -117,13 +121,25 @@ export class TSTypeGenerator implements TypeGenerator {
const serviceDecl = `service: ${srvName}Def`; const serviceDecl = `service: ${srvName}Def`;
const serviceIdDecl = `serviceId: string`; const serviceIdDecl = `serviceId: string`;
const registerServiceArgs = [ const functionOverloadsWithDefaultServiceId = [
[serviceDecl], [serviceDecl],
[serviceIdDecl, serviceDecl], [serviceIdDecl, serviceDecl],
[peerDecl, serviceDecl], [peerDecl, serviceDecl],
[peerDecl, serviceIdDecl, 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
? functionOverloadsWithDefaultServiceId
: functionOverloadsWithoutDefaultServiceId;
return [ return [
interfaces, interfaces,
...registerServiceArgs.map((registerServiceArg) => { ...registerServiceArgs.map((registerServiceArg) => {

View File

@ -20,7 +20,8 @@ import { recursiveRenameLaquaProps } from "../utils.js";
import { TypeGenerator } from "./interfaces.js"; import { TypeGenerator } from "./interfaces.js";
interface DefaultServiceId { // Actual value of defaultServiceId which comes from aqua-api
export interface DefaultServiceId {
s_Some__f_value?: string; s_Some__f_value?: string;
} }

View File

@ -15,7 +15,7 @@
*/ */
import { generateSources, generateTypes } from "./generate/index.js"; import { generateSources, generateTypes } from "./generate/index.js";
import { CompilationResult, OutputType } from "./generate/interfaces.js"; import { CompilationResult } from "./generate/interfaces.js";
import { getPackageJsonContent } from "./utils.js"; import { getPackageJsonContent } from "./utils.js";
interface JsOutput { interface JsOutput {
@ -33,6 +33,7 @@ type LanguageOutput = {
}; };
type NothingToGenerate = null; type NothingToGenerate = null;
type OutputType = "js" | "ts";
export default async function aquaToJs<T extends OutputType>( export default async function aquaToJs<T extends OutputType>(
res: CompilationResult, res: CompilationResult,
@ -52,8 +53,7 @@ export default async function aquaToJs<T extends OutputType>(
sources: generateSources(res, "js", packageJson), sources: generateSources(res, "js", packageJson),
types: generateTypes(res, 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
({ ({
sources: generateSources(res, "ts", packageJson), sources: generateSources(res, "ts", packageJson),
} as LanguageOutput[T]); } as LanguageOutput[T]);

View File

@ -16,7 +16,7 @@
import assert from "assert"; import assert from "assert";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import path from "path"; import { join } from "path";
import { import {
ArrowType, ArrowType,
@ -27,24 +27,26 @@ import {
SimpleTypes, SimpleTypes,
UnlabeledProductType, UnlabeledProductType,
} from "@fluencelabs/interfaces"; } from "@fluencelabs/interfaces";
import { z } from "zod";
export interface PackageJson { const packageJsonSchema = z.object({
name: string; name: z.string(),
version: string; version: z.string(),
devDependencies: { devDependencies: z.object({
["@fluencelabs/aqua-api"]: string; // @fluencelabs/aqua-api version is included as part of the comment at the top of each js and ts file
}; ["@fluencelabs/aqua-api"]: z.string(),
} }),
});
export type PackageJson = z.infer<typeof packageJsonSchema>;
export async function getPackageJsonContent(): Promise<PackageJson> { export async function getPackageJsonContent(): Promise<PackageJson> {
const content = await readFile( const content = await readFile(
new URL(path.join("..", "package.json"), import.meta.url), new URL(join("..", "package.json"), import.meta.url),
"utf-8", "utf-8",
); );
// TODO: Add validation here return packageJsonSchema.parse(JSON.parse(content));
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return JSON.parse(content) as PackageJson;
} }
export function getFuncArgs( export function getFuncArgs(

View File

@ -14,60 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import type { SecurityTetraplet } from "@fluencelabs/avm";
import { InterfaceToType, MaybePromise } from "./utils.js";
/** /**
* Peer ID's id as a base58 string (multihash/CIDv0). * Peer ID's id as a base58 string (multihash/CIDv0).
*/ */
export type PeerIdB58 = string; export type PeerIdB58 = string;
/**
* Additional information about a service call
* @typeparam ArgName
*/
export type CallParams<ArgName extends string | null> = {
/**
* The identifier of particle which triggered the call
*/
particleId: string;
/**
* The peer id which created the particle
*/
initPeerId: PeerIdB58;
/**
* Particle's timestamp when it was created
*/
timestamp: number;
/**
* Time to live in milliseconds. The time after the particle should be expired
*/
ttl: number;
/**
* Particle's signature
*/
signature?: string;
/**
* Security tetraplets
*/
tetraplets: ArgName extends string
? Record<ArgName, InterfaceToType<SecurityTetraplet>[]>
: Record<string, never>;
};
export type ServiceImpl = Record<
string,
(
...args: [...JSONArray, CallParams<string>]
) => MaybePromise<JSONValue | undefined>
>;
export type JSONValue = export type JSONValue =
| string | string
| number | number

View File

@ -25,6 +25,11 @@ export type SimpleTypes =
export type NonArrowType = SimpleTypes | ProductType; export type NonArrowType = SimpleTypes | ProductType;
export type NonArrowSimpleType =
| SimpleTypes
| UnlabeledProductType
| LabeledProductType<SimpleTypes>;
export type TopType = { export type TopType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
@ -154,7 +159,13 @@ export type ProductType = UnlabeledProductType | LabeledProductType;
* ArrowType is a profunctor pointing its domain to codomain. * ArrowType is a profunctor pointing its domain to codomain.
* Profunctor means variance: Arrow is contravariant on domain, and variant on codomain. * Profunctor means variance: Arrow is contravariant on domain, and variant on codomain.
*/ */
export type ArrowType<T extends LabeledProductType | UnlabeledProductType> = { export type ArrowType<
T extends
| LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
| UnlabeledProductType =
| LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
| UnlabeledProductType,
> = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
@ -174,14 +185,14 @@ export type ArrowType<T extends LabeledProductType | UnlabeledProductType> = {
/** /**
* Arrow which domain contains only non-arrow types * Arrow which domain contains only non-arrow types
*/ */
export type ArrowWithoutCallbacks = ArrowType< export type ArrowWithoutCallbacks = ArrowType<UnlabeledProductType>;
UnlabeledProductType | LabeledProductType<SimpleTypes>
>;
/** /**
* Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows) * Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows)
*/ */
export type ArrowWithCallbacks = ArrowType<LabeledProductType>; export type ArrowWithCallbacks = ArrowType<
LabeledProductType<SimpleTypes | ArrowWithoutCallbacks>
>;
export interface FunctionCallConstants { export interface FunctionCallConstants {
/** /**
@ -232,9 +243,7 @@ export interface FunctionCallDef {
/** /**
* Underlying arrow which represents function in aqua * Underlying arrow which represents function in aqua
*/ */
arrow: ArrowType< arrow: ArrowWithCallbacks;
LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
>;
/** /**
* Names of the different entities used in generated air script * Names of the different entities used in generated air script
@ -255,37 +264,8 @@ export interface ServiceDef {
* List of functions which the service consists of * List of functions which the service consists of
*/ */
functions: functions:
| LabeledProductType<ArrowType<LabeledProductType<SimpleTypes>>> | LabeledProductType<
ArrowType<LabeledProductType<SimpleTypes> | UnlabeledProductType>
>
| NilType; | NilType;
} }
/**
* Options to configure Aqua function execution
*/
export interface FnConfig {
/**
* Sets the TTL (time to live) for particle responsible for the function execution
* If the option is not set the default TTL from FluencePeer config is used
*/
ttl?: number;
}
export const getArgumentTypes = (
def: FunctionCallDef,
): {
[key: string]: NonArrowType | ArrowWithoutCallbacks;
} => {
if (def.arrow.domain.tag !== "labeledProduct") {
throw new Error("Should be impossible");
}
return def.arrow.domain.fields;
};
export const isReturnTypeVoid = (def: FunctionCallDef): boolean => {
if (def.arrow.codomain.tag === "nil") {
return true;
}
return def.arrow.codomain.items.length === 0;
};

View File

@ -1,103 +0,0 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JSONValue } from "../commonTypes.js";
import {
FnConfig,
FunctionCallDef,
ServiceDef,
} from "./aquaTypeDefinitions.js";
/**
* Type for callback passed as aqua function argument
*/
export type ArgCallbackFunction = (
...args: JSONValue[]
) => JSONValue | Promise<JSONValue>;
/**
* Arguments passed to Aqua function
*/
export type PassedArgs = { [key: string]: JSONValue | ArgCallbackFunction };
/**
* Arguments for callAquaFunction function
*/
// TODO: move to js-client side
export interface CallAquaFunctionArgs {
/**
* Peer to call the function on
*/
peer: unknown;
/**
* Function definition
*/
def: FunctionCallDef;
/**
* Air script used by the aqua function
*/
script: string;
/**
* Function configuration
*/
config: FnConfig;
/**
* Arguments to pass to the function
*/
args: PassedArgs;
}
/**
* Call a function from Aqua script
*/
export type CallAquaFunctionType = (
args: CallAquaFunctionArgs,
) => Promise<unknown>;
/**
* Arguments for registerService function
*/
export interface RegisterServiceArgs {
/**
* Peer to register the service on
*/
peer: unknown;
/**
* Service definition
*/
def: ServiceDef;
/**
* Service id
*/
serviceId: string | undefined;
/**
* Service implementation
*/
service: unknown;
}
/**
* Register a service defined in Aqua on a Fluence peer
*/
export type RegisterServiceType = (args: RegisterServiceArgs) => void;

View File

@ -15,6 +15,5 @@
*/ */
export * from "./compilerSupport/aquaTypeDefinitions.js"; export * from "./compilerSupport/aquaTypeDefinitions.js";
export * from "./compilerSupport/compilerSupportInterface.js";
export * from "./commonTypes.js"; export * from "./commonTypes.js";
export * from "./future.js"; export * from "./future.js";

View File

@ -24,7 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@fluencelabs/avm": "0.54.0", "@fluencelabs/avm": "0.54.0",
"@fluencelabs/marine-js": "0.7.2", "@fluencelabs/marine-js": "0.8.0",
"@fluencelabs/marine-worker": "0.4.2", "@fluencelabs/marine-worker": "0.4.2",
"@fluencelabs/threads": "^2.0.0" "@fluencelabs/threads": "^2.0.0"
}, },

View File

@ -14,18 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { FetchedPackages, getVersionedPackage } from "../types.js"; import { FetchResourceFn, getVersionedPackage } from "../types.js";
/** /**
* @param pkg name of package with version * @param pkg name of package with version
* @param assetPath path of required asset in given package * @param assetPath path of required asset in given package
* @param root CDN domain in browser or file system root in node * @param root CDN domain in browser or file system root in node
*/ */
export async function fetchResource( export const fetchResource: FetchResourceFn = async (pkg, assetPath, root) => {
pkg: FetchedPackages,
assetPath: string,
root: string,
) {
const refinedAssetPath = assetPath.startsWith("/") const refinedAssetPath = assetPath.startsWith("/")
? assetPath.slice(1) ? assetPath.slice(1)
: assetPath; : assetPath;
@ -36,4 +32,4 @@ export async function fetchResource(
return fetch(url).catch(() => { return fetch(url).catch(() => {
throw new Error(`Cannot fetch from ${url.toString()}`); throw new Error(`Cannot fetch from ${url.toString()}`);
}); });
} };

View File

@ -18,21 +18,14 @@ import { readFile } from "fs/promises";
import { createRequire } from "module"; import { createRequire } from "module";
import { sep, posix, join } from "path"; import { sep, posix, join } from "path";
import { FetchedPackages, getVersionedPackage } from "../types.js"; import { FetchResourceFn, getVersionedPackage } from "../types.js";
/** /**
* @param pkg name of package with version * @param pkg name of package with version
* @param assetPath path of required asset in given package * @param assetPath path of required asset in given package
* @param root CDN domain in browser or js-client itself in node
*/ */
export async function fetchResource( export const fetchResource: FetchResourceFn = async (pkg, assetPath) => {
pkg: FetchedPackages,
assetPath: string,
root: string,
) {
const { name } = getVersionedPackage(pkg); const { name } = getVersionedPackage(pkg);
// TODO: `root` will be handled somehow in the future. For now, we use filesystem root where js-client is running;
root = "/";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const packagePathIndex = require.resolve(name); const packagePathIndex = require.resolve(name);
@ -47,7 +40,7 @@ export async function fetchResource(
throw new Error(`Cannot find dependency ${name} in path ${posixPath}`); throw new Error(`Cannot find dependency ${name} in path ${posixPath}`);
} }
const pathToResource = join(root, packagePath, assetPath); const pathToResource = join(packagePath, assetPath);
const file = await readFile(pathToResource); const file = await readFile(pathToResource);
@ -60,4 +53,4 @@ export async function fetchResource(
: "application/text", : "application/text",
}, },
}); });
} };

View File

@ -20,7 +20,7 @@ import versions from "./versions.js";
export type FetchedPackages = keyof typeof versions; export type FetchedPackages = keyof typeof versions;
type VersionedPackage = { name: string; version: string }; type VersionedPackage = { name: string; version: string };
export type GetWorker = ( export type GetWorkerFn = (
pkg: FetchedPackages, pkg: FetchedPackages,
CDNUrl: string, CDNUrl: string,
) => Promise<Worker>; ) => Promise<Worker>;
@ -31,3 +31,9 @@ export const getVersionedPackage = (pkg: FetchedPackages): VersionedPackage => {
version: versions[pkg], version: versions[pkg],
}; };
}; };
export type FetchResourceFn = (
pkg: FetchedPackages,
assetPath: string,
root: string,
) => Promise<Response>;

View File

@ -17,9 +17,9 @@
import { BlobWorker } from "@fluencelabs/threads/master"; import { BlobWorker } from "@fluencelabs/threads/master";
import { fetchResource } from "../fetchers/browser.js"; import { fetchResource } from "../fetchers/browser.js";
import type { FetchedPackages, GetWorker } from "../types.js"; import type { FetchedPackages, GetWorkerFn } from "../types.js";
export const getWorker: GetWorker = async ( export const getWorker: GetWorkerFn = async (
pkg: FetchedPackages, pkg: FetchedPackages,
CDNUrl: string, CDNUrl: string,
) => { ) => {

View File

@ -20,10 +20,10 @@ import { fileURLToPath } from "url";
import { Worker } from "@fluencelabs/threads/master"; import { Worker } from "@fluencelabs/threads/master";
import type { FetchedPackages, GetWorker } from "../types.js"; import type { FetchedPackages, GetWorkerFn } from "../types.js";
import { getVersionedPackage } from "../types.js"; import { getVersionedPackage } from "../types.js";
export const getWorker: GetWorker = (pkg: FetchedPackages) => { export const getWorker: GetWorkerFn = (pkg: FetchedPackages) => {
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const pathToThisFile = dirname(fileURLToPath(import.meta.url)); const pathToThisFile = dirname(fileURLToPath(import.meta.url));

View File

@ -43,8 +43,6 @@
"@libp2p/peer-id-factory": "3.0.3", "@libp2p/peer-id-factory": "3.0.3",
"@libp2p/websockets": "7.0.4", "@libp2p/websockets": "7.0.4",
"@multiformats/multiaddr": "11.3.0", "@multiformats/multiaddr": "11.3.0",
"assert": "2.1.0",
"async": "3.2.4",
"bs58": "5.0.0", "bs58": "5.0.0",
"buffer": "6.0.3", "buffer": "6.0.3",
"debug": "4.3.4", "debug": "4.3.4",
@ -55,14 +53,12 @@
"libp2p": "0.46.6", "libp2p": "0.46.6",
"multiformats": "11.0.1", "multiformats": "11.0.1",
"rxjs": "7.5.5", "rxjs": "7.5.5",
"ts-pattern": "3.3.3",
"uint8arrays": "4.0.3", "uint8arrays": "4.0.3",
"uuid": "8.3.2", "uuid": "8.3.2",
"zod": "3.22.4" "zod": "3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@fluencelabs/aqua-api": "0.9.3", "@fluencelabs/aqua-api": "0.9.3",
"@fluencelabs/marine-js": "0.7.2",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.3",
"@types/bs58": "4.0.1", "@types/bs58": "4.0.1",
"@types/debug": "4.1.7", "@types/debug": "4.1.7",

View File

@ -15,182 +15,201 @@
*/ */
import type { import type {
FnConfig, ArrowWithoutCallbacks,
FunctionCallDef, FunctionCallDef,
JSONValue,
ServiceDef, ServiceDef,
PassedArgs, SimpleTypes,
ServiceImpl,
} from "@fluencelabs/interfaces"; } from "@fluencelabs/interfaces";
import { getArgumentTypes } from "@fluencelabs/interfaces"; import { z } from "zod";
import { CallAquaFunctionConfig } from "./compilerSupport/callFunction.js";
import {
aqua2js,
SchemaValidationError,
js2aqua,
wrapJsFunction,
} from "./compilerSupport/conversions.js";
import { ServiceImpl } from "./compilerSupport/types.js";
import { FluencePeer } from "./jsPeer/FluencePeer.js"; import { FluencePeer } from "./jsPeer/FluencePeer.js";
import { callAquaFunction, Fluence, registerService } from "./index.js"; import { callAquaFunction, Fluence, registerService } from "./index.js";
export const isFluencePeer = ( function validateAquaConfig(
fluencePeerCandidate: unknown, config: unknown,
): fluencePeerCandidate is FluencePeer => { ): asserts config is CallAquaFunctionConfig | undefined {
return fluencePeerCandidate instanceof FluencePeer; z.union([
}; z.object({
ttl: z.number().optional(),
}),
z.undefined(),
]).parse(config);
}
/** /**
* Convenience function to support Aqua `func` generation backend * 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 * The compiler only need to generate a call the function and provide the corresponding definitions and the air script
* *
* @param rawFnArgs - raw arguments passed by user to the generated function * @param args - raw arguments passed by user to the generated function
* @param def - function definition generated by the Aqua compiler * @param def - function definition generated by the Aqua compiler
* @param script - air script with function execution logic generated by the Aqua compiler * @param script - air script with function execution logic generated by the Aqua compiler
*/ */
export const v5_callFunction = async ( export const v5_callFunction = async (
rawFnArgs: unknown[], args: [
client: FluencePeer | (JSONValue | ServiceImpl[string]),
...args: (JSONValue | ServiceImpl[string])[],
],
def: FunctionCallDef, def: FunctionCallDef,
script: string, script: string,
): Promise<unknown> => { ): Promise<JSONValue> => {
const { args, client: peer, config } = extractFunctionArgs(rawFnArgs, def); const [peerOrArg, ...rest] = args;
return callAquaFunction({ if (!(peerOrArg instanceof FluencePeer)) {
args, return await v5_callFunction(
def, [getDefaultPeer(), peerOrArg, ...rest],
def,
script,
);
}
const argNames = Object.keys(
def.arrow.domain.tag === "nil" ? [] : def.arrow.domain.fields,
);
const schemaArgCount = argNames.length;
type FunctionArg = SimpleTypes | ArrowWithoutCallbacks;
const schemaFunctionArgs: Record<string, FunctionArg> =
def.arrow.domain.tag === "nil" ? {} : def.arrow.domain.fields;
// if there are more args than expected in schema (schemaArgCount) then last arg is config
const config = schemaArgCount < rest.length ? rest.pop() : undefined;
validateAquaConfig(config);
const callArgs = Object.fromEntries<JSONValue | ServiceImpl[string]>(
rest.slice(0, schemaArgCount).map((arg, i) => {
const argName = argNames[i];
const argSchema = schemaFunctionArgs[argName];
if (argSchema.tag === "arrow") {
if (typeof arg !== "function") {
throw new SchemaValidationError(
[argName],
argSchema,
"function",
arg,
);
}
return [argName, wrapJsFunction(arg, argSchema)];
}
if (typeof arg === "function") {
throw new SchemaValidationError(
[argName],
argSchema,
"non-function value",
arg,
);
}
return [argName, js2aqua(arg, argSchema, { path: [def.functionName] })];
}),
);
const returnTypeVoid =
def.arrow.codomain.tag === "nil" || def.arrow.codomain.items.length === 0;
const returnSchema =
def.arrow.codomain.tag === "unlabeledProduct" &&
def.arrow.codomain.items.length === 1
? def.arrow.codomain.items[0]
: def.arrow.codomain;
let result = await callAquaFunction({
script, script,
peer: peerOrArg,
args: callArgs,
config, config,
peer,
}); });
if (returnTypeVoid) {
result = null;
}
return aqua2js(result, returnSchema);
}; };
const getDefaultPeer = (): FluencePeer => {
if (Fluence.defaultClient == null) {
throw new Error(
"Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?",
);
}
return Fluence.defaultClient;
};
const getDefaultServiceId = (def: ServiceDef) => {
if (def.defaultServiceId == null) {
throw new Error("Service ID is not provided");
}
return def.defaultServiceId;
};
type RegisterServiceType =
| [ServiceImpl]
| [string, ServiceImpl]
| [FluencePeer, ServiceImpl]
| [FluencePeer, string, ServiceImpl];
/** /**
* Convenience function to support Aqua `service` generation backend * Convenience function to support Aqua `service` generation backend
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script * The compiler only need to generate a call the function and provide the corresponding definitions and the air script
* @param args - raw arguments passed by user to the generated function * @param args - raw arguments passed by user to the generated function
* TODO: dont forget to add jsdoc for new arg
* @param def - service definition generated by the Aqua compiler * @param def - service definition generated by the Aqua compiler
*/ */
export const v5_registerService = (args: unknown[], def: ServiceDef): void => { export const v5_registerService = (
// TODO: Support this in aqua-to-js package args: RegisterServiceType,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions def: ServiceDef,
const service: ServiceImpl = args.pop() as ServiceImpl; ): void => {
if (args.length === 1) {
v5_registerService(
[getDefaultPeer(), getDefaultServiceId(def), args[0]],
def,
);
const { peer, serviceId } = extractServiceArgs(args, def.defaultServiceId); return;
}
if (args.length === 2) {
if (args[0] instanceof FluencePeer) {
v5_registerService([args[0], getDefaultServiceId(def), args[1]], def);
return;
}
v5_registerService([getDefaultPeer(), args[0], args[1]], def);
return;
}
const [peer, serviceId, serviceImpl] = args;
// 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, wrapJsFunction(func, serviceSchema[name])];
}),
);
registerService({ registerService({
def, service: wrappedServiceImpl,
service,
serviceId,
peer, peer,
serviceId,
}); });
}; };
function isConfig(arg: unknown): arg is FnConfig {
return typeof arg === "object" && arg !== null;
}
/**
* Arguments could be passed in one these configurations:
* [...actualArgs]
* [peer, ...actualArgs]
* [...actualArgs, config]
* [peer, ...actualArgs, config]
*
* This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, config, args }
*/
function extractFunctionArgs(
args: unknown[],
def: FunctionCallDef,
): {
client: FluencePeer;
config: FnConfig;
args: PassedArgs;
} {
const argumentTypes = getArgumentTypes(def);
const argumentNames = Object.keys(argumentTypes);
const numberOfExpectedArgs = argumentNames.length;
let peer: FluencePeer;
let config: FnConfig;
if (isFluencePeer(args[0])) {
peer = args[0];
args = args.slice(1);
} else {
if (Fluence.defaultClient == null) {
throw new Error(
"Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?",
);
}
peer = Fluence.defaultClient;
}
const maybeConfig = args[numberOfExpectedArgs];
if (isConfig(maybeConfig)) {
config = maybeConfig;
} else {
config = {};
}
const structuredArgs = args.slice(0, numberOfExpectedArgs);
if (structuredArgs.length !== numberOfExpectedArgs) {
throw new Error(
`Incorrect number of arguments. Expecting ${numberOfExpectedArgs}`,
);
}
const argsRes = argumentNames.reduce((acc, name, index) => {
return { ...acc, [name]: structuredArgs[index] };
}, {});
return {
client: peer,
args: argsRes,
config: config,
};
}
/**
* Arguments could be passed in one these configurations:
* [serviceObject]
* [peer, serviceObject]
* [defaultId, serviceObject]
* [peer, defaultId, serviceObject]
*
* Where serviceObject is the raw object with function definitions passed by user
*
* This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, serviceId, service }
*/
const extractServiceArgs = (
args: unknown[],
defaultServiceId?: string,
): {
peer: FluencePeer;
serviceId: string | undefined;
} => {
let peer: FluencePeer;
let serviceId: string | undefined;
if (isFluencePeer(args[0])) {
peer = args[0];
args = args.slice(1);
} else {
if (Fluence.defaultClient == null) {
throw new Error(
"Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?",
);
}
peer = Fluence.defaultClient;
}
if (typeof args[0] === "string") {
serviceId = args[0];
} else {
serviceId = defaultServiceId;
}
return {
peer,
serviceId,
};
};

View File

@ -17,15 +17,56 @@
import { JSONValue } from "@fluencelabs/interfaces"; import { JSONValue } from "@fluencelabs/interfaces";
import { it, describe, expect } from "vitest"; import { it, describe, expect } from "vitest";
import { ExpirationError } from "../../jsPeer/errors.js";
import { CallServiceData } from "../../jsServiceHost/interfaces.js"; import { CallServiceData } from "../../jsServiceHost/interfaces.js";
import { doNothing } from "../../jsServiceHost/serviceUtils.js";
import { handleTimeout } from "../../particle/Particle.js"; import { handleTimeout } from "../../particle/Particle.js";
import { registerHandlersHelper, withClient } from "../../util/testUtils.js"; import { registerHandlersHelper, withClient } from "../../util/testUtils.js";
import { checkConnection } from "../checkConnection.js"; import { checkConnection } from "../checkConnection.js";
import { nodes, RELAY } from "./connection.js"; import { nodes, RELAY } from "./connection.js";
const ONE_SECOND = 1000;
describe("FluenceClient usage test suite", () => { describe("FluenceClient usage test suite", () => {
it("Should stop particle processing after TTL is reached", async () => {
await withClient(RELAY, { defaultTtlMs: 600 }, async (peer) => {
const script = `
(seq
(call %init_peer_id% ("load" "relay") [] init_relay)
(call init_relay ("peer" "timeout") [60000 "Do you really want to wait for so long?"])
)`;
const particle = await peer.internals.createNewParticle(script);
const start = Date.now();
const promise = new Promise<JSONValue>((resolve, reject) => {
registerHandlersHelper(peer, particle, {
load: {
relay: () => {
return peer.getRelayPeerId();
},
},
callbackSrv: {
response: () => {
resolve({});
return "";
},
},
});
peer.internals.initiateParticle(particle, resolve, reject);
});
await expect(promise).rejects.toThrow(ExpirationError);
expect(
Date.now() - 500,
"Particle processing didn't stop after TTL is reached",
).toBeGreaterThanOrEqual(start);
});
});
it("should make a call through network", async () => { it("should make a call through network", async () => {
await withClient(RELAY, {}, async (peer) => { await withClient(RELAY, {}, async (peer) => {
// arrange // arrange
@ -71,7 +112,11 @@ describe("FluenceClient usage test suite", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(result).toBe("hello world!"); expect(result).toBe("hello world!");
@ -124,7 +169,11 @@ describe("FluenceClient usage test suite", () => {
throw particle; throw particle;
} }
peer1.internals.initiateParticle(particle, doNothing); peer1.internals.initiateParticle(
particle,
() => {},
() => {},
);
expect(await res).toEqual("test"); expect(await res).toEqual("test");
}); });
@ -172,13 +221,17 @@ describe("FluenceClient usage test suite", () => {
); );
}); });
it("With connection options: defaultTTL", async () => { it(
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => { "With connection options: defaultTTL",
const isConnected = await checkConnection(peer); async () => {
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeFalsy(); expect(isConnected).toBeFalsy();
}); });
}); },
ONE_SECOND,
);
}); });
it.skip("Should throw correct error when the client tries to send a particle not to the relay", async () => { it.skip("Should throw correct error when the client tries to send a particle not to the relay", async () => {
@ -206,15 +259,15 @@ describe("FluenceClient usage test suite", () => {
}, },
}); });
peer.internals.initiateParticle(particle, (stage) => { peer.internals.initiateParticle(
if (stage.stage === "sendingError") { particle,
reject(stage.errorMessage); () => {},
} (error: Error) => {
}); reject(error);
},
);
}); });
await promise;
await expect(promise).rejects.toMatch( await expect(promise).rejects.toMatch(
"Particle is expected to be sent to only the single peer (relay which client is connected to)", "Particle is expected to be sent to only the single peer (relay which client is connected to)",
); );

View File

@ -110,6 +110,7 @@ export const checkConnection = async (
peer.internals.initiateParticle( peer.internals.initiateParticle(
particle, particle,
() => {},
handleTimeout(() => { handleTimeout(() => {
reject("particle timed out"); reject("particle timed out");
}), }),

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { z } from "zod";
/** /**
* Peer ID's id as a base58 string (multihash/CIDv0). * Peer ID's id as a base58 string (multihash/CIDv0).
*/ */
@ -33,20 +35,30 @@ export type Node = {
* - string: multiaddr in string format * - string: multiaddr in string format
* - Node: node structure, @see Node * - Node: node structure, @see Node
*/ */
export type RelayOptions = string | Node; export const relaySchema = z.union([
z.string(),
z.object({
peerId: z.string(),
multiaddr: z.string(),
}),
]);
export type RelayOptions = z.infer<typeof relaySchema>;
/** /**
* Fluence Peer's key pair types * Fluence Peer's key pair types
*/ */
export type KeyTypes = "RSA" | "Ed25519" | "secp256k1"; export type KeyTypes = "RSA" | "Ed25519" | "secp256k1";
const keyPairOptionsSchema = z.object({
type: z.literal("Ed25519"),
source: z.union([z.literal("random"), z.instanceof(Uint8Array)]),
});
/** /**
* Options to specify key pair used in Fluence Peer * Options to specify key pair used in Fluence Peer
*/ */
export type KeyPairOptions = { export type KeyPairOptions = z.infer<typeof keyPairOptionsSchema>;
type: "Ed25519";
source: "random" | Uint8Array;
};
/** /**
* Fluence JS Client connection states as string literals * Fluence JS Client connection states as string literals
@ -63,17 +75,10 @@ export const ConnectionStates = [
*/ */
export type ConnectionState = (typeof ConnectionStates)[number]; export type ConnectionState = (typeof ConnectionStates)[number];
export interface IFluenceInternalApi {
/**
* Internal API
*/
internals: unknown;
}
/** /**
* Public API of Fluence JS Client * Public API of Fluence JS Client
*/ */
export interface IFluenceClient extends IFluenceInternalApi { export interface IFluenceClient {
/** /**
* Connect to the Fluence network * Connect to the Fluence network
*/ */
@ -107,65 +112,66 @@ export interface IFluenceClient extends IFluenceInternalApi {
getRelayPeerId(): string; getRelayPeerId(): string;
} }
export const configSchema = z
.object({
/**
* Specify the KeyPair to be used to identify the Fluence Peer.
* Will be generated randomly if not specified
*/
keyPair: keyPairOptionsSchema,
/**
* Options to configure the connection to the Fluence network
*/
connectionOptions: z
.object({
/**
* 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
*/
skipCheckConnection: z.boolean(),
/**
* The dialing timeout in milliseconds
*/
dialTimeoutMs: z.number(),
/**
* The maximum number of inbound streams for the libp2p node.
* Default: 1024
*/
maxInboundStreams: z.number(),
/**
* The maximum number of outbound streams for the libp2p node.
* Default: 1024
*/
maxOutboundStreams: z.number(),
})
.partial(),
/**
* Sets the default TTL for all particles originating from the peer with no TTL specified.
* If the originating particle's TTL is defined then that value will be used
* If the option is not set default TTL will be 7000
*/
defaultTtlMs: z.number(),
/**
* Property for passing custom CDN Url to load dependencies from browser. https://unpkg.com used by default
*/
CDNUrl: z.string(),
/**
* Enables\disabled various debugging features
*/
debug: z
.object({
/**
* If set to true, newly initiated particle ids will be printed to console.
* Useful to see what particle id is responsible for aqua function
*/
printParticleId: z.boolean(),
})
.partial(),
})
.partial();
/** /**
* Configuration used when initiating Fluence Client * Configuration used when initiating Fluence Client
*/ */
export interface ClientConfig { export type ClientConfig = z.infer<typeof configSchema>;
/**
* Specify the KeyPair to be used to identify the Fluence Peer.
* Will be generated randomly if not specified
*/
keyPair?: KeyPairOptions;
/**
* Options to configure the connection to the Fluence network
*/
connectionOptions?: {
/**
* 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
*/
skipCheckConnection?: boolean;
/**
* The dialing timeout in milliseconds
*/
dialTimeoutMs?: number;
/**
* The maximum number of inbound streams for the libp2p node.
* Default: 1024
*/
maxInboundStreams?: number;
/**
* The maximum number of outbound streams for the libp2p node.
* Default: 1024
*/
maxOutboundStreams?: number;
};
/**
* Sets the default TTL for all particles originating from the peer with no TTL specified.
* If the originating particle's TTL is defined then that value will be used
* If the option is not set default TTL will be 7000
*/
defaultTtlMs?: number;
/**
* Property for passing custom CDN Url to load dependencies from browser. https://unpkg.com used by default
*/
CDNUrl?: string;
/**
* Enables\disabled various debugging features
*/
debug?: {
/**
* If set to true, newly initiated particle ids will be printed to console.
* Useful to see what particle id is responsible for aqua function
*/
printParticleId?: boolean;
};
}

View File

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { JSONValue, NonArrowType } from "@fluencelabs/interfaces"; import { JSONValue, NonArrowSimpleType } from "@fluencelabs/interfaces";
import { it, describe, expect, test } from "vitest"; import { it, describe, expect, test } from "vitest";
import { aqua2ts, ts2aqua } from "../conversions.js"; import { aqua2js, js2aqua } from "../conversions.js";
const i32 = { tag: "scalar", name: "i32" } as const; const i32 = { tag: "scalar", name: "i32" } as const;
@ -172,7 +172,7 @@ const nestedStructs = [
interface ConversionTestArgs { interface ConversionTestArgs {
aqua: JSONValue; aqua: JSONValue;
ts: JSONValue; ts: JSONValue;
type: NonArrowType; type: NonArrowSimpleType;
} }
describe("Conversion from aqua to typescript", () => { describe("Conversion from aqua to typescript", () => {
@ -200,8 +200,8 @@ describe("Conversion from aqua to typescript", () => {
// arrange // arrange
// act // act
const tsFromAqua = aqua2ts(aqua, type); const tsFromAqua = aqua2js(aqua, type);
const aquaFromTs = ts2aqua(ts, type); const aquaFromTs = js2aqua(ts, type, { path: [] });
// assert // assert
expect(tsFromAqua).toStrictEqual(ts); expect(tsFromAqua).toStrictEqual(ts);
@ -231,8 +231,8 @@ describe("Conversion corner cases", () => {
}; };
// act // act
const aqua = ts2aqua(valueInTs, type); const aqua = js2aqua(valueInTs, type, { path: [] });
const ts = aqua2ts(valueInAqua, type); const ts = aqua2js(valueInAqua, type);
// assert // assert
expect(aqua).toStrictEqual({ expect(aqua).toStrictEqual({

View File

@ -14,18 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import assert from "assert"; import { JSONValue } from "@fluencelabs/interfaces";
import {
FnConfig,
FunctionCallDef,
getArgumentTypes,
isReturnTypeVoid,
PassedArgs,
} from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js"; import { logger } from "../util/logger.js";
import { ArgCallbackFunction } from "../util/testUtils.js";
import { import {
errorHandlingService, errorHandlingService,
@ -51,95 +44,48 @@ const log = logger("aqua");
* @returns * @returns
*/ */
type CallAquaFunctionArgs = { export type CallAquaFunctionArgs = {
def: FunctionCallDef;
script: string; script: string;
config: FnConfig; config: CallAquaFunctionConfig | undefined;
peer: FluencePeer; peer: FluencePeer;
args: PassedArgs; args: { [key: string]: JSONValue | ArgCallbackFunction };
fireAndForget?: boolean | undefined;
};
export type CallAquaFunctionConfig = {
ttl?: number;
}; };
export const callAquaFunction = async ({ export const callAquaFunction = async ({
def,
script, script,
config, config = {},
peer, peer,
args, args,
}: CallAquaFunctionArgs) => { }: CallAquaFunctionArgs) => {
// TODO: this function should be rewritten. We can remove asserts if we wont check definition there log.trace("calling aqua function %j", { script, config, args });
log.trace("calling aqua function %j", { def, script, config, args });
const argumentTypes = getArgumentTypes(def);
const particle = await peer.internals.createNewParticle(script, config.ttl); const particle = await peer.internals.createNewParticle(script, config.ttl);
return new Promise((resolve, reject) => { return new Promise<JSONValue>((resolve, reject) => {
// Registering function args as a services
for (const [name, argVal] of Object.entries(args)) { for (const [name, argVal] of Object.entries(args)) {
const type = argumentTypes[name];
let service: ServiceDescription; let service: ServiceDescription;
if (type.tag === "arrow") { if (typeof argVal === "function") {
// TODO: Add validation here service = userHandlerService("callbackSrv", name, argVal);
assert(
typeof argVal === "function",
"Should not be possible, bad types",
);
service = userHandlerService(
def.names.callbackSrv,
[name, type],
argVal,
);
} else { } else {
// TODO: Add validation here service = injectValueService("getDataSrv", name, argVal);
assert(
typeof argVal !== "function",
"Should not be possible, bad types",
);
service = injectValueService(def.names.getDataSrv, name, type, argVal);
} }
registerParticleScopeService(peer, particle, service); registerParticleScopeService(peer, particle, service);
} }
registerParticleScopeService(peer, particle, responseService(def, resolve)); registerParticleScopeService(peer, particle, responseService(resolve));
registerParticleScopeService(peer, particle, injectRelayService(def, peer)); registerParticleScopeService(peer, particle, injectRelayService(peer));
registerParticleScopeService( registerParticleScopeService(peer, particle, errorHandlingService(reject));
peer,
particle,
errorHandlingService(def, reject),
);
peer.internals.initiateParticle(particle, (stage) => { peer.internals.initiateParticle(particle, resolve, reject);
// If function is void, then it's completed when one of the two conditions is met:
// 1. The particle is sent to the network (state 'sent')
// 2. All CallRequests are executed, e.g., all variable loading and local function calls are completed (state 'localWorkDone')
if (
isReturnTypeVoid(def) &&
(stage.stage === "sent" || stage.stage === "localWorkDone")
) {
resolve(undefined);
}
if (stage.stage === "sendingError") {
reject(
`Could not send particle for ${def.functionName}: not connected (particle id: ${particle.id})`,
);
}
if (stage.stage === "expired") {
reject(
`Particle expired after ttl of ${particle.ttl}ms for function ${def.functionName} (particle id: ${particle.id})`,
);
}
if (stage.stage === "interpreterError") {
reject(
`Script interpretation failed for ${def.functionName}: ${stage.errorMessage} (particle id: ${particle.id})`,
);
}
});
}); });
}; };

View File

@ -14,222 +14,241 @@
* limitations under the License. * limitations under the License.
*/ */
// TODO: This file is a mess. Need to refactor it later import {
/* eslint-disable */
// @ts-nocheck
import assert from "assert";
import type {
ArrowType, ArrowType,
ArrowWithoutCallbacks, ArrowWithoutCallbacks,
JSONArray,
JSONValue, JSONValue,
NonArrowType, LabeledProductType,
NonArrowSimpleType,
ScalarType,
SimpleTypes,
UnlabeledProductType,
} from "@fluencelabs/interfaces"; } from "@fluencelabs/interfaces";
import { match } from "ts-pattern";
import { CallServiceData } from "../jsServiceHost/interfaces.js"; import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { jsonify } from "../util/utils.js";
/** import { ServiceImpl } from "./types.js";
* Convert value from its representation in aqua language to representation in typescript
* @param value - value as represented in aqua export class SchemaValidationError extends Error {
* @param type - definition of the aqua type constructor(
* @returns value represented in typescript public path: string[],
*/ schema: NonArrowSimpleType | ArrowWithoutCallbacks,
export const aqua2ts = (value: JSONValue, type: NonArrowType): JSONValue => { expected: string,
const res = match(type) provided: JSONValue | ServiceImpl[string],
.with({ tag: "nil" }, () => { ) {
const given =
provided === null
? "null"
: Array.isArray(provided)
? "array"
: typeof provided;
const message = `Aqua type mismatch. Path: ${path.join(
".",
)}; Expected: ${expected}; Given: ${given}; \nSchema: ${JSON.stringify(
schema,
)}; \nTry recompiling rust services and aqua. Make sure you are using up-to-date versions of aqua libraries`;
super(message);
}
}
interface ValidationContext {
path: string[];
}
const numberTypes = [
"u8",
"u16",
"u32",
"u64",
"i8",
"i16",
"i32",
"i64",
"f32",
"f64",
] as const;
function isScalar(
schema: ScalarType,
arg: JSONValue,
{ path }: ValidationContext,
) {
if (numberTypes.includes(schema.name)) {
if (typeof arg !== "number") {
throw new SchemaValidationError(path, schema, "number", arg);
}
} else if (schema.name === "bool") {
if (typeof arg !== "boolean") {
throw new SchemaValidationError(path, schema, "boolean", arg);
}
} else if (schema.name === "string") {
if (typeof arg !== "string") {
throw new SchemaValidationError(path, schema, "string", arg);
}
} else {
throw new SchemaValidationError(path, schema, schema.name, arg);
}
return arg;
}
export function aqua2js(
value: JSONValue,
schema: NonArrowSimpleType,
): JSONValue {
if (schema.tag === "nil") {
return null;
} else if (schema.tag === "option") {
if (!Array.isArray(value)) {
throw new SchemaValidationError([], schema, "array", value);
}
if (value.length === 0) {
return null; return null;
}) } else {
.with({ tag: "option" }, (opt) => { return aqua2js(value[0], schema.type);
assert(Array.isArray(value), "Should not be possible, bad types"); }
} else if (
schema.tag === "scalar" ||
schema.tag === "bottomType" ||
schema.tag === "topType"
) {
return value;
} else if (schema.tag === "array") {
if (!Array.isArray(value)) {
throw new SchemaValidationError([], schema, "array", value);
}
if (value.length === 0) { return value.map((y) => {
return null; return aqua2js(y, schema.type);
} else {
return aqua2ts(value[0], opt.type);
}
})
.with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => {
return value;
})
.with({ tag: "array" }, (arr) => {
assert(Array.isArray(value), "Should not be possible, bad types");
return value.map((y) => {
return aqua2ts(y, arr.type);
});
})
.with({ tag: "struct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: "labeledProduct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((type, index) => {
return aqua2ts(value[index], type);
});
})
// uncomment to check that every pattern in matched
// .exhaustive();
.otherwise(() => {
throw new Error("Unexpected tag: " + jsonify(type));
}); });
} else if (schema.tag === "unlabeledProduct") {
if (!Array.isArray(value)) {
throw new SchemaValidationError([], schema, "array", value);
}
return res; return value.map((y, i) => {
}; return aqua2js(y, schema.items[i]);
/**
* Convert call service arguments list from their aqua representation to representation in typescript
* @param req - call service data
* @param arrow - aqua type definition
* @returns arguments in typescript representation
*/
export const aquaArgs2Ts = (
req: CallServiceData,
arrow: ArrowWithoutCallbacks,
): JSONArray => {
const argTypes = match(arrow.domain)
.with({ tag: "labeledProduct" }, (x) => {
return Object.values(x.fields);
})
.with({ tag: "unlabeledProduct" }, (x) => {
return x.items;
})
.with({ tag: "nil" }, (x) => {
return [];
})
// uncomment to check that every pattern in matched
// .exhaustive()
.otherwise(() => {
throw new Error("Unexpected tag: " + jsonify(arrow.domain));
}); });
} else if (["labeledProduct", "struct"].includes(schema.tag)) {
if (typeof value !== "object" || value == null || Array.isArray(value)) {
throw new SchemaValidationError([], schema, "object", value);
}
if (req.args.length !== argTypes.length) { return Object.fromEntries(
throw new Error( Object.entries(schema.fields).map(([key, type]) => {
`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`, const val = aqua2js(value[key], type);
return [key, val];
}),
); );
} else {
throw new SchemaValidationError([], schema, "never", value);
} }
}
return req.args.map((arg, index) => { export function js2aqua(
return aqua2ts(arg, argTypes[index]); value: JSONValue,
}); schema: NonArrowSimpleType,
}; { path }: ValidationContext,
): JSONValue {
if (schema.tag === "nil") {
if (value !== null) {
throw new SchemaValidationError(path, schema, "null", value);
}
/** return value;
* Convert value from its typescript representation to representation in aqua } else if (schema.tag === "option") {
* @param value - the value as represented in typescript // option means 'type | null'
* @param type - definition of the aqua type return value == null ? [] : [js2aqua(value, schema.type, { path })];
* @returns value represented in aqua } else if (schema.tag === "topType") {
*/ // topType equals to 'any'
export const ts2aqua = (value: JSONValue, type: NonArrowType): JSONValue => { return value;
const res = match(type) } else if (schema.tag === "bottomType") {
.with({ tag: "nil" }, () => { // bottomType equals to 'never'
return null; throw new SchemaValidationError(path, schema, "never", value);
}) } else if (schema.tag === "scalar") {
.with({ tag: "option" }, (opt) => { return isScalar(schema, value, { path });
if (value === null || value === undefined) { } else if (schema.tag === "array") {
return []; if (!Array.isArray(value)) {
} else { throw new SchemaValidationError(path, schema, "array", value);
return [ts2aqua(value, opt.type)]; }
}
}) return value.map((y, i) => {
.with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => { return js2aqua(y, schema.type, { path: [...path, `[${i}]`] });
return value; });
}) } else if (schema.tag === "unlabeledProduct") {
.with({ tag: "array" }, (arr) => { if (!Array.isArray(value)) {
assert(Array.isArray(value), "Should not be possible, bad types"); throw new SchemaValidationError(path, schema, "array", value);
return value.map((y) => { }
return ts2aqua(y, arr.type);
}); return value.map((y, i) => {
}) return js2aqua(y, schema.items[i], { path: [...path, `[${i}]`] });
.with({ tag: "struct" }, (x) => { });
return Object.entries(x.fields).reduce((agg, [key, type]) => { } else if (["labeledProduct", "struct"].includes(schema.tag)) {
const val = ts2aqua(value[key], type); if (typeof value !== "object" || value === null || Array.isArray(value)) {
return { ...agg, [key]: val }; throw new SchemaValidationError(path, schema, "object", value);
}, {}); }
})
.with({ tag: "labeledProduct" }, (x) => { return Object.fromEntries(
return Object.entries(x.fields).reduce((agg, [key, type]) => { Object.entries(schema.fields).map(([key, type]) => {
const val = ts2aqua(value[key], type); const val = js2aqua(value[key], type, { path: [...path, key] });
return { ...agg, [key]: val }; return [key, val];
}, {}); }),
}) );
.with({ tag: "unlabeledProduct" }, (x) => { } else {
return x.items.map((type, index) => { throw new SchemaValidationError(path, schema, "never", value);
return ts2aqua(value[index], type); }
}); }
})
// uncomment to check that every pattern in matched // Wrapping function, converting its arguments to aqua before call and back to js after call.
// .exhaustive() // It makes callbacks and service functions defined by user operate on js types seamlessly
.otherwise(() => { export const wrapJsFunction = (
throw new Error("Unexpected tag: " + jsonify(type)); func: ServiceImpl[string],
schema:
| ArrowWithoutCallbacks
| ArrowType<LabeledProductType<SimpleTypes> | UnlabeledProductType>,
): ServiceImpl[string] => {
return async (...args) => {
// These assertions used to correctly destructure tuple. It's impossible to do without asserts due to ts limitations.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const jsonArgs = args.slice(0, args.length - 1) as JSONValue[];
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const context = args[args.length - 1] as ParticleContext;
const schemaArgs =
schema.domain.tag === "nil"
? []
: schema.domain.tag === "unlabeledProduct"
? schema.domain.items
: Object.values(schema.domain.fields);
if (schemaArgs.length !== jsonArgs.length) {
throw new Error(
`Schema and generated air doesn't match. Air has been called with ${jsonArgs.length} args and schema contains ${schemaArgs.length} args`,
);
}
const tsArgs = jsonArgs.map((arg, i) => {
return aqua2js(arg, schemaArgs[i]);
}); });
return res; const returnTypeVoid =
}; schema.codomain.tag === "nil" || schema.codomain.items.length === 0;
/** const resultSchema =
* Convert return type of the service from it's typescript representation to representation in aqua schema.codomain.tag === "unlabeledProduct" &&
* @param returnValue - the value as represented in typescript schema.codomain.items.length === 1
* @param arrowType - the arrow type which describes the service ? schema.codomain.items[0]
* @returns - value represented in aqua : schema.codomain;
*/
export const returnType2Aqua = ( let result = await func(...tsArgs, context);
returnValue: any,
arrowType: ArrowType<NonArrowType>, if (returnTypeVoid) {
) => { result = null;
if (arrowType.codomain.tag === "nil") { }
return {};
} return js2aqua(result, resultSchema, { path: [] });
};
if (arrowType.codomain.items.length === 0) {
return {};
}
if (arrowType.codomain.items.length === 1) {
return ts2aqua(returnValue, arrowType.codomain.items[0]);
}
return arrowType.codomain.items.map((type, index) => {
return ts2aqua(returnValue[index], type);
});
};
/**
* Converts response value from aqua its representation to representation in typescript
* @param req - call service data
* @param arrow - aqua type definition
* @returns response value in typescript representation
*/
export const responseServiceValue2ts = (
req: CallServiceData,
arrow: ArrowType<any>,
) => {
return match(arrow.codomain)
.with({ tag: "nil" }, () => {
return null;
})
.with({ tag: "unlabeledProduct" }, (x) => {
if (x.items.length === 0) {
return null;
}
if (x.items.length === 1) {
return aqua2ts(req.args[0], x.items[0]);
}
return req.args.map((y, index) => {
return aqua2ts(y, x.items[index]);
});
})
.exhaustive();
}; };

View File

@ -14,66 +14,59 @@
* limitations under the License. * limitations under the License.
*/ */
import type { ServiceDef, ServiceImpl } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js"; import { logger } from "../util/logger.js";
import { registerGlobalService, userHandlerService } from "./services.js"; import { registerGlobalService, userHandlerService } from "./services.js";
import { ServiceImpl } from "./types.js";
const log = logger("aqua"); const log = logger("aqua");
interface RegisterServiceArgs { interface RegisterServiceArgs {
peer: FluencePeer; peer: FluencePeer;
def: ServiceDef; serviceId: string;
serviceId: string | undefined;
service: ServiceImpl; service: ServiceImpl;
} }
const findAllPossibleRegisteredServiceFunctions = (
service: ServiceImpl,
): Set<string> => {
let prototype: Record<string, unknown> = service;
const serviceMethods = new Set<string>();
do {
Object.getOwnPropertyNames(prototype)
.filter((prop) => {
return typeof prototype[prop] === "function" && prop !== "constructor";
})
.forEach((prop) => {
return serviceMethods.add(prop);
});
// coercing 'any' type to 'Record' bcs object prototype is actually an object
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
prototype = Object.getPrototypeOf(prototype) as Record<string, unknown>;
} while (prototype.constructor !== Object);
return serviceMethods;
};
export const registerService = ({ export const registerService = ({
peer, peer,
def, serviceId,
serviceId = def.defaultServiceId,
service, service,
}: RegisterServiceArgs) => { }: RegisterServiceArgs) => {
// TODO: Need to refactor this. We can compute function types from service implementation, making func more type safe log.trace("registering aqua service %o", { serviceId, service });
log.trace("registering aqua service %o", { def, serviceId, service });
// Checking for missing keys const serviceFunctions = findAllPossibleRegisteredServiceFunctions(service);
const requiredKeys =
def.functions.tag === "nil" ? [] : Object.keys(def.functions.fields);
const incorrectServiceDefinitions = requiredKeys.filter((f) => { for (const serviceFunction of serviceFunctions) {
return !(f in service); const handler = service[serviceFunction];
}); const userDefinedHandler = handler.bind(service);
if (serviceId == null) {
throw new Error("Service ID must be specified");
}
if (incorrectServiceDefinitions.length > 0) {
throw new Error(
`Error registering service ${serviceId}: missing functions: ` +
incorrectServiceDefinitions
.map((d) => {
return "'" + d + "'";
})
.join(", "),
);
}
const singleFunctions =
def.functions.tag === "nil" ? [] : Object.entries(def.functions.fields);
for (const singleFunction of singleFunctions) {
const [name] = singleFunction;
// The function has type of (arg1, arg2, arg3, ... , callParams) => CallServiceResultType | void
// Account for the fact that user service might be defined as a class - .bind(...)
const userDefinedHandler = service[name].bind(service);
const serviceDescription = userHandlerService( const serviceDescription = userHandlerService(
serviceId, serviceId,
singleFunction, serviceFunction,
userDefinedHandler, userDefinedHandler,
); );

View File

@ -14,32 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import { SecurityTetraplet } from "@fluencelabs/avm"; import { JSONValue } from "@fluencelabs/interfaces";
import {
CallParams,
ArrowWithoutCallbacks,
FunctionCallDef,
NonArrowType,
ServiceImpl,
JSONValue,
} from "@fluencelabs/interfaces";
import { fromUint8Array } from "js-base64";
import { match } from "ts-pattern";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { import {
CallServiceData, CallServiceData,
GenericCallServiceHandler, GenericCallServiceHandler,
ParticleContext,
ResultCodes, ResultCodes,
} from "../jsServiceHost/interfaces.js"; } from "../jsServiceHost/interfaces.js";
import { Particle } from "../particle/Particle.js"; import { Particle } from "../particle/Particle.js";
import { import { ServiceImpl } from "./types.js";
aquaArgs2Ts,
responseServiceValue2ts,
returnType2Aqua,
ts2aqua,
} from "./conversions.js";
export interface ServiceDescription { export interface ServiceDescription {
serviceId: string; serviceId: string;
@ -50,10 +36,10 @@ export interface ServiceDescription {
/** /**
* Creates a service which injects relay's peer id into aqua space * Creates a service which injects relay's peer id into aqua space
*/ */
export const injectRelayService = (def: FunctionCallDef, peer: FluencePeer) => { export const injectRelayService = (peer: FluencePeer) => {
return { return {
serviceId: def.names.getDataSrv, serviceId: "getDataSrv",
fnName: def.names.relay, fnName: "-relay-",
handler: () => { handler: () => {
return { return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
@ -69,7 +55,6 @@ export const injectRelayService = (def: FunctionCallDef, peer: FluencePeer) => {
export const injectValueService = ( export const injectValueService = (
serviceId: string, serviceId: string,
fnName: string, fnName: string,
valueType: NonArrowType,
value: JSONValue, value: JSONValue,
) => { ) => {
return { return {
@ -78,7 +63,7 @@ export const injectValueService = (
handler: () => { handler: () => {
return { return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
result: ts2aqua(value, valueType), result: value,
}; };
}, },
}; };
@ -87,15 +72,17 @@ export const injectValueService = (
/** /**
* Creates a service which is used to return value from aqua function into typescript space * Creates a service which is used to return value from aqua function into typescript space
*/ */
export const responseService = ( export const responseService = (resolveCallback: (val: JSONValue) => void) => {
def: FunctionCallDef,
resolveCallback: (val: JSONValue) => void,
) => {
return { return {
serviceId: def.names.responseSrv, serviceId: "callbackSrv",
fnName: def.names.responseFnName, fnName: "response",
handler: (req: CallServiceData) => { handler: (req: CallServiceData) => {
const userFunctionReturn = responseServiceValue2ts(req, def.arrow); const userFunctionReturn =
req.args.length === 0
? null
: req.args.length === 1
? req.args[0]
: req.args;
setTimeout(() => { setTimeout(() => {
resolveCallback(userFunctionReturn); resolveCallback(userFunctionReturn);
@ -113,12 +100,11 @@ export const responseService = (
* Creates a service which is used to return errors from aqua function into typescript space * Creates a service which is used to return errors from aqua function into typescript space
*/ */
export const errorHandlingService = ( export const errorHandlingService = (
def: FunctionCallDef,
rejectCallback: (err: JSONValue) => void, rejectCallback: (err: JSONValue) => void,
) => { ) => {
return { return {
serviceId: def.names.errorHandlingSrv, serviceId: "errorHandlingSrv",
fnName: def.names.errorFnName, fnName: "error",
handler: (req: CallServiceData) => { handler: (req: CallServiceData) => {
const [err] = req.args; const [err] = req.args;
@ -139,21 +125,19 @@ export const errorHandlingService = (
*/ */
export const userHandlerService = ( export const userHandlerService = (
serviceId: string, serviceId: string,
arrowType: [string, ArrowWithoutCallbacks], fnName: string,
userHandler: ServiceImpl[string], userHandler: ServiceImpl[string],
) => { ) => {
const [fnName, type] = arrowType;
return { return {
serviceId, serviceId,
fnName, fnName,
handler: async (req: CallServiceData) => { handler: async (req: CallServiceData) => {
const args: [...JSONValue[], CallParams<string>] = [ const args: [...JSONValue[], ParticleContext] = [
...aquaArgs2Ts(req, type), ...req.args,
extractCallParams(req, type), req.particleContext,
]; ];
const rawResult = await userHandler.bind(null)(...args); const result = await userHandler.bind(null)(...args);
const result = returnType2Aqua(rawResult, type);
return { return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
@ -163,46 +147,6 @@ export const userHandlerService = (
}; };
}; };
/**
* Extracts call params from from call service data according to aqua type definition
*/
const extractCallParams = (
req: CallServiceData,
arrow: ArrowWithoutCallbacks,
): CallParams<string> => {
const names: (string | undefined)[] = match(arrow.domain)
.with({ tag: "nil" }, () => {
return [];
})
.with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((_, index) => {
return "arg" + index;
});
})
.with({ tag: "labeledProduct" }, (x) => {
return Object.keys(x.fields);
})
.exhaustive();
const tetraplets: Record<string, SecurityTetraplet[]> = {};
for (let i = 0; i < req.args.length; i++) {
const name = names[i];
if (name != null) {
tetraplets[name] = req.tetraplets[i];
}
}
const callParams = {
...req.particleContext,
signature: fromUint8Array(req.particleContext.signature),
tetraplets,
};
return callParams;
};
export const registerParticleScopeService = ( export const registerParticleScopeService = (
peer: FluencePeer, peer: FluencePeer,
particle: Particle, particle: Particle,

View File

@ -14,19 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { JSONArray, JSONValue } from "@fluencelabs/interfaces";
Worker,
type Worker as WorkerImplementation,
} from "@fluencelabs/threads/master";
import { LazyLoader } from "../interfaces.js"; import { ParticleContext } from "../jsServiceHost/interfaces.js";
export class WorkerLoader extends LazyLoader<WorkerImplementation> { export type MaybePromise<T> = T | Promise<T>;
constructor() {
super(() => { export type ServiceImpl = Record<
return new Worker( string,
"../../../node_modules/@fluencelabs/marine-worker/dist/index.js", (...args: [...JSONArray, ParticleContext]) => MaybePromise<JSONValue>
); >;
});
}
}

View File

@ -17,7 +17,7 @@
import { noise } from "@chainsafe/libp2p-noise"; import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux"; import { yamux } from "@chainsafe/libp2p-yamux";
import { PeerIdB58 } from "@fluencelabs/interfaces"; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { Stream } from "@libp2p/interface/connection"; import type { Stream } from "@libp2p/interface/connection";
import type { PeerId } from "@libp2p/interface/peer-id"; import type { PeerId } from "@libp2p/interface/peer-id";
import { peerIdFromString } from "@libp2p/peer-id"; import { peerIdFromString } from "@libp2p/peer-id";
import { webSockets } from "@libp2p/websockets"; import { webSockets } from "@libp2p/websockets";

View File

@ -91,7 +91,11 @@ describe.skip("Ephemeral networks tests", () => {
}); });
// act // act
client.internals.initiateParticle(particle, () => {}); client.internals.initiateParticle(
particle,
() => {},
() => {},
);
// assert // assert
await expect(promise).resolves.toBe("success"); await expect(promise).resolves.toBe("success");

View File

@ -15,13 +15,13 @@
*/ */
import { PeerIdB58 } from "@fluencelabs/interfaces"; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js"; import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js"; import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { KeyPair } from "../keypair/index.js"; import { KeyPair } from "../keypair/index.js";
import { WasmLoaderFromNpm } from "../marine/deps-loader/node.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js"; import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { WorkerLoader } from "../marine/worker-script/workerLoader.js";
import { EphemeralNetwork } from "./network.js"; import { EphemeralNetwork } from "./network.js";
@ -35,25 +35,60 @@ export class EphemeralNetworkClient extends FluencePeer {
network: EphemeralNetwork, network: EphemeralNetwork,
relay: PeerIdB58, relay: PeerIdB58,
) { ) {
const workerLoader = new WorkerLoader(); const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
const controlModuleLoader = new WasmLoaderFromNpm( let marineJsWasm: ArrayBuffer;
"@fluencelabs/marine-js", let avmWasm: ArrayBuffer;
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
const marine = new MarineBackgroundRunner( const marine = new MarineBackgroundRunner(
workerLoader, {
controlModuleLoader, async getValue() {
avmModuleLoader, // TODO: load worker with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
); );
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
super(config, keyPair, marine, new JsServiceHost(), conn); super(config, keyPair, marine, new JsServiceHost(), conn);
} }
} }

View File

@ -15,16 +15,14 @@
*/ */
import { PeerIdB58 } from "@fluencelabs/interfaces"; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { IConnection } from "../connection/interfaces.js"; import { IConnection } from "../connection/interfaces.js";
import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js"; import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js"; import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { fromBase64Sk, KeyPair } from "../keypair/index.js"; import { fromBase64Sk, KeyPair } from "../keypair/index.js";
import {
WorkerLoaderFromFs,
WasmLoaderFromNpm,
} from "../marine/deps-loader/node.js";
import { IMarineHost } from "../marine/interfaces.js"; import { IMarineHost } from "../marine/interfaces.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js"; import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { Particle } from "../particle/Particle.js"; import { Particle } from "../particle/Particle.js";
@ -224,24 +222,7 @@ class EphemeralPeer extends FluencePeer {
export class EphemeralNetwork { export class EphemeralNetwork {
private peers: Map<PeerIdB58, EphemeralPeer> = new Map(); private peers: Map<PeerIdB58, EphemeralPeer> = new Map();
workerLoader: WorkerLoaderFromFs; constructor(readonly config: EphemeralConfig) {}
controlModuleLoader: WasmLoaderFromNpm;
avmModuleLoader: WasmLoaderFromNpm;
constructor(readonly config: EphemeralConfig) {
// shared worker for all the peers
this.workerLoader = new WorkerLoaderFromFs("../../marine/worker-script");
this.controlModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/marine-js",
"marine-js.wasm",
);
this.avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
}
/** /**
* Starts the Ephemeral network up * Starts the Ephemeral network up
@ -252,10 +233,54 @@ export class EphemeralNetwork {
const promises = this.config.peers.map(async (x) => { const promises = this.config.peers.map(async (x) => {
const kp = await fromBase64Sk(x.sk); const kp = await fromBase64Sk(x.sk);
const [marineJsWasm, avmWasm] = await Promise.all([
fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
}),
fetchResource("@fluencelabs/avm", "/dist/avm.wasm", "/").then((res) => {
return res.arrayBuffer();
}),
]);
const marine = new MarineBackgroundRunner( const marine = new MarineBackgroundRunner(
this.workerLoader, {
this.controlModuleLoader, async getValue() {
this.avmModuleLoader, // TODO: load worker with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
start(): Promise<void> {
return Promise.resolve(undefined);
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
start(): Promise<void> {
return Promise.resolve(undefined);
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
); );
const peerId = kp.getPeerId(); const peerId = kp.getPeerId();

View File

@ -16,12 +16,15 @@
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher"; import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver"; import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { ZodError } from "zod";
import { ClientPeer, makeClientPeerConfig } from "./clientPeer/ClientPeer.js"; import { ClientPeer, makeClientPeerConfig } from "./clientPeer/ClientPeer.js";
import { import {
ClientConfig, ClientConfig,
configSchema,
ConnectionState, ConnectionState,
RelayOptions, RelayOptions,
relaySchema,
} from "./clientPeer/types.js"; } from "./clientPeer/types.js";
import { callAquaFunction } from "./compilerSupport/callFunction.js"; import { callAquaFunction } from "./compilerSupport/callFunction.js";
import { registerService } from "./compilerSupport/registerService.js"; import { registerService } from "./compilerSupport/registerService.js";
@ -33,34 +36,34 @@ const createClient = async (
relay: RelayOptions, relay: RelayOptions,
config: ClientConfig = {}, config: ClientConfig = {},
): Promise<ClientPeer> => { ): Promise<ClientPeer> => {
try {
relay = relaySchema.parse(relay);
config = configSchema.parse(config);
} catch (e) {
if (e instanceof ZodError) {
throw new Error(JSON.stringify(e.format()));
}
}
const CDNUrl = config.CDNUrl ?? DEFAULT_CDN_URL; const CDNUrl = config.CDNUrl ?? DEFAULT_CDN_URL;
const fetchMarineJsWasm = async () => { const [marineJsWasm, avmWasm] = await Promise.all([
const resource = await fetchResource( fetchResource(
"@fluencelabs/marine-js", "@fluencelabs/marine-js",
"/dist/marine-js.wasm", "/dist/marine-js.wasm",
CDNUrl, CDNUrl,
); ).then((res) => {
return res.arrayBuffer();
return resource.arrayBuffer(); }),
}; fetchResource("@fluencelabs/avm", "/dist/avm.wasm", CDNUrl).then((res) => {
return res.arrayBuffer();
const fetchAvmWasm = async () => { }),
const resource = await fetchResource( ]);
"@fluencelabs/avm",
"/dist/avm.wasm",
CDNUrl,
);
return resource.arrayBuffer();
};
const marineJsWasm = await fetchMarineJsWasm();
const avmWasm = await fetchAvmWasm();
const marine = new MarineBackgroundRunner( const marine = new MarineBackgroundRunner(
{ {
async getValue() { async getValue() {
// TODO: load worker with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", CDNUrl); return getWorker("@fluencelabs/marine-worker", CDNUrl);
}, },
start() { start() {
@ -174,10 +177,14 @@ export type {
KeyPairOptions, KeyPairOptions,
} from "./clientPeer/types.js"; } from "./clientPeer/types.js";
export type { ParticleContext } from "./jsServiceHost/interfaces.js";
export { v5_callFunction, v5_registerService } from "./api.js"; export { v5_callFunction, v5_registerService } from "./api.js";
export { createClient, callAquaFunction, registerService }; export { createClient, callAquaFunction, registerService };
export { ClientPeer } from "./clientPeer/ClientPeer.js";
// Deprecated exports. Later they will be exposed only under js-client/keypair path // Deprecated exports. Later they will be exposed only under js-client/keypair path
export { export {
KeyPair, KeyPair,

View File

@ -22,6 +22,7 @@ import {
KeyPairFormat, KeyPairFormat,
serializeAvmArgs, serializeAvmArgs,
} from "@fluencelabs/avm"; } from "@fluencelabs/avm";
import { JSONValue } from "@fluencelabs/interfaces";
import { fromUint8Array } from "js-base64"; import { fromUint8Array } from "js-base64";
import { import {
concatMap, concatMap,
@ -55,7 +56,6 @@ import {
getActualTTL, getActualTTL,
hasExpired, hasExpired,
Particle, Particle,
ParticleExecutionStage,
ParticleQueueItem, ParticleQueueItem,
} from "../particle/Particle.js"; } from "../particle/Particle.js";
import { registerSig } from "../services/_aqua/services.js"; import { registerSig } from "../services/_aqua/services.js";
@ -67,6 +67,8 @@ import { Tracing } from "../services/Tracing.js";
import { logger } from "../util/logger.js"; import { logger } from "../util/logger.js";
import { jsonify, isString, getErrorMessage } from "../util/utils.js"; import { jsonify, isString, getErrorMessage } from "../util/utils.js";
import { ExpirationError, InterpreterError, SendError } from "./errors.js";
const log_particle = logger("particle"); const log_particle = logger("particle");
const log_peer = logger("peer"); const log_peer = logger("peer");
@ -247,11 +249,13 @@ export abstract class FluencePeer {
/** /**
* Initiates a new particle execution starting from local peer * Initiates a new particle execution starting from local peer
* @param particle - particle to start execution of * @param particle - particle to start execution of
* @param onStageChange - callback for reacting on particle state changes * @param onSuccess - callback which is called when particle execution succeed
* @param onError - callback which is called when particle execution fails
*/ */
initiateParticle: ( initiateParticle: (
particle: IParticle, particle: IParticle,
onStageChange: (stage: ParticleExecutionStage) => void, onSuccess: (result: JSONValue) => void,
onError: (error: Error) => void,
): void => { ): void => {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error( throw new Error(
@ -268,7 +272,8 @@ export abstract class FluencePeer {
this._incomingParticles.next({ this._incomingParticles.next({
particle: particle, particle: particle,
callResults: [], callResults: [],
onStageChange: onStageChange, onSuccess,
onError,
}); });
}, },
@ -329,6 +334,7 @@ export abstract class FluencePeer {
registerTracing(this, "tracingSrv", this._classServices.tracing); registerTracing(this, "tracingSrv", this._classServices.tracing);
} }
// TODO: too long, refactor
private _startParticleProcessing() { private _startParticleProcessing() {
this._particleSourceSubscription = this.connection.particleSource.subscribe( this._particleSourceSubscription = this.connection.particleSource.subscribe(
{ {
@ -336,7 +342,8 @@ export abstract class FluencePeer {
this._incomingParticles.next({ this._incomingParticles.next({
particle: p, particle: p,
callResults: [], callResults: [],
onStageChange: () => {}, onSuccess: () => {},
onError: () => {},
}); });
}, },
}, },
@ -471,10 +478,11 @@ export abstract class FluencePeer {
item.result.message, item.result.message,
); );
item.onStageChange({ item.onError(
stage: "interpreterError", new InterpreterError(
errorMessage: item.result.message, `Script interpretation failed: ${item.result.message} (particle id: ${item.particle.id})`,
}); ),
);
return; return;
} }
@ -493,10 +501,11 @@ export abstract class FluencePeer {
this.decodeAvmData(item.result.data), this.decodeAvmData(item.result.data),
); );
item.onStageChange({ item.onError(
stage: "interpreterError", new InterpreterError(
errorMessage: item.result.errorMessage, `Script interpretation failed: ${item.result.errorMessage} (particle id: ${item.particle.id})`,
}); ),
);
return; return;
} }
@ -508,10 +517,6 @@ export abstract class FluencePeer {
this.decodeAvmData(item.result.data), this.decodeAvmData(item.result.data),
); );
setTimeout(() => {
item.onStageChange({ stage: "interpreted" });
}, 0);
let connectionPromise: Promise<void> = Promise.resolve(); let connectionPromise: Promise<void> = Promise.resolve();
// send particle further if requested // send particle further if requested
@ -534,8 +539,6 @@ export abstract class FluencePeer {
"id %s. send successful", "id %s. send successful",
newParticle.id, newParticle.id,
); );
item.onStageChange({ stage: "sent" });
}) })
.catch((e: unknown) => { .catch((e: unknown) => {
log_particle.error( log_particle.error(
@ -544,10 +547,13 @@ export abstract class FluencePeer {
e, e,
); );
item.onStageChange({ const message = getErrorMessage(e);
stage: "sendingError",
errorMessage: getErrorMessage(e), item.onError(
}); new SendError(
`Could not send particle: (particle id: ${item.particle.id}, message: ${message})`,
),
);
}); });
} }
@ -560,7 +566,10 @@ export abstract class FluencePeer {
args: cr.arguments, args: cr.arguments,
serviceId: cr.serviceId, serviceId: cr.serviceId,
tetraplets: cr.tetraplets, tetraplets: cr.tetraplets,
particleContext: getParticleContext(item.particle), particleContext: getParticleContext(
item.particle,
cr.tetraplets,
),
}; };
void this._execSingleCallRequest(req) void this._execSingleCallRequest(req)
@ -582,6 +591,14 @@ export abstract class FluencePeer {
}; };
}) })
.then((res) => { .then((res) => {
if (
req.serviceId === "callbackSrv" &&
req.fnName === "response"
) {
// Particle already processed
return;
}
const serviceResult = { const serviceResult = {
result: jsonify(res.result), result: jsonify(res.result),
retCode: res.retCode, retCode: res.retCode,
@ -599,8 +616,6 @@ export abstract class FluencePeer {
}); });
}); });
} }
} else {
item.onStageChange({ stage: "localWorkDone" });
} }
return connectionPromise; return connectionPromise;
@ -623,7 +638,11 @@ export abstract class FluencePeer {
this.jsServiceHost.removeParticleScopeHandlers(particleId); this.jsServiceHost.removeParticleScopeHandlers(particleId);
item.onStageChange({ stage: "expired" }); item.onError(
new ExpirationError(
`Particle expired after ttl of ${item.particle.ttl}ms (particle id: ${item.particle.id})`,
),
);
} }
private decodeAvmData(data: Uint8Array) { private decodeAvmData(data: Uint8Array) {

View File

@ -44,7 +44,11 @@ describe("Basic AVM functionality in Fluence Peer tests", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toBe("1"); expect(res).toBe("1");
@ -85,7 +89,11 @@ describe("Basic AVM functionality in Fluence Peer tests", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toStrictEqual(["1", "2"]); expect(res).toStrictEqual(["1", "2"]);
@ -126,7 +134,11 @@ describe("Basic AVM functionality in Fluence Peer tests", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toBe("fast_result"); expect(res).toBe("fast_result");
@ -178,7 +190,11 @@ describe("Basic AVM functionality in Fluence Peer tests", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toBe("failed_with_timeout"); expect(res).toBe("failed_with_timeout");

View File

@ -94,7 +94,11 @@ describe("FluencePeer flow tests", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toEqual(expect.arrayContaining(["test1", "test1"])); expect(res).toEqual(expect.arrayContaining(["test1", "test1"]));

View File

@ -16,36 +16,11 @@
import { it, describe, expect } from "vitest"; import { it, describe, expect } from "vitest";
import { isFluencePeer } from "../../api.js";
import { handleTimeout } from "../../particle/Particle.js"; import { handleTimeout } from "../../particle/Particle.js";
import { import { registerHandlersHelper, withPeer } from "../../util/testUtils.js";
mkTestPeer,
registerHandlersHelper,
withPeer,
} from "../../util/testUtils.js";
import { FluencePeer } from "../FluencePeer.js"; import { FluencePeer } from "../FluencePeer.js";
describe("FluencePeer usage test suite", () => { describe("FluencePeer usage test suite", () => {
it("should perform test for FluencePeer class correctly", async () => {
// arrange
const peer = await mkTestPeer();
const number = 1;
const object = { str: "Hello!" };
const undefinedVal = undefined;
// act
const isPeerPeer = isFluencePeer(peer);
const isNumberPeer = isFluencePeer(number);
const isObjectPeer = isFluencePeer(object);
const isUndefinedPeer = isFluencePeer(undefinedVal);
// act
expect(isPeerPeer).toBe(true);
expect(isNumberPeer).toBe(false);
expect(isObjectPeer).toBe(false);
expect(isUndefinedPeer).toBe(false);
});
it("Should successfully call identity on local peer", async function () { it("Should successfully call identity on local peer", async function () {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
@ -72,7 +47,11 @@ describe("FluencePeer usage test suite", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toBe("test"); expect(res).toBe("test");
@ -130,7 +109,11 @@ describe("FluencePeer usage test suite", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
expect(res).toBe(null); expect(res).toBe(null);
@ -167,7 +150,11 @@ describe("FluencePeer usage test suite", () => {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(
particle,
() => {},
handleTimeout(reject),
);
}); });
await expect(promise).rejects.toMatchObject({ await expect(promise).rejects.toMatchObject({
@ -205,6 +192,6 @@ async function callIncorrectService(peer: FluencePeer) {
}, },
}); });
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, () => {}, handleTimeout(reject));
}); });
} }

View File

@ -0,0 +1,21 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class ExpirationError extends Error {}
export class InterpreterError extends Error {}
export class SendError extends Error {}

View File

@ -79,7 +79,7 @@ export enum ResultCodes {
/** /**
* Particle context. Contains additional information about particle which triggered `call` air instruction from AVM * Particle context. Contains additional information about particle which triggered `call` air instruction from AVM
*/ */
export interface ParticleContext { export type ParticleContext = {
/** /**
* The identifier of particle which triggered the call * The identifier of particle which triggered the call
*/ */
@ -104,7 +104,12 @@ export interface ParticleContext {
* Particle's signature * Particle's signature
*/ */
signature: Uint8Array; signature: Uint8Array;
}
/**
* Security Tetraplets received from AVM and copied here
*/
tetraplets: SecurityTetraplet[][];
};
/** /**
* Represents the information passed from AVM when a `call` air instruction is executed on the local peer * Represents the information passed from AVM when a `call` air instruction is executed on the local peer

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { SecurityTetraplet } from "@fluencelabs/avm";
import { JSONArray } from "@fluencelabs/interfaces"; import { JSONArray } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
@ -28,10 +29,6 @@ import {
ResultCodes, ResultCodes,
} from "./interfaces.js"; } from "./interfaces.js";
export const doNothing = () => {
return undefined;
};
export const WrapFnIntoServiceCall = ( export const WrapFnIntoServiceCall = (
fn: (args: JSONArray) => CallServiceResultType | undefined, fn: (args: JSONArray) => CallServiceResultType | undefined,
) => { ) => {
@ -51,13 +48,17 @@ export class ServiceError extends Error {
} }
} }
export const getParticleContext = (particle: IParticle): ParticleContext => { export const getParticleContext = (
particle: IParticle,
tetraplets: SecurityTetraplet[][],
): ParticleContext => {
return { return {
particleId: particle.id, particleId: particle.id,
initPeerId: particle.initPeerId, initPeerId: particle.initPeerId,
timestamp: particle.timestamp, timestamp: particle.timestamp,
ttl: particle.ttl, ttl: particle.ttl,
signature: particle.signature, signature: particle.signature,
tetraplets,
}; };
}; };

View File

@ -1,98 +0,0 @@
/**
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Buffer } from "buffer";
import fs from "fs";
import { createRequire } from "module";
import path from "path";
import {
Worker,
type Worker as WorkerImplementation,
} from "@fluencelabs/threads/master";
import { LazyLoader } from "../interfaces.js";
const require = createRequire(import.meta.url);
const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
const sab = new SharedArrayBuffer(buffer.length);
const tmp = new Uint8Array(sab);
tmp.set(buffer, 0);
return sab;
};
/**
* Load wasm file from npm package. Only works in nodejs environment.
* The function returns SharedArrayBuffer compatible with FluenceAppService methods.
* @param source - object specifying the source of the file. Consist two fields: package name and file path.
* @returns SharedArrayBuffer with the wasm file
*/
export const loadWasmFromNpmPackage = async (source: {
package: string;
file: string;
}): Promise<SharedArrayBuffer> => {
const packagePath = require.resolve(source.package);
const filePath = path.join(path.dirname(packagePath), source.file);
return loadWasmFromFileSystem(filePath);
};
/**
* Load wasm file from the file system. Only works in nodejs environment.
* The functions returns SharedArrayBuffer compatible with FluenceAppService methods.
* @param filePath - path to the wasm file
* @returns SharedArrayBuffer with the wasm fileWorker
*/
export const loadWasmFromFileSystem = async (
filePath: string,
): Promise<SharedArrayBuffer> => {
const buffer = await fs.promises.readFile(filePath);
return bufferToSharedArrayBuffer(buffer);
};
export class WasmLoaderFromFs extends LazyLoader<SharedArrayBuffer> {
constructor(filePath: string) {
super(() => {
return loadWasmFromFileSystem(filePath);
});
}
}
export class WasmLoaderFromNpm extends LazyLoader<SharedArrayBuffer> {
constructor(pkg: string, file: string) {
super(() => {
return loadWasmFromNpmPackage({ package: pkg, file: file });
});
}
}
export class WorkerLoaderFromFs extends LazyLoader<WorkerImplementation> {
constructor(scriptPath: string) {
super(() => {
return new Worker(scriptPath);
});
}
}
export class WorkerLoaderFromNpm extends LazyLoader<WorkerImplementation> {
constructor(pkg: string, file: string) {
super(() => {
const packagePath = require.resolve(pkg);
const scriptPath = path.join(path.dirname(packagePath), file);
return new Worker(scriptPath);
});
}
}

View File

@ -14,11 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import {
CallResultsArray,
InterpreterResult,
RunParameters,
} from "@fluencelabs/avm";
import { JSONObject, JSONValue, JSONArray } from "@fluencelabs/interfaces"; import { JSONObject, JSONValue, JSONArray } from "@fluencelabs/interfaces";
import { CallParameters } from "@fluencelabs/marine-worker"; import { CallParameters } from "@fluencelabs/marine-worker";
import type { Worker as WorkerImplementation } from "@fluencelabs/threads/master"; import type { Worker as WorkerImplementation } from "@fluencelabs/threads/master";
@ -58,22 +53,6 @@ export interface IMarineHost extends IStartable {
): Promise<JSONValue>; ): Promise<JSONValue>;
} }
/**
* Interface for different implementations of AVM runner
*/
export interface IAvmRunner extends IStartable {
/**
* Run AVM interpreter with the specified parameters
*/
run(
runParams: RunParameters,
air: string,
prevData: Uint8Array,
data: Uint8Array,
callResults: CallResultsArray,
): Promise<InterpreterResult | Error>;
}
/** /**
* Interface for something which can hold a value * Interface for something which can hold a value
*/ */
@ -94,32 +73,3 @@ export interface IWasmLoader
export interface IWorkerLoader export interface IWorkerLoader
extends IValueLoader<WorkerImplementation | Promise<WorkerImplementation>>, extends IValueLoader<WorkerImplementation | Promise<WorkerImplementation>>,
IStartable {} IStartable {}
/**
* Lazy loader for some value. Value is loaded only when `start` method is called
*/
export class LazyLoader<T> implements IStartable, IValueLoader<T> {
private value: T | null = null;
constructor(private loadValue: () => Promise<T> | T) {}
getValue(): T {
if (this.value == null) {
throw new Error(
"Value has not been loaded. Call `start` method to load the value.",
);
}
return this.value;
}
async start() {
if (this.value !== null) {
return;
}
this.value = await this.loadValue();
}
async stop() {}
}

View File

@ -15,11 +15,13 @@
*/ */
import { CallResultsArray } from "@fluencelabs/avm"; import { CallResultsArray } from "@fluencelabs/avm";
import { JSONValue } from "@fluencelabs/interfaces";
import { fromUint8Array, toUint8Array } from "js-base64"; import { fromUint8Array, toUint8Array } from "js-base64";
import { concat } from "uint8arrays/concat"; import { concat } from "uint8arrays/concat";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
import { ExpirationError } from "../jsPeer/errors.js";
import { KeyPair } from "../keypair/index.js"; import { KeyPair } from "../keypair/index.js";
import { numberToLittleEndianBytes } from "../util/bytes.js"; import { numberToLittleEndianBytes } from "../util/bytes.js";
@ -183,15 +185,16 @@ export type ParticleExecutionStage =
export interface ParticleQueueItem { export interface ParticleQueueItem {
particle: IParticle; particle: IParticle;
callResults: CallResultsArray; callResults: CallResultsArray;
onStageChange: (state: ParticleExecutionStage) => void; onSuccess: (result: JSONValue) => void;
onError: (error: Error) => void;
} }
/** /**
* Helper function to handle particle at expired stage * Helper function to handle particle at expired stage
*/ */
export const handleTimeout = (fn: () => void) => { export const handleTimeout = (fn: () => void) => {
return (stage: ParticleExecutionStage) => { return (error: Error) => {
if (stage.stage === "expired") { if (error instanceof ExpirationError) {
fn(); fn();
} }
}; };

View File

@ -14,58 +14,44 @@
* limitations under the License. * limitations under the License.
*/ */
import { Buffer } from "buffer"; import { readFile } from "fs/promises";
import * as fs from "fs";
import { CallParams } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { getErrorMessage } from "../util/utils.js"; import { getErrorMessage } from "../util/utils.js";
import { NodeUtilsDef, registerNodeUtils } from "./_aqua/node-utils.js"; import { registerNodeUtils } from "./_aqua/node-utils.js";
import { SecurityGuard } from "./securityGuard.js"; import { SecurityGuard } from "./securityGuard.js";
import { defaultGuard } from "./SingleModuleSrv.js"; import { defaultGuard } from "./SingleModuleSrv.js";
export class NodeUtils implements NodeUtilsDef { export class NodeUtils {
constructor(private peer: FluencePeer) { constructor(private peer: FluencePeer) {
this.securityGuard_readFile = defaultGuard(this.peer); this.securityGuard_readFile = defaultGuard(this.peer);
} }
securityGuard_readFile: SecurityGuard<"path">; securityGuard_readFile: SecurityGuard;
async read_file(path: string, callParams: CallParams<"path">) { async read_file(path: string, callParams: ParticleContext) {
if (!this.securityGuard_readFile(callParams)) { if (!this.securityGuard_readFile(callParams)) {
return { return {
success: false, success: false,
error: "Security guard validation failed", error: ["Security guard validation failed"],
content: null, content: null,
}; };
} }
try { try {
// Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't const data = await readFile(path, "base64");
const data = await new Promise<Buffer>((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err != null) {
reject(err);
return;
}
resolve(data);
});
});
return { return {
success: true, success: true,
// TODO: this is strange bug. content: [data],
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
content: data as unknown as string,
error: null, error: null,
}; };
} catch (err: unknown) { } catch (err: unknown) {
return { return {
success: false, success: false,
error: getErrorMessage(err), error: [getErrorMessage(err)],
content: null, content: null,
}; };
} }

View File

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { CallParams, PeerIdB58 } from "@fluencelabs/interfaces"; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { KeyPair } from "../keypair/index.js"; import { KeyPair } from "../keypair/index.js";
import { SigDef } from "./_aqua/services.js";
import { import {
allowOnlyParticleOriginatedAt, allowOnlyParticleOriginatedAt,
allowServiceFn, allowServiceFn,
@ -28,7 +28,7 @@ import {
} from "./securityGuard.js"; } from "./securityGuard.js";
export const defaultSigGuard = (peerId: PeerIdB58) => { export const defaultSigGuard = (peerId: PeerIdB58) => {
return and<"data">( return and(
allowOnlyParticleOriginatedAt(peerId), allowOnlyParticleOriginatedAt(peerId),
or( or(
allowServiceFn("trust-graph", "get_trust_bytes"), allowServiceFn("trust-graph", "get_trust_bytes"),
@ -43,23 +43,23 @@ export const defaultSigGuard = (peerId: PeerIdB58) => {
type SignReturnType = type SignReturnType =
| { | {
error: null; error: [];
signature: number[]; signature: [number[]];
success: true; success: true;
} }
| { | {
error: string; error: [string];
signature: null; signature: [];
success: false; success: false;
}; };
export class Sig implements SigDef { export class Sig {
constructor(private keyPair: KeyPair) {} constructor(private keyPair: KeyPair) {}
/** /**
* Configurable security guard for sign method * Configurable security guard for sign method
*/ */
securityGuard: SecurityGuard<"data"> = () => { securityGuard: SecurityGuard = () => {
return true; return true;
}; };
@ -75,13 +75,13 @@ export class Sig implements SigDef {
*/ */
async sign( async sign(
data: number[], data: number[],
callParams: CallParams<"data">, context: ParticleContext,
): Promise<SignReturnType> { ): Promise<SignReturnType> {
if (!this.securityGuard(callParams)) { if (!this.securityGuard(context)) {
return { return {
success: false, success: false,
error: "Security guard validation failed", error: ["Security guard validation failed"],
signature: null, signature: [],
}; };
} }
@ -89,8 +89,8 @@ export class Sig implements SigDef {
return { return {
success: true, success: true,
error: null, error: [],
signature: Array.from(signedData), signature: [Array.from(signedData)],
}; };
} }

View File

@ -16,13 +16,12 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { CallParams } from "@fluencelabs/interfaces";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { FluencePeer } from "../jsPeer/FluencePeer.js"; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { getErrorMessage } from "../util/utils.js"; import { getErrorMessage } from "../util/utils.js";
import { SrvDef } from "./_aqua/single-module-srv.js";
import { import {
allowOnlyParticleOriginatedAt, allowOnlyParticleOriginatedAt,
SecurityGuard, SecurityGuard,
@ -32,7 +31,8 @@ export const defaultGuard = (peer: FluencePeer) => {
return allowOnlyParticleOriginatedAt(peer.keyPair.getPeerId()); return allowOnlyParticleOriginatedAt(peer.keyPair.getPeerId());
}; };
export class Srv implements SrvDef { // Service for registering marine modules in js-client's marine runtime
export class Srv {
private services: Set<string> = new Set(); private services: Set<string> = new Set();
constructor(private peer: FluencePeer) { constructor(private peer: FluencePeer) {
@ -40,16 +40,13 @@ export class Srv implements SrvDef {
this.securityGuard_remove = defaultGuard(this.peer); this.securityGuard_remove = defaultGuard(this.peer);
} }
securityGuard_create: SecurityGuard<"wasm_b64_content">; securityGuard_create: SecurityGuard;
async create( async create(wasm_b64_content: string, callParams: ParticleContext) {
wasm_b64_content: string,
callParams: CallParams<"wasm_b64_content">,
) {
if (!this.securityGuard_create(callParams)) { if (!this.securityGuard_create(callParams)) {
return { return {
success: false, success: false,
error: "Security guard validation failed", error: ["Marine services could be registered on %init_peer_id% only"],
service_id: null, service_id: null,
}; };
} }
@ -66,25 +63,25 @@ export class Srv implements SrvDef {
return { return {
success: true, success: true,
service_id: newServiceId, service_id: [newServiceId],
error: null, error: null,
}; };
} catch (err: unknown) { } catch (err: unknown) {
return { return {
success: true, success: true,
service_id: null, service_id: null,
error: getErrorMessage(err), error: [getErrorMessage(err)],
}; };
} }
} }
securityGuard_remove: SecurityGuard<"service_id">; securityGuard_remove: SecurityGuard;
async remove(service_id: string, callParams: CallParams<"service_id">) { async remove(service_id: string, callParams: ParticleContext) {
if (!this.securityGuard_remove(callParams)) { if (!this.securityGuard_remove(callParams)) {
return { return {
success: false, success: false,
error: "Security guard validation failed", error: ["Marine services could be remove on %init_peer_id% only"],
service_id: null, service_id: null,
}; };
} }
@ -92,7 +89,7 @@ export class Srv implements SrvDef {
if (!this.services.has(service_id)) { if (!this.services.has(service_id)) {
return { return {
success: false, success: false,
error: `Service with id ${service_id} not found`, error: [`Service with id ${service_id} not found`],
}; };
} }

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CallParams } from "@fluencelabs/interfaces"; import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { TracingDef } from "./_aqua/tracing.js"; import { TracingDef } from "./_aqua/tracing.js";
@ -22,7 +22,7 @@ export class Tracing implements TracingDef {
tracingEvent( tracingEvent(
arrowName: string, arrowName: string,
event: string, event: string,
callParams: CallParams<"arrowName" | "event">, callParams: ParticleContext,
): void { ): void {
// This console log is intentional // This console log is intentional
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -16,11 +16,14 @@
import assert from "assert"; import assert from "assert";
import { CallParams, JSONArray } from "@fluencelabs/interfaces"; import { JSONArray } from "@fluencelabs/interfaces";
import { toUint8Array } from "js-base64"; import { toUint8Array } from "js-base64";
import { it, describe, expect, test } from "vitest"; import { it, describe, expect, test } from "vitest";
import { CallServiceData } from "../../jsServiceHost/interfaces.js"; import {
CallServiceData,
ParticleContext,
} from "../../jsServiceHost/interfaces.js";
import { KeyPair } from "../../keypair/index.js"; import { KeyPair } from "../../keypair/index.js";
import { builtInServices } from "../builtins.js"; import { builtInServices } from "../builtins.js";
import { allowServiceFn } from "../securityGuard.js"; import { allowServiceFn } from "../securityGuard.js";
@ -51,32 +54,32 @@ describe("Tests for default handler", () => {
serviceId | fnName | args | retCode | result serviceId | fnName | args | retCode | result
${"op"} | ${"identity"} | ${[]} | ${0} | ${{}} ${"op"} | ${"identity"} | ${[]} | ${0} | ${{}}
${"op"} | ${"identity"} | ${[1]} | ${0} | ${1} ${"op"} | ${"identity"} | ${[1]} | ${0} | ${1}
${"op"} | ${"identity"} | ${[1, 2]} | ${1} | ${"identity accepts up to 1 arguments, received 2 arguments"} ${"op"} | ${"identity"} | ${[1, 2]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"noop"} | ${[1, 2]} | ${0} | ${{}} ${"op"} | ${"noop"} | ${[1, 2]} | ${0} | ${{}}
${"op"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]} ${"op"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]}
${"op"} | ${"array_length"} | ${[[1, 2, 3]]} | ${0} | ${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"}
${"op"} | ${"concat"} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]} ${"op"} | ${"concat"} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]}
${"op"} | ${"concat"} | ${[[1, 2]]} | ${0} | ${[1, 2]} ${"op"} | ${"concat"} | ${[[1, 2]]} | ${0} | ${[1, 2]}
${"op"} | ${"concat"} | ${[]} | ${0} | ${[]} ${"op"} | ${"concat"} | ${[]} | ${0} | ${[]}
${"op"} | ${"concat"} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"} ${"op"} | ${"concat"} | ${[1, [1, 2], 1]} | ${1} | ${"Argument 0 expected to be of type array, Got number"}
${"op"} | ${"string_to_b58"} | ${["test"]} | ${0} | ${"3yZe7d"} ${"op"} | ${"string_to_b58"} | ${["test"]} | ${0} | ${"3yZe7d"}
${"op"} | ${"string_to_b58"} | ${["test", 1]} | ${1} | ${"string_to_b58 accepts only one string argument"} ${"op"} | ${"string_to_b58"} | ${["test", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"string_from_b58"} | ${["3yZe7d"]} | ${0} | ${"test"} ${"op"} | ${"string_from_b58"} | ${["3yZe7d"]} | ${0} | ${"test"}
${"op"} | ${"string_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"string_from_b58 accepts only one string argument"} ${"op"} | ${"string_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116]]} | ${0} | ${"3yZe7d"} ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116]]} | ${0} | ${"3yZe7d"}
${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"bytes_to_b58 accepts only single argument: array of numbers"} ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"bytes_from_b58"} | ${["3yZe7d"]} | ${0} | ${[116, 101, 115, 116]} ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d"]} | ${0} | ${[116, 101, 115, 116]}
${"op"} | ${"bytes_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"bytes_from_b58 accepts only one string argument"} ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"sha256_string"} | ${["hello, world!"]} | ${0} | ${"QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u"} ${"op"} | ${"sha256_string"} | ${["hello, world!"]} | ${0} | ${"QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u"}
${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"sha256_string accepts 1 argument, found: 2"} ${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"sha256_string accepts 1 argument, found: 0"} ${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"}
${"op"} | ${"concat_strings"} | ${[]} | ${0} | ${""} ${"op"} | ${"concat_strings"} | ${[]} | ${0} | ${""}
${"op"} | ${"concat_strings"} | ${["a", "b", "c"]} | ${0} | ${"abc"} ${"op"} | ${"concat_strings"} | ${["a", "b", "c"]} | ${0} | ${"abc"}
${"peer"} | ${"timeout"} | ${[200, []]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} ${"peer"} | ${"timeout"} | ${[200, []]} | ${1} | ${"Argument 1 expected to be of type string, Got array"}
${"peer"} | ${"timeout"} | ${[200, "test"]} | ${0} | ${"test"} ${"peer"} | ${"timeout"} | ${[200, "test"]} | ${0} | ${"test"}
${"peer"} | ${"timeout"} | ${[]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} ${"peer"} | ${"timeout"} | ${[]} | ${1} | ${"Expected 2 argument(s). Got 0"}
${"peer"} | ${"timeout"} | ${[200, "test", 1]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"} ${"peer"} | ${"timeout"} | ${[200, "test", 1]} | ${1} | ${"Expected 2 argument(s). Got 3"}
${"debug"} | ${"stringify"} | ${[]} | ${0} | ${'"<empty argument list>"'} ${"debug"} | ${"stringify"} | ${[]} | ${0} | ${'"<empty argument list>"'}
${"debug"} | ${"stringify"} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20} ${"debug"} | ${"stringify"} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20}
${"debug"} | ${"stringify"} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour} ${"debug"} | ${"stringify"} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour}
@ -149,6 +152,7 @@ describe("Tests for default handler", () => {
timestamp: 595951200, timestamp: 595951200,
ttl: 595961200, ttl: 595961200,
signature: new Uint8Array([]), signature: new Uint8Array([]),
tetraplets: [],
}, },
}; };
@ -185,6 +189,7 @@ describe("Tests for default handler", () => {
timestamp: 595951200, timestamp: 595951200,
ttl: 595961200, ttl: 595961200,
signature: new Uint8Array([]), signature: new Uint8Array([]),
tetraplets: [],
}, },
}; };
@ -243,14 +248,15 @@ const makeTestTetraplet = (
initPeerId: string, initPeerId: string,
serviceId: string, serviceId: string,
fnName: string, fnName: string,
): CallParams<"data"> => { ): ParticleContext => {
return { return {
particleId: "", particleId: "",
timestamp: 0, timestamp: 0,
ttl: 0, ttl: 0,
initPeerId: initPeerId, initPeerId: initPeerId,
tetraplets: { signature: new Uint8Array([]),
data: [ tetraplets: [
[
{ {
peer_pk: initPeerId, peer_pk: initPeerId,
function_name: fnName, function_name: fnName,
@ -258,7 +264,7 @@ const makeTestTetraplet = (
json_path: "", json_path: "",
}, },
], ],
}, ],
}; };
}; };
@ -273,7 +279,7 @@ describe("Sig service tests", () => {
); );
expect(res.success).toBe(true); expect(res.success).toBe(true);
expect(res.signature).toStrictEqual(testDataSig); expect(res.signature).toStrictEqual([testDataSig]);
}); });
it("sig.verify should return true for the correct signature", async () => { it("sig.verify should return true for the correct signature", async () => {
@ -305,7 +311,7 @@ describe("Sig service tests", () => {
expect(signature.success).toBe(true); expect(signature.success).toBe(true);
assert(signature.success); assert(signature.success);
const res = await sig.verify(signature.signature, testData); const res = await sig.verify(signature.signature[0], testData);
expect(res).toBe(true); expect(res).toBe(true);
}); });
@ -334,7 +340,7 @@ describe("Sig service tests", () => {
); );
expect(res.success).toBe(false); expect(res.success).toBe(false);
expect(res.error).toBe("Security guard validation failed"); expect(res.error).toStrictEqual(["Security guard validation failed"]);
}); });
it("sig.sign with defaultSigGuard should not allow particles initiated from other peers", async () => { it("sig.sign with defaultSigGuard should not allow particles initiated from other peers", async () => {
@ -352,7 +358,7 @@ describe("Sig service tests", () => {
); );
expect(res.success).toBe(false); expect(res.success).toBe(false);
expect(res.error).toBe("Security guard validation failed"); expect(res.error).toStrictEqual(["Security guard validation failed"]);
}); });
it("changing securityGuard should work", async () => { it("changing securityGuard should work", async () => {

View File

@ -17,7 +17,6 @@
import { it, describe, expect, beforeEach, afterEach } from "vitest"; import { it, describe, expect, beforeEach, afterEach } from "vitest";
import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { doNothing } from "../../jsServiceHost/serviceUtils.js";
import { mkTestPeer } from "../../util/testUtils.js"; import { mkTestPeer } from "../../util/testUtils.js";
let peer: FluencePeer; let peer: FluencePeer;
@ -72,7 +71,12 @@ describe("Sig service test suite", () => {
}); });
const p = await peer.internals.createNewParticle(script); const p = await peer.internals.createNewParticle(script);
peer.internals.initiateParticle(p, doNothing);
peer.internals.initiateParticle(
p,
() => {},
() => {},
);
const [ const [
nestedFirst, nestedFirst,

View File

@ -17,7 +17,6 @@
import * as path from "path"; import * as path from "path";
import * as url from "url"; import * as url from "url";
import { ServiceDef, ServiceImpl } from "@fluencelabs/interfaces";
import { it, describe, expect, beforeAll } from "vitest"; import { it, describe, expect, beforeAll } from "vitest";
import { registerService } from "../../compilerSupport/registerService.js"; import { registerService } from "../../compilerSupport/registerService.js";
@ -29,8 +28,6 @@ import { Sig } from "../Sig.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
let aqua: Record<string, CompiledFnCall>; let aqua: Record<string, CompiledFnCall>;
let sigDef: ServiceDef;
let dataProviderDef: ServiceDef;
describe("Sig service test suite", () => { describe("Sig service test suite", () => {
beforeAll(async () => { beforeAll(async () => {
@ -39,11 +36,9 @@ describe("Sig service test suite", () => {
"../../../aqua_test/sigService.aqua", "../../../aqua_test/sigService.aqua",
); );
const { services, functions } = await compileAqua(pathToAquaFiles); const { functions } = await compileAqua(pathToAquaFiles);
aqua = functions; aqua = functions;
sigDef = services["Sig"];
dataProviderDef = services["DataProvider"];
}); });
it("Use custom sig service, success path", async () => { it("Use custom sig service, success path", async () => {
@ -52,18 +47,16 @@ describe("Sig service test suite", () => {
const customSig = new Sig(customKeyPair); const customSig = new Sig(customKeyPair);
const data = [1, 2, 3, 4, 5]; const data = [1, 2, 3, 4, 5];
const anyService: Record<never, unknown> = customSig;
registerService({ registerService({
peer, peer,
def: sigDef,
serviceId: "CustomSig", serviceId: "CustomSig",
// TODO: fix this after changing registerService signature service: anyService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: customSig as unknown as ServiceImpl,
}); });
registerService({ registerService({
peer, peer,
def: dataProviderDef,
serviceId: "data", serviceId: "data",
service: { service: {
provide_data: () => { provide_data: () => {
@ -81,7 +74,7 @@ describe("Sig service test suite", () => {
const isSigCorrect = await customSig.verify( const isSigCorrect = await customSig.verify(
// TODO: Use compiled ts wrappers // TODO: Use compiled ts wrappers
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(result as { signature: number[] }).signature, (result as { signature: [number[]] }).signature[0],
data, data,
); );
@ -95,18 +88,16 @@ describe("Sig service test suite", () => {
const customSig = new Sig(customKeyPair); const customSig = new Sig(customKeyPair);
const data = [1, 2, 3, 4, 5]; const data = [1, 2, 3, 4, 5];
const anyService: Record<never, unknown> = customSig;
registerService({ registerService({
peer, peer,
def: sigDef,
serviceId: "CustomSig", serviceId: "CustomSig",
// TODO: fix this after changing registerService signature service: anyService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: customSig as unknown as ServiceImpl,
}); });
registerService({ registerService({
peer, peer,
def: dataProviderDef,
serviceId: "data", serviceId: "data",
service: { service: {
provide_data: () => { provide_data: () => {
@ -130,7 +121,6 @@ describe("Sig service test suite", () => {
registerService({ registerService({
peer: peer, peer: peer,
def: dataProviderDef,
serviceId: "data", serviceId: "data",
service: { service: {
provide_data: () => { provide_data: () => {
@ -146,6 +136,11 @@ describe("Sig service test suite", () => {
}); });
expect(callAsSigRes).toHaveProperty("success", false); expect(callAsSigRes).toHaveProperty("success", false);
expect(callAsPeerIdRes).toHaveProperty("error", [
"Security guard validation failed",
]);
expect(callAsPeerIdRes).toHaveProperty("success", false); expect(callAsPeerIdRes).toHaveProperty("success", false);
sig.securityGuard = () => { sig.securityGuard = () => {
@ -167,7 +162,8 @@ describe("Sig service test suite", () => {
const isValid = await sig.verify( const isValid = await sig.verify(
// TODO: Use compiled ts wrappers // TODO: Use compiled ts wrappers
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(callAsSigResAfterGuardChange as { signature: number[] }).signature, (callAsSigResAfterGuardChange as { signature: [number[]] })
.signature[0],
data, data,
); );

View File

@ -14,90 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
/**
* This compiled aqua file was modified to make it work in monorepo
*/
import { CallParams, ServiceImpl } from "@fluencelabs/interfaces";
import { registerService } from "../../compilerSupport/registerService.js"; import { registerService } from "../../compilerSupport/registerService.js";
import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { NodeUtils } from "../NodeUtils.js"; import { NodeUtils } from "../NodeUtils.js";
// Services
export interface NodeUtilsDef {
read_file: (
path: string,
callParams: CallParams<"path">,
) =>
| { content: string | null; error: string | null; success: boolean }
| Promise<{
content: string | null;
error: string | null;
success: boolean;
}>;
}
export function registerNodeUtils( export function registerNodeUtils(
peer: FluencePeer, peer: FluencePeer,
serviceId: string, serviceId: string,
service: NodeUtils, service: NodeUtils,
) { ) {
const nodeUtilsService: Record<never, unknown> = service;
registerService({ registerService({
peer, peer,
// TODO: fix this after changing registerService signature service: nodeUtilsService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
serviceId, serviceId,
def: {
defaultServiceId: "node_utils",
functions: {
tag: "labeledProduct",
fields: {
read_file: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
path: {
tag: "scalar",
name: "string",
},
},
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "struct",
name: "ReadFileResult",
fields: {
content: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
error: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
success: {
tag: "scalar",
name: "bool",
},
},
},
],
},
},
},
},
},
}); });
} }

View File

@ -14,33 +14,29 @@
* limitations under the License. * limitations under the License.
*/ */
/**
* This compiled aqua file was modified to make it work in monorepo
*/
import { CallParams, ServiceImpl } from "@fluencelabs/interfaces";
import { registerService } from "../../compilerSupport/registerService.js"; import { registerService } from "../../compilerSupport/registerService.js";
import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ParticleContext } from "../../jsServiceHost/interfaces.js";
import { Sig } from "../Sig.js"; import { Sig } from "../Sig.js";
// Services // Services
export interface SigDef { export interface SigDef {
get_peer_id: (callParams: CallParams<null>) => string | Promise<string>; get_peer_id: (callParams: ParticleContext) => string | Promise<string>;
sign: ( sign: (
data: number[], data: number[],
callParams: CallParams<"data">, callParams: ParticleContext,
) => ) =>
| { error: string | null; signature: number[] | null; success: boolean } | { error: [string?]; signature: [number[]?]; success: boolean }
| Promise<{ | Promise<{
error: string | null; error: [string?];
signature: number[] | null; signature: [number[]?];
success: boolean; success: boolean;
}>; }>;
verify: ( verify: (
signature: number[], signature: number[],
data: number[], data: number[],
callParams: CallParams<"signature" | "data">, callParams: ParticleContext,
) => boolean | Promise<boolean>; ) => boolean | Promise<boolean>;
} }
@ -49,113 +45,12 @@ export function registerSig(
serviceId: string, serviceId: string,
service: Sig, service: Sig,
) { ) {
const sigService: Record<never, unknown> = service;
registerService({ registerService({
peer, peer,
// TODO: fix this after changing registerService signature service: sigService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
serviceId, serviceId,
def: {
defaultServiceId: "sig",
functions: {
tag: "labeledProduct",
fields: {
get_peer_id: {
tag: "arrow",
domain: {
tag: "nil",
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "scalar",
name: "string",
},
],
},
},
sign: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
data: {
tag: "array",
type: {
tag: "scalar",
name: "u8",
},
},
},
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "struct",
name: "SignResult",
fields: {
error: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
signature: {
tag: "option",
type: {
tag: "array",
type: {
tag: "scalar",
name: "u8",
},
},
},
success: {
tag: "scalar",
name: "bool",
},
},
},
],
},
},
verify: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
signature: {
tag: "array",
type: {
tag: "scalar",
name: "u8",
},
},
data: {
tag: "array",
type: {
tag: "scalar",
name: "u8",
},
},
},
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "scalar",
name: "bool",
},
],
},
},
},
},
},
}); });
} }

View File

@ -14,149 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
/**
* This compiled aqua file was modified to make it work in monorepo
*/
import { CallParams, ServiceImpl } from "@fluencelabs/interfaces";
import { registerService } from "../../compilerSupport/registerService.js"; import { registerService } from "../../compilerSupport/registerService.js";
import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { Srv } from "../SingleModuleSrv.js"; import { Srv } from "../SingleModuleSrv.js";
// Services
export interface SrvDef {
create: (
wasm_b64_content: string,
callParams: CallParams<"wasm_b64_content">,
) =>
| { error: string | null; service_id: string | null; success: boolean }
| Promise<{
error: string | null;
service_id: string | null;
success: boolean;
}>;
list: (callParams: CallParams<null>) => string[] | Promise<string[]>;
remove: (
service_id: string,
callParams: CallParams<"service_id">,
) =>
| { error: string | null; success: boolean }
| Promise<{ error: string | null; success: boolean }>;
}
export function registerSrv( export function registerSrv(
peer: FluencePeer, peer: FluencePeer,
serviceId: string, serviceId: string,
service: Srv, service: Srv,
) { ) {
const singleModuleService: Record<never, unknown> = service;
registerService({ registerService({
peer, peer,
serviceId, serviceId,
// TODO: fix this after changing registerService signature service: singleModuleService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
def: {
defaultServiceId: "single_module_srv",
functions: {
tag: "labeledProduct",
fields: {
create: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
wasm_b64_content: {
tag: "scalar",
name: "string",
},
},
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "struct",
name: "ServiceCreationResult",
fields: {
error: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
service_id: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
success: {
tag: "scalar",
name: "bool",
},
},
},
],
},
},
list: {
tag: "arrow",
domain: {
tag: "nil",
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "array",
type: {
tag: "scalar",
name: "string",
},
},
],
},
},
remove: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
service_id: {
tag: "scalar",
name: "string",
},
},
},
codomain: {
tag: "unlabeledProduct",
items: [
{
tag: "struct",
name: "RemoveResult",
fields: {
error: {
tag: "option",
type: {
tag: "scalar",
name: "string",
},
},
success: {
tag: "scalar",
name: "bool",
},
},
},
],
},
},
},
},
},
}); });
} }

View File

@ -17,10 +17,10 @@
/** /**
* This compiled aqua file was modified to make it work in monorepo * This compiled aqua file was modified to make it work in monorepo
*/ */
import { CallParams, ServiceImpl } from "@fluencelabs/interfaces";
import { registerService } from "../../compilerSupport/registerService.js"; import { registerService } from "../../compilerSupport/registerService.js";
import { FluencePeer } from "../../jsPeer/FluencePeer.js"; import { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ParticleContext } from "../../jsServiceHost/interfaces.js";
import { Tracing } from "../Tracing.js"; import { Tracing } from "../Tracing.js";
// Services // Services
@ -29,7 +29,7 @@ export interface TracingDef {
tracingEvent: ( tracingEvent: (
arrowName: string, arrowName: string,
event: string, event: string,
callParams: CallParams<"arrowName" | "event">, callParams: ParticleContext,
) => void | Promise<void>; ) => void | Promise<void>;
} }
@ -38,40 +38,11 @@ export function registerTracing(
serviceId: string, serviceId: string,
service: Tracing, service: Tracing,
) { ) {
const tracingService: Record<never, unknown> = service;
registerService({ registerService({
peer, peer,
serviceId, serviceId,
// TODO: fix this after changing registerService signature service: tracingService,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
def: {
defaultServiceId: "tracingSrv",
functions: {
tag: "labeledProduct",
fields: {
tracingEvent: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {
arrowName: {
tag: "scalar",
name: "string",
},
event: {
tag: "scalar",
name: "string",
},
},
},
codomain: {
tag: "nil",
},
},
},
},
},
}); });
} }
// Functions

View File

@ -14,74 +14,144 @@
* limitations under the License. * limitations under the License.
*/ */
import assert from "assert";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { JSONValue } from "@fluencelabs/interfaces"; import { JSONValue } from "@fluencelabs/interfaces";
import bs58 from "bs58"; import bs58 from "bs58";
import { sha256 } from "multiformats/hashes/sha2"; import { sha256 } from "multiformats/hashes/sha2";
import { z } from "zod";
import { import {
CallServiceData,
CallServiceResult, CallServiceResult,
CallServiceResultType, CallServiceResultType,
GenericCallServiceHandler, GenericCallServiceHandler,
ResultCodes, ResultCodes,
} from "../jsServiceHost/interfaces.js"; } from "../jsServiceHost/interfaces.js";
import { getErrorMessage, isString, jsonify } from "../util/utils.js"; import { getErrorMessage, jsonify } from "../util/utils.js";
const success = ( const success = (result: CallServiceResultType): CallServiceResult => {
// TODO: Remove unknown after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
result: CallServiceResultType | unknown,
): CallServiceResult => {
return { return {
// TODO: Remove type assertion after adding validation to builtin inputs result,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
result: result as CallServiceResultType,
retCode: ResultCodes.success, retCode: ResultCodes.success,
}; };
}; };
const error = ( const error = (error: CallServiceResultType): CallServiceResult => {
// TODO: Remove unknown after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
error: CallServiceResultType | unknown,
): CallServiceResult => {
return { return {
// TODO: Remove type assertion after adding validation to builtin inputs result: error,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
result: error as CallServiceResultType,
retCode: ResultCodes.error, retCode: ResultCodes.error,
}; };
}; };
const chunk = <T>(arr: T[]): T[][] => {
const res: T[][] = [];
const chunkSize = 2;
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
res.push(chunk);
}
return res;
};
const errorNotImpl = (methodName: string) => { const errorNotImpl = (methodName: string) => {
return error( return error(
`The JS implementation of Peer does not support "${methodName}"`, `The JS implementation of Peer does not support "${methodName}"`,
); );
}; };
const makeJsonImpl = (args: [Record<string, JSONValue>, ...JSONValue[]]) => { const parseWithSchema = <T extends z.ZodTypeAny>(
const [obj, ...kvs] = args; schema: T,
req: CallServiceData,
): [z.infer<T>, null] | [null, string] => {
const result = schema.safeParse(req.args, {
errorMap: (issue, ctx) => {
if (
issue.code === z.ZodIssueCode.invalid_type &&
issue.path.length === 1 &&
typeof issue.path[0] === "number"
) {
const [arg] = issue.path;
return {
message: `Argument ${arg} expected to be of type ${issue.expected}, Got ${issue.received}`,
};
}
const toMerge: Record<string, JSONValue> = {}; if (issue.code === z.ZodIssueCode.too_big) {
return {
message: `Expected ${
issue.maximum
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} argument(s). Got ${ctx.data.length}`,
};
}
for (let i = 0; i < kvs.length / 2; i++) { if (issue.code === z.ZodIssueCode.too_small) {
const k = kvs[i * 2]; return {
message: `Expected ${
issue.minimum
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} argument(s). Got ${ctx.data.length}`,
};
}
if (!isString(k)) { if (issue.code === z.ZodIssueCode.invalid_union) {
return error(`Argument ${i * 2 + 1} is expected to be string`); return {
} // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Expected argument(s). Got ${ctx.data.length}`,
};
}
const v = kvs[i * 2 + 1]; return { message: ctx.defaultError };
toMerge[k] = v; },
});
if (result.success) {
return [result.data, null];
} else {
return [null, result.error.errors[0].message];
} }
const res = { ...obj, ...toMerge };
return success(res);
}; };
// TODO: These assert made for silencing more stricter ts rules. Will be fixed in DXJ-493 const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];
const jsonSchema: z.ZodType<Json> = z.lazy(() => {
return z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]);
});
const jsonImplSchema = z
.tuple([z.record(jsonSchema)])
.rest(z.tuple([z.string(), jsonSchema]));
const makeJsonImpl = (args: z.infer<typeof jsonImplSchema>) => {
const [obj, ...kvs] = args;
return success({ ...obj, ...Object.fromEntries(kvs) });
};
type withSchema = <T extends z.ZodTypeAny>(
arg: T,
) => (
arg1: (value: z.infer<T>) => CallServiceResult | Promise<CallServiceResult>,
) => (req: CallServiceData) => CallServiceResult | Promise<CallServiceResult>;
const withSchema: withSchema = <T extends z.ZodTypeAny>(schema: T) => {
return (bound) => {
return (req) => {
const [value, message] = parseWithSchema(schema, req);
if (message != null) {
return error(message);
}
return bound(value);
};
};
};
export const builtInServices: Record< export const builtInServices: Record<
string, string,
Record<string, GenericCallServiceHandler> Record<string, GenericCallServiceHandler>
@ -116,29 +186,16 @@ export const builtInServices: Record<
return errorNotImpl("peer.get_contact"); return errorNotImpl("peer.get_contact");
}, },
timeout: (req) => { timeout: withSchema(z.tuple([z.number(), z.string()]))(
if (req.args.length !== 2) { ([durationMs, msg]) => {
return error( return new Promise((resolve) => {
"timeout accepts exactly two arguments: timeout duration in ms and a message string", setTimeout(() => {
); const res = success(msg);
} resolve(res);
}, durationMs);
const durationMs = req.args[0]; });
const message = req.args[1]; },
),
if (typeof durationMs !== "number" || typeof message !== "string") {
return error(
"timeout accepts exactly two arguments: timeout duration in ms and a message string",
);
}
return new Promise((resolve) => {
setTimeout(() => {
const res = success(message);
resolve(res);
}, durationMs);
});
},
}, },
kad: { kad: {
@ -246,120 +303,48 @@ export const builtInServices: Record<
return success(req.args); return success(req.args);
}, },
array_length: (req) => { array_length: withSchema(z.tuple([z.array(z.unknown())]))(([arr]) => {
if (req.args.length !== 1) { return success(arr.length);
return error( }),
"array_length accepts exactly one argument, found: " +
req.args.length,
);
} else {
assert(Array.isArray(req.args[0]));
return success(req.args[0].length);
}
},
identity: (req) => { identity: withSchema(z.array(jsonSchema).max(1))((args) => {
if (req.args.length > 1) { return success(args.length === 0 ? {} : args[0]);
return error( }),
`identity accepts up to 1 arguments, received ${req.args.length} arguments`,
);
} else {
return success(req.args.length === 0 ? {} : req.args[0]);
}
},
concat: (req) => { concat: withSchema(z.array(z.array(z.unknown())))((args) => {
const incorrectArgIndices = req.args // // Schema is used with unknown type to prevent useless runtime check
.map((x, i): [boolean, number] => {
return [Array.isArray(x), i];
})
.filter(([isArray]) => {
return !isArray;
})
.map(([, index]) => {
return index;
});
if (incorrectArgIndices.length > 0) {
const str = incorrectArgIndices.join(", ");
return error(
`All arguments of 'concat' must be arrays: arguments ${str} are not`,
);
} else {
// TODO: remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return success([].concat(...(req.args as never[][])));
}
},
string_to_b58: (req) => {
if (req.args.length !== 1) {
return error("string_to_b58 accepts only one string argument");
} else {
const [input] = req.args;
// TODO: remove after adding validation
assert(typeof input === "string");
return success(bs58.encode(new TextEncoder().encode(input)));
}
},
string_from_b58: (req) => {
if (req.args.length !== 1) {
return error("string_from_b58 accepts only one string argument");
} else {
const [input] = req.args;
// TODO: remove after adding validation
assert(typeof input === "string");
return success(new TextDecoder().decode(bs58.decode(input)));
}
},
bytes_to_b58: (req) => {
if (req.args.length !== 1 || !Array.isArray(req.args[0])) {
return error(
"bytes_to_b58 accepts only single argument: array of numbers",
);
} else {
// TODO: remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argumentArray = req.args[0] as number[];
return success(bs58.encode(new Uint8Array(argumentArray)));
}
},
bytes_from_b58: (req) => {
if (req.args.length !== 1) {
return error("bytes_from_b58 accepts only one string argument");
} else {
const [input] = req.args;
// TODO: remove after adding validation
assert(typeof input === "string");
return success(Array.from(bs58.decode(input)));
}
},
sha256_string: async (req) => {
if (req.args.length !== 1) {
return error(
`sha256_string accepts 1 argument, found: ${req.args.length}`,
);
} else {
const [input] = req.args;
// TODO: remove after adding validation
assert(typeof input === "string");
const inBuffer = Buffer.from(input);
const multihash = await sha256.digest(inBuffer);
return success(bs58.encode(multihash.bytes));
}
},
concat_strings: (req) => {
// TODO: remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = "".concat(...(req.args as string[])); const arr = args as never[][];
return success(res);
}, return success(arr.flat());
}),
string_to_b58: withSchema(z.tuple([z.string()]))(([input]) => {
return success(bs58.encode(new TextEncoder().encode(input)));
}),
string_from_b58: withSchema(z.tuple([z.string()]))(([input]) => {
return success(new TextDecoder().decode(bs58.decode(input)));
}),
bytes_to_b58: withSchema(z.tuple([z.array(z.number())]))(([input]) => {
return success(bs58.encode(new Uint8Array(input)));
}),
bytes_from_b58: withSchema(z.tuple([z.string()]))(([input]) => {
return success(Array.from(bs58.decode(input)));
}),
sha256_string: withSchema(z.tuple([z.string()]))(async ([input]) => {
const inBuffer = Buffer.from(input);
const multihash = await sha256.digest(inBuffer);
return success(bs58.encode(multihash.bytes));
}),
concat_strings: withSchema(z.array(z.string()))((args) => {
return success(args.join(""));
}),
}, },
debug: { debug: {
@ -379,365 +364,187 @@ export const builtInServices: Record<
}, },
math: { math: {
add: (req) => { add: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x + y); return success(x + y);
}, }),
sub: (req) => { sub: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x - y); return success(x - y);
}, }),
mul: (req) => { mul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x * y); return success(x * y);
}, }),
fmul: (req) => { fmul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(Math.floor(x * y)); return success(Math.floor(x * y));
}, }),
div: (req) => { div: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(Math.floor(x / y)); return success(Math.floor(x / y));
}, }),
rem: (req) => { rem: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x % y); return success(x % y);
}, }),
pow: (req) => { pow: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(Math.pow(x, y)); return success(Math.pow(x, y));
}, }),
log: (req) => { log: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(Math.log(y) / Math.log(x)); return success(Math.log(y) / Math.log(x));
}, }),
}, },
cmp: { cmp: {
gt: (req) => { gt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x > y); return success(x > y);
}, }),
gte: (req) => { gte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x >= y); return success(x >= y);
}, }),
lt: (req) => { lt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x < y); return success(x < y);
}, }),
lte: (req) => { lte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x <= y); return success(x <= y);
}, }),
cmp: (req) => { cmp: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [x, y] = req.args;
// TODO: Remove after adding validation
assert(typeof x === "number" && typeof y === "number");
return success(x === y ? 0 : x > y ? 1 : -1); return success(x === y ? 0 : x > y ? 1 : -1);
}, }),
}, },
array: { array: {
sum: (req) => { sum: withSchema(z.tuple([z.array(z.number())]))(([xs]) => {
let err;
if ((err = checkForArgumentsCount(req, 1)) != null) {
return err;
}
// TODO: Remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const [xs] = req.args as [number[]];
return success( return success(
xs.reduce((agg, cur) => { xs.reduce((agg, cur) => {
return agg + cur; return agg + cur;
}, 0), }, 0),
); );
}, }),
dedup: (req) => { dedup: withSchema(z.tuple([z.array(z.any())]))(([xs]) => {
let err;
if ((err = checkForArgumentsCount(req, 1)) != null) {
return err;
}
const [xs] = req.args;
// TODO: Remove after adding validation
assert(Array.isArray(xs));
const set = new Set(xs); const set = new Set(xs);
return success(Array.from(set)); return success(Array.from(set));
}, }),
intersect: (req) => { intersect: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
let err; ([xs, ys]) => {
const intersection = xs.filter((x) => {
return ys.includes(x);
});
if ((err = checkForArgumentsCount(req, 2)) != null) { return success(intersection);
return err; },
} ),
const [xs, ys] = req.args; diff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
// TODO: Remove after adding validation ([xs, ys]) => {
assert(Array.isArray(xs) && Array.isArray(ys)); const diff = xs.filter((x) => {
return !ys.includes(x);
});
const intersection = xs.filter((x) => { return success(diff);
return ys.includes(x); },
}); ),
return success(intersection); sdiff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
}, ([xs, ys]) => {
const sdiff = [
xs.filter((y) => {
return !ys.includes(y);
}),
ys.filter((x) => {
return !xs.includes(x);
}),
].flat();
diff: (req) => { return success(sdiff);
let err; },
),
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [xs, ys] = req.args;
// TODO: Remove after adding validation
assert(Array.isArray(xs) && Array.isArray(ys));
const diff = xs.filter((x) => {
return !ys.includes(x);
});
return success(diff);
},
sdiff: (req) => {
let err;
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
const [xs, ys] = req.args;
// TODO: Remove after adding validation
assert(Array.isArray(xs) && Array.isArray(ys));
const sdiff = [
// force new line
...xs.filter((y) => {
return !ys.includes(y);
}),
...ys.filter((x) => {
return !xs.includes(x);
}),
];
return success(sdiff);
},
}, },
json: { json: {
obj: (req) => { obj: withSchema(
let err; z
.array(z.unknown())
.refine(
(arr) => {
return arr.length % 2 === 0;
},
(arr) => {
return {
message: "Expected even number of argument(s). Got " + arr.length,
};
},
)
.transform((args) => {
return chunk(args);
})
.pipe(z.array(z.tuple([z.string(), jsonSchema]))),
)((args) => {
return makeJsonImpl([{}, ...args]);
}),
if ((err = checkForArgumentsCountEven(req)) != null) { put: withSchema(
return err; z
} .tuple([z.record(jsonSchema), z.string(), jsonSchema])
.transform(
([obj, name, value]): [{ [key: string]: Json }, [string, Json]] => {
return [obj, [name, value]];
},
),
)(makeJsonImpl),
// TODO: remove after adding validation puts: withSchema(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions z
return makeJsonImpl([{}, ...req.args] as [ .array(z.unknown())
Record<string, JSONValue>, .refine(
...JSONValue[], (arr) => {
]); return arr.length >= 3;
}, },
(value) => {
return {
message: `Expected more than 3 argument(s). Got ${value.length}`,
};
},
)
.refine(
(arr) => {
return arr.length % 2 === 1;
},
{
message: "Argument count must be odd.",
},
)
.transform((args) => {
return [args[0], ...chunk(args.slice(1))];
})
.pipe(jsonImplSchema),
)(makeJsonImpl),
put: (req) => { stringify: withSchema(z.tuple([z.record(z.string(), jsonSchema)]))(
let err; ([json]) => {
const res = JSON.stringify(json);
if ((err = checkForArgumentsCount(req, 3)) != null) { return success(res);
return err; },
} ),
if ((err = checkForArgumentType(req, 0, "object")) != null) {
return err;
}
return makeJsonImpl(
// TODO: remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
req.args as [Record<string, JSONValue>, ...JSONValue[]],
);
},
puts: (req) => {
let err;
if ((err = checkForArgumentsCountOdd(req)) != null) {
return err;
}
if ((err = checkForArgumentsCountMoreThan(req, 3)) != null) {
return err;
}
if ((err = checkForArgumentType(req, 0, "object")) != null) {
return err;
}
return makeJsonImpl(
// TODO: remove after adding validation
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
req.args as [Record<string, JSONValue>, ...JSONValue[]],
);
},
stringify: (req) => {
let err;
if ((err = checkForArgumentsCount(req, 1)) != null) {
return err;
}
if ((err = checkForArgumentType(req, 0, "object")) != null) {
return err;
}
const [json] = req.args;
const res = JSON.stringify(json);
return success(res);
},
parse: (req) => {
let err;
if ((err = checkForArgumentsCount(req, 1)) != null) {
return err;
}
if ((err = checkForArgumentType(req, 0, "string")) != null) {
return err;
}
const [raw] = req.args;
parse: withSchema(z.tuple([z.string()]))(([raw]) => {
try { try {
// TODO: Remove after adding validation // Parsing any argument here yields JSONValue
assert(typeof raw === "string"); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const json = JSON.parse(raw); const json = JSON.parse(raw) as JSONValue;
return success(json); return success(json);
} catch (err: unknown) { } catch (err: unknown) {
return error(getErrorMessage(err)); return error(getErrorMessage(err));
} }
}, }),
}, },
"run-console": { "run-console": {
@ -749,59 +556,3 @@ export const builtInServices: Record<
}, },
}, },
} as const; } as const;
const checkForArgumentsCount = (
req: { args: Array<unknown> },
count: number,
) => {
if (req.args.length !== count) {
return error(`Expected ${count} argument(s). Got ${req.args.length}`);
}
return null;
};
const checkForArgumentsCountMoreThan = (
req: { args: Array<unknown> },
count: number,
) => {
if (req.args.length < count) {
return error(
`Expected more than ${count} argument(s). Got ${req.args.length}`,
);
}
return null;
};
const checkForArgumentsCountEven = (req: { args: Array<unknown> }) => {
if (req.args.length % 2 === 1) {
return error(`Expected even number of argument(s). Got ${req.args.length}`);
}
return null;
};
const checkForArgumentsCountOdd = (req: { args: Array<unknown> }) => {
if (req.args.length % 2 === 0) {
return error(`Expected odd number of argument(s). Got ${req.args.length}`);
}
return null;
};
const checkForArgumentType = (
req: { args: Array<unknown> },
index: number,
type: string,
) => {
const actual = typeof req.args[index];
if (actual !== type) {
return error(
`Argument ${index} expected to be of type ${type}, Got ${actual}`,
);
}
return null;
};

View File

@ -15,25 +15,25 @@
*/ */
import { SecurityTetraplet } from "@fluencelabs/avm"; import { SecurityTetraplet } from "@fluencelabs/avm";
import { CallParams, PeerIdB58 } from "@fluencelabs/interfaces"; import { PeerIdB58 } from "@fluencelabs/interfaces";
type ArgName = string | null; import { ParticleContext } from "../jsServiceHost/interfaces.js";
// Helpers for validating service function
/** /**
* A predicate of call params for sig service's sign method which determines whether signing operation is allowed or not * A predicate of call params for sig service's sign method which determines whether signing operation is allowed or not
*/ */
export type SecurityGuard<T extends ArgName> = ( export type SecurityGuard = (params: ParticleContext) => boolean;
params: CallParams<T>,
) => boolean;
/** /**
* Only allow calls when tetraplet for 'data' argument satisfies the predicate * Only allow calls when tetraplet for 'data' argument satisfies the predicate
*/ */
export const allowTetraplet = <T extends ArgName>( export const allowTetraplet = (
pred: (tetraplet: SecurityTetraplet) => boolean, pred: (tetraplet: SecurityTetraplet) => boolean,
): SecurityGuard<T> => { ): SecurityGuard => {
return (params) => { return (params) => {
const t = params.tetraplets["data"][0]; const t = params.tetraplets[0][0];
return pred(t); return pred(t);
}; };
}; };
@ -41,10 +41,10 @@ export const allowTetraplet = <T extends ArgName>(
/** /**
* Only allow data which comes from the specified serviceId and fnName * Only allow data which comes from the specified serviceId and fnName
*/ */
export const allowServiceFn = <T extends ArgName>( export const allowServiceFn = (
serviceId: string, serviceId: string,
fnName: string, fnName: string,
): SecurityGuard<T> => { ): SecurityGuard => {
return allowTetraplet((t) => { return allowTetraplet((t) => {
return t.service_id === serviceId && t.function_name === fnName; return t.service_id === serviceId && t.function_name === fnName;
}); });
@ -53,9 +53,7 @@ export const allowServiceFn = <T extends ArgName>(
/** /**
* Only allow data originated from the specified json_path * Only allow data originated from the specified json_path
*/ */
export const allowExactJsonPath = <T extends ArgName>( export const allowExactJsonPath = (jsonPath: string): SecurityGuard => {
jsonPath: string,
): SecurityGuard<T> => {
return allowTetraplet((t) => { return allowTetraplet((t) => {
return t.json_path === jsonPath; return t.json_path === jsonPath;
}); });
@ -64,9 +62,9 @@ export const allowExactJsonPath = <T extends ArgName>(
/** /**
* Only allow signing when particle is initiated at the specified peer * Only allow signing when particle is initiated at the specified peer
*/ */
export const allowOnlyParticleOriginatedAt = <T extends ArgName>( export const allowOnlyParticleOriginatedAt = (
peerId: PeerIdB58, peerId: PeerIdB58,
): SecurityGuard<T> => { ): SecurityGuard => {
return (params) => { return (params) => {
return params.initPeerId === peerId; return params.initPeerId === peerId;
}; };
@ -76,9 +74,7 @@ export const allowOnlyParticleOriginatedAt = <T extends ArgName>(
* Only allow signing when all of the predicates are satisfied. * Only allow signing when all of the predicates are satisfied.
* Useful for predicates reuse * Useful for predicates reuse
*/ */
export const and = <T extends ArgName>( export const and = (...predicates: SecurityGuard[]): SecurityGuard => {
...predicates: SecurityGuard<T>[]
): SecurityGuard<T> => {
return (params) => { return (params) => {
return predicates.every((x) => { return predicates.every((x) => {
return x(params); return x(params);
@ -90,9 +86,7 @@ export const and = <T extends ArgName>(
* Only allow signing when any of the predicates are satisfied. * Only allow signing when any of the predicates are satisfied.
* Useful for predicates reuse * Useful for predicates reuse
*/ */
export const or = <T extends ArgName>( export const or = (...predicates: SecurityGuard[]): SecurityGuard => {
...predicates: SecurityGuard<T>[]
): SecurityGuard<T> => {
return (params) => { return (params) => {
return predicates.some((x) => { return predicates.some((x) => {
return x(params); return x(params);

View File

@ -20,23 +20,24 @@ import { Path, Aqua } from "@fluencelabs/aqua-api/aqua-api.js";
import { import {
FunctionCallDef, FunctionCallDef,
JSONArray, JSONArray,
PassedArgs, JSONValue,
ServiceDef, ServiceDef,
} from "@fluencelabs/interfaces"; } from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { Subject, Subscribable } from "rxjs"; import { Subject, Subscribable } from "rxjs";
import { ClientPeer, makeClientPeerConfig } from "../clientPeer/ClientPeer.js"; import { ClientPeer, makeClientPeerConfig } from "../clientPeer/ClientPeer.js";
import { ClientConfig, RelayOptions } from "../clientPeer/types.js"; import { ClientConfig, RelayOptions } from "../clientPeer/types.js";
import { callAquaFunction } from "../compilerSupport/callFunction.js"; import { callAquaFunction } from "../compilerSupport/callFunction.js";
import { ServiceImpl } from "../compilerSupport/types.js";
import { IConnection } from "../connection/interfaces.js"; import { IConnection } from "../connection/interfaces.js";
import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js"; import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { CallServiceResultType } from "../jsServiceHost/interfaces.js"; import { CallServiceResultType } from "../jsServiceHost/interfaces.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js"; import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { WrapFnIntoServiceCall } from "../jsServiceHost/serviceUtils.js"; import { WrapFnIntoServiceCall } from "../jsServiceHost/serviceUtils.js";
import { KeyPair } from "../keypair/index.js"; import { KeyPair } from "../keypair/index.js";
import { WasmLoaderFromNpm } from "../marine/deps-loader/node.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js"; import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { WorkerLoader } from "../marine/worker-script/workerLoader.js";
import { Particle } from "../particle/Particle.js"; import { Particle } from "../particle/Particle.js";
export const registerHandlersHelper = ( export const registerHandlersHelper = (
@ -73,6 +74,16 @@ interface FunctionInfo {
funcDef: FunctionCallDef; funcDef: FunctionCallDef;
} }
/**
* Type for callback passed as aqua function argument
*/
export type ArgCallbackFunction = ServiceImpl[string];
/**
* Arguments passed to Aqua function
*/
export type PassedArgs = { [key: string]: JSONValue | ArgCallbackFunction };
export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => { export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
await fs.access(aquaFile); await fs.access(aquaFile);
@ -92,7 +103,6 @@ export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
.map(([name, fnInfo]: [string, FunctionInfo]) => { .map(([name, fnInfo]: [string, FunctionInfo]) => {
const callFn = (peer: FluencePeer, args: PassedArgs) => { const callFn = (peer: FluencePeer, args: PassedArgs) => {
return callAquaFunction({ return callAquaFunction({
def: fnInfo.funcDef,
script: fnInfo.script, script: fnInfo.script,
config: {}, config: {},
peer: peer, peer: peer,
@ -136,25 +146,60 @@ class NoopConnection implements IConnection {
export class TestPeer extends FluencePeer { export class TestPeer extends FluencePeer {
constructor(keyPair: KeyPair, connection: IConnection) { constructor(keyPair: KeyPair, connection: IConnection) {
const workerLoader = new WorkerLoader(); const jsHost = new JsServiceHost();
const controlModuleLoader = new WasmLoaderFromNpm( let marineJsWasm: ArrayBuffer;
"@fluencelabs/marine-js", let avmWasm: ArrayBuffer;
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
const marine = new MarineBackgroundRunner( const marine = new MarineBackgroundRunner(
workerLoader, {
controlModuleLoader, async getValue() {
avmModuleLoader, // TODO: load worker with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
); );
const jsHost = new JsServiceHost();
super(DEFAULT_CONFIG, keyPair, marine, jsHost, connection); super(DEFAULT_CONFIG, keyPair, marine, jsHost, connection);
} }
} }
@ -181,26 +226,63 @@ export const withClient = async (
config: ClientConfig, config: ClientConfig,
action: (client: ClientPeer) => Promise<void>, action: (client: ClientPeer) => Promise<void>,
) => { ) => {
const workerLoader = new WorkerLoader();
const controlModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/marine-js",
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm("@fluencelabs/avm", "avm.wasm");
const marine = new MarineBackgroundRunner(
workerLoader,
controlModuleLoader,
avmModuleLoader,
);
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig( const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(
relay, relay,
config, config,
); );
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
);
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine); const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine);
try { try {

View File

@ -24,7 +24,7 @@
"vitest": "0.34.6" "vitest": "0.34.6"
}, },
"dependencies": { "dependencies": {
"@fluencelabs/marine-js": "0.7.2", "@fluencelabs/marine-js": "0.8.0",
"observable-fns": "0.6.1", "observable-fns": "0.6.1",
"@fluencelabs/threads": "^2.0.0" "@fluencelabs/threads": "^2.0.0"
} }

View File

@ -28,7 +28,6 @@ import type {
} from "@fluencelabs/marine-js/dist/types"; } from "@fluencelabs/marine-js/dist/types";
import { import {
defaultCallParameters, defaultCallParameters,
JSONValue,
logLevelToEnv, logLevelToEnv,
} from "@fluencelabs/marine-js/dist/types"; } from "@fluencelabs/marine-js/dist/types";
import { expose } from "@fluencelabs/threads/worker"; import { expose } from "@fluencelabs/threads/worker";
@ -140,9 +139,7 @@ const toExpose = {
throw new Error(`service with id=${serviceId} not found`); throw new Error(`service with id=${serviceId} not found`);
} }
// TODO: Make MarineService return JSONValue type return srv.call(functionName, args, callParams);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return srv.call(functionName, args, callParams) as JSONValue;
}, },
onLogMessage() { onLogMessage() {

6765
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"include": ["**/src/**/*"] "include": ["packages"],
"exclude": ["node_modules", "dist", "build"]
} }