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
7 changes: 2 additions & 5 deletions pocs/petrinaut-hazel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@hashintel/block-design-system": "0.0.5",
"@hashintel/design-system": "0.0.9-canary.2",
"@hashintel/petrinaut": "0.0.5",
"@hashintel/petrinaut": "0.0.5-state-reporting-variant.2",
"@mui/material": "5.18.0",
"@mui/system": "5.18.0",
"elkjs": "0.10.0",
"immer": "10.1.3",
"reactflow": "11.11.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-use": "17.6.0",
"uuid": "11.1.0"
"react-use": "17.6.0"
},
"devDependencies": {
"@types/react": "18.3.12",
Expand Down
107 changes: 81 additions & 26 deletions pocs/petrinaut-hazel/src/main/app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useState } from "react";
import { useCallback, useState } from "react";
import {
Petrinaut,
defaultTokenTypes,
type PetriNetDefinitionObject,
} from "@hashintel/petrinaut";
import { useHazelIntegration } from "./app/use-hazel-integration";
import {
useHazelIntegration,
type HazelValue,
} from "./app/use-hazel-integration";
import { produce } from "immer";
import type { MinimalNetMetadata, SimulationState } from "@hashintel/petrinaut";

const createDefaultNetDefinition = (): PetriNetDefinitionObject => {
return {
Expand All @@ -15,6 +19,8 @@ const createDefaultNetDefinition = (): PetriNetDefinitionObject => {
};
};

const existingNets: MinimalNetMetadata[] = [];

/**
* An incomplete type guard to check if a value is a valid Petri net definition.
* Does not check the content of arrays.
Expand All @@ -41,6 +47,22 @@ const isValidNetDefinition = (
return true;
};

/**
* Hazel errors if sent an empty array at the root of an object value returned, e.g. { simulationState: [] }.
*/
const stripEmptyTuple = (
simulationState: SimulationState,
): HazelValue["simulationState"] => {
if (
simulationState.length === 0 ||
Object.keys(simulationState[0]).length === 0
) {
return undefined;
}

return simulationState;
};

/**
* Wraps Petrinaut with the event handlers necessary for a Hazel Livelit.
*/
Expand All @@ -50,6 +72,7 @@ export const App = () => {

const [netDefinition, setNetDefinition] =
useState<PetriNetDefinitionObject | null>(null);
const [simulationState, setSimulationState] = useState<SimulationState>([]);

const { setSyntax } = useHazelIntegration({
id,
Expand All @@ -60,57 +83,89 @@ export const App = () => {
try {
const parsedValue = JSON.parse(value);

if (isValidNetDefinition(parsedValue)) {
setNetDefinition(parsedValue);
if (isValidNetDefinition(parsedValue.netDefinition)) {
setNetDefinition(parsedValue.netDefinition);
setSimulationState(parsedValue.simulationState ?? []);
} else {
console.error("Invalid net definition", parsedValue);
console.error("Invalid net definition", parsedValue.netDefinition);
const defaultNetDefinition = createDefaultNetDefinition();
setNetDefinition(defaultNetDefinition);
setSyntax(JSON.stringify(defaultNetDefinition));

setSyntax({
netDefinition: defaultNetDefinition,
simulationState: stripEmptyTuple(simulationState),
});
Comment on lines +94 to +97
Copy link

Choose a reason for hiding this comment

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

Bug: Uses stale simulationState value in error case. When the net definition is invalid, setSimulationState() is never called (only called on line 88 in the valid case), so the simulationState variable contains stale data from a previous render. This sends incorrect simulation state back to Hazel.

Fix: Reset simulation state before calling setSyntax:

setSimulationState([]);
setSyntax({
    netDefinition: defaultNetDefinition,
    simulationState: undefined,
});
Suggested change
setSyntax({
netDefinition: defaultNetDefinition,
simulationState: stripEmptyTuple(simulationState),
});
setSimulationState([]);
setSyntax({
netDefinition: defaultNetDefinition,
simulationState: undefined,
});

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

}
} catch (error) {
console.error("Error parsing net definition as JSON", error);
}
},
});

const reportSimulationState = useCallback(
(simulationState: SimulationState) => {
console.log("Simulation state reported");
setSimulationState(simulationState);

setSyntax({
netDefinition: netDefinition as PetriNetDefinitionObject,
simulationState: stripEmptyTuple(simulationState),
});
},
[netDefinition, setSyntax],
);

const mutatePetriNetDefinition = useCallback(
(definitionMutationFn: (definition: PetriNetDefinitionObject) => void) => {
setNetDefinition((existingDefinition) => {
const newDefinition = produce(existingDefinition, definitionMutationFn);
setSyntax({
netDefinition: newDefinition as PetriNetDefinitionObject,
simulationState: stripEmptyTuple(simulationState),
});
return newDefinition;
});
},
[setSyntax, simulationState],
);

if (!netDefinition) {
if (typeof window !== "undefined" && window.self === window.top) {
return (
<p style={{ padding: 15 }}>
This application is designed to be run in an iFrame.
</p>
);
}
return null;
}

return (
<Petrinaut
key={id}
hideNetManagementControls
hideNetManagementControls="includeLoadExampleOnly"
petriNetId={id}
petriNetDefinition={netDefinition}
existingNets={[]}
mutatePetriNetDefinition={(definitionMutationFn) => {
setNetDefinition((existingDefinition) => {
const newDefinition = produce(
existingDefinition,
definitionMutationFn,
);

setSyntax(JSON.stringify(newDefinition));

return newDefinition;
});
}}
existingNets={existingNets}
mutatePetriNetDefinition={mutatePetriNetDefinition}
parentNet={null}
createNewNet={() => {
throw new Error(
"Petrinaut should not be attemping to create new nets when wrapped by Patchwork",
);
createNewNet={({ petriNetDefinition }) => {
setNetDefinition(petriNetDefinition);
setSimulationState([]);
setSyntax({
netDefinition: petriNetDefinition,
simulationState: undefined,
});
}}
loadPetriNet={() => {
throw new Error(
"Petrinaut should not be attemping to load other nets when wrapped by Patchwork",
"Petrinaut should not be attemping to load other nets when used as a Hazel livelit",
);
}}
reportSimulationState={reportSimulationState}
setTitle={() => {
throw new Error(
"Petrinaut should not be attemping to set the net title when wrapped by Patchwork",
"Petrinaut should not be attemping to set the net title when used as a Hazel livelit",
);
}}
title={""}
Expand Down
18 changes: 14 additions & 4 deletions pocs/petrinaut-hazel/src/main/app/use-hazel-integration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PetriNetDefinitionObject } from "@hashintel/petrinaut";
import type { SimulationState } from "@hashintel/petrinaut/dist/petrinaut/types";
import { useCallback, useEffect, useState } from "react";

export type MessageToHazel =
Expand Down Expand Up @@ -35,6 +37,11 @@ type HazelIntegrationConfig = {
onInit: (value: string) => void;
};

export type HazelValue = {
netDefinition: PetriNetDefinitionObject;
simulationState: SimulationState | undefined;
};

const sendToHazel = (message: MessageToHazel, targetOrigin: string) => {
if (window.parent && window.parent !== window) {
console.log("Sending message to Hazel", message);
Expand All @@ -43,7 +50,7 @@ const sendToHazel = (message: MessageToHazel, targetOrigin: string) => {
};

/**
* Core Hazel integration for SolidJS - handles protocol, messaging, and setup
* Core Hazel integration - handles protocol, messaging, and setup
*/
export const useHazelIntegration = (config: HazelIntegrationConfig) => {
const { id, codec, onInit } = config;
Expand All @@ -53,8 +60,11 @@ export const useHazelIntegration = (config: HazelIntegrationConfig) => {
new URLSearchParams(window.location.search).get("parentOrigin") || "*";

const setSyntax = useCallback(
(value: string) => {
sendToHazel({ type: "setSyntax", id, codec, value }, targetOrigin);
(value: HazelValue) => {
sendToHazel(
{ type: "setSyntax", id, codec, value: JSON.stringify(value) },
targetOrigin,
);
},
[id, codec, targetOrigin],
);
Expand Down Expand Up @@ -89,7 +99,7 @@ export const useHazelIntegration = (config: HazelIntegrationConfig) => {
return () => {
window.removeEventListener("message", handleMessage);
};
});
}, [hasInit, id, onInit, targetOrigin]);

return {
setSyntax,
Expand Down
4 changes: 4 additions & 0 deletions pocs/petrinaut-hazel/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export default defineConfig({
},

plugins: [react()],

server: {
port: 5179,
},
});
34 changes: 21 additions & 13 deletions pocs/petrinaut-hazel/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -508,20 +508,18 @@
echarts "5.6.0"
react-loading-skeleton "3.5.0"

"@hashintel/petrinaut@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@hashintel/petrinaut/-/petrinaut-0.0.5.tgz#28fb78da72e07fe9cd7985e04ebfad969e6957cb"
integrity sha512-MlnT1EHx2S1O3o9TKT3Qg8Xsc9op8EBHvMpbX02sYAJjY5MJyoJy1k+RzROFwdT8tQhQQu1ea/A+/ehOGlWZeQ==
"@hashintel/petrinaut@0.0.5-state-reporting-variant.2":
version "0.0.5-state-reporting-variant.2"
resolved "https://registry.yarnpkg.com/@hashintel/petrinaut/-/petrinaut-0.0.5-state-reporting-variant.2.tgz#54cdc1ce36f27a72e132ee1384adb992d0b15045"
integrity sha512-Mmp95R78X0aGn16i7qehzpxvONidVKqWdBxNNjpN2J0D6Dq/rQs/LLZK+IOAncO9ILpn9M0XkmuGLIo2X31Z6w==
dependencies:
"@emotion/react" "11.14.0"
"@fortawesome/free-solid-svg-icons" "6.7.2"
"@hashintel/block-design-system" "0.0.5"
"@hashintel/design-system" "0.0.9-canary.2"
"@mui/material" "5.18.0"
"@mui/system" "5.18.0"
elkjs "0.10.0"
elkjs "0.11.0"
reactflow "11.11.4"
uuid "11.1.0"
uuid "13.0.0"
web-worker "1.4.1"

"@humanwhocodes/config-array@^0.11.10":
version "0.11.14"
Expand Down Expand Up @@ -1621,10 +1619,10 @@ electron-to-chromium@^1.5.204:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.209.tgz#403e7a84933b7206bb2e737d897042b2a6ef8d3e"
integrity sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==

elkjs@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.10.0.tgz#abe2aa6cb25e7439b708fab873b2448d26ed33a1"
integrity sha512-v/3r+3Bl2NMrWmVoRTMBtHtWvRISTix/s9EfnsfEWApNrsmNjqgqJOispCGg46BPwIFdkag3N/HYSxJczvCm6w==
elkjs@0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.11.0.tgz#ca57b209ff66531c0f77ff32b9667aaeaad45d1b"
integrity sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw==

error-ex@^1.3.1:
version "1.3.2"
Expand Down Expand Up @@ -2943,6 +2941,11 @@ uuid@11.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==

uuid@13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==

uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
Expand All @@ -2959,6 +2962,11 @@ vite@5:
optionalDependencies:
fsevents "~2.3.3"

web-worker@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.4.1.tgz#894e71a8f6fce059dc175864fcdf9e633eb7cc6b"
integrity sha512-bEYkHEaeUTjiWscVoW7UBLmAV1S9v0AELr9+3B94Ps1G6E5N/jmSth1e5RZoWbLZWqkI/eyb7KT3sto0ugRpLg==

which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
Expand Down