Skip to content

Conversation

@MartianGreed
Copy link
Collaborator

@MartianGreed MartianGreed commented Nov 14, 2025

Closes #

Introduced changes

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added relevant tests
  • Add a dedicated CI job for new examples
  • Performed self-review of the code

Summary by CodeRabbit

  • New Features

    • gRPC subscriptions now gracefully handle network errors with automatic reconnection support.
  • Dependencies

    • Added @dojoengine/grpc package.
  • Documentation

    • Updated React example demonstrating SDK initialization and entity subscription workflows.

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

Walkthrough

The changes add graceful network error handling to the gRPC subscription layer with automatic reconnection logic. A React example is updated to demonstrate full SDK initialization and subscription management. The @dojoengine/grpc package is added as a workspace dependency.

Changes

Cohort / File(s) Summary
Changeset & Dependencies
.changeset/tall-birds-thank.md, example/package.json
Added changeset documenting graceful handling of network errors in gRPC subscriptions (patch bump). Added @dojoengine/grpc as workspace dependency in example project.
React Example Integration
example/frameworks/react/src/main.tsx
Replaced minimal UI bootstrap with complete Dojo SDK initialization flow. Added SDK setup with toriiUrl/worldAddress configuration, subscription management via ToriiQueryBuilder and KeysClause, and lifecycle binding to component mount/unmount. Switched manifest source to manifest_sepolia.json.
gRPC Client Enhancement
packages/grpc/src/torii-client.ts
Introduced setupStream function for stream lifecycle management with network error detection and reconnection logic. Added per-subscription state tracking (lastMessage) and reconnect callback. Implemented automatic stream recovery on network failures with message re-delivery on reconnection.

Sequence Diagram

sequenceDiagram
    participant App as React App
    participant Sub as ToriiSubscription
    participant Stream as gRPC Stream
    participant Handler as StreamHandler

    App->>Sub: initialize subscription<br/>(setupStream(false))
    Sub->>Stream: create stream
    Stream->>Handler: onMessage
    Handler->>Sub: cache lastMessage
    
    alt Network Error
        Stream->>Handler: onError (network detected)
        Handler->>Sub: trigger reconnect<br/>(setupStream(true))
        Sub->>Stream: create new stream
        Stream->>Handler: onReconnect callback
        Handler->>Sub: re-deliver lastMessage
    else Completion
        Stream->>Handler: onComplete
        Handler->>Sub: cleanup & delete
    end
    
    App->>Sub: unmount/cancel
    Sub->>Sub: set cancellation flag
    Sub->>Stream: cleanup
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • packages/grpc/src/torii-client.ts — Review the reconnection logic, error detection heuristics, and state management (lastMessage caching, cancellation flag). Verify setupStream correctly handles stream lifecycle across reconnects.
  • example/frameworks/react/src/main.tsx — Examine SDK initialization sequence, subscription wiring with KeysClause/ToriiQueryBuilder, and useEffect cleanup for subscription lifecycle binding.
  • Cross-cutting concern — Ensure error handling doesn't mask non-network failures and that reconnection doesn't leak resources on cancellation.

Poem

🐰 A stream once broke, but now it's wise,
With reconnect logic, it'll rise!
From gRPC nodes to React views so bright,
The network stumbles—no more fright! 🌟
Graceful recovery, messages flow anew,
The Dojo SDK's dream comes true! 🚀

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is empty, containing only the template structure with no actual content filled in. All sections lack meaningful information and the checklist is entirely unchecked. Fill in the 'Introduced changes' section with a summary of the modifications, link any relevant GitHub issues, and update the checklist to reflect completed tasks.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding graceful handling of subscription network errors in the grpc module.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/grpcnetworkerror

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/grpc/src/torii-client.ts (1)

335-418: Subscription cancel() does not stop the underlying stream, and the suggested fix will not work

The core issue is real: cancel() only flips isCancelled without terminating the ServerStreamingCall, allowing the server stream to consume resources. However, the @protobuf-ts/runtime-rpc library removed RPC.cancel(); you must use AbortSignal instead via RpcOptions.

The suggested subscription.stream.cancel() call will fail because that method does not exist on ServerStreamingCall.

The correct approach requires:

  1. Create an AbortController when setting up each stream
  2. Pass its signal via RpcOptions to all createStream() calls (e.g., in subscribeEntities, subscribeTokens, etc.)
  3. Store the controller so cancel() can call abortController.abort()
  4. Add the isCancelled guard in onMessage as originally suggested

This is more involved than the diff indicates—it requires modifying how each subscription method wires cancellation through to the underlying client calls.

🧹 Nitpick comments (1)
packages/grpc/src/torii-client.ts (1)

171-176: Per‑subscription lastMessage tracking is reasonable

Storing the last response on ToriiSubscription for replay on reconnect is a sensible approach; the generic object type is acceptable here, though you could tighten it later by threading TRes through ToriiSubscription if you decide to make it strongly typed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e008283 and 3ec0a91.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • .changeset/tall-birds-thank.md (1 hunks)
  • example/frameworks/react/src/main.tsx (1 hunks)
  • example/package.json (1 hunks)
  • packages/grpc/src/torii-client.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
example/frameworks/react/src/main.tsx (3)
packages/core/src/config/index.ts (1)
  • createDojoConfig (25-39)
packages/grpc/src/torii-client.ts (1)
  • Subscription (184-206)
packages/core/src/provider/DojoProvider.ts (2)
  • DojoProvider (485-485)
  • DojoProvider (493-494)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-examples
  • GitHub Check: build-packages
  • GitHub Check: build
