feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
164
apps/core/src/adapters/cloud/crud.ts
Normal file
164
apps/core/src/adapters/cloud/crud.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
WorkspaceCRUD,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
deleteWorkspaceMutation,
|
||||
getWorkspaceQuery,
|
||||
getWorkspacesQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
|
||||
import { migrateLocalBlobStorage } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
createIndexedDBProvider,
|
||||
DEFAULT_DB_NAME,
|
||||
} from '@toeverything/y-indexeddb';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { proxy } from 'valtio/vanilla';
|
||||
|
||||
const Y = Workspace.Y;
|
||||
|
||||
async function deleteLocalBlobStorage(id: string) {
|
||||
const storage = createIndexeddbStorage(id);
|
||||
const keys = await storage.crud.list();
|
||||
for (const key of keys) {
|
||||
await storage.crud.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to persistence the state into local storage
|
||||
// because if a user clicks create multiple time and nothing happened
|
||||
// because of the server delay or something, he/she will wait.
|
||||
// and also the user journey of creating workspace is long.
|
||||
const createdWorkspaces = proxy<string[]>([]);
|
||||
|
||||
export const CRUD: WorkspaceCRUD<WorkspaceFlavour.AFFINE_CLOUD> = {
|
||||
create: async blockSuiteWorkspace => {
|
||||
if (createdWorkspaces.some(id => id === blockSuiteWorkspace.id)) {
|
||||
throw new Error('workspace already created');
|
||||
}
|
||||
const { createWorkspace } = await fetcher({
|
||||
query: createWorkspaceMutation,
|
||||
variables: {
|
||||
init: new File(
|
||||
[Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)],
|
||||
'initBinary.yDoc'
|
||||
),
|
||||
},
|
||||
});
|
||||
createdWorkspaces.push(blockSuiteWorkspace.id);
|
||||
const newBLockSuiteWorkspace = getOrCreateWorkspace(
|
||||
createWorkspace.id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
);
|
||||
|
||||
Y.applyUpdate(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
[...blockSuiteWorkspace.doc.subdocs].map(async subdoc => {
|
||||
subdoc.load();
|
||||
return subdoc.whenLoaded.then(() => {
|
||||
newBLockSuiteWorkspace.doc.subdocs.forEach(newSubdoc => {
|
||||
if (newSubdoc.guid === subdoc.guid) {
|
||||
Y.applyUpdate(newSubdoc, Y.encodeStateAsUpdate(subdoc));
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const provider = createIndexedDBProvider(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
DEFAULT_DB_NAME
|
||||
);
|
||||
provider.connect();
|
||||
migrateLocalBlobStorage(blockSuiteWorkspace.id, createWorkspace.id)
|
||||
.then(() => deleteLocalBlobStorage(blockSuiteWorkspace.id))
|
||||
.catch(e => {
|
||||
console.error('error when moving blob storage:', e);
|
||||
});
|
||||
// todo(himself65): delete old workspace in the future
|
||||
return createWorkspace.id;
|
||||
},
|
||||
delete: async workspace => {
|
||||
await fetcher({
|
||||
query: deleteWorkspaceMutation,
|
||||
variables: {
|
||||
id: workspace.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
get: async id => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await fetcher({
|
||||
query: getWorkspaceQuery,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
} satisfies AffineCloudWorkspace;
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspace:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
list: async () => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const { workspaces } = await fetcher({
|
||||
query: getWorkspacesQuery,
|
||||
});
|
||||
const ids = workspaces.map(({ id }) => id);
|
||||
|
||||
return ids.map(
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
}) satisfies AffineCloudWorkspace
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspaces:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceUISchema,
|
||||
} from '@affine/env/workspace';
|
||||
import { lazy, useCallback } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
const LoginCard = lazy(() =>
|
||||
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
|
||||
default: LoginCard,
|
||||
}))
|
||||
);
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
LoginCard,
|
||||
Header: WorkspaceHeader,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
const isOwner = useIsWorkspaceOwner(currentWorkspaceId);
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_CLOUD>;
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
@@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {
|
||||
'app:access': async () => true,
|
||||
'app:init': () => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
nanoid(),
|
||||
@@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
CRUD,
|
||||
UI: {
|
||||
Header: WorkspaceHeader,
|
||||
Provider: ({ children }) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
Provider,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
@@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { type WorkspaceUISchema } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import { BlockSuitePageList, PageDetailEditor, Provider } from '../shared';
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
Header: () => {
|
||||
return null;
|
||||
},
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: () => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;
|
||||
@@ -1,5 +1,11 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const Provider = lazy(() =>
|
||||
import('../components/cloud/provider').then(({ Provider }) => ({
|
||||
default: Provider,
|
||||
}))
|
||||
);
|
||||
|
||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/new-workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
import { CRUD as CloudCRUD } from './cloud/crud';
|
||||
import { UI as CloudUI } from './cloud/ui';
|
||||
import { LocalAdapter } from './local';
|
||||
import { UI as PublicCloudUI } from './public-cloud/ui';
|
||||
|
||||
const unimplemented = () => {
|
||||
throw new Error('Not implemented');
|
||||
@@ -26,26 +29,24 @@ export const WorkspaceAdapters = {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
loadPriority: LoadPriority.HIGH,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
CRUD: {
|
||||
get: unimplemented,
|
||||
list: bypassList,
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
Events: {
|
||||
'app:access': async () => {
|
||||
try {
|
||||
const { getSession } = await import('next-auth/react');
|
||||
const session = await getSession();
|
||||
return !!session;
|
||||
} catch (e) {
|
||||
console.error('failed to get session', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
} as Partial<AppEvents>,
|
||||
CRUD: CloudCRUD,
|
||||
UI: CloudUI,
|
||||
},
|
||||
[WorkspaceFlavour.PUBLIC]: {
|
||||
[WorkspaceFlavour.AFFINE_PUBLIC]: {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.PUBLIC,
|
||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
@@ -55,14 +56,7 @@ export const WorkspaceAdapters = {
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
UI: PublicCloudUI,
|
||||
},
|
||||
} satisfies {
|
||||
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
||||
|
||||
Reference in New Issue
Block a user