diff --git a/packages/@tests/aqua/_aqua/smoke_test.aqua b/packages/@tests/aqua/_aqua/smoke_test.aqua index 9a2cf31b..f3f7ef04 100644 --- a/packages/@tests/aqua/_aqua/smoke_test.aqua +++ b/packages/@tests/aqua/_aqua/smoke_test.aqua @@ -1,5 +1,9 @@ import "@fluencelabs/registry/resources-api.aqua" -func smokeTest(label: string) -> ?string, *string: +service HelloWorld("hello-world"): + hello(str: string) -> string + +func smokeTest(label: string) -> ?string, *string, string: res, errors <- createResource(label) - <- res, errors + hello <- HelloWorld.hello("Fluence user") + <- res, errors, hello \ No newline at end of file diff --git a/packages/@tests/aqua/package.json b/packages/@tests/aqua/package.json index f3e02467..d95f271f 100644 --- a/packages/@tests/aqua/package.json +++ b/packages/@tests/aqua/package.json @@ -17,8 +17,8 @@ "author": "Fluence Labs", "license": "Apache-2.0", "dependencies": { - "@fluencelabs/js-client.api": "workspace:*", - "@fluencelabs/fluence-network-environment": "1.0.13", + "@fluencelabs/js-client.api": "workspace:^", + "@fluencelabs/fluence-network-environment": "1.0.14", "base64-js": "1.5.1" }, "devDependencies": { diff --git a/packages/@tests/aqua/src/_aqua/smoke_test.ts b/packages/@tests/aqua/src/_aqua/smoke_test.ts index 4fadaef4..f4a25ac8 100644 --- a/packages/@tests/aqua/src/_aqua/smoke_test.ts +++ b/packages/@tests/aqua/src/_aqua/smoke_test.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +// @ts-nocheck /** * * This file is auto-generated. Do not edit manually: changes may be erased. @@ -6,17 +8,54 @@ * Aqua version: 0.9.4 * */ - -// eslint-disable -//ts-nocheck import type { IFluenceClient as IFluenceClient$$, CallParams as CallParams$$ } from '@fluencelabs/js-client.api'; import { v5_callFunction as callFunction$$, v5_registerService as registerService$$ } from '@fluencelabs/js-client.api'; // Services +export interface HelloWorldDef { + hello: (str: string, callParams: CallParams$$<'str'>) => string | Promise; +} +export function registerHelloWorld(service: HelloWorldDef): void; +export function registerHelloWorld(serviceId: string, service: HelloWorldDef): void; +export function registerHelloWorld(peer: IFluenceClient$$, service: HelloWorldDef): void; +export function registerHelloWorld(peer: IFluenceClient$$, serviceId: string, service: HelloWorldDef): void; + +export function registerHelloWorld(...args: any) { + registerService$$(args, { + defaultServiceId: 'hello-world', + functions: { + tag: 'labeledProduct', + fields: { + hello: { + tag: 'arrow', + domain: { + tag: 'labeledProduct', + fields: { + str: { + tag: 'scalar', + name: 'string', + }, + }, + }, + codomain: { + tag: 'unlabeledProduct', + items: [ + { + tag: 'scalar', + name: 'string', + }, + ], + }, + }, + }, + }, + }); +} + // Functions -export type SmokeTestResult = [string | null, string[]]; +export type SmokeTestResult = [string | null, string[], string]; export function smokeTest(label: string, config?: { ttl?: number }): Promise; export function smokeTest(peer: IFluenceClient$$, label: string, config?: { ttl?: number }): Promise; @@ -27,208 +66,211 @@ export function smokeTest(...args: any) { (seq (seq (seq - (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) - (call %init_peer_id% ("getDataSrv" "label") [] label) - ) - (new $resource_id - (new $successful - (seq + (seq + (call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-) + (call %init_peer_id% ("getDataSrv" "label") [] label) + ) + (new $resource_id + (new $successful (seq (seq - (call %init_peer_id% ("peer" "timestamp_sec") [] t) - (xor - (seq + (seq + (call %init_peer_id% ("peer" "timestamp_sec") [] t) + (xor (seq - (call -relay- ("registry" "get_key_bytes") [label [] t [] ""] bytes) - (xor - (call %init_peer_id% ("sig" "sign") [bytes] result) - (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) - ) - ) - (xor - (match result.$.success! false - (ap result.$.error.[0]! $error) - ) (seq + (call -relay- ("registry" "get_key_bytes") [label [] t [] ""] bytes) + (xor + (call %init_peer_id% ("sig" "sign") [bytes] result) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1]) + ) + ) + (xor + (match result.$.success! false + (ap result.$.error.[0]! $error) + ) (seq (seq (seq (seq (seq - (ap result.$.signature! result_flat) - (call -relay- ("registry" "get_key_id") [label %init_peer_id%] id) + (seq + (ap result.$.signature! result_flat) + (call -relay- ("registry" "get_key_id") [label %init_peer_id%] id) + ) + (call -relay- ("op" "string_to_b58") [id] k) ) - (call -relay- ("op" "string_to_b58") [id] k) + (call -relay- ("kad" "neighborhood") [k [] []] nodes) ) - (call -relay- ("kad" "neighborhood") [k [] []] nodes) - ) - (par - (fold nodes n-0 - (par - (seq - (xor - (xor - (seq - (seq - (seq - (call n-0 ("peer" "timestamp_sec") [] t-0) - (call n-0 ("trust-graph" "get_weight") [%init_peer_id% t-0] weight) - ) - (call n-0 ("registry" "register_key") [label [] t [] "" result_flat.$.[0]! weight t-0] result-0) - ) - (xor - (match result-0.$.success! true - (ap true $successful) - ) - (ap result-0.$.error! $error) - ) - ) - (call n-0 ("op" "noop") []) - ) - (seq - (call -relay- ("op" "noop") []) - (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) - ) - ) - (call -relay- ("op" "noop") []) - ) - (next n-0) - ) - (never) - ) - (null) - ) - ) - (new $status - (new $result-1 - (seq - (seq - (seq - (par - (seq - (seq - (seq - (call -relay- ("math" "sub") [1 1] sub) - (new $successful_test - (seq - (seq - (seq - (call -relay- ("math" "add") [sub 1] successful_incr) - (fold $successful s - (seq - (seq - (ap s $successful_test) - (canon -relay- $successful_test #successful_iter_canon) - ) - (xor - (match #successful_iter_canon.length successful_incr - (null) - ) - (next s) - ) - ) - (never) - ) - ) - (canon -relay- $successful_test #successful_result_canon) - ) - (ap #successful_result_canon successful_gate) - ) - ) - ) - (call -relay- ("math" "sub") [1 1] sub-0) - ) - (ap "ok" $status) - ) - (call -relay- ("peer" "timeout") [6000 "timeout"] $status) - ) - (new $status_test - (seq - (seq - (seq - (call -relay- ("math" "add") [0 1] status_incr) - (fold $status s - (seq - (seq - (ap s $status_test) - (canon -relay- $status_test #status_iter_canon) - ) - (xor - (match #status_iter_canon.length status_incr - (null) - ) - (next s) - ) - ) - (never) - ) - ) - (canon -relay- $status_test #status_result_canon) - ) - (ap #status_result_canon status_gate) - ) - ) - ) - (xor - (match status_gate.$.[0]! "ok" - (ap true $result-1) - ) - (ap false $result-1) - ) - ) - (new $result-1_test - (seq + (par + (fold nodes n-0 + (par (seq - (seq - (call -relay- ("math" "add") [0 1] result-1_incr) - (fold $result-1 s + (xor + (xor (seq (seq - (ap s $result-1_test) - (canon -relay- $result-1_test #result-1_iter_canon) + (seq + (call n-0 ("peer" "timestamp_sec") [] t-0) + (call n-0 ("trust-graph" "get_weight") [%init_peer_id% t-0] weight) + ) + (call n-0 ("registry" "register_key") [label [] t [] "" result_flat.$.[0]! weight t-0] result-0) ) (xor - (match #result-1_iter_canon.length result-1_incr - (null) + (match result-0.$.success! true + (ap true $successful) ) - (next s) + (ap result-0.$.error! $error) ) ) - (never) + (call n-0 ("op" "noop") []) + ) + (seq + (call -relay- ("op" "noop") []) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) ) ) - (canon -relay- $result-1_test #result-1_result_canon) + (call -relay- ("op" "noop") []) + ) + (next n-0) + ) + (never) + ) + (null) + ) + ) + (new $status + (new $result-1 + (seq + (seq + (seq + (par + (seq + (seq + (seq + (call -relay- ("math" "sub") [1 1] sub) + (new $successful_test + (seq + (seq + (seq + (call -relay- ("math" "add") [sub 1] successful_incr) + (fold $successful s + (seq + (seq + (ap s $successful_test) + (canon -relay- $successful_test #successful_iter_canon) + ) + (xor + (match #successful_iter_canon.length successful_incr + (null) + ) + (next s) + ) + ) + (never) + ) + ) + (canon -relay- $successful_test #successful_result_canon) + ) + (ap #successful_result_canon successful_gate) + ) + ) + ) + (call -relay- ("math" "sub") [1 1] sub-0) + ) + (ap "ok" $status) + ) + (call -relay- ("peer" "timeout") [6000 "timeout"] $status) + ) + (new $status_test + (seq + (seq + (seq + (call -relay- ("math" "add") [0 1] status_incr) + (fold $status s + (seq + (seq + (ap s $status_test) + (canon -relay- $status_test #status_iter_canon) + ) + (xor + (match #status_iter_canon.length status_incr + (null) + ) + (next s) + ) + ) + (never) + ) + ) + (canon -relay- $status_test #status_result_canon) + ) + (ap #status_result_canon status_gate) + ) + ) + ) + (xor + (match status_gate.$.[0]! "ok" + (ap true $result-1) + ) + (ap false $result-1) + ) + ) + (new $result-1_test + (seq + (seq + (seq + (call -relay- ("math" "add") [0 1] result-1_incr) + (fold $result-1 s + (seq + (seq + (ap s $result-1_test) + (canon -relay- $result-1_test #result-1_iter_canon) + ) + (xor + (match #result-1_iter_canon.length result-1_incr + (null) + ) + (next s) + ) + ) + (never) + ) + ) + (canon -relay- $result-1_test #result-1_result_canon) + ) + (ap #result-1_result_canon result-1_gate) ) - (ap #result-1_result_canon result-1_gate) ) ) ) ) ) - ) - (xor - (match result-1_gate.$.[0]! false - (ap "resource wasn't created: timeout exceeded" $error) + (xor + (match result-1_gate.$.[0]! false + (ap "resource wasn't created: timeout exceeded" $error) + ) + (ap id $resource_id) ) - (ap id $resource_id) ) ) ) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 3]) ) - (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 3]) ) + (canon %init_peer_id% $resource_id #-resource_id-fix-0) ) - (canon %init_peer_id% $resource_id #-resource_id-fix-0) + (ap #-resource_id-fix-0 -resource_id-flat-0) ) - (ap #-resource_id-fix-0 -resource_id-flat-0) ) ) ) + (call %init_peer_id% ("hello-world" "hello") ["Fluence user"] hello) ) (xor (seq (canon %init_peer_id% $error #error_canon) - (call %init_peer_id% ("callbackSrv" "response") [-resource_id-flat-0 #error_canon]) + (call %init_peer_id% ("callbackSrv" "response") [-resource_id-flat-0 #error_canon hello]) ) (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 4]) ) @@ -268,6 +310,10 @@ export function smokeTest(...args: any) { name: 'string', }, }, + { + tag: 'scalar', + name: 'string', + }, ], }, }, @@ -284,3 +330,5 @@ export function smokeTest(...args: any) { script, ); } + +/* eslint-enable */ diff --git a/packages/@tests/aqua/src/index.ts b/packages/@tests/aqua/src/index.ts index 34daa1a9..4b480291 100644 --- a/packages/@tests/aqua/src/index.ts +++ b/packages/@tests/aqua/src/index.ts @@ -1,50 +1,65 @@ import { fromByteArray } from 'base64-js'; import { Fluence } from '@fluencelabs/js-client.api'; -import { krasnodar } from '@fluencelabs/fluence-network-environment'; -import { smokeTest } from './_aqua/smoke_test.js'; +import { kras, randomKras } from '@fluencelabs/fluence-network-environment'; +import { registerHelloWorld, smokeTest } from './_aqua/smoke_test.js'; // const relay = { // multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3', // peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3', // }; -const relay = krasnodar[4]; +const relay = randomKras(); -const rndSk = () => { - // if (getRandomValues) { - // return getRandomValues(new Uint8Array(32)); - // } - // @ts-ignore - // return globalThis.crypto.webcrypto.getRandomValues(new Uint8Array(32)); +function generateRandomUint8Array() { + const uint8Array = new Uint8Array(32); + for (let i = 0; i < uint8Array.length; i++) { + uint8Array[i] = Math.floor(Math.random() * 256); + } + return uint8Array; +} + +const optsWithRandomKeyPair = () => { + return { + keyPair: { + type: 'Ed25519', + source: generateRandomUint8Array(), + }, + } as const; }; export const main = async () => { - console.log('starting fluence...'); - await Fluence.start({ - relay: relay, - // keyPair: { - // type: 'Ed25519', - // source: rndSk(), - // }, - }); + try { + Fluence.onConnectionStateChange((state) => console.info('connection state changed: ', state)); - console.log('started fluence'); - const p = await Fluence.getPeer(); + console.log('connecting to Fluence Network...'); + await Fluence.connect(relay, optsWithRandomKeyPair()); - console.log('my peer id: ', p.getStatus().peerId); - console.log('my sk id: ', fromByteArray(p.getSk())); + console.log('connected'); - console.log('running some aqua...'); - const [res, errors] = await smokeTest('my_resource'); - if (res === null) { - console.log('aqua failed, errors', errors); - } else { - console.log('aqua finished, result', res); + await registerHelloWorld({ + hello(str) { + return 'Hello, ' + str + '!'; + }, + }); + + const client = await Fluence.getClient(); + + console.log('my peer id: ', client.getPeerId()); + console.log('my sk id: ', fromByteArray(client.getPeerSecretKey())); + + console.log('running some aqua...'); + const [res, errors, hello] = await smokeTest('my_resource'); + console.log(hello); + if (res === null) { + console.log('aqua failed, errors', errors); + } else { + console.log('aqua finished, result', res); + } + } finally { + console.log('disconnecting from Fluence Network...'); + await Fluence.disconnect(); + console.log('disconnected'); } - - console.log('stopping fluence...'); - await Fluence.stop(); - console.log('stopped fluence...'); }; export const runMain = () => { diff --git a/packages/@tests/frameworks/cra-ts/package.json b/packages/@tests/frameworks/cra-ts/package.json index abc78663..455abc76 100644 --- a/packages/@tests/frameworks/cra-ts/package.json +++ b/packages/@tests/frameworks/cra-ts/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@fluencelabs/js-client.api": "workspace:*", - "@test/aqua_for_test": "workspace:*", + "@fluencelabs/js-client.api": "workspace:^", + "@test/aqua_for_test": "workspace:^", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "13.4.0", "@testing-library/user-event": "13.5.0", diff --git a/packages/@tests/smoke_node/package.json b/packages/@tests/smoke_node/package.json index 69c723ac..7e4903f1 100644 --- a/packages/@tests/smoke_node/package.json +++ b/packages/@tests/smoke_node/package.json @@ -17,9 +17,9 @@ "author": "Fluence Labs", "license": "Apache-2.0", "dependencies": { - "@fluencelabs/js-client.api": "workspace:*", - "@fluencelabs/js-client.node": "workspace:*", - "@test/aqua_for_test": "workspace:*" + "@fluencelabs/js-client.api": "workspace:^", + "@fluencelabs/js-client.node": "workspace:^", + "@test/aqua_for_test": "workspace:^" }, "devDependencies": {} } diff --git a/packages/@tests/smoke_web/package.json b/packages/@tests/smoke_web/package.json index fba2f4d8..d2bf3aa8 100644 --- a/packages/@tests/smoke_web/package.json +++ b/packages/@tests/smoke_web/package.json @@ -18,7 +18,7 @@ "license": "Apache-2.0", "dependencies": {}, "devDependencies": { - "@fluencelabs/js-client.web.standalone": "workspace:*", + "@fluencelabs/js-client.web.standalone": "workspace:^", "http-server": "14.1.1" } } diff --git a/packages/client/api/src/compilerSupport/implementation.ts b/packages/client/api/src/compilerSupport/implementation.ts index 5d8dbad6..d9442f18 100644 --- a/packages/client/api/src/compilerSupport/implementation.ts +++ b/packages/client/api/src/compilerSupport/implementation.ts @@ -19,7 +19,7 @@ import type { IFluenceClient } from '@fluencelabs/interfaces'; import { getArgumentTypes } from '@fluencelabs/interfaces'; import { isFluencePeer } from '@fluencelabs/interfaces'; -import { getDefaultPeer } from '../util.js'; +import { getFluenceInterface } from '../util.js'; /** * Convenience function to support Aqua `func` generation backend @@ -30,12 +30,20 @@ import { getDefaultPeer } from '../util.js'; * @param script - air script with function execution logic generated by the Aqua compiler */ export const callFunction = async (rawFnArgs: Array, def: FunctionCallDef, script: string): Promise => { - const { args, peer, config } = await extractFunctionArgs(rawFnArgs, def); - return peer.compilerSupport.callFunction({ + const { args, client: peer, config } = await extractFunctionArgs(rawFnArgs, def); + if (peer.internals.getConnectionState() !== 'connected') { + throw new Error( + 'Could not call the Aqua function because client is disconnected. Did you forget to call Fluence.connect()?', + ); + } + + const fluence = await getFluenceInterface(); + return fluence.callAquaFunction({ args, def, script, config: config || {}, + peer: peer, }); }; @@ -47,10 +55,21 @@ export const callFunction = async (rawFnArgs: Array, def: FunctionCallDef, */ export const registerService = async (args: any[], def: ServiceDef): Promise => { const { peer, service, serviceId } = await extractServiceArgs(args, def.defaultServiceId); - return peer.compilerSupport.registerService({ + + // TODO: TBH service registration is just putting some stuff into a hashmap + // there should not be such a check at all + if (peer.internals.getConnectionState() !== 'connected') { + throw new Error( + 'Could not register Aqua service because the client is disconnected. Did you forget to call Fluence.connect()?', + ); + } + + const fluence = await getFluenceInterface(); + return fluence.registerService({ def, service, serviceId, + peer, }); }; @@ -68,7 +87,7 @@ const extractFunctionArgs = async ( args: any[], def: FunctionCallDef, ): Promise<{ - peer: IFluenceClient; + client: IFluenceClient; config?: FnConfig; args: { [key: string]: any }; }> => { @@ -84,7 +103,8 @@ const extractFunctionArgs = async ( structuredArgs = args.slice(1, numberOfExpectedArgs + 1); config = args[numberOfExpectedArgs + 1]; } else { - peer = await getDefaultPeer(); + const fluence = await getFluenceInterface(); + peer = fluence.defaultClient; structuredArgs = args.slice(0, numberOfExpectedArgs); config = args[numberOfExpectedArgs]; } @@ -96,7 +116,7 @@ const extractFunctionArgs = async ( const argsRes = argumentNames.reduce((acc, name, index) => ({ ...acc, [name]: structuredArgs[index] }), {}); return { - peer: peer, + client: peer, config: config, args: argsRes, }; @@ -124,7 +144,8 @@ const extractServiceArgs = async ( if (isFluencePeer(args[0])) { peer = args[0]; } else { - peer = await getDefaultPeer(); + const fluence = await getFluenceInterface(); + peer = fluence.defaultClient; } if (typeof args[0] === 'string') { diff --git a/packages/client/api/src/index.ts b/packages/client/api/src/index.ts index 7e0d9ca2..c2cd2152 100644 --- a/packages/client/api/src/index.ts +++ b/packages/client/api/src/index.ts @@ -1,5 +1,11 @@ -import { getDefaultPeer } from './util.js'; -import type { IFluenceClient, ClientOptions } from '@fluencelabs/interfaces'; +import { getFluenceInterface, getFluenceInterfaceFromGlobalThis } from './util.js'; +import { + IFluenceClient, + ClientOptions, + RelayOptions, + ConnectionState, + ConnectionStates, +} from '@fluencelabs/interfaces'; export type { IFluenceClient, ClientOptions, CallParams } from '@fluencelabs/interfaces'; export { @@ -30,33 +36,56 @@ export { } from './compilerSupport/implementation.js'; /** - * Public interface to Fluence JS + * Public interface to Fluence Network */ export const Fluence = { /** - * Initializes the default peer: starts the Aqua VM, initializes the default call service handlers - * and (optionally) connect to the Fluence network - * @param options - object specifying peer configuration + * Connect to the Fluence network + * @param relay - relay node to connect to + * @param options - client options */ - start: async (options?: ClientOptions): Promise => { - const peer = await getDefaultPeer(); - return peer.start(options); + connect: async (relay: RelayOptions, options?: ClientOptions): Promise => { + const fluence = await getFluenceInterface(); + return fluence.defaultClient.connect(relay, options); }, /** - * Un-initializes the default peer: stops all the underlying workflows, stops the Aqua VM - * and disconnects from the Fluence network + * Disconnect from the Fluence network */ - stop: async (): Promise => { - const peer = await getDefaultPeer(); - return peer.stop(); + disconnect: async (): Promise => { + const fluence = await getFluenceInterface(); + return fluence.defaultClient.disconnect(); }, /** - * Get the default peer instance - * @returns the default peer instance + * Handle connection state changes. Immediately returns the current connection state */ - getPeer: async (): Promise => { - return getDefaultPeer(); + onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState { + const optimisticResult = getFluenceInterfaceFromGlobalThis(); + if (optimisticResult) { + return optimisticResult.defaultClient.onConnectionStateChange(handler); + } + + getFluenceInterface().then((fluence) => fluence.defaultClient.onConnectionStateChange(handler)); + + return 'disconnected'; + }, + + /** + * Low level API. Get the underlying client instance which holds the connection to the network + * @returns IFluenceClient instance + */ + getClient: async (): Promise => { + const fluence = await getFluenceInterface(); + return fluence.defaultClient; }, }; + +/** + * Low level API. Generally you need Fluence.connect() instead. + * @returns IFluenceClient instance + */ +export const createClient = async (): Promise => { + const fluence = await getFluenceInterface(); + return fluence.clientFactory(); +}; diff --git a/packages/client/api/src/util.ts b/packages/client/api/src/util.ts index e12a0874..646b7e98 100644 --- a/packages/client/api/src/util.ts +++ b/packages/client/api/src/util.ts @@ -1,12 +1,23 @@ -import type { IFluenceClient } from '@fluencelabs/interfaces'; +import type { CallAquaFunction, IFluenceClient, RegisterService } from '@fluencelabs/interfaces'; -const getPeerFromGlobalThis = (): IFluenceClient | undefined => { - // @ts-ignore - return globalThis.defaultPeer; +type PublicFluenceInterface = { + clientFactory: () => IFluenceClient; + defaultClient: IFluenceClient; + callAquaFunction: CallAquaFunction; + registerService: RegisterService; }; -// TODO: DXJ-271 -const REJECT_MESSAGE = 'You probably forgot to add script tag. Read about it here: '; +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