Spaces:
Running
Running
Backend can now put results in the CRDT.
Browse files- server/crdt.py +72 -12
- server/lynxkite_ops.py +0 -5
- web/src/LynxKiteFlow.svelte +2 -0
- web/src/NodeWithParams.svelte +2 -1
- web/src/NodeWithVisualization.svelte +2 -1
server/crdt.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
'''CRDT is used to synchronize workspace state for backend and frontend(s).'''
|
|
|
2 |
import contextlib
|
3 |
import fastapi
|
4 |
import os.path
|
@@ -10,6 +11,7 @@ import pycrdt_websocket.ystore
|
|
10 |
router = fastapi.APIRouter()
|
11 |
|
12 |
def ws_exception_handler(exception, log):
|
|
|
13 |
log.exception(exception)
|
14 |
return True
|
15 |
|
@@ -18,34 +20,93 @@ 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'] = pycrdt.Array()
|
22 |
-
ws['edges'] = pycrdt.Array()
|
23 |
-
ws['env'] = 'unset'
|
24 |
# Replay updates from the store.
|
25 |
try:
|
26 |
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
27 |
ydoc.apply_update(update)
|
28 |
except pycrdt_websocket.ystore.YDocNotFound:
|
29 |
pass
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
async def get_room(self, name: str) -> pycrdt_websocket.YRoom:
|
35 |
-
print('get_room', name, self.rooms)
|
36 |
if name not in self.rooms:
|
37 |
self.rooms[name] = await self.init_room(name)
|
38 |
-
print('get_room2', name, self.rooms)
|
39 |
room = self.rooms[name]
|
40 |
await self.start_room(room)
|
41 |
return room
|
42 |
|
43 |
-
print('new WebsocketServer')
|
44 |
websocket_server = WebsocketServer(exception_handler=ws_exception_handler, auto_clean_rooms=False)
|
45 |
asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
|
46 |
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
@contextlib.asynccontextmanager
|
51 |
async def lifespan(app):
|
@@ -58,5 +119,4 @@ def sanitize_path(path):
|
|
58 |
@router.websocket("/ws/crdt/{room_name}")
|
59 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
60 |
room_name = sanitize_path(room_name)
|
61 |
-
print('room_name', room_name)
|
62 |
await asgi_server({'path': room_name}, websocket._receive, websocket._send)
|
|
|
1 |
'''CRDT is used to synchronize workspace state for backend and frontend(s).'''
|
2 |
+
import asyncio
|
3 |
import contextlib
|
4 |
import fastapi
|
5 |
import os.path
|
|
|
11 |
router = fastapi.APIRouter()
|
12 |
|
13 |
def ws_exception_handler(exception, log):
|
14 |
+
print('exception', exception)
|
15 |
log.exception(exception)
|
16 |
return True
|
17 |
|
|
|
20 |
ystore = pycrdt_websocket.ystore.FileYStore(f'crdt_data/{name}.crdt')
|
21 |
ydoc = pycrdt.Doc()
|
22 |
ydoc['workspace'] = ws = pycrdt.Map()
|
|
|
|
|
|
|
23 |
# Replay updates from the store.
|
24 |
try:
|
25 |
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
26 |
ydoc.apply_update(update)
|
27 |
except pycrdt_websocket.ystore.YDocNotFound:
|
28 |
pass
|
29 |
+
if 'nodes' not in ws:
|
30 |
+
ws['nodes'] = pycrdt.Array()
|
31 |
+
if 'edges' not in ws:
|
32 |
+
ws['edges'] = pycrdt.Array()
|
33 |
+
if 'env' not in ws:
|
34 |
+
ws['env'] = 'unset'
|
35 |
+
room = pycrdt_websocket.YRoom(ystore=ystore, ydoc=ydoc)
|
36 |
+
room.ws = ws
|
37 |
+
def on_change(changes):
|
38 |
+
asyncio.create_task(workspace_changed(changes, ws))
|
39 |
+
ws.observe_deep(on_change)
|
40 |
+
return room
|
41 |
|
42 |
async def get_room(self, name: str) -> pycrdt_websocket.YRoom:
|
|
|
43 |
if name not in self.rooms:
|
44 |
self.rooms[name] = await self.init_room(name)
|
|
|
45 |
room = self.rooms[name]
|
46 |
await self.start_room(room)
|
47 |
return room
|
48 |
|
|
|
49 |
websocket_server = WebsocketServer(exception_handler=ws_exception_handler, auto_clean_rooms=False)
|
50 |
asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
|
51 |
|
52 |
+
last_ws_input = None
|
53 |
+
def clean_input(ws_pyd):
|
54 |
+
for node in ws_pyd.nodes:
|
55 |
+
node.data.display = None
|
56 |
+
node.position.x = 0
|
57 |
+
node.position.y = 0
|
58 |
+
if node.model_extra:
|
59 |
+
for key in list(node.model_extra.keys()):
|
60 |
+
delattr(node, key)
|
61 |
+
|
62 |
+
def crdt_update(crdt_obj, python_obj):
|
63 |
+
if isinstance(python_obj, dict):
|
64 |
+
for key, value in python_obj.items():
|
65 |
+
if isinstance(value, dict):
|
66 |
+
if crdt_obj.get(key) is None:
|
67 |
+
crdt_obj[key] = pycrdt.Map()
|
68 |
+
crdt_update(crdt_obj[key], value)
|
69 |
+
elif isinstance(value, list):
|
70 |
+
if crdt_obj.get(key) is None:
|
71 |
+
crdt_obj[key] = pycrdt.Array()
|
72 |
+
crdt_update(crdt_obj[key], value)
|
73 |
+
else:
|
74 |
+
print('set', key, value)
|
75 |
+
crdt_obj[key] = value
|
76 |
+
elif isinstance(python_obj, list):
|
77 |
+
for i, value in enumerate(python_obj):
|
78 |
+
if isinstance(value, dict):
|
79 |
+
if i >= len(crdt_obj):
|
80 |
+
crdt_obj.append(pycrdt.Map())
|
81 |
+
crdt_update(crdt_obj[i], value)
|
82 |
+
elif isinstance(value, list):
|
83 |
+
if i >= len(crdt_obj):
|
84 |
+
crdt_obj.append(pycrdt.Array())
|
85 |
+
crdt_update(crdt_obj[i], value)
|
86 |
+
else:
|
87 |
+
if i >= len(crdt_obj):
|
88 |
+
crdt_obj.append(value)
|
89 |
+
else:
|
90 |
+
print('set', i, value)
|
91 |
+
crdt_obj[i] = value
|
92 |
+
else:
|
93 |
+
raise ValueError('Invalid type:', python_obj)
|
94 |
+
|
95 |
+
async def workspace_changed(e, ws_crdt):
|
96 |
+
global last_ws_input
|
97 |
+
from . import workspace
|
98 |
+
ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
|
99 |
+
clean_input(ws_pyd)
|
100 |
+
if ws_pyd == last_ws_input:
|
101 |
+
return
|
102 |
+
print('ws changed')
|
103 |
+
last_ws_input = ws_pyd.model_copy(deep=True)
|
104 |
+
workspace.execute(ws_pyd)
|
105 |
+
for nc, np in zip(ws_crdt['nodes'], ws_pyd.nodes):
|
106 |
+
if 'data' not in nc:
|
107 |
+
nc['data'] = pycrdt.Map()
|
108 |
+
# Display is added as an opaque Box.
|
109 |
+
nc['data']['display'] = np.data.display
|
110 |
|
111 |
@contextlib.asynccontextmanager
|
112 |
async def lifespan(app):
|
|
|
119 |
@router.websocket("/ws/crdt/{room_name}")
|
120 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
121 |
room_name = sanitize_path(room_name)
|
|
|
122 |
await asgi_server({'path': room_name}, websocket._receive, websocket._send)
|
server/lynxkite_ops.py
CHANGED
@@ -105,11 +105,6 @@ def execute(ws):
|
|
105 |
data = node.data
|
106 |
op = catalog[data.title]
|
107 |
params = {**data.params}
|
108 |
-
if op.sub_nodes:
|
109 |
-
sub_nodes = children.get(node.id, [])
|
110 |
-
sub_node_ids = [node.id for node in sub_nodes]
|
111 |
-
sub_edges = [edge for edge in ws.edges if edge.source in sub_node_ids]
|
112 |
-
params['sub_flow'] = {'nodes': sub_nodes, 'edges': sub_edges}
|
113 |
# Convert inputs.
|
114 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|
115 |
if p.type == nx.Graph and isinstance(x, Bundle):
|
|
|
105 |
data = node.data
|
106 |
op = catalog[data.title]
|
107 |
params = {**data.params}
|
|
|
|
|
|
|
|
|
|
|
108 |
# Convert inputs.
|
109 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|
110 |
if p.type == nx.Graph and isinstance(x, Bundle):
|
web/src/LynxKiteFlow.svelte
CHANGED
@@ -136,7 +136,9 @@
|
|
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 |
}
|
|
|
136 |
const edge = {
|
137 |
id: `${params.source} ${params.target}`,
|
138 |
source: params.source,
|
139 |
+
sourceHandle: params.sourceHandle,
|
140 |
target: params.target,
|
141 |
+
targetHandle: params.targetHandle,
|
142 |
};
|
143 |
$store.workspace.edges.push(edge);
|
144 |
}
|
web/src/NodeWithParams.svelte
CHANGED
@@ -15,10 +15,11 @@
|
|
15 |
$store.workspace.nodes[i].data.params[name] = newValue;
|
16 |
updateNodeInternals();
|
17 |
}
|
|
|
18 |
</script>
|
19 |
|
20 |
<LynxKiteNode {...$$props}>
|
21 |
-
{#each
|
22 |
<NodeParameter
|
23 |
{name}
|
24 |
{value}
|
|
|
15 |
$store.workspace.nodes[i].data.params[name] = newValue;
|
16 |
updateNodeInternals();
|
17 |
}
|
18 |
+
$: params = data?.params ? Object.entries(data.params) : [];
|
19 |
</script>
|
20 |
|
21 |
<LynxKiteNode {...$$props}>
|
22 |
+
{#each params as [name, value]}
|
23 |
<NodeParameter
|
24 |
{name}
|
25 |
{value}
|
web/src/NodeWithVisualization.svelte
CHANGED
@@ -5,11 +5,12 @@
|
|
5 |
import { init } from 'echarts';
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
|
|
8 |
</script>
|
9 |
|
10 |
<NodeWithParams {...$$props}>
|
11 |
{#if data.display}
|
12 |
-
<Chart {init} options={
|
13 |
{/if}
|
14 |
</NodeWithParams>
|
15 |
<style>
|
|
|
5 |
import { init } from 'echarts';
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
8 |
+
$: display = JSON.parse(JSON.stringify(data?.display?.value));
|
9 |
</script>
|
10 |
|
11 |
<NodeWithParams {...$$props}>
|
12 |
{#if data.display}
|
13 |
+
<Chart {init} options={display} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
|
14 |
{/if}
|
15 |
</NodeWithParams>
|
16 |
<style>
|