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:
Akim
2023-08-25 00:15:49 +07:00
committed by GitHub
parent 2d2f5591cf
commit 97c24918d8
130 changed files with 3350 additions and 3119 deletions

View 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));

View 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"
}
}

View 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,
};
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}

View File

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

View 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/`));
}

View 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);
}
}

View 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'
}
});
}

View 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';

View File

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

View File

@ -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 () {

View File

@ -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';

View File

@ -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

View File

@ -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'));
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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 {

View 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);
});
};

View File

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

View File

@ -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
}

View File

@ -1,3 +0,0 @@
import { copyFileSync } from 'fs';
copyFileSync('./dist/marine/worker-script/index.js', './src/marine/worker-script/index.js');

View File

@ -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"
}
}

View File

@ -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> {}
}

View 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!);

View 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"
}
}

View File

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

View File

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