mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2024-12-03 17:40:18 +00:00
feat(js-client)!: Segregation of responsibility between js-client packages [fixes DXJ-525] (#378)
Schema validation in js-client
This commit is contained in:
parent
638da47bc2
commit
f4a550dd22
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@ -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
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
@ -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
2
ci.cjs
@ -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;
|
||||
}
|
||||
|
||||
|
51
packages/@tests/aqua/compile-aqua.ts
Normal file
51
packages/@tests/aqua/compile-aqua.ts
Normal 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,
|
||||
);
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -77,7 +77,6 @@ const getRelayTime = () => {
|
||||
|
||||
return callAquaFunction({
|
||||
args,
|
||||
def,
|
||||
script,
|
||||
config,
|
||||
peer: Fluence.defaultClient,
|
||||
|
@ -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: [
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignorePatterns": ["src/**/__snapshots__/**/*"]
|
||||
"ignorePatterns": ["src/**/__snapshots__/**/*", "src/**/*.js"]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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]) => {
|
||||
|
20
packages/core/aqua-to-js/src/generate/__test__/__snapshots__/generate.snap.d.ts
vendored
Normal file
20
packages/core/aqua-to-js/src/generate/__test__/__snapshots__/generate.snap.d.ts
vendored
Normal 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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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)},
|
||||
|
@ -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';`;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
@ -15,6 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from "./compilerSupport/aquaTypeDefinitions.js";
|
||||
export * from "./compilerSupport/compilerSupportInterface.js";
|
||||
export * from "./commonTypes.js";
|
||||
export * from "./future.js";
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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()}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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>;
|
||||
|
@ -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,
|
||||
) => {
|
||||
|
@ -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));
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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)",
|
||||
);
|
||||
|
@ -110,6 +110,7 @@ export const checkConnection = async (
|
||||
|
||||
peer.internals.initiateParticle(
|
||||
particle,
|
||||
() => {},
|
||||
handleTimeout(() => {
|
||||
reject("particle timed out");
|
||||
}),
|
||||
|
@ -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>;
|
||||
|
@ -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({
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
@ -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: [] });
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
>;
|
@ -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";
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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"]));
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
21
packages/core/js-client/src/jsPeer/errors.ts
Normal file
21
packages/core/js-client/src/jsPeer/errors.ts
Normal 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 {}
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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)],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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`],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 () => {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
6765
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["**/src/**/*"]
|
||||
"include": ["packages"],
|
||||
"exclude": ["node_modules", "dist", "build"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user