diff --git a/app.html b/app.html index b1e617d..67ec011 100644 --- a/app.html +++ b/app.html @@ -3,7 +3,7 @@ - + Story Editor diff --git a/index.html b/index.html index d0e6b72..b7354cf 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + Story Editor diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 44e1f7f..c49594d 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,4 +1,3 @@ -import * as Y from 'yjs'; import { EditorState } from '@codemirror/state'; import { keymap, EditorView } from '@codemirror/view'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; @@ -7,29 +6,35 @@ import { syntaxHighlighting, } from '@codemirror/language'; import { yCollab } from 'y-codemirror.next'; -import { onCleanup, onMount } from 'solid-js'; -import { syncDoc } from '../sync'; +import { createEffect, createResource, onCleanup, Show } from 'solid-js'; +import { LocalDocument } from '../sync'; interface EditorProps { file: string; } function Editor(props: EditorProps) { - let doc: Y.Doc; let editorDiv; let editorView: EditorView; + const [syncedDoc] = createResource(async () => + LocalDocument.load(props.file) + ); + + createEffect(() => { + const doc = syncedDoc(); + if (!doc) { + return; + } + const ydoc = doc.doc; - onMount(() => { - doc = new Y.Doc(); - syncDoc(props.file, doc); const state = EditorState.create({ - doc: doc.getText().toString(), + doc: ydoc.getText().toString(), extensions: [ EditorView.lineWrapping, history(), keymap.of([...defaultKeymap, ...historyKeymap]), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - yCollab(doc.getText(), null), + yCollab(ydoc.getText(), null), ], }); editorView = new EditorView({ state, parent: editorDiv }); @@ -37,17 +42,19 @@ function Editor(props: EditorProps) { onCleanup(() => { editorView?.destroy(); - doc?.destroy(); + syncedDoc()?.finish(); }); return (

You are editing {props.file}.

-
+ +
+
); } diff --git a/src/sync.ts b/src/sync.ts index 0e2ac23..c2eefff 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,15 +1,56 @@ +import _ from 'lodash'; import * as Y from 'yjs'; +import { openDB } from 'idb'; -const fakeStore = new Map(); +const DB_NAME = 'synced-docs'; +const LOCAL_UPDATE_STORE = 'local-updates'; -export function syncDoc(id: string, doc: Y.Doc) { - const updates = fakeStore.get(id) ?? []; - fakeStore.set(id, updates); - for (const update of updates) { - Y.applyUpdate(doc, update); +const db = await openDB(DB_NAME, 2, { + upgrade(db, oldVersion, newVersion) { + console.log(`Running upgrade from ${oldVersion} to ${newVersion}`); + db.createObjectStore(LOCAL_UPDATE_STORE); + }, +}); + +export class LocalDocument { + readonly id: string; + readonly doc: Y.Doc; + private updates: Uint8Array[]; + + constructor(id: string) { + this.id = id; + this.doc = new Y.Doc(); + this.updates = []; + + const flush = _.debounce(() => this.flush(), 1000); + this.doc.on('update', (update: Uint8Array) => { + this.updates.push(update); + flush(); + }); } - doc.on('update', (update: Uint8Array) => { - updates.push(update); - }); + public static async load(id: string): Promise { + const updates: Uint8Array[] = await db.getAll( + LOCAL_UPDATE_STORE, + IDBKeyRange.bound([id, -Infinity], [id, Infinity]) + ); + const result = new LocalDocument(id); + Y.applyUpdate(result.doc, Y.mergeUpdates(updates)); + return result; + } + + public async flush(): Promise { + const batch = this.updates; + this.updates = []; + if (batch.length > 0) { + const update = Y.mergeUpdates(batch); + const key = [this.id, new Date().valueOf()]; + await db.add(LOCAL_UPDATE_STORE, update, key); + } + } + + public async finish(): Promise { + await this.flush(); + this.doc.destroy(); + } }