feat(js-client)!: Adding strictes eslint and ts config to all packages [fixes DXJ-464] (#355)

* introduce eslint

* Fix all eslint errors

* Eslint fix and some touches

* Fix tests

* Fix misc errors

* change semver

* change semver #2

* Fix path

* Fix path #2

* freeze lock file in CI

* fix package install

* Fix formatting of surrounding files

* Add empty prettier config

* Fix formatting

* Fix build errors

* Remove unused deps

* remove changelog from formatting

* deps cleanup

* make resource importers async

* Refactor

* Fix error message

* remove comment

* more refactoring

* Update packages/core/js-client/src/compilerSupport/registerService.ts

Co-authored-by: shamsartem <shamsartem@gmail.com>

* refactoring

* refactoring fix

* optimize import

* Update packages/@tests/smoke/node/src/index.ts

Co-authored-by: shamsartem <shamsartem@gmail.com>

* Revert package

* Fix pnpm lock

* Lint-fix

* Fix CI

* Update tests

* Fix build

* Fix import

* Use forked threads dep

* Use fixed version

* Update threads

* Fix lint

* Fix test

* Fix test

* Add polyfill for assert

* Add subpath import

* Fix tests

* Fix deps

---------

Co-authored-by: shamsartem <shamsartem@gmail.com>
This commit is contained in:
Akim 2023-10-17 22:14:08 +07:00 committed by GitHub
parent b46933252a
commit 919c7d6ea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
135 changed files with 10529 additions and 9167 deletions

View File

@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

133
.eslintrc.json Normal file
View File

@ -0,0 +1,133 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"project": ["./tsconfig.eslint.json"],
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier"
],
"plugins": [
"@typescript-eslint",
"import",
"license-header",
"unused-imports"
],
"ignorePatterns": ["**/node_modules/**/*", "**/dist/**/*"],
"rules": {
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"no-console": ["error"],
"arrow-body-style": ["error", "always"],
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"curly": ["error", "all"],
"no-unused-expressions": ["error"],
"dot-notation": ["off"],
"object-curly-spacing": ["error", "always"],
"padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "multiline-expression",
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": "multiline-expression"
},
{
"blankLine": "always",
"prev": "multiline-block-like",
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": "multiline-block-like"
},
{
"blankLine": "always",
"prev": "multiline-const",
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": "multiline-const"
},
{
"blankLine": "always",
"prev": "multiline-let",
"next": "*"
},
{
"blankLine": "always",
"prev": "*",
"next": "multiline-let"
},
{
"blankLine": "any",
"prev": "case",
"next": "case"
}
],
"import/extensions": ["error", "ignorePackages"],
"import/no-unresolved": "off",
"import/no-cycle": ["error"],
"import/order": [
"error",
{
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"node/no-unsupported-features/es-syntax": "off",
"node/no-unpublished-import": "off",
"node/no-missing-import": "off",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "no-public"
}
],
"@typescript-eslint/strict-boolean-expressions": [
"error",
{
"allowString": false,
"allowNumber": false,
"allowNullableObject": false,
"allowNullableBoolean": false,
"allowNullableString": false,
"allowNullableNumber": false,
"allowAny": false
}
],
"@typescript-eslint/consistent-type-assertions": [
"error",
{
"assertionStyle": "never"
}
],
"unused-imports/no-unused-imports": "error",
"license-header/header": ["error", "./resources/license-header.js"]
}
}

View File

@ -88,6 +88,11 @@ jobs:
registry-url: "https://npm.fluence.dev" registry-url: "https://npm.fluence.dev"
cache: "pnpm" cache: "pnpm"
- run: pnpm -r i
- run: pnpm -r build
- run: pnpm lint-check
- name: Override dependencies - name: Override dependencies
uses: fluencelabs/github-actions/pnpm-set-dependency@main uses: fluencelabs/github-actions/pnpm-set-dependency@main
with: with:
@ -97,10 +102,8 @@ jobs:
"@fluencelabs/marine-js": "${{ inputs.marine-js-version }}" "@fluencelabs/marine-js": "${{ inputs.marine-js-version }}"
} }
- run: pnpm -r --no-frozen-lockfile i - run: pnpm -r i
- run: pnpm -r build
- run: pnpm -r test - run: pnpm -r test
- name: Dump rust-peer logs - name: Dump rust-peer logs
if: always() if: always()
uses: jwalton/gh-docker-logs@v2 uses: jwalton/gh-docker-logs@v2

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.log *.log
.idea .idea
.eslintcache
# Dependency directories # Dependency directories
node_modules/ node_modules/

View File

@ -1 +1,10 @@
.github .github
.eslintcache
pnpm-lock.yaml
**/node_modules
**/dist
**/build
**/public
**/CHANGELOG.md

1
.prettierrc Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -1,8 +0,0 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 4,
useTabs: false,
};

17
ci.cjs
View File

@ -5,7 +5,7 @@ const path = require("path");
function printUsage() { function printUsage() {
console.log( console.log(
`Usage: "ci check-consistency" or "ci bump-version %postfix%" or "ci get-version"` `Usage: "ci check-consistency" or "ci bump-version %postfix%" or "ci get-version"`,
); );
} }
@ -43,18 +43,17 @@ async function getPackageJsonsRecursive(currentPath) {
(await fs.readdir(currentPath, { withFileTypes: true })) (await fs.readdir(currentPath, { withFileTypes: true }))
.filter( .filter(
(file) => (file) =>
file.name !== "node_modules" && file.name !== "@tests" && file.name !== "node_modules" &&
(file.isDirectory() || file.name === "package.json") file.name !== "@tests" &&
(file.isDirectory() || file.name === "package.json"),
) )
.map((file) => .map((file) =>
file.isDirectory() file.isDirectory()
? getPackageJsonsRecursive( ? getPackageJsonsRecursive(path.join(currentPath, file.name))
path.join(currentPath, file.name)
)
: Promise.resolve([ : Promise.resolve([
path.join(process.cwd(), currentPath, file.name), path.join(process.cwd(), currentPath, file.name),
]) ]),
) ),
) )
).flat(); ).flat();
} }
@ -103,7 +102,7 @@ async function checkConsistency(file, versionsMap) {
if (versionInDep !== version) { if (versionInDep !== version) {
console.log( console.log(
`Error, versions don't match: ${name}:${version} !== ${versionInDep}`, `Error, versions don't match: ${name}:${version} !== ${versionInDep}`,
file file,
); );
process.exit(1); process.exit(1);
} }

View File

@ -8,15 +8,29 @@
"node": ">=10", "node": ">=10",
"pnpm": ">=3" "pnpm": ">=3"
}, },
"scripts": {
"lint-check": "pnpm run prettier --check && pnpm run eslint",
"lint-fix": "pnpm run prettier --write && pnpm run eslint --fix",
"prettier": "prettier .",
"eslint": "eslint --cache \"**/src/**/*.{js,ts}\""
},
"author": "Fluence Labs", "author": "Fluence Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"http-server": "14.1.1", "@total-typescript/ts-reset": "0.5.1",
"puppeteer": "19.7.2", "@tsconfig/strictest": "2.0.2",
"@types/node": "18.13.0", "@types/node": "18.13.0",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"eslint": "8.50.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-license-header": "0.6.0",
"eslint-plugin-unused-imports": "3.0.0",
"http-server": "14.1.1",
"prettier": "3.0.3",
"puppeteer": "19.7.2",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"typescript": "4.7", "typescript": "5.1.6"
"@fluencelabs/aqua-lib": "0.6.0",
"@fluencelabs/aqua": "0.9.1-374"
} }
} }

View File

@ -0,0 +1,6 @@
{
"ignorePatterns": ["**/*.css"],
"rules": {
"no-console": "off"
}
}

View File

@ -53,3 +53,6 @@ func marineTest(wasm64: string) -> f64:
<- res <- res
func callHappy(a: string, b: f64, c: f64, d: string -> f64) -> f64:
res <- d("abc")
<- res

View File

@ -17,13 +17,13 @@
"author": "Fluence Labs", "author": "Fluence Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fluencelabs/js-client": "workspace:^",
"base64-js": "1.5.1" "base64-js": "1.5.1"
}, },
"devDependencies": { "devDependencies": {
"@fluencelabs/cli": "0.7.2",
"@fluencelabs/registry": "0.8.2",
"@fluencelabs/aqua-lib": "0.6.0", "@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/trust-graph": "3.1.2"
} }
} }

View File

@ -8,13 +8,14 @@
* Aqua version: 0.12.0 * Aqua version: 0.12.0
* *
*/ */
import type { IFluenceClient as IFluenceClient$$, CallParams as CallParams$$ } from '@fluencelabs/js-client'; import type {
IFluenceClient as IFluenceClient$$,
CallParams as CallParams$$,
} from "@fluencelabs/js-client";
import { import {
v5_callFunction as callFunction$$, v5_callFunction as callFunction$$,
v5_registerService as registerService$$, v5_registerService as registerService$$,
} from '@fluencelabs/js-client'; } from "@fluencelabs/js-client";
// Services // Services
@ -30,49 +31,42 @@ export const test_script = `
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 0]) (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 0])
) )
) )
` `;
export function test(config?: { ttl?: number }): Promise<void>;
export function test(
config?: {ttl?: number}
): Promise<void>;
export function test( export function test(
peer: IFluenceClient$$, peer: IFluenceClient$$,
config?: {ttl?: number} config?: { ttl?: number },
): Promise<void>; ): Promise<void>;
export function test(...args: any) { export function test(...args: any) {
return callFunction$$( return callFunction$$(
args, args,
{ {
"functionName" : "test", functionName: "test",
"arrow" : { arrow: {
"tag" : "arrow", tag: "arrow",
"domain" : { domain: {
"tag" : "labeledProduct", tag: "labeledProduct",
"fields" : { fields: {},
}
}, },
"codomain" : { codomain: {
"tag" : "nil" tag: "nil",
}
}, },
"names" : { },
"relay" : "-relay-", names: {
"getDataSrv" : "getDataSrv", relay: "-relay-",
"callbackSrv" : "callbackSrv", getDataSrv: "getDataSrv",
"responseSrv" : "callbackSrv", callbackSrv: "callbackSrv",
"responseFnName" : "response", responseSrv: "callbackSrv",
"errorHandlingSrv" : "errorHandlingSrv", responseFnName: "response",
"errorFnName" : "error" errorHandlingSrv: "errorHandlingSrv",
} errorFnName: "error",
}, },
test_script },
) test_script,
);
} }
/* eslint-enable */ /* eslint-enable */

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +1,117 @@
import { fromByteArray } from 'base64-js'; /**
import { Fluence } from '@fluencelabs/js-client'; * Copyright 2023 Fluence Labs Limited
import type { ClientConfig } from '@fluencelabs/js-client'; *
import { registerHelloWorld, helloTest, marineTest, resourceTest } from './_aqua/smoke_test.js'; * Licensed under the Apache License, Version 2.0 (the "License");
import { test as particleTest } from './_aqua/finalize_particle.js'; * you may not use this file except in compliance with the License.
import { wasm } from './wasmb64.js'; * 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 { Fluence } from "@fluencelabs/js-client";
import type { ClientConfig } from "@fluencelabs/js-client";
import { fromByteArray } from "base64-js";
import { test as particleTest } from "./_aqua/finalize_particle.js";
import {
registerHelloWorld,
helloTest,
marineTest,
} from "./_aqua/smoke_test.js";
import { wasm } from "./wasmb64.js";
const relay = { const relay = {
multiaddr: '/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', multiaddr:
peerId: '12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', "/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
peerId: "12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
}; };
function generateRandomUint8Array() { function generateRandomUint8Array() {
const uint8Array = new Uint8Array(32); const uint8Array = new Uint8Array(32);
for (let i = 0; i < uint8Array.length; i++) { for (let i = 0; i < uint8Array.length; i++) {
uint8Array[i] = Math.floor(Math.random() * 256); uint8Array[i] = Math.floor(Math.random() * 256);
} }
return uint8Array; return uint8Array;
} }
const optsWithRandomKeyPair = (): ClientConfig => { const optsWithRandomKeyPair = (): ClientConfig => {
return { return {
keyPair: { keyPair: {
type: 'Ed25519', type: "Ed25519",
source: generateRandomUint8Array(), source: generateRandomUint8Array(),
}, },
} as const; } as const;
}; };
export type TestResult = { type: 'success'; data: string } | { type: 'failure'; error: string }; export type TestResult =
| { type: "success"; data: string }
| { type: "failure"; error: string };
export const runTest = async (): Promise<TestResult> => { export const runTest = async (): Promise<TestResult> => {
try { try {
Fluence.onConnectionStateChange((state) => console.info('connection state changed: ', state)); Fluence.onConnectionStateChange((state) => {
console.info("connection state changed: ", state);
});
console.log('connecting to Fluence Network...'); console.log("connecting to Fluence Network...");
console.log('multiaddr: ', relay.multiaddr); console.log("multiaddr: ", relay.multiaddr);
await Fluence.connect(relay, optsWithRandomKeyPair()); await Fluence.connect(relay, optsWithRandomKeyPair());
console.log('connected'); console.log("connected");
const relayPeerId = (await Fluence.getClient()).getRelayPeerId(); const relayPeerId = Fluence.getClient().getRelayPeerId();
console.log('relay:', relayPeerId); console.log("relay:", relayPeerId);
await registerHelloWorld({ registerHelloWorld({
hello(str) { hello(str) {
return 'Hello, ' + str + '!'; return "Hello, " + str + "!";
}, },
}); });
const client = await Fluence.getClient(); const client = Fluence.getClient();
console.log('my peer id: ', client.getPeerId()); console.log("my peer id: ", client.getPeerId());
console.log('my sk id: ', fromByteArray(client.getPeerSecretKey())); console.log("my sk id: ", fromByteArray(client.getPeerSecretKey()));
console.log('running hello test...'); console.log("running hello test...");
const hello = await helloTest(); const hello = await helloTest();
console.log('hello test finished, result: ', hello); console.log("hello test finished, result: ", hello);
console.log('running marine test...'); console.log("running marine test...");
const marine = await marineTest(wasm); const marine = await marineTest(wasm);
console.log('running particle test...'); console.log("running particle test...");
await particleTest(); await particleTest();
console.log('marine test finished, result: ', marine); console.log("marine test finished, result: ", marine);
const returnVal = { const returnVal = {
hello, hello,
marine, marine,
}; };
return { type: 'success', data: JSON.stringify(returnVal) };
return { type: "success", data: JSON.stringify(returnVal) };
} finally { } finally {
console.log('disconnecting from Fluence Network...'); console.log("disconnecting from Fluence Network...");
await Fluence.disconnect(); await Fluence.disconnect();
console.log('disconnected'); console.log("disconnected");
} }
}; };
export const runMain = () => { export const runMain = () => {
runTest() runTest()
.then(() => console.log('done!')) .then(() => {
.catch((err) => console.error('error: ', err)); console.log("done!");
})
.catch((err) => {
console.error("error: ", err);
});
}; };

File diff suppressed because one or more lines are too long

View File

@ -4,5 +4,6 @@
"outDir": "./dist", "outDir": "./dist",
"module": "NodeNext" "module": "NodeNext"
}, },
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@ -1,4 +1,21 @@
import '@fluencelabs/js-client'; /**
import { runTest } from '@test/aqua_for_test'; * 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.
*/
runTest().then(() => console.log('Smoke tests succeed!')); import "@fluencelabs/js-client";
import { runTest } from "@test/aqua_for_test";
await runTest();
console.log("Smoke tests succeed!");

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@test/aqua_for_test": "workspace:^", "@test/aqua_for_test": "workspace:*",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0", "@testing-library/react": "13.4.0",
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
@ -15,11 +15,10 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "4.9.5",
"web-vitals": "2.1.4" "web-vitals": "2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@test/test-utils": "workspace:^", "@test/test-utils": "workspace:*",
"puppeteer": "19.7.2" "puppeteer": "19.7.2"
}, },
"scripts": { "scripts": {

View File

@ -1,7 +1,7 @@
import { runTest, TestResult } from '@test/aqua_for_test'; import { runTest, TestResult } from "@test/aqua_for_test";
import React from 'react'; import React from "react";
import logo from './logo.svg'; import logo from "./logo.svg";
import './App.css'; import "./App.css";
function App() { function App() {
const [result, setResult] = React.useState<TestResult | null>(null); const [result, setResult] = React.useState<TestResult | null>(null);
@ -12,7 +12,7 @@ function App() {
setResult(res); setResult(res);
}) })
.catch((err) => { .catch((err) => {
setResult({ type: 'failure', error: err.toString() }); setResult({ type: "failure", error: err.toString() });
}); });
}; };
@ -27,9 +27,18 @@ function App() {
Click to run test Click to run test
</button> </button>
{result && result.type === 'success' && <div id="res">{result.data}</div>} {result && result.type === "success" && (
{result && result.type === 'failure' && <div id="error">{result.error}</div>} <div id="res">{result.data}</div>
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer"> )}
{result && result.type === "failure" && (
<div id="error">{result.error}</div>
)}
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React Learn React
</a> </a>
</header> </header>

View File

@ -1,13 +1,13 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace; monospace;
} }

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom/client'; import ReactDOM from "react-dom/client";
import './index.css'; import "./index.css";
import App from './App'; import App from "./App";
import reportWebVitals from './reportWebVitals'; import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />

View File

@ -1,8 +1,8 @@
import { ReportHandler } from 'web-vitals'; import { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);
getFID(onPerfEntry); getFID(onPerfEntry);
getFCP(onPerfEntry); getFCP(onPerfEntry);

View File

@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'; import "@testing-library/jest-dom";

View File

@ -1,49 +1,53 @@
import puppeteer from 'puppeteer'; import puppeteer from "puppeteer";
import { dirname, join } from 'path'; import { dirname, join } from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
import { CDN_PUBLIC_PATH, startContentServer, stopServer } from '@test/test-utils'; import {
import { access, symlink } from 'fs/promises'; CDN_PUBLIC_PATH,
startContentServer,
stopServer,
} from "@test/test-utils";
import { access, symlink } from "fs/promises";
const port = 3001; const port = 3001;
const uri = `http://localhost:${port}/`; const uri = `http://localhost:${port}/`;
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const publicPath = join(__dirname, '../build/'); const publicPath = join(__dirname, "../build/");
const test = async () => { const test = async () => {
const localServer = await startContentServer(port, publicPath); const localServer = await startContentServer(port, publicPath);
try { try {
await access(join(publicPath, 'source')) await access(join(publicPath, "source"));
} catch { } catch {
await symlink(CDN_PUBLIC_PATH, join(publicPath, 'source')); await symlink(CDN_PUBLIC_PATH, join(publicPath, "source"));
} }
console.log('starting puppeteer...'); console.log("starting puppeteer...");
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = (await browser.pages())[0]; const page = (await browser.pages())[0];
// uncomment to debug what's happening inside the browser // uncomment to debug what's happening inside the browser
// page.on('console', (msg) => console.log('// from console: ', msg.text())); // page.on('console', (msg) => console.log('// from console: ', msg.text()));
console.log('going to the page in browser...'); console.log("going to the page in browser...");
await page.goto(uri); await page.goto(uri);
console.log('clicking button...'); console.log("clicking button...");
await page.click('#btn'); await page.click("#btn");
console.log('waiting for result to appear...'); console.log("waiting for result to appear...");
const elem = await page.waitForSelector('#res'); const elem = await page.waitForSelector("#res");
console.log('getting the content of result div...'); console.log("getting the content of result div...");
const content = await elem?.evaluate((x) => x.textContent); const content = await elem?.evaluate((x) => x.textContent);
console.log('raw result: ', content); console.log("raw result: ", content);
await browser.close(); await browser.close();
await stopServer(localServer); await stopServer(localServer);
if (!content) { if (!content) {
throw new Error('smoke test failed!'); throw new Error("smoke test failed!");
} }
}; };
test().then(() => console.log('smoke tests succeed!')); test().then(() => console.log("smoke tests succeed!"));

View File

@ -19,8 +19,8 @@
"author": "Fluence Labs", "author": "Fluence Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fluencelabs/js-client": "workspace:^", "@fluencelabs/js-client": "workspace:*",
"@test/test-utils": "workspace:../../test-utils" "@test/test-utils": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"puppeteer": "19.7.2" "puppeteer": "19.7.2"

View File

@ -1,8 +1,9 @@
const fluence = globalThis.fluence; const fluence = globalThis.fluence;
const relay = { const relay = {
multiaddr: '/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', multiaddr:
peerId: '12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', "/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
peerId: "12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
}; };
const getRelayTime = () => { const getRelayTime = () => {
@ -37,36 +38,36 @@ const getRelayTime = () => {
)`; )`;
const def = { const def = {
functionName: 'getRelayTime', functionName: "getRelayTime",
arrow: { arrow: {
tag: 'arrow', tag: "arrow",
domain: { domain: {
tag: 'labeledProduct', tag: "labeledProduct",
fields: { fields: {
relayPeerId: { relayPeerId: {
tag: 'scalar', tag: "scalar",
name: 'string', name: "string",
}, },
}, },
}, },
codomain: { codomain: {
tag: 'unlabeledProduct', tag: "unlabeledProduct",
items: [ items: [
{ {
tag: 'scalar', tag: "scalar",
name: 'u64', name: "u64",
}, },
], ],
}, },
}, },
names: { names: {
relay: '-relay-', relay: "-relay-",
getDataSrv: 'getDataSrv', getDataSrv: "getDataSrv",
callbackSrv: 'callbackSrv', callbackSrv: "callbackSrv",
responseSrv: 'callbackSrv', responseSrv: "callbackSrv",
responseFnName: 'response', responseFnName: "response",
errorHandlingSrv: 'errorHandlingSrv', errorHandlingSrv: "errorHandlingSrv",
errorFnName: 'error', errorFnName: "error",
}, },
}; };
@ -83,28 +84,28 @@ const getRelayTime = () => {
}; };
const main = async () => { const main = async () => {
console.log('starting fluence...'); console.log("starting fluence...");
fluence.defaultClient = await fluence.clientFactory(relay); fluence.defaultClient = await fluence.clientFactory(relay, {});
console.log('started fluence'); console.log("started fluence");
console.log('getting relay time...'); console.log("getting relay time...");
const relayTime = await getRelayTime(); const relayTime = await getRelayTime();
console.log('got relay time, ', relayTime); console.log("got relay time, ", relayTime);
console.log('stopping fluence...'); console.log("stopping fluence...");
await fluence.defaultClient.stop(); await fluence.defaultClient.stop();
console.log('stopped fluence...'); console.log("stopped fluence...");
return relayTime; return relayTime;
}; };
const btn = document.getElementById('btn'); const btn = document.getElementById("btn");
btn.addEventListener('click', () => { btn.addEventListener("click", () => {
main().then((res) => { main().then((res) => {
const inner = document.createElement('div'); const inner = document.createElement("div");
inner.id = 'res'; inner.id = "res";
inner.innerText = res; inner.innerText = res;
document.getElementById('res-placeholder').appendChild(inner); document.getElementById("res-placeholder").appendChild(inner);
}); });
}); });

View File

@ -1,49 +1,76 @@
import puppeteer from 'puppeteer'; /**
import { dirname, join } from 'path'; * Copyright 2023 Fluence Labs Limited
import { fileURLToPath } from 'url'; *
* 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 { CDN_PUBLIC_PATH, startCdn, startContentServer, stopServer } from '@test/test-utils'; import { symlink, access } from "fs/promises";
import { symlink, access } from 'fs/promises'; import { dirname, join } from "path";
import { fileURLToPath } from "url";
import {
CDN_PUBLIC_PATH,
startContentServer,
stopServer,
} from "@test/test-utils";
import puppeteer from "puppeteer";
const port = 3000; const port = 3000;
const uri = `http://localhost:${port}/`; const uri = `http://localhost:${port}/`;
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const publicPath = join(__dirname, '../public/'); const publicPath = join(__dirname, "../public/");
const test = async () => { const test = async () => {
const localServer = await startContentServer(port, publicPath); const localServer = await startContentServer(port, publicPath);
try { try {
await access(join(publicPath, 'source')) await access(join(publicPath, "source"));
} catch { } catch {
await symlink(CDN_PUBLIC_PATH, join(publicPath, 'source')); await symlink(CDN_PUBLIC_PATH, join(publicPath, "source"));
} }
console.log('starting puppeteer...'); console.log("starting puppeteer...");
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = (await browser.pages())[0]; const page = (await browser.pages())[0];
// uncomment to debug what's happening inside the browser // uncomment to debug what's happening inside the browser
// page.on('console', (msg) => console.log('// from console: ', msg.text())); // page.on('console', (msg) => console.log('// from console: ', msg.text()));
console.log('going to the page in browser...'); console.log("going to the page in browser...");
await page.goto(uri); await page.goto(uri);
console.log('clicking button...'); console.log("clicking button...");
await page.click('#btn'); await page.click("#btn");
console.log('waiting for result to appear...'); console.log("waiting for result to appear...");
const elem = await page.waitForSelector('#res'); const elem = await page.waitForSelector("#res");
console.log('getting the content of result div...'); console.log("getting the content of result div...");
const content = await elem?.evaluate((x) => x.textContent);
console.log('raw result: ', content); const content = await elem?.evaluate((x) => {
return x.textContent;
});
console.log("raw result: ", content);
await browser.close(); await browser.close();
await stopServer(localServer); await stopServer(localServer);
if (!content) { if (content == null) {
throw new Error('smoke test failed!'); throw new Error("smoke test failed!");
} }
}; };
test().then(() => console.log('smoke tests succeed!')); void test().then(() => {
console.log("smoke tests succeed!");
});

View File

@ -3,5 +3,5 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist" "outDir": "./dist"
}, },
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "public"]
} }

View File

@ -1,31 +1,65 @@
import handler from 'serve-handler'; /**
import { createServer } from 'http'; * Copyright 2023 Fluence Labs Limited
import type { Server } from 'http'; *
* 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 { dirname, join } from 'path'; import { createServer } from "http";
import { fileURLToPath } from 'url'; import type { Server } from "http";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
export const CDN_PUBLIC_PATH = join(__dirname, '../../../core/js-client/dist/browser'); export const CDN_PUBLIC_PATH = join(
__dirname,
"../../../core/js-client/dist/browser",
);
export const startCdn = (port: number) => startContentServer(port, CDN_PUBLIC_PATH); export const startCdn = (port: number) => {
return startContentServer(port, CDN_PUBLIC_PATH);
};
export const startContentServer = (port: number, publicDir: string): Promise<Server> => { export const startContentServer = (
port: number,
publicDir: string,
): Promise<Server> => {
const server = createServer((request, response) => { const server = createServer((request, response) => {
return handler(request, response, { void handler(request, response, {
public: publicDir, public: publicDir,
rewrites: [{ rewrites: [
source: '/js-client.min.js', {
destination: '/source/index.umd.cjs' source: "/js-client.min.js",
}], destination: "/source/index.umd.cjs",
headers: [{ },
source: '**/*', ],
headers: [ headers: [
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }, {
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' } source: "**/*",
] headers: [
}] {
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
],
}); });
}); });
@ -41,7 +75,7 @@ export const startContentServer = (port: number, publicDir: string): Promise<Ser
export const stopServer = (app: Server): Promise<void> => { export const stopServer = (app: Server): Promise<void> => {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
app.close(() => { app.close(() => {
console.log('server stopped'); console.log("server stopped");
resolve(); resolve();
}); });
}); });

View File

