Skip to content

Commit

Permalink
Merge pull request #26 from fechan/stale-peripherals
Browse files Browse the repository at this point in the history
Handle stale peripherals
  • Loading branch information
fechan authored Jun 9, 2024
2 parents d5b2b50 + 46e8230 commit a513b59
Show file tree
Hide file tree
Showing 21 changed files with 317 additions and 217 deletions.
101 changes: 60 additions & 41 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {

import "reactflow/dist/style.css";

import { edgeTypes, getEdgesForFactory, updateEdgesForFactory } from "./edges";
import { getNodesForFactory, nodeTypes, updateNodesForFactory } from "./nodes";
import { edgeTypes, getEdgesForFactory } from "./edges";
import { getNodesForFactory, nodeTypes } from "./nodes";

import { EdgeOptions } from "./components/EdgeOptions";
import { NewSessionModal } from "./components/NewSessionModal";
Expand All @@ -31,6 +31,7 @@ import { TempEdgeOptions } from "./components/TempEdgeOptions";
import { getLayoutedElements } from "./Layouting";
import toast, { Toaster } from "react-hot-toast";
import { Toast } from "./components/Toast";
import { MissingPeriphs } from "./components/MissingPeriphs";

const DEFAULT_ENDPOINT = (process.env.NODE_ENV === "production") ? "wss://sigils.fredchan.org" : "ws://localhost:3000";

Expand All @@ -50,14 +51,22 @@ export default function App() {
});
const [ showNewSessionModal, setShowNewSessionModal ] = useState(true);

const { factory, addsAndDeletes, groupParents, setFactory, patchFactory } = useFactoryStore();
const { factory, version, setFactory, patchFactory } = useFactoryStore();


// reqsNeedingLayout keys: Request IDs that, when fulfilled, should trigger graph layouting
// values: Boolean that's true if the Request has been fulfilled
type PendingRequests = {[requestId: string]: boolean};
const [ reqsNeedingLayout, setReqsNeedingLayout ] = useState({} as PendingRequests);
const addReqNeedingLayout = useCallback((reqId: string) => {
setReqsNeedingLayout({...reqsNeedingLayout, [reqId]: true});
}, [reqsNeedingLayout, setReqsNeedingLayout]);

const { getIntersectingNodes, getNode } = useReactFlow();
const [ nodes, setNodes, onNodesChange ] = useNodesState([]);
const [ edges, setEdges, onEdgesChange ] = useEdgesState([]);
const [ reactFlowInstance, setReactFlowInstance ] = useState(null as (ReactFlowInstance<any, any> | null));

const [ needLayout, setNeedLayout ] = useState(false);
const [ tempEdge, setTempEdge ] = useState(null as (Edge | null));

const { dropTarget, setDropTarget, clearDropTarget } = useDropTargetStore();
Expand All @@ -71,13 +80,13 @@ export default function App() {
);

const onEdgesDelete: OnEdgesDelete = useCallback(
(edges) => GraphUpdateCallbacks.onEdgesDelete(edges, sendMessage),
[sendMessage]
(edges) => GraphUpdateCallbacks.onEdgesDelete(edges, sendMessage, addReqNeedingLayout),
[sendMessage, addReqNeedingLayout]
);

const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
(oldEdge, newConnection) => GraphUpdateCallbacks.onEdgeUpdate(oldEdge, newConnection, sendMessage, setEdges),
[sendMessage, setEdges]
(oldEdge, newConnection) => GraphUpdateCallbacks.onEdgeUpdate(oldEdge, newConnection, sendMessage, addReqNeedingLayout),
[sendMessage, setEdges, addReqNeedingLayout]
);

