mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 18:11:32 +03:00
chore: prohibit using mergeUpdates
(#3701)
This commit is contained in:
parent
e9f4912665
commit
9639143df4
10
.eslintrc.js
10
.eslintrc.js
@ -31,6 +31,11 @@ const createPattern = packageName => [
|
|||||||
message: 'Use `useNavigateHelper` instead',
|
message: 'Use `useNavigateHelper` instead',
|
||||||
importNames: ['useNavigate'],
|
importNames: ['useNavigate'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: ['yjs'],
|
||||||
|
message: 'Do not use this API because it has a bug',
|
||||||
|
importNames: ['mergeUpdates'],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const allPackages = [
|
const allPackages = [
|
||||||
@ -155,6 +160,11 @@ const config = {
|
|||||||
message: 'Use `useNavigateHelper` instead',
|
message: 'Use `useNavigateHelper` instead',
|
||||||
importNames: ['useNavigate'],
|
importNames: ['useNavigate'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: ['yjs'],
|
||||||
|
message: 'Do not use this API because it has a bug',
|
||||||
|
importNames: ['mergeUpdates'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -24,7 +24,7 @@ import { currentPageIdAtom } from '@toeverything/infra/atom';
|
|||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { pageSettingFamily, setPageModeAtom } from '../../../../atoms';
|
import { pageSettingFamily, setPageModeAtom } from '../../../../atoms';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
@ -35,6 +35,7 @@ import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
|||||||
import { usePageHelper } from '../../block-suite-page-list/utils';
|
import { usePageHelper } from '../../block-suite-page-list/utils';
|
||||||
import { LanguageMenu } from './language-menu';
|
import { LanguageMenu } from './language-menu';
|
||||||
import { MenuThemeModeSwitch } from './theme-mode-switch';
|
import { MenuThemeModeSwitch } from './theme-mode-switch';
|
||||||
|
|
||||||
const CommonMenu = () => {
|
const CommonMenu = () => {
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
@ -117,8 +118,8 @@ export const PageMenu = ({ rename }: PageMenuProps) => {
|
|||||||
const currentPageMeta = currentPage.meta;
|
const currentPageMeta = currentPage.meta;
|
||||||
const newPage = createPage();
|
const newPage = createPage();
|
||||||
await newPage.waitForLoaded();
|
await newPage.waitForLoaded();
|
||||||
const update = Y.encodeStateAsUpdate(currentPage.spaceDoc);
|
const update = encodeStateAsUpdate(currentPage.spaceDoc);
|
||||||
Y.applyUpdate(newPage.spaceDoc, update);
|
applyUpdate(newPage.spaceDoc, update);
|
||||||
setPageMeta(newPage.id, {
|
setPageMeta(newPage.id, {
|
||||||
tags: currentPageMeta.tags,
|
tags: currentPageMeta.tags,
|
||||||
favorite: currentPageMeta.favorite,
|
favorite: currentPageMeta.favorite,
|
||||||
|
@ -2,7 +2,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { SqliteConnection } from '@affine/native';
|
import { SqliteConnection } from '@affine/native';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
import { removeWithRetry } from '../../../../tests/utils';
|
import { removeWithRetry } from '../../../../tests/utils';
|
||||||
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
|
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
|
||||||
@ -41,14 +41,14 @@ describe('migrateToSubdocAndReplaceDatabase', () => {
|
|||||||
expect(subdocUpdate).toBeDefined();
|
expect(subdocUpdate).toBeDefined();
|
||||||
|
|
||||||
// apply updates
|
// apply updates
|
||||||
const rootDoc = new Y.Doc();
|
const rootDoc = new YDoc();
|
||||||
Y.applyUpdate(rootDoc, rootUpdate);
|
applyUpdate(rootDoc, rootUpdate);
|
||||||
|
|
||||||
// check if root doc has one subdoc
|
// check if root doc has one subdoc
|
||||||
expect(rootDoc.subdocs.size).toBe(1);
|
expect(rootDoc.subdocs.size).toBe(1);
|
||||||
|
|
||||||
// populates subdoc
|
// populates subdoc
|
||||||
Y.applyUpdate(rootDoc.subdocs.values().next().value, subdocUpdate);
|
applyUpdate(rootDoc.subdocs.values().next().value, subdocUpdate);
|
||||||
|
|
||||||
// check if root doc's meta is correct
|
// check if root doc's meta is correct
|
||||||
const meta = rootDoc.getMap('meta').toJSON();
|
const meta = rootDoc.getMap('meta').toJSON();
|
||||||
@ -59,9 +59,7 @@ describe('migrateToSubdocAndReplaceDatabase', () => {
|
|||||||
expect(pageMeta.title).toBe('Welcome to AFFiNEd');
|
expect(pageMeta.title).toBe('Welcome to AFFiNEd');
|
||||||
|
|
||||||
// get the subdoc through id
|
// get the subdoc through id
|
||||||
const subDoc = rootDoc
|
const subDoc = rootDoc.getMap('spaces').get(`space:${pageMeta.id}`) as YDoc;
|
||||||
.getMap('spaces')
|
|
||||||
.get(`space:${pageMeta.id}`) as Y.Doc;
|
|
||||||
expect(subDoc).toEqual(rootDoc.subdocs.values().next().value);
|
expect(subDoc).toEqual(rootDoc.subdocs.values().next().value);
|
||||||
|
|
||||||
await db.close();
|
await db.close();
|
||||||
|
@ -3,7 +3,7 @@ import path from 'node:path';
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { afterEach, expect, test, vi } from 'vitest';
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { removeWithRetry } from '../../../../tests/utils';
|
import { removeWithRetry } from '../../../../tests/utils';
|
||||||
import { dbSubjects } from '../subjects';
|
import { dbSubjects } from '../subjects';
|
||||||
@ -21,18 +21,18 @@ afterEach(async () => {
|
|||||||
await removeWithRetry(tmpDir);
|
await removeWithRetry(tmpDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
let testYDoc: Y.Doc;
|
let testYDoc: YDoc;
|
||||||
let testYSubDoc: Y.Doc;
|
let testYSubDoc: YDoc;
|
||||||
|
|
||||||
function getTestUpdates() {
|
function getTestUpdates() {
|
||||||
testYDoc = new Y.Doc();
|
testYDoc = new YDoc();
|
||||||
const yText = testYDoc.getText('test');
|
const yText = testYDoc.getText('test');
|
||||||
yText.insert(0, 'hello');
|
yText.insert(0, 'hello');
|
||||||
|
|
||||||
testYSubDoc = new Y.Doc();
|
testYSubDoc = new YDoc();
|
||||||
testYDoc.getMap('subdocs').set('test-subdoc', testYSubDoc);
|
testYDoc.getMap('subdocs').set('test-subdoc', testYSubDoc);
|
||||||
|
|
||||||
const updates = Y.encodeStateAsUpdate(testYDoc);
|
const updates = encodeStateAsUpdate(testYDoc);
|
||||||
|
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ function getTestSubDocUpdates() {
|
|||||||
const yText = testYSubDoc.getText('test');
|
const yText = testYSubDoc.getText('test');
|
||||||
yText.insert(0, 'hello');
|
yText.insert(0, 'hello');
|
||||||
|
|
||||||
const updates = Y.encodeStateAsUpdate(testYSubDoc);
|
const updates = encodeStateAsUpdate(testYSubDoc);
|
||||||
|
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate, transact } from 'yjs';
|
||||||
|
|
||||||
export function mergeUpdate(updates: Uint8Array[]) {
|
export function mergeUpdate(updates: Uint8Array[]) {
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new YDoc();
|
||||||
Y.transact(yDoc, () => {
|
transact(yDoc, () => {
|
||||||
for (const update of updates) {
|
for (const update of updates) {
|
||||||
Y.applyUpdate(yDoc, update);
|
applyUpdate(yDoc, update);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Y.encodeStateAsUpdate(yDoc);
|
return encodeStateAsUpdate(yDoc);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { migrateToSubdoc } from '@affine/env/blocksuite';
|
|||||||
import { SqliteConnection } from '@affine/native';
|
import { SqliteConnection } from '@affine/native';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { mainRPC } from '../main-rpc';
|
import { mainRPC } from '../main-rpc';
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ export const migrateToSubdocAndReplaceDatabase = async (path: string) => {
|
|||||||
await db.connect();
|
await db.connect();
|
||||||
|
|
||||||
const rows = await db.getAllUpdates();
|
const rows = await db.getAllUpdates();
|
||||||
const originalDoc = new Y.Doc();
|
const originalDoc = new YDoc();
|
||||||
|
|
||||||
// 1. apply all updates to the root doc
|
// 1. apply all updates to the root doc
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
Y.applyUpdate(originalDoc, row.data);
|
applyUpdate(originalDoc, row.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. migrate using migrateToSubdoc
|
// 2. migrate using migrateToSubdoc
|
||||||
@ -40,10 +40,10 @@ export const copyToTemp = async (path: string) => {
|
|||||||
|
|
||||||
async function replaceRows(
|
async function replaceRows(
|
||||||
db: SqliteConnection,
|
db: SqliteConnection,
|
||||||
doc: Y.Doc,
|
doc: YDoc,
|
||||||
isRoot: boolean
|
isRoot: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const migratedUpdates = Y.encodeStateAsUpdate(doc);
|
const migratedUpdates = encodeStateAsUpdate(doc);
|
||||||
const docId = isRoot ? undefined : doc.guid;
|
const docId = isRoot ? undefined : doc.guid;
|
||||||
const rows = [{ data: migratedUpdates, docId: docId }];
|
const rows = [{ data: migratedUpdates, docId: docId }];
|
||||||
await db.replaceUpdates(docId, rows);
|
await db.replaceUpdates(docId, rows);
|
||||||
|
@ -2,7 +2,7 @@ import assert from 'node:assert';
|
|||||||
|
|
||||||
import type { InsertRow } from '@affine/native';
|
import type { InsertRow } from '@affine/native';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import type { YOrigin } from '../type';
|
import type { YOrigin } from '../type';
|
||||||
@ -16,7 +16,7 @@ const FLUSH_MAX_WAIT_TIME = 10000;
|
|||||||
// todo: trim db when it is too big
|
// todo: trim db when it is too big
|
||||||
export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||||
role = 'secondary';
|
role = 'secondary';
|
||||||
yDoc = new Y.Doc();
|
yDoc = new YDoc();
|
||||||
firstConnected = false;
|
firstConnected = false;
|
||||||
destroyed = false;
|
destroyed = false;
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
|
const onSubdocs = ({ added }: { added: Set<YDoc> }) => {
|
||||||
added.forEach(subdoc => {
|
added.forEach(subdoc => {
|
||||||
this.setupListener(subdoc.guid);
|
this.setupListener(subdoc.guid);
|
||||||
});
|
});
|
||||||
@ -214,7 +214,7 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
) => {
|
) => {
|
||||||
const doc = this.getDoc(docId);
|
const doc = this.getDoc(docId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
Y.applyUpdate(this.yDoc, data, origin);
|
applyUpdate(this.yDoc, data, origin);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'[SecondaryWorkspaceSQLiteDB] applyUpdate: doc not found',
|
'[SecondaryWorkspaceSQLiteDB] applyUpdate: doc not found',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { InsertRow } from '@affine/native';
|
import type { InsertRow } from '@affine/native';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import type { YOrigin } from '../type';
|
import type { YOrigin } from '../type';
|
||||||
@ -13,7 +13,7 @@ const TRIM_SIZE = 500;
|
|||||||
|
|
||||||
export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||||
role = 'primary';
|
role = 'primary';
|
||||||
yDoc = new Y.Doc();
|
yDoc = new YDoc();
|
||||||
firstConnected = false;
|
firstConnected = false;
|
||||||
|
|
||||||
update$ = new Subject<void>();
|
update$ = new Subject<void>();
|
||||||
@ -78,7 +78,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
doc.subdocs.forEach(subdoc => {
|
doc.subdocs.forEach(subdoc => {
|
||||||
this.setupListener(subdoc.guid);
|
this.setupListener(subdoc.guid);
|
||||||
});
|
});
|
||||||
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
|
const onSubdocs = ({ added }: { added: Set<YDoc> }) => {
|
||||||
logger.info('onSubdocs', this.workspaceId, docId, added);
|
logger.info('onSubdocs', this.workspaceId, docId, added);
|
||||||
added.forEach(subdoc => {
|
added.forEach(subdoc => {
|
||||||
this.setupListener(subdoc.guid);
|
this.setupListener(subdoc.guid);
|
||||||
@ -126,7 +126,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
getDocAsUpdates = (docId?: string) => {
|
getDocAsUpdates = (docId?: string) => {
|
||||||
const doc = docId ? this.getDoc(docId) : this.yDoc;
|
const doc = docId ? this.getDoc(docId) : this.yDoc;
|
||||||
if (doc) {
|
if (doc) {
|
||||||
return Y.encodeStateAsUpdate(doc);
|
return encodeStateAsUpdate(doc);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@ -144,7 +144,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
// yjs-idb will always trim the db for the first time after DB is loaded
|
// yjs-idb will always trim the db for the first time after DB is loaded
|
||||||
const doc = this.getDoc(docId);
|
const doc = this.getDoc(docId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
Y.applyUpdate(doc, data, origin);
|
applyUpdate(doc, data, origin);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('[WorkspaceSQLiteDB] applyUpdate: doc not found', docId);
|
logger.warn('[WorkspaceSQLiteDB] applyUpdate: doc not found', docId);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ import { readFileSync } from 'fs';
|
|||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import type { Array as YArray, Map as YMap } from 'yjs';
|
||||||
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
import { migrateToSubdoc } from '../blocksuite/index.js';
|
import { migrateToSubdoc } from '../blocksuite/index.js';
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ const fixturePath = resolve(
|
|||||||
'workspace.ydoc'
|
'workspace.ydoc'
|
||||||
);
|
);
|
||||||
const yDocBuffer = readFileSync(fixturePath);
|
const yDocBuffer = readFileSync(fixturePath);
|
||||||
const doc = new Y.Doc();
|
const doc = new Doc();
|
||||||
Y.applyUpdate(doc, new Uint8Array(yDocBuffer));
|
applyUpdate(doc, new Uint8Array(yDocBuffer));
|
||||||
const migratedDoc = migrateToSubdoc(doc);
|
const migratedDoc = migrateToSubdoc(doc);
|
||||||
|
|
||||||
describe('subdoc', () => {
|
describe('subdoc', () => {
|
||||||
@ -23,8 +24,8 @@ describe('subdoc', () => {
|
|||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
binary[i] = (json as any)[i];
|
binary[i] = (json as any)[i];
|
||||||
}
|
}
|
||||||
const doc = new Y.Doc();
|
const doc = new Doc();
|
||||||
Y.applyUpdate(doc, binary);
|
applyUpdate(doc, binary);
|
||||||
{
|
{
|
||||||
// invoke data
|
// invoke data
|
||||||
doc.getMap('space:hello-world');
|
doc.getMap('space:hello-world');
|
||||||
@ -32,7 +33,7 @@ describe('subdoc', () => {
|
|||||||
}
|
}
|
||||||
const blocks = doc.getMap('space:hello-world').toJSON();
|
const blocks = doc.getMap('space:hello-world').toJSON();
|
||||||
const newDoc = migrateToSubdoc(doc);
|
const newDoc = migrateToSubdoc(doc);
|
||||||
const subDoc = newDoc.getMap('spaces').get('space:hello-world') as Y.Doc;
|
const subDoc = newDoc.getMap('spaces').get('space:hello-world') as Doc;
|
||||||
const data = (subDoc.toJSON() as any).blocks;
|
const data = (subDoc.toJSON() as any).blocks;
|
||||||
Object.keys(data).forEach(id => {
|
Object.keys(data).forEach(id => {
|
||||||
if (id === 'xyWNqindHH') {
|
if (id === 'xyWNqindHH') {
|
||||||
@ -50,23 +51,23 @@ describe('subdoc', () => {
|
|||||||
|
|
||||||
test('Test fixture should be set correctly', () => {
|
test('Test fixture should be set correctly', () => {
|
||||||
const meta = doc.getMap('space:meta');
|
const meta = doc.getMap('space:meta');
|
||||||
const versions = meta.get('versions') as Y.Map<unknown>;
|
const versions = meta.get('versions') as YMap<unknown>;
|
||||||
expect(versions.get('affine:code')).toBeTypeOf('number');
|
expect(versions.get('affine:code')).toBeTypeOf('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Meta data should be migrated correctly', () => {
|
test('Meta data should be migrated correctly', () => {
|
||||||
const originalMeta = doc.getMap('space:meta');
|
const originalMeta = doc.getMap('space:meta');
|
||||||
const originalVersions = originalMeta.get('versions') as Y.Map<unknown>;
|
const originalVersions = originalMeta.get('versions') as YMap<unknown>;
|
||||||
|
|
||||||
const meta = migratedDoc.getMap('meta');
|
const meta = migratedDoc.getMap('meta');
|
||||||
const blockVersions = meta.get('blockVersions') as Y.Map<unknown>;
|
const blockVersions = meta.get('blockVersions') as YMap<unknown>;
|
||||||
|
|
||||||
expect(meta.get('workspaceVersion')).toBe(1);
|
expect(meta.get('workspaceVersion')).toBe(1);
|
||||||
expect(blockVersions.get('affine:code')).toBe(
|
expect(blockVersions.get('affine:code')).toBe(
|
||||||
originalVersions.get('affine:code')
|
originalVersions.get('affine:code')
|
||||||
);
|
);
|
||||||
expect((meta.get('pages') as Y.Array<unknown>).length).toBe(
|
expect((meta.get('pages') as YArray<unknown>).length).toBe(
|
||||||
(originalMeta.get('pages') as Y.Array<unknown>).length
|
(originalMeta.get('pages') as YArray<unknown>).length
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(blockVersions.get('affine:embed')).toBeUndefined();
|
expect(blockVersions.get('affine:embed')).toBeUndefined();
|
||||||
|
56
packages/env/src/blocksuite/subdoc-migration.ts
vendored
56
packages/env/src/blocksuite/subdoc-migration.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import type { Schema } from '@blocksuite/store';
|
import type { Schema } from '@blocksuite/store';
|
||||||
import * as Y from 'yjs';
|
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
|
|
||||||
type XYWH = [number, number, number, number];
|
type XYWH = [number, number, number, number];
|
||||||
|
|
||||||
@ -7,10 +7,10 @@ function deserializeXYWH(xywh: string): XYWH {
|
|||||||
return JSON.parse(xywh) as XYWH;
|
return JSON.parse(xywh) as XYWH;
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateDatabase(data: Y.Map<unknown>) {
|
function migrateDatabase(data: YMap<unknown>) {
|
||||||
data.delete('prop:mode');
|
data.delete('prop:mode');
|
||||||
data.set('prop:views', new Y.Array());
|
data.set('prop:views', new YArray());
|
||||||
const columns = (data.get('prop:columns') as Y.Array<unknown>).toJSON() as {
|
const columns = (data.get('prop:columns') as YArray<unknown>).toJSON() as {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
hide: boolean;
|
hide: boolean;
|
||||||
@ -31,7 +31,7 @@ function migrateDatabase(data: Y.Map<unknown>) {
|
|||||||
mode: 'table',
|
mode: 'table',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const cells = (data.get('prop:cells') as Y.Map<unknown>).toJSON() as Record<
|
const cells = (data.get('prop:cells') as YMap<unknown>).toJSON() as Record<
|
||||||
string,
|
string,
|
||||||
Record<
|
Record<
|
||||||
string,
|
string,
|
||||||
@ -90,7 +90,7 @@ function migrateDatabase(data: Y.Map<unknown>) {
|
|||||||
|
|
||||||
function runBlockMigration(
|
function runBlockMigration(
|
||||||
flavour: string,
|
flavour: string,
|
||||||
data: Y.Map<unknown>,
|
data: YMap<unknown>,
|
||||||
version: number
|
version: number
|
||||||
) {
|
) {
|
||||||
if (flavour === 'affine:frame') {
|
if (flavour === 'affine:frame') {
|
||||||
@ -99,12 +99,12 @@ function runBlockMigration(
|
|||||||
}
|
}
|
||||||
if (flavour === 'affine:surface' && version <= 3) {
|
if (flavour === 'affine:surface' && version <= 3) {
|
||||||
if (data.has('elements')) {
|
if (data.has('elements')) {
|
||||||
const elements = data.get('elements') as Y.Map<unknown>;
|
const elements = data.get('elements') as YMap<unknown>;
|
||||||
migrateSurface(elements);
|
migrateSurface(elements);
|
||||||
data.set('prop:elements', elements.clone());
|
data.set('prop:elements', elements.clone());
|
||||||
data.delete('elements');
|
data.delete('elements');
|
||||||
} else {
|
} else {
|
||||||
data.set('prop:elements', new Y.Map());
|
data.set('prop:elements', new YMap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (flavour === 'affine:embed') {
|
if (flavour === 'affine:embed') {
|
||||||
@ -116,8 +116,8 @@ function runBlockMigration(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateSurface(data: Y.Map<unknown>) {
|
function migrateSurface(data: YMap<unknown>) {
|
||||||
for (const [, value] of <IterableIterator<[string, Y.Map<unknown>]>>(
|
for (const [, value] of <IterableIterator<[string, YMap<unknown>]>>(
|
||||||
data.entries()
|
data.entries()
|
||||||
)) {
|
)) {
|
||||||
if (value.get('type') === 'connector') {
|
if (value.get('type') === 'connector') {
|
||||||
@ -126,7 +126,7 @@ function migrateSurface(data: Y.Map<unknown>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateSurfaceConnector(data: Y.Map<any>) {
|
function migrateSurfaceConnector(data: YMap<any>) {
|
||||||
let id = data.get('startElement')?.id;
|
let id = data.get('startElement')?.id;
|
||||||
const controllers = data.get('controllers');
|
const controllers = data.get('controllers');
|
||||||
const length = controllers.length;
|
const length = controllers.length;
|
||||||
@ -164,7 +164,7 @@ function migrateSurfaceConnector(data: Y.Map<any>) {
|
|||||||
data.delete('xywh');
|
data.delete('xywh');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBlockVersions(versions: Y.Map<number>) {
|
function updateBlockVersions(versions: YMap<number>) {
|
||||||
const frameVersion = versions.get('affine:frame');
|
const frameVersion = versions.get('affine:frame');
|
||||||
if (frameVersion !== undefined) {
|
if (frameVersion !== undefined) {
|
||||||
versions.set('affine:note', frameVersion);
|
versions.set('affine:note', frameVersion);
|
||||||
@ -181,12 +181,12 @@ function updateBlockVersions(versions: Y.Map<number>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateMeta(oldDoc: Y.Doc, newDoc: Y.Doc) {
|
function migrateMeta(oldDoc: YDoc, newDoc: YDoc) {
|
||||||
const originalMeta = oldDoc.getMap('space:meta');
|
const originalMeta = oldDoc.getMap('space:meta');
|
||||||
const originalVersions = originalMeta.get('versions') as Y.Map<number>;
|
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
||||||
const originalPages = originalMeta.get('pages') as Y.Array<Y.Map<unknown>>;
|
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
||||||
const meta = newDoc.getMap('meta');
|
const meta = newDoc.getMap('meta');
|
||||||
const pages = new Y.Array();
|
const pages = new YArray();
|
||||||
const blockVersions = originalVersions.clone();
|
const blockVersions = originalVersions.clone();
|
||||||
|
|
||||||
meta.set('workspaceVersion', 1);
|
meta.set('workspaceVersion', 1);
|
||||||
@ -196,7 +196,7 @@ function migrateMeta(oldDoc: Y.Doc, newDoc: Y.Doc) {
|
|||||||
|
|
||||||
updateBlockVersions(blockVersions);
|
updateBlockVersions(blockVersions);
|
||||||
const mapList = originalPages.map(page => {
|
const mapList = originalPages.map(page => {
|
||||||
const map = new Y.Map();
|
const map = new YMap();
|
||||||
Array.from(page.entries())
|
Array.from(page.entries())
|
||||||
.filter(([key]) => key !== 'subpageIds')
|
.filter(([key]) => key !== 'subpageIds')
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
@ -207,16 +207,16 @@ function migrateMeta(oldDoc: Y.Doc, newDoc: Y.Doc) {
|
|||||||
pages.push(mapList);
|
pages.push(mapList);
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateBlocks(oldDoc: Y.Doc, newDoc: Y.Doc) {
|
function migrateBlocks(oldDoc: YDoc, newDoc: YDoc) {
|
||||||
const spaces = newDoc.getMap('spaces');
|
const spaces = newDoc.getMap('spaces');
|
||||||
const originalMeta = oldDoc.getMap('space:meta');
|
const originalMeta = oldDoc.getMap('space:meta');
|
||||||
const originalVersions = originalMeta.get('versions') as Y.Map<number>;
|
const originalVersions = originalMeta.get('versions') as YMap<number>;
|
||||||
const originalPages = originalMeta.get('pages') as Y.Array<Y.Map<unknown>>;
|
const originalPages = originalMeta.get('pages') as YArray<YMap<unknown>>;
|
||||||
originalPages.forEach(page => {
|
originalPages.forEach(page => {
|
||||||
const id = page.get('id') as string;
|
const id = page.get('id') as string;
|
||||||
const spaceId = id.startsWith('space:') ? id : `space:${id}`;
|
const spaceId = id.startsWith('space:') ? id : `space:${id}`;
|
||||||
const originalBlocks = oldDoc.getMap(spaceId) as Y.Map<unknown>;
|
const originalBlocks = oldDoc.getMap(spaceId) as YMap<unknown>;
|
||||||
const subdoc = new Y.Doc();
|
const subdoc = new YDoc();
|
||||||
spaces.set(spaceId, subdoc);
|
spaces.set(spaceId, subdoc);
|
||||||
const blocks = subdoc.getMap('blocks');
|
const blocks = subdoc.getMap('blocks');
|
||||||
Array.from(originalBlocks.entries()).forEach(([key, value]) => {
|
Array.from(originalBlocks.entries()).forEach(([key, value]) => {
|
||||||
@ -231,19 +231,19 @@ function migrateBlocks(oldDoc: Y.Doc, newDoc: Y.Doc) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migrateToSubdoc(doc: Y.Doc): Y.Doc {
|
export function migrateToSubdoc(doc: YDoc): YDoc {
|
||||||
const needMigration = Array.from(doc.getMap('space:meta').keys()).length > 0;
|
const needMigration = Array.from(doc.getMap('space:meta').keys()).length > 0;
|
||||||
if (!needMigration) {
|
if (!needMigration) {
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
const output = new Y.Doc();
|
const output = new YDoc();
|
||||||
migrateMeta(doc, output);
|
migrateMeta(doc, output);
|
||||||
migrateBlocks(doc, output);
|
migrateBlocks(doc, output);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function migrateDatabaseBlockTo3(rootDoc: Y.Doc, schema: Schema) {
|
export async function migrateDatabaseBlockTo3(rootDoc: YDoc, schema: Schema) {
|
||||||
const spaces = rootDoc.getMap('spaces') as Y.Map<any>;
|
const spaces = rootDoc.getMap('spaces') as YMap<any>;
|
||||||
spaces.forEach(space => {
|
spaces.forEach(space => {
|
||||||
schema.upgradePage(
|
schema.upgradePage(
|
||||||
{
|
{
|
||||||
@ -261,7 +261,7 @@ export async function migrateDatabaseBlockTo3(rootDoc: Y.Doc, schema: Schema) {
|
|||||||
space
|
space
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const meta = rootDoc.getMap('meta') as Y.Map<unknown>;
|
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||||
const versions = meta.get('blockVersions') as Y.Map<number>;
|
const versions = meta.get('blockVersions') as YMap<number>;
|
||||||
versions.set('affine:database', 3);
|
versions.set('affine:database', 3);
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,44 @@ import assert from 'node:assert';
|
|||||||
import { beforeEach, describe, test } from 'node:test';
|
import { beforeEach, describe, test } from 'node:test';
|
||||||
|
|
||||||
import { encoding } from 'lib0';
|
import { encoding } from 'lib0';
|
||||||
import * as Y from 'yjs';
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
import { Storage } from '../index.js';
|
import { Storage } from '../index.js';
|
||||||
|
|
||||||
// update binary by y.doc.text('content').insert('hello world')
|
// update binary by y.doc.text('content').insert('hello world')
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
let init = Buffer.from([1,1,160,238,169,240,10,0,4,1,7,99,111,110,116,101,110,116,11,104,101,108,108,111,32,119,111,114,108,100,0])
|
let init = Buffer.from([
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
160,
|
||||||
|
238,
|
||||||
|
169,
|
||||||
|
240,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
99,
|
||||||
|
111,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
11,
|
||||||
|
104,
|
||||||
|
101,
|
||||||
|
108,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
32,
|
||||||
|
119,
|
||||||
|
111,
|
||||||
|
114,
|
||||||
|
108,
|
||||||
|
100,
|
||||||
|
0])
|
||||||
describe('Test jwst storage binding', () => {
|
describe('Test jwst storage binding', () => {
|
||||||
/** @type { Storage } */
|
/** @type { Storage } */
|
||||||
let storage;
|
let storage;
|
||||||
@ -48,8 +79,8 @@ describe('Test jwst storage binding', () => {
|
|||||||
const update = await storage.load(workspace.doc.guid);
|
const update = await storage.load(workspace.doc.guid);
|
||||||
assert(update !== null);
|
assert(update !== null);
|
||||||
|
|
||||||
const doc = new Y.Doc();
|
const doc = new Doc();
|
||||||
Y.applyUpdate(doc, update);
|
applyUpdate(doc, update);
|
||||||
|
|
||||||
let text = doc.getText('content');
|
let text = doc.getText('content');
|
||||||
assert.equal(text.toJSON(), 'hello world');
|
assert.equal(text.toJSON(), 'hello world');
|
||||||
@ -67,8 +98,8 @@ describe('Test jwst storage binding', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const update2 = await storage.load(workspace.doc.guid);
|
const update2 = await storage.load(workspace.doc.guid);
|
||||||
const doc2 = new Y.Doc();
|
const doc2 = new Doc();
|
||||||
Y.applyUpdate(doc2, update2);
|
applyUpdate(doc2, update2);
|
||||||
|
|
||||||
text = doc2.getText('content');
|
text = doc2.getText('content');
|
||||||
assert.equal(text.toJSON(), 'hello my world!');
|
assert.equal(text.toJSON(), 'hello my world!');
|
||||||
@ -80,8 +111,8 @@ describe('Test jwst storage binding', () => {
|
|||||||
const update = await storage.load(workspace.doc.guid);
|
const update = await storage.load(workspace.doc.guid);
|
||||||
assert(update !== null);
|
assert(update !== null);
|
||||||
|
|
||||||
const doc = new Y.Doc();
|
const doc = new Doc();
|
||||||
Y.applyUpdate(doc, update);
|
applyUpdate(doc, update);
|
||||||
|
|
||||||
let text = doc.getText('content');
|
let text = doc.getText('content');
|
||||||
assert.equal(text.toJSON(), 'hello world');
|
assert.equal(text.toJSON(), 'hello world');
|
||||||
@ -103,8 +134,8 @@ describe('Test jwst storage binding', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const update2 = await storage.load(workspace.doc.guid);
|
const update2 = await storage.load(workspace.doc.guid);
|
||||||
const doc2 = new Y.Doc();
|
const doc2 = new Doc();
|
||||||
Y.applyUpdate(doc2, update2);
|
applyUpdate(doc2, update2);
|
||||||
|
|
||||||
text = doc2.getText('content');
|
text = doc2.getText('content');
|
||||||
assert.equal(text.toJSON(), 'hello my world!');
|
assert.equal(text.toJSON(), 'hello my world!');
|
||||||
|
@ -435,15 +435,13 @@ describe('utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('overwrite binary', async () => {
|
test('overwrite binary', async () => {
|
||||||
await overwriteBinary('test', new Uint8Array([1, 2, 3]));
|
const doc = new Doc();
|
||||||
|
const map = doc.getMap();
|
||||||
|
map.set('1', 1);
|
||||||
|
await overwriteBinary('test', new Uint8Array(encodeStateAsUpdate(doc)));
|
||||||
{
|
{
|
||||||
const binary = await downloadBinary('test');
|
const binary = await downloadBinary('test');
|
||||||
expect(binary).toEqual(new Uint8Array([1, 2, 3]));
|
expect(binary).toEqual(new Uint8Array(encodeStateAsUpdate(doc)));
|
||||||
}
|
|
||||||
await overwriteBinary('test', new Uint8Array([0, 0]));
|
|
||||||
{
|
|
||||||
const binary = await downloadBinary('test');
|
|
||||||
expect(binary).toEqual(new Uint8Array([0, 0]));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { openDB } from 'idb';
|
import { openDB } from 'idb';
|
||||||
import type { Doc } from 'yjs';
|
import type { Doc } from 'yjs';
|
||||||
import { diffUpdate, mergeUpdates } from 'yjs';
|
import { diffUpdate } from 'yjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type BlockSuiteBinaryDB,
|
type BlockSuiteBinaryDB,
|
||||||
@ -16,6 +16,7 @@ import {
|
|||||||
type UpdateMessage,
|
type UpdateMessage,
|
||||||
upgradeDB,
|
upgradeDB,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
|
import { mergeUpdates } from './utils';
|
||||||
|
|
||||||
let mergeCount = 500;
|
let mergeCount = 500;
|
||||||
|
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import type { IDBPDatabase } from 'idb';
|
import type { IDBPDatabase } from 'idb';
|
||||||
import { openDB } from 'idb';
|
import { openDB } from 'idb';
|
||||||
import { mergeUpdates } from 'yjs';
|
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import type { BlockSuiteBinaryDB, OldYjsDB, UpdateMessage } from './shared';
|
import type { BlockSuiteBinaryDB, OldYjsDB, UpdateMessage } from './shared';
|
||||||
import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared';
|
import { dbVersion, DEFAULT_DB_NAME, upgradeDB } from './shared';
|
||||||
|
|
||||||
let allDb: IDBDatabaseInfo[];
|
let allDb: IDBDatabaseInfo[];
|
||||||
|
|
||||||
|
export function mergeUpdates(updates: Uint8Array[]) {
|
||||||
|
const doc = new Doc();
|
||||||
|
updates.forEach(update => {
|
||||||
|
applyUpdate(doc, update);
|
||||||
|
});
|
||||||
|
return encodeStateAsUpdate(doc);
|
||||||
|
}
|
||||||
|
|
||||||
async function databaseExists(name: string): Promise<boolean> {
|
async function databaseExists(name: string): Promise<boolean> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const req = indexedDB.open(name);
|
const req = indexedDB.open(name);
|
||||||
|
Loading…
Reference in New Issue
Block a user