darabos commited on
Commit
23cab17
·
1 Parent(s): 5b78906

CRDT for node positions is working.

Browse files
README.md CHANGED
@@ -1,11 +1,11 @@
1
  # LynxKite 2024
2
 
3
- This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite).
4
- It is not compatible with the original LynxKite. The primary goals of this rewrite are:
5
- - Target GPU clusters instead of Hadoop clusters.
6
- We use Python instead of Scala, RAPIDS instead of Apache Spark.
7
- - More extensible backend. Make it easy to add new LynxKite boxes.
8
- Make it easy to use our frontend for other purposes, configuring and executing other pipelines.
9
 
10
  Current status: **PROTOTYPE**
11
 
@@ -15,6 +15,7 @@ To run the backend:
15
 
16
  ```bash
17
  pip install -r requirements.txt
 
18
  uvicorn server.main:app --reload
19
  ```
20
 
 
1
  # LynxKite 2024
2
 
3
+ This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite). It is not compatible with the
4
+ original LynxKite. The primary goals of this rewrite are:
5
+
6
+ - Target GPU clusters instead of Hadoop clusters. We use Python instead of Scala, RAPIDS instead of Apache Spark.
7
+ - More extensible backend. Make it easy to add new LynxKite boxes. Make it easy to use our frontend for other purposes,
8
+ configuring and executing other pipelines.
9
 
10
  Current status: **PROTOTYPE**
11
 
 
15
 
16
  ```bash
17
  pip install -r requirements.txt
18
+ PYTHONPATH=. pydantic2ts --module server.workspace --output ./web/src/apiTypes.ts
19
  uvicorn server.main:app --reload
20
  ```
21
 
requirements.txt CHANGED
@@ -4,6 +4,7 @@ networkx
4
  numpy
5
  orjson
6
  pandas
 
7
  scipy
8
  uvicorn[standard]
9
  pycrdt
 
4
  numpy
5
  orjson
6
  pandas
7
+ pydantic-to-typescript
8
  scipy
9
  uvicorn[standard]
10
  pycrdt
web/package-lock.json CHANGED
@@ -11,6 +11,8 @@
11
  "@iconify-json/tabler": "^1.2.10",
12
  "@svgr/core": "^8.1.0",
13
  "@svgr/plugin-jsx": "^8.1.0",
 
 
14
  "@types/node": "^22.10.1",
15
  "@xyflow/react": "^12.3.5",
16
  "daisyui": "^4.12.20",
@@ -713,6 +715,24 @@
713
  "node": ">=14"
714
  }
715
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  "node_modules/@rollup/rollup-darwin-arm64": {
717
  "version": "4.28.1",
718
  "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
@@ -1013,6 +1033,40 @@
1013
  "@swc/counter": "^0.1.3"
1014
  }
1015
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  "node_modules/@types/cookie": {
1017
  "version": "0.6.0",
1018
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
 
11
  "@iconify-json/tabler": "^1.2.10",
12
  "@svgr/core": "^8.1.0",
13
  "@svgr/plugin-jsx": "^8.1.0",
14
+ "@syncedstore/core": "^0.6.0",
15
+ "@syncedstore/react": "^0.6.0",
16
  "@types/node": "^22.10.1",
17
  "@xyflow/react": "^12.3.5",
18
  "daisyui": "^4.12.20",
 
715
  "node": ">=14"
716
  }
717
  },
718
+ "node_modules/@reactivedata/react": {
719
+ "version": "0.2.2",
720
+ "resolved": "https://registry.npmjs.org/@reactivedata/react/-/react-0.2.2.tgz",
721
+ "integrity": "sha512-fJ8qoHRbicQnVcnwfHa9FO73mbHFfl590xpuGM4Mo1P2ClaDATfl9oUrrySE+tbA//M9cn8De8HTwjoNqt91sw==",
722
+ "license": "MIT",
723
+ "dependencies": {
724
+ "@reactivedata/reactive": "^0.2.2"
725
+ },
726
+ "peerDependencies": {
727
+ "react": "^16.8.0 || ^17 || ^18"
728
+ }
729
+ },
730
+ "node_modules/@reactivedata/reactive": {
731
+ "version": "0.2.2",
732
+ "resolved": "https://registry.npmjs.org/@reactivedata/reactive/-/reactive-0.2.2.tgz",
733
+ "integrity": "sha512-KnINM/Sng25QAv6sHkJO9q/XyslLegCF5jTsTSVu+AouY3uZDVf4Am99xNCqsfqFZFvnTBBDvCsHNdvTVGvPEA==",
734
+ "license": "MIT"
735
+ },
736
  "node_modules/@rollup/rollup-darwin-arm64": {
737
  "version": "4.28.1",
738
  "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
 
1033
  "@swc/counter": "^0.1.3"
1034
  }
1035
  },
