mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
refactor: workspace manager (#5060)
This commit is contained in:
Vendored
+10
@@ -1,5 +1,6 @@
|
||||
/// <reference types="@blocksuite/global" />
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isDesktop, isServer } from './constant.js';
|
||||
@@ -149,3 +150,12 @@ export function setupGlobal() {
|
||||
|
||||
globalThis.$AFFINE_SETUP = true;
|
||||
}
|
||||
|
||||
export function setupEditorFlags(workspace: Workspace) {
|
||||
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
||||
workspace.awarenessStore.setFlag(
|
||||
key as keyof BlockSuiteFeatureFlags,
|
||||
value
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+11
-123
@@ -1,10 +1,4 @@
|
||||
import type {
|
||||
ActiveDocProvider,
|
||||
PassiveDocProvider,
|
||||
Workspace as BlockSuiteWorkspace,
|
||||
} from '@blocksuite/store';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import type { DataSourceAdapter } from 'y-provider';
|
||||
|
||||
export enum WorkspaceSubPath {
|
||||
ALL = 'all',
|
||||
@@ -14,73 +8,6 @@ export enum WorkspaceSubPath {
|
||||
SHARED = 'shared',
|
||||
}
|
||||
|
||||
export interface AffineDownloadProvider extends PassiveDocProvider {
|
||||
flavour: 'affine-download';
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the first binary from local IndexedDB
|
||||
*/
|
||||
export interface BroadCastChannelProvider extends PassiveDocProvider {
|
||||
flavour: 'broadcast-channel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Long polling provider with local IndexedDB
|
||||
*/
|
||||
export interface LocalIndexedDBBackgroundProvider
|
||||
extends DataSourceAdapter,
|
||||
PassiveDocProvider {
|
||||
flavour: 'local-indexeddb-background';
|
||||
}
|
||||
|
||||
export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider {
|
||||
flavour: 'local-indexeddb';
|
||||
}
|
||||
|
||||
export interface SQLiteProvider extends PassiveDocProvider, DataSourceAdapter {
|
||||
flavour: 'sqlite';
|
||||
}
|
||||
|
||||
export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
|
||||
flavour: 'sqlite-download';
|
||||
}
|
||||
|
||||
export interface AffineSocketIOProvider
|
||||
extends PassiveDocProvider,
|
||||
DataSourceAdapter {
|
||||
flavour: 'affine-socket-io';
|
||||
}
|
||||
|
||||
type BaseWorkspace = {
|
||||
flavour: string;
|
||||
id: string;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
};
|
||||
|
||||
export interface AffineCloudWorkspace extends BaseWorkspace {
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD;
|
||||
id: string;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
}
|
||||
|
||||
export interface LocalWorkspace extends BaseWorkspace {
|
||||
flavour: WorkspaceFlavour.LOCAL;
|
||||
id: string;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
}
|
||||
|
||||
export interface AffinePublicWorkspace extends BaseWorkspace {
|
||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC;
|
||||
id: string;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
}
|
||||
|
||||
export type AffineOfficialWorkspace =
|
||||
| AffineCloudWorkspace
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace;
|
||||
|
||||
export enum ReleaseType {
|
||||
// if workspace is not released yet, we will not show it in the workspace list
|
||||
UNRELEASED = 'unreleased',
|
||||
@@ -99,7 +26,6 @@ export enum WorkspaceFlavour {
|
||||
*/
|
||||
AFFINE_CLOUD = 'affine-cloud',
|
||||
LOCAL = 'local',
|
||||
AFFINE_PUBLIC = 'affine-public',
|
||||
}
|
||||
|
||||
export const settingPanel = {
|
||||
@@ -112,68 +38,30 @@ export const settingPanel = {
|
||||
export const settingPanelValues = Object.values(settingPanel);
|
||||
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
||||
|
||||
// built-in workspaces
|
||||
export interface WorkspaceRegistry {
|
||||
[WorkspaceFlavour.LOCAL]: LocalWorkspace;
|
||||
[WorkspaceFlavour.AFFINE_PUBLIC]: AffinePublicWorkspace;
|
||||
[WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace;
|
||||
}
|
||||
|
||||
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
|
||||
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
||||
delete: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<void>;
|
||||
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
|
||||
// not supported yet
|
||||
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
||||
list: () => Promise<WorkspaceRegistry[Flavour][]>;
|
||||
}
|
||||
|
||||
type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
|
||||
currentWorkspaceId: string;
|
||||
export type WorkspaceHeaderProps = {
|
||||
rightSlot?: ReactNode;
|
||||
currentEntry:
|
||||
| {
|
||||
subPath: WorkspaceSubPath;
|
||||
}
|
||||
| {
|
||||
pageId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
onDeleteLocalWorkspace: () => void;
|
||||
onDeleteCloudWorkspace: () => void;
|
||||
onLeaveWorkspace: () => void;
|
||||
onTransformWorkspace: <
|
||||
From extends keyof WorkspaceRegistry,
|
||||
To extends keyof WorkspaceRegistry,
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
|
||||
interface FC<P> {
|
||||
(props: P): ReactNode;
|
||||
}
|
||||
|
||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
|
||||
export interface WorkspaceUISchema {
|
||||
Provider: FC<PropsWithChildren>;
|
||||
LoginCard?: FC<object>;
|
||||
}
|
||||
|
||||
export interface AppEvents {
|
||||
// event there is no workspace
|
||||
// usually used to initialize workspace adapter
|
||||
'app:init': () => string[];
|
||||
// event if you have access to workspace adapter
|
||||
'app:access': () => Promise<boolean>;
|
||||
'service:start': () => void;
|
||||
'service:stop': () => void;
|
||||
}
|
||||
|
||||
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
|
||||
releaseType: ReleaseType;
|
||||
flavour: Flavour;
|
||||
// The Adapter will be loaded according to the priority
|
||||
loadPriority: LoadPriority;
|
||||
Events: Partial<AppEvents>;
|
||||
// Fetch necessary data for the first render
|
||||
CRUD: WorkspaceCRUD<Flavour>;
|
||||
UI: WorkspaceUISchema<Flavour>;
|
||||
UI: WorkspaceUISchema;
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { ActiveDocProvider, Workspace } from '@blocksuite/store';
|
||||
import type { PassiveDocProvider } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai/vanilla';
|
||||
import { atom } from 'jotai/vanilla';
|
||||
import { atomEffect } from 'jotai-effect';
|
||||
|
||||
/**
|
||||
* Map: guid -> Workspace
|
||||
*/
|
||||
export const INTERNAL_BLOCKSUITE_HASH_MAP = new Map<string, Workspace>([]);
|
||||
|
||||
const workspaceActiveAtomWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<Workspace>>
|
||||
>();
|
||||
|
||||
const workspaceActiveWeakMap = new WeakMap<Workspace, boolean>();
|
||||
const workspaceEffectAtomWeakMap = new WeakMap<Workspace, Atom<void>>();
|
||||
|
||||
export async function waitForWorkspace(workspace: Workspace) {
|
||||
if (workspaceActiveWeakMap.get(workspace) !== true) {
|
||||
const providers = workspace.providers.filter(
|
||||
(provider): provider is ActiveDocProvider =>
|
||||
'active' in provider && provider.active === true
|
||||
);
|
||||
for (const provider of providers) {
|
||||
provider.sync();
|
||||
// we will wait for the necessary providers to be ready
|
||||
await provider.whenReady;
|
||||
}
|
||||
// timeout is INFINITE
|
||||
workspaceActiveWeakMap.set(workspace, true);
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspace(id: string) {
|
||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
return INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
||||
}
|
||||
|
||||
export function getBlockSuiteWorkspaceAtom(
|
||||
id: string
|
||||
): [workspaceAtom: Atom<Promise<Workspace>>, workspaceEffectAtom: Atom<void>] {
|
||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
const workspace = INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
||||
if (!workspaceActiveAtomWeakMap.has(workspace)) {
|
||||
const baseAtom = atom(async () => {
|
||||
await waitForWorkspace(workspace);
|
||||
return workspace;
|
||||
});
|
||||
workspaceActiveAtomWeakMap.set(workspace, baseAtom);
|
||||
}
|
||||
if (!workspaceEffectAtomWeakMap.has(workspace)) {
|
||||
const effectAtom = atomEffect(() => {
|
||||
const providers = workspace.providers.filter(
|
||||
(provider): provider is PassiveDocProvider =>
|
||||
'passive' in provider && provider.passive === true
|
||||
);
|
||||
providers.forEach(provider => {
|
||||
provider.connect();
|
||||
});
|
||||
return () => {
|
||||
providers.forEach(provider => {
|
||||
provider.disconnect();
|
||||
});
|
||||
};
|
||||
});
|
||||
workspaceEffectAtomWeakMap.set(workspace, effectAtom);
|
||||
}
|
||||
|
||||
return [
|
||||
workspaceActiveAtomWeakMap.get(workspace) as Atom<Promise<Workspace>>,
|
||||
workspaceEffectAtomWeakMap.get(workspace) as Atom<void>,
|
||||
];
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { Schema, Workspace } from '@blocksuite/store';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { getDefaultStore } from 'jotai/vanilla';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getBlockSuiteWorkspaceAtom,
|
||||
INTERNAL_BLOCKSUITE_HASH_MAP,
|
||||
} from '../__internal__/workspace.js';
|
||||
|
||||
test('blocksuite atom', async () => {
|
||||
const sync = vi.fn();
|
||||
let connected = false;
|
||||
const connect = vi.fn(() => (connected = true));
|
||||
const workspace = new Workspace({
|
||||
schema: new Schema(),
|
||||
id: '1',
|
||||
providerCreators: [
|
||||
() => ({
|
||||
flavour: 'fake',
|
||||
active: true,
|
||||
sync,
|
||||
get whenReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
() => ({
|
||||
flavour: 'fake-2',
|
||||
passive: true,
|
||||
get connected() {
|
||||
return connected;
|
||||
},
|
||||
connect,
|
||||
disconnect: vi.fn(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
INTERNAL_BLOCKSUITE_HASH_MAP.set('1', workspace);
|
||||
|
||||
{
|
||||
const [atom, effectAtom] = getBlockSuiteWorkspaceAtom('1');
|
||||
const store = getDefaultStore();
|
||||
const result = await store.get(atom);
|
||||
expect(result).toBe(workspace);
|
||||
expect(sync).toBeCalledTimes(1);
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
|
||||
store.sub(effectAtom, vi.fn());
|
||||
await waitFor(() => expect(connect).toBeCalledTimes(1));
|
||||
expect(connected).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -1,14 +1,3 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { getBlockSuiteWorkspaceAtom } from '../__internal__/workspace';
|
||||
|
||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const currentPageIdAtom = atom<string | null>(null);
|
||||
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
|
||||
const workspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(workspaceId);
|
||||
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
||||
return get(currentWorkspaceAtom);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export * from './initialization';
|
||||
export * from './migration/blob';
|
||||
export { migratePages as forceUpgradePages } from './migration/blocksuite'; // campatible with electron
|
||||
export {
|
||||
migratePages as forceUpgradePages,
|
||||
migrateGuidCompatibility,
|
||||
} from './migration/blocksuite'; // campatible with electron
|
||||
export * from './migration/fixing';
|
||||
export { migrateToSubdoc } from './migration/subdoc';
|
||||
export { migrateToSubdoc, upgradeV1ToV2 } from './migration/subdoc';
|
||||
export * from './migration/workspace';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,12 +2,9 @@ import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page, PageMeta, Workspace } from '@blocksuite/store';
|
||||
import type { createStore, WritableAtom } from 'jotai/vanilla';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
import { migratePages } from '../migration/blocksuite';
|
||||
import {
|
||||
checkWorkspaceCompatibility,
|
||||
MigrationPoint,
|
||||
} from '../migration/workspace';
|
||||
import { getLatestVersions } from '../migration/blocksuite';
|
||||
|
||||
export async function initEmptyPage(page: Page, title?: string) {
|
||||
await page.load(() => {
|
||||
@@ -261,10 +258,11 @@ export async function buildShowcaseWorkspace(
|
||||
|
||||
// The showcase building will create multiple pages once, and may skip the version writing.
|
||||
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
||||
const compatibilityResult = checkWorkspaceCompatibility(workspace);
|
||||
if (compatibilityResult === MigrationPoint.BlockVersion) {
|
||||
await migratePages(workspace.doc, workspace.schema);
|
||||
}
|
||||
workspace.doc.getMap('meta').set('pageVersion', 2);
|
||||
const newVersions = getLatestVersions(workspace.schema);
|
||||
workspace.doc
|
||||
.getMap('meta')
|
||||
.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||
|
||||
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
||||
const newId = idMap[oldId];
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createIndexeddbStorage } from '@blocksuite/store';
|
||||
|
||||
export async function migrateLocalBlobStorage(from: string, to: string) {
|
||||
const fromStorage = createIndexeddbStorage(from);
|
||||
const toStorage = createIndexeddbStorage(to);
|
||||
const keys = await fromStorage.crud.list();
|
||||
for (const key of keys) {
|
||||
const value = await fromStorage.crud.get(key);
|
||||
if (!value) {
|
||||
console.warn('cannot find blob:', key);
|
||||
continue;
|
||||
}
|
||||
await toStorage.crud.set(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { Schema } from '@blocksuite/store';
|
||||
import type { Doc as YDoc } from 'yjs';
|
||||
import { Map as YMap } from 'yjs';
|
||||
import type { Array as YArray } from 'yjs';
|
||||
import {
|
||||
applyUpdate,
|
||||
Doc as YDoc,
|
||||
encodeStateAsUpdate,
|
||||
Map as YMap,
|
||||
transact,
|
||||
} from 'yjs';
|
||||
|
||||
const getLatestVersions = (schema: Schema): Record<string, number> => {
|
||||
export const getLatestVersions = (schema: Schema): Record<string, number> => {
|
||||
return [...schema.flavourSchemaMap.entries()].reduce(
|
||||
(record, [flavour, schema]) => {
|
||||
record[flavour] = schema.version;
|
||||
@@ -28,14 +34,62 @@ export async function migratePages(
|
||||
|
||||
// Hard code to upgrade page version to 2.
|
||||
// Let e2e to ensure the data version is correct.
|
||||
const pageVersion = meta.get('pageVersion');
|
||||
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||
meta.set('pageVersion', 2);
|
||||
}
|
||||
return transact(
|
||||
rootDoc,
|
||||
() => {
|
||||
const pageVersion = meta.get('pageVersion');
|
||||
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||
meta.set('pageVersion', 2);
|
||||
}
|
||||
|
||||
const newVersions = getLatestVersions(schema);
|
||||
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||
return Object.entries(oldVersions).some(
|
||||
([flavour, version]) => newVersions[flavour] !== version
|
||||
const newVersions = getLatestVersions(schema);
|
||||
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||
return Object.entries(oldVersions).some(
|
||||
([flavour, version]) => newVersions[flavour] !== version
|
||||
);
|
||||
},
|
||||
'migratePages',
|
||||
/**
|
||||
* transact as remote update, because blocksuite will skip local changes.
|
||||
* https://github.com/toeverything/blocksuite/blob/9c2df3f7aa5617c050e0dccdd73e99bb67e0c0f7/packages/store/src/reactive/utils.ts#L143
|
||||
*/
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// patch root doc's space guid compatibility issue
|
||||
//
|
||||
// in version 0.10, page id in spaces no longer has prefix "space:"
|
||||
// The data flow for fetching a doc's updates is:
|
||||
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
||||
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
||||
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
||||
// - when fetching the rows of this doc using the doc id === page id,
|
||||
// it will return empty since there is no updates associated with the page id
|
||||
export function migrateGuidCompatibility(rootDoc: YDoc) {
|
||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||
pages?.forEach(page => {
|
||||
const pageId = page.get('id') as string | undefined;
|
||||
if (pageId?.includes(':')) {
|
||||
// remove the prefix "space:" from page id
|
||||
page.set('id', pageId.split(':').at(-1));
|
||||
}
|
||||
});
|
||||
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
||||
spaces?.forEach((doc: YDoc, pageId: string) => {
|
||||
if (pageId.includes(':')) {
|
||||
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
||||
const newDoc = new YDoc();
|
||||
// clone the original doc. yjs is not happy to use the same doc instance
|
||||
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
||||
newDoc.guid = doc.guid;
|
||||
spaces.set(newPageId, newDoc);
|
||||
// should remove the old doc, otherwise we will do it again in the next run
|
||||
spaces.delete(pageId);
|
||||
console.debug(
|
||||
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,48 +1,5 @@
|
||||
import type { Array as YArray, Map as YMap } from 'yjs';
|
||||
import { Doc as YDoc, transact } from 'yjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
// patch root doc's space guid compatibility issue
|
||||
//
|
||||
// in version 0.10, page id in spaces no longer has prefix "space:"
|
||||
// The data flow for fetching a doc's updates is:
|
||||
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
||||
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
||||
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
||||
// - when fetching the rows of this doc using the doc id === page id,
|
||||
// it will return empty since there is no updates associated with the page id
|
||||
export function guidCompatibilityFix(rootDoc: YDoc) {
|
||||
let changed = false;
|
||||
transact(rootDoc, () => {
|
||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||
pages?.forEach(page => {
|
||||
const pageId = page.get('id') as string | undefined;
|
||||
if (pageId?.includes(':')) {
|
||||
// remove the prefix "space:" from page id
|
||||
page.set('id', pageId.split(':').at(-1));
|
||||
}
|
||||
});
|
||||
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
||||
spaces?.forEach((doc: YDoc, pageId: string) => {
|
||||
if (pageId.includes(':')) {
|
||||
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
||||
const newDoc = new YDoc();
|
||||
// clone the original doc. yjs is not happy to use the same doc instance
|
||||
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
||||
newDoc.guid = doc.guid;
|
||||
spaces.set(newPageId, newDoc);
|
||||
// should remove the old doc, otherwise we will do it again in the next run
|
||||
spaces.delete(pageId);
|
||||
changed = true;
|
||||
console.debug(
|
||||
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
import type { Doc as YDoc, Map as YMap } from 'yjs';
|
||||
import { transact } from 'yjs';
|
||||
|
||||
/**
|
||||
* Hard code to fix workspace version to be compatible with legacy data.
|
||||
@@ -55,13 +12,35 @@ export function fixWorkspaceVersion(rootDoc: YDoc) {
|
||||
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
|
||||
* Blocksuite just set the value, do nothing else.
|
||||
*/
|
||||
const workspaceVersion = meta.get('workspaceVersion');
|
||||
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
|
||||
meta.set('workspaceVersion', 2);
|
||||
|
||||
function doFix() {
|
||||
const workspaceVersion = meta.get('workspaceVersion');
|
||||
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
|
||||
transact(
|
||||
rootDoc,
|
||||
() => {
|
||||
meta.set('workspaceVersion', 2);
|
||||
},
|
||||
'fixWorkspaceVersion',
|
||||
// transact as remote update, because blocksuite will skip local changes.
|
||||
false
|
||||
);
|
||||
}
|
||||
const pageVersion = meta.get('pageVersion');
|
||||
if (typeof pageVersion !== 'number') {
|
||||
meta.set('pageVersion', 1);
|
||||
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||
transact(
|
||||
rootDoc,
|
||||
() => {
|
||||
meta.set('pageVersion', 2);
|
||||
},
|
||||
'fixPageVersion',
|
||||
// transact as remote update, because blocksuite will skip local changes.
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
doFix();
|
||||
|
||||
// do fix every time when meta changed
|
||||
meta.observe(() => doFix());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
@@ -264,19 +263,17 @@ export function migrateToSubdoc(oldDoc: YDoc): YDoc {
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
export const upgradeV1ToV2 = async (
|
||||
oldDoc: YDoc,
|
||||
createWorkspace: () => Promise<Workspace>
|
||||
) => {
|
||||
/**
|
||||
* upgrade oldDoc to v2, write to targetDoc
|
||||
*/
|
||||
export const upgradeV1ToV2 = async (oldDoc: YDoc, targetDoc: YDoc) => {
|
||||
const newDoc = migrateToSubdoc(oldDoc);
|
||||
const newWorkspace = await createWorkspace();
|
||||
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
||||
applyUpdate(targetDoc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
||||
newDoc.getSubdocs().forEach(subdoc => {
|
||||
newWorkspace.doc.getSubdocs().forEach(newDoc => {
|
||||
targetDoc.getSubdocs().forEach(newDoc => {
|
||||
if (subdoc.guid === newDoc.guid) {
|
||||
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
|
||||
}
|
||||
});
|
||||
});
|
||||
return newWorkspace;
|
||||
};
|
||||
|
||||
@@ -1,63 +1,50 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { Schema } from '@blocksuite/store';
|
||||
import type { Doc as YDoc } from 'yjs';
|
||||
|
||||
import { migratePages } from './blocksuite';
|
||||
import { upgradeV1ToV2 } from './subdoc';
|
||||
|
||||
interface MigrationOptions {
|
||||
doc: YDoc;
|
||||
schema: Schema;
|
||||
createWorkspace: () => Promise<Workspace>;
|
||||
}
|
||||
|
||||
function createMigrationQueue(options: MigrationOptions) {
|
||||
return [
|
||||
async (doc: YDoc) => {
|
||||
const newWorkspace = await upgradeV1ToV2(doc, options.createWorkspace);
|
||||
return newWorkspace.doc;
|
||||
},
|
||||
async (doc: YDoc) => {
|
||||
await migratePages(doc, options.schema);
|
||||
return doc;
|
||||
},
|
||||
];
|
||||
}
|
||||
import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||
|
||||
/**
|
||||
* For split migrate function from MigrationQueue.
|
||||
*/
|
||||
export enum MigrationPoint {
|
||||
SubDoc = 1,
|
||||
BlockVersion = 2,
|
||||
}
|
||||
|
||||
export async function migrateWorkspace(
|
||||
point: MigrationPoint,
|
||||
options: MigrationOptions
|
||||
) {
|
||||
const migrationQueue = createMigrationQueue(options);
|
||||
const migrationFns = migrationQueue.slice(point - 1);
|
||||
|
||||
let doc = options.doc;
|
||||
for (const migrate of migrationFns) {
|
||||
doc = await migrate(doc);
|
||||
}
|
||||
return doc;
|
||||
GuidFix = 2,
|
||||
BlockVersion = 3,
|
||||
}
|
||||
|
||||
export function checkWorkspaceCompatibility(
|
||||
workspace: Workspace
|
||||
): MigrationPoint | null {
|
||||
const workspaceDocJSON = workspace.doc.toJSON();
|
||||
const spaceMetaObj = workspaceDocJSON['space:meta'];
|
||||
const docKeys = Object.keys(workspaceDocJSON);
|
||||
const haveSpaceMeta = !!spaceMetaObj && Object.keys(spaceMetaObj).length > 0;
|
||||
// check if there is any key starts with 'space:' on root doc
|
||||
const spaceMetaObj = workspace.doc.share.get('space:meta') as
|
||||
| YMap<any>
|
||||
| undefined;
|
||||
const docKeys = Array.from(workspace.doc.share.keys());
|
||||
const haveSpaceMeta = !!spaceMetaObj && spaceMetaObj.size > 0;
|
||||
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
|
||||
if (haveSpaceMeta || haveLegacySpace) {
|
||||
return MigrationPoint.SubDoc;
|
||||
}
|
||||
|
||||
// exit if no pages
|
||||
if (!workspace.meta.pages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check guid compatibility
|
||||
const meta = workspace.doc.getMap('meta') as YMap<unknown>;
|
||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||
for (const page of pages) {
|
||||
const pageId = page.get('id') as string | undefined;
|
||||
if (pageId?.includes(':')) {
|
||||
return MigrationPoint.GuidFix;
|
||||
}
|
||||
}
|
||||
const spaces = workspace.doc.getMap('spaces') as YMap<YDoc>;
|
||||
for (const [pageId, _] of spaces) {
|
||||
if (pageId.includes(':')) {
|
||||
return MigrationPoint.GuidFix;
|
||||
}
|
||||
}
|
||||
|
||||
const hasVersion = workspace.meta.hasVersion;
|
||||
if (!hasVersion) {
|
||||
return MigrationPoint.BlockVersion;
|
||||
|
||||
@@ -18,10 +18,6 @@ export default defineConfig({
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||
'__internal__/workspace': resolve(
|
||||
root,
|
||||
'src/__internal__/workspace.ts'
|
||||
),
|
||||
'__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'),
|
||||
},
|
||||
formats: ['es', 'cjs'],
|
||||
|
||||
Reference in New Issue
Block a user