import { openDB } from 'idb'; import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector, UndoManager, } from 'yjs'; import type { BlockSuiteBinaryDB, WorkspaceMilestone } from './shared'; import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared'; const snapshotOrigin = 'snapshot-origin'; /** * @internal */ const saveAlert = (event: BeforeUnloadEvent) => { event.preventDefault(); return (event.returnValue = 'Data is not saved. Are you sure you want to leave?'); }; export const writeOperation = async (op: Promise) => { window.addEventListener('beforeunload', saveAlert, { capture: true, }); await op; window.removeEventListener('beforeunload', saveAlert, { capture: true, }); }; export function revertUpdate( doc: Doc, snapshotUpdate: Uint8Array, getMetadata: (key: string) => 'Text' | 'Map' | 'Array' ) { const snapshotDoc = new Doc(); applyUpdate(snapshotDoc, snapshotUpdate, snapshotOrigin); const currentStateVector = encodeStateVector(doc); const snapshotStateVector = encodeStateVector(snapshotDoc); const changesSinceSnapshotUpdate = encodeStateAsUpdate( doc, snapshotStateVector ); const undoManager = new UndoManager( [...snapshotDoc.share.keys()].map(key => { const type = getMetadata(key); if (type === 'Text') { return snapshotDoc.getText(key); } else if (type === 'Map') { return snapshotDoc.getMap(key); } else if (type === 'Array') { return snapshotDoc.getArray(key); } throw new Error('Unknown type'); }), { trackedOrigins: new Set([snapshotOrigin]), } ); applyUpdate(snapshotDoc, changesSinceSnapshotUpdate, snapshotOrigin); undoManager.undo(); const revertChangesSinceSnapshotUpdate = encodeStateAsUpdate( snapshotDoc, currentStateVector ); applyUpdate(doc, revertChangesSinceSnapshotUpdate, snapshotOrigin); } export class EarlyDisconnectError extends Error { constructor() { super('Early disconnect'); } } export class CleanupWhenConnectingError extends Error { constructor() { super('Cleanup when connecting'); } } export const markMilestone = async ( id: string, doc: Doc, name: string, dbName = DEFAULT_DB_NAME ): Promise => { const dbPromise = openDB(dbName, dbVersion, { upgrade: upgradeDB, }); const db = await dbPromise; const store = db .transaction('milestone', 'readwrite') .objectStore('milestone'); const milestone = await store.get('id'); const binary = encodeStateAsUpdate(doc); if (!milestone) { await store.put({ id, milestone: { [name]: binary, }, }); } else { milestone.milestone[name] = binary; await store.put(milestone); } }; export const getMilestones = async ( id: string, dbName: string = DEFAULT_DB_NAME ): Promise => { const dbPromise = openDB(dbName, dbVersion, { upgrade: upgradeDB, }); const db = await dbPromise; const store = db .transaction('milestone', 'readonly') .objectStore('milestone'); const milestone = await store.get(id); if (!milestone) { return null; } return milestone.milestone; }; export * from './provider'; export * from './shared'; export * from './utils';