1036
+ "node_modules/@syncedstore/core": {
1037
+ "version": "0.6.0",
1038
+ "resolved": "https://registry.npmjs.org/@syncedstore/core/-/core-0.6.0.tgz",
1039
+ "integrity": "sha512-6TtjEoYJsceYi8u1oRecXwbbLmjHaU0S7HvVfOaEdDfphZLGm/faVuA2fpazqc28F0yIFGvYzvPEBUJn9vqRNw==",
1040
+ "license": "MIT",
1041
+ "dependencies": {
1042
+ "@reactivedata/reactive": "^0.2.0",
1043
+ "@syncedstore/yjs-reactive-bindings": "^0.6.0"
1044
+ },
1045
+ "peerDependencies": {
1046
+ "yjs": "^13.5.13"
1047
+ }
1048
+ },
1049
+ "node_modules/@syncedstore/react": {
1050
+ "version": "0.6.0",
1051
+ "resolved": "https://registry.npmjs.org/@syncedstore/react/-/react-0.6.0.tgz",
1052
+ "integrity": "sha512-pc8ycnBuH2wp7Td5nGzP9Dn2mEW9Yg1qF0yGdjJ5UdgLEwfRkq+8/hdQl+alcoSribv9St/Bi5hJckX8GVsAbQ==",
1053
+ "license": "MIT",
1054
+ "dependencies": {
1055
+ "@reactivedata/react": "^0.2.1"
1056
+ },
1057
+ "peerDependencies": {
1058
+ "@syncedstore/core": "*"
1059
+ }
1060
+ },
1061
+ "node_modules/@syncedstore/yjs-reactive-bindings": {
1062
+ "version": "0.6.0",
1063
+ "resolved": "https://registry.npmjs.org/@syncedstore/yjs-reactive-bindings/-/yjs-reactive-bindings-0.6.0.tgz",
1064
+ "integrity": "sha512-VF78h0J4iOt79YU9d6j5E6bFKu7WXYuiI2ue9ZnA+T4SNVn8viRvg0AHm3NqHzudZZUgYT3dpnbv1/ZmU7yPZQ==",
1065
+ "license": "MIT",
1066
+ "peerDependencies": {
1067
+ "yjs": "^13.5.13"
1068
+ }
1069
+ },
1070
  "node_modules/@types/cookie": {
1071
  "version": "0.6.0",
1072
  "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
web/package.json CHANGED
@@ -13,6 +13,8 @@
13
  "@iconify-json/tabler": "^1.2.10",
14
  "@svgr/core": "^8.1.0",
15
  "@svgr/plugin-jsx": "^8.1.0",
 
 
16
  "@types/node": "^22.10.1",
17
  "@xyflow/react": "^12.3.5",
18
  "daisyui": "^4.12.20",
 
13
  "@iconify-json/tabler": "^1.2.10",
14
  "@svgr/core": "^8.1.0",
15
  "@svgr/plugin-jsx": "^8.1.0",
16
+ "@syncedstore/core": "^0.6.0",
17
+ "@syncedstore/react": "^0.6.0",
18
  "@types/node": "^22.10.1",
19
  "@xyflow/react": "^12.3.5",
20
  "daisyui": "^4.12.20",
web/src/workspace/Workspace.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // The LynxKite workspace editor.
2
  import { useParams } from "react-router";
3
  import useSWR from 'swr';
4
- import { useMemo, useCallback } from "react";
5
  import favicon from '../assets/favicon.ico';
6
  import {
7
  ReactFlow,
@@ -9,7 +9,10 @@ import {
9
  MiniMap,
10
  MarkerType,
11
  useReactFlow,
 
12
  ReactFlowProvider,
 
 
13
  type XYPosition,
14
  type Node,
15
  type Edge,
@@ -22,8 +25,8 @@ import ArrowBack from '~icons/tabler/arrow-back.jsx';
22
  import Backspace from '~icons/tabler/backspace.jsx';
23
  // @ts-ignore
24
  import Atom from '~icons/tabler/atom.jsx';
25
- // import { syncedStore, getYjsDoc } from "@syncedstore/core";
26
- // import { useSyncedStore } from "@syncedstore/react";
27
  import { WebsocketProvider } from "y-websocket";
28
  import NodeWithParams from './nodes/NodeWithParams';
29
  // import NodeWithVisualization from './NodeWithVisualization';
@@ -37,10 +40,6 @@ import { LynxKiteState } from './LynxKiteState';
37
  import '@xyflow/react/dist/style.css';
38
  import { Workspace } from "../apiTypes.ts";
39
 
40
-
41
- import { useShallow } from 'zustand/react/shallow';
42
- import { useStore, selector, doc } from './store';
43
-
44
  export default function (props: any) {
45
  return (
46
  <ReactFlowProvider>
@@ -51,17 +50,55 @@ export default function (props: any) {
51
 
52
 
53
  function LynxKiteFlow() {
 
54
  const { screenToFlowPosition } = useReactFlow();
55
- const store = useStore(
56
- useShallow(selector),
57
- );
58
  const { path } = useParams();
59
 
60
- // const sstore = syncedStore({ workspace: {} });
61
- // const doc = getYjsDoc(sstore);
62
- const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc);
63
  wsProvider; // Just to disable the lint warning. The life cycle of this object is a mystery.
64
- // const state: { workspace: Workspace } = useSyncedStore(sstore);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
67
  const catalog = useSWR('/api/catalog', fetcher);
@@ -80,8 +117,8 @@ function LynxKiteFlow() {
80
  </div>
81
  <EnvironmentSelector
82
  options={Object.keys(catalog.data || {})}
83
- value={store.env}
84
- onChange={(env) => store.setEnv(env)}
85
  />
86
  <div className="tools text-secondary">
87
  <a href=""><Atom /></a>
@@ -90,20 +127,19 @@ function LynxKiteFlow() {
90
  </div>
91
  </div>
92
  <div style={{ height: "100%", width: '100vw' }}>
93
- <LynxKiteState.Provider value={store}>
94
  <ReactFlow
95
- nodes={store.nodes as Node[]}
96
- edges={store.edges}
97
  nodeTypes={nodeTypes} fitView
98
- onNodesChange={store.onNodesChange}
99
- onEdgesChange={store.onEdgesChange}
100
  proOptions={{ hideAttribution: true }}
101
  maxZoom={3}
102
  minZoom={0.3}
103
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
104
  >
105
  <Controls />
106
- <MiniMap />
107
  {/* {#if nodeSearchSettings}
108
  <NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
109
  {/if} */}
 
1
  // The LynxKite workspace editor.
2
  import { useParams } from "react-router";
3
  import useSWR from 'swr';
4
+ import { useMemo, useCallback, useState } from "react";
5
  import favicon from '../assets/favicon.ico';
6
  import {
7
  ReactFlow,
 
9
  MiniMap,
10
  MarkerType,
11
  useReactFlow,
12
+ useUpdateNodeInternals,
13
  ReactFlowProvider,
14
+ applyEdgeChanges,
15
+ applyNodeChanges,
16
  type XYPosition,
17
  type Node,
18
  type Edge,
 
25
  import Backspace from '~icons/tabler/backspace.jsx';
26
  // @ts-ignore
27
  import Atom from '~icons/tabler/atom.jsx';
28
+ import { syncedStore, getYjsDoc } from "@syncedstore/core";
29
+ import { useSyncedStore } from "@syncedstore/react";
30
  import { WebsocketProvider } from "y-websocket";
31
  import NodeWithParams from './nodes/NodeWithParams';
32
  // import NodeWithVisualization from './NodeWithVisualization';
 
40
  import '@xyflow/react/dist/style.css';
41
  import { Workspace } from "../apiTypes.ts";
42
 
 
 
 
 
43
  export default function (props: any) {
44
  return (
45
  <ReactFlowProvider>
 
50
 
51
 
52
  function LynxKiteFlow() {
53
+ const updateNodeInternals = useUpdateNodeInternals();
54
  const { screenToFlowPosition } = useReactFlow();
55
+ const [nodes, setNodes] = useState([] as Node[]);
56
+ const [edges, setEdges] = useState([] as Edge[]);
 
57
  const { path } = useParams();
58
 
59
+ const sstore = syncedStore({ workspace: {} });
60
+ const doc = getYjsDoc(sstore);
61
+ const wsProvider = useMemo(() => new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc), [path]);
62
  wsProvider; // Just to disable the lint warning. The life cycle of this object is a mystery.
63
+ const state: { workspace: Workspace } = useSyncedStore(sstore);
64
+ const onNodesChange = useCallback(
65
+ (changes: any[]) => {
66
+ setNodes((nds) => applyNodeChanges(changes, nds));
67
+ for (const ch of changes) {
68
+ if (ch.type === 'position') {
69
+ const node = state.workspace?.nodes?.find((n) => n.id === ch.id);
70
+ if (node) {
71
+ node.position = ch.position;
72
+ }
73
+ }
74
+ }
75
+ },
76
+ [],
77
+ );
78
+ const onEdgesChange = useCallback(
79
+ (changes: any[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
80
+ [],
81
+ );
82
+ if (state?.workspace?.nodes && JSON.stringify(nodes) !== JSON.stringify([...state.workspace.nodes as Node[]])) {
83
+ const updated = Object.fromEntries(state.workspace.nodes.map((n) => [n.id, n]));
84
+ const oldNodes = Object.fromEntries(nodes.map((n) => [n.id, n]));
85
+ const updatedNodes = nodes.filter(n => updated[n.id]).map((n) => ({ ...n, ...updated[n.id] })) as Node[];
86
+ const newNodes = state.workspace.nodes.filter((n) => !oldNodes[n.id]);
87
+ const allNodes = [...updatedNodes, ...newNodes];
88
+ if (JSON.stringify(allNodes) !== JSON.stringify(nodes)) {
89
+ setNodes(allNodes as Node[]);
90
+ }
91
+ }
92
+ if (state?.workspace?.edges && JSON.stringify(edges) !== JSON.stringify([...state.workspace.edges as Edge[]])) {
93
+ const updated = Object.fromEntries(state.workspace.edges.map((e) => [e.id, e]));
94
+ const oldEdges = Object.fromEntries(edges.map((e) => [e.id, e]));
95
+ const updatedEdges = edges.filter(e => updated[e.id]).map((e) => ({ ...e, ...updated[e.id] })) as Edge[];
96
+ const newEdges = state.workspace.edges.filter((e) => !oldEdges[e.id]);
97
+ const allEdges = [...updatedEdges, ...newEdges];
98
+ if (JSON.stringify(allEdges) !== JSON.stringify(edges)) {
99
+ setEdges(allEdges as Edge[]);
100
+ }
101
+ }
102
 
103
  const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
104
  const catalog = useSWR('/api/catalog', fetcher);
 
117
  </div>
118
  <EnvironmentSelector
119
  options={Object.keys(catalog.data || {})}
120
+ value={state.workspace.env!}
121
+ onChange={(env) => { state.workspace.env = env; }}
122
  />
123
  <div className="tools text-secondary">
124
  <a href=""><Atom /></a>
 
127
  </div>
128
  </div>
129
  <div style={{ height: "100%", width: '100vw' }}>
130
+ <LynxKiteState.Provider value={state.workspace}>
131
  <ReactFlow
132
+ nodes={nodes}
133
+ edges={edges}
134
  nodeTypes={nodeTypes} fitView
135
+ onNodesChange={onNodesChange}
136
+ onEdgesChange={onEdgesChange}
137
  proOptions={{ hideAttribution: true }}
138
  maxZoom={3}
139
  minZoom={0.3}
140
  defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
141
  >
142
  <Controls />
 
143
  {/* {#if nodeSearchSettings}
144
  <NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
145
  {/if} */}
web/src/workspace/store.ts DELETED
@@ -1,69 +0,0 @@
1
- // Like described in https://reactflow.dev/learn/advanced-use/state-management
2
- // but with https://github.com/joebobmiles/zustand-middleware-yjs/ added.
3
- import {
4
- type Edge,
5
- type Node,
6
- type OnNodesChange,
7
- type OnEdgesChange,
8
- type OnConnect,
9
- } from '@xyflow/react';
10
- import * as apiTypes from "../apiTypes.ts";
11
- import * as Y from "yjs";
12
- import yjs from "zustand-middleware-yjs";
13
- export const doc = new Y.Doc();
14
-
15
- export type FlowState = {
16
- env: string;
17
- nodes: apiTypes.WorkspaceNode[];
18
- edges: apiTypes.WorkspaceEdge[];
19
- onNodesChange: OnNodesChange<Node>;
20
- onEdgesChange: OnEdgesChange;
21
- onConnect: OnConnect;
22
- setNodes: (nodes: Node[]) => void;
23
- setEdges: (edges: Edge[]) => void;
24
- setEnv: (env: string) => void;
25
- };
26
-
27
- import { create } from 'zustand';
28
- import { addEdge, applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
29
-
30
- export const useStore = create<FlowState>(yjs(doc, "shared", (set: any, get: any) => ({
31
- env: 'LynxKite',
32
- nodes: [],
33
- edges: [],
34
- onNodesChange: (changes: any[]) => {
35
- set({
36
- nodes: applyNodeChanges(changes, get().nodes),
37
- });
38
- },
39
- onEdgesChange: (changes: any[]) => {
40
- set({
41
- edges: applyEdgeChanges(changes, get().edges),
42
- });
43
- },
44
- onConnect: (connection: any) => {
45
- set({
46
- edges: addEdge(connection, get().edges),
47
- });
48
- },
49
- setNodes: (nodes: Node[]) => {
50
- console.log("setNodes", { nodes });
51
- set({ nodes });
52
- },
53
- setEdges: (edges: Edge[]) => {
54
- set({ edges });
55
- },
56
- setEnv: (env: string) => {
57
- set({ env });
58
- },
59
- })));
60
-
61
- export const selector = (state: FlowState) => ({
62
- nodes: state.nodes,
63
- edges: state.edges,
64
- env: state.env,
65
- onNodesChange: state.onNodesChange,
66
- onEdgesChange: state.onEdgesChange,
67
- onConnect: state.onConnect,
68
- setEnv: state.setEnv,
69
- });