Spaces:
Running
Running
Client-side CRDT working for nodes and edges.
Browse files- server/crdt.py +2 -2
- web/package.json +0 -1
- web/src/LynxKiteFlow.svelte +58 -67
- web/src/LynxKiteNode.svelte +6 -2
- web/src/NodeWithParams.svelte +10 -2
server/crdt.py
CHANGED
@@ -18,8 +18,8 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
18 |
ystore = pycrdt_websocket.ystore.FileYStore(f'crdt_data/{name}.crdt')
|
19 |
ydoc = pycrdt.Doc()
|
20 |
ydoc['workspace'] = ws = pycrdt.Map()
|
21 |
-
ws['nodes'] =
|
22 |
-
ws['edges'] =
|
23 |
ws['env'] = 'unset'
|
24 |
# Replay updates from the store.
|
25 |
try:
|
|
|
18 |
ystore = pycrdt_websocket.ystore.FileYStore(f'crdt_data/{name}.crdt')
|
19 |
ydoc = pycrdt.Doc()
|
20 |
ydoc['workspace'] = ws = pycrdt.Map()
|
21 |
+
ws['nodes'] = pycrdt.Array()
|
22 |
+
ws['edges'] = pycrdt.Array()
|
23 |
ws['env'] = 'unset'
|
24 |
# Replay updates from the store.
|
25 |
try:
|
web/package.json
CHANGED
@@ -29,7 +29,6 @@
|
|
29 |
"@sveltestack/svelte-query": "^1.6.0",
|
30 |
"@xyflow/svelte": "^0.1.3",
|
31 |
"bootstrap": "^5.3.3",
|
32 |
-
"deep-object-diff": "^1.1.9",
|
33 |
"echarts": "^5.5.0",
|
34 |
"fuse.js": "^7.0.0",
|
35 |
"svelte-echarts": "^1.0.0-rc1",
|
|
|
29 |
"@sveltestack/svelte-query": "^1.6.0",
|
30 |
"@xyflow/svelte": "^0.1.3",
|
31 |
"bootstrap": "^5.3.3",
|
|
|
32 |
"echarts": "^5.5.0",
|
33 |
"fuse.js": "^7.0.0",
|
34 |
"svelte-echarts": "^1.0.0-rc1",
|
web/src/LynxKiteFlow.svelte
CHANGED
@@ -1,12 +1,13 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {
|
3 |
-
import { writable
|
4 |
import {
|
5 |
SvelteFlow,
|
6 |
Controls,
|
7 |
MiniMap,
|
8 |
MarkerType,
|
9 |
useSvelteFlow,
|
|
|
10 |
type XYPosition,
|
11 |
type Node,
|
12 |
type Edge,
|
@@ -29,18 +30,23 @@
|
|
29 |
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
30 |
import { svelteSyncedStore } from "@syncedstore/svelte";
|
31 |
import { WebsocketProvider } from "y-websocket";
|
|
|
32 |
|
33 |
function getCRDTStore(path) {
|
34 |
const sstore = syncedStore({ workspace: {} });
|
35 |
const doc = getYjsDoc(sstore);
|
36 |
const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path, doc);
|
37 |
-
wsProvider.on('sync', function(isSynced: boolean) {
|
38 |
-
console.log('synced', isSynced, 'ydoc', doc.toJSON());
|
39 |
-
});
|
40 |
return {store: svelteSyncedStore(sstore), sstore, doc};
|
41 |
}
|
42 |
$: connection = getCRDTStore(path);
|
43 |
$: store = connection.store;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
export let path = '';
|
46 |
|
@@ -55,30 +61,8 @@
|
|
55 |
area: NodeWithArea,
|
56 |
};
|
57 |
|
58 |
-
|
59 |
-
|
60 |
-
const ss = derived(store, (store) => store.workspace[field]?.value);
|
61 |
-
ss.set = (value) => {
|
62 |
-
console.log('set called', field, value);
|
63 |
-
$store.workspace[field] = value;
|
64 |
-
};
|
65 |
-
ss.update = (fn) => {
|
66 |
-
console.log('update called', field);
|
67 |
-
console.log(JSON.stringify($store));
|
68 |
-
console.log(JSON.stringify($store.workspace.nodes));
|
69 |
-
const before = $store.workspace[field];
|
70 |
-
console.log({before});
|
71 |
-
const after = fn(before);
|
72 |
-
console.log({after});
|
73 |
-
$store.workspace[field] = after;
|
74 |
-
};
|
75 |
-
return ss;
|
76 |
-
}
|
77 |
-
$: nodes = substore(store, 'nodes');
|
78 |
-
$: edges = substore(store, 'edges');
|
79 |
-
|
80 |
-
// const nodes = writable<Node[]>([]);
|
81 |
-
// const edges = writable<Edge[]>([]);
|
82 |
|
83 |
function closeNodeSearch() {
|
84 |
nodeSearchSettings = undefined;
|
@@ -96,35 +80,31 @@
|
|
96 |
}
|
97 |
function addNode(e) {
|
98 |
const meta = {...e.detail};
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
meta
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
114 |
node.id = `${title} ${i}`;
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
node.
|
121 |
-
|
122 |
-
|
123 |
-
const parent = nodes.find((x) => x.id === node.parentId);
|
124 |
-
node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
125 |
-
}
|
126 |
-
return [...nodes, node];
|
127 |
-
});
|
128 |
closeNodeSearch();
|
129 |
}
|
130 |
const catalog = useQuery(['catalog'], async () => {
|
@@ -152,19 +132,29 @@
|
|
152 |
parentId: node.id,
|
153 |
};
|
154 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
$: parentDir = path.split('/').slice(0, -1).join('/');
|
156 |
-
$: console.log($store);
|
157 |
-
// <br>{JSON.stringify($store)}
|
158 |
-
// <br>{JSON.stringify($store?.workspace)}
|
159 |
-
// <br>{JSON.stringify($store?.workspace?.nodes)}
|
160 |
-
// <br>{JSON.stringify($store?.workspace?.nodes?.value)}
|
161 |
-
// <br>{$store.workspace?.nodes?.toArray()}
|
162 |
-
// <br>{$store.workspace?.nodes?.length}
|
163 |
-
|
164 |
</script>
|
165 |
|
166 |
<div class="page">
|
167 |
-
<br>{JSON.stringify($store)}
|
168 |
{#if $store.workspace !== undefined}
|
169 |
<div class="top-bar">
|
170 |
<div class="ws-name">
|
@@ -176,7 +166,6 @@
|
|
176 |
options={Object.keys($catalog.data || {})}
|
177 |
value={$store.workspace.env}
|
178 |
onChange={(env) => {
|
179 |
-
console.log('env change', env);
|
180 |
$store.workspace.env = env;
|
181 |
}}
|
182 |
/>
|
@@ -189,6 +178,8 @@
|
|
189 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
190 |
on:paneclick={toggleNodeSearch}
|
191 |
on:nodeclick={nodeClick}
|
|
|
|
|
192 |
proOptions={{ hideAttribution: true }}
|
193 |
maxZoom={3}
|
194 |
minZoom={0.3}
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { setContext } from 'svelte';
|
3 |
+
import { writable } from 'svelte/store';
|
4 |
import {
|
5 |
SvelteFlow,
|
6 |
Controls,
|
7 |
MiniMap,
|
8 |
MarkerType,
|
9 |
useSvelteFlow,
|
10 |
+
useUpdateNodeInternals,
|
11 |
type XYPosition,
|
12 |
type Node,
|
13 |
type Edge,
|
|
|
30 |
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
31 |
import { svelteSyncedStore } from "@syncedstore/svelte";
|
32 |
import { WebsocketProvider } from "y-websocket";
|
33 |
+
const updateNodeInternals = useUpdateNodeInternals();
|
34 |
|
35 |
function getCRDTStore(path) {
|
36 |
const sstore = syncedStore({ workspace: {} });
|
37 |
const doc = getYjsDoc(sstore);
|
38 |
const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path, doc);
|
|
|
|
|
|
|
39 |
return {store: svelteSyncedStore(sstore), sstore, doc};
|
40 |
}
|
41 |
$: connection = getCRDTStore(path);
|
42 |
$: store = connection.store;
|
43 |
+
$: store.subscribe((value) => {
|
44 |
+
if (!value?.workspace?.edges) return;
|
45 |
+
$nodes = [...value.workspace.nodes];
|
46 |
+
$edges = [...value.workspace.edges];
|
47 |
+
updateNodeInternals();
|
48 |
+
});
|
49 |
+
$: setContext('LynxKite store', store);
|
50 |
|
51 |
export let path = '';
|
52 |
|
|
|
61 |
area: NodeWithArea,
|
62 |
};
|
63 |
|
64 |
+
const nodes = writable<Node[]>([]);
|
65 |
+
const edges = writable<Edge[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
function closeNodeSearch() {
|
68 |
nodeSearchSettings = undefined;
|
|
|
80 |
}
|
81 |
function addNode(e) {
|
82 |
const meta = {...e.detail};
|
83 |
+
const node = {
|
84 |
+
type: meta.type,
|
85 |
+
data: {
|
86 |
+
meta: meta,
|
87 |
+
title: meta.name,
|
88 |
+
params: Object.fromEntries(
|
89 |
+
Object.values(meta.params).map((p) => [p.name, p.default])),
|
90 |
+
},
|
91 |
+
};
|
92 |
+
node.position = screenToFlowPosition({x: nodeSearchSettings.pos.x, y: nodeSearchSettings.pos.y});
|
93 |
+
const title = node.data.title;
|
94 |
+
let i = 1;
|
95 |
+
node.id = `${title} ${i}`;
|
96 |
+
const nodes = $store.workspace.nodes;
|
97 |
+
while (nodes.find((x) => x.id === node.id)) {
|
98 |
+
i += 1;
|
99 |
node.id = `${title} ${i}`;
|
100 |
+
}
|
101 |
+
node.parentId = nodeSearchSettings.parentId;
|
102 |
+
if (node.parentId) {
|
103 |
+
node.extent = 'parent';
|
104 |
+
const parent = nodes.find((x) => x.id === node.parentId);
|
105 |
+
node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
106 |
+
}
|
107 |
+
nodes.push(node);
|
|
|
|
|
|
|
|
|
|
|
108 |
closeNodeSearch();
|
109 |
}
|
110 |
const catalog = useQuery(['catalog'], async () => {
|
|
|
132 |
parentId: node.id,
|
133 |
};
|
134 |
}
|
135 |
+
function onConnect(params: Connection) {
|
136 |
+
const edge = {
|
137 |
+
id: `${params.source} ${params.target}`,
|
138 |
+
source: params.source,
|
139 |
+
target: params.target,
|
140 |
+
};
|
141 |
+
$store.workspace.edges.push(edge);
|
142 |
+
}
|
143 |
+
function onDelete(params) {
|
144 |
+
const { nodes, edges } = params;
|
145 |
+
for (const node of nodes) {
|
146 |
+
const index = $store.workspace.nodes.findIndex((x) => x.id === node.id);
|
147 |
+
if (index !== -1) $store.workspace.nodes.splice(index, 1);
|
148 |
+
}
|
149 |
+
for (const edge of edges) {
|
150 |
+
const index = $store.workspace.edges.findIndex((x) => x.id === edge.id);
|
151 |
+
if (index !== -1) $store.workspace.edges.splice(index, 1);
|
152 |
+
}
|
153 |
+
}
|
154 |
$: parentDir = path.split('/').slice(0, -1).join('/');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
</script>
|
156 |
|
157 |
<div class="page">
|
|
|
158 |
{#if $store.workspace !== undefined}
|
159 |
<div class="top-bar">
|
160 |
<div class="ws-name">
|
|
|
166 |
options={Object.keys($catalog.data || {})}
|
167 |
value={$store.workspace.env}
|
168 |
onChange={(env) => {
|
|
|
169 |
$store.workspace.env = env;
|
170 |
}}
|
171 |
/>
|
|
|
178 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
179 |
on:paneclick={toggleNodeSearch}
|
180 |
on:nodeclick={nodeClick}
|
181 |
+
onconnect={onConnect}
|
182 |
+
ondelete={onDelete}
|
183 |
proOptions={{ hideAttribution: true }}
|
184 |
maxZoom={3}
|
185 |
minZoom={0.3}
|
web/src/LynxKiteNode.svelte
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<script lang="ts">
|
|
|
2 |
import { Handle, useSvelteFlow, useUpdateNodeInternals, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
3 |
import ChevronDownRight from 'virtual:icons/tabler/chevron-down-right';
|
4 |
|
@@ -24,11 +25,14 @@
|
|
24 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
25 |
export let onToggle = () => {};
|
26 |
|
|
|
27 |
$: expanded = !data.collapsed;
|
28 |
function titleClicked() {
|
29 |
-
|
30 |
-
data =
|
31 |
onToggle({ expanded });
|
|
|
|
|
32 |
updateNodeInternals();
|
33 |
}
|
34 |
function asPx(n: number | undefined) {
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { getContext } from 'svelte';
|
3 |
import { Handle, useSvelteFlow, useUpdateNodeInternals, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
4 |
import ChevronDownRight from 'virtual:icons/tabler/chevron-down-right';
|
5 |
|
|
|
25 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
26 |
export let onToggle = () => {};
|
27 |
|
28 |
+
$: store = getContext('LynxKite store');
|
29 |
$: expanded = !data.collapsed;
|
30 |
function titleClicked() {
|
31 |
+
const i = $store.workspace.nodes.findIndex((n) => n.id === id);
|
32 |
+
$store.workspace.nodes[i].data.collapsed = expanded;
|
33 |
onToggle({ expanded });
|
34 |
+
// Trigger update.
|
35 |
+
data = data;
|
36 |
updateNodeInternals();
|
37 |
}
|
38 |
function asPx(n: number | undefined) {
|
web/src/NodeWithParams.svelte
CHANGED
@@ -1,12 +1,20 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {
|
|
|
3 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
4 |
import NodeParameter from './NodeParameter.svelte';
|
5 |
type $$Props = NodeProps;
|
6 |
export let id: $$Props['id'];
|
7 |
export let data: $$Props['data'];
|
8 |
const { updateNodeData } = useSvelteFlow();
|
|
|
9 |
$: metaParams = data.meta?.params;
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
</script>
|
11 |
|
12 |
<LynxKiteNode {...$$props}>
|
@@ -15,7 +23,7 @@
|
|
15 |
{name}
|
16 |
{value}
|
17 |
meta={metaParams?.[name]}
|
18 |
-
onChange={(
|
19 |
/>
|
20 |
{/each}
|
21 |
<slot />
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { getContext } from 'svelte';
|
3 |
+
import { type NodeProps, useSvelteFlow, useUpdateNodeInternals } from '@xyflow/svelte';
|
4 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
5 |
import NodeParameter from './NodeParameter.svelte';
|
6 |
type $$Props = NodeProps;
|
7 |
export let id: $$Props['id'];
|
8 |
export let data: $$Props['data'];
|
9 |
const { updateNodeData } = useSvelteFlow();
|
10 |
+
const updateNodeInternals = useUpdateNodeInternals();
|
11 |
$: metaParams = data.meta?.params;
|
12 |
+
$: store = getContext('LynxKite store');
|
13 |
+
function setParam(name, newValue) {
|
14 |
+
const i = $store.workspace.nodes.findIndex((n) => n.id === id);
|
15 |
+
$store.workspace.nodes[i].data.params[name] = newValue;
|
16 |
+
updateNodeInternals();
|
17 |
+
}
|
18 |
</script>
|
19 |
|
20 |
<LynxKiteNode {...$$props}>
|
|
|
23 |
{name}
|
24 |
{value}
|
25 |
meta={metaParams?.[name]}
|
26 |
+
onChange={(value) => setParam(name, value)}
|
27 |
/>
|
28 |
{/each}
|
29 |
<slot />
|