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
with:
js-client-snapshots: "${{ needs.js-client.outputs.js-client-snapshots }}"
nox-image: "fluencelabs/nox:unstable_minimal"
nox-image: "fluencelabs/nox:0.4.2"
flox:
needs:
- js-client

View File

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

View File

@ -2,11 +2,13 @@
.eslintcache
pnpm-lock.yaml
**/node_modules
**/dist
**/build
**/public
node_modules
dist
build
public
**/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) {
const check = (x, version) => {
if (version.includes("*")) {
if (version.includes("*") || version.includes("^")) {
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",
"scripts": {
"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",
"author": "Fluence Labs",
@ -20,10 +20,12 @@
"base64-js": "1.5.1"
},
"devDependencies": {
"@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
"@fluencelabs/aqua-lib": "0.6.0",
"@fluencelabs/cli": "0.7.2",
"@fluencelabs/js-client": "workspace:^",
"@fluencelabs/registry": "0.8.2",
"@fluencelabs/trust-graph": "3.1.2"
"@fluencelabs/aqua-to-js": "workspace:*",
"@fluencelabs/js-client": "workspace:*",
"@fluencelabs/registry": "0.8.8-1",
"@fluencelabs/trust-graph": "3.1.2",
"ts-node": "10.9.1"
}
}

View File

@ -2,71 +2,68 @@
// @ts-nocheck
/**
*
* This file is auto-generated. Do not edit manually: changes may be erased.
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* Aqua version: 0.12.0
* This file is generated using:
* @fluencelabs/aqua-api version: 0.12.4-main-cee4448-2196-1
* @fluencelabs/aqua-to-js version: 0.2.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$$,
CallParams as CallParams$$,
} from "@fluencelabs/js-client";
import {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$,
} 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 {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$
} from '@fluencelabs/js-client';
// Services
// Functions
export const test_script = `
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(xor
(call -relay- ("op" "noop") [])
(fail %last_error%)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 0])
)
)
`;
(xor
(seq
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(call -relay- ("op" "noop") [])
(fail :error:)
)
)
(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(
peer: IFluenceClient$$,
config?: { ttl?: number },
): Promise<void>;
export type TestResult = Promise<void>;
export function test(...args: any) {
return callFunction$$(
args,
{
functionName: "test",
arrow: {
tag: "arrow",
domain: {
tag: "labeledProduct",
fields: {},
export function test(...args: TestParams): TestResult {
return callFunction$$(
args,
{
"functionName": "test",
"arrow": {
"domain": {
"fields": {},
"tag": "labeledProduct"
},
codomain: {
tag: "nil",
"codomain": {
"tag": "nil"
},
},
names: {
relay: "-relay-",
getDataSrv: "getDataSrv",
callbackSrv: "callbackSrv",
responseSrv: "callbackSrv",
responseFnName: "response",
errorHandlingSrv: "errorHandlingSrv",
errorFnName: "error",
},
"tag": "arrow"
},
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...");
const marine = await marineTest(wasm);
console.log("marine test finished, result: ", marine);
console.log("running particle test...");
await particleTest();
console.log("marine test finished, result: ", marine);
await particleTest();
const returnVal = {
hello,

View File

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

View File

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

View File

@ -61,21 +61,16 @@ export const startContentServer = (
source: "/js-client.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",
destination: "/node_modules/@fluencelabs/:name/dist/:asset",
destination:
"/node_modules/@fluencelabs/js-client-isomorphic/node_modules/@fluencelabs/:name/dist/:asset",
},
{
source:
"/@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: [

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"
},
"devDependencies": {
"@fluencelabs/aqua-api": "0.12.0",
"@fluencelabs/aqua-api": "0.12.4-main-cee4448-2196-1",
"@fluencelabs/aqua-lib": "0.7.3",
"@fluencelabs/interfaces": "workspace:*",
"@fluencelabs/js-client": "workspace:^",
"@fluencelabs/registry": "0.8.7",
"@fluencelabs/spell": "0.5.20",
"@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.
*/
import { ArrowWithoutCallbacks, NonArrowType } from "@fluencelabs/interfaces";
import { ArrowType, NonArrowType } from "@fluencelabs/interfaces";
import { match, P } from "ts-pattern";
import { getFuncArgs } from "./utils.js";
export function genTypeName(
t: NonArrowType | ArrowWithoutCallbacks,
t: NonArrowType | ArrowType,
name: string,
): readonly [string | undefined, string] {
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)
.with({ tag: "nil" }, () => {
return "null";
@ -120,16 +120,7 @@ export function typeToTs(t: NonArrowType | ArrowWithoutCallbacks): string {
return [name, typeToTs(type)];
});
const generic =
args.length === 0
? "null"
: args
.map(([name]) => {
return `'${name}'`;
})
.join(" | ");
args.push(["callParams", `CallParams$$<${generic}>`]);
args.push(["callParams", `ParticleContext$$`]);
const funcArgs = args
.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.
*/
import url from "url";
import { fileURLToPath } from "url";
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 { generateTypes, generateSources } from "../index.js";
import { CompilationResult } from "../interfaces.js";
let res: Omit<CompilationResult, "funcCall">;
let pkg: PackageJson;
describe("Aqua to js/ts compiler", () => {
it("compiles smoke tests successfully", async () => {
const res = await compileFromPath({
filePath: url.fileURLToPath(
beforeAll(async () => {
res = await compileFromPath({
filePath: fileURLToPath(
new URL("./sources/smoke_test.aqua", import.meta.url),
),
imports: ["./node_modules"],
targetType: "air",
});
const pkg: PackageJson = {
pkg = {
...(await getPackageJsonContent()),
version: "0.0.0",
devDependencies: {
"@fluencelabs/aqua-api": "0.0.0",
},
};
});
// TODO: see https://github.com/fluencelabs/js-client/pull/366#discussion_r1370567711
// @ts-expect-error don't use compileFromPath directly here
it("matches js snapshots", async () => {
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);
expect(jsResult).toMatchSnapshot();
expect(jsTypes).toMatchSnapshot();
await expect(jsResult).toMatchFileSnapshot(
"./__snapshots__/generate.snap.js",
);
// TODO: see https://github.com/fluencelabs/js-client/pull/366#discussion_r1370567711
// @ts-expect-error don't use compileFromPath directly here
await expect(jsTypes).toMatchFileSnapshot(
"./__snapshots__/generate.snap.d.ts",
);
});
it("matches ts snapshots", async () => {
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.
*/
import { recursiveRenameLaquaProps } from "../utils.js";
import { capitalize, recursiveRenameLaquaProps } from "../utils.js";
import { AquaFunction, TypeGenerator } from "./interfaces.js";
@ -40,8 +40,11 @@ ${func.script}\`;
${typeGenerator.funcType(func)}
export function ${func.funcDef.functionName}(${typeGenerator.type(
"...args",
"any[]",
)}) {
`${capitalize(func.funcDef.functionName)}Params`,
)})${typeGenerator.type(
"",
`${capitalize(func.funcDef.functionName)}Result`,
)} {
return callFunction$$(
args,
${JSON.stringify(recursiveRenameLaquaProps(funcDef), null, 4)},

View File

@ -35,12 +35,13 @@ export default function generateHeader(
*/
${
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 {
v5_callFunction as callFunction$$,
v5_registerService as registerService$$,
v5_registerService as registerService$$
} from '@fluencelabs/js-client';`;
}

View File

@ -20,6 +20,8 @@ import { genTypeName, typeToTs } from "../common.js";
import { CLIENT } from "../constants.js";
import { capitalize, getFuncArgs } from "../utils.js";
import { DefaultServiceId } from "./service.js";
export interface TypeGenerator {
type(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}`]);
const argsDefs = args.map(([, def]) => {
return " " + def;
return def;
});
const argsDesc = args
@ -66,28 +68,30 @@ export class TSTypeGenerator implements TypeGenerator {
});
const functionOverloads = [
argsDefs.join(",\n"),
[` peer: ${CLIENT}`, ...argsDefs].join(",\n"),
argsDefs.join(", "),
[`peer: ${CLIENT}`, ...argsDefs].join(", "),
];
const [resTypeDesc, resType] = genTypeName(
funcDef.arrow.codomain,
capitalize(funcDef.functionName) + "Result",
capitalize(funcDef.functionName) + "ResultType",
);
const functionOverloadArgsType = functionOverloads
.map((overload) => {
return `[${overload}]`;
})
.join(" | ");
return [
argsDesc.join("\n"),
resTypeDesc ?? "",
functionOverloads
.flatMap((fo) => {
return [
`export function ${funcDef.functionName}(`,
fo,
`): Promise<${resType}>;`,
"",
];
})
.join("\n"),
`export type ${capitalize(
funcDef.functionName,
)}Params = ${functionOverloadArgsType};`,
`export type ${capitalize(
funcDef.functionName,
)}Result = Promise<${resType}>;\n`,
]
.filter((s) => {
return s !== "";
@ -117,13 +121,25 @@ export class TSTypeGenerator implements TypeGenerator {
const serviceDecl = `service: ${srvName}Def`;
const serviceIdDecl = `serviceId: string`;
const registerServiceArgs = [
const functionOverloadsWithDefaultServiceId = [
[serviceDecl],
[serviceIdDecl, serviceDecl],
[peerDecl, serviceDecl],
[peerDecl, serviceIdDecl, serviceDecl],
];
const functionOverloadsWithoutDefaultServiceId = [
[serviceIdDecl, serviceDecl],
[peerDecl, serviceIdDecl, serviceDecl],
];
const registerServiceArgs =
// This wrong type comes from aqua team. We need to discuss fix with them
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(srvDef.defaultServiceId as DefaultServiceId).s_Some__f_value != null
? functionOverloadsWithDefaultServiceId
: functionOverloadsWithoutDefaultServiceId;
return [
interfaces,
...registerServiceArgs.map((registerServiceArg) => {

View File

@ -20,7 +20,8 @@ import { recursiveRenameLaquaProps } from "../utils.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;
}

View File

@ -15,7 +15,7 @@
*/
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";
interface JsOutput {
@ -33,6 +33,7 @@ type LanguageOutput = {
};
type NothingToGenerate = null;
type OutputType = "js" | "ts";
export default async function aquaToJs<T extends OutputType>(
res: CompilationResult,
@ -52,8 +53,7 @@ export default async function aquaToJs<T extends OutputType>(
sources: generateSources(res, "js", packageJson),
types: generateTypes(res, packageJson),
}
: // TODO: probably there is a way to remove this type assert
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
: // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
({
sources: generateSources(res, "ts", packageJson),
} as LanguageOutput[T]);

View File

@ -16,7 +16,7 @@
import assert from "assert";
import { readFile } from "fs/promises";
import path from "path";
import { join } from "path";
import {
ArrowType,
@ -27,24 +27,26 @@ import {
SimpleTypes,
UnlabeledProductType,
} from "@fluencelabs/interfaces";
import { z } from "zod";
export interface PackageJson {
name: string;
version: string;
devDependencies: {
["@fluencelabs/aqua-api"]: string;
};
}
const packageJsonSchema = z.object({
name: z.string(),
version: z.string(),
devDependencies: z.object({
// @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> {
const content = await readFile(
new URL(path.join("..", "package.json"), import.meta.url),
new URL(join("..", "package.json"), import.meta.url),
"utf-8",
);
// TODO: Add validation here
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return JSON.parse(content) as PackageJson;
return packageJsonSchema.parse(JSON.parse(content));
}
export function getFuncArgs(

View File

@ -14,60 +14,11 @@
* 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).
*/
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 =
| string
| number

View File

@ -25,6 +25,11 @@ export type SimpleTypes =
export type NonArrowType = SimpleTypes | ProductType;
export type NonArrowSimpleType =
| SimpleTypes
| UnlabeledProductType
| LabeledProductType<SimpleTypes>;
export type TopType = {
/**
* Type descriptor. Used for pattern-matching
@ -154,7 +159,13 @@ export type ProductType = UnlabeledProductType | LabeledProductType;
* ArrowType is a profunctor pointing its domain to 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
*/
@ -174,14 +185,14 @@ export type ArrowType<T extends LabeledProductType | UnlabeledProductType> = {
/**
* Arrow which domain contains only non-arrow types
*/
export type ArrowWithoutCallbacks = ArrowType<
UnlabeledProductType | LabeledProductType<SimpleTypes>
>;
export type ArrowWithoutCallbacks = ArrowType<UnlabeledProductType>;
/**
* 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 {
/**
@ -232,9 +243,7 @@ export interface FunctionCallDef {
/**
* Underlying arrow which represents function in aqua
*/
arrow: ArrowType<
LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
>;
arrow: ArrowWithCallbacks;
/**
* 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
*/
functions:
| LabeledProductType<ArrowType<LabeledProductType<SimpleTypes>>>
| LabeledProductType<
ArrowType<LabeledProductType<SimpleTypes> | UnlabeledProductType>
>
| 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/compilerSupportInterface.js";
export * from "./commonTypes.js";
export * from "./future.js";

View File

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

View File

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

View File

@ -18,21 +18,14 @@ import { readFile } from "fs/promises";
import { createRequire } from "module";
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 assetPath path of required asset in given package
* @param root CDN domain in browser or js-client itself in node
*/
export async function fetchResource(
pkg: FetchedPackages,
assetPath: string,
root: string,
) {
export const fetchResource: FetchResourceFn = async (pkg, assetPath) => {
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 packagePathIndex = require.resolve(name);
@ -47,7 +40,7 @@ export async function fetchResource(
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);
@ -60,4 +53,4 @@ export async function fetchResource(
: "application/text",
},
});
}
};

View File

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

View File

@ -20,10 +20,10 @@ import { fileURLToPath } from "url";
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";
export const getWorker: GetWorker = (pkg: FetchedPackages) => {
export const getWorker: GetWorkerFn = (pkg: FetchedPackages) => {
const require = createRequire(import.meta.url);
const pathToThisFile = dirname(fileURLToPath(import.meta.url));

View File

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

View File

@ -15,182 +15,201 @@
*/
import type {
FnConfig,
ArrowWithoutCallbacks,
FunctionCallDef,
JSONValue,
ServiceDef,
PassedArgs,
ServiceImpl,
SimpleTypes,
} 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 { callAquaFunction, Fluence, registerService } from "./index.js";
export const isFluencePeer = (
fluencePeerCandidate: unknown,
): fluencePeerCandidate is FluencePeer => {
return fluencePeerCandidate instanceof FluencePeer;
};
function validateAquaConfig(
config: unknown,
): asserts config is CallAquaFunctionConfig | undefined {
z.union([
z.object({
ttl: z.number().optional(),
}),
z.undefined(),
]).parse(config);
}
/**
* 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
*
* @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 script - air script with function execution logic generated by the Aqua compiler
*/
export const v5_callFunction = async (
rawFnArgs: unknown[],
args: [
client: FluencePeer | (JSONValue | ServiceImpl[string]),
...args: (JSONValue | ServiceImpl[string])[],
],
def: FunctionCallDef,
script: string,
): Promise<unknown> => {
const { args, client: peer, config } = extractFunctionArgs(rawFnArgs, def);
): Promise<JSONValue> => {
const [peerOrArg, ...rest] = args;
return callAquaFunction({
args,
def,
if (!(peerOrArg instanceof FluencePeer)) {
return await v5_callFunction(
[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,
peer: peerOrArg,
args: callArgs,
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
* 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
* TODO: dont forget to add jsdoc for new arg
* @param def - service definition generated by the Aqua compiler
*/
export const v5_registerService = (args: unknown[], def: ServiceDef): void => {
// TODO: Support this in aqua-to-js package
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const service: ServiceImpl = args.pop() as ServiceImpl;
export const v5_registerService = (
args: RegisterServiceType,
def: ServiceDef,
): 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({
def,
service,
serviceId,
service: wrappedServiceImpl,
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 { it, describe, expect } from "vitest";
import { ExpirationError } from "../../jsPeer/errors.js";
import { CallServiceData } from "../../jsServiceHost/interfaces.js";
import { doNothing } from "../../jsServiceHost/serviceUtils.js";
import { handleTimeout } from "../../particle/Particle.js";
import { registerHandlersHelper, withClient } from "../../util/testUtils.js";
import { checkConnection } from "../checkConnection.js";
import { nodes, RELAY } from "./connection.js";
const ONE_SECOND = 1000;
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 () => {
await withClient(RELAY, {}, async (peer) => {
// 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!");
@ -124,7 +169,11 @@ describe("FluenceClient usage test suite", () => {
throw particle;
}
peer1.internals.initiateParticle(particle, doNothing);
peer1.internals.initiateParticle(
particle,
() => {},
() => {},
);
expect(await res).toEqual("test");
});
@ -172,13 +221,17 @@ describe("FluenceClient usage test suite", () => {
);
});
it("With connection options: defaultTTL", async () => {
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => {
const isConnected = await checkConnection(peer);
it(
"With connection options: defaultTTL",
async () => {
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeFalsy();
});
});
expect(isConnected).toBeFalsy();
});
},
ONE_SECOND,
);
});
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) => {
if (stage.stage === "sendingError") {
reject(stage.errorMessage);
}
});
peer.internals.initiateParticle(
particle,
() => {},
(error: Error) => {
reject(error);
},
);
});
await promise;
await expect(promise).rejects.toMatch(
"Particle is expected to be sent to only the single peer (relay which client is connected to)",
);

View File

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

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
import { z } from "zod";
/**
* Peer ID's id as a base58 string (multihash/CIDv0).
*/
@ -33,20 +35,30 @@ export type Node = {
* - string: multiaddr in string format
* - 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
*/
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
*/
export type KeyPairOptions = {
type: "Ed25519";
source: "random" | Uint8Array;
};
export type KeyPairOptions = z.infer<typeof keyPairOptionsSchema>;
/**
* Fluence JS Client connection states as string literals
@ -63,17 +75,10 @@ export const ConnectionStates = [
*/
export type ConnectionState = (typeof ConnectionStates)[number];
export interface IFluenceInternalApi {
/**
* Internal API
*/
internals: unknown;
}
/**
* Public API of Fluence JS Client
*/
export interface IFluenceClient extends IFluenceInternalApi {
export interface IFluenceClient {
/**
* Connect to the Fluence network
*/
@ -107,65 +112,66 @@ export interface IFluenceClient extends IFluenceInternalApi {
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
*/
export interface ClientConfig {
/**
* 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;
};
}
export type ClientConfig = z.infer<typeof configSchema>;

View File

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

View File

@ -14,18 +14,11 @@
* limitations under the License.
*/
import assert from "assert";
import {
FnConfig,
FunctionCallDef,
getArgumentTypes,
isReturnTypeVoid,
PassedArgs,
} from "@fluencelabs/interfaces";
import { JSONValue } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js";
import { ArgCallbackFunction } from "../util/testUtils.js";
import {
errorHandlingService,
@ -51,95 +44,48 @@ const log = logger("aqua");
* @returns
*/
type CallAquaFunctionArgs = {
def: FunctionCallDef;
export type CallAquaFunctionArgs = {
script: string;
config: FnConfig;
config: CallAquaFunctionConfig | undefined;
peer: FluencePeer;
args: PassedArgs;
args: { [key: string]: JSONValue | ArgCallbackFunction };
fireAndForget?: boolean | undefined;
};
export type CallAquaFunctionConfig = {
ttl?: number;
};
export const callAquaFunction = async ({
def,
script,
config,
config = {},
peer,
args,
}: CallAquaFunctionArgs) => {
// TODO: this function should be rewritten. We can remove asserts if we wont check definition there
log.trace("calling aqua function %j", { def, script, config, args });
const argumentTypes = getArgumentTypes(def);
log.trace("calling aqua function %j", { script, config, args });
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)) {
const type = argumentTypes[name];
let service: ServiceDescription;
if (type.tag === "arrow") {
// TODO: Add validation here
assert(
typeof argVal === "function",
"Should not be possible, bad types",
);
service = userHandlerService(
def.names.callbackSrv,
[name, type],
argVal,
);
if (typeof argVal === "function") {
service = userHandlerService("callbackSrv", name, argVal);
} else {
// TODO: Add validation here
assert(
typeof argVal !== "function",
"Should not be possible, bad types",
);
service = injectValueService(def.names.getDataSrv, name, type, argVal);
service = injectValueService("getDataSrv", name, argVal);
}
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(
peer,
particle,
errorHandlingService(def, reject),
);
registerParticleScopeService(peer, particle, errorHandlingService(reject));
peer.internals.initiateParticle(particle, (stage) => {
// 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})`,
);
}
});
peer.internals.initiateParticle(particle, resolve, reject);
});
};

View File

@ -14,222 +14,241 @@
* limitations under the License.
*/
// TODO: This file is a mess. Need to refactor it later
/* eslint-disable */
// @ts-nocheck
import assert from "assert";
import type {
import {
ArrowType,
ArrowWithoutCallbacks,
JSONArray,
JSONValue,
NonArrowType,
LabeledProductType,
NonArrowSimpleType,
ScalarType,
SimpleTypes,
UnlabeledProductType,
} from "@fluencelabs/interfaces";
import { match } from "ts-pattern";
import { CallServiceData } from "../jsServiceHost/interfaces.js";
import { jsonify } from "../util/utils.js";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
/**
* Convert value from its representation in aqua language to representation in typescript
* @param value - value as represented in aqua
* @param type - definition of the aqua type
* @returns value represented in typescript
*/
export const aqua2ts = (value: JSONValue, type: NonArrowType): JSONValue => {
const res = match(type)
.with({ tag: "nil" }, () => {
import { ServiceImpl } from "./types.js";
export class SchemaValidationError extends Error {
constructor(
public path: string[],
schema: NonArrowSimpleType | ArrowWithoutCallbacks,
expected: string,
provided: JSONValue | ServiceImpl[string],
) {
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;
})
.with({ tag: "option" }, (opt) => {
assert(Array.isArray(value), "Should not be possible, bad types");
} else {
return aqua2js(value[0], schema.type);
}
} 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 null;
} 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));
return value.map((y) => {
return aqua2js(y, schema.type);
});
} else if (schema.tag === "unlabeledProduct") {
if (!Array.isArray(value)) {
throw new SchemaValidationError([], schema, "array", value);
}
return res;
};
/**
* 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));
return value.map((y, i) => {
return aqua2js(y, schema.items[i]);
});
} 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) {
throw new Error(
`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`,
return Object.fromEntries(
Object.entries(schema.fields).map(([key, type]) => {
const val = aqua2js(value[key], type);
return [key, val];
}),
);
} else {
throw new SchemaValidationError([], schema, "never", value);
}
}
return req.args.map((arg, index) => {
return aqua2ts(arg, argTypes[index]);
});
};
export function js2aqua(
value: JSONValue,
schema: NonArrowSimpleType,
{ path }: ValidationContext,
): JSONValue {
if (schema.tag === "nil") {
if (value !== null) {
throw new SchemaValidationError(path, schema, "null", value);
}
/**
* Convert value from its typescript representation to representation in aqua
* @param value - the value as represented in typescript
* @param type - definition of the aqua type
* @returns value represented in aqua
*/
export const ts2aqua = (value: JSONValue, type: NonArrowType): JSONValue => {
const res = match(type)
.with({ tag: "nil" }, () => {
return null;
})
.with({ tag: "option" }, (opt) => {
if (value === null || value === undefined) {
return [];
} else {
return [ts2aqua(value, 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 ts2aqua(y, arr.type);
});
})
.with({ tag: "struct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: "labeledProduct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((type, index) => {
return ts2aqua(value[index], type);
});
})
// uncomment to check that every pattern in matched
// .exhaustive()
.otherwise(() => {
throw new Error("Unexpected tag: " + jsonify(type));
return value;
} else if (schema.tag === "option") {
// option means 'type | null'
return value == null ? [] : [js2aqua(value, schema.type, { path })];
} else if (schema.tag === "topType") {
// topType equals to 'any'
return value;
} else if (schema.tag === "bottomType") {
// bottomType equals to 'never'
throw new SchemaValidationError(path, schema, "never", value);
} else if (schema.tag === "scalar") {
return isScalar(schema, value, { path });
} else if (schema.tag === "array") {
if (!Array.isArray(value)) {
throw new SchemaValidationError(path, schema, "array", value);
}
return value.map((y, i) => {
return js2aqua(y, schema.type, { path: [...path, `[${i}]`] });
});
} else if (schema.tag === "unlabeledProduct") {
if (!Array.isArray(value)) {
throw new SchemaValidationError(path, schema, "array", value);
}
return value.map((y, i) => {
return js2aqua(y, schema.items[i], { path: [...path, `[${i}]`] });
});
} else if (["labeledProduct", "struct"].includes(schema.tag)) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new SchemaValidationError(path, schema, "object", value);
}
return Object.fromEntries(
Object.entries(schema.fields).map(([key, type]) => {
const val = js2aqua(value[key], type, { path: [...path, key] });
return [key, val];
}),
);
} else {
throw new SchemaValidationError(path, schema, "never", value);
}
}
// Wrapping function, converting its arguments to aqua before call and back to js after call.
// It makes callbacks and service functions defined by user operate on js types seamlessly
export const wrapJsFunction = (
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;
};
/**
* Convert return type of the service from it's typescript representation to representation in aqua
* @param returnValue - the value as represented in typescript
* @param arrowType - the arrow type which describes the service
* @returns - value represented in aqua
*/
export const returnType2Aqua = (
returnValue: any,
arrowType: ArrowType<NonArrowType>,
) => {
if (arrowType.codomain.tag === "nil") {
return {};
}
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();
const returnTypeVoid =
schema.codomain.tag === "nil" || schema.codomain.items.length === 0;
const resultSchema =
schema.codomain.tag === "unlabeledProduct" &&
schema.codomain.items.length === 1
? schema.codomain.items[0]
: schema.codomain;
let result = await func(...tsArgs, context);
if (returnTypeVoid) {
result = null;
}
return js2aqua(result, resultSchema, { path: [] });
};
};

View File

@ -14,66 +14,59 @@
* limitations under the License.
*/
import type { ServiceDef, ServiceImpl } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js";
import { registerGlobalService, userHandlerService } from "./services.js";
import { ServiceImpl } from "./types.js";
const log = logger("aqua");
interface RegisterServiceArgs {
peer: FluencePeer;
def: ServiceDef;
serviceId: string | undefined;
serviceId: string;
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 = ({
peer,
def,
serviceId = def.defaultServiceId,
serviceId,
service,
}: 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", { def, serviceId, service });
log.trace("registering aqua service %o", { serviceId, service });
// Checking for missing keys
const requiredKeys =
def.functions.tag === "nil" ? [] : Object.keys(def.functions.fields);
const serviceFunctions = findAllPossibleRegisteredServiceFunctions(service);
const incorrectServiceDefinitions = requiredKeys.filter((f) => {
return !(f in 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);
for (const serviceFunction of serviceFunctions) {
const handler = service[serviceFunction];
const userDefinedHandler = handler.bind(service);
const serviceDescription = userHandlerService(
serviceId,
singleFunction,
serviceFunction,
userDefinedHandler,
);

View File

@ -14,32 +14,18 @@
* limitations under the License.
*/
import { SecurityTetraplet } from "@fluencelabs/avm";
import {
CallParams,
ArrowWithoutCallbacks,
FunctionCallDef,
NonArrowType,
ServiceImpl,
JSONValue,
} from "@fluencelabs/interfaces";
import { fromUint8Array } from "js-base64";
import { match } from "ts-pattern";
import { JSONValue } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import {
CallServiceData,
GenericCallServiceHandler,
ParticleContext,
ResultCodes,
} from "../jsServiceHost/interfaces.js";
import { Particle } from "../particle/Particle.js";
import {
aquaArgs2Ts,
responseServiceValue2ts,
returnType2Aqua,
ts2aqua,
} from "./conversions.js";
import { ServiceImpl } from "./types.js";
export interface ServiceDescription {
serviceId: string;
@ -50,10 +36,10 @@ export interface ServiceDescription {
/**
* 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 {
serviceId: def.names.getDataSrv,
fnName: def.names.relay,
serviceId: "getDataSrv",
fnName: "-relay-",
handler: () => {
return {
retCode: ResultCodes.success,
@ -69,7 +55,6 @@ export const injectRelayService = (def: FunctionCallDef, peer: FluencePeer) => {
export const injectValueService = (
serviceId: string,
fnName: string,
valueType: NonArrowType,
value: JSONValue,
) => {
return {
@ -78,7 +63,7 @@ export const injectValueService = (
handler: () => {
return {
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
*/
export const responseService = (
def: FunctionCallDef,
resolveCallback: (val: JSONValue) => void,
) => {
export const responseService = (resolveCallback: (val: JSONValue) => void) => {
return {
serviceId: def.names.responseSrv,
fnName: def.names.responseFnName,
serviceId: "callbackSrv",
fnName: "response",
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(() => {
resolveCallback(userFunctionReturn);
@ -113,12 +100,11 @@ export const responseService = (
* Creates a service which is used to return errors from aqua function into typescript space
*/
export const errorHandlingService = (
def: FunctionCallDef,
rejectCallback: (err: JSONValue) => void,
) => {
return {
serviceId: def.names.errorHandlingSrv,
fnName: def.names.errorFnName,
serviceId: "errorHandlingSrv",
fnName: "error",
handler: (req: CallServiceData) => {
const [err] = req.args;
@ -139,21 +125,19 @@ export const errorHandlingService = (
*/
export const userHandlerService = (
serviceId: string,
arrowType: [string, ArrowWithoutCallbacks],
fnName: string,
userHandler: ServiceImpl[string],
) => {
const [fnName, type] = arrowType;
return {
serviceId,
fnName,
handler: async (req: CallServiceData) => {
const args: [...JSONValue[], CallParams<string>] = [
...aquaArgs2Ts(req, type),
extractCallParams(req, type),
const args: [...JSONValue[], ParticleContext] = [
...req.args,
req.particleContext,
];
const rawResult = await userHandler.bind(null)(...args);
const result = returnType2Aqua(rawResult, type);
const result = await userHandler.bind(null)(...args);
return {
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 = (
peer: FluencePeer,
particle: Particle,

View File

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

View File

@ -17,7 +17,7 @@
import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux";
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 { peerIdFromString } from "@libp2p/peer-id";
import { webSockets } from "@libp2p/websockets";

View File

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

View File

@ -15,13 +15,13 @@
*/
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 { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { KeyPair } from "../keypair/index.js";
import { WasmLoaderFromNpm } from "../marine/deps-loader/node.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { WorkerLoader } from "../marine/worker-script/workerLoader.js";
import { EphemeralNetwork } from "./network.js";
@ -35,25 +35,60 @@ export class EphemeralNetworkClient extends FluencePeer {
network: EphemeralNetwork,
relay: PeerIdB58,
) {
const workerLoader = new WorkerLoader();
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
const controlModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/marine-js",
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
workerLoader,
controlModuleLoader,
avmModuleLoader,
{
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 conn = network.getRelayConnection(keyPair.getPeerId(), relay);
super(config, keyPair, marine, new JsServiceHost(), conn);
}
}

View File

@ -15,16 +15,14 @@
*/
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 { IConnection } from "../connection/interfaces.js";
import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { fromBase64Sk, KeyPair } from "../keypair/index.js";
import {
WorkerLoaderFromFs,
WasmLoaderFromNpm,
} from "../marine/deps-loader/node.js";
import { IMarineHost } from "../marine/interfaces.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { Particle } from "../particle/Particle.js";
@ -224,24 +222,7 @@ class EphemeralPeer extends FluencePeer {
export class EphemeralNetwork {
private peers: Map<PeerIdB58, EphemeralPeer> = new Map();
workerLoader: WorkerLoaderFromFs;
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",
);
}
constructor(readonly config: EphemeralConfig) {}
/**
* Starts the Ephemeral network up
@ -252,10 +233,54 @@ export class EphemeralNetwork {
const promises = this.config.peers.map(async (x) => {
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(
this.workerLoader,
this.controlModuleLoader,
this.avmModuleLoader,
{
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;
},
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();

View File

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

View File

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

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");
@ -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"]);
@ -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");
@ -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");

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"]));

View File

@ -16,36 +16,11 @@
import { it, describe, expect } from "vitest";
import { isFluencePeer } from "../../api.js";
import { handleTimeout } from "../../particle/Particle.js";
import {
mkTestPeer,
registerHandlersHelper,
withPeer,
} from "../../util/testUtils.js";
import { registerHandlersHelper, withPeer } from "../../util/testUtils.js";
import { FluencePeer } from "../FluencePeer.js";
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 () {
await withPeer(async (peer) => {
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");
@ -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);
@ -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({
@ -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
*/
export interface ParticleContext {
export type ParticleContext = {
/**
* The identifier of particle which triggered the call
*/
@ -104,7 +104,12 @@ export interface ParticleContext {
* Particle's signature
*/
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

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { SecurityTetraplet } from "@fluencelabs/avm";
import { JSONArray } from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
@ -28,10 +29,6 @@ import {
ResultCodes,
} from "./interfaces.js";
export const doNothing = () => {
return undefined;
};
export const WrapFnIntoServiceCall = (
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 {
particleId: particle.id,
initPeerId: particle.initPeerId,
timestamp: particle.timestamp,
ttl: particle.ttl,
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.
*/
import {
CallResultsArray,
InterpreterResult,
RunParameters,
} from "@fluencelabs/avm";
import { JSONObject, JSONValue, JSONArray } from "@fluencelabs/interfaces";
import { CallParameters } from "@fluencelabs/marine-worker";
import type { Worker as WorkerImplementation } from "@fluencelabs/threads/master";
@ -58,22 +53,6 @@ export interface IMarineHost extends IStartable {
): 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
*/
@ -94,32 +73,3 @@ export interface IWasmLoader
export interface IWorkerLoader
extends IValueLoader<WorkerImplementation | Promise<WorkerImplementation>>,
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 { JSONValue } from "@fluencelabs/interfaces";
import { fromUint8Array, toUint8Array } from "js-base64";
import { concat } from "uint8arrays/concat";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { ExpirationError } from "../jsPeer/errors.js";
import { KeyPair } from "../keypair/index.js";
import { numberToLittleEndianBytes } from "../util/bytes.js";
@ -183,15 +185,16 @@ export type ParticleExecutionStage =
export interface ParticleQueueItem {
particle: IParticle;
callResults: CallResultsArray;
onStageChange: (state: ParticleExecutionStage) => void;
onSuccess: (result: JSONValue) => void;
onError: (error: Error) => void;
}
/**
* Helper function to handle particle at expired stage
*/
export const handleTimeout = (fn: () => void) => {
return (stage: ParticleExecutionStage) => {
if (stage.stage === "expired") {
return (error: Error) => {
if (error instanceof ExpirationError) {
fn();
}
};

View File

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

View File

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

View File

@ -16,13 +16,12 @@
import { Buffer } from "buffer";
import { CallParams } from "@fluencelabs/interfaces";
import { v4 as uuidv4 } from "uuid";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { getErrorMessage } from "../util/utils.js";
import { SrvDef } from "./_aqua/single-module-srv.js";
import {
allowOnlyParticleOriginatedAt,
SecurityGuard,
@ -32,7 +31,8 @@ export const defaultGuard = (peer: FluencePeer) => {
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();
constructor(private peer: FluencePeer) {
@ -40,16 +40,13 @@ export class Srv implements SrvDef {
this.securityGuard_remove = defaultGuard(this.peer);
}
securityGuard_create: SecurityGuard<"wasm_b64_content">;
securityGuard_create: SecurityGuard;
async create(
wasm_b64_content: string,
callParams: CallParams<"wasm_b64_content">,
) {
async create(wasm_b64_content: string, callParams: ParticleContext) {
if (!this.securityGuard_create(callParams)) {
return {
success: false,
error: "Security guard validation failed",
error: ["Marine services could be registered on %init_peer_id% only"],
service_id: null,
};
}
@ -66,25 +63,25 @@ export class Srv implements SrvDef {
return {
success: true,
service_id: newServiceId,
service_id: [newServiceId],
error: null,
};
} catch (err: unknown) {
return {
success: true,
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)) {
return {
success: false,
error: "Security guard validation failed",
error: ["Marine services could be remove on %init_peer_id% only"],
service_id: null,
};
}
@ -92,7 +89,7 @@ export class Srv implements SrvDef {
if (!this.services.has(service_id)) {
return {
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.
*/
import { CallParams } from "@fluencelabs/interfaces";
import { ParticleContext } from "../jsServiceHost/interfaces.js";
import { TracingDef } from "./_aqua/tracing.js";
@ -22,7 +22,7 @@ export class Tracing implements TracingDef {
tracingEvent(
arrowName: string,
event: string,
callParams: CallParams<"arrowName" | "event">,
callParams: ParticleContext,
): void {
// This console log is intentional
// eslint-disable-next-line no-console

View File

@ -16,11 +16,14 @@
import assert from "assert";
import { CallParams, JSONArray } from "@fluencelabs/interfaces";
import { JSONArray } from "@fluencelabs/interfaces";
import { toUint8Array } from "js-base64";
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 { builtInServices } from "../builtins.js";
import { allowServiceFn } from "../securityGuard.js";
@ -51,32 +54,32 @@ describe("Tests for default handler", () => {
serviceId | fnName | args | retCode | result
${"op"} | ${"identity"} | ${[]} | ${0} | ${{}}
${"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"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]}
${"op"} | ${"array_length"} | ${[[1, 2, 3]]} | ${0} | ${3}
${"op"} | ${"array_length"} | ${[]} | ${1} | ${"array_length accepts exactly one argument, found: 0"}
${"op"} | ${"array_length"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"}
${"op"} | ${"concat"} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]}
${"op"} | ${"concat"} | ${[[1, 2]]} | ${0} | ${[1, 2]}
${"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", 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", 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], 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", 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!", true]} | ${1} | ${"sha256_string accepts 1 argument, found: 2"}
${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"sha256_string accepts 1 argument, found: 0"}
${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"Expected 1 argument(s). Got 2"}
${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"Expected 1 argument(s). Got 0"}
${"op"} | ${"concat_strings"} | ${[]} | ${0} | ${""}
${"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"} | ${[]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"}
${"peer"} | ${"timeout"} | ${[200, "test", 1]} | ${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} | ${"Expected 2 argument(s). Got 3"}
${"debug"} | ${"stringify"} | ${[]} | ${0} | ${'"<empty argument list>"'}
${"debug"} | ${"stringify"} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20}
${"debug"} | ${"stringify"} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour}
@ -149,6 +152,7 @@ describe("Tests for default handler", () => {
timestamp: 595951200,
ttl: 595961200,
signature: new Uint8Array([]),
tetraplets: [],
},
};
@ -185,6 +189,7 @@ describe("Tests for default handler", () => {
timestamp: 595951200,
ttl: 595961200,
signature: new Uint8Array([]),
tetraplets: [],
},
};
@ -243,14 +248,15 @@ const makeTestTetraplet = (
initPeerId: string,
serviceId: string,
fnName: string,
): CallParams<"data"> => {
): ParticleContext => {
return {
particleId: "",
timestamp: 0,
ttl: 0,
initPeerId: initPeerId,
tetraplets: {
data: [
signature: new Uint8Array([]),
tetraplets: [
[
{
peer_pk: initPeerId,
function_name: fnName,
@ -258,7 +264,7 @@ const makeTestTetraplet = (
json_path: "",
},
],
},
],
};
};
@ -273,7 +279,7 @@ describe("Sig service tests", () => {
);
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 () => {
@ -305,7 +311,7 @@ describe("Sig service tests", () => {
expect(signature.success).toBe(true);
assert(signature.success);
const res = await sig.verify(signature.signature, testData);
const res = await sig.verify(signature.signature[0], testData);
expect(res).toBe(true);
});
@ -334,7 +340,7 @@ describe("Sig service tests", () => {
);
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 () => {
@ -352,7 +358,7 @@ describe("Sig service tests", () => {
);
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 () => {

View File

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

View File

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

View File

@ -14,90 +14,21 @@
* 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 { FluencePeer } from "../../jsPeer/FluencePeer.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(
peer: FluencePeer,
serviceId: string,
service: NodeUtils,
) {
const nodeUtilsService: Record<never, unknown> = service;
registerService({
peer,
// TODO: fix this after changing registerService signature
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
service: nodeUtilsService,
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.
*/
/**
* 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 { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ParticleContext } from "../../jsServiceHost/interfaces.js";
import { Sig } from "../Sig.js";
// Services
export interface SigDef {
get_peer_id: (callParams: CallParams<null>) => string | Promise<string>;
get_peer_id: (callParams: ParticleContext) => string | Promise<string>;
sign: (
data: number[],
callParams: CallParams<"data">,
callParams: ParticleContext,
) =>
| { error: string | null; signature: number[] | null; success: boolean }
| { error: [string?]; signature: [number[]?]; success: boolean }
| Promise<{
error: string | null;
signature: number[] | null;
error: [string?];
signature: [number[]?];
success: boolean;
}>;
verify: (
signature: number[],
data: number[],
callParams: CallParams<"signature" | "data">,
callParams: ParticleContext,
) => boolean | Promise<boolean>;
}
@ -49,113 +45,12 @@ export function registerSig(
serviceId: string,
service: Sig,
) {
const sigService: Record<never, unknown> = service;
registerService({
peer,
// TODO: fix this after changing registerService signature
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
service: service as unknown as ServiceImpl,
service: sigService,
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.
*/
/**
* 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 { FluencePeer } from "../../jsPeer/FluencePeer.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(
peer: FluencePeer,
serviceId: string,
service: Srv,
) {
const singleModuleService: Record<never, unknown> = service;
registerService({
peer,
serviceId,
// TODO: fix this after changing registerService signature
// 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",
},
},
},
],
},
},
},
},
},
service: singleModuleService,
});
}

View File

@ -17,10 +17,10 @@
/**
* 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 { FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ParticleContext } from "../../jsServiceHost/interfaces.js";
import { Tracing } from "../Tracing.js";
// Services
@ -29,7 +29,7 @@ export interface TracingDef {
tracingEvent: (
arrowName: string,
event: string,
callParams: CallParams<"arrowName" | "event">,
callParams: ParticleContext,
) => void | Promise<void>;
}
@ -38,40 +38,11 @@ export function registerTracing(
serviceId: string,
service: Tracing,
) {
const tracingService: Record<never, unknown> = service;
registerService({
peer,
serviceId,
// TODO: fix this after changing registerService signature
// 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",
},
},
},
},
},
service: tracingService,
});
}
// Functions

View File

@ -14,74 +14,144 @@
* limitations under the License.
*/
import assert from "assert";
import { Buffer } from "buffer";
import { JSONValue } from "@fluencelabs/interfaces";
import bs58 from "bs58";
import { sha256 } from "multiformats/hashes/sha2";
import { z } from "zod";
import {
CallServiceData,
CallServiceResult,
CallServiceResultType,
GenericCallServiceHandler,
ResultCodes,
} from "../jsServiceHost/interfaces.js";
import { getErrorMessage, isString, jsonify } from "../util/utils.js";
import { getErrorMessage, jsonify } from "../util/utils.js";
const success = (
// TODO: Remove unknown after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
result: CallServiceResultType | unknown,
): CallServiceResult => {
const success = (result: CallServiceResultType): CallServiceResult => {
return {
// TODO: Remove type assertion after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
result: result as CallServiceResultType,
result,
retCode: ResultCodes.success,
};
};
const error = (
// TODO: Remove unknown after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
error: CallServiceResultType | unknown,
): CallServiceResult => {
const error = (error: CallServiceResultType): CallServiceResult => {
return {
// TODO: Remove type assertion after adding validation to builtin inputs
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
result: error as CallServiceResultType,
result: 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) => {
return error(
`The JS implementation of Peer does not support "${methodName}"`,
);
};
const makeJsonImpl = (args: [Record<string, JSONValue>, ...JSONValue[]]) => {
const [obj, ...kvs] = args;
const parseWithSchema = <T extends z.ZodTypeAny>(
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++) {
const k = kvs[i * 2];
if (issue.code === z.ZodIssueCode.too_small) {
return {
message: `Expected ${
issue.minimum
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} argument(s). Got ${ctx.data.length}`,
};
}
if (!isString(k)) {
return error(`Argument ${i * 2 + 1} is expected to be string`);
}
if (issue.code === z.ZodIssueCode.invalid_union) {
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];
toMerge[k] = v;
return { message: ctx.defaultError };
},
});
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<
string,
Record<string, GenericCallServiceHandler>
@ -116,29 +186,16 @@ export const builtInServices: Record<
return errorNotImpl("peer.get_contact");
},
timeout: (req) => {
if (req.args.length !== 2) {
return error(
"timeout accepts exactly two arguments: timeout duration in ms and a message string",
);
}
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);
});
},
timeout: withSchema(z.tuple([z.number(), z.string()]))(
([durationMs, msg]) => {
return new Promise((resolve) => {
setTimeout(() => {
const res = success(msg);
resolve(res);
}, durationMs);
});
},
),
},
kad: {
@ -246,120 +303,48 @@ export const builtInServices: Record<
return success(req.args);
},
array_length: (req) => {
if (req.args.length !== 1) {
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);
}
},
array_length: withSchema(z.tuple([z.array(z.unknown())]))(([arr]) => {
return success(arr.length);
}),
identity: (req) => {
if (req.args.length > 1) {
return error(
`identity accepts up to 1 arguments, received ${req.args.length} arguments`,
);
} else {
return success(req.args.length === 0 ? {} : req.args[0]);
}
},
identity: withSchema(z.array(jsonSchema).max(1))((args) => {
return success(args.length === 0 ? {} : args[0]);
}),
concat: (req) => {
const incorrectArgIndices = req.args //
.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
concat: withSchema(z.array(z.array(z.unknown())))((args) => {
// Schema is used with unknown type to prevent useless runtime check
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = "".concat(...(req.args as string[]));
return success(res);
},
const arr = args as never[][];
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: {
@ -379,365 +364,187 @@ export const builtInServices: Record<
},
math: {
add: (req) => {
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");
add: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x + y);
},
}),
sub: (req) => {
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");
sub: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x - y);
},
}),
mul: (req) => {
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");
mul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x * y);
},
}),
fmul: (req) => {
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");
fmul: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(Math.floor(x * y));
},
}),
div: (req) => {
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");
div: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(Math.floor(x / y));
},
}),
rem: (req) => {
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");
rem: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x % y);
},
}),
pow: (req) => {
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");
pow: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(Math.pow(x, y));
},
}),
log: (req) => {
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");
log: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(Math.log(y) / Math.log(x));
},
}),
},
cmp: {
gt: (req) => {
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");
gt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x > y);
},
}),
gte: (req) => {
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");
gte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x >= y);
},
}),
lt: (req) => {
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");
lt: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x < y);
},
}),
lte: (req) => {
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");
lte: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x <= y);
},
}),
cmp: (req) => {
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");
cmp: withSchema(z.tuple([z.number(), z.number()]))(([x, y]) => {
return success(x === y ? 0 : x > y ? 1 : -1);
},
}),
},
array: {
sum: (req) => {
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[]];
sum: withSchema(z.tuple([z.array(z.number())]))(([xs]) => {
return success(
xs.reduce((agg, cur) => {
return agg + cur;
}, 0),
);
},
}),
dedup: (req) => {
let err;
if ((err = checkForArgumentsCount(req, 1)) != null) {
return err;
}
const [xs] = req.args;
// TODO: Remove after adding validation
assert(Array.isArray(xs));
dedup: withSchema(z.tuple([z.array(z.any())]))(([xs]) => {
const set = new Set(xs);
return success(Array.from(set));
},
}),
intersect: (req) => {
let err;
intersect: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
([xs, ys]) => {
const intersection = xs.filter((x) => {
return ys.includes(x);
});
if ((err = checkForArgumentsCount(req, 2)) != null) {
return err;
}
return success(intersection);
},
),
const [xs, ys] = req.args;
// TODO: Remove after adding validation
assert(Array.isArray(xs) && Array.isArray(ys));
diff: withSchema(z.tuple([z.array(z.any()), z.array(z.any())]))(
([xs, ys]) => {
const diff = xs.filter((x) => {
return !ys.includes(x);
});
const intersection = xs.filter((x) => {
return ys.includes(x);
});
return success(diff);
},
),
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) => {
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);
},
return success(sdiff);
},
),
},
json: {
obj: (req) => {
let err;
obj: withSchema(
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) {
return err;
}
put: withSchema(
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
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return makeJsonImpl([{}, ...req.args] as [
Record<string, JSONValue>,
...JSONValue[],
]);
},
puts: withSchema(
z
.array(z.unknown())
.refine(
(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) => {
let err;
if ((err = checkForArgumentsCount(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[]],
);
},
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;
stringify: withSchema(z.tuple([z.record(z.string(), jsonSchema)]))(
([json]) => {
const res = JSON.stringify(json);
return success(res);
},
),
parse: withSchema(z.tuple([z.string()]))(([raw]) => {
try {
// TODO: Remove after adding validation
assert(typeof raw === "string");
const json = JSON.parse(raw);
// Parsing any argument here yields JSONValue
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const json = JSON.parse(raw) as JSONValue;
return success(json);
} catch (err: unknown) {
return error(getErrorMessage(err));
}
},
}),
},
"run-console": {
@ -749,59 +556,3 @@ export const builtInServices: Record<
},
},
} 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 { 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
*/
export type SecurityGuard<T extends ArgName> = (
params: CallParams<T>,
) => boolean;
export type SecurityGuard = (params: ParticleContext) => boolean;
/**
* Only allow calls when tetraplet for 'data' argument satisfies the predicate
*/
export const allowTetraplet = <T extends ArgName>(
export const allowTetraplet = (
pred: (tetraplet: SecurityTetraplet) => boolean,
): SecurityGuard<T> => {
): SecurityGuard => {
return (params) => {
const t = params.tetraplets["data"][0];
const t = params.tetraplets[0][0];
return pred(t);
};
};
@ -41,10 +41,10 @@ export const allowTetraplet = <T extends ArgName>(
/**
* Only allow data which comes from the specified serviceId and fnName
*/
export const allowServiceFn = <T extends ArgName>(
export const allowServiceFn = (
serviceId: string,
fnName: string,
): SecurityGuard<T> => {
): SecurityGuard => {
return allowTetraplet((t) => {
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
*/
export const allowExactJsonPath = <T extends ArgName>(
jsonPath: string,
): SecurityGuard<T> => {
export const allowExactJsonPath = (jsonPath: string): SecurityGuard => {
return allowTetraplet((t) => {
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
*/
export const allowOnlyParticleOriginatedAt = <T extends ArgName>(
export const allowOnlyParticleOriginatedAt = (
peerId: PeerIdB58,
): SecurityGuard<T> => {
): SecurityGuard => {
return (params) => {
return params.initPeerId === peerId;
};
@ -76,9 +74,7 @@ export const allowOnlyParticleOriginatedAt = <T extends ArgName>(
* Only allow signing when all of the predicates are satisfied.
* Useful for predicates reuse
*/
export const and = <T extends ArgName>(
...predicates: SecurityGuard<T>[]
): SecurityGuard<T> => {
export const and = (...predicates: SecurityGuard[]): SecurityGuard => {
return (params) => {
return predicates.every((x) => {
return x(params);
@ -90,9 +86,7 @@ export const and = <T extends ArgName>(
* Only allow signing when any of the predicates are satisfied.
* Useful for predicates reuse
*/
export const or = <T extends ArgName>(
...predicates: SecurityGuard<T>[]
): SecurityGuard<T> => {
export const or = (...predicates: SecurityGuard[]): SecurityGuard => {
return (params) => {
return predicates.some((x) => {
return x(params);

View File

@ -20,23 +20,24 @@ import { Path, Aqua } from "@fluencelabs/aqua-api/aqua-api.js";
import {
FunctionCallDef,
JSONArray,
PassedArgs,
JSONValue,
ServiceDef,
} 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 { ClientPeer, makeClientPeerConfig } from "../clientPeer/ClientPeer.js";
import { ClientConfig, RelayOptions } from "../clientPeer/types.js";
import { callAquaFunction } from "../compilerSupport/callFunction.js";
import { ServiceImpl } from "../compilerSupport/types.js";
import { IConnection } from "../connection/interfaces.js";
import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { CallServiceResultType } from "../jsServiceHost/interfaces.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { WrapFnIntoServiceCall } from "../jsServiceHost/serviceUtils.js";
import { KeyPair } from "../keypair/index.js";
import { WasmLoaderFromNpm } from "../marine/deps-loader/node.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { WorkerLoader } from "../marine/worker-script/workerLoader.js";
import { Particle } from "../particle/Particle.js";
export const registerHandlersHelper = (
@ -73,6 +74,16 @@ interface FunctionInfo {
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> => {
await fs.access(aquaFile);
@ -92,7 +103,6 @@ export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
.map(([name, fnInfo]: [string, FunctionInfo]) => {
const callFn = (peer: FluencePeer, args: PassedArgs) => {
return callAquaFunction({
def: fnInfo.funcDef,
script: fnInfo.script,
config: {},
peer: peer,
@ -136,25 +146,60 @@ class NoopConnection implements IConnection {
export class TestPeer extends FluencePeer {
constructor(keyPair: KeyPair, connection: IConnection) {
const workerLoader = new WorkerLoader();
const jsHost = new JsServiceHost();
const controlModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/marine-js",
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
workerLoader,
controlModuleLoader,
avmModuleLoader,
{
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 jsHost = new JsServiceHost();
super(DEFAULT_CONFIG, keyPair, marine, jsHost, connection);
}
}
@ -181,26 +226,63 @@ export const withClient = async (
config: ClientConfig,
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(
relay,
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);
try {

View File

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

View File

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