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.)

: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)
: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.
PR Code Suggestions ✨
Latest suggestions up to 9e1b4d3
| Category | Suggestion | 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
| Category | Suggestion | 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
|