Spaces:
Running
Running
Pass code edits through CRDT.
Browse files
examples/word2vec.py
CHANGED
@@ -22,3 +22,8 @@ def word2vec_1000():
|
|
22 |
@op(ENV, "Take first N")
|
23 |
def first_n(df: pd.DataFrame, *, n=10):
|
24 |
return df.head(n)
|
|
|
|
|
|
|
|
|
|
|
|
22 |
@op(ENV, "Take first N")
|
23 |
def first_n(df: pd.DataFrame, *, n=10):
|
24 |
return df.head(n)
|
25 |
+
|
26 |
+
|
27 |
+
@op(ENV, "Sample N")
|
28 |
+
def sample_n(df: pd.DataFrame, *, n=10):
|
29 |
+
return df.sample(n)
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
@@ -26,11 +26,11 @@ def ws_exception_handler(exception, log):
|
|
26 |
return True
|
27 |
|
28 |
|
29 |
-
class
|
30 |
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
31 |
"""Initialize a room for the workspace with the given name.
|
32 |
|
33 |
-
The workspace is loaded from "
|
34 |
"""
|
35 |
crdt_path = pathlib.Path(".crdt")
|
36 |
path = crdt_path / f"{name}.crdt"
|
@@ -82,6 +82,37 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
82 |
return room
|
83 |
|
84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
last_ws_input = None
|
86 |
|
87 |
|
@@ -247,14 +278,21 @@ async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, d
|
|
247 |
print(f"Finished running {name} in {ws_pyd.env}.")
|
248 |
|
249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
@contextlib.asynccontextmanager
|
251 |
async def lifespan(app):
|
252 |
-
global
|
253 |
-
|
254 |
-
|
255 |
-
)
|
256 |
-
async with
|
257 |
-
|
|
|
258 |
print("closing websocket server")
|
259 |
|
260 |
|
@@ -265,5 +303,12 @@ def sanitize_path(path):
|
|
265 |
@router.websocket("/ws/crdt/{room_name}")
|
266 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
267 |
room_name = sanitize_path(room_name)
|
268 |
-
server = pycrdt_websocket.ASGIServer(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
269 |
await server({"path": room_name}, websocket._receive, websocket._send)
|
|
|
26 |
return True
|
27 |
|
28 |
|
29 |
+
class WorkspaceWebsocketServer(pycrdt_websocket.WebsocketServer):
|
30 |
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
31 |
"""Initialize a room for the workspace with the given name.
|
32 |
|
33 |
+
The workspace is loaded from ".crdt" if it exists there, or from a JSON file, or a new workspace is created.
|
34 |
"""
|
35 |
crdt_path = pathlib.Path(".crdt")
|
36 |
path = crdt_path / f"{name}.crdt"
|
|
|
82 |
return room
|
83 |
|
84 |
|
85 |
+
class CodeWebsocketServer(WorkspaceWebsocketServer):
|
86 |
+
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
87 |
+
"""Initialize a room for a text document with the given name."""
|
88 |
+
crdt_path = pathlib.Path(".crdt")
|
89 |
+
path = crdt_path / f"{name}.crdt"
|
90 |
+
assert path.is_relative_to(crdt_path)
|
91 |
+
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
92 |
+
ydoc = pycrdt.Doc()
|
93 |
+
ydoc["text"] = text = pycrdt.Text()
|
94 |
+
# Replay updates from the store.
|
95 |
+
try:
|
96 |
+
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
97 |
+
ydoc.apply_update(update)
|
98 |
+
except pycrdt_websocket.ystore.YDocNotFound:
|
99 |
+
pass
|
100 |
+
if len(text) == 0:
|
101 |
+
if os.path.exists(name):
|
102 |
+
with open(name) as f:
|
103 |
+
text += f.read()
|
104 |
+
room = pycrdt_websocket.YRoom(
|
105 |
+
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
106 |
+
)
|
107 |
+
room.text = text
|
108 |
+
|
109 |
+
def on_change(changes):
|
110 |
+
asyncio.create_task(code_changed(name, changes, text))
|
111 |
+
|
112 |
+
text.observe(on_change)
|
113 |
+
return room
|
114 |
+
|
115 |
+
|
116 |
last_ws_input = None
|
117 |
|
118 |
|
|
|
278 |
print(f"Finished running {name} in {ws_pyd.env}.")
|
279 |
|
280 |
|
281 |
+
async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
|
282 |
+
# TODO: Make this more fancy?
|
283 |
+
with open(name, "w") as f:
|
284 |
+
f.write(str(text))
|
285 |
+
|
286 |
+
|
287 |
@contextlib.asynccontextmanager
|
288 |
async def lifespan(app):
|
289 |
+
global ws_websocket_server
|
290 |
+
global code_websocket_server
|
291 |
+
ws_websocket_server = WorkspaceWebsocketServer(auto_clean_rooms=False)
|
292 |
+
code_websocket_server = CodeWebsocketServer(auto_clean_rooms=False)
|
293 |
+
async with ws_websocket_server:
|
294 |
+
async with code_websocket_server:
|
295 |
+
yield
|
296 |
print("closing websocket server")
|
297 |
|
298 |
|
|
|
303 |
@router.websocket("/ws/crdt/{room_name}")
|
304 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
305 |
room_name = sanitize_path(room_name)
|
306 |
+
server = pycrdt_websocket.ASGIServer(ws_websocket_server)
|
307 |
+
await server({"path": room_name}, websocket._receive, websocket._send)
|
308 |
+
|
309 |
+
|
310 |
+
@router.websocket("/ws/code/crdt/{room_name}")
|
311 |
+
async def code_crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
312 |
+
room_name = sanitize_path(room_name)
|
313 |
+
server = pycrdt_websocket.ASGIServer(code_websocket_server)
|
314 |
await server({"path": room_name}, websocket._receive, websocket._send)
|
lynxkite-app/src/lynxkite_app/main.py
CHANGED
@@ -39,15 +39,6 @@ def get_catalog(workspace: str):
|
|
39 |
return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
|
40 |
|
41 |
|
42 |
-
@app.get("/api/getCode")
|
43 |
-
def get_code(path: str):
|
44 |
-
path = data_path / path
|
45 |
-
assert path.is_relative_to(data_path)
|
46 |
-
with open(path) as f:
|
47 |
-
code = f.read()
|
48 |
-
return {"code": code}
|
49 |
-
|
50 |
-
|
51 |
class SaveRequest(workspace.BaseConfig):
|
52 |
path: str
|
53 |
ws: workspace.Workspace
|
|
|
39 |
return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
|
40 |
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
class SaveRequest(workspace.BaseConfig):
|
43 |
path: str
|
44 |
ws: workspace.Workspace
|
lynxkite-app/web/package-lock.json
CHANGED
@@ -29,6 +29,7 @@
|
|
29 |
"react-router-dom": "^7.0.2",
|
30 |
"swr": "^2.2.5",
|
31 |
"unplugin-icons": "^0.21.0",
|
|
|
32 |
"y-websocket": "^2.0.4",
|
33 |
"yjs": "^13.6.20"
|
34 |
},
|
@@ -7397,6 +7398,23 @@
|
|
7397 |
"yjs": "^13.0.0"
|
7398 |
}
|
7399 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7400 |
"node_modules/y-protocols": {
|
7401 |
"version": "1.0.6",
|
7402 |
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
|
|
|
29 |
"react-router-dom": "^7.0.2",
|
30 |
"swr": "^2.2.5",
|
31 |
"unplugin-icons": "^0.21.0",
|
32 |
+
"y-monaco": "^0.1.6",
|
33 |
"y-websocket": "^2.0.4",
|
34 |
"yjs": "^13.6.20"
|
35 |
},
|
|
|
7398 |
"yjs": "^13.0.0"
|
7399 |
}
|
7400 |
},
|
7401 |
+
"node_modules/y-monaco": {
|
7402 |
+
"version": "0.1.6",
|
7403 |
+
"resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz",
|
7404 |
+
"integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==",
|
7405 |
+
"license": "MIT",
|
7406 |
+
"dependencies": {
|
7407 |
+
"lib0": "^0.2.43"
|
7408 |
+
},
|
7409 |
+
"engines": {
|
7410 |
+
"node": ">=12.0.0",
|
7411 |
+
"npm": ">=6.0.0"
|
7412 |
+
},
|
7413 |
+
"peerDependencies": {
|
7414 |
+
"monaco-editor": ">=0.20.0",
|
7415 |
+
"yjs": "^13.3.1"
|
7416 |
+
}
|
7417 |
+
},
|
7418 |
"node_modules/y-protocols": {
|
7419 |
"version": "1.0.6",
|
7420 |
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
|
lynxkite-app/web/package.json
CHANGED
@@ -32,6 +32,7 @@
|
|
32 |
"react-router-dom": "^7.0.2",
|
33 |
"swr": "^2.2.5",
|
34 |
"unplugin-icons": "^0.21.0",
|
|
|
35 |
"y-websocket": "^2.0.4",
|
36 |
"yjs": "^13.6.20"
|
37 |
},
|
|
|
32 |
"react-router-dom": "^7.0.2",
|
33 |
"swr": "^2.2.5",
|
34 |
"unplugin-icons": "^0.21.0",
|
35 |
+
"y-monaco": "^0.1.6",
|
36 |
"y-websocket": "^2.0.4",
|
37 |
"yjs": "^13.6.20"
|
38 |
},
|
lynxkite-app/web/src/Code.tsx
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
// Full-page editor for code files.
|
2 |
|
3 |
-
import Editor from "@monaco-editor/react";
|
4 |
-
import {
|
5 |
-
import { useEffect } from "react";
|
6 |
import { useParams } from "react-router";
|
7 |
-
import
|
|
|
8 |
// @ts-ignore
|
9 |
import Atom from "~icons/tabler/atom.jsx";
|
10 |
// @ts-ignore
|
@@ -13,20 +14,41 @@ import Backspace from "~icons/tabler/backspace.jsx";
|
|
13 |
import Close from "~icons/tabler/x.jsx";
|
14 |
import favicon from "./assets/favicon.ico";
|
15 |
import theme from "./code-theme.ts";
|
|
|
|
|
16 |
|
17 |
export default function Code() {
|
18 |
-
useEffect(() => {
|
19 |
-
const initMonaco = async () => {
|
20 |
-
const monaco = await loader.init();
|
21 |
-
monaco.editor.defineTheme("lynxkite", theme);
|
22 |
-
};
|
23 |
-
initMonaco();
|
24 |
-
}, []);
|
25 |
const { path } = useParams();
|
26 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
27 |
-
const
|
28 |
-
|
29 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
return (
|
31 |
<div className="workspace">
|
32 |
<div className="top-bar bg-neutral">
|
@@ -46,29 +68,19 @@ export default function Code() {
|
|
46 |
</a>
|
47 |
</div>
|
48 |
</div>
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
defaultValue={code.data.code}
|
63 |
-
theme="lynxkite"
|
64 |
-
options={{
|
65 |
-
cursorStyle: "block",
|
66 |
-
cursorBlinking: "solid",
|
67 |
-
minimap: { enabled: false },
|
68 |
-
renderLineHighlight: "none",
|
69 |
-
}}
|
70 |
-
/>
|
71 |
-
)}
|
72 |
</div>
|
73 |
);
|
74 |
}
|
|
|
1 |
// Full-page editor for code files.
|
2 |
|
3 |
+
import Editor, { type Monaco } from "@monaco-editor/react";
|
4 |
+
import type { editor } from "monaco-editor";
|
5 |
+
import { useEffect, useRef } from "react";
|
6 |
import { useParams } from "react-router";
|
7 |
+
import { WebsocketProvider } from "y-websocket";
|
8 |
+
import * as Y from "yjs";
|
9 |
// @ts-ignore
|
10 |
import Atom from "~icons/tabler/atom.jsx";
|
11 |
// @ts-ignore
|
|
|
14 |
import Close from "~icons/tabler/x.jsx";
|
15 |
import favicon from "./assets/favicon.ico";
|
16 |
import theme from "./code-theme.ts";
|
17 |
+
// For some reason y-monaco is gigantic. The other Monaco packages are small.
|
18 |
+
const MonacoBinding = await import("y-monaco").then((m) => m.MonacoBinding);
|
19 |
|
20 |
export default function Code() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
const { path } = useParams();
|
22 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
23 |
+
const ydoc = useRef<any>();
|
24 |
+
const wsProvider = useRef<any>();
|
25 |
+
const monacoBinding = useRef<any>();
|
26 |
+
function beforeMount(monaco: Monaco) {
|
27 |
+
monaco.editor.defineTheme("lynxkite", theme);
|
28 |
+
}
|
29 |
+
function onMount(_editor: editor.IStandaloneCodeEditor) {
|
30 |
+
ydoc.current = new Y.Doc();
|
31 |
+
const text = ydoc.current.getText("text");
|
32 |
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
33 |
+
wsProvider.current = new WebsocketProvider(
|
34 |
+
`${proto}//${location.host}/ws/code/crdt`,
|
35 |
+
path!,
|
36 |
+
ydoc.current,
|
37 |
+
);
|
38 |
+
monacoBinding.current = new MonacoBinding(
|
39 |
+
text,
|
40 |
+
_editor.getModel()!,
|
41 |
+
new Set([_editor]),
|
42 |
+
wsProvider.current.awareness,
|
43 |
+
);
|
44 |
+
}
|
45 |
+
useEffect(() => {
|
46 |
+
return () => {
|
47 |
+
ydoc.current?.destroy();
|
48 |
+
wsProvider.current?.destroy();
|
49 |
+
monacoBinding.current?.destroy();
|
50 |
+
};
|
51 |
+
});
|
52 |
return (
|
53 |
<div className="workspace">
|
54 |
<div className="top-bar bg-neutral">
|
|
|
68 |
</a>
|
69 |
</div>
|
70 |
</div>
|
71 |
+
<Editor
|
72 |
+
defaultLanguage="python"
|
73 |
+
theme="lynxkite"
|
74 |
+
path={path}
|
75 |
+
beforeMount={beforeMount}
|
76 |
+
onMount={onMount}
|
77 |
+
options={{
|
78 |
+
cursorStyle: "block",
|
79 |
+
cursorBlinking: "solid",
|
80 |
+
minimap: { enabled: false },
|
81 |
+
renderLineHighlight: "none",
|
82 |
+
}}
|
83 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
</div>
|
85 |
);
|
86 |
}
|