feat: Support instance context [fixes DXJ-541] (#392)

* Support context

* Update doc

* Refactor

* Cover by doc

* Fix lint
This commit is contained in:
Akim 2023-12-06 22:03:20 +07:00 committed by GitHub
parent 44eb1493b3
commit 1578b791ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 220 additions and 325 deletions

View File

@ -164,6 +164,18 @@ localStorage.debug = "fluence:*";
In Chromium-based web browsers (e.g. Brave, Chrome, and Electron), the JavaScript console will be default—only to show
messages logged by debug if the "Verbose" log level is enabled.
## Low level usage
JS client also has an API for low level interaction with AVM and Marine JS.
It could be handy in advanced scenarios when a user fetches AIR dynamically or generates AIR without default Aqua compiler.
`callAquaFunction` Allows to call aqua function without schema.
`registerService` Gives an ability to register service without schema. Passed `service` could be
- Plain object. In this case all function properties will be registered as AIR service functions.
- Class instance. All class methods without inherited ones will be registered as AIR service functions.
## Development
To hack on the Fluence JS Client itself, please refer to the [development page](./DEVELOPING.md).

View File

@ -0,0 +1,16 @@
service Calc("calc"):
add(n: f32)
subtract(n: f32)
multiply(n: f32)
divide(n: f32)
reset()
getResult() -> f32
func demoCalc() -> f32:
Calc.add(10)
Calc.multiply(5)
Calc.subtract(8)
Calc.divide(6)
res <- Calc.getResult()
<- res

View File

@ -0,0 +1,84 @@
/**
* 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 { fileURLToPath } from "url";
import { compileFromPath } from "@fluencelabs/aqua-api";
import { ServiceDef } from "@fluencelabs/interfaces";
import { describe, expect, it } from "vitest";
import { v5_registerService } from "./api.js";
import { callAquaFunction } from "./compilerSupport/callFunction.js";
import { withPeer } from "./util/testUtils.js";
class CalcParent {
protected _state: number = 0;
add(n: number) {
this._state += n;
}
subtract(n: number) {
this._state -= n;
}
}
class Calc extends CalcParent {
multiply(n: number) {
this._state *= n;
}
divide(n: number) {
this._state /= n;
}
reset() {
this._state = 0;
}
getResult() {
return this._state;
}
}
describe("User API methods", () => {
it("registers user class service and calls own and inherited methods correctly", async () => {
await withPeer(async (peer) => {
const calcService: Record<never, unknown> = new Calc();
const { functions, services } = await compileFromPath({
filePath: fileURLToPath(new URL("../aqua/calc.aqua", import.meta.url)),
});
const typedServices: Record<string, ServiceDef> = services;
const { script } = functions["demoCalc"];
v5_registerService([peer, "calc", calcService], {
defaultServiceId: "calc",
functions: typedServices["Calc"].functions,
});
const res = await callAquaFunction({
args: {},
peer,
script,
});
expect(res).toBe(7);
});
});
});

View File

@ -202,10 +202,16 @@ export const v5_registerService = (
// Schema for every function in service
const serviceSchema = def.functions.tag === "nil" ? {} : def.functions.fields;
// Wrapping service impl to convert their args ts -> aqua and backwards
// Wrapping service functions, selecting only those listed in schema, to convert their args js -> aqua and backwards
const wrappedServiceImpl = Object.fromEntries(
Object.entries(serviceImpl).map(([name, func]) => {
return [name, wrapJsFunction(func, serviceSchema[name])];
Object.keys(serviceSchema).map((schemaKey) => {
return [
schemaKey,
wrapJsFunction(
serviceImpl[schemaKey].bind(serviceImpl),
serviceSchema[schemaKey],
),
] as const;
}),
);

View File

@ -46,7 +46,7 @@ const log = logger("aqua");
export type CallAquaFunctionArgs = {
script: string;
config: CallAquaFunctionConfig | undefined;
config?: CallAquaFunctionConfig | undefined;
peer: FluencePeer;
args: { [key: string]: JSONValue | ArgCallbackFunction };
};

View File

@ -28,27 +28,22 @@ interface RegisterServiceArgs {
service: ServiceImpl;
}
// This function iterates on plain object or class instance functions ignoring inherited functions and prototype chain.
const findAllPossibleRegisteredServiceFunctions = (
service: ServiceImpl,
): Set<string> => {
let prototype: Record<string, unknown> = service;
const serviceMethods = new Set<string>();
): Array<string> => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const prototype = Object.getPrototypeOf(service) as ServiceImpl;
do {
Object.getOwnPropertyNames(prototype)
.filter((prop) => {
return typeof prototype[prop] === "function" && prop !== "constructor";
})
.forEach((prop) => {
return serviceMethods.add(prop);
});
const isClassInstance = prototype.constructor !== Object;
// coercing 'any' type to 'Record' bcs object prototype is actually an object
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
prototype = Object.getPrototypeOf(prototype) as Record<string, unknown>;
} while (prototype.constructor !== Object);
if (isClassInstance) {
service = prototype;
}
return serviceMethods;
return Object.getOwnPropertyNames(service).filter((prop) => {
return typeof service[prop] === "function" && prop !== "constructor";
});
};
export const registerService = ({

View File

@ -14,13 +14,15 @@
* limitations under the License.
*/
import { it, describe, expect, beforeEach, afterEach } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DEFAULT_CONFIG, FluencePeer } from "../../jsPeer/FluencePeer.js";
import { ResultCodes } from "../../jsServiceHost/interfaces.js";
import { KeyPair } from "../../keypair/index.js";
import { loadMarineDeps } from "../../marine/loader.js";
import { MarineBackgroundRunner } from "../../marine/worker/index.js";
import { EphemeralNetworkClient } from "../client.js";
import { EphemeralNetwork, defaultConfig } from "../network.js";
import { defaultConfig, EphemeralNetwork } from "../network.js";
let en: EphemeralNetwork;
let client: FluencePeer;
@ -33,7 +35,11 @@ describe.skip("Ephemeral networks tests", () => {
await en.up();
const kp = await KeyPair.randomEd25519();
client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, en, relay);
const marineDeps = await loadMarineDeps("/");
const marine = new MarineBackgroundRunner(...marineDeps);
client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, marine, en, relay);
await client.start();
});

