diff --git a/packages/y-indexeddb/src/__tests__/index.spec.ts b/packages/y-indexeddb/src/__tests__/index.spec.ts index 10476b4b2..641203427 100644 --- a/packages/y-indexeddb/src/__tests__/index.spec.ts +++ b/packages/y-indexeddb/src/__tests__/index.spec.ts @@ -3,6 +3,7 @@ */ import 'fake-indexeddb/auto'; +import { initPage } from '@affine/env/blocksuite'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { assertExists, uuidv4, Workspace } from '@blocksuite/store'; import { openDB } from 'idb'; @@ -14,13 +15,15 @@ import type { WorkspacePersist } from '../index'; import { createIndexedDBProvider, dbVersion, + DEFAULT_DB_NAME, + downloadBinary, getMilestones, markMilestone, revertUpdate, setMergeCount, } from '../index'; -async function getUpdates(id: string): Promise { +async function getUpdates(id: string): Promise { const db = await openDB(rootDBName, dbVersion); const store = await db .transaction('workspace', 'readonly') @@ -33,7 +36,7 @@ async function getUpdates(id: string): Promise { let id: string; let workspace: Workspace; -const rootDBName = 'affine-local'; +const rootDBName = DEFAULT_DB_NAME; beforeEach(() => { id = uuidv4(); @@ -62,7 +65,12 @@ describe('indexeddb provider', () => { const data = await store.get(id); expect(data).toEqual({ id, - updates: [], + updates: [ + { + timestamp: expect.any(Number), + update: encodeStateAsUpdate(workspace.doc), + }, + ], }); const page = workspace.createPage('page0'); const pageBlockId = page.addBlock('affine:page', { title: '' }); @@ -129,6 +137,7 @@ describe('indexeddb provider', () => { provider.connect(); const p1 = provider.whenSynced; await p1; + const snapshot = encodeStateAsUpdate(workspace.doc); provider.disconnect(); { const page = workspace.createPage('page0'); @@ -138,7 +147,8 @@ describe('indexeddb provider', () => { } { const updates = await getUpdates(workspace.id); - expect(updates).toEqual([]); + expect(updates.length).toBe(1); + expect(updates[0]).toEqual(snapshot); } provider.connect(); const p2 = provider.whenSynced; @@ -292,3 +302,32 @@ describe('milestone', () => { } }); }); + +describe('utils', () => { + test('download binary', async () => { + const page = workspace.createPage('page0'); + initPage(page); + const provider = createIndexedDBProvider( + workspace.id, + workspace.doc, + rootDBName + ); + provider.connect(); + await provider.whenSynced; + provider.disconnect(); + const update = await downloadBinary(workspace.id, rootDBName); + expect(update).toBeInstanceOf(Uint8Array); + const newWorkspace = new Workspace({ + id, + isSSR: true, + }); + newWorkspace.register(AffineSchemas).register(__unstableSchemas); + applyUpdate(newWorkspace.doc, update); + await new Promise(resolve => + setTimeout(() => { + expect(workspace.doc.toJSON()).toEqual(newWorkspace.doc.toJSON()); + resolve(); + }, 0) + ); + }); +}); diff --git a/packages/y-indexeddb/src/index.ts b/packages/y-indexeddb/src/index.ts index 81fe48d30..f20196ada 100644 --- a/packages/y-indexeddb/src/index.ts +++ b/packages/y-indexeddb/src/index.ts @@ -1,5 +1,5 @@ import { openDB } from 'idb'; -import type { DBSchema, IDBPDatabase } from 'idb/build/entry'; +import type { IDBPDatabase } from 'idb/build/entry'; import { applyUpdate, diffUpdate, @@ -10,11 +10,23 @@ import { UndoManager, } from 'yjs'; +import type { + BlockSuiteBinaryDB, + IndexedDBProvider, + OldYjsDB, + WorkspaceMilestone, +} from './shared'; +import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared'; + const indexeddbOrigin = Symbol('indexeddb-provider-origin'); const snapshotOrigin = Symbol('snapshot-origin'); let mergeCount = 500; +export function setMergeCount(count: number) { + mergeCount = count; +} + async function databaseExists(name: string): Promise { return new Promise(resolve => { const req = indexedDB.open(name); @@ -78,62 +90,11 @@ export class EarlyDisconnectError extends Error { } } -export function setMergeCount(count: number) { - mergeCount = count; -} - -export const dbVersion = 1; - -export function upgradeDB(db: IDBPDatabase) { - db.createObjectStore('workspace', { keyPath: 'id' }); - db.createObjectStore('milestone', { keyPath: 'id' }); -} - -export interface IndexedDBProvider { - connect: () => void; - disconnect: () => void; - cleanup: () => void; - whenSynced: Promise; -} - -export type UpdateMessage = { - timestamp: number; - update: Uint8Array; -}; - -export type WorkspacePersist = { - id: string; - updates: UpdateMessage[]; -}; - -export type WorkspaceMilestone = { - id: string; - milestone: Record; -}; - -export interface BlockSuiteBinaryDB extends DBSchema { - workspace: { - key: string; - value: WorkspacePersist; - }; - milestone: { - key: string; - value: WorkspaceMilestone; - }; -} - -export interface OldYjsDB extends DBSchema { - updates: { - key: number; - value: Uint8Array; - }; -} - export const markMilestone = async ( id: string, doc: Doc, name: string, - dbName = 'affine-local' + dbName = DEFAULT_DB_NAME ): Promise => { const dbPromise = openDB(dbName, dbVersion, { upgrade: upgradeDB, @@ -159,7 +120,7 @@ export const markMilestone = async ( export const getMilestones = async ( id: string, - dbName = 'affine-local' + dbName = DEFAULT_DB_NAME ): Promise => { const dbPromise = openDB(dbName, dbVersion, { upgrade: upgradeDB, @@ -180,7 +141,7 @@ let allDb: IDBDatabaseInfo[]; export const createIndexedDBProvider = ( id: string, doc: Doc, - dbName = 'affine-local' + dbName = DEFAULT_DB_NAME ): IndexedDBProvider => { let resolve: () => void; let reject: (reason?: unknown) => void; @@ -356,7 +317,12 @@ export const createIndexedDBProvider = ( if (!data) { await db.put('workspace', { id, - updates: [], + updates: [ + { + timestamp: Date.now(), + update: encodeStateAsUpdate(doc), + }, + ], }); } else { const updates = data.updates.map(({ update }) => update); @@ -406,3 +372,6 @@ export const createIndexedDBProvider = ( return apis; }; + +export * from './shared'; +export * from './utils'; diff --git a/packages/y-indexeddb/src/shared.ts b/packages/y-indexeddb/src/shared.ts new file mode 100644 index 000000000..a79452b5b --- /dev/null +++ b/packages/y-indexeddb/src/shared.ts @@ -0,0 +1,49 @@ +import type { DBSchema, IDBPDatabase } from 'idb/build/entry'; + +export const dbVersion = 1; +export const DEFAULT_DB_NAME = 'affine-local' as const; + +export function upgradeDB(db: IDBPDatabase) { + db.createObjectStore('workspace', { keyPath: 'id' }); + db.createObjectStore('milestone', { keyPath: 'id' }); +} + +export interface IndexedDBProvider { + connect: () => void; + disconnect: () => void; + cleanup: () => void; + whenSynced: Promise; +} + +export type UpdateMessage = { + timestamp: number; + update: Uint8Array; +}; + +export type WorkspacePersist = { + id: string; + updates: UpdateMessage[]; +}; + +export type WorkspaceMilestone = { + id: string; + milestone: Record; +}; + +export interface BlockSuiteBinaryDB extends DBSchema { + workspace: { + key: string; + value: WorkspacePersist; + }; + milestone: { + key: string; + value: WorkspaceMilestone; + }; +} + +export interface OldYjsDB extends DBSchema { + updates: { + key: number; + value: Uint8Array; + }; +} diff --git a/packages/y-indexeddb/src/utils.ts b/packages/y-indexeddb/src/utils.ts new file mode 100644 index 000000000..f1cd74cec --- /dev/null +++ b/packages/y-indexeddb/src/utils.ts @@ -0,0 +1,22 @@ +import { openDB } from 'idb'; +import { mergeUpdates } from 'yjs'; + +import type { BlockSuiteBinaryDB, UpdateMessage } from './shared'; +import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared'; + +export async function downloadBinary( + id: string, + dbName = DEFAULT_DB_NAME +): Promise { + const dbPromise = openDB(dbName, dbVersion, { + upgrade: upgradeDB, + }); + const db = await dbPromise; + const t = db.transaction('workspace', 'readonly').objectStore('workspace'); + const doc = await t.get(id); + if (!doc) { + return new Uint8Array(0); + } else { + return mergeUpdates(doc.updates.map(({ update }) => update)); + } +}