From bb1224f9ee1de109e79dc7dfda02ef0e634b8890 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Thu, 30 Mar 2023 18:21:26 -0500 Subject: [PATCH] feat: add affine global channel (#1762) --- apps/web/src/atoms/index.ts | 22 +---- apps/web/src/blocksuite/index.ts | 3 +- .../src/blocksuite/providers/affine/index.ts | 2 +- .../providers/broad-cast-channel/index.ts | 2 +- apps/web/src/blocksuite/providers/index.ts | 10 +- .../panel/collaboration/index.tsx | 2 +- .../panel/publish/index.tsx | 7 +- .../header/header-right-items/SyncUser.tsx | 6 +- .../pure/workspace-avatar/index.tsx | 4 +- .../components/pure/workspace-card/index.tsx | 10 +- .../pure/workspace-list-modal/index.tsx | 10 +- .../pure/workspace-slider-bar/Pivots.tsx | 6 +- .../WorkspaceSelector/WorkspaceSelector.tsx | 4 +- .../pure/workspace-slider-bar/index.tsx | 4 +- apps/web/src/hooks/__tests__/index.spec.tsx | 9 +- .../src/hooks/affine/use-affine-log-out.ts | 2 +- .../affine/use-toggle-workspace-publish.ts | 4 +- .../hooks/current/use-current-workspace.ts | 6 +- .../src/hooks/use-create-first-workspace.ts | 2 +- ...-router-with-current-workspace-and-page.ts | 7 +- apps/web/src/hooks/use-transform-workspace.ts | 2 +- apps/web/src/hooks/use-workspaces.ts | 8 +- apps/web/src/layouts/index.tsx | 27 +++++- apps/web/src/pages/_app.tsx | 2 +- apps/web/src/pages/_debug/broadcast.dev.tsx | 2 +- .../src/pages/workspace/[workspaceId]/all.tsx | 6 +- apps/web/src/plugins/affine/fetcher.ts | 5 +- apps/web/src/plugins/affine/index.tsx | 2 +- apps/web/src/plugins/index.tsx | 8 -- apps/web/src/plugins/local/index.tsx | 4 +- apps/web/src/shared/apis.ts | 2 +- apps/web/src/shared/index.ts | 55 +---------- packages/workspace/package.json | 1 + packages/workspace/src/affine/api/index.ts | 33 +++++-- packages/workspace/src/affine/channel.ts | 93 +++++++++++++++++++ packages/workspace/src/affine/sync.ts | 71 ++++++++++++++ packages/workspace/src/atom.ts | 18 ++++ packages/workspace/src/type.ts | 59 +++++++++++- 38 files changed, 358 insertions(+), 162 deletions(-) create mode 100644 packages/workspace/src/affine/channel.ts create mode 100644 packages/workspace/src/affine/sync.ts create mode 100644 packages/workspace/src/atom.ts diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 558e7bced7..cc0e9dcbfd 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -1,13 +1,13 @@ -import type { WorkspaceFlavour } from '@affine/workspace/type'; +import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import type { EditorContainer } from '@blocksuite/editor'; import type { Page } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store'; -import { atom, createStore } from 'jotai'; +import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import { unstable_batchedUpdates } from 'react-dom'; import { WorkspacePlugins } from '../plugins'; -import type { RemWorkspace } from '../shared'; +import type { AllWorkspace } from '../shared'; // workspace necessary atoms export const currentWorkspaceIdAtom = atom(null); export const currentPageIdAtom = atom(null); @@ -34,19 +34,7 @@ export const openWorkspacesModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false); -export const jotaiStore = createStore(); - -type JotaiWorkspace = { - id: string; - flavour: WorkspaceFlavour; -}; - -export const jotaiWorkspacesAtom = atomWithStorage( - 'jotai-workspaces', - [] -); - -export const workspacesAtom = atom>(async get => { +export const workspacesAtom = atom>(async get => { const flavours: string[] = Object.values(WorkspacePlugins).map( plugin => plugin.flavour ); @@ -62,7 +50,7 @@ export const workspacesAtom = atom>(async get => { return CRUD.get(workspace.id); }) ); - return workspaces.filter(workspace => workspace !== null) as RemWorkspace[]; + return workspaces.filter(workspace => workspace !== null) as AllWorkspace[]; }); type View = { id: string; mode: 'page' | 'edgeless' }; diff --git a/apps/web/src/blocksuite/index.ts b/apps/web/src/blocksuite/index.ts index aa5a3d9d75..cc33651cf2 100644 --- a/apps/web/src/blocksuite/index.ts +++ b/apps/web/src/blocksuite/index.ts @@ -1,6 +1,7 @@ import { config } from '@affine/env'; +import type { Provider } from '@affine/workspace/type'; -import type { BlockSuiteWorkspace, Provider } from '../shared'; +import type { BlockSuiteWorkspace } from '../shared'; import { createAffineWebSocketProvider, createBroadCastChannelProvider, diff --git a/apps/web/src/blocksuite/providers/affine/index.ts b/apps/web/src/blocksuite/providers/affine/index.ts index 75f265d439..7531888589 100644 --- a/apps/web/src/blocksuite/providers/affine/index.ts +++ b/apps/web/src/blocksuite/providers/affine/index.ts @@ -1,6 +1,6 @@ +import type { AffineDownloadProvider } from '@affine/workspace/type'; import { assertExists } from '@blocksuite/store'; -import type { AffineDownloadProvider } from '../../../shared'; import { BlockSuiteWorkspace } from '../../../shared'; import { affineApis } from '../../../shared/apis'; import { providerLogger } from '../../logger'; diff --git a/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts b/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts index 01b9cc9de2..e850093595 100644 --- a/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts +++ b/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts @@ -1,3 +1,4 @@ +import type { BroadCastChannelProvider } from '@affine/workspace/type'; import { assertExists } from '@blocksuite/store'; import type { Awareness } from 'y-protocols/awareness'; import { @@ -5,7 +6,6 @@ import { encodeAwarenessUpdate, } from 'y-protocols/awareness'; -import type { BroadCastChannelProvider } from '../../../shared'; import { BlockSuiteWorkspace } from '../../../shared'; import { providerLogger } from '../../logger'; import type { diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts index b771938555..ec9ac482bc 100644 --- a/apps/web/src/blocksuite/providers/index.ts +++ b/apps/web/src/blocksuite/providers/index.ts @@ -1,13 +1,13 @@ import { KeckProvider } from '@affine/workspace/affine/keck'; import { getLoginStorage } from '@affine/workspace/affine/login'; +import type { + AffineWebSocketProvider, + LocalIndexedDBProvider, +} from '@affine/workspace/type'; import { assertExists } from '@blocksuite/store'; import { IndexeddbPersistence } from 'y-indexeddb'; -import type { - AffineWebSocketProvider, - BlockSuiteWorkspace, - LocalIndexedDBProvider, -} from '../../shared'; +import type { BlockSuiteWorkspace } from '../../shared'; import { providerLogger } from '../logger'; import { createBroadCastChannelProvider } from './broad-cast-channel'; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx index be3ea1af18..237f662c4b 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx @@ -1,6 +1,7 @@ import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { PermissionType } from '@affine/workspace/affine/api'; +import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { DeleteTemporarilyIcon, @@ -11,7 +12,6 @@ import type React from 'react'; import { useCallback, useState } from 'react'; import { useMembers } from '../../../../../hooks/affine/use-members'; -import type { AffineWorkspace, LocalWorkspace } from '../../../../../shared'; import { toast } from '../../../../../utils'; import { Unreachable } from '../../../affine-error-eoundary'; import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal'; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx index 3fc42eff50..d969355553 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx @@ -6,17 +6,14 @@ import { Wrapper, } from '@affine/component'; import { useTranslation } from '@affine/i18n'; +import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { Box } from '@mui/material'; import type React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish'; -import type { - AffineOfficialWorkspace, - AffineWorkspace, - LocalWorkspace, -} from '../../../../../shared'; +import type { AffineOfficialWorkspace } from '../../../../../shared'; import { toast } from '../../../../../utils'; import { Unreachable } from '../../../affine-error-eoundary'; import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal'; diff --git a/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx index de95be39d0..9cf06621d3 100644 --- a/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx @@ -5,6 +5,7 @@ import { setLoginStorage, SignMethod, } from '@affine/workspace/affine/login'; +import type { LocalWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { CloudWorkspaceIcon, @@ -18,10 +19,7 @@ import React, { useEffect, useState } from 'react'; import { affineAuth } from '../../../../hooks/affine/use-affine-log-in'; import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace'; -import type { - AffineOfficialWorkspace, - LocalWorkspace, -} from '../../../../shared'; +import type { AffineOfficialWorkspace } from '../../../../shared'; import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal'; const IconWrapper = styled('div')(({ theme }) => { diff --git a/apps/web/src/components/pure/workspace-avatar/index.tsx b/apps/web/src/components/pure/workspace-avatar/index.tsx index bc88f166e8..213929d456 100644 --- a/apps/web/src/components/pure/workspace-avatar/index.tsx +++ b/apps/web/src/components/pure/workspace-avatar/index.tsx @@ -3,7 +3,7 @@ import type React from 'react'; import { memo } from 'react'; import { useBlockSuiteWorkspaceAvatarUrl } from '../../../hooks/use-blocksuite-workspace-avatar-url'; -import type { BlockSuiteWorkspace, RemWorkspace } from '../../../shared'; +import type { AllWorkspace, BlockSuiteWorkspace } from '../../../shared'; import { stringToColour } from '../../../utils'; interface AvatarProps { @@ -75,7 +75,7 @@ export const Avatar: React.FC = memo(function Avatar({ export type WorkspaceUnitAvatarProps = { size?: number; - workspace: RemWorkspace | null; + workspace: AllWorkspace | null; style?: React.CSSProperties; }; diff --git a/apps/web/src/components/pure/workspace-card/index.tsx b/apps/web/src/components/pure/workspace-card/index.tsx index 5378f1732e..990d9d15f2 100644 --- a/apps/web/src/components/pure/workspace-card/index.tsx +++ b/apps/web/src/components/pure/workspace-card/index.tsx @@ -6,7 +6,7 @@ import type React from 'react'; import { useCallback } from 'react'; import { useBlockSuiteWorkspaceName } from '../../../hooks/use-blocksuite-workspace-name'; -import type { RemWorkspace } from '../../../shared'; +import type { AllWorkspace } from '../../../shared'; import { CloudWorkspaceIcon, JoinedWorkspaceIcon, @@ -22,7 +22,7 @@ import { StyleWorkspaceTitle, } from './styles'; export type WorkspaceTypeProps = { - workspace: RemWorkspace; + workspace: AllWorkspace; }; const WorkspaceType: React.FC = ({ workspace }) => { @@ -58,9 +58,9 @@ const WorkspaceType: React.FC = ({ workspace }) => { export type WorkspaceCardProps = { currentWorkspaceId: string | null; - workspace: RemWorkspace; - onClick: (workspace: RemWorkspace) => void; - onSettingClick: (workspace: RemWorkspace) => void; + workspace: AllWorkspace; + onClick: (workspace: AllWorkspace) => void; + onSettingClick: (workspace: AllWorkspace) => void; }; export const WorkspaceCard: React.FC = ({ diff --git a/apps/web/src/components/pure/workspace-list-modal/index.tsx b/apps/web/src/components/pure/workspace-list-modal/index.tsx index 5e43b19c0b..c771de808a 100644 --- a/apps/web/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/web/src/components/pure/workspace-list-modal/index.tsx @@ -8,7 +8,7 @@ import { useTranslation } from '@affine/i18n'; import type { AccessTokenMessage } from '@affine/workspace/affine/login'; import { HelpIcon, PlusIcon } from '@blocksuite/icons'; -import type { RemWorkspace } from '../../../shared'; +import type { AllWorkspace } from '../../../shared'; import { Footer } from '../footer'; import { WorkspaceCard } from '../workspace-card'; import { LanguageMenu } from './language-menu'; @@ -28,12 +28,12 @@ import { interface WorkspaceModalProps { user: AccessTokenMessage | null; - workspaces: RemWorkspace[]; - currentWorkspaceId: RemWorkspace['id'] | null; + workspaces: AllWorkspace[]; + currentWorkspaceId: AllWorkspace['id'] | null; open: boolean; onClose: () => void; - onClickWorkspace: (workspace: RemWorkspace) => void; - onClickWorkspaceSetting: (workspace: RemWorkspace) => void; + onClickWorkspace: (workspace: AllWorkspace) => void; + onClickWorkspaceSetting: (workspace: AllWorkspace) => void; onClickLogin: () => void; onClickLogout: () => void; onCreateWorkspace: () => void; diff --git a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx index 07a020e35e..145d0e18e9 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/Pivots.tsx @@ -5,7 +5,7 @@ import type { PageMeta } from '@blocksuite/store'; import type { MouseEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; -import type { RemWorkspace } from '../../../shared'; +import type { AllWorkspace } from '../../../shared'; import type { TreeNode } from '../../affine/pivots'; import { PivotRender, @@ -20,7 +20,7 @@ export const PivotInternal = ({ openPage, allMetas, }: { - currentWorkspace: RemWorkspace; + currentWorkspace: AllWorkspace; openPage: (pageId: string) => void; allMetas: PageMeta[]; }) => { @@ -68,7 +68,7 @@ export const Pivots = ({ openPage, allMetas, }: { - currentWorkspace: RemWorkspace; + currentWorkspace: AllWorkspace; openPage: (pageId: string) => void; allMetas: PageMeta[]; }) => { diff --git a/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx index ac24e8a5d3..5a42094b3b 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx @@ -3,7 +3,7 @@ import type React from 'react'; import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import { useBlockSuiteWorkspaceName } from '../../../../hooks/use-blocksuite-workspace-name'; -import type { RemWorkspace } from '../../../../shared'; +import type { AllWorkspace } from '../../../../shared'; import { WorkspaceAvatar } from '../../workspace-avatar'; import { StyledSelectorContainer, @@ -13,7 +13,7 @@ import { } from './styles'; export type WorkspaceSelectorProps = { - currentWorkspace: RemWorkspace | null; + currentWorkspace: AllWorkspace | null; onClick: () => void; }; diff --git a/apps/web/src/components/pure/workspace-slider-bar/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/index.tsx index 1921c0273c..54a7cd65b0 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -13,7 +13,7 @@ import { useCallback } from 'react'; import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status'; import { usePageMeta } from '../../../hooks/use-page-meta'; -import type { RemWorkspace } from '../../../shared'; +import type { AllWorkspace } from '../../../shared'; import { SidebarSwitch } from '../../affine/sidebar-switch'; import { ChangeLog } from './changeLog'; import Favorite from './favorite'; @@ -39,7 +39,7 @@ export type WorkSpaceSliderBarProps = { isPublicWorkspace: boolean; onOpenQuickSearchModal: () => void; onOpenWorkspaceListModal: () => void; - currentWorkspace: RemWorkspace | null; + currentWorkspace: AllWorkspace | null; currentPageId: string | null; openPage: (pageId: string) => void; createPage: () => Page; diff --git a/apps/web/src/hooks/__tests__/index.spec.tsx b/apps/web/src/hooks/__tests__/index.spec.tsx index 6f7402d3e0..e2cc840ce2 100644 --- a/apps/web/src/hooks/__tests__/index.spec.tsx +++ b/apps/web/src/hooks/__tests__/index.spec.tsx @@ -5,6 +5,8 @@ import 'fake-indexeddb/auto'; import assert from 'node:assert'; +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; +import type { LocalWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import type { Page } from '@blocksuite/store'; @@ -17,13 +19,8 @@ import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes'; import type React from 'react'; import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; -import { - currentWorkspaceIdAtom, - jotaiWorkspacesAtom, - workspacesAtom, -} from '../../atoms'; +import { currentWorkspaceIdAtom, workspacesAtom } from '../../atoms'; import { LocalPlugin } from '../../plugins/local'; -import type { LocalWorkspace } from '../../shared'; import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared'; import { useGuideHidden, diff --git a/apps/web/src/hooks/affine/use-affine-log-out.ts b/apps/web/src/hooks/affine/use-affine-log-out.ts index 78ce0d89d9..c119aec6db 100644 --- a/apps/web/src/hooks/affine/use-affine-log-out.ts +++ b/apps/web/src/hooks/affine/use-affine-log-out.ts @@ -1,11 +1,11 @@ import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import { clearLoginStorage } from '@affine/workspace/affine/login'; +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { useSetAtom } from 'jotai'; import { useRouter } from 'next/router'; import { useCallback } from 'react'; -import { jotaiWorkspacesAtom } from '../../atoms'; import { WorkspacePlugins } from '../../plugins'; export function useAffineLogOut() { diff --git a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts index 3031a7908d..7f661f49f7 100644 --- a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts +++ b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts @@ -1,9 +1,9 @@ +import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; +import type { AffineWorkspace } from '@affine/workspace/type'; import { useCallback } from 'react'; import useSWR from 'swr'; -import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms'; import { QueryKey } from '../../plugins/affine/fetcher'; -import type { AffineWorkspace } from '../../shared'; import { affineApis } from '../../shared/apis'; export function useToggleWorkspacePublish(workspace: AffineWorkspace) { diff --git a/apps/web/src/hooks/current/use-current-workspace.ts b/apps/web/src/hooks/current/use-current-workspace.ts index f59d5ac7c3..c787dd302f 100644 --- a/apps/web/src/hooks/current/use-current-workspace.ts +++ b/apps/web/src/hooks/current/use-current-workspace.ts @@ -7,9 +7,9 @@ import { currentWorkspaceIdAtom, workspacesAtom, } from '../../atoms'; -import type { RemWorkspace } from '../../shared'; +import type { AllWorkspace } from '../../shared'; -export const currentWorkspaceAtom = atom>( +export const currentWorkspaceAtom = atom>( async get => { const id = get(currentWorkspaceIdAtom); const workspaces = await get(workspacesAtom); @@ -23,7 +23,7 @@ export const lastWorkspaceIdAtom = atomWithStorage( ); export function useCurrentWorkspace(): [ - RemWorkspace | null, + AllWorkspace | null, (id: string | null) => void ] { const currentWorkspace = useAtomValue(currentWorkspaceAtom); diff --git a/apps/web/src/hooks/use-create-first-workspace.ts b/apps/web/src/hooks/use-create-first-workspace.ts index bcd2f14ef7..3bbc0b9892 100644 --- a/apps/web/src/hooks/use-create-first-workspace.ts +++ b/apps/web/src/hooks/use-create-first-workspace.ts @@ -1,11 +1,11 @@ import { DEFAULT_WORKSPACE_NAME } from '@affine/env'; +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; import { useAtom } from 'jotai'; import { useEffect } from 'react'; -import { jotaiWorkspacesAtom } from '../atoms'; import { LocalPlugin } from '../plugins/local'; export function useCreateFirstWorkspace() { diff --git a/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts b/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts index d792b83411..e76ba1a163 100644 --- a/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts +++ b/apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts @@ -1,9 +1,10 @@ +import { jotaiStore } from '@affine/workspace/atom'; import { WorkspaceFlavour } from '@affine/workspace/type'; import type { NextRouter } from 'next/router'; import { useEffect } from 'react'; -import { currentPageIdAtom, jotaiStore } from '../atoms'; -import type { RemWorkspace } from '../shared'; +import { currentPageIdAtom } from '../atoms'; +import type { AllWorkspace } from '../shared'; import { WorkspaceSubPath } from '../shared'; import { useCurrentPageId } from './current/use-current-page-id'; import { useCurrentWorkspace } from './current/use-current-workspace'; @@ -11,7 +12,7 @@ import { RouteLogic, useRouterHelper } from './use-router-helper'; import { useWorkspaces } from './use-workspaces'; export function findSuitablePageId( - workspace: RemWorkspace, + workspace: AllWorkspace, targetId: string ): string | null { switch (workspace.flavour) { diff --git a/apps/web/src/hooks/use-transform-workspace.ts b/apps/web/src/hooks/use-transform-workspace.ts index 10fc9cf464..703c2cc686 100644 --- a/apps/web/src/hooks/use-transform-workspace.ts +++ b/apps/web/src/hooks/use-transform-workspace.ts @@ -1,9 +1,9 @@ +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; import type { WorkspaceFlavour } from '@affine/workspace/type'; import type { WorkspaceRegistry } from '@affine/workspace/type'; import { useSetAtom } from 'jotai'; import { useCallback } from 'react'; -import { jotaiWorkspacesAtom } from '../atoms'; import { WorkspacePlugins } from '../plugins'; /** diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 86f26ae887..437ca646b0 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -1,15 +1,17 @@ +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; +import type { LocalWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback, useEffect } from 'react'; -import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms'; +import { workspacesAtom } from '../atoms'; import { WorkspacePlugins } from '../plugins'; import { LocalPlugin } from '../plugins/local'; -import type { LocalWorkspace, RemWorkspace } from '../shared'; +import type { AllWorkspace } from '../shared'; -export function useWorkspaces(): RemWorkspace[] { +export function useWorkspaces(): AllWorkspace[] { return useAtomValue(workspacesAtom); } diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/index.tsx index 80c90198fb..f40c5de3a5 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/index.tsx @@ -1,5 +1,8 @@ import { DebugLogger } from '@affine/debug'; import { setUpLanguage, useTranslation } from '@affine/i18n'; +import { createAffineGlobalChannel } from '@affine/workspace/affine/sync'; +import { jotaiWorkspacesAtom } from '@affine/workspace/atom'; +import { WorkspaceFlavour } from '@affine/workspace/type'; import { assertExists, nanoid } from '@blocksuite/store'; import { NoSsr } from '@mui/material'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; @@ -11,7 +14,6 @@ import { Suspense, useCallback, useEffect } from 'react'; import { currentWorkspaceIdAtom, - jotaiWorkspacesAtom, openQuickSearchModalAtom, openWorkspacesModalAtom, workspaceLockAtom, @@ -33,13 +35,13 @@ import { useRouterTitle } from '../hooks/use-router-title'; import { useWorkspaces } from '../hooks/use-workspaces'; import { WorkspacePlugins } from '../plugins'; import { ModalProvider } from '../providers/ModalProvider'; -import type { RemWorkspace } from '../shared'; +import type { AllWorkspace } from '../shared'; import { pathGenerator, publicPathGenerator } from '../shared'; import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles'; declare global { // eslint-disable-next-line no-var - var currentWorkspace: RemWorkspace; + var currentWorkspace: AllWorkspace; } const QuickSearchModal = dynamic( @@ -89,6 +91,10 @@ export const QuickSearch: React.FC = () => { }; const logger = new DebugLogger('workspace-layout'); + +const affineGlobalChannel = createAffineGlobalChannel( + WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD +); export const WorkspaceLayout: React.FC = function WorkspacesSuspense({ children }) { const { i18n } = useTranslation(); @@ -105,6 +111,7 @@ export const WorkspaceLayout: React.FC = const lists = Object.values(WorkspacePlugins) .sort((a, b) => a.loadPriority - b.loadPriority) .map(({ CRUD }) => CRUD.list); + async function fetch() { const items = []; for (const list of lists) { @@ -121,6 +128,7 @@ export const WorkspaceLayout: React.FC = set([...items]); logger.info('mount first data:', items); } + fetch(); return () => { controller.abort(); @@ -128,6 +136,19 @@ export const WorkspaceLayout: React.FC = }; }, [set]); const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); + const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom); + + useEffect(() => { + const flavour = jotaiWorkspaces.find( + x => x.id === currentWorkspaceId + )?.flavour; + if (flavour === WorkspaceFlavour.AFFINE) { + affineGlobalChannel.connect(); + return () => { + affineGlobalChannel.disconnect(); + }; + } + }, [currentWorkspaceId, jotaiWorkspaces]); return ( {/* fixme(himself65): don't re-render whole modals */} diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 04243deab9..b7a8ffb98d 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -2,6 +2,7 @@ import '../styles/globals.css'; import { config, setupGlobal } from '@affine/env'; import { createI18n, I18nextProvider } from '@affine/i18n'; +import { jotaiStore } from '@affine/workspace/atom'; import type { EmotionCache } from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import { Provider } from 'jotai'; @@ -11,7 +12,6 @@ import { useRouter } from 'next/router'; import type { ReactElement } from 'react'; import React, { Suspense, useEffect, useMemo } from 'react'; -import { jotaiStore } from '../atoms'; import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary'; import { ProviderComposer } from '../components/provider-composer'; import { PageLoading } from '../components/pure/loading'; diff --git a/apps/web/src/pages/_debug/broadcast.dev.tsx b/apps/web/src/pages/_debug/broadcast.dev.tsx index 7818cc93d3..9356c3a61e 100644 --- a/apps/web/src/pages/_debug/broadcast.dev.tsx +++ b/apps/web/src/pages/_debug/broadcast.dev.tsx @@ -1,5 +1,6 @@ import { Button } from '@affine/component'; import { DebugLogger } from '@affine/debug'; +import type { BroadCastChannelProvider } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; import { Typography } from '@mui/material'; @@ -9,7 +10,6 @@ import { useEffect, useMemo, useState } from 'react'; import { createBroadCastChannelProvider } from '../../blocksuite/providers'; import PageList from '../../components/blocksuite/block-suite-page-list/page-list'; import { StyledPage, StyledWrapper } from '../../layouts/styles'; -import type { BroadCastChannelProvider } from '../../shared'; import { toast } from '../../utils'; const logger = new DebugLogger('broadcast'); diff --git a/apps/web/src/pages/workspace/[workspaceId]/all.tsx b/apps/web/src/pages/workspace/[workspaceId]/all.tsx index ffcced55b4..25c7236679 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/all.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/all.tsx @@ -1,4 +1,5 @@ import { useTranslation } from '@affine/i18n'; +import type { LocalIndexedDBProvider } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { FolderIcon } from '@blocksuite/icons'; import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; @@ -17,10 +18,7 @@ import { useRouterHelper } from '../../../hooks/use-router-helper'; import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { WorkspaceLayout } from '../../../layouts'; import { WorkspacePlugins } from '../../../plugins'; -import type { - LocalIndexedDBProvider, - NextPageWithLayout, -} from '../../../shared'; +import type { NextPageWithLayout } from '../../../shared'; const AllPage: NextPageWithLayout = () => { const router = useRouter(); diff --git a/apps/web/src/plugins/affine/fetcher.ts b/apps/web/src/plugins/affine/fetcher.ts index bb221364d5..302f609ec5 100644 --- a/apps/web/src/plugins/affine/fetcher.ts +++ b/apps/web/src/plugins/affine/fetcher.ts @@ -1,12 +1,13 @@ import { getLoginStorage } from '@affine/workspace/affine/login'; +import { jotaiStore } from '@affine/workspace/atom'; +import type { AffineWorkspace } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { assertExists } from '@blocksuite/store'; -import { jotaiStore, workspacesAtom } from '../../atoms'; +import { workspacesAtom } from '../../atoms'; import { createAffineProviders } from '../../blocksuite'; import { Unreachable } from '../../components/affine/affine-error-eoundary'; -import type { AffineWorkspace } from '../../shared'; import { affineApis } from '../../shared/apis'; type Query = (typeof QueryKey)[keyof typeof QueryKey]; diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index f135f2aa4f..a694a01ca0 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -1,4 +1,5 @@ import { getLoginStorage } from '@affine/workspace/affine/login'; +import type { AffineWorkspace } from '@affine/workspace/type'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createJSONStorage } from 'jotai/utils'; @@ -11,7 +12,6 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { PageDetailEditor } from '../../components/page-detail-editor'; -import type { AffineWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared'; import { affineApis } from '../../shared/apis'; import { initPage } from '../../utils'; diff --git a/apps/web/src/plugins/index.tsx b/apps/web/src/plugins/index.tsx index bac7e3ee2c..c914d51bc7 100644 --- a/apps/web/src/plugins/index.tsx +++ b/apps/web/src/plugins/index.tsx @@ -5,17 +5,9 @@ import type { } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type'; -import type { AffineWorkspace, LocalWorkspace } from '../shared'; import { AffinePlugin } from './affine'; import { LocalPlugin } from './local'; -declare module '@affine/workspace/type' { - interface WorkspaceRegistry { - [WorkspaceFlavour.AFFINE]: AffineWorkspace; - [WorkspaceFlavour.LOCAL]: LocalWorkspace; - } -} - export interface WorkspacePlugin { flavour: Flavour; // Plugin will be loaded according to the priority diff --git a/apps/web/src/plugins/local/index.tsx b/apps/web/src/plugins/local/index.tsx index ee97a8c287..e776a008f0 100644 --- a/apps/web/src/plugins/local/index.tsx +++ b/apps/web/src/plugins/local/index.tsx @@ -1,4 +1,5 @@ import { DEFAULT_WORKSPACE_NAME } from '@affine/env'; +import type { LocalWorkspace } from '@affine/workspace/type'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; @@ -12,9 +13,8 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { PageDetailEditor } from '../../components/page-detail-editor'; -import type { LocalWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared'; -import { initPage } from '../../utils/blocksuite'; +import { initPage } from '../../utils'; import type { WorkspacePlugin } from '..'; const getStorage = () => createJSONStorage(() => localStorage); diff --git a/apps/web/src/shared/apis.ts b/apps/web/src/shared/apis.ts index 4e3f08cc05..d7a3b5cc8d 100644 --- a/apps/web/src/shared/apis.ts +++ b/apps/web/src/shared/apis.ts @@ -6,8 +6,8 @@ import { import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import type { LoginResponse } from '@affine/workspace/affine/login'; import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login'; +import { jotaiStore } from '@affine/workspace/atom'; -import { jotaiStore } from '../atoms'; import { isValidIPAddress } from '../utils'; let prefixUrl = '/'; diff --git a/apps/web/src/shared/index.ts b/apps/web/src/shared/index.ts index eac47a1a2e..6534fdcff2 100644 --- a/apps/web/src/shared/index.ts +++ b/apps/web/src/shared/index.ts @@ -1,5 +1,4 @@ -import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api'; -import type { WorkspaceFlavour } from '@affine/workspace/type'; +import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { NextPage } from 'next'; import type { ReactElement, ReactNode } from 'react'; @@ -12,59 +11,9 @@ declare global { } } -export interface AffineWorkspace extends RemoteWorkspace { - flavour: WorkspaceFlavour.AFFINE; - // empty - blockSuiteWorkspace: BlockSuiteWorkspace; - providers: Provider[]; -} - -export interface LocalWorkspace { - flavour: WorkspaceFlavour.LOCAL; - id: string; - blockSuiteWorkspace: BlockSuiteWorkspace; - providers: Provider[]; -} - -export type BaseProvider = { - flavour: string; - // if this is true, we will connect the provider on the background - background: boolean; - connect: () => void; - disconnect: () => void; - // cleanup data when workspace is removed - cleanup: () => void; -}; - -export interface BackgroundProvider extends BaseProvider { - background: true; - callbacks: Set<() => void>; -} - -export interface AffineDownloadProvider extends BaseProvider { - flavour: 'affine-download'; -} - -export interface BroadCastChannelProvider extends BaseProvider { - flavour: 'broadcast-channel'; -} - -export interface LocalIndexedDBProvider extends BackgroundProvider { - flavour: 'local-indexeddb'; -} - -export interface AffineWebSocketProvider extends BaseProvider { - flavour: 'affine-websocket'; -} - -export type Provider = - | LocalIndexedDBProvider - | AffineWebSocketProvider - | BroadCastChannelProvider; - export type AffineOfficialWorkspace = AffineWorkspace | LocalWorkspace; -export type RemWorkspace = AffineOfficialWorkspace; +export type AllWorkspace = AffineOfficialWorkspace; export type NextPageWithLayout

, IP = P> = NextPage< P, diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 256732ff7b..745b162ce3 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -2,6 +2,7 @@ "name": "@affine/workspace", "private": true, "exports": { + "./atom": "./src/atom.ts", "./utils": "./src/utils.ts", "./type": "./src/type.ts", "./affine/*": "./src/affine/*.ts", diff --git a/packages/workspace/src/affine/api/index.ts b/packages/workspace/src/affine/api/index.ts index f4a5a4a8c7..0068895f44 100644 --- a/packages/workspace/src/affine/api/index.ts +++ b/packages/workspace/src/affine/api/index.ts @@ -94,17 +94,30 @@ export enum PermissionType { Owner = 99, } -export interface Workspace { - id: string; - type: WorkspaceType; - public: boolean; - permission: PermissionType; -} +export const userSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + avatar_url: z.string(), + create_at: z.string(), +}); -export interface WorkspaceDetail extends Workspace { - owner: User; - member_count: number; -} +export const workspaceSchema = z.object({ + id: z.string(), + type: z.nativeEnum(WorkspaceType), + public: z.boolean(), + permission: z.nativeEnum(PermissionType), +}); + +export type Workspace = z.infer; + +export const workspaceDetailSchema = z.object({ + ...workspaceSchema.shape, + owner: userSchema, + member_count: z.number(), +}); + +export type WorkspaceDetail = z.infer; export interface Permission { id: string; diff --git a/packages/workspace/src/affine/channel.ts b/packages/workspace/src/affine/channel.ts new file mode 100644 index 0000000000..ce9595b973 --- /dev/null +++ b/packages/workspace/src/affine/channel.ts @@ -0,0 +1,93 @@ +import { DebugLogger } from '@affine/debug'; +import { + getLoginStorage, + isExpired, + parseIdToken, +} from '@affine/workspace/affine/login'; +import { assertExists } from '@blocksuite/global/utils'; +import * as url from 'lib0/url'; +import * as websocket from 'lib0/websocket'; + +const RECONNECT_INTERVAL_TIME = 500; +const MAX_RECONNECT_TIMES = 50; + +export class WebsocketClient { + public readonly baseServerUrl: string; + private _client: websocket.WebsocketClient | null = null; + public shouldReconnect = false; + private _retryTimes = 0; + private _logger = new DebugLogger('affine:channel'); + private _callback: ((message: any) => void) | null = null; + + constructor(serverUrl: string) { + while (serverUrl.endsWith('/')) { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + this.baseServerUrl = serverUrl; + } + + public connect(callback: (message: any) => void) { + const loginResponse = getLoginStorage(); + assertExists(loginResponse, 'loginResponse is null'); + const encodedParams = url.encodeQueryParams({ + token: loginResponse.token, + }); + const serverUrl = + this.baseServerUrl + + (encodedParams.length === 0 ? '' : '?' + encodedParams); + this._client = new websocket.WebsocketClient(serverUrl); + this._callback = callback; + this._setupChannel(); + + this._client.on('message', this._callback); + } + + public disconnect() { + assertExists(this._client, 'client is null'); + if (this._callback) { + this._client.off('message', this._callback); + } + this._client.disconnect(); + this._client.destroy(); + this._client = null; + } + + private _setupChannel() { + assertExists(this._client, 'client is null'); + const client = this._client; + client.on('connect', () => { + this._logger.debug('Affine channel connected'); + this.shouldReconnect = true; + this._retryTimes = 0; + }); + + client.on('disconnect', ({ error }: { error: Error }) => { + if (error) { + const loginResponse = getLoginStorage(); + const isLogin = loginResponse + ? isExpired(parseIdToken(loginResponse.token)) + : false; + // Try to re-connect if connect error has occurred + if (this.shouldReconnect && isLogin && !client.connected) { + try { + setTimeout(() => { + if (this._retryTimes <= MAX_RECONNECT_TIMES) { + assertExists(this._callback, 'callback is null'); + this.connect(this._callback); + this._logger.info( + `try reconnect channel ${++this._retryTimes} times` + ); + } else { + this._logger.error( + 'reconnect failed, max reconnect times reached' + ); + } + }, RECONNECT_INTERVAL_TIME); + } catch (e) { + this._logger.error('reconnect failed', e); + } + } + } + }); + } +} diff --git a/packages/workspace/src/affine/sync.ts b/packages/workspace/src/affine/sync.ts new file mode 100644 index 0000000000..f8ecfc09b3 --- /dev/null +++ b/packages/workspace/src/affine/sync.ts @@ -0,0 +1,71 @@ +import { + workspaceDetailSchema, + workspaceSchema, +} from '@affine/workspace/affine/api'; +import { WebsocketClient } from '@affine/workspace/affine/channel'; +import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; +import type { WorkspaceCRUD } from '@affine/workspace/type'; +import type { WorkspaceFlavour } from '@affine/workspace/type'; +import { assertExists } from '@blocksuite/global/utils'; +import { z } from 'zod'; + +const channelMessageSchema = z.object({ + ws_list: z.array(workspaceSchema), + ws_details: z.record(workspaceDetailSchema), + metadata: z.record( + z.object({ + avatar: z.string(), + name: z.string(), + }) + ), +}); + +type ChannelMessage = z.infer; + +export function createAffineGlobalChannel( + crud: WorkspaceCRUD +) { + let client: WebsocketClient | null; + + async function handleMessage(channelMessage: ChannelMessage) { + const parseResult = channelMessageSchema.safeParse(channelMessage); + if (!parseResult.success) { + console.error( + 'channelMessageSchema.safeParse(channelMessage) failed', + parseResult + ); + } + const { ws_details } = channelMessage; + const currentWorkspaces = await crud.list(); + for (const [id] of Object.entries(ws_details)) { + const workspaceIndex = currentWorkspaces.findIndex( + workspace => workspace.id === id + ); + + // If the workspace is not in the current workspace list, remove it + if (workspaceIndex === -1) { + jotaiStore.set(jotaiWorkspacesAtom, workspaces => { + const idx = workspaces.findIndex(workspace => workspace.id === id); + workspaces.splice(idx, 1); + return [...workspaces]; + }); + } + } + } + + return { + connect: () => { + client = new WebsocketClient( + `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ + window.location.host + }/api/global/sync` + ); + client.connect(handleMessage); + }, + disconnect: () => { + assertExists(client, 'client is null'); + client.disconnect(); + client = null; + }, + }; +} diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts new file mode 100644 index 0000000000..6ce7776136 --- /dev/null +++ b/packages/workspace/src/atom.ts @@ -0,0 +1,18 @@ +import type { WorkspaceFlavour } from '@affine/workspace/type'; +import { createStore } from 'jotai/index'; +import { atomWithStorage } from 'jotai/utils'; + +export type JotaiWorkspace = { + id: string; + flavour: WorkspaceFlavour; +}; + +// root primitive atom that stores the list of workspaces which could be used in the app +// if a workspace is not in this list, it should not be used in the app +export const jotaiWorkspacesAtom = atomWithStorage( + 'jotai-workspaces', + [] +); + +// global jotai store, which is used to store all the atoms +export const jotaiStore = createStore(); diff --git a/packages/workspace/src/type.ts b/packages/workspace/src/type.ts index afbad15304..b8836b9759 100644 --- a/packages/workspace/src/type.ts +++ b/packages/workspace/src/type.ts @@ -1,6 +1,57 @@ +import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { FC } from 'react'; +export type BaseProvider = { + flavour: string; + // if this is true, we will connect the provider on the background + background: boolean; + connect: () => void; + disconnect: () => void; + // cleanup data when workspace is removed + cleanup: () => void; +}; + +export interface BackgroundProvider extends BaseProvider { + background: true; + callbacks: Set<() => void>; +} + +export interface AffineDownloadProvider extends BaseProvider { + flavour: 'affine-download'; +} + +export interface BroadCastChannelProvider extends BaseProvider { + flavour: 'broadcast-channel'; +} + +export interface LocalIndexedDBProvider extends BackgroundProvider { + flavour: 'local-indexeddb'; +} + +export interface AffineWebSocketProvider extends BaseProvider { + flavour: 'affine-websocket'; +} + +export type Provider = + | LocalIndexedDBProvider + | AffineWebSocketProvider + | BroadCastChannelProvider; + +export interface AffineWorkspace extends RemoteWorkspace { + flavour: WorkspaceFlavour.AFFINE; + // empty + blockSuiteWorkspace: BlockSuiteWorkspace; + providers: Provider[]; +} + +export interface LocalWorkspace { + flavour: WorkspaceFlavour.LOCAL; + id: string; + blockSuiteWorkspace: BlockSuiteWorkspace; + providers: Provider[]; +} + export const enum LoadPriority { HIGH = 1, MEDIUM = 2, @@ -11,6 +62,7 @@ export const enum WorkspaceFlavour { AFFINE = 'affine', LOCAL = 'local', } + export const settingPanel = { General: 'general', Collaboration: 'collaboration', @@ -21,8 +73,11 @@ export const settingPanel = { export const settingPanelValues = [...Object.values(settingPanel)] as const; export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel]; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface WorkspaceRegistry {} +// built-in workspaces +export interface WorkspaceRegistry { + [WorkspaceFlavour.AFFINE]: AffineWorkspace; + [WorkspaceFlavour.LOCAL]: LocalWorkspace; +} export interface WorkspaceCRUD { create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise;