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 { 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', () => {

View File

@@ -15,6 +15,23 @@ const snapshotOrigin = Symbol('snapshot-origin');
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(
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<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,
},
],
});
}
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<IDBPDatabase<OldYjsDB>>(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<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
.transaction('workspace', 'readwrite')
.objectStore('workspace');