feat: add affine global channel (#1762)

This commit is contained in:
Himself65
2023-03-30 18:21:26 -05:00
committed by GitHub
parent 3fa7d17dca
commit bb1224f9ee
38 changed files with 358 additions and 162 deletions
+5 -17
View File
@@ -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<string | null>(null);
export const currentPageIdAtom = atom<string | null>(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<JotaiWorkspace[]>(
'jotai-workspaces',
[]
);
export const workspacesAtom = atom<Promise<RemWorkspace[]>>(async get => {
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
@@ -62,7 +50,7 @@ export const workspacesAtom = atom<Promise<RemWorkspace[]>>(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' };
+2 -1
View File
@@ -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,
@@ -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';
@@ -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 {
+5 -5
View File
@@ -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';
@@ -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';
@@ -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';
@@ -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 }) => {
@@ -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<AvatarProps> = memo<AvatarProps>(function Avatar({
export type WorkspaceUnitAvatarProps = {
size?: number;
workspace: RemWorkspace | null;
workspace: AllWorkspace | null;
style?: React.CSSProperties;
};
@@ -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<WorkspaceTypeProps> = ({ workspace }) => {
@@ -58,9 +58,9 @@ const WorkspaceType: React.FC<WorkspaceTypeProps> = ({ 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<WorkspaceCardProps> = ({
@@ -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;
@@ -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[];
}) => {
@@ -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;
};
@@ -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;
+3 -6
View File
@@ -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,
@@ -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() {
@@ -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) {
@@ -7,9 +7,9 @@ import {
currentWorkspaceIdAtom,
workspacesAtom,
} from '../../atoms';
import type { RemWorkspace } from '../../shared';
import type { AllWorkspace } from '../../shared';
export const currentWorkspaceAtom = atom<Promise<RemWorkspace | null>>(
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>(
async get => {
const id = get(currentWorkspaceIdAtom);
const workspaces = await get(workspacesAtom);
@@ -23,7 +23,7 @@ export const lastWorkspaceIdAtom = atomWithStorage<string | null>(
);
export function useCurrentWorkspace(): [
RemWorkspace | null,
AllWorkspace | null,
(id: string | null) => void
] {
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
@@ -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() {
@@ -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) {
@@ -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';
/**
+5 -3
View File
@@ -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);
}
+24 -3
View File
@@ -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<React.PropsWithChildren> =
function WorkspacesSuspense({ children }) {
const { i18n } = useTranslation();
@@ -105,6 +111,7 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
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<React.PropsWithChildren> =
set([...items]);
logger.info('mount first data:', items);
}
fetch();
return () => {
controller.abort();
@@ -128,6 +136,19 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
};
}, [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 (
<NoSsr>
{/* fixme(himself65): don't re-render whole modals */}
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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');
@@ -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();
+3 -2
View File
@@ -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];
+1 -1
View File
@@ -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';
-8
View File
@@ -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 extends WorkspaceFlavour> {
flavour: Flavour;
// Plugin will be loaded according to the priority
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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 = '/';
+2 -53
View File
@@ -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<P = Record<string, unknown>, IP = P> = NextPage<
P,
+1
View File
@@ -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",
+23 -10
View File
@@ -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<typeof workspaceSchema>;
export const workspaceDetailSchema = z.object({
...workspaceSchema.shape,
owner: userSchema,
member_count: z.number(),
});
export type WorkspaceDetail = z.infer<typeof workspaceDetailSchema>;
export interface Permission {
id: string;
+93
View File
@@ -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);
}
}
}
});
}
}
+71
View File
@@ -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<typeof channelMessageSchema>;
export function createAffineGlobalChannel(
crud: WorkspaceCRUD<WorkspaceFlavour.AFFINE>
) {
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;
},
};
}
+18
View File
@@ -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<JotaiWorkspace[]>(
'jotai-workspaces',
[]
);
// global jotai store, which is used to store all the atoms
export const jotaiStore = createStore();
+57 -2
View File
@@ -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<Flavour extends keyof WorkspaceRegistry> {
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;