Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-birds-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dojoengine/grpc": patch
---

feat(grpc): gracefully handle subscription network error
22 changes: 11 additions & 11 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "dojo.js",
Expand Down Expand Up @@ -29,6 +28,7 @@
"dependencies": {
"@dojoengine/core": "workspace:*",
"@dojoengine/create-burner": "workspace:*",
"@dojoengine/grpc": "workspace:*",
"@dojoengine/react": "workspace:*",
"@dojoengine/recs": "2.0.13",
"@dojoengine/sdk": "workspace:*",
Expand Down Expand Up @@ -63,7 +63,7 @@
},
"packages/core": {
"name": "@dojoengine/core",
"version": "1.8.3",
"version": "1.8.5",
"bin": {
"compile-abi": "./dist/cli/compile-abi.js",
},
Expand All @@ -83,7 +83,7 @@
},
"packages/create-burner": {
"name": "@dojoengine/create-burner",
"version": "1.8.3",
"version": "1.8.6",
"dependencies": {
"@dojoengine/core": "workspace:*",
"@scure/bip32": "^1.5.0",
Expand Down Expand Up @@ -118,7 +118,7 @@
},
"packages/create-dojo": {
"name": "@dojoengine/create-dojo",
"version": "1.8.2",
"version": "1.8.3",
"bin": "./dist/index.cjs",
"dependencies": {
"@inquirer/prompts": "^3.3.2",
Expand All @@ -142,7 +142,7 @@
},
"packages/grpc": {
"name": "@dojoengine/grpc",
"version": "1.8.3",
"version": "1.8.4",
"dependencies": {
"@grpc/grpc-js": "^1.13.4",
"@protobuf-ts/grpcweb-transport": "^2.11.1",
Expand All @@ -169,7 +169,7 @@
},
"packages/internal": {
"name": "@dojoengine/internal",
"version": "1.7.3",
"version": "1.7.6",
"dependencies": {
"@dojoengine/core": "workspace:*",
"@dojoengine/torii-client": "catalog:",
Expand All @@ -189,7 +189,7 @@
},
"packages/predeployed-connector": {
"name": "@dojoengine/predeployed-connector",
"version": "1.8.2",
"version": "1.8.3",
"dependencies": {
"@starknet-io/types-js": "catalog:",
},
Expand All @@ -215,7 +215,7 @@
},
"packages/react": {
"name": "@dojoengine/react",
"version": "1.8.2",
"version": "1.8.4",
"dependencies": {
"@dojoengine/recs": "2.0.13",
"@dojoengine/state": "workspace:*",
Expand Down Expand Up @@ -250,7 +250,7 @@
},
"packages/sdk": {
"name": "@dojoengine/sdk",
"version": "1.8.4",
"version": "1.8.8",
"dependencies": {
"@dojoengine/core": "workspace:*",
"@dojoengine/grpc": "workspace:*",
Expand Down Expand Up @@ -294,7 +294,7 @@
},
"packages/state": {
"name": "@dojoengine/state",
"version": "1.8.2",
"version": "1.8.4",
"dependencies": {
"@dojoengine/recs": "2.0.13",
"@dojoengine/torii-client": "catalog:",
Expand All @@ -313,7 +313,7 @@
},
"packages/utils": {
"name": "@dojoengine/utils",
"version": "1.8.2",
"version": "1.8.3",
"dependencies": {
"@dojoengine/recs": "2.0.13",
"@latticexyz/utils": "^2.2.8",
Expand Down
70 changes: 57 additions & 13 deletions example/frameworks/react/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,68 @@
import { StrictMode, useEffect, useState } from "react";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";

import App from "./App";
import { DojoProvider } from "@dojoengine/core";
import manifest from "../../../core/manifest_dev.json" with { type: "json" };
import { createDojoConfig } from "@dojoengine/core";
const dojoConfig = createDojoConfig({ manifest: manifest });
import { init, KeysClause, ToriiQueryBuilder } from "@dojoengine/sdk";
import { Subscription } from "@dojoengine/grpc";
import { constants } from "starknet";

import manifest from "./manifest_sepolia.json" with { type: "json" };
const dojoConfig = createDojoConfig({
manifest: manifest,
toriiUrl: "https://api.cartridge.gg/x/nums-bal/torii",
});

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]);
Comment on lines +16 to +36
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.


function Root() {
const provider = new DojoProvider(dojoConfig.manifest);
return <div>Hello Dojo</div>;
}

const container = document.getElementById("root");
if (!container) {
throw new Error("Root element not found");
async function main() {
const sdk = await init({
client: {
toriiUrl: dojoConfig.toriiUrl,
worldAddress: dojoConfig.manifest.world.address,
},
domain: {
name: "nums",
version: "1.0",
chainId: constants.StarknetChainId.SN_SEPOLIA,
revision: "1",
},
});

const container = document.getElementById("root");
if (!container) {
throw new Error("Root element not found");
}

createRoot(container).render(
<StrictMode>
<Root sdk={sdk} />
</StrictMode>
);
}

createRoot(container).render(
<StrictMode>
<Root />
</StrictMode>
);
main().catch(console.error);
Loading
Loading