const onNodeDrag: NodeDragHandler = useCallback(
Expand All @@ -86,8 +95,8 @@ export default function App() {
);

const onNodeDragStop: NodeDragHandler = useCallback(
(mouseEvent: MouseEvent, node: Node) => GraphUpdateCallbacks.onNodeDragStop(mouseEvent, node, dropTarget, clearDropTarget, sendMessage, reactFlowInstance, factory),
[setNodes, clearDropTarget, dropTarget, sendMessage, reactFlowInstance, factory]
(mouseEvent: MouseEvent, node: Node) => GraphUpdateCallbacks.onNodeDragStop(mouseEvent, node, dropTarget, clearDropTarget, sendMessage, reactFlowInstance, factory, addReqNeedingLayout),
[setNodes, clearDropTarget, dropTarget, sendMessage, reactFlowInstance, factory, addReqNeedingLayout]
);

const onDragOver: DragEventHandler = useCallback(
Expand All @@ -96,8 +105,8 @@ export default function App() {
);

const onDrop: DragEventHandler = useCallback(
(event: DragEvent) => GraphUpdateCallbacks.onDrop(event, reactFlowInstance, sendMessage, factory),
[reactFlowInstance, sendMessage, setNodes, factory]
(event: DragEvent) => GraphUpdateCallbacks.onDrop(event, reactFlowInstance, factory, sendMessage, addReqNeedingLayout),
[reactFlowInstance, factory, sendMessage, setNodes, addReqNeedingLayout]
);

const beforeNodesChange = useCallback(
Expand Down Expand Up @@ -127,36 +136,30 @@ export default function App() {
}, [readyState])

useEffect(() => {
setNodes(getNodesForFactory(factory));
setEdges(getEdgesForFactory(factory));
setNeedLayout(true);
}, [factory]);

useEffect(() => {
if (
addsAndDeletes.groups.adds.size > 0 ||
addsAndDeletes.groups.deletes.size > 0 ||
addsAndDeletes.machines.adds.size > 0 ||
addsAndDeletes.machines.deletes.size > 0
) {
setNodes(nodes => updateNodesForFactory(nodes, addsAndDeletes, groupParents));
setNeedLayout(true);
}

if (addsAndDeletes.pipes.adds.size > 0 || addsAndDeletes.pipes.deletes.size > 0) {
setEdges(edges => updateEdgesForFactory(edges, factory, addsAndDeletes))
const nodes = getNodesForFactory(factory);
const edges = getEdgesForFactory(factory);
setEdges(edges);

// determine if we need layout and remove requests from reqsNeedingLayout
// that have been fulfilled
let needLayout = false;
const unfulfilledReqs: PendingRequests = {};
for (let [req, fulfilled] of Object.entries(reqsNeedingLayout)) {
if (fulfilled) {
needLayout = true;
} else {
unfulfilledReqs[req] = false;
}
}
}, [addsAndDeletes]);
setReqsNeedingLayout(unfulfilledReqs);

useEffect(() => {
(async () => {
if (needLayout) {
if (needLayout) {
(async () => {
const layouted = await getLayoutedElements(nodes, edges, factory);
setNodes(layouted);
setNeedLayout(false);
}
})();
}, [needLayout]);
})();
}
}, [factory, version]);

useEffect(() => {
if (lastMessage !== null && typeof lastMessage.data === "string") {
Expand All @@ -178,12 +181,16 @@ export default function App() {

if (successRes.respondingTo === "FactoryGet") {
const factoryGetRes = message as FactoryGetRes;
setReqsNeedingLayout({...reqsNeedingLayout, [factoryGetRes.reqId]: true});
setFactory(factoryGetRes.factory);
return;
}

if ("diff" in successRes) {
const factoryUpdateRes = successRes as FactoryUpdateRes;
if (factoryUpdateRes.respondingTo in reqsNeedingLayout) {
setReqsNeedingLayout({...reqsNeedingLayout, [factoryUpdateRes.reqId]: true});
}
patchFactory(factoryUpdateRes.diff);
return;
}
Expand All @@ -206,7 +213,12 @@ export default function App() {
}
} else if (message.type === "CcUpdatedFactory") {
const ccUpdatedFactory = message as CcUpdatedFactory;
patchFactory(ccUpdatedFactory.diff);
setReqsNeedingLayout({...reqsNeedingLayout, ["force-update-layout"]: true});
// HACK: setTimeout makes sure setReqsNeedingLayout happens before patchFactory.
// I have no idea why this is necessary, because the race condition doesn't happen
// when I need to update both states in other situations, like after receiving
// a FactoryUpdateRes
setTimeout(() => patchFactory(ccUpdatedFactory.diff), 100);
return;
} else if (message.type === "IdleTimeout") {
const idleTimeout = message as IdleTimeout;
Expand All @@ -227,15 +239,16 @@ export default function App() {

{ showNewSessionModal && <NewSessionModal
sendMessage={ sendMessage }
addReqNeedingLayout={ addReqNeedingLayout }
sessionId={sessionId}
setSessionId={setSessionId}
/>
}

{ tempEdge && <TempEdgeOptions
addReqNeedingLayout={ addReqNeedingLayout }
sendMessage={ sendMessage }
tempEdge={ tempEdge }
setTempEdge={ setTempEdge }
tempEdge={ tempEdge }
onCancel={ () => {
setTempEdge(null);
setEdges(edges.filter(edge => edge.type !== "temp"));
Expand Down Expand Up @@ -265,6 +278,12 @@ export default function App() {
<GroupOptions sendMessage={ sendMessage } />
<MachineOptions sendMessage={ sendMessage } />
</Panel>
<Panel position="top-left">
<MissingPeriphs
sendMessage={ sendMessage }
addReqNeedingLayout={addReqNeedingLayout}
/>
</Panel>
<Background className="bg-neutral-700" />
<MiniMap />
<Controls />
Expand Down
42 changes: 31 additions & 11 deletions client/src/GraphUpdateCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import { v4 as uuidv4 } from "uuid";
import { CombineHandlers } from "./CombineHandlers";
import { splitPeripheralFromMachine, splitSlotFromGroup } from "./SplitHandlers";

function onEdgesDelete(edges: Edge[], sendMessage: SendMessage) {
function onEdgesDelete(
edges: Edge[],
sendMessage: SendMessage,
addReqNeedingLayout: (reqId: string) => void
) {
for (let edge of edges) {
const reqId = uuidv4();
const pipeDelReq: PipeDelReq = {
type: "PipeDel",
reqId: uuidv4(),
reqId: reqId,
pipeId: edge.id,
};
addReqNeedingLayout(reqId);
sendMessage(JSON.stringify(pipeDelReq));
}
}
Expand All @@ -37,17 +43,25 @@ function onConnect(connection: Connection, setTempEdge: Dispatch<SetStateAction<
setTempEdge(tempEdge);
}

function onEdgeUpdate(oldEdge: Edge, newConnection: Connection, sendMessage: SendMessage, setEdges: Dispatch<SetStateAction<Edge[]>>) {

function onEdgeUpdate(
oldEdge: Edge,
newConnection: Connection,
sendMessage: SendMessage,
addReqNeedingLayout: (reqId: string) => void
) {
if (newConnection.source !== null && newConnection.target !== null) {
const reqId = uuidv4();
const pipeEditReq: PipeEditReq = {
type: "PipeEdit",
reqId: uuidv4(),
reqId: reqId,
pipeId: oldEdge.id,
edits: {
from: newConnection.source,
to: newConnection.target,
}
};
addReqNeedingLayout(reqId);
sendMessage(JSON.stringify(pipeEditReq));
}
}
Expand Down Expand Up @@ -138,7 +152,8 @@ function onNodeDragStop(
clearDropTarget: () => void,
sendMessage: SendMessage,
reactFlowInstance: (ReactFlowInstance | null),
factory: Factory
factory: Factory,
addReqNeedingLayout: (reqId: string) => void
) {
if (!reactFlowInstance) return;

Expand All @@ -151,11 +166,13 @@ function onNodeDragStop(
}

if (messages) {
const reqId = uuidv4();
const batchReq: BatchRequest = {
type: "BatchRequest",
reqId: uuidv4(),
reqId: reqId,
requests: messages,
};
addReqNeedingLayout(reqId);
sendMessage(JSON.stringify(batchReq));
}

Expand All @@ -164,10 +181,11 @@ function onNodeDragStop(
// update node x/y

let nodeEditReq: MachineEditReq | GroupEditReq;
const reqId = uuidv4();
if (draggedNode.type === "machine") {
nodeEditReq = {
type: "MachineEdit",
reqId: uuidv4(),
reqId: reqId,
machineId: draggedNode.id,
edits: {
x: draggedNode.position.x,
Expand All @@ -177,7 +195,7 @@ function onNodeDragStop(
} else if (draggedNode.type === "slot-group") {
nodeEditReq = {
type: "GroupEdit",
reqId: uuidv4(),
reqId: reqId,
groupId: draggedNode.id,
edits: {
x: draggedNode.position.x,
Expand All @@ -198,8 +216,9 @@ function onDragOver(event: DragEvent) {
function onDrop(
event: DragEvent,
reactFlowInstance: (ReactFlowInstance | null),
factory: Factory,
sendMessage: SendMessage,
factory: Factory
addReqNeedingLayout: (reqId: string) => void
) {
event.preventDefault();

Expand Down Expand Up @@ -240,12 +259,13 @@ function onDrop(
}

if (requests && requests.length > 0) {
const reqId = uuidv4();
const batchReq: BatchRequest = {
type: "BatchRequest",
reqId: uuidv4(),
reqId: reqId,
requests: requests,
}

addReqNeedingLayout(reqId);
sendMessage(JSON.stringify(batchReq));
}
}
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/ItemSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { GroupId, MachineId, Slot } from "@server/types/core-types";
import { SIZES } from "../nodes/GroupNode";
import { DragEvent } from "react";
import { stringToColor } from "../StringToColor";
import { useFactoryStore } from "../stores/factory";
import { useShallow } from "zustand/react/shallow";

export interface ItemSlotProps {
slotIdx: number,
Expand All @@ -17,6 +19,8 @@ export interface ItemSlotDragData {
}

export function ItemSlot ({ slotIdx, slot, machineId, oldGroupId }: ItemSlotProps) {
const isMissing = useFactoryStore(useShallow(state => state.factory.missing[slot.periphId]));

function onDragStart(event: DragEvent<HTMLDivElement>) {
const dragData: ItemSlotDragData = {
slot: slot,
Expand All @@ -32,7 +36,8 @@ export function ItemSlot ({ slotIdx, slot, machineId, oldGroupId }: ItemSlotProp
draggable
className={
"nodrag absolute border w-[30px] h-[30px] flex items-center justify-center bg-mcgui-slot-bg hover:bg-blue-400 text-white hover:text-black" +
"border-2 border-b-mcgui-slot-border-light border-e-mcgui-slot-border-light border-t-mcgui-slot-border-dark border-s-mcgui-slot-border-dark "
"border-2 border-b-mcgui-slot-border-light border-e-mcgui-slot-border-light border-t-mcgui-slot-border-dark border-s-mcgui-slot-border-dark " +
(isMissing ? "opacity-30" : "")
}
style={{
top: Math.floor(slotIdx / 9) * SIZES.slot + SIZES.slotContainerPadding + SIZES.paddingTop,
Expand Down
23 changes: 23 additions & 0 deletions client/src/components/MissingPeripheralBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState } from "react";
import { stringToColor } from "../StringToColor";

export interface MissingPeripheralBadgeProps {
periphId: string,
};

export function MissingPeripheralBadge({ periphId }: MissingPeripheralBadgeProps) {
const [ hover, setHover ] = useState(false);

return (
<div
className="mcui-button text-sm h-8 text-white w-full flex justify-center items-center"
style={{
backgroundColor: hover ? "rgb(220 38 38)" : stringToColor(periphId)
}}
onMouseOver={ () => setHover(true) }
onMouseOut={ () => setHover(false) }
>
<span>{ hover ? "Remove" : periphId.split(":")[1] }</span>
</div>
);
}
Loading

0 comments on commit a513b59

Please sign in to comment.