Skip to content

feat: example of polling mirror node to simulate TopicMessageQuery #3180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

ivaylonikolov7
Copy link
Contributor

Description:
Polling mirror node rest api to stimulate TopicMessageQuery

Notes for reviewer:

Checklist

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

@lfdt-bot
Copy link
Contributor

lfdt-bot commented Jun 24, 2025

🎉 Snyk checks have passed. No issues have been found so far.

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

Copy link

codecov bot commented Jun 24, 2025

Codecov Report

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

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

@ivaylonikolov7 ivaylonikolov7 force-pushed the feat/polling-mirror-node branch 2 times, most recently from 84c7896 to e33ac81 Compare June 24, 2025 20:49
@ivaylonikolov7 ivaylonikolov7 marked this pull request as ready for review July 3, 2025 21:19
@ivaylonikolov7 ivaylonikolov7 requested review from a team as code owners July 3, 2025 21:19
@ivaylonikolov7 ivaylonikolov7 requested a review from agadzhalov July 3, 2025 21:19
@ivaylonikolov7
Copy link
Contributor Author

@CodiumAI-Agent /improve

@CodiumAI-Agent
Copy link

PR Code Suggestions ✨

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

Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
@ivaylonikolov7 ivaylonikolov7 force-pushed the feat/polling-mirror-node branch from b8df467 to d91dba5 Compare July 28, 2025 08:59
let lastMessagesLength = 0;

// eslint-disable-next-line no-constant-condition
while (true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider using a setInterval stored in a constant so it can be cleared when needed. As it stands, this while (true) loop will run indefinitely once executed, which isn’t ideal for an example. If this is meant to demonstrate polling behavior, it's better to follow best practices by including a way to stop or clean up the polling loop.

Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
function pollUntilReady(dataEmitter, topicId) {
let lastMessagesLength = 0;

setInterval(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about assigning this interval to a timer variable that is passed as a dependency trought the function so it can be cleared when polling is no longer needed (for example when the user navigates from a page). WDYT?

Copy link
Contributor Author

@ivaylonikolov7 ivaylonikolov7 Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eslint cries that we have added a constant that doesn't get used. I think the user can handle this and will set it to a constant and pass it to clearInterval when needed.

lastMessagesLength = currentMessagesLength;
}
})().catch(console.error); // Handle any errors from the async function
}, 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about assign this to a const variable e.g. something like POLLING_INTERVAL?

Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Signed-off-by: Ivaylo Nikolov <ivaylo.nikolov@limechain.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants