Skip to content
Open
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
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

95 changes: 95 additions & 0 deletions app/Viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client"

import { useEffect, useRef } from "react"
import createImageIdsAndCacheMetaData from "../lib/createImageIdsAndCacheMetaData"
import {
RenderingEngine,
Enums,
type Types,
volumeLoader,
} from "@cornerstonejs/core"
import { init as csRenderInit } from "@cornerstonejs/core"
import { init as csToolsInit } from "@cornerstonejs/tools"
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"


function Viewer() {
const elementRef = useRef<HTMLDivElement>(null)
const running = useRef(false)

useEffect(() => {
const setup = async () => {
if (running.current) {
return
}
running.current = true

await csRenderInit()
await csToolsInit()
dicomImageLoaderInit({ maxWebWorkers: 1 })

// Get Cornerstone imageIds and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
SeriesInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
})

// Instantiate a rendering engine
const renderingEngineId = "myRenderingEngine"
const renderingEngine = new RenderingEngine(renderingEngineId)
const viewportId = "CT"

const viewportInput = {
viewportId,
type: Enums.ViewportType.ORTHOGRAPHIC,
element: elementRef.current,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
},
}

renderingEngine.enableElement(viewportInput)

// Get the stack viewport that was created
const viewport = renderingEngine.getViewport(
viewportId
) as Types.IVolumeViewport

// Define a volume in memory
const volumeId = "streamingImageVolume"
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
})

// Set the volume to load
// @ts-ignore
volume.load()

// Set the volume on the viewport and it's default properties
await viewport.setVolumes([{ volumeId }])

// Render the image
viewport.render()
}

setup()

// Create a stack viewport
}, [elementRef, running])
Comment on lines +20 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect app/Viewer.tsx around the referenced lines
ls -la
sed -n '1,200p' app/Viewer.tsx

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 3937


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any existing cleanup/destroy patterns for RenderingEngine
rg -n "RenderingEngine|destroy\(\)|enableElement\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' app/polyseg/Viewer.tsx

echo "---- destroy() usages ----"
rg -n "destroy\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 6148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- app/Viewer.tsx: useEffect cleanup / return () ----"
rg -n "useEffect\\(|return \\(\\) =>|return \\(\\s*\\)" -S app/Viewer.tsx

echo "---- app/polyseg/Viewer.tsx: effect cleanup patterns ----"
rg -n "useEffect\\(|return \\(\\) =>|return \\(\\s*\\)|destroy\\(" -S app/polyseg/Viewer.tsx

echo "---- repo-wide destroy() search (should confirm none) ----"
rg -n "destroy\\(" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "myRenderingEngine" -S app lib

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the RenderingEngine implementation in `@cornerstonejs/core`
rg -n "class RenderingEngine" node_modules/@cornerstonejs/core -S || true
rg -n "RenderingEngine.*destroy\\(|destroy\\(\\).*RenderingEngine|destroy\\(" node_modules/@cornerstonejs/core -S | head -n 50

Repository: cornerstonejs/nextjs-cornerstone3d

Length of output: 2532


Return cleanup from the effect + reset running.current on init failure to avoid leaks and “stuck” init.

In app/Viewer.tsx, running.current is set to true and never cleared if any awaited init step throws (no try/catch), so the viewer can get stuck in the “already running” state. Also, the effect doesn’t return a cleanup, so the created RenderingEngine is never torn down on unmount—yet RenderingEngine in @cornerstonejs/core provides a destroy() API and the repo currently has no destroy() calls.

