Skip to content

Commit 0d09e55

Browse files
authored
Merge pull request #79 from windingtree/feat/new-nodeprovider
feat: 🎸 Updated NodeProvider in the react package
2 parents 0580026 + 8eae00e commit 0d09e55

File tree

11 files changed

+163
-48
lines changed

11 files changed

+163
-48
lines changed

examples/manager/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { App } from './App.js';
44
import {
55
AppConfig,
66
ConfigProvider,
7-
NodeProvider,
87
WalletProvider,
98
ContractsProvider,
9+
NodeProvider,
1010
} from '@windingtree/sdk-react/providers';
1111
import { hardhat, polygonZkEvmTestnet } from 'viem/chains';
1212
import { contractsConfig } from 'wtmp-examples-shared-files/dist/index.js';

examples/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ const main = async (): Promise<void> => {
289289
secret: 'secret',
290290
ownerAccount: entityOwnerAddress,
291291
protocolContracts: contractsManager,
292+
cors: ['http://localhost:5173'],
292293
});
293294

294295
apiServer.start(appRouter);

packages/node-api/src/server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ export interface NodeApiServerOptions {
5555
* If not provided, some default value or policy might be used.
5656
*/
5757
expire?: string | number;
58+
59+
/**
60+
* CORS origins
61+
*/
62+
cors: string[];
5863
}
5964

6065
/**
@@ -218,6 +223,8 @@ export class NodeApiServer {
218223
ownerAccount?: Address;
219224
/** The duration (as a string or number) after which the access token will expire */
220225
expire: string | number;
226+
/** CORS origins */
227+
cors: string[];
221228

