Migrate to using random IDs for documents

This commit is contained in:
2025-09-03 01:18:52 -07:00
parent 0567c625e3
commit 100470b8e2
4 changed files with 85 additions and 22 deletions
+1 -1
View File
@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/app.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
+14 -1
View File
@@ -9,6 +9,8 @@ import {
import Chooser from './Chooser'; import Chooser from './Chooser';
import Editor from './Editor'; import Editor from './Editor';
import { HashRouter, Route, useNavigate } from '@solidjs/router'; import { HashRouter, Route, useNavigate } from '@solidjs/router';
import { defaultVault } from '../sync';
import * as Y from 'yjs';
interface NavbarProps { interface NavbarProps {
fileId: Accessor<string | null>; fileId: Accessor<string | null>;
@@ -28,6 +30,17 @@ export function useNavbar(): NavbarProps {
function Layout(props: any) { function Layout(props: any) {
const navigate = useNavigate(); const navigate = useNavigate();
const [fileId, setFileId] = createSignal<string | null>(null); const [fileId, setFileId] = createSignal<string | null>(null);
const title = () => {
const id = fileId();
if (id === null) {
return null;
} else {
return (defaultVault.doc.getMap() as Y.Map<any>)
.get('docs')
.get(id)
.get('title') as string;
}
};
return ( return (
<NavbarContext.Provider value={{ fileId, setFileId }}> <NavbarContext.Provider value={{ fileId, setFileId }}>
@@ -36,7 +49,7 @@ function Layout(props: any) {
<Show when={fileId() !== null}> <Show when={fileId() !== null}>
<button onclick={() => navigate('/')}>Back</button> <button onclick={() => navigate('/')}>Back</button>
</Show> </Show>
<div class="title">{fileId() ?? 'Choose a file'}</div> <div class="title">{title() ?? 'Choose a file'}</div>
</div> </div>
{props.children} {props.children}
</div> </div>
+11 -5
View File
@@ -2,20 +2,26 @@ import { For, onMount } from 'solid-js';
import './Chooser.css'; import './Chooser.css';
import { useNavbar } from './App'; import { useNavbar } from './App';
import { useNavigate } from '@solidjs/router'; import { useNavigate } from '@solidjs/router';
import { defaultVault } from '../sync';
import * as Y from 'yjs';
function Chooser() { function Chooser() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setFileId } = useNavbar(); const { setFileId } = useNavbar();
onMount(() => setFileId(null)); onMount(() => setFileId(null));
const files = ['foo', 'bar', 'baz']; const docsMap = defaultVault.doc.getMap().get('docs') as Y.Map<any>;
const titles: Record<string, string> = {};
for (const [id, docInfo] of docsMap.entries()) {
titles[id] = docInfo.get('title');
}
return ( return (
<> <>
<For each={files}> <For each={Object.entries(titles)}>
{(availableFile) => ( {([id, title]: [string, string]) => (
<button class="file" onclick={() => navigate(`doc/${availableFile}`)}> <button class="file" onclick={() => navigate(`doc/${id}`)}>
{availableFile} {title}
{/* {availableFile === props.file() ? '(*)' : ''} */} {/* {availableFile === props.file() ? '(*)' : ''} */}
</button> </button>
)} )}
+59 -15
View File
@@ -19,21 +19,6 @@ const db = await openDB(DB_NAME, 3, {
}, },
}); });
async function getOrCreateVault() {
const records = await db.getAll(VAULT_STORE);
if (records.length > 1) {
throw new Error(`Expected 1 vault record, got ${records.length}`);
} else if (records.length === 0) {
const key = generateId();
const value = { id: key };
await db.add(VAULT_STORE, value, key);
records.push(value);
}
return records[0];
}
await getOrCreateVault();
export class LocalDocument { export class LocalDocument {
readonly id: string; readonly id: string;
readonly doc: Y.Doc; readonly doc: Y.Doc;
@@ -46,6 +31,7 @@ export class LocalDocument {
const flush = _.debounce(() => this.flush(), 1000); const flush = _.debounce(() => this.flush(), 1000);
this.doc.on('update', (update: Uint8Array) => { this.doc.on('update', (update: Uint8Array) => {
// TODO: Is this generating update events on load?
this.updates.push(update); this.updates.push(update);
flush(); flush();
}); });
@@ -56,6 +42,7 @@ export class LocalDocument {
LOCAL_UPDATE_STORE, LOCAL_UPDATE_STORE,
IDBKeyRange.bound([id, -Infinity], [id, Infinity]) IDBKeyRange.bound([id, -Infinity], [id, Infinity])
); );
// console.log(`load(${id}): got ${updates.length} updates`);
const result = new LocalDocument(id); const result = new LocalDocument(id);
Y.applyUpdate(result.doc, Y.mergeUpdates(updates)); Y.applyUpdate(result.doc, Y.mergeUpdates(updates));
return result; return result;
@@ -76,3 +63,60 @@ export class LocalDocument {
this.doc.destroy(); this.doc.destroy();
} }
} }
async function getOrCreateVaultDoc() {
const records = await db.getAll(VAULT_STORE);
if (records.length > 1) {
throw new Error(`Expected 1 vault record, got ${records.length}`);
} else if (records.length === 0) {
const key = generateId();
const value = { id: key };
await db.add(VAULT_STORE, value, key);
records.push(value);
}
const id = records[0].id as string;
if (!id) {
throw new Error("Vault record didn't have id");
}
const vault = await LocalDocument.load(id);
const vaultMap = vault.doc.getMap();
if (!vaultMap.has('id')) {
vaultMap.set('id', id);
}
if (!vaultMap.has('docs')) {
vaultMap.set('docs', new Y.Map());
}
return vault;
}
export const defaultVault = await getOrCreateVaultDoc();
const docsMap = defaultVault.doc.getMap().get('docs') as Y.Map<any>;
if (docsMap.size === 0) {
console.log('Running vault migration...');
for (const oldKey of ['foo', 'bar', 'baz']) {
const newId = generateId();
const docInfo = new Y.Map([['title', oldKey]]);
docsMap.set(newId, docInfo);
const tx = db.transaction(LOCAL_UPDATE_STORE, 'readwrite');
const recordKeys = (await tx.store.getAllKeys(
IDBKeyRange.bound([oldKey, -Infinity], [oldKey, Infinity])
)) as [string, number][];
console.log(
`Found ${recordKeys.length} records for old document key ${oldKey}`
);
for (const oldRecordKey of recordKeys) {
const record = await tx.store.get(oldRecordKey);
const newRecordKey = [newId, oldRecordKey[1]];
await tx.store.put(record, newRecordKey);
}
await tx.store.delete(
IDBKeyRange.bound([oldKey, -Infinity], [oldKey, Infinity])
);
tx.commit();
}
}
(window as any).Y = Y;