View File

@ -15,13 +15,11 @@
*/
import { PeerIdB58 } from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { KeyPair } from "../keypair/index.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { IMarineHost } from "../marine/interfaces.js";
import { EphemeralNetwork } from "./network.js";
@ -32,63 +30,12 @@ export class EphemeralNetworkClient extends FluencePeer {
constructor(
config: PeerConfig,
keyPair: KeyPair,
marine: IMarineHost,
network: EphemeralNetwork,
relay: PeerIdB58,
) {
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker in parallel with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
);
super(config, keyPair, marine, new JsServiceHost(), conn);
}
}

View File

@ -15,8 +15,6 @@
*/
import { PeerIdB58 } from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { Subject } from "rxjs";
import { IConnection } from "../connection/interfaces.js";
@ -24,6 +22,7 @@ import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { fromBase64Sk, KeyPair } from "../keypair/index.js";
import { IMarineHost } from "../marine/interfaces.js";
import { loadMarineDeps } from "../marine/loader.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { Particle } from "../particle/Particle.js";
import { logger } from "../util/logger.js";
@ -233,55 +232,8 @@ export class EphemeralNetwork {
const promises = this.config.peers.map(async (x) => {
const kp = await fromBase64Sk(x.sk);
const [marineJsWasm, avmWasm] = await Promise.all([
fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
}),
fetchResource("@fluencelabs/avm", "/dist/avm.wasm", "/").then((res) => {
return res.arrayBuffer();
}),
]);
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker in parallel with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
start(): Promise<void> {
return Promise.resolve(undefined);
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
start(): Promise<void> {
return Promise.resolve(undefined);
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
);
const marineDeps = await loadMarineDeps("/");
const marine = new MarineBackgroundRunner(...marineDeps);
const peerId = kp.getPeerId();

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { ZodError } from "zod";
import { ClientPeer, makeClientPeerConfig } from "./clientPeer/ClientPeer.js";
@ -28,6 +26,7 @@ import {
} from "./clientPeer/types.js";
import { callAquaFunction } from "./compilerSupport/callFunction.js";
import { registerService } from "./compilerSupport/registerService.js";
import { loadMarineDeps } from "./marine/loader.js";
import { MarineBackgroundRunner } from "./marine/worker/index.js";
const DEFAULT_CDN_URL = "https://unpkg.com";
@ -47,55 +46,8 @@ const createClient = async (
const CDNUrl = config.CDNUrl ?? DEFAULT_CDN_URL;
const [marineJsWasm, avmWasm] = await Promise.all([
fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
CDNUrl,
).then((res) => {
return res.arrayBuffer();
}),
fetchResource("@fluencelabs/avm", "/dist/avm.wasm", CDNUrl).then((res) => {
return res.arrayBuffer();
}),
]);
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker in parallel with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", CDNUrl);
},
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 marineDeps = await loadMarineDeps(CDNUrl);
const marine = new MarineBackgroundRunner(...marineDeps);
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(
relay,

View File

@ -16,7 +16,6 @@
import { JSONObject, JSONValue, JSONArray } from "@fluencelabs/interfaces";
import { CallParameters } from "@fluencelabs/marine-worker";
import type { Worker as WorkerImplementation } from "@fluencelabs/threads/master";
import { IStartable } from "../util/commonTypes.js";
@ -52,24 +51,3 @@ export interface IMarineHost extends IStartable {
callParams?: CallParameters,
): Promise<JSONValue>;
}
/**
* Interface for something which can hold a value
*/
export interface IValueLoader<T> {
getValue(): T;
}
/**
* Interface for something which can load wasm files
*/
export interface IWasmLoader
extends IValueLoader<ArrayBuffer | SharedArrayBuffer>,
IStartable {}
/**
* Interface for something which can thread.js based worker
*/
export interface IWorkerLoader
extends IValueLoader<WorkerImplementation | Promise<WorkerImplementation>>,
IStartable {}

View File

@ -0,0 +1,47 @@
/**
* 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 } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { Worker } from "@fluencelabs/threads/master";
type StrategyReturnType = [
marineJsWasm: ArrayBuffer,
avmWasm: ArrayBuffer,
worker: Worker,
];
export const loadMarineDeps = async (
CDNUrl: string,
): Promise<StrategyReturnType> => {
const [marineJsWasm, avmWasm] = await Promise.all([
fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
CDNUrl,
).then((res) => {
return res.arrayBuffer();
}),
fetchResource("@fluencelabs/avm", "/dist/avm.wasm", CDNUrl).then((res) => {
return res.arrayBuffer();
}),
]);
// TODO: load worker in parallel with avm and marine, test that it works
const worker = await getWorker("@fluencelabs/marine-worker", CDNUrl);
return [marineJsWasm, avmWasm, worker];
};

View File

@ -21,10 +21,15 @@ import type {
JSONValueNonNullable,
CallParameters,
} from "@fluencelabs/marine-worker";
import { ModuleThread, Thread, spawn } from "@fluencelabs/threads/master";
import {
ModuleThread,
Thread,
spawn,
Worker,
} from "@fluencelabs/threads/master";
import { MarineLogger, marineLogger } from "../../util/logger.js";
import { IMarineHost, IWasmLoader, IWorkerLoader } from "../interfaces.js";
import { IMarineHost } from "../interfaces.js";
export class MarineBackgroundRunner implements IMarineHost {
private workerThread?: ModuleThread<MarineBackgroundInterface>;
@ -32,9 +37,9 @@ export class MarineBackgroundRunner implements IMarineHost {
private loggers = new Map<string, MarineLogger>();
constructor(
private workerLoader: IWorkerLoader,
private controlModuleLoader: IWasmLoader,
private avmWasmLoader: IWasmLoader,
private marineJsWasm: ArrayBuffer,
private avmWasm: ArrayBuffer,
private worker: Worker,
) {}
async hasService(serviceId: string) {
@ -58,16 +63,8 @@ export class MarineBackgroundRunner implements IMarineHost {
throw new Error("Worker thread already initialized");
}
await this.controlModuleLoader.start();
const wasm = this.controlModuleLoader.getValue();
await this.avmWasmLoader.start();
await this.workerLoader.start();
const worker = await this.workerLoader.getValue();
const workerThread: ModuleThread<MarineBackgroundInterface> =
await spawn<MarineBackgroundInterface>(worker);
await spawn<MarineBackgroundInterface>(this.worker);
const logfn: LogFunction = (message) => {
const serviceLogger = this.loggers.get(message.service);
@ -80,9 +77,9 @@ export class MarineBackgroundRunner implements IMarineHost {
};
workerThread.onLogMessage().subscribe(logfn);
await workerThread.init(wasm);
await workerThread.init(this.marineJsWasm);
this.workerThread = workerThread;
await this.createService(this.avmWasmLoader.getValue(), "avm");
await this.createService(this.avmWasm, "avm");
}
async createService(

View File

@ -23,8 +23,6 @@ import {
JSONValue,
ServiceDef,
} from "@fluencelabs/interfaces";
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
import { Subject, Subscribable } from "rxjs";
import { ClientPeer, makeClientPeerConfig } from "../clientPeer/ClientPeer.js";
@ -40,6 +38,8 @@ import {
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
import { WrapFnIntoServiceCall } from "../jsServiceHost/serviceUtils.js";
import { KeyPair } from "../keypair/index.js";
import { IMarineHost } from "../marine/interfaces.js";
import { loadMarineDeps } from "../marine/loader.js";
import { MarineBackgroundRunner } from "../marine/worker/index.js";
import { Particle } from "../particle/Particle.js";
@ -146,61 +146,9 @@ class NoopConnection implements IConnection {
}
export class TestPeer extends FluencePeer {
constructor(keyPair: KeyPair, connection: IConnection) {
constructor(keyPair: KeyPair, connection: IConnection, marine: IMarineHost) {
const jsHost = new JsServiceHost();
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker in parallel with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
);
super(DEFAULT_CONFIG, keyPair, marine, jsHost, connection);
}
}
@ -208,7 +156,11 @@ export class TestPeer extends FluencePeer {
export const mkTestPeer = async () => {
const kp = await KeyPair.randomEd25519();
const conn = new NoopConnection();
return new TestPeer(kp, conn);
const marineDeps = await loadMarineDeps("/");
const marine = new MarineBackgroundRunner(...marineDeps);
return new TestPeer(kp, conn, marine);
};
export const withPeer = async (action: (p: FluencePeer) => Promise<void>) => {
@ -232,57 +184,8 @@ export const withClient = async (
config,
);
let marineJsWasm: ArrayBuffer;
let avmWasm: ArrayBuffer;
const marine = new MarineBackgroundRunner(
{
async getValue() {
// TODO: load worker in parallel with avm and marine, test that it works
return getWorker("@fluencelabs/marine-worker", "/");
},
start() {
return Promise.resolve(undefined);
},
stop() {
return Promise.resolve(undefined);
},
},
{
getValue() {
return marineJsWasm;
},
async start(): Promise<void> {
marineJsWasm = await fetchResource(
"@fluencelabs/marine-js",
"/dist/marine-js.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
{
getValue() {
return avmWasm;
},
async start(): Promise<void> {
avmWasm = await fetchResource(
"@fluencelabs/avm",
"/dist/avm.wasm",
"/",
).then((res) => {
return res.arrayBuffer();
});
},
stop(): Promise<void> {
return Promise.resolve(undefined);
},
},
);
const marineDeps = await loadMarineDeps("/");
const marine = new MarineBackgroundRunner(...marineDeps);
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine);