mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2025-06-26 06:11:33 +00:00
feat!: Unify all packages (#327)
* * Separate marine worker as a package * Trying to fix tests * Finalizing test fixes * fix: rename back to Fluence CLI (#320) chore: rename back to Fluence CLI * fix(deps): update dependency @fluencelabs/avm to v0.43.1 (#322) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: release master (#324) * chore: release master * chore: Regenerate pnpm lock file * feat: use marine-js 0.7.2 (#321) * use marine-js 0.5.0 * increace some timeouts * increace some timeouts * use latest marine + remove larger timeouts * propagate CallParameters type * use marine 0.7.2 * Temp use node 18 and 20 * Comment out node 20.x --------- Co-authored-by: Anatoly Laskaris <github_me@nahsi.dev> * chore: Fix test with node 18/20 error message (#323) * Fix test with node 18/20 error message * Run tests on node 18 and 20 * Enhance description * Fix type and obj property --------- Co-authored-by: Anatoly Laskaris <github_me@nahsi.dev> * * Separate marine worker as a package * Trying to fix tests * Finalizing test fixes * * Refactoring packages. * Using CDN to load .wasm deps. * Setting up tests for new architecture * Fix almost all tests * Fix last strange test * Remove package specific packages * Remove avm class as it looks excessive * marine worker new version * misc refactoring/remove console.log's * Rename package js-peer to js-client * Move service info to marine worker * Change CDN path * Fix worker race confition * Remove buffer type * Remove turned off headless mode in platform tests * Remove async keyword to make tests pass * Remove util package * Make js-client.api package just reexport interface from js-client main package * Update package info in CI * Fix review comments * Remove test entry from marine-worker package * Misc fixes * Fix worker type * Add fetchers * Specify correct versions for js-client package * Set first ver for js-client * Update libp2p and related dep versions to the latest * Build all deps into package itself * Fix review * Refine package * Fix comment * Update packages/core/js-client/src/fetchers/browser.ts * Update packages/core/js-client/src/fetchers/index.ts * Update packages/core/js-client/src/fetchers/node.ts * Update packages/core/js-client/src/jsPeer/FluencePeer.ts * Update packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts * Update packages/core/js-client/src/jsPeer/FluencePeer.ts Co-authored-by: shamsartem <shamsartem@gmail.com> * Delete outdated file * Need types for build to work * Inline func call * Add comments to replacement lines. P.S. we can remove some of them after update libp2p --------- Co-authored-by: shamsartem <shamsartem@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: fluencebot <116741523+fluencebot@users.noreply.github.com> Co-authored-by: Valery Antopol <valery.antopol@gmail.com> Co-authored-by: Anatoly Laskaris <github_me@nahsi.dev>
This commit is contained in:
102
packages/core/js-client/build.ts
Normal file
102
packages/core/js-client/build.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import path, { dirname } from 'path';
|
||||
import type { InlineConfig, PluginOption } from 'vite';
|
||||
import { build } from 'vite';
|
||||
import { builtinModules, createRequire } from 'module';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import inject from '@rollup/plugin-inject';
|
||||
import stdLibBrowser from 'node-stdlib-browser';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { rm, rename } from 'fs/promises';
|
||||
import { replaceCodePlugin } from 'vite-plugin-replace';
|
||||
import pkg from './package.json' assert { type: 'json' };
|
||||
import libAssetsPlugin from '@laynezh/vite-plugin-lib-assets';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const commonConfig = (isNode: boolean): InlineConfig & Required<Pick<InlineConfig, 'build'>> => {
|
||||
const esbuildShim = require.resolve('node-stdlib-browser/helpers/esbuild/shim');
|
||||
return {
|
||||
build: {
|
||||
target: 'modules',
|
||||
minify: 'esbuild',
|
||||
lib: {
|
||||
entry: './src/index.ts',
|
||||
name: 'js-client',
|
||||
fileName: `${isNode ? 'node' : 'browser'}/index`,
|
||||
},
|
||||
outDir: './dist',
|
||||
emptyOutDir: false,
|
||||
...(isNode ? {
|
||||
rollupOptions: {
|
||||
external: [...builtinModules, ...builtinModules.map(bm => `node:${bm}`)],
|
||||
plugins: [
|
||||
// @ts-ignore
|
||||
inject({
|
||||
self: 'global',
|
||||
'WorkerScope': ['worker_threads', '*'],
|
||||
'Worker': ['worker_threads', 'Worker'],
|
||||
'isMainThread': ['worker_threads', 'isMainThread'],
|
||||
})
|
||||
]
|
||||
}
|
||||
} : {
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
{
|
||||
// @ts-ignore
|
||||
...inject({
|
||||
global: [esbuildShim, 'global'],
|
||||
process: [esbuildShim, 'process'],
|
||||
Buffer: [esbuildShim, 'Buffer']
|
||||
}), enforce: 'post'
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
},
|
||||
plugins: [tsconfigPaths(), libAssetsPlugin({
|
||||
include: ['**/*.wasm*', '**/marine-worker.umd.cjs*'],
|
||||
publicUrl: '/',
|
||||
}), ...(isNode ? [replaceCodePlugin({
|
||||
replacements: [
|
||||
// After 'threads' package is built, it produces wrong output, which throws runtime errors.
|
||||
// This code aims to fix such places.
|
||||
// Should remove this after we move from threads to other package.
|
||||
{ from: 'eval("require")("worker_threads")', to: 'WorkerScope' },
|
||||
{ from: 'eval("require")("worker_threads")', to: 'WorkerScope' },
|
||||
]
|
||||
})] : [])] as PluginOption[],
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
browserField: !isNode,
|
||||
conditions: isNode ? ['node'] : ['browser']
|
||||
},
|
||||
// Used only by browser
|
||||
define: {
|
||||
__JS_CLIENT_VERSION__: pkg.version,
|
||||
__ENV__: isNode ? 'node' : 'browser'
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const buildClient = async () => {
|
||||
const nodeConfig = commonConfig(true);
|
||||
const browserConfig = commonConfig(false);
|
||||
|
||||
try {
|
||||
await rm('./dist', { recursive: true });
|
||||
} catch {}
|
||||
|
||||
await build(nodeConfig);
|
||||
await build(browserConfig);
|
||||
};
|
||||
|
||||
buildClient()
|
||||
.then(() => console.log('Built successfully'))
|
||||
.catch((err) => console.error('failed', err));
|
70
packages/core/js-client/package.json
Normal file
70
packages/core/js-client/package.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@fluencelabs/js-client",
|
||||
"version": "0.0.10",
|
||||
"description": "Client for interacting with Fluence network",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/browser/index.js",
|
||||
"unpkg": "./dist/browser/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"node": "./dist/node/index.js",
|
||||
"default": "./dist/browser/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node --loader ts-node/esm build.ts && tsc --emitDeclarationOnly",
|
||||
"test": "vitest --threads false run"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-noise": "13.0.0",
|
||||
"@fluencelabs/interfaces": "0.8.1",
|
||||
"@libp2p/crypto": "2.0.3",
|
||||
"@libp2p/interface": "0.1.2",
|
||||
"@libp2p/mplex": "9.0.4",
|
||||
"@libp2p/peer-id": "3.0.2",
|
||||
"@libp2p/peer-id-factory": "3.0.3",
|
||||
"@libp2p/websockets": "7.0.4",
|
||||
"@multiformats/multiaddr": "11.3.0",
|
||||
"async": "3.2.4",
|
||||
"bs58": "5.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"debug": "4.3.4",
|
||||
"it-length-prefixed": "8.0.4",
|
||||
"it-map": "2.0.0",
|
||||
"it-pipe": "2.0.5",
|
||||
"js-base64": "3.7.5",
|
||||
"libp2p": "0.46.6",
|
||||
"multiformats": "11.0.1",
|
||||
"rxjs": "7.5.5",
|
||||
"threads": "1.7.0",
|
||||
"ts-pattern": "3.3.3",
|
||||
"uint8arrays": "4.0.3",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fluencelabs/aqua-api": "0.9.3",
|
||||
"@fluencelabs/avm": "0.43.1",
|
||||
"@fluencelabs/marine-js": "0.7.2",
|
||||
"@fluencelabs/marine-worker": "workspace:*",
|
||||
"@laynezh/vite-plugin-lib-assets": "0.5.2",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@types/bs58": "4.0.1",
|
||||
"@types/debug": "4.1.7",
|
||||
"@types/uuid": "8.3.2",
|
||||
"node-stdlib-browser": "1.2.0",
|
||||
"vite": "4.0.4",
|
||||
"vite-plugin-replace": "0.1.1",
|
||||
"vite-tsconfig-paths": "4.0.3",
|
||||
"vitest": "0.29.7"
|
||||
}
|
||||
}
|
173
packages/core/js-client/src/api.ts
Normal file
173
packages/core/js-client/src/api.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 type { FnConfig, FunctionCallDef, ServiceDef } from '@fluencelabs/interfaces';
|
||||
import type { IFluenceClient } from '@fluencelabs/interfaces';
|
||||
import { getArgumentTypes } from '@fluencelabs/interfaces';
|
||||
import { isFluencePeer } from '@fluencelabs/interfaces';
|
||||
import { callAquaFunction, Fluence, registerService } from './index.js';
|
||||
|
||||
/**
|
||||
* Convenience function to support Aqua `func` generation backend
|
||||
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script
|
||||
*
|
||||
* @param rawFnArgs - raw arguments passed by user to the generated function
|
||||
* @param def - function definition generated by the Aqua compiler
|
||||
* @param script - air script with function execution logic generated by the Aqua compiler
|
||||
*/
|
||||
export const v5_callFunction = async (
|
||||
rawFnArgs: Array<any>,
|
||||
def: FunctionCallDef,
|
||||
script: string,
|
||||
): Promise<unknown> => {
|
||||
const { args, client: peer, config } = await extractFunctionArgs(rawFnArgs, def);
|
||||
|
||||
return callAquaFunction({
|
||||
args,
|
||||
def,
|
||||
script,
|
||||
config: config || {},
|
||||
peer: peer,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function to support Aqua `service` generation backend
|
||||
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script
|
||||
* @param args - raw arguments passed by user to the generated function
|
||||
* @param def - service definition generated by the Aqua compiler
|
||||
*/
|
||||
export const v5_registerService = async (args: any[], def: ServiceDef): Promise<unknown> => {
|
||||
const { peer, service, serviceId } = await extractServiceArgs(args, def.defaultServiceId);
|
||||
|
||||
return registerService({
|
||||
def,
|
||||
service,
|
||||
serviceId,
|
||||
peer,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Arguments could be passed in one these configurations:
|
||||
* [...actualArgs]
|
||||
* [peer, ...actualArgs]
|
||||
* [...actualArgs, config]
|
||||
* [peer, ...actualArgs, config]
|
||||
*
|
||||
* This function select the appropriate configuration and returns
|
||||
* arguments in a structured way of: { peer, config, args }
|
||||
*/
|
||||
const extractFunctionArgs = async (
|
||||
args: any[],
|
||||
def: FunctionCallDef,
|
||||
): Promise<{
|
||||
client: IFluenceClient;
|
||||
config?: FnConfig;
|
||||
args: { [key: string]: any };
|
||||
}> => {
|
||||
const argumentTypes = getArgumentTypes(def);
|
||||
const argumentNames = Object.keys(argumentTypes);
|
||||
const numberOfExpectedArgs = argumentNames.length;
|
||||
|
||||
let peer: IFluenceClient;
|
||||
let structuredArgs: any[];
|
||||
let config: FnConfig;
|
||||
if (isFluencePeer(args[0])) {
|
||||
peer = args[0];
|
||||
structuredArgs = args.slice(1, numberOfExpectedArgs + 1);
|
||||
config = args[numberOfExpectedArgs + 1];
|
||||
} else {
|
||||
if (!Fluence.defaultClient) {
|
||||
throw new Error(
|
||||
'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?',
|
||||
);
|
||||
}
|
||||
peer = Fluence.defaultClient;
|
||||
structuredArgs = args.slice(0, numberOfExpectedArgs);
|
||||
config = args[numberOfExpectedArgs];
|
||||
}
|
||||
|
||||
if (structuredArgs.length !== numberOfExpectedArgs) {
|
||||
throw new Error(`Incorrect number of arguments. Expecting ${numberOfExpectedArgs}`);
|
||||
}
|
||||
|
||||
const argsRes = argumentNames.reduce((acc, name, index) => ({ ...acc, [name]: structuredArgs[index] }), {});
|
||||
|
||||
return {
|
||||
client: peer,
|
||||
config: config,
|
||||
args: argsRes,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Arguments could be passed in one these configurations:
|
||||
* [serviceObject]
|
||||
* [peer, serviceObject]
|
||||
* [defaultId, serviceObject]
|
||||
* [peer, defaultId, serviceObject]
|
||||
*
|
||||
* Where serviceObject is the raw object with function definitions passed by user
|
||||
*
|
||||
* This function select the appropriate configuration and returns
|
||||
* arguments in a structured way of: { peer, serviceId, service }
|
||||
*/
|
||||
const extractServiceArgs = async (
|
||||
args: any[],
|
||||
defaultServiceId?: string,
|
||||
): Promise<{ peer: IFluenceClient; serviceId: string; service: any }> => {
|
||||
let peer: IFluenceClient;
|
||||
let serviceId: any;
|
||||
let service: any;
|
||||
if (isFluencePeer(args[0])) {
|
||||
peer = args[0];
|
||||
} else {
|
||||
if (!Fluence.defaultClient) {
|
||||
throw new Error(
|
||||
'Could not register Aqua service because the client is not initialized. Did you forget to call Fluence.connect()?',
|
||||
);
|
||||
}
|
||||
peer = Fluence.defaultClient;
|
||||
}
|
||||
|
||||
if (typeof args[0] === 'string') {
|
||||
serviceId = args[0];
|
||||
} else if (typeof args[1] === 'string') {
|
||||
serviceId = args[1];
|
||||
} else {
|
||||
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 {
|
||||
peer: peer,
|
||||
serviceId: serviceId,
|
||||
service: service,
|
||||
};
|
||||
};
|
@ -63,11 +63,10 @@ export class ClientPeer extends FluencePeer implements IFluenceClient {
|
||||
relayConfig: RelayConnectionConfig,
|
||||
keyPair: KeyPair,
|
||||
marine: IMarineHost,
|
||||
avmRunner: IAvmRunner,
|
||||
) {
|
||||
const relayConnection = new RelayConnection(relayConfig);
|
||||
|
||||
super(peerConfig, keyPair, marine, new JsServiceHost(), avmRunner, relayConnection);
|
||||
super(peerConfig, keyPair, marine, new JsServiceHost(), relayConnection);
|
||||
this.relayPeerId = relayConnection.getRelayPeerId();
|
||||
this.relayConnection = relayConnection;
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
import { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { pipe } from 'it-pipe';
|
||||
import { encode, decode } from 'it-length-prefixed';
|
||||
import type { PeerId } from '@libp2p/interface-peer-id';
|
||||
import type { PeerId } from '@libp2p/interface/peer-id';
|
||||
import { createLibp2p, Libp2p } from 'libp2p';
|
||||
|
||||
import { noise } from '@chainsafe/libp2p-noise';
|
||||
@ -115,6 +115,10 @@ export class RelayConnection implements IStartable, IConnection {
|
||||
connectionManager: {
|
||||
dialTimeout: this.config.dialTimeoutMs,
|
||||
},
|
||||
connectionGater: {
|
||||
// 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)
|
||||
}
|
||||
});
|
||||
|
||||
this.lib2p2Peer = lib2p2Peer;
|
@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
|
||||
import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js';
|
||||
import { KeyPair } from '../keypair/index.js';
|
||||
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
|
||||
@ -31,9 +30,8 @@ export class EphemeralNetworkClient extends FluencePeer {
|
||||
const workerLoader = new WorkerLoader();
|
||||
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
|
||||
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
|
||||
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, avmModuleLoader);
|
||||
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
|
||||
super(config, keyPair, marine, new JsServiceHost(), avm, conn);
|
||||
super(config, keyPair, marine, new JsServiceHost(), conn);
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ import { Subject } from 'rxjs';
|
||||
import { Particle } from '../particle/Particle.js';
|
||||
|
||||
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
|
||||
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
|
||||
import { DEFAULT_CONFIG, FluencePeer } from '../jsPeer/FluencePeer.js';
|
||||
import { IConnection } from '../connection/interfaces.js';
|
||||
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
|
||||
@ -194,9 +193,9 @@ export class EphemeralConnection implements IConnection, IEphemeralConnection {
|
||||
class EphemeralPeer extends FluencePeer {
|
||||
ephemeralConnection: EphemeralConnection;
|
||||
|
||||
constructor(keyPair: KeyPair, marine: IMarineHost, avm: IAvmRunner) {
|
||||
constructor(keyPair: KeyPair, marine: IMarineHost) {
|
||||
const conn = new EphemeralConnection(keyPair.getPeerId());
|
||||
super(DEFAULT_CONFIG, keyPair, marine, new JsServiceHost(), avm, conn);
|
||||
super(DEFAULT_CONFIG, keyPair, marine, new JsServiceHost(), conn);
|
||||
|
||||
this.ephemeralConnection = conn;
|
||||
}
|
||||
@ -228,14 +227,13 @@ export class EphemeralNetwork {
|
||||
|
||||
const promises = this.config.peers.map(async (x) => {
|
||||
const kp = await fromBase64Sk(x.sk);
|
||||
const marine = new MarineBackgroundRunner(this.workerLoader, this.controlModuleLoader);
|
||||
const avm = new MarineBasedAvmRunner(marine, this.avmModuleLoader);
|
||||
const marine = new MarineBackgroundRunner(this.workerLoader, this.controlModuleLoader, this.avmModuleLoader);
|
||||
const peerId = kp.getPeerId();
|
||||
if (peerId !== x.peerId) {
|
||||
throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`);
|
||||
}
|
||||
|
||||
return new EphemeralPeer(kp, marine, avm);
|
||||
return new EphemeralPeer(kp, marine);
|
||||
});
|
||||
|
||||
const peers = await Promise.all(promises);
|
19
packages/core/js-client/src/fetchers/browser.ts
Normal file
19
packages/core/js-client/src/fetchers/browser.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 async function fetchResource(assetPath: string, version: string) {
|
||||
return fetch(new globalThis.URL(`@fluencelabs/js-client@${version}/dist` + assetPath, `https://unpkg.com/`));
|
||||
}
|
30
packages/core/js-client/src/fetchers/index.ts
Normal file
30
packages/core/js-client/src/fetchers/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { fetchResource as fetchResourceBrowser } from './browser.js';
|
||||
import { fetchResource as fetchResourceNode } from './node.js';
|
||||
import process from 'process';
|
||||
|
||||
const isNode = typeof process !== 'undefined' && process?.release?.name === 'node';
|
||||
|
||||
export async function fetchResource(assetPath: string, version: string) {
|
||||
switch (true) {
|
||||
case isNode:
|
||||
return fetchResourceNode(assetPath, version);
|
||||
default:
|
||||
return fetchResourceBrowser(assetPath, version);
|
||||
}
|
||||
}
|
44
packages/core/js-client/src/fetchers/node.ts
Normal file
44
packages/core/js-client/src/fetchers/node.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 fs from 'fs';
|
||||
import url from 'url';
|
||||
import path from 'path';
|
||||
|
||||
export async function fetchResource(assetPath: string, version: string) {
|
||||
const file = await new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
// Cannot use 'fs/promises' with current vite config. This module is not polyfilled by default.
|
||||
const root = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const workerFilePath = path.join(root, '..', assetPath);
|
||||
fs.readFile(workerFilePath, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
'Content-type':
|
||||
assetPath.endsWith('.wasm')
|
||||
? 'application/wasm'
|
||||
: assetPath.endsWith('.js')
|
||||
? 'application/javascript'
|
||||
: 'application/text'
|
||||
}
|
||||
});
|
||||
}
|
165
packages/core/js-client/src/index.ts
Normal file
165
packages/core/js-client/src/index.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 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 avmWasmUrl from '../node_modules/@fluencelabs/avm/dist/avm.wasm?url';
|
||||
import marineJsWasmUrl from '../node_modules/@fluencelabs/marine-js/dist/marine-js.wasm?url';
|
||||
import workerCodeUrl from '../node_modules/@fluencelabs/marine-worker/dist/__ENV__/marine-worker.umd.cjs?url';
|
||||
|
||||
const JS_CLIENT_VERSION = '__JS_CLIENT_VERSION__';
|
||||
|
||||
const isNode = typeof process !== 'undefined' && process?.release?.name === 'node';
|
||||
|
||||
const fetchWorkerCode = () => fetchResource(workerCodeUrl, JS_CLIENT_VERSION).then(res => res.text());
|
||||
const fetchMarineJsWasm = () => fetchResource(marineJsWasmUrl, JS_CLIENT_VERSION).then(res => res.arrayBuffer());
|
||||
const fetchAvmWasm = () => fetchResource(avmWasmUrl, JS_CLIENT_VERSION).then(res => res.arrayBuffer());
|
||||
|
||||
const createClient = async (relay: RelayOptions, config: ClientConfig): Promise<IFluenceClient> => {
|
||||
const workerCode = await fetchWorkerCode();
|
||||
|
||||
const marineJsWasm = await fetchMarineJsWasm();
|
||||
const avmWasm = await fetchAvmWasm();
|
||||
|
||||
const marine = new MarineBackgroundRunner({
|
||||
getValue() {
|
||||
return BlobWorker.fromText(workerCode)
|
||||
},
|
||||
start() {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
stop() {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
}, {
|
||||
getValue() {
|
||||
return marineJsWasm;
|
||||
}, start(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}, stop(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}, {
|
||||
getValue() {
|
||||
return avmWasm;
|
||||
}, start(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}, stop(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
});
|
||||
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config);
|
||||
const client: IFluenceClient = new ClientPeer(peerConfig, relayConfig, keyPair, marine);
|
||||
if (isNode) {
|
||||
doRegisterNodeUtils(client);
|
||||
}
|
||||
await client.connect();
|
||||
return client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Public interface to Fluence Network
|
||||
*/
|
||||
export const Fluence = {
|
||||
defaultClient: undefined as (IFluenceClient | undefined),
|
||||
/**
|
||||
* Connect to the Fluence network
|
||||
* @param relay - relay node to connect to
|
||||
* @param config - client configuration
|
||||
*/
|
||||
connect: async function(relay: RelayOptions, config: ClientConfig): Promise<void> {
|
||||
const client = await createClient(relay, config);
|
||||
this.defaultClient = client;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnect from the Fluence network
|
||||
*/
|
||||
disconnect: async function(): Promise<void> {
|
||||
await this.defaultClient?.disconnect();
|
||||
this.defaultClient = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle connection state changes. Immediately returns the current connection state
|
||||
*/
|
||||
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState {
|
||||
return this.defaultClient?.onConnectionStateChange(handler) || 'disconnected';
|
||||
},
|
||||
|
||||
/**
|
||||
* Low level API. Get the underlying client instance which holds the connection to the network
|
||||
* @returns IFluenceClient instance
|
||||
*/
|
||||
getClient: async function(): Promise<IFluenceClient> {
|
||||
if (!this.defaultClient) {
|
||||
throw new Error('Fluence client is not initialized. Call Fluence.connect() first');
|
||||
}
|
||||
return this.defaultClient;
|
||||
},
|
||||
};
|
||||
|
||||
export type { IFluenceClient, ClientConfig, CallParams } from '@fluencelabs/interfaces';
|
||||
|
||||
export type {
|
||||
ArrayType,
|
||||
ArrowType,
|
||||
ArrowWithCallbacks,
|
||||
ArrowWithoutCallbacks,
|
||||
BottomType,
|
||||
FunctionCallConstants,
|
||||
FunctionCallDef,
|
||||
LabeledProductType,
|
||||
NilType,
|
||||
NonArrowType,
|
||||
OptionType,
|
||||
ProductType,
|
||||
ScalarNames,
|
||||
ScalarType,
|
||||
ServiceDef,
|
||||
StructType,
|
||||
TopType,
|
||||
UnlabeledProductType,
|
||||
CallAquaFunctionType,
|
||||
CallAquaFunctionArgs,
|
||||
PassedArgs,
|
||||
FnConfig,
|
||||
RegisterServiceType,
|
||||
RegisterServiceArgs,
|
||||
} from '@fluencelabs/interfaces';
|
||||
|
||||
export { v5_callFunction, v5_registerService } from './api.js';
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.new_fluence = Fluence;
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.fluence = {
|
||||
clientFactory: createClient,
|
||||
callAquaFunction,
|
||||
registerService,
|
||||
};
|
||||
|
||||
export { createClient, callAquaFunction, registerService };
|
||||
export { getFluenceInterface, getFluenceInterfaceFromGlobalThis } from './util/loadClient.js';
|
@ -16,7 +16,7 @@
|
||||
import { KeyPair } from '../keypair/index.js';
|
||||
|
||||
import type { PeerIdB58 } from '@fluencelabs/interfaces';
|
||||
import { KeyPairFormat } from '@fluencelabs/avm';
|
||||
import { deserializeAvmResult, InterpreterResult, KeyPairFormat, serializeAvmArgs } from '@fluencelabs/avm';
|
||||
import {
|
||||
cloneWithNewData,
|
||||
getActualTTL,
|
||||
@ -91,7 +91,6 @@ export abstract class FluencePeer {
|
||||
public readonly keyPair: KeyPair,
|
||||
protected readonly marineHost: IMarineHost,
|
||||
protected readonly jsServiceHost: IJsServiceHost,
|
||||
protected readonly avmRunner: IAvmRunner,
|
||||
protected readonly connection: IConnection,
|
||||
) {
|
||||
this._initServices();
|
||||
@ -110,9 +109,7 @@ export abstract class FluencePeer {
|
||||
if (this.config?.debug?.printParticleId) {
|
||||
this.printParticleId = true;
|
||||
}
|
||||
|
||||
await this.marineHost.start();
|
||||
await this.avmRunner.start();
|
||||
|
||||
this._startParticleProcessing();
|
||||
this.isInitialized = true;
|
||||
@ -128,7 +125,6 @@ export abstract class FluencePeer {
|
||||
this._particleSourceSubscription?.unsubscribe();
|
||||
this._stopParticleProcessing();
|
||||
await this.marineHost.stop();
|
||||
await this.avmRunner.stop();
|
||||
|
||||
this.isInitialized = false;
|
||||
log_peer.trace('stopped Fluence peer');
|
||||
@ -159,8 +155,8 @@ export abstract class FluencePeer {
|
||||
* Removes the specified marine service from the Fluence peer
|
||||
* @param serviceId - the service id to remove
|
||||
*/
|
||||
removeMarineService(serviceId: string): void {
|
||||
this.marineHost.removeService(serviceId);
|
||||
async removeMarineService(serviceId: string): Promise<void> {
|
||||
await this.marineHost.removeService(serviceId);
|
||||
}
|
||||
|
||||
// internal api
|
||||
@ -385,7 +381,8 @@ export abstract class FluencePeer {
|
||||
|
||||
log_particle.debug('id %s. sending particle to interpreter', item.particle.id);
|
||||
log_particle.trace('id %s. prevData: %a', item.particle.id, prevData);
|
||||
const avmCallResult = await this.avmRunner.run(
|
||||
|
||||
const args = serializeAvmArgs(
|
||||
{
|
||||
initPeerId: item.particle.initPeerId,
|
||||
currentPeerId: this.keyPair.getPeerId(),
|
||||
@ -401,6 +398,14 @@ export abstract class FluencePeer {
|
||||
item.callResults,
|
||||
);
|
||||
|
||||
let avmCallResult: InterpreterResult | Error;
|
||||
try {
|
||||
const res = await this.marineHost.callService('avm', 'invoke', args, defaultCallParameters);
|
||||
avmCallResult = deserializeAvmResult(res);
|
||||
} catch (e) {
|
||||
avmCallResult = e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
||||
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) {
|
||||
const newData = Buffer.from(avmCallResult.data);
|
||||
prevData = newData;
|
||||
@ -521,7 +526,7 @@ export abstract class FluencePeer {
|
||||
const particleId = req.particleContext.particleId;
|
||||
log_particle.trace('id %s. executing call service handler %j', particleId, req);
|
||||
|
||||
if (this.marineHost && this.marineHost.hasService(req.serviceId)) {
|
||||
if (this.marineHost && await this.marineHost.hasService(req.serviceId)) {
|
||||
// TODO build correct CallParameters instead of default ones
|
||||
const result = await this.marineHost.callService(req.serviceId, req.fnName, req.args, defaultCallParameters);
|
||||
|
@ -64,9 +64,8 @@ describe('KeyPair tests', () => {
|
||||
|
||||
// act
|
||||
const res = await keyPair.signBytes(testData);
|
||||
|
||||
// assert
|
||||
expect(res).toStrictEqual(testDataSig);
|
||||
expect(new Uint8Array(res)).toStrictEqual(testDataSig);
|
||||
});
|
||||
|
||||
it('verify', async function () {
|
@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { PeerId } from '@libp2p/interface-peer-id';
|
||||
import type { PeerId } from '@libp2p/interface/peer-id';
|
||||
import { generateKeyPairFromSeed, generateKeyPair } from '@libp2p/crypto/keys';
|
||||
import { createFromPrivKey } from '@libp2p/peer-id-factory';
|
||||
import type { PrivateKey } from '@libp2p/interface-keys';
|
||||
import type { PrivateKey } from '@libp2p/interface/keys';
|
||||
import { toUint8Array } from 'js-base64';
|
||||
import * as bs58 from 'bs58';
|
||||
import { KeyPairOptions } from '@fluencelabs/interfaces';
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
import { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
|
||||
import { IStartable, JSONArray, JSONObject, CallParameters } from '../util/commonTypes.js';
|
||||
import { Buffer } from 'buffer';
|
||||
// @ts-ignore
|
||||
import type { WorkerImplementation } from 'threads/dist/types/master';
|
||||
|
||||
@ -26,17 +25,17 @@ export interface IMarineHost extends IStartable {
|
||||
/**
|
||||
* Creates marine service from the given module and service id
|
||||
*/
|
||||
createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise<void>;
|
||||
createService(serviceModule: ArrayBuffer | SharedArrayBuffer, serviceId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes marine service with the given service id
|
||||
*/
|
||||
removeService(serviceId: string): void;
|
||||
removeService(serviceId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns true if any service with the specified service id is registered
|
||||
*/
|
||||
hasService(serviceId: string): boolean;
|
||||
hasService(serviceId: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Calls the specified function of the specified service with the given arguments
|
||||
@ -75,7 +74,7 @@ export interface IValueLoader<T> {
|
||||
/**
|
||||
* Interface for something which can load wasm files
|
||||
*/
|
||||
export interface IWasmLoader extends IValueLoader<SharedArrayBuffer | Buffer>, IStartable {}
|
||||
export interface IWasmLoader extends IValueLoader<ArrayBuffer | SharedArrayBuffer>, IStartable {}
|
||||
|
||||
/**
|
||||
* Interface for something which can thread.js based worker
|
@ -21,6 +21,6 @@ import { LazyLoader } from '../interfaces.js';
|
||||
|
||||
export class WorkerLoader extends LazyLoader<WorkerImplementation> {
|
||||
constructor() {
|
||||
super(() => new Worker('./'));
|
||||
super(() => new Worker('../../../node_modules/@fluencelabs/marine-worker/dist/node/marine-worker.umd.cjs'));
|
||||
}
|
||||
}
|
@ -16,43 +16,50 @@
|
||||
|
||||
import type { JSONArray, JSONObject, CallParameters } from '@fluencelabs/marine-js/dist/types';
|
||||
import { LogFunction, logLevelToEnv } from '@fluencelabs/marine-js/dist/types';
|
||||
import type { MarineBackgroundInterface } from '../worker-script/index.js';
|
||||
import type { MarineBackgroundInterface } from '@fluencelabs/marine-worker';
|
||||
// @ts-ignore
|
||||
import { spawn, Thread } from 'threads';
|
||||
// @ts-ignore
|
||||
import type { ModuleThread } from 'threads';
|
||||
import { Buffer } from 'buffer';
|
||||
import { ModuleThread, spawn, Thread } from 'threads';
|
||||
|
||||
import { MarineLogger, marineLogger } from '../../util/logger.js';
|
||||
import { IMarineHost, IWasmLoader, IWorkerLoader } from '../interfaces.js';
|
||||
|
||||
export class MarineBackgroundRunner implements IMarineHost {
|
||||
private marineServices = new Set<string>();
|
||||
private workerThread?: ModuleThread<MarineBackgroundInterface>;
|
||||
private workerThread?: MarineBackgroundInterface;
|
||||
|
||||
private loggers: Map<string, MarineLogger> = new Map();
|
||||
private loggers = new Map<string, MarineLogger>();
|
||||
|
||||
constructor(private workerLoader: IWorkerLoader, private controlModuleLoader: IWasmLoader) {}
|
||||
constructor(private workerLoader: IWorkerLoader, private controlModuleLoader: IWasmLoader, private avmWasmLoader: IWasmLoader) {}
|
||||
|
||||
hasService(serviceId: string): boolean {
|
||||
return this.marineServices.has(serviceId);
|
||||
async hasService(serviceId: string) {
|
||||
if (!this.workerThread) {
|
||||
throw new Error('Worker is not initialized');
|
||||
}
|
||||
|
||||
return this.workerThread.hasService(serviceId);
|
||||
}
|
||||
|
||||
removeService(serviceId: string): void {
|
||||
this.marineServices.delete(serviceId);
|
||||
async removeService(serviceId: string) {
|
||||
if (!this.workerThread) {
|
||||
throw new Error('Worker is not initialized');
|
||||
}
|
||||
|
||||
await this.workerThread.removeService(serviceId);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.workerThread) {
|
||||
return;
|
||||
throw new Error('Worker thread already initialized');
|
||||
}
|
||||
|
||||
this.marineServices = new Set();
|
||||
await this.workerLoader.start();
|
||||
|
||||
await this.controlModuleLoader.start();
|
||||
const worker = this.workerLoader.getValue();
|
||||
const wasm = this.controlModuleLoader.getValue();
|
||||
this.workerThread = await spawn<MarineBackgroundInterface>(worker, { timeout: 99999999 });
|
||||
|
||||
await this.avmWasmLoader.start();
|
||||
|
||||
await this.workerLoader.start();
|
||||
const worker = this.workerLoader.getValue();
|
||||
|
||||
const workerThread = await spawn<MarineBackgroundInterface>(worker);
|
||||
const logfn: LogFunction = (message) => {
|
||||
const serviceLogger = this.loggers.get(message.service);
|
||||
if (!serviceLogger) {
|
||||
@ -60,13 +67,15 @@ export class MarineBackgroundRunner implements IMarineHost {
|
||||
}
|
||||
serviceLogger[message.level](message.message);
|
||||
};
|
||||
this.workerThread.onLogMessage().subscribe(logfn);
|
||||
await this.workerThread.init(wasm);
|
||||
workerThread.onLogMessage().subscribe(logfn);
|
||||
await workerThread.init(wasm);
|
||||
this.workerThread = workerThread;
|
||||
await this.createService(this.avmWasmLoader.getValue(), 'avm');
|
||||
}
|
||||
|
||||
async createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
|
||||
async createService(serviceModule: ArrayBuffer | SharedArrayBuffer, serviceId: string): Promise<void> {
|
||||
if (!this.workerThread) {
|
||||
throw 'Worker is not initialized';
|
||||
throw new Error('Worker is not initialized');
|
||||
}
|
||||
|
||||
// The logging level is controlled by the environment variable passed to enable debug logs.
|
||||
@ -74,10 +83,9 @@ export class MarineBackgroundRunner implements IMarineHost {
|
||||
const env = logLevelToEnv('trace');
|
||||
this.loggers.set(serviceId, marineLogger(serviceId));
|
||||
await this.workerThread.createService(serviceModule, serviceId, env);
|
||||
this.marineServices.add(serviceId);
|
||||
}
|
||||
|
||||
callService(
|
||||
async callService(
|
||||
serviceId: string,
|
||||
functionName: string,
|
||||
args: JSONArray | JSONObject,
|
||||
@ -94,8 +102,7 @@ export class MarineBackgroundRunner implements IMarineHost {
|
||||
if (!this.workerThread) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.marineServices.clear();
|
||||
|
||||
await this.workerThread.terminate();
|
||||
await Thread.terminate(this.workerThread);
|
||||
}
|
@ -18,8 +18,9 @@ import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces';
|
||||
import { defaultGuard } from './SingleModuleSrv.js';
|
||||
import { NodeUtilsDef, registerNodeUtils } from './_aqua/node-utils.js';
|
||||
import { SecurityGuard } from './securityGuard.js';
|
||||
import { readFile } from 'fs/promises';
|
||||
import * as fs from 'fs';
|
||||
import { FluencePeer } from '../jsPeer/FluencePeer.js';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export class NodeUtils implements NodeUtilsDef {
|
||||
constructor(private peer: FluencePeer) {
|
||||
@ -39,10 +40,18 @@ export class NodeUtils implements NodeUtilsDef {
|
||||
|
||||
try {
|
||||
// Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't
|
||||
const data: any = await readFile(path);
|
||||
const data = await new Promise<Buffer>((resolve, reject) => {
|
||||
fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
content: data,
|
||||
content: data as unknown as string,
|
||||
error: null,
|
||||
};
|
||||
} catch (err: any) {
|
@ -70,7 +70,7 @@ export class Srv implements SrvDef {
|
||||
|
||||
securityGuard_remove: SecurityGuard<'service_id'>;
|
||||
|
||||
remove(service_id: string, callParams: CallParams<'service_id'>) {
|
||||
async remove(service_id: string, callParams: CallParams<'service_id'>) {
|
||||
if (!this.securityGuard_remove(callParams)) {
|
||||
return {
|
||||
success: false,
|
||||
@ -86,7 +86,7 @@ export class Srv implements SrvDef {
|
||||
};
|
||||
}
|
||||
|
||||
this.peer.removeMarineService(service_id);
|
||||
await this.peer.removeMarineService(service_id);
|
||||
this.services.delete(service_id);
|
||||
|
||||
return {
|
80
packages/core/js-client/src/util/loadClient.ts
Normal file
80
packages/core/js-client/src/util/loadClient.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 type {
|
||||
CallAquaFunctionType,
|
||||
ClientConfig,
|
||||
IFluenceClient,
|
||||
RegisterServiceType,
|
||||
RelayOptions,
|
||||
} from '@fluencelabs/interfaces';
|
||||
|
||||
type PublicFluenceInterface = {
|
||||
defaultClient: IFluenceClient | undefined;
|
||||
clientFactory: (relay: RelayOptions, config?: ClientConfig) => Promise<IFluenceClient>;
|
||||
callAquaFunction: CallAquaFunctionType;
|
||||
registerService: RegisterServiceType;
|
||||
};
|
||||
|
||||
export const getFluenceInterfaceFromGlobalThis = (): PublicFluenceInterface | undefined => {
|
||||
// @ts-ignore
|
||||
return globalThis.fluence;
|
||||
};
|
||||
|
||||
// TODO: fix link DXJ-271
|
||||
const REJECT_MESSAGE = `Could not load Fluence JS Client library.
|
||||
If you are using Node.js that probably means that you forgot in install or import the @fluencelabs/js-client.node package.
|
||||
If you are using a browser, then you probably forgot to add the <script> tag to your HTML.
|
||||
Please refer to the documentation page for more details: https://fluence.dev/docs/build/js-client/installation
|
||||
`;
|
||||
|
||||
// Let's assume that if the library has not been loaded in 5 seconds, then the user has forgotten to add the script tag
|
||||
const POLL_PEER_TIMEOUT = 5000;
|
||||
|
||||
// The script might be cached so need to try loading it ASAP, thus short interval
|
||||
const POLL_PEER_INTERVAL = 100;
|
||||
|
||||
/**
|
||||
* Wait until the js client script it loaded and return the default peer from globalThis
|
||||
*/
|
||||
export const getFluenceInterface = (): Promise<PublicFluenceInterface> => {
|
||||
// If the script is already loaded, then return the value immediately
|
||||
const optimisticResult = getFluenceInterfaceFromGlobalThis();
|
||||
if (optimisticResult) {
|
||||
return Promise.resolve(optimisticResult);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// This function is internal
|
||||
// Make it sure that would be zero way for unnecessary types
|
||||
// to break out into the public API
|
||||
let interval: any;
|
||||
let hits = POLL_PEER_TIMEOUT / POLL_PEER_INTERVAL;
|
||||
interval = setInterval(() => {
|
||||
if (hits === 0) {
|
||||
clearInterval(interval);
|
||||
reject(REJECT_MESSAGE);
|
||||
}
|
||||
|
||||
let res = getFluenceInterfaceFromGlobalThis();
|
||||
if (res) {
|
||||
clearInterval(interval);
|
||||
resolve(res);
|
||||
}
|
||||
hits--;
|
||||
}, POLL_PEER_INTERVAL);
|
||||
});
|
||||
};
|
@ -23,14 +23,13 @@ import { ClientConfig, IFluenceClient, RelayOptions, ServiceDef } from '@fluence
|
||||
import { callAquaFunction } from '../compilerSupport/callFunction.js';
|
||||
|
||||
import { MarineBackgroundRunner } from '../marine/worker/index.js';
|
||||
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
|
||||
import { WorkerLoader } from '../marine/worker-script/workerLoader.js';
|
||||
import { KeyPair } from '../keypair/index.js';
|
||||
import { Subject, Subscribable } from 'rxjs';
|
||||
import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js';
|
||||
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
|
||||
import { ClientPeer, makeClientPeerConfig } from '../clientPeer/ClientPeer.js';
|
||||
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
|
||||
import { WasmLoaderFromNpm, WorkerLoaderFromNpm } from '../marine/deps-loader/node.js';
|
||||
import { IConnection } from '../connection/interfaces.js';
|
||||
|
||||
export const registerHandlersHelper = (
|
||||
@ -99,10 +98,9 @@ export class TestPeer extends FluencePeer {
|
||||
const workerLoader = new WorkerLoader();
|
||||
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
|
||||
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, avmModuleLoader);
|
||||
const jsHost = new JsServiceHost();
|
||||
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
|
||||
super(DEFAULT_CONFIG, keyPair, marine, jsHost, avm, connection);
|
||||
super(DEFAULT_CONFIG, keyPair, marine, jsHost, connection);
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,10 +128,9 @@ export const withClient = async (
|
||||
const workerLoader = new WorkerLoader();
|
||||
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
|
||||
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
|
||||
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
|
||||
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader, avmModuleLoader);
|
||||
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config);
|
||||
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm);
|
||||
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine);
|
||||
try {
|
||||
await client.connect();
|
||||
await action(client);
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
"types": ["vite/client"],
|
||||
"outDir": "dist/types",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"esModuleInterop": true
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
import { copyFileSync } from 'fs';
|
||||
|
||||
copyFileSync('./dist/marine/worker-script/index.js', './src/marine/worker-script/index.js');
|
@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "@fluencelabs/js-peer",
|
||||
"version": "0.9.1",
|
||||
"description": "TypeScript implementation of Fluence Peer",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "node ./copy-worker-script-workaround.mjs && vitest --threads false run"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fluencelabs/interfaces": "0.8.1",
|
||||
"@fluencelabs/avm": "0.43.1",
|
||||
"@fluencelabs/marine-js": "0.7.2",
|
||||
"multiformats": "11.0.1",
|
||||
"debug": "4.3.4",
|
||||
"async": "3.2.4",
|
||||
"bs58": "5.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"@libp2p/peer-id": "2.0.1",
|
||||
"rxjs": "7.5.5",
|
||||
"ts-pattern": "3.3.3",
|
||||
"uuid": "8.3.2",
|
||||
"threads": "1.7.0",
|
||||
"@libp2p/crypto": "1.0.8",
|
||||
"@libp2p/peer-id-factory": "2.0.1",
|
||||
"@libp2p/interface-peer-id": "2.0.1",
|
||||
"@libp2p/interface-keys": "1.0.7",
|
||||
"js-base64": "3.7.5",
|
||||
"it-length-prefixed": "8.0.4",
|
||||
"it-pipe": "2.0.5",
|
||||
"it-map": "2.0.0",
|
||||
"uint8arrays": "4.0.3",
|
||||
"@chainsafe/libp2p-noise": "11.0.0",
|
||||
"libp2p": "0.42.2",
|
||||
"@libp2p/interfaces": "3.3.1",
|
||||
"@libp2p/interface-connection": "3.0.8",
|
||||
"@libp2p/mplex": "7.1.1",
|
||||
"@libp2p/websockets": "5.0.3",
|
||||
"@multiformats/multiaddr": "11.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fluencelabs/aqua-api": "0.9.3",
|
||||
"@fluencelabs/aqua-lib": "0.6.0",
|
||||
"@fluencelabs/fluence-network-environment": "1.0.13",
|
||||
"@types/bs58": "4.0.1",
|
||||
"@types/uuid": "8.3.2",
|
||||
"@types/debug": "4.1.7",
|
||||
"vitest": "0.29.7"
|
||||
}
|
||||
}
|
@ -1,51 +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 type { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
|
||||
import { deserializeAvmResult, serializeAvmArgs } from '@fluencelabs/avm';
|
||||
import { IAvmRunner, IMarineHost, IWasmLoader } from '../marine/interfaces.js';
|
||||
import { defaultCallParameters } from "@fluencelabs/marine-js/dist/types"
|
||||
|
||||
export class MarineBasedAvmRunner implements IAvmRunner {
|
||||
constructor(private marine: IMarineHost, private avmWasmLoader: IWasmLoader) {}
|
||||
|
||||
async run(
|
||||
runParams: RunParameters,
|
||||
air: string,
|
||||
prevData: Uint8Array,
|
||||
data: Uint8Array,
|
||||
callResults: CallResultsArray,
|
||||
): Promise<InterpreterResult | Error> {
|
||||
const args = serializeAvmArgs(runParams, air, prevData, data, callResults);
|
||||
|
||||
let avmCallResult: InterpreterResult | Error;
|
||||
try {
|
||||
const res = await this.marine.callService('avm', 'invoke', args, defaultCallParameters);
|
||||
avmCallResult = deserializeAvmResult(res);
|
||||
} catch (e) {
|
||||
avmCallResult = e instanceof Error ? e : new Error((e as any).toString());
|
||||
}
|
||||
|
||||
return avmCallResult;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.marine.start();
|
||||
await this.avmWasmLoader.start();
|
||||
await this.marine.createService(this.avmWasmLoader.getValue(), 'avm');
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {}
|
||||
}
|
68
packages/core/marine-worker/build.ts
Normal file
68
packages/core/marine-worker/build.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { build, defineConfig, InlineConfig, PluginOption, UserConfig, UserConfigExport } from 'vite'
|
||||
import { dirname, resolve } from 'path';
|
||||
import { builtinModules, createRequire } from 'module';
|
||||
import inject from '@rollup/plugin-inject';
|
||||
// @ts-ignore
|
||||
import merge from 'deepmerge';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { replaceCodePlugin } from 'vite-plugin-replace';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuildShim = require.resolve('node-stdlib-browser/helpers/esbuild/shim');
|
||||
|
||||
const commonConfig = defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(dirname(fileURLToPath(import.meta.url)), 'src/index.ts'),
|
||||
name: 'MarineWorker'
|
||||
},
|
||||
}
|
||||
}) as UserConfig;
|
||||
|
||||
const browserConfig: InlineConfig = await merge(commonConfig, defineConfig({
|
||||
build: {
|
||||
outDir: 'dist/browser',
|
||||
},
|
||||
plugins: [{
|
||||
// @ts-ignore
|
||||
...inject({
|
||||
global: [esbuildShim, 'global'],
|
||||
process: [esbuildShim, 'process'],
|
||||
Buffer: [esbuildShim, 'Buffer']
|
||||
}), enforce: 'post'
|
||||
} as PluginOption],
|
||||
}) as UserConfig);
|
||||
|
||||
const nodeConfig: InlineConfig = await merge(commonConfig, defineConfig({
|
||||
build: {
|
||||
target: 'es2022',
|
||||
outDir: 'dist/node',
|
||||
rollupOptions: {
|
||||
external: [...builtinModules],
|
||||
plugins: [
|
||||
// @ts-ignore
|
||||
inject({
|
||||
self: 'global',
|
||||
'WorkerScope': ['worker_threads', '*'],
|
||||
'Worker': ['worker_threads', 'Worker'],
|
||||
'isMainThread': ['worker_threads', 'isMainThread'],
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
replaceCodePlugin({
|
||||
replacements: [
|
||||
{ from: 'eval("require")("worker_threads")', to: 'WorkerScope' },
|
||||
{ from: 'eval("require")("worker_threads")', to: 'WorkerScope' },
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
browserField: false,
|
||||
}
|
||||
}) as UserConfig);
|
||||
|
||||
|
||||
await build(browserConfig!);
|
||||
await build(nodeConfig!);
|
51
packages/core/marine-worker/package.json
Normal file
51
packages/core/marine-worker/package.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@fluencelabs/marine-worker",
|
||||
"version": "0.2.10",
|
||||
"description": "Marine worker",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/node/marine-worker.umd.cjs",
|
||||
"unpkg": "./dist/browser/marine-worker.umd.cjs",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"node": "./dist/node/marine-worker.umd.cjs",
|
||||
"default": "./dist/browser/marine-worker.umd.cjs"
|
||||
},
|
||||
"./dist/marine-worker.js": {
|
||||
"node": "./dist/node/marine-worker.js",
|
||||
"default": "./dist/browser/marine-worker.js"
|
||||
},
|
||||
"./dist/marine-worker.umd.cjs": {
|
||||
"node": "./dist/node/marine-worker.umd.cjs",
|
||||
"default": "./dist/browser/marine-worker.umd.cjs"
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc --emitDeclarationOnly && node --loader ts-node/esm build.ts"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [],
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@types/node": "20.4.5",
|
||||
"deepmerge": "4.3.1",
|
||||
"node-stdlib-browser": "1.2.0",
|
||||
"typescript": "5.1.6",
|
||||
"vite": "4.0.4",
|
||||
"vite-plugin-dts": "3.4.0",
|
||||
"vite-plugin-replace": "0.1.1",
|
||||
"vitest": "0.29.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluencelabs/avm": "0.43.1",
|
||||
"@fluencelabs/marine-js": "0.7.2",
|
||||
"observable-fns": "0.6.1",
|
||||
"threads": "1.7.0"
|
||||
}
|
||||
}
|
@ -17,14 +17,9 @@
|
||||
import { MarineService } from '@fluencelabs/marine-js/dist/MarineService';
|
||||
import type { Env, MarineModuleConfig, MarineServiceConfig, ModuleDescriptor } from '@fluencelabs/marine-js/dist/config'
|
||||
import type { JSONArray, JSONObject, LogMessage, CallParameters } from '@fluencelabs/marine-js/dist/types';
|
||||
import { Buffer } from 'buffer';
|
||||
// @ts-ignore
|
||||
import { Observable, Subject } from 'threads/observable';
|
||||
// @ts-ignore
|
||||
import { expose } from 'threads/worker';
|
||||
|
||||
let marineServices = new Map<string, MarineService>();
|
||||
let controlModule: WebAssembly.Module | undefined;
|
||||
import { Observable, Subject } from 'observable-fns';
|
||||
// @ts-ignore no types provided for package
|
||||
import { expose } from 'threads';
|
||||
|
||||
const createSimpleModuleDescriptor = (name: string, envs?: Env): ModuleDescriptor => {
|
||||
return {
|
||||
@ -46,19 +41,17 @@ const createSimpleMarineService = (name: string, env? : Env): MarineServiceConfi
|
||||
}
|
||||
}
|
||||
|
||||
let marineServices = new Map<string, MarineService>();
|
||||
let controlModule: WebAssembly.Module | undefined;
|
||||
const onLogMessage = new Subject<LogMessage>();
|
||||
|
||||
const asArray = (buf: SharedArrayBuffer | Buffer) => {
|
||||
return new Uint8Array(buf);
|
||||
};
|
||||
|
||||
const toExpose = {
|
||||
init: async (controlModuleWasm: SharedArrayBuffer | Buffer): Promise<void> => {
|
||||
controlModule = await WebAssembly.compile(asArray(controlModuleWasm));
|
||||
init: async (controlModuleWasm: ArrayBuffer | SharedArrayBuffer) => {
|
||||
controlModule = new WebAssembly.Module(new Uint8Array(controlModuleWasm));
|
||||
},
|
||||
|
||||
createService: async (
|
||||
wasm: SharedArrayBuffer | Buffer,
|
||||
wasm: ArrayBuffer | SharedArrayBuffer,
|
||||
serviceId: string,
|
||||
envs?: Env,
|
||||
): Promise<void> => {
|
||||
@ -66,6 +59,10 @@ const toExpose = {
|
||||
throw new Error('MarineJS is not initialized. To initialize call `init` function');
|
||||
}
|
||||
|
||||
if (marineServices.has(serviceId)) {
|
||||
throw new Error(`Service with name ${serviceId} already registered`);
|
||||
}
|
||||
|
||||
const marineConfig = createSimpleMarineService(serviceId, envs);
|
||||
const modules = {[serviceId]: new Uint8Array(wasm)}
|
||||
const srv = new MarineService(
|
||||
@ -80,14 +77,28 @@ const toExpose = {
|
||||
marineServices.set(serviceId, srv);
|
||||
},
|
||||
|
||||
terminate: () => {
|
||||
hasService: async (serviceId: string) => {
|
||||
return marineServices.has(serviceId);
|
||||
},
|
||||
|
||||
removeService: async (serviceId: string) => {
|
||||
if (serviceId === 'avm') {
|
||||
throw new Error('Cannot remove \'avm\' service');
|
||||
}
|
||||
|
||||
marineServices.get(serviceId)?.terminate();
|
||||
return marineServices.delete(serviceId);
|
||||
},
|
||||
|
||||
terminate: async () => {
|
||||
marineServices.forEach((val, key) => {
|
||||
val.terminate();
|
||||
});
|
||||
marineServices.clear();
|
||||
onLogMessage.complete();
|
||||
},
|
||||
|
||||
callService: (serviceId: string, functionName: string, args: JSONArray | JSONObject, callParams: CallParameters): unknown => {
|
||||
callService: async (serviceId: string, functionName: string, args: JSONArray | JSONObject, callParams: any) => {
|
||||
const srv = marineServices.get(serviceId);
|
||||
if (!srv) {
|
||||
throw new Error(`service with id=${serviceId} not found`);
|
||||
@ -101,6 +112,14 @@ const toExpose = {
|
||||
},
|
||||
};
|
||||
|
||||
export type MarineBackgroundInterface = typeof toExpose;
|
||||
type ExposedInterface<T extends {[key: string]: (...args: any[]) => unknown}> = {
|
||||
[P in keyof T]: ReturnType<T[P]> extends Observable<unknown>
|
||||
? T[P]
|
||||
: ReturnType<T[P]> extends Promise<unknown>
|
||||
? T[P]
|
||||
: (...args: Parameters<T[P]>) => Promise<ReturnType<T[P]>>
|
||||
};
|
||||
|
||||
export type MarineBackgroundInterface = ExposedInterface<typeof toExpose>;
|
||||
|
||||
expose(toExpose);
|
8
packages/core/marine-worker/tsconfig.json
Normal file
8
packages/core/marine-worker/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Reference in New Issue
Block a user