222229
/**
223230
* Creates an instance of NodeApiServerOptions.
@@ -234,6 +241,7 @@ export class NodeApiServer {
234241
secret,
235242
ownerAccount,
236243
expire,
244+
cors,
237245
} = options;
238246

239247
// TODO Validate NodeApiServerOptions
@@ -243,6 +251,7 @@ export class NodeApiServer {
243251
this.secret = secret;
244252
this.ownerAccount = ownerAccount;
245253
this.expire = expire ?? '1h';
254+
this.cors = cors || ['*']; // All origins are allowed by default
246255

247256
/** Initialize the UsersDb instance with the provided options */
248257
this.users = new UsersDb({ storage: storage['users'], prefix });
@@ -390,7 +399,7 @@ export class NodeApiServer {
390399
// Create a http server for handling of HTTP requests
391400
// TODO Implement origin configuration via .env
392401
this.server = createServer((req, res) => {
393-
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5174');
402+
res.setHeader('Access-Control-Allow-Origin', this.cors.join(', '));
394403
res.setHeader('Access-Control-Request-Method', 'GET');
395404
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
396405
res.setHeader(

packages/node-api/test/api.nodeApiServer.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ describe('NodeApiServer', () => {
9090
secret: 'secret',
9191
ownerAccount: owner.address,
9292
protocolContracts: contractsManager,
93+
cors: ['*'],
9394
};
9495
server = new NodeApiServer(options);
9596

packages/react/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@
3232
"default": "./dist/providers.cjs"
3333
}
3434
},
35+
"./hooks": {
36+
"import": {
37+
"types": "./dist/hooks/index.d.ts",
38+
"default": "./dist/hooks.es.js"
39+
},
40+
"require": {
41+
"types": "./dist/hooks/index.d.ts",
42+
"default": "./dist/hooks.cjs"
43+
}
44+
},
3545
"./utils": {
3646
"import": {
3747
"types": "./dist/utils/index.d.ts",
@@ -45,6 +55,7 @@
4555
},
4656
"devDependencies": {
4757
"@trpc/client": "^10.44.1",
58+
"@trpc/server": "^10.44.1",
4859
"@types/react": "^18.2.15",
4960
"@types/react-dom": "^18.2.7",
5061
"@vitejs/plugin-react": "^4.0.3",
@@ -53,6 +64,7 @@
5364
"@windingtree/sdk-node-api": "workspace:*",
5465
"@windingtree/sdk-storage": "workspace:*",
5566
"@windingtree/sdk-types": "workspace:*",
67+
"@windingtree/sdk-logger": "workspace:*",
5668
"eslint": "^8.45.0",
5769
"eslint-config-react-app": "^7.0.1",
5870
"react": "^18.2.0",

packages/react/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './usePoller.js';

packages/react/src/hooks/usePoller.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useEffect } from 'react';
2+
import { createLogger } from '@windingtree/sdk-logger';
3+
4+
// Initialize a logger for the hook
5+
const logger = createLogger('usePoller');
6+
7+
/**
8+
* Custom React hook for running a function at regular intervals.
9+
*
10+
* @param fn - The function to be executed periodically.
11+
* @param delay - The delay (in milliseconds) between each execution.
12+
* @param enabled - Boolean to enable or disable the polling.
13+
* @param name - Name of the poller for logging purposes.
14+
* @param maxFailures - Maximum number of allowed consecutive failures.
15+
*/
16+
export const usePoller = (
17+
fn: () => void,
18+
delay: number | null,
19+
enabled = true,
20+
name = ' ',
21+
maxFailures = 100,
22+
): void => {
23+
useEffect(() => {
24+
let failures = 0;
25+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
26+
27+
// Schedules the next execution of fnRunner
28+
const scheduleNextRun = () => {
29+
if (enabled && delay !== null) {
30+
timeoutId = setTimeout(fnRunner, delay);
31+
}
32+
};
33+
34+
// Function to be executed at each interval
35+
const fnRunner = async (): Promise<void> => {
36+
if (failures >= maxFailures) {
37+
// Stop polling after reaching maximum failures
38+
logger.error(`Poller ${name} stopped after reaching max failures`);
39+
return;
40+
}
41+
42+
try {
43+
// Execute the provided function
44+
await Promise.resolve(fn());
45+
// Schedule the next run after successful execution
46+
scheduleNextRun();
47+
} catch (error) {
48+
failures++;
49+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
50+
logger.error(`Poller ${name} error: ${errorMessage}`);
51+
// Schedule the next run even if an error occurred
52+
scheduleNextRun();
53+
}
54+
};
55+
56+
if (enabled) {
57+
// Start the initial run
58+
scheduleNextRun();
59+
}
60+
61+
// Cleanup function for useEffect
62+
return () => {
63+
if (timeoutId) {
64+
// Clear the timeout when the component is unmounted or dependencies change
65+
clearTimeout(timeoutId);
66+
}
67+
logger.trace(`Poller ${name} stopped`);
68+
};
69+
}, [fn, delay, name, enabled, maxFailures]);
70+
};

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * as providers from './providers/index.js';
22
export * as utils from './utils/index.js';
3+
export * as hooks from './hooks/index.js';

packages/react/src/providers/NodeProvider/NodeProviderContext.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { createContext, useContext } from 'react';
1+
import { createContext, useContext, Context } from 'react';
2+
import type { AnyRouter } from '@trpc/server';
23
import { createTRPCProxyClient } from '@trpc/client';
34
import type { AppRouter } from '@windingtree/sdk-node-api/router';
45

5-
export interface NodeContextData {
6-
node?: ReturnType<typeof createTRPCProxyClient<AppRouter>> | undefined;
6+
export interface NodeContextData<TRouter extends AnyRouter = AppRouter> {
7+
node?: ReturnType<typeof createTRPCProxyClient<TRouter>> | undefined;
78
nodeConnected: boolean;
89
nodeError?: string;
910
}
@@ -12,8 +13,10 @@ export const NodeContext = createContext<NodeContextData>(
1213
{} as NodeContextData,
1314
);
1415

15-
export const useNode = () => {
16-
const context = useContext(NodeContext);
16+
export const useNode = <TRouter extends AnyRouter = AppRouter>() => {
17+
const context = useContext<NodeContextData<TRouter>>(
18+
NodeContext as Context<NodeContextData<TRouter>>,
19+
);
1720

1821
if (context === undefined) {
1922
throw new Error('useNode must be used within a "NodeContext"');

packages/react/src/providers/NodeProvider/index.tsx

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,82 @@
1-
import {
2-
CreateTRPCClientOptions,
3-
createTRPCProxyClient,
4-
httpBatchLink,
5-
} from '@trpc/client';
1+
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
62
import superjson from 'superjson';
7-
import { PropsWithChildren, useState, useEffect } from 'react';
3+
import {
4+
type PropsWithChildren,
5+
useState,
6+
useEffect,
7+
useCallback,
8+
} from 'react';
89
import { NodeContext } from './NodeProviderContext.js';
10+
import { AppRouter } from '@windingtree/sdk-node-api/router';
911
import { unauthorizedLink } from '@windingtree/sdk-node-api/client';
10-
import type { AppRouter } from '@windingtree/sdk-node-api/router';
1112
import { useConfig } from '../ConfigProvider/ConfigProviderContext.js';
13+
import { usePoller } from '../../hooks/usePoller.js';
14+
import { createLogger } from '@windingtree/sdk-logger';
15+
16+
// Initialize logger
17+
const logger = createLogger('NodeProvider');
1218

1319
export const NodeProvider = ({ children }: PropsWithChildren) => {
14-
const { nodeHost, setAuth, resetAuth } = useConfig();
20+
const { nodeHost, resetAuth } = useConfig();
1521
const [node, setNode] = useState<
1622
ReturnType<typeof createTRPCProxyClient<AppRouter>> | undefined
1723
>();
1824
const [error, setError] = useState<string | undefined>();
25+
const [isConnected, setIsConnected] = useState<boolean>(false);
1926

20-
const stopClient = () => {
21-
try {
22-
setError(() => undefined);
23-
setNode(() => undefined);
24-
} catch (error) {
25-
setError((error as Error).message || 'Unknown node provider error');
26-
}
27-
};
27+
// Function to stop and reset the client
28+
const stopClient = useCallback(() => {
29+
setError(undefined);
30+
setNode(undefined);
31+
}, []);
2832

29-
useEffect(() => {
30-
if (!nodeHost) {
31-
stopClient();
33+
// Function to check the connection
34+
const checkConnection = useCallback(async () => {
35+
setError(undefined);
36+
37+
if (!node) {
38+
setIsConnected(false);
3239
return;
3340
}
3441

42+
try {
43+
const { message } = await node.service.ping.query();
44+
setIsConnected(message === 'pong');
45+
} catch (err) {
46+
setIsConnected(false);
47+
setError('Unable to connect the Node');
48+
logger.error(err);
49+
}
50+
}, [node]);
51+
52+
// Initialize and clean up the client
53+
useEffect(() => {
3554
const startClient = async () => {
36-
try {
37-
setError(undefined);
55+
if (!nodeHost) {
56+
return;
57+
}
3858

59+
try {
3960
const tRpcNode = createTRPCProxyClient<AppRouter>({
40-
transformer:
41-
superjson as unknown as CreateTRPCClientOptions<AppRouter>['transformer'],
61+
transformer: superjson,
4262
links: [
4363
unauthorizedLink(resetAuth),
4464
httpBatchLink({
4565
url: nodeHost,
4666
fetch(url, options) {
4767
return fetch(url, {
4868
...options,
49-
// allows to send cookies to the server
5069
credentials: 'include',
5170
});
5271
},
5372
}),
5473
],
5574
});
5675

57-
const { message } = await tRpcNode.service.ping.query();
58-
59-
if (message === 'pong') {
60-
setNode(() => tRpcNode);
61-
}
62-
} catch (error) {
63-
console.log(error);
64-
setNode(() => undefined);
65-
let errMessage = (error as Error).message;
66-
67-
if (errMessage === 'Failed to fetch') {
68-
errMessage = 'Node connection failed';
69-
}
70-
71-
setError(() => errMessage || 'Unknown node provider error');
76+
setNode(() => tRpcNode);
77+
} catch (err) {
78+
setError((err as Error).message || 'Unknown node provider error');
79+
logger.error(err);
7280
}
7381
};
7482

@@ -77,13 +85,16 @@ export const NodeProvider = ({ children }: PropsWithChildren) => {
7785
return () => {
7886
stopClient();
7987
};
80-
}, [nodeHost, setAuth, resetAuth]);
88+
}, [stopClient, resetAuth, nodeHost]);
89+
90+
// Polling for connection check
91+
usePoller(checkConnection, 5000, true, 'NodeConnection');
8192

8293
return (
8394
<NodeContext.Provider
8495
value={{
8596
node,
86-
nodeConnected: Boolean(node),
97+
nodeConnected: isConnected,
8798
nodeError: error,
8899
}}
89100
>

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)