From be3f13b34a7323de9f0f9b5d08191e8221feeba2 Mon Sep 17 00:00:00 2001 From: Kostiantyn Smyrnov Date: Sun, 24 Dec 2023 17:07:52 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Updated=20NodeProvid?= =?UTF-8?q?er=20in=20the=20react=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/manager/src/main.tsx | 2 +- examples/node/src/index.ts | 1 + packages/node-api/src/server.ts | 11 ++- .../node-api/test/api.nodeApiServer.spec.ts | 1 + packages/react/package.json | 12 +++ packages/react/src/hooks/index.ts | 1 + packages/react/src/hooks/usePoller.ts | 59 ++++++++++++ packages/react/src/index.ts | 1 + .../NodeProvider/NodeProviderContext.ts | 13 ++- .../src/providers/NodeProvider/index.tsx | 93 +++++++++++-------- pnpm-lock.yaml | 6 ++ 11 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 packages/react/src/hooks/index.ts create mode 100644 packages/react/src/hooks/usePoller.ts diff --git a/examples/manager/src/main.tsx b/examples/manager/src/main.tsx index 5e16705a..7fae903d 100644 --- a/examples/manager/src/main.tsx +++ b/examples/manager/src/main.tsx @@ -4,9 +4,9 @@ import { App } from './App.js'; import { AppConfig, ConfigProvider, - NodeProvider, WalletProvider, ContractsProvider, + NodeProvider, } from '@windingtree/sdk-react/providers'; import { hardhat, polygonZkEvmTestnet } from 'viem/chains'; import { contractsConfig } from 'wtmp-examples-shared-files/dist/index.js'; diff --git a/examples/node/src/index.ts b/examples/node/src/index.ts index 418dcafd..d228348d 100644 --- a/examples/node/src/index.ts +++ b/examples/node/src/index.ts @@ -289,6 +289,7 @@ const main = async (): Promise => { secret: 'secret', ownerAccount: entityOwnerAddress, protocolContracts: contractsManager, + cors: ['http://localhost:5173'], }); apiServer.start(appRouter); diff --git a/packages/node-api/src/server.ts b/packages/node-api/src/server.ts index 8705f4d7..14504924 100644 --- a/packages/node-api/src/server.ts +++ b/packages/node-api/src/server.ts @@ -55,6 +55,11 @@ export interface NodeApiServerOptions { * If not provided, some default value or policy might be used. */ expire?: string | number; + + /** + * CORS origins + */ + cors: string[]; } /** @@ -218,6 +223,8 @@ export class NodeApiServer { ownerAccount?: Address; /** The duration (as a string or number) after which the access token will expire */ expire: string | number; + /** CORS origins */ + cors: string[]; /** * Creates an instance of NodeApiServerOptions. @@ -234,6 +241,7 @@ export class NodeApiServer { secret, ownerAccount, expire, + cors, } = options; // TODO Validate NodeApiServerOptions @@ -243,6 +251,7 @@ export class NodeApiServer { this.secret = secret; this.ownerAccount = ownerAccount; this.expire = expire ?? '1h'; + this.cors = cors || ['*']; // All origins are allowed by default /** Initialize the UsersDb instance with the provided options */ this.users = new UsersDb({ storage: storage['users'], prefix }); @@ -390,7 +399,7 @@ export class NodeApiServer { // Create a http server for handling of HTTP requests // TODO Implement origin configuration via .env this.server = createServer((req, res) => { - res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5174'); + res.setHeader('Access-Control-Allow-Origin', this.cors.join(', ')); res.setHeader('Access-Control-Request-Method', 'GET'); res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); res.setHeader( diff --git a/packages/node-api/test/api.nodeApiServer.spec.ts b/packages/node-api/test/api.nodeApiServer.spec.ts index ae15e3ef..5af2a15b 100644 --- a/packages/node-api/test/api.nodeApiServer.spec.ts +++ b/packages/node-api/test/api.nodeApiServer.spec.ts @@ -90,6 +90,7 @@ describe('NodeApiServer', () => { secret: 'secret', ownerAccount: owner.address, protocolContracts: contractsManager, + cors: ['*'], }; server = new NodeApiServer(options); diff --git a/packages/react/package.json b/packages/react/package.json index 7950bcf8..116e666e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -32,6 +32,16 @@ "default": "./dist/providers.cjs" } }, + "./hooks": { + "import": { + "types": "./dist/hooks/index.d.ts", + "default": "./dist/hooks.es.js" + }, + "require": { + "types": "./dist/hooks/index.d.ts", + "default": "./dist/hooks.cjs" + } + }, "./utils": { "import": { "types": "./dist/utils/index.d.ts", @@ -45,6 +55,7 @@ }, "devDependencies": { "@trpc/client": "^10.44.1", + "@trpc/server": "^10.44.1", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", @@ -53,6 +64,7 @@ "@windingtree/sdk-node-api": "workspace:*", "@windingtree/sdk-storage": "workspace:*", "@windingtree/sdk-types": "workspace:*", + "@windingtree/sdk-logger": "workspace:*", "eslint": "^8.45.0", "eslint-config-react-app": "^7.0.1", "react": "^18.2.0", diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts new file mode 100644 index 00000000..cf0e0120 --- /dev/null +++ b/packages/react/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './usePoller.js'; diff --git a/packages/react/src/hooks/usePoller.ts b/packages/react/src/hooks/usePoller.ts new file mode 100644 index 00000000..314e4b6e --- /dev/null +++ b/packages/react/src/hooks/usePoller.ts @@ -0,0 +1,59 @@ +import { useEffect } from 'react'; +import { createLogger } from '@windingtree/sdk-logger'; + +// Initialize a logger for the hook +const logger = createLogger('usePoller'); + +/** + * Custom React hook for running a function at regular intervals. + * + * @param fn - The function to be executed periodically. + * @param delay - The delay (in milliseconds) between each execution. + * @param enabled - Boolean to enable or disable the polling. + * @param name - Name of the poller for logging purposes. + * @param maxFailures - Maximum number of allowed consecutive failures. + */ +export const usePoller = ( + fn: () => void, + delay: number | null, + enabled = true, + name = ' ', + maxFailures = 100, +): void => { + useEffect(() => { + let failures = 0; + let intervalId: ReturnType; + + // Check if the poller should be running. + if (enabled && delay !== null && failures < maxFailures) { + // Function to be run at each interval. + const fnRunner = async (): Promise => { + try { + // Execute the provided function. + const context = fn(); + // Wait for the function if it returns a Promise. + await Promise.resolve(context); + } catch (error) { + // Increment failure count and log error. + failures++; + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logger.error(`Poller ${name} error: ${errorMessage}`); + } + }; + + // Setting up the interval. + intervalId = setInterval(fnRunner, delay); + logger.trace(`Poller ${name} started`); + } + + // Cleanup function for the useEffect. + return () => { + // Clear the interval when the component is unmounted or dependencies change. + if (intervalId) { + clearInterval(intervalId); + } + logger.trace(`Poller ${name} stopped`); + }; + }, [fn, delay, name, enabled, maxFailures]); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 66e37891..acdc7635 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,2 +1,3 @@ export * as providers from './providers/index.js'; export * as utils from './utils/index.js'; +export * as hooks from './hooks/index.js'; diff --git a/packages/react/src/providers/NodeProvider/NodeProviderContext.ts b/packages/react/src/providers/NodeProvider/NodeProviderContext.ts index f6f09f95..2f23555b 100644 --- a/packages/react/src/providers/NodeProvider/NodeProviderContext.ts +++ b/packages/react/src/providers/NodeProvider/NodeProviderContext.ts @@ -1,9 +1,10 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, Context } from 'react'; +import type { AnyRouter } from '@trpc/server'; import { createTRPCProxyClient } from '@trpc/client'; import type { AppRouter } from '@windingtree/sdk-node-api/router'; -export interface NodeContextData { - node?: ReturnType> | undefined; +export interface NodeContextData { + node?: ReturnType> | undefined; nodeConnected: boolean; nodeError?: string; } @@ -12,8 +13,10 @@ export const NodeContext = createContext( {} as NodeContextData, ); -export const useNode = () => { - const context = useContext(NodeContext); +export const useNode = () => { + const context = useContext>( + NodeContext as Context>, + ); if (context === undefined) { throw new Error('useNode must be used within a "NodeContext"'); diff --git a/packages/react/src/providers/NodeProvider/index.tsx b/packages/react/src/providers/NodeProvider/index.tsx index 4a0950d9..2cfaca72 100644 --- a/packages/react/src/providers/NodeProvider/index.tsx +++ b/packages/react/src/providers/NodeProvider/index.tsx @@ -1,44 +1,64 @@ -import { - CreateTRPCClientOptions, - createTRPCProxyClient, - httpBatchLink, -} from '@trpc/client'; +import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import superjson from 'superjson'; -import { PropsWithChildren, useState, useEffect } from 'react'; +import { + type PropsWithChildren, + useState, + useEffect, + useCallback, +} from 'react'; import { NodeContext } from './NodeProviderContext.js'; +import { AppRouter } from '@windingtree/sdk-node-api/router'; import { unauthorizedLink } from '@windingtree/sdk-node-api/client'; -import type { AppRouter } from '@windingtree/sdk-node-api/router'; import { useConfig } from '../ConfigProvider/ConfigProviderContext.js'; +import { usePoller } from '../../hooks/usePoller.js'; +import { createLogger } from '@windingtree/sdk-logger'; + +// Initialize logger +const logger = createLogger('NodeProvider'); export const NodeProvider = ({ children }: PropsWithChildren) => { - const { nodeHost, setAuth, resetAuth } = useConfig(); + const { nodeHost, resetAuth } = useConfig(); const [node, setNode] = useState< ReturnType> | undefined >(); const [error, setError] = useState(); + const [isConnected, setIsConnected] = useState(false); - const stopClient = () => { - try { - setError(() => undefined); - setNode(() => undefined); - } catch (error) { - setError((error as Error).message || 'Unknown node provider error'); - } - }; + // Function to stop and reset the client + const stopClient = useCallback(() => { + setError(undefined); + setNode(undefined); + }, []); - useEffect(() => { - if (!nodeHost) { - stopClient(); + // Function to check the connection + const checkConnection = useCallback(async () => { + setError(undefined); + + if (!node) { + setIsConnected(false); return; } + try { + const { message } = await node.service.ping.query(); + setIsConnected(message === 'pong'); + } catch (err) { + setIsConnected(false); + setError('Unable to connect the Node'); + logger.error(err); + } + }, [node]); + + // Initialize and clean up the client + useEffect(() => { const startClient = async () => { - try { - setError(undefined); + if (!nodeHost) { + return; + } + try { const tRpcNode = createTRPCProxyClient({ - transformer: - superjson as unknown as CreateTRPCClientOptions['transformer'], + transformer: superjson, links: [ unauthorizedLink(resetAuth), httpBatchLink({ @@ -46,7 +66,6 @@ export const NodeProvider = ({ children }: PropsWithChildren) => { fetch(url, options) { return fetch(url, { ...options, - // allows to send cookies to the server credentials: 'include', }); }, @@ -54,21 +73,10 @@ export const NodeProvider = ({ children }: PropsWithChildren) => { ], }); - const { message } = await tRpcNode.service.ping.query(); - - if (message === 'pong') { - setNode(() => tRpcNode); - } - } catch (error) { - console.log(error); - setNode(() => undefined); - let errMessage = (error as Error).message; - - if (errMessage === 'Failed to fetch') { - errMessage = 'Node connection failed'; - } - - setError(() => errMessage || 'Unknown node provider error'); + setNode(() => tRpcNode); + } catch (err) { + setError((err as Error).message || 'Unknown node provider error'); + logger.error(err); } }; @@ -77,13 +85,16 @@ export const NodeProvider = ({ children }: PropsWithChildren) => { return () => { stopClient(); }; - }, [nodeHost, setAuth, resetAuth]); + }, [stopClient, resetAuth, nodeHost]); + + // Polling for connection check + usePoller(checkConnection, 5000, true, 'NodeConnection'); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd210b9d..e751ce2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -700,6 +700,9 @@ importers: '@trpc/client': specifier: ^10.44.1 version: 10.44.1(@trpc/server@10.44.1) + '@trpc/server': + specifier: ^10.44.1 + version: 10.44.1 '@types/react': specifier: ^18.2.15 version: 18.2.43 @@ -715,6 +718,9 @@ importers: '@windingtree/sdk-contracts-manager': specifier: workspace:* version: link:../contracts-manger + '@windingtree/sdk-logger': + specifier: workspace:* + version: link:../logger '@windingtree/sdk-node-api': specifier: workspace:* version: link:../node-api From 8eae00eedc90e0862bc5c7a8bf980be815a5a26a Mon Sep 17 00:00:00 2001 From: Kostiantyn Smyrnov Date: Sun, 24 Dec 2023 17:22:12 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Updated=20usePoller?= =?UTF-8?q?=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/react/src/hooks/usePoller.ts | 61 ++++++++++++++++----------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/react/src/hooks/usePoller.ts b/packages/react/src/hooks/usePoller.ts index 314e4b6e..da2d3e7e 100644 --- a/packages/react/src/hooks/usePoller.ts +++ b/packages/react/src/hooks/usePoller.ts @@ -22,36 +22,47 @@ export const usePoller = ( ): void => { useEffect(() => { let failures = 0; - let intervalId: ReturnType; + let timeoutId: ReturnType | undefined; - // Check if the poller should be running. - if (enabled && delay !== null && failures < maxFailures) { - // Function to be run at each interval. - const fnRunner = async (): Promise => { - try { - // Execute the provided function. - const context = fn(); - // Wait for the function if it returns a Promise. - await Promise.resolve(context); - } catch (error) { - // Increment failure count and log error. - failures++; - const errorMessage = - error instanceof Error ? error.message : 'Unknown error'; - logger.error(`Poller ${name} error: ${errorMessage}`); - } - }; + // Schedules the next execution of fnRunner + const scheduleNextRun = () => { + if (enabled && delay !== null) { + timeoutId = setTimeout(fnRunner, delay); + } + }; + + // Function to be executed at each interval + const fnRunner = async (): Promise => { + if (failures >= maxFailures) { + // Stop polling after reaching maximum failures + logger.error(`Poller ${name} stopped after reaching max failures`); + return; + } + + try { + // Execute the provided function + await Promise.resolve(fn()); + // Schedule the next run after successful execution + scheduleNextRun(); + } catch (error) { + failures++; + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error(`Poller ${name} error: ${errorMessage}`); + // Schedule the next run even if an error occurred + scheduleNextRun(); + } + }; - // Setting up the interval. - intervalId = setInterval(fnRunner, delay); - logger.trace(`Poller ${name} started`); + if (enabled) { + // Start the initial run + scheduleNextRun(); } - // Cleanup function for the useEffect. + // Cleanup function for useEffect return () => { - // Clear the interval when the component is unmounted or dependencies change. - if (intervalId) { - clearInterval(intervalId); + if (timeoutId) { + // Clear the timeout when the component is unmounted or dependencies change + clearTimeout(timeoutId); } logger.trace(`Poller ${name} stopped`); };