Skip to content

Commit 37c0f10

Browse files
committed
wip
1 parent 03af6ed commit 37c0f10

23 files changed

Lines changed: 335 additions & 250 deletions

backend/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ authors = [
1010
]
1111

1212
dependencies = [
13+
"loro~=1.8.2",
14+
"automerge @ git+https://github.com/bugbakery/automerge-py.git@ca6d8d3",
1315
"redis~=5.0",
1416
"fastapi~=0.115",
1517
"uvicorn[standard]~=0.20",

backend/transcribee_backend/helpers/sync.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,15 @@ async def broadcast_sender(self):
7777
# websocket connection eventually. Since it is not touched on the way, we can pass a list of
7878
# bytes here instead of just bytes as would be allowed by the asgi spec:
7979
# https://asgi.readthedocs.io/en/latest/specs/www.html#send-send-event
80-
message = [bytes([SyncMessageType.FULL_DOCUMENT])]
80+
tag_change = bytes([SyncMessageType.CHANGE])
8181
for update in self._session.exec(statement):
82-
message.append(len(update.change_bytes).to_bytes(4) + update.change_bytes + bytes([SyncMessageType.CHANGE]))
83-
# message.append(update.change_bytes)
84-
print("hello", len(update.change_bytes))
85-
message[-1] = message[-1][:-1]
86-
await self._ws.send_bytes(message) # type: ignore
87-
# END
82+
await self._ws.send_bytes(tag_change + len(update.change_bytes).to_bytes(4) + update.change_bytes)
8883

89-
await self._ws.send_bytes(bytes([SyncMessageType.CHANGE_BACKLOG_COMPLETE]))
84+
await self._ws.send_bytes(bytes([SyncMessageType.BACKLOG_COMPLETE]))
9085

9186
while True:
92-
msg = await self._msg_queue.get()
93-
await self._ws.send_bytes(bytes([SyncMessageType.CHANGE]) + len(msg).to_bytes(4) + msg)
87+
msg: bytes = await self._msg_queue.get()
88+
await self._ws.send_bytes(tag_change + len(msg).to_bytes(4) + msg)
9489

9590
async def run(self):
9691
await self._ws.accept()

backend/transcribee_backend/media_storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def unsign(user_sig: str, max_age: int) -> str:
9191
raise ValueError()
9292
signature = b64_decode(signature.encode())
9393

94-
logging.warning(f"Verifying {value=} {signature=}")
94+
logging.debug(f"Verifying {value=} {signature=}")
9595
raw_signature = salted_hmac(
9696
key_salt=MEDIA_SIGNATURE_TYPE, secret=settings.secret_key, value=value
9797
)

backend/uv.lock

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@zip.js/zip.js": "^2.7.31",
6565
"clsx": "^1.2.1",
6666
"fast-equals": "^5.2.2",
67-
"loro-crdt": "^1.4.4",
67+
"loro-crdt": "^1.9.0",
6868
"openapi-typescript-fetch": "github:bugbakery/openapi-typescript-fetch#4b5cc33983c5a658feedd628d417599db2bd672c",
6969
"react": "^18.2.0",
7070
"react-base16-styling": "^0.9.1",
Lines changed: 73 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useLocation } from 'wouter';
2-
import { useEffect, useRef, useState } from 'react';
2+
import { useEffect, useMemo, useRef, useState } from 'react';
33
import { Editor, createEditor } from 'slate';
44
import { withReact } from 'slate-react';
55
import ReconnectingWebSocket from 'reconnecting-websocket';
@@ -10,22 +10,36 @@ import { LoroDoc } from 'loro-crdt';
1010

1111
enum MessageSyncType {
1212
Change = 1,
13-
ChangeBacklogComplete = 2,
14-
FullDoc = 3,
13+
BacklogComplete = 2,
14+
}
15+
16+
function int_from_32bit_big_endian(msg_data: Uint8Array, idx: number): number {
17+
let to_return = msg_data[idx + 0];
18+
to_return = (to_return << 8) + msg_data[idx + 1];
19+
to_return = (to_return << 8) + msg_data[idx + 2];
20+
to_return = (to_return << 8) + msg_data[idx + 3];
21+
return to_return;
1522
}
1623

