mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 09:51:35 +03:00
refactor: remove package @affine/datacenter
(#1705)
This commit is contained in:
parent
021bf6534b
commit
ed29c5fbd9
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -5,7 +5,6 @@
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"cSpell.words": [
|
||||
"blocksuite",
|
||||
"datacenter",
|
||||
"livedemo",
|
||||
"yarn",
|
||||
"jwst",
|
||||
|
@ -68,7 +68,6 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
'@affine/component',
|
||||
'@affine/datacenter',
|
||||
'@affine/i18n',
|
||||
'@affine/debug',
|
||||
'@affine/env',
|
||||
|
@ -10,7 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/datacenter": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { atom } from 'jotai/index';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export const publicWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
||||
@ -11,7 +12,7 @@ export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
||||
if (!workspaceId) {
|
||||
throw new Error('No workspace id');
|
||||
}
|
||||
const binary = await apis.downloadWorkspace(workspaceId, true);
|
||||
const binary = await affineApis.downloadWorkspace(workspaceId, true);
|
||||
// fixme: this is a hack
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const prefixUrl = params.get('prefixUrl')
|
||||
@ -21,7 +22,9 @@ export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
||||
workspaceId,
|
||||
(k: string) =>
|
||||
// fixme: token could be expired
|
||||
({ api: `${prefixUrl}api/workspace`, token: apis.auth.token }[k])
|
||||
({ api: `${prefixUrl}api/workspace`, token: getLoginStorage()?.token }[
|
||||
k
|
||||
])
|
||||
);
|
||||
BlockSuiteWorkspace.Y.applyUpdate(
|
||||
blockSuiteWorkspace.doc,
|
||||
|
@ -2,7 +2,7 @@ import { assertExists } from '@blocksuite/store';
|
||||
|
||||
import type { AffineDownloadProvider } from '../../../shared';
|
||||
import { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { apis } from '../../../shared/apis';
|
||||
import { affineApis } from '../../../shared/apis';
|
||||
import { providerLogger } from '../../logger';
|
||||
|
||||
const hashMap = new Map<string, ArrayBuffer>();
|
||||
@ -25,7 +25,7 @@ export const createAffineDownloadProvider = (
|
||||
);
|
||||
return;
|
||||
}
|
||||
apis.downloadWorkspace(id, false).then(binary => {
|
||||
affineApis.downloadWorkspace(id, false).then(binary => {
|
||||
hashMap.set(id, binary);
|
||||
providerLogger.debug('applyUpdate');
|
||||
BlockSuiteWorkspace.Y.applyUpdate(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { WebsocketProvider } from '@affine/datacenter';
|
||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { WebsocketProvider } from '@affine/workspace/affine/sync';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||
|
||||
@ -7,7 +8,6 @@ import type {
|
||||
BlockSuiteWorkspace,
|
||||
LocalIndexedDBProvider,
|
||||
} from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { providerLogger } from '../logger';
|
||||
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
||||
|
||||
@ -32,7 +32,7 @@ const createAffineWebSocketProvider = (
|
||||
blockSuiteWorkspace.id,
|
||||
blockSuiteWorkspace.doc,
|
||||
{
|
||||
params: { token: apis.auth.refresh },
|
||||
params: { token: getLoginStorage()?.token ?? '' },
|
||||
// @ts-expect-error ignore the type
|
||||
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
||||
// we maintain broadcast channel by ourselves
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RequestError } from '@affine/datacenter';
|
||||
import { RequestError } from '@affine/workspace/affine/api';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type { ErrorInfo } from 'react';
|
||||
import type React from 'react';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||
import { PermissionType } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { PermissionType } from '@affine/workspace/affine/api';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import {
|
||||
getLoginStorage,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
@ -10,13 +15,13 @@ import { assertEquals, assertExists } from '@blocksuite/store';
|
||||
import { useRouter } from 'next/router';
|
||||
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 { apis } from '../../../../shared/apis';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
|
||||
const IconWrapper = styled('div')(({ theme }) => {
|
||||
@ -112,8 +117,13 @@ export const SyncUser = () => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConform={async () => {
|
||||
if (!apis.auth.isLogin) {
|
||||
await apis.signInWithGoogle();
|
||||
if (!getLoginStorage()) {
|
||||
const response = await affineAuth.generateToken(
|
||||
SignMethod.Google
|
||||
);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
}
|
||||
router.reload();
|
||||
return;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { IconButton } from '@affine/component';
|
||||
import { Tooltip } from '@affine/component';
|
||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type React from 'react';
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { MessageCode } from '@affine/datacenter';
|
||||
import { messages } from '@affine/datacenter';
|
||||
import { MessageCode, Messages } from '@affine/env/constant';
|
||||
import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login';
|
||||
import type React from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { affineAuth } from '../../../hooks/affine/use-affine-log-in';
|
||||
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||
import { apis } from '../../../shared/apis';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
declare global {
|
||||
@ -33,9 +33,12 @@ export const MessageCenter: React.FC = memo(function MessageCenter() {
|
||||
event.detail.code === MessageCode.loginError)
|
||||
) {
|
||||
setPopup(true);
|
||||
apis
|
||||
.signInWithGoogle()
|
||||
.then(() => {
|
||||
affineAuth
|
||||
.generateToken(SignMethod.Google)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
}
|
||||
setPopup(false);
|
||||
})
|
||||
.catch(() => {
|
||||
@ -43,7 +46,7 @@ export const MessageCenter: React.FC = memo(function MessageCenter() {
|
||||
onLogout();
|
||||
});
|
||||
} else {
|
||||
toast(messages[event.detail.code].message);
|
||||
toast(Messages[event.detail.code].message);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PermissionType } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { PermissionType } from '@affine/workspace/affine/api';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
|
@ -4,8 +4,8 @@ import {
|
||||
ModalWrapper,
|
||||
Tooltip,
|
||||
} from '@affine/component';
|
||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
|
||||
|
||||
import type { RemWorkspace } from '../../../shared';
|
||||
|
@ -1,26 +1,30 @@
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
createAffineAuth,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { apis } from '../../shared/apis';
|
||||
import { toast } from '../../utils';
|
||||
|
||||
export const affineAuth = createAffineAuth();
|
||||
|
||||
export function useAffineLogIn() {
|
||||
const router = useRouter();
|
||||
const setUser = useSetAtom(currentAffineUserAtom);
|
||||
return useCallback(async () => {
|
||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
apis.auth.setLogin(response);
|
||||
const user = parseIdToken(response.token);
|
||||
setUser(user);
|
||||
router.reload();
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
}, [router]);
|
||||
}, [router, setUser]);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import { clearLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@ -6,13 +7,12 @@ import { useCallback } from 'react';
|
||||
|
||||
import { jotaiWorkspacesAtom } from '../../atoms';
|
||||
import { WorkspacePlugins } from '../../plugins';
|
||||
import { apis } from '../../shared/apis';
|
||||
|
||||
export function useAffineLogOut() {
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
const router = useRouter();
|
||||
const setCurrentUser = useSetAtom(currentAffineUserAtom);
|
||||
return useCallback(() => {
|
||||
apis.auth.clear();
|
||||
set(workspaces =>
|
||||
workspaces.filter(
|
||||
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
|
||||
@ -20,6 +20,7 @@ export function useAffineLogOut() {
|
||||
);
|
||||
WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.();
|
||||
clearLoginStorage();
|
||||
setCurrentUser(null);
|
||||
router.reload();
|
||||
}, [router, set]);
|
||||
}, [router, set, setCurrentUser]);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
} from '@affine/workspace/affine/login';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineAuth } from './use-affine-log-in';
|
||||
|
||||
const logger = new DebugLogger('auth-token');
|
||||
@ -22,7 +21,6 @@ const revalidate = async () => {
|
||||
const response = await affineAuth.refreshToken(storage);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
apis.auth.setLogin(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PermissionType } from '@affine/datacenter';
|
||||
import { PermissionType } from '@affine/workspace/affine/api';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../shared';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { Member } from '@affine/datacenter';
|
||||
import type { Member } from '@affine/workspace/affine/api';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data, mutate } = useSWR<Member[]>(
|
||||
@ -15,7 +15,7 @@ export function useMembers(workspaceId: string) {
|
||||
|
||||
const inviteMember = useCallback(
|
||||
async (email: string) => {
|
||||
await apis.inviteMember({
|
||||
await affineApis.inviteMember({
|
||||
id: workspaceId,
|
||||
email,
|
||||
});
|
||||
@ -26,8 +26,7 @@ export function useMembers(workspaceId: string) {
|
||||
|
||||
const removeMember = useCallback(
|
||||
async (permissionId: number) => {
|
||||
// fixme: what about the workspaceId?
|
||||
await apis.removeMember({
|
||||
await affineApis.removeMember({
|
||||
permissionId,
|
||||
});
|
||||
return mutate();
|
||||
|
@ -4,13 +4,13 @@ import useSWR from 'swr';
|
||||
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import type { AffineWorkspace } from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||
const { mutate } = useSWR(QueryKey.getWorkspaces);
|
||||
return useCallback(
|
||||
async (isPublish: boolean) => {
|
||||
await apis.updateWorkspace({
|
||||
await affineApis.updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: isPublish,
|
||||
});
|
||||
|
@ -1,11 +1,7 @@
|
||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
export function useCurrentUser(): AccessTokenMessage | null {
|
||||
const { data } = useSWR<AccessTokenMessage | null>(QueryKey.getUser, {
|
||||
fallbackData: null,
|
||||
});
|
||||
return data ?? null;
|
||||
return useAtomValue(currentAffineUserAtom);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
import { Button } from '@affine/component';
|
||||
import type { Permission } from '@affine/datacenter';
|
||||
import type { Permission } from '@affine/workspace/affine/api';
|
||||
import {
|
||||
SucessfulDuotoneIcon,
|
||||
UnsucessfulDuotoneIcon,
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
getLoginStorage,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type';
|
||||
import {
|
||||
settingPanel,
|
||||
@ -7,7 +14,7 @@ import {
|
||||
} from '@affine/workspace/type';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -16,6 +23,7 @@ import React, { useCallback, useEffect } from 'react';
|
||||
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { affineAuth } from '../../../hooks/affine/use-affine-log-in';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||
import { useTransformWorkspace } from '../../../hooks/use-transform-workspace';
|
||||
@ -23,7 +31,6 @@ import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts';
|
||||
import { WorkspacePlugins } from '../../../plugins';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
import { apis } from '../../../shared/apis';
|
||||
|
||||
const settingPanelAtom = atomWithStorage<SettingPanel>(
|
||||
'workspaceId',
|
||||
@ -101,15 +108,20 @@ const SettingPage: NextPageWithLayout = () => {
|
||||
return helper.deleteWorkspace(workspaceId);
|
||||
}, [currentWorkspace, helper]);
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
const setUser = useSetAtom(currentAffineUserAtom);
|
||||
const onTransformWorkspace = useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
const needRefresh = to === WorkspaceFlavour.AFFINE && !apis.auth.isLogin;
|
||||
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
|
||||
if (needRefresh) {
|
||||
await apis.signInWithGoogle();
|
||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
setUser(parseIdToken(response.token));
|
||||
}
|
||||
}
|
||||
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||
await router.replace({
|
||||
@ -119,11 +131,8 @@ const SettingPage: NextPageWithLayout = () => {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
if (needRefresh) {
|
||||
router.reload();
|
||||
}
|
||||
},
|
||||
[router, transformWorkspace]
|
||||
[router, setUser, transformWorkspace]
|
||||
);
|
||||
if (!router.isReady) {
|
||||
return <PageLoading />;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
@ -6,7 +7,7 @@ import { jotaiStore, workspacesAtom } from '../../atoms';
|
||||
import { createAffineProviders } from '../../blocksuite';
|
||||
import { Unreachable } from '../../components/affine/affine-error-eoundary';
|
||||
import type { AffineWorkspace } from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
||||
|
||||
@ -17,24 +18,21 @@ export const fetcher = async (
|
||||
| [Query, string]
|
||||
| [Query, string, string]
|
||||
) => {
|
||||
if (query === QueryKey.getUser) {
|
||||
return apis.auth.user ?? null;
|
||||
}
|
||||
if (Array.isArray(query)) {
|
||||
if (query[0] === QueryKey.downloadWorkspace) {
|
||||
if (typeof query[2] !== 'boolean') {
|
||||
throw new Unreachable();
|
||||
}
|
||||
return apis.downloadWorkspace(query[1], query[2]);
|
||||
return affineApis.downloadWorkspace(query[1], query[2]);
|
||||
} else if (query[0] === QueryKey.getMembers) {
|
||||
return apis.getWorkspaceMembers({
|
||||
return affineApis.getWorkspaceMembers({
|
||||
id: query[1],
|
||||
});
|
||||
} else if (query[0] === QueryKey.getUserByEmail) {
|
||||
if (typeof query[2] !== 'string') {
|
||||
throw new Unreachable();
|
||||
}
|
||||
return apis.getUserByEmail({
|
||||
return affineApis.getUserByEmail({
|
||||
workspace_id: query[1],
|
||||
email: query[2],
|
||||
});
|
||||
@ -57,19 +55,19 @@ export const fetcher = async (
|
||||
if (typeof invitingCode !== 'string') {
|
||||
throw new TypeError('invitingCode must be a string');
|
||||
}
|
||||
return apis.acceptInviting({
|
||||
return affineApis.acceptInviting({
|
||||
invitingCode,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (query === QueryKey.getWorkspaces) {
|
||||
return apis.getWorkspaces().then(workspaces => {
|
||||
return affineApis.getWorkspaces().then(workspaces => {
|
||||
return workspaces.map(workspace => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspace.id,
|
||||
(k: string) =>
|
||||
// fixme: token could be expired
|
||||
({ api: '/api/workspace', token: apis.auth.token }[k])
|
||||
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||
);
|
||||
const remWorkspace: AffineWorkspace = {
|
||||
...workspace,
|
||||
@ -81,14 +79,13 @@ export const fetcher = async (
|
||||
});
|
||||
});
|
||||
}
|
||||
return (apis as any)[query]();
|
||||
return (affineApis as any)[query]();
|
||||
}
|
||||
};
|
||||
|
||||
export const QueryKey = {
|
||||
acceptInvite: 'acceptInvite',
|
||||
getImage: 'getImage',
|
||||
getUser: 'getUser',
|
||||
getWorkspaces: 'getWorkspaces',
|
||||
downloadWorkspace: 'downloadWorkspace',
|
||||
getMembers: 'getMembers',
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { createJSONStorage } from 'jotai/utils';
|
||||
@ -12,8 +13,8 @@ import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page
|
||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||
import type { AffineWorkspace } from '../../shared';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { apis, clientAuth } from '../../shared/apis';
|
||||
import { initPage } from '../../utils/blocksuite';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
import { initPage } from '../../utils';
|
||||
import type { WorkspacePlugin } from '..';
|
||||
import { QueryKey } from './fetcher';
|
||||
|
||||
@ -39,7 +40,7 @@ const getPersistenceAllWorkspace = () => {
|
||||
item.id,
|
||||
(k: string) =>
|
||||
// fixme: token could be expired
|
||||
({ api: '/api/workspace', token: apis.auth.token }[k])
|
||||
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||
);
|
||||
const affineWorkspace: AffineWorkspace = {
|
||||
...item,
|
||||
@ -65,7 +66,9 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
||||
blockSuiteWorkspace.doc
|
||||
);
|
||||
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
|
||||
const { id } = await affineApis.createWorkspace(
|
||||
new Blob([binary.buffer])
|
||||
);
|
||||
// fixme: syncing images
|
||||
const newWorkspaceId = id;
|
||||
|
||||
@ -77,12 +80,7 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
||||
const url = await blobs.get(id);
|
||||
if (url) {
|
||||
const blob = await fetch(url).then(res => res.blob());
|
||||
await clientAuth.put(`api/workspace/${newWorkspaceId}/blob`, {
|
||||
body: blob,
|
||||
headers: {
|
||||
'Content-Type': blob.type,
|
||||
},
|
||||
});
|
||||
await affineApis.uploadBlob(newWorkspaceId, blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,14 +101,14 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
||||
items.filter(item => item.id !== workspace.id)
|
||||
);
|
||||
}
|
||||
await apis.deleteWorkspace({
|
||||
await affineApis.deleteWorkspace({
|
||||
id: workspace.id,
|
||||
});
|
||||
await mutate(matcher => matcher === QueryKey.getWorkspaces);
|
||||
},
|
||||
get: async workspaceId => {
|
||||
try {
|
||||
if (!apis.auth.isLogin) {
|
||||
if (!getLoginStorage()) {
|
||||
const workspaces = getPersistenceAllWorkspace();
|
||||
return (
|
||||
workspaces.find(workspace => workspace.id === workspaceId) ?? null
|
||||
@ -130,69 +128,65 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
||||
list: async () => {
|
||||
const allWorkspaces = getPersistenceAllWorkspace();
|
||||
try {
|
||||
if (apis.auth.isLogin) {
|
||||
const workspaces = await apis.getWorkspaces().then(workspaces => {
|
||||
return workspaces.map(workspace => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspace.id,
|
||||
(k: string) =>
|
||||
// fixme: token could be expired
|
||||
({ api: '/api/workspace', token: apis.auth.token }[k])
|
||||
);
|
||||
const dump = workspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
});
|
||||
const old = storage.getItem(kAffineLocal);
|
||||
if (
|
||||
Array.isArray(old) &&
|
||||
old.every(item => schema.safeParse(item).success)
|
||||
) {
|
||||
const data = [...dump];
|
||||
old.forEach((item: z.infer<typeof schema>) => {
|
||||
const has = dump.find(dump => dump.id === item.id);
|
||||
if (!has) {
|
||||
data.push(item);
|
||||
}
|
||||
});
|
||||
storage.setItem(kAffineLocal, [...data]);
|
||||
}
|
||||
|
||||
const affineWorkspace: AffineWorkspace = {
|
||||
...workspace,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
providers: [...createAffineProviders(blockSuiteWorkspace)],
|
||||
};
|
||||
return affineWorkspace;
|
||||
});
|
||||
});
|
||||
workspaces.forEach(workspace => {
|
||||
const idx = allWorkspaces.findIndex(
|
||||
({ id }) => id === workspace.id
|
||||
const workspaces = await affineApis.getWorkspaces().then(workspaces => {
|
||||
return workspaces.map(workspace => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
workspace.id,
|
||||
(k: string) =>
|
||||
// fixme: token could be expired
|
||||
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||
);
|
||||
if (idx !== -1) {
|
||||
allWorkspaces.splice(idx, 1, workspace);
|
||||
} else {
|
||||
allWorkspaces.push(workspace);
|
||||
const dump = workspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
});
|
||||
const old = storage.getItem(kAffineLocal);
|
||||
if (
|
||||
Array.isArray(old) &&
|
||||
old.every(item => schema.safeParse(item).success)
|
||||
) {
|
||||
const data = [...dump];
|
||||
old.forEach((item: z.infer<typeof schema>) => {
|
||||
const has = dump.find(dump => dump.id === item.id);
|
||||
if (!has) {
|
||||
data.push(item);
|
||||
}
|
||||
});
|
||||
storage.setItem(kAffineLocal, [...data]);
|
||||
}
|
||||
});
|
||||
|
||||
// only save data when login in
|
||||
const dump = allWorkspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
const affineWorkspace: AffineWorkspace = {
|
||||
...workspace,
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
providers: [...createAffineProviders(blockSuiteWorkspace)],
|
||||
};
|
||||
return affineWorkspace;
|
||||
});
|
||||
storage.setItem(kAffineLocal, [...dump]);
|
||||
}
|
||||
});
|
||||
workspaces.forEach(workspace => {
|
||||
const idx = allWorkspaces.findIndex(({ id }) => id === workspace.id);
|
||||
if (idx !== -1) {
|
||||
allWorkspaces.splice(idx, 1, workspace);
|
||||
} else {
|
||||
allWorkspaces.push(workspace);
|
||||
}
|
||||
});
|
||||
|
||||
// only save data when login in
|
||||
const dump = allWorkspaces.map(workspace => {
|
||||
return {
|
||||
id: workspace.id,
|
||||
type: workspace.type,
|
||||
public: workspace.public,
|
||||
permission: workspace.permission,
|
||||
} satisfies z.infer<typeof schema>;
|
||||
});
|
||||
storage.setItem(kAffineLocal, [...dump]);
|
||||
} catch (e) {
|
||||
console.error('fetch affine workspaces failed', e);
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
import {
|
||||
createAuthClient,
|
||||
createBareClient,
|
||||
getApis,
|
||||
GoogleAuth,
|
||||
} from '@affine/datacenter';
|
||||
import { config } from '@affine/env';
|
||||
import {
|
||||
createUserApis,
|
||||
createWorkspaceApis,
|
||||
} from '@affine/workspace/affine/api';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import type { LoginResponse } from '@affine/workspace/affine/login';
|
||||
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
|
||||
|
||||
import { isValidIPAddress } from '../utils/is-valid-ip-address';
|
||||
import { jotaiStore } from '../atoms';
|
||||
import { isValidIPAddress } from '../utils';
|
||||
|
||||
let prefixUrl = '/';
|
||||
if (typeof window === 'undefined') {
|
||||
@ -31,32 +29,27 @@ if (typeof window === 'undefined') {
|
||||
params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string);
|
||||
}
|
||||
|
||||
const affineApis = {} as ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>;
|
||||
Object.assign(affineApis, createUserApis(prefixUrl));
|
||||
Object.assign(affineApis, createWorkspaceApis(prefixUrl));
|
||||
|
||||
if (!globalThis.AFFINE_APIS) {
|
||||
globalThis.AFFINE_APIS = affineApis;
|
||||
globalThis.setLogin = (response: LoginResponse) => {
|
||||
jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token));
|
||||
setLoginStorage(response);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var affineApis:
|
||||
var setLogin: typeof setLoginStorage;
|
||||
// eslint-disable-next-line no-var
|
||||
var AFFINE_APIS:
|
||||
| undefined
|
||||
| (ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>);
|
||||
}
|
||||
|
||||
const affineApis = {} as ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>;
|
||||
Object.assign(affineApis, createUserApis(prefixUrl));
|
||||
Object.assign(affineApis, createWorkspaceApis(prefixUrl));
|
||||
if (!globalThis.affineApis) {
|
||||
globalThis.affineApis = affineApis;
|
||||
}
|
||||
|
||||
const bareAuth = createBareClient(prefixUrl);
|
||||
const googleAuth = new GoogleAuth(bareAuth);
|
||||
export const clientAuth = createAuthClient(bareAuth, googleAuth);
|
||||
export const apis = getApis(bareAuth, clientAuth, googleAuth);
|
||||
|
||||
if (!globalThis.AFFINE_APIS) {
|
||||
globalThis.AFFINE_APIS = apis;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var AFFINE_APIS: ReturnType<typeof getApis>;
|
||||
}
|
||||
export { affineApis };
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Workspace as RemoteWorkspace } from '@affine/datacenter';
|
||||
import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api';
|
||||
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { NextPage } from 'next';
|
||||
|
@ -4,9 +4,5 @@
|
||||
const defaultExclude = require('@istanbuljs/schema/default-exclude');
|
||||
|
||||
module.exports = {
|
||||
exclude: [
|
||||
...defaultExclude,
|
||||
// data-center will be removed in the future, we don't need to coverage it
|
||||
'packages/data-center/**',
|
||||
],
|
||||
exclude: [...defaultExclude],
|
||||
};
|
||||
|
4
packages/data-center/.gitignore
vendored
4
packages/data-center/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@affine/datacenter",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "./src/index.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/toeverything/AFFiNE.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fake-indexeddb": "4.0.1",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@blocksuite/blocks": "0.5.0-20230324040005-14417c2",
|
||||
"@blocksuite/global": "0.5.0-20230324040005-14417c2",
|
||||
"@blocksuite/store": "0.5.0-20230324040005-14417c2",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"encoding": "^0.1.13",
|
||||
"firebase": "^9.18.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"js-base64": "^3.7.5",
|
||||
"ky": "^0.33.3",
|
||||
"ky-universal": "^0.11.0",
|
||||
"lib0": "^0.2.69",
|
||||
"lit": "^2.6.1",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.5.50"
|
||||
}
|
||||
}
|
@ -1,454 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
|
||||
import { MessageCenter } from './message';
|
||||
import { AffineProvider } from './provider';
|
||||
import type {
|
||||
BaseProvider,
|
||||
CreateWorkspaceInfoParams,
|
||||
UpdateWorkspaceMetaParams,
|
||||
} from './provider/base';
|
||||
import { LocalProvider } from './provider/local';
|
||||
import type { Message } from './types';
|
||||
import { createBlocksuiteWorkspace } from './utils';
|
||||
import { WorkspaceUnit } from './workspace-unit';
|
||||
import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection';
|
||||
import { WorkspaceUnitCollection } from './workspace-unit-collection';
|
||||
|
||||
/**
|
||||
* @class DataCenter
|
||||
* @classdesc Data center is made for managing different providers for business
|
||||
*/
|
||||
|
||||
export class DataCenter {
|
||||
private readonly _workspaceUnitCollection = new WorkspaceUnitCollection();
|
||||
private readonly _logger = new DebugLogger('datacenter');
|
||||
private _workspaceInstances: Map<string, BlocksuiteWorkspace> = new Map();
|
||||
private _messageCenter = MessageCenter.getInstance();
|
||||
|
||||
/**
|
||||
* A mainProvider must exist as the only data trustworthy source.
|
||||
*/
|
||||
private _mainProvider?: BaseProvider;
|
||||
providerMap: Map<string, BaseProvider> = new Map();
|
||||
|
||||
static initEmpty() {
|
||||
return new DataCenter();
|
||||
}
|
||||
|
||||
static async init(exclude: 'affine'[] = []): Promise<DataCenter> {
|
||||
const dc = new DataCenter();
|
||||
const getInitParams = () => {
|
||||
return {
|
||||
logger: dc._logger,
|
||||
workspaces: dc._workspaceUnitCollection.createScope(),
|
||||
messageCenter: dc._messageCenter,
|
||||
};
|
||||
};
|
||||
// TODO: switch different provider
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.CLIENT_APP &&
|
||||
typeof window.__TAURI_IPC__ === 'function'
|
||||
) {
|
||||
const { TauriIPCProvider } = await import('./provider/tauri-ipc');
|
||||
await dc.registerProvider(new TauriIPCProvider(getInitParams()));
|
||||
} else {
|
||||
await dc.registerProvider(new LocalProvider(getInitParams()));
|
||||
}
|
||||
if (!exclude.includes('affine')) {
|
||||
await dc.registerProvider(new AffineProvider(getInitParams()));
|
||||
}
|
||||
|
||||
for (const provider of dc.providerMap.values()) {
|
||||
await provider.loadWorkspaces();
|
||||
}
|
||||
|
||||
return dc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register provider.
|
||||
* We will automatically set the first provider to default provider.
|
||||
*/
|
||||
async registerProvider(provider: BaseProvider) {
|
||||
if (!this._mainProvider) {
|
||||
this._mainProvider = provider;
|
||||
}
|
||||
|
||||
await provider.init();
|
||||
this.providerMap.set(provider.id, provider);
|
||||
}
|
||||
|
||||
setMainProvider(providerId: string) {
|
||||
this._mainProvider = this.providerMap.get(providerId);
|
||||
}
|
||||
|
||||
get providers() {
|
||||
return Array.from(this.providerMap.values());
|
||||
}
|
||||
|
||||
public get workspaces() {
|
||||
return this._workspaceUnitCollection.workspaces;
|
||||
}
|
||||
|
||||
public async refreshWorkspaces() {
|
||||
return Promise.allSettled(
|
||||
Object.values(this.providerMap).map(provider => provider.loadWorkspaces())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* create new workspace , new workspace is a local workspace
|
||||
* @param {string} name workspace name
|
||||
* @returns {Promise<Workspace>}
|
||||
*/
|
||||
public async createWorkspace(params: CreateWorkspaceInfoParams) {
|
||||
assert(
|
||||
this._mainProvider,
|
||||
'There is no provider. You should add provider first.'
|
||||
);
|
||||
|
||||
const workspaceUnit = await this._mainProvider.createWorkspace(params);
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete workspace by id
|
||||
* @param {string} workspaceId workspace id
|
||||
*/
|
||||
public async deleteWorkspace(workspaceId: string) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
assert(provider, `Workspace exists, but we couldn't find its provider.`);
|
||||
await provider.deleteWorkspace(workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* get a new workspace only has room id
|
||||
* @param {string} workspaceId workspace id
|
||||
*/
|
||||
private _getBlocksuiteWorkspace(workspaceId: string) {
|
||||
// const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
// assert(workspaceInfo, 'Workspace not found');
|
||||
return (
|
||||
// this._workspaceInstances.get(workspaceId) ||
|
||||
createBlocksuiteWorkspace({ id: workspaceId })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* login to all providers, it will default run all auth ,
|
||||
* maybe need a params to control which provider to auth
|
||||
*/
|
||||
public async login(providerId = 'affine') {
|
||||
const provider = this.providerMap.get(providerId);
|
||||
assert(provider, `provide '${providerId}' is not registered`);
|
||||
await provider.auth();
|
||||
provider.loadWorkspaces();
|
||||
}
|
||||
|
||||
/**
|
||||
* logout from all providers
|
||||
*/
|
||||
public async logout(providerId = 'affine') {
|
||||
const provider = this.providerMap.get(providerId);
|
||||
assert(provider, `provide '${providerId}' is not registered`);
|
||||
await provider.logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* load workspace instance by id
|
||||
* @param {string} workspaceId workspace id
|
||||
* @returns {Promise<WorkspaceUnit>}
|
||||
*/
|
||||
public async loadWorkspace(
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceUnit | null> {
|
||||
const workspaceUnit = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceUnit, 'Workspace not found');
|
||||
const currentProvider = this.providerMap.get(workspaceUnit.provider);
|
||||
if (currentProvider) {
|
||||
currentProvider.closeWorkspace(workspaceId);
|
||||
}
|
||||
const provider = this.providerMap.get(workspaceUnit.provider);
|
||||
assert(provider, `provide '${workspaceUnit.provider}' is not registered`);
|
||||
this._logger.debug(
|
||||
`Loading ${workspaceUnit.provider} workspace: `,
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const workspace = this._getBlocksuiteWorkspace(workspaceId);
|
||||
this._workspaceInstances.set(workspaceId, workspace);
|
||||
await provider.warpWorkspace(workspace);
|
||||
this._workspaceUnitCollection.workspaces.forEach(workspaceUnit => {
|
||||
const provider = this.providerMap.get(workspaceUnit.provider);
|
||||
assert(provider);
|
||||
provider.closeWorkspace(workspaceUnit.id);
|
||||
});
|
||||
workspaceUnit.setBlocksuiteWorkspace(workspace);
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
public async loadPublicWorkspace(workspaceId: string) {
|
||||
// FIXME: hard code for public workspace
|
||||
const provider = this.providerMap.get('affine');
|
||||
assert(provider);
|
||||
const blocksuiteWorkspace = this._getBlocksuiteWorkspace(workspaceId);
|
||||
await provider.loadPublicWorkspace(blocksuiteWorkspace);
|
||||
|
||||
const workspaceUnitForPublic = new WorkspaceUnit({
|
||||
id: workspaceId,
|
||||
name: blocksuiteWorkspace.meta.name ?? '',
|
||||
avatar: blocksuiteWorkspace.meta.avatar ?? '',
|
||||
owner: undefined,
|
||||
published: true,
|
||||
provider: 'affine',
|
||||
memberCount: 1,
|
||||
syncMode: 'core',
|
||||
});
|
||||
|
||||
workspaceUnitForPublic.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
return workspaceUnitForPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* get user info by provider id
|
||||
* @param {string} providerId the provider name of workspace
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
public async getUserInfo(providerId = 'affine') {
|
||||
// XXX: maybe return all user info
|
||||
const provider = this.providerMap.get(providerId);
|
||||
assert(provider, `provide '${providerId}' is not registered`);
|
||||
return provider.getUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* listen workspaces list change
|
||||
* @param {Function} callback callback function
|
||||
*/
|
||||
public onWorkspacesChange(
|
||||
callback: (workspaces: WorkspaceUnitCollectionChangeEvent) => void,
|
||||
{ immediate = true }: { immediate?: boolean } = {}
|
||||
) {
|
||||
if (immediate) {
|
||||
callback({
|
||||
added: this._workspaceUnitCollection.workspaces,
|
||||
});
|
||||
}
|
||||
this._workspaceUnitCollection.on('change', callback);
|
||||
return () => {
|
||||
this._workspaceUnitCollection.off('change', callback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* change workspaces meta
|
||||
* @param {WorkspaceMeta} workspaceMeta workspace meta
|
||||
* @param {WorkspaceUnit} workspace workspace instance
|
||||
*/
|
||||
public async updateWorkspaceMeta(
|
||||
{ name, avatar }: UpdateWorkspaceMetaParams,
|
||||
workspaceUnit: WorkspaceUnit
|
||||
) {
|
||||
assert(workspaceUnit?.id, 'No workspace to set meta');
|
||||
const workspace = workspaceUnit.blocksuiteWorkspace;
|
||||
assert(workspace);
|
||||
const update: Partial<UpdateWorkspaceMetaParams> = {};
|
||||
if (name) {
|
||||
workspace.meta.setName(name);
|
||||
update.name = name;
|
||||
}
|
||||
if (avatar) {
|
||||
workspace.meta.setAvatar(avatar);
|
||||
update.avatar = avatar;
|
||||
}
|
||||
// may run for change workspace meta
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceUnit.id);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
provider?.updateWorkspaceMeta(workspaceUnit.id, update);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* leave workspace by id
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async leaveWorkspace(workspaceId: string) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
if (provider) {
|
||||
await provider.closeWorkspace(workspaceId);
|
||||
await provider.leaveWorkspace(workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
public async setWorkspacePublish(workspaceId: string, isPublish: boolean) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
if (provider) {
|
||||
await provider.publish(workspaceId, isPublish);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* invite the new member to the workspace
|
||||
* @param {string} workspaceId workspace id
|
||||
* @param {string} email
|
||||
*/
|
||||
public async inviteMember(workspaceId: string, email: string) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
if (provider) {
|
||||
await provider.invite(workspaceId, email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* remove the new member to the workspace
|
||||
*/
|
||||
public async removeMember(workspaceId: string, permissionId: number) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
if (provider) {
|
||||
await provider.removeMember(permissionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get user info by email
|
||||
* @param workspaceId
|
||||
* @param email
|
||||
* @param provider
|
||||
* @returns {Promise<User>} User info
|
||||
*/
|
||||
public async getUserByEmail(
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
provider = 'affine'
|
||||
) {
|
||||
const providerInstance = this.providerMap.get(provider);
|
||||
if (providerInstance) {
|
||||
return await providerInstance.getUserByEmail(workspaceId, email);
|
||||
}
|
||||
}
|
||||
|
||||
public async enableProvider(
|
||||
workspaceUnit: WorkspaceUnit,
|
||||
providerId = 'affine'
|
||||
) {
|
||||
if (workspaceUnit.provider === providerId) {
|
||||
this._logger.error('Workspace provider is same');
|
||||
return;
|
||||
}
|
||||
const provider = this.providerMap.get(providerId);
|
||||
assert(provider);
|
||||
const newWorkspaceUnit = await provider.extendWorkspace(workspaceUnit);
|
||||
|
||||
// Currently we only allow enable one provider, so after enable new provider,
|
||||
// delete the old workspace from its provider.
|
||||
const oldProvider = this.providerMap.get(workspaceUnit.provider);
|
||||
assert(oldProvider);
|
||||
await oldProvider.deleteWorkspace(workspaceUnit.id);
|
||||
|
||||
return newWorkspaceUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable workspace cloud
|
||||
* @param {string} id ID of workspace.
|
||||
*/
|
||||
public async enableWorkspaceCloud(workspaceUnit: WorkspaceUnit) {
|
||||
return this.enableProvider(workspaceUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* clear all workspaces and data
|
||||
*/
|
||||
public async clear() {
|
||||
for (const provider of this.providerMap.values()) {
|
||||
await provider.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a file to import the workspace
|
||||
* @param {File} file file of workspace.
|
||||
*/
|
||||
public async importWorkspace(file: File) {
|
||||
file;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a file ,and export it to local file system
|
||||
* @param {string} id ID of workspace.
|
||||
*/
|
||||
public async exportWorkspace(id: string) {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* get blob url by workspaces id
|
||||
* @param id
|
||||
* @returns {Promise<string | null>} blob url
|
||||
*/
|
||||
async getBlob(
|
||||
workspaceUnit: WorkspaceUnit,
|
||||
id: string
|
||||
): Promise<string | null> {
|
||||
const blob = await workspaceUnit.blocksuiteWorkspace?.blobs;
|
||||
return (await blob?.get(id)) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* up load blob and get a blob url
|
||||
* @param id
|
||||
* @returns {Promise<string | null>} blob url
|
||||
*/
|
||||
async setBlob(workspace: WorkspaceUnit, blob: Blob): Promise<string> {
|
||||
const blobStorage = await workspace.blocksuiteWorkspace?.blobs;
|
||||
return (await blobStorage?.set(blob)) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* get members of a workspace
|
||||
* @param workspaceId
|
||||
*/
|
||||
async getMembers(workspaceId: string) {
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspaceId);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
const provider = this.providerMap.get(workspaceInfo.provider);
|
||||
if (provider) {
|
||||
return await provider.getWorkspaceMembers(workspaceId);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* accept invitation
|
||||
* @param {string} inviteCode
|
||||
* @returns {Promise<Permission | null>} permission
|
||||
*/
|
||||
async acceptInvitation(inviteCode: string, providerStr = 'affine') {
|
||||
const provider = this.providerMap.get(providerStr);
|
||||
if (provider) {
|
||||
return await provider.acceptInvitation(inviteCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMessage(cb: (message: Message) => void) {
|
||||
return this._messageCenter.onMessage(cb);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
CLIENT_APP?: boolean;
|
||||
__editoVersion?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
@ -1,43 +0,0 @@
|
||||
import { DataCenter } from './datacenter';
|
||||
|
||||
const _initializeDataCenter = () => {
|
||||
let _dataCenterInstance: Promise<DataCenter>;
|
||||
|
||||
return () => {
|
||||
if (!_dataCenterInstance) {
|
||||
_dataCenterInstance = DataCenter.init();
|
||||
_dataCenterInstance.then(dc => {
|
||||
try {
|
||||
if (window) {
|
||||
(window as any).dc = dc;
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return dc;
|
||||
});
|
||||
}
|
||||
|
||||
return _dataCenterInstance;
|
||||
};
|
||||
};
|
||||
|
||||
export const getDataCenter = _initializeDataCenter();
|
||||
|
||||
export type { DataCenter };
|
||||
export * from './message';
|
||||
export { messages } from './message/code';
|
||||
export { AffineProvider } from './provider/affine';
|
||||
export * from './provider/affine/apis';
|
||||
export { getAuthorizer, GoogleAuth } from './provider/affine/apis/google';
|
||||
export {
|
||||
createAuthClient,
|
||||
createBareClient,
|
||||
} from './provider/affine/apis/request';
|
||||
export { RequestError } from './provider/affine/apis/request-error';
|
||||
export * from './provider/affine/apis/workspace';
|
||||
export { WebsocketProvider } from './provider/affine/sync';
|
||||
export { IndexedDBProvider } from './provider/local/indexeddb/indexeddb';
|
||||
export * from './types';
|
||||
export { WorkspaceUnit } from './workspace-unit';
|
@ -1,65 +0,0 @@
|
||||
export enum MessageCode {
|
||||
loginError,
|
||||
noPermission,
|
||||
loadListFailed,
|
||||
getDetailFailed,
|
||||
createWorkspaceFailed,
|
||||
getMembersFailed,
|
||||
updateWorkspaceFailed,
|
||||
deleteWorkspaceFailed,
|
||||
inviteMemberFailed,
|
||||
removeMemberFailed,
|
||||
acceptInvitingFailed,
|
||||
getBlobFailed,
|
||||
leaveWorkspaceFailed,
|
||||
downloadWorkspaceFailed,
|
||||
refreshTokenError,
|
||||
}
|
||||
|
||||
export const messages = {
|
||||
[MessageCode.loginError]: {
|
||||
message: 'Login failed',
|
||||
},
|
||||
[MessageCode.noPermission]: {
|
||||
message: 'No permission',
|
||||
},
|
||||
[MessageCode.loadListFailed]: {
|
||||
message: 'Load list failed',
|
||||
},
|
||||
[MessageCode.getDetailFailed]: {
|
||||
message: 'Get detail failed',
|
||||
},
|
||||
[MessageCode.createWorkspaceFailed]: {
|
||||
message: 'Create workspace failed',
|
||||
},
|
||||
[MessageCode.getMembersFailed]: {
|
||||
message: 'Get members failed',
|
||||
},
|
||||
[MessageCode.updateWorkspaceFailed]: {
|
||||
message: 'Update workspace failed',
|
||||
},
|
||||
[MessageCode.deleteWorkspaceFailed]: {
|
||||
message: 'Delete workspace failed',
|
||||
},
|
||||
[MessageCode.inviteMemberFailed]: {
|
||||
message: 'Invite member failed',
|
||||
},
|
||||
[MessageCode.removeMemberFailed]: {
|
||||
message: 'Remove member failed',
|
||||
},
|
||||
[MessageCode.acceptInvitingFailed]: {
|
||||
message: 'Accept inviting failed',
|
||||
},
|
||||
[MessageCode.getBlobFailed]: {
|
||||
message: 'Get blob failed',
|
||||
},
|
||||
[MessageCode.leaveWorkspaceFailed]: {
|
||||
message: 'Leave workspace failed',
|
||||
},
|
||||
[MessageCode.downloadWorkspaceFailed]: {
|
||||
message: 'Download workspace failed',
|
||||
},
|
||||
[MessageCode.refreshTokenError]: {
|
||||
message: 'Refresh token failed',
|
||||
},
|
||||
} as const;
|
@ -1,2 +0,0 @@
|
||||
export { MessageCode } from './code';
|
||||
export { MessageCenter } from './message';
|
@ -1,47 +0,0 @@
|
||||
import { Observable } from 'lib0/observable';
|
||||
|
||||
import type { Message } from '../types';
|
||||
import { MessageCode, messages } from './code';
|
||||
|
||||
export class MessageCenter extends Observable<string> {
|
||||
private _messages: Record<number, Omit<Message, 'provider' | 'code'>> =
|
||||
messages;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static instance: MessageCenter;
|
||||
|
||||
static getInstance() {
|
||||
if (!MessageCenter.instance) {
|
||||
MessageCenter.instance = new MessageCenter();
|
||||
}
|
||||
return MessageCenter.instance;
|
||||
}
|
||||
|
||||
static messageCode = MessageCode;
|
||||
|
||||
public getMessageSender(provider: string) {
|
||||
return this._send.bind(this, provider);
|
||||
}
|
||||
|
||||
private _send(provider: string, messageCode: MessageCode) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('affine-error', {
|
||||
detail: {
|
||||
code: messageCode,
|
||||
},
|
||||
})
|
||||
);
|
||||
this.emit('message', [
|
||||
{ ...this._messages[messageCode], provider, code: messageCode },
|
||||
]);
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: Message) => void) {
|
||||
this.on('message', callback);
|
||||
return () => {
|
||||
this.off('message', callback);
|
||||
};
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import type { AccessTokenMessage, Apis } from '../apis';
|
||||
|
||||
const user: AccessTokenMessage = {
|
||||
created_at: Date.now(),
|
||||
exp: 100000000,
|
||||
email: 'demo@demo.demo',
|
||||
id: '123',
|
||||
name: 'demo',
|
||||
avatar_url: 'demo-avatar-url',
|
||||
};
|
||||
|
||||
export const apis = {
|
||||
signInWithGoogle: () => {
|
||||
return Promise.resolve(user);
|
||||
},
|
||||
createWorkspace: mate => {
|
||||
return Promise.resolve({ id: 'test' });
|
||||
},
|
||||
} as Apis;
|
@ -1,491 +0,0 @@
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
import type { KyInstance } from 'ky/distribution/types/ky';
|
||||
|
||||
import { MessageCenter } from '../../message';
|
||||
import type { User } from '../../types';
|
||||
import { applyUpdate } from '../../utils';
|
||||
import type { SyncMode } from '../../workspace-unit';
|
||||
import { WorkspaceUnit } from '../../workspace-unit';
|
||||
import type {
|
||||
CreateWorkspaceInfoParams,
|
||||
ProviderConstructorParams,
|
||||
} from '../base';
|
||||
import { BaseProvider } from '../base';
|
||||
import type { Apis, Workspace, WorkspaceDetail } from './apis';
|
||||
import { getApis } from './apis';
|
||||
import { createGoogleAuth } from './apis/google';
|
||||
import { createAuthClient, createBareClient } from './apis/request';
|
||||
import { WebsocketClient } from './channel';
|
||||
import { WebsocketProvider } from './sync';
|
||||
import { createWorkspaceUnit, loadWorkspaceUnit, migrateBlobDB } from './utils';
|
||||
|
||||
type ChannelMessage = {
|
||||
ws_list: Workspace[];
|
||||
ws_details: Record<string, WorkspaceDetail>;
|
||||
metadata: Record<string, { avatar: string; name: string }>;
|
||||
};
|
||||
|
||||
export interface AffineProviderConstructorParams
|
||||
extends ProviderConstructorParams {
|
||||
apis?: Apis;
|
||||
}
|
||||
|
||||
const {
|
||||
Y: { encodeStateAsUpdate },
|
||||
} = BlocksuiteWorkspace;
|
||||
|
||||
export class AffineProvider extends BaseProvider {
|
||||
private readonly bareClient: KyInstance;
|
||||
private readonly authClient: KyInstance;
|
||||
|
||||
public id = 'affine';
|
||||
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
|
||||
private _apis: Apis;
|
||||
private _channel?: WebsocketClient;
|
||||
private _refreshToken?: string;
|
||||
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||
private _workspaceLoadingQueue: Map<string, Promise<WorkspaceUnit>> =
|
||||
new Map();
|
||||
|
||||
private _workspaces$: Promise<Workspace[]> | undefined;
|
||||
|
||||
constructor({ apis, ...params }: AffineProviderConstructorParams) {
|
||||
super(params);
|
||||
this.bareClient = createBareClient('/');
|
||||
const googleAuth = createGoogleAuth(this.bareClient);
|
||||
this.authClient = createAuthClient(this.bareClient, googleAuth);
|
||||
this._apis = apis || getApis(this.bareClient, this.authClient, googleAuth);
|
||||
}
|
||||
|
||||
public get apis() {
|
||||
return Object.freeze(this._apis);
|
||||
}
|
||||
|
||||
override async init() {
|
||||
this._apis.auth.onChange(() => {
|
||||
if (this._apis.auth.isLogin) {
|
||||
this._reconnectChannel();
|
||||
} else {
|
||||
this._destroyChannel();
|
||||
}
|
||||
});
|
||||
|
||||
if (this._apis.auth.isExpired && this._apis.auth.refresh) {
|
||||
// do we need to await the following?
|
||||
this._apis.auth.refreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
private _reconnectChannel() {
|
||||
if (this._refreshToken !== this._apis.auth.refresh) {
|
||||
// need to reconnect
|
||||
this._destroyChannel();
|
||||
|
||||
this._channel = new WebsocketClient(
|
||||
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||
window.location.host
|
||||
}/api/global/sync/`,
|
||||
this._apis.auth,
|
||||
{
|
||||
params: {
|
||||
token: this._apis.auth.refresh,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this._channel.on('message', (msg: ChannelMessage) => {
|
||||
this._handlerAffineListMessage(msg);
|
||||
});
|
||||
|
||||
this._refreshToken = this._apis.auth.refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private _destroyChannel() {
|
||||
if (this._channel) {
|
||||
this._channel.disconnect();
|
||||
this._channel.destroy();
|
||||
this._channel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlerAffineListMessage({
|
||||
ws_details,
|
||||
metadata,
|
||||
}: ChannelMessage) {
|
||||
this._logger.debug('receive server message');
|
||||
const newlyCreatedWorkspaces: WorkspaceUnit[] = [];
|
||||
const currentWorkspaceIds = this._workspaces.list().map(w => w.id);
|
||||
const newlyRemovedWorkspaceIds = currentWorkspaceIds;
|
||||
for (const [id, detail] of Object.entries(ws_details)) {
|
||||
const { name, avatar } = metadata[id];
|
||||
|
||||
/**
|
||||
* collect the workspaces that need to be removed in the context
|
||||
*/
|
||||
const workspaceIndex = currentWorkspaceIds.indexOf(id);
|
||||
const ifWorkspaceExist = workspaceIndex !== -1;
|
||||
if (ifWorkspaceExist) {
|
||||
newlyRemovedWorkspaceIds.splice(workspaceIndex, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* if workspace name is not empty, it is a valid workspace, so sync its state
|
||||
*/
|
||||
if (name) {
|
||||
const workspace = {
|
||||
name: name,
|
||||
avatar,
|
||||
owner: {
|
||||
name: detail.owner.name,
|
||||
id: detail.owner.id,
|
||||
email: detail.owner.email,
|
||||
avatar: detail.owner.avatar_url,
|
||||
},
|
||||
published: detail.public,
|
||||
memberCount: detail.member_count,
|
||||
provider: this.id,
|
||||
syncMode: 'core' as SyncMode,
|
||||
};
|
||||
if (this._workspaces.get(id)) {
|
||||
// update workspaces
|
||||
this._workspaces.update(id, workspace);
|
||||
} else {
|
||||
if (!this._workspaceLoadingQueue.has(id)) {
|
||||
const p = loadWorkspaceUnit({ id, ...workspace }, this._apis);
|
||||
this._workspaceLoadingQueue.set(id, p);
|
||||
newlyCreatedWorkspaces.push(await p);
|
||||
this._workspaceLoadingQueue.delete(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[log warn] ${id} name is empty`);
|
||||
}
|
||||
}
|
||||
|
||||
// sync newlyCreatedWorkspaces to context
|
||||
this._workspaces.add(newlyCreatedWorkspaces);
|
||||
|
||||
// sync newlyRemoveWorkspaces to context
|
||||
this._workspaces.remove(newlyRemovedWorkspaceIds);
|
||||
}
|
||||
|
||||
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
|
||||
const { doc, id } = workspace;
|
||||
assert(id);
|
||||
assert(doc);
|
||||
let ws = this._wsMap.get(workspace);
|
||||
if (!ws) {
|
||||
const wsUrl = `${
|
||||
window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
}://${window.location.host}/api/sync/`;
|
||||
ws = new WebsocketProvider(wsUrl, id, doc, {
|
||||
params: { token: this._apis.auth.refresh },
|
||||
// @ts-expect-error ignore the type
|
||||
awareness: workspace.awarenessStore.awareness,
|
||||
});
|
||||
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
||||
name: this._apis.auth.user?.name ?? 'other',
|
||||
id: Number(this._apis.auth.user?.id ?? -1),
|
||||
color: '#ffa500',
|
||||
});
|
||||
|
||||
this._wsMap.set(workspace, ws);
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
private async _applyCloudUpdates(
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
published = false
|
||||
) {
|
||||
const { id: workspaceId } = blocksuiteWorkspace;
|
||||
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
|
||||
const updates = await this._apis.downloadWorkspace(workspaceId, published);
|
||||
await applyUpdate(blocksuiteWorkspace, new Uint8Array(updates));
|
||||
}
|
||||
|
||||
override async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
|
||||
await this._applyCloudUpdates(blocksuiteWorkspace, true);
|
||||
return blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
override async warpWorkspace(workspace: BlocksuiteWorkspace) {
|
||||
workspace.setGettingBlobOptions(
|
||||
(k: string) => ({ api: '/api/workspace', token: this.getToken() }[k])
|
||||
);
|
||||
// FIXME: if add indexedDB cache in the future, can remove following line.
|
||||
await this._applyCloudUpdates(workspace);
|
||||
const { id } = workspace;
|
||||
assert(id);
|
||||
this.linkLocal(workspace);
|
||||
const ws = this._getWebsocketProvider(workspace);
|
||||
// close all websocket links
|
||||
Array.from(this._wsMap.entries()).forEach(([blocksuiteWorkspace, ws]) => {
|
||||
if (blocksuiteWorkspace !== workspace) {
|
||||
ws.disconnect();
|
||||
}
|
||||
});
|
||||
ws.connect();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
assert(ws);
|
||||
ws.once('synced', () => resolve());
|
||||
ws.once('lost-connection', () => resolve());
|
||||
ws.once('connection-error', () => reject());
|
||||
});
|
||||
return workspace;
|
||||
}
|
||||
|
||||
override async loadWorkspaces() {
|
||||
if (!this._apis.auth.isLogin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// cache workspaces and workspaceUnits results so that simultaneous calls
|
||||
// to loadWorkspaces will not cause multiple requests
|
||||
if (!this._workspaces$) {
|
||||
this._workspaces$ = this._apis.getWorkspaces();
|
||||
}
|
||||
|
||||
const workspacesList = await this._workspaces$;
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspacesList.map(async w => {
|
||||
let p = this._workspaceLoadingQueue.get(w.id);
|
||||
if (!p) {
|
||||
// may only need to load the primary one instead of all of them?
|
||||
// it will take a long time to load all of the workspaces
|
||||
// at least we shall use p-map to load them in chunks
|
||||
p = loadWorkspaceUnit(
|
||||
{
|
||||
id: w.id,
|
||||
name: '',
|
||||
avatar: undefined,
|
||||
owner: undefined,
|
||||
published: w.public,
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
},
|
||||
this._apis
|
||||
);
|
||||
this._workspaceLoadingQueue.set(w.id, p);
|
||||
}
|
||||
const workspaceUnit = await p;
|
||||
this._workspaceLoadingQueue.delete(w.id);
|
||||
|
||||
return workspaceUnit;
|
||||
})
|
||||
);
|
||||
|
||||
// release cache
|
||||
this._workspaces$ = undefined;
|
||||
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
}
|
||||
|
||||
override async auth() {
|
||||
if (this._apis.auth.isLogin) {
|
||||
await this._apis.auth.refreshToken();
|
||||
if (this._apis.auth.isLogin && !this._apis.auth.isExpired) {
|
||||
// login success
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this._apis.signInWithGoogle?.();
|
||||
|
||||
if (!user) {
|
||||
this._sendMessage(MessageCenter.messageCode.loginError);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: may need to update related workspace attributes on user info change?
|
||||
public override async getUserInfo(): Promise<User | undefined> {
|
||||
const user = this._apis.auth.user;
|
||||
return user
|
||||
? {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatar: user.avatar_url,
|
||||
email: user.email,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public override async deleteWorkspace(id: string): Promise<void> {
|
||||
await this.closeWorkspace(id);
|
||||
// IndexedDBProvider.delete(id);
|
||||
await this._apis.deleteWorkspace({ id });
|
||||
this._workspaces.remove(id);
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
for (const w of this._workspaces.list()) {
|
||||
if (w.id) {
|
||||
try {
|
||||
await this.deleteWorkspace(w.id);
|
||||
this._workspaces.remove(w.id);
|
||||
} catch (e) {
|
||||
this._logger.error('has a problem of delete workspace ', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._workspaces.clear();
|
||||
}
|
||||
|
||||
public override async closeWorkspace(id: string) {
|
||||
// const idb = this._idbMap.get(id);
|
||||
// idb?.destroy();
|
||||
const workspaceUnit = this._workspaces.get(id);
|
||||
const ws = workspaceUnit?.blocksuiteWorkspace
|
||||
? this._wsMap.get(workspaceUnit?.blocksuiteWorkspace)
|
||||
: null;
|
||||
if (!ws) {
|
||||
console.error('close workspace websocket which not exist.');
|
||||
}
|
||||
ws?.disconnect();
|
||||
}
|
||||
|
||||
public override async leaveWorkspace(id: string): Promise<void> {
|
||||
await this._apis.leaveWorkspace({ id });
|
||||
}
|
||||
|
||||
public override async invite(id: string, email: string): Promise<void> {
|
||||
return await this._apis.inviteMember({ id, email });
|
||||
}
|
||||
|
||||
public override async removeMember(permissionId: number): Promise<void> {
|
||||
return await this._apis.removeMember({ permissionId });
|
||||
}
|
||||
|
||||
public override async linkLocal(workspace: BlocksuiteWorkspace) {
|
||||
return workspace;
|
||||
// assert(workspace.id);
|
||||
// let idb = this._idbMap.get(workspace.id);
|
||||
// idb?.destroy();
|
||||
// idb = new IndexedDBProvider(workspace.id, workspace.doc);
|
||||
// this._idbMap.set(workspace.id, idb);
|
||||
// await idb.whenSynced;
|
||||
// this._logger('Local data loaded');
|
||||
// return workspace;
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
const workspaceUnitForUpload = await createWorkspaceUnit(
|
||||
{
|
||||
id: '',
|
||||
name: meta.name,
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
},
|
||||
this._apis
|
||||
);
|
||||
const { id } = await this._apis.createWorkspace(
|
||||
new Blob([
|
||||
encodeStateAsUpdate(workspaceUnitForUpload.blocksuiteWorkspace!.doc)
|
||||
.buffer,
|
||||
])
|
||||
);
|
||||
|
||||
const workspaceUnit = await createWorkspaceUnit(
|
||||
{
|
||||
id,
|
||||
name: meta.name,
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
},
|
||||
this._apis
|
||||
);
|
||||
|
||||
this._workspaces.add(workspaceUnit);
|
||||
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
public override async publish(id: string, isPublish: boolean): Promise<void> {
|
||||
await this._apis.updateWorkspace({ id, public: isPublish });
|
||||
}
|
||||
|
||||
public override getToken(): string {
|
||||
return this._apis.auth.token;
|
||||
}
|
||||
|
||||
public override async getUserByEmail(
|
||||
workspace_id: string,
|
||||
email: string
|
||||
): Promise<User | null> {
|
||||
const users = await this._apis.getUserByEmail({ workspace_id, email });
|
||||
return users?.length
|
||||
? {
|
||||
id: users[0].id,
|
||||
name: users[0].name,
|
||||
avatar: users[0].avatar_url,
|
||||
email: users[0].email,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
public override async extendWorkspace(
|
||||
workspaceUnit: WorkspaceUnit
|
||||
): Promise<WorkspaceUnit> {
|
||||
const { id } = await this._apis.createWorkspace(
|
||||
new Blob([
|
||||
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace!.doc).buffer,
|
||||
])
|
||||
);
|
||||
const newWorkspaceUnit = new WorkspaceUnit({
|
||||
id,
|
||||
name: workspaceUnit.name,
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
syncMode: 'core',
|
||||
});
|
||||
await migrateBlobDB(workspaceUnit.id, id);
|
||||
|
||||
const blocksuiteWorkspace =
|
||||
await this._apis.createBlockSuiteWorkspaceWithAuth(id);
|
||||
assert(workspaceUnit.blocksuiteWorkspace);
|
||||
await applyUpdate(
|
||||
blocksuiteWorkspace,
|
||||
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
this._workspaces.add(newWorkspaceUnit);
|
||||
return newWorkspaceUnit;
|
||||
}
|
||||
|
||||
public override async logout(): Promise<void> {
|
||||
this._apis.auth.clear();
|
||||
this._destroyChannel();
|
||||
this._wsMap.forEach(ws => ws.disconnect());
|
||||
this._workspaces.clear(false);
|
||||
await this._apis.signOutFirebase();
|
||||
}
|
||||
|
||||
public override async getWorkspaceMembers(id: string) {
|
||||
return this._apis.getWorkspaceMembers({ id });
|
||||
}
|
||||
|
||||
public override async acceptInvitation(invitingCode: string) {
|
||||
return await this._apis.acceptInviting({ invitingCode });
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { GoogleAuth } from '../google';
|
||||
|
||||
describe('class Auth', () => {
|
||||
test('parse tokens', () => {
|
||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||
expect(GoogleAuth.parseIdToken(tokenString)).toEqual({
|
||||
avatar_url: 'https://test.com/avatar',
|
||||
created_at: 1675678920358,
|
||||
email: 'test@gmail.com',
|
||||
exp: 1675679520,
|
||||
id: 6,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
test('parse invalid tokens', () => {
|
||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||
expect(GoogleAuth.parseIdToken(tokenString)).toEqual(null);
|
||||
});
|
||||
});
|
@ -1,288 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import type { User } from 'firebase/auth';
|
||||
import { connectAuthEmulator } from 'firebase/auth';
|
||||
import {
|
||||
type Auth as FirebaseAuth,
|
||||
getAuth as getFirebaseAuth,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
} from 'firebase/auth';
|
||||
import { decode } from 'js-base64';
|
||||
import type { KyInstance } from 'ky/distribution/types/ky';
|
||||
|
||||
import { MessageCenter } from '../../../message';
|
||||
import { storage } from '../storage';
|
||||
import { RequestError } from './request-error';
|
||||
|
||||
export interface AccessTokenMessage {
|
||||
created_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
|
||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
type LoginParams = {
|
||||
type: 'Google' | 'Refresh';
|
||||
token: string;
|
||||
};
|
||||
|
||||
type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
// TODO: organize storage keys in a better way
|
||||
const AFFINE_LOGIN_STORAGE_KEY = 'affine:login';
|
||||
const messageCenter = MessageCenter.getInstance();
|
||||
const sendMessage = messageCenter.getMessageSender('affine');
|
||||
const { messageCode } = MessageCenter;
|
||||
|
||||
/**
|
||||
* Use refresh token to get a new access token (JWT)
|
||||
* The returned token also contains the user info payload.
|
||||
*/
|
||||
const createDoLogin =
|
||||
(bareClient: KyInstance) =>
|
||||
(params: LoginParams): Promise<LoginResponse> =>
|
||||
bareClient.post('api/user/token', { json: params }).json();
|
||||
|
||||
export class GoogleAuth {
|
||||
private readonly _logger;
|
||||
private _accessToken = ''; // idtoken (JWT)
|
||||
private _refreshToken = '';
|
||||
|
||||
private _user: AccessTokenMessage | null = null;
|
||||
private _padding?: Promise<LoginResponse>;
|
||||
private readonly _doLogin: ReturnType<typeof createDoLogin>;
|
||||
|
||||
constructor(bareClient: KyInstance) {
|
||||
this._logger = new DebugLogger('token');
|
||||
this._logger.enabled = true;
|
||||
this._doLogin = createDoLogin(bareClient);
|
||||
|
||||
this.restoreLogin();
|
||||
}
|
||||
|
||||
setLogin(login: LoginResponse) {
|
||||
this._accessToken = login.token;
|
||||
this._refreshToken = login.refresh;
|
||||
this._user = GoogleAuth.parseIdToken(this._accessToken);
|
||||
|
||||
this.triggerChange(this._user);
|
||||
this.storeLogin();
|
||||
}
|
||||
|
||||
private storeLogin() {
|
||||
if (this.refresh) {
|
||||
const { token, refresh } = this;
|
||||
storage.setItem(
|
||||
AFFINE_LOGIN_STORAGE_KEY,
|
||||
JSON.stringify({ token, refresh })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private restoreLogin() {
|
||||
const loginStr = storage.getItem(AFFINE_LOGIN_STORAGE_KEY);
|
||||
if (!loginStr) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const login: LoginResponse = JSON.parse(loginStr);
|
||||
this.setLogin(login);
|
||||
} catch (err) {
|
||||
this._logger.warn('Failed to parse login info', err);
|
||||
}
|
||||
}
|
||||
|
||||
async initToken(token: string) {
|
||||
try {
|
||||
const res = await this._doLogin({
|
||||
token,
|
||||
type: 'Google',
|
||||
});
|
||||
this.setLogin(res);
|
||||
return this._user;
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.loginError);
|
||||
throw new RequestError('Login failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken?: string) {
|
||||
if (!this._padding) {
|
||||
this._padding = this._doLogin({
|
||||
type: 'Refresh',
|
||||
token: refreshToken || this._refreshToken,
|
||||
});
|
||||
this._refreshToken = refreshToken || this._refreshToken;
|
||||
}
|
||||
try {
|
||||
const res = await this._padding;
|
||||
if (res && (!refreshToken || refreshToken !== this._refreshToken)) {
|
||||
this.setLogin(res);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.refreshTokenError);
|
||||
throw new RequestError('Refresh token failed', error);
|
||||
} finally {
|
||||
// clear on settled
|
||||
this._padding = undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get user() {
|
||||
// computed through access token
|
||||
return this._user;
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this._accessToken;
|
||||
}
|
||||
|
||||
get refresh() {
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
get isLogin() {
|
||||
return !!this._refreshToken;
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
if (!this._user) return true;
|
||||
// exp is in seconds
|
||||
return Date.now() > this._user.exp * 1000;
|
||||
}
|
||||
|
||||
static parseIdToken(token: string): AccessTokenMessage | null {
|
||||
try {
|
||||
return JSON.parse(decode(token.split('.')[1]));
|
||||
} catch (error) {
|
||||
// todo: log errors?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private callbacks: Callback[] = [];
|
||||
private lastState: AccessTokenMessage | null = null;
|
||||
|
||||
triggerChange(user: AccessTokenMessage | null) {
|
||||
this.lastState = user;
|
||||
this.callbacks.forEach(callback => callback(user));
|
||||
}
|
||||
|
||||
onChange(callback: Callback) {
|
||||
this.callbacks.push(callback);
|
||||
callback(this.lastState);
|
||||
}
|
||||
|
||||
offChange(callback: Callback) {
|
||||
const index = this.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._accessToken = '';
|
||||
this._refreshToken = '';
|
||||
storage.removeItem(AFFINE_LOGIN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export function createGoogleAuth(bareAuth: KyInstance): GoogleAuth {
|
||||
return new GoogleAuth(bareAuth);
|
||||
}
|
||||
|
||||
// Connect emulators based on env vars
|
||||
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
|
||||
|
||||
export const getAuthorizer = (googleAuth: GoogleAuth) => {
|
||||
let _firebaseAuth: FirebaseAuth | null = null;
|
||||
const logger = new DebugLogger('authorizer');
|
||||
|
||||
// getAuth will send requests on calling thus we can lazy init it
|
||||
const getAuth = () => {
|
||||
try {
|
||||
if (!_firebaseAuth) {
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId:
|
||||
process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
_firebaseAuth = getFirebaseAuth(app);
|
||||
}
|
||||
if (envConnectEmulators && !(window as any).firebaseAuthEmulatorStarted) {
|
||||
connectAuthEmulator(_firebaseAuth, 'http://localhost:9099', {
|
||||
disableWarnings: true,
|
||||
});
|
||||
(window as any).firebaseAuthEmulatorStarted = true;
|
||||
}
|
||||
return _firebaseAuth;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize firebase', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getToken = async () => {
|
||||
const currentUser = getAuth()?.currentUser;
|
||||
if (currentUser) {
|
||||
await currentUser.getIdTokenResult(true);
|
||||
if (!currentUser.isAnonymous) {
|
||||
return currentUser.getIdToken();
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
const idToken = await getToken();
|
||||
let loginUser: AccessTokenMessage | null = null;
|
||||
if (idToken) {
|
||||
loginUser = await googleAuth.initToken(idToken);
|
||||
} else {
|
||||
const firebaseAuth = getAuth();
|
||||
if (firebaseAuth) {
|
||||
const googleAuthProvider = new GoogleAuthProvider();
|
||||
// make sure the user has a chance to select an account
|
||||
// https://developers.google.com/identity/openid-connect/openid-connect#prompt
|
||||
googleAuthProvider.setCustomParameters({
|
||||
prompt: 'select_account',
|
||||
});
|
||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||
const idToken = await user.user.getIdToken();
|
||||
loginUser = await googleAuth.initToken(idToken);
|
||||
}
|
||||
}
|
||||
return loginUser;
|
||||
};
|
||||
|
||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||
getAuth()?.onAuthStateChanged(callback);
|
||||
};
|
||||
|
||||
const signOutFirebase = async () => {
|
||||
const firebaseAuth = getAuth();
|
||||
if (firebaseAuth?.currentUser) {
|
||||
await signOut(firebaseAuth);
|
||||
}
|
||||
};
|
||||
|
||||
return [signInWithGoogle, onAuthStateChanged, signOutFirebase] as const;
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
export type { Callback } from './google';
|
||||
|
||||
import type { KyInstance } from 'ky/distribution/types/ky';
|
||||
|
||||
import type { createGoogleAuth, GoogleAuth } from './google';
|
||||
import { getAuthorizer } from './google';
|
||||
import { createUserApis } from './user';
|
||||
import { createWorkspaceApis } from './workspace';
|
||||
|
||||
// See https://twitter.com/mattpocockuk/status/1622730173446557697
|
||||
// TODO: move to ts utils?
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
} & {};
|
||||
|
||||
export type Apis = Prettify<
|
||||
ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis> & {
|
||||
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||
signOutFirebase: ReturnType<typeof getAuthorizer>[2];
|
||||
} & { auth: ReturnType<typeof createGoogleAuth> }
|
||||
>;
|
||||
|
||||
export const getApis = (
|
||||
bareClient: KyInstance,
|
||||
authClient: KyInstance,
|
||||
googleAuth: GoogleAuth
|
||||
): Apis => {
|
||||
const [signInWithGoogle, onAuthStateChanged, signOutFirebase] =
|
||||
getAuthorizer(googleAuth);
|
||||
return {
|
||||
...createUserApis(bareClient, authClient),
|
||||
...createWorkspaceApis(bareClient, authClient, googleAuth),
|
||||
signInWithGoogle,
|
||||
signOutFirebase,
|
||||
onAuthStateChanged,
|
||||
auth: googleAuth,
|
||||
};
|
||||
};
|
||||
|
||||
export type { AccessTokenMessage } from './google';
|
||||
export type { Member, Workspace, WorkspaceDetail } from './workspace';
|
||||
export * from './workspace';
|
@ -1,9 +0,0 @@
|
||||
export class RequestError extends Error {
|
||||
constructor(message: string, cause: unknown | null = null) {
|
||||
super(message);
|
||||
this.name = 'RequestError';
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestError;
|
@ -1,65 +0,0 @@
|
||||
import ky from 'ky-universal';
|
||||
|
||||
import { MessageCenter } from '../../../message';
|
||||
import type { GoogleAuth } from './google';
|
||||
|
||||
type KyInstance = typeof ky;
|
||||
|
||||
const messageCenter = MessageCenter.getInstance();
|
||||
|
||||
const _sendMessage = messageCenter.getMessageSender('affine');
|
||||
|
||||
export const createBareClient = (prefixUrl: string): KyInstance =>
|
||||
ky.extend({
|
||||
prefixUrl: prefixUrl,
|
||||
retry: 1,
|
||||
// todo: report timeout error
|
||||
timeout: 60000,
|
||||
hooks: {
|
||||
beforeError: [
|
||||
error => {
|
||||
const { response } = error;
|
||||
if (response.status === 401) {
|
||||
_sendMessage(MessageCenter.messageCode.noPermission);
|
||||
}
|
||||
return error;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const refreshTokenIfExpired = async (googleAuth: GoogleAuth) => {
|
||||
if (googleAuth.isLogin && googleAuth.isExpired) {
|
||||
try {
|
||||
await googleAuth.refreshToken();
|
||||
} catch (err) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createAuthClient = (
|
||||
bareClient: KyInstance,
|
||||
googleAuth: GoogleAuth
|
||||
): KyInstance =>
|
||||
bareClient.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async request => {
|
||||
if (googleAuth.isLogin) {
|
||||
await refreshTokenIfExpired(googleAuth);
|
||||
request.headers.set('Authorization', googleAuth.token);
|
||||
} else {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
beforeRetry: [
|
||||
async ({ request }) => {
|
||||
await refreshTokenIfExpired(googleAuth);
|
||||
request.headers.set('Authorization', googleAuth.token);
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
@ -1,25 +0,0 @@
|
||||
import type { KyInstance } from 'ky/distribution/types/ky';
|
||||
|
||||
export interface GetUserByEmailParams {
|
||||
email: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export function createUserApis(bareClient: KyInstance, authClient: KyInstance) {
|
||||
return {
|
||||
getUserByEmail: async (
|
||||
params: GetUserByEmailParams
|
||||
): Promise<User[] | null> => {
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
return authClient.get('api/user', { searchParams }).json<User[] | null>();
|
||||
},
|
||||
} as const;
|
||||
}
|
@ -1,260 +0,0 @@
|
||||
import type { KyInstance } from 'ky/distribution/types/ky';
|
||||
|
||||
import { MessageCenter } from '../../../message';
|
||||
import { createBlocksuiteWorkspace as _createBlocksuiteWorkspace } from '../../../utils';
|
||||
import type { GoogleAuth } from './google';
|
||||
import RequestError from './request-error';
|
||||
import type { User } from './user';
|
||||
const messageCenter = MessageCenter.getInstance();
|
||||
|
||||
const sendMessage = messageCenter.getMessageSender('affine');
|
||||
|
||||
const { messageCode } = MessageCenter;
|
||||
|
||||
export interface GetWorkspaceDetailParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum WorkspaceType {
|
||||
Private = 0,
|
||||
Normal = 1,
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 10,
|
||||
Owner = 99,
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
type: WorkspaceType;
|
||||
public: boolean;
|
||||
permission: PermissionType;
|
||||
}
|
||||
|
||||
export interface WorkspaceDetail extends Workspace {
|
||||
owner: User;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string;
|
||||
type: PermissionType;
|
||||
workspace_id: string;
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
accepted: boolean;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export interface RegisteredUser extends User {
|
||||
type: 'Registered';
|
||||
}
|
||||
|
||||
export interface UnregisteredUser {
|
||||
type: 'Unregistered';
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Member extends Permission {
|
||||
user: RegisteredUser | UnregisteredUser;
|
||||
}
|
||||
|
||||
export interface GetWorkspaceMembersParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CreateWorkspaceParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceParams {
|
||||
id: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface RemoveMemberParams {
|
||||
permissionId: number;
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
invitingCode: string;
|
||||
}
|
||||
|
||||
export interface LeaveWorkspaceParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
export function createWorkspaceApis(
|
||||
bareClient: KyInstance,
|
||||
authClient: KyInstance,
|
||||
googleAuth: GoogleAuth
|
||||
) {
|
||||
return {
|
||||
getWorkspaces: async (): Promise<Workspace[]> => {
|
||||
try {
|
||||
return await authClient
|
||||
.get('api/workspace', {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.loadListFailed);
|
||||
throw new RequestError('load list failed', error);
|
||||
}
|
||||
},
|
||||
getWorkspaceDetail: async (
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> => {
|
||||
try {
|
||||
return await authClient.get(`api/workspace/${params.id}`).json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getDetailFailed);
|
||||
throw new RequestError('get detail failed', error);
|
||||
}
|
||||
},
|
||||
getWorkspaceMembers: async (
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> => {
|
||||
try {
|
||||
return await authClient
|
||||
.get(`api/workspace/${params.id}/permission`)
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getMembersFailed);
|
||||
throw new RequestError('get members failed', error);
|
||||
}
|
||||
},
|
||||
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
|
||||
try {
|
||||
return await authClient
|
||||
.post('api/workspace', { body: encodedYDoc })
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.createWorkspaceFailed);
|
||||
throw new RequestError('create workspace failed', error);
|
||||
}
|
||||
},
|
||||
updateWorkspace: async (
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<{ public: boolean | null }> => {
|
||||
try {
|
||||
return await authClient
|
||||
.post(`api/workspace/${params.id}`, {
|
||||
json: {
|
||||
public: params.public,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.updateWorkspaceFailed);
|
||||
throw new RequestError('update workspace failed', error);
|
||||
}
|
||||
},
|
||||
deleteWorkspace: async (params: DeleteWorkspaceParams): Promise<void> => {
|
||||
try {
|
||||
await authClient.delete(`api/workspace/${params.id}`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.deleteWorkspaceFailed);
|
||||
throw new RequestError('delete workspace failed', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Notice: Only support normal(contrast to private) workspace.
|
||||
*/
|
||||
inviteMember: async (params: InviteMemberParams): Promise<void> => {
|
||||
try {
|
||||
await authClient.post(`api/workspace/${params.id}/permission`, {
|
||||
json: {
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.inviteMemberFailed);
|
||||
throw new RequestError('invite member failed', error);
|
||||
}
|
||||
},
|
||||
removeMember: async (params: RemoveMemberParams): Promise<void> => {
|
||||
try {
|
||||
await authClient.delete(`api/permission/${params.permissionId}`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.removeMemberFailed);
|
||||
throw new RequestError('remove member failed', error);
|
||||
}
|
||||
},
|
||||
acceptInviting: async (
|
||||
params: AcceptInvitingParams
|
||||
): Promise<Permission> => {
|
||||
try {
|
||||
return await bareClient
|
||||
.post(`api/invitation/${params.invitingCode}`)
|
||||
.json();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.acceptInvitingFailed);
|
||||
throw new RequestError('accept inviting failed', error);
|
||||
}
|
||||
},
|
||||
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
|
||||
return authClient.put('api/blob', { body: params.blob }).text();
|
||||
},
|
||||
getBlob: async (params: { blobId: string }): Promise<ArrayBuffer> => {
|
||||
try {
|
||||
return await authClient.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.getBlobFailed);
|
||||
throw new RequestError('get blob failed', error);
|
||||
}
|
||||
},
|
||||
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
|
||||
try {
|
||||
await authClient.delete(`api/workspace/${id}/permission`);
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.leaveWorkspaceFailed);
|
||||
throw new RequestError('leave workspace failed', error);
|
||||
}
|
||||
},
|
||||
downloadWorkspace: async (
|
||||
workspaceId: string,
|
||||
published = false
|
||||
): Promise<ArrayBuffer> => {
|
||||
try {
|
||||
if (published) {
|
||||
return await bareClient
|
||||
.get(`api/public/doc/${workspaceId}`)
|
||||
.arrayBuffer();
|
||||
}
|
||||
return await authClient
|
||||
.get(`api/workspace/${workspaceId}/doc`)
|
||||
.arrayBuffer();
|
||||
} catch (error) {
|
||||
sendMessage(messageCode.downloadWorkspaceFailed);
|
||||
throw new RequestError('download workspace failed', error);
|
||||
}
|
||||
},
|
||||
createBlockSuiteWorkspaceWithAuth: async (newWorkspaceId: string) => {
|
||||
if (googleAuth.isExpired && googleAuth.isLogin) {
|
||||
await googleAuth.refreshToken();
|
||||
}
|
||||
return _createBlocksuiteWorkspace({
|
||||
id: newWorkspaceId,
|
||||
blobOptionsGetter: (k: string) =>
|
||||
// token could be expired
|
||||
({ api: '/api/workspace', token: googleAuth.token }[k]),
|
||||
});
|
||||
},
|
||||
} as const;
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import * as url from 'lib0/url';
|
||||
import * as websocket from 'lib0/websocket';
|
||||
|
||||
import type { GoogleAuth } from './apis/google';
|
||||
|
||||
const RECONNECT_INTERVAL_TIME = 500;
|
||||
const MAX_RECONNECT_TIMES = 50;
|
||||
|
||||
export class WebsocketClient extends websocket.WebsocketClient {
|
||||
public shouldReconnect = false;
|
||||
private _retryTimes = 0;
|
||||
private _auth: GoogleAuth;
|
||||
private _logger = new DebugLogger('affine:channel');
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
auth: GoogleAuth,
|
||||
options?: ConstructorParameters<typeof websocket.WebsocketClient>[1] & {
|
||||
params: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
const params = options?.params || {};
|
||||
// ensure that url is always ends with /
|
||||
while (serverUrl[serverUrl.length - 1] === '/') {
|
||||
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
|
||||
}
|
||||
const encodedParams = url.encodeQueryParams(params);
|
||||
const newUrl =
|
||||
serverUrl + '/' + (encodedParams.length === 0 ? '' : '?' + encodedParams);
|
||||
super(newUrl, options);
|
||||
this._auth = auth;
|
||||
this._setupChannel();
|
||||
}
|
||||
|
||||
private _setupChannel() {
|
||||
this.on('connect', () => {
|
||||
this._logger.debug('Affine channel connected');
|
||||
this.shouldReconnect = true;
|
||||
this._retryTimes = 0;
|
||||
});
|
||||
|
||||
this.on('disconnect', ({ error }: { error: Error }) => {
|
||||
if (error) {
|
||||
// Try reconnect if connect error has occurred
|
||||
if (this.shouldReconnect && this._auth.isLogin && !this.connected) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
|
||||
this.connect();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { clear, createStore, getMany, keys, setMany } from 'idb-keyval';
|
||||
import * as idb from 'lib0/indexeddb';
|
||||
|
||||
type IDBInstance<T = ArrayBufferLike> = {
|
||||
keys: () => Promise<string[]>;
|
||||
clear: () => Promise<void>;
|
||||
deleteDB: () => Promise<void>;
|
||||
setMany: (entries: [string, T][]) => Promise<void>;
|
||||
getMany: (keys: string[]) => Promise<T[]>;
|
||||
};
|
||||
|
||||
export function getDatabase<T = ArrayBufferLike>(
|
||||
type: string,
|
||||
database: string
|
||||
): IDBInstance<T> {
|
||||
const name = `${database}_${type}`;
|
||||
const db = createStore(name, type);
|
||||
return {
|
||||
keys: () => keys(db),
|
||||
clear: () => clear(db),
|
||||
deleteDB: () => idb.deleteDB(name),
|
||||
setMany: entries => setMany(entries, db),
|
||||
getMany: keys => getMany(keys, db),
|
||||
};
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './affine';
|
@ -1,3 +0,0 @@
|
||||
import { varStorage } from 'lib0/storage';
|
||||
|
||||
export const storage = varStorage as Storage;
|
@ -1,94 +0,0 @@
|
||||
import { applyUpdate } from '../../utils';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { WorkspaceUnit } from '../../workspace-unit';
|
||||
import { setDefaultAvatar } from '../utils';
|
||||
import type { Apis } from './apis';
|
||||
import { getDatabase } from './idb-kv';
|
||||
|
||||
export const loadWorkspaceUnit = async (
|
||||
params: WorkspaceUnitCtorParams,
|
||||
apis: Apis
|
||||
) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
const blocksuiteWorkspace = await apis.createBlockSuiteWorkspaceWithAuth(
|
||||
workspaceUnit.id
|
||||
);
|
||||
|
||||
const updates = await apis.downloadWorkspace(
|
||||
workspaceUnit.id,
|
||||
params.published
|
||||
);
|
||||
applyUpdate(blocksuiteWorkspace, new Uint8Array(updates));
|
||||
|
||||
const details = await apis.getWorkspaceDetail({ id: workspaceUnit.id });
|
||||
const owner = details?.owner;
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
workspaceUnit.update({
|
||||
name: blocksuiteWorkspace.meta.name,
|
||||
avatar: blocksuiteWorkspace.meta.avatar,
|
||||
memberCount: details?.member_count || 1,
|
||||
owner: owner
|
||||
? {
|
||||
id: owner.id,
|
||||
name: owner.name,
|
||||
avatar: owner.avatar_url,
|
||||
email: owner.email,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
export const createWorkspaceUnit = async (
|
||||
params: WorkspaceUnitCtorParams,
|
||||
apis: Apis
|
||||
) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = await apis.createBlockSuiteWorkspaceWithAuth(
|
||||
workspaceUnit.id
|
||||
);
|
||||
|
||||
blocksuiteWorkspace.meta.setName(workspaceUnit.name);
|
||||
if (!workspaceUnit.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar });
|
||||
}
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
interface PendingTask {
|
||||
id: string;
|
||||
blob: ArrayBufferLike;
|
||||
}
|
||||
|
||||
export const migrateBlobDB = async (
|
||||
oldWorkspaceId: string,
|
||||
newWorkspaceId: string
|
||||
) => {
|
||||
const oldDB = getDatabase('blob', oldWorkspaceId);
|
||||
const oldPendingDB = getDatabase<PendingTask>('pending', oldWorkspaceId);
|
||||
|
||||
const newDB = getDatabase('blob', newWorkspaceId);
|
||||
const newPendingDB = getDatabase<PendingTask>('pending', newWorkspaceId);
|
||||
|
||||
const keys = await oldDB.keys();
|
||||
const values = await oldDB.getMany(keys);
|
||||
const entries = keys.map((key, index) => {
|
||||
return [key, values[index]] as [string, ArrayBufferLike];
|
||||
});
|
||||
await newDB.setMany(entries);
|
||||
|
||||
const pendingEntries = entries.map(([id, blob]) => {
|
||||
return [id, { id, blob }] as [string, PendingTask];
|
||||
});
|
||||
await newPendingDB.setMany(pendingEntries);
|
||||
|
||||
await oldDB.clear();
|
||||
await oldPendingDB.clear();
|
||||
};
|
@ -1,240 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
|
||||
import type { MessageCenter } from '../message';
|
||||
import type { User } from '../types';
|
||||
import type { WorkspaceUnit, WorkspaceUnitCtorParams } from '../workspace-unit';
|
||||
import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection';
|
||||
import type { Member } from './affine/apis';
|
||||
import type { Permission } from './affine/apis/workspace';
|
||||
|
||||
export interface ProviderConstructorParams {
|
||||
workspaces: WorkspaceUnitCollectionScope;
|
||||
messageCenter: MessageCenter;
|
||||
}
|
||||
|
||||
export type WorkspaceMeta0 = WorkspaceUnitCtorParams;
|
||||
export type CreateWorkspaceInfoParams = Pick<WorkspaceUnitCtorParams, 'name'>;
|
||||
export type UpdateWorkspaceMetaParams = Partial<
|
||||
Pick<WorkspaceUnitCtorParams, 'name' | 'avatar'>
|
||||
>;
|
||||
|
||||
export abstract class BaseProvider {
|
||||
/** provider id */
|
||||
public readonly id: string = 'base';
|
||||
/** workspace unit collection */
|
||||
protected _workspaces!: WorkspaceUnitCollectionScope;
|
||||
protected _logger: DebugLogger;
|
||||
/** send message with message center */
|
||||
protected _sendMessage!: ReturnType<
|
||||
InstanceType<typeof MessageCenter>['getMessageSender']
|
||||
>;
|
||||
|
||||
public constructor({ workspaces, messageCenter }: ProviderConstructorParams) {
|
||||
this._workspaces = workspaces;
|
||||
this._sendMessage = messageCenter.getMessageSender(this.id);
|
||||
this._logger = new DebugLogger(`provider:${this.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* hook after provider registered
|
||||
*/
|
||||
public async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* auth provider
|
||||
*/
|
||||
public async auth() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* logout provider
|
||||
*/
|
||||
public async logout() {
|
||||
return;
|
||||
}
|
||||
|
||||
public getToken(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* warp workspace with provider functions
|
||||
* @param workspace
|
||||
* @returns
|
||||
*/
|
||||
public async warpWorkspace(
|
||||
workspace: BlocksuiteWorkspace
|
||||
): Promise<BlocksuiteWorkspace> {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Temporary for public workspace
|
||||
* @param blocksuiteWorkspace
|
||||
* @returns
|
||||
*/
|
||||
public async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
|
||||
return blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* load workspaces
|
||||
**/
|
||||
public async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
* get auth user info
|
||||
* @returns
|
||||
*/
|
||||
public async getUserInfo(): Promise<User | undefined> {
|
||||
return;
|
||||
}
|
||||
|
||||
// async getBlob(id: string): Promise<string | null> {
|
||||
// return await this._blobs.get(id);
|
||||
// }
|
||||
|
||||
// async setBlob(blob: Blob): Promise<string> {
|
||||
// return await this._blobs.set(blob);
|
||||
// }
|
||||
|
||||
/**
|
||||
* clear all local data in provider
|
||||
*/
|
||||
async clear() {
|
||||
// this._blobs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* delete workspace include all data
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async deleteWorkspace(id: string): Promise<void> {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* leave workspace by workspace id
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async leaveWorkspace(id: string): Promise<void> {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* close db link and websocket connection and other resources
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async closeWorkspace(id: string) {
|
||||
id;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* invite workspace member
|
||||
* @param id workspace id
|
||||
*/
|
||||
public async invite(id: string, email: string): Promise<void> {
|
||||
id;
|
||||
email;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove workspace member by permission id
|
||||
* @param permissionId
|
||||
*/
|
||||
public async removeMember(permissionId: number): Promise<void> {
|
||||
permissionId;
|
||||
return;
|
||||
}
|
||||
|
||||
public async publish(id: string, isPublish: boolean): Promise<void> {
|
||||
id;
|
||||
isPublish;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* change workspace meta by workspace id , work for cached list in different provider
|
||||
* @param id
|
||||
* @param meta
|
||||
* @returns
|
||||
*/
|
||||
public async updateWorkspaceMeta(
|
||||
id: string,
|
||||
params: UpdateWorkspaceMetaParams
|
||||
): Promise<void> {
|
||||
id;
|
||||
params;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* create workspace by workspace meta
|
||||
* @param {CreateWorkspaceInfoParams} meta
|
||||
*/
|
||||
public async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
throw new Error(`provider: ${this.id} createWorkspace not implemented`);
|
||||
}
|
||||
|
||||
public async extendWorkspace(
|
||||
workspaceUnit: WorkspaceUnit
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
throw new Error(`provider: ${this.id} extendWorkspace not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
* get user by email
|
||||
* @param {string} id
|
||||
* @param {string} email
|
||||
* @returns
|
||||
*/
|
||||
public async getUserByEmail(id: string, email: string): Promise<User | null> {
|
||||
email;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* link workspace to local caches
|
||||
* @param workspace
|
||||
* @returns
|
||||
*/
|
||||
public async linkLocal(
|
||||
workspace: BlocksuiteWorkspace
|
||||
): Promise<BlocksuiteWorkspace> {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* get workspace members
|
||||
* @param {string} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
public getWorkspaceMembers(workspaceId: string): Promise<Member[]> {
|
||||
workspaceId;
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* accept invitation
|
||||
* @param {string} inviteCode
|
||||
* @returns
|
||||
*/
|
||||
public async acceptInvitation(
|
||||
inviteCode: string
|
||||
): Promise<Permission | null> {
|
||||
inviteCode;
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './affine/affine';
|
@ -1 +0,0 @@
|
||||
export * from './local';
|
@ -1,208 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import * as idb from 'lib0/indexeddb';
|
||||
import { Observable } from 'lib0/observable';
|
||||
|
||||
const customStoreName = 'custom';
|
||||
const updatesStoreName = 'updates';
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 500;
|
||||
|
||||
const {
|
||||
Y: { applyUpdate, transact, encodeStateAsUpdate },
|
||||
} = BlocksuiteWorkspace;
|
||||
|
||||
type Doc = Parameters<typeof transact>[0];
|
||||
|
||||
const fetchUpdates = async (provider: IndexedDBProvider) => {
|
||||
const [updatesStore] = idb.transact(provider.db as IDBDatabase, [
|
||||
updatesStoreName,
|
||||
]); // , 'readonly')
|
||||
if (updatesStore) {
|
||||
const updates = await idb.getAll(
|
||||
updatesStore,
|
||||
idb.createIDBKeyRangeLowerBound(provider._dbref, false)
|
||||
);
|
||||
transact(
|
||||
provider.doc,
|
||||
() => {
|
||||
updates.forEach(val => applyUpdate(provider.doc, val));
|
||||
},
|
||||
provider,
|
||||
false
|
||||
);
|
||||
const lastKey = await idb.getLastKey(updatesStore);
|
||||
provider._dbref = lastKey + 1;
|
||||
const cnt = await idb.count(updatesStore);
|
||||
provider._dbsize = cnt;
|
||||
}
|
||||
return updatesStore;
|
||||
};
|
||||
|
||||
const storeState = (provider: IndexedDBProvider, forceStore = true) =>
|
||||
fetchUpdates(provider).then(updatesStore => {
|
||||
if (
|
||||
updatesStore &&
|
||||
(forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE)
|
||||
) {
|
||||
idb
|
||||
.addAutoKey(updatesStore, encodeStateAsUpdate(provider.doc))
|
||||
.then(() =>
|
||||
idb.del(
|
||||
updatesStore,
|
||||
idb.createIDBKeyRangeUpperBound(provider._dbref, true)
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
idb.count(updatesStore).then(cnt => {
|
||||
provider._dbsize = cnt;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export class IndexedDBProvider extends Observable<string> {
|
||||
doc: Doc;
|
||||
name: string;
|
||||
_dbref: number;
|
||||
_dbsize: number;
|
||||
private _destroyed: boolean;
|
||||
whenSynced: Promise<IndexedDBProvider>;
|
||||
db: IDBDatabase | null;
|
||||
private _db: Promise<IDBDatabase>;
|
||||
private _storeTimeout: number;
|
||||
private _storeTimeoutId: NodeJS.Timeout | null;
|
||||
private _storeUpdate: (update: Uint8Array, origin: any) => void;
|
||||
|
||||
constructor(name: string, doc: Doc) {
|
||||
super();
|
||||
this.doc = doc;
|
||||
this.name = name;
|
||||
this._dbref = 0;
|
||||
this._dbsize = 0;
|
||||
this._destroyed = false;
|
||||
this.db = null;
|
||||
this._db = idb.openDB(name, db =>
|
||||
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||
);
|
||||
|
||||
this.whenSynced = this._db.then(async db => {
|
||||
this.db = db;
|
||||
const currState = encodeStateAsUpdate(doc);
|
||||
const updatesStore = await fetchUpdates(this);
|
||||
if (updatesStore) {
|
||||
await idb.addAutoKey(updatesStore, currState);
|
||||
}
|
||||
if (this._destroyed) {
|
||||
return this;
|
||||
}
|
||||
this.emit('synced', [this]);
|
||||
return this;
|
||||
});
|
||||
|
||||
// Timeout in ms untill data is merged and persisted in idb.
|
||||
this._storeTimeout = 1000;
|
||||
|
||||
this._storeTimeoutId = null;
|
||||
|
||||
this._storeUpdate = (update: Uint8Array, origin: any) => {
|
||||
if (this.db && origin !== this) {
|
||||
const [updatesStore] = idb.transact(
|
||||
/** @type {IDBDatabase} */ this.db,
|
||||
[updatesStoreName]
|
||||
);
|
||||
if (updatesStore) {
|
||||
idb.addAutoKey(updatesStore, update);
|
||||
}
|
||||
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
|
||||
// debounce store call
|
||||
if (this._storeTimeoutId !== null) {
|
||||
clearTimeout(this._storeTimeoutId);
|
||||
}
|
||||
this._storeTimeoutId = setTimeout(() => {
|
||||
storeState(this, false);
|
||||
this._storeTimeoutId = null;
|
||||
}, this._storeTimeout);
|
||||
}
|
||||
}
|
||||
};
|
||||
doc.on('update', this._storeUpdate);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
doc.on('destroy', this.destroy);
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
if (this._storeTimeoutId) {
|
||||
clearTimeout(this._storeTimeoutId);
|
||||
}
|
||||
this.doc.off('update', this._storeUpdate);
|
||||
this.doc.off('destroy', this.destroy);
|
||||
this._destroyed = true;
|
||||
return this._db.then(db => {
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this instance and removes all data from indexeddb.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async clearData(): Promise<void> {
|
||||
return this.destroy().then(() => {
|
||||
idb.deleteDB(this.name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String | number | ArrayBuffer | Date} key
|
||||
* @return {Promise<String | number | ArrayBuffer | Date | any>}
|
||||
*/
|
||||
async get(
|
||||
key: string | number | ArrayBuffer | Date
|
||||
): Promise<string | number | ArrayBuffer | Date | any> {
|
||||
return this._db.then(db => {
|
||||
const [custom] = idb.transact(db, [customStoreName], 'readonly');
|
||||
if (custom) {
|
||||
return idb.get(custom, key);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String | number | ArrayBuffer | Date} key
|
||||
* @param {String | number | ArrayBuffer | Date} value
|
||||
* @return {Promise<String | number | ArrayBuffer | Date>}
|
||||
*/
|
||||
async set(
|
||||
key: string | number | ArrayBuffer | Date,
|
||||
value: string | number | ArrayBuffer | Date
|
||||
): Promise<string | number | ArrayBuffer | Date> {
|
||||
return this._db.then(db => {
|
||||
const [custom] = idb.transact(db, [customStoreName]);
|
||||
if (custom) {
|
||||
return idb.put(custom, value, key);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String | number | ArrayBuffer | Date} key
|
||||
* @return {Promise<undefined>}
|
||||
*/
|
||||
async del(key: string | number | ArrayBuffer | Date): Promise<undefined> {
|
||||
return this._db.then(db => {
|
||||
const [custom] = idb.transact(db, [customStoreName]);
|
||||
if (custom) {
|
||||
return idb.del(custom, key);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
static delete(name: string): Promise<void> {
|
||||
return idb.deleteDB(name);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
import * as idb from 'lib0/indexeddb';
|
||||
|
||||
import { applyUpdate } from '../../../utils';
|
||||
|
||||
const { encodeStateAsUpdate, mergeUpdates } = BlocksuiteWorkspace.Y;
|
||||
|
||||
export const writeUpdatesToLocal = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.id;
|
||||
assert(workspaceId);
|
||||
await idb.deleteDB(workspaceId);
|
||||
const db = await idb.openDB(workspaceId, db =>
|
||||
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||
);
|
||||
const currState = encodeStateAsUpdate(blocksuiteWorkspace.doc);
|
||||
const [updatesStore] = idb.transact(db, ['updates']); // , 'readonly')
|
||||
|
||||
if (updatesStore) {
|
||||
await idb.addAutoKey(updatesStore, currState);
|
||||
}
|
||||
db.close();
|
||||
};
|
||||
|
||||
export const applyLocalUpdates = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.id;
|
||||
assert(workspaceId, 'Blocksuite workspace without room(workspaceId).');
|
||||
const db = await idb.openDB(workspaceId, db =>
|
||||
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||
);
|
||||
|
||||
const [updatesStore] = idb.transact(db, ['updates']); // , 'readonly')
|
||||
if (updatesStore) {
|
||||
const updates = await idb.getAll(updatesStore);
|
||||
const mergedUpdates = mergeUpdates(updates);
|
||||
await applyUpdate(blocksuiteWorkspace, mergedUpdates);
|
||||
}
|
||||
return blocksuiteWorkspace;
|
||||
};
|
@ -1,61 +0,0 @@
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { MessageCenter } from '../../message';
|
||||
import { WorkspaceUnitCollection } from '../../workspace-unit-collection';
|
||||
import { LocalProvider } from './local';
|
||||
|
||||
describe('local provider', () => {
|
||||
const workspaceMetaCollection = new WorkspaceUnitCollection();
|
||||
const provider = new LocalProvider({
|
||||
workspaces: workspaceMetaCollection.createScope(),
|
||||
messageCenter: new MessageCenter(),
|
||||
});
|
||||
|
||||
const workspaceName = 'workspace-test';
|
||||
let workspaceId: string | undefined;
|
||||
|
||||
test('create workspace', async () => {
|
||||
const workspaceUnit = await provider.createWorkspace({
|
||||
name: workspaceName,
|
||||
});
|
||||
workspaceId = workspaceUnit?.id;
|
||||
|
||||
expect(workspaceMetaCollection.workspaces.length).toEqual(1);
|
||||
expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName);
|
||||
});
|
||||
|
||||
test('workspace list cache', async () => {
|
||||
const workspacesMetaCollection1 = new WorkspaceUnitCollection();
|
||||
const provider1 = new LocalProvider({
|
||||
workspaces: workspacesMetaCollection1.createScope(),
|
||||
messageCenter: new MessageCenter(),
|
||||
});
|
||||
await provider1.loadWorkspaces();
|
||||
expect(workspacesMetaCollection1.workspaces.length).toEqual(1);
|
||||
expect(workspacesMetaCollection1.workspaces[0].name).toEqual(workspaceName);
|
||||
expect(workspacesMetaCollection1.workspaces[0].id).toEqual(workspaceId);
|
||||
});
|
||||
|
||||
test('update workspace', async () => {
|
||||
await provider.updateWorkspaceMeta(workspaceId!, {
|
||||
name: '1111',
|
||||
});
|
||||
expect(workspaceMetaCollection.workspaces[0].name).toEqual('1111');
|
||||
});
|
||||
|
||||
test('delete workspace', async () => {
|
||||
expect(workspaceMetaCollection.workspaces.length).toEqual(1);
|
||||
/**
|
||||
* FIXME
|
||||
* If we don't wrap setTimeout,
|
||||
* Running deleteWorkspace will crash the worker, and get error like next line:
|
||||
* InvalidStateError: An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.
|
||||
* */
|
||||
setTimeout(async () => {
|
||||
await provider.deleteWorkspace(workspaceMetaCollection.workspaces[0].id);
|
||||
expect(workspaceMetaCollection.workspaces.length).toEqual(0);
|
||||
}, 10);
|
||||
});
|
||||
});
|
@ -1,125 +0,0 @@
|
||||
import type { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { uuidv4 } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
import { varStorage as storage } from 'lib0/storage';
|
||||
|
||||
import type { WorkspaceUnit } from '../../workspace-unit';
|
||||
import type {
|
||||
CreateWorkspaceInfoParams,
|
||||
ProviderConstructorParams,
|
||||
UpdateWorkspaceMetaParams,
|
||||
WorkspaceMeta0,
|
||||
} from '../base';
|
||||
import { BaseProvider } from '../base';
|
||||
import { IndexedDBProvider } from './indexeddb/indexeddb';
|
||||
import { applyLocalUpdates } from './indexeddb/utils';
|
||||
import { createWorkspaceUnit, loadWorkspaceUnit } from './utils';
|
||||
|
||||
const WORKSPACE_KEY = 'workspaces';
|
||||
|
||||
export class LocalProvider extends BaseProvider {
|
||||
public id = 'local';
|
||||
private _idbMap: Map<BlocksuiteWorkspace, IndexedDBProvider> = new Map();
|
||||
|
||||
constructor(params: ProviderConstructorParams) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
private _storeWorkspaces(workspaceUnits: WorkspaceUnit[]) {
|
||||
storage.setItem(
|
||||
WORKSPACE_KEY,
|
||||
JSON.stringify(
|
||||
workspaceUnits.map(w => {
|
||||
return w.toJSON();
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override async linkLocal(workspace: BlocksuiteWorkspace) {
|
||||
assert(workspace.id);
|
||||
let idb = this._idbMap.get(workspace);
|
||||
if (!idb) {
|
||||
idb = new IndexedDBProvider(workspace.id, workspace.doc);
|
||||
}
|
||||
this._idbMap.set(workspace, idb);
|
||||
this._logger.debug('Local data loaded');
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public override async warpWorkspace(
|
||||
workspace: BlocksuiteWorkspace
|
||||
): Promise<BlocksuiteWorkspace> {
|
||||
assert(workspace.id);
|
||||
await applyLocalUpdates(workspace);
|
||||
await this.linkLocal(workspace);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
override async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
const workspaceStr = storage.getItem(WORKSPACE_KEY);
|
||||
if (workspaceStr) {
|
||||
try {
|
||||
const workspaceMetas = JSON.parse(workspaceStr) as WorkspaceMeta0[];
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspaceMetas.map(meta => {
|
||||
return loadWorkspaceUnit(meta);
|
||||
})
|
||||
);
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
} catch (error) {
|
||||
this._logger.error(`Failed to parse workspaces from storage`);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public override async deleteWorkspace(id: string): Promise<void> {
|
||||
const workspace = this._workspaces.get(id);
|
||||
if (workspace) {
|
||||
IndexedDBProvider.delete(id);
|
||||
this._workspaces.remove(id);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
if (workspace.blocksuiteWorkspace) {
|
||||
this._idbMap.delete(workspace.blocksuiteWorkspace);
|
||||
}
|
||||
} else {
|
||||
this._logger.error(`Failed to delete workspace ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
public override async updateWorkspaceMeta(
|
||||
id: string,
|
||||
meta: UpdateWorkspaceMetaParams
|
||||
) {
|
||||
this._workspaces.update(id, meta);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
name: meta.name,
|
||||
id: uuidv4(),
|
||||
published: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
});
|
||||
this._workspaces.add(workspaceUnit);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
const workspaces = await this.loadWorkspaces();
|
||||
workspaces.forEach(ws => IndexedDBProvider.delete(ws.id));
|
||||
this._storeWorkspaces([]);
|
||||
this._workspaces.clear();
|
||||
this._idbMap.clear();
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import { createBlocksuiteWorkspace } from '../../utils';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { WorkspaceUnit } from '../../workspace-unit';
|
||||
import { setDefaultAvatar } from '../utils';
|
||||
import { applyLocalUpdates, writeUpdatesToLocal } from './indexeddb/utils';
|
||||
|
||||
export const loadWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace({
|
||||
id: workspaceUnit.id,
|
||||
blobOptionsGetter: (k: string) => undefined,
|
||||
});
|
||||
|
||||
await applyLocalUpdates(blocksuiteWorkspace);
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace({
|
||||
id: workspaceUnit.id,
|
||||
blobOptionsGetter: (k: string) => undefined,
|
||||
});
|
||||
blocksuiteWorkspace.meta.setName(workspaceUnit.name);
|
||||
if (!workspaceUnit.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar });
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
await writeUpdatesToLocal(blocksuiteWorkspace);
|
||||
}
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
@ -1,63 +0,0 @@
|
||||
// import assert from 'assert';
|
||||
|
||||
// import { LocalProvider } from '../local/index.js';
|
||||
// import { WebsocketProvider } from './sync.js';
|
||||
|
||||
// export class SelfHostedProvider extends LocalProvider {
|
||||
// static id = 'selfhosted';
|
||||
// private _ws?: WebsocketProvider;
|
||||
|
||||
// constructor() {
|
||||
// super();
|
||||
// }
|
||||
|
||||
// async destroy() {
|
||||
// this._ws?.disconnect();
|
||||
// }
|
||||
|
||||
// async initData() {
|
||||
// const databases = await indexedDB.databases();
|
||||
// await super.initData(
|
||||
// // set locally to true if exists a same name db
|
||||
// databases
|
||||
// .map(db => db.name)
|
||||
// .filter(v => v)
|
||||
// .includes(this._workspace.id)
|
||||
// );
|
||||
|
||||
// const workspace = this._workspace;
|
||||
// const doc = workspace.doc;
|
||||
|
||||
// if (workspace.id) {
|
||||
// try {
|
||||
// // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
// this._ws = new WebsocketProvider(this.host, workspace.id, doc);
|
||||
// await new Promise<void>((resolve, reject) => {
|
||||
// // TODO: synced will also be triggered on reconnection after losing sync
|
||||
// // There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
// assert(this._ws);
|
||||
// this._ws.once('synced', () => resolve());
|
||||
// this._ws.once('lost-connection', () => resolve());
|
||||
// this._ws.once('connection-error', () => reject());
|
||||
// });
|
||||
// this._slots.listAdd.emit({
|
||||
// workspace: workspace.id,
|
||||
// provider: this.id,
|
||||
// locally: true,
|
||||
// });
|
||||
// } catch (e) {
|
||||
// this._logger('Failed to init cloud workspace', e);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // if after update, the space:meta is empty
|
||||
// // then we need to get map with doc
|
||||
// // just a workaround for yjs
|
||||
// doc.getMap('space:meta');
|
||||
// }
|
||||
|
||||
// private get host() {
|
||||
// const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// return `${protocol}//${location.host}/collaboration/`;
|
||||
// }
|
||||
// }
|
@ -1,508 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
/**
|
||||
* @module provider/websocket
|
||||
*/
|
||||
|
||||
/* eslint-env browser */
|
||||
|
||||
// import * as Y from 'yjs'; // eslint-disable-line
|
||||
import * as bc from 'lib0/broadcastchannel';
|
||||
import * as decoding from 'lib0/decoding';
|
||||
import * as encoding from 'lib0/encoding';
|
||||
import * as math from 'lib0/math';
|
||||
import { Observable } from 'lib0/observable';
|
||||
import * as time from 'lib0/time';
|
||||
import * as url from 'lib0/url';
|
||||
import * as authProtocol from 'y-protocols/auth';
|
||||
import * as awarenessProtocol from 'y-protocols/awareness';
|
||||
import * as syncProtocol from 'y-protocols/sync';
|
||||
|
||||
export const messageSync = 0;
|
||||
export const messageQueryAwareness = 3;
|
||||
export const messageAwareness = 1;
|
||||
export const messageAuth = 2;
|
||||
|
||||
/**
|
||||
* encoder, decoder, provider, emitSynced, messageType
|
||||
* @type {Array<function(encoding.Encoder, decoding.Decoder, WebsocketProvider, boolean, number):void>}
|
||||
*/
|
||||
const messageHandlers = [];
|
||||
|
||||
messageHandlers[messageSync] = (
|
||||
encoder,
|
||||
decoder,
|
||||
provider,
|
||||
emitSynced,
|
||||
_messageType
|
||||
) => {
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
const syncMessageType = syncProtocol.readSyncMessage(
|
||||
decoder,
|
||||
encoder,
|
||||
provider.doc,
|
||||
provider
|
||||
);
|
||||
if (
|
||||
emitSynced &&
|
||||
syncMessageType === syncProtocol.messageYjsSyncStep2 &&
|
||||
!provider.synced
|
||||
) {
|
||||
provider.synced = true;
|
||||
}
|
||||
};
|
||||
|
||||
messageHandlers[messageQueryAwareness] = (
|
||||
encoder,
|
||||
_decoder,
|
||||
provider,
|
||||
_emitSynced,
|
||||
_messageType
|
||||
) => {
|
||||
encoding.writeVarUint(encoder, messageAwareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(
|
||||
provider.awareness,
|
||||
Array.from(provider.awareness.getStates().keys())
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
messageHandlers[messageAwareness] = (
|
||||
_encoder,
|
||||
decoder,
|
||||
provider,
|
||||
_emitSynced,
|
||||
_messageType
|
||||
) => {
|
||||
awarenessProtocol.applyAwarenessUpdate(
|
||||
provider.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
provider
|
||||
);
|
||||
};
|
||||
|
||||
messageHandlers[messageAuth] = (
|
||||
_encoder,
|
||||
decoder,
|
||||
provider,
|
||||
_emitSynced,
|
||||
_messageType
|
||||
) => {
|
||||
authProtocol.readAuthMessage(decoder, provider.doc, (_ydoc, reason) =>
|
||||
permissionDeniedHandler(provider, reason)
|
||||
);
|
||||
};
|
||||
|
||||
// @todo - this should depend on awareness.outdatedTime
|
||||
const messageReconnectTimeout = 30000;
|
||||
|
||||
/**
|
||||
* @param {WebsocketProvider} provider
|
||||
* @param {string} reason
|
||||
*/
|
||||
const permissionDeniedHandler = (provider, reason) =>
|
||||
console.warn(`Permission denied to access ${provider.url}.\n${reason}`);
|
||||
|
||||
/**
|
||||
* @param {WebsocketProvider} provider
|
||||
* @param {Uint8Array} buf
|
||||
* @param {boolean} emitSynced
|
||||
* @return {encoding.Encoder}
|
||||
*/
|
||||
const readMessage = (provider, buf, emitSynced) => {
|
||||
const decoder = decoding.createDecoder(buf);
|
||||
const encoder = encoding.createEncoder();
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
const messageHandler = provider.messageHandlers[messageType];
|
||||
if (/** @type {any} */ (messageHandler)) {
|
||||
messageHandler(encoder, decoder, provider, emitSynced, messageType);
|
||||
} else {
|
||||
console.error('Unable to compute message');
|
||||
}
|
||||
return encoder;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {WebsocketProvider} provider
|
||||
*/
|
||||
const setupWS = provider => {
|
||||
if (provider.shouldConnect && provider.ws === null) {
|
||||
const websocket = new provider._WS(provider.url, 'AFFiNE');
|
||||
websocket.binaryType = 'arraybuffer';
|
||||
provider.ws = websocket;
|
||||
provider.wsconnecting = true;
|
||||
provider.wsconnected = false;
|
||||
provider.synced = false;
|
||||
|
||||
websocket.onmessage = event => {
|
||||
provider.wsLastMessageReceived = time.getUnixTime();
|
||||
const encoder = readMessage(provider, new Uint8Array(event.data), true);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
websocket.send(encoding.toUint8Array(encoder));
|
||||
}
|
||||
};
|
||||
websocket.onerror = event => {
|
||||
provider.emit('connection-error', [event, provider]);
|
||||
};
|
||||
websocket.onclose = event => {
|
||||
provider.emit('connection-close', [event, provider]);
|
||||
provider.ws = null;
|
||||
provider.wsconnecting = false;
|
||||
if (provider.wsconnected) {
|
||||
provider.wsconnected = false;
|
||||
provider.synced = false;
|
||||
// update awareness (all users except local left)
|
||||
awarenessProtocol.removeAwarenessStates(
|
||||
provider.awareness,
|
||||
Array.from(provider.awareness.getStates().keys()).filter(
|
||||
client => client !== provider.doc.clientID
|
||||
),
|
||||
provider
|
||||
);
|
||||
provider.emit('status', [
|
||||
{
|
||||
status: 'disconnected',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
provider.wsUnsuccessfulReconnects++;
|
||||
}
|
||||
// Start with no reconnect timeout and increase timeout by
|
||||
// using exponential backoff starting with 100ms
|
||||
setTimeout(
|
||||
setupWS,
|
||||
math.min(
|
||||
math.pow(2, provider.wsUnsuccessfulReconnects) * 100,
|
||||
provider.maxBackoffTime
|
||||
),
|
||||
provider
|
||||
);
|
||||
};
|
||||
websocket.onopen = () => {
|
||||
provider.wsLastMessageReceived = time.getUnixTime();
|
||||
provider.wsconnecting = false;
|
||||
provider.wsconnected = true;
|
||||
provider.wsUnsuccessfulReconnects = 0;
|
||||
provider.emit('status', [
|
||||
{
|
||||
status: 'connected',
|
||||
},
|
||||
]);
|
||||
// always send sync step 1 when connected
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
syncProtocol.writeSyncStep1(encoder, provider.doc);
|
||||
websocket.send(encoding.toUint8Array(encoder));
|
||||
// broadcast local awareness state
|
||||
if (provider.awareness.getLocalState() !== null) {
|
||||
const encoderAwarenessState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessState, messageAwareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoderAwarenessState,
|
||||
awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [
|
||||
provider.doc.clientID,
|
||||
])
|
||||
);
|
||||
websocket.send(encoding.toUint8Array(encoderAwarenessState));
|
||||
}
|
||||
};
|
||||
|
||||
provider.emit('status', [
|
||||
{
|
||||
status: 'connecting',
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {WebsocketProvider} provider
|
||||
* @param {ArrayBuffer} buf
|
||||
*/
|
||||
const broadcastMessage = (provider, buf) => {
|
||||
if (provider.wsconnected) {
|
||||
/** @type {WebSocket} */ (provider.ws).send(buf);
|
||||
}
|
||||
if (provider.bcconnected) {
|
||||
bc.publish(provider.bcChannel, buf, provider);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
|
||||
* The document name is attached to the provided url. I.e. the following example
|
||||
* creates a websocket connection to http://localhost:1234/my-document-name
|
||||
*
|
||||
* @example
|
||||
* import * as Y from 'yjs'
|
||||
* import { WebsocketProvider } from 'y-websocket'
|
||||
* const doc = new Y.Doc()
|
||||
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
|
||||
*
|
||||
* @extends {Observable<string>}
|
||||
*/
|
||||
export class WebsocketProvider extends Observable {
|
||||
/**
|
||||
* @param {string} serverUrl
|
||||
* @param {string} roomname
|
||||
* @param {Y.Doc} doc
|
||||
* @param {object} [opts]
|
||||
* @param {boolean} [opts.connect]
|
||||
* @param {awarenessProtocol.Awareness} [opts.awareness]
|
||||
* @param {Object<string,string>} [opts.params]
|
||||
* @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill
|
||||
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
|
||||
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
|
||||
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
|
||||
*/
|
||||
constructor(
|
||||
serverUrl,
|
||||
roomname,
|
||||
doc,
|
||||
{
|
||||
connect = true,
|
||||
awareness = new awarenessProtocol.Awareness(doc),
|
||||
params = {},
|
||||
WebSocketPolyfill = WebSocket,
|
||||
resyncInterval = -1,
|
||||
maxBackoffTime = 2500,
|
||||
disableBc = false,
|
||||
} = {}
|
||||
) {
|
||||
super();
|
||||
// ensure that url is always ends with /
|
||||
while (serverUrl[serverUrl.length - 1] === '/') {
|
||||
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
|
||||
}
|
||||
const encodedParams = url.encodeQueryParams(params);
|
||||
this.maxBackoffTime = maxBackoffTime;
|
||||
this.bcChannel = serverUrl + '/' + roomname;
|
||||
this.url =
|
||||
serverUrl +
|
||||
'/' +
|
||||
roomname +
|
||||
(encodedParams.length === 0 ? '' : '?' + encodedParams);
|
||||
this.roomname = roomname;
|
||||
this.doc = doc;
|
||||
this._WS = WebSocketPolyfill;
|
||||
this.awareness = awareness;
|
||||
this.wsconnected = false;
|
||||
this.wsconnecting = false;
|
||||
this.bcconnected = false;
|
||||
this.disableBc = disableBc;
|
||||
this.wsUnsuccessfulReconnects = 0;
|
||||
this.messageHandlers = messageHandlers.slice();
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._synced = false;
|
||||
/**
|
||||
* @type {WebSocket?}
|
||||
*/
|
||||
this.ws = null;
|
||||
this.wsLastMessageReceived = 0;
|
||||
/**
|
||||
* Whether to connect to other peers or not
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.shouldConnect = connect;
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this._resyncInterval = 0;
|
||||
if (resyncInterval > 0) {
|
||||
this._resyncInterval = /** @type {any} */ (
|
||||
setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
// resend sync step 1
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
syncProtocol.writeSyncStep1(encoder, doc);
|
||||
this.ws.send(encoding.toUint8Array(encoder));
|
||||
}
|
||||
}, resyncInterval)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} data
|
||||
* @param {any} origin
|
||||
*/
|
||||
this._bcSubscriber = (data, origin) => {
|
||||
if (origin !== this) {
|
||||
const encoder = readMessage(this, new Uint8Array(data), false);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this);
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
|
||||
* @param {Uint8Array} update
|
||||
* @param {any} origin
|
||||
*/
|
||||
this._updateHandler = (update, origin) => {
|
||||
if (origin !== this) {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageSync);
|
||||
syncProtocol.writeUpdate(encoder, update);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
}
|
||||
};
|
||||
this.doc.on('update', this._updateHandler);
|
||||
/**
|
||||
* @param {any} changed
|
||||
* @param {any} _origin
|
||||
*/
|
||||
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
|
||||
const changedClients = added.concat(updated).concat(removed);
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageAwareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
|
||||
);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
};
|
||||
this._unloadHandler = () => {
|
||||
awarenessProtocol.removeAwarenessStates(
|
||||
this.awareness,
|
||||
[doc.clientID],
|
||||
'window unload'
|
||||
);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unload', this._unloadHandler);
|
||||
} else if (typeof process !== 'undefined') {
|
||||
process.on('exit', this._unloadHandler);
|
||||
}
|
||||
awareness.on('update', this._awarenessUpdateHandler);
|
||||
this._checkInterval = /** @type {any} */ (
|
||||
setInterval(() => {
|
||||
if (
|
||||
this.wsconnected &&
|
||||
messageReconnectTimeout <
|
||||
time.getUnixTime() - this.wsLastMessageReceived
|
||||
) {
|
||||
// no message received in a long time - not even your own awareness
|
||||
// updates (which are updated every 15 seconds)
|
||||
/** @type {WebSocket} */ (this.ws).close();
|
||||
}
|
||||
}, messageReconnectTimeout / 10)
|
||||
);
|
||||
if (connect) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get synced() {
|
||||
return this._synced;
|
||||
}
|
||||
|
||||
set synced(state) {
|
||||
if (this._synced !== state) {
|
||||
this._synced = state;
|
||||
this.emit('synced', [state]);
|
||||
this.emit('sync', [state]);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._resyncInterval !== 0) {
|
||||
clearInterval(this._resyncInterval);
|
||||
}
|
||||
clearInterval(this._checkInterval);
|
||||
this.disconnect();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('unload', this._unloadHandler);
|
||||
} else if (typeof process !== 'undefined') {
|
||||
process.off('exit', this._unloadHandler);
|
||||
}
|
||||
this.awareness.off('update', this._awarenessUpdateHandler);
|
||||
this.doc.off('update', this._updateHandler);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
connectBc() {
|
||||
if (this.disableBc) {
|
||||
return;
|
||||
}
|
||||
if (!this.bcconnected) {
|
||||
bc.subscribe(this.bcChannel, this._bcSubscriber);
|
||||
this.bcconnected = true;
|
||||
}
|
||||
// send sync step1 to bc
|
||||
// write sync step 1
|
||||
const encoderSync = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderSync, messageSync);
|
||||
syncProtocol.writeSyncStep1(encoderSync, this.doc);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this);
|
||||
// broadcast local state
|
||||
const encoderState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderState, messageSync);
|
||||
syncProtocol.writeSyncStep2(encoderState, this.doc);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this);
|
||||
// write queryAwareness
|
||||
const encoderAwarenessQuery = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
|
||||
bc.publish(
|
||||
this.bcChannel,
|
||||
encoding.toUint8Array(encoderAwarenessQuery),
|
||||
this
|
||||
);
|
||||
// broadcast local awareness state
|
||||
const encoderAwarenessState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessState, messageAwareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoderAwarenessState,
|
||||
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
||||
this.doc.clientID,
|
||||
])
|
||||
);
|
||||
bc.publish(
|
||||
this.bcChannel,
|
||||
encoding.toUint8Array(encoderAwarenessState),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
disconnectBc() {
|
||||
// broadcast message with local awareness state set to null (indicating disconnect)
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, messageAwareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(
|
||||
this.awareness,
|
||||
[this.doc.clientID],
|
||||
new Map()
|
||||
)
|
||||
);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
if (this.bcconnected) {
|
||||
bc.unsubscribe(this.bcChannel, this._bcSubscriber);
|
||||
this.bcconnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldConnect = false;
|
||||
this.disconnectBc();
|
||||
if (this.ws !== null) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.shouldConnect = true;
|
||||
if (!this.wsconnected && this.ws === null) {
|
||||
setupWS(this);
|
||||
this.connectBc();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { BlobSyncState } from '@blocksuite/store';
|
||||
import { Slot } from '@blocksuite/store';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import type {
|
||||
BlobId,
|
||||
BlobProvider,
|
||||
BlobSyncStateChangeEvent,
|
||||
BlobURL,
|
||||
} from '@blocksuite/store/dist/persistence/blob/types';
|
||||
|
||||
import * as ipcMethods from '../ipc/methods';
|
||||
|
||||
export class IPCBlobProvider implements BlobProvider {
|
||||
#ipc = ipcMethods;
|
||||
|
||||
#workspace: string;
|
||||
|
||||
readonly slots = {
|
||||
onBlobSyncStateChange: new Slot<BlobSyncStateChangeEvent>(),
|
||||
};
|
||||
|
||||
private constructor(workspace: string) {
|
||||
this.#workspace = workspace;
|
||||
}
|
||||
|
||||
static async init(workspace: string): Promise<IPCBlobProvider> {
|
||||
const provider = new IPCBlobProvider(workspace);
|
||||
return provider;
|
||||
}
|
||||
|
||||
get uploading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get blobs() {
|
||||
// TODO: implement blob list in Octobase
|
||||
return Promise.resolve([]) as Promise<string[]>;
|
||||
}
|
||||
|
||||
async get(id: BlobId): Promise<BlobURL | null> {
|
||||
const blobArray = await this.#ipc.getBlob({
|
||||
id,
|
||||
});
|
||||
// Make a Blob from the bytes
|
||||
const blob = new Blob([new Uint8Array(blobArray)], { type: 'image/bmp' });
|
||||
this.slots.onBlobSyncStateChange.emit({
|
||||
id,
|
||||
state: BlobSyncState.Success,
|
||||
});
|
||||
return window.URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async set(blob: Blob): Promise<BlobId> {
|
||||
// TODO: skip if already has
|
||||
const blobID = await this.#ipc.putBlob({
|
||||
blob: Array.from(new Uint8Array(await blob.arrayBuffer())),
|
||||
});
|
||||
this.slots.onBlobSyncStateChange.emit({
|
||||
id: blobID,
|
||||
state: BlobSyncState.Success,
|
||||
});
|
||||
return blobID;
|
||||
}
|
||||
|
||||
async delete(id: BlobId): Promise<void> {
|
||||
// TODO: implement blob delete in Octobase
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
// TODO: implement blob clear in Octobase, use workspace id #workspace
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
import type { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { User } from '../../types';
|
||||
import { applyUpdate } from '../../utils';
|
||||
import type { WorkspaceUnit } from '../../workspace-unit';
|
||||
import type {
|
||||
CreateWorkspaceInfoParams,
|
||||
ProviderConstructorParams,
|
||||
} from '../base';
|
||||
import { LocalProvider } from '../local';
|
||||
import { loadWorkspaceUnit } from '../local/utils';
|
||||
import { IPCBlobProvider } from './blocksuite-provider/blob';
|
||||
import type { IPCMethodsType } from './ipc/methods';
|
||||
import type { WorkspaceWithPermission } from './ipc/types/workspace';
|
||||
import { createWorkspaceUnit } from './utils';
|
||||
|
||||
/**
|
||||
* init - createUser - create first workspace and ydoc - loadWorkspace - return the first workspace - wrapWorkspace - #initDocFromIPC - applyUpdate - on('update') - updateYDocument
|
||||
*
|
||||
* (init - createUser - error) loadWorkspace - return the first workspace - wrapWorkspace - #initDocFromIPC - applyUpdate - on('update') - updateYDocument
|
||||
*/
|
||||
export class TauriIPCProvider extends LocalProvider {
|
||||
public id = 'tauri-ipc';
|
||||
static defaultUserEmail = 'xxx@xx.xx';
|
||||
|
||||
/**
|
||||
* // TODO: We only have one user in this version of app client. But may support switch user later.
|
||||
*/
|
||||
#userID?: string;
|
||||
#ipc: IPCMethodsType | undefined;
|
||||
|
||||
constructor(params: ProviderConstructorParams) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
async init(ipc?: IPCMethodsType) {
|
||||
if (ipc) {
|
||||
this.#ipc = ipc;
|
||||
} else {
|
||||
this.#ipc = await import('./ipc/methods');
|
||||
}
|
||||
try {
|
||||
const user = await this.#ipc?.getUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
});
|
||||
this.#userID = user.id;
|
||||
} catch (error) {
|
||||
// maybe user not existed, we create a default user if we don't have one.
|
||||
try {
|
||||
const user = await this.#ipc?.createUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
name: 'xxx',
|
||||
password: 'xxx',
|
||||
avatar_url: '',
|
||||
});
|
||||
this.#userID = user.id;
|
||||
} catch (error) {
|
||||
// maybe user existed, which can be omited
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get auth user info
|
||||
* @returns
|
||||
*/
|
||||
public async getUserInfo(): Promise<User | undefined> {
|
||||
const user = await this.#ipc?.getUser({
|
||||
email: TauriIPCProvider.defaultUserEmail,
|
||||
});
|
||||
if (user?.name !== undefined) {
|
||||
return {
|
||||
...user,
|
||||
avatar: user?.avatar_url || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async #initDocFromIPC(
|
||||
workspaceID: string,
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) {
|
||||
this._logger.debug(`Loading ${workspaceID}...`);
|
||||
const result = await this.#ipc?.getYDocument({ id: workspaceID });
|
||||
if (result) {
|
||||
const updates = result.updates.map(
|
||||
binaryUpdate => new Uint8Array(binaryUpdate)
|
||||
);
|
||||
|
||||
const mergedUpdate = Y.mergeUpdates(updates);
|
||||
await applyUpdate(blocksuiteWorkspace, mergedUpdate);
|
||||
this._logger.debug(`Loaded: ${workspaceID}`);
|
||||
}
|
||||
}
|
||||
|
||||
async #connectDocToIPC(
|
||||
workspaceID: string,
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) {
|
||||
this._logger.debug(`Connecting yDoc for ${workspaceID}...`);
|
||||
blocksuiteWorkspace.doc.on('update', async (update: Uint8Array) => {
|
||||
try {
|
||||
const binary = Y.encodeStateAsUpdate(blocksuiteWorkspace.doc);
|
||||
const success = await this.#ipc?.updateYDocument({
|
||||
update: Array.from(binary),
|
||||
id: workspaceID,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(`YDoc update failed, id: ${workspaceID}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: write error log to disk, and add button to open them in settings panel
|
||||
this._logger.error("#yDocument.on('update'", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await super.clear();
|
||||
}
|
||||
|
||||
override async warpWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
|
||||
const { id } = blocksuiteWorkspace;
|
||||
assert(id);
|
||||
|
||||
(await blocksuiteWorkspace.blobs)?.setProvider(
|
||||
await IPCBlobProvider.init(id)
|
||||
);
|
||||
await this.#initDocFromIPC(id, blocksuiteWorkspace);
|
||||
await this.#connectDocToIPC(id, blocksuiteWorkspace);
|
||||
|
||||
return blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
this._logger.debug('Creating client app workspace');
|
||||
assert(this.#ipc);
|
||||
assert(this.#userID);
|
||||
const { id } = await this.#ipc.createWorkspace({
|
||||
name: meta.name,
|
||||
// TODO: get userID here
|
||||
user_id: this.#userID,
|
||||
});
|
||||
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
name: meta.name,
|
||||
id,
|
||||
published: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: this.id,
|
||||
});
|
||||
this._workspaces.add(workspaceUnit);
|
||||
const doc = workspaceUnit?.blocksuiteWorkspace?.doc;
|
||||
if (doc) {
|
||||
const update = Y.encodeStateAsUpdate(doc);
|
||||
const success = await this.#ipc?.updateYDocument({
|
||||
update: Array.from(update),
|
||||
id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(`YDoc update failed, id: ${id}`);
|
||||
}
|
||||
}
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
override async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
assert(this.#ipc);
|
||||
assert(this.#userID);
|
||||
const { workspaces } = await this.#ipc.getWorkspaces({
|
||||
user_id: this.#userID,
|
||||
});
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspaces.map((meta: WorkspaceWithPermission) => {
|
||||
return loadWorkspaceUnit({
|
||||
...meta,
|
||||
memberCount: 1,
|
||||
// TODO: load name here
|
||||
name: '',
|
||||
provider: this.id,
|
||||
syncMode: 'all',
|
||||
});
|
||||
})
|
||||
);
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import type { GetBlob, PutBlob } from './types/blob';
|
||||
import type {
|
||||
GetDocumentParameter,
|
||||
GetDocumentResponse,
|
||||
YDocumentUpdate,
|
||||
} from './types/document';
|
||||
import type { CreateUser, GetUserParameters } from './types/user';
|
||||
import type {
|
||||
CreateWorkspace,
|
||||
CreateWorkspaceResult,
|
||||
GetWorkspace,
|
||||
GetWorkspaceResult,
|
||||
GetWorkspaces,
|
||||
GetWorkspacesResult,
|
||||
User,
|
||||
} from './types/workspace';
|
||||
|
||||
export interface IPCMethodsType {
|
||||
updateYDocument: typeof updateYDocument;
|
||||
getYDocument: typeof getYDocument;
|
||||
createWorkspace: typeof createWorkspace;
|
||||
getWorkspaces: typeof getWorkspaces;
|
||||
getWorkspace: typeof getWorkspace;
|
||||
putBlob: typeof putBlob;
|
||||
getBlob: typeof getBlob;
|
||||
createUser: typeof createUser;
|
||||
getUser: typeof getUser;
|
||||
}
|
||||
|
||||
export const updateYDocument = async (parameters: YDocumentUpdate) =>
|
||||
await invoke<boolean>('update_y_document', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getYDocument = async (parameters: GetDocumentParameter) =>
|
||||
await invoke<GetDocumentResponse>('get_doc', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const createWorkspace = async (parameters: CreateWorkspace) =>
|
||||
await invoke<CreateWorkspaceResult>('create_workspace', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getWorkspaces = async (parameters: GetWorkspaces) =>
|
||||
await invoke<GetWorkspacesResult>('get_workspaces', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getWorkspace = async (parameters: GetWorkspace) =>
|
||||
await invoke<GetWorkspaceResult>('get_workspace', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const putBlob = async (parameters: PutBlob) =>
|
||||
await invoke<string>('put_blob', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getBlob = async (parameters: GetBlob) =>
|
||||
await invoke<number[]>('get_blob', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
/**
|
||||
* This will create a private workspace too.
|
||||
* @returns
|
||||
*/
|
||||
export const createUser = async (parameters: CreateUser) =>
|
||||
await invoke<User>('create_user', {
|
||||
parameters,
|
||||
});
|
||||
|
||||
export const getUser = async (parameters: GetUserParameters) =>
|
||||
await invoke<User>('get_user', {
|
||||
parameters,
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IBlobParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["Put"],
|
||||
"properties": {
|
||||
"Put": {
|
||||
"$ref": "#/definitions/PutBlob"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["Get"],
|
||||
"properties": {
|
||||
"Get": {
|
||||
"$ref": "#/definitions/GetBlob"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"GetBlob": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"PutBlob": {
|
||||
"type": "object",
|
||||
"required": ["blob"],
|
||||
"properties": {
|
||||
"blob": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"workspace_id": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IBlobParameters =
|
||||
| {
|
||||
Put: PutBlob;
|
||||
}
|
||||
| {
|
||||
Get: GetBlob;
|
||||
};
|
||||
|
||||
export interface PutBlob {
|
||||
blob: number[];
|
||||
workspace_id?: string | null;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetBlob {
|
||||
id: string;
|
||||
workspace_id?: string | null;
|
||||
[k: string]: unknown;
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IDocumentParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["YDocumentUpdate"],
|
||||
"properties": {
|
||||
"YDocumentUpdate": {
|
||||
"$ref": "#/definitions/YDocumentUpdate"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateDocumentParameter"],
|
||||
"properties": {
|
||||
"CreateDocumentParameter": {
|
||||
"$ref": "#/definitions/CreateDocumentParameter"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetDocumentParameter"],
|
||||
"properties": {
|
||||
"GetDocumentParameter": {
|
||||
"$ref": "#/definitions/GetDocumentParameter"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetDocumentResponse"],
|
||||
"properties": {
|
||||
"GetDocumentResponse": {
|
||||
"$ref": "#/definitions/GetDocumentResponse"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateDocumentParameter": {
|
||||
"type": "object",
|
||||
"required": ["workspace_id", "workspace_name"],
|
||||
"properties": {
|
||||
"workspace_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDocumentParameter": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetDocumentResponse": {
|
||||
"type": "object",
|
||||
"required": ["updates"],
|
||||
"properties": {
|
||||
"updates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"YDocumentUpdate": {
|
||||
"type": "object",
|
||||
"required": ["id", "update"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"update": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IDocumentParameters =
|
||||
| {
|
||||
YDocumentUpdate: YDocumentUpdate;
|
||||
}
|
||||
| {
|
||||
CreateDocumentParameter: CreateDocumentParameter;
|
||||
}
|
||||
| {
|
||||
GetDocumentParameter: GetDocumentParameter;
|
||||
}
|
||||
| {
|
||||
GetDocumentResponse: GetDocumentResponse;
|
||||
};
|
||||
|
||||
export interface YDocumentUpdate {
|
||||
id: string;
|
||||
update: number[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CreateDocumentParameter {
|
||||
workspace_id: string;
|
||||
workspace_name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetDocumentParameter {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetDocumentResponse {
|
||||
updates: number[][];
|
||||
[k: string]: unknown;
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IUserParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateUser"],
|
||||
"properties": {
|
||||
"CreateUser": {
|
||||
"$ref": "#/definitions/CreateUser"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["User"],
|
||||
"properties": {
|
||||
"User": {
|
||||
"$ref": "#/definitions/User"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetUserParameters"],
|
||||
"properties": {
|
||||
"GetUserParameters": {
|
||||
"$ref": "#/definitions/GetUserParameters"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateUser": {
|
||||
"type": "object",
|
||||
"required": ["email", "name", "password"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetUserParameters": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "email", "id", "name"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IUserParameters =
|
||||
| {
|
||||
CreateUser: CreateUser;
|
||||
}
|
||||
| {
|
||||
User: User;
|
||||
}
|
||||
| {
|
||||
GetUserParameters: GetUserParameters;
|
||||
};
|
||||
|
||||
export interface CreateUser {
|
||||
avatar_url?: string | null;
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface User {
|
||||
avatar_url?: string | null;
|
||||
created_at: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetUserParameters {
|
||||
email: string;
|
||||
[k: string]: unknown;
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "IWorkspaceParameters",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateWorkspace"],
|
||||
"properties": {
|
||||
"CreateWorkspace": {
|
||||
"$ref": "#/definitions/CreateWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspace"],
|
||||
"properties": {
|
||||
"GetWorkspace": {
|
||||
"$ref": "#/definitions/GetWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspaces"],
|
||||
"properties": {
|
||||
"GetWorkspaces": {
|
||||
"$ref": "#/definitions/GetWorkspaces"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspaceResult"],
|
||||
"properties": {
|
||||
"GetWorkspaceResult": {
|
||||
"$ref": "#/definitions/GetWorkspaceResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["GetWorkspacesResult"],
|
||||
"properties": {
|
||||
"GetWorkspacesResult": {
|
||||
"$ref": "#/definitions/GetWorkspacesResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["UpdateWorkspace"],
|
||||
"properties": {
|
||||
"UpdateWorkspace": {
|
||||
"$ref": "#/definitions/UpdateWorkspace"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["CreateWorkspaceResult"],
|
||||
"properties": {
|
||||
"CreateWorkspaceResult": {
|
||||
"$ref": "#/definitions/CreateWorkspaceResult"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CreateWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["name", "user_id"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "only set name, avatar is update in datacenter to yDoc directly",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateWorkspaceResult": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspaceResult": {
|
||||
"type": "object",
|
||||
"required": ["workspace"],
|
||||
"properties": {
|
||||
"workspace": {
|
||||
"$ref": "#/definitions/WorkspaceDetail"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspaces": {
|
||||
"type": "object",
|
||||
"required": ["user_id"],
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetWorkspacesResult": {
|
||||
"type": "object",
|
||||
"required": ["workspaces"],
|
||||
"properties": {
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/WorkspaceWithPermission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionType": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1, 10, 99]
|
||||
},
|
||||
"UpdateWorkspace": {
|
||||
"type": "object",
|
||||
"required": ["id", "public"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "email", "id", "name"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkspaceDetail": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "id", "member_count", "public", "type"],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"member_count": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"owner": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/User"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/WorkspaceType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WorkspaceType": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1]
|
||||
},
|
||||
"WorkspaceWithPermission": {
|
||||
"type": "object",
|
||||
"required": ["created_at", "id", "permission", "public", "type"],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permission": {
|
||||
"$ref": "#/definitions/PermissionType"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/WorkspaceType"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type IWorkspaceParameters =
|
||||
| {
|
||||
CreateWorkspace: CreateWorkspace;
|
||||
}
|
||||
| {
|
||||
GetWorkspace: GetWorkspace;
|
||||
}
|
||||
| {
|
||||
GetWorkspaces: GetWorkspaces;
|
||||
}
|
||||
| {
|
||||
GetWorkspaceResult: GetWorkspaceResult;
|
||||
}
|
||||
| {
|
||||
GetWorkspacesResult: GetWorkspacesResult;
|
||||
}
|
||||
| {
|
||||
UpdateWorkspace: UpdateWorkspace;
|
||||
}
|
||||
| {
|
||||
CreateWorkspaceResult: CreateWorkspaceResult;
|
||||
};
|
||||
export type WorkspaceType = 0 | 1;
|
||||
export type PermissionType = 0 | 1 | 10 | 99;
|
||||
|
||||
export interface CreateWorkspace {
|
||||
/**
|
||||
* only set name, avatar is update in datacenter to yDoc directly
|
||||
*/
|
||||
name: string;
|
||||
user_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspace {
|
||||
id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspaces {
|
||||
user_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspaceResult {
|
||||
workspace: WorkspaceDetail;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface WorkspaceDetail {
|
||||
created_at: number;
|
||||
id: string;
|
||||
member_count: number;
|
||||
owner?: User | null;
|
||||
public: boolean;
|
||||
type: WorkspaceType;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface User {
|
||||
avatar_url?: string | null;
|
||||
created_at: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface GetWorkspacesResult {
|
||||
workspaces: WorkspaceWithPermission[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface WorkspaceWithPermission {
|
||||
created_at: number;
|
||||
id: string;
|
||||
permission: PermissionType;
|
||||
public: boolean;
|
||||
type: WorkspaceType;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface UpdateWorkspace {
|
||||
id: number;
|
||||
public: boolean;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CreateWorkspaceResult {
|
||||
id: string;
|
||||
name: string;
|
||||
[k: string]: unknown;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { createBlocksuiteWorkspace } from '../../utils';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { WorkspaceUnit } from '../../workspace-unit';
|
||||
import { setDefaultAvatar } from '../utils';
|
||||
import { IPCBlobProvider } from './blocksuite-provider/blob';
|
||||
|
||||
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace({
|
||||
id: workspaceUnit.id,
|
||||
blobOptionsGetter: (k: string) => undefined,
|
||||
});
|
||||
blocksuiteWorkspace.meta.setName(workspaceUnit.name);
|
||||
(await blocksuiteWorkspace.blobs)?.setProvider(
|
||||
await IPCBlobProvider.init(workspaceUnit.id)
|
||||
);
|
||||
if (!workspaceUnit.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar });
|
||||
}
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import type { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import assert from 'assert';
|
||||
|
||||
import { getDefaultHeadImgBlob } from '../utils';
|
||||
|
||||
export const setDefaultAvatar = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const blob = await getDefaultHeadImgBlob(blocksuiteWorkspace.meta.name ?? '');
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
const blobStorage = await blocksuiteWorkspace.blobs;
|
||||
assert(blobStorage, 'No blob storage');
|
||||
const avatar = await blobStorage.set(blob);
|
||||
if (avatar) {
|
||||
blocksuiteWorkspace.meta.setAvatar(avatar);
|
||||
}
|
||||
};
|
@ -1,89 +0,0 @@
|
||||
import {
|
||||
clear,
|
||||
createStore,
|
||||
del,
|
||||
entries,
|
||||
get,
|
||||
keys,
|
||||
set,
|
||||
setMany,
|
||||
} from 'idb-keyval';
|
||||
|
||||
export type ConfigStore<T = any> = {
|
||||
get: (key: string) => Promise<T | undefined>;
|
||||
set: (key: string, value: T) => Promise<void>;
|
||||
setMany: (values: [string, T][]) => Promise<void>;
|
||||
keys: () => Promise<string[]>;
|
||||
entries: () => Promise<[string, T][]>;
|
||||
delete: (key: string) => Promise<void>;
|
||||
clear: () => Promise<void>;
|
||||
};
|
||||
|
||||
const initialIndexedDB = <T = any>(database: string): ConfigStore<T> => {
|
||||
const store = createStore(`affine:${database}`, 'database');
|
||||
return {
|
||||
get: (key: string) => get<T>(key, store),
|
||||
set: (key: string, value: T) => set(key, value, store),
|
||||
setMany: (values: [string, T][]) => setMany(values, store),
|
||||
keys: () => keys(store),
|
||||
entries: () => entries(store),
|
||||
delete: (key: string) => del(key, store),
|
||||
clear: () => clear(store),
|
||||
};
|
||||
};
|
||||
|
||||
const scopedIndexedDB = () => {
|
||||
const idb = initialIndexedDB('global');
|
||||
const storeCache = new Map<string, Readonly<ConfigStore>>();
|
||||
|
||||
return <T = any>(scope: string): Readonly<ConfigStore<T>> => {
|
||||
if (!storeCache.has(scope)) {
|
||||
const prefix = `${scope}:`;
|
||||
const store = {
|
||||
get: async (key: string) => idb.get(prefix + key),
|
||||
set: (key: string, value: T) => idb.set(prefix + key, value),
|
||||
setMany: (values: [string, T][]) =>
|
||||
idb.setMany(values.map(([k, v]) => [`${scope}:${k}`, v])),
|
||||
keys: () =>
|
||||
idb
|
||||
.keys()
|
||||
.then(keys =>
|
||||
keys
|
||||
.filter(k => k.startsWith(prefix))
|
||||
.map(k => k.slice(prefix.length))
|
||||
),
|
||||
entries: () =>
|
||||
idb
|
||||
.entries()
|
||||
.then(entries =>
|
||||
entries
|
||||
.filter(([k]) => k.startsWith(prefix))
|
||||
.map(([k, v]) => [k.slice(prefix.length), v] as [string, T])
|
||||
),
|
||||
delete: (key: string) => idb.delete(prefix + key),
|
||||
clear: async () => {
|
||||
await idb
|
||||
.keys()
|
||||
.then(keys =>
|
||||
Promise.all(
|
||||
keys.filter(k => k.startsWith(prefix)).map(k => del(k))
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
storeCache.set(scope, store);
|
||||
}
|
||||
|
||||
return storeCache.get(scope) as ConfigStore<T>;
|
||||
};
|
||||
};
|
||||
|
||||
let lazyKVConfigure: ReturnType<typeof scopedIndexedDB> | undefined = undefined;
|
||||
|
||||
export const getKVConfigure = (scope: string) => {
|
||||
if (!lazyKVConfigure) {
|
||||
lazyKVConfigure = scopedIndexedDB();
|
||||
}
|
||||
return lazyKVConfigure(scope);
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
// export type WorkspaceInfo = {
|
||||
// name: string;
|
||||
// id: string;
|
||||
// isPublish?: boolean;
|
||||
// avatar?: string;
|
||||
// owner?: User;
|
||||
// isLocal?: boolean;
|
||||
// memberCount: number;
|
||||
// provider: string;
|
||||
// };
|
||||
import type { DebugLogger } from '@affine/debug';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
__TAURI_IPC__: Function;
|
||||
}
|
||||
}
|
||||
|
||||
export type User = {
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
// export type WorkspaceMeta = Pick<WorkspaceInfo, 'name' | 'avatar'>;
|
||||
export type Logger = DebugLogger;
|
||||
|
||||
export type Message = {
|
||||
code: number;
|
||||
message: string;
|
||||
provider: string;
|
||||
};
|
@ -1,77 +0,0 @@
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { StoreOptions } from '@blocksuite/store';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
|
||||
export const createBlocksuiteWorkspace = (options: StoreOptions) => {
|
||||
return new BlocksuiteWorkspace({
|
||||
defaultFlags: {},
|
||||
isSSR: typeof window === 'undefined',
|
||||
...options,
|
||||
})
|
||||
.register(AffineSchemas)
|
||||
.register(__unstableSchemas);
|
||||
};
|
||||
|
||||
const DefaultHeadImgColors = [
|
||||
['#C6F2F3', '#0C6066'],
|
||||
['#FFF5AB', '#896406'],
|
||||
['#FFCCA7', '#8F4500'],
|
||||
['#FFCECE', '#AF1212'],
|
||||
['#E3DEFF', '#511AAB'],
|
||||
];
|
||||
|
||||
export async function getDefaultHeadImgBlob(
|
||||
workspaceName: string
|
||||
): Promise<Blob | null> {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = 100;
|
||||
canvas.width = 100;
|
||||
if (!canvas.getContext) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
if (ctx) {
|
||||
const randomNumber = Math.floor(Math.random() * 5);
|
||||
const randomColor = DefaultHeadImgColors[randomNumber];
|
||||
ctx.fillStyle = randomColor[0];
|
||||
ctx.fillRect(0, 0, 100, 100);
|
||||
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
|
||||
ctx.fillStyle = randomColor[1];
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(workspaceName[0], 50, 50);
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}, 'image/png');
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const applyUpdate = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
updates: Uint8Array
|
||||
) => {
|
||||
if (updates && updates.byteLength) {
|
||||
await new Promise(resolve => {
|
||||
// FIXME: if we merge two empty doc, there will no update event.
|
||||
// So we set a timer to cancel update listener.
|
||||
const doc = blocksuiteWorkspace.doc;
|
||||
const timer = setTimeout(() => {
|
||||
doc.off('update', resolve);
|
||||
resolve(undefined);
|
||||
}, 1000);
|
||||
doc.once('update', () => {
|
||||
clearTimeout(timer);
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates));
|
||||
});
|
||||
}
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { WorkspaceUnit } from './workspace-unit';
|
||||
import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection';
|
||||
import { WorkspaceUnitCollection } from './workspace-unit-collection';
|
||||
|
||||
describe('workspace meta collection observable', () => {
|
||||
const workspaceUnitCollection = new WorkspaceUnitCollection();
|
||||
|
||||
const scope = workspaceUnitCollection.createScope();
|
||||
|
||||
test('add workspace', () => {
|
||||
workspaceUnitCollection.once(
|
||||
'change',
|
||||
(event: WorkspaceUnitCollectionChangeEvent) => {
|
||||
expect(event.added?.[0]?.id).toEqual('123');
|
||||
}
|
||||
);
|
||||
const workspaceUnit = new WorkspaceUnit({
|
||||
id: '123',
|
||||
name: 'test',
|
||||
avatar: undefined,
|
||||
memberCount: 1,
|
||||
provider: '',
|
||||
syncMode: 'core',
|
||||
});
|
||||
scope.add(workspaceUnit);
|
||||
});
|
||||
|
||||
test('list workspace', () => {
|
||||
const list = scope.list();
|
||||
expect(list.length).toEqual(1);
|
||||
expect(list[0].id).toEqual('123');
|
||||
});
|
||||
|
||||
test('get workspace', () => {
|
||||
expect(scope.get('123')?.id).toEqual('123');
|
||||
});
|
||||
|
||||
test('update workspace', () => {
|
||||
workspaceUnitCollection.once(
|
||||
'change',
|
||||
(event: WorkspaceUnitCollectionChangeEvent) => {
|
||||
expect(event.updated?.name).toEqual('demo');
|
||||
}
|
||||
);
|
||||
scope.update('123', { name: 'demo' });
|
||||
});
|
||||
|
||||
test('get workspace form other scope', () => {
|
||||
const scope1 = workspaceUnitCollection.createScope();
|
||||
expect(scope1.get('123')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('delete workspace', () => {
|
||||
workspaceUnitCollection.once(
|
||||
'change',
|
||||
(event: WorkspaceUnitCollectionChangeEvent) => {
|
||||
expect(event.deleted?.[0]?.id).toEqual('123');
|
||||
}
|
||||
);
|
||||
scope.remove('123');
|
||||
});
|
||||
});
|
@ -1,177 +0,0 @@
|
||||
import { Observable } from 'lib0/observable';
|
||||
|
||||
import type {
|
||||
UpdateWorkspaceUnitParams,
|
||||
WorkspaceUnit,
|
||||
} from './workspace-unit';
|
||||
|
||||
export interface WorkspaceUnitCollectionScope {
|
||||
get: (workspaceId: string) => WorkspaceUnit | undefined;
|
||||
list: () => WorkspaceUnit[];
|
||||
add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void;
|
||||
remove: (workspaceId: string | string[], isUpdate?: boolean) => boolean;
|
||||
clear: (isUpdate?: boolean) => void;
|
||||
update: (
|
||||
workspaceId: string,
|
||||
workspaceUnit: UpdateWorkspaceUnitParams
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface WorkspaceUnitCollectionChangeEvent {
|
||||
added?: WorkspaceUnit[];
|
||||
deleted?: WorkspaceUnit[];
|
||||
updated?: WorkspaceUnit;
|
||||
}
|
||||
|
||||
export class WorkspaceUnitCollection {
|
||||
private _events = new Observable();
|
||||
private _workspaceUnitMap = new Map<string, WorkspaceUnit>();
|
||||
|
||||
get workspaces(): WorkspaceUnit[] {
|
||||
return Array.from(this._workspaceUnitMap.values());
|
||||
}
|
||||
|
||||
public on(
|
||||
type: 'change',
|
||||
callback: (event: WorkspaceUnitCollectionChangeEvent) => void
|
||||
) {
|
||||
this._events.on(type, callback);
|
||||
}
|
||||
|
||||
public off(
|
||||
type: 'change',
|
||||
callback: (event: WorkspaceUnitCollectionChangeEvent) => void
|
||||
) {
|
||||
this._events.off(type, callback);
|
||||
}
|
||||
|
||||
public once(
|
||||
type: 'change',
|
||||
callback: (event: WorkspaceUnitCollectionChangeEvent) => void
|
||||
) {
|
||||
this._events.once(type, callback);
|
||||
}
|
||||
|
||||
find(workspaceId: string) {
|
||||
return this._workspaceUnitMap.get(workspaceId);
|
||||
}
|
||||
|
||||
createScope(): WorkspaceUnitCollectionScope {
|
||||
const scopedWorkspaceIds = new Set<string>();
|
||||
|
||||
const get = (workspaceId: string) => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return;
|
||||
}
|
||||
return this._workspaceUnitMap.get(workspaceId);
|
||||
};
|
||||
|
||||
const add = (workspaceUnit: WorkspaceUnit | WorkspaceUnit[]) => {
|
||||
const workspaceUnits = Array.isArray(workspaceUnit)
|
||||
? workspaceUnit
|
||||
: [workspaceUnit];
|
||||
let added = false;
|
||||
|
||||
workspaceUnits.forEach(workspaceUnit => {
|
||||
if (this._workspaceUnitMap.has(workspaceUnit.id)) {
|
||||
// FIXME: multiple add same workspace
|
||||
return;
|
||||
}
|
||||
added = true;
|
||||
this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit);
|
||||
scopedWorkspaceIds.add(workspaceUnit.id);
|
||||
});
|
||||
|
||||
if (!added) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
added: workspaceUnits,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
};
|
||||
|
||||
const remove = (workspaceId: string | string[], isUpdate = true) => {
|
||||
const workspaceIds = Array.isArray(workspaceId)
|
||||
? workspaceId
|
||||
: [workspaceId];
|
||||
const workspaceUnits: WorkspaceUnit[] = [];
|
||||
|
||||
workspaceIds.forEach(workspaceId => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return;
|
||||
}
|
||||
const workspaceUnit = this._workspaceUnitMap.get(workspaceId);
|
||||
if (workspaceUnit) {
|
||||
const ret = this._workspaceUnitMap.delete(workspaceId);
|
||||
// If deletion failed, return.
|
||||
if (!ret) {
|
||||
return;
|
||||
}
|
||||
|
||||
workspaceUnits.push(workspaceUnit);
|
||||
scopedWorkspaceIds.delete(workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
if (!workspaceUnits.length) {
|
||||
return false;
|
||||
}
|
||||
if (isUpdate) {
|
||||
this._events.emit('change', [
|
||||
{
|
||||
deleted: workspaceUnits,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const clear = (isUpdate = true) => {
|
||||
remove(Array.from(scopedWorkspaceIds), isUpdate);
|
||||
};
|
||||
|
||||
const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceUnit = this._workspaceUnitMap.get(workspaceId);
|
||||
if (!workspaceUnit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
workspaceUnit.update(meta);
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
updated: workspaceUnit,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
};
|
||||
|
||||
// TODO: need to optimize
|
||||
const list = () => {
|
||||
const workspaceUnits: WorkspaceUnit[] = [];
|
||||
scopedWorkspaceIds.forEach(id => {
|
||||
const workspaceUnit = this._workspaceUnitMap.get(id);
|
||||
if (workspaceUnit) {
|
||||
workspaceUnits.push(workspaceUnit);
|
||||
}
|
||||
});
|
||||
return workspaceUnits;
|
||||
};
|
||||
|
||||
return {
|
||||
get,
|
||||
list,
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
update,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import type {
|
||||
BlobOptionsGetter,
|
||||
Workspace as BlocksuiteWorkspace,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { User } from './types';
|
||||
|
||||
export type SyncMode = 'all' | 'core';
|
||||
|
||||
export interface WorkspaceUnitCtorParams {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
owner?: User;
|
||||
published?: boolean;
|
||||
memberCount: number;
|
||||
provider: string;
|
||||
syncMode: SyncMode;
|
||||
|
||||
blobOptionsGetter?: BlobOptionsGetter;
|
||||
blocksuiteWorkspace?: BlocksuiteWorkspace | null;
|
||||
}
|
||||
|
||||
export type UpdateWorkspaceUnitParams = Partial<
|
||||
Omit<WorkspaceUnitCtorParams, 'id'>
|
||||
>;
|
||||
|
||||
export class WorkspaceUnit {
|
||||
public readonly id: string;
|
||||
public name!: string;
|
||||
public avatar?: string;
|
||||
public owner?: User;
|
||||
public published?: boolean;
|
||||
public memberCount!: number;
|
||||
public provider!: string;
|
||||
public syncMode: 'all' | 'core' = 'core';
|
||||
|
||||
private _blocksuiteWorkspace?: BlocksuiteWorkspace | null;
|
||||
|
||||
constructor(params: WorkspaceUnitCtorParams) {
|
||||
this.id = params.id;
|
||||
this.update(params);
|
||||
}
|
||||
|
||||
get isPublish() {
|
||||
console.error('Suggest changing to published');
|
||||
return this.published;
|
||||
}
|
||||
|
||||
get isLocal() {
|
||||
console.error('Suggest changing to syncMode');
|
||||
return this.syncMode === 'all';
|
||||
}
|
||||
|
||||
get blocksuiteWorkspace() {
|
||||
return this._blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
setBlocksuiteWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace | null) {
|
||||
if (blocksuiteWorkspace && blocksuiteWorkspace.id !== this.id) {
|
||||
throw new Error('Workspace id inconsistent.');
|
||||
}
|
||||
this._blocksuiteWorkspace = blocksuiteWorkspace;
|
||||
}
|
||||
|
||||
update(params: UpdateWorkspaceUnitParams) {
|
||||
Object.assign(this, params);
|
||||
if (params.blocksuiteWorkspace) {
|
||||
this.setBlocksuiteWorkspace(params.blocksuiteWorkspace);
|
||||
}
|
||||
if (params.blobOptionsGetter && this.blocksuiteWorkspace) {
|
||||
this.blocksuiteWorkspace.setGettingBlobOptions(params.blobOptionsGetter);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): Omit<
|
||||
WorkspaceUnitCtorParams,
|
||||
'blocksuiteWorkspace' | 'blobOptionsGetter'
|
||||
> {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
avatar: this.avatar,
|
||||
owner: this.owner,
|
||||
published: this.published,
|
||||
memberCount: this.memberCount,
|
||||
provider: this.provider,
|
||||
syncMode: this.syncMode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal only for debug use
|
||||
*/
|
||||
exportWorkspaceYDoc(): void {
|
||||
this._blocksuiteWorkspace?.exportYDoc();
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"]
|
||||
}
|
@ -5,7 +5,8 @@
|
||||
"./utils": "./src/utils.ts",
|
||||
"./type": "./src/type.ts",
|
||||
"./affine/*": "./src/affine/*.ts",
|
||||
"./affine/api": "./src/affine/api/index.ts"
|
||||
"./affine/api": "./src/affine/api/index.ts",
|
||||
"./affine/sync": "./src/affine/sync.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:^",
|
||||
@ -18,6 +19,7 @@
|
||||
"jotai": "^2.0.3",
|
||||
"js-base64": "^3.7.5",
|
||||
"ky": "^0.33.3",
|
||||
"lib0": "^0.2.73",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"zod": "^3.21.4"
|
||||
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { MessageCode } from '@affine/datacenter';
|
||||
import { MessageCode } from '@affine/env/constant';
|
||||
import userA from '@affine-test/fixtures/userA.json';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
@ -295,13 +295,14 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
||||
throw new RequestError(MessageCode.acceptInvitingFailed, e);
|
||||
});
|
||||
},
|
||||
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
|
||||
uploadBlob: async (workspaceId: string, blob: Blob): Promise<string> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + 'api/blob', {
|
||||
method: 'PUT',
|
||||
body: params.blob,
|
||||
body: blob,
|
||||
headers: {
|
||||
'Content-Type': blob.type,
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.text());
|
||||
|
@ -77,7 +77,7 @@ export async function loginUser(
|
||||
) {
|
||||
await page.evaluate(async token => {
|
||||
// @ts-ignore
|
||||
globalThis.AFFINE_APIS.auth.setLogin(token);
|
||||
globalThis.setLogin(token);
|
||||
}, token);
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,6 @@
|
||||
"./packages/component/src/components/block-suite-editor/index"
|
||||
],
|
||||
"@affine/templates/*": ["./packages/templates/src/*"],
|
||||
"@affine/datacenter": ["./packages/datacenter/src"],
|
||||
"@affine/i18n": ["./packages/i18n/src"],
|
||||
"@affine/debug": ["./packages/debug"],
|
||||
"@affine/env": ["./packages/env"],
|
||||
@ -42,9 +41,6 @@
|
||||
{
|
||||
"path": "./apps/desktop"
|
||||
},
|
||||
{
|
||||
"path": "./packages/data-center"
|
||||
},
|
||||
{
|
||||
"path": "./packages/component"
|
||||
},
|
||||
|
@ -26,10 +26,6 @@ export default defineConfig({
|
||||
provider: 'istanbul', // or 'c8'
|
||||
reporter: ['lcov'],
|
||||
reportsDirectory: '.coverage/store',
|
||||
exclude: [
|
||||
// data center will be removed in the future
|
||||
'packages/data-center',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
71
yarn.lock
71
yarn.lock
@ -110,30 +110,6 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/datacenter@workspace:*, @affine/datacenter@workspace:packages/data-center":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/datacenter@workspace:packages/data-center"
|
||||
dependencies:
|
||||
"@affine/debug": "workspace:*"
|
||||
"@blocksuite/blocks": 0.5.0-20230324040005-14417c2
|
||||
"@blocksuite/global": 0.5.0-20230324040005-14417c2
|
||||
"@blocksuite/store": 0.5.0-20230324040005-14417c2
|
||||
"@tauri-apps/api": ^1.2.0
|
||||
encoding: ^0.1.13
|
||||
fake-indexeddb: 4.0.1
|
||||
firebase: ^9.18.0
|
||||
idb-keyval: ^6.2.0
|
||||
js-base64: ^3.7.5
|
||||
ky: ^0.33.3
|
||||
ky-universal: ^0.11.0
|
||||
lib0: ^0.2.69
|
||||
lit: ^2.6.1
|
||||
typescript: ^5.0.2
|
||||
y-protocols: ^1.0.5
|
||||
yjs: ^13.5.50
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/debug@workspace:*, @affine/debug@workspace:packages/debug":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/debug@workspace:packages/debug"
|
||||
@ -219,7 +195,6 @@ __metadata:
|
||||
resolution: "@affine/web@workspace:apps/web"
|
||||
dependencies:
|
||||
"@affine/component": "workspace:*"
|
||||
"@affine/datacenter": "workspace:*"
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
"@affine/i18n": "workspace:*"
|
||||
@ -285,6 +260,7 @@ __metadata:
|
||||
jotai: ^2.0.3
|
||||
js-base64: ^3.7.5
|
||||
ky: ^0.33.3
|
||||
lib0: ^0.2.73
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
zod: ^3.21.4
|
||||
@ -7766,15 +7742,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abort-controller@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "abort-controller@npm:3.0.0"
|
||||
dependencies:
|
||||
event-target-shim: ^5.0.0
|
||||
checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"accepts@npm:~1.3.5, accepts@npm:~1.3.8":
|
||||
version: 1.3.8
|
||||
resolution: "accepts@npm:1.3.8"
|
||||
@ -11458,13 +11425,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"events@npm:^3.2.0, events@npm:^3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "events@npm:3.3.0"
|
||||
@ -15007,22 +14967,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ky-universal@npm:^0.11.0":
|
||||
version: 0.11.0
|
||||
resolution: "ky-universal@npm:0.11.0"
|
||||
dependencies:
|
||||
abort-controller: ^3.0.0
|
||||
node-fetch: ^3.2.10
|
||||
peerDependencies:
|
||||
ky: ">=0.31.4"
|
||||
web-streams-polyfill: ">=3.2.1"
|
||||
peerDependenciesMeta:
|
||||
web-streams-polyfill:
|
||||
optional: true
|
||||
checksum: 42e4c91551a0d17465d6a117de2d1fa4efdf38c4e29dbd70af0c5b7ac0ee13994bceca9af2a0ac21a943d1cd22557ea664abe79f25e096d30a6baca0a0265a12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ky@npm:^0.33.3":
|
||||
version: 0.33.3
|
||||
resolution: "ky@npm:0.33.3"
|
||||
@ -15084,7 +15028,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lib0@npm:^0.2.35, lib0@npm:^0.2.42, lib0@npm:^0.2.68, lib0@npm:^0.2.69, lib0@npm:^0.2.72":
|
||||
"lib0@npm:^0.2.35, lib0@npm:^0.2.42, lib0@npm:^0.2.68, lib0@npm:^0.2.69, lib0@npm:^0.2.72, lib0@npm:^0.2.73":
|
||||
version: 0.2.73
|
||||
resolution: "lib0@npm:0.2.73"
|
||||
dependencies:
|
||||
@ -16343,17 +16287,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^3.2.10":
|
||||
version: 3.3.1
|
||||
resolution: "node-fetch@npm:3.3.1"
|
||||
dependencies:
|
||||
data-uri-to-buffer: ^4.0.0
|
||||
fetch-blob: ^3.1.4
|
||||
formdata-polyfill: ^4.0.10
|
||||
checksum: 62145fd3ba4770a76110bc31fdc0054ab2f5442b5ce96e9c4b39fc9e94a3d305560eec76e1165d9259eab866e02a8eecf9301062bb5dfc9f08a4d08b69d223dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp-build@npm:^4.2.1":
|
||||
version: 4.6.0
|
||||
resolution: "node-gyp-build@npm:4.6.0"
|
||||
|
Loading…
Reference in New Issue
Block a user