Skip to content

Commit 1578b79

Browse files
author
Akim
authored
feat: Support instance context [fixes DXJ-541] (#392)
* Support context * Update doc * Refactor * Cover by doc * Fix lint
1 parent 44eb149 commit 1578b79

File tree

14 files changed

+220
-325
lines changed

14 files changed

+220
-325
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ localStorage.debug = "fluence:*";
164164
In Chromium-based web browsers (e.g. Brave, Chrome, and Electron), the JavaScript console will be default—only to show
165165
messages logged by debug if the "Verbose" log level is enabled.
166166

167+
## Low level usage
168+
169+
JS client also has an API for low level interaction with AVM and Marine JS.
170+
It could be handy in advanced scenarios when a user fetches AIR dynamically or generates AIR without default Aqua compiler.
171+
172+
`callAquaFunction` Allows to call aqua function without schema.
173+
174+
`registerService` Gives an ability to register service without schema. Passed `service` could be
175+
176+
- Plain object. In this case all function properties will be registered as AIR service functions.
177+
- Class instance. All class methods without inherited ones will be registered as AIR service functions.
178+
167179
## Development
168180

169181
To hack on the Fluence JS Client itself, please refer to the [development page](./DEVELOPING.md).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
service Calc("calc"):
2+
add(n: f32)
3+
subtract(n: f32)
4+
multiply(n: f32)
5+
divide(n: f32)
6+
reset()
7+
getResult() -> f32
8+
9+
10+
func demoCalc() -> f32:
11+
Calc.add(10)
12+
Calc.multiply(5)
13+
Calc.subtract(8)
14+
Calc.divide(6)
15+
res <- Calc.getResult()
16+
<- res
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright 2023 Fluence Labs Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { fileURLToPath } from "url";
18+
19+
import { compileFromPath } from "@fluencelabs/aqua-api";
20+
import { ServiceDef } from "@fluencelabs/interfaces";
21+
import { describe, expect, it } from "vitest";
22+
23+
import { v5_registerService } from "./api.js";
24+
import { callAquaFunction } from "./compilerSupport/callFunction.js";
25+
import { withPeer } from "./util/testUtils.js";
26+
27+
class CalcParent {
28+
protected _state: number = 0;
29+
30+
add(n: number) {
31+
this._state += n;
32+
}
33+
34+
subtract(n: number) {
35+
this._state -= n;
36+
}
37+
}
38+
39+
class Calc extends CalcParent {
40+
multiply(n: number) {
41+
this._state *= n;
42+
}
43+
44+
divide(n: number) {
45+
this._state /= n;
46+
}
47+
48+
reset() {
49+
this._state = 0;
50+
}
51+
52+
getResult() {
53+
return this._state;
54+
}
55+
}
56+
57+
describe("User API methods", () => {
58+
it("registers user class service and calls own and inherited methods correctly", async () => {
59+
await withPeer(async (peer) => {
60+
const calcService: Record<never, unknown> = new Calc();
61+
62+
const { functions, services } = await compileFromPath({
63+
filePath: fileURLToPath(new URL("../aqua/calc.aqua", import.meta.url)),
64+
});
65+
66+
const typedServices: Record<string, ServiceDef> = services;
67+
68+
const { script } = functions["demoCalc"];
69+
70+
v5_registerService([peer, "calc", calcService], {
71+
defaultServiceId: "calc",
72+
functions: typedServices["Calc"].functions,
73+
});
74+
75+
const res = await callAquaFunction({
76+
args: {},
77+
peer,
78+
script,
79+
});
80+
81+
expect(res).toBe(7);
82+
});
83+
});
84+
});

packages/core/js-client/src/api.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,16 @@ export const v5_registerService = (
202202
// Schema for every function in service
203203
const serviceSchema = def.functions.tag === "nil" ? {} : def.functions.fields;
204204

205-
// Wrapping service impl to convert their args ts -> aqua and backwards
205+
// Wrapping service functions, selecting only those listed in schema, to convert their args js -> aqua and backwards
206206
const wrappedServiceImpl = Object.fromEntries(
207-
Object.entries(serviceImpl).map(([name, func]) => {
208-
return [name, wrapJsFunction(func, serviceSchema[name])];
207+
Object.keys(serviceSchema).map((schemaKey) => {
208+
return [
209+
schemaKey,
210+
wrapJsFunction(
211+
serviceImpl[schemaKey].bind(serviceImpl),
212+
serviceSchema[schemaKey],
213+
),
214+
] as const;
209215
}),
210216
);
211217

packages/core/js-client/src/compilerSupport/callFunction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const log = logger("aqua");
4646

4747
export type CallAquaFunctionArgs = {
4848
script: string;
49-
config: CallAquaFunctionConfig | undefined;
49+
config?: CallAquaFunctionConfig | undefined;
5050
peer: FluencePeer;
5151
args: { [key: string]: JSONValue | ArgCallbackFunction };
5252
};

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,22 @@ interface RegisterServiceArgs {
2828
service: ServiceImpl;
2929
}
3030

31+
// This function iterates on plain object or class instance functions ignoring inherited functions and prototype chain.
3132
const findAllPossibleRegisteredServiceFunctions = (
3233
service: ServiceImpl,
33-
): Set<string> => {
34-
let prototype: Record<string, unknown> = service;
35-
const serviceMethods = new Set<string>();
34+
): Array<string> => {
35+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
36+
const prototype = Object.getPrototypeOf(service) as ServiceImpl;
3637

37-
do {
38-
Object.getOwnPropertyNames(prototype)
39-
.filter((prop) => {
40-
return typeof prototype[prop] === "function" && prop !== "constructor";
41-
})
42-
.forEach((prop) => {
43-
return serviceMethods.add(prop);
44-
});
38+
const isClassInstance = prototype.constructor !== Object;
4539

46-
// coercing 'any' type to 'Record' bcs object prototype is actually an object
47-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
48-
prototype = Object.getPrototypeOf(prototype) as Record<string, unknown>;
49-
} while (prototype.constructor !== Object);
40+
if (isClassInstance) {
41+
service = prototype;
42+
}
5043

51-
return serviceMethods;
44+
return Object.getOwnPropertyNames(service).filter((prop) => {
45+
return typeof service[prop] === "function" && prop !== "constructor";
46+
});
5247
};
5348

5449
export const registerService = ({

packages/core/js-client/src/ephemeral/__test__/ephemeral.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { it, describe, expect, beforeEach, afterEach } from "vitest";
17+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
1818

1919
import { DEFAULT_CONFIG, FluencePeer } from "../../jsPeer/FluencePeer.js";
2020
import { ResultCodes } from "../../jsServiceHost/interfaces.js";
2121
import { KeyPair } from "../../keypair/index.js";
22+
import { loadMarineDeps } from "../../marine/loader.js";
23+
import { MarineBackgroundRunner } from "../../marine/worker/index.js";
2224
import { EphemeralNetworkClient } from "../client.js";
23-
import { EphemeralNetwork, defaultConfig } from "../network.js";
25+
import { defaultConfig, EphemeralNetwork } from "../network.js";
2426

2527
let en: EphemeralNetwork;
2628
let client: FluencePeer;
@@ -33,7 +35,11 @@ describe.skip("Ephemeral networks tests", () => {
3335
await en.up();
3436

3537
const kp = await KeyPair.randomEd25519();
36-
client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, en, relay);
38+
39+
const marineDeps = await loadMarineDeps("/");
40+
const marine = new MarineBackgroundRunner(...marineDeps);
41+
42+
client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, marine, en, relay);
3743
await client.start();
3844
});
3945

packages/core/js-client/src/ephemeral/client.ts

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@
1515
*/
1616

1717
import { PeerIdB58 } from "@fluencelabs/interfaces";
18-
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
19-
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
2018

2119
import { FluencePeer, PeerConfig } from "../jsPeer/FluencePeer.js";
2220
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
2321
import { KeyPair } from "../keypair/index.js";
24-
import { MarineBackgroundRunner } from "../marine/worker/index.js";
22+
import { IMarineHost } from "../marine/interfaces.js";
2523

2624
import { EphemeralNetwork } from "./network.js";
2725

@@ -32,63 +30,12 @@ export class EphemeralNetworkClient extends FluencePeer {
3230
constructor(
3331
config: PeerConfig,
3432
keyPair: KeyPair,
33+
marine: IMarineHost,
3534
network: EphemeralNetwork,
3635
relay: PeerIdB58,
3736
) {
3837
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
3938

40-
let marineJsWasm: ArrayBuffer;
41-
let avmWasm: ArrayBuffer;
42-
43-
const marine = new MarineBackgroundRunner(
44-
{
45-
async getValue() {
46-
// TODO: load worker in parallel with avm and marine, test that it works
47-
return getWorker("@fluencelabs/marine-worker", "/");
48-
},
49-
start() {
50-
return Promise.resolve(undefined);
51-
},
52-
stop() {
53-
return Promise.resolve(undefined);
54-
},
55-
},
56-
{
57-
getValue() {
58-
return marineJsWasm;
59-
},
60-
async start(): Promise<void> {
61-
marineJsWasm = await fetchResource(
62-
"@fluencelabs/marine-js",
63-
"/dist/marine-js.wasm",
64-
"/",
65-
).then((res) => {
66-
return res.arrayBuffer();
67-
});
68-
},
69-
stop(): Promise<void> {
70-
return Promise.resolve(undefined);
71-
},
72-
},
73-
{
74-
getValue() {
75-
return avmWasm;
76-
},
77-
async start(): Promise<void> {
78-
avmWasm = await fetchResource(
79-
"@fluencelabs/avm",
80-
"/dist/avm.wasm",
81-
"/",
82-
).then((res) => {
83-
return res.arrayBuffer();
84-
});
85-
},
86-
stop(): Promise<void> {
87-
return Promise.resolve(undefined);
88-
},
89-
},
90-
);
91-
9239
super(config, keyPair, marine, new JsServiceHost(), conn);
9340
}
9441
}

packages/core/js-client/src/ephemeral/network.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
*/
1616

1717
import { PeerIdB58 } from "@fluencelabs/interfaces";
18-
import { fetchResource } from "@fluencelabs/js-client-isomorphic/fetcher";
19-
import { getWorker } from "@fluencelabs/js-client-isomorphic/worker-resolver";
2018
import { Subject } from "rxjs";
2119

2220
import { IConnection } from "../connection/interfaces.js";
2321
import { DEFAULT_CONFIG, FluencePeer } from "../jsPeer/FluencePeer.js";
2422
import { JsServiceHost } from "../jsServiceHost/JsServiceHost.js";
2523
import { fromBase64Sk, KeyPair } from "../keypair/index.js";
2624
import { IMarineHost } from "../marine/interfaces.js";
25+
import { loadMarineDeps } from "../marine/loader.js";
2726
import { MarineBackgroundRunner } from "../marine/worker/index.js";
2827
import { Particle } from "../particle/Particle.js";
2928
import { logger } from "../util/logger.js";
@@ -233,55 +232,8 @@ export class EphemeralNetwork {
233232
const promises = this.config.peers.map(async (x) => {
234233
const kp = await fromBase64Sk(x.sk);
235234

236-
const [marineJsWasm, avmWasm] = await Promise.all([
237-
fetchResource(
238-
"@fluencelabs/marine-js",
239-
"/dist/marine-js.wasm",
240-
"/",
241-
).then((res) => {
242-
return res.arrayBuffer();
243-
}),
244-
fetchResource("@fluencelabs/avm", "/dist/avm.wasm", "/").then((res) => {
245-
return res.arrayBuffer();
246-
}),
247-
]);
248-
249-
const marine = new MarineBackgroundRunner(
250-
{
251-
async getValue() {
252-
// TODO: load worker in parallel with avm and marine, test that it works
253-
return getWorker("@fluencelabs/marine-worker", "/");
254-
},
255-
start() {
256-
return Promise.resolve(undefined);
257-
},
258-
stop() {
259-
return Promise.resolve(undefined);
260-
},
261-
},
262-
{
263-
getValue() {
264-
return marineJsWasm;
265-
},
266-
start(): Promise<void> {
267-
return Promise.resolve(undefined);
268-
},
269-
stop(): Promise<void> {
270-
return Promise.resolve(undefined);
271-
},
272-
},
273-
{
274-
getValue() {
275-
return avmWasm;
276-
},
277-
start(): Promise<void> {
278-
return Promise.resolve(undefined);
279-
},
280-
stop(): Promise<void> {
281-
return Promise.resolve(undefined);
282-
},
283-
},
284-
);
235+
const marineDeps = await loadMarineDeps("/");
236+
const marine = new MarineBackgroundRunner(...marineDeps);
285237

286238
const peerId = kp.getPeerId();
287239

0 commit comments

Comments
 (0)