feat: support force sync by click (#4089)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
@@ -1,4 +1,20 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { createVar, keyframes, style } from '@vanilla-extract/css';
|
||||||
export const workspaceAvatarStyle = style({
|
export const workspaceAvatarStyle = style({
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const speedVar = createVar('speedVar');
|
||||||
|
|
||||||
|
const rotate = keyframes({
|
||||||
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
'50%': { transform: 'rotate(180deg)' },
|
||||||
|
'100%': { transform: 'rotate(360deg)' },
|
||||||
|
});
|
||||||
|
export const loading = style({
|
||||||
|
vars: {
|
||||||
|
[speedVar]: '1.5s',
|
||||||
|
},
|
||||||
|
textRendering: 'optimizeLegibility',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
animation: `${rotate} ${speedVar} infinite linear`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
|
||||||
|
import { loading, speedVar } from './index.css';
|
||||||
|
|
||||||
|
export type LoadingProps = {
|
||||||
|
size?: number;
|
||||||
|
speed?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loading = ({ size, speed = 1.2 }: LoadingProps) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={loading}
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
focusable="false"
|
||||||
|
data-icon="loading"
|
||||||
|
width={size ? `${size}px` : '.8em'}
|
||||||
|
height={size ? `${size}px` : '.8em'}
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
...assignInlineVars({
|
||||||
|
[speedVar]: `${speed}s`,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
LocalWorkspaceIcon,
|
LocalWorkspaceIcon,
|
||||||
NoNetworkIcon,
|
NoNetworkIcon,
|
||||||
|
UnsyncIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { Tooltip } from '@toeverything/components/tooltip';
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||||
import type React from 'react';
|
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
type KeyboardEvent,
|
||||||
|
type MouseEvent,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
import { useDatasourceSync } from '../../../../hooks/use-datasource-sync';
|
||||||
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
||||||
import type { AllWorkspace } from '../../../../shared';
|
import type { AllWorkspace } from '../../../../shared';
|
||||||
import { workspaceAvatarStyle } from './index.css';
|
import { workspaceAvatarStyle } from './index.css';
|
||||||
|
import { Loading } from './loading-icon';
|
||||||
import {
|
import {
|
||||||
StyledSelectorContainer,
|
StyledSelectorContainer,
|
||||||
StyledSelectorWrapper,
|
StyledSelectorWrapper,
|
||||||
@@ -25,6 +34,125 @@ export interface WorkspaceSelectorProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hoverAtom = atom(false);
|
||||||
|
|
||||||
|
const CloudWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CloudWorkspaceIcon />
|
||||||
|
AFFiNE Cloud
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const SyncingWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Loading />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const UnSyncWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UnsyncIcon />
|
||||||
|
Wait for upload
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocalWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LocalWorkspaceIcon />
|
||||||
|
Local
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OfflineStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NoNetworkIcon />
|
||||||
|
Offline
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkspaceStatus = ({
|
||||||
|
currentWorkspace,
|
||||||
|
}: {
|
||||||
|
currentWorkspace: AllWorkspace;
|
||||||
|
}) => {
|
||||||
|
const isOnline = useSystemOnline();
|
||||||
|
// todo: finish display sync status
|
||||||
|
const [forceSyncStatus, startForceSync] = useDatasourceSync(
|
||||||
|
currentWorkspace.blockSuiteWorkspace
|
||||||
|
);
|
||||||
|
const setIsHovered = useSetAtom(hoverAtom);
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
return 'Saved locally';
|
||||||
|
}
|
||||||
|
if (!isOnline) {
|
||||||
|
return 'Disconnected, please check your network connection';
|
||||||
|
}
|
||||||
|
switch (forceSyncStatus.type) {
|
||||||
|
case 'syncing':
|
||||||
|
return 'Syncing with AFFiNE Cloud';
|
||||||
|
case 'error':
|
||||||
|
return 'Sync failed due to server issues, please try again later.';
|
||||||
|
default:
|
||||||
|
return 'Sync with AFFiNE Cloud';
|
||||||
|
}
|
||||||
|
}, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]);
|
||||||
|
const CloudWorkspaceSyncStatus = useCallback(() => {
|
||||||
|
if (forceSyncStatus.type === 'syncing') {
|
||||||
|
return SyncingWorkspaceStatus();
|
||||||
|
} else if (forceSyncStatus.type === 'error') {
|
||||||
|
return UnSyncWorkspaceStatus();
|
||||||
|
} else {
|
||||||
|
return CloudWorkspaceStatus();
|
||||||
|
}
|
||||||
|
}, [forceSyncStatus.type]);
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<Tooltip
|
||||||
|
content={content}
|
||||||
|
portalOptions={{
|
||||||
|
container,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledWorkspaceStatus
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setIsHovered(true);
|
||||||
|
}}
|
||||||
|
ref={setContainer}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
onClick={useCallback(
|
||||||
|
(e: MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startForceSync();
|
||||||
|
},
|
||||||
|
[startForceSync]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||||
|
!isOnline ? (
|
||||||
|
<OfflineStatus />
|
||||||
|
) : (
|
||||||
|
<CloudWorkspaceSyncStatus />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<LocalWorkspaceStatus />
|
||||||
|
)}
|
||||||
|
</StyledWorkspaceStatus>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo-Doma Co-locate WorkspaceListModal with {@link WorkspaceSelector},
|
* @todo-Doma Co-locate WorkspaceListModal with {@link WorkspaceSelector},
|
||||||
* because it's never used elsewhere.
|
* because it's never used elsewhere.
|
||||||
@@ -36,13 +164,11 @@ export const WorkspaceSelector = ({
|
|||||||
const [name] = useBlockSuiteWorkspaceName(
|
const [name] = useBlockSuiteWorkspaceName(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
||||||
// Open dialog when `Enter` or `Space` pressed
|
// Open dialog when `Enter` or `Space` pressed
|
||||||
// TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default
|
// TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default
|
||||||
// TODO: Delete this?
|
// TODO: Delete this?
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// TODO-Doma Rename this callback to `onOpenDialog` or something to reduce ambiguity.
|
// TODO-Doma Rename this callback to `onOpenDialog` or something to reduce ambiguity.
|
||||||
@@ -51,41 +177,8 @@ export const WorkspaceSelector = ({
|
|||||||
},
|
},
|
||||||
[onClick]
|
[onClick]
|
||||||
);
|
);
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const isHovered = useAtomValue(hoverAtom);
|
||||||
const isOnline = useSystemOnline();
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!isOnline) {
|
|
||||||
return 'Disconnected, please check your network connection';
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
loginStatus === 'authenticated' &&
|
|
||||||
currentWorkspace.flavour !== 'local'
|
|
||||||
) {
|
|
||||||
return 'Sync with AFFiNE Cloud';
|
|
||||||
}
|
|
||||||
return 'Saved locally';
|
|
||||||
}, [currentWorkspace.flavour, isOnline, loginStatus]);
|
|
||||||
|
|
||||||
const WorkspaceStatus = () => {
|
|
||||||
if (!isOnline) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NoNetworkIcon />
|
|
||||||
Offline
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{currentWorkspace.flavour === 'local' ? (
|
|
||||||
<LocalWorkspaceIcon />
|
|
||||||
) : (
|
|
||||||
<CloudWorkspaceIcon />
|
|
||||||
)}
|
|
||||||
{currentWorkspace.flavour === 'local' ? 'Local' : 'AFFiNE Cloud'}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectorContainer
|
<StyledSelectorContainer
|
||||||
role="button"
|
role="button"
|
||||||
@@ -106,25 +199,7 @@ export const WorkspaceSelector = ({
|
|||||||
<StyledWorkspaceName data-testid="workspace-name">
|
<StyledWorkspaceName data-testid="workspace-name">
|
||||||
{name}
|
{name}
|
||||||
</StyledWorkspaceName>
|
</StyledWorkspaceName>
|
||||||
<div style={{ display: 'flex' }}>
|
<WorkspaceStatus currentWorkspace={currentWorkspace} />
|
||||||
<Tooltip
|
|
||||||
content={content}
|
|
||||||
portalOptions={{
|
|
||||||
container,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledWorkspaceStatus
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setIsHovered(true);
|
|
||||||
}}
|
|
||||||
ref={setContainer}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<WorkspaceStatus />
|
|
||||||
</StyledWorkspaceStatus>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</StyledSelectorWrapper>
|
</StyledSelectorWrapper>
|
||||||
</StyledSelectorContainer>
|
</StyledSelectorContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
88
apps/core/src/hooks/use-datasource-sync.ts
Normal file
88
apps/core/src/hooks/use-datasource-sync.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
|
import type {
|
||||||
|
AffineSocketIOProvider,
|
||||||
|
LocalIndexedDBBackgroundProvider,
|
||||||
|
SQLiteProvider,
|
||||||
|
} from '@affine/env/workspace';
|
||||||
|
import { type Status, syncDataSource } from '@affine/y-provider';
|
||||||
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { startTransition, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
export function useDatasourceSync(workspace: Workspace) {
|
||||||
|
const [status, setStatus] = useState<Status>({
|
||||||
|
type: 'idle',
|
||||||
|
});
|
||||||
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
const providers = workspace.providers;
|
||||||
|
const remoteProvider: AffineSocketIOProvider | undefined = useMemo(() => {
|
||||||
|
return providers.find(
|
||||||
|
(provider): provider is AffineSocketIOProvider =>
|
||||||
|
provider.flavour === 'affine-socket-io'
|
||||||
|
);
|
||||||
|
}, [providers]);
|
||||||
|
const localProvider = useMemo(() => {
|
||||||
|
const sqliteProvider = providers.find(
|
||||||
|
(provider): provider is SQLiteProvider => provider.flavour === 'sqlite'
|
||||||
|
);
|
||||||
|
const indexedDbProvider = providers.find(
|
||||||
|
(provider): provider is LocalIndexedDBBackgroundProvider =>
|
||||||
|
provider.flavour === 'local-indexeddb-background'
|
||||||
|
);
|
||||||
|
const provider = sqliteProvider || indexedDbProvider;
|
||||||
|
assertExists(provider, 'no local provider');
|
||||||
|
return provider;
|
||||||
|
}, [providers]);
|
||||||
|
return [
|
||||||
|
status,
|
||||||
|
useCallback(() => {
|
||||||
|
if (!remoteProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startTransition(() => {
|
||||||
|
setStatus({
|
||||||
|
type: 'syncing',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
syncDataSource(
|
||||||
|
() => [
|
||||||
|
workspace.doc.guid,
|
||||||
|
...[...workspace.doc.subdocs].map(doc => doc.guid),
|
||||||
|
],
|
||||||
|
remoteProvider.datasource,
|
||||||
|
localProvider.datasource
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setStatus({
|
||||||
|
type: 'synced',
|
||||||
|
});
|
||||||
|
pushNotification({
|
||||||
|
title: 'Synced successfully',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
startTransition(() => {
|
||||||
|
setStatus({
|
||||||
|
type: 'error',
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
pushNotification({
|
||||||
|
title: 'Unable to Sync',
|
||||||
|
message: 'Server error, please try again later.',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
remoteProvider,
|
||||||
|
localProvider.datasource,
|
||||||
|
workspace.doc.guid,
|
||||||
|
workspace.doc.subdocs,
|
||||||
|
pushNotification,
|
||||||
|
]),
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
10
packages/env/src/workspace.ts
vendored
10
packages/env/src/workspace.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import type { StatusAdapter } from '@affine/y-provider';
|
import type { DataSourceAdapter } from '@affine/y-provider';
|
||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import type {
|
import type {
|
||||||
@@ -32,7 +32,7 @@ export interface BroadCastChannelProvider extends PassiveDocProvider {
|
|||||||
* Long polling provider with local IndexedDB
|
* Long polling provider with local IndexedDB
|
||||||
*/
|
*/
|
||||||
export interface LocalIndexedDBBackgroundProvider
|
export interface LocalIndexedDBBackgroundProvider
|
||||||
extends StatusAdapter,
|
extends DataSourceAdapter,
|
||||||
PassiveDocProvider {
|
PassiveDocProvider {
|
||||||
flavour: 'local-indexeddb-background';
|
flavour: 'local-indexeddb-background';
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider {
|
|||||||
flavour: 'local-indexeddb';
|
flavour: 'local-indexeddb';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SQLiteProvider extends PassiveDocProvider, StatusAdapter {
|
export interface SQLiteProvider extends PassiveDocProvider, DataSourceAdapter {
|
||||||
flavour: 'sqlite';
|
flavour: 'sqlite';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,9 @@ export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
|
|||||||
flavour: 'sqlite-download';
|
flavour: 'sqlite-download';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffineSocketIOProvider extends PassiveDocProvider {
|
export interface AffineSocketIOProvider
|
||||||
|
extends PassiveDocProvider,
|
||||||
|
DataSourceAdapter {
|
||||||
flavour: 'affine-socket-io';
|
flavour: 'affine-socket-io';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Status, StatusAdapter } from '@affine/y-provider';
|
import type { DataSourceAdapter, Status } from '@affine/y-provider';
|
||||||
import { useCallback, useSyncExternalStore } from 'react';
|
import { useCallback, useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
type UIStatus =
|
type UIStatus =
|
||||||
@@ -7,9 +7,9 @@ type UIStatus =
|
|||||||
type: 'unknown';
|
type: 'unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useDataSourceStatus(datasource: StatusAdapter): UIStatus {
|
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
datasource.subscribeStatusChange,
|
provider.subscribeStatusChange,
|
||||||
useCallback(() => datasource.status, [datasource])
|
useCallback(() => provider.status, [provider])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,15 @@ const createAffineSocketIOProvider: DocProviderCreator = (
|
|||||||
{ awareness }
|
{ awareness }
|
||||||
): AffineSocketIOProvider => {
|
): AffineSocketIOProvider => {
|
||||||
const dataSource = createAffineDataSource(id, doc, awareness);
|
const dataSource = createAffineDataSource(id, doc, awareness);
|
||||||
|
const lazyProvider = createLazyProvider(doc, dataSource, {
|
||||||
|
origin: 'affine-socket-io',
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
flavour: 'affine-socket-io',
|
flavour: 'affine-socket-io',
|
||||||
...createLazyProvider(doc, dataSource),
|
...lazyProvider,
|
||||||
|
get status() {
|
||||||
|
return lazyProvider.status;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +56,7 @@ const createIndexedDBBackgroundProvider: DocProviderCreator = (
|
|||||||
let connected = false;
|
let connected = false;
|
||||||
return {
|
return {
|
||||||
flavour: 'local-indexeddb-background',
|
flavour: 'local-indexeddb-background',
|
||||||
|
datasource: indexeddbProvider.datasource,
|
||||||
passive: true,
|
passive: true,
|
||||||
get status() {
|
get status() {
|
||||||
return indexeddbProvider.status;
|
return indexeddbProvider.status;
|
||||||
|
|||||||
@@ -54,11 +54,12 @@ export const createSQLiteProvider: DocProviderCreator = (
|
|||||||
id,
|
id,
|
||||||
rootDoc
|
rootDoc
|
||||||
): SQLiteProvider => {
|
): SQLiteProvider => {
|
||||||
let datasource: ReturnType<typeof createDatasource> | null = null;
|
const datasource = createDatasource(id);
|
||||||
let provider: ReturnType<typeof createLazyProvider> | null = null;
|
let provider: ReturnType<typeof createLazyProvider> | null = null;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
return {
|
return {
|
||||||
flavour: 'sqlite',
|
flavour: 'sqlite',
|
||||||
|
datasource,
|
||||||
passive: true,
|
passive: true,
|
||||||
get status() {
|
get status() {
|
||||||
assertExists(provider);
|
assertExists(provider);
|
||||||
@@ -69,14 +70,12 @@ export const createSQLiteProvider: DocProviderCreator = (
|
|||||||
return provider.subscribeStatusChange(onStatusChange);
|
return provider.subscribeStatusChange(onStatusChange);
|
||||||
},
|
},
|
||||||
connect: () => {
|
connect: () => {
|
||||||
datasource = createDatasource(id);
|
|
||||||
provider = createLazyProvider(rootDoc, datasource, { origin: 'sqlite' });
|
provider = createLazyProvider(rootDoc, datasource, { origin: 'sqlite' });
|
||||||
provider.connect();
|
provider.connect();
|
||||||
connected = true;
|
connected = true;
|
||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
provider?.disconnect();
|
provider?.disconnect();
|
||||||
datasource = null;
|
|
||||||
provider = null;
|
provider = null;
|
||||||
connected = false;
|
connected = false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
writeOperation,
|
writeOperation,
|
||||||
} from '@affine/y-provider';
|
} from '@affine/y-provider';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import type { IDBPDatabase } from 'idb';
|
||||||
import { openDB } from 'idb';
|
import { openDB } from 'idb';
|
||||||
import type { Doc } from 'yjs';
|
import type { Doc } from 'yjs';
|
||||||
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
|
import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs';
|
||||||
@@ -31,13 +32,20 @@ export const createIndexedDBDatasource = ({
|
|||||||
dbName: string;
|
dbName: string;
|
||||||
mergeCount?: number;
|
mergeCount?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
let dbPromise: Promise<IDBPDatabase<BlockSuiteBinaryDB>> | null = null;
|
||||||
upgrade: upgradeDB,
|
const getDb = async () => {
|
||||||
});
|
if (dbPromise === null) {
|
||||||
|
dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
||||||
|
upgrade: upgradeDB,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dbPromise;
|
||||||
|
};
|
||||||
|
|
||||||
const adapter = {
|
const adapter = {
|
||||||
queryDocState: async (guid, options) => {
|
queryDocState: async (guid, options) => {
|
||||||
try {
|
try {
|
||||||
const db = await dbPromise;
|
const db = await getDb();
|
||||||
const store = db
|
const store = db
|
||||||
.transaction('workspace', 'readonly')
|
.transaction('workspace', 'readonly')
|
||||||
.objectStore('workspace');
|
.objectStore('workspace');
|
||||||
@@ -64,7 +72,7 @@ export const createIndexedDBDatasource = ({
|
|||||||
},
|
},
|
||||||
sendDocUpdate: async (guid, update) => {
|
sendDocUpdate: async (guid, update) => {
|
||||||
try {
|
try {
|
||||||
const db = await dbPromise;
|
const db = await getDb();
|
||||||
const store = db
|
const store = db
|
||||||
.transaction('workspace', 'readwrite')
|
.transaction('workspace', 'readwrite')
|
||||||
.objectStore('workspace');
|
.objectStore('workspace');
|
||||||
@@ -96,10 +104,15 @@ export const createIndexedDBDatasource = ({
|
|||||||
return {
|
return {
|
||||||
...adapter,
|
...adapter,
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
dbPromise.then(db => db.close()).catch(console.error);
|
getDb()
|
||||||
|
.then(db => db.close())
|
||||||
|
.then(() => {
|
||||||
|
dbPromise = null;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
},
|
},
|
||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
const db = await dbPromise;
|
const db = await getDb();
|
||||||
await db.clear('workspace');
|
await db.clear('workspace');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -112,7 +125,7 @@ export const createIndexedDBProvider = (
|
|||||||
doc: Doc,
|
doc: Doc,
|
||||||
dbName: string = DEFAULT_DB_NAME
|
dbName: string = DEFAULT_DB_NAME
|
||||||
): IndexedDBProvider => {
|
): IndexedDBProvider => {
|
||||||
let datasource: ReturnType<typeof createIndexedDBDatasource> | null = null;
|
const datasource = createIndexedDBDatasource({ dbName, mergeCount });
|
||||||
let provider: ReturnType<typeof createLazyProvider> | null = null;
|
let provider: ReturnType<typeof createLazyProvider> | null = null;
|
||||||
|
|
||||||
const apis = {
|
const apis = {
|
||||||
@@ -128,14 +141,12 @@ export const createIndexedDBProvider = (
|
|||||||
if (apis.connected) {
|
if (apis.connected) {
|
||||||
apis.disconnect();
|
apis.disconnect();
|
||||||
}
|
}
|
||||||
datasource = createIndexedDBDatasource({ dbName, mergeCount });
|
|
||||||
provider = createLazyProvider(doc, datasource, { origin: 'idb' });
|
provider = createLazyProvider(doc, datasource, { origin: 'idb' });
|
||||||
provider.connect();
|
provider.connect();
|
||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
datasource?.disconnect();
|
datasource?.disconnect();
|
||||||
provider?.disconnect();
|
provider?.disconnect();
|
||||||
datasource = null;
|
|
||||||
provider = null;
|
provider = null;
|
||||||
},
|
},
|
||||||
cleanup: async () => {
|
cleanup: async () => {
|
||||||
@@ -144,6 +155,7 @@ export const createIndexedDBProvider = (
|
|||||||
get connected() {
|
get connected() {
|
||||||
return provider?.connected || false;
|
return provider?.connected || false;
|
||||||
},
|
},
|
||||||
|
datasource,
|
||||||
} satisfies IndexedDBProvider;
|
} satisfies IndexedDBProvider;
|
||||||
|
|
||||||
return apis;
|
return apis;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { StatusAdapter } from '@affine/y-provider';
|
import type { DataSourceAdapter } from '@affine/y-provider';
|
||||||
import type { DBSchema, IDBPDatabase } from 'idb';
|
import type { DBSchema, IDBPDatabase } from 'idb';
|
||||||
|
|
||||||
export const dbVersion = 1;
|
export const dbVersion = 1;
|
||||||
@@ -9,7 +9,7 @@ export function upgradeDB(db: IDBPDatabase<BlockSuiteBinaryDB>) {
|
|||||||
db.createObjectStore('milestone', { keyPath: 'id' });
|
db.createObjectStore('milestone', { keyPath: 'id' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexedDBProvider extends StatusAdapter {
|
export interface IndexedDBProvider extends DataSourceAdapter {
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
cleanup: () => Promise<void>;
|
cleanup: () => Promise<void>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DocState, StatusAdapter } from './types';
|
import type { DocState } from './types';
|
||||||
|
|
||||||
export interface DatasourceDocAdapter extends Partial<StatusAdapter> {
|
export interface DatasourceDocAdapter {
|
||||||
/**
|
/**
|
||||||
* request diff update from other clients
|
* request diff update from other clients
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from 'yjs';
|
} from 'yjs';
|
||||||
|
|
||||||
import type { DatasourceDocAdapter } from './data-source';
|
import type { DatasourceDocAdapter } from './data-source';
|
||||||
import type { StatusAdapter } from './types';
|
import type { DataSourceAdapter } from './types';
|
||||||
import type { Status } from './types';
|
import type { Status } from './types';
|
||||||
|
|
||||||
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
||||||
@@ -45,7 +45,7 @@ export const createLazyProvider = (
|
|||||||
rootDoc: Doc,
|
rootDoc: Doc,
|
||||||
datasource: DatasourceDocAdapter,
|
datasource: DatasourceDocAdapter,
|
||||||
options: LazyProviderOptions = {}
|
options: LazyProviderOptions = {}
|
||||||
): DocProvider & StatusAdapter => {
|
): DocProvider & DataSourceAdapter => {
|
||||||
let connected = false;
|
let connected = false;
|
||||||
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
||||||
const disposableMap = new Map<string, Set<() => void>>();
|
const disposableMap = new Map<string, Set<() => void>>();
|
||||||
@@ -62,21 +62,17 @@ export const createLazyProvider = (
|
|||||||
const callbackSet = new Set<() => void>();
|
const callbackSet = new Set<() => void>();
|
||||||
const changeStatus = (newStatus: Status) => {
|
const changeStatus = (newStatus: Status) => {
|
||||||
// simulate a stack, each syncing and synced should be paired
|
// simulate a stack, each syncing and synced should be paired
|
||||||
if (newStatus.type === 'idle') {
|
|
||||||
if (connected && syncingStack !== 0) {
|
|
||||||
console.error('syncingStatus !== 0, this should not happen');
|
|
||||||
}
|
|
||||||
syncingStack = 0;
|
|
||||||
}
|
|
||||||
if (newStatus.type === 'syncing') {
|
if (newStatus.type === 'syncing') {
|
||||||
syncingStack++;
|
syncingStack++;
|
||||||
}
|
} else if (newStatus.type === 'synced' || newStatus.type === 'error') {
|
||||||
if (newStatus.type === 'synced' || newStatus.type === 'error') {
|
|
||||||
syncingStack--;
|
syncingStack--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (syncingStack < 0) {
|
if (syncingStack < 0) {
|
||||||
console.error('syncingStatus < 0, this should not happen');
|
console.error(
|
||||||
|
'syncingStatus < 0, this should not happen',
|
||||||
|
options.origin
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (syncingStack === 0) {
|
if (syncingStack === 0) {
|
||||||
@@ -85,6 +81,17 @@ export const createLazyProvider = (
|
|||||||
if (newStatus.type !== 'synced') {
|
if (newStatus.type !== 'synced') {
|
||||||
currentStatus = newStatus;
|
currentStatus = newStatus;
|
||||||
}
|
}
|
||||||
|
if (syncingStack === 0) {
|
||||||
|
if (!connected) {
|
||||||
|
currentStatus = {
|
||||||
|
type: 'idle',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentStatus = {
|
||||||
|
type: 'synced',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
callbackSet.forEach(cb => cb());
|
callbackSet.forEach(cb => cb());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,12 +109,6 @@ export const createLazyProvider = (
|
|||||||
stateVector: encodeStateVector(doc),
|
stateVector: encodeStateVector(doc),
|
||||||
})
|
})
|
||||||
.then(remoteUpdate => {
|
.then(remoteUpdate => {
|
||||||
if (!connected) {
|
|
||||||
changeStatus({
|
|
||||||
type: 'idle',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changeStatus({
|
changeStatus({
|
||||||
type: 'synced',
|
type: 'synced',
|
||||||
});
|
});
|
||||||
@@ -201,9 +202,6 @@ export const createLazyProvider = (
|
|||||||
function setupDatasourceListeners() {
|
function setupDatasourceListeners() {
|
||||||
assertExists(abortController, 'abortController should be defined');
|
assertExists(abortController, 'abortController should be defined');
|
||||||
const unsubscribe = datasource.onDocUpdate?.((guid, update) => {
|
const unsubscribe = datasource.onDocUpdate?.((guid, update) => {
|
||||||
if (!connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changeStatus({
|
changeStatus({
|
||||||
type: 'syncing',
|
type: 'syncing',
|
||||||
});
|
});
|
||||||
@@ -283,12 +281,6 @@ export const createLazyProvider = (
|
|||||||
// but we want to populate the cache for later update events
|
// but we want to populate the cache for later update events
|
||||||
connectDoc(rootDoc)
|
connectDoc(rootDoc)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!connected) {
|
|
||||||
changeStatus({
|
|
||||||
type: 'idle',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
changeStatus({
|
changeStatus({
|
||||||
type: 'synced',
|
type: 'synced',
|
||||||
});
|
});
|
||||||
@@ -305,9 +297,6 @@ export const createLazyProvider = (
|
|||||||
|
|
||||||
async function disconnect() {
|
async function disconnect() {
|
||||||
connected = false;
|
connected = false;
|
||||||
changeStatus({
|
|
||||||
type: 'idle',
|
|
||||||
});
|
|
||||||
disposeAll();
|
disposeAll();
|
||||||
assertExists(abortController, 'abortController should be defined');
|
assertExists(abortController, 'abortController should be defined');
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
@@ -349,5 +338,7 @@ export const createLazyProvider = (
|
|||||||
passive: true,
|
passive: true,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
|
|
||||||
|
datasource,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { DatasourceDocAdapter } from './data-source';
|
||||||
|
|
||||||
export type Status =
|
export type Status =
|
||||||
| {
|
| {
|
||||||
type: 'idle';
|
type: 'idle';
|
||||||
@@ -10,11 +12,13 @@ export type Status =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'error';
|
type: 'error';
|
||||||
error: Error;
|
error: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StatusAdapter {
|
export interface DataSourceAdapter {
|
||||||
|
datasource: DatasourceDocAdapter;
|
||||||
readonly status: Status;
|
readonly status: Status;
|
||||||
|
|
||||||
subscribeStatusChange(onStatusChange: () => void): () => void;
|
subscribeStatusChange(onStatusChange: () => void): () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user