fix(y-indexeddb): migration in firefox (#1904)

This commit is contained in:
Himself65
2023-04-12 22:42:17 -05:00
committed by GitHub
parent 6180a4c3cb
commit f20a151e57
2 changed files with 141 additions and 46 deletions

View File

@@ -7,6 +7,7 @@ import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { assertExists, uuidv4, Workspace } from '@blocksuite/store'; import { assertExists, uuidv4, Workspace } from '@blocksuite/store';
import { openDB } from 'idb'; import { openDB } from 'idb';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { IndexeddbPersistence } from 'y-indexeddb';
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
import type { WorkspacePersist } from '../index'; import type { WorkspacePersist } from '../index';
@@ -45,6 +46,7 @@ beforeEach(() => {
afterEach(() => { afterEach(() => {
indexedDB.deleteDatabase('affine-local'); indexedDB.deleteDatabase('affine-local');
localStorage.clear();
}); });
describe('indexeddb provider', () => { 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', () => { describe('milestone', () => {

View File

@@ -15,6 +15,23 @@ const snapshotOrigin = Symbol('snapshot-origin');
let mergeCount = 500; let mergeCount = 500;
async function databaseExists(name: string): Promise<boolean> {
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( export function revertUpdate(
doc: Doc, doc: Doc,
snapshotUpdate: Uint8Array, snapshotUpdate: Uint8Array,
@@ -158,12 +175,13 @@ export const getMilestones = async (
return milestone.milestone; return milestone.milestone;
}; };
let allDb: IDBDatabaseInfo[];
export const createIndexedDBProvider = ( export const createIndexedDBProvider = (
id: string, id: string,
doc: Doc, doc: Doc,
dbName = 'affine-local' dbName = 'affine-local'
): IndexedDBProvider => { ): IndexedDBProvider => {
let allDb: IDBDatabaseInfo[];
let resolve: () => void; let resolve: () => void;
let reject: (reason?: unknown) => void; let reject: (reason?: unknown) => void;
let early = true; let early = true;
@@ -238,53 +256,96 @@ export const createIndexedDBProvider = (
doc.on('destroy', handleDestroy); doc.on('destroy', handleDestroy);
// only run promise below, otherwise the logic is incorrect // only run promise below, otherwise the logic is incorrect
const db = await dbPromise; const db = await dbPromise;
if (!allDb || localStorage.getItem(`${dbName}-migration`) !== 'true') { do {
allDb = await indexedDB.databases(); if (!allDb || localStorage.getItem(`${dbName}-migration`) !== 'true') {
// run the migration try {
await Promise.all( allDb = await indexedDB.databases();
allDb.map(meta => { } catch {
if (meta.name && meta.version === 1) { // in firefox, `indexedDB.databases` is not exist
const name = meta.name; if (await databaseExists(id)) {
const version = meta.version; await openDB<IDBPDatabase<OldYjsDB>>(id, 1).then(async oldDB => {
return openDB<IDBPDatabase<OldYjsDB>>(name, version).then( if (!oldDB.objectStoreNames.contains('updates')) {
async oldDB => { return;
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,
},
],
});
}
} }
); 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;
} }
}) }
); // run the migration
localStorage.setItem(`${dbName}-migration`, 'true'); await Promise.all(
} allDb.map(meta => {
if (meta.name && meta.version === 1) {
const name = meta.name;
const version = meta.version;
return openDB<IDBPDatabase<OldYjsDB>>(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 const store = db
.transaction('workspace', 'readwrite') .transaction('workspace', 'readwrite')
.objectStore('workspace'); .objectStore('workspace');