mirror of
https://github.com/fluencelabs/fluent-pad
synced 2025-04-25 08:52:14 +00:00
tidy up code
This commit is contained in:
parent
f4c79f46ab
commit
d3f71f1da2
@ -1,171 +1,72 @@
|
|||||||
import * as Automerge from 'automerge';
|
import _ from 'lodash';
|
||||||
import DiffMatchPatch from 'diff-match-patch';
|
import { useEffect, useState } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { subscribeToEvent } from '@fluencelabs/fluence';
|
||||||
|
|
||||||
import { fluentPadServiceId, notifyTextUpdateFnName } from 'src/fluence/constants';
|
import { fluentPadServiceId, notifyTextUpdateFnName } from 'src/fluence/constants';
|
||||||
import { useFluenceClient } from './FluenceClientContext';
|
import { useFluenceClient } from './FluenceClientContext';
|
||||||
|
import { getUpdatedDocFromText, initDoc, SyncClient } from './sync';
|
||||||
import * as calls from 'src/fluence/calls';
|
import * as calls from 'src/fluence/calls';
|
||||||
import { FluenceClient, subscribeToEvent } from '@fluencelabs/fluence';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
interface TextDoc {
|
const broadcastUpdates = _.debounce((text: string, syncClient: SyncClient) => {
|
||||||
value: Automerge.Text;
|
let doc = syncClient.getDoc();
|
||||||
}
|
if (doc) {
|
||||||
|
let newDoc = getUpdatedDocFromText(doc, text);
|
||||||
const dmp = new DiffMatchPatch();
|
syncClient.syncDoc(newDoc);
|
||||||
|
|
||||||
const getUpdatedDocFromText = (oldDoc: TextDoc | null, newText: string) => {
|
|
||||||
const prevText = oldDoc ? oldDoc.value.toString() : '';
|
|
||||||
const diff = dmp.diff_main(prevText, newText);
|
|
||||||
dmp.diff_cleanupSemantic(diff);
|
|
||||||
const patches = dmp.patch_make(prevText, diff);
|
|
||||||
|
|
||||||
const newDoc = Automerge.change(oldDoc, (doc) => {
|
|
||||||
patches.forEach((patch) => {
|
|
||||||
let idx = patch.start1;
|
|
||||||
patch.diffs.forEach(([operation, changeText]) => {
|
|
||||||
switch (operation) {
|
|
||||||
case 1: // Insertion
|
|
||||||
doc.value.insertAt!(idx, ...changeText.split(''));
|
|
||||||
break;
|
|
||||||
case 0: // No Change
|
|
||||||
idx += changeText.length;
|
|
||||||
break;
|
|
||||||
case -1: // Deletion
|
|
||||||
for (let i = 0; i < changeText.length; i++) {
|
|
||||||
doc.value.deleteAt!(idx);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return newDoc;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseState = (entry: string) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(entry);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('couldnt parse state format: ' + entry);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
}, 100);
|
||||||
|
|
||||||
const applyStates = (startingDoc: TextDoc | null, entries: calls.Entry[]) => {
|
|
||||||
let res = startingDoc;
|
|
||||||
for (let entry of entries) {
|
|
||||||
const state = parseState(entry.body) as TextDoc;
|
|
||||||
if (state) {
|
|
||||||
if (!res) {
|
|
||||||
res = state;
|
|
||||||
} else {
|
|
||||||
res = Automerge.merge(res, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res === null) {
|
|
||||||
res = Automerge.from({
|
|
||||||
value: new Automerge.Text(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const broadcastUpdates = _.debounce(async (client: FluenceClient, doc: TextDoc) => {
|
|
||||||
const entry = {
|
|
||||||
fluentPadState: Automerge.save(doc),
|
|
||||||
};
|
|
||||||
const entryStr = JSON.stringify(entry);
|
|
||||||
|
|
||||||
await calls.addEntry(client, entryStr);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
export const CollaborativeEditor = () => {
|
export const CollaborativeEditor = () => {
|
||||||
const client = useFluenceClient()!;
|
const client = useFluenceClient()!;
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
// const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const [syncClient, setSyncClient] = useState(new SyncClient());
|
||||||
const docSetRef = useRef(new Automerge.DocSet<TextDoc>());
|
|
||||||
const [amConnection, setAmConnection] = useState<any>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const doc = Automerge.from({ value: new Automerge.Text() });
|
syncClient.syncDoc(initDoc());
|
||||||
docSetRef.current.setDoc('doc', doc);
|
syncClient.handleDocUpdate = (doc) => {
|
||||||
docSetRef.current.registerHandler((id, doc) => {
|
console.log('syncClient.handleDocUpdate');
|
||||||
if (id === 'doc') {
|
setText(doc.text.toString());
|
||||||
setText(doc.value.toString());
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
const connection = new Automerge.Connection(docSetRef.current, (msg) => {
|
|
||||||
console.log('on update');
|
|
||||||
calls.addEntry(client, JSON.stringify(msg));
|
|
||||||
});
|
|
||||||
connection.open();
|
|
||||||
setAmConnection(connection);
|
|
||||||
|
|
||||||
const unsub1 = subscribeToEvent(client, fluentPadServiceId, notifyTextUpdateFnName, (args, tetraplets) => {
|
syncClient.handleSendChanges = (changes: string) => {
|
||||||
const [authorPeerId, stateStr, isAuthorized] = args;
|
console.log('syncClient.handleSendChanges');
|
||||||
|
calls.addEntry(client, changes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsub = subscribeToEvent(client, fluentPadServiceId, notifyTextUpdateFnName, (args, tetraplets) => {
|
||||||
|
const [authorPeerId, changes, isAuthorized] = args;
|
||||||
if (authorPeerId === client.selfPeerId.toB58String()) {
|
if (authorPeerId === client.selfPeerId.toB58String()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = parseState(stateStr);
|
if (changes) {
|
||||||
console.log(state);
|
syncClient.receiveChanges(changes);
|
||||||
if (state) {
|
|
||||||
connection.receiveMsg(state);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
syncClient.start();
|
||||||
|
|
||||||
// don't block
|
// don't block
|
||||||
calls.getHistory(client).then((res) => {
|
calls.getHistory(client).then((res) => {
|
||||||
for (let e of res) {
|
for (let e of res) {
|
||||||
try {
|
syncClient.receiveChanges(e.body);
|
||||||
const msg = JSON.parse(e.body);
|
|
||||||
connection.receiveMsg(msg);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("history didn't work", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// setText(newDoc);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsub1();
|
unsub();
|
||||||
|
syncClient.stop();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const amHistory = text
|
|
||||||
// ? Automerge.getHistory(text).map((x) => {
|
|
||||||
// return x.snapshot.value;
|
|
||||||
// })
|
|
||||||
// : [];
|
|
||||||
|
|
||||||
// const textValue = text ? text.value : '';
|
|
||||||
|
|
||||||
const handleTextUpdate = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleTextUpdate = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setText(e.target.value);
|
const newText = e.target.value;
|
||||||
|
setText(newText);
|
||||||
let doc = docSetRef.current.getDoc('doc');
|
broadcastUpdates(newText, syncClient);
|
||||||
if (doc) {
|
|
||||||
let res = getUpdatedDocFromText(doc, e.target.value);
|
|
||||||
console.log(res);
|
|
||||||
docSetRef.current.setDoc('doc', res!);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<textarea value={text} onChange={handleTextUpdate} />
|
<textarea value={text} onChange={handleTextUpdate} />
|
||||||
<div>
|
|
||||||
Automerge changes:
|
|
||||||
<ul>
|
|
||||||
{/* {amHistory.map((value, index) => (
|
|
||||||
<li key={index}>{value}</li>
|
|
||||||
))} */}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
100
client/src/app/sync.ts
Normal file
100
client/src/app/sync.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { from, Connection, Doc, DocSet, Text, Message, change } from 'automerge';
|
||||||
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
|
|
||||||
|
export interface TextDoc {
|
||||||
|
text: Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initDoc = () => {
|
||||||
|
return from({
|
||||||
|
text: new Text(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SyncClient<T = TextDoc> {
|
||||||
|
private static globalDocId = 'fluent-pad-doc';
|
||||||
|
|
||||||
|
private docSet: DocSet<T>;
|
||||||
|
private connection: Connection<T>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.docSet = new DocSet<T>();
|
||||||
|
this.connection = new Connection<T>(this.docSet, this.doSendMessage.bind(this));
|
||||||
|
this.docSet.registerHandler(this.doHandleDocUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.connection.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDoc() {
|
||||||
|
return this.docSet.getDoc(SyncClient.globalDocId);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncDoc(doc: Doc<T>) {
|
||||||
|
this.docSet.setDoc(SyncClient.globalDocId, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveChanges(changes: string) {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(changes);
|
||||||
|
this.connection.receiveMsg(msg);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Couldnt receive message', changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDocUpdate?: (doc: Doc<T>) => void;
|
||||||
|
|
||||||
|
handleSendChanges?: (changes: string) => void;
|
||||||
|
|
||||||
|
private doSendMessage(msg: Message) {
|
||||||
|
if (this.handleSendChanges) {
|
||||||
|
const body = JSON.stringify(msg);
|
||||||
|
this.handleSendChanges(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private doHandleDocUpdate(docId: string, doc: Doc<T>) {
|
||||||
|
if (docId === SyncClient.globalDocId && this.handleDocUpdate) {
|
||||||
|
console.log(docId, doc);
|
||||||
|
this.handleDocUpdate(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmp = new DiffMatchPatch();
|
||||||
|
|
||||||
|
export const getUpdatedDocFromText = (oldDoc: TextDoc, newText: string) => {
|
||||||
|
const prevText = oldDoc.text.toString();
|
||||||
|
const diff = dmp.diff_main(prevText, newText);
|
||||||
|
dmp.diff_cleanupSemantic(diff);
|
||||||
|
const patches = dmp.patch_make(prevText, diff);
|
||||||
|
|
||||||
|
const newDoc = change(oldDoc, (doc) => {
|
||||||
|
patches.forEach((patch) => {
|
||||||
|
let idx = patch.start1;
|
||||||
|
patch.diffs.forEach(([operation, changeText]) => {
|
||||||
|
switch (operation) {
|
||||||
|
case 1: // Insertion
|
||||||
|
doc.text.insertAt!(idx, ...changeText.split(''));
|
||||||
|
break;
|
||||||
|
case 0: // No Change
|
||||||
|
idx += changeText.length;
|
||||||
|
break;
|
||||||
|
case -1: // Deletion
|
||||||
|
for (let i = 0; i < changeText.length; i++) {
|
||||||
|
doc.text.deleteAt!(idx);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return newDoc;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user