🔇 Additional comments (6)
packages/grpc/src/torii-client.ts (2)

138-138: Runtime import of ServerStreamingCall looks correct

Switching ServerStreamingCall to a value import is appropriate since you now store and use the streaming call instance at runtime.


208-214: onReconnect hook integrates cleanly

Adding onReconnect to StreamHandlerOptions and invoking it in setupStream(true) provides a clear extension point for callers without affecting existing behavior.

example/package.json (1)

25-40: Adding @dojoengine/grpc dependency is consistent with usage

Including @dojoengine/grpc as a workspace dependency matches the new imports in the React example and keeps the example package aligned with the SDK/GRPC integration.

.changeset/tall-birds-thank.md (1)

1-5: Changeset accurately documents the grpc behavior change

The patch entry and summary line clearly describe the new graceful handling of subscription network errors for @dojoengine/grpc.

example/frameworks/react/src/main.tsx (2)

1-15: Imports and Dojo config wiring look consistent

The new SDK/grpc/starknet imports and the createDojoConfig usage with manifest_sepolia.json and explicit toriiUrl are coherent with the intended Sepolia/torii setup.


42-68: SDK initialization and rendering flow are sound

Wrapping SDK initialization in main(), passing dojoConfig’s torii/worldAddress, and rendering <Root sdk={sdk} /> inside StrictMode is a clear and idiomatic bootstrap for the example.

Comment on lines +16 to +36
function Root({ sdk }) {
const [sub, setSub] = useState<Subscription | null>(null);

const subscription = useCallback(async () => {
return await sdk.subscribeEntityQuery({
query: new ToriiQueryBuilder().withClause(
KeysClause([], [], "VariableLen").build()
),
callback: ({ data, error }) => {
console.log(data, error);
},
});
}, [sdk]);
useEffect(() => {
subscription().then(([data, sub]) => setSub(sub));
return () => {
if (sub) {
sub.cancel();
}
};
}, [sdk]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Effect cleanup never sees the latest subscription; leads to leaked subscriptions

In Root, sub is stored in state but not included in the useEffect dependency array, so the cleanup function always captures the initial sub value (null) and never cancels the actual subscription. This can leave the grpc subscription running after unmount or re‑render.

A simple fix is to keep the subscription in a ref and drive the effect off the memoized subscribe function instead of state:

-import { StrictMode, useCallback, useEffect, useState } from "react";
+import { StrictMode, useCallback, useEffect, useRef } from "react";
@@
-function Root({ sdk }) {
-    const [sub, setSub] = useState<Subscription | null>(null);
-
-    const subscription = useCallback(async () => {
-        return await sdk.subscribeEntityQuery({
+function Root({ sdk }) {
+    const subRef = useRef<Subscription | null>(null);
+
+    const subscribe = useCallback(async () => {
+        const [, sub] = await sdk.subscribeEntityQuery({
             query: new ToriiQueryBuilder().withClause(
                 KeysClause([], [], "VariableLen").build()
             ),
             callback: ({ data, error }) => {
                 console.log(data, error);
             },
         });
-    }, [sdk]);
-    useEffect(() => {
-        subscription().then(([data, sub]) => setSub(sub));
-        return () => {
-            if (sub) {
-                sub.cancel();
-            }
-        };
-    }, [sdk]);
+        subRef.current = sub;
+    }, [sdk]);
+
+    useEffect(() => {
+        subscribe().catch(console.error);
+        return () => {
+            subRef.current?.cancel();
+        };
+    }, [subscribe]);

This ensures the most recent subscription is always cancelled on cleanup and avoids re‑subscribing due to state changes.


I'll verify the review comment by examining the actual code in the repository.


---

🏁 Script executed:

```shell
#!/bin/bash
# Locate and examine the file in question
fd -t f "main.tsx" --path "*/react/src/*"

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Extract the relevant section from the file
if [ -f "example/frameworks/react/src/main.tsx" ]; then
  sed -n '16,36p' "example/frameworks/react/src/main.tsx"
fi

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Get broader context around the function to understand imports and full structure
if [ -f "example/frameworks/react/src/main.tsx" ]; then
  cat "example/frameworks/react/src/main.tsx"
fi

Length of output: 1960


Fix memory leak in useEffect cleanup—subscription never gets cancelled

The bug is confirmed. The cleanup function captures the initial sub value (null) due to missing sub in the dependency array, so the actual subscription object returned by subscription().then() is never cancelled. This leaves the grpc connection open after component unmount or re-render.

The suggested refactor using useRef is the correct approach. Apply the provided diff or equivalent to ensure the most recent subscription is stored in a ref and properly cleaned up on unmount.

🤖 Prompt for AI Agents
In example/frameworks/react/src/main.tsx around lines 16 to 36, the useEffect
cleanup captures a stale `sub` (initial null) because `sub` is not tracked in
deps, so the real subscription never gets cancelled; fix by replacing the local
state subscription tracking with a useRef (e.g., const subRef =
useRef<Subscription | null>(null)), when the subscription promise resolves
assign subRef.current = sub (and cancel any previous subRef.current before
replacing), update the cleanup to cancel subRef.current, and adjust the effect
deps to include only the stable subscription callback (or sdk) so the latest ref
is always used for cleanup.

@MartianGreed MartianGreed merged commit ccd73c9 into main Nov 14, 2025
10 checks passed
@MartianGreed MartianGreed deleted the feat/grpcnetworkerror branch November 14, 2025 17:56
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.

2 participants