hedera-sdk-js icon indicating copy to clipboard operation
hedera-sdk-js copied to clipboard

feat: example of polling mirror node to simulate TopicMessageQuery

Open ivaylonikolov7 opened this issue 4 months ago • 2 comments
trafficstars

Description: Polling mirror node rest api to stimulate TopicMessageQuery

Notes for reviewer:

Checklist

  • [x] Documented (Code comments, README, etc.)
  • [x] Tested (unit, integration, etc.)

ivaylonikolov7 avatar Jun 24 '25 15:06 ivaylonikolov7

:tada: Snyk checks have passed. No issues have been found so far.

:white_check_mark: security/snyk check is complete. No issues have been found. (View Details)

lfdt-bot avatar Jun 24 '25 15:06 lfdt-bot

Codecov Report

:white_check_mark: All modified and coverable lines are covered by tests. see 1 file with indirect coverage changes

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Jun 24 '25 15:06 codecov[bot]

@CodiumAI-Agent /improve

ivaylonikolov7 avatar Jul 07 '25 03:07 ivaylonikolov7

PR Code Suggestions ✨

Latest suggestions up to 9e1b4d3

CategorySuggestion                                                                                                                                    Impact
General
Manage emitter subscriptions in useEffect

Instantiate the emitter once with a ref and register the listener inside a useEffect
to avoid adding a new listener on every render, and clean it up on unmount. This
prevents memory leaks and duplicate event handling.

examples/frontend-examples/src/app/components/TopicListener.js [8-31]

-const dataEmitter = new EventEmitter();
-dataEmitter.on("newMessages", (message) => {
-    setMessages((prevMessages) => [...prevMessages, message]);
-    setLastUpdate(new Date());
-});
+const dataEmitterRef = useRef(new EventEmitter());
 
+useEffect(() => {
+    const emitter = dataEmitterRef.current;
+    const handler = (message) => {
+        setMessages((prev) => [...prev, message]);
+        setLastUpdate(new Date());
+    };
+    emitter.on("newMessages", handler);
+    return () => {
+        emitter.off("newMessages", handler);
+    };
+}, []);
+
Suggestion importance[1-10]: 9

__

Why: The dataEmitter.on call and new EventEmitter() instantiation occur on every render, causing duplicate listeners and memory leaks; moving them into a useEffect with a ref ensures a single subscription.

High
Possible issue
Use browser-compatible base64 decoding

Replace the Node.js Buffer-based decoding with a browser-compatible approach using
atob and TextDecoder to avoid runtime errors in the client. This ensures the base64
message is decoded correctly in the browser.

examples/frontend-examples/src/app/components/TopicListener.js [179-182]

-const decodedMessage = Buffer.from(
-    newMessages.message,
-    "base64",
-).toString("utf-8");
+const decodedMessage = new TextDecoder().decode(
+  Uint8Array.from(
+    atob(newMessages.message),
+    (c) => c.charCodeAt(0)
+  )
+);
Suggestion importance[1-10]: 8

__

Why: Using Buffer.from in a client-side React component can break in browsers; replacing it with atob and TextDecoder prevents runtime errors and ensures correct decoding.

Medium
Add error handling to polling loop

Wrap the polling logic in a try/catch inside the interval callback to handle network
or JSON parsing errors gracefully and prevent unhandled promise rejections from
crashing the app.

examples/frontend-examples/src/app/components/TopicListener.js [161-186]

 pollingIntervalRef.current = setInterval(async () => {
-    const res = await fetch(`${BASE_URL}/api/v1/topics/${topicId}/messages`);
-    const data = await res.json();
-    // ...process messages...
+    try {
+        const res = await fetch(`${BASE_URL}/api/v1/topics/${topicId}/messages`);
+        const data = await res.json();
+        // ...process messages...
+    } catch (err) {
+        console.error("Polling error:", err);
+    }
 }, POLLING_INTERVAL);
Suggestion importance[1-10]: 6

__