1724
export function useAutomergeWebsocketEditor(
1825
url: string,
1926
{ onInitialSyncComplete }: { onInitialSyncComplete: (editor?: Editor) => void },
20-
): [Editor?, Paragraph[]?] {
27+
): [Editor, Paragraph[]?] {
2128
const debug = useDebugMode();
22-
const [editorAndInitialValue, setEditorAndInitialValue] = useState<null | {
23-
editor: Editor;
24-
initialValue: Paragraph[];
25-
}>(null);
26-
const editorRef = useRef<undefined | Editor>();
27-
if (editorRef.current !== editorAndInitialValue?.editor)
28-
editorRef.current = editorAndInitialValue?.editor;
29+
const [initialValue, setInitialValue] = useState<null | Paragraph[]>(null);
30+
const editor = useMemo(() => {
31+
const doc = new LoroDoc();
32+
const baseEditor = createEditor();
33+
const editorWithReact = withReact(baseEditor);
34+
35+
doc.subscribeLocalUpdates(sendDocChange);
36+
37+
const editor = withLoroDoc(editorWithReact, doc);
38+
editor._doc = doc;
39+
40+
return editor;
41+
}, [url]);
42+
2943
const wsRef = useRef<ReconnectingWebSocket | null>(null);
3044

3145
function sendDocChange(change: Uint8Array) {
@@ -37,97 +51,60 @@ export function useAutomergeWebsocketEditor(
3751
useEffect(() => {
3852
const ws = new ReconnectingWebSocket(url, [], { debug });
3953

40-
let bytesReceived = 0;
4154
console.time('initialSync');
4255

43-
const createNewEditor = (doc: LoroDoc) => {
44-
const baseEditor = createEditor();
45-
const editorWithReact = withReact(baseEditor);
46-
47-
doc.subscribeLocalUpdates(sendDocChange)
48-
// editor.addDocChangeListener(sendDocChange);
49-
50-
const editor = withLoroDoc(editorWithReact, doc);
51-
editor.doc = doc;
52-
53-
onInitialSyncComplete(editor);
54-
55-
setEditorAndInitialValue((oldValue) => {
56-
// oldValue?.editor.removeDocChangeListener(sendDocChange);
57-
// const initialValue =
58-
// doc.children !== undefined
59-
// ? JSON.parse(JSON.stringify(migratedDoc.children))
60-
// : [];
61-
return { editor: editor, initialValue: editor.doc.toJSON().root.children };
62-
});
63-
};
64-
65-
const onMessage = async (event: MessageEvent) => {
66-
let msg_data = new Uint8Array(await event.data.arrayBuffer());
67-
bytesReceived += msg_data.length;
68-
const updates = [];
69-
let idx = 0;
70-
let backlogComplete = false;
71-
let fullDoc = false;
72-
console.log("message", msg_data)
73-
while (idx < msg_data.length) {
74-
const msg_type = msg_data[idx];
75-
idx += 1;
76-
if ((msg_type === MessageSyncType.Change) || (msg_type === MessageSyncType.FullDoc)) {
77-
let msg_len = msg_data[idx + 0];
78-
msg_len = (msg_len << 8) + msg_data[idx + 1];
79-
msg_len = (msg_len << 8) + msg_data[idx + 2];
80-
msg_len = (msg_len << 8) + msg_data[idx + 3];
81-
82-
// const msg_len = ((((msg_data[idx + 0] << 8 + msg_data[idx + 1]) << 8) + msg_data[idx + 2]) << 8) + msg_data[idx + 3];
83-
console.log(msg_len, msg_data[idx + 0], msg_data[idx + 1], msg_data[idx + 2], msg_data[idx + 3])
84-
idx += 4;
85-
updates.push(msg_data.slice(idx, idx + msg_len))
86-
idx += msg_len
87-
88-
// if (
89-
// !editorRef.current ||
90-
// Automerge.decodeChange(msg).actor == Automerge.getActorId(editorRef.current.doc)
91-
// )
92-
// return;
93-
94-
// HistoryEditor.withoutSaving(editorRef.current, () => {
95-
// editorRef.current?.setDoc(newDoc);
96-
// });
97-
98-
} else if (msg_type === MessageSyncType.ChangeBacklogComplete) {
99-
backlogComplete = true;
100-
console.info('backlog complete');
101-
break;
102-
} else if (msg_type === MessageSyncType.FullDoc) {
103-
fullDoc = true;
56+
const updates = [];
57+
let initialSyncDone = false;
58+
59+
const generator = messageGenerator();
60+
generator.next();
61+
62+
async function* messageGenerator(): AsyncGenerator<void, void, Message> {
63+
while (true) {
64+
const message = yield;
65+
if (message) {
66+
const msg_data = new Uint8Array(await message.arrayBuffer());
67+
const msg_type = msg_data[0];
68+
69+
console.log(msg_data);
70+
if (msg_type === MessageSyncType.Change) {
71+
let idx = 1;
72+
while (idx < msg_data.length) {
73+
const msg_len = int_from_32bit_big_endian(msg_data, idx);
74+
console.log(msg_len);
75+
idx += 4;
76+
updates.push(msg_data.slice(idx, idx + msg_len));
77+
idx += msg_len;
78+
}
79+
if (initialSyncDone) {
80+
console.time('importBatch');
81+
console.log(`updates:`, updates);
82+
console.log(editor._doc.importBatch(updates));
83+
console.timeEnd('importBatch');
84+
}
85+
} else if (msg_type === MessageSyncType.BacklogComplete) {
86+
console.time('importBatch');
87+
console.log(`updates:`, updates);
88+
console.log(editor._doc.importBatch(updates));
89+
console.timeEnd('importBatch');
90+
editor.onInitialSyncComplete();
91+
setInitialValue(editor._doc.getMap('root').get('children').toJSON());
92+
console.timeEnd('initialSync');
93+
onInitialSyncComplete(editor);
94+
console.info('backlog complete');
95+
initialSyncDone = true;
96+
}
10497
}
10598
}
99+
}
106100

107-
// TODO(robin): HACK
108-
if ((fullDoc || !editorRef.current) && updates.length > 0) {
109-
const doc = new LoroDoc()
110-
console.time('importBatch');
111-
console.log(updates)
112-
console.log(doc.importBatch(updates));
113-
console.timeEnd('importBatch');
114-
115-
createNewEditor(doc);
116-
} else {
117-
if (updates.length > 0) {
118-
console.time('importBatch');
119-
editorRef.current?._doc.importBatch(updates);
120-
console.timeEnd('importBatch');
121-
}
122-
}
123-
124-
if (backlogComplete) {
125-
onInitialSyncComplete(editorRef.current);
126-
}
101+
const onMessage = async (event: MessageEvent) => {
102+
await generator.next(event.data);
127103
};
128104
ws.addEventListener('message', (msg) => {
129105
onMessage(msg).catch((e) => {
130-
alert(`error while loading automerge message occured: ${e}`);
106+
console.log(`error while loading sync message occured`, e);
107+
alert(`error while loading sync message occured: ${e}`);
131108
navigate('/');
132109
});
133110
});
@@ -138,7 +115,7 @@ export function useAutomergeWebsocketEditor(
138115
wsRef.current = null;
139116
ws.close();
140117
};
141-
}, [url, setEditorAndInitialValue]);
118+
}, [url]);
142119

143-
return [editorAndInitialValue?.editor, editorAndInitialValue?.initialValue];
120+
return [editor, initialValue];
144121
}

frontend/src/editor/loro_apply.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ const applyOperation = (doc: LoroMap<SlateNode>, op: Operation): LoroMap<SlateNo
8080
getChildren(doc, op.path).insertContainer(idx, convertToLoro(op.node));
8181
} else if (op.type === 'set_node') {
8282
const entry = getNode(doc, op.path);
83-
const { newProperties } = op;
84-
for (const [key, value] of Object.entries(newProperties)) {
85-
if (value !== undefined) {
83+
const { newProperties, properties } = op;
84+
for (const key of Object.keys(properties)) {
85+
const value = newProperties[key];
86+
if (value !== undefined && value !== null) {
8687
mapSet(entry, key, convertToLoro(value));
8788
} else {
8889
entry.delete(key);

frontend/src/editor/player.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ export function PlayerBar({
8282
const time = audio.playtime || 0;
8383
let startTimeOfElement = 0;
8484

85-
if (!editor.children) return;
85+
if (!editor.docProxy.children) return;
8686

87+
console.log(editor._doc.toJSON())
8788
// we loop from the back to the front to get the first element that is no longer too far
8889
// (if no text is at the current time, we highlight the text before)
89-
outer: for (let i = editor.children.length - 1; i >= 0; i--) {
90-
const paragraph = editor.children[i];
90+
outer: for (let i = editor.docProxy.children.length - 1; i >= 0; i--) {
91+
const paragraph = editor.docProxy.children[i];
9192
if ('children' in paragraph) {
9293
for (let j = paragraph.children.length - 1; j >= 0; j--) {
9394
const word = paragraph.children[j];

0 commit comments

Comments
 (0)