(frontend) pad persistance

This commit manage the persistance of the pad.
We save the pad in different ways:
- when the user close the tab or the browser
- when the user leave the page (go to another pad
  by example)
- every 1 minute
----
- We save the pad only if the pad has been modified.
- Pads are collaborative, to not save multiple
  times the same pad, we save the pad only if
  the user is the last to have modified the pad.
----
Because of the collaborative aspect of the pads,
the best way to store our pad is to save the
Y.Doc, to do so the recommended way is to convert
the Y.Doc to a Uint8Array and then to a string
(base64). Our pad are saved as a string in a
object in a Minio bucket.
This commit is contained in:
Anthony LC
2024-05-17 23:06:11 +02:00
committed by Anthony LC
parent d75649d18a
commit 64d508c260
4 changed files with 148 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import { WebrtcProvider } from 'y-webrtc';
import { Box } from '@/components';
import { useAuthStore } from '@/core/auth';
import useSavePad from '../hook/useSavePad';
import { usePadStore } from '../stores';
import { Pad } from '../types';
import { randomColor } from '../utils';
@@ -37,6 +38,7 @@ interface BlockNoteContentProps {
export const BlockNoteContent = ({ pad, provider }: BlockNoteContentProps) => {
const { userData } = useAuthStore();
const { setEditor, padsStore } = usePadStore();
useSavePad(pad.id, provider.doc);
const storedEditor = padsStore?.[pad.id]?.editor;
const editor = useMemo(() => {

View File

@@ -0,0 +1,83 @@
import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
import { useUpdatePad } from '../api/useUpdatePad';
import { toBase64 } from '../utils';
const useSavePad = (padId: string, doc: Y.Doc) => {
const { mutate: updatePad } = useUpdatePad();
const [initialDoc, setInitialDoc] = useState<string>(
toBase64(Y.encodeStateAsUpdate(doc)),
);
/**
* Update initial doc when doc is updated by other users,
* so only the user typing will trigger the save.
* This is to avoid saving the same doc multiple time.
*/
useEffect(() => {
const onUpdate = (
_uintArray: Uint8Array,
_pluginKey: string,
updatedDoc: Y.Doc,
transaction: Y.Transaction,
) => {
if (!transaction.local) {
setInitialDoc(toBase64(Y.encodeStateAsUpdate(updatedDoc)));
}
};
doc.on('update', onUpdate);
return () => {
doc.off('update', onUpdate);
};
}, [doc]);
const savePad = useCallback(() => {
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
/**
* Save only if the doc has changed.
*/
if (initialDoc === newDoc) {
return;
}
setInitialDoc(newDoc);
updatePad({
id: padId,
content: newDoc,
});
}, [initialDoc, padId, doc, updatePad]);
const timeout = useRef<NodeJS.Timeout>();
const router = useRouter();
useEffect(() => {
if (timeout.current) {
clearTimeout(timeout.current);
}
const onSave = () => {
savePad();
};
// Save every minute
timeout.current = setInterval(onSave, 60000);
// Save when the user leaves the page
addEventListener('beforeunload', onSave);
// Save when the user navigates to another page
router.events.on('routeChangeStart', onSave);
return () => {
clearTimeout(timeout.current);
removeEventListener('beforeunload', onSave);
router.events.off('routeChangeStart', onSave);
};
}, [router.events, savePad]);
};
export default useSavePad;

View File

@@ -22,3 +22,7 @@ function hslToHex(h: number, s: number, l: number) {
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export const toBase64 = (
str: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>,
) => Buffer.from(str).toString('base64');