Why: Wrapping the fetch and res.json() calls in a try/catch inside the interval prevents unhandled promise rejections and improves resilience, though it’s a non-blocking enhancement.

Low

Previous suggestions

Suggestions up to commit b8df467
CategorySuggestion                                                                                                                                    Impact
Security
Remove client-side key usage

Exposing the private operator key on the client is a security risk; move Hedera
transaction signing to a secure server or cloud function and only expose
non-sensitive data to the frontend.

examples/frontend-examples/src/app/topic/message-query/page.js [25-27]

-const operatorKey = PrivateKey.fromStringED25519(
-    process.env.NEXT_PUBLIC_OPERATOR_KEY,
-);
+// On client: call secure API endpoint instead of using PrivateKey directly
+// Example:
+// const response = await fetch('/api/create-topic', { method: 'POST' });
+// const { topicId } = await response.json();
+// setTopicId(topicId);
Suggestion importance[1-10]: 9

__

Why: Exposing the operator private key in the client is a critical security vulnerability and should be moved to a secure backend.

High
Possible issue
Prevent listener re-registration

Move emitter instantiation into a stable ref and subscribe inside a useEffect with
cleanup to prevent re-registering listeners on every render and avoid memory leaks.

examples/frontend-examples/src/app/components/TopicListener.js [8-23]

-const dataEmitter = new EventEmitter();
-dataEmitter.on("newMessages", (message) => {
-    setMessages((prevMessages) => [...prevMessages, message]);
+const dataEmitter = useRef(new EventEmitter()).current;
+
+useEffect(() => {
+  const handler = (message) => {
+    setMessages(prev => [...prev, message]);
     setLastUpdate(new Date());
-});
+  };
+  dataEmitter.on("newMessages", handler);
+  return () => {
+    dataEmitter.off("newMessages", handler);
+  };
+}, [dataEmitter]);
Suggestion importance[1-10]: 8

__

Why: This prevents the EventEmitter listener from being re-registered on every render, avoiding memory leaks and duplicate message handling.

Medium
Emit all new messages

When new batches have more than one message, only the first one is processed;
iterate over the slice to emit all new messages and update lastMessagesLength after
processing.

examples/frontend-examples/src/app/components/TopicListener.js [171-177]

-const newMessages = data.messages.slice(lastMessagesLength)[0];
-const decodedMessage = Buffer.from(
-    newMessages.message,
-    "base64",
-).toString("utf-8");
-dataEmitter.emit("newMessages", decodedMessage);
+const newBatch = data.messages.slice(lastMessagesLength);
+newBatch.forEach(msg => {
+  const decoded = Buffer.from(msg.message, "base64").toString("utf-8");
+  dataEmitter.emit("newMessages", decoded);
+});
 lastMessagesLength = currentMessagesLength;
Suggestion importance[1-10]: 8

__

Why: Looping over the new messages ensures no incoming messages are dropped, fixing a functional bug when multiple messages arrive in one poll.

Medium
General
Support polling cancellation

Add an AbortController signal to pollMirrorNode and break the loop when the
component unmounts to avoid runaway polling. Pass the controller’s signal to the
function and check signal.aborted in the loop.

examples/frontend-examples/src/app/components/TopicListener.js [147-152]

-async function pollMirrorNode(dataEmitter, topicId) {
+async function pollMirrorNode(dataEmitter, topicId, signal) {
     let lastMessagesLength = 0;
 
-    // eslint-disable-next-line no-constant-condition
-    while (true) {
+    while (!signal.aborted) {
         // ...
     }
 }
 
+// In the component:
+useEffect(() => {
+  const controller = new AbortController();
+  pollMirrorNode(dataEmitter, topicId, controller.signal);
+  return () => controller.abort();
+}, [dataEmitter, topicId]);
+
Suggestion importance[1-10]: 7

__

Why: Adding an AbortController allows the infinite polling loop to be cleanly stopped on unmount, preventing runaway requests but is a lower-severity enhancement.

Medium

CodiumAI-Agent avatar Jul 07 '25 03:07 CodiumAI-Agent