@ -3,5 +3,6 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist" "outDir": "./dist"
}, },
"include": ["src"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

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

View File

@ -2,9 +2,9 @@
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* devDependencies - devDependencies
* @fluencelabs/js-client bumped to 0.1.7 - @fluencelabs/js-client bumped to 0.1.7
### Dependencies ### Dependencies
@ -20,7 +20,6 @@
## 0.0.1 (2023-09-22) ## 0.0.1 (2023-09-22)
### Features ### Features
* **aqua-compiler:** JS-client aqua wrapper [fixes DXJ-461] ([#347](https://github.com/fluencelabs/js-client/issues/347)) ([7fff3b1](https://github.com/fluencelabs/js-client/commit/7fff3b1c0374eef76ab4e665b13cf97b5c50ff70)) - **aqua-compiler:** JS-client aqua wrapper [fixes DXJ-461] ([#347](https://github.com/fluencelabs/js-client/issues/347)) ([7fff3b1](https://github.com/fluencelabs/js-client/commit/7fff3b1c0374eef76ab4e665b13cf97b5c50ff70))

View File

@ -25,7 +25,6 @@
"@fluencelabs/registry": "0.8.7", "@fluencelabs/registry": "0.8.7",
"@fluencelabs/spell": "0.5.20", "@fluencelabs/spell": "0.5.20",
"@fluencelabs/trust-graph": "0.4.7", "@fluencelabs/trust-graph": "0.4.7",
"typescript": "5.1.6", "vitest": "0.34.6"
"vitest": "0.29.7"
} }
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,85 +14,136 @@
* limitations under the License. * limitations under the License.
*/ */
import { ArrowType, ArrowWithoutCallbacks, NonArrowType, ProductType } from '@fluencelabs/interfaces'; import { ArrowWithoutCallbacks, NonArrowType } from "@fluencelabs/interfaces";
import { match, P } from 'ts-pattern'; import { match, P } from "ts-pattern";
import { getFuncArgs } from './utils.js';
export function genTypeName(t: NonArrowType | ProductType<NonArrowType> | ArrowWithoutCallbacks, name: string): readonly [string | undefined, string] { import { getFuncArgs } from "./utils.js";
export function genTypeName(
t: NonArrowType | ArrowWithoutCallbacks,
name: string,
): readonly [string | undefined, string] {
const genType = typeToTs(t); const genType = typeToTs(t);
return match(t) return match(t)
.with( .with({ tag: "nil" }, () => {
{ tag: 'nil' }, return [undefined, "void"] as const;
() => [undefined, 'void'] as const })
).with( .with({ tag: "struct" }, () => {
{ tag: 'struct' }, return [`export type ${name} = ${genType}`, name] as const;
() => [`export type ${name} = ${genType}`, name] as const })
).with( .with({ tag: P.union("labeledProduct", "unlabeledProduct") }, (item) => {
{ tag: P.union('labeledProduct', 'unlabeledProduct') }, const args =
(item) => { item.tag === "labeledProduct" ? Object.values(item.fields) : item.items;
const args = item.tag === 'labeledProduct'
? Object.values(item.fields)
: item.items;
if (args.length === 1) { if (args.length === 1) {
return genTypeName(args[0], name); return genTypeName(args[0], name);
} }
return [`export type ${name} = ${genType}`, name] as const; return [`export type ${name} = ${genType}`, name] as const;
}, })
).otherwise(() => [undefined, genType] as const); .otherwise(() => {
return [undefined, genType] as const;
});
} }
export function typeToTs(t: NonArrowType | ArrowWithoutCallbacks | ProductType<NonArrowType>): string { export function typeToTs(t: NonArrowType | ArrowWithoutCallbacks): string {
return match(t) return match(t)
.with({ tag: "nil" }, () => {
return "null";
})
.with({ tag: "option" }, ({ type }) => {
return typeToTs(type) + " | null";
})
.with({ tag: "scalar" }, ({ name }) => {
return match(name)
.with( .with(
{ tag: 'nil' }, P.union(
() => 'null' "u8",
).with( "u16",
{ tag: 'option' }, "u32",
({ type }) => typeToTs(type) + ' | null' "u64",
).with( "i8",
{ tag: 'scalar' }, "i16",
({ name }) => match(name) "i32",
.with(P.union('u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64', 'f32', 'f64'), () => 'number') "i64",
.with('bool', () => 'boolean') "f32",
.with('string', () => 'string') "f64",
.with(P._, () => 'any').exhaustive() ),
).with( () => {
{ tag: 'array' }, return "number";
({ type }) => typeToTs(type) + '[]' },
).with( )
{ tag: 'struct' }, .with("bool", () => {
({ fields }) => `{ ${Object.entries(fields).map(([field, type]) => `${field}: ${typeToTs(type)};`).join(' ')} }` return "boolean";
).with( })
{ tag: 'labeledProduct' }, .with("string", () => {
({ fields }) => `{ ${Object.entries(fields).map(([field, type]) => `${field}: ${typeToTs(type)};`).join(' ')} }` return "string";
).with( })
{ tag: 'unlabeledProduct' }, .with(P._, () => {
({ items }) => `[${items.map(item => typeToTs(item)).join(', ')}]` return "any";
).with( })
{ tag: 'arrow' }, .exhaustive();
({ tag, domain, codomain }) => { })
const retType = codomain.tag === 'nil' .with({ tag: "array" }, ({ type }) => {
? 'void' return typeToTs(type) + "[]";
})
.with({ tag: "struct" }, ({ fields }) => {
return `{ ${Object.entries(fields)
.map(([field, type]) => {
return `${field}: ${typeToTs(type)};`;
})
.join(" ")} }`;
})
.with({ tag: "labeledProduct" }, ({ fields }) => {
return `{ ${Object.entries(fields)
.map(([field, type]) => {
return `${field}: ${typeToTs(type)};`;
})
.join(" ")} }`;
})
.with({ tag: "unlabeledProduct" }, ({ items }) => {
return `[${items
.map((item) => {
return typeToTs(item);
})
.join(", ")}]`;
})
.with({ tag: "arrow" }, ({ domain, codomain }) => {
const retType =
codomain.tag === "nil"
? "void"
: codomain.items.length === 1 : codomain.items.length === 1
? typeToTs(codomain.items[0]) ? typeToTs(codomain.items[0])
: typeToTs(codomain); : typeToTs(codomain);
const args = getFuncArgs(domain).map(([name, type]) => ([name, typeToTs(type)])); const args = getFuncArgs(domain).map(([name, type]) => {
return [name, typeToTs(type)];
});
const generic = args.length === 0 ? 'null' : args.map(([name]) => `'${name}'`).join(' | '); const generic =
args.push(['callParams', `CallParams$$<${generic}>`]); args.length === 0
? "null"
: args
.map(([name]) => {
return `'${name}'`;
})
.join(" | ");
const funcArgs = args.map(([name, type]) => `${name}: ${type}`).join(', '); args.push(["callParams", `CallParams$$<${generic}>`]);
const funcArgs = args
.map(([name, type]) => {
return `${name}: ${type}`;
})
.join(", ");
return `(${funcArgs}) => ${retType} | Promise<${retType}>`; return `(${funcArgs}) => ${retType} | Promise<${retType}>`;
} })
).with( .with({ tag: "topType" }, () => {
{ tag: 'topType' }, return "unknown";
() => 'unknown' })
).with( .with({ tag: "bottomType" }, () => {
{ tag: 'bottomType' }, return "never";
() => 'never' })
).exhaustive(); .exhaustive();
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,4 +14,4 @@
* limitations under the License. * limitations under the License.
*/ */
export const CLIENT = 'IFluenceClient$$'; export const CLIENT = "IFluenceClient$$";

View File

@ -1,61 +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 {
ArrayType,
BottomType, LabeledProductType,
NilType,
NonArrowType,
OptionType,
ScalarType,
StructType,
TopType, UnlabeledProductType
} from '@fluencelabs/interfaces';
// Type definitions for inferring ts types from air json definition
// In the future we may remove string type declaration and move to type inference.
type GetTsTypeFromScalar<T extends ScalarType> = T['name'] extends 'u8' | 'u16' | 'u32' | 'u64' | 'i8' | 'i16' | 'i32' | 'i64' | 'f32' | 'f64'
? number
: T['name'] extends 'bool'
? boolean
: T['name'] extends 'string'
? string
: never;
type MapTuple<T> = { [K in keyof T]: T[K] extends NonArrowType ? GetTsType<T[K]> : never }
type GetTsType<T extends NonArrowType> = T extends NilType
? null
: T extends ArrayType
? GetTsType<T['type']>[]
: T extends StructType
? { [K in keyof T]: GetTsType<T> }
: T extends OptionType
? GetTsType<T['type']> | null
: T extends ScalarType
? GetTsTypeFromScalar<T>
: T extends TopType
? unknown
: T extends BottomType
? never
: T extends Exclude<UnlabeledProductType<infer H>, NilType>
? MapTuple<H>
: T extends Exclude<LabeledProductType<infer H>, NilType>
? H extends NonArrowType
? { [K in keyof T['fields']]: GetTsType<H> }
: never
: never;

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,35 +14,39 @@
* limitations under the License. * limitations under the License.
*/ */
import { describe, expect, it } from 'vitest'; import url from "url";
import { generateTypes, generateSources } from '../index.js';
import { compileFromPath } from '@fluencelabs/aqua-api';
import url from 'url';
import { getPackageJsonContent, PackageJson } from '../../utils.js';
describe('Aqua to js/ts compiler', () => { import { compileFromPath } from "@fluencelabs/aqua-api";
it('compiles smoke tests successfully', async () => { import { describe, expect, it } from "vitest";
import { getPackageJsonContent, PackageJson } from "../../utils.js";
import { generateTypes, generateSources } from "../index.js";
describe("Aqua to js/ts compiler", () => {
it("compiles smoke tests successfully", async () => {
const res = await compileFromPath({ const res = await compileFromPath({
filePath: url.fileURLToPath(new URL('./sources/smoke_test.aqua', import.meta.url)), filePath: url.fileURLToPath(
imports: ['./node_modules'], new URL("./sources/smoke_test.aqua", import.meta.url),
targetType: 'air' ),
imports: ["./node_modules"],
targetType: "air",
}); });
const pkg: PackageJson = { const pkg: PackageJson = {
...(await getPackageJsonContent()), ...(await getPackageJsonContent()),
version: '0.0.0', version: "0.0.0",
devDependencies: { devDependencies: {
'@fluencelabs/aqua-api': '0.0.0' "@fluencelabs/aqua-api": "0.0.0",
}, },
}; };
const jsResult = await generateSources(res, 'js', pkg); const jsResult = generateSources(res, "js", pkg);
const jsTypes = await generateTypes(res, pkg); const jsTypes = generateTypes(res, pkg);
expect(jsResult).toMatchSnapshot(); expect(jsResult).toMatchSnapshot();
expect(jsTypes).toMatchSnapshot(); expect(jsTypes).toMatchSnapshot();
const tsResult = await generateSources(res, 'ts', pkg); const tsResult = generateSources(res, "ts", pkg);
expect(tsResult).toMatchSnapshot(); expect(tsResult).toMatchSnapshot();
}); });

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,24 +14,38 @@
* limitations under the License. * limitations under the License.
*/ */
import { recursiveRenameLaquaProps } from '../utils.js'; import { recursiveRenameLaquaProps } from "../utils.js";
import { AquaFunction, TypeGenerator } from './interfaces.js';
export function generateFunctions(typeGenerator: TypeGenerator, functions: Record<string, AquaFunction>) { import { AquaFunction, TypeGenerator } from "./interfaces.js";
return Object.values(functions).map(func => generateFunction(typeGenerator, func)).join('\n\n');
export function generateFunctions(
typeGenerator: TypeGenerator,
functions: Record<string, AquaFunction>,
) {
return Object.values(functions)
.map((func) => {
return generateFunction(typeGenerator, func);
})
.join("\n\n");
} }
type DeepToType<T> = { [K in keyof T]: DeepToType<T[K]> };
function generateFunction(typeGenerator: TypeGenerator, func: AquaFunction) { function generateFunction(typeGenerator: TypeGenerator, func: AquaFunction) {
const scriptConstName = func.funcDef.functionName + '_script'; const funcDef: DeepToType<typeof func.funcDef> = func.funcDef;
const scriptConstName = func.funcDef.functionName + "_script";
return `export const ${scriptConstName} = \` return `export const ${scriptConstName} = \`
${func.script}\`; ${func.script}\`;
${typeGenerator.funcType(func)} ${typeGenerator.funcType(func)}
export function ${func.funcDef.functionName}(${typeGenerator.type('...args', 'any[]')}) { export function ${func.funcDef.functionName}(${typeGenerator.type(
"...args",
"any[]",
)}) {
return callFunction$$( return callFunction$$(
args, args,
${JSON.stringify(recursiveRenameLaquaProps(func.funcDef), null, 4)}, ${JSON.stringify(recursiveRenameLaquaProps(funcDef), null, 4)},
${scriptConstName} ${scriptConstName}
); );
}` }`;
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,22 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
import { OutputType } from './interfaces.js'; import { PackageJson } from "../utils.js";
import { PackageJson } from '../utils.js';
export default function generateHeader({ version, devDependencies }: PackageJson, outputType: OutputType) { import { OutputType } from "./interfaces.js";
export default function generateHeader(
{ version, devDependencies }: PackageJson,
outputType: OutputType,
) {
return `/* eslint-disable */ return `/* eslint-disable */
// @ts-nocheck // @ts-nocheck
/** /**
* *
* This file is generated using: * This file is generated using:
* @fluencelabs/aqua-api version: ${devDependencies['@fluencelabs/aqua-api']} * @fluencelabs/aqua-api version: ${devDependencies["@fluencelabs/aqua-api"]}
* @fluencelabs/aqua-to-js version: ${version} * @fluencelabs/aqua-to-js version: ${version}
* 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 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 * If you find any bugs in generated JS/TS, please write an issue on GitHub: https://github.com/fluencelabs/js-client/issues
* *
*/ */
${outputType === 'ts' ? 'import type { IFluenceClient as IFluenceClient$$, CallParams as CallParams$$ } from \'@fluencelabs/js-client\';' : ''} ${
outputType === "ts"
? "import type { IFluenceClient as IFluenceClient$$, CallParams as CallParams$$ } from '@fluencelabs/js-client';"
: ""
}
import { import {
v5_callFunction as callFunction$$, v5_callFunction as callFunction$$,

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,46 +14,80 @@
* limitations under the License. * limitations under the License.
*/ */
import { CompilationResult, JSTypeGenerator, OutputType, TSTypeGenerator, TypeGenerator } from './interfaces.js'; import { PackageJson } from "../utils.js";
import { PackageJson } from '../utils.js';
import { generateServices } from './service.js'; import { generateFunctions } from "./function.js";
import { generateFunctions } from './function.js'; import header from "./header.js";
import header from './header.js'; import {
CompilationResult,
JSTypeGenerator,
OutputType,
TSTypeGenerator,
TypeGenerator,
} from "./interfaces.js";
import { generateServices } from "./service.js";
const typeGenerators: Record<OutputType, TypeGenerator> = { const typeGenerators: Record<OutputType, TypeGenerator> = {
'js': new JSTypeGenerator(), js: new JSTypeGenerator(),
'ts': new TSTypeGenerator() ts: new TSTypeGenerator(),
}; };
export async function generateSources({ services, functions }: CompilationResult, outputType: OutputType, packageJson: PackageJson) { export function generateSources(
{ services, functions }: CompilationResult,
outputType: OutputType,
packageJson: PackageJson,
) {
const typeGenerator = typeGenerators[outputType]; const typeGenerator = typeGenerators[outputType];
return `${header(packageJson, outputType)} return `${header(packageJson, outputType)}
${Object.entries(services).length > 0 ? `// Services ${
Object.entries(services).length > 0
? `// Services
${generateServices(typeGenerator, services)} ${generateServices(typeGenerator, services)}
` : ''} `
${Object.entries(functions).length > 0 ? `// Functions : ""
}
${
Object.entries(functions).length > 0
? `// Functions
${generateFunctions(typeGenerator, functions)} ${generateFunctions(typeGenerator, functions)}
`: ''}` `
: ""
}`;
} }
export async function generateTypes({ services, functions }: CompilationResult, packageJson: PackageJson) { export function generateTypes(
const typeGenerator = typeGenerators['ts']; { services, functions }: CompilationResult,
packageJson: PackageJson,
) {
const typeGenerator = typeGenerators["ts"];
const generatedServices = Object.entries(services) const generatedServices = Object.entries(services)
.map(([srvName, srvDef]) => typeGenerator.serviceType(srvName, srvDef)) .map(([srvName, srvDef]) => {
.join('\n'); return typeGenerator.serviceType(srvName, srvDef);
})
.join("\n");
const generatedFunctions = Object.entries(functions) const generatedFunctions = Object.entries(functions)
.map(([funcName, funcDef]) => typeGenerator.funcType(funcDef)) .map(([, funcDef]) => {
.join('\n'); return typeGenerator.funcType(funcDef);
})
.join("\n");
return `${header(packageJson, 'ts')} return `${header(packageJson, "ts")}
${Object.entries(services).length > 0 ? `// Services ${
Object.entries(services).length > 0
? `// Services
${generatedServices} ${generatedServices}
` : ''} `
${Object.entries(functions).length > 0 ? `// Functions : ""
${generatedFunctions} }
`: ''}` ${
Object.entries(functions).length > 0
? `// Functions
${generatedFunctions}
`
: ""
}`;
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,10 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { CLIENT } from '../constants.js'; import { FunctionCallDef, ServiceDef } from "@fluencelabs/interfaces";
import { FunctionCallDef, ServiceDef } from '@fluencelabs/interfaces';
import { genTypeName, typeToTs } from '../common.js'; import { genTypeName, typeToTs } from "../common.js";
import { capitalize, getFuncArgs } from '../utils.js'; import { CLIENT } from "../constants.js";
import { capitalize, getFuncArgs } from "../utils.js";
export interface TypeGenerator { export interface TypeGenerator {
type(field: string, type: string): string; type(field: string, type: string): string;
@ -42,58 +43,94 @@ export class TSTypeGenerator implements TypeGenerator {
funcType({ funcDef }: AquaFunction): string { funcType({ funcDef }: AquaFunction): string {
const args = getFuncArgs(funcDef.arrow.domain).map(([name, type]) => { const args = getFuncArgs(funcDef.arrow.domain).map(([name, type]) => {
const [typeDesc, t] = genTypeName(type, capitalize(funcDef.functionName) + 'Arg' + capitalize(name)); const [typeDesc, t] = genTypeName(
type,
capitalize(funcDef.functionName) + "Arg" + capitalize(name),
);
return [typeDesc, `${name}: ${t}`] as const; return [typeDesc, `${name}: ${t}`] as const;
}); });
args.push([undefined, `config?: {ttl?: number}`]); args.push([undefined, `config?: {ttl?: number}`]);
const argsDefs = args.map(([, def]) => " " + def); const argsDefs = args.map(([, def]) => {
const argsDesc = args.filter(([desc]) => desc !== undefined).map(([desc]) => desc); return " " + def;
});
const argsDesc = args
.filter(([desc]) => {
return desc !== undefined;
})
.map(([desc]) => {
return desc;
});
const functionOverloads = [ const functionOverloads = [
argsDefs.join(',\n'), argsDefs.join(",\n"),
[` peer: ${CLIENT}`, ...argsDefs].join(',\n') [` peer: ${CLIENT}`, ...argsDefs].join(",\n"),
]; ];
const [resTypeDesc, resType] = genTypeName(funcDef.arrow.codomain, capitalize(funcDef.functionName) + "Result"); const [resTypeDesc, resType] = genTypeName(
funcDef.arrow.codomain,
capitalize(funcDef.functionName) + "Result",
);
return [ return [
argsDesc.join('\n'), argsDesc.join("\n"),
resTypeDesc || "", resTypeDesc ?? "",
functionOverloads.flatMap(fo => [ functionOverloads
.flatMap((fo) => {
return [
`export function ${funcDef.functionName}(`, `export function ${funcDef.functionName}(`,
fo, fo,
`): Promise<${resType}>;`, `): Promise<${resType}>;`,
'' "",
]).join('\n') ];
].filter(s => s !== '').join('\n\n'); })
.join("\n"),
]
.filter((s) => {
return s !== "";
})
.join("\n\n");
} }
serviceType(srvName: string, srvDef: ServiceDef): string { serviceType(srvName: string, srvDef: ServiceDef): string {
const members = srvDef.functions.tag === 'nil' ? [] : Object.entries(srvDef.functions.fields); const members =
srvDef.functions.tag === "nil"
? []
: Object.entries(srvDef.functions.fields);
const interfaceDefs = members const interfaceDefs = members
.map(([name, arrow]) => { .map(([name, arrow]) => {
return ` ${name}: ${typeToTs(arrow)};`; return ` ${name}: ${typeToTs(arrow)};`;
}) })
.join('\n'); .join("\n");
const interfaces = [`export interface ${srvName}Def {`, interfaceDefs, '}'].join('\n'); const interfaces = [
`export interface ${srvName}Def {`,
interfaceDefs,
"}",
].join("\n");
const peerDecl = `peer: ${CLIENT}`; const peerDecl = `peer: ${CLIENT}`;
const serviceDecl = `service: ${srvName}Def`; const serviceDecl = `service: ${srvName}Def`;
const serviceIdDecl = `serviceId: string`; const serviceIdDecl = `serviceId: string`;
const registerServiceArgs = [ const registerServiceArgs = [
[serviceDecl], [serviceDecl],
[serviceIdDecl, serviceDecl], [serviceIdDecl, serviceDecl],
[peerDecl, serviceDecl], [peerDecl, serviceDecl],
[peerDecl, serviceIdDecl, serviceDecl] [peerDecl, serviceIdDecl, serviceDecl],
]; ];
return [interfaces, ...registerServiceArgs.map(registerServiceArg => { return [
const args = registerServiceArg.join(', '); interfaces,
return `export function register${srvName}(${args}): void;` ...registerServiceArgs.map((registerServiceArg) => {
})].join('\n'); const args = registerServiceArg.join(", ");
return `export function register${srvName}(${args}): void;`;
}),
].join("\n");
} }
} }
@ -102,20 +139,20 @@ export class JSTypeGenerator implements TypeGenerator {
return field; return field;
} }
generic(field: string, type: string): string { generic(field: string): string {
return field; return field;
} }
type(field: string, type: string): string { type(field: string): string {
return field; return field;
} }
funcType(): string { funcType(): string {
return ''; return "";
} }
serviceType(): string { serviceType(): string {
return ''; return "";
} }
} }
@ -133,4 +170,4 @@ export interface EntityGenerator {
generate(compilationResult: CompilationResult): string; generate(compilationResult: CompilationResult): string;
} }
export type OutputType = 'js' | 'ts'; export type OutputType = "js" | "ts";

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,45 +14,74 @@
* limitations under the License. * limitations under the License.
*/ */
import { ServiceDef } from '@fluencelabs/interfaces'; import { ServiceDef } from "@fluencelabs/interfaces";
import { recursiveRenameLaquaProps } from '../utils.js';
import { TypeGenerator } from './interfaces.js'; import { recursiveRenameLaquaProps } from "../utils.js";
import { TypeGenerator } from "./interfaces.js";
interface DefaultServiceId { interface DefaultServiceId {
s_Some__f_value?: string s_Some__f_value?: string;
} }
export function generateServices(typeGenerator: TypeGenerator, services: Record<string, ServiceDef>) { export function generateServices(
const generated = Object.entries(services).map(([srvName, srvDef]) => generateService(typeGenerator, srvName, srvDef)).join('\n\n'); typeGenerator: TypeGenerator,
services: Record<string, ServiceDef>,
) {
const generated = Object.entries(services)
.map(([srvName, srvDef]) => {
return generateService(typeGenerator, srvName, srvDef);
})
.join("\n\n");
return generated + '\n'; return generated + "\n";
} }
function generateService(typeGenerator: TypeGenerator, srvName: string, srvDef: ServiceDef) { function generateService(
typeGenerator: TypeGenerator,
srvName: string,
srvDef: ServiceDef,
) {
return [ return [
typeGenerator.serviceType(srvName, srvDef), typeGenerator.serviceType(srvName, srvDef),
generateRegisterServiceOverload(typeGenerator, srvName, srvDef) generateRegisterServiceOverload(typeGenerator, srvName, srvDef),
].join('\n'); ].join("\n");
} }
function generateRegisterServiceOverload(typeGenerator: TypeGenerator, srvName: string, srvDef: ServiceDef) { function generateRegisterServiceOverload(
typeGenerator: TypeGenerator,
srvName: string,
srvDef: ServiceDef,
) {
return [ return [
`export function register${srvName}(${typeGenerator.type('...args', 'any[]')}) {`, `export function register${srvName}(${typeGenerator.type(
' registerService$$(', "...args",
' args,', "any[]",
)}) {`,
" registerService$$(",
" args,",
` ${serviceToJson(srvDef)}`, ` ${serviceToJson(srvDef)}`,
' );', " );",
'}' "}",
].join('\n'); ].join("\n");
} }
function serviceToJson(service: ServiceDef): string { function serviceToJson(service: ServiceDef): string {
return JSON.stringify({ return JSON.stringify(
...( {
(service.defaultServiceId as DefaultServiceId)?.s_Some__f_value // This assertion is required because aqua-api gives bad types
? { defaultServiceId: (service.defaultServiceId as DefaultServiceId).s_Some__f_value } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
: {} ...((service.defaultServiceId as DefaultServiceId).s_Some__f_value != null
), ? {
functions: recursiveRenameLaquaProps(service.functions) defaultServiceId:
}, null, 4); // This assertion is required because aqua-api gives bad types
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(service.defaultServiceId as DefaultServiceId).s_Some__f_value,
}
: {}),
functions: recursiveRenameLaquaProps(service.functions),
},
null,
4,
);
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,12 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { generateSources, generateTypes } from "./generate/index.js";
generateSources, import { CompilationResult, OutputType } from "./generate/interfaces.js";
generateTypes, import { getPackageJsonContent } from "./utils.js";
} from './generate/index.js';
import { CompilationResult, OutputType } from './generate/interfaces.js';
import { getPackageJsonContent } from './utils.js';
interface JsOutput { interface JsOutput {
sources: string; sources: string;
@ -31,17 +28,33 @@ interface TsOutput {
} }
type LanguageOutput = { type LanguageOutput = {
"js": JsOutput, js: JsOutput;
"ts": TsOutput ts: TsOutput;
}; };
export default async function aquaToJs<T extends OutputType>(res: CompilationResult, outputType: T): Promise<LanguageOutput[T]> { type NothingToGenerate = null;
export default async function aquaToJs<T extends OutputType>(
res: CompilationResult,
outputType: T,
): Promise<LanguageOutput[T] | NothingToGenerate> {
if (
Object.keys(res.services).length === 0 &&
Object.keys(res.functions).length === 0
) {
return null;
}
const packageJson = await getPackageJsonContent(); const packageJson = await getPackageJsonContent();
return outputType === 'js' ? { return outputType === "js"
sources: await generateSources(res, 'js', packageJson), ? {
types: await generateTypes(res, packageJson) sources: generateSources(res, "js", packageJson),
} : { types: generateTypes(res, packageJson),
sources: await generateSources(res, 'ts', packageJson), }
} as LanguageOutput[T]; : // TODO: probably there is a way to remove this type assert
}; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
({
sources: generateSources(res, "ts", packageJson),
} as LanguageOutput[T]);
}

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,53 +14,90 @@
* limitations under the License. * limitations under the License.
*/ */
import { ArrowWithoutCallbacks, NonArrowType, ProductType } from '@fluencelabs/interfaces'; import assert from "assert";
import { readFile } from 'fs/promises'; import { readFile } from "fs/promises";
import path from 'path'; import path from "path";
import {
ArrowType,
ArrowWithoutCallbacks,
JSONValue,
LabeledProductType,
NilType,
SimpleTypes,
UnlabeledProductType,
} from "@fluencelabs/interfaces";
export interface PackageJson { export interface PackageJson {
name: string; name: string;
version: string; version: string;
devDependencies: { devDependencies: {
['@fluencelabs/aqua-api']: string ["@fluencelabs/aqua-api"]: string;
} };
} }
export async function getPackageJsonContent(): Promise<PackageJson> { export async function getPackageJsonContent(): Promise<PackageJson> {
const content = await readFile(new URL(path.join('..', 'package.json'), import.meta.url), 'utf-8'); const content = await readFile(
return JSON.parse(content); new URL(path.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;
} }
export function getFuncArgs(domain: ProductType<NonArrowType | ArrowWithoutCallbacks>): [string, NonArrowType | ArrowWithoutCallbacks][] { export function getFuncArgs(
if (domain.tag === 'labeledProduct') { domain:
return Object.entries(domain.fields).map(([label, type]) => [label, type]); | LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
} else if (domain.tag === 'unlabeledProduct') { | UnlabeledProductType
return domain.items.map((type, index) => ['arg' + index, type]); | NilType,
): [string, SimpleTypes | ArrowWithoutCallbacks][] {
if (domain.tag === "labeledProduct") {
return Object.entries(domain.fields).map(([label, type]) => {
return [label, type];
});
} else if (domain.tag === "unlabeledProduct") {
return domain.items.map((type, index) => {
return ["arg" + index, type];
});
} else { } else {
return []; return [];
} }
} }
export function recursiveRenameLaquaProps(obj: unknown): unknown { export function recursiveRenameLaquaProps(obj: JSONValue): unknown {
if (typeof obj !== 'object' || obj === null) return obj; if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map(item => recursiveRenameLaquaProps(item)); return obj.map((item) => {
return recursiveRenameLaquaProps(item);
});
} }
return Object.getOwnPropertyNames(obj).reduce((acc, prop) => { return Object.getOwnPropertyNames(obj).reduce((acc, prop) => {
let accessProp = prop; let accessProp = prop;
if (prop.includes('Laqua_js')) {
if (prop.includes("Laqua_js")) {
// Last part of the property separated by "_" is a correct name // Last part of the property separated by "_" is a correct name
const refinedProperty = prop.split('_').pop()!; const refinedProperty = prop.split("_").pop();
if (refinedProperty == null) {
throw new Error(`Bad property name: ${prop}.`);
}
if (refinedProperty in obj) { if (refinedProperty in obj) {
accessProp = refinedProperty; accessProp = refinedProperty;
} }
} }
assert(accessProp in obj);
return { return {
...acc, ...acc,
[accessProp]: recursiveRenameLaquaProps(obj[accessProp as keyof typeof obj]) [accessProp]: recursiveRenameLaquaProps(obj[accessProp]),
}; };
}, {}); }, {});
} }

View File

@ -6,5 +6,5 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/**/__test__"], "exclude": ["node_modules", "dist", "src/**/__test__"]
} }

View File

@ -2,85 +2,74 @@
## [0.8.2](https://github.com/fluencelabs/js-client/compare/interfaces-v0.8.1...interfaces-v0.8.2) (2023-08-24) ## [0.8.2](https://github.com/fluencelabs/js-client/compare/interfaces-v0.8.1...interfaces-v0.8.2) (2023-08-24)
### Features ### Features
* use marine-js 0.7.2 ([#321](https://github.com/fluencelabs/js-client/issues/321)) ([c99a509](https://github.com/fluencelabs/js-client/commit/c99a509c8743471856b0beb25696ffe7357d5399)) - use marine-js 0.7.2 ([#321](https://github.com/fluencelabs/js-client/issues/321)) ([c99a509](https://github.com/fluencelabs/js-client/commit/c99a509c8743471856b0beb25696ffe7357d5399))
## [0.8.1](https://github.com/fluencelabs/js-client/compare/interfaces-v0.8.0...interfaces-v0.8.1) (2023-08-08) ## [0.8.1](https://github.com/fluencelabs/js-client/compare/interfaces-v0.8.0...interfaces-v0.8.1) (2023-08-08)
### Bug Fixes ### Bug Fixes
* **deps:** update dependency @fluencelabs/avm to v0.43.1 ([#322](https://github.com/fluencelabs/js-client/issues/322)) ([c1d1fa6](https://github.com/fluencelabs/js-client/commit/c1d1fa6659b6dc2c6707786748b3410fab7f1bcd)) - **deps:** update dependency @fluencelabs/avm to v0.43.1 ([#322](https://github.com/fluencelabs/js-client/issues/322)) ([c1d1fa6](https://github.com/fluencelabs/js-client/commit/c1d1fa6659b6dc2c6707786748b3410fab7f1bcd))
## [0.8.0](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.6...interfaces-v0.8.0) (2023-06-29) ## [0.8.0](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.6...interfaces-v0.8.0) (2023-06-29)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) - **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315)
### Features ### Features
* **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) ([8bae6e2](https://github.com/fluencelabs/js-client/commit/8bae6e24e62153b567f320ccecc7bce76bc826d1)) - **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) ([8bae6e2](https://github.com/fluencelabs/js-client/commit/8bae6e24e62153b567f320ccecc7bce76bc826d1))
## [0.7.6](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.5...interfaces-v0.7.6) (2023-06-20) ## [0.7.6](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.5...interfaces-v0.7.6) (2023-06-20)
### Features ### Features
* support signatures [fixes DXJ-389] ([#310](https://github.com/fluencelabs/js-client/issues/310)) ([a60dfe0](https://github.com/fluencelabs/js-client/commit/a60dfe0d680b4d9ac5092dec64e2ebf478bf80eb)) - support signatures [fixes DXJ-389] ([#310](https://github.com/fluencelabs/js-client/issues/310)) ([a60dfe0](https://github.com/fluencelabs/js-client/commit/a60dfe0d680b4d9ac5092dec64e2ebf478bf80eb))
## [0.7.5](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.4...interfaces-v0.7.5) (2023-04-04) ## [0.7.5](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.4...interfaces-v0.7.5) (2023-04-04)
### Features ### Features
* Cleaning up technical debts ([#295](https://github.com/fluencelabs/js-client/issues/295)) ([0b2f12d](https://github.com/fluencelabs/js-client/commit/0b2f12d8ac223db341d6c30ff403166b3eae2e56)) - Cleaning up technical debts ([#295](https://github.com/fluencelabs/js-client/issues/295)) ([0b2f12d](https://github.com/fluencelabs/js-client/commit/0b2f12d8ac223db341d6c30ff403166b3eae2e56))
## [0.7.4](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.3...interfaces-v0.7.4) (2023-03-31) ## [0.7.4](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.3...interfaces-v0.7.4) (2023-03-31)
### Features ### Features
* **logs:** Use `debug.js` library for logging [DXJ-327] ([#285](https://github.com/fluencelabs/js-client/issues/285)) ([e95c34a](https://github.com/fluencelabs/js-client/commit/e95c34a79220bd8ecdcee806802ac3d69a2af0cb)) - **logs:** Use `debug.js` library for logging [DXJ-327] ([#285](https://github.com/fluencelabs/js-client/issues/285)) ([e95c34a](https://github.com/fluencelabs/js-client/commit/e95c34a79220bd8ecdcee806802ac3d69a2af0cb))
## [0.7.3](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.2...interfaces-v0.7.3) (2023-02-16) ## [0.7.3](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.2...interfaces-v0.7.3) (2023-02-16)
### Bug Fixes ### Bug Fixes
* Trigger release to publish packages that were built ([#262](https://github.com/fluencelabs/js-client/issues/262)) ([47abf38](https://github.com/fluencelabs/js-client/commit/47abf3882956ffbdc52df372db26ba6252e8306b)) - Trigger release to publish packages that were built ([#262](https://github.com/fluencelabs/js-client/issues/262)) ([47abf38](https://github.com/fluencelabs/js-client/commit/47abf3882956ffbdc52df372db26ba6252e8306b))
## [0.7.2](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.1...interfaces-v0.7.2) (2023-02-16) ## [0.7.2](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.1...interfaces-v0.7.2) (2023-02-16)
### Features ### Features
* Add `getRelayPeerId` method for `IFluenceClient` ([#260](https://github.com/fluencelabs/js-client/issues/260)) ([a10278a](https://github.com/fluencelabs/js-client/commit/a10278afaa782a307feb10c4eac060094c101230)) - Add `getRelayPeerId` method for `IFluenceClient` ([#260](https://github.com/fluencelabs/js-client/issues/260)) ([a10278a](https://github.com/fluencelabs/js-client/commit/a10278afaa782a307feb10c4eac060094c101230))
## [0.7.1](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.0...interfaces-v0.7.1) (2023-02-16) ## [0.7.1](https://github.com/fluencelabs/js-client/compare/interfaces-v0.7.0...interfaces-v0.7.1) (2023-02-16)
### Features ### Features
* Simplify JS Client public API ([#257](https://github.com/fluencelabs/js-client/issues/257)) ([9daaf41](https://github.com/fluencelabs/js-client/commit/9daaf410964d43228192c829c7ff785db6e88081)) - Simplify JS Client public API ([#257](https://github.com/fluencelabs/js-client/issues/257)) ([9daaf41](https://github.com/fluencelabs/js-client/commit/9daaf410964d43228192c829c7ff785db6e88081))
## [0.7.0](https://github.com/fluencelabs/fluence-js/compare/interfaces-v0.6.0...interfaces-v0.7.0) (2023-02-15) ## [0.7.0](https://github.com/fluencelabs/fluence-js/compare/interfaces-v0.6.0...interfaces-v0.7.0) (2023-02-15)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) - Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246))
* Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) - Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243))
### Features ### Features
* Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) ([d4bb8fb](https://github.com/fluencelabs/fluence-js/commit/d4bb8fb42964b3ba25154232980b9ae82c21e627)) - Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) ([d4bb8fb](https://github.com/fluencelabs/fluence-js/commit/d4bb8fb42964b3ba25154232980b9ae82c21e627))
* Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) ([9667c4f](https://github.com/fluencelabs/fluence-js/commit/9667c4fec6868f984bba13249f3c47d293396406)) - Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) ([9667c4f](https://github.com/fluencelabs/fluence-js/commit/9667c4fec6868f984bba13249f3c47d293396406))
### Bug Fixes ### Bug Fixes
* NodeJS package building ([#248](https://github.com/fluencelabs/fluence-js/issues/248)) ([0d05e51](https://github.com/fluencelabs/fluence-js/commit/0d05e517d89529af513fcb96cfa6c722ccc357a7)) - NodeJS package building ([#248](https://github.com/fluencelabs/fluence-js/issues/248)) ([0d05e51](https://github.com/fluencelabs/fluence-js/commit/0d05e517d89529af513fcb96cfa6c722ccc357a7))

View File

@ -51,6 +51,6 @@
"devDependencies": { "devDependencies": {
"@multiformats/multiaddr": "11.3.0", "@multiformats/multiaddr": "11.3.0",
"@fluencelabs/avm": "0.48.0", "@fluencelabs/avm": "0.48.0",
"@fluencelabs/marine-js": "0.7.2" "hotscript": "1.0.13"
} }
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { SecurityTetraplet } from '@fluencelabs/avm';
import type { SecurityTetraplet } from "@fluencelabs/avm";
import { InterfaceToType, MaybePromise } from "./utils.js";
/** /**
* Peer ID's id as a base58 string (multihash/CIDv0). * Peer ID's id as a base58 string (multihash/CIDv0).
@ -32,7 +35,7 @@ export type Node = {
* Additional information about a service call * Additional information about a service call
* @typeparam ArgName * @typeparam ArgName
*/ */
export interface CallParams<ArgName extends string | null> { export type CallParams<ArgName extends string | null> = {
/** /**
* The identifier of particle which triggered the call * The identifier of particle which triggered the call
*/ */
@ -61,5 +64,24 @@ export interface CallParams<ArgName extends string | null> {
/** /**
* Security tetraplets * Security tetraplets
*/ */
tetraplets: ArgName extends string ? Record<ArgName, SecurityTetraplet[]> : Record<string, never>; 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
| boolean
| null
| { [x: string]: JSONValue }
| Array<JSONValue>;
export type JSONArray = Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,77 +13,85 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
type SimpleTypes = ScalarType | OptionType | ArrayType | StructType | TopType | BottomType | NilType;
export type NonArrowType = SimpleTypes | ProductType<SimpleTypes>; export type SimpleTypes =
| ScalarType
| OptionType
| ArrayType
| StructType
| TopType
| BottomType
| NilType;
export type NonArrowType = SimpleTypes | ProductType;
export type TopType = { export type TopType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'topType'; tag: "topType";
}; };
export type BottomType = { export type BottomType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'bottomType'; tag: "bottomType";
}; };
export type OptionType = { export type OptionType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'option'; tag: "option";
/** /**
* Underlying type of the option * Underlying type of the option
*/ */
type: NonArrowType; type: SimpleTypes;
}; };
export type NilType = { export type NilType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'nil'; tag: "nil";
}; };
export type ArrayType = { export type ArrayType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'array'; tag: "array";
/** /**
* Type of array elements * Type of array elements
*/ */
type: NonArrowType; type: SimpleTypes;
}; };
/** /**
* All possible scalar type names * All possible scalar type names
*/ */
export type ScalarNames = export type ScalarNames =
| 'u8' | "u8"
| 'u16' | "u16"
| 'u32' | "u32"
| 'u64' | "u64"
| 'i8' | "i8"
| 'i16' | "i16"
| 'i32' | "i32"
| 'i64' | "i64"
| 'f32' | "f32"
| 'f64' | "f64"
| 'bool' | "bool"
| 'string'; | "string";
export type ScalarType = { export type ScalarType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'scalar'; tag: "scalar";
/** /**
* Name of the scalar type * Name of the scalar type
@ -95,7 +103,7 @@ export type StructType = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'struct'; tag: "struct";
/** /**
* Struct name * Struct name
@ -105,65 +113,75 @@ export type StructType = {
/** /**
* Struct fields * Struct fields
*/ */
fields: { [key: string]: NonArrowType }; fields: { [key: string]: SimpleTypes };
}; };
export type LabeledProductType<T> = { export type LabeledProductType<
T extends
| SimpleTypes
| ArrowType<LabeledProductType<SimpleTypes> | UnlabeledProductType> =
| SimpleTypes
| ArrowType<LabeledProductType<SimpleTypes> | UnlabeledProductType>,
K extends { [key: string]: T } = { [key: string]: T },
> = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'labeledProduct'; tag: "labeledProduct";
/** /**
* Labelled product fields * Labelled product fields
*/ */
fields: { [key: string]: T }; fields: K;
}; };
export type UnlabeledProductType<T> = { export type UnlabeledProductType<T extends Array<SimpleTypes> = SimpleTypes[]> =
{
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'unlabeledProduct'; tag: "unlabeledProduct";
/** /**
* Items in unlabelled product * Items in unlabelled product
*/ */
items: Array<T>; items: T;
}; };
export type ProductType<T> = UnlabeledProductType<T> | LabeledProductType<T> | NilType; export type ProductType = UnlabeledProductType | LabeledProductType;
/** /**
* ArrowType is a profunctor pointing its domain to codomain. * ArrowType is a profunctor pointing its domain to codomain.
* Profunctor means variance: Arrow is contravariant on domain, and variant on codomain. * Profunctor means variance: Arrow is contravariant on domain, and variant on codomain.
*/ */
export type ArrowType<T> = { export type ArrowType<T extends LabeledProductType | UnlabeledProductType> = {
/** /**
* Type descriptor. Used for pattern-matching * Type descriptor. Used for pattern-matching
*/ */
tag: 'arrow'; tag: "arrow";
/** /**
* Where this Arrow is defined * Where this Arrow is defined
*/ */
domain: ProductType<T>; domain: T | NilType;
/** /**
* Where this Arrow points to * Where this Arrow points to
*/ */
codomain: UnlabeledProductType<NonArrowType> | NilType; codomain: UnlabeledProductType | NilType;
}; };
/** /**
* Arrow which domain contains only non-arrow types * Arrow which domain contains only non-arrow types
*/ */
export type ArrowWithoutCallbacks = ArrowType<NonArrowType>; export type ArrowWithoutCallbacks = ArrowType<
UnlabeledProductType | LabeledProductType<SimpleTypes>
>;
/** /**
* Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows) * Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows)
*/ */
export type ArrowWithCallbacks = ArrowType<NonArrowType | ArrowWithoutCallbacks>; export type ArrowWithCallbacks = ArrowType<LabeledProductType>;
export interface FunctionCallConstants { export interface FunctionCallConstants {
/** /**
@ -214,7 +232,9 @@ export interface FunctionCallDef {
/** /**
* Underlying arrow which represents function in aqua * Underlying arrow which represents function in aqua
*/ */
arrow: ArrowWithCallbacks; arrow: ArrowType<
LabeledProductType<SimpleTypes | ArrowType<UnlabeledProductType>>
>;
/** /**
* Names of the different entities used in generated air script * Names of the different entities used in generated air script
@ -234,7 +254,9 @@ export interface ServiceDef {
/** /**
* List of functions which the service consists of * List of functions which the service consists of
*/ */
functions: LabeledProductType<ArrowWithoutCallbacks> | NilType; functions:
| LabeledProductType<ArrowType<LabeledProductType<SimpleTypes>>>
| NilType;
} }
/** /**
@ -253,17 +275,17 @@ export const getArgumentTypes = (
): { ): {
[key: string]: NonArrowType | ArrowWithoutCallbacks; [key: string]: NonArrowType | ArrowWithoutCallbacks;
} => { } => {
if (def.arrow.domain.tag !== 'labeledProduct') { if (def.arrow.domain.tag !== "labeledProduct") {
throw new Error('Should be impossible'); throw new Error("Should be impossible");
} }
return def.arrow.domain.fields; return def.arrow.domain.fields;
}; };
export const isReturnTypeVoid = (def: FunctionCallDef): boolean => { export const isReturnTypeVoid = (def: FunctionCallDef): boolean => {
if (def.arrow.codomain.tag === 'nil') { if (def.arrow.codomain.tag === "nil") {
return true; return true;
} }
return def.arrow.codomain.items.length == 0; return def.arrow.codomain.items.length === 0;
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,13 +13,27 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { IFluenceInternalApi } from '../fluenceClient.js';
import { FnConfig, FunctionCallDef, ServiceDef } from './aquaTypeDefinitions.js'; import { JSONValue } from "../commonTypes.js";
import { IFluenceInternalApi } from "../fluenceClient.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 * Arguments passed to Aqua function
*/ */
export type PassedArgs = { [key: string]: any }; export type PassedArgs = { [key: string]: JSONValue | ArgCallbackFunction };
/** /**
* Arguments for callAquaFunction function * Arguments for callAquaFunction function
@ -54,7 +68,9 @@ export interface CallAquaFunctionArgs {
/** /**
* Call a function from Aqua script * Call a function from Aqua script
*/ */
export type CallAquaFunctionType = (args: CallAquaFunctionArgs) => Promise<unknown>; export type CallAquaFunctionType = (
args: CallAquaFunctionArgs,
) => Promise<unknown>;
/** /**
* Arguments for registerService function * Arguments for registerService function
@ -78,7 +94,7 @@ export interface RegisterServiceArgs {
/** /**
* Service implementation * Service implementation
*/ */
service: any; service: unknown;
} }
/** /**

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { Node } from './commonTypes.js';
import type { Node } from "./commonTypes.js";
/** /**
* A node in Fluence network a client can connect to. * A node in Fluence network a client can connect to.
@ -26,14 +27,14 @@ export type RelayOptions = string | Node;
/** /**
* Fluence Peer's key pair types * Fluence Peer's key pair types
*/ */
export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1'; export type KeyTypes = "RSA" | "Ed25519" | "secp256k1";
/** /**
* Options to specify key pair used in Fluence Peer * Options to specify key pair used in Fluence Peer
*/ */
export type KeyPairOptions = { export type KeyPairOptions = {
type: 'Ed25519'; type: "Ed25519";
source: 'random' | Uint8Array; source: "random" | Uint8Array;
}; };
/** /**
@ -97,7 +98,12 @@ export interface ClientConfig {
/** /**
* Fluence JS Client connection states as string literals * Fluence JS Client connection states as string literals
*/ */
export const ConnectionStates = ['disconnected', 'connecting', 'connected', 'disconnecting'] as const; export const ConnectionStates = [
"disconnected",
"connecting",
"connected",
"disconnecting",
] as const;
/** /**
* Fluence JS Client connection states * Fluence JS Client connection states
@ -108,7 +114,7 @@ export interface IFluenceInternalApi {
/** /**
* Internal API * Internal API
*/ */
internals: any; internals: unknown;
} }
/** /**
@ -128,7 +134,9 @@ export interface IFluenceClient extends IFluenceInternalApi {
/** /**
* Handle connection state changes. Immediately returns current connection state * Handle connection state changes. Immediately returns current connection state
*/ */
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState; onConnectionStateChange(
handler: (state: ConnectionState) => void,
): ConnectionState;
/** /**
* Return peer's secret key as byte array. * Return peer's secret key as byte array.
@ -145,7 +153,3 @@ export interface IFluenceClient extends IFluenceInternalApi {
*/ */
getRelayPeerId(): string; getRelayPeerId(): string;
} }

View File

@ -0,0 +1,90 @@
/**
* 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 {
ArrayType,
ArrowType,
LabeledProductType,
NilType,
OptionType,
ScalarType,
SimpleTypes,
StructType,
TopType,
UnlabeledProductType,
} from "@fluencelabs/interfaces";
import { Call, Pipe, Objects, Tuples, Unions, Fn } from "hotscript";
// Type definitions for inferring ts types from air json definition
// In the future we may remove string type declaration and move to type inference.
type GetTsTypeFromScalar<T extends ScalarType> = [T["name"]] extends [
"u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f32" | "f64",
]
? number
: [T["name"]] extends ["bool"]
? boolean
: [T["name"]] extends ["string"]
? string
: never;
type MapTuple<T> = {
[K in keyof T]: [T[K]] extends [SimpleTypes] ? GetSimpleType<T[K]> : never;
};
type UnpackIfSingle<T> = [T] extends [[infer R]] ? R : T;
type GetSimpleType<T> = [T] extends [NilType]
? null
: [T] extends [ArrayType]
? GetSimpleType<T["type"]>[]
: [T] extends [StructType]
? { [K in keyof T["fields"]]: GetSimpleType<T["fields"][K]> }
: [T] extends [OptionType]
? GetSimpleType<T["type"]> | null
: [T] extends [ScalarType]
? GetTsTypeFromScalar<T>
: [T] extends [TopType]
? unknown
: never;
interface Access<T> extends Fn {
return: __GetTsType<Call<Objects.Get<this["arg0"]>, T>>;
}
type __GetTsType<T> = [T] extends [SimpleTypes]
? GetSimpleType<T>
: [T] extends [UnlabeledProductType]
? MapTuple<T["items"]>
: [T] extends [LabeledProductType]
? { [K in keyof T["fields"]]: __GetTsType<T["fields"][K]> }
: [T] extends [ArrowType<infer H>]
? (
...t: [H] extends [UnlabeledProductType<infer K>]
? MapTuple<K>
: [H] extends [LabeledProductType<infer _V, infer K>]
? Pipe<K, [Objects.Keys, Unions.ToTuple, Tuples.Map<Access<K>>]>
: []
) => [T["codomain"]] extends [UnlabeledProductType]
? UnpackIfSingle<MapTuple<T["codomain"]["items"]>>
: undefined
: never;
type DeepMutable<T> = {
-readonly [K in keyof T]: DeepMutable<T[K]>;
};
export type GetTsType<T> = __GetTsType<DeepMutable<T>>;

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export * from './compilerSupport/aquaTypeDefinitions.js';
export * from './compilerSupport/compilerSupportInterface.js'; export * from "./compilerSupport/aquaTypeDefinitions.js";
export * from './commonTypes.js'; export * from "./compilerSupport/compilerSupportInterface.js";
export * from './fluenceClient.js'; export * from "./commonTypes.js";
export * from "./fluenceClient.js";
export * from "./future.js";

View File

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

View File

@ -1,7 +1,8 @@
{ {
"extends": "../../../tsconfig.json", "extends": "../../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./dist" "outDir": "./dist",
"rootDir": "src"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]

View File

@ -25,271 +25,234 @@
## [0.1.7](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.6...js-client-v0.1.7) (2023-09-22) ## [0.1.7](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.6...js-client-v0.1.7) (2023-09-22)
### Bug Fixes ### Bug Fixes
* **deps:** update dependency @fluencelabs/avm to v0.48.0 ([#350](https://github.com/fluencelabs/js-client/issues/350)) ([945908a](https://github.com/fluencelabs/js-client/commit/945908a992976f2ad953bcaa3918741f890ffeeb)) - **deps:** update dependency @fluencelabs/avm to v0.48.0 ([#350](https://github.com/fluencelabs/js-client/issues/350)) ([945908a](https://github.com/fluencelabs/js-client/commit/945908a992976f2ad953bcaa3918741f890ffeeb))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* devDependencies - devDependencies
* @fluencelabs/marine-worker bumped to 0.3.3 - @fluencelabs/marine-worker bumped to 0.3.3
## [0.1.6](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.5...js-client-v0.1.6) (2023-09-15) ## [0.1.6](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.5...js-client-v0.1.6) (2023-09-15)
### Bug Fixes ### Bug Fixes
* **deps:** update dependency @fluencelabs/avm to v0.47.0 ([#341](https://github.com/fluencelabs/js-client/issues/341)) ([f186f20](https://github.com/fluencelabs/js-client/commit/f186f209366c29f12e6677e03564ee2fa14b51ae)) - **deps:** update dependency @fluencelabs/avm to v0.47.0 ([#341](https://github.com/fluencelabs/js-client/issues/341)) ([f186f20](https://github.com/fluencelabs/js-client/commit/f186f209366c29f12e6677e03564ee2fa14b51ae))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* devDependencies - devDependencies
* @fluencelabs/marine-worker bumped to 0.3.2 - @fluencelabs/marine-worker bumped to 0.3.2
## [0.1.5](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.4...js-client-v0.1.5) (2023-09-14) ## [0.1.5](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.4...js-client-v0.1.5) (2023-09-14)
### Bug Fixes ### Bug Fixes
* **libp2p:** Add fluence protocol to local peer store protocols [fixes DXJ-471] ([#343](https://github.com/fluencelabs/js-client/issues/343)) ([88fcf02](https://github.com/fluencelabs/js-client/commit/88fcf02d5fd3d28db619427c31b38154646f7ad2)) - **libp2p:** Add fluence protocol to local peer store protocols [fixes DXJ-471] ([#343](https://github.com/fluencelabs/js-client/issues/343)) ([88fcf02](https://github.com/fluencelabs/js-client/commit/88fcf02d5fd3d28db619427c31b38154646f7ad2))
## [0.1.4](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.3...js-client-v0.1.4) (2023-09-14) ## [0.1.4](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.3...js-client-v0.1.4) (2023-09-14)
### Bug Fixes ### Bug Fixes
* Fire and forget [fixes DXJ-446] ([#336](https://github.com/fluencelabs/js-client/issues/336)) ([e0a970d](https://github.com/fluencelabs/js-client/commit/e0a970d86a13f1617778a461c1c4d558d7dbafcb)) - Fire and forget [fixes DXJ-446] ([#336](https://github.com/fluencelabs/js-client/issues/336)) ([e0a970d](https://github.com/fluencelabs/js-client/commit/e0a970d86a13f1617778a461c1c4d558d7dbafcb))
## [0.1.3](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.2...js-client-v0.1.3) (2023-09-07) ## [0.1.3](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.2...js-client-v0.1.3) (2023-09-07)
### Bug Fixes ### Bug Fixes
* **deps:** update dependency @fluencelabs/avm to v0.46.0 ([#338](https://github.com/fluencelabs/js-client/issues/338)) ([8e6918c](https://github.com/fluencelabs/js-client/commit/8e6918c4da5bc4cdfe1c840312f477d782d9ca20)) - **deps:** update dependency @fluencelabs/avm to v0.46.0 ([#338](https://github.com/fluencelabs/js-client/issues/338)) ([8e6918c](https://github.com/fluencelabs/js-client/commit/8e6918c4da5bc4cdfe1c840312f477d782d9ca20))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* devDependencies - devDependencies
* @fluencelabs/marine-worker bumped to 0.3.1 - @fluencelabs/marine-worker bumped to 0.3.1
## [0.1.2](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.1...js-client-v0.1.2) (2023-09-05) ## [0.1.2](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.1...js-client-v0.1.2) (2023-09-05)
### Features ### Features
* remove obsolete packages [fixes DXJ-462] ([#337](https://github.com/fluencelabs/js-client/issues/337)) ([e7e6176](https://github.com/fluencelabs/js-client/commit/e7e617661f39e1df36a703d5dad93ba52a338919)) - remove obsolete packages [fixes DXJ-462] ([#337](https://github.com/fluencelabs/js-client/issues/337)) ([e7e6176](https://github.com/fluencelabs/js-client/commit/e7e617661f39e1df36a703d5dad93ba52a338919))
### Bug Fixes ### Bug Fixes
* **logger:** Change formatter that collides with new libp2p version [fixes DXJ-459] ([#334](https://github.com/fluencelabs/js-client/issues/334)) ([18a972b](https://github.com/fluencelabs/js-client/commit/18a972b573559d0717ec93a95b8c63dd1cbcd93b)) - **logger:** Change formatter that collides with new libp2p version [fixes DXJ-459] ([#334](https://github.com/fluencelabs/js-client/issues/334)) ([18a972b](https://github.com/fluencelabs/js-client/commit/18a972b573559d0717ec93a95b8c63dd1cbcd93b))
## [0.1.1](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.0...js-client-v0.1.1) (2023-08-25) ## [0.1.1](https://github.com/fluencelabs/js-client/compare/js-client-v0.1.0...js-client-v0.1.1) (2023-08-25)
### Bug Fixes ### Bug Fixes
* Use info log level instead trace [Fixes DXJ-457] ([#328](https://github.com/fluencelabs/js-client/issues/328)) ([477c6f0](https://github.com/fluencelabs/js-client/commit/477c6f0c151ef6759aaa2802c5e9907065d58e17)) - Use info log level instead trace [Fixes DXJ-457] ([#328](https://github.com/fluencelabs/js-client/issues/328)) ([477c6f0](https://github.com/fluencelabs/js-client/commit/477c6f0c151ef6759aaa2802c5e9907065d58e17))
## [0.1.0](https://github.com/fluencelabs/js-client/compare/js-client-v0.0.10...js-client-v0.1.0) (2023-08-24) ## [0.1.0](https://github.com/fluencelabs/js-client/compare/js-client-v0.0.10...js-client-v0.1.0) (2023-08-24)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* Unify all packages ([#327](https://github.com/fluencelabs/js-client/issues/327)) - Unify all packages ([#327](https://github.com/fluencelabs/js-client/issues/327))
### Features ### Features
* Unify all packages ([#327](https://github.com/fluencelabs/js-client/issues/327)) ([97c2491](https://github.com/fluencelabs/js-client/commit/97c24918d84b34e7ac58337838dc8343cbd44b19)) - Unify all packages ([#327](https://github.com/fluencelabs/js-client/issues/327)) ([97c2491](https://github.com/fluencelabs/js-client/commit/97c24918d84b34e7ac58337838dc8343cbd44b19))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.8.1 to 0.8.2 - @fluencelabs/interfaces bumped from 0.8.1 to 0.8.2
* devDependencies - devDependencies
* @fluencelabs/marine-worker bumped to 0.3.0 - @fluencelabs/marine-worker bumped to 0.3.0
## [0.9.1](https://github.com/fluencelabs/js-client/compare/js-peer-v0.9.0...js-peer-v0.9.1) (2023-08-08) ## [0.9.1](https://github.com/fluencelabs/js-client/compare/js-peer-v0.9.0...js-peer-v0.9.1) (2023-08-08)
### Bug Fixes ### Bug Fixes
* **deps:** update dependency @fluencelabs/avm to v0.43.1 ([#322](https://github.com/fluencelabs/js-client/issues/322)) ([c1d1fa6](https://github.com/fluencelabs/js-client/commit/c1d1fa6659b6dc2c6707786748b3410fab7f1bcd)) - **deps:** update dependency @fluencelabs/avm to v0.43.1 ([#322](https://github.com/fluencelabs/js-client/issues/322)) ([c1d1fa6](https://github.com/fluencelabs/js-client/commit/c1d1fa6659b6dc2c6707786748b3410fab7f1bcd))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.8.0 to 0.8.1 - @fluencelabs/interfaces bumped from 0.8.0 to 0.8.1
## [0.9.0](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.10...js-peer-v0.9.0) (2023-06-29) ## [0.9.0](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.10...js-peer-v0.9.0) (2023-06-29)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) - **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315)
### Features ### Features
* **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) ([8bae6e2](https://github.com/fluencelabs/js-client/commit/8bae6e24e62153b567f320ccecc7bce76bc826d1)) - **avm:** avm 0.40.0 (https://github.com/fluencelabs/js-client/pull/315) ([8bae6e2](https://github.com/fluencelabs/js-client/commit/8bae6e24e62153b567f320ccecc7bce76bc826d1))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.6 to 0.8.0 - @fluencelabs/interfaces bumped from 0.7.6 to 0.8.0
## [0.8.10](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.9...js-peer-v0.8.10) (2023-06-20) ## [0.8.10](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.9...js-peer-v0.8.10) (2023-06-20)
### Features ### Features
* support signatures [fixes DXJ-389] ([#310](https://github.com/fluencelabs/js-client/issues/310)) ([a60dfe0](https://github.com/fluencelabs/js-client/commit/a60dfe0d680b4d9ac5092dec64e2ebf478bf80eb)) - support signatures [fixes DXJ-389] ([#310](https://github.com/fluencelabs/js-client/issues/310)) ([a60dfe0](https://github.com/fluencelabs/js-client/commit/a60dfe0d680b4d9ac5092dec64e2ebf478bf80eb))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.5 to 0.7.6 - @fluencelabs/interfaces bumped from 0.7.5 to 0.7.6
## [0.8.9](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.8...js-peer-v0.8.9) (2023-06-14) ## [0.8.9](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.8...js-peer-v0.8.9) (2023-06-14)
### Features ### Features
* Add tracing service [fixes DXJ-388] ([#307](https://github.com/fluencelabs/js-client/issues/307)) ([771086f](https://github.com/fluencelabs/js-client/commit/771086fddf52b7a5a1280894c7238e409cdf6a64)) - Add tracing service [fixes DXJ-388] ([#307](https://github.com/fluencelabs/js-client/issues/307)) ([771086f](https://github.com/fluencelabs/js-client/commit/771086fddf52b7a5a1280894c7238e409cdf6a64))
* improve ttl error message ([#300](https://github.com/fluencelabs/js-client/issues/300)) ([9821183](https://github.com/fluencelabs/js-client/commit/9821183d53870240cb5700be67cb8d57533b954b)) - improve ttl error message ([#300](https://github.com/fluencelabs/js-client/issues/300)) ([9821183](https://github.com/fluencelabs/js-client/commit/9821183d53870240cb5700be67cb8d57533b954b))
## [0.8.8](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.7...js-peer-v0.8.8) (2023-05-30) ## [0.8.8](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.7...js-peer-v0.8.8) (2023-05-30)
### Features ### Features
* add run-console ([#305](https://github.com/fluencelabs/js-client/issues/305)) ([cf1f029](https://github.com/fluencelabs/js-client/commit/cf1f02963c1d7e1a17866f5798901a0f61b8bc31)) - add run-console ([#305](https://github.com/fluencelabs/js-client/issues/305)) ([cf1f029](https://github.com/fluencelabs/js-client/commit/cf1f02963c1d7e1a17866f5798901a0f61b8bc31))
## [0.8.7](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.6...js-peer-v0.8.7) (2023-04-04) ## [0.8.7](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.6...js-peer-v0.8.7) (2023-04-04)
### Features ### Features
* Cleaning up technical debts ([#295](https://github.com/fluencelabs/js-client/issues/295)) ([0b2f12d](https://github.com/fluencelabs/js-client/commit/0b2f12d8ac223db341d6c30ff403166b3eae2e56)) - Cleaning up technical debts ([#295](https://github.com/fluencelabs/js-client/issues/295)) ([0b2f12d](https://github.com/fluencelabs/js-client/commit/0b2f12d8ac223db341d6c30ff403166b3eae2e56))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.4 to 0.7.5 - @fluencelabs/interfaces bumped from 0.7.4 to 0.7.5
## [0.8.6](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.5...js-peer-v0.8.6) (2023-03-31) ## [0.8.6](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.5...js-peer-v0.8.6) (2023-03-31)
### Features ### Features
* **logs:** Use `debug.js` library for logging [DXJ-327] ([#285](https://github.com/fluencelabs/js-client/issues/285)) ([e95c34a](https://github.com/fluencelabs/js-client/commit/e95c34a79220bd8ecdcee806802ac3d69a2af0cb)) - **logs:** Use `debug.js` library for logging [DXJ-327] ([#285](https://github.com/fluencelabs/js-client/issues/285)) ([e95c34a](https://github.com/fluencelabs/js-client/commit/e95c34a79220bd8ecdcee806802ac3d69a2af0cb))
* **test:** Automate smoke tests for JS Client [DXJ-293] ([#282](https://github.com/fluencelabs/js-client/issues/282)) ([10d7eae](https://github.com/fluencelabs/js-client/commit/10d7eaed809dde721b582d4b3228a48bbec50884)) - **test:** Automate smoke tests for JS Client [DXJ-293] ([#282](https://github.com/fluencelabs/js-client/issues/282)) ([10d7eae](https://github.com/fluencelabs/js-client/commit/10d7eaed809dde721b582d4b3228a48bbec50884))
### Bug Fixes ### Bug Fixes
* **test:** All tests are working with vitest [DXJ-306] ([#291](https://github.com/fluencelabs/js-client/issues/291)) ([58ad3ca](https://github.com/fluencelabs/js-client/commit/58ad3ca6f666e8580997bb47609947645903436d)) - **test:** All tests are working with vitest [DXJ-306] ([#291](https://github.com/fluencelabs/js-client/issues/291)) ([58ad3ca](https://github.com/fluencelabs/js-client/commit/58ad3ca6f666e8580997bb47609947645903436d))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.3 to 0.7.4 - @fluencelabs/interfaces bumped from 0.7.3 to 0.7.4
## [0.8.5](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.4...js-peer-v0.8.5) (2023-03-03) ## [0.8.5](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.4...js-peer-v0.8.5) (2023-03-03)
### Bug Fixes ### Bug Fixes
* Increase number of inbound and outbound streams to 1024 ([#280](https://github.com/fluencelabs/js-client/issues/280)) ([1ccc483](https://github.com/fluencelabs/js-client/commit/1ccc4835328426b546f31e1646d3a49ed042fdf9)) - Increase number of inbound and outbound streams to 1024 ([#280](https://github.com/fluencelabs/js-client/issues/280)) ([1ccc483](https://github.com/fluencelabs/js-client/commit/1ccc4835328426b546f31e1646d3a49ed042fdf9))
## [0.8.4](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.3...js-peer-v0.8.4) (2023-02-22) ## [0.8.4](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.3...js-peer-v0.8.4) (2023-02-22)
### Bug Fixes ### Bug Fixes
* `nodenext` moduleResolution for js peer ([#271](https://github.com/fluencelabs/js-client/issues/271)) ([78d98f1](https://github.com/fluencelabs/js-client/commit/78d98f15c12431dee9fdd7b9869d57760503f8c7)) - `nodenext` moduleResolution for js peer ([#271](https://github.com/fluencelabs/js-client/issues/271)) ([78d98f1](https://github.com/fluencelabs/js-client/commit/78d98f15c12431dee9fdd7b9869d57760503f8c7))
## [0.8.3](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.2...js-peer-v0.8.3) (2023-02-16) ## [0.8.3](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.2...js-peer-v0.8.3) (2023-02-16)
### Bug Fixes ### Bug Fixes
* Trigger release to publish packages that were built ([#262](https://github.com/fluencelabs/js-client/issues/262)) ([47abf38](https://github.com/fluencelabs/js-client/commit/47abf3882956ffbdc52df372db26ba6252e8306b)) - Trigger release to publish packages that were built ([#262](https://github.com/fluencelabs/js-client/issues/262)) ([47abf38](https://github.com/fluencelabs/js-client/commit/47abf3882956ffbdc52df372db26ba6252e8306b))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.2 to 0.7.3 - @fluencelabs/interfaces bumped from 0.7.2 to 0.7.3
## [0.8.2](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.1...js-peer-v0.8.2) (2023-02-16) ## [0.8.2](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.1...js-peer-v0.8.2) (2023-02-16)
### Features ### Features
* Add `getRelayPeerId` method for `IFluenceClient` ([#260](https://github.com/fluencelabs/js-client/issues/260)) ([a10278a](https://github.com/fluencelabs/js-client/commit/a10278afaa782a307feb10c4eac060094c101230)) - Add `getRelayPeerId` method for `IFluenceClient` ([#260](https://github.com/fluencelabs/js-client/issues/260)) ([a10278a](https://github.com/fluencelabs/js-client/commit/a10278afaa782a307feb10c4eac060094c101230))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.1 to 0.7.2 - @fluencelabs/interfaces bumped from 0.7.1 to 0.7.2
## [0.8.1](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.0...js-peer-v0.8.1) (2023-02-16) ## [0.8.1](https://github.com/fluencelabs/js-client/compare/js-peer-v0.8.0...js-peer-v0.8.1) (2023-02-16)
### Features ### Features
* Simplify JS Client public API ([#257](https://github.com/fluencelabs/js-client/issues/257)) ([9daaf41](https://github.com/fluencelabs/js-client/commit/9daaf410964d43228192c829c7ff785db6e88081)) - Simplify JS Client public API ([#257](https://github.com/fluencelabs/js-client/issues/257)) ([9daaf41](https://github.com/fluencelabs/js-client/commit/9daaf410964d43228192c829c7ff785db6e88081))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.7.0 to 0.7.1 - @fluencelabs/interfaces bumped from 0.7.0 to 0.7.1
## [0.8.0](https://github.com/fluencelabs/fluence-js/compare/js-peer-v0.7.0...js-peer-v0.8.0) (2023-02-15) ## [0.8.0](https://github.com/fluencelabs/fluence-js/compare/js-peer-v0.7.0...js-peer-v0.8.0) (2023-02-15)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) - Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246))
* Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) - Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243))
### Features ### Features
* Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) ([d4bb8fb](https://github.com/fluencelabs/fluence-js/commit/d4bb8fb42964b3ba25154232980b9ae82c21e627)) - Expose updated JS Client API via `js-client.api` package ([#246](https://github.com/fluencelabs/fluence-js/issues/246)) ([d4bb8fb](https://github.com/fluencelabs/fluence-js/commit/d4bb8fb42964b3ba25154232980b9ae82c21e627))
* Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) ([9667c4f](https://github.com/fluencelabs/fluence-js/commit/9667c4fec6868f984bba13249f3c47d293396406)) - Standalone web JS Client ([#243](https://github.com/fluencelabs/fluence-js/issues/243)) ([9667c4f](https://github.com/fluencelabs/fluence-js/commit/9667c4fec6868f984bba13249f3c47d293396406))
### Bug Fixes ### Bug Fixes
* NodeJS package building ([#248](https://github.com/fluencelabs/fluence-js/issues/248)) ([0d05e51](https://github.com/fluencelabs/fluence-js/commit/0d05e517d89529af513fcb96cfa6c722ccc357a7)) - NodeJS package building ([#248](https://github.com/fluencelabs/fluence-js/issues/248)) ([0d05e51](https://github.com/fluencelabs/fluence-js/commit/0d05e517d89529af513fcb96cfa6c722ccc357a7))
### Dependencies ### Dependencies
* The following workspace dependencies were updated - The following workspace dependencies were updated
* dependencies - dependencies
* @fluencelabs/interfaces bumped from 0.6.0 to 0.7.0 - @fluencelabs/interfaces bumped from 0.6.0 to 0.7.0

View File

@ -17,6 +17,12 @@
"node": "./dist/index.js", "node": "./dist/index.js",
"default": "./dist/browser/index.js" "default": "./dist/browser/index.js"
}, },
"imports": {
"#fetcher": {
"node": "./dist/fetchers/node.js",
"default": "./dist/fetchers/browser.js"
}
},
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc && vite build", "build": "tsc && vite build",
@ -28,6 +34,7 @@
"dependencies": { "dependencies": {
"@chainsafe/libp2p-noise": "13.0.0", "@chainsafe/libp2p-noise": "13.0.0",
"@chainsafe/libp2p-yamux": "5.0.0", "@chainsafe/libp2p-yamux": "5.0.0",
"@fluencelabs/avm": "0.48.0",
"@fluencelabs/interfaces": "workspace:*", "@fluencelabs/interfaces": "workspace:*",
"@fluencelabs/marine-worker": "0.3.3", "@fluencelabs/marine-worker": "0.3.3",
"@libp2p/crypto": "2.0.3", "@libp2p/crypto": "2.0.3",
@ -36,6 +43,7 @@
"@libp2p/peer-id-factory": "3.0.3", "@libp2p/peer-id-factory": "3.0.3",
"@libp2p/websockets": "7.0.4", "@libp2p/websockets": "7.0.4",
"@multiformats/multiaddr": "11.3.0", "@multiformats/multiaddr": "11.3.0",
"assert": "2.1.0",
"async": "3.2.4", "async": "3.2.4",
"bs58": "5.0.0", "bs58": "5.0.0",
"buffer": "6.0.3", "buffer": "6.0.3",
@ -47,22 +55,23 @@
"libp2p": "0.46.6", "libp2p": "0.46.6",
"multiformats": "11.0.1", "multiformats": "11.0.1",
"rxjs": "7.5.5", "rxjs": "7.5.5",
"threads": "1.7.0", "threads": "fluencelabs/threads.js#b00a5342380b0278d3ae56dcfb170effb3cad7cd",
"ts-pattern": "3.3.3", "ts-pattern": "3.3.3",
"uint8arrays": "4.0.3", "uint8arrays": "4.0.3",
"uuid": "8.3.2" "uuid": "8.3.2",
"zod": "3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@fluencelabs/aqua-api": "0.9.3", "@fluencelabs/aqua-api": "0.9.3",
"@fluencelabs/avm": "0.48.0",
"@fluencelabs/marine-js": "0.7.2", "@fluencelabs/marine-js": "0.7.2",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.3",
"@types/bs58": "4.0.1", "@types/bs58": "4.0.1",
"@types/debug": "4.1.7", "@types/debug": "4.1.7",
"@types/node": "20.7.0", "@types/node": "20.7.0",
"@types/uuid": "8.3.2", "@types/uuid": "8.3.2",
"vite": "4.0.4", "hotscript": "1.0.13",
"vite": "4.4.11",
"vite-tsconfig-paths": "4.0.3", "vite-tsconfig-paths": "4.0.3",
"vitest": "0.29.7" "vitest": "0.34.6"
} }
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,13 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FnConfig, FunctionCallDef, ServiceDef } from '@fluencelabs/interfaces'; import type {
import type { IFluenceClient } from '@fluencelabs/interfaces'; FnConfig,
import { getArgumentTypes } from '@fluencelabs/interfaces'; FunctionCallDef,
import { callAquaFunction, Fluence, registerService } from './index.js'; ServiceDef,
import { FluencePeer } from './jsPeer/FluencePeer.js'; PassedArgs,
ServiceImpl,
} from "@fluencelabs/interfaces";
import { getArgumentTypes } from "@fluencelabs/interfaces";
export const isFluencePeer = (fluencePeerCandidate: unknown): fluencePeerCandidate is IFluenceClient => { 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; return fluencePeerCandidate instanceof FluencePeer;
}; };
@ -33,18 +42,18 @@ export const isFluencePeer = (fluencePeerCandidate: unknown): fluencePeerCandida
* @param script - air script with function execution logic generated by the Aqua compiler * @param script - air script with function execution logic generated by the Aqua compiler
*/ */
export const v5_callFunction = async ( export const v5_callFunction = async (
rawFnArgs: Array<any>, rawFnArgs: unknown[],
def: FunctionCallDef, def: FunctionCallDef,
script: string, script: string,
): Promise<unknown> => { ): Promise<unknown> => {
const { args, client: peer, config } = await extractFunctionArgs(rawFnArgs, def); const { args, client: peer, config } = extractFunctionArgs(rawFnArgs, def);
return callAquaFunction({ return callAquaFunction({
args, args,
def, def,
script, script,
config: config || {}, config,
peer: peer, peer,
}); });
}; };
@ -52,12 +61,17 @@ export const v5_callFunction = async (
* Convenience function to support Aqua `service` generation backend * Convenience function to support Aqua `service` generation backend
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script * The compiler only need to generate a call the function and provide the corresponding definitions and the air script
* @param args - raw arguments passed by user to the generated function * @param args - raw arguments passed by user to the generated function
* TODO: dont forget to add jsdoc for new arg
* @param def - service definition generated by the Aqua compiler * @param def - service definition generated by the Aqua compiler
*/ */
export const v5_registerService = async (args: any[], def: ServiceDef): Promise<unknown> => { export const v5_registerService = (args: unknown[], def: ServiceDef): void => {
const { peer, service, serviceId } = await extractServiceArgs(args, def.defaultServiceId); // TODO: Support this in aqua-to-js package
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const service: ServiceImpl = args.pop() as ServiceImpl;
return registerService({ const { peer, serviceId } = extractServiceArgs(args, def.defaultServiceId);
registerService({
def, def,
service, service,
serviceId, serviceId,
@ -65,6 +79,10 @@ export const v5_registerService = async (args: any[], def: ServiceDef): Promise<
}); });
}; };
function isConfig(arg: unknown): arg is FnConfig {
return typeof arg === "object" && arg !== null;
}
/** /**
* Arguments could be passed in one these configurations: * Arguments could be passed in one these configurations:
* [...actualArgs] * [...actualArgs]
@ -75,48 +93,60 @@ export const v5_registerService = async (args: any[], def: ServiceDef): Promise<
* This function select the appropriate configuration and returns * This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, config, args } * arguments in a structured way of: { peer, config, args }
*/ */
const extractFunctionArgs = async ( function extractFunctionArgs(
args: any[], args: unknown[],
def: FunctionCallDef, def: FunctionCallDef,
): Promise<{ ): {
client: IFluenceClient; client: FluencePeer;
config?: FnConfig; config: FnConfig;
args: { [key: string]: any }; args: PassedArgs;
}> => { } {
const argumentTypes = getArgumentTypes(def); const argumentTypes = getArgumentTypes(def);
const argumentNames = Object.keys(argumentTypes); const argumentNames = Object.keys(argumentTypes);
const numberOfExpectedArgs = argumentNames.length; const numberOfExpectedArgs = argumentNames.length;
let peer: IFluenceClient; let peer: FluencePeer;
let structuredArgs: any[];
let config: FnConfig; let config: FnConfig;
if (isFluencePeer(args[0])) { if (isFluencePeer(args[0])) {
peer = args[0]; peer = args[0];
structuredArgs = args.slice(1, numberOfExpectedArgs + 1); args = args.slice(1);
config = args[numberOfExpectedArgs + 1];
} else { } else {
if (!Fluence.defaultClient) { if (Fluence.defaultClient == null) {
throw new Error( throw new Error(
'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?', "Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?",
); );
} }
peer = Fluence.defaultClient; peer = Fluence.defaultClient;
structuredArgs = args.slice(0, numberOfExpectedArgs);
config = args[numberOfExpectedArgs];
} }
const maybeConfig = args[numberOfExpectedArgs];
if (isConfig(maybeConfig)) {
config = maybeConfig;
} else {
config = {};
}
const structuredArgs = args.slice(0, numberOfExpectedArgs);
if (structuredArgs.length !== numberOfExpectedArgs) { if (structuredArgs.length !== numberOfExpectedArgs) {
throw new Error(`Incorrect number of arguments. Expecting ${numberOfExpectedArgs}`); throw new Error(
`Incorrect number of arguments. Expecting ${numberOfExpectedArgs}`,
);
} }
const argsRes = argumentNames.reduce((acc, name, index) => ({ ...acc, [name]: structuredArgs[index] }), {}); const argsRes = argumentNames.reduce((acc, name, index) => {
return { ...acc, [name]: structuredArgs[index] };
}, {});
return { return {
client: peer, client: peer,
config: config,
args: argsRes, args: argsRes,
config: config,
}; };
}; }
/** /**
* Arguments could be passed in one these configurations: * Arguments could be passed in one these configurations:
@ -130,48 +160,37 @@ const extractFunctionArgs = async (
* This function select the appropriate configuration and returns * This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, serviceId, service } * arguments in a structured way of: { peer, serviceId, service }
*/ */
const extractServiceArgs = async ( const extractServiceArgs = (
args: any[], args: unknown[],
defaultServiceId?: string, defaultServiceId?: string,
): Promise<{ peer: IFluenceClient; serviceId: string; service: any }> => { ): {
let peer: IFluenceClient; peer: FluencePeer;
let serviceId: any; serviceId: string | undefined;
let service: any; } => {
let peer: FluencePeer;
let serviceId: string | undefined;
if (isFluencePeer(args[0])) { if (isFluencePeer(args[0])) {
peer = args[0]; peer = args[0];
args = args.slice(1);
} else { } else {
if (!Fluence.defaultClient) { if (Fluence.defaultClient == null) {
throw new Error( throw new Error(
'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?', "Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?",
); );
} }
peer = Fluence.defaultClient; peer = Fluence.defaultClient;
} }
if (typeof args[0] === 'string') { if (typeof args[0] === "string") {
serviceId = args[0]; serviceId = args[0];
} else if (typeof args[1] === 'string') {
serviceId = args[1];
} else { } else {
serviceId = defaultServiceId; serviceId = defaultServiceId;
} }
// Figuring out which overload is the service.
// If the first argument is not Fluence Peer and it is an object, then it can only be the service def
// If the first argument is peer, we are checking further. The second argument might either be
// an object, that it must be the service object
// or a string, which is the service id. In that case the service is the third argument
if (!isFluencePeer(args[0]) && typeof args[0] === 'object') {
service = args[0];
} else if (typeof args[1] === 'object') {
service = args[1];
} else {
service = args[2];
}
return { return {
peer: peer, peer,
serviceId: serviceId, serviceId,
service: service,
}; };
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,16 +13,26 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { ClientConfig, ConnectionState, IFluenceClient, PeerIdB58, RelayOptions } from '@fluencelabs/interfaces';
import { RelayConnection, RelayConnectionConfig } from '../connection/RelayConnection.js';
import { fromOpts, KeyPair } from '../keypair/index.js';
import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js';
import { relayOptionToMultiaddr } from '../util/libp2pUtils.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
import { logger } from '../util/logger.js';
const log = logger('client'); import {
ClientConfig,
ConnectionState,
IFluenceClient,
RelayOptions,
} from "@fluencelabs/interfaces";
import {
RelayConnection,
RelayConnectionConfig,
} from "../connection/RelayConnection.js";
import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { fromOpts, KeyPair } from "../keypair/index.js";
import { IMarineHost } from "../marine/interfaces.js";
import { relayOptionToMultiaddr } from "../util/libp2pUtils.js";
import { logger } from "../util/logger.js";
const log = logger("client");
const DEFAULT_TTL_MS = 7000; const DEFAULT_TTL_MS = 7000;
const MAX_OUTBOUND_STREAMS = 1024; const MAX_OUTBOUND_STREAMS = 1024;
@ -31,24 +41,34 @@ const MAX_INBOUND_STREAMS = 1024;
export const makeClientPeerConfig = async ( export const makeClientPeerConfig = async (
relay: RelayOptions, relay: RelayOptions,
config: ClientConfig, config: ClientConfig,
): Promise<{ peerConfig: PeerConfig; relayConfig: RelayConnectionConfig; keyPair: KeyPair }> => { ): Promise<{
const opts = config?.keyPair || { type: 'Ed25519', source: 'random' }; peerConfig: PeerConfig;
relayConfig: RelayConnectionConfig;
keyPair: KeyPair;
}> => {
const opts = config.keyPair ?? { type: "Ed25519", source: "random" };
const keyPair = await fromOpts(opts); const keyPair = await fromOpts(opts);
const relayAddress = relayOptionToMultiaddr(relay); const relayAddress = relayOptionToMultiaddr(relay);
return { return {
peerConfig: { peerConfig: {
debug: { debug: {
printParticleId: config?.debug?.printParticleId || false, printParticleId: config.debug?.printParticleId ?? false,
}, },
defaultTtlMs: config?.defaultTtlMs || DEFAULT_TTL_MS, defaultTtlMs: config.defaultTtlMs ?? DEFAULT_TTL_MS,
}, },
relayConfig: { relayConfig: {
peerId: keyPair.getLibp2pPeerId(), peerId: keyPair.getLibp2pPeerId(),
relayAddress: relayAddress, relayAddress: relayAddress,
dialTimeoutMs: config?.connectionOptions?.dialTimeoutMs, ...(config.connectionOptions?.dialTimeoutMs != null
maxInboundStreams: config?.connectionOptions?.maxInboundStreams || MAX_OUTBOUND_STREAMS, ? {
maxOutboundStreams: config?.connectionOptions?.maxOutboundStreams || MAX_INBOUND_STREAMS, dialTimeout: config.connectionOptions.dialTimeoutMs,
}
: {}),
maxInboundStreams:
config.connectionOptions?.maxInboundStreams ?? MAX_OUTBOUND_STREAMS,
maxOutboundStreams:
config.connectionOptions?.maxOutboundStreams ?? MAX_INBOUND_STREAMS,
}, },
keyPair: keyPair, keyPair: keyPair,
}; };
@ -61,7 +81,13 @@ export class ClientPeer extends FluencePeer implements IFluenceClient {
keyPair: KeyPair, keyPair: KeyPair,
marine: IMarineHost, marine: IMarineHost,
) { ) {
super(peerConfig, keyPair, marine, new JsServiceHost(), new RelayConnection(relayConfig)); super(
peerConfig,
keyPair,
marine,
new JsServiceHost(),
new RelayConnection(relayConfig),
);
} }
getPeerId(): string { getPeerId(): string {
@ -72,14 +98,16 @@ export class ClientPeer extends FluencePeer implements IFluenceClient {
return this.keyPair.toEd25519PrivateKey(); return this.keyPair.toEd25519PrivateKey();
} }
connectionState: ConnectionState = 'disconnected'; connectionState: ConnectionState = "disconnected";
connectionStateChangeHandler: (state: ConnectionState) => void = () => {}; connectionStateChangeHandler: (state: ConnectionState) => void = () => {};
getRelayPeerId(): string { getRelayPeerId(): string {
return this.internals.getRelayPeerId(); return this.internals.getRelayPeerId();
} }
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { onConnectionStateChange(
handler: (state: ConnectionState) => void,
): ConnectionState {
this.connectionStateChangeHandler = handler; this.connectionStateChangeHandler = handler;
return this.connectionState; return this.connectionState;
@ -104,20 +132,20 @@ export class ClientPeer extends FluencePeer implements IFluenceClient {
return this.stop(); return this.stop();
} }
async start(): Promise<void> { override async start(): Promise<void> {
log.trace('connecting to Fluence network'); log.trace("connecting to Fluence network");
this.changeConnectionState('connecting'); this.changeConnectionState("connecting");
await super.start(); await super.start();
// TODO: check connection (`checkConnection` function) here // TODO: check connection (`checkConnection` function) here
this.changeConnectionState('connected'); this.changeConnectionState("connected");
log.trace('connected'); log.trace("connected");
} }
async stop(): Promise<void> { override async stop(): Promise<void> {
log.trace('disconnecting from Fluence network'); log.trace("disconnecting from Fluence network");
this.changeConnectionState('disconnecting'); this.changeConnectionState("disconnecting");
await super.stop(); await super.stop();
this.changeConnectionState('disconnected'); this.changeConnectionState("disconnected");
log.trace('disconnected'); log.trace("disconnected");
} }
} }

View File

@ -1,13 +1,32 @@
import { it, describe, expect } from 'vitest'; /**
import { handleTimeout } from '../../particle/Particle.js'; * Copyright 2023 Fluence Labs Limited
import { doNothing } from '../../jsServiceHost/serviceUtils.js'; *
import { registerHandlersHelper, withClient } from '../../util/testUtils.js'; * Licensed under the Apache License, Version 2.0 (the "License");
import { checkConnection } from '../checkConnection.js'; * you may not use this file except in compliance with the License.
import { nodes, RELAY } from './connection.js'; * You may obtain a copy of the License at
import { CallServiceData } from '../../jsServiceHost/interfaces.js'; *
* 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.
*/
describe('FluenceClient usage test suite', () => { import { JSONValue } from "@fluencelabs/interfaces";
it('should make a call through network', async () => { import { it, describe, expect } from "vitest";
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";
describe("FluenceClient usage test suite", () => {
it("should make a call through network", async () => {
await withClient(RELAY, {}, async (peer) => { await withClient(RELAY, {}, async (peer) => {
// arrange // arrange
@ -28,9 +47,10 @@ describe('FluenceClient usage test suite', () => {
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const result = await new Promise<string>((resolve, reject) => { const result = await new Promise<JSONValue>((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
@ -40,11 +60,11 @@ describe('FluenceClient usage test suite', () => {
}, },
}, },
callback: { callback: {
callback: (args: any) => { callback: (args): undefined => {
const [val] = args; const [val] = args;
resolve(val); resolve(val);
}, },
error: (args: any) => { error: (args): undefined => {
const [error] = args; const [error] = args;
reject(error); reject(error);
}, },
@ -54,11 +74,11 @@ describe('FluenceClient usage test suite', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(result).toBe('hello world!'); expect(result).toBe("hello world!");
}); });
}); });
it('check connection should work', async function () { it("check connection should work", async function () {
await withClient(RELAY, {}, async (peer) => { await withClient(RELAY, {}, async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
@ -66,7 +86,7 @@ describe('FluenceClient usage test suite', () => {
}); });
}); });
it('check connection should work with ttl', async function () { it("check connection should work with ttl", async function () {
await withClient(RELAY, {}, async (peer) => { await withClient(RELAY, {}, async (peer) => {
const isConnected = await checkConnection(peer, 10000); const isConnected = await checkConnection(peer, 10000);
@ -74,17 +94,21 @@ describe('FluenceClient usage test suite', () => {
}); });
}); });
it('two clients should work inside the same time javascript process', async () => { it("two clients should work inside the same time javascript process", async () => {
await withClient(RELAY, {}, async (peer1) => { await withClient(RELAY, {}, async (peer1) => {
await withClient(RELAY, {}, async (peer2) => { await withClient(RELAY, {}, async (peer2) => {
const res = new Promise((resolve) => { const res = new Promise((resolve) => {
peer2.internals.regHandler.common('test', 'test', (req: CallServiceData) => { peer2.internals.regHandler.common(
"test",
"test",
(req: CallServiceData) => {
resolve(req.args[0]); resolve(req.args[0]);
return { return {
result: {}, result: {},
retCode: 0, retCode: 0,
}; };
}); },
);
}); });
const script = ` const script = `
@ -93,6 +117,7 @@ describe('FluenceClient usage test suite', () => {
(call "${peer2.getPeerId()}" ("test" "test") ["test"]) (call "${peer2.getPeerId()}" ("test" "test") ["test"])
) )
`; `;
const particle = await peer1.internals.createNewParticle(script); const particle = await peer1.internals.createNewParticle(script);
if (particle instanceof Error) { if (particle instanceof Error) {
@ -101,13 +126,13 @@ describe('FluenceClient usage test suite', () => {
peer1.internals.initiateParticle(particle, doNothing); peer1.internals.initiateParticle(particle, doNothing);
expect(await res).toEqual('test'); expect(await res).toEqual("test");
}); });
}); });
}); });
describe('should make connection to network', () => { describe("should make connection to network", () => {
it('address as string', async () => { it("address as string", async () => {
await withClient(nodes[0].multiaddr, {}, async (peer) => { await withClient(nodes[0].multiaddr, {}, async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
@ -115,7 +140,7 @@ describe('FluenceClient usage test suite', () => {
}); });
}); });
it('address as node', async () => { it("address as node", async () => {
await withClient(nodes[0], {}, async (peer) => { await withClient(nodes[0], {}, async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
@ -123,23 +148,31 @@ describe('FluenceClient usage test suite', () => {
}); });
}); });
it('With connection options: dialTimeout', async () => { it("With connection options: dialTimeout", async () => {
await withClient(RELAY, { connectionOptions: { dialTimeoutMs: 100000 } }, async (peer) => { await withClient(
RELAY,
{ connectionOptions: { dialTimeoutMs: 100000 } },
async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy(); expect(isConnected).toBeTruthy();
}); },
);
}); });
it('With connection options: skipCheckConnection', async () => { it("With connection options: skipCheckConnection", async () => {
await withClient(RELAY, { connectionOptions: { skipCheckConnection: true } }, async (peer) => { await withClient(
RELAY,
{ connectionOptions: { skipCheckConnection: true } },
async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy(); expect(isConnected).toBeTruthy();
}); },
);
}); });
it('With connection options: defaultTTL', async () => { it("With connection options: defaultTTL", async () => {
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => { await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => {
const isConnected = await checkConnection(peer); const isConnected = await checkConnection(peer);
@ -148,22 +181,25 @@ describe('FluenceClient usage test suite', () => {
}); });
}); });
it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => { it.skip("Should throw correct error when the client tries to send a particle not to the relay", async () => {
await withClient(RELAY, {}, async (peer) => { await withClient(RELAY, {}, async (peer) => {
const script = ` const script = `
(xor (xor
(call "incorrect_peer_id" ("any" "service") []) (call "incorrect_peer_id" ("any" "service") [])
(call %init_peer_id% ("callback" "error") [%last_error%]) (call %init_peer_id% ("callback" "error") [%last_error%])
)`; )`;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const promise = new Promise((resolve, reject) => {
const promise = new Promise((_resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
callback: { callback: {
error: (args: any) => { error: (args): undefined => {
const [error] = args; const [error] = args;
reject(error); reject(error);
}, },
@ -171,7 +207,7 @@ describe('FluenceClient usage test suite', () => {
}); });
peer.internals.initiateParticle(particle, (stage) => { peer.internals.initiateParticle(particle, (stage) => {
if (stage.stage === 'sendingError') { if (stage.stage === "sendingError") {
reject(stage.errorMessage); reject(stage.errorMessage);
} }
}); });
@ -180,7 +216,7 @@ describe('FluenceClient usage test suite', () => {
await promise; await promise;
await expect(promise).rejects.toMatch( await expect(promise).rejects.toMatch(
'Particle is expected to be sent to only the single peer (relay which client is connected to)', "Particle is expected to be sent to only the single peer (relay which client is connected to)",
); );
}); });
}); });

View File

@ -1,7 +1,24 @@
/**
* 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 const nodes = [ export const nodes = [
{ {
multiaddr: '/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', multiaddr:
peerId: '12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR', "/ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
peerId: "12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR",
}, },
]; ];

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,19 +13,25 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { ClientPeer } from './ClientPeer.js';
import { logger } from '../util/logger.js'; import { JSONValue } from "@fluencelabs/interfaces";
import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js';
import { handleTimeout } from '../particle/Particle.js';
const log = logger('connection'); import { WrapFnIntoServiceCall } from "../jsServiceHost/serviceUtils.js";
import { handleTimeout } from "../particle/Particle.js";
import { logger } from "../util/logger.js";
import { ClientPeer } from "./ClientPeer.js";
const log = logger("connection");
/** /**
* Checks the network connection by sending a ping-like request to relay node * Checks the network connection by sending a ping-like request to relay node
* @param { ClientPeer } peer - The Fluence Client instance. * @param { ClientPeer } peer - The Fluence Client instance.
*/ */
export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<boolean> => { export const checkConnection = async (
peer: ClientPeer,
ttl?: number,
): Promise<boolean> => {
const msg = Math.random().toString(36).substring(7); const msg = Math.random().toString(36).substring(7);
const script = ` const script = `
@ -45,17 +51,19 @@ export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<b
(call %init_peer_id% ("callback" "error") [%last_error%]) (call %init_peer_id% ("callback" "error") [%last_error%])
) )
)`; )`;
const particle = await peer.internals.createNewParticle(script, ttl); const particle = await peer.internals.createNewParticle(script, ttl);
const promise = new Promise<string>((resolve, reject) => { const promise = new Promise<JSONValue>((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
peer.internals.regHandler.forParticle( peer.internals.regHandler.forParticle(
particle.id, particle.id,
'load', "load",
'relay', "relay",
WrapFnIntoServiceCall(() => { WrapFnIntoServiceCall(() => {
return peer.getRelayPeerId(); return peer.getRelayPeerId();
}), }),
@ -63,8 +71,8 @@ export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<b
peer.internals.regHandler.forParticle( peer.internals.regHandler.forParticle(
particle.id, particle.id,
'load', "load",
'msg', "msg",
WrapFnIntoServiceCall(() => { WrapFnIntoServiceCall(() => {
return msg; return msg;
}), }),
@ -72,26 +80,30 @@ export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<b
peer.internals.regHandler.forParticle( peer.internals.regHandler.forParticle(
particle.id, particle.id,
'callback', "callback",
'callback', "callback",
WrapFnIntoServiceCall((args) => { WrapFnIntoServiceCall((args) => {
const [val] = args; const [val] = args;
setTimeout(() => { setTimeout(() => {
resolve(val); resolve(val);
}, 0); }, 0);
return {}; return {};
}), }),
); );
peer.internals.regHandler.forParticle( peer.internals.regHandler.forParticle(
particle.id, particle.id,
'callback', "callback",
'error', "error",
WrapFnIntoServiceCall((args) => { WrapFnIntoServiceCall((args) => {
const [error] = args; const [error] = args;
setTimeout(() => { setTimeout(() => {
reject(error); reject(error);
}, 0); }, 0);
return {}; return {};
}), }),
); );
@ -99,19 +111,28 @@ export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<b
peer.internals.initiateParticle( peer.internals.initiateParticle(
particle, particle,
handleTimeout(() => { handleTimeout(() => {
reject('particle timed out'); reject("particle timed out");
}), }),
); );
}); });
try { try {
const result = await promise; const result = await promise;
if (result != msg) {
log.error("unexpected behavior. 'identity' must return the passed arguments."); if (result !== msg) {
log.error(
"unexpected behavior. 'identity' must return the passed arguments.",
);
} }
return true; return true;
} catch (e) { } catch (e) {
log.error('error on establishing connection. Relay: %s error: %j', peer.getRelayPeerId(), e); log.error(
"error on establishing connection. Relay: %s error: %j",
peer.getRelayPeerId(),
e,
);
return false; return false;
} }
}; };

View File

@ -1,19 +1,37 @@
import { it, describe, expect, test } from 'vitest'; /**
import { aqua2ts, ts2aqua } from '../conversions.js'; * 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.
*/
const i32 = { tag: 'scalar', name: 'i32' } as const; import { JSONValue, NonArrowType } from "@fluencelabs/interfaces";
import { it, describe, expect, test } from "vitest";
import { aqua2ts, ts2aqua } from "../conversions.js";
const i32 = { tag: "scalar", name: "i32" } as const;
const opt_i32 = { const opt_i32 = {
tag: 'option', tag: "option",
type: i32, type: i32,
} as const; } as const;
const array_i32 = { tag: 'array', type: i32 }; const array_i32 = { tag: "array", type: i32 };
const array_opt_i32 = { tag: 'array', type: opt_i32 }; const array_opt_i32 = { tag: "array", type: opt_i32 };
const labeledProduct = { const labeledProduct = {
tag: 'labeledProduct', tag: "labeledProduct",
fields: { fields: {
a: i32, a: i32,
b: opt_i32, b: opt_i32,
@ -22,8 +40,8 @@ const labeledProduct = {
}; };
const struct = { const struct = {
tag: 'struct', tag: "struct",
name: 'someStruct', name: "someStruct",
fields: { fields: {
a: i32, a: i32,
b: opt_i32, b: opt_i32,
@ -61,7 +79,7 @@ const structs = [
]; ];
const labeledProduct2 = { const labeledProduct2 = {
tag: 'labeledProduct', tag: "labeledProduct",
fields: { fields: {
x: i32, x: i32,
y: i32, y: i32,
@ -69,15 +87,15 @@ const labeledProduct2 = {
}; };
const nestedLabeledProductType = { const nestedLabeledProductType = {
tag: 'labeledProduct', tag: "labeledProduct",
fields: { fields: {
a: labeledProduct2, a: labeledProduct2,
b: { b: {
tag: 'option', tag: "option",
type: labeledProduct2, type: labeledProduct2,
}, },
c: { c: {
tag: 'array', tag: "array",
type: labeledProduct2, type: labeledProduct2,
}, },
}, },
@ -151,7 +169,13 @@ const nestedStructs = [
}, },
]; ];
describe('Conversion from aqua to typescript', () => { interface ConversionTestArgs {
aqua: JSONValue;
ts: JSONValue;
type: NonArrowType;
}
describe("Conversion from aqua to typescript", () => {
test.each` test.each`
aqua | ts | type aqua | ts | type
${1} | ${1} | ${i32} ${1} | ${1} | ${i32}
@ -171,8 +195,8 @@ describe('Conversion from aqua to typescript', () => {
${nestedStructs[1].aqua} | ${nestedStructs[1].ts} | ${nestedLabeledProductType} ${nestedStructs[1].aqua} | ${nestedStructs[1].ts} | ${nestedLabeledProductType}
`( `(
// //
'aqua: $aqua. ts: $ts. type: $type', "aqua: $aqua. ts: $ts. type: $type",
async ({ aqua, ts, type }) => { ({ aqua, ts, type }: ConversionTestArgs) => {
// arrange // arrange
// act // act
@ -186,11 +210,11 @@ describe('Conversion from aqua to typescript', () => {
); );
}); });
describe('Conversion corner cases', () => { describe("Conversion corner cases", () => {
it('Should accept undefined in object entry', () => { it("Should accept undefined in object entry", () => {
// arrange // arrange
const type = { const type = {
tag: 'labeledProduct', tag: "labeledProduct",
fields: { fields: {
x: opt_i32, x: opt_i32,
y: opt_i32, y: opt_i32,
@ -200,6 +224,7 @@ describe('Conversion corner cases', () => {
const valueInTs = { const valueInTs = {
x: 1, x: 1,
}; };
const valueInAqua = { const valueInAqua = {
x: [1], x: [1],
y: [], y: [],

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { CallAquaFunctionType, getArgumentTypes, isReturnTypeVoid } from '@fluencelabs/interfaces';
import assert from "assert";
import {
FnConfig,
FunctionCallDef,
getArgumentTypes,
isReturnTypeVoid,
PassedArgs,
} from "@fluencelabs/interfaces";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js";
import { import {
errorHandlingService, errorHandlingService,
@ -23,12 +35,9 @@ import {
responseService, responseService,
ServiceDescription, ServiceDescription,
userHandlerService, userHandlerService,
} from './services.js'; } from "./services.js";
import { logger } from '../util/logger.js'; const log = logger("aqua");
import { IParticle } from '../particle/interfaces.js';
const log = logger('aqua');
/** /**
* Convenience function which does all the internal work of creating particles * Convenience function which does all the internal work of creating particles
@ -41,25 +50,55 @@ const log = logger('aqua');
* @param args - args in the form of JSON where each key corresponds to the name of the argument * @param args - args in the form of JSON where each key corresponds to the name of the argument
* @returns * @returns
*/ */
export const callAquaFunction: CallAquaFunctionType = async ({ def, script, config, peer, args }) => {
log.trace('calling aqua function %j', { def, script, config, args }); type CallAquaFunctionArgs = {
def: FunctionCallDef;
script: string;
config: FnConfig;
peer: FluencePeer;
args: PassedArgs;
};
export const callAquaFunction = async ({
def,
script,
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); const argumentTypes = getArgumentTypes(def);
const particle = await peer.internals.createNewParticle(script, config?.ttl); const particle = await peer.internals.createNewParticle(script, config.ttl);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (particle instanceof Error) { for (const [name, argVal] of Object.entries(args)) {
return reject(particle.message);
}
for (let [name, argVal] of Object.entries(args)) {
const type = argumentTypes[name]; const type = argumentTypes[name];
let service: ServiceDescription; let service: ServiceDescription;
if (type.tag === 'arrow') {
service = userHandlerService(def.names.callbackSrv, [name, type], argVal); 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,
);
} else { } 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(def.names.getDataSrv, name, type, argVal);
} }
registerParticleScopeService(peer, particle, service); registerParticleScopeService(peer, particle, service);
} }
@ -67,31 +106,40 @@ export const callAquaFunction: CallAquaFunctionType = async ({ def, script, conf
registerParticleScopeService(peer, particle, injectRelayService(def, peer)); registerParticleScopeService(peer, particle, injectRelayService(def, peer));
registerParticleScopeService(peer, particle, errorHandlingService(def, reject)); registerParticleScopeService(
peer,
particle,
errorHandlingService(def, reject),
);
peer.internals.initiateParticle(particle, (stage: any) => { peer.internals.initiateParticle(particle, (stage) => {
// If function is void, then it's completed when one of the two conditions is met: // 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') // 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') // 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')) { if (
isReturnTypeVoid(def) &&
(stage.stage === "sent" || stage.stage === "localWorkDone")
) {
resolve(undefined); resolve(undefined);
} }
if (stage.stage === 'sendingError') { if (stage.stage === "sendingError") {
reject(`Could not send particle for ${def.functionName}: not connected (particle id: ${particle.id})`); reject(
`Could not send particle for ${def.functionName}: not connected (particle id: ${particle.id})`,
);
} }
if (stage.stage === 'expired') { if (stage.stage === "expired") {
reject( reject(
`Particle expired after ttl of ${particle.ttl}ms for function ${def.functionName} (particle id: ${particle.id})`, `Particle expired after ttl of ${particle.ttl}ms for function ${def.functionName} (particle id: ${particle.id})`,
); );
} }
if (stage.stage === 'interpreterError') { if (stage.stage === "interpreterError") {
reject( reject(
`Script interpretation failed for ${def.functionName}: ${stage.errorMessage} (particle id: ${particle.id})`, `Script interpretation failed for ${def.functionName}: ${stage.errorMessage} (particle id: ${particle.id})`,
); );
} }
}); });
}) });
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,10 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { jsonify } from '../util/utils.js';
import { match } from 'ts-pattern'; // TODO: This file is a mess. Need to refactor it later
import type { ArrowType, ArrowWithoutCallbacks, NonArrowType } from '@fluencelabs/interfaces'; /* eslint-disable */
import { CallServiceData } from '../jsServiceHost/interfaces.js'; // @ts-nocheck
import assert from "assert";
import type {
ArrowType,
ArrowWithoutCallbacks,
JSONArray,
JSONValue,
NonArrowType,
} from "@fluencelabs/interfaces";
import { match } from "ts-pattern";
import { CallServiceData } from "../jsServiceHost/interfaces.js";
import { jsonify } from "../util/utils.js";
/** /**
* Convert value from its representation in aqua language to representation in typescript * Convert value from its representation in aqua language to representation in typescript
@ -24,38 +38,42 @@ import { CallServiceData } from '../jsServiceHost/interfaces.js';
* @param type - definition of the aqua type * @param type - definition of the aqua type
* @returns value represented in typescript * @returns value represented in typescript
*/ */
export const aqua2ts = (value: any, type: NonArrowType): any => { export const aqua2ts = (value: JSONValue, type: NonArrowType): JSONValue => {
const res = match(type) const res = match(type)
.with({ tag: 'nil' }, () => { .with({ tag: "nil" }, () => {
return null; return null;
}) })
.with({ tag: 'option' }, (opt) => { .with({ tag: "option" }, (opt) => {
assert(Array.isArray(value), "Should not be possible, bad types");
if (value.length === 0) { if (value.length === 0) {
return null; return null;
} else { } else {
return aqua2ts(value[0], opt.type); return aqua2ts(value[0], opt.type);
} }
}) })
// @ts-ignore .with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => {
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
return value; return value;
}) })
.with({ tag: 'array' }, (arr) => { .with({ tag: "array" }, (arr) => {
return value.map((y: any) => aqua2ts(y, arr.type)); assert(Array.isArray(value), "Should not be possible, bad types");
return value.map((y) => {
return aqua2ts(y, arr.type);
});
}) })
.with({ tag: 'struct' }, (x) => { .with({ tag: "struct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => { return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type); const val = aqua2ts(value[key], type);
return { ...agg, [key]: val }; return { ...agg, [key]: val };
}, {}); }, {});
}) })
.with({ tag: 'labeledProduct' }, (x) => { .with({ tag: "labeledProduct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => { return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type); const val = aqua2ts(value[key], type);
return { ...agg, [key]: val }; return { ...agg, [key]: val };
}, {}); }, {});
}) })
.with({ tag: 'unlabeledProduct' }, (x) => { .with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((type, index) => { return x.items.map((type, index) => {
return aqua2ts(value[index], type); return aqua2ts(value[index], type);
}); });
@ -63,8 +81,9 @@ export const aqua2ts = (value: any, type: NonArrowType): any => {
// uncomment to check that every pattern in matched // uncomment to check that every pattern in matched
// .exhaustive(); // .exhaustive();
.otherwise(() => { .otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(type)); throw new Error("Unexpected tag: " + jsonify(type));
}); });
return res; return res;
}; };
@ -74,25 +93,30 @@ export const aqua2ts = (value: any, type: NonArrowType): any => {
* @param arrow - aqua type definition * @param arrow - aqua type definition
* @returns arguments in typescript representation * @returns arguments in typescript representation
*/ */
export const aquaArgs2Ts = (req: CallServiceData, arrow: ArrowWithoutCallbacks) => { export const aquaArgs2Ts = (
req: CallServiceData,
arrow: ArrowWithoutCallbacks,
): JSONArray => {
const argTypes = match(arrow.domain) const argTypes = match(arrow.domain)
.with({ tag: 'labeledProduct' }, (x) => { .with({ tag: "labeledProduct" }, (x) => {
return Object.values(x.fields); return Object.values(x.fields);
}) })
.with({ tag: 'unlabeledProduct' }, (x) => { .with({ tag: "unlabeledProduct" }, (x) => {
return x.items; return x.items;
}) })
.with({ tag: 'nil' }, (x) => { .with({ tag: "nil" }, (x) => {
return []; return [];
}) })
// uncomment to check that every pattern in matched // uncomment to check that every pattern in matched
// .exhaustive() // .exhaustive()
.otherwise(() => { .otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(arrow.domain)); throw new Error("Unexpected tag: " + jsonify(arrow.domain));
}); });
if (req.args.length !== argTypes.length) { if (req.args.length !== argTypes.length) {
throw new Error(`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`); throw new Error(
`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`,
);
} }
return req.args.map((arg, index) => { return req.args.map((arg, index) => {
@ -106,38 +130,40 @@ export const aquaArgs2Ts = (req: CallServiceData, arrow: ArrowWithoutCallbacks)
* @param type - definition of the aqua type * @param type - definition of the aqua type
* @returns value represented in aqua * @returns value represented in aqua
*/ */
export const ts2aqua = (value: any, type: NonArrowType): any => { export const ts2aqua = (value: JSONValue, type: NonArrowType): JSONValue => {
const res = match(type) const res = match(type)
.with({ tag: 'nil' }, () => { .with({ tag: "nil" }, () => {
return null; return null;
}) })
.with({ tag: 'option' }, (opt) => { .with({ tag: "option" }, (opt) => {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return []; return [];
} else { } else {
return [ts2aqua(value, opt.type)]; return [ts2aqua(value, opt.type)];
} }
}) })
// @ts-ignore .with({ tag: "scalar" }, { tag: "bottomType" }, { tag: "topType" }, () => {
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
return value; return value;
}) })
.with({ tag: 'array' }, (arr) => { .with({ tag: "array" }, (arr) => {
return value.map((y: any) => ts2aqua(y, arr.type)); assert(Array.isArray(value), "Should not be possible, bad types");
return value.map((y) => {
return ts2aqua(y, arr.type);
});
}) })
.with({ tag: 'struct' }, (x) => { .with({ tag: "struct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => { return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type); const val = ts2aqua(value[key], type);
return { ...agg, [key]: val }; return { ...agg, [key]: val };
}, {}); }, {});
}) })
.with({ tag: 'labeledProduct' }, (x) => { .with({ tag: "labeledProduct" }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => { return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type); const val = ts2aqua(value[key], type);
return { ...agg, [key]: val }; return { ...agg, [key]: val };
}, {}); }, {});
}) })
.with({ tag: 'unlabeledProduct' }, (x) => { .with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((type, index) => { return x.items.map((type, index) => {
return ts2aqua(value[index], type); return ts2aqua(value[index], type);
}); });
@ -145,7 +171,7 @@ export const ts2aqua = (value: any, type: NonArrowType): any => {
// uncomment to check that every pattern in matched // uncomment to check that every pattern in matched
// .exhaustive() // .exhaustive()
.otherwise(() => { .otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(type)); throw new Error("Unexpected tag: " + jsonify(type));
}); });
return res; return res;
@ -157,8 +183,11 @@ export const ts2aqua = (value: any, type: NonArrowType): any => {
* @param arrowType - the arrow type which describes the service * @param arrowType - the arrow type which describes the service
* @returns - value represented in aqua * @returns - value represented in aqua
*/ */
export const returnType2Aqua = (returnValue: any, arrowType: ArrowType<NonArrowType>) => { export const returnType2Aqua = (
if (arrowType.codomain.tag === 'nil') { returnValue: any,
arrowType: ArrowType<NonArrowType>,
) => {
if (arrowType.codomain.tag === "nil") {
return {}; return {};
} }
@ -181,21 +210,26 @@ export const returnType2Aqua = (returnValue: any, arrowType: ArrowType<NonArrowT
* @param arrow - aqua type definition * @param arrow - aqua type definition
* @returns response value in typescript representation * @returns response value in typescript representation
*/ */
export const responseServiceValue2ts = (req: CallServiceData, arrow: ArrowType<any>) => { export const responseServiceValue2ts = (
req: CallServiceData,
arrow: ArrowType<any>,
) => {
return match(arrow.codomain) return match(arrow.codomain)
.with({ tag: 'nil' }, () => { .with({ tag: "nil" }, () => {
return undefined; return null;
}) })
.with({ tag: 'unlabeledProduct' }, (x) => { .with({ tag: "unlabeledProduct" }, (x) => {
if (x.items.length === 0) { if (x.items.length === 0) {
return undefined; return null;
} }
if (x.items.length === 1) { if (x.items.length === 1) {
return aqua2ts(req.args[0], x.items[0]); return aqua2ts(req.args[0], x.items[0]);
} }
return req.args.map((y, index) => aqua2ts(y, x.items[index])); return req.args.map((y, index) => {
return aqua2ts(y, x.items[index]);
});
}) })
.exhaustive(); .exhaustive();
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,43 +13,72 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RegisterServiceType } from '@fluencelabs/interfaces';
import { registerGlobalService, userHandlerService } from './services.js';
import { logger } from '../util/logger.js'; import type { ServiceDef, ServiceImpl } from "@fluencelabs/interfaces";
const log = logger('aqua'); import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { logger } from "../util/logger.js";
export const registerService: RegisterServiceType = ({ peer, def, serviceId, service }) => { import { registerGlobalService, userHandlerService } from "./services.js";
log.trace('registering aqua service %o', { def, serviceId, service });
const log = logger("aqua");
interface RegisterServiceArgs {
peer: FluencePeer;
def: ServiceDef;
serviceId: string | undefined;
service: ServiceImpl;
}
export const registerService = ({
peer,
def,
serviceId = def.defaultServiceId,
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 });
// Checking for missing keys // Checking for missing keys
const requiredKeys = def.functions.tag === 'nil' ? [] : Object.keys(def.functions.fields); const requiredKeys =
const incorrectServiceDefinitions = requiredKeys.filter((f) => !(f in service)); def.functions.tag === "nil" ? [] : Object.keys(def.functions.fields);
if (!!incorrectServiceDefinitions.length) {
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( throw new Error(
`Error registering service ${serviceId}: missing functions: ` + `Error registering service ${serviceId}: missing functions: ` +
incorrectServiceDefinitions.map((d) => "'" + d + "'").join(', '), incorrectServiceDefinitions
.map((d) => {
return "'" + d + "'";
})
.join(", "),
); );
} }
if (!serviceId) { const singleFunctions =
serviceId = def.defaultServiceId; def.functions.tag === "nil" ? [] : Object.entries(def.functions.fields);
}
if (!serviceId) { for (const singleFunction of singleFunctions) {
throw new Error('Service ID must be specified'); const [name] = singleFunction;
}
const singleFunctions = def.functions.tag === 'nil' ? [] : Object.entries(def.functions.fields);
for (let singleFunction of singleFunctions) {
let [name, type] = singleFunction;
// The function has type of (arg1, arg2, arg3, ... , callParams) => CallServiceResultType | void // 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(...) // Account for the fact that user service might be defined as a class - .bind(...)
const userDefinedHandler = service[name].bind(service); const userDefinedHandler = service[name].bind(service);
const serviceDescription = userHandlerService(serviceId, singleFunction, userDefinedHandler); const serviceDescription = userHandlerService(
serviceId,
singleFunction,
userDefinedHandler,
);
registerGlobalService(peer, serviceDescription); registerGlobalService(peer, serviceDescription);
} }
log.trace('aqua service registered %s', serviceId);
log.trace("aqua service registered %s", serviceId);
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,22 +13,33 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { SecurityTetraplet } from '@fluencelabs/avm';
import { match } from 'ts-pattern';
import { Particle } from '../particle/Particle.js'; import { SecurityTetraplet } from "@fluencelabs/avm";
import { aquaArgs2Ts, responseServiceValue2ts, returnType2Aqua, ts2aqua } from './conversions.js';
import { import {
CallParams, CallParams,
ArrowWithoutCallbacks, ArrowWithoutCallbacks,
FunctionCallConstants,
FunctionCallDef, FunctionCallDef,
NonArrowType, NonArrowType,
IFluenceInternalApi, ServiceImpl,
} from '@fluencelabs/interfaces'; JSONValue,
import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js'; } from "@fluencelabs/interfaces";
import { fromUint8Array } from 'js-base64'; import { fromUint8Array } from "js-base64";
import { match } from "ts-pattern";
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import {
CallServiceData,
GenericCallServiceHandler,
ResultCodes,
} from "../jsServiceHost/interfaces.js";
import { Particle } from "../particle/Particle.js";
import {
aquaArgs2Ts,
responseServiceValue2ts,
returnType2Aqua,
ts2aqua,
} from "./conversions.js";
export interface ServiceDescription { export interface ServiceDescription {
serviceId: string; serviceId: string;
@ -39,7 +50,7 @@ export interface ServiceDescription {
/** /**
* Creates a service which injects relay's peer id into aqua space * Creates a service which injects relay's peer id into aqua space
*/ */
export const injectRelayService = (def: FunctionCallDef, peer: IFluenceInternalApi) => { export const injectRelayService = (def: FunctionCallDef, peer: FluencePeer) => {
return { return {
serviceId: def.names.getDataSrv, serviceId: def.names.getDataSrv,
fnName: def.names.relay, fnName: def.names.relay,
@ -55,7 +66,12 @@ export const injectRelayService = (def: FunctionCallDef, peer: IFluenceInternalA
/** /**
* Creates a service which injects plain value into aqua space * Creates a service which injects plain value into aqua space
*/ */
export const injectValueService = (serviceId: string, fnName: string, valueType: NonArrowType, value: any) => { export const injectValueService = (
serviceId: string,
fnName: string,
valueType: NonArrowType,
value: JSONValue,
) => {
return { return {
serviceId: serviceId, serviceId: serviceId,
fnName: fnName, fnName: fnName,
@ -71,7 +87,10 @@ export const injectValueService = (serviceId: string, fnName: string, valueType:
/** /**
* Creates a service which is used to return value from aqua function into typescript space * Creates a service which is used to return value from aqua function into typescript space
*/ */
export const responseService = (def: FunctionCallDef, resolveCallback: Function) => { export const responseService = (
def: FunctionCallDef,
resolveCallback: (val: JSONValue) => void,
) => {
return { return {
serviceId: def.names.responseSrv, serviceId: def.names.responseSrv,
fnName: def.names.responseFnName, fnName: def.names.responseFnName,
@ -93,15 +112,20 @@ export const responseService = (def: FunctionCallDef, resolveCallback: Function)
/** /**
* Creates a service which is used to return errors from aqua function into typescript space * Creates a service which is used to return errors from aqua function into typescript space
*/ */
export const errorHandlingService = (def: FunctionCallDef, rejectCallback: Function) => { export const errorHandlingService = (
def: FunctionCallDef,
rejectCallback: (err: JSONValue) => void,
) => {
return { return {
serviceId: def.names.errorHandlingSrv, serviceId: def.names.errorHandlingSrv,
fnName: def.names.errorFnName, fnName: def.names.errorFnName,
handler: (req: CallServiceData) => { handler: (req: CallServiceData) => {
const [err, _] = req.args; const [err] = req.args;
setTimeout(() => { setTimeout(() => {
rejectCallback(err); rejectCallback(err);
}, 0); }, 0);
return { return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
result: {}, result: {},
@ -116,15 +140,19 @@ export const errorHandlingService = (def: FunctionCallDef, rejectCallback: Funct
export const userHandlerService = ( export const userHandlerService = (
serviceId: string, serviceId: string,
arrowType: [string, ArrowWithoutCallbacks], arrowType: [string, ArrowWithoutCallbacks],
userHandler: (...args: Array<unknown>) => Promise<unknown>, userHandler: ServiceImpl[string],
) => { ) => {
const [fnName, type] = arrowType; const [fnName, type] = arrowType;
return { return {
serviceId, serviceId,
fnName, fnName,
handler: async (req: CallServiceData) => { handler: async (req: CallServiceData) => {
const args = [...aquaArgs2Ts(req, type), extractCallParams(req, type)]; const args: [...JSONValue[], CallParams<string>] = [
const rawResult = await userHandler.apply(null, args); ...aquaArgs2Ts(req, type),
extractCallParams(req, type),
];
const rawResult = await userHandler.bind(null)(...args);
const result = returnType2Aqua(rawResult, type); const result = returnType2Aqua(rawResult, type);
return { return {
@ -135,44 +163,34 @@ export const userHandlerService = (
}; };
}; };
/**
* Converts argument of aqua function to a corresponding service.
* For arguments of non-arrow types the resulting service injects the argument into aqua space.
* For arguments of arrow types the resulting service calls the corresponding function.
*/
export const argToServiceDef = (
arg: any,
argName: string,
argType: NonArrowType | ArrowWithoutCallbacks,
names: FunctionCallConstants,
): ServiceDescription => {
if (argType.tag === 'arrow') {
return userHandlerService(names.callbackSrv, [argName, argType], arg);
} else {
return injectValueService(names.getDataSrv, argName, arg, argType);
}
};
/** /**
* Extracts call params from from call service data according to aqua type definition * Extracts call params from from call service data according to aqua type definition
*/ */
const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks): CallParams<any> => { const extractCallParams = (
const names = match(arrow.domain) req: CallServiceData,
.with({ tag: 'nil' }, () => { arrow: ArrowWithoutCallbacks,
return [] as string[]; ): CallParams<string> => {
const names: (string | undefined)[] = match(arrow.domain)
.with({ tag: "nil" }, () => {
return [];
}) })
.with({ tag: 'labeledProduct' }, (x) => { .with({ tag: "unlabeledProduct" }, (x) => {
return x.items.map((_, index) => {
return "arg" + index;
});
})
.with({ tag: "labeledProduct" }, (x) => {
return Object.keys(x.fields); return Object.keys(x.fields);
}) })
.with({ tag: 'unlabeledProduct' }, (x) => {
return x.items.map((_, index) => 'arg' + index);
})
.exhaustive(); .exhaustive();
const tetraplets: Record<string, SecurityTetraplet[]> = {}; const tetraplets: Record<string, SecurityTetraplet[]> = {};
for (let i = 0; i < req.args.length; i++) { for (let i = 0; i < req.args.length; i++) {
if (names[i]) { const name = names[i];
tetraplets[names[i]] = req.tetraplets[i];
if (name != null) {
tetraplets[name] = req.tetraplets[i];
} }
} }
@ -186,13 +204,25 @@ const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks):
}; };
export const registerParticleScopeService = ( export const registerParticleScopeService = (
peer: IFluenceInternalApi, peer: FluencePeer,
particle: Particle, particle: Particle,
service: ServiceDescription, service: ServiceDescription,
) => { ) => {
peer.internals.regHandler.forParticle(particle.id, service.serviceId, service.fnName, service.handler); peer.internals.regHandler.forParticle(
particle.id,
service.serviceId,
service.fnName,
service.handler,
);
}; };
export const registerGlobalService = (peer: IFluenceInternalApi, service: ServiceDescription) => { export const registerGlobalService = (
peer.internals.regHandler.common(service.serviceId, service.fnName, service.handler); peer: FluencePeer,
service: ServiceDescription,
) => {
peer.internals.regHandler.common(
service.serviceId,
service.fnName,
service.handler,
);
}; };

View File

@ -1,5 +1,5 @@
/* /**
* Copyright 2020 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,38 +13,41 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { pipe } from 'it-pipe';
import { decode, encode } from 'it-length-prefixed';
import type { PeerId } from '@libp2p/interface/peer-id';
import { createLibp2p, Libp2p } from 'libp2p';
import { noise } from '@chainsafe/libp2p-noise'; import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from '@chainsafe/libp2p-yamux'; import { yamux } from "@chainsafe/libp2p-yamux";
import { webSockets } from '@libp2p/websockets'; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { all } from '@libp2p/websockets/filters'; import { Stream } from "@libp2p/interface/connection";
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'; import type { PeerId } from "@libp2p/interface/peer-id";
import { peerIdFromString } from "@libp2p/peer-id";
import { webSockets } from "@libp2p/websockets";
import { all } from "@libp2p/websockets/filters";
import { multiaddr, type Multiaddr } from "@multiformats/multiaddr";
import { decode, encode } from "it-length-prefixed";
import map from "it-map";
import { pipe } from "it-pipe";
import { createLibp2p, Libp2p } from "libp2p";
import { identifyService } from "libp2p/identify";
import { pingService } from "libp2p/ping";
import { Subject } from "rxjs";
import { fromString } from "uint8arrays/from-string";
import { toString } from "uint8arrays/to-string";
import map from 'it-map'; import { KeyPair } from "../keypair/index.js";
import { fromString } from 'uint8arrays/from-string'; import { IParticle } from "../particle/interfaces.js";
import { toString } from 'uint8arrays/to-string'; import {
buildParticleMessage,
Particle,
serializeToString,
} from "../particle/Particle.js";
import { throwHasNoPeerId } from "../util/libp2pUtils.js";
import { logger } from "../util/logger.js";
import { logger } from '../util/logger.js'; import { IConnection } from "./interfaces.js";
import { Subject } from 'rxjs';
import { throwIfHasNoPeerId } from '../util/libp2pUtils.js';
import { IConnection } from './interfaces.js';
import { IParticle } from '../particle/interfaces.js';
import { buildParticleMessage, Particle, serializeToString, verifySignature } from '../particle/Particle.js';
import { identifyService } from 'libp2p/identify';
import { pingService } from 'libp2p/ping';
import { unmarshalPublicKey } from '@libp2p/crypto/keys';
import { peerIdFromString } from '@libp2p/peer-id';
import { Stream } from '@libp2p/interface/connection';
import { KeyPair } from '../keypair/index.js';
const log = logger('connection'); const log = logger("connection");
export const PROTOCOL_NAME = '/fluence/particle/2.0.0'; export const PROTOCOL_NAME = "/fluence/particle/2.0.0";
/** /**
* Options to configure fluence relay connection * Options to configure fluence relay connection
@ -84,15 +87,21 @@ export interface RelayConnectionConfig {
export class RelayConnection implements IConnection { export class RelayConnection implements IConnection {
private relayAddress: Multiaddr; private relayAddress: Multiaddr;
private lib2p2Peer: Libp2p | null = null; private lib2p2Peer: Libp2p | null = null;
private relayPeerId: string;
constructor(private config: RelayConnectionConfig) { constructor(private config: RelayConnectionConfig) {
this.relayAddress = multiaddr(this.config.relayAddress); this.relayAddress = multiaddr(this.config.relayAddress);
throwIfHasNoPeerId(this.relayAddress); const peerId = this.relayAddress.getPeerId();
if (peerId == null) {
throwHasNoPeerId(this.relayAddress);
}
this.relayPeerId = peerId;
} }
getRelayPeerId(): string { getRelayPeerId(): string {
// since we check for peer id in constructor, we can safely use ! here return this.relayPeerId;
return this.relayAddress.getPeerId()!;
} }
supportsRelay(): boolean { supportsRelay(): boolean {
@ -117,11 +126,17 @@ export class RelayConnection implements IConnection {
streamMuxers: [yamux()], streamMuxers: [yamux()],
connectionEncryption: [noise()], connectionEncryption: [noise()],
connectionManager: { connectionManager: {
...(this.config.dialTimeoutMs != null
? {
dialTimeout: this.config.dialTimeoutMs, dialTimeout: this.config.dialTimeoutMs,
}
: {}),
}, },
connectionGater: { connectionGater: {
// By default, this function forbids connections to private peers. For example multiaddr with ip 127.0.0.1 isn't allowed // By default, this function forbids connections to private peers. For example multiaddr with ip 127.0.0.1 isn't allowed
denyDialMultiaddr: () => Promise.resolve(false), denyDialMultiaddr: () => {
return Promise.resolve(false);
},
}, },
services: { services: {
identify: identifyService(), identify: identifyService(),
@ -129,7 +144,10 @@ export class RelayConnection implements IConnection {
}, },
}); });
const supportedProtocols = (await this.lib2p2Peer.peerStore.get(this.lib2p2Peer.peerId)).protocols; const supportedProtocols = (
await this.lib2p2Peer.peerStore.get(this.lib2p2Peer.peerId)
).protocols;
await this.lib2p2Peer.peerStore.patch(this.lib2p2Peer.peerId, { await this.lib2p2Peer.peerStore.patch(this.lib2p2Peer.peerId, {
protocols: [...supportedProtocols, PROTOCOL_NAME], protocols: [...supportedProtocols, PROTOCOL_NAME],
}); });
@ -147,9 +165,12 @@ export class RelayConnection implements IConnection {
await this.lib2p2Peer.stop(); await this.lib2p2Peer.stop();
} }
async sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise<void> { async sendParticle(
nextPeerIds: PeerIdB58[],
particle: IParticle,
): Promise<void> {
if (this.lib2p2Peer === null) { if (this.lib2p2Peer === null) {
throw new Error('Relay connection is not started'); throw new Error("Relay connection is not started");
} }
if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.getRelayPeerId()) { if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.getRelayPeerId()) {
@ -160,29 +181,44 @@ export class RelayConnection implements IConnection {
); );
} }
log.trace('sending particle...'); log.trace("sending particle...");
// Reusing active connection here // Reusing active connection here
const stream = await this.lib2p2Peer.dialProtocol(this.relayAddress, PROTOCOL_NAME); const stream = await this.lib2p2Peer.dialProtocol(
log.trace('created stream with id ', stream.id); this.relayAddress,
PROTOCOL_NAME,
);
log.trace("created stream with id ", stream.id);
const sink = stream.sink; const sink = stream.sink;
await pipe([fromString(serializeToString(particle))], encode(), sink); await pipe([fromString(serializeToString(particle))], encode(), sink);
log.trace('data written to sink'); log.trace("data written to sink");
} }
// Await will appear after uncommenting lines in func body
// eslint-disable-next-line @typescript-eslint/require-await
private async processIncomingMessage(msg: string, stream: Stream) { private async processIncomingMessage(msg: string, stream: Stream) {
let particle: Particle | undefined; let particle: Particle | undefined;
try { try {
particle = Particle.fromString(msg); particle = Particle.fromString(msg);
log.trace('got particle from stream with id %s and particle id %s', stream.id, particle.id);
log.trace(
"got particle from stream with id %s and particle id %s",
stream.id,
particle.id,
);
const initPeerId = peerIdFromString(particle.initPeerId); const initPeerId = peerIdFromString(particle.initPeerId);
if (initPeerId.publicKey === undefined) { if (initPeerId.publicKey === undefined) {
log.error( log.error(
'cannot retrieve public key from init_peer_id. particle id: %s. init_peer_id: %s', "cannot retrieve public key from init_peer_id. particle id: %s. init_peer_id: %s",
particle.id, particle.id,
particle.initPeerId, particle.initPeerId,
); );
return; return;
} }
@ -191,57 +227,67 @@ export class RelayConnection implements IConnection {
buildParticleMessage(particle), buildParticleMessage(particle),
particle.signature, particle.signature,
); );
if (isVerified) { if (isVerified) {
this.particleSource.next(particle); this.particleSource.next(particle);
} else { } else {
log.trace('particle signature is incorrect. rejecting particle with id: %s', particle.id); log.trace(
"particle signature is incorrect. rejecting particle with id: %s",
particle.id,
);
} }
} catch (e) { } catch (e) {
const particleId = particle?.id; const particleId = particle?.id;
const particleIdMessage = typeof particleId === 'string' ? `. particle id: ${particleId}` : '';
log.error(`error on handling an incoming message: %O%s`, e, particleIdMessage); const particleIdMessage =
typeof particleId === "string" ? `. particle id: ${particleId}` : "";
log.error(
`error on handling an incoming message: %O%s`,
e,
particleIdMessage,
);
} }
} }
private async connect() { private async connect() {
if (this.lib2p2Peer === null) { if (this.lib2p2Peer === null) {
throw new Error('Relay connection is not started'); throw new Error("Relay connection is not started");
} }
await this.lib2p2Peer.handle( await this.lib2p2Peer.handle(
[PROTOCOL_NAME], [PROTOCOL_NAME],
async ({ connection, stream }) => ({ stream }) => {
pipe( void pipe(
stream.source, stream.source,
decode(), decode(),
(source) => map(source, (buf) => toString(buf.subarray())), (source) => {
return map(source, (buf) => {
return toString(buf.subarray());
});
},
async (source) => { async (source) => {
try { try {
for await (const msg of source) { for await (const msg of source) {
await this.processIncomingMessage(msg, stream); await this.processIncomingMessage(msg, stream);
} }
} catch (e) { } catch (e) {
log.error('connection closed: %j', e); log.error("connection closed: %j", e);
} }
}, },
), );
},
{ {
maxInboundStreams: this.config.maxInboundStreams, maxInboundStreams: this.config.maxInboundStreams,
maxOutboundStreams: this.config.maxOutboundStreams, maxOutboundStreams: this.config.maxOutboundStreams,
}, },
); );
log.debug("dialing to the node with client's address: %s", this.lib2p2Peer.peerId.toString()); log.debug(
"dialing to the node with client's address: %s",
this.lib2p2Peer.peerId.toString(),
);
try {
await this.lib2p2Peer.dial(this.relayAddress); await this.lib2p2Peer.dial(this.relayAddress);
} catch (e: any) {
if (e.name === 'AggregateError' && e._errors?.length === 1) {
const error = e._errors[0];
throw new Error(`Error dialing node ${this.relayAddress}:\n${error.code}\n${error.message}`);
} else {
throw e;
}
}
} }
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import type { Subscribable } from 'rxjs'; import type { PeerIdB58 } from "@fluencelabs/interfaces";
import { IParticle } from '../particle/interfaces.js'; import type { Subscribable } from "rxjs";
import { IStartable } from '../util/commonTypes.js';
import { IParticle } from "../particle/interfaces.js";
import { IStartable } from "../util/commonTypes.js";
/** /**
* Interface for connection used in Fluence Peer. * Interface for connection used in Fluence Peer.

View File

@ -1,17 +1,33 @@
import { it, describe, expect, beforeEach, afterEach } from 'vitest'; /**
import { DEFAULT_CONFIG, FluencePeer } from '../../jsPeer/FluencePeer.js'; * Copyright 2023 Fluence Labs Limited
import { CallServiceData, ResultCodes } from '../../jsServiceHost/interfaces.js'; *
import { KeyPair } from '../../keypair/index.js'; * Licensed under the Apache License, Version 2.0 (the "License");
import { EphemeralNetworkClient } from '../client.js'; * 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 { EphemeralNetwork, defaultConfig } from '../network.js'; import { it, describe, expect, beforeEach, afterEach } from "vitest";
import { DEFAULT_CONFIG, FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ResultCodes } from "../../jsServiceHost/interfaces.js";
import { KeyPair } from "../../keypair/index.js";
import { EphemeralNetworkClient } from "../client.js";
import { EphemeralNetwork, defaultConfig } from "../network.js";
let en: EphemeralNetwork; let en: EphemeralNetwork;
let client: FluencePeer; let client: FluencePeer;
const relay = defaultConfig.peers[0].peerId; const relay = defaultConfig.peers[0].peerId;
// TODO: race condition here. Needs to be fixed // TODO: race condition here. Needs to be fixed
describe.skip('Ephemeral networks tests', () => { describe.skip("Ephemeral networks tests", () => {
beforeEach(async () => { beforeEach(async () => {
en = new EphemeralNetwork(defaultConfig); en = new EphemeralNetwork(defaultConfig);
await en.up(); await en.up();
@ -22,17 +38,15 @@ describe.skip('Ephemeral networks tests', () => {
}); });
afterEach(async () => { afterEach(async () => {
if (client) {
await client.stop(); await client.stop();
}
if (en) {
await en.down(); await en.down();
}
}); });
it('smoke test', async function () { it("smoke test", async function () {
// arrange // arrange
const peers = defaultConfig.peers.map((x) => x.peerId); const peers = defaultConfig.peers.map((x) => {
return x.peerId;
});
const script = ` const script = `
(seq (seq
@ -62,19 +76,24 @@ describe.skip('Ephemeral networks tests', () => {
const particle = await client.internals.createNewParticle(script); const particle = await client.internals.createNewParticle(script);
const promise = new Promise<string>((resolve) => { const promise = new Promise<string>((resolve) => {
client.internals.regHandler.forParticle(particle.id, 'test', 'test', (req: CallServiceData) => { client.internals.regHandler.forParticle(
resolve('success'); particle.id,
"test",
"test",
() => {
resolve("success");
return { return {
result: 'test', result: "test",
retCode: ResultCodes.success, retCode: ResultCodes.success,
}; };
}); },
);
}); });
// act // act
client.internals.initiateParticle(particle, () => {}); client.internals.initiateParticle(particle, () => {});
// assert // assert
await expect(promise).resolves.toBe('success'); await expect(promise).resolves.toBe("success");
}); });
}); });

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,24 +13,46 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js'; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { KeyPair } from '../keypair/index.js';
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js";
import { WorkerLoader } from '../marine/worker-script/workerLoader.js'; import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { MarineBackgroundRunner } from '../marine/worker/index.js'; import { KeyPair } from "../keypair/index.js";
import { EphemeralNetwork } from './network.js'; import { WasmLoaderFromNpm } from "../marine/deps-loader/node.js";
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js'; import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { WorkerLoader } from "../marine/worker-script/workerLoader.js";
import { EphemeralNetwork } from "./network.js";
/** /**
* Ephemeral network client is a FluencePeer that connects to a relay peer in an ephemeral network. * Ephemeral network client is a FluencePeer that connects to a relay peer in an ephemeral network.
*/ */
export class EphemeralNetworkClient extends FluencePeer { export class EphemeralNetworkClient extends FluencePeer {
constructor(config: PeerConfig, keyPair: KeyPair, network: EphemeralNetwork, relay: PeerIdB58) { constructor(
config: PeerConfig,
keyPair: KeyPair,
network: EphemeralNetwork,
relay: PeerIdB58,
) {
const workerLoader = new WorkerLoader(); const workerLoader = new WorkerLoader();
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm'); const controlModuleLoader = new WasmLoaderFromNpm(
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, avmModuleLoader); "@fluencelabs/marine-js",
"marine-js.wasm",
);
const avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
const marine = new MarineBackgroundRunner(
workerLoader,
controlModuleLoader,
avmModuleLoader,
);
const conn = network.getRelayConnection(keyPair.getPeerId(), relay); const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
super(config, keyPair, marine, new JsServiceHost(), conn); super(config, keyPair, marine, new JsServiceHost(), conn);
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,23 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { fromBase64Sk, KeyPair } from '../keypair/index.js';
import { MarineBackgroundRunner } from '../marine/worker/index.js';
import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js'; import { PeerIdB58 } from "@fluencelabs/interfaces";
import { Subject } from "rxjs";
import { logger } from '../util/logger.js'; import { IConnection } from "../connection/interfaces.js";
import { Subject } from 'rxjs'; import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { Particle } from '../particle/Particle.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";
import { logger } from "../util/logger.js";
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js'; const log = logger("ephemeral");
import { DEFAULT_CONFIG, FluencePeer } from '../jsPeer/FluencePeer.js';
import { IConnection } from '../connection/interfaces.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
const log = logger('ephemeral');
interface EphemeralConfig { interface EphemeralConfig {
peers: Array<{ peers: Array<{
@ -41,84 +42,84 @@ interface EphemeralConfig {
export const defaultConfig = { export const defaultConfig = {
peers: [ peers: [
{ {
peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx', peerId: "12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx",
sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=', sk: "dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=",
}, },
{ {
peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV', peerId: "12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV",
sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=', sk: "dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=",
}, },
{ {
peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB', peerId: "12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB",
sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=', sk: "TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=",
}, },
{ {
peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK', peerId: "12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK",
sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=', sk: "hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=",
}, },
{ {
peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V', peerId: "12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V",
sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=', sk: "LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=",
}, },
{ {
peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF', peerId: "12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF",
sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=', sk: "XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=",
}, },
{ {
peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ', peerId: "12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ",
sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=', sk: "KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=",
}, },
{ {
peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD', peerId: "12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD",
sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=', sk: "GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=",
}, },
{ {
peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC', peerId: "12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC",
sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=', sk: "UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=",
}, },
{ {
peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi', peerId: "12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi",
sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=', sk: "8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=",
}, },
{ {
peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C', peerId: "12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C",
sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=', sk: "qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=",
}, },
{ {
peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6', peerId: "12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6",
sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=', sk: "vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=",
}, },
{ {
peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU', peerId: "12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU",
sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=', sk: "YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=",
}, },
{ {
peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC', peerId: "12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC",
sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=', sk: "ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=",
}, },
{ {
peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk', peerId: "12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk",
sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=', sk: "xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=",
}, },
{ {
peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY', peerId: "12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY",
sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=', sk: "1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=",
}, },
{ {
peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1', peerId: "12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1",
sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=', sk: "eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=",
}, },
{ {
peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny', peerId: "12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny",
sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=', sk: "UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=",
}, },
{ {
peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv', peerId: "12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv",
sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=', sk: "8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=",
}, },
{ {
peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4', peerId: "12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4",
sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=', sk: "RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=",
}, },
], ],
}; };
@ -160,7 +161,7 @@ export class EphemeralConnection implements IEphemeralConnection {
} }
disconnectFromAll() { disconnectFromAll() {
for (let other of this.connections.values()) { for (const other of this.connections.values()) {
this.disconnectFromOther(other); this.disconnectFromOther(other);
} }
} }
@ -168,29 +169,36 @@ export class EphemeralConnection implements IEphemeralConnection {
particleSource = new Subject<Particle>(); particleSource = new Subject<Particle>();
receiveParticle(particle: Particle): void { receiveParticle(particle: Particle): void {
this.particleSource.next(Particle.fromString(particle.toString())); this.particleSource.next(particle);
} }
async sendParticle(nextPeerIds: string[], particle: Particle): Promise<void> { sendParticle(nextPeerIds: string[], particle: Particle): Promise<void> {
const from = this.selfPeerId; const from = this.selfPeerId;
for (let to of nextPeerIds) {
for (const to of nextPeerIds) {
const destConnection = this.connections.get(to); const destConnection = this.connections.get(to);
if (destConnection === undefined) { if (destConnection === undefined) {
log.error('peer %s has no connection with %s', from, to); log.error("peer %s has no connection with %s", from, to);
continue; continue;
} }
// log.trace(`Sending particle from %s, to %j, particleId %s`, from, to, particle.id); // log.trace(`Sending particle from %s, to %j, particleId %s`, from, to, particle.id);
destConnection.receiveParticle(particle); destConnection.receiveParticle(particle);
} }
return Promise.resolve();
} }
getRelayPeerId(): string { getRelayPeerId(): string {
if (this.connections.size === 1) { const firstMapKey = this.connections.keys().next();
return this.connections.keys().next().value;
// Empty map
if (firstMapKey.done === true) {
throw new Error("relay is not supported in this Ephemeral network peer");
} }
throw new Error('relay is not supported in this Ephemeral network peer'); return firstMapKey.value;
} }
supportsRelay(): boolean { supportsRelay(): boolean {
@ -220,25 +228,42 @@ export class EphemeralNetwork {
controlModuleLoader: WasmLoaderFromNpm; controlModuleLoader: WasmLoaderFromNpm;
avmModuleLoader: WasmLoaderFromNpm; avmModuleLoader: WasmLoaderFromNpm;
constructor(public readonly config: EphemeralConfig) { constructor(readonly config: EphemeralConfig) {
// shared worker for all the peers // shared worker for all the peers
this.workerLoader = new WorkerLoaderFromFs('../../marine/worker-script'); 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'); this.controlModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/marine-js",
"marine-js.wasm",
);
this.avmModuleLoader = new WasmLoaderFromNpm(
"@fluencelabs/avm",
"avm.wasm",
);
} }
/** /**
* Starts the Ephemeral network up * Starts the Ephemeral network up
*/ */
async up(): Promise<void> { async up(): Promise<void> {
log.trace('starting ephemeral network up...'); log.trace("starting ephemeral network up...");
const promises = this.config.peers.map(async (x) => { const promises = this.config.peers.map(async (x) => {
const kp = await fromBase64Sk(x.sk); const kp = await fromBase64Sk(x.sk);
const marine = new MarineBackgroundRunner(this.workerLoader, this.controlModuleLoader, this.avmModuleLoader);
const marine = new MarineBackgroundRunner(
this.workerLoader,
this.controlModuleLoader,
this.avmModuleLoader,
);
const peerId = kp.getPeerId(); const peerId = kp.getPeerId();
if (peerId !== x.peerId) { if (peerId !== x.peerId) {
throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`); throw new Error(
`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`,
);
} }
return new EphemeralPeer(kp, marine); return new EphemeralPeer(kp, marine);
@ -252,14 +277,19 @@ export class EphemeralNetwork {
continue; continue;
} }
peers[i].ephemeralConnection.connectToOther(peers[j].ephemeralConnection); peers[i].ephemeralConnection.connectToOther(
peers[j].ephemeralConnection,
);
} }
} }
const startPromises = peers.map((x) => x.start()); const startPromises = peers.map((x) => {
return x.start();
});
await Promise.all(startPromises); await Promise.all(startPromises);
for (let p of peers) { for (const p of peers) {
this.peers.set(p.keyPair.getPeerId(), p); this.peers.set(p.keyPair.getPeerId(), p);
} }
} }
@ -268,16 +298,17 @@ export class EphemeralNetwork {
* Shuts the ephemeral network down. Will disconnect all connected peers. * Shuts the ephemeral network down. Will disconnect all connected peers.
*/ */
async down(): Promise<void> { async down(): Promise<void> {
log.trace('shutting down ephemeral network...'); log.trace("shutting down ephemeral network...");
const peers = Array.from(this.peers.entries()); const peers = Array.from(this.peers.entries());
const promises = peers.map(async ([k, p]) => {
await p.ephemeralConnection.disconnectFromAll(); const promises = peers.map(async ([, p]) => {
p.ephemeralConnection.disconnectFromAll();
await p.stop(); await p.stop();
}); });
await Promise.all(promises); await Promise.all(promises);
this.peers.clear(); this.peers.clear();
log.trace('ephemeral network shut down'); log.trace("ephemeral network shut down");
} }
/** /**
@ -285,6 +316,7 @@ export class EphemeralNetwork {
*/ */
getRelayConnection(peerId: PeerIdB58, relayPeerId: PeerIdB58): IConnection { getRelayConnection(peerId: PeerIdB58, relayPeerId: PeerIdB58): IConnection {
const relay = this.peers.get(relayPeerId); const relay = this.peers.get(relayPeerId);
if (relay === undefined) { if (relay === undefined) {
throw new Error(`Peer ${relayPeerId} is not found`); throw new Error(`Peer ${relayPeerId} is not found`);
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -21,20 +21,41 @@ interface PackageJsonContent {
// This will be substituted in build phase // This will be substituted in build phase
const packageJsonContentString = `__PACKAGE_JSON_CONTENT__`; const packageJsonContentString = `__PACKAGE_JSON_CONTENT__`;
let parsedPackageJsonContent: PackageJsonContent; let parsedPackageJsonContent: PackageJsonContent | undefined;
const PRIMARY_CDN = "https://unpkg.com/"; const PRIMARY_CDN = "https://unpkg.com/";
export async function fetchResource(pkg: string, assetPath: string) { export async function fetchResource(pkg: string, assetPath: string) {
const packageJsonContent = parsedPackageJsonContent || (parsedPackageJsonContent = JSON.parse(packageJsonContentString)); const packageJsonContent =
const version = packageJsonContent.dependencies[pkg] || packageJsonContent.devDependencies[pkg]; parsedPackageJsonContent ??
// TODO: Should be validated
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(parsedPackageJsonContent = JSON.parse(
packageJsonContentString,
) as PackageJsonContent);
const version =
packageJsonContent.dependencies[pkg] ??
packageJsonContent.devDependencies[pkg];
if (version === undefined) { if (version === undefined) {
const availableDeps = [...Object.keys(packageJsonContent.dependencies), ...Object.keys(packageJsonContent.devDependencies)]; const availableDeps = [
throw new Error(`Cannot find version of ${pkg} in package.json. Available versions: ${availableDeps.join(',')}`); ...Object.keys(packageJsonContent.dependencies),
...Object.keys(packageJsonContent.devDependencies),
];
throw new Error(
`Cannot find version of ${pkg} in package.json. Available versions: ${availableDeps.join(
",",
)}`,
);
} }
const refinedAssetPath = assetPath.startsWith('/') ? assetPath.slice(1) : assetPath; const refinedAssetPath = assetPath.startsWith("/")
? assetPath.slice(1)
: assetPath;
return fetch(new globalThis.URL(`${pkg}@${version}/` + refinedAssetPath, PRIMARY_CDN)); return fetch(
new globalThis.URL(`${pkg}@${version}/` + refinedAssetPath, PRIMARY_CDN),
);
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,17 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { fetchResource as fetchResourceBrowser } from './browser.js'; import process from "process";
import { fetchResource as fetchResourceNode } from './node.js';
import process from 'process';
const isNode = typeof process !== 'undefined' && process?.release?.name === 'node'; import { fetchResource as fetchResourceIsomorphic } from "#fetcher";
const isNode =
// process.release is undefined in browser env
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
typeof process !== "undefined" && process.release?.name === "node";
export async function fetchResource(pkg: string, path: string) { export async function fetchResource(pkg: string, path: string) {
switch (true) { switch (true) {
case isNode: case isNode:
return fetchResourceNode(pkg, path); return fetchResourceIsomorphic(pkg, path);
default: default:
return fetchResourceBrowser(pkg, path); return fetchResourceIsomorphic(pkg, path);
} }
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import fs from 'fs'; import fs from "fs";
import path from 'path'; import module from "module";
import module from 'module'; import path from "path";
export async function fetchResource(pkg: string, assetPath: string) { export async function fetchResource(pkg: string, assetPath: string) {
const require = module.createRequire(import.meta.url); const require = module.createRequire(import.meta.url);
@ -29,7 +29,7 @@ export async function fetchResource(pkg: string, assetPath: string) {
const packagePath = matches?.[0]; const packagePath = matches?.[0];
if (!packagePath) { if (packagePath == null) {
throw new Error(`Cannot find dependency ${pkg} in path ${posixPath}`); throw new Error(`Cannot find dependency ${pkg} in path ${posixPath}`);
} }
@ -38,22 +38,22 @@ export async function fetchResource(pkg: string, assetPath: string) {
const file = await new Promise<ArrayBuffer>((resolve, reject) => { const file = await new Promise<ArrayBuffer>((resolve, reject) => {
// Cannot use 'fs/promises' with current vite config. This module is not polyfilled by default. // Cannot use 'fs/promises' with current vite config. This module is not polyfilled by default.
fs.readFile(pathToResource, (err, data) => { fs.readFile(pathToResource, (err, data) => {
if (err) { if (err != null) {
reject(err); reject(err);
return; return;
} }
resolve(data); resolve(data);
}); });
}); });
return new Response(file, { return new Response(file, {
headers: { headers: {
'Content-type': "Content-type": assetPath.endsWith(".wasm")
assetPath.endsWith('.wasm') ? "application/wasm"
? 'application/wasm' : assetPath.endsWith(".js")
: assetPath.endsWith('.js') ? "application/javascript"
? 'application/javascript' : "application/text",
: 'application/text' },
}
}); });
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,41 +13,82 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { ClientConfig, IFluenceClient, RelayOptions, ConnectionState, CallAquaFunctionType, RegisterServiceType } from '@fluencelabs/interfaces';
import { ClientPeer, makeClientPeerConfig } from './clientPeer/ClientPeer.js';
import { callAquaFunction } from './compilerSupport/callFunction.js';
import { registerService } from './compilerSupport/registerService.js';
import { MarineBackgroundRunner } from './marine/worker/index.js';
// @ts-ignore
import { BlobWorker, Worker } from 'threads';
import { doRegisterNodeUtils } from './services/NodeUtils.js';
import { fetchResource } from './fetchers/index.js';
import process from 'process';
import path from 'path';
import url from 'url';
import module from 'module';
const isNode = typeof process !== 'undefined' && process?.release?.name === 'node'; import module from "module";
import path from "path";
import process from "process";
import url from "url";
const fetchWorkerCode = () => fetchResource('@fluencelabs/marine-worker', '/dist/browser/marine-worker.umd.cjs').then(res => res.text()); import type {
const fetchMarineJsWasm = () => fetchResource('@fluencelabs/marine-js', '/dist/marine-js.wasm').then(res => res.arrayBuffer()); ClientConfig,
const fetchAvmWasm = () => fetchResource('@fluencelabs/avm', '/dist/avm.wasm').then(res => res.arrayBuffer()); ConnectionState,
RelayOptions,
} from "@fluencelabs/interfaces";
import { BlobWorker, Worker } from "threads/master";
const createClient = async (relay: RelayOptions, config: ClientConfig): Promise<IFluenceClient> => { import { ClientPeer, makeClientPeerConfig } from "./clientPeer/ClientPeer.js";
import { callAquaFunction } from "./compilerSupport/callFunction.js";
import { registerService } from "./compilerSupport/registerService.js";
import { fetchResource } from "./fetchers/index.js";
import { MarineBackgroundRunner } from "./marine/worker/index.js";
import { doRegisterNodeUtils } from "./services/NodeUtils.js";
const isNode =
// process.release is undefined in browser env
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
typeof process !== "undefined" && process.release?.name === "node";
const fetchWorkerCode = async () => {
const resource = await fetchResource(
"@fluencelabs/marine-worker",
"/dist/browser/marine-worker.umd.cjs",
);
return resource.text();
};
const fetchMarineJsWasm = async () => {
const resource = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
);
return resource.arrayBuffer();
};
const fetchAvmWasm = async () => {
const resource = await fetchResource("@fluencelabs/avm", "/dist/avm.wasm");
return resource.arrayBuffer();
};
const createClient = async (
relay: RelayOptions,
config: ClientConfig,
): Promise<ClientPeer> => {
const marineJsWasm = await fetchMarineJsWasm(); const marineJsWasm = await fetchMarineJsWasm();
const avmWasm = await fetchAvmWasm(); const avmWasm = await fetchAvmWasm();
const marine = new MarineBackgroundRunner({ const marine = new MarineBackgroundRunner(
{
async getValue() { async getValue() {
if (isNode) { if (isNode) {
const require = module.createRequire(import.meta.url); const require = module.createRequire(import.meta.url);
const pathToThisFile = path.dirname(url.fileURLToPath(import.meta.url));
const pathToWorker = require.resolve('@fluencelabs/marine-worker'); const pathToThisFile = path.dirname(
const relativePathToWorker = path.relative(pathToThisFile, pathToWorker); url.fileURLToPath(import.meta.url),
);
const pathToWorker = require.resolve("@fluencelabs/marine-worker");
const relativePathToWorker = path.relative(
pathToThisFile,
pathToWorker,
);
return new Worker(relativePathToWorker); return new Worker(relativePathToWorker);
} else { } else {
const workerCode = await fetchWorkerCode(); const workerCode = await fetchWorkerCode();
return BlobWorker.fromText(workerCode) return BlobWorker.fromText(workerCode);
} }
}, },
start() { start() {
@ -56,28 +97,42 @@ const createClient = async (relay: RelayOptions, config: ClientConfig): Promise<
stop() { stop() {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}, },
}, { },
{
getValue() { getValue() {
return marineJsWasm; return marineJsWasm;
}, start(): Promise<void> { },
start(): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}, stop(): Promise<void> { },
stop(): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} },
}, { },
{
getValue() { getValue() {
return avmWasm; return avmWasm;
}, start(): Promise<void> { },
start(): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}, stop(): Promise<void> { },
stop(): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} },
}); },
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config); );
const client: IFluenceClient = new ClientPeer(peerConfig, relayConfig, keyPair, marine);
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(
relay,
config,
);
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine);
if (isNode) { if (isNode) {
doRegisterNodeUtils(client); doRegisterNodeUtils(client);
} }
await client.connect(); await client.connect();
return client; return client;
}; };
@ -85,22 +140,31 @@ const createClient = async (relay: RelayOptions, config: ClientConfig): Promise<
/** /**
* Public interface to Fluence Network * Public interface to Fluence Network
*/ */
export const Fluence = { interface FluencePublicApi {
defaultClient: undefined as (IFluenceClient | undefined), defaultClient: ClientPeer | undefined;
connect: (relay: RelayOptions, config: ClientConfig) => Promise<void>;
disconnect: () => Promise<void>;
onConnectionStateChange: (
handler: (state: ConnectionState) => void,
) => ConnectionState;
getClient: () => ClientPeer;
}
export const Fluence: FluencePublicApi = {
defaultClient: undefined,
/** /**
* Connect to the Fluence network * Connect to the Fluence network
* @param relay - relay node to connect to * @param relay - relay node to connect to
* @param config - client configuration * @param config - client configuration
*/ */
connect: async function(relay: RelayOptions, config: ClientConfig): Promise<void> { connect: async function (relay, config) {
const client = await createClient(relay, config); this.defaultClient = await createClient(relay, config);
this.defaultClient = client;
}, },
/** /**
* Disconnect from the Fluence network * Disconnect from the Fluence network
*/ */
disconnect: async function(): Promise<void> { disconnect: async function (): Promise<void> {
await this.defaultClient?.disconnect(); await this.defaultClient?.disconnect();
this.defaultClient = undefined; this.defaultClient = undefined;
}, },
@ -108,23 +172,32 @@ export const Fluence = {
/** /**
* Handle connection state changes. Immediately returns the current connection state * Handle connection state changes. Immediately returns the current connection state
*/ */
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { onConnectionStateChange(handler) {
return this.defaultClient?.onConnectionStateChange(handler) || 'disconnected'; return (
this.defaultClient?.onConnectionStateChange(handler) ?? "disconnected"
);
}, },
/** /**
* Low level API. Get the underlying client instance which holds the connection to the network * Low level API. Get the underlying client instance which holds the connection to the network
* @returns IFluenceClient instance * @returns IFluenceClient instance
*/ */
getClient: async function(): Promise<IFluenceClient> { getClient: function () {
if (!this.defaultClient) { if (this.defaultClient == null) {
throw new Error('Fluence client is not initialized. Call Fluence.connect() first'); throw new Error(
"Fluence client is not initialized. Call Fluence.connect() first",
);
} }
return this.defaultClient; return this.defaultClient;
}, },
}; };
export type { IFluenceClient, ClientConfig, CallParams } from '@fluencelabs/interfaces'; export type {
IFluenceClient,
ClientConfig,
CallParams,
} from "@fluencelabs/interfaces";
export type { export type {
ArrayType, ArrayType,
@ -151,14 +224,14 @@ export type {
FnConfig, FnConfig,
RegisterServiceType, RegisterServiceType,
RegisterServiceArgs, RegisterServiceArgs,
} from '@fluencelabs/interfaces'; } from "@fluencelabs/interfaces";
export { v5_callFunction, v5_registerService } from './api.js'; export { v5_callFunction, v5_registerService } from "./api.js";
// @ts-ignore // @ts-expect-error Writing to global object like this prohibited by ts
globalThis.new_fluence = Fluence; globalThis.new_fluence = Fluence;
// @ts-ignore // @ts-expect-error Writing to global object like this prohibited by ts
globalThis.fluence = { globalThis.fluence = {
clientFactory: createClient, clientFactory: createClient,
callAquaFunction, callAquaFunction,
@ -166,4 +239,9 @@ globalThis.fluence = {
}; };
export { createClient, callAquaFunction, registerService }; export { createClient, callAquaFunction, registerService };
export { getFluenceInterface, getFluenceInterfaceFromGlobalThis } from './util/loadClient.js'; export {
KeyPair,
fromBase64Sk,
fromBase58Sk,
fromOpts,
} from "./keypair/index.js";

View File

@ -1,5 +1,5 @@
/* /**
* Copyright 2021 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,10 +13,44 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { KeyPair } from '../keypair/index.js';
import type { PeerIdB58 } from '@fluencelabs/interfaces'; import { Buffer } from "buffer";
import { deserializeAvmResult, InterpreterResult, KeyPairFormat, serializeAvmArgs } from '@fluencelabs/avm';
import {
deserializeAvmResult,
InterpreterResult,
KeyPairFormat,
serializeAvmArgs,
} from "@fluencelabs/avm";
import { defaultCallParameters } from "@fluencelabs/marine-js/dist/types";
import { fromUint8Array } from "js-base64";
import {
concatMap,
filter,
groupBy,
lastValueFrom,
mergeMap,
pipe,
Subject,
tap,
Unsubscribable,
} from "rxjs";
import { IConnection } from "../connection/interfaces.js";
import {
CallServiceData,
CallServiceResult,
IJsServiceHost,
ResultCodes,
} from "../jsServiceHost/interfaces.js";
import {
getParticleContext,
registerDefaultServices,
ServiceError,
} from "../jsServiceHost/serviceUtils.js";
import { KeyPair } from "../keypair/index.js";
import { IMarineHost } from "../marine/interfaces.js";
import { IParticle } from "../particle/interfaces.js";
import { import {
cloneWithNewData, cloneWithNewData,
getActualTTL, getActualTTL,
@ -24,50 +58,18 @@ import {
Particle, Particle,
ParticleExecutionStage, ParticleExecutionStage,
ParticleQueueItem, ParticleQueueItem,
} from '../particle/Particle.js'; } from "../particle/Particle.js";
import { defaultCallParameters } from '@fluencelabs/marine-js/dist/types' import { registerSig } from "../services/_aqua/services.js";
import { jsonify, isString } from '../util/utils.js'; import { registerSrv } from "../services/_aqua/single-module-srv.js";
import { import { registerTracing } from "../services/_aqua/tracing.js";
concatAll, import { defaultSigGuard, Sig } from "../services/Sig.js";
concatMap, import { Srv } from "../services/SingleModuleSrv.js";
filter, import { Tracing } from "../services/Tracing.js";
from, import { logger } from "../util/logger.js";
groupBy, import { jsonify, isString, getErrorMessage } from "../util/utils.js";
lastValueFrom,
mergeAll,
mergeMap,
Observable,
pipe,
Subject,
tap,
Unsubscribable
} from 'rxjs';
import { defaultSigGuard, Sig } from '../services/Sig.js';
import { registerSig } from '../services/_aqua/services.js';
import { registerSrv } from '../services/_aqua/single-module-srv.js';
import { registerTracing } from '../services/_aqua/tracing.js';
import { Buffer } from 'buffer';
import { Srv } from '../services/SingleModuleSrv.js'; const log_particle = logger("particle");
import { Tracing } from '../services/Tracing.js'; const log_peer = logger("peer");
import { logger } from '../util/logger.js';
import { getParticleContext, registerDefaultServices, ServiceError } from '../jsServiceHost/serviceUtils.js';
import { IParticle } from '../particle/interfaces.js';
import { IConnection } from '../connection/interfaces.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import {
CallServiceData,
CallServiceResult,
GenericCallServiceHandler,
IJsServiceHost,
ResultCodes,
} from '../jsServiceHost/interfaces.js';
import { JSONValue } from '../util/commonTypes.js';
import { fromUint8Array } from 'js-base64';
const log_particle = logger('particle');
const log_peer = logger('peer');
export type PeerConfig = { export type PeerConfig = {
/** /**
@ -103,7 +105,7 @@ export const DEFAULT_CONFIG: PeerConfig = {
export abstract class FluencePeer { export abstract class FluencePeer {
constructor( constructor(
protected readonly config: PeerConfig, protected readonly config: PeerConfig,
public readonly keyPair: KeyPair, readonly keyPair: KeyPair,
protected readonly marineHost: IMarineHost, protected readonly marineHost: IMarineHost,
protected readonly jsServiceHost: IJsServiceHost, protected readonly jsServiceHost: IJsServiceHost,
protected readonly connection: IConnection, protected readonly connection: IConnection,
@ -112,16 +114,18 @@ export abstract class FluencePeer {
} }
async start(): Promise<void> { async start(): Promise<void> {
log_peer.trace('starting Fluence peer'); log_peer.trace("starting Fluence peer");
if (this.config?.debug?.printParticleId) {
if (this.config.debug.printParticleId) {
this.printParticleId = true; this.printParticleId = true;
} }
await this.marineHost.start(); await this.marineHost.start();
this._startParticleProcessing(); this._startParticleProcessing();
this.isInitialized = true; this.isInitialized = true;
await this.connection.start(); await this.connection.start();
log_peer.trace('started Fluence peer'); log_peer.trace("started Fluence peer");
} }
/** /**
@ -129,20 +133,20 @@ export abstract class FluencePeer {
* and disconnects from the Fluence network * and disconnects from the Fluence network
*/ */
async stop() { async stop() {
log_peer.trace('stopping Fluence peer'); log_peer.trace("stopping Fluence peer");
this._particleSourceSubscription?.unsubscribe(); this._particleSourceSubscription?.unsubscribe();
log_peer.trace('Waiting for all particles to finish execution'); log_peer.trace("Waiting for all particles to finish execution");
this._incomingParticles.complete(); this._incomingParticles.complete();
await this._incomingParticlePromise; await this._incomingParticlePromise;
log_peer.trace('All particles finished execution'); log_peer.trace("All particles finished execution");
this._stopParticleProcessing(); this._stopParticleProcessing();
await this.marineHost.stop(); await this.marineHost.stop();
await this.connection.stop(); await this.connection.stop();
this.isInitialized = false; this.isInitialized = false;
log_peer.trace('stopped Fluence peer'); log_peer.trace("stopped Fluence peer");
} }
/** /**
@ -154,11 +158,10 @@ export abstract class FluencePeer {
* @param wasm - buffer with the wasm file for service * @param wasm - buffer with the wasm file for service
* @param serviceId - the service id by which the service can be accessed in aqua * @param serviceId - the service id by which the service can be accessed in aqua
*/ */
async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> { async registerMarineService(
if (!this.marineHost) { wasm: SharedArrayBuffer | Buffer,
throw new Error("Can't register marine service: peer is not initialized"); serviceId: string,
} ): Promise<void> {
if (this.jsServiceHost.hasService(serviceId)) { if (this.jsServiceHost.hasService(serviceId)) {
throw new Error(`Service with '${serviceId}' id already exists`); throw new Error(`Service with '${serviceId}' id already exists`);
} }
@ -174,35 +177,47 @@ export abstract class FluencePeer {
await this.marineHost.removeService(serviceId); await this.marineHost.removeService(serviceId);
} }
// internal api
/** /**
* @private Is not intended to be used manually. Subject to change * @private Is not intended to be used manually. Subject to change
*/ */
get internals() { get internals() {
return { return {
getServices: () => this._classServices, getServices: () => {
return this._classServices;
},
getRelayPeerId: () => { getRelayPeerId: () => {
if (this.connection.supportsRelay()) { if (this.connection.supportsRelay()) {
return this.connection.getRelayPeerId(); return this.connection.getRelayPeerId();
} }
throw new Error('Relay is not supported by the current connection'); throw new Error("Relay is not supported by the current connection");
}, },
parseAst: async (air: string): Promise<{ success: boolean; data: any }> => { parseAst: async (
air: string,
): Promise<{ success: boolean; data: unknown }> => {
if (!this.isInitialized) { if (!this.isInitialized) {
new Error("Can't use avm: peer is not initialized"); new Error("Can't use avm: peer is not initialized");
} }
const res = await this.marineHost.callService('avm', 'ast', [air], defaultCallParameters); const res = await this.marineHost.callService(
"avm",
"ast",
[air],
defaultCallParameters,
);
if (!isString(res)) { if (!isString(res)) {
throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`); throw new Error(
`Call to avm:ast expected to return string. Actual return: ${JSON.stringify(
res,
)}`,
);
} }
try { try {
if (res.startsWith('error')) { if (res.startsWith("error")) {
return { return {
success: false, success: false,
data: res, data: res,
@ -214,12 +229,25 @@ export abstract class FluencePeer {
}; };
} }
} catch (err) { } catch (err) {
throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err); throw new Error(
"Failed to call avm. Result: " +
res +
". Error: " +
getErrorMessage(err),
);
} }
}, },
createNewParticle: (script: string, ttl: number = this.config.defaultTtlMs): Promise<IParticle> => { createNewParticle: (
return Particle.createNew(script, this.keyPair.getPeerId(), ttl, this.keyPair); script: string,
ttl: number = this.config.defaultTtlMs,
): Promise<IParticle> => {
return Particle.createNew(
script,
this.keyPair.getPeerId(),
ttl,
this.keyPair,
);
}, },
/** /**
@ -227,13 +255,20 @@ export abstract class FluencePeer {
* @param particle - particle to start execution of * @param particle - particle to start execution of
* @param onStageChange - callback for reacting on particle state changes * @param onStageChange - callback for reacting on particle state changes
*/ */
initiateParticle: (particle: IParticle, onStageChange: (stage: ParticleExecutionStage) => void): void => { initiateParticle: (
particle: IParticle,
onStageChange: (stage: ParticleExecutionStage) => void,
): void => {
if (!this.isInitialized) { if (!this.isInitialized) {
throw new Error('Cannot initiate new particle: peer is not initialized'); throw new Error(
"Cannot initiate new particle: peer is not initialized",
);
} }
if (this.printParticleId) { if (this.printParticleId) {
console.log('Particle id: ', particle.id); // This is intended console-log
// eslint-disable-next-line no-console
console.log("Particle id: ", particle.id);
} }
this._incomingParticles.next({ this._incomingParticles.next({
@ -250,11 +285,15 @@ export abstract class FluencePeer {
/** /**
* Register handler for all particles * Register handler for all particles
*/ */
common: this.jsServiceHost.registerGlobalHandler.bind(this.jsServiceHost), common: this.jsServiceHost.registerGlobalHandler.bind(
this.jsServiceHost,
),
/** /**
* Register handler which will be called only for particle with the specific id * Register handler which will be called only for particle with the specific id
*/ */
forParticle: this.jsServiceHost.registerParticleScopeHandler.bind(this.jsServiceHost), forParticle: this.jsServiceHost.registerParticleScopeHandler.bind(
this.jsServiceHost,
),
}, },
}; };
} }
@ -290,36 +329,54 @@ export abstract class FluencePeer {
registerDefaultServices(this); registerDefaultServices(this);
this._classServices.sig.securityGuard = defaultSigGuard(peerId); this._classServices.sig.securityGuard = defaultSigGuard(peerId);
registerSig(this, 'sig', this._classServices.sig); registerSig(this, "sig", this._classServices.sig);
registerSig(this, peerId, this._classServices.sig); registerSig(this, peerId, this._classServices.sig);
registerSrv(this, 'single_module_srv', this._classServices.srv); registerSrv(this, "single_module_srv", this._classServices.srv);
registerTracing(this, 'tracingSrv', this._classServices.tracing); registerTracing(this, "tracingSrv", this._classServices.tracing);
} }
private _startParticleProcessing() { private _startParticleProcessing() {
this._particleSourceSubscription = this.connection.particleSource.subscribe({ this._particleSourceSubscription = this.connection.particleSource.subscribe(
{
next: (p) => { next: (p) => {
this._incomingParticles.next({ particle: p, callResults: [], onStageChange: () => {} }); this._incomingParticles.next({
}, particle: p,
callResults: [],
onStageChange: () => {},
}); });
},
},
);
this._incomingParticlePromise = lastValueFrom(this._incomingParticles this._incomingParticlePromise = lastValueFrom(
.pipe( this._incomingParticles.pipe(
tap((item) => { tap((item) => {
log_particle.debug('id %s. received:', item.particle.id); log_particle.debug("id %s. received:", item.particle.id);
log_particle.trace('id %s. data: %j', item.particle.id, {
log_particle.trace("id %s. data: %j", item.particle.id, {
initPeerId: item.particle.initPeerId, initPeerId: item.particle.initPeerId,
timestamp: item.particle.timestamp, timestamp: item.particle.timestamp,
ttl: item.particle.ttl, ttl: item.particle.ttl,
signature: item.particle.signature, signature: item.particle.signature,
}); });
log_particle.trace('id %s. script: %s', item.particle.id, item.particle.script); log_particle.trace(
log_particle.trace('id %s. call results: %j', item.particle.id, item.callResults); "id %s. script: %s",
item.particle.id,
item.particle.script,
);
log_particle.trace(
"id %s. call results: %j",
item.particle.id,
item.callResults,
);
}), }),
filterExpiredParticles(this._expireParticle.bind(this)), filterExpiredParticles(this._expireParticle.bind(this)),
groupBy(item => fromUint8Array(item.particle.signature)), groupBy((item) => {
mergeMap(group$ => { return fromUint8Array(item.particle.signature);
}),
mergeMap((group$) => {
let prevData: Uint8Array = Buffer.from([]); let prevData: Uint8Array = Buffer.from([]);
let firstRun = true; let firstRun = true;
@ -334,7 +391,7 @@ export abstract class FluencePeer {
firstRun = false; firstRun = false;
} }
if (!this.isInitialized || this.marineHost === undefined) { if (!this.isInitialized) {
// If `.stop()` was called return null to stop particle processing immediately // If `.stop()` was called return null to stop particle processing immediately
return null; return null;
} }
@ -344,8 +401,16 @@ export abstract class FluencePeer {
// MUST happen sequentially (in a critical section). // MUST happen sequentially (in a critical section).
// Otherwise the race might occur corrupting the prevData // Otherwise the race might occur corrupting the prevData
log_particle.debug('id %s. sending particle to interpreter', item.particle.id); log_particle.debug(
log_particle.trace('id %s. prevData: %s', item.particle.id, this.decodeAvmData(prevData).slice(0, 50)); "id %s. sending particle to interpreter",
item.particle.id,
);
log_particle.trace(
"id %s. prevData: %s",
item.particle.id,
this.decodeAvmData(prevData).slice(0, 50),
);
const args = serializeAvmArgs( const args = serializeAvmArgs(
{ {
@ -364,14 +429,24 @@ export abstract class FluencePeer {
); );
let avmCallResult: InterpreterResult | Error; let avmCallResult: InterpreterResult | Error;
try { try {
const res = await this.marineHost.callService('avm', 'invoke', args, defaultCallParameters); const res = await this.marineHost.callService(
"avm",
"invoke",
args,
defaultCallParameters,
);
avmCallResult = deserializeAvmResult(res); avmCallResult = deserializeAvmResult(res);
} catch (e) { } catch (e) {
avmCallResult = e instanceof Error ? e : new Error(String(e)); avmCallResult = e instanceof Error ? e : new Error(String(e));
} }
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) { if (
!(avmCallResult instanceof Error) &&
avmCallResult.retCode === 0
) {
const newData = Buffer.from(avmCallResult.data); const newData = Buffer.from(avmCallResult.data);
prevData = newData; prevData = newData;
} }
@ -381,8 +456,14 @@ export abstract class FluencePeer {
result: avmCallResult, result: avmCallResult,
}; };
}), }),
filter((item): item is NonNullable<typeof item> => item !== null), filter((item): item is NonNullable<typeof item> => {
filterExpiredParticles<ParticleQueueItem & {result: Error | InterpreterResult }>(this._expireParticle.bind(this)), return item !== null;
}),
filterExpiredParticles<
ParticleQueueItem & {
result: Error | InterpreterResult;
}
>(this._expireParticle.bind(this)),
mergeMap(async (item) => { mergeMap(async (item) => {
// If peer was stopped, do not proceed further // If peer was stopped, do not proceed further
if (!this.isInitialized) { if (!this.isInitialized) {
@ -391,60 +472,89 @@ export abstract class FluencePeer {
// Do not continue if there was an error in particle interpretation // Do not continue if there was an error in particle interpretation
if (item.result instanceof Error) { if (item.result instanceof Error) {
log_particle.error('id %s. interpreter failed: %s', item.particle.id, item.result.message); log_particle.error(
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message }); "id %s. interpreter failed: %s",
item.particle.id,
item.result.message,
);
item.onStageChange({
stage: "interpreterError",
errorMessage: item.result.message,
});
return; return;
} }
if (item.result.retCode !== 0) { if (item.result.retCode !== 0) {
log_particle.error( log_particle.error(
'id %s. interpreter failed: retCode: %d, message: %s', "id %s. interpreter failed: retCode: %d, message: %s",
item.particle.id, item.particle.id,
item.result.retCode, item.result.retCode,
item.result.errorMessage, item.result.errorMessage,
); );
log_particle.trace('id %s. avm data: %s', item.particle.id, this.decodeAvmData(item.result.data));
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage }); log_particle.trace(
"id %s. avm data: %s",
item.particle.id,
this.decodeAvmData(item.result.data),
);
item.onStageChange({
stage: "interpreterError",
errorMessage: item.result.errorMessage,
});
return; return;
} }
log_particle.trace( log_particle.trace(
'id %s. interpreter result: retCode: %d, avm data: %s', "id %s. interpreter result: retCode: %d, avm data: %s",
item.particle.id, item.particle.id,
item.result.retCode, item.result.retCode,
this.decodeAvmData(item.result.data) this.decodeAvmData(item.result.data),
); );
setTimeout(() => { setTimeout(() => {
item.onStageChange({ stage: 'interpreted' }); item.onStageChange({ stage: "interpreted" });
}, 0); }, 0);
let connectionPromise: Promise<void> = Promise.resolve(); let connectionPromise: Promise<void> = Promise.resolve();
// send particle further if requested // send particle further if requested
if (item.result.nextPeerPks.length > 0) { if (item.result.nextPeerPks.length > 0) {
const newParticle = cloneWithNewData(item.particle, Buffer.from(item.result.data)); const newParticle = cloneWithNewData(
item.particle,
// Do not send particle after the peer has been stopped Buffer.from(item.result.data),
if (!this.isInitialized) { );
return;
}
log_particle.debug( log_particle.debug(
'id %s. sending particle into network. Next peer ids: %s', "id %s. sending particle into network. Next peer ids: %s",
newParticle.id, newParticle.id,
item.result.nextPeerPks.toString(), item.result.nextPeerPks.toString(),
); );
connectionPromise = this.connection connectionPromise = this.connection
?.sendParticle(item.result.nextPeerPks, newParticle) .sendParticle(item.result.nextPeerPks, newParticle)
.then(() => { .then(() => {
log_particle.trace('id %s. send successful', newParticle.id); log_particle.trace(
item.onStageChange({ stage: 'sent' }); "id %s. send successful",
newParticle.id,
);
item.onStageChange({ stage: "sent" });
}) })
.catch((e: any) => { .catch((e: unknown) => {
log_particle.error('id %s. send failed %j', newParticle.id, e); log_particle.error(
item.onStageChange({ stage: 'sendingError', errorMessage: e.toString() }); "id %s. send failed %j",
newParticle.id,
e,
);
item.onStageChange({
stage: "sendingError",
errorMessage: getErrorMessage(e),
});
}); });
} }
@ -460,7 +570,7 @@ export abstract class FluencePeer {
particleContext: getParticleContext(item.particle), particleContext: getParticleContext(item.particle),
}; };
this._execSingleCallRequest(req) void this._execSingleCallRequest(req)
.catch((err): CallServiceResult => { .catch((err): CallServiceResult => {
if (err instanceof ServiceError) { if (err instanceof ServiceError) {
return { return {
@ -471,9 +581,11 @@ export abstract class FluencePeer {
return { return {
retCode: ResultCodes.error, retCode: ResultCodes.error,
result: `Service call failed. fnName="${req.fnName}" serviceId="${ result: `Service call failed. fnName="${
req.fnName
}" serviceId="${
req.serviceId req.serviceId
}" error: ${err.toString()}`, }" error: ${getErrorMessage(err)}`,
}; };
}) })
.then((res) => { .then((res) => {
@ -482,7 +594,11 @@ export abstract class FluencePeer {
retCode: res.retCode, retCode: res.retCode,
}; };
const newParticle = cloneWithNewData(item.particle, Buffer.from([])); const newParticle = cloneWithNewData(
item.particle,
Buffer.from([]),
);
this._incomingParticles.next({ this._incomingParticles.next({
...item, ...item,
particle: newParticle, particle: newParticle,
@ -491,45 +607,59 @@ export abstract class FluencePeer {
}); });
} }
} else { } else {
item.onStageChange({ stage: 'localWorkDone' }); item.onStageChange({ stage: "localWorkDone" });
} }
return connectionPromise; return connectionPromise;
}), }),
);
) }),
}) ),
), { defaultValue: undefined }); { defaultValue: undefined },
);
} }
private _expireParticle(item: ParticleQueueItem) { private _expireParticle(item: ParticleQueueItem) {
const particleId = item.particle.id; const particleId = item.particle.id;
log_particle.debug( log_particle.debug(
'id %s. particle has expired after %d. Deleting particle-related queues and handlers', "id %s. particle has expired after %d. Deleting particle-related queues and handlers",
item.particle.id, item.particle.id,
item.particle.ttl, item.particle.ttl,
); );
this.jsServiceHost.removeParticleScopeHandlers(particleId); this.jsServiceHost.removeParticleScopeHandlers(particleId);
item.onStageChange({ stage: 'expired' }); item.onStageChange({ stage: "expired" });
} }
private decodeAvmData(data: Uint8Array) { private decodeAvmData(data: Uint8Array) {
return new TextDecoder().decode(data.buffer); return new TextDecoder().decode(data.buffer);
} }
private async _execSingleCallRequest(req: CallServiceData): Promise<CallServiceResult> { private async _execSingleCallRequest(
req: CallServiceData,
): Promise<CallServiceResult> {
const particleId = req.particleContext.particleId; const particleId = req.particleContext.particleId;
log_particle.trace('id %s. executing call service handler %j', particleId, req);
if (this.marineHost && await this.marineHost.hasService(req.serviceId)) { log_particle.trace(
"id %s. executing call service handler %j",
particleId,
req,
);
if (await this.marineHost.hasService(req.serviceId)) {
// TODO build correct CallParameters instead of default ones // TODO build correct CallParameters instead of default ones
const result = await this.marineHost.callService(req.serviceId, req.fnName, req.args, defaultCallParameters); const result = await this.marineHost.callService(
req.serviceId,
req.fnName,
req.args,
defaultCallParameters,
);
return { return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
result: result as JSONValue, result: result,
}; };
} }
@ -538,13 +668,19 @@ export abstract class FluencePeer {
if (res === null) { if (res === null) {
res = { res = {
retCode: ResultCodes.error, retCode: ResultCodes.error,
result: `No service found for service call: serviceId='${req.serviceId}', fnName='${ result: `No service found for service call: serviceId='${
req.fnName req.serviceId
}' args='${jsonify(req.args)}'`, }', fnName='${req.fnName}' args='${jsonify(req.args)}'`,
}; };
} }
log_particle.trace('id %s. executed call service handler, req: %j, res: %j ', particleId, req, res); log_particle.trace(
"id %s. executed call service handler, req: %j, res: %j ",
particleId,
req,
res,
);
return res; return res;
} }
@ -556,13 +692,17 @@ export abstract class FluencePeer {
} }
} }
function filterExpiredParticles<T extends ParticleQueueItem>(onParticleExpiration: (item: T) => void) { function filterExpiredParticles<T extends ParticleQueueItem>(
onParticleExpiration: (item: T) => void,
) {
return pipe( return pipe(
tap((item: T) => { tap((item: T) => {
if (hasExpired(item.particle)) { if (hasExpired(item.particle)) {
onParticleExpiration(item); onParticleExpiration(item);
} }
}), }),
filter((x) => !hasExpired(x.particle)), filter((x) => {
return !hasExpired(x.particle);
}),
); );
} }

View File

@ -1,23 +1,43 @@
import { it, describe, expect } from 'vitest'; /**
import { registerHandlersHelper, withPeer } from '../../util/testUtils.js'; * Copyright 2023 Fluence Labs Limited
import { handleTimeout } from '../../particle/Particle.js'; *
* 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.
*/
describe('Basic AVM functionality in Fluence Peer tests', () => { import { JSONValue } from "@fluencelabs/interfaces";
it('Simple call', async () => { import { it, describe, expect } from "vitest";
import { handleTimeout } from "../../particle/Particle.js";
import { registerHandlersHelper, withPeer } from "../../util/testUtils.js";
describe("Basic AVM functionality in Fluence Peer tests", () => {
it("Simple call", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(call %init_peer_id% ("print" "print") ["1"]) (call %init_peer_id% ("print" "print") ["1"])
`; `;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise<string>((resolve, reject) => { const res = await new Promise<JSONValue>((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
print: { print: {
print: (args: Array<string>) => { print: (args): undefined => {
const [res] = args; const [res] = args;
resolve(res); resolve(res);
}, },
@ -27,11 +47,11 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(res).toBe('1'); expect(res).toBe("1");
}); });
}); });
it('Par call', async () => { it("Par call", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(seq (seq
@ -42,20 +62,23 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
(call %init_peer_id% ("print" "print") ["2"]) (call %init_peer_id% ("print" "print") ["2"])
) )
`; `;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise<string[]>((resolve, reject) => { const res = await new Promise<JSONValue[]>((resolve, reject) => {
const res: any[] = []; const res: JSONValue[] = [];
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
print: { print: {
print: (args: any) => { print: (args): undefined => {
res.push(args[0]); res.push(args[0]);
if (res.length == 2) {
if (res.length === 2) {
resolve(res); resolve(res);
} }
}, },
@ -65,11 +88,11 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(res).toStrictEqual(['1', '2']); expect(res).toStrictEqual(["1", "2"]);
}); });
}); });
it('Timeout in par call: race', async () => { it("Timeout in par call: race", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(seq (seq
@ -86,16 +109,18 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
) )
) )
`; `;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise((resolve, reject) => { const res = await new Promise((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
return: { return: {
return: (args: any) => { return: (args): undefined => {
resolve(args[0]); resolve(args[0]);
}, },
}, },
@ -104,11 +129,11 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(res).toBe('fast_result'); expect(res).toBe("fast_result");
}); });
}); });
it('Timeout in par call: wait', async () => { it("Timeout in par call: wait", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(seq (seq
@ -136,16 +161,18 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
) )
) )
`; `;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise((resolve, reject) => { const res = await new Promise((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
return: { return: {
return: (args: any) => { return: (args): undefined => {
resolve(args[0]); resolve(args[0]);
}, },
}, },
@ -154,7 +181,7 @@ describe('Basic AVM functionality in Fluence Peer tests', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(res).toBe('failed_with_timeout'); expect(res).toBe("failed_with_timeout");
}); });
}); });
}); });

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,13 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { describe, expect, it } from 'vitest';
import { registerHandlersHelper, withPeer } from '../../util/testUtils.js';
import { handleTimeout } from '../../particle/Particle.js';
import { CallServiceData, ResultCodes } from '../../jsServiceHost/interfaces.js';
describe('FluencePeer flow tests', () => { import assert from "assert";
it('should execute par instruction in parallel', async function () {
import { JSONValue } from "@fluencelabs/interfaces";
import { describe, expect, it } from "vitest";
import {
CallServiceData,
ResultCodes,
} from "../../jsServiceHost/interfaces.js";
import { handleTimeout } from "../../particle/Particle.js";
import { registerHandlersHelper, withPeer } from "../../util/testUtils.js";
describe("FluencePeer flow tests", () => {
it("should execute par instruction in parallel", async function () {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(par (par
@ -36,9 +44,14 @@ describe('FluencePeer flow tests', () => {
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise<any>((resolve, reject) => { const res = await new Promise((resolve, reject) => {
peer.internals.regHandler.forParticle(particle.id, 'flow', 'timeout', (req: CallServiceData) => { peer.internals.regHandler.forParticle(
particle.id,
"flow",
"timeout",
(req: CallServiceData) => {
const [timeout, message] = req.args; const [timeout, message] = req.args;
assert(typeof timeout === "number");
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
@ -46,29 +59,34 @@ describe('FluencePeer flow tests', () => {
result: message, result: message,
retCode: ResultCodes.success, retCode: ResultCodes.success,
}; };
resolve(res); resolve(res);
}, timeout); }, timeout);
}); });
}); },
);
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
const values: any[] = []; const values: JSONValue[] = [];
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
callback: { callback: {
callback1: (args: any) => { callback1: (args): undefined => {
const [val] = args; const [val] = args;
values.push(val); values.push(val);
if (values.length === 2) { if (values.length === 2) {
resolve(values); resolve(values);
} }
}, },
callback2: (args: any) => { callback2: (args): undefined => {
const [val] = args; const [val] = args;
values.push(val); values.push(val);
if (values.length === 2) { if (values.length === 2) {
resolve(values); resolve(values);
} }
@ -79,7 +97,7 @@ describe('FluencePeer flow tests', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
await expect(res).toEqual(expect.arrayContaining(["test1", "test1"])); expect(res).toEqual(expect.arrayContaining(["test1", "test1"]));
}); });
}, 1500); }, 1500);
}); });

View File

@ -1,10 +1,26 @@
import { it, describe, expect } from 'vitest'; /**
* 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 { withPeer } from '../../util/testUtils.js'; import { it, describe, expect } from "vitest";
describe('Parse ast tests', () => { import { withPeer } from "../../util/testUtils.js";
it('Correct ast should be parsed correctly', async () => {
withPeer(async (peer) => { describe("Parse ast tests", () => {
it("Correct ast should be parsed correctly", async () => {
await withPeer(async (peer) => {
const air = `(null)`; const air = `(null)`;
const res = await peer.internals.parseAst(air); const res = await peer.internals.parseAst(air);
@ -15,14 +31,15 @@ describe('Parse ast tests', () => {
}); });
}); });
it('Incorrect ast should result in corresponding error', async () => { it("Incorrect ast should result in corresponding error", async () => {
withPeer(async (peer) => { await withPeer(async (peer) => {
const air = `(null`; const air = `(null`;
const res = await peer.internals.parseAst(air); const res = await peer.internals.parseAst(air);
expect(res).toStrictEqual({ expect(res).toStrictEqual({
success: false, success: false,
data: expect.stringContaining('error'), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data: expect.stringContaining("error"),
}); });
}); });
}); });

View File

@ -1,16 +1,36 @@
import { it, describe, expect } from 'vitest'; /**
* 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 { isFluencePeer } from '../../api.js'; import { it, describe, expect } from "vitest";
import { mkTestPeer, registerHandlersHelper, withPeer } from '../../util/testUtils.js';
import { handleTimeout } from '../../particle/Particle.js';
import { FluencePeer } from '../FluencePeer.js';
describe('FluencePeer usage test suite', () => { import { isFluencePeer } from "../../api.js";
it('should perform test for FluencePeer class correctly', async () => { import { handleTimeout } from "../../particle/Particle.js";
import {
mkTestPeer,
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 // arrange
const peer = await mkTestPeer(); const peer = await mkTestPeer();
const number = 1; const number = 1;
const object = { str: 'Hello!' }; const object = { str: "Hello!" };
const undefinedVal = undefined; const undefinedVal = undefined;
// act // act
@ -26,7 +46,7 @@ describe('FluencePeer usage test suite', () => {
expect(isUndefinedPeer).toBe(false); expect(isUndefinedPeer).toBe(false);
}); });
it('Should successfully call identity on local peer', async function () { it("Should successfully call identity on local peer", async function () {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(seq (seq
@ -34,16 +54,18 @@ describe('FluencePeer usage test suite', () => {
(call %init_peer_id% ("callback" "callback") [res]) (call %init_peer_id% ("callback" "callback") [res])
) )
`; `;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise<string>((resolve, reject) => { const res = await new Promise((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
callback: { callback: {
callback: async (args: any) => { callback: (args): undefined => {
const [res] = args; const [res] = args;
resolve(res); resolve(res);
}, },
@ -53,15 +75,16 @@ describe('FluencePeer usage test suite', () => {
peer.internals.initiateParticle(particle, handleTimeout(reject)); peer.internals.initiateParticle(particle, handleTimeout(reject));
}); });
expect(res).toBe('test'); expect(res).toBe("test");
}); });
}); });
it('Should throw correct message when calling non existing local service', async function () { it("Should throw correct message when calling non existing local service", async function () {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const res = callIncorrectService(peer); const res = callIncorrectService(peer);
await expect(res).rejects.toMatchObject({ await expect(res).rejects.toMatchObject({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining( message: expect.stringContaining(
`"No service found for service call: serviceId='incorrect', fnName='incorrect' args='[]'"`, `"No service found for service call: serviceId='incorrect', fnName='incorrect' args='[]'"`,
), ),
@ -70,7 +93,7 @@ describe('FluencePeer usage test suite', () => {
}); });
}); });
it('Should not crash if undefined is passed as a variable', async () => { it("Should not crash if undefined is passed as a variable", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(seq (seq
@ -80,23 +103,27 @@ describe('FluencePeer usage test suite', () => {
(call %init_peer_id% ("callback" "callback") [res]) (call %init_peer_id% ("callback" "callback") [res])
) )
)`; )`;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const res = await new Promise<any>((resolve, reject) => { const res = await new Promise((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
load: { load: {
arg: () => undefined, arg: () => {
return undefined;
},
}, },
callback: { callback: {
callback: (args: any) => { callback: (args): undefined => {
const [val] = args; const [val] = args;
resolve(val); resolve(val);
}, },
error: (args: any) => { error: (args): undefined => {
const [error] = args; const [error] = args;
reject(error); reject(error);
}, },
@ -110,28 +137,30 @@ describe('FluencePeer usage test suite', () => {
}); });
}); });
it('Should not crash if an error ocurred in user-defined handler', async () => { it("Should not crash if an error ocurred in user-defined handler", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
const script = ` const script = `
(xor (xor
(call %init_peer_id% ("load" "arg") [] arg) (call %init_peer_id% ("load" "arg") [] arg)
(call %init_peer_id% ("callback" "error") [%last_error%]) (call %init_peer_id% ("callback" "error") [%last_error%])
)`; )`;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
const promise = new Promise<any>((_resolve, reject) => { const promise = new Promise<never>((_resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
load: { load: {
arg: () => { arg: () => {
throw new Error('my super custom error message'); throw new Error("my super custom error message");
}, },
}, },
callback: { callback: {
error: (args: any) => { error: (args): undefined => {
const [error] = args; const [error] = args;
reject(error); reject(error);
}, },
@ -142,31 +171,34 @@ describe('FluencePeer usage test suite', () => {
}); });
await expect(promise).rejects.toMatchObject({ await expect(promise).rejects.toMatchObject({
message: expect.stringContaining('my super custom error message'), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
message: expect.stringContaining("my super custom error message"),
}); });
}); });
}); });
}); });
async function callIncorrectService(peer: FluencePeer): Promise<string[]> { async function callIncorrectService(peer: FluencePeer) {
const script = ` const script = `
(xor (xor
(call %init_peer_id% ("incorrect" "incorrect") [] res) (call %init_peer_id% ("incorrect" "incorrect") [] res)
(call %init_peer_id% ("callback" "error") [%last_error%]) (call %init_peer_id% ("callback" "error") [%last_error%])
)`; )`;
const particle = await peer.internals.createNewParticle(script); const particle = await peer.internals.createNewParticle(script);
return new Promise<any[]>((resolve, reject) => { return new Promise<unknown[]>((resolve, reject) => {
if (particle instanceof Error) { if (particle instanceof Error) {
return reject(particle.message); reject(particle.message);
return;
} }
registerHandlersHelper(peer, particle, { registerHandlersHelper(peer, particle, {
callback: { callback: {
callback: (args: any) => { callback: (args): undefined => {
resolve(args); resolve(args);
}, },
error: (args: any) => { error: (args): undefined => {
const [error] = args; const [error] = args;
reject(error); reject(error);
}, },

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,17 +13,29 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { CallServiceData, CallServiceResult, GenericCallServiceHandler, IJsServiceHost } from './interfaces.js';
import {
CallServiceData,
CallServiceResult,
GenericCallServiceHandler,
IJsServiceHost,
} from "./interfaces.js";
export class JsServiceHost implements IJsServiceHost { export class JsServiceHost implements IJsServiceHost {
private particleScopeHandlers = new Map<string, Map<string, GenericCallServiceHandler>>(); private particleScopeHandlers = new Map<
string,
Map<string, GenericCallServiceHandler>
>();
private commonHandlers = new Map<string, GenericCallServiceHandler>(); private commonHandlers = new Map<string, GenericCallServiceHandler>();
/** /**
* Returns true if any handler for the specified serviceId is registered * Returns true if any handler for the specified serviceId is registered
*/ */
hasService(serviceId: string): boolean { hasService(serviceId: string): boolean {
return this.commonHandlers.has(serviceId) || this.particleScopeHandlers.has(serviceId); return (
this.commonHandlers.has(serviceId) ||
this.particleScopeHandlers.has(serviceId)
);
} }
/** /**
@ -40,31 +52,28 @@ export class JsServiceHost implements IJsServiceHost {
* @param fnName Function name as specified in `call` air instruction * @param fnName Function name as specified in `call` air instruction
* @param particleId Particle ID * @param particleId Particle ID
*/ */
getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null { getHandler(
serviceId: string,
fnName: string,
particleId: string,
): GenericCallServiceHandler | null {
const key = serviceFnKey(serviceId, fnName); const key = serviceFnKey(serviceId, fnName);
const psh = this.particleScopeHandlers.get(particleId); return (
let handler: GenericCallServiceHandler | undefined = undefined; this.particleScopeHandlers.get(particleId)?.get(key) ??
this.commonHandlers.get(key) ??
// we should prioritize handler for this particle if there is one null
// if particle-scoped handler exist for this particle try getting handler there );
if (psh !== undefined) {
handler = psh.get(key);
}
// then try to find a common handler for all particles with this service-fn key
// if there is no particle-specific handler, get one from common map
if (handler === undefined) {
handler = this.commonHandlers.get(key);
}
return handler || null;
} }
/** /**
* Execute service call for specified call service data. Return null if no handler was found * Execute service call for specified call service data. Return null if no handler was found
*/ */
async callService(req: CallServiceData): Promise<CallServiceResult | null> { async callService(req: CallServiceData): Promise<CallServiceResult | null> {
const handler = this.getHandler(req.serviceId, req.fnName, req.particleContext.particleId); const handler = this.getHandler(
req.serviceId,
req.fnName,
req.particleContext.particleId,
);
if (handler === null) { if (handler === null) {
return null; return null;
@ -73,6 +82,7 @@ export class JsServiceHost implements IJsServiceHost {
const result = await handler(req); const result = await handler(req);
// Otherwise AVM might break // Otherwise AVM might break
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (result.result === undefined) { if (result.result === undefined) {
result.result = null; result.result = null;
} }
@ -83,7 +93,11 @@ export class JsServiceHost implements IJsServiceHost {
/** /**
* Register handler for all particles * Register handler for all particles
*/ */
registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void { registerGlobalHandler(
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
): void {
this.commonHandlers.set(serviceFnKey(serviceId, fnName), handler); this.commonHandlers.set(serviceFnKey(serviceId, fnName), handler);
} }
@ -97,6 +111,7 @@ export class JsServiceHost implements IJsServiceHost {
handler: GenericCallServiceHandler, handler: GenericCallServiceHandler,
): void { ): void {
let psh = this.particleScopeHandlers.get(particleId); let psh = this.particleScopeHandlers.get(particleId);
if (psh === undefined) { if (psh === undefined) {
psh = new Map<string, GenericCallServiceHandler>(); psh = new Map<string, GenericCallServiceHandler>();
this.particleScopeHandlers.set(particleId, psh); this.particleScopeHandlers.set(particleId, psh);

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import type { SecurityTetraplet } from '@fluencelabs/avm'; import type { SecurityTetraplet } from "@fluencelabs/avm";
import { JSONValue } from '../util/commonTypes.js'; import type { PeerIdB58 } from "@fluencelabs/interfaces";
import { JSONArray, JSONValue } from "@fluencelabs/interfaces";
/** /**
* JS Service host a low level interface for managing pure javascript services. * JS Service host a low level interface for managing pure javascript services.
@ -33,7 +34,11 @@ export interface IJsServiceHost {
* @param fnName Function name as specified in `call` air instruction * @param fnName Function name as specified in `call` air instruction
* @param particleId Particle ID * @param particleId Particle ID
*/ */
getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null; getHandler(
serviceId: string,
fnName: string,
particleId: string,
): GenericCallServiceHandler | null;
/** /**
* Execute service call for specified call service data * Execute service call for specified call service data
@ -43,7 +48,11 @@ export interface IJsServiceHost {
/** /**
* Register handler for all particles * Register handler for all particles
*/ */
registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void; registerGlobalHandler(
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
): void;
/** /**
* Register handler which will be called only for particle with the specific id * Register handler which will be called only for particle with the specific id
@ -114,7 +123,7 @@ export interface CallServiceData {
/** /**
* Arguments as specified in `call` air instruction * Arguments as specified in `call` air instruction
*/ */
args: any[]; args: JSONArray;
/** /**
* Security Tetraplets received from AVM * Security Tetraplets received from AVM
@ -135,7 +144,9 @@ export type CallServiceResultType = JSONValue;
/** /**
* Generic call service handler * Generic call service handler
*/ */
export type GenericCallServiceHandler = (req: CallServiceData) => CallServiceResult | Promise<CallServiceResult>; export type GenericCallServiceHandler = (
req: CallServiceData,
) => CallServiceResult | Promise<CallServiceResult>;
/** /**
* Represents the result of the `call` air instruction to be returned into AVM * Represents the result of the `call` air instruction to be returned into AVM

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,25 +13,35 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { FluencePeer } from '../jsPeer/FluencePeer.js';
import { IParticle } from '../particle/interfaces.js'; import { JSONArray } from "@fluencelabs/interfaces";
import { builtInServices } from '../services/builtins.js';
import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { IParticle } from "../particle/interfaces.js";
import { builtInServices } from "../services/builtins.js";
import { import {
CallServiceData, CallServiceData,
CallServiceResult, CallServiceResult,
CallServiceResultType, CallServiceResultType,
ParticleContext, ParticleContext,
ResultCodes, ResultCodes,
} from './interfaces.js'; } from "./interfaces.js";
export const doNothing = (..._args: Array<unknown>) => undefined; export const doNothing = () => {
return undefined;
};
export const WrapFnIntoServiceCall = export const WrapFnIntoServiceCall = (
(fn: (args: any[]) => CallServiceResultType) => fn: (args: JSONArray) => CallServiceResultType | undefined,
(req: CallServiceData): CallServiceResult => ({ ) => {
return (req: CallServiceData): CallServiceResult => {
return {
retCode: ResultCodes.success, retCode: ResultCodes.success,
result: fn(req.args), result: fn(req.args) ?? null,
}); };
};
};
export class ServiceError extends Error { export class ServiceError extends Error {
constructor(message: string) { constructor(message: string) {

View File

@ -15,11 +15,11 @@
*/ */
import bs58 from "bs58"; import bs58 from "bs58";
import { fromUint8Array, toUint8Array } from 'js-base64'; import { fromUint8Array, toUint8Array } from "js-base64";
import { it, describe, expect } from "vitest"; import { it, describe, expect } from "vitest";
import { fromBase64Sk, KeyPair } from '../index.js';
import { Particle, serializeToString, buildParticleMessage } from '../../particle/Particle.js'; import { Particle, buildParticleMessage } from "../../particle/Particle.js";
import { fromBase64Sk, KeyPair } from "../index.js";
const key = "+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk="; const key = "+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk=";
const keyBytes = toUint8Array(key); const keyBytes = toUint8Array(key);
@ -27,8 +27,9 @@ const keyBytes = toUint8Array(key);
const testData = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9, 10]); const testData = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9, 10]);
const testDataSig = Uint8Array.from([ const testDataSig = Uint8Array.from([
224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132, 107, 77, 224, 67, 99, 106, 76, 29, 144, 224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132,
121, 122, 169, 36, 173, 58, 80, 170, 102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29, 107, 77, 224, 67, 99, 106, 76, 29, 144, 121, 122, 169, 36, 173, 58, 80, 170,
102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29,
86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15, 86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15,
]); ]);
@ -112,24 +113,49 @@ describe("KeyPair tests", () => {
}); });
it("validates particle signature checks", async function () { it("validates particle signature checks", async function () {
const keyPair = await fromBase64Sk("7h48PQ/f1rS9TxacmgODxbD42Il9B3KC117jvOPppPE="); const keyPair = await fromBase64Sk(
expect(bs58.encode(keyPair.getLibp2pPeerId().toBytes())).toBe("12D3KooWANqfCDrV79MZdMnMqTvDdqSAPSxdgFY1L6DCq2DVGB4D"); "7h48PQ/f1rS9TxacmgODxbD42Il9B3KC117jvOPppPE=",
);
expect(bs58.encode(keyPair.getLibp2pPeerId().toBytes())).toBe(
"12D3KooWANqfCDrV79MZdMnMqTvDdqSAPSxdgFY1L6DCq2DVGB4D",
);
const message = toUint8Array(btoa("message")); const message = toUint8Array(btoa("message"));
const signature = await keyPair.signBytes(message); const signature = await keyPair.signBytes(message);
const verified = await keyPair.verify(message, signature); const verified = await keyPair.verify(message, signature);
expect(verified).toBe(true); expect(verified).toBe(true);
expect(fromUint8Array(signature)).toBe("sBW7H6/1fwAwF86ldwVm9BDu0YH3w30oFQjTWX0Tiu9yTVZHmxkV2OX4GL5jn0Iz0CrasGcOfozzkZwtJBPMBg==");
const particle = await Particle.createNew("abc", keyPair.getPeerId(), 7000, keyPair, "2883f959-e9e7-4843-8c37-205d393ca372", 1696934545662); expect(fromUint8Array(signature)).toBe(
"sBW7H6/1fwAwF86ldwVm9BDu0YH3w30oFQjTWX0Tiu9yTVZHmxkV2OX4GL5jn0Iz0CrasGcOfozzkZwtJBPMBg==",
);
const particle = await Particle.createNew(
"abc",
keyPair.getPeerId(),
7000,
keyPair,
"2883f959-e9e7-4843-8c37-205d393ca372",
1696934545662,
);
const particle_bytes = buildParticleMessage(particle); const particle_bytes = buildParticleMessage(particle);
expect(fromUint8Array(particle_bytes)).toBe("Mjg4M2Y5NTktZTllNy00ODQzLThjMzctMjA1ZDM5M2NhMzcy/kguGYsBAABYGwAAYWJj");
const isParticleVerified = await KeyPair.verifyWithPublicKey(keyPair.getPublicKey(), particle_bytes, particle.signature); expect(fromUint8Array(particle_bytes)).toBe(
"Mjg4M2Y5NTktZTllNy00ODQzLThjMzctMjA1ZDM5M2NhMzcy/kguGYsBAABYGwAAYWJj",
);
const isParticleVerified = await KeyPair.verifyWithPublicKey(
keyPair.getPublicKey(),
particle_bytes,
particle.signature,
);
expect(isParticleVerified).toBe(true); expect(isParticleVerified).toBe(true);
expect(fromUint8Array(particle.signature)).toBe("KceXDnOfqe0dOnAxiDsyWBIvUq6WHoT0ge+VMHXOZsjZvCNH7/10oufdlYfcPomfv28On6E87ZhDcHGBZcb7Bw=="); expect(fromUint8Array(particle.signature)).toBe(
"KceXDnOfqe0dOnAxiDsyWBIvUq6WHoT0ge+VMHXOZsjZvCNH7/10oufdlYfcPomfv28On6E87ZhDcHGBZcb7Bw==",
);
}); });
}); });

View File

@ -1,31 +1,55 @@
import { it, describe, expect, beforeAll } from 'vitest'; /**
* 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 * as fs from 'fs'; import * as fs from "fs";
import * as url from 'url'; import * as path from "path";
import * as path from 'path'; import * as url from "url";
import { compileAqua, withPeer } from '../../util/testUtils.js';
let aqua: any; import { it, describe, expect, beforeAll } from "vitest";
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
describe('Marine js tests', () => { import { compileAqua, CompiledFnCall, withPeer } from "../../util/testUtils.js";
let aqua: Record<string, CompiledFnCall>;
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
describe("Marine js tests", () => {
beforeAll(async () => { beforeAll(async () => {
const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/marine-js.aqua'); const pathToAquaFiles = path.join(
const { services, functions } = await compileAqua(pathToAquaFiles); __dirname,
"../../../aqua_test/marine-js.aqua",
);
const { functions } = await compileAqua(pathToAquaFiles);
aqua = functions; aqua = functions;
}); });
it('should call marine service correctly', async () => { it("should call marine service correctly", async () => {
await withPeer(async (peer) => { await withPeer(async (peer) => {
// arrange // arrange
const wasm = await fs.promises.readFile(path.join(__dirname, '../../../data_for_test/greeting.wasm')); const wasm = await fs.promises.readFile(
await peer.registerMarineService(wasm, 'greeting'); path.join(__dirname, "../../../data_for_test/greeting.wasm"),
);
await peer.registerMarineService(wasm, "greeting");
// act // act
const res = await aqua.call(peer, { arg: 'test' }); const res = await aqua["call"](peer, { arg: "test" });
// assert // assert
expect(res).toBe('Hi, Hi, Hi, test'); expect(res).toBe("Hi, Hi, Hi, test");
}); });
}); });
}); });

View File

@ -1,40 +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.
*/
// @ts-ignore
import { BlobWorker } from 'threads';
import { fromBase64, toUint8Array } from 'js-base64';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
import { Buffer } from 'buffer';
import { LazyLoader } from '../interfaces.js';
export class InlinedWorkerLoader extends LazyLoader<WorkerImplementation> {
constructor(b64script: string) {
super(() => {
const script = fromBase64(b64script);
return BlobWorker.fromText(script);
});
}
}
export class InlinedWasmLoader extends LazyLoader<Buffer> {
constructor(b64wasm: string) {
super(() => {
const wasm = toUint8Array(b64wasm);
return Buffer.from(wasm);
});
}
}

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,16 +13,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { createRequire } from 'module';
// @ts-ignore import { Buffer } from "buffer";
import type { WorkerImplementation } from 'threads/dist/types/master'; import fs from "fs";
// @ts-ignore import { createRequire } from "module";
import { Worker } from 'threads'; import path from "path";
import { Buffer } from 'buffer';
import * as fs from 'fs'; import { Worker, type Worker as WorkerImplementation } from "threads/master";
import * as path from 'path';
import { LazyLoader } from '../interfaces.js'; import { LazyLoader } from "../interfaces.js";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
@ -39,7 +38,10 @@ const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
* @param source - object specifying the source of the file. Consist two fields: package name and file path. * @param source - object specifying the source of the file. Consist two fields: package name and file path.
* @returns SharedArrayBuffer with the wasm file * @returns SharedArrayBuffer with the wasm file
*/ */
export const loadWasmFromNpmPackage = async (source: { package: string; file: string }): Promise<SharedArrayBuffer> => { export const loadWasmFromNpmPackage = async (source: {
package: string;
file: string;
}): Promise<SharedArrayBuffer> => {
const packagePath = require.resolve(source.package); const packagePath = require.resolve(source.package);
const filePath = path.join(path.dirname(packagePath), source.file); const filePath = path.join(path.dirname(packagePath), source.file);
return loadWasmFromFileSystem(filePath); return loadWasmFromFileSystem(filePath);
@ -51,26 +53,34 @@ export const loadWasmFromNpmPackage = async (source: { package: string; file: st
* @param filePath - path to the wasm file * @param filePath - path to the wasm file
* @returns SharedArrayBuffer with the wasm fileWorker * @returns SharedArrayBuffer with the wasm fileWorker
*/ */
export const loadWasmFromFileSystem = async (filePath: string): Promise<SharedArrayBuffer> => { export const loadWasmFromFileSystem = async (
filePath: string,
): Promise<SharedArrayBuffer> => {
const buffer = await fs.promises.readFile(filePath); const buffer = await fs.promises.readFile(filePath);
return bufferToSharedArrayBuffer(buffer); return bufferToSharedArrayBuffer(buffer);
}; };
export class WasmLoaderFromFs extends LazyLoader<SharedArrayBuffer> { export class WasmLoaderFromFs extends LazyLoader<SharedArrayBuffer> {
constructor(filePath: string) { constructor(filePath: string) {
super(() => loadWasmFromFileSystem(filePath)); super(() => {
return loadWasmFromFileSystem(filePath);
});
} }
} }
export class WasmLoaderFromNpm extends LazyLoader<SharedArrayBuffer> { export class WasmLoaderFromNpm extends LazyLoader<SharedArrayBuffer> {
constructor(pkg: string, file: string) { constructor(pkg: string, file: string) {
super(() => loadWasmFromNpmPackage({ package: pkg, file: file })); super(() => {
return loadWasmFromNpmPackage({ package: pkg, file: file });
});
} }
} }
export class WorkerLoaderFromFs extends LazyLoader<WorkerImplementation> { export class WorkerLoaderFromFs extends LazyLoader<WorkerImplementation> {
constructor(scriptPath: string) { constructor(scriptPath: string) {
super(() => new Worker(scriptPath)); super(() => {
return new Worker(scriptPath);
});
} }
} }

View File

@ -1,63 +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 { LazyLoader } from '../interfaces.js';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
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 the server. Only works in browsers.
* The function will try load file into SharedArrayBuffer if the site is cross-origin isolated.
* Otherwise the return value fallbacks to Buffer which is less performant but is still compatible with FluenceAppService methods.
* We strongly recommend to set-up cross-origin headers. For more details see: See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
* Filename is relative to current origin.
* @param filePath - path to the wasm file relative to current origin
* @returns Either SharedArrayBuffer or Buffer with the wasm file
*/
export const loadWasmFromUrl = async (filePath: string): Promise<SharedArrayBuffer | Buffer> => {
const fullUrl = window.location.origin + '/' + filePath;
const res = await fetch(fullUrl);
const ab = await res.arrayBuffer();
new Uint8Array(ab);
const buffer = Buffer.from(ab);
// only convert to shared buffers if necessary CORS headers have been set:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
if (crossOriginIsolated) {
return bufferToSharedArrayBuffer(buffer);
}
return buffer;
};
export class WasmLoaderFromUrl extends LazyLoader<SharedArrayBuffer | Buffer> {
constructor(filePath: string) {
super(() => loadWasmFromUrl(filePath));
}
}
export class WorkerLoaderFromUrl extends LazyLoader<WorkerImplementation> {
constructor(scriptPath: string) {
super(() => new Worker(scriptPath));
}
}

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,10 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
import { IStartable, JSONArray, JSONObject, CallParameters } from '../util/commonTypes.js'; import {
// @ts-ignore CallResultsArray,
import type { WorkerImplementation } from 'threads/dist/types/master'; InterpreterResult,
RunParameters,
} from "@fluencelabs/avm";
import { JSONObject, JSONValue, JSONArray } from "@fluencelabs/interfaces";
import type { Worker as WorkerImplementation } from "threads/master";
import { IStartable, CallParameters } from "../util/commonTypes.js";
/** /**
* Contract for marine host implementations. Marine host is responsible for creating calling and removing marine services * Contract for marine host implementations. Marine host is responsible for creating calling and removing marine services
@ -25,7 +31,10 @@ export interface IMarineHost extends IStartable {
/** /**
* Creates marine service from the given module and service id * Creates marine service from the given module and service id
*/ */
createService(serviceModule: ArrayBuffer | SharedArrayBuffer, serviceId: string): Promise<void>; createService(
serviceModule: ArrayBuffer | SharedArrayBuffer,
serviceId: string,
): Promise<void>;
/** /**
* Removes marine service with the given service id * Removes marine service with the given service id
@ -45,7 +54,7 @@ export interface IMarineHost extends IStartable {
functionName: string, functionName: string,
args: JSONArray | JSONObject, args: JSONArray | JSONObject,
callParams: CallParameters, callParams: CallParameters,
): Promise<unknown>; ): Promise<JSONValue>;
} }
/** /**
@ -74,12 +83,16 @@ export interface IValueLoader<T> {
/** /**
* Interface for something which can load wasm files * Interface for something which can load wasm files
*/ */
export interface IWasmLoader extends IValueLoader<ArrayBuffer | SharedArrayBuffer>, IStartable {} export interface IWasmLoader
extends IValueLoader<ArrayBuffer | SharedArrayBuffer>,
IStartable {}
/** /**
* Interface for something which can thread.js based worker * Interface for something which can thread.js based worker
*/ */
export interface IWorkerLoader extends IValueLoader<WorkerImplementation>, IStartable {} export interface IWorkerLoader
extends IValueLoader<WorkerImplementation | Promise<WorkerImplementation>>,
IStartable {}
/** /**
* Lazy loader for some value. Value is loaded only when `start` method is called * Lazy loader for some value. Value is loaded only when `start` method is called
@ -91,7 +104,9 @@ export class LazyLoader<T> implements IStartable, IValueLoader<T> {
getValue(): T { getValue(): T {
if (this.value == null) { if (this.value == null) {
throw new Error('Value has not been loaded. Call `start` method to load the value.'); throw new Error(
"Value has not been loaded. Call `start` method to load the value.",
);
} }
return this.value; return this.value;

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master'; import { Worker, type Worker as WorkerImplementation } from "threads/master";
// @ts-ignore
import { Worker } from 'threads'; import { LazyLoader } from "../interfaces.js";
import { LazyLoader } from '../interfaces.js';
export class WorkerLoader extends LazyLoader<WorkerImplementation> { export class WorkerLoader extends LazyLoader<WorkerImplementation> {
constructor() { constructor() {
super(() => new Worker('../../../node_modules/@fluencelabs/marine-worker/dist/index.js')); super(() => {
return new Worker(
"../../../node_modules/@fluencelabs/marine-worker/dist/index.js",
);
});
} }
} }

View File

@ -1,5 +1,5 @@
/* /**
* Copyright 2022 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,41 +14,49 @@
* limitations under the License. * limitations under the License.
*/ */
import type { JSONArray, JSONObject, CallParameters } from '@fluencelabs/marine-js/dist/types'; import { JSONValue } from "@fluencelabs/interfaces";
import { LogFunction, logLevelToEnv } from '@fluencelabs/marine-js/dist/types'; import type {
import type { MarineBackgroundInterface } from '@fluencelabs/marine-worker'; JSONArray,
// @ts-ignore JSONObject,
import { ModuleThread, spawn, Thread } from 'threads'; CallParameters,
} from "@fluencelabs/marine-js/dist/types";
import { LogFunction, logLevelToEnv } from "@fluencelabs/marine-js/dist/types";
import type { MarineBackgroundInterface } from "@fluencelabs/marine-worker";
import { ModuleThread, Thread, spawn } from "threads/master";
import { MarineLogger, marineLogger } from '../../util/logger.js'; import { MarineLogger, marineLogger } from "../../util/logger.js";
import { IMarineHost, IWasmLoader, IWorkerLoader } from '../interfaces.js'; import { IMarineHost, IWasmLoader, IWorkerLoader } from "../interfaces.js";
export class MarineBackgroundRunner implements IMarineHost { export class MarineBackgroundRunner implements IMarineHost {
private workerThread?: MarineBackgroundInterface; private workerThread?: ModuleThread<MarineBackgroundInterface>;
private loggers = new Map<string, MarineLogger>(); private loggers = new Map<string, MarineLogger>();
constructor(private workerLoader: IWorkerLoader, private controlModuleLoader: IWasmLoader, private avmWasmLoader: IWasmLoader) {} constructor(
private workerLoader: IWorkerLoader,
private controlModuleLoader: IWasmLoader,
private avmWasmLoader: IWasmLoader,
) {}
async hasService(serviceId: string) { async hasService(serviceId: string) {
if (!this.workerThread) { if (this.workerThread == null) {
throw new Error('Worker is not initialized'); throw new Error("Worker is not initialized");
} }
return this.workerThread.hasService(serviceId); return this.workerThread.hasService(serviceId);
} }
async removeService(serviceId: string) { async removeService(serviceId: string) {
if (!this.workerThread) { if (this.workerThread == null) {
throw new Error('Worker is not initialized'); throw new Error("Worker is not initialized");
} }
await this.workerThread.removeService(serviceId); await this.workerThread.removeService(serviceId);
} }
async start(): Promise<void> { async start(): Promise<void> {
if (this.workerThread) { if (this.workerThread != null) {
throw new Error('Worker thread already initialized'); throw new Error("Worker thread already initialized");
} }
await this.controlModuleLoader.start(); await this.controlModuleLoader.start();
@ -59,28 +67,36 @@ export class MarineBackgroundRunner implements IMarineHost {
await this.workerLoader.start(); await this.workerLoader.start();
const worker = await this.workerLoader.getValue(); const worker = await this.workerLoader.getValue();
const workerThread = await spawn<MarineBackgroundInterface>(worker); const workerThread: ModuleThread<MarineBackgroundInterface> =
await spawn<MarineBackgroundInterface>(worker);
const logfn: LogFunction = (message) => { const logfn: LogFunction = (message) => {
const serviceLogger = this.loggers.get(message.service); const serviceLogger = this.loggers.get(message.service);
if (!serviceLogger) {
if (serviceLogger == null) {
return; return;
} }
serviceLogger[message.level](message.message); serviceLogger[message.level](message.message);
}; };
workerThread.onLogMessage().subscribe(logfn); workerThread.onLogMessage().subscribe(logfn);
await workerThread.init(wasm); await workerThread.init(wasm);
this.workerThread = workerThread; this.workerThread = workerThread;
await this.createService(this.avmWasmLoader.getValue(), 'avm'); await this.createService(this.avmWasmLoader.getValue(), "avm");
} }
async createService(serviceModule: ArrayBuffer | SharedArrayBuffer, serviceId: string): Promise<void> { async createService(
if (!this.workerThread) { serviceModule: ArrayBuffer | SharedArrayBuffer,
throw new Error('Worker is not initialized'); serviceId: string,
): Promise<void> {
if (this.workerThread == null) {
throw new Error("Worker is not initialized");
} }
// The logging level is controlled by the environment variable passed to enable debug logs. // The logging level is controlled by the environment variable passed to enable debug logs.
// We enable all possible log levels passing the control for exact printouts to the logger // We enable all possible log levels passing the control for exact printouts to the logger
const env = logLevelToEnv('info'); const env = logLevelToEnv("info");
this.loggers.set(serviceId, marineLogger(serviceId)); this.loggers.set(serviceId, marineLogger(serviceId));
await this.workerThread.createService(serviceModule, serviceId, env); await this.workerThread.createService(serviceModule, serviceId, env);
} }
@ -90,16 +106,21 @@ export class MarineBackgroundRunner implements IMarineHost {
functionName: string, functionName: string,
args: JSONArray | JSONObject, args: JSONArray | JSONObject,
callParams: CallParameters, callParams: CallParameters,
): Promise<unknown> { ): Promise<JSONValue> {
if (!this.workerThread) { if (this.workerThread == null) {
throw 'Worker is not initialized'; throw new Error("Worker is not initialized");
} }
return this.workerThread.callService(serviceId, functionName, args, callParams); return this.workerThread.callService(
serviceId,
functionName,
args,
callParams,
);
} }
async stop(): Promise<void> { async stop(): Promise<void> {
if (!this.workerThread) { if (this.workerThread == null) {
return; return;
} }

View File

@ -1,5 +1,5 @@
/* /**
* Copyright 2020 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,21 +18,32 @@ import { CallResultsArray } from "@fluencelabs/avm";
import { fromUint8Array, toUint8Array } from "js-base64"; import { fromUint8Array, toUint8Array } from "js-base64";
import { concat } from "uint8arrays/concat"; import { concat } from "uint8arrays/concat";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { KeyPair } from "../keypair/index.js"; import { KeyPair } from "../keypair/index.js";
import { numberToLittleEndianBytes } from "../util/bytes.js"; import { numberToLittleEndianBytes } from "../util/bytes.js";
import { IParticle } from "./interfaces.js"; import { IParticle } from "./interfaces.js";
const particleSchema = z.object({
id: z.string(),
timestamp: z.number().positive(),
script: z.string(),
data: z.string(),
ttl: z.number().positive(),
init_peer_id: z.string(),
signature: z.array(z.number()),
});
export class Particle implements IParticle { export class Particle implements IParticle {
constructor( constructor(
public readonly id: string, readonly id: string,
public readonly timestamp: number, readonly timestamp: number,
public readonly script: string, readonly script: string,
public readonly data: Uint8Array, readonly data: Uint8Array,
public readonly ttl: number, readonly ttl: number,
public readonly initPeerId: string, readonly initPeerId: string,
public readonly signature: Uint8Array readonly signature: Uint8Array,
) {} ) {}
static async createNew( static async createNew(
@ -62,17 +73,28 @@ export class Particle implements IParticle {
static fromString(str: string): Particle { static fromString(str: string): Particle {
const json = JSON.parse(str); const json = JSON.parse(str);
const res = new Particle(
json.id,
json.timestamp,
json.script,
toUint8Array(json.data),
json.ttl,
json.init_peer_id,
new Uint8Array(json.signature)
);
return res; const res = particleSchema.safeParse(json);
if (!res.success) {
throw new Error(
`Particle format invalid. Errors: ${JSON.stringify(
res.error.flatten(),
)}`,
);
}
const data = res.data;
return new Particle(
data.id,
data.timestamp,
data.script,
toUint8Array(data.data),
data.ttl,
data.init_peer_id,
new Uint8Array(data.signature),
);
} }
} }
@ -109,16 +131,6 @@ export const hasExpired = (particle: IParticle): boolean => {
return getActualTTL(particle) <= 0; return getActualTTL(particle) <= 0;
}; };
/**
* Validates that particle signature is correct
*/
export const verifySignature = async (particle: IParticle, publicKey: Uint8Array): Promise<boolean> => {
// TODO: Uncomment this when nox roll out particle signatures
return true;
// const message = buildParticleMessage(particle);
// return unmarshalPublicKey(publicKey).verify(message, particle.signature);
}
/** /**
* Creates a particle clone with new data * Creates a particle clone with new data
*/ */
@ -182,5 +194,5 @@ export const handleTimeout = (fn: () => void) => {
if (stage.stage === "expired") { if (stage.stage === "expired") {
fn(); fn();
} }
} };
} };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { PeerIdB58 } from "@fluencelabs/interfaces";
/** /**
* Immutable part of the particle. * Immutable part of the particle.

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,26 +14,30 @@
* limitations under the License. * limitations under the License.
*/ */
import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces'; import { Buffer } from "buffer";
import { defaultGuard } from './SingleModuleSrv.js'; import * as fs from "fs";
import { NodeUtilsDef, registerNodeUtils } from './_aqua/node-utils.js';
import { SecurityGuard } from './securityGuard.js'; import { CallParams } from "@fluencelabs/interfaces";
import * as fs from 'fs';
import { FluencePeer } from '../jsPeer/FluencePeer.js'; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { Buffer } from 'buffer'; import { getErrorMessage } from "../util/utils.js";
import { NodeUtilsDef, registerNodeUtils } from "./_aqua/node-utils.js";
import { SecurityGuard } from "./securityGuard.js";
import { defaultGuard } from "./SingleModuleSrv.js";
export class NodeUtils implements NodeUtilsDef { export class NodeUtils implements NodeUtilsDef {
constructor(private peer: FluencePeer) { constructor(private peer: FluencePeer) {
this.securityGuard_readFile = defaultGuard(this.peer); this.securityGuard_readFile = defaultGuard(this.peer);
} }
securityGuard_readFile: SecurityGuard<'path'>; securityGuard_readFile: SecurityGuard<"path">;
async read_file(path: string, callParams: CallParams<'path'>) { async read_file(path: string, callParams: CallParams<"path">) {
if (!this.securityGuard_readFile(callParams)) { if (!this.securityGuard_readFile(callParams)) {
return { return {
success: false, success: false,
error: 'Security guard validation failed', error: "Security guard validation failed",
content: null, content: null,
}; };
} }
@ -42,22 +46,26 @@ export class NodeUtils implements NodeUtilsDef {
// Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't // Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't
const data = await new Promise<Buffer>((resolve, reject) => { const data = await new Promise<Buffer>((resolve, reject) => {
fs.readFile(path, (err, data) => { fs.readFile(path, (err, data) => {
if (err) { if (err != null) {
reject(err); reject(err);
return; return;
} }
resolve(data); resolve(data);
})
}); });
});
return { return {
success: true, success: true,
// TODO: this is strange bug.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
content: data as unknown as string, content: data as unknown as string,
error: null, error: null,
}; };
} catch (err: any) { } catch (err: unknown) {
return { return {
success: false, success: false,
error: err.message, error: getErrorMessage(err),
content: null, content: null,
}; };
} }
@ -65,6 +73,6 @@ export class NodeUtils implements NodeUtilsDef {
} }
// HACK:: security guard functions must be ported to user API // HACK:: security guard functions must be ported to user API
export const doRegisterNodeUtils = (peer: any) => { export const doRegisterNodeUtils = (peer: FluencePeer) => {
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer)); registerNodeUtils(peer, "node_utils", new NodeUtils(peer));
}; };

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,33 +14,52 @@
* limitations under the License. * limitations under the License.
*/ */
import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces'; import { CallParams, PeerIdB58 } from "@fluencelabs/interfaces";
import { KeyPair } from '../keypair/index.js';
import { FluencePeer } from '../jsPeer/FluencePeer.js'; import { KeyPair } from "../keypair/index.js";
import { SigDef } from './_aqua/services.js';
import { allowOnlyParticleOriginatedAt, allowServiceFn, and, or, SecurityGuard } from './securityGuard.js'; import { SigDef } from "./_aqua/services.js";
import {
allowOnlyParticleOriginatedAt,
allowServiceFn,
and,
or,
SecurityGuard,
} from "./securityGuard.js";
export const defaultSigGuard = (peerId: PeerIdB58) => { export const defaultSigGuard = (peerId: PeerIdB58) => {
return and<'data'>( return and<"data">(
allowOnlyParticleOriginatedAt(peerId), allowOnlyParticleOriginatedAt(peerId),
or( or(
allowServiceFn('trust-graph', 'get_trust_bytes'), allowServiceFn("trust-graph", "get_trust_bytes"),
allowServiceFn('trust-graph', 'get_revocation_bytes'), allowServiceFn("trust-graph", "get_revocation_bytes"),
allowServiceFn('registry', 'get_key_bytes'), allowServiceFn("registry", "get_key_bytes"),
allowServiceFn('registry', 'get_record_bytes'), allowServiceFn("registry", "get_record_bytes"),
allowServiceFn('registry', 'get_record_metadata_bytes'), allowServiceFn("registry", "get_record_metadata_bytes"),
allowServiceFn('registry', 'get_tombstone_bytes'), allowServiceFn("registry", "get_tombstone_bytes"),
), ),
); );
}; };
type SignReturnType =
| {
error: null;
signature: number[];
success: true;
}
| {
error: string;
signature: null;
success: false;
};
export class Sig implements SigDef { export class Sig implements SigDef {
constructor(private keyPair: KeyPair) {} constructor(private keyPair: KeyPair) {}
/** /**
* Configurable security guard for sign method * Configurable security guard for sign method
*/ */
securityGuard: SecurityGuard<'data'> = (params) => { securityGuard: SecurityGuard<"data"> = () => {
return true; return true;
}; };
@ -56,12 +75,12 @@ export class Sig implements SigDef {
*/ */
async sign( async sign(
data: number[], data: number[],
callParams: CallParams<'data'>, callParams: CallParams<"data">,
): Promise<{ error: string | null; signature: number[] | null; success: boolean }> { ): Promise<SignReturnType> {
if (!this.securityGuard(callParams)) { if (!this.securityGuard(callParams)) {
return { return {
success: false, success: false,
error: 'Security guard validation failed', error: "Security guard validation failed",
signature: null, signature: null,
}; };
} }
@ -79,10 +98,9 @@ export class Sig implements SigDef {
* Verifies the signature. Required by aqua * Verifies the signature. Required by aqua
*/ */
verify(signature: number[], data: number[]): Promise<boolean> { verify(signature: number[], data: number[]): Promise<boolean> {
return this.keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature)) return this.keyPair.verify(
Uint8Array.from(data),
Uint8Array.from(signature),
);
} }
} }
export const getDefaultSig = (peer: FluencePeer) => {
peer.registerMarineService;
};

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,15 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { v4 as uuidv4 } from 'uuid'; import { Buffer } from "buffer";
import { SrvDef } from './_aqua/single-module-srv.js';
import { FluencePeer } from '../jsPeer/FluencePeer.js'; import { CallParams } from "@fluencelabs/interfaces";
import { CallParams } from '@fluencelabs/interfaces'; import { v4 as uuidv4 } from "uuid";
import { Buffer } from 'buffer';
import { allowOnlyParticleOriginatedAt, SecurityGuard } from './securityGuard.js'; import { FluencePeer } from "../jsPeer/FluencePeer.js";
import { getErrorMessage } from "../util/utils.js";
import { SrvDef } from "./_aqua/single-module-srv.js";
import {
allowOnlyParticleOriginatedAt,
SecurityGuard,
} from "./securityGuard.js";
export const defaultGuard = (peer: FluencePeer) => { export const defaultGuard = (peer: FluencePeer) => {
return allowOnlyParticleOriginatedAt<any>(peer.keyPair.getPeerId()); return allowOnlyParticleOriginatedAt(peer.keyPair.getPeerId());
}; };
export class Srv implements SrvDef { export class Srv implements SrvDef {
@ -33,20 +40,23 @@ export class Srv implements SrvDef {
this.securityGuard_remove = defaultGuard(this.peer); this.securityGuard_remove = defaultGuard(this.peer);
} }
securityGuard_create: SecurityGuard<'wasm_b64_content'>; securityGuard_create: SecurityGuard<"wasm_b64_content">;
async create(wasm_b64_content: string, callParams: CallParams<'wasm_b64_content'>) { async create(
wasm_b64_content: string,
callParams: CallParams<"wasm_b64_content">,
) {
if (!this.securityGuard_create(callParams)) { if (!this.securityGuard_create(callParams)) {
return { return {
success: false, success: false,
error: 'Security guard validation failed', error: "Security guard validation failed",
service_id: null, service_id: null,
}; };
} }
try { try {
const newServiceId = uuidv4(); const newServiceId = uuidv4();
const buffer = Buffer.from(wasm_b64_content, 'base64'); const buffer = Buffer.from(wasm_b64_content, "base64");
// TODO:: figure out why SharedArrayBuffer is not working here // TODO:: figure out why SharedArrayBuffer is not working here
// const sab = new SharedArrayBuffer(buffer.length); // const sab = new SharedArrayBuffer(buffer.length);
// const tmp = new Uint8Array(sab); // const tmp = new Uint8Array(sab);
@ -59,22 +69,22 @@ export class Srv implements SrvDef {
service_id: newServiceId, service_id: newServiceId,
error: null, error: null,
}; };
} catch (err: any) { } catch (err: unknown) {
return { return {
success: true, success: true,
service_id: null, service_id: null,
error: err.message, error: getErrorMessage(err),
}; };
} }
} }
securityGuard_remove: SecurityGuard<'service_id'>; securityGuard_remove: SecurityGuard<"service_id">;
async remove(service_id: string, callParams: CallParams<'service_id'>) { async remove(service_id: string, callParams: CallParams<"service_id">) {
if (!this.securityGuard_remove(callParams)) { if (!this.securityGuard_remove(callParams)) {
return { return {
success: false, success: false,
error: 'Security guard validation failed', error: "Security guard validation failed",
service_id: null, service_id: null,
}; };
} }

View File

@ -1,4 +1,4 @@
/* /**
* Copyright 2023 Fluence Labs Limited * Copyright 2023 Fluence Labs Limited
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -14,11 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import { CallParams } from '@fluencelabs/interfaces'; import { CallParams } from "@fluencelabs/interfaces";
import { TracingDef } from './_aqua/tracing.js';
import { TracingDef } from "./_aqua/tracing.js";
export class Tracing implements TracingDef { export class Tracing implements TracingDef {
tracingEvent(arrowName: string, event: string, callParams: CallParams<'arrowName' | 'event'>): void { tracingEvent(
console.log('[%s] (%s) %s', callParams.particleId, arrowName, event); arrowName: string,
event: string,
callParams: CallParams<"arrowName" | "event">,
): void {
// This console log is intentional
// eslint-disable-next-line no-console
console.log("[%s] (%s) %s", callParams.particleId, arrowName, event);
} }
} }

View File

@ -1,12 +1,30 @@
import { it, describe, expect, test } from 'vitest'; /**
* 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 { CallParams } from '@fluencelabs/interfaces'; import assert from "assert";
import { toUint8Array } from 'js-base64';
import { KeyPair } from '../../keypair/index.js'; import { CallParams, JSONArray } from "@fluencelabs/interfaces";
import { Sig, defaultSigGuard } from '../Sig.js'; import { toUint8Array } from "js-base64";
import { allowServiceFn } from '../securityGuard.js'; import { it, describe, expect, test } from "vitest";
import { builtInServices } from '../builtins.js';
import { CallServiceData } from '../../jsServiceHost/interfaces.js'; import { CallServiceData } from "../../jsServiceHost/interfaces.js";
import { KeyPair } from "../../keypair/index.js";
import { builtInServices } from "../builtins.js";
import { allowServiceFn } from "../securityGuard.js";
import { Sig, defaultSigGuard } from "../Sig.js";
const a10b20 = `{ const a10b20 = `{
"a": 10, "a": 10,
@ -20,97 +38,105 @@ const oneTwoThreeFour = `[
4 4
]`; ]`;
describe('Tests for default handler', () => { interface ServiceCallType {
serviceId: string;
fnName: string;
args: JSONArray;
retCode: 0 | 1;
result: unknown;
}
describe("Tests for default handler", () => {
test.each` test.each`
serviceId | fnName | args | retCode | result serviceId | fnName | args | retCode | result
${'op'} | ${'identity'} | ${[]} | ${0} | ${{}} ${"op"} | ${"identity"} | ${[]} | ${0} | ${{}}
${'op'} | ${'identity'} | ${[1]} | ${0} | ${1} ${"op"} | ${"identity"} | ${[1]} | ${0} | ${1}
${'op'} | ${'identity'} | ${[1, 2]} | ${1} | ${'identity accepts up to 1 arguments, received 2 arguments'} ${"op"} | ${"identity"} | ${[1, 2]} | ${1} | ${"identity accepts up to 1 arguments, received 2 arguments"}
${'op'} | ${'noop'} | ${[1, 2]} | ${0} | ${{}} ${"op"} | ${"noop"} | ${[1, 2]} | ${0} | ${{}}
${'op'} | ${'array'} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]} ${"op"} | ${"array"} | ${[1, 2, 3]} | ${0} | ${[1, 2, 3]}
${'op'} | ${'array_length'} | ${[[1, 2, 3]]} | ${0} | ${3} ${"op"} | ${"array_length"} | ${[[1, 2, 3]]} | ${0} | ${3}
${'op'} | ${'array_length'} | ${[]} | ${1} | ${'array_length accepts exactly one argument, found: 0'} ${"op"} | ${"array_length"} | ${[]} | ${1} | ${"array_length accepts exactly one argument, found: 0"}
${'op'} | ${'concat'} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]} ${"op"} | ${"concat"} | ${[[1, 2], [3, 4], [5, 6]]} | ${0} | ${[1, 2, 3, 4, 5, 6]}
${'op'} | ${'concat'} | ${[[1, 2]]} | ${0} | ${[1, 2]} ${"op"} | ${"concat"} | ${[[1, 2]]} | ${0} | ${[1, 2]}
${'op'} | ${'concat'} | ${[]} | ${0} | ${[]} ${"op"} | ${"concat"} | ${[]} | ${0} | ${[]}
${'op'} | ${'concat'} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"} ${"op"} | ${"concat"} | ${[1, [1, 2], 1]} | ${1} | ${"All arguments of 'concat' must be arrays: arguments 0, 2 are not"}
${'op'} | ${'string_to_b58'} | ${['test']} | ${0} | ${'3yZe7d'} ${"op"} | ${"string_to_b58"} | ${["test"]} | ${0} | ${"3yZe7d"}
${'op'} | ${'string_to_b58'} | ${['test', 1]} | ${1} | ${'string_to_b58 accepts only one string argument'} ${"op"} | ${"string_to_b58"} | ${["test", 1]} | ${1} | ${"string_to_b58 accepts only one string argument"}
${'op'} | ${'string_from_b58'} | ${['3yZe7d']} | ${0} | ${'test'} ${"op"} | ${"string_from_b58"} | ${["3yZe7d"]} | ${0} | ${"test"}
${'op'} | ${'string_from_b58'} | ${['3yZe7d', 1]} | ${1} | ${'string_from_b58 accepts only one string argument'} ${"op"} | ${"string_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"string_from_b58 accepts only one string argument"}
${'op'} | ${'bytes_to_b58'} | ${[[116, 101, 115, 116]]} | ${0} | ${'3yZe7d'} ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116]]} | ${0} | ${"3yZe7d"}
${'op'} | ${'bytes_to_b58'} | ${[[116, 101, 115, 116], 1]} | ${1} | ${'bytes_to_b58 accepts only single argument: array of numbers'} ${"op"} | ${"bytes_to_b58"} | ${[[116, 101, 115, 116], 1]} | ${1} | ${"bytes_to_b58 accepts only single argument: array of numbers"}
${'op'} | ${'bytes_from_b58'} | ${['3yZe7d']} | ${0} | ${[116, 101, 115, 116]} ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d"]} | ${0} | ${[116, 101, 115, 116]}
${'op'} | ${'bytes_from_b58'} | ${['3yZe7d', 1]} | ${1} | ${'bytes_from_b58 accepts only one string argument'} ${"op"} | ${"bytes_from_b58"} | ${["3yZe7d", 1]} | ${1} | ${"bytes_from_b58 accepts only one string argument"}
${'op'} | ${'sha256_string'} | ${['hello, world!']} | ${0} | ${'QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u'} ${"op"} | ${"sha256_string"} | ${["hello, world!"]} | ${0} | ${"QmVQ8pg6L1tpoWYeq6dpoWqnzZoSLCh7E96fCFXKvfKD3u"}
${'op'} | ${'sha256_string'} | ${['hello, world!', true]} | ${0} | ${'84V7ZxLW7qKsx1Qvbd63BdGaHxUc3TfT2MBPqAXM7Wyu'} ${"op"} | ${"sha256_string"} | ${["hello, world!", true]} | ${1} | ${"sha256_string accepts 1 argument, found: 2"}
${'op'} | ${'sha256_string'} | ${[]} | ${1} | ${'sha256_string accepts 1-3 arguments, found: 0'} ${"op"} | ${"sha256_string"} | ${[]} | ${1} | ${"sha256_string accepts 1 argument, found: 0"}
${'op'} | ${'concat_strings'} | ${[]} | ${0} | ${''} ${"op"} | ${"concat_strings"} | ${[]} | ${0} | ${""}
${'op'} | ${'concat_strings'} | ${['a', 'b', 'c']} | ${0} | ${'abc'} ${"op"} | ${"concat_strings"} | ${["a", "b", "c"]} | ${0} | ${"abc"}
${'peer'} | ${'timeout'} | ${[200, []]} | ${0} | ${[]} ${"peer"} | ${"timeout"} | ${[200, []]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"}
${'peer'} | ${'timeout'} | ${[200, ['test']]} | ${0} | ${['test']} ${"peer"} | ${"timeout"} | ${[200, "test"]} | ${0} | ${"test"}
${'peer'} | ${'timeout'} | ${[]} | ${1} | ${'timeout accepts exactly two arguments: timeout duration in ms and a message string'} ${"peer"} | ${"timeout"} | ${[]} | ${1} | ${"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"} | ${[200, "test", 1]} | ${1} | ${"timeout accepts exactly two arguments: timeout duration in ms and a message string"}
${'debug'} | ${'stringify'} | ${[]} | ${0} | ${'"<empty argument list>"'} ${"debug"} | ${"stringify"} | ${[]} | ${0} | ${'"<empty argument list>"'}
${'debug'} | ${'stringify'} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20} ${"debug"} | ${"stringify"} | ${[{ a: 10, b: 20 }]} | ${0} | ${a10b20}
${'debug'} | ${'stringify'} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour} ${"debug"} | ${"stringify"} | ${[1, 2, 3, 4]} | ${0} | ${oneTwoThreeFour}
${'math'} | ${'add'} | ${[2, 2]} | ${0} | ${4} ${"math"} | ${"add"} | ${[2, 2]} | ${0} | ${4}
${'math'} | ${'add'} | ${[2]} | ${1} | ${'Expected 2 argument(s). Got 1'} ${"math"} | ${"add"} | ${[2]} | ${1} | ${"Expected 2 argument(s). Got 1"}
${'math'} | ${'sub'} | ${[2, 2]} | ${0} | ${0} ${"math"} | ${"sub"} | ${[2, 2]} | ${0} | ${0}
${'math'} | ${'sub'} | ${[2, 3]} | ${0} | ${-1} ${"math"} | ${"sub"} | ${[2, 3]} | ${0} | ${-1}
${'math'} | ${'mul'} | ${[2, 2]} | ${0} | ${4} ${"math"} | ${"mul"} | ${[2, 2]} | ${0} | ${4}
${'math'} | ${'mul'} | ${[2, 0]} | ${0} | ${0} ${"math"} | ${"mul"} | ${[2, 0]} | ${0} | ${0}
${'math'} | ${'mul'} | ${[2, -1]} | ${0} | ${-2} ${"math"} | ${"mul"} | ${[2, -1]} | ${0} | ${-2}
${'math'} | ${'fmul'} | ${[10, 0.66]} | ${0} | ${6} ${"math"} | ${"fmul"} | ${[10, 0.66]} | ${0} | ${6}
${'math'} | ${'fmul'} | ${[0.5, 0.5]} | ${0} | ${0} ${"math"} | ${"fmul"} | ${[0.5, 0.5]} | ${0} | ${0}
${'math'} | ${'fmul'} | ${[100.5, 0.5]} | ${0} | ${50} ${"math"} | ${"fmul"} | ${[100.5, 0.5]} | ${0} | ${50}
${'math'} | ${'div'} | ${[2, 2]} | ${0} | ${1} ${"math"} | ${"div"} | ${[2, 2]} | ${0} | ${1}
${'math'} | ${'div'} | ${[2, 3]} | ${0} | ${0} ${"math"} | ${"div"} | ${[2, 3]} | ${0} | ${0}
${'math'} | ${'div'} | ${[10, 5]} | ${0} | ${2} ${"math"} | ${"div"} | ${[10, 5]} | ${0} | ${2}
${'math'} | ${'rem'} | ${[10, 3]} | ${0} | ${1} ${"math"} | ${"rem"} | ${[10, 3]} | ${0} | ${1}
${'math'} | ${'pow'} | ${[2, 2]} | ${0} | ${4} ${"math"} | ${"pow"} | ${[2, 2]} | ${0} | ${4}
${'math'} | ${'pow'} | ${[2, 0]} | ${0} | ${1} ${"math"} | ${"pow"} | ${[2, 0]} | ${0} | ${1}
${'math'} | ${'log'} | ${[2, 2]} | ${0} | ${1} ${"math"} | ${"log"} | ${[2, 2]} | ${0} | ${1}
${'math'} | ${'log'} | ${[2, 4]} | ${0} | ${2} ${"math"} | ${"log"} | ${[2, 4]} | ${0} | ${2}
${'cmp'} | ${'gt'} | ${[2, 4]} | ${0} | ${false} ${"cmp"} | ${"gt"} | ${[2, 4]} | ${0} | ${false}
${'cmp'} | ${'gte'} | ${[2, 4]} | ${0} | ${false} ${"cmp"} | ${"gte"} | ${[2, 4]} | ${0} | ${false}
${'cmp'} | ${'gte'} | ${[4, 2]} | ${0} | ${true} ${"cmp"} | ${"gte"} | ${[4, 2]} | ${0} | ${true}
${'cmp'} | ${'gte'} | ${[2, 2]} | ${0} | ${true} ${"cmp"} | ${"gte"} | ${[2, 2]} | ${0} | ${true}
${'cmp'} | ${'lt'} | ${[2, 4]} | ${0} | ${true} ${"cmp"} | ${"lt"} | ${[2, 4]} | ${0} | ${true}
${'cmp'} | ${'lte'} | ${[2, 4]} | ${0} | ${true} ${"cmp"} | ${"lte"} | ${[2, 4]} | ${0} | ${true}
${'cmp'} | ${'lte'} | ${[4, 2]} | ${0} | ${false} ${"cmp"} | ${"lte"} | ${[4, 2]} | ${0} | ${false}
${'cmp'} | ${'lte'} | ${[2, 2]} | ${0} | ${true} ${"cmp"} | ${"lte"} | ${[2, 2]} | ${0} | ${true}
${'cmp'} | ${'cmp'} | ${[2, 4]} | ${0} | ${-1} ${"cmp"} | ${"cmp"} | ${[2, 4]} | ${0} | ${-1}
${'cmp'} | ${'cmp'} | ${[2, -4]} | ${0} | ${1} ${"cmp"} | ${"cmp"} | ${[2, -4]} | ${0} | ${1}
${'cmp'} | ${'cmp'} | ${[2, 2]} | ${0} | ${0} ${"cmp"} | ${"cmp"} | ${[2, 2]} | ${0} | ${0}
${'array'} | ${'sum'} | ${[[1, 2, 3]]} | ${0} | ${6} ${"array"} | ${"sum"} | ${[[1, 2, 3]]} | ${0} | ${6}
${'array'} | ${'dedup'} | ${[['a', 'a', 'b', 'c', 'a', 'b', 'c']]} | ${0} | ${['a', 'b', 'c']} ${"array"} | ${"dedup"} | ${[["a", "a", "b", "c", "a", "b", "c"]]} | ${0} | ${["a", "b", "c"]}
${'array'} | ${'intersect'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['b', 'c']} ${"array"} | ${"intersect"} | ${[["a", "b", "c"], ["c", "b", "d"]]} | ${0} | ${["b", "c"]}
${'array'} | ${'diff'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['a']} ${"array"} | ${"diff"} | ${[["a", "b", "c"], ["c", "b", "d"]]} | ${0} | ${["a"]}
${'array'} | ${'sdiff'} | ${[['a', 'b', 'c'], ['c', 'b', 'd']]} | ${0} | ${['a', 'd']} ${"array"} | ${"sdiff"} | ${[["a", "b", "c"], ["c", "b", "d"]]} | ${0} | ${["a", "d"]}
${'json'} | ${'obj'} | ${['a', 10, 'b', 'string', 'c', null]} | ${0} | ${{ a: 10, b: 'string', c: null }} ${"json"} | ${"obj"} | ${["a", 10, "b", "string", "c", null]} | ${0} | ${{ a: 10, b: "string", c: null }}
${'json'} | ${'obj'} | ${['a', 10, 'b', 'string', 'c']} | ${1} | ${'Expected even number of argument(s). Got 5'} ${"json"} | ${"obj"} | ${["a", 10, "b", "string", "c"]} | ${1} | ${"Expected even number of argument(s). Got 5"}
${'json'} | ${'obj'} | ${[]} | ${0} | ${{}} ${"json"} | ${"obj"} | ${[]} | ${0} | ${{}}
${'json'} | ${'put'} | ${[{}, 'a', 10]} | ${0} | ${{ a: 10 }} ${"json"} | ${"put"} | ${[{}, "a", 10]} | ${0} | ${{ a: 10 }}
${'json'} | ${'put'} | ${[{ b: 11 }, 'a', 10]} | ${0} | ${{ a: 10, b: 11 }} ${"json"} | ${"put"} | ${[{ b: 11 }, "a", 10]} | ${0} | ${{ a: 10, b: 11 }}
${'json'} | ${'put'} | ${['a', 'a', 11]} | ${1} | ${'Argument 0 expected to be of type object, Got string'} ${"json"} | ${"put"} | ${["a", "a", 11]} | ${1} | ${"Argument 0 expected to be of type object, Got string"}
${'json'} | ${'put'} | ${[{}, 'a', 10, 'b', 20]} | ${1} | ${'Expected 3 argument(s). Got 5'} ${"json"} | ${"put"} | ${[{}, "a", 10, "b", 20]} | ${1} | ${"Expected 3 argument(s). Got 5"}
${'json'} | ${'put'} | ${[{}]} | ${1} | ${'Expected 3 argument(s). Got 1'} ${"json"} | ${"put"} | ${[{}]} | ${1} | ${"Expected 3 argument(s). Got 1"}
${'json'} | ${'puts'} | ${[{}, 'a', 10]} | ${0} | ${{ a: 10 }} ${"json"} | ${"puts"} | ${[{}, "a", 10]} | ${0} | ${{ a: 10 }}
${'json'} | ${'puts'} | ${[{ b: 11 }, 'a', 10]} | ${0} | ${{ a: 10, b: 11 }} ${"json"} | ${"puts"} | ${[{ b: 11 }, "a", 10]} | ${0} | ${{ a: 10, b: 11 }}
${'json'} | ${'puts'} | ${[{}, 'a', 10, 'b', 'string', 'c', null]} | ${0} | ${{ a: 10, b: 'string', c: null }} ${"json"} | ${"puts"} | ${[{}, "a", 10, "b", "string", "c", null]} | ${0} | ${{ a: 10, b: "string", c: null }}
${'json'} | ${'puts'} | ${[{ x: 'text' }, 'a', 10, 'b', 'string']} | ${0} | ${{ a: 10, b: 'string', x: 'text' }} ${"json"} | ${"puts"} | ${[{ x: "text" }, "a", 10, "b", "string"]} | ${0} | ${{ a: 10, b: "string", x: "text" }}
${'json'} | ${'puts'} | ${[{}]} | ${1} | ${'Expected more than 3 argument(s). Got 1'} ${"json"} | ${"puts"} | ${[{}]} | ${1} | ${"Expected more than 3 argument(s). Got 1"}
${'json'} | ${'puts'} | ${['a', 'a', 11]} | ${1} | ${'Argument 0 expected to be of type object, Got string'} ${"json"} | ${"puts"} | ${["a", "a", 11]} | ${1} | ${"Argument 0 expected to be of type object, Got string"}
${'json'} | ${'stringify'} | ${[{ a: 10, b: 'string', c: null }]} | ${0} | ${'{"a":10,"b":"string","c":null}'} ${"json"} | ${"stringify"} | ${[{ a: 10, b: "string", c: null }]} | ${0} | ${'{"a":10,"b":"string","c":null}'}
${'json'} | ${'stringify'} | ${[1]} | ${1} | ${'Argument 0 expected to be of type object, Got number'} ${"json"} | ${"stringify"} | ${[1]} | ${1} | ${"Argument 0 expected to be of type object, Got number"}
${'json'} | ${'parse'} | ${['{"a":10,"b":"string","c":null}']} | ${0} | ${{ a: 10, b: 'string', c: null }} ${"json"} | ${"parse"} | ${['{"a":10,"b":"string","c":null}']} | ${0} | ${{ a: 10, b: "string", c: null }}
${'json'} | ${'parse'} | ${['incorrect']} | ${1} | ${'Unexpected token i in JSON at position 0'} ${"json"} | ${"parse"} | ${["incorrect"]} | ${1} | ${"Unexpected token i in JSON at position 0"}
${'json'} | ${'parse'} | ${[10]} | ${1} | ${'Argument 0 expected to be of type string, Got number'} ${"json"} | ${"parse"} | ${[10]} | ${1} | ${"Argument 0 expected to be of type string, Got number"}
`( `(
// //
'$fnName with $args expected retcode: $retCode and result: $result', "$fnName with $args expected retcode: $retCode and result: $result",
async ({ serviceId, fnName, args, retCode, result }) => { async ({ serviceId, fnName, args, retCode, result }: ServiceCallType) => {
// arrange // arrange
const req: CallServiceData = { const req: CallServiceData = {
serviceId: serviceId, serviceId: serviceId,
@ -118,8 +144,8 @@ describe('Tests for default handler', () => {
args: args, args: args,
tetraplets: [], tetraplets: [],
particleContext: { particleContext: {
particleId: 'some', particleId: "some",
initPeerId: 'init peer id', initPeerId: "init peer id",
timestamp: 595951200, timestamp: 595951200,
ttl: 595961200, ttl: 595961200,
signature: new Uint8Array([]), signature: new Uint8Array([]),
@ -132,8 +158,10 @@ describe('Tests for default handler', () => {
// Our test cases above depend on node error message. In node 20 error message for JSON.parse has changed. // Our test cases above depend on node error message. In node 20 error message for JSON.parse has changed.
// Simple and fast solution for this specific case is to unify both variations into node 18 version error format. // Simple and fast solution for this specific case is to unify both variations into node 18 version error format.
if (res.result === 'Unexpected token \'i\', "incorrect" is not valid JSON') { if (
res.result = 'Unexpected token i in JSON at position 0'; res.result === "Unexpected token 'i', \"incorrect\" is not valid JSON"
) {
res.result = "Unexpected token i in JSON at position 0";
} }
// assert // assert
@ -144,16 +172,16 @@ describe('Tests for default handler', () => {
}, },
); );
it('should return correct error message for identiy service', async () => { it("should return correct error message for identiy service", async () => {
// arrange // arrange
const req: CallServiceData = { const req: CallServiceData = {
serviceId: 'peer', serviceId: "peer",
fnName: 'identify', fnName: "identify",
args: [], args: [],
tetraplets: [], tetraplets: [],
particleContext: { particleContext: {
particleId: 'some', particleId: "some",
initPeerId: 'init peer id', initPeerId: "init peer id",
timestamp: 595951200, timestamp: 595951200,
ttl: 595961200, ttl: 595961200,
signature: new Uint8Array([]), signature: new Uint8Array([]),
@ -169,22 +197,27 @@ describe('Tests for default handler', () => {
retCode: 0, retCode: 0,
result: { result: {
external_addresses: [], external_addresses: [],
node_version: expect.stringContaining('js'), // stringContaining method returns any for some reason
air_version: expect.stringContaining('js'), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
node_version: expect.stringContaining("js"),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
air_version: expect.stringContaining("js"),
}, },
}); });
}); });
}); });
const key = '+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk='; const key = "+cmeYlZKj+MfSa9dpHV+BmLPm6wq4inGlsPlQ1GvtPk=";
const context = (async () => { const context = (async () => {
const keyBytes = toUint8Array(key); const keyBytes = toUint8Array(key);
const kp = await KeyPair.fromEd25519SK(keyBytes); const kp = await KeyPair.fromEd25519SK(keyBytes);
const res = { const res = {
peerKeyPair: kp, peerKeyPair: kp,
peerId: kp.getPeerId(), peerId: kp.getPeerId(),
}; };
return res; return res;
})(); })();
@ -192,44 +225,58 @@ const testData = [1, 2, 3, 4, 5, 6, 7, 9, 10];
// signature produced by KeyPair created from key above (`key` variable) // signature produced by KeyPair created from key above (`key` variable)
const testDataSig = [ const testDataSig = [
224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132, 107, 77, 224, 67, 99, 106, 76, 29, 144, 224, 104, 245, 206, 140, 248, 27, 72, 68, 133, 111, 10, 164, 197, 242, 132,
121, 122, 169, 36, 173, 58, 80, 170, 102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29, 107, 77, 224, 67, 99, 106, 76, 29, 144, 121, 122, 169, 36, 173, 58, 80, 170,
102, 137, 253, 157, 247, 168, 87, 162, 223, 188, 214, 203, 220, 52, 246, 29,
86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15, 86, 77, 71, 224, 248, 16, 213, 254, 75, 78, 239, 243, 222, 241, 15,
]; ];
// signature produced by KeyPair created from some random KeyPair // signature produced by KeyPair created from some random KeyPair
const testDataWrongSig = [ const testDataWrongSig = [
116, 247, 189, 118, 236, 53, 147, 123, 219, 75, 176, 105, 101, 108, 233, 137, 97, 14, 146, 132, 252, 70, 51, 153, 116, 247, 189, 118, 236, 53, 147, 123, 219, 75, 176, 105, 101, 108, 233, 137,
237, 167, 156, 150, 36, 90, 229, 108, 166, 231, 255, 137, 8, 246, 125, 0, 213, 150, 83, 196, 237, 221, 131, 159, 97, 14, 146, 132, 252, 70, 51, 153, 237, 167, 156, 150, 36, 90, 229, 108, 166,
157, 159, 25, 109, 95, 160, 181, 65, 254, 238, 47, 156, 240, 151, 58, 14, 231, 255, 137, 8, 246, 125, 0, 213, 150, 83, 196, 237, 221, 131, 159, 157,
159, 25, 109, 95, 160, 181, 65, 254, 238, 47, 156, 240, 151, 58, 14,
]; ];
const makeTetraplet = (initPeerId: string, serviceId?: string, fnName?: string): CallParams<'data'> => { const makeTestTetraplet = (
initPeerId: string,
serviceId: string,
fnName: string,
): CallParams<"data"> => {
return { return {
particleId: "",
timestamp: 0,
ttl: 0,
initPeerId: initPeerId, initPeerId: initPeerId,
tetraplets: { tetraplets: {
data: [ data: [
{ {
peer_pk: initPeerId,
function_name: fnName, function_name: fnName,
service_id: serviceId, service_id: serviceId,
json_path: "",
}, },
], ],
}, },
} as any; };
}; };
describe('Sig service tests', () => { describe("Sig service tests", () => {
it('sig.sign should create the correct signature', async () => { it("sig.sign should create the correct signature", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
const res = await sig.sign(testData, makeTetraplet(ctx.peerId)); const res = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "any_service", "any_func"),
);
expect(res.success).toBe(true); expect(res.success).toBe(true);
expect(res.signature).toStrictEqual(testDataSig); expect(res.signature).toStrictEqual(testDataSig);
}); });
it('sig.verify should return true for the correct signature', async () => { it("sig.verify should return true for the correct signature", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
@ -238,7 +285,7 @@ describe('Sig service tests', () => {
expect(res).toBe(true); expect(res).toBe(true);
}); });
it('sig.verify should return false for the incorrect signature', async () => { it("sig.verify should return false for the incorrect signature", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
@ -247,63 +294,93 @@ describe('Sig service tests', () => {
expect(res).toBe(false); expect(res).toBe(false);
}); });
it('sign-verify call chain should work', async () => { it("sign-verify call chain should work", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
const signature = await sig.sign(testData, makeTetraplet(ctx.peerId)); const signature = await sig.sign(
const res = await sig.verify(signature.signature as number[], testData); testData,
makeTestTetraplet(ctx.peerId, "any_service", "any_func"),
);
expect(signature.success).toBe(true);
assert(signature.success);
const res = await sig.verify(signature.signature, testData);
expect(res).toBe(true); expect(res).toBe(true);
}); });
it('sig.sign with defaultSigGuard should work for correct callParams', async () => { it("sig.sign with defaultSigGuard should work for correct callParams", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
sig.securityGuard = defaultSigGuard(ctx.peerId); sig.securityGuard = defaultSigGuard(ctx.peerId);
const signature = await sig.sign(testData, makeTetraplet(ctx.peerId, 'registry', 'get_route_bytes')); const signature = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "registry", "get_route_bytes"),
);
await expect(signature).toBeDefined(); expect(signature).toBeDefined();
}); });
it('sig.sign with defaultSigGuard should not allow particles initiated from incorrect service', async () => { it("sig.sign with defaultSigGuard should not allow particles initiated from incorrect service", async () => {
const ctx = await context;
const sig = new Sig(ctx.peerKeyPair);
sig.securityGuard = defaultSigGuard(ctx.peerId);
const res = await sig.sign(testData, makeTetraplet(ctx.peerId, 'other_service', 'other_fn'));
await expect(res.success).toBe(false);
await expect(res.error).toBe('Security guard validation failed');
});
it('sig.sign with defaultSigGuard should not allow particles initiated from other peers', async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
sig.securityGuard = defaultSigGuard(ctx.peerId); sig.securityGuard = defaultSigGuard(ctx.peerId);
const res = await sig.sign( const res = await sig.sign(
testData, testData,
makeTetraplet((await KeyPair.randomEd25519()).getPeerId(), 'registry', 'get_key_bytes'), makeTestTetraplet(ctx.peerId, "other_service", "other_fn"),
); );
await expect(res.success).toBe(false); expect(res.success).toBe(false);
await expect(res.error).toBe('Security guard validation failed'); expect(res.error).toBe("Security guard validation failed");
}); });
it('changing securityGuard should work', async () => { it("sig.sign with defaultSigGuard should not allow particles initiated from other peers", async () => {
const ctx = await context; const ctx = await context;
const sig = new Sig(ctx.peerKeyPair); const sig = new Sig(ctx.peerKeyPair);
sig.securityGuard = allowServiceFn('test', 'test'); sig.securityGuard = defaultSigGuard(ctx.peerId);
const successful1 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'test', 'test')); const res = await sig.sign(
const unSuccessful1 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'wrong', 'wrong')); testData,
makeTestTetraplet(
(await KeyPair.randomEd25519()).getPeerId(),
"registry",
"get_key_bytes",
),
);
sig.securityGuard = allowServiceFn('wrong', 'wrong'); expect(res.success).toBe(false);
expect(res.error).toBe("Security guard validation failed");
});
const successful2 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'wrong', 'wrong')); it("changing securityGuard should work", async () => {
const unSuccessful2 = await sig.sign(testData, makeTetraplet(ctx.peerId, 'test', 'test')); const ctx = await context;
const sig = new Sig(ctx.peerKeyPair);
sig.securityGuard = allowServiceFn("test", "test");
const successful1 = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "test", "test"),
);
const unSuccessful1 = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "wrong", "wrong"),
);
sig.securityGuard = allowServiceFn("wrong", "wrong");
const successful2 = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "wrong", "wrong"),
);
const unSuccessful2 = await sig.sign(
testData,
makeTestTetraplet(ctx.peerId, "test", "test"),
);
expect(successful1.success).toBe(true); expect(successful1.success).toBe(true);
expect(successful2.success).toBe(true); expect(successful2.success).toBe(true);

Some files were not shown because too many files have changed in this diff Show More