♻️ Proposed fix
   useEffect(() => {
+    let cancelled = false
+    let renderingEngine: RenderingEngine | undefined
+
     const setup = async () => {
-      if (running.current) {
+      if (running.current || !elementRef.current) {
         return
       }
+
       running.current = true
-
-      await csRenderInit()
-      await csToolsInit()
-      dicomImageLoaderInit({ maxWebWorkers: 1 })
-
-      // Get Cornerstone imageIds and fetch metadata into RAM
-      const imageIds = await createImageIdsAndCacheMetaData({
-        StudyInstanceUID:
-          "1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
-        SeriesInstanceUID:
-          "1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
-        wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
-      })
-
-      // Instantiate a rendering engine
-      const renderingEngineId = "myRenderingEngine"
-      const renderingEngine = new RenderingEngine(renderingEngineId)
-      const viewportId = "CT"
-
-      const viewportInput = {
-        viewportId,
-        type: Enums.ViewportType.ORTHOGRAPHIC,
-        element: elementRef.current,
-        defaultOptions: {
-          orientation: Enums.OrientationAxis.SAGITTAL,
-        },
-      }
-
-      renderingEngine.enableElement(viewportInput)
-
-      // Get the stack viewport that was created
-      const viewport = renderingEngine.getViewport(
-        viewportId
-      ) as Types.IVolumeViewport
-
-      // Define a volume in memory
-      const volumeId = "streamingImageVolume"
-      const volume = await volumeLoader.createAndCacheVolume(volumeId, {
-        imageIds,
-      })
-
-      // Set the volume to load
-      // `@ts-ignore`
-      volume.load()
-
-      // Set the volume on the viewport and it's default properties
-      await viewport.setVolumes([{ volumeId }])
-
-      // Render the image
-      viewport.render()
+
+      try {
+        await csRenderInit()
+        await csToolsInit()
+        dicomImageLoaderInit({ maxWebWorkers: 1 })
+
+        const imageIds = await createImageIdsAndCacheMetaData({
+          StudyInstanceUID:
+            "1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
+          SeriesInstanceUID:
+            "1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
+          wadoRsRoot: "https://d14fa38qiwhyfd.cloudfront.net/dicomweb",
+        })
+
+        const renderingEngineId = "myRenderingEngine"
+        renderingEngine = new RenderingEngine(renderingEngineId)
+        const viewportId = "CT"
+
+        renderingEngine.enableElement({
+          viewportId,
+          type: Enums.ViewportType.ORTHOGRAPHIC,
+          element: elementRef.current,
+          defaultOptions: {
+            orientation: Enums.OrientationAxis.SAGITTAL,
+          },
+        })
+
+        const viewport = renderingEngine.getViewport(
+          viewportId
+        ) as Types.IVolumeViewport
+
+        const volumeId = "streamingImageVolume"
+        const volume = await volumeLoader.createAndCacheVolume(volumeId, {
+          imageIds,
+        })
+
+        // `@ts-ignore`
+        volume.load()
+
+        if (cancelled) {
+          return
+        }
+
+        await viewport.setVolumes([{ volumeId }])
+
+        if (!cancelled) {
+          viewport.render()
+        }
+      } catch (error) {
+        running.current = false
+        throw error
+      }
     }
 
-    setup()
+    void setup()
 
-    // Create a stack viewport
-  }, [elementRef, running])
+    return () => {
+      cancelled = true
+      running.current = false
+      renderingEngine?.destroy()
+    }
+  }, [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Viewer.tsx` around lines 20 - 81, Wrap the async init in a try/finally so
running.current is reset on any error (set running.current = true before try,
set to false in finally) and add a cleanup function returned from the useEffect
that tears down the created resources: call renderingEngine.destroy() (and undo
enableElement if needed) and stop/cleanup the volume/loader if applicable;
locate the async setup in Viewer.tsx (the setup function inside useEffect), the
running ref (running.current), the RenderingEngine instance (new
RenderingEngine(renderingEngineId) / renderingEngine.enableElement), and the
created volume (volume.load / volumeId) and ensure those are cleaned up in the
returned cleanup callback and on init failure.


return (
<div
ref={elementRef}
style={{
width: "512px",
height: "512px",
backgroundColor: "#000",
}}
></div>
)
}

export default Viewer
4 changes: 1 addition & 3 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

:root {
--foreground-rgb: 0, 0, 0;
Expand Down
94 changes: 4 additions & 90 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,9 @@
"use client"

import { useEffect, useRef } from "react"
import createImageIdsAndCacheMetaData from "../lib/createImageIdsAndCacheMetaData"
import {
RenderingEngine,
Enums,
type Types,
volumeLoader,
} from "@cornerstonejs/core"
import { init as csRenderInit } from "@cornerstonejs/core"
import { init as csToolsInit } from "@cornerstonejs/tools"
import { init as dicomImageLoaderInit } from "@cornerstonejs/dicom-image-loader"
import dynamic from "next/dynamic"

const Viewer = dynamic(() => import("./Viewer"), { ssr: false })

function App() {
const elementRef = useRef<HTMLDivElement>(null)
const running = useRef(false)

useEffect(() => {
const setup = async () => {
if (running.current) {
return
}
running.current = true

await csRenderInit()
await csToolsInit()
dicomImageLoaderInit({ maxWebWorkers: 1 })

// Get Cornerstone imageIds and fetch metadata into RAM
const imageIds = await createImageIdsAndCacheMetaData({
StudyInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463",
SeriesInstanceUID:
"1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561",
wadoRsRoot: "https://d3t6nz73ql33tx.cloudfront.net/dicomweb",
})

// Instantiate a rendering engine
const renderingEngineId = "myRenderingEngine"
const renderingEngine = new RenderingEngine(renderingEngineId)
const viewportId = "CT"

const viewportInput = {
viewportId,
type: Enums.ViewportType.ORTHOGRAPHIC,
element: elementRef.current,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
},
}

renderingEngine.enableElement(viewportInput)

// Get the stack viewport that was created
const viewport = renderingEngine.getViewport(
viewportId
) as Types.IVolumeViewport

// Define a volume in memory
const volumeId = "streamingImageVolume"
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
})

// Set the volume to load
// @ts-ignore
volume.load()

// Set the volume on the viewport and it's default properties
viewport.setVolumes([{ volumeId }])

// Render the image
viewport.render()
}

setup()

// Create a stack viewport
}, [elementRef, running])

return (
<div
ref={elementRef}
style={{
width: "512px",
height: "512px",
backgroundColor: "#000",
}}
></div>
)
export default function Page() {
return <Viewer />
}

export default App
Loading