mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-21 20:44:40 +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",
|
"editor.formatOnSaveMode": "file",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"blocksuite",
|
"blocksuite",
|
||||||
"datacenter",
|
|
||||||
"livedemo",
|
"livedemo",
|
||||||
"yarn",
|
"yarn",
|
||||||
"jwst",
|
"jwst",
|
||||||
|
@ -68,7 +68,6 @@ const nextConfig = {
|
|||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
'@affine/component',
|
'@affine/component',
|
||||||
'@affine/datacenter',
|
|
||||||
'@affine/i18n',
|
'@affine/i18n',
|
||||||
'@affine/debug',
|
'@affine/debug',
|
||||||
'@affine/env',
|
'@affine/env',
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine/component": "workspace:*",
|
"@affine/component": "workspace:*",
|
||||||
"@affine/datacenter": "workspace:*",
|
|
||||||
"@affine/debug": "workspace:*",
|
"@affine/debug": "workspace:*",
|
||||||
"@affine/env": "workspace:*",
|
"@affine/env": "workspace:*",
|
||||||
"@affine/i18n": "workspace:*",
|
"@affine/i18n": "workspace:*",
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { atom } from 'jotai/index';
|
import { atom } from 'jotai/index';
|
||||||
|
|
||||||
import { BlockSuiteWorkspace } from '../../shared';
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { apis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
export const publicWorkspaceIdAtom = atom<string | null>(null);
|
export const publicWorkspaceIdAtom = atom<string | null>(null);
|
||||||
export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
||||||
@ -11,7 +12,7 @@ export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
|||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error('No workspace id');
|
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
|
// fixme: this is a hack
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const prefixUrl = params.get('prefixUrl')
|
const prefixUrl = params.get('prefixUrl')
|
||||||
@ -21,7 +22,9 @@ export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
(k: string) =>
|
(k: string) =>
|
||||||
// fixme: token could be expired
|
// 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.Y.applyUpdate(
|
||||||
blockSuiteWorkspace.doc,
|
blockSuiteWorkspace.doc,
|
||||||
|
@ -2,7 +2,7 @@ import { assertExists } from '@blocksuite/store';
|
|||||||
|
|
||||||
import type { AffineDownloadProvider } from '../../../shared';
|
import type { AffineDownloadProvider } from '../../../shared';
|
||||||
import { BlockSuiteWorkspace } from '../../../shared';
|
import { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { apis } from '../../../shared/apis';
|
import { affineApis } from '../../../shared/apis';
|
||||||
import { providerLogger } from '../../logger';
|
import { providerLogger } from '../../logger';
|
||||||
|
|
||||||
const hashMap = new Map<string, ArrayBuffer>();
|
const hashMap = new Map<string, ArrayBuffer>();
|
||||||
@ -25,7 +25,7 @@ export const createAffineDownloadProvider = (
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apis.downloadWorkspace(id, false).then(binary => {
|
affineApis.downloadWorkspace(id, false).then(binary => {
|
||||||
hashMap.set(id, binary);
|
hashMap.set(id, binary);
|
||||||
providerLogger.debug('applyUpdate');
|
providerLogger.debug('applyUpdate');
|
||||||
BlockSuiteWorkspace.Y.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 { assertExists } from '@blocksuite/store';
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
|
||||||
@ -7,7 +8,6 @@ import type {
|
|||||||
BlockSuiteWorkspace,
|
BlockSuiteWorkspace,
|
||||||
LocalIndexedDBProvider,
|
LocalIndexedDBProvider,
|
||||||
} from '../../shared';
|
} from '../../shared';
|
||||||
import { apis } from '../../shared/apis';
|
|
||||||
import { providerLogger } from '../logger';
|
import { providerLogger } from '../logger';
|
||||||
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ const createAffineWebSocketProvider = (
|
|||||||
blockSuiteWorkspace.id,
|
blockSuiteWorkspace.id,
|
||||||
blockSuiteWorkspace.doc,
|
blockSuiteWorkspace.doc,
|
||||||
{
|
{
|
||||||
params: { token: apis.auth.refresh },
|
params: { token: getLoginStorage()?.token ?? '' },
|
||||||
// @ts-expect-error ignore the type
|
// @ts-expect-error ignore the type
|
||||||
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
||||||
// we maintain broadcast channel by ourselves
|
// 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 { NextRouter } from 'next/router';
|
||||||
import type { ErrorInfo } from 'react';
|
import type { ErrorInfo } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
|
||||||
import { PermissionType } from '@affine/datacenter';
|
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import { PermissionType } from '@affine/workspace/affine/api';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
|
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import {
|
||||||
|
getLoginStorage,
|
||||||
|
setLoginStorage,
|
||||||
|
SignMethod,
|
||||||
|
} from '@affine/workspace/affine/login';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
@ -10,13 +15,13 @@ import { assertEquals, assertExists } from '@blocksuite/store';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { affineAuth } from '../../../../hooks/affine/use-affine-log-in';
|
||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||||
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
||||||
import type {
|
import type {
|
||||||
AffineOfficialWorkspace,
|
AffineOfficialWorkspace,
|
||||||
LocalWorkspace,
|
LocalWorkspace,
|
||||||
} from '../../../../shared';
|
} from '../../../../shared';
|
||||||
import { apis } from '../../../../shared/apis';
|
|
||||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||||
|
|
||||||
const IconWrapper = styled('div')(({ theme }) => {
|
const IconWrapper = styled('div')(({ theme }) => {
|
||||||
@ -112,8 +117,13 @@ export const SyncUser = () => {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
onConform={async () => {
|
onConform={async () => {
|
||||||
if (!apis.auth.isLogin) {
|
if (!getLoginStorage()) {
|
||||||
await apis.signInWithGoogle();
|
const response = await affineAuth.generateToken(
|
||||||
|
SignMethod.Google
|
||||||
|
);
|
||||||
|
if (response) {
|
||||||
|
setLoginStorage(response);
|
||||||
|
}
|
||||||
router.reload();
|
router.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FlexWrapper } from '@affine/component';
|
import { FlexWrapper } from '@affine/component';
|
||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
import { Tooltip } from '@affine/component';
|
import { Tooltip } from '@affine/component';
|
||||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||||
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { MessageCode } from '@affine/datacenter';
|
import { MessageCode, Messages } from '@affine/env/constant';
|
||||||
import { messages } from '@affine/datacenter';
|
import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { memo, useEffect, useState } 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 { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||||
import { apis } from '../../../shared/apis';
|
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -33,9 +33,12 @@ export const MessageCenter: React.FC = memo(function MessageCenter() {
|
|||||||
event.detail.code === MessageCode.loginError)
|
event.detail.code === MessageCode.loginError)
|
||||||
) {
|
) {
|
||||||
setPopup(true);
|
setPopup(true);
|
||||||
apis
|
affineAuth
|
||||||
.signInWithGoogle()
|
.generateToken(SignMethod.Google)
|
||||||
.then(() => {
|
.then(response => {
|
||||||
|
if (response) {
|
||||||
|
setLoginStorage(response);
|
||||||
|
}
|
||||||
setPopup(false);
|
setPopup(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -43,7 +46,7 @@ export const MessageCenter: React.FC = memo(function MessageCenter() {
|
|||||||
onLogout();
|
onLogout();
|
||||||
});
|
});
|
||||||
} else {
|
} 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 { useTranslation } from '@affine/i18n';
|
||||||
|
import { PermissionType } from '@affine/workspace/affine/api';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
@ -4,8 +4,8 @@ import {
|
|||||||
ModalWrapper,
|
ModalWrapper,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
|
||||||
import { useTranslation } from '@affine/i18n';
|
import { useTranslation } from '@affine/i18n';
|
||||||
|
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||||
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
|
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
|
||||||
|
|
||||||
import type { RemWorkspace } from '../../../shared';
|
import type { RemWorkspace } from '../../../shared';
|
||||||
|
@ -1,26 +1,30 @@
|
|||||||
|
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||||
import {
|
import {
|
||||||
createAffineAuth,
|
createAffineAuth,
|
||||||
|
parseIdToken,
|
||||||
setLoginStorage,
|
setLoginStorage,
|
||||||
SignMethod,
|
SignMethod,
|
||||||
} from '@affine/workspace/affine/login';
|
} from '@affine/workspace/affine/login';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { apis } from '../../shared/apis';
|
|
||||||
import { toast } from '../../utils';
|
import { toast } from '../../utils';
|
||||||
|
|
||||||
export const affineAuth = createAffineAuth();
|
export const affineAuth = createAffineAuth();
|
||||||
|
|
||||||
export function useAffineLogIn() {
|
export function useAffineLogIn() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const setUser = useSetAtom(currentAffineUserAtom);
|
||||||
return useCallback(async () => {
|
return useCallback(async () => {
|
||||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||||
if (response) {
|
if (response) {
|
||||||
setLoginStorage(response);
|
setLoginStorage(response);
|
||||||
apis.auth.setLogin(response);
|
const user = parseIdToken(response.token);
|
||||||
|
setUser(user);
|
||||||
router.reload();
|
router.reload();
|
||||||
} else {
|
} else {
|
||||||
toast('Login failed');
|
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 { clearLoginStorage } from '@affine/workspace/affine/login';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
@ -6,13 +7,12 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { jotaiWorkspacesAtom } from '../../atoms';
|
import { jotaiWorkspacesAtom } from '../../atoms';
|
||||||
import { WorkspacePlugins } from '../../plugins';
|
import { WorkspacePlugins } from '../../plugins';
|
||||||
import { apis } from '../../shared/apis';
|
|
||||||
|
|
||||||
export function useAffineLogOut() {
|
export function useAffineLogOut() {
|
||||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const setCurrentUser = useSetAtom(currentAffineUserAtom);
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
apis.auth.clear();
|
|
||||||
set(workspaces =>
|
set(workspaces =>
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
|
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
|
||||||
@ -20,6 +20,7 @@ export function useAffineLogOut() {
|
|||||||
);
|
);
|
||||||
WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.();
|
WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.();
|
||||||
clearLoginStorage();
|
clearLoginStorage();
|
||||||
|
setCurrentUser(null);
|
||||||
router.reload();
|
router.reload();
|
||||||
}, [router, set]);
|
}, [router, set, setCurrentUser]);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@affine/workspace/affine/login';
|
} from '@affine/workspace/affine/login';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { apis } from '../../shared/apis';
|
|
||||||
import { affineAuth } from './use-affine-log-in';
|
import { affineAuth } from './use-affine-log-in';
|
||||||
|
|
||||||
const logger = new DebugLogger('auth-token');
|
const logger = new DebugLogger('auth-token');
|
||||||
@ -22,7 +21,6 @@ const revalidate = async () => {
|
|||||||
const response = await affineAuth.refreshToken(storage);
|
const response = await affineAuth.refreshToken(storage);
|
||||||
if (response) {
|
if (response) {
|
||||||
setLoginStorage(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';
|
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 { useCallback } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||||
import { apis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
export function useMembers(workspaceId: string) {
|
export function useMembers(workspaceId: string) {
|
||||||
const { data, mutate } = useSWR<Member[]>(
|
const { data, mutate } = useSWR<Member[]>(
|
||||||
@ -15,7 +15,7 @@ export function useMembers(workspaceId: string) {
|
|||||||
|
|
||||||
const inviteMember = useCallback(
|
const inviteMember = useCallback(
|
||||||
async (email: string) => {
|
async (email: string) => {
|
||||||
await apis.inviteMember({
|
await affineApis.inviteMember({
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
@ -26,8 +26,7 @@ export function useMembers(workspaceId: string) {
|
|||||||
|
|
||||||
const removeMember = useCallback(
|
const removeMember = useCallback(
|
||||||
async (permissionId: number) => {
|
async (permissionId: number) => {
|
||||||
// fixme: what about the workspaceId?
|
await affineApis.removeMember({
|
||||||
await apis.removeMember({
|
|
||||||
permissionId,
|
permissionId,
|
||||||
});
|
});
|
||||||
return mutate();
|
return mutate();
|
||||||
|
@ -4,13 +4,13 @@ import useSWR from 'swr';
|
|||||||
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
||||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
import type { AffineWorkspace } from '../../shared';
|
||||||
import { apis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||||
const { mutate } = useSWR(QueryKey.getWorkspaces);
|
const { mutate } = useSWR(QueryKey.getWorkspaces);
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (isPublish: boolean) => {
|
async (isPublish: boolean) => {
|
||||||
await apis.updateWorkspace({
|
await affineApis.updateWorkspace({
|
||||||
id: workspace.id,
|
id: workspace.id,
|
||||||
public: isPublish,
|
public: isPublish,
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import type { AccessTokenMessage } from '@affine/datacenter';
|
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||||
import useSWR from 'swr';
|
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
|
||||||
|
|
||||||
export function useCurrentUser(): AccessTokenMessage | null {
|
export function useCurrentUser(): AccessTokenMessage | null {
|
||||||
const { data } = useSWR<AccessTokenMessage | null>(QueryKey.getUser, {
|
return useAtomValue(currentAffineUserAtom);
|
||||||
fallbackData: null,
|
|
||||||
});
|
|
||||||
return data ?? null;
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { displayFlex, styled } from '@affine/component';
|
import { displayFlex, styled } from '@affine/component';
|
||||||
import { Button } from '@affine/component';
|
import { Button } from '@affine/component';
|
||||||
import type { Permission } from '@affine/datacenter';
|
import type { Permission } from '@affine/workspace/affine/api';
|
||||||
import {
|
import {
|
||||||
SucessfulDuotoneIcon,
|
SucessfulDuotoneIcon,
|
||||||
UnsucessfulDuotoneIcon,
|
UnsucessfulDuotoneIcon,
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { useTranslation } from '@affine/i18n';
|
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 type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type';
|
||||||
import {
|
import {
|
||||||
settingPanel,
|
settingPanel,
|
||||||
@ -7,7 +14,7 @@ import {
|
|||||||
} from '@affine/workspace/type';
|
} from '@affine/workspace/type';
|
||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@ -16,6 +23,7 @@ import React, { useCallback, useEffect } from 'react';
|
|||||||
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
||||||
import { PageLoading } from '../../../components/pure/loading';
|
import { PageLoading } from '../../../components/pure/loading';
|
||||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
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 { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||||
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
|
||||||
import { useTransformWorkspace } from '../../../hooks/use-transform-workspace';
|
import { useTransformWorkspace } from '../../../hooks/use-transform-workspace';
|
||||||
@ -23,7 +31,6 @@ import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
|
|||||||
import { WorkspaceLayout } from '../../../layouts';
|
import { WorkspaceLayout } from '../../../layouts';
|
||||||
import { WorkspacePlugins } from '../../../plugins';
|
import { WorkspacePlugins } from '../../../plugins';
|
||||||
import type { NextPageWithLayout } from '../../../shared';
|
import type { NextPageWithLayout } from '../../../shared';
|
||||||
import { apis } from '../../../shared/apis';
|
|
||||||
|
|
||||||
const settingPanelAtom = atomWithStorage<SettingPanel>(
|
const settingPanelAtom = atomWithStorage<SettingPanel>(
|
||||||
'workspaceId',
|
'workspaceId',
|
||||||
@ -101,15 +108,20 @@ const SettingPage: NextPageWithLayout = () => {
|
|||||||
return helper.deleteWorkspace(workspaceId);
|
return helper.deleteWorkspace(workspaceId);
|
||||||
}, [currentWorkspace, helper]);
|
}, [currentWorkspace, helper]);
|
||||||
const transformWorkspace = useTransformWorkspace();
|
const transformWorkspace = useTransformWorkspace();
|
||||||
|
const setUser = useSetAtom(currentAffineUserAtom);
|
||||||
const onTransformWorkspace = useCallback(
|
const onTransformWorkspace = useCallback(
|
||||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||||
from: From,
|
from: From,
|
||||||
to: To,
|
to: To,
|
||||||
workspace: WorkspaceRegistry[From]
|
workspace: WorkspaceRegistry[From]
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const needRefresh = to === WorkspaceFlavour.AFFINE && !apis.auth.isLogin;
|
const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage();
|
||||||
if (needRefresh) {
|
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);
|
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||||
await router.replace({
|
await router.replace({
|
||||||
@ -119,11 +131,8 @@ const SettingPage: NextPageWithLayout = () => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (needRefresh) {
|
|
||||||
router.reload();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[router, transformWorkspace]
|
[router, setUser, transformWorkspace]
|
||||||
);
|
);
|
||||||
if (!router.isReady) {
|
if (!router.isReady) {
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
@ -6,7 +7,7 @@ import { jotaiStore, workspacesAtom } from '../../atoms';
|
|||||||
import { createAffineProviders } from '../../blocksuite';
|
import { createAffineProviders } from '../../blocksuite';
|
||||||
import { Unreachable } from '../../components/affine/affine-error-eoundary';
|
import { Unreachable } from '../../components/affine/affine-error-eoundary';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
import type { AffineWorkspace } from '../../shared';
|
||||||
import { apis } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
|
|
||||||
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
||||||
|
|
||||||
@ -17,24 +18,21 @@ export const fetcher = async (
|
|||||||
| [Query, string]
|
| [Query, string]
|
||||||
| [Query, string, string]
|
| [Query, string, string]
|
||||||
) => {
|
) => {
|
||||||
if (query === QueryKey.getUser) {
|
|
||||||
return apis.auth.user ?? null;
|
|
||||||
}
|
|
||||||
if (Array.isArray(query)) {
|
if (Array.isArray(query)) {
|
||||||
if (query[0] === QueryKey.downloadWorkspace) {
|
if (query[0] === QueryKey.downloadWorkspace) {
|
||||||
if (typeof query[2] !== 'boolean') {
|
if (typeof query[2] !== 'boolean') {
|
||||||
throw new Unreachable();
|
throw new Unreachable();
|
||||||
}
|
}
|
||||||
return apis.downloadWorkspace(query[1], query[2]);
|
return affineApis.downloadWorkspace(query[1], query[2]);
|
||||||
} else if (query[0] === QueryKey.getMembers) {
|
} else if (query[0] === QueryKey.getMembers) {
|
||||||
return apis.getWorkspaceMembers({
|
return affineApis.getWorkspaceMembers({
|
||||||
id: query[1],
|
id: query[1],
|
||||||
});
|
});
|
||||||
} else if (query[0] === QueryKey.getUserByEmail) {
|
} else if (query[0] === QueryKey.getUserByEmail) {
|
||||||
if (typeof query[2] !== 'string') {
|
if (typeof query[2] !== 'string') {
|
||||||
throw new Unreachable();
|
throw new Unreachable();
|
||||||
}
|
}
|
||||||
return apis.getUserByEmail({
|
return affineApis.getUserByEmail({
|
||||||
workspace_id: query[1],
|
workspace_id: query[1],
|
||||||
email: query[2],
|
email: query[2],
|
||||||
});
|
});
|
||||||
@ -57,19 +55,19 @@ export const fetcher = async (
|
|||||||
if (typeof invitingCode !== 'string') {
|
if (typeof invitingCode !== 'string') {
|
||||||
throw new TypeError('invitingCode must be a string');
|
throw new TypeError('invitingCode must be a string');
|
||||||
}
|
}
|
||||||
return apis.acceptInviting({
|
return affineApis.acceptInviting({
|
||||||
invitingCode,
|
invitingCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (query === QueryKey.getWorkspaces) {
|
if (query === QueryKey.getWorkspaces) {
|
||||||
return apis.getWorkspaces().then(workspaces => {
|
return affineApis.getWorkspaces().then(workspaces => {
|
||||||
return workspaces.map(workspace => {
|
return workspaces.map(workspace => {
|
||||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
(k: string) =>
|
(k: string) =>
|
||||||
// fixme: token could be expired
|
// fixme: token could be expired
|
||||||
({ api: '/api/workspace', token: apis.auth.token }[k])
|
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||||
);
|
);
|
||||||
const remWorkspace: AffineWorkspace = {
|
const remWorkspace: AffineWorkspace = {
|
||||||
...workspace,
|
...workspace,
|
||||||
@ -81,14 +79,13 @@ export const fetcher = async (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (apis as any)[query]();
|
return (affineApis as any)[query]();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryKey = {
|
export const QueryKey = {
|
||||||
acceptInvite: 'acceptInvite',
|
acceptInvite: 'acceptInvite',
|
||||||
getImage: 'getImage',
|
getImage: 'getImage',
|
||||||
getUser: 'getUser',
|
|
||||||
getWorkspaces: 'getWorkspaces',
|
getWorkspaces: 'getWorkspaces',
|
||||||
downloadWorkspace: 'downloadWorkspace',
|
downloadWorkspace: 'downloadWorkspace',
|
||||||
getMembers: 'getMembers',
|
getMembers: 'getMembers',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getLoginStorage } from '@affine/workspace/affine/login';
|
||||||
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { createJSONStorage } from 'jotai/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 { PageDetailEditor } from '../../components/page-detail-editor';
|
||||||
import type { AffineWorkspace } from '../../shared';
|
import type { AffineWorkspace } from '../../shared';
|
||||||
import { BlockSuiteWorkspace } from '../../shared';
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { apis, clientAuth } from '../../shared/apis';
|
import { affineApis } from '../../shared/apis';
|
||||||
import { initPage } from '../../utils/blocksuite';
|
import { initPage } from '../../utils';
|
||||||
import type { WorkspacePlugin } from '..';
|
import type { WorkspacePlugin } from '..';
|
||||||
import { QueryKey } from './fetcher';
|
import { QueryKey } from './fetcher';
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ const getPersistenceAllWorkspace = () => {
|
|||||||
item.id,
|
item.id,
|
||||||
(k: string) =>
|
(k: string) =>
|
||||||
// fixme: token could be expired
|
// fixme: token could be expired
|
||||||
({ api: '/api/workspace', token: apis.auth.token }[k])
|
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||||
);
|
);
|
||||||
const affineWorkspace: AffineWorkspace = {
|
const affineWorkspace: AffineWorkspace = {
|
||||||
...item,
|
...item,
|
||||||
@ -65,7 +66,9 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
|||||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
||||||
blockSuiteWorkspace.doc
|
blockSuiteWorkspace.doc
|
||||||
);
|
);
|
||||||
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
|
const { id } = await affineApis.createWorkspace(
|
||||||
|
new Blob([binary.buffer])
|
||||||
|
);
|
||||||
// fixme: syncing images
|
// fixme: syncing images
|
||||||
const newWorkspaceId = id;
|
const newWorkspaceId = id;
|
||||||
|
|
||||||
@ -77,12 +80,7 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
|||||||
const url = await blobs.get(id);
|
const url = await blobs.get(id);
|
||||||
if (url) {
|
if (url) {
|
||||||
const blob = await fetch(url).then(res => res.blob());
|
const blob = await fetch(url).then(res => res.blob());
|
||||||
await clientAuth.put(`api/workspace/${newWorkspaceId}/blob`, {
|
await affineApis.uploadBlob(newWorkspaceId, blob);
|
||||||
body: blob,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': blob.type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,14 +101,14 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
|||||||
items.filter(item => item.id !== workspace.id)
|
items.filter(item => item.id !== workspace.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await apis.deleteWorkspace({
|
await affineApis.deleteWorkspace({
|
||||||
id: workspace.id,
|
id: workspace.id,
|
||||||
});
|
});
|
||||||
await mutate(matcher => matcher === QueryKey.getWorkspaces);
|
await mutate(matcher => matcher === QueryKey.getWorkspaces);
|
||||||
},
|
},
|
||||||
get: async workspaceId => {
|
get: async workspaceId => {
|
||||||
try {
|
try {
|
||||||
if (!apis.auth.isLogin) {
|
if (!getLoginStorage()) {
|
||||||
const workspaces = getPersistenceAllWorkspace();
|
const workspaces = getPersistenceAllWorkspace();
|
||||||
return (
|
return (
|
||||||
workspaces.find(workspace => workspace.id === workspaceId) ?? null
|
workspaces.find(workspace => workspace.id === workspaceId) ?? null
|
||||||
@ -130,69 +128,65 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
|
|||||||
list: async () => {
|
list: async () => {
|
||||||
const allWorkspaces = getPersistenceAllWorkspace();
|
const allWorkspaces = getPersistenceAllWorkspace();
|
||||||
try {
|
try {
|
||||||
if (apis.auth.isLogin) {
|
const workspaces = await affineApis.getWorkspaces().then(workspaces => {
|
||||||
const workspaces = await apis.getWorkspaces().then(workspaces => {
|
return workspaces.map(workspace => {
|
||||||
return workspaces.map(workspace => {
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
workspace.id,
|
||||||
workspace.id,
|
(k: string) =>
|
||||||
(k: string) =>
|
// fixme: token could be expired
|
||||||
// fixme: token could be expired
|
({ api: '/api/workspace', token: getLoginStorage()?.token }[k])
|
||||||
({ 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
|
|
||||||
);
|
);
|
||||||
if (idx !== -1) {
|
const dump = workspaces.map(workspace => {
|
||||||
allWorkspaces.splice(idx, 1, workspace);
|
return {
|
||||||
} else {
|
id: workspace.id,
|
||||||
allWorkspaces.push(workspace);
|
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 affineWorkspace: AffineWorkspace = {
|
||||||
const dump = allWorkspaces.map(workspace => {
|
...workspace,
|
||||||
return {
|
flavour: WorkspaceFlavour.AFFINE,
|
||||||
id: workspace.id,
|
blockSuiteWorkspace,
|
||||||
type: workspace.type,
|
providers: [...createAffineProviders(blockSuiteWorkspace)],
|
||||||
public: workspace.public,
|
};
|
||||||
permission: workspace.permission,
|
return affineWorkspace;
|
||||||
} satisfies z.infer<typeof schema>;
|
|
||||||
});
|
});
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('fetch affine workspaces failed', 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 { config } from '@affine/env';
|
||||||
import {
|
import {
|
||||||
createUserApis,
|
createUserApis,
|
||||||
createWorkspaceApis,
|
createWorkspaceApis,
|
||||||
} from '@affine/workspace/affine/api';
|
} 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 = '/';
|
let prefixUrl = '/';
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@ -31,32 +29,27 @@ if (typeof window === 'undefined') {
|
|||||||
params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string);
|
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 {
|
declare global {
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var affineApis:
|
var setLogin: typeof setLoginStorage;
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var AFFINE_APIS:
|
||||||
| undefined
|
| undefined
|
||||||
| (ReturnType<typeof createUserApis> &
|
| (ReturnType<typeof createUserApis> &
|
||||||
ReturnType<typeof createWorkspaceApis>);
|
ReturnType<typeof createWorkspaceApis>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const affineApis = {} as ReturnType<typeof createUserApis> &
|
export { affineApis };
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
@ -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 type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
|
@ -4,9 +4,5 @@
|
|||||||
const defaultExclude = require('@istanbuljs/schema/default-exclude');
|
const defaultExclude = require('@istanbuljs/schema/default-exclude');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
exclude: [
|
exclude: [...defaultExclude],
|
||||||
...defaultExclude,
|
|
||||||
// data-center will be removed in the future, we don't need to coverage it
|
|
||||||
'packages/data-center/**',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
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",
|
"./utils": "./src/utils.ts",
|
||||||
"./type": "./src/type.ts",
|
"./type": "./src/type.ts",
|
||||||
"./affine/*": "./src/affine/*.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": {
|
"dependencies": {
|
||||||
"@affine-test/fixtures": "workspace:^",
|
"@affine-test/fixtures": "workspace:^",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"jotai": "^2.0.3",
|
"jotai": "^2.0.3",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"ky": "^0.33.3",
|
"ky": "^0.33.3",
|
||||||
|
"lib0": "^0.2.73",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
import { MessageCode } from '@affine/datacenter';
|
import { MessageCode } from '@affine/env/constant';
|
||||||
import userA from '@affine-test/fixtures/userA.json';
|
import userA from '@affine-test/fixtures/userA.json';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
|
@ -295,13 +295,14 @@ export function createWorkspaceApis(prefixUrl = '/') {
|
|||||||
throw new RequestError(MessageCode.acceptInvitingFailed, e);
|
throw new RequestError(MessageCode.acceptInvitingFailed, e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
|
uploadBlob: async (workspaceId: string, blob: Blob): Promise<string> => {
|
||||||
const auth = getLoginStorage();
|
const auth = getLoginStorage();
|
||||||
assertExists(auth);
|
assertExists(auth);
|
||||||
return fetch(prefixUrl + 'api/blob', {
|
return fetch(prefixUrl + 'api/blob', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: params.blob,
|
body: blob,
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': blob.type,
|
||||||
Authorization: auth.token,
|
Authorization: auth.token,
|
||||||
},
|
},
|
||||||
}).then(r => r.text());
|
}).then(r => r.text());
|
||||||
|
@ -77,7 +77,7 @@ export async function loginUser(
|
|||||||
) {
|
) {
|
||||||
await page.evaluate(async token => {
|
await page.evaluate(async token => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
globalThis.AFFINE_APIS.auth.setLogin(token);
|
globalThis.setLogin(token);
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
"./packages/component/src/components/block-suite-editor/index"
|
"./packages/component/src/components/block-suite-editor/index"
|
||||||
],
|
],
|
||||||
"@affine/templates/*": ["./packages/templates/src/*"],
|
"@affine/templates/*": ["./packages/templates/src/*"],
|
||||||
"@affine/datacenter": ["./packages/datacenter/src"],
|
|
||||||
"@affine/i18n": ["./packages/i18n/src"],
|
"@affine/i18n": ["./packages/i18n/src"],
|
||||||
"@affine/debug": ["./packages/debug"],
|
"@affine/debug": ["./packages/debug"],
|
||||||
"@affine/env": ["./packages/env"],
|
"@affine/env": ["./packages/env"],
|
||||||
@ -42,9 +41,6 @@
|
|||||||
{
|
{
|
||||||
"path": "./apps/desktop"
|
"path": "./apps/desktop"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "./packages/data-center"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "./packages/component"
|
"path": "./packages/component"
|
||||||
},
|
},
|
||||||
|
@ -26,10 +26,6 @@ export default defineConfig({
|
|||||||
provider: 'istanbul', // or 'c8'
|
provider: 'istanbul', // or 'c8'
|
||||||
reporter: ['lcov'],
|
reporter: ['lcov'],
|
||||||
reportsDirectory: '.coverage/store',
|
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
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@affine/debug@workspace:*, @affine/debug@workspace:packages/debug":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/debug@workspace:packages/debug"
|
resolution: "@affine/debug@workspace:packages/debug"
|
||||||
@ -219,7 +195,6 @@ __metadata:
|
|||||||
resolution: "@affine/web@workspace:apps/web"
|
resolution: "@affine/web@workspace:apps/web"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@affine/component": "workspace:*"
|
"@affine/component": "workspace:*"
|
||||||
"@affine/datacenter": "workspace:*"
|
|
||||||
"@affine/debug": "workspace:*"
|
"@affine/debug": "workspace:*"
|
||||||
"@affine/env": "workspace:*"
|
"@affine/env": "workspace:*"
|
||||||
"@affine/i18n": "workspace:*"
|
"@affine/i18n": "workspace:*"
|
||||||
@ -285,6 +260,7 @@ __metadata:
|
|||||||
jotai: ^2.0.3
|
jotai: ^2.0.3
|
||||||
js-base64: ^3.7.5
|
js-base64: ^3.7.5
|
||||||
ky: ^0.33.3
|
ky: ^0.33.3
|
||||||
|
lib0: ^0.2.73
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
zod: ^3.21.4
|
zod: ^3.21.4
|
||||||
@ -7766,15 +7742,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"accepts@npm:~1.3.5, accepts@npm:~1.3.8":
|
||||||
version: 1.3.8
|
version: 1.3.8
|
||||||
resolution: "accepts@npm:1.3.8"
|
resolution: "accepts@npm:1.3.8"
|
||||||
@ -11458,13 +11425,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"events@npm:^3.2.0, events@npm:^3.3.0":
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
resolution: "events@npm:3.3.0"
|
resolution: "events@npm:3.3.0"
|
||||||
@ -15007,22 +14967,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"ky@npm:^0.33.3":
|
||||||
version: 0.33.3
|
version: 0.33.3
|
||||||
resolution: "ky@npm:0.33.3"
|
resolution: "ky@npm:0.33.3"
|
||||||
@ -15084,7 +15028,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 0.2.73
|
||||||
resolution: "lib0@npm:0.2.73"
|
resolution: "lib0@npm:0.2.73"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -16343,17 +16287,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"node-gyp-build@npm:^4.2.1":
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
resolution: "node-gyp-build@npm:4.6.0"
|
resolution: "node-gyp-build@npm:4.6.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user