darabos commited on
Commit
8a41bbe
·
1 Parent(s): d2fc137

Use straight Y.js to observe backend updates.

Browse files
Files changed (1) hide show
  1. web/src/workspace/Workspace.tsx +54 -54
web/src/workspace/Workspace.tsx CHANGED
@@ -1,16 +1,16 @@
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,
8
  Controls,
9
  MarkerType,
10
- useReactFlow,
11
  ReactFlowProvider,
12
  applyEdgeChanges,
13
  applyNodeChanges,
 
14
  type XYPosition,
15
  type Node,
16
  type Edge,
@@ -24,7 +24,6 @@ import Backspace from '~icons/tabler/backspace.jsx';
24
  // @ts-ignore
25
  import Atom from '~icons/tabler/atom.jsx';
26
  import { syncedStore, getYjsDoc } from "@syncedstore/core";
27
- import { useSyncedStore } from "@syncedstore/react";
28
  import { WebsocketProvider } from "y-websocket";
29
  import NodeWithParams from './nodes/NodeWithParams';
30
  // import NodeWithVisualization from './NodeWithVisualization';
@@ -48,64 +47,65 @@ export default function (props: any) {
48
 
49
 
50
  function LynxKiteFlow() {
51
- const reactFlow = useReactFlow();
52
  const [nodes, setNodes] = useState([] as Node[]);
53
  const [edges, setEdges] = useState([] as Edge[]);
54
  const { path } = useParams();
55
-
56
- const sstore = syncedStore({ workspace: {} as Workspace });
57
- const doc = getYjsDoc(sstore);
58
- const wsProvider = useMemo(() => new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc), [path]);
59
- wsProvider; // Just to disable the lint warning. The life cycle of this object is a mystery.
60
- const state = useSyncedStore(sstore);
61
- const onNodesChange = useCallback(
62
- (changes: any[]) => {
63
- setNodes((nds) => applyNodeChanges(changes, nds));
64
- const wnodes = state.workspace!.nodes!;
65
- for (const ch of changes) {
66
- const nodeIndex = wnodes.findIndex((n) => n.id === ch.id);
67
- if (nodeIndex === -1) continue;
68
- const node = wnodes[nodeIndex];
69
- if (!node) continue;
70
- if (ch.type === 'position') {
71
- node.position = ch.position;
72
- } else if (ch.type === 'select') {
73
- } else if (ch.type === 'dimensions') {
74
- } else if (ch.type === 'replace') {
75
- node.data.collapsed = ch.item.data.collapsed;
76
- node.data.params = { ...ch.item.data.params };
77
- } else {
78
- console.log('Unknown node change', ch);
79
  }
80
  }
81
- },
82
- [],
83
- );
84
- const onEdgesChange = useCallback(
85
- (changes: any[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
86
- [],
87
- );
88
-
89
- if (state?.workspace?.nodes && JSON.stringify(nodes) !== JSON.stringify([...state.workspace.nodes as Node[]])) {
90
- const updated = Object.fromEntries(state.workspace.nodes.map((n) => [n.id, n]));
91
- const oldNodes = Object.fromEntries(nodes.map((n) => [n.id, n]));
92
- const updatedNodes = nodes.filter(n => updated[n.id]).map((n) => ({ ...n, ...updated[n.id] })) as Node[];
93
- const newNodes = state.workspace.nodes.filter((n) => !oldNodes[n.id]);
94
- const allNodes = [...updatedNodes, ...newNodes];
95
- if (JSON.stringify(allNodes) !== JSON.stringify(nodes)) {
96
- setNodes(allNodes as Node[]);
97
  }
98
- }
99
- if (state?.workspace?.edges && JSON.stringify(edges) !== JSON.stringify([...state.workspace.edges as Edge[]])) {
100
- const updated = Object.fromEntries(state.workspace.edges.map((e) => [e.id, e]));
101
- const oldEdges = Object.fromEntries(edges.map((e) => [e.id, e]));
102
- const updatedEdges = edges.filter(e => updated[e.id]).map((e) => ({ ...e, ...updated[e.id] })) as Edge[];
103
- const newEdges = state.workspace.edges.filter((e) => !oldEdges[e.id]);
104
- const allEdges = [...updatedEdges, ...newEdges];
105
- if (JSON.stringify(allEdges) !== JSON.stringify(edges)) {
106
- setEdges(allEdges as Edge[]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
- }
 
 
 
109
 
110
  const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
111
  const catalog = useSWR('/api/catalog', fetcher);
 
1
  // The LynxKite workspace editor.
2
  import { useParams } from "react-router";
3
  import useSWR from 'swr';
4
+ import { useEffect, useMemo, useCallback, useState } from "react";
5
  import favicon from '../assets/favicon.ico';
6
  import {
7
  ReactFlow,
8
  Controls,
9
  MarkerType,
 
10
  ReactFlowProvider,
11
  applyEdgeChanges,
12
  applyNodeChanges,
13
+ useUpdateNodeInternals,
14
  type XYPosition,
15
  type Node,
16
  type Edge,
 
24
  // @ts-ignore
25
  import Atom from '~icons/tabler/atom.jsx';
26
  import { syncedStore, getYjsDoc } from "@syncedstore/core";
 
27
  import { WebsocketProvider } from "y-websocket";
28
  import NodeWithParams from './nodes/NodeWithParams';
29
  // import NodeWithVisualization from './NodeWithVisualization';
 
47
 
48
 
49
  function LynxKiteFlow() {
50
+ const updateNodeInternals = useUpdateNodeInternals()
51
  const [nodes, setNodes] = useState([] as Node[]);
52
  const [edges, setEdges] = useState([] as Edge[]);
53
  const { path } = useParams();
54
+ const [state, setState] = useState({ workspace: {} as Workspace });
55
+ useEffect(() => {
56
+ const state = syncedStore({ workspace: {} as Workspace });
57
+ setState(state);
58
+ const doc = getYjsDoc(state);
59
+ const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc);
60
+ const onChange = (update: any, origin: any, doc: any, tr: any) => {
61
+ if (origin === wsProvider) {
62
+ // An update from the CRDT. Apply it to the local state.
63
+ // This is only necessary because ReactFlow keeps secret internal copies of our stuff.
64
+ if (!state.workspace) return;
65
+ if (!state.workspace.nodes) return;
66
+ if (!state.workspace.edges) return;
67
+ setNodes([...state.workspace.nodes] as Node[]);
68
+ setEdges([...state.workspace.edges] as Edge[]);
69
+ for (const node of state.workspace.nodes) {
70
+ // Make sure the internal copies are updated.
71
+ updateNodeInternals(node.id);
 
 
 
 
 
 
72
  }
73
  }
74
+ };
75
+ doc.on('update', onChange);
76
+ return () => {
77
+ doc.destroy();
78
+ wsProvider.destroy();
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
+ }, [path]);
81
+
82
+ const onNodesChange = (changes: any[]) => {
83
+ // An update from the UI. Apply it to the local state...
84
+ setNodes((nds) => applyNodeChanges(changes, nds));
85
+ // ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
86
+ const wnodes = state.workspace?.nodes;
87
+ if (!wnodes) return;
88
+ for (const ch of changes) {
89
+ const nodeIndex = wnodes.findIndex((n) => n.id === ch.id);
90
+ if (nodeIndex === -1) continue;
91
+ const node = wnodes[nodeIndex];
92
+ if (!node) continue;
93
+ // Position events sometimes come with NaN values. Ignore them.
94
+ if (ch.type === 'position' && !isNaN(ch.position.x) && !isNaN(ch.position.y)) {
95
+ Object.assign(node.position, ch.position);
96
+ } else if (ch.type === 'select') {
97
+ } else if (ch.type === 'dimensions') {
98
+ } else if (ch.type === 'replace') {
99
+ node.data.collapsed = ch.item.data.collapsed;
100
+ node.data.params = { ...ch.item.data.params };
101
+ } else {
102
+ console.log('Unknown node change', ch);
103
+ }
104
  }
105
+ };
106
+ const onEdgesChange = (changes: any[]) => {
107
+ setEdges((eds) => applyEdgeChanges(changes, eds));
108
+ };
109
 
110
  const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
111
  const catalog = useSWR('/api/catalog', fetcher);