feat(core): new worker workspace engine (#9257)

This commit is contained in:
EYHN
2025-01-17 00:22:18 +08:00
committed by GitHub
parent 7dc470e7ea
commit a2ffdb4047
219 changed files with 4267 additions and 7194 deletions

View File

@@ -1,8 +1,7 @@
import { PropertyName, PropertyRoot, PropertyValue } from '@affine/component';
import { DocsService } from '@affine/core/modules/doc';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { i18nTime, useI18n } from '@affine/i18n';
import { DateTimeIcon, HistoryIcon } from '@blocksuite/icons/rc';
import { DateTimeIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import type { ConfigType } from 'dayjs';
@@ -19,11 +18,7 @@ export const TimeRow = ({
className?: string;
}) => {
const t = useI18n();
const workspaceService = useService(WorkspaceService);
const docsService = useService(DocsService);
const { syncing, retrying, serverClock } = useLiveData(
workspaceService.workspace.engine.doc.docState$(docId)
);
const docRecord = useLiveData(docsService.list.doc$(docId));
const docMeta = useLiveData(docRecord?.meta$);
@@ -43,38 +38,14 @@ export const TimeRow = ({
: null;
return (
<>
<PropertyRoot>
<PropertyName name={t['Created']()} icon={<DateTimeIcon />} />
<PropertyValue>
{docMeta ? formatI18nTime(docMeta.createDate) : localizedCreateTime}
</PropertyValue>
</PropertyRoot>
{serverClock ? (
<PropertyRoot>
<PropertyName
name={t[
!syncing && !retrying ? 'Updated' : 'com.affine.syncing'
]()}
icon={<HistoryIcon />}
/>
<PropertyValue>
{!syncing && !retrying
? formatI18nTime(serverClock)
: docMeta?.updatedDate
? formatI18nTime(docMeta.updatedDate)
: null}
</PropertyValue>
</PropertyRoot>
) : docMeta?.updatedDate ? (
<PropertyRoot>
<PropertyName name={t['Updated']()} icon={<HistoryIcon />} />
<PropertyValue>{formatI18nTime(docMeta.updatedDate)}</PropertyValue>
</PropertyRoot>
) : null}
</>
<PropertyRoot>
<PropertyName name={t['Created']()} icon={<DateTimeIcon />} />
<PropertyValue>
{docMeta ? formatI18nTime(docMeta.createDate) : localizedCreateTime}
</PropertyValue>
</PropertyRoot>
);
}, [docMeta, retrying, serverClock, syncing, t]);
}, [docMeta, t]);
const dTimestampElement = useDebouncedValue(timestampElement, 500);

View File

@@ -63,7 +63,6 @@ const SettingModalInner = ({
const currentServerId = useLiveData(
globalContextService.globalContext.serverId.$
);
console.log(currentServerId);
const serversService = useService(ServersService);
const defaultServerService = useService(DefaultServerService);
const currentServer =

View File

@@ -39,8 +39,8 @@ export const DesktopExportPanel = ({ workspace }: ExportPanelProps) => {
type: 'workspace',
});
if (isOnline) {
await workspace.engine.waitForDocSynced();
await workspace.engine.blob.sync();
await workspace.engine.doc.waitForSynced();
await workspace.engine.blob.fullSync();
}
const result = await desktopApi.handler?.dialog.saveDBFileAs(workspaceId);

View File

@@ -8,9 +8,7 @@ import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import { FrameworkScope, useService } from '@toeverything/infra';
import { useCallback } from 'react';
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { EnableCloudPanel } from './enable-cloud';
@@ -34,17 +32,6 @@ export const WorkspaceSettingDetail = ({
const workspaceInfo = useWorkspaceInfo(workspace);
const handleResetSyncStatus = useCallback(() => {
workspace?.engine.doc
.resetSyncStatus()
.then(() => {
window.location.reload();
})
.catch(err => {
console.error(err);
});
}, [workspace]);
if (!workspace) {
return null;
}
@@ -71,8 +58,10 @@ export const WorkspaceSettingDetail = ({
<TemplateDocSetting />
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
<EnableCloudPanel onCloseSetting={onCloseSetting} />
<WorkspaceQuotaPanel />
<MembersPanel onChangeSettingState={onChangeSettingState} />
{workspace.flavour !== 'local' && <WorkspaceQuotaPanel />}
{workspace.flavour !== 'local' && (
<MembersPanel onChangeSettingState={onChangeSettingState} />
)}
</SettingWrapper>
<SharingPanel />
{BUILD_CONFIG.isElectron && (
@@ -82,19 +71,6 @@ export const WorkspaceSettingDetail = ({
)}
<SettingWrapper>
<DeleteLeaveWorkspace onCloseSetting={onCloseSetting} />
<SettingRow
name={
<span style={{ color: 'var(--affine-text-secondary-color)' }}>
{t['com.affine.resetSyncStatus.button']()}
</span>
}
desc={t['com.affine.resetSyncStatus.description']()}
style={{ cursor: 'pointer' }}
onClick={handleResetSyncStatus}
data-testid="reset-sync-status"
>
<ArrowRightSmallIcon />
</SettingRow>
</SettingWrapper>
</FrameworkScope>
</FrameworkScope>

View File

@@ -9,9 +9,10 @@ import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
import { CameraIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import type { KeyboardEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { map } from 'rxjs';
import * as style from './style.css';
@@ -24,8 +25,18 @@ export const ProfilePanel = () => {
useEffect(() => {
permissionService.permission.revalidate();
}, [permissionService]);
const workspaceIsReady = useLiveData(workspace?.engine.rootDocState$)?.ready;
const workspaceIsReady = useLiveData(
useMemo(() => {
return workspace
? LiveData.from(
workspace.engine.doc
.docState$(workspace.id)
.pipe(map(v => v.ready)),
false
)
: null;
}, [workspace])
);
const [name, setName] = useState('');
useEffect(() => {

View File

@@ -33,7 +33,7 @@ const useLoadAttachment = (pageId: string, attachmentId: string) => {
if (!doc.blockSuiteDoc.ready) {
doc.blockSuiteDoc.load();
}
doc.setPriorityLoad(10);
const dispose = doc.addPriorityLoad(10);
doc
.waitForSyncReady()
@@ -47,6 +47,7 @@ const useLoadAttachment = (pageId: string, attachmentId: string) => {
return () => {
release();
dispose();
};
}, [docRecord, docsService, pageId, attachmentId]);

View File

@@ -41,10 +41,7 @@ const useLoadDoc = (pageId: string) => {
// set sync engine priority target
useEffect(() => {
currentWorkspace.engine.doc.setPriority(pageId, 10);
return () => {
currentWorkspace.engine.doc.setPriority(pageId, 5);
};
return currentWorkspace.engine.doc.addPriority(pageId, 10);
}, [currentWorkspace, pageId]);
const isInTrash = useLiveData(doc?.meta$.map(meta => meta.trash));

View File

@@ -16,6 +16,7 @@ import {
import { ZipTransformer } from '@blocksuite/affine/blocks';
import {
FrameworkScope,
LiveData,
useLiveData,
useService,
useServices,
@@ -28,6 +29,7 @@ import {
useParams,
useSearchParams,
} from 'react-router-dom';
import { map } from 'rxjs';
import * as _Y from 'yjs';
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
@@ -247,7 +249,20 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
}, [meta, workspacesService]);
const isRootDocReady =
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
useLiveData(
useMemo(
() =>
workspace
? LiveData.from(
workspace.engine.doc
.docState$(workspace.id)
.pipe(map(v => v.ready)),
false
)
: null,
[workspace]
)
) ?? false;
useEffect(() => {
if (workspace) {

View File

@@ -25,13 +25,13 @@ export const WorkspaceLayout = function WorkspaceLayout({
<WorkspaceDialogs />
{/* ---- some side-effect components ---- */}
{currentWorkspace?.flavour === 'local' ? (
<LocalQuotaModal />
) : (
{currentWorkspace?.flavour !== 'local' ? (
<>
<CloudQuotaModal />
<QuotaCheck workspaceMeta={currentWorkspace.meta} />
</>
) : (
<LocalQuotaModal />
)}
<AiLoginRequiredModal />
<WorkspaceSideEffects />

View File

@@ -5,12 +5,7 @@ import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-s
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { AppContainer } from '@affine/core/desktop/components/app-container';
import {
AuthService,
FetchService,
GraphQLService,
ServerService,
} from '@affine/core/modules/cloud';
import { AuthService, ServerService } from '@affine/core/modules/cloud';
import { type Doc, DocsService } from '@affine/core/modules/doc';
import {
type Editor,
@@ -19,13 +14,11 @@ import {
EditorsService,
} from '@affine/core/modules/editor';
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { ShareReaderService } from '@affine/core/modules/share-doc';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import {
type Workspace,
WorkspacesService,
} from '@affine/core/modules/workspace';
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
import { useI18n } from '@affine/i18n';
import {
type DocMode,
@@ -35,22 +28,9 @@ import {
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
import { DisposableGroup } from '@blocksuite/global/utils';
import { Logo1Icon } from '@blocksuite/icons/rc';
import {
EmptyBlobStorage,
FrameworkScope,
ReadonlyDocStorage,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { PageNotFound } from '../../404';
@@ -65,15 +45,6 @@ export const SharePage = ({
workspaceId: string;
docId: string;
}) => {
const { shareReaderService, serverService } = useServices({
ShareReaderService,
ServerService,
});
const isLoading = useLiveData(shareReaderService.reader.isLoading$);
const error = useLiveData(shareReaderService.reader.error$);
const data = useLiveData(shareReaderService.reader.data$);
const location = useLocation();
const { mode, selector, isTemplate, templateName, templateSnapshotUrl } =
@@ -105,47 +76,26 @@ export const SharePage = ({
};
}, [location.search]);
useEffect(() => {
shareReaderService.reader.loadShare({
serverId: serverService.server.id,
workspaceId,
docId,
});
}, [shareReaderService, docId, workspaceId, serverService.server.id]);
let element: ReactNode = null;
if (isLoading) {
element = null;
} else if (data) {
element = (
return (
<AppContainer>
<SharePageInner
workspaceId={data.workspaceId}
docId={data.docId}
workspaceBinary={data.workspaceBinary}
docBinary={data.docBinary}
publishMode={mode || data.publishMode}
workspaceId={workspaceId}
docId={docId}
key={workspaceId + ':' + docId}
publishMode={mode ?? undefined}
selector={selector}
isTemplate={isTemplate}
templateName={templateName}
templateSnapshotUrl={templateSnapshotUrl}
/>
);
} else if (error) {
// TODO(@JimmFly): handle error
element = <PageNotFound />;
} else {
element = <PageNotFound noPermission />;
}
return <AppContainer fallback={!element}>{element}</AppContainer>;
</AppContainer>
);
};
const SharePageInner = ({
workspaceId,
docId,
workspaceBinary,
docBinary,
publishMode = 'page' as DocMode,
publishMode = 'page',
selector,
isTemplate,
templateName,
@@ -153,20 +103,18 @@ const SharePageInner = ({
}: {
workspaceId: string;
docId: string;
workspaceBinary: Uint8Array;
docBinary: Uint8Array;
publishMode?: DocMode;
selector?: EditorSelector;
isTemplate?: boolean;
templateName?: string;
templateSnapshotUrl?: string;
}) => {
const serverService = useService(ServerService);
const workspacesService = useService(WorkspacesService);
const fetchService = useService(FetchService);
const graphQLService = useService(GraphQLService);
const [workspace, setWorkspace] = useState<Workspace | null>(null);
const [page, setPage] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
const [noPermission, setNoPermission] = useState(false);
const [editorContainer, setActiveBlocksuiteEditor] =
useActiveBlocksuiteEditor();
@@ -181,38 +129,41 @@ const SharePageInner = ({
isSharedMode: true,
},
{
getDocStorage() {
return new ReadonlyDocStorage({
[workspaceId]: workspaceBinary,
[docId]: docBinary,
});
},
getAwarenessConnections() {
return [];
},
getDocServer() {
return null;
},
getLocalBlobStorage() {
return EmptyBlobStorage;
},
getRemoteBlobStorages() {
return [
new CloudBlobStorage(workspaceId, fetchService, graphQLService),
];
local: {
doc: {
name: 'StaticCloudDocStorage',
opts: {
id: workspaceId,
serverBaseUrl: serverService.server.baseUrl,
},
},
blob: {
name: 'CloudBlobStorage',
opts: {
id: workspaceId,
serverBaseUrl: serverService.server.baseUrl,
},
},
},
remotes: {},
}
);
setWorkspace(workspace);
workspace.engine
.waitForRootDocReady()
.then(() => {
workspace.engine.doc
.waitForDocLoaded(workspace.id)
.then(async () => {
const { doc } = workspace.scope.get(DocsService).open(docId);
doc.blockSuiteDoc.load();
doc.blockSuiteDoc.readonly = true;
await workspace.engine.doc.waitForDocLoaded(docId);
if (!doc.blockSuiteDoc.root) {
throw new Error('Doc is empty');
}
setPage(doc);
const editor = doc.scope.get(EditorsService).createEditor();
@@ -226,6 +177,7 @@ const SharePageInner = ({
})
.catch(err => {
console.error(err);
setNoPermission(true);
});
}, [
docId,
@@ -233,10 +185,7 @@ const SharePageInner = ({
workspacesService,
publishMode,
selector,
workspaceBinary,
docBinary,
fetchService,
graphQLService,
serverService.server.baseUrl,
]);
const t = useI18n();
@@ -281,6 +230,10 @@ const SharePageInner = ({
[editor, setActiveBlocksuiteEditor, jumpToPageBlock, openPage, workspaceId]
);
if (noPermission) {
return <PageNotFound noPermission />;
}
if (!workspace || !page || !editor) {
return;
}