diff --git a/package-lock.json b/package-lock.json index c14eb92..168f9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.8.2", "i18next": "^25.3.2", + "mitt": "^3.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.6.0" @@ -2347,6 +2348,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index c787dfe..1e0c31f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.8.2", "i18next": "^25.3.2", + "mitt": "^3.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.6.0" diff --git a/src/App.tsx b/src/App.tsx index 22d0fc4..7f9af49 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import "./App.scss"; -import Screening from "./pages/Screening"; +import Screening from "./pages/Screening/"; function App() { return ( diff --git a/src/pages/Screening.tsx b/src/pages/Screening.tsx deleted file mode 100644 index 6a48d34..0000000 --- a/src/pages/Screening.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { Background, Controls, Edge, ReactFlow, Node, useNodesState, useEdgesState, Connection } from "@xyflow/react"; -import * as node from "./node/"; - -import "@xyflow/react/dist/style.css"; - -import { Editor, Impl as EditorImpl } from "./context/Editor"; - -const nodeTypes = { - fetch_ToshoListed: node.fetch.ToshoListed, - present_TableDisplay: node.present.TableDisplay, -}; -const initialNodes: Node[] = [ - { - id: "n1", - type: "fetch_ToshoListed", - position: {x:0, y:0}, - data: {prime: true, standard: true, growth: true}, - deletable: false, - }, - { - id: "n2", - type: "present_TableDisplay", - position: {x:0, y:100}, - data: {}, - }, - { - id: "n3", - position: {x:0, y:200}, - data: {label: "hello"}, - }, -]; - -export default function Screening() { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); - - const editor = useMemo(()=>new EditorImpl(setNodes, setEdges, 100), []); - - const onConnect = useCallback( - (conn: Connection)=>editor.connect(conn), []); - const onDisconnect = useCallback( - (conns: Edge[])=>editor.disconnect(conns.map((x)=>x as Connection)), []); - const onNodesDelete = useCallback( - (nodes: Node[])=>editor.remove(nodes.map((x)=>x.id)), []); - - return ( - -
- - - - -
-
- ); -} diff --git a/src/pages/Screening/Bus.tsx b/src/pages/Screening/Bus.tsx new file mode 100644 index 0000000..26dd91f --- /dev/null +++ b/src/pages/Screening/Bus.tsx @@ -0,0 +1,31 @@ +import { createContext } from "react"; +import { Node, Edge } from "@xyflow/react"; +import mitt, { Emitter } from "mitt"; + +export type Events = { + // ---- editing request events (compo -> ?) ---- + reqRemoveNode: string; + reqModifyNode: {id: string, data: any}; + + // ---- editing completion events (view -> ?) ---- + onNodeAdded: Node; + onNodeRemoved: string; + onNodeModified: Node; + + onEdgeAdded: Edge; + onEdgeRemoved: string; + onEdgeModified: Edge; + + // ---- running events (emitted by runner) ---- + onNodeReset: {id: string}, + onNodePending: {id: string}, + onNodeRunning: {id: string, symbols: string[], remaining: number | undefined}, + onNodeAborted: {id: string, msg: string}, + onNodeDone: {id: string}, +}; + +export type Bus = Emitter; +export default Bus; +export const createBus = ()=> mitt(); + +export const BusContext = createContext(undefined); diff --git a/src/pages/Screening/Runner.tsx b/src/pages/Screening/Runner.tsx new file mode 100644 index 0000000..373df30 --- /dev/null +++ b/src/pages/Screening/Runner.tsx @@ -0,0 +1,80 @@ +import { Node } from "@xyflow/react"; +import Bus from "./Bus"; +import { Emitter } from "mitt"; + +import nodes from "./node/"; + +export interface Iface { +}; + +export class Impl implements Iface { + _bus: Bus; + _threads: Set; + + constructor(bus: Bus) { + this._bus = bus; + this._threads = new Set(); + } + + _schedule(node: Node) { + this._threads.add(node.id); + run(this._bus, node). + finally(()=>this._threads.delete(node.id)); + } +}; + +async function run(bus: Bus, node: Node) { + const [finisher, aborted] = makeFinisher(); + const destroy = listenAll(bus, { + onNodeRemoved: (id)=>{ if (id===node.id) { finisher(); } }, + onNodeModified: ({id})=>{ if (id===node.id) { finisher(); } }, + }); + try { + bus.emit("onNodePending", { id: node.id }); + await execTask(bus, node, aborted.then(()=>{throw new Error("aborted")})); + bus.emit("onNodeDone", { id: node.id }); + } catch (e: any) { + bus.emit("onNodeAborted", { id: node.id, msg: e }); + } finally { + destroy(); + } +} + +async function execTask(bus: Bus, node: Node, aborted: Promise) { + switch (node.type) { + case "fetch_ToshoListed": + const data: nodes.fetch.ToshoListedData = node.data; + data; + const sleep = new Promise(resolve => setTimeout(resolve, 1)); + await Promise.race([sleep, aborted]); + bus.emit("onNodeRunning", { id: node.id, symbols: ["T/1234"], remaining: 32 }); + break; + default: + throw Error("unknown node type: "+node.type); + } +} + +function listenAll>( + emitter: Emitter, + listeners: { + [K in keyof E]?: (event: E[K]) => void; + } +): () => void { + for (const key in listeners) { + const handler = listeners[key]; + if (handler) emitter.on(key, handler); + } + + return () => { + for (const key in listeners) { + const handler = listeners[key]; + if (handler) emitter.off(key, handler); + } + }; +} + +function makeFinisher(): [()=>void, Promise] { + let r!: (v: void) => void; + const p = new Promise((x)=>r = x); + return [()=>r(), p]; +} diff --git a/src/pages/Screening/index.tsx b/src/pages/Screening/index.tsx new file mode 100644 index 0000000..2026015 --- /dev/null +++ b/src/pages/Screening/index.tsx @@ -0,0 +1,125 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Background, + Connection, + Controls, + Edge, + EdgeChange, + ReactFlow, + Node, + NodeChange, + addEdge, + applyNodeChanges, + applyEdgeChanges, +} from "@xyflow/react"; +import * as node from "./node/"; + +import "@xyflow/react/dist/style.css"; + +import Bus, { createBus, BusContext, Events } from "./Bus"; + +const nodeTypes = { + fetch_ToshoListed: node.fetch.ToshoListed, + present_TableDisplay: node.present.TableDisplay, +}; +const initialNodes: Node[] = [ + { + id: "n1", + type: "fetch_ToshoListed", + position: {x:0, y:0}, + data: {prime: true, standard: true, growth: true}, + deletable: false, + }, + { + id: "n2", + type: "present_TableDisplay", + position: {x:0, y:100}, + data: {}, + }, +]; + +export default function Screening() { + const bus = useMemo(createBus, []); + + const [nodes, setNodes] = useState(initialNodes); + const [edges, setEdges] = useState([] as Edge[]); + + listen(bus, "reqRemoveNode", + (id)=>setNodes((x)=>x.filter(y=>y.id!==id))); + listen(bus, "reqModifyNode", + ({id, data})=>setNodes((x)=>x.map(y => y.id===id? {...y, data: data}: y))); + + const onEdgesDelete = useCallback( + (conns: Edge[])=>conns.map(x=>bus.emit("onEdgeRemoved", x.id)), [bus]); + const onNodesChange = useCallback((changes: NodeChange[])=>{ + tellNodeChangesToBus(bus, changes); + setNodes((nds)=>applyNodeChanges(changes, nds)); + }, [bus]); + const onEdgesChange = useCallback((changes: EdgeChange[])=>{ + tellEdgeChangesToBus(bus, changes); + setEdges((eds)=>applyEdgeChanges(changes, eds)); + }, [bus]); + const onConnect = useCallback((conn: Connection)=>{ + bus.emit("onEdgeAdded", conn as Edge), [bus]; + setEdges((eds)=>addEdge(conn, eds)); + }, [bus]); + + return ( + +
+ + + + +
+
+ ); +} + +function listen( + bus: Bus, + name: K, + handler: (p: Events[K])=>void, +): void { + useEffect(()=>{ + bus.on(name, handler); + return ()=>bus.off(name, handler); + }, [bus, handler]); +} + +function tellNodeChangesToBus(bus: Bus, changes: NodeChange[]) { + changes. + filter(x=>x.type==="add"). + map(y=>bus.emit("onNodeAdded", y.item)); + + changes. + filter(x=>x.type==="remove"). + map(y=>bus.emit("onNodeRemoved", y.id)); + + changes. + filter(x=>x.type==="replace"). + map(y=>bus.emit("onNodeModified", y.item)); +} + +function tellEdgeChangesToBus(bus: Bus, changes: EdgeChange[]) { + changes. + filter(x=>x.type==="add"). + map(y=>bus.emit("onEdgeAdded", y.item)); + + changes. + filter(x=>x.type==="remove"). + map(y=>bus.emit("onEdgeRemoved", y.id)); + + changes. + filter(x=>x.type==="replace"). + map(y=>bus.emit("onEdgeModified", y.item)); +} diff --git a/src/pages/node/Base.tsx b/src/pages/Screening/node/Base.tsx similarity index 70% rename from src/pages/node/Base.tsx rename to src/pages/Screening/node/Base.tsx index 0489488..dfb73c2 100644 --- a/src/pages/node/Base.tsx +++ b/src/pages/Screening/node/Base.tsx @@ -2,9 +2,9 @@ import { useTranslation } from "react-i18next"; import { ReactNode, useContext, useCallback } from "react"; import { NodeProps } from "@xyflow/react"; -import style from "./style.module.scss"; +import { BusContext } from "../Bus"; -import Editor from "../context/Editor"; +import style from "./style.module.scss"; type Params = { title: ReactNode, @@ -14,19 +14,21 @@ type Params = { export default function Base(p: Params) { const {t} = useTranslation(); - const editor = useContext(Editor); - if (editor === undefined) { - throw new Error("no editor context"); + const bus = useContext(BusContext); + if (bus === undefined) { + throw Error("bus is not installed"); } - const onDelete = useCallback(()=>editor.remove([p.node.id]), [p.node]); + const onRemove = useCallback( + ()=>bus.emit("reqRemoveNode", p.node.id), + [p.node.id]); return (
{p.title}
-
diff --git a/src/pages/node/fetch.tsx b/src/pages/Screening/node/fetch.tsx similarity index 72% rename from src/pages/node/fetch.tsx rename to src/pages/Screening/node/fetch.tsx index 50ba131..c0e2b04 100644 --- a/src/pages/node/fetch.tsx +++ b/src/pages/Screening/node/fetch.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { Handle, Node, NodeProps, Position } from "@xyflow/react"; import Base from "./Base"; -import Editor from "../context/Editor"; +import { BusContext } from "../Bus"; export type ToshoListed = Node<{ prime: boolean, @@ -15,11 +15,15 @@ export function ToshoListed(node: NodeProps) { const {t} = useTranslation(); - const editor = useContext(Editor); - if (editor === undefined) { - throw new Error("no editor context"); + const bus = useContext(BusContext); + if (bus === undefined) { + throw Error("bus is not installed"); } + const update = + (market: string, value: boolean)=> + bus.emit("reqModifyNode", {id: id, data: {[market]: value}}); + return ( ) { type="checkbox" name="prime" checked={data.prime} - onChange={(e)=>editor.modify(id, {...data, prime: e.target.checked})} + onChange={(e)=>update("prime", e.target.checked)} /> {t("terms.toshoPrime")}
@@ -40,7 +44,7 @@ export function ToshoListed(node: NodeProps) { type="checkbox" name="standard" checked={data.standard} - onChange={(e)=>editor.modify(id, {...data, standard: e.target.checked})} + onChange={(e)=>update("standard", e.target.checked)} /> {t("terms.toshoStandard")}
@@ -49,7 +53,7 @@ export function ToshoListed(node: NodeProps) { type="checkbox" name="growth" checked={data.growth} - onChange={(e)=>editor.modify(id, {...data, growth: e.target.checked})} + onChange={(e)=>update("growth", e.target.checked)} /> {t("terms.toshoGrowth")} diff --git a/src/pages/node/index.tsx b/src/pages/Screening/node/index.tsx similarity index 100% rename from src/pages/node/index.tsx rename to src/pages/Screening/node/index.tsx diff --git a/src/pages/node/present.tsx b/src/pages/Screening/node/present.tsx similarity index 95% rename from src/pages/node/present.tsx rename to src/pages/Screening/node/present.tsx index 09a49ec..acd4af0 100644 --- a/src/pages/node/present.tsx +++ b/src/pages/Screening/node/present.tsx @@ -8,7 +8,6 @@ export function TableDisplay(node: NodeProps) {
diff --git a/src/pages/node/style.module.scss b/src/pages/Screening/node/style.module.scss similarity index 100% rename from src/pages/node/style.module.scss rename to src/pages/Screening/node/style.module.scss diff --git a/src/pages/context/Editor.tsx b/src/pages/context/Editor.tsx deleted file mode 100644 index 52e7c5e..0000000 --- a/src/pages/context/Editor.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Dispatch, SetStateAction, createContext } from "react"; -import { Connection, EdgeType, NodeType, addEdge } from "@xyflow/react"; - -type Pos = {x: int, y: int,}; - -export interface Iface { - add(type: string, pos: Pos, data: any): string; - remove(id: string[]): void; - modify(id: string, data: any): void; - - connect(conn: Connection): void; - disconnect(conn: Connection[]): void; -}; - -export const Editor = createContext(undefined); -export default Editor; - -type SetNodes = Dispatch>; -type SetEdges = Dispatch>; -export class Impl implements Iface { - _setNodes: SetNodes; - _setEdges: SetEdges; - _nextId: number = 0; - - constructor(setNodes: SetNodes, setEdges: SetEdges, nextId: number = 0) { - this._setNodes = setNodes; - this._setEdges = setEdges; - this._nextId = nextId; - } - - add(type: string, pos: Pos, data: any): string { - const ret = this._genNextId(); - this._setNodes((nds)=>[...nds, { - id: ret, - type: type, - pos: pos, - data: data, - }]); - return ret; - } - remove(ids: string[]): void { - this._setNodes((nds)=>nds.filter((x)=>!ids.includes(x.id))); - } - modify(id: string, data: any): void { - this._setNodes( - (nds)=>nds.map((n)=>n.id === id? {...n, data: data}: n) - ); - } - - connect(c: Connection): void { - this._setEdges((eds)=>addEdge({ - ...c, - deletable: true, - }, eds)); - } - disconnect(cs: Connection[]): void { - this._setEdges((eds)=>eds.filter((e)=>!cs.some(c=>matchEdgeAndConn(e, c)))); - } - - _genNextId(): number { - return "n"+(_nextId++); - } -}; - -function matchEdgeAndConn(e: Edge, c: Connection) { - return ( - (e.source === c.source) && - ((e.sourceHandle??null) == (c.sourceHandle??null)) && - (e.target == c.target) && - ((e.targetHandle??null) == (c.targetHandle??null)) - ); -}