Use real persistence to IndexedDB

This commit is contained in:
2025-08-23 14:34:40 -07:00
parent dec6b0fe0f
commit 7475be087e
4 changed files with 74 additions and 26 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:;base64,iVBORw0KGgo="> <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<title>Story Editor</title> <title>Story Editor</title>
</head> </head>
<body> <body>
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:;base64,iVBORw0KGgo="> <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<title>Story Editor</title> <title>Story Editor</title>
</head> </head>
<body> <body>
+22 -15
View File
@@ -1,4 +1,3 @@
import * as Y from 'yjs';
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { keymap, EditorView } from '@codemirror/view'; import { keymap, EditorView } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
@@ -7,29 +6,35 @@ import {
syntaxHighlighting, syntaxHighlighting,
} from '@codemirror/language'; } from '@codemirror/language';
import { yCollab } from 'y-codemirror.next'; import { yCollab } from 'y-codemirror.next';
import { onCleanup, onMount } from 'solid-js'; import { createEffect, createResource, onCleanup, Show } from 'solid-js';
import { syncDoc } from '../sync'; import { LocalDocument } from '../sync';
interface EditorProps { interface EditorProps {
file: string; file: string;
} }
function Editor(props: EditorProps) { function Editor(props: EditorProps) {
let doc: Y.Doc;
let editorDiv; let editorDiv;
let editorView: EditorView; 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({ const state = EditorState.create({
doc: doc.getText().toString(), doc: ydoc.getText().toString(),
extensions: [ extensions: [
EditorView.lineWrapping, EditorView.lineWrapping,
history(), history(),
keymap.of([...defaultKeymap, ...historyKeymap]), keymap.of([...defaultKeymap, ...historyKeymap]),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }), syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
yCollab(doc.getText(), null), yCollab(ydoc.getText(), null),
], ],
}); });
editorView = new EditorView({ state, parent: editorDiv }); editorView = new EditorView({ state, parent: editorDiv });
@@ -37,17 +42,19 @@ function Editor(props: EditorProps) {
onCleanup(() => { onCleanup(() => {
editorView?.destroy(); editorView?.destroy();
doc?.destroy(); syncedDoc()?.finish();
}); });
return ( return (
<div> <div>
<p>You are editing {props.file}.</p> <p>You are editing {props.file}.</p>
<div <Show when={syncedDoc()}>
ref={editorDiv} <div
class="editor-component" ref={editorDiv}
style={{ border: '1px solid black' }} class="editor-component"
/> style={{ border: '1px solid black' }}
/>
</Show>
</div> </div>
); );
} }
+50 -9
View File
@@ -1,15 +1,56 @@
import _ from 'lodash';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { openDB } from 'idb';
const fakeStore = new Map<string, Uint8Array[]>(); const DB_NAME = 'synced-docs';
const LOCAL_UPDATE_STORE = 'local-updates';
export function syncDoc(id: string, doc: Y.Doc) { const db = await openDB(DB_NAME, 2, {
const updates = fakeStore.get(id) ?? []; upgrade(db, oldVersion, newVersion) {
fakeStore.set(id, updates); console.log(`Running upgrade from ${oldVersion} to ${newVersion}`);
for (const update of updates) { db.createObjectStore(LOCAL_UPDATE_STORE);
Y.applyUpdate(doc, update); },
});
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) => { public static async load(id: string): Promise<LocalDocument> {
updates.push(update); 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<void> {
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<void> {
await this.flush();
this.doc.destroy();
}
} }