mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(y-indexeddb): remove id (#2810)
This commit is contained in:
@@ -66,7 +66,7 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
|
|||||||
WorkspaceFlavour.LOCAL
|
WorkspaceFlavour.LOCAL
|
||||||
);
|
);
|
||||||
BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary);
|
BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary);
|
||||||
const persistence = createIndexedDBProvider(id, blockSuiteWorkspace.doc);
|
const persistence = createIndexedDBProvider(blockSuiteWorkspace.doc);
|
||||||
persistence.connect();
|
persistence.connect();
|
||||||
await persistence.whenSynced.then(() => {
|
await persistence.whenSynced.then(() => {
|
||||||
persistence.disconnect();
|
persistence.disconnect();
|
||||||
|
|||||||
@@ -82,10 +82,7 @@ const createAffineWebSocketProvider = (
|
|||||||
const createIndexedDBBackgroundProvider = (
|
const createIndexedDBBackgroundProvider = (
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||||
): LocalIndexedDBBackgroundProvider => {
|
): LocalIndexedDBBackgroundProvider => {
|
||||||
const indexeddbProvider = create(
|
const indexeddbProvider = create(blockSuiteWorkspace.doc);
|
||||||
blockSuiteWorkspace.id,
|
|
||||||
blockSuiteWorkspace.doc
|
|
||||||
);
|
|
||||||
const callbacks = new CallbackSet();
|
const callbacks = new CallbackSet();
|
||||||
return {
|
return {
|
||||||
flavour: 'local-indexeddb-background',
|
flavour: 'local-indexeddb-background',
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
# @toeverything/y-indexeddb
|
# @toeverything/y-indexeddb
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- persistence data in indexeddb
|
||||||
|
- sub-documents support
|
||||||
|
- fully TypeScript
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createIndexedDBProvider, downloadBinary } from '@toeverything/y-indexeddb';
|
import { createIndexedDBProvider, downloadBinary } from '@toeverything/y-indexeddb';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
|
const yDoc = new Y.Doc({
|
||||||
|
// we use `guid` as unique key
|
||||||
|
guid: 'my-doc',
|
||||||
|
});
|
||||||
|
|
||||||
// sync yDoc with indexedDB
|
// sync yDoc with indexedDB
|
||||||
const provider = createIndexedDBProvider('docName', yDoc);
|
const provider = createIndexedDBProvider(yDoc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced.then(() => {
|
await provider.whenSynced.then(() => {
|
||||||
console.log('synced');
|
console.log('synced');
|
||||||
@@ -16,7 +26,7 @@ await provider.whenSynced.then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// dowload binary data from indexedDB for once
|
// dowload binary data from indexedDB for once
|
||||||
downloadBinary('docName').then(blob => {
|
downloadBinary(yDoc.guid).then(blob => {
|
||||||
if (blob !== false) {
|
if (blob !== false) {
|
||||||
Y.applyUpdate(yDoc, blob);
|
Y.applyUpdate(yDoc, blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
describe('indexeddb provider', () => {
|
describe('indexeddb provider', () => {
|
||||||
test('connect', async () => {
|
test('connect', async () => {
|
||||||
const provider = createIndexedDBProvider(workspace.id, workspace.doc);
|
const provider = createIndexedDBProvider(workspace.doc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
const db = await openDB(rootDBName, dbVersion);
|
const db = await openDB(rootDBName, dbVersion);
|
||||||
@@ -119,11 +119,7 @@ describe('indexeddb provider', () => {
|
|||||||
})
|
})
|
||||||
.register(AffineSchemas)
|
.register(AffineSchemas)
|
||||||
.register(__unstableSchemas);
|
.register(__unstableSchemas);
|
||||||
const provider2 = createIndexedDBProvider(
|
const provider2 = createIndexedDBProvider(secondWorkspace.doc, rootDBName);
|
||||||
secondWorkspace.id,
|
|
||||||
secondWorkspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider2.connect();
|
provider2.connect();
|
||||||
await provider2.whenSynced;
|
await provider2.whenSynced;
|
||||||
expect(Workspace.Y.encodeStateAsUpdate(secondWorkspace.doc)).toEqual(
|
expect(Workspace.Y.encodeStateAsUpdate(secondWorkspace.doc)).toEqual(
|
||||||
@@ -132,11 +128,7 @@ describe('indexeddb provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('disconnect suddenly', async () => {
|
test('disconnect suddenly', async () => {
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||||
workspace.id,
|
|
||||||
workspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
const fn = vi.fn();
|
const fn = vi.fn();
|
||||||
provider.connect();
|
provider.connect();
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
@@ -146,11 +138,7 @@ describe('indexeddb provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('connect and disconnect', async () => {
|
test('connect and disconnect', async () => {
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||||
workspace.id,
|
|
||||||
workspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
expect(provider.connected).toBe(true);
|
expect(provider.connected).toBe(true);
|
||||||
const p1 = provider.whenSynced;
|
const p1 = provider.whenSynced;
|
||||||
@@ -186,7 +174,7 @@ describe('indexeddb provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('cleanup', async () => {
|
test('cleanup', async () => {
|
||||||
const provider = createIndexedDBProvider(workspace.id, workspace.doc);
|
const provider = createIndexedDBProvider(workspace.doc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
const db = await openDB(rootDBName, dbVersion);
|
const db = await openDB(rootDBName, dbVersion);
|
||||||
@@ -212,7 +200,7 @@ describe('indexeddb provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('cleanup when connecting', async () => {
|
test('cleanup when connecting', async () => {
|
||||||
const provider = createIndexedDBProvider(workspace.id, workspace.doc);
|
const provider = createIndexedDBProvider(workspace.doc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await expect(() => provider.cleanup()).rejects.toThrowError(
|
await expect(() => provider.cleanup()).rejects.toThrowError(
|
||||||
CleanupWhenConnectingError
|
CleanupWhenConnectingError
|
||||||
@@ -224,11 +212,7 @@ describe('indexeddb provider', () => {
|
|||||||
|
|
||||||
test('merge', async () => {
|
test('merge', async () => {
|
||||||
setMergeCount(5);
|
setMergeCount(5);
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||||
workspace.id,
|
|
||||||
workspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
{
|
{
|
||||||
const page = workspace.createPage({ id: 'page0' });
|
const page = workspace.createPage({ id: 'page0' });
|
||||||
@@ -247,21 +231,20 @@ describe('indexeddb provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("data won't be lost", async () => {
|
test("data won't be lost", async () => {
|
||||||
const id = uuidv4();
|
|
||||||
const doc = new Workspace.Y.Doc();
|
const doc = new Workspace.Y.Doc();
|
||||||
const map = doc.getMap('map');
|
const map = doc.getMap('map');
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
map.set(`${i}`, i);
|
map.set(`${i}`, i);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const provider = createIndexedDBProvider(id, doc, rootDBName);
|
const provider = createIndexedDBProvider(doc, rootDBName);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const newDoc = new Workspace.Y.Doc();
|
const newDoc = new Workspace.Y.Doc();
|
||||||
const provider = createIndexedDBProvider(id, newDoc, rootDBName);
|
const provider = createIndexedDBProvider(newDoc, rootDBName);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
@@ -280,8 +263,10 @@ describe('indexeddb provider', () => {
|
|||||||
await persistence.destroy();
|
await persistence.destroy();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const yDoc = new Doc();
|
const yDoc = new Doc({
|
||||||
const provider = createIndexedDBProvider('test', yDoc);
|
guid: 'test',
|
||||||
|
});
|
||||||
|
const provider = createIndexedDBProvider(yDoc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
@@ -293,9 +278,11 @@ describe('indexeddb provider', () => {
|
|||||||
throw new Error('not supported');
|
throw new Error('not supported');
|
||||||
});
|
});
|
||||||
await expect(indexedDB.databases).rejects.toThrow('not supported');
|
await expect(indexedDB.databases).rejects.toThrow('not supported');
|
||||||
const yDoc = new Doc();
|
const yDoc = new Doc({
|
||||||
|
guid: 'test',
|
||||||
|
});
|
||||||
expect(indexedDB.databases).toBeCalledTimes(1);
|
expect(indexedDB.databases).toBeCalledTimes(1);
|
||||||
const provider = createIndexedDBProvider('test', yDoc);
|
const provider = createIndexedDBProvider(yDoc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
expect(indexedDB.databases).toBeCalledTimes(2);
|
expect(indexedDB.databases).toBeCalledTimes(2);
|
||||||
@@ -314,8 +301,10 @@ describe('indexeddb provider', () => {
|
|||||||
expect(event).toBe('beforeunload');
|
expect(event).toBe('beforeunload');
|
||||||
return oldRemoveEventListener(event, fn, options);
|
return oldRemoveEventListener(event, fn, options);
|
||||||
});
|
});
|
||||||
const doc = new Doc();
|
const doc = new Doc({
|
||||||
const provider = createIndexedDBProvider('1', doc);
|
guid: '1',
|
||||||
|
});
|
||||||
|
const provider = createIndexedDBProvider(doc);
|
||||||
const map = doc.getMap('map');
|
const map = doc.getMap('map');
|
||||||
map.set('1', 1);
|
map.set('1', 1);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
@@ -394,21 +383,25 @@ describe('subDoc', () => {
|
|||||||
test('basic', async () => {
|
test('basic', async () => {
|
||||||
let json1: any, json2: any;
|
let json1: any, json2: any;
|
||||||
{
|
{
|
||||||
const doc = new Doc();
|
const doc = new Doc({
|
||||||
|
guid: 'test',
|
||||||
|
});
|
||||||
const map = doc.getMap();
|
const map = doc.getMap();
|
||||||
const subDoc = new Doc();
|
const subDoc = new Doc();
|
||||||
subDoc.load();
|
subDoc.load();
|
||||||
map.set('1', subDoc);
|
map.set('1', subDoc);
|
||||||
map.set('2', 'test');
|
map.set('2', 'test');
|
||||||
const provider = createIndexedDBProvider('test', doc);
|
const provider = createIndexedDBProvider(doc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
json1 = doc.toJSON();
|
json1 = doc.toJSON();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const doc = new Doc();
|
const doc = new Doc({
|
||||||
const provider = createIndexedDBProvider('test', doc);
|
guid: 'test',
|
||||||
|
});
|
||||||
|
const provider = createIndexedDBProvider(doc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
const map = doc.getMap();
|
const map = doc.getMap();
|
||||||
@@ -427,11 +420,7 @@ describe('subDoc', () => {
|
|||||||
});
|
});
|
||||||
await page0.waitForLoaded();
|
await page0.waitForLoaded();
|
||||||
const { paragraphBlockId: paragraphBlockIdPage1 } = initEmptyPage(page0);
|
const { paragraphBlockId: paragraphBlockIdPage1 } = initEmptyPage(page0);
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||||
workspace.id,
|
|
||||||
workspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
const page1 = workspace.createPage({
|
const page1 = workspace.createPage({
|
||||||
id: 'page1',
|
id: 'page1',
|
||||||
@@ -446,11 +435,7 @@ describe('subDoc', () => {
|
|||||||
isSSR: true,
|
isSSR: true,
|
||||||
});
|
});
|
||||||
newWorkspace.register(AffineSchemas).register(__unstableSchemas);
|
newWorkspace.register(AffineSchemas).register(__unstableSchemas);
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(newWorkspace.doc, rootDBName);
|
||||||
newWorkspace.id,
|
|
||||||
newWorkspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
const page0 = newWorkspace.getPage('page0') as Page;
|
const page0 = newWorkspace.getPage('page0') as Page;
|
||||||
@@ -474,11 +459,7 @@ describe('utils', () => {
|
|||||||
const page = workspace.createPage({ id: 'page0' });
|
const page = workspace.createPage({ id: 'page0' });
|
||||||
await page.waitForLoaded();
|
await page.waitForLoaded();
|
||||||
initEmptyPage(page);
|
initEmptyPage(page);
|
||||||
const provider = createIndexedDBProvider(
|
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||||
workspace.id,
|
|
||||||
workspace.doc,
|
|
||||||
rootDBName
|
|
||||||
);
|
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
|
|||||||
@@ -148,10 +148,16 @@ type SubDocsEvent = {
|
|||||||
loaded: Set<Doc>;
|
loaded: Set<Doc>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use `doc.guid` as the unique key, please make sure it not changes.
|
||||||
|
*/
|
||||||
export const createIndexedDBProvider = (
|
export const createIndexedDBProvider = (
|
||||||
id: string,
|
|
||||||
doc: Doc,
|
doc: Doc,
|
||||||
dbName: string = DEFAULT_DB_NAME
|
dbName: string = DEFAULT_DB_NAME,
|
||||||
|
/**
|
||||||
|
* In the future, migrate will be removed and there will be a separate function
|
||||||
|
*/
|
||||||
|
migrate = true
|
||||||
): IndexedDBProvider => {
|
): IndexedDBProvider => {
|
||||||
let resolve: () => void;
|
let resolve: () => void;
|
||||||
let reject: (reason?: unknown) => void;
|
let reject: (reason?: unknown) => void;
|
||||||
@@ -336,15 +342,21 @@ export const createIndexedDBProvider = (
|
|||||||
reject = _reject;
|
reject = _reject;
|
||||||
});
|
});
|
||||||
connected = true;
|
connected = true;
|
||||||
trackDoc(id, doc);
|
trackDoc(doc.guid, doc);
|
||||||
// only the runs `await` below, otherwise the logic is incorrect
|
// only the runs `await` below, otherwise the logic is incorrect
|
||||||
const db = await dbPromise;
|
const db = await dbPromise;
|
||||||
await tryMigrate(db, id, dbName);
|
if (migrate) {
|
||||||
|
// Tips:
|
||||||
|
// this is only backward compatible with the yjs official version of y-indexeddb
|
||||||
|
await tryMigrate(db, doc.guid, dbName);
|
||||||
|
}
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recursively save all docs into indexeddb
|
||||||
const docs: [string, Doc][] = [];
|
const docs: [string, Doc][] = [];
|
||||||
docs.push([id, doc]);
|
docs.push([doc.guid, doc]);
|
||||||
while (docs.length > 0) {
|
while (docs.length > 0) {
|
||||||
const [id, doc] = docs.pop() as [string, Doc];
|
const [id, doc] = docs.pop() as [string, Doc];
|
||||||
await saveDocOperation(id, doc);
|
await saveDocOperation(id, doc);
|
||||||
@@ -361,13 +373,13 @@ export const createIndexedDBProvider = (
|
|||||||
if (early) {
|
if (early) {
|
||||||
reject(new EarlyDisconnectError());
|
reject(new EarlyDisconnectError());
|
||||||
}
|
}
|
||||||
unTrackDoc(id, doc);
|
unTrackDoc(doc.guid, doc);
|
||||||
},
|
},
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
throw new CleanupWhenConnectingError();
|
throw new CleanupWhenConnectingError();
|
||||||
}
|
}
|
||||||
await (await dbPromise).delete('workspace', id);
|
await (await dbPromise).delete('workspace', doc.guid);
|
||||||
},
|
},
|
||||||
whenSynced: Promise.resolve(),
|
whenSynced: Promise.resolve(),
|
||||||
get connected() {
|
get connected() {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export async function tryMigrate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadBinary(
|
export async function downloadBinary(
|
||||||
id: string,
|
guid: string,
|
||||||
dbName = DEFAULT_DB_NAME
|
dbName = DEFAULT_DB_NAME
|
||||||
): Promise<UpdateMessage['update'] | false> {
|
): Promise<UpdateMessage['update'] | false> {
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
||||||
@@ -136,7 +136,7 @@ export async function downloadBinary(
|
|||||||
});
|
});
|
||||||
const db = await dbPromise;
|
const db = await dbPromise;
|
||||||
const t = db.transaction('workspace', 'readonly').objectStore('workspace');
|
const t = db.transaction('workspace', 'readonly').objectStore('workspace');
|
||||||
const doc = await t.get(id);
|
const doc = await t.get(guid);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user