From f20a151e5761a07c4252eabc7566af0e739b207f Mon Sep 17 00:00:00 2001 From: Himself65 Date: Wed, 12 Apr 2023 22:42:17 -0500 Subject: [PATCH] fix(y-indexeddb): migration in firefox (#1904) --- .../y-indexeddb/src/__tests__/index.spec.ts | 34 ++++ packages/y-indexeddb/src/index.ts | 153 ++++++++++++------ 2 files changed, 141 insertions(+), 46 deletions(-) diff --git a/packages/y-indexeddb/src/__tests__/index.spec.ts b/packages/y-indexeddb/src/__tests__/index.spec.ts index d2a3786e7d..10476b4b26 100644 --- a/packages/y-indexeddb/src/__tests__/index.spec.ts +++ b/packages/y-indexeddb/src/__tests__/index.spec.ts @@ -7,6 +7,7 @@ import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { assertExists, uuidv4, Workspace } from '@blocksuite/store'; import { openDB } from 'idb'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { IndexeddbPersistence } from 'y-indexeddb'; import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; import type { WorkspacePersist } from '../index'; @@ -45,6 +46,7 @@ beforeEach(() => { afterEach(() => { indexedDB.deleteDatabase('affine-local'); + localStorage.clear(); }); describe('indexeddb provider', () => { @@ -196,6 +198,38 @@ describe('indexeddb provider', () => { }); } }); + + test('migration', async () => { + { + const yDoc = new Doc(); + yDoc.getMap().set('foo', 'bar'); + const persistence = new IndexeddbPersistence('test', yDoc); + await persistence.whenSynced; + persistence.destroy(); + } + { + const yDoc = new Doc(); + const provider = createIndexedDBProvider('test', yDoc); + provider.connect(); + await provider.whenSynced; + await new Promise(resolve => setTimeout(resolve, 0)); + expect(yDoc.getMap().get('foo')).toBe('bar'); + } + localStorage.clear(); + { + indexedDB.databases = vi.fn(async () => { + throw new Error('not supported'); + }); + expect(indexedDB.databases).rejects.toThrow('not supported'); + const yDoc = new Doc(); + expect(indexedDB.databases).toBeCalledTimes(1); + const provider = createIndexedDBProvider('test', yDoc); + provider.connect(); + await provider.whenSynced; + expect(indexedDB.databases).toBeCalledTimes(2); + expect(yDoc.getMap().get('foo')).toBe('bar'); + } + }); }); describe('milestone', () => { diff --git a/packages/y-indexeddb/src/index.ts b/packages/y-indexeddb/src/index.ts index 636e9565cc..81fe48d30b 100644 --- a/packages/y-indexeddb/src/index.ts +++ b/packages/y-indexeddb/src/index.ts @@ -15,6 +15,23 @@ const snapshotOrigin = Symbol('snapshot-origin'); let mergeCount = 500; +async function databaseExists(name: string): Promise { + return new Promise(resolve => { + const req = indexedDB.open(name); + let existed = true; + req.onsuccess = function () { + req.result.close(); + if (!existed) { + indexedDB.deleteDatabase(name); + } + resolve(existed); + }; + req.onupgradeneeded = function () { + existed = false; + }; + }); +} + export function revertUpdate( doc: Doc, snapshotUpdate: Uint8Array, @@ -158,12 +175,13 @@ export const getMilestones = async ( return milestone.milestone; }; +let allDb: IDBDatabaseInfo[]; + export const createIndexedDBProvider = ( id: string, doc: Doc, dbName = 'affine-local' ): IndexedDBProvider => { - let allDb: IDBDatabaseInfo[]; let resolve: () => void; let reject: (reason?: unknown) => void; let early = true; @@ -238,53 +256,96 @@ export const createIndexedDBProvider = ( doc.on('destroy', handleDestroy); // only run promise below, otherwise the logic is incorrect const db = await dbPromise; - if (!allDb || localStorage.getItem(`${dbName}-migration`) !== 'true') { - allDb = await indexedDB.databases(); - // run the migration - await Promise.all( - allDb.map(meta => { - if (meta.name && meta.version === 1) { - const name = meta.name; - const version = meta.version; - return openDB>(name, version).then( - async oldDB => { - if (!oldDB.objectStoreNames.contains('updates')) { - return; - } - const t = oldDB - .transaction('updates', 'readonly') - .objectStore('updates'); - const updates = await t.getAll(); - if ( - !Array.isArray(updates) || - !updates.every(update => update instanceof Uint8Array) - ) { - return; - } - const update = mergeUpdates(updates); - const workspaceTransaction = db - .transaction('workspace', 'readwrite') - .objectStore('workspace'); - const data = await workspaceTransaction.get(name); - if (!data) { - console.log('upgrading the database'); - await workspaceTransaction.put({ - id: name, - updates: [ - { - timestamp: Date.now(), - update, - }, - ], - }); - } + do { + if (!allDb || localStorage.getItem(`${dbName}-migration`) !== 'true') { + try { + allDb = await indexedDB.databases(); + } catch { + // in firefox, `indexedDB.databases` is not exist + if (await databaseExists(id)) { + await openDB>(id, 1).then(async oldDB => { + if (!oldDB.objectStoreNames.contains('updates')) { + return; } - ); + const t = oldDB + .transaction('updates', 'readonly') + .objectStore('updates'); + const updates = await t.getAll(); + if ( + !Array.isArray(updates) || + !updates.every(update => update instanceof Uint8Array) + ) { + return; + } + const update = mergeUpdates(updates); + const workspaceTransaction = db + .transaction('workspace', 'readwrite') + .objectStore('workspace'); + const data = await workspaceTransaction.get(id); + if (!data) { + console.log('upgrading the database'); + await workspaceTransaction.put({ + id, + updates: [ + { + timestamp: Date.now(), + update, + }, + ], + }); + } + }); + break; } - }) - ); - localStorage.setItem(`${dbName}-migration`, 'true'); - } + } + // run the migration + await Promise.all( + allDb.map(meta => { + if (meta.name && meta.version === 1) { + const name = meta.name; + const version = meta.version; + return openDB>(name, version).then( + async oldDB => { + if (!oldDB.objectStoreNames.contains('updates')) { + return; + } + const t = oldDB + .transaction('updates', 'readonly') + .objectStore('updates'); + const updates = await t.getAll(); + if ( + !Array.isArray(updates) || + !updates.every(update => update instanceof Uint8Array) + ) { + return; + } + const update = mergeUpdates(updates); + const workspaceTransaction = db + .transaction('workspace', 'readwrite') + .objectStore('workspace'); + const data = await workspaceTransaction.get(name); + if (!data) { + console.log('upgrading the database'); + await workspaceTransaction.put({ + id: name, + updates: [ + { + timestamp: Date.now(), + update, + }, + ], + }); + } + } + ); + } + }) + ); + localStorage.setItem(`${dbName}-migration`, 'true'); + break; + } + // eslint-disable-next-line no-constant-condition + } while (false); const store = db .transaction('workspace', 'readwrite') .objectStore('workspace');