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,089 changes: 1,557 additions & 1,532 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.6",
"@fluffylabs/shared-ui": "^0.4.3",
"@fluffylabs/shared-ui": "^0.4.5",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@tailwindcss/vite": "^4.1.16",
"@typeberry/lib": "^0.2.0-ca77d76",
"@typeberry/lib": "^0.3.1",
"@uiw/react-codemirror": "^4.25.2",
"blake2b": "^2.1.4",
"cytoscape": "^3.33.1",
Expand All @@ -40,7 +40,7 @@
"tailwindcss": "^4.1.14",
"tippy.js": "^6.3.7",
"vite-bundle-analyzer": "^1.2.3",
"zod": "^4.1.11"
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
Expand All @@ -54,16 +54,16 @@
"@types/react-cytoscapejs": "^1.2.5",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react-swc": "^4.1.0",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/coverage-v8": "^4.0.8",
"eslint": "^9.35.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.4.0",
"jsdom": "^26.0.0",
"globals": "^16.5.0",
"jsdom": "^27.1.0",
"sass-embedded": "^1.93.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"vite": "^7.1.12",
"vitest": "^3.0.5"
"vitest": "^4.0.8"
}
}
22 changes: 12 additions & 10 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,18 @@ vi.mock('lucide-react', () => ({
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
value: vi.fn(function(query) {
return {
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});

describe('App', () => {
Expand Down
12 changes: 7 additions & 5 deletions src/components/InspectStateViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { config, bytes, state_merkleization as lib, hash } from "@typeberry/lib";
import { CompositeViewer, CompositeDiff } from './viewer';
import ServiceViewer from './ServiceViewer';
Expand Down Expand Up @@ -56,10 +56,12 @@ const InspectStateViewer = ({
const preStateAccess = useLoadState(preState, setError, 'preState', chainSpec);
const stateAccess = useLoadState(state, setError, 'postState', chainSpec);

// Expose all states to the global window object for DevTools inspection.
(window as unknown as { preState: unknown }).preState = preStateAccess;
(window as unknown as { postState: unknown }).postState = stateAccess;
(window as unknown as { state: unknown }).state = stateAccess;
useEffect(() => {
// Expose all states to the global window object for DevTools inspection.
(window as unknown as { preState: unknown }).preState = preStateAccess;
(window as unknown as { postState: unknown }).postState = stateAccess;
(window as unknown as { state: unknown }).state = stateAccess;
}, [preStateAccess, stateAccess]);

// Function to get raw value from original state data
const getRawValue = (rawKey: string, stateData: Record<string, string> | undefined) => {
Expand Down
32 changes: 18 additions & 14 deletions src/components/JsonEditorDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,27 @@ describe('JsonEditorDialog', () => {
// Mock matchMedia for dark mode detection
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
value: vi.fn(function(query) {
return {
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});

// Mock MutationObserver
global.MutationObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
}));
global.MutationObserver = vi.fn(function() {
return {
observe: vi.fn(),
disconnect: vi.fn(),
};
}) as any;
});

afterEach(() => {
Expand Down
17 changes: 9 additions & 8 deletions src/components/JsonEditorDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,6 @@ const JsonEditorDialog = ({
};
}, []);

// Update editor content when initialContent changes
useEffect(() => {
if (isOpen) {
setEditorContent(initialContent);
validateJson(initialContent);
}
}, [isOpen, initialContent]);

// Validate JSON content
const validateJson = (content: string) => {
try {
Expand All @@ -74,6 +66,15 @@ const JsonEditorDialog = ({
}
};

// Update editor content when initialContent changes
useEffect(() => {
if (isOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setEditorContent(initialContent);
validateJson(initialContent);
}
}, [isOpen, initialContent]);

// Handle content changes in editor
const handleContentChange = (value: string) => {
setEditorContent(value);
Expand Down
1 change: 1 addition & 0 deletions src/components/ServiceViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ServiceViewer = ({ preStateAccess, stateAccess, preState, state, searchTer

useEffect(() => {
if (discoveredServiceIds.length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setServiceIdsInput(discoveredServiceIds.map(formatServiceIdUnsigned).join(', '));
}
}, [discoveredServiceIds]);
Comment on lines 25 to 30
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor to avoid state synchronization in effect.

Setting state inside useEffect to mirror derived values is an anti-pattern. The lint suppression masks a design issue rather than a legitimate exception.

Consider these alternatives:

  1. Derive directly from state (preferred): Remove serviceIdsInput state and compute the display value from discoveredServiceIds in the render phase.
  2. Use a reset key: If user edits are needed, add a key to the input component that changes when discoveredServiceIds changes, triggering a re-mount with fresh initial state.

Apply this diff to derive the value directly:

  const discoveredServiceIds = useMemo(() => {
    return extractServiceIdsFromState(state);
  }, [state]);

- const [serviceIdsInput, setServiceIdsInput] = useState(() => {
-   return discoveredServiceIds.length > 0 ? discoveredServiceIds.map(formatServiceIdUnsigned).join(', ') : '0';
- });
-
- useEffect(() => {
-   if (discoveredServiceIds.length > 0) {
-     // eslint-disable-next-line react-hooks/set-state-in-effect
-     setServiceIdsInput(discoveredServiceIds.map(formatServiceIdUnsigned).join(', '));
-   }
- }, [discoveredServiceIds]);
+ const defaultServiceIdsInput = useMemo(() => {
+   return discoveredServiceIds.length > 0 ? discoveredServiceIds.map(formatServiceIdUnsigned).join(', ') : '0';
+ }, [discoveredServiceIds]);
+
+ const [serviceIdsInput, setServiceIdsInput] = useState(defaultServiceIdsInput);

  const serviceIds = useMemo(() => {
    return parseServiceIds(serviceIdsInput);
  }, [serviceIdsInput]);

If the input needs to reset when state changes, add a key to force remount:

- <ServiceIdsInput
+ <ServiceIdsInput
+   key={JSON.stringify(discoveredServiceIds)}
    value={serviceIdsInput}
    onChange={setServiceIdsInput}
  />

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/ServiceViewer.tsx around lines 25 to 30 the component sets
serviceIdsInput inside a useEffect to mirror discoveredServiceIds (with an
eslint suppression) which is an anti-pattern; remove the effect and either
(preferred) delete the serviceIdsInput state and compute the input display value
directly from discoveredServiceIds in render (e.g.
value={discoveredServiceIds.map(formatServiceIdUnsigned).join(', ')}) or (if
users must edit and you need to reset on discovery changes) keep local state but
add a changing key on the input tied to discoveredServiceIds (e.g.
key={discoveredServiceIds.join(',')} ) so the input remounts with fresh initial
state when discoveredServiceIds change.

Expand Down
2 changes: 2 additions & 0 deletions src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const SettingsDialog = ({ isOpen, onClose }: SettingsDialogProps) => {

useEffect(() => {
if (!isOpen) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedGpVersion(utils.CURRENT_VERSION as unknown as string);
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedSuite(utils.CURRENT_SUITE as unknown as string);
}, [isOpen]);

Expand Down
22 changes: 12 additions & 10 deletions src/components/UploadScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ vi.mock('lucide-react', () => ({
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
value: vi.fn(function(query) {
return {
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});

describe('UploadScreen', () => {
Expand Down
71 changes: 43 additions & 28 deletions src/components/UploadScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { Upload, FileText, AlertCircle, Edit, FolderOpen } from 'lucide-react';
import JsonEditorDialog from './JsonEditorDialog';
import { validateFile, validateJsonContent, type JsonValidationResult, StfStateType } from '../utils';

import stfTestVectorFixture from '../utils/fixtures/00000041.json';
import jip4ChainspecFixture from '../utils/fixtures/dev-tiny.json';
import stfGenesisFixture from '../utils/fixtures/genesis.json';
import typeberryConfigFixture from '../utils/fixtures/typeberry-dev.json';
import ExamplesModal from '@/trie/components/ExamplesModal';
import type { AppState, UploadState } from '@/types/shared';
import {StateKindSelector} from './StateKindSelector';
Expand All @@ -16,29 +12,29 @@ import {Button} from '@fluffylabs/shared-ui';
interface ExampleFile {
name: string;
description: string;
content: string;
content: () => Promise<string>;
}

const EXAMPLE_FILES: ExampleFile[] = [
{
name: 'STF Test Vector',
description: 'Example with pre-state and post-state data',
content: JSON.stringify(stfTestVectorFixture, null, 2)
content: async () => JSON.stringify(await import('../utils/fixtures/00000041.json'), null, 2),
},
{
name: 'JIP-4 Chain Spec',
description: 'Genesis state from chain specification',
content: JSON.stringify(jip4ChainspecFixture, null, 2)
content: async () => JSON.stringify(await import('../utils/fixtures/dev-tiny.json'), null, 2)
},
{
name: 'STF Genesis',
description: 'Initial state with header information',
content: JSON.stringify(stfGenesisFixture, null, 2)
content: async () => JSON.stringify(await import('../utils/fixtures/genesis.json'), null, 2)
},
{
name: 'Typeberry Config',
description: 'Typeberry framework configuration',
content: JSON.stringify(typeberryConfigFixture, null, 2)
content: async () => JSON.stringify(await import('../utils/fixtures/typeberry-dev.json'), null, 2),
}
];

Expand All @@ -61,6 +57,7 @@ export const UploadScreen = ({
const { uploadState, selectedState } = appState;
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [formatError, setFormatError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);

const clearUpload = useCallback(() => {
onClearUpload();
Expand Down Expand Up @@ -135,10 +132,17 @@ export const UploadScreen = ({
validateManualContent();
}, [handleUploadStateWithStorage]);

const handleExampleLoad = useCallback((exampleContent: string) => {
const handleExampleLoad = useCallback(async (exampleContent: () => Promise<string>) => {
clearUpload();
setIsLoading(true);
let content = '';
try {
content = await exampleContent();
} finally {
setIsLoading(false);
}

const validation = validateJsonContent(exampleContent);
const validation = validateJsonContent(content);

const newUploadState = {
file: null,
Expand All @@ -164,6 +168,7 @@ export const UploadScreen = ({
<p className="text-muted-foreground">
Upload a serialized state dump to inspect it or try loading one of the examples:{' '}
<button
disabled={isLoading}
onClick={() => handleExampleLoad(EXAMPLE_FILES[0].content)}
className="text-primary hover:text-primary/80 hover:underline transition-colors"
title={EXAMPLE_FILES[0].description}
Expand All @@ -172,6 +177,7 @@ export const UploadScreen = ({
</button>
,{' '}
<button
disabled={isLoading}
onClick={() => handleExampleLoad(EXAMPLE_FILES[2].content)}
className="text-primary hover:text-primary/80 hover:underline transition-colors"
title={EXAMPLE_FILES[2].description}
Expand All @@ -180,6 +186,7 @@ export const UploadScreen = ({
</button>
,{' '}
<button
disabled={isLoading}
onClick={() => handleExampleLoad(EXAMPLE_FILES[1].content)}
className="text-primary hover:text-primary/80 hover:underline transition-colors"
title={EXAMPLE_FILES[1].description}
Expand All @@ -188,6 +195,7 @@ export const UploadScreen = ({
</button>
,{' '}
<button
disabled={isLoading}
onClick={() => handleExampleLoad(EXAMPLE_FILES[3].content)}
className="text-primary hover:text-primary/80 hover:underline transition-colors"
title={EXAMPLE_FILES[3].description}
Expand All @@ -198,11 +206,12 @@ export const UploadScreen = ({
<p className="text-muted-foreground">
Instead of loading full JAM state you can also try out&nbsp;
<ExamplesModal
onSelect={(rows) => handleExampleLoad(JSON.stringify({
onSelect={(rows) => handleExampleLoad(async () => JSON.stringify({
state: rows
}, null, 2))}
button={(open) => (
<button
disabled={isLoading}
onClick={open}
className="text-primary hover:text-primary/80 hover:underline transition-colors"
title="open trie examples"
Expand Down Expand Up @@ -254,6 +263,10 @@ export const UploadScreen = ({
{(uploadState.file.size / 1024).toFixed(1)} KB
</p>
</div>
) : isLoading ? (
<div className="space-y-2">
Loading example...
</div>
) : (
<div className="space-y-2">
<p className="text-foreground font-medium">
Expand All @@ -267,26 +280,28 @@ export const UploadScreen = ({
</div>

{/* Action Buttons */}
<div className="flex flex-wrap gap-3 justify-center">
{/* Browse Button (if no file uploaded) */}
{!isLoading && (
<div className="flex flex-wrap gap-3 justify-center">
{/* Browse Button (if no file uploaded) */}
<Button
onClick={open}
variant="primary"
size="lg"
>
<FolderOpen className="h-4 w-4" />
<span>{(!uploadState.file && !uploadState.content) ? 'Upload' : 'Change'}</span>
</Button>

<Button
onClick={open}
variant="primary"
onClick={handleManualEdit}
variant="secondary"
size="lg"
>
<FolderOpen className="h-4 w-4" />
<span>{(!uploadState.file && !uploadState.content) ? 'Upload' : 'Change'}</span>
<Edit className="h-4 w-4" />
<span>{(!uploadState.file && !uploadState.content) ? 'JSON' : 'Edit'}</span>
</Button>

<Button
onClick={handleManualEdit}
variant="secondary"
size="lg"
>
<Edit className="h-4 w-4" />
<span>{(!uploadState.file && !uploadState.content) ? 'JSON' : 'Edit'}</span>
</Button>
</div>
</div>
)}
</div>
</div>

Expand Down
Loading
Loading