From ed29c5fbd9a27d0a1d71c704f292ea4d7d8031a3 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Mon, 27 Mar 2023 17:48:22 -0500 Subject: [PATCH] refactor: remove package `@affine/datacenter` (#1705) --- .vscode/settings.json | 1 - apps/web/next.config.mjs | 1 - apps/web/package.json | 1 - apps/web/src/atoms/public-workspace/index.ts | 9 +- .../src/blocksuite/providers/affine/index.ts | 4 +- apps/web/src/blocksuite/providers/index.ts | 6 +- .../affine/affine-error-eoundary.tsx | 2 +- .../panel/collaboration/index.tsx | 2 +- .../header/header-right-items/SyncUser.tsx | 16 +- apps/web/src/components/pure/footer/index.tsx | 2 +- .../components/pure/message-center/index.tsx | 17 +- .../components/pure/workspace-card/index.tsx | 2 +- .../pure/workspace-list-modal/index.tsx | 2 +- .../web/src/hooks/affine/use-affine-log-in.ts | 10 +- .../src/hooks/affine/use-affine-log-out.ts | 7 +- .../affine/use-affine-refresh-auth-token.ts | 2 - .../hooks/affine/use-is-workspace-owner.ts | 2 +- apps/web/src/hooks/affine/use-members.ts | 9 +- .../affine/use-toggle-workspace-publish.ts | 4 +- .../web/src/hooks/current/use-current-user.ts | 12 +- apps/web/src/pages/invite/[invite_code].tsx | 2 +- .../pages/workspace/[workspaceId]/setting.tsx | 25 +- apps/web/src/plugins/affine/fetcher.ts | 21 +- apps/web/src/plugins/affine/index.tsx | 136 +++-- apps/web/src/shared/apis.ts | 51 +- apps/web/src/shared/index.ts | 2 +- nyc.config.js | 6 +- packages/data-center/.gitignore | 4 - packages/data-center/package.json | 32 -- packages/data-center/src/datacenter.ts | 454 ---------------- packages/data-center/src/global-types.ts | 8 - packages/data-center/src/index.ts | 43 -- packages/data-center/src/message/code.ts | 65 --- packages/data-center/src/message/index.ts | 2 - packages/data-center/src/message/message.ts | 47 -- .../provider/affine/__tests__/mock-apis.ts | 19 - .../data-center/src/provider/affine/affine.ts | 491 ----------------- .../affine/apis/__tests__/auth.spec.ts | 22 - .../src/provider/affine/apis/google.ts | 288 ---------- .../src/provider/affine/apis/index.ts | 45 -- .../src/provider/affine/apis/request-error.ts | 9 - .../src/provider/affine/apis/request.ts | 65 --- .../src/provider/affine/apis/user.ts | 25 - .../src/provider/affine/apis/workspace.ts | 260 --------- .../src/provider/affine/channel.ts | 66 --- .../data-center/src/provider/affine/idb-kv.ts | 25 - .../data-center/src/provider/affine/index.ts | 1 - .../src/provider/affine/storage.ts | 3 - .../data-center/src/provider/affine/utils.ts | 94 ---- packages/data-center/src/provider/base.ts | 240 --------- packages/data-center/src/provider/index.ts | 1 - .../data-center/src/provider/local/index.ts | 1 - .../src/provider/local/indexeddb/indexeddb.ts | 208 ------- .../src/provider/local/indexeddb/utils.ts | 43 -- .../src/provider/local/local.spec.ts | 61 --- .../data-center/src/provider/local/local.ts | 125 ----- .../data-center/src/provider/local/utils.ts | 41 -- .../src/provider/selfhosted/index.ts.bk | 63 --- .../src/provider/selfhosted/sync.js | 508 ------------------ .../tauri-ipc/blocksuite-provider/blob.ts | 72 --- .../src/provider/tauri-ipc/index.ts | 196 ------- .../src/provider/tauri-ipc/ipc/methods.ts | 79 --- .../provider/tauri-ipc/ipc/types/blob.json | 57 -- .../src/provider/tauri-ipc/ipc/types/blob.ts | 24 - .../tauri-ipc/ipc/types/document.json | 103 ---- .../provider/tauri-ipc/ipc/types/document.ts | 38 -- .../provider/tauri-ipc/ipc/types/user.json | 87 --- .../src/provider/tauri-ipc/ipc/types/user.ts | 36 -- .../tauri-ipc/ipc/types/workspace.json | 240 --------- .../provider/tauri-ipc/ipc/types/workspace.ts | 90 ---- .../src/provider/tauri-ipc/utils.ts | 26 - packages/data-center/src/provider/utils.ts | 22 - packages/data-center/src/store.ts | 89 --- packages/data-center/src/types/index.ts | 34 -- packages/data-center/src/utils/index.ts | 77 --- .../src/workspace-unit-collection.spec.ts | 64 --- .../src/workspace-unit-collection.ts | 177 ------ packages/data-center/src/workspace-unit.ts | 98 ---- packages/data-center/tsconfig.json | 4 - packages/workspace/package.json | 4 +- .../src/affine/__tests__/api.spec.ts | 2 +- packages/workspace/src/affine/api/index.ts | 5 +- .../provider => workspace/src}/affine/sync.js | 0 tests/libs/utils.ts | 2 +- tsconfig.json | 4 - vitest.config.ts | 4 - yarn.lock | 71 +-- 87 files changed, 187 insertions(+), 5231 deletions(-) delete mode 100644 packages/data-center/.gitignore delete mode 100644 packages/data-center/package.json delete mode 100644 packages/data-center/src/datacenter.ts delete mode 100644 packages/data-center/src/global-types.ts delete mode 100644 packages/data-center/src/index.ts delete mode 100644 packages/data-center/src/message/code.ts delete mode 100644 packages/data-center/src/message/index.ts delete mode 100644 packages/data-center/src/message/message.ts delete mode 100644 packages/data-center/src/provider/affine/__tests__/mock-apis.ts delete mode 100644 packages/data-center/src/provider/affine/affine.ts delete mode 100644 packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts delete mode 100644 packages/data-center/src/provider/affine/apis/google.ts delete mode 100644 packages/data-center/src/provider/affine/apis/index.ts delete mode 100644 packages/data-center/src/provider/affine/apis/request-error.ts delete mode 100644 packages/data-center/src/provider/affine/apis/request.ts delete mode 100644 packages/data-center/src/provider/affine/apis/user.ts delete mode 100644 packages/data-center/src/provider/affine/apis/workspace.ts delete mode 100644 packages/data-center/src/provider/affine/channel.ts delete mode 100644 packages/data-center/src/provider/affine/idb-kv.ts delete mode 100644 packages/data-center/src/provider/affine/index.ts delete mode 100644 packages/data-center/src/provider/affine/storage.ts delete mode 100644 packages/data-center/src/provider/affine/utils.ts delete mode 100644 packages/data-center/src/provider/base.ts delete mode 100644 packages/data-center/src/provider/index.ts delete mode 100644 packages/data-center/src/provider/local/index.ts delete mode 100644 packages/data-center/src/provider/local/indexeddb/indexeddb.ts delete mode 100644 packages/data-center/src/provider/local/indexeddb/utils.ts delete mode 100644 packages/data-center/src/provider/local/local.spec.ts delete mode 100644 packages/data-center/src/provider/local/local.ts delete mode 100644 packages/data-center/src/provider/local/utils.ts delete mode 100644 packages/data-center/src/provider/selfhosted/index.ts.bk delete mode 100644 packages/data-center/src/provider/selfhosted/sync.js delete mode 100644 packages/data-center/src/provider/tauri-ipc/blocksuite-provider/blob.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/index.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/methods.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/blob.json delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/blob.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/document.json delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/user.json delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/user.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.json delete mode 100644 packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.ts delete mode 100644 packages/data-center/src/provider/tauri-ipc/utils.ts delete mode 100644 packages/data-center/src/provider/utils.ts delete mode 100644 packages/data-center/src/store.ts delete mode 100644 packages/data-center/src/types/index.ts delete mode 100644 packages/data-center/src/utils/index.ts delete mode 100644 packages/data-center/src/workspace-unit-collection.spec.ts delete mode 100644 packages/data-center/src/workspace-unit-collection.ts delete mode 100644 packages/data-center/src/workspace-unit.ts delete mode 100644 packages/data-center/tsconfig.json rename packages/{data-center/src/provider => workspace/src}/affine/sync.js (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 13aac741da..83cd6aa5fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,6 @@ "editor.formatOnSaveMode": "file", "cSpell.words": [ "blocksuite", - "datacenter", "livedemo", "yarn", "jwst", diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5dcfcf13d1..d14e65738e 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -68,7 +68,6 @@ const nextConfig = { reactStrictMode: true, transpilePackages: [ '@affine/component', - '@affine/datacenter', '@affine/i18n', '@affine/debug', '@affine/env', diff --git a/apps/web/package.json b/apps/web/package.json index f9f66740eb..40264eb9a0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@affine/component": "workspace:*", - "@affine/datacenter": "workspace:*", "@affine/debug": "workspace:*", "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", diff --git a/apps/web/src/atoms/public-workspace/index.ts b/apps/web/src/atoms/public-workspace/index.ts index be780d6f44..ede6e3a1b3 100644 --- a/apps/web/src/atoms/public-workspace/index.ts +++ b/apps/web/src/atoms/public-workspace/index.ts @@ -1,8 +1,9 @@ +import { getLoginStorage } from '@affine/workspace/affine/login'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { atom } from 'jotai/index'; import { BlockSuiteWorkspace } from '../../shared'; -import { apis } from '../../shared/apis'; +import { affineApis } from '../../shared/apis'; export const publicWorkspaceIdAtom = atom(null); export const publicBlockSuiteAtom = atom>( @@ -11,7 +12,7 @@ export const publicBlockSuiteAtom = atom>( if (!workspaceId) { throw new Error('No workspace id'); } - const binary = await apis.downloadWorkspace(workspaceId, true); + const binary = await affineApis.downloadWorkspace(workspaceId, true); // fixme: this is a hack const params = new URLSearchParams(window.location.search); const prefixUrl = params.get('prefixUrl') @@ -21,7 +22,9 @@ export const publicBlockSuiteAtom = atom>( workspaceId, (k: string) => // fixme: token could be expired - ({ api: `${prefixUrl}api/workspace`, token: apis.auth.token }[k]) + ({ api: `${prefixUrl}api/workspace`, token: getLoginStorage()?.token }[ + k + ]) ); BlockSuiteWorkspace.Y.applyUpdate( blockSuiteWorkspace.doc, diff --git a/apps/web/src/blocksuite/providers/affine/index.ts b/apps/web/src/blocksuite/providers/affine/index.ts index d75d32d65f..75f265d439 100644 --- a/apps/web/src/blocksuite/providers/affine/index.ts +++ b/apps/web/src/blocksuite/providers/affine/index.ts @@ -2,7 +2,7 @@ import { assertExists } from '@blocksuite/store'; import type { AffineDownloadProvider } from '../../../shared'; import { BlockSuiteWorkspace } from '../../../shared'; -import { apis } from '../../../shared/apis'; +import { affineApis } from '../../../shared/apis'; import { providerLogger } from '../../logger'; const hashMap = new Map(); @@ -25,7 +25,7 @@ export const createAffineDownloadProvider = ( ); return; } - apis.downloadWorkspace(id, false).then(binary => { + affineApis.downloadWorkspace(id, false).then(binary => { hashMap.set(id, binary); providerLogger.debug('applyUpdate'); BlockSuiteWorkspace.Y.applyUpdate( diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts index 2b21ad88d2..251976348f 100644 --- a/apps/web/src/blocksuite/providers/index.ts +++ b/apps/web/src/blocksuite/providers/index.ts @@ -1,4 +1,5 @@ -import { WebsocketProvider } from '@affine/datacenter'; +import { getLoginStorage } from '@affine/workspace/affine/login'; +import { WebsocketProvider } from '@affine/workspace/affine/sync'; import { assertExists } from '@blocksuite/store'; import { IndexeddbPersistence } from 'y-indexeddb'; @@ -7,7 +8,6 @@ import type { BlockSuiteWorkspace, LocalIndexedDBProvider, } from '../../shared'; -import { apis } from '../../shared/apis'; import { providerLogger } from '../logger'; import { createBroadCastChannelProvider } from './broad-cast-channel'; @@ -32,7 +32,7 @@ const createAffineWebSocketProvider = ( blockSuiteWorkspace.id, blockSuiteWorkspace.doc, { - params: { token: apis.auth.refresh }, + params: { token: getLoginStorage()?.token ?? '' }, // @ts-expect-error ignore the type awareness: blockSuiteWorkspace.awarenessStore.awareness, // we maintain broadcast channel by ourselves diff --git a/apps/web/src/components/affine/affine-error-eoundary.tsx b/apps/web/src/components/affine/affine-error-eoundary.tsx index 9141c69f4a..d099e51521 100644 --- a/apps/web/src/components/affine/affine-error-eoundary.tsx +++ b/apps/web/src/components/affine/affine-error-eoundary.tsx @@ -1,4 +1,4 @@ -import { RequestError } from '@affine/datacenter'; +import { RequestError } from '@affine/workspace/affine/api'; import type { NextRouter } from 'next/router'; import type { ErrorInfo } from 'react'; import type React from 'react'; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx index dfdec876ee..be3ea1af18 100644 --- a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx @@ -1,6 +1,6 @@ import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component'; -import { PermissionType } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; +import { PermissionType } from '@affine/workspace/affine/api'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { DeleteTemporarilyIcon, diff --git a/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx index dd0d2a5746..de95be39d0 100644 --- a/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx @@ -1,5 +1,10 @@ import { displayFlex, IconButton, styled, Tooltip } from '@affine/component'; import { useTranslation } from '@affine/i18n'; +import { + getLoginStorage, + setLoginStorage, + SignMethod, +} from '@affine/workspace/affine/login'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { CloudWorkspaceIcon, @@ -10,13 +15,13 @@ import { assertEquals, assertExists } from '@blocksuite/store'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; +import { affineAuth } from '../../../../hooks/affine/use-affine-log-in'; import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace'; import type { AffineOfficialWorkspace, LocalWorkspace, } from '../../../../shared'; -import { apis } from '../../../../shared/apis'; import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal'; const IconWrapper = styled('div')(({ theme }) => { @@ -112,8 +117,13 @@ export const SyncUser = () => { setOpen(false); }} onConform={async () => { - if (!apis.auth.isLogin) { - await apis.signInWithGoogle(); + if (!getLoginStorage()) { + const response = await affineAuth.generateToken( + SignMethod.Google + ); + if (response) { + setLoginStorage(response); + } router.reload(); return; } diff --git a/apps/web/src/components/pure/footer/index.tsx b/apps/web/src/components/pure/footer/index.tsx index 7a96c9b3da..0b03107de4 100644 --- a/apps/web/src/components/pure/footer/index.tsx +++ b/apps/web/src/components/pure/footer/index.tsx @@ -1,8 +1,8 @@ import { FlexWrapper } from '@affine/component'; import { IconButton } from '@affine/component'; import { Tooltip } from '@affine/component'; -import type { AccessTokenMessage } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; +import type { AccessTokenMessage } from '@affine/workspace/affine/login'; import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons'; import type { CSSProperties } from 'react'; import type React from 'react'; diff --git a/apps/web/src/components/pure/message-center/index.tsx b/apps/web/src/components/pure/message-center/index.tsx index 132b968755..9f6e9f1e3d 100644 --- a/apps/web/src/components/pure/message-center/index.tsx +++ b/apps/web/src/components/pure/message-center/index.tsx @@ -1,10 +1,10 @@ -import { MessageCode } from '@affine/datacenter'; -import { messages } from '@affine/datacenter'; +import { MessageCode, Messages } from '@affine/env/constant'; +import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login'; import type React from 'react'; import { memo, useEffect, useState } from 'react'; +import { affineAuth } from '../../../hooks/affine/use-affine-log-in'; import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out'; -import { apis } from '../../../shared/apis'; import { toast } from '../../../utils'; declare global { @@ -33,9 +33,12 @@ export const MessageCenter: React.FC = memo(function MessageCenter() { event.detail.code === MessageCode.loginError) ) { setPopup(true); - apis - .signInWithGoogle() - .then(() => { + affineAuth + .generateToken(SignMethod.Google) + .then(response => { + if (response) { + setLoginStorage(response); + } setPopup(false); }) .catch(() => { @@ -43,7 +46,7 @@ export const MessageCenter: React.FC = memo(function MessageCenter() { onLogout(); }); } else { - toast(messages[event.detail.code].message); + toast(Messages[event.detail.code].message); } }; diff --git a/apps/web/src/components/pure/workspace-card/index.tsx b/apps/web/src/components/pure/workspace-card/index.tsx index e2843e2cb8..5378f1732e 100644 --- a/apps/web/src/components/pure/workspace-card/index.tsx +++ b/apps/web/src/components/pure/workspace-card/index.tsx @@ -1,5 +1,5 @@ -import { PermissionType } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; +import { PermissionType } from '@affine/workspace/affine/api'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { SettingsIcon } from '@blocksuite/icons'; import type React from 'react'; diff --git a/apps/web/src/components/pure/workspace-list-modal/index.tsx b/apps/web/src/components/pure/workspace-list-modal/index.tsx index 449d06a47d..5e43b19c0b 100644 --- a/apps/web/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/web/src/components/pure/workspace-list-modal/index.tsx @@ -4,8 +4,8 @@ import { ModalWrapper, Tooltip, } from '@affine/component'; -import type { AccessTokenMessage } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; +import type { AccessTokenMessage } from '@affine/workspace/affine/login'; import { HelpIcon, PlusIcon } from '@blocksuite/icons'; import type { RemWorkspace } from '../../../shared'; diff --git a/apps/web/src/hooks/affine/use-affine-log-in.ts b/apps/web/src/hooks/affine/use-affine-log-in.ts index 8eb3d0a9bd..00f23bbf60 100644 --- a/apps/web/src/hooks/affine/use-affine-log-in.ts +++ b/apps/web/src/hooks/affine/use-affine-log-in.ts @@ -1,26 +1,30 @@ +import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import { createAffineAuth, + parseIdToken, setLoginStorage, SignMethod, } from '@affine/workspace/affine/login'; +import { useSetAtom } from 'jotai'; import { useRouter } from 'next/router'; import { useCallback } from 'react'; -import { apis } from '../../shared/apis'; import { toast } from '../../utils'; export const affineAuth = createAffineAuth(); export function useAffineLogIn() { const router = useRouter(); + const setUser = useSetAtom(currentAffineUserAtom); return useCallback(async () => { const response = await affineAuth.generateToken(SignMethod.Google); if (response) { setLoginStorage(response); - apis.auth.setLogin(response); + const user = parseIdToken(response.token); + setUser(user); router.reload(); } else { toast('Login failed'); } - }, [router]); + }, [router, setUser]); } diff --git a/apps/web/src/hooks/affine/use-affine-log-out.ts b/apps/web/src/hooks/affine/use-affine-log-out.ts index 6af933c022..78ce0d89d9 100644 --- a/apps/web/src/hooks/affine/use-affine-log-out.ts +++ b/apps/web/src/hooks/affine/use-affine-log-out.ts @@ -1,3 +1,4 @@ +import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; import { clearLoginStorage } from '@affine/workspace/affine/login'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { useSetAtom } from 'jotai'; @@ -6,13 +7,12 @@ import { useCallback } from 'react'; import { jotaiWorkspacesAtom } from '../../atoms'; import { WorkspacePlugins } from '../../plugins'; -import { apis } from '../../shared/apis'; export function useAffineLogOut() { const set = useSetAtom(jotaiWorkspacesAtom); const router = useRouter(); + const setCurrentUser = useSetAtom(currentAffineUserAtom); return useCallback(() => { - apis.auth.clear(); set(workspaces => workspaces.filter( workspace => workspace.flavour !== WorkspaceFlavour.AFFINE @@ -20,6 +20,7 @@ export function useAffineLogOut() { ); WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.(); clearLoginStorage(); + setCurrentUser(null); router.reload(); - }, [router, set]); + }, [router, set, setCurrentUser]); } diff --git a/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts index 5796db4dfb..7d005e37e3 100644 --- a/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts +++ b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts @@ -7,7 +7,6 @@ import { } from '@affine/workspace/affine/login'; import useSWR from 'swr'; -import { apis } from '../../shared/apis'; import { affineAuth } from './use-affine-log-in'; const logger = new DebugLogger('auth-token'); @@ -22,7 +21,6 @@ const revalidate = async () => { const response = await affineAuth.refreshToken(storage); if (response) { setLoginStorage(response); - apis.auth.setLogin(response); } } } diff --git a/apps/web/src/hooks/affine/use-is-workspace-owner.ts b/apps/web/src/hooks/affine/use-is-workspace-owner.ts index 0a78200483..f8dc9c38ce 100644 --- a/apps/web/src/hooks/affine/use-is-workspace-owner.ts +++ b/apps/web/src/hooks/affine/use-is-workspace-owner.ts @@ -1,4 +1,4 @@ -import { PermissionType } from '@affine/datacenter'; +import { PermissionType } from '@affine/workspace/affine/api'; import type { AffineOfficialWorkspace } from '../../shared'; diff --git a/apps/web/src/hooks/affine/use-members.ts b/apps/web/src/hooks/affine/use-members.ts index 37e576adb9..46cf5abb4e 100644 --- a/apps/web/src/hooks/affine/use-members.ts +++ b/apps/web/src/hooks/affine/use-members.ts @@ -1,9 +1,9 @@ -import type { Member } from '@affine/datacenter'; +import type { Member } from '@affine/workspace/affine/api'; import { useCallback } from 'react'; import useSWR from 'swr'; import { QueryKey } from '../../plugins/affine/fetcher'; -import { apis } from '../../shared/apis'; +import { affineApis } from '../../shared/apis'; export function useMembers(workspaceId: string) { const { data, mutate } = useSWR( @@ -15,7 +15,7 @@ export function useMembers(workspaceId: string) { const inviteMember = useCallback( async (email: string) => { - await apis.inviteMember({ + await affineApis.inviteMember({ id: workspaceId, email, }); @@ -26,8 +26,7 @@ export function useMembers(workspaceId: string) { const removeMember = useCallback( async (permissionId: number) => { - // fixme: what about the workspaceId? - await apis.removeMember({ + await affineApis.removeMember({ permissionId, }); return mutate(); diff --git a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts index 270ed4969d..3031a7908d 100644 --- a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts +++ b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts @@ -4,13 +4,13 @@ import useSWR from 'swr'; import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms'; import { QueryKey } from '../../plugins/affine/fetcher'; import type { AffineWorkspace } from '../../shared'; -import { apis } from '../../shared/apis'; +import { affineApis } from '../../shared/apis'; export function useToggleWorkspacePublish(workspace: AffineWorkspace) { const { mutate } = useSWR(QueryKey.getWorkspaces); return useCallback( async (isPublish: boolean) => { - await apis.updateWorkspace({ + await affineApis.updateWorkspace({ id: workspace.id, public: isPublish, }); diff --git a/apps/web/src/hooks/current/use-current-user.ts b/apps/web/src/hooks/current/use-current-user.ts index 7204d2cec6..1bf4f2373a 100644 --- a/apps/web/src/hooks/current/use-current-user.ts +++ b/apps/web/src/hooks/current/use-current-user.ts @@ -1,11 +1,7 @@ -import type { AccessTokenMessage } from '@affine/datacenter'; -import useSWR from 'swr'; - -import { QueryKey } from '../../plugins/affine/fetcher'; +import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; +import type { AccessTokenMessage } from '@affine/workspace/affine/login'; +import { useAtomValue } from 'jotai'; export function useCurrentUser(): AccessTokenMessage | null { - const { data } = useSWR(QueryKey.getUser, { - fallbackData: null, - }); - return data ?? null; + return useAtomValue(currentAffineUserAtom); } diff --git a/apps/web/src/pages/invite/[invite_code].tsx b/apps/web/src/pages/invite/[invite_code].tsx index 2a35c36726..32c5c362c9 100644 --- a/apps/web/src/pages/invite/[invite_code].tsx +++ b/apps/web/src/pages/invite/[invite_code].tsx @@ -1,6 +1,6 @@ import { displayFlex, styled } from '@affine/component'; import { Button } from '@affine/component'; -import type { Permission } from '@affine/datacenter'; +import type { Permission } from '@affine/workspace/affine/api'; import { SucessfulDuotoneIcon, UnsucessfulDuotoneIcon, diff --git a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx index ad7297beb4..d167c865da 100644 --- a/apps/web/src/pages/workspace/[workspaceId]/setting.tsx +++ b/apps/web/src/pages/workspace/[workspaceId]/setting.tsx @@ -1,4 +1,11 @@ import { useTranslation } from '@affine/i18n'; +import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; +import { + getLoginStorage, + parseIdToken, + setLoginStorage, + SignMethod, +} from '@affine/workspace/affine/login'; import type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type'; import { settingPanel, @@ -7,7 +14,7 @@ import { } from '@affine/workspace/type'; import { SettingsIcon } from '@blocksuite/icons'; import { assertExists } from '@blocksuite/store'; -import { useAtom } from 'jotai'; +import { useAtom, useSetAtom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import Head from 'next/head'; import { useRouter } from 'next/router'; @@ -16,6 +23,7 @@ import React, { useCallback, useEffect } from 'react'; import { Unreachable } from '../../../components/affine/affine-error-eoundary'; import { PageLoading } from '../../../components/pure/loading'; import { WorkspaceTitle } from '../../../components/pure/workspace-title'; +import { affineAuth } from '../../../hooks/affine/use-affine-log-in'; import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace'; import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace'; import { useTransformWorkspace } from '../../../hooks/use-transform-workspace'; @@ -23,7 +31,6 @@ import { useWorkspacesHelper } from '../../../hooks/use-workspaces'; import { WorkspaceLayout } from '../../../layouts'; import { WorkspacePlugins } from '../../../plugins'; import type { NextPageWithLayout } from '../../../shared'; -import { apis } from '../../../shared/apis'; const settingPanelAtom = atomWithStorage( 'workspaceId', @@ -101,15 +108,20 @@ const SettingPage: NextPageWithLayout = () => { return helper.deleteWorkspace(workspaceId); }, [currentWorkspace, helper]); const transformWorkspace = useTransformWorkspace(); + const setUser = useSetAtom(currentAffineUserAtom); const onTransformWorkspace = useCallback( async ( from: From, to: To, workspace: WorkspaceRegistry[From] ): Promise => { - const needRefresh = to === WorkspaceFlavour.AFFINE && !apis.auth.isLogin; + const needRefresh = to === WorkspaceFlavour.AFFINE && !getLoginStorage(); if (needRefresh) { - await apis.signInWithGoogle(); + const response = await affineAuth.generateToken(SignMethod.Google); + if (response) { + setLoginStorage(response); + setUser(parseIdToken(response.token)); + } } const workspaceId = await transformWorkspace(from, to, workspace); await router.replace({ @@ -119,11 +131,8 @@ const SettingPage: NextPageWithLayout = () => { workspaceId, }, }); - if (needRefresh) { - router.reload(); - } }, - [router, transformWorkspace] + [router, setUser, transformWorkspace] ); if (!router.isReady) { return ; diff --git a/apps/web/src/plugins/affine/fetcher.ts b/apps/web/src/plugins/affine/fetcher.ts index d452f8db1b..bb221364d5 100644 --- a/apps/web/src/plugins/affine/fetcher.ts +++ b/apps/web/src/plugins/affine/fetcher.ts @@ -1,3 +1,4 @@ +import { getLoginStorage } from '@affine/workspace/affine/login'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { assertExists } from '@blocksuite/store'; @@ -6,7 +7,7 @@ import { jotaiStore, workspacesAtom } from '../../atoms'; import { createAffineProviders } from '../../blocksuite'; import { Unreachable } from '../../components/affine/affine-error-eoundary'; import type { AffineWorkspace } from '../../shared'; -import { apis } from '../../shared/apis'; +import { affineApis } from '../../shared/apis'; type Query = (typeof QueryKey)[keyof typeof QueryKey]; @@ -17,24 +18,21 @@ export const fetcher = async ( | [Query, string] | [Query, string, string] ) => { - if (query === QueryKey.getUser) { - return apis.auth.user ?? null; - } if (Array.isArray(query)) { if (query[0] === QueryKey.downloadWorkspace) { if (typeof query[2] !== 'boolean') { throw new Unreachable(); } - return apis.downloadWorkspace(query[1], query[2]); + return affineApis.downloadWorkspace(query[1], query[2]); } else if (query[0] === QueryKey.getMembers) { - return apis.getWorkspaceMembers({ + return affineApis.getWorkspaceMembers({ id: query[1], }); } else if (query[0] === QueryKey.getUserByEmail) { if (typeof query[2] !== 'string') { throw new Unreachable(); } - return apis.getUserByEmail({ + return affineApis.getUserByEmail({ workspace_id: query[1], email: query[2], }); @@ -57,19 +55,19 @@ export const fetcher = async ( if (typeof invitingCode !== 'string') { throw new TypeError('invitingCode must be a string'); } - return apis.acceptInviting({ + return affineApis.acceptInviting({ invitingCode, }); } } else { if (query === QueryKey.getWorkspaces) { - return apis.getWorkspaces().then(workspaces => { + return affineApis.getWorkspaces().then(workspaces => { return workspaces.map(workspace => { const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( workspace.id, (k: string) => // fixme: token could be expired - ({ api: '/api/workspace', token: apis.auth.token }[k]) + ({ api: '/api/workspace', token: getLoginStorage()?.token }[k]) ); const remWorkspace: AffineWorkspace = { ...workspace, @@ -81,14 +79,13 @@ export const fetcher = async ( }); }); } - return (apis as any)[query](); + return (affineApis as any)[query](); } }; export const QueryKey = { acceptInvite: 'acceptInvite', getImage: 'getImage', - getUser: 'getUser', getWorkspaces: 'getWorkspaces', downloadWorkspace: 'downloadWorkspace', getMembers: 'getMembers', diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index d858180940..c0facda071 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -1,3 +1,4 @@ +import { getLoginStorage } from '@affine/workspace/affine/login'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createJSONStorage } from 'jotai/utils'; @@ -12,8 +13,8 @@ import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page import { PageDetailEditor } from '../../components/page-detail-editor'; import type { AffineWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared'; -import { apis, clientAuth } from '../../shared/apis'; -import { initPage } from '../../utils/blocksuite'; +import { affineApis } from '../../shared/apis'; +import { initPage } from '../../utils'; import type { WorkspacePlugin } from '..'; import { QueryKey } from './fetcher'; @@ -39,7 +40,7 @@ const getPersistenceAllWorkspace = () => { item.id, (k: string) => // fixme: token could be expired - ({ api: '/api/workspace', token: apis.auth.token }[k]) + ({ api: '/api/workspace', token: getLoginStorage()?.token }[k]) ); const affineWorkspace: AffineWorkspace = { ...item, @@ -65,7 +66,9 @@ export const AffinePlugin: WorkspacePlugin = { const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate( blockSuiteWorkspace.doc ); - const { id } = await apis.createWorkspace(new Blob([binary.buffer])); + const { id } = await affineApis.createWorkspace( + new Blob([binary.buffer]) + ); // fixme: syncing images const newWorkspaceId = id; @@ -77,12 +80,7 @@ export const AffinePlugin: WorkspacePlugin = { const url = await blobs.get(id); if (url) { const blob = await fetch(url).then(res => res.blob()); - await clientAuth.put(`api/workspace/${newWorkspaceId}/blob`, { - body: blob, - headers: { - 'Content-Type': blob.type, - }, - }); + await affineApis.uploadBlob(newWorkspaceId, blob); } } } @@ -103,14 +101,14 @@ export const AffinePlugin: WorkspacePlugin = { items.filter(item => item.id !== workspace.id) ); } - await apis.deleteWorkspace({ + await affineApis.deleteWorkspace({ id: workspace.id, }); await mutate(matcher => matcher === QueryKey.getWorkspaces); }, get: async workspaceId => { try { - if (!apis.auth.isLogin) { + if (!getLoginStorage()) { const workspaces = getPersistenceAllWorkspace(); return ( workspaces.find(workspace => workspace.id === workspaceId) ?? null @@ -130,69 +128,65 @@ export const AffinePlugin: WorkspacePlugin = { list: async () => { const allWorkspaces = getPersistenceAllWorkspace(); try { - if (apis.auth.isLogin) { - const workspaces = await apis.getWorkspaces().then(workspaces => { - return workspaces.map(workspace => { - const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( - workspace.id, - (k: string) => - // fixme: token could be expired - ({ api: '/api/workspace', token: apis.auth.token }[k]) - ); - const dump = workspaces.map(workspace => { - return { - id: workspace.id, - type: workspace.type, - public: workspace.public, - permission: workspace.permission, - } satisfies z.infer; - }); - const old = storage.getItem(kAffineLocal); - if ( - Array.isArray(old) && - old.every(item => schema.safeParse(item).success) - ) { - const data = [...dump]; - old.forEach((item: z.infer) => { - const has = dump.find(dump => dump.id === item.id); - if (!has) { - data.push(item); - } - }); - storage.setItem(kAffineLocal, [...data]); - } - - const affineWorkspace: AffineWorkspace = { - ...workspace, - flavour: WorkspaceFlavour.AFFINE, - blockSuiteWorkspace, - providers: [...createAffineProviders(blockSuiteWorkspace)], - }; - return affineWorkspace; - }); - }); - workspaces.forEach(workspace => { - const idx = allWorkspaces.findIndex( - ({ id }) => id === workspace.id + const workspaces = await affineApis.getWorkspaces().then(workspaces => { + return workspaces.map(workspace => { + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + workspace.id, + (k: string) => + // fixme: token could be expired + ({ api: '/api/workspace', token: getLoginStorage()?.token }[k]) ); - if (idx !== -1) { - allWorkspaces.splice(idx, 1, workspace); - } else { - allWorkspaces.push(workspace); + const dump = workspaces.map(workspace => { + return { + id: workspace.id, + type: workspace.type, + public: workspace.public, + permission: workspace.permission, + } satisfies z.infer; + }); + const old = storage.getItem(kAffineLocal); + if ( + Array.isArray(old) && + old.every(item => schema.safeParse(item).success) + ) { + const data = [...dump]; + old.forEach((item: z.infer) => { + const has = dump.find(dump => dump.id === item.id); + if (!has) { + data.push(item); + } + }); + storage.setItem(kAffineLocal, [...data]); } - }); - // only save data when login in - const dump = allWorkspaces.map(workspace => { - return { - id: workspace.id, - type: workspace.type, - public: workspace.public, - permission: workspace.permission, - } satisfies z.infer; + const affineWorkspace: AffineWorkspace = { + ...workspace, + flavour: WorkspaceFlavour.AFFINE, + blockSuiteWorkspace, + providers: [...createAffineProviders(blockSuiteWorkspace)], + }; + return affineWorkspace; }); - storage.setItem(kAffineLocal, [...dump]); - } + }); + workspaces.forEach(workspace => { + const idx = allWorkspaces.findIndex(({ id }) => id === workspace.id); + if (idx !== -1) { + allWorkspaces.splice(idx, 1, workspace); + } else { + allWorkspaces.push(workspace); + } + }); + + // only save data when login in + const dump = allWorkspaces.map(workspace => { + return { + id: workspace.id, + type: workspace.type, + public: workspace.public, + permission: workspace.permission, + } satisfies z.infer; + }); + storage.setItem(kAffineLocal, [...dump]); } catch (e) { console.error('fetch affine workspaces failed', e); } diff --git a/apps/web/src/shared/apis.ts b/apps/web/src/shared/apis.ts index 28b106739f..0f485a614e 100644 --- a/apps/web/src/shared/apis.ts +++ b/apps/web/src/shared/apis.ts @@ -1,16 +1,14 @@ -import { - createAuthClient, - createBareClient, - getApis, - GoogleAuth, -} from '@affine/datacenter'; import { config } from '@affine/env'; import { createUserApis, createWorkspaceApis, } from '@affine/workspace/affine/api'; +import { currentAffineUserAtom } from '@affine/workspace/affine/atom'; +import type { LoginResponse } from '@affine/workspace/affine/login'; +import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login'; -import { isValidIPAddress } from '../utils/is-valid-ip-address'; +import { jotaiStore } from '../atoms'; +import { isValidIPAddress } from '../utils'; let prefixUrl = '/'; if (typeof window === 'undefined') { @@ -31,32 +29,27 @@ if (typeof window === 'undefined') { params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string); } +const affineApis = {} as ReturnType & + ReturnType; +Object.assign(affineApis, createUserApis(prefixUrl)); +Object.assign(affineApis, createWorkspaceApis(prefixUrl)); + +if (!globalThis.AFFINE_APIS) { + globalThis.AFFINE_APIS = affineApis; + globalThis.setLogin = (response: LoginResponse) => { + jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token)); + setLoginStorage(response); + }; +} + declare global { // eslint-disable-next-line no-var - var affineApis: + var setLogin: typeof setLoginStorage; + // eslint-disable-next-line no-var + var AFFINE_APIS: | undefined | (ReturnType & ReturnType); } -const affineApis = {} as ReturnType & - ReturnType; -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; -} +export { affineApis }; diff --git a/apps/web/src/shared/index.ts b/apps/web/src/shared/index.ts index cf73b8cdd2..eac47a1a2e 100644 --- a/apps/web/src/shared/index.ts +++ b/apps/web/src/shared/index.ts @@ -1,4 +1,4 @@ -import type { Workspace as RemoteWorkspace } from '@affine/datacenter'; +import type { Workspace as RemoteWorkspace } from '@affine/workspace/affine/api'; import type { WorkspaceFlavour } from '@affine/workspace/type'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { NextPage } from 'next'; diff --git a/nyc.config.js b/nyc.config.js index cb39092358..8ae468fbd0 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -4,9 +4,5 @@ const defaultExclude = require('@istanbuljs/schema/default-exclude'); module.exports = { - exclude: [ - ...defaultExclude, - // data-center will be removed in the future, we don't need to coverage it - 'packages/data-center/**', - ], + exclude: [...defaultExclude], }; diff --git a/packages/data-center/.gitignore b/packages/data-center/.gitignore deleted file mode 100644 index 75e854d8dc..0000000000 --- a/packages/data-center/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ diff --git a/packages/data-center/package.json b/packages/data-center/package.json deleted file mode 100644 index 2f10898606..0000000000 --- a/packages/data-center/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts deleted file mode 100644 index b04a55955b..0000000000 --- a/packages/data-center/src/datacenter.ts +++ /dev/null @@ -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 = new Map(); - private _messageCenter = MessageCenter.getInstance(); - - /** - * A mainProvider must exist as the only data trustworthy source. - */ - private _mainProvider?: BaseProvider; - providerMap: Map = new Map(); - - static initEmpty() { - return new DataCenter(); - } - - static async init(exclude: 'affine'[] = []): Promise { - 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} - */ - 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} - */ - public async loadWorkspace( - workspaceId: string - ): Promise { - 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} - */ - 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 = {}; - 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 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} blob url - */ - async getBlob( - workspaceUnit: WorkspaceUnit, - id: string - ): Promise { - const blob = await workspaceUnit.blocksuiteWorkspace?.blobs; - return (await blob?.get(id)) || ''; - } - - /** - * up load blob and get a blob url - * @param id - * @returns {Promise} blob url - */ - async setBlob(workspace: WorkspaceUnit, blob: Blob): Promise { - 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 - */ - 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); - } -} diff --git a/packages/data-center/src/global-types.ts b/packages/data-center/src/global-types.ts deleted file mode 100644 index 9e71ac5b62..0000000000 --- a/packages/data-center/src/global-types.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare global { - interface Window { - CLIENT_APP?: boolean; - __editoVersion?: string; - } -} - -export {}; diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts deleted file mode 100644 index 758d59dfed..0000000000 --- a/packages/data-center/src/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DataCenter } from './datacenter'; - -const _initializeDataCenter = () => { - let _dataCenterInstance: Promise; - - 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'; diff --git a/packages/data-center/src/message/code.ts b/packages/data-center/src/message/code.ts deleted file mode 100644 index f9bb200948..0000000000 --- a/packages/data-center/src/message/code.ts +++ /dev/null @@ -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; diff --git a/packages/data-center/src/message/index.ts b/packages/data-center/src/message/index.ts deleted file mode 100644 index 1214a8e4b6..0000000000 --- a/packages/data-center/src/message/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MessageCode } from './code'; -export { MessageCenter } from './message'; diff --git a/packages/data-center/src/message/message.ts b/packages/data-center/src/message/message.ts deleted file mode 100644 index 5e771a6058..0000000000 --- a/packages/data-center/src/message/message.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Observable } from 'lib0/observable'; - -import type { Message } from '../types'; -import { MessageCode, messages } from './code'; - -export class MessageCenter extends Observable { - private _messages: Record> = - 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); - }; - } -} diff --git a/packages/data-center/src/provider/affine/__tests__/mock-apis.ts b/packages/data-center/src/provider/affine/__tests__/mock-apis.ts deleted file mode 100644 index 6c95d5e6ce..0000000000 --- a/packages/data-center/src/provider/affine/__tests__/mock-apis.ts +++ /dev/null @@ -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; diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts deleted file mode 100644 index 8d37259d36..0000000000 --- a/packages/data-center/src/provider/affine/affine.ts +++ /dev/null @@ -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; - metadata: Record; -}; - -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 = new Map(); - private _apis: Apis; - private _channel?: WebsocketClient; - private _refreshToken?: string; - // private _idbMap: Map = new Map(); - private _workspaceLoadingQueue: Map> = - new Map(); - - private _workspaces$: Promise | 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((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 { - 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 { - await this.closeWorkspace(id); - // IndexedDBProvider.delete(id); - await this._apis.deleteWorkspace({ id }); - this._workspaces.remove(id); - } - - public override async clear(): Promise { - 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 { - await this._apis.leaveWorkspace({ id }); - } - - public override async invite(id: string, email: string): Promise { - return await this._apis.inviteMember({ id, email }); - } - - public override async removeMember(permissionId: number): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 }); - } -} diff --git a/packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts b/packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts deleted file mode 100644 index 2b26c02ac7..0000000000 --- a/packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/data-center/src/provider/affine/apis/google.ts b/packages/data-center/src/provider/affine/apis/google.ts deleted file mode 100644 index d9469bbaca..0000000000 --- a/packages/data-center/src/provider/affine/apis/google.ts +++ /dev/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 => - 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; - private readonly _doLogin: ReturnType; - - 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; -}; diff --git a/packages/data-center/src/provider/affine/apis/index.ts b/packages/data-center/src/provider/affine/apis/index.ts deleted file mode 100644 index 53d69acb11..0000000000 --- a/packages/data-center/src/provider/affine/apis/index.ts +++ /dev/null @@ -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 = { - [K in keyof T]: T[K]; - // eslint-disable-next-line @typescript-eslint/ban-types -} & {}; - -export type Apis = Prettify< - ReturnType & - ReturnType & { - signInWithGoogle: ReturnType[0]; - onAuthStateChanged: ReturnType[1]; - signOutFirebase: ReturnType[2]; - } & { auth: ReturnType } ->; - -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'; diff --git a/packages/data-center/src/provider/affine/apis/request-error.ts b/packages/data-center/src/provider/affine/apis/request-error.ts deleted file mode 100644 index 9ab8e401a4..0000000000 --- a/packages/data-center/src/provider/affine/apis/request-error.ts +++ /dev/null @@ -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; diff --git a/packages/data-center/src/provider/affine/apis/request.ts b/packages/data-center/src/provider/affine/apis/request.ts deleted file mode 100644 index 7034baaed9..0000000000 --- a/packages/data-center/src/provider/affine/apis/request.ts +++ /dev/null @@ -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); - }, - ], - }, - }); diff --git a/packages/data-center/src/provider/affine/apis/user.ts b/packages/data-center/src/provider/affine/apis/user.ts deleted file mode 100644 index 720de5700e..0000000000 --- a/packages/data-center/src/provider/affine/apis/user.ts +++ /dev/null @@ -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 => { - const searchParams = new URLSearchParams({ ...params }); - return authClient.get('api/user', { searchParams }).json(); - }, - } as const; -} diff --git a/packages/data-center/src/provider/affine/apis/workspace.ts b/packages/data-center/src/provider/affine/apis/workspace.ts deleted file mode 100644 index 9e1dd89580..0000000000 --- a/packages/data-center/src/provider/affine/apis/workspace.ts +++ /dev/null @@ -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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - return authClient.put('api/blob', { body: params.blob }).text(); - }, - getBlob: async (params: { blobId: string }): Promise => { - 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 => { - 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; -} diff --git a/packages/data-center/src/provider/affine/channel.ts b/packages/data-center/src/provider/affine/channel.ts deleted file mode 100644 index e37dc5c314..0000000000 --- a/packages/data-center/src/provider/affine/channel.ts +++ /dev/null @@ -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[1] & { - params: Record; - } - ) { - 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); - } - } - } - }); - } -} diff --git a/packages/data-center/src/provider/affine/idb-kv.ts b/packages/data-center/src/provider/affine/idb-kv.ts deleted file mode 100644 index 6edd8a8530..0000000000 --- a/packages/data-center/src/provider/affine/idb-kv.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { clear, createStore, getMany, keys, setMany } from 'idb-keyval'; -import * as idb from 'lib0/indexeddb'; - -type IDBInstance = { - keys: () => Promise; - clear: () => Promise; - deleteDB: () => Promise; - setMany: (entries: [string, T][]) => Promise; - getMany: (keys: string[]) => Promise; -}; - -export function getDatabase( - type: string, - database: string -): IDBInstance { - 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), - }; -} diff --git a/packages/data-center/src/provider/affine/index.ts b/packages/data-center/src/provider/affine/index.ts deleted file mode 100644 index 8f9dc079c0..0000000000 --- a/packages/data-center/src/provider/affine/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './affine'; diff --git a/packages/data-center/src/provider/affine/storage.ts b/packages/data-center/src/provider/affine/storage.ts deleted file mode 100644 index 95031de655..0000000000 --- a/packages/data-center/src/provider/affine/storage.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { varStorage } from 'lib0/storage'; - -export const storage = varStorage as Storage; diff --git a/packages/data-center/src/provider/affine/utils.ts b/packages/data-center/src/provider/affine/utils.ts deleted file mode 100644 index 5a7c695cbf..0000000000 --- a/packages/data-center/src/provider/affine/utils.ts +++ /dev/null @@ -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('pending', oldWorkspaceId); - - const newDB = getDatabase('blob', newWorkspaceId); - const newPendingDB = getDatabase('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(); -}; diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts deleted file mode 100644 index 58f89273e9..0000000000 --- a/packages/data-center/src/provider/base.ts +++ /dev/null @@ -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; -export type UpdateWorkspaceMetaParams = Partial< - Pick ->; - -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['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 { - return workspace; - } - - /** - * @deprecated Temporary for public workspace - * @param blocksuiteWorkspace - * @returns - */ - public async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) { - return blocksuiteWorkspace; - } - - /** - * load workspaces - **/ - public async loadWorkspaces(): Promise { - throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); - } - - /** - * get auth user info - * @returns - */ - public async getUserInfo(): Promise { - return; - } - - // async getBlob(id: string): Promise { - // return await this._blobs.get(id); - // } - - // async setBlob(blob: Blob): Promise { - // 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 { - id; - return; - } - - /** - * leave workspace by workspace id - * @param id workspace id - */ - public async leaveWorkspace(id: string): Promise { - 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 { - id; - email; - return; - } - - /** - * remove workspace member by permission id - * @param permissionId - */ - public async removeMember(permissionId: number): Promise { - permissionId; - return; - } - - public async publish(id: string, isPublish: boolean): Promise { - 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 { - id; - params; - return; - } - - /** - * create workspace by workspace meta - * @param {CreateWorkspaceInfoParams} meta - */ - public async createWorkspace( - meta: CreateWorkspaceInfoParams - ): Promise { - throw new Error(`provider: ${this.id} createWorkspace not implemented`); - } - - public async extendWorkspace( - workspaceUnit: WorkspaceUnit - ): Promise { - 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 { - email; - return null; - } - - /** - * link workspace to local caches - * @param workspace - * @returns - */ - public async linkLocal( - workspace: BlocksuiteWorkspace - ): Promise { - return workspace; - } - - /** - * get workspace members - * @param {string} workspaceId - * @returns - */ - public getWorkspaceMembers(workspaceId: string): Promise { - workspaceId; - return Promise.resolve([]); - } - - /** - * accept invitation - * @param {string} inviteCode - * @returns - */ - public async acceptInvitation( - inviteCode: string - ): Promise { - inviteCode; - return null; - } -} diff --git a/packages/data-center/src/provider/index.ts b/packages/data-center/src/provider/index.ts deleted file mode 100644 index abfae4ce71..0000000000 --- a/packages/data-center/src/provider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './affine/affine'; diff --git a/packages/data-center/src/provider/local/index.ts b/packages/data-center/src/provider/local/index.ts deleted file mode 100644 index 0646cef75a..0000000000 --- a/packages/data-center/src/provider/local/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './local'; diff --git a/packages/data-center/src/provider/local/indexeddb/indexeddb.ts b/packages/data-center/src/provider/local/indexeddb/indexeddb.ts deleted file mode 100644 index 11453aab74..0000000000 --- a/packages/data-center/src/provider/local/indexeddb/indexeddb.ts +++ /dev/null @@ -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[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 { - doc: Doc; - name: string; - _dbref: number; - _dbsize: number; - private _destroyed: boolean; - whenSynced: Promise; - db: IDBDatabase | null; - private _db: Promise; - 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} - */ - async clearData(): Promise { - return this.destroy().then(() => { - idb.deleteDB(this.name); - }); - } - - /** - * @param {String | number | ArrayBuffer | Date} key - * @return {Promise} - */ - async get( - key: string | number | ArrayBuffer | Date - ): Promise { - 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} - */ - async set( - key: string | number | ArrayBuffer | Date, - value: string | number | ArrayBuffer | Date - ): Promise { - 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} - */ - async del(key: string | number | ArrayBuffer | Date): Promise { - 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 { - return idb.deleteDB(name); - } -} diff --git a/packages/data-center/src/provider/local/indexeddb/utils.ts b/packages/data-center/src/provider/local/indexeddb/utils.ts deleted file mode 100644 index f0c755ee61..0000000000 --- a/packages/data-center/src/provider/local/indexeddb/utils.ts +++ /dev/null @@ -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; -}; diff --git a/packages/data-center/src/provider/local/local.spec.ts b/packages/data-center/src/provider/local/local.spec.ts deleted file mode 100644 index 3e6f524a1b..0000000000 --- a/packages/data-center/src/provider/local/local.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts deleted file mode 100644 index d88171e856..0000000000 --- a/packages/data-center/src/provider/local/local.ts +++ /dev/null @@ -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 = 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 { - assert(workspace.id); - await applyLocalUpdates(workspace); - await this.linkLocal(workspace); - return workspace; - } - - override async loadWorkspaces(): Promise { - 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 { - 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 { - 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 { - const workspaces = await this.loadWorkspaces(); - workspaces.forEach(ws => IndexedDBProvider.delete(ws.id)); - this._storeWorkspaces([]); - this._workspaces.clear(); - this._idbMap.clear(); - } -} diff --git a/packages/data-center/src/provider/local/utils.ts b/packages/data-center/src/provider/local/utils.ts deleted file mode 100644 index b8492974ef..0000000000 --- a/packages/data-center/src/provider/local/utils.ts +++ /dev/null @@ -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; -}; diff --git a/packages/data-center/src/provider/selfhosted/index.ts.bk b/packages/data-center/src/provider/selfhosted/index.ts.bk deleted file mode 100644 index b347a6056b..0000000000 --- a/packages/data-center/src/provider/selfhosted/index.ts.bk +++ /dev/null @@ -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((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/`; -// } -// } diff --git a/packages/data-center/src/provider/selfhosted/sync.js b/packages/data-center/src/provider/selfhosted/sync.js deleted file mode 100644 index 03fca20820..0000000000 --- a/packages/data-center/src/provider/selfhosted/sync.js +++ /dev/null @@ -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} - */ -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} - */ -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} [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(); - } - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/blocksuite-provider/blob.ts b/packages/data-center/src/provider/tauri-ipc/blocksuite-provider/blob.ts deleted file mode 100644 index cd8d1cee54..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/blocksuite-provider/blob.ts +++ /dev/null @@ -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(), - }; - - private constructor(workspace: string) { - this.#workspace = workspace; - } - - static async init(workspace: string): Promise { - const provider = new IPCBlobProvider(workspace); - return provider; - } - - get uploading() { - return false; - } - - get blobs() { - // TODO: implement blob list in Octobase - return Promise.resolve([]) as Promise; - } - - async get(id: BlobId): Promise { - 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 { - // 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 { - // TODO: implement blob delete in Octobase - } - - async clear(): Promise { - // TODO: implement blob clear in Octobase, use workspace id #workspace - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/index.ts b/packages/data-center/src/provider/tauri-ipc/index.ts deleted file mode 100644 index 392870572e..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/index.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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; - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts b/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts deleted file mode 100644 index e062ae583b..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/methods.ts +++ /dev/null @@ -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('update_y_document', { - parameters, - }); - -export const getYDocument = async (parameters: GetDocumentParameter) => - await invoke('get_doc', { - parameters, - }); - -export const createWorkspace = async (parameters: CreateWorkspace) => - await invoke('create_workspace', { - parameters, - }); - -export const getWorkspaces = async (parameters: GetWorkspaces) => - await invoke('get_workspaces', { - parameters, - }); - -export const getWorkspace = async (parameters: GetWorkspace) => - await invoke('get_workspace', { - parameters, - }); - -export const putBlob = async (parameters: PutBlob) => - await invoke('put_blob', { - parameters, - }); - -export const getBlob = async (parameters: GetBlob) => - await invoke('get_blob', { - parameters, - }); - -/** - * This will create a private workspace too. - * @returns - */ -export const createUser = async (parameters: CreateUser) => - await invoke('create_user', { - parameters, - }); - -export const getUser = async (parameters: GetUserParameters) => - await invoke('get_user', { - parameters, - }); diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.json b/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.json deleted file mode 100644 index 9b00a5148e..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.json +++ /dev/null @@ -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"] - } - } - } - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.ts b/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.ts deleted file mode 100644 index 7f53c1c392..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/blob.ts +++ /dev/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; -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json deleted file mode 100644 index bd504bf000..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.json +++ /dev/null @@ -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 - } - } - } - } - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts b/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts deleted file mode 100644 index fa03019609..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/document.ts +++ /dev/null @@ -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; -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/user.json b/packages/data-center/src/provider/tauri-ipc/ipc/types/user.json deleted file mode 100644 index 9b3f54ec4f..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/user.json +++ /dev/null @@ -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" - } - } - } - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/user.ts b/packages/data-center/src/provider/tauri-ipc/ipc/types/user.ts deleted file mode 100644 index b03c418a64..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/user.ts +++ /dev/null @@ -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; -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.json b/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.json deleted file mode 100644 index 88ed9e5146..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.json +++ /dev/null @@ -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" - } - } - } - } -} diff --git a/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.ts b/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.ts deleted file mode 100644 index 025a1befb5..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/ipc/types/workspace.ts +++ /dev/null @@ -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; -} diff --git a/packages/data-center/src/provider/tauri-ipc/utils.ts b/packages/data-center/src/provider/tauri-ipc/utils.ts deleted file mode 100644 index 5bfb8cb47f..0000000000 --- a/packages/data-center/src/provider/tauri-ipc/utils.ts +++ /dev/null @@ -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; -}; diff --git a/packages/data-center/src/provider/utils.ts b/packages/data-center/src/provider/utils.ts deleted file mode 100644 index 638ea55af1..0000000000 --- a/packages/data-center/src/provider/utils.ts +++ /dev/null @@ -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); - } -}; diff --git a/packages/data-center/src/store.ts b/packages/data-center/src/store.ts deleted file mode 100644 index 7f666ba048..0000000000 --- a/packages/data-center/src/store.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - clear, - createStore, - del, - entries, - get, - keys, - set, - setMany, -} from 'idb-keyval'; - -export type ConfigStore = { - get: (key: string) => Promise; - set: (key: string, value: T) => Promise; - setMany: (values: [string, T][]) => Promise; - keys: () => Promise; - entries: () => Promise<[string, T][]>; - delete: (key: string) => Promise; - clear: () => Promise; -}; - -const initialIndexedDB = (database: string): ConfigStore => { - const store = createStore(`affine:${database}`, 'database'); - return { - get: (key: string) => get(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>(); - - return (scope: string): Readonly> => { - 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; - }; -}; - -let lazyKVConfigure: ReturnType | undefined = undefined; - -export const getKVConfigure = (scope: string) => { - if (!lazyKVConfigure) { - lazyKVConfigure = scopedIndexedDB(); - } - return lazyKVConfigure(scope); -}; diff --git a/packages/data-center/src/types/index.ts b/packages/data-center/src/types/index.ts deleted file mode 100644 index d0c2cfec5d..0000000000 --- a/packages/data-center/src/types/index.ts +++ /dev/null @@ -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; -export type Logger = DebugLogger; - -export type Message = { - code: number; - message: string; - provider: string; -}; diff --git a/packages/data-center/src/utils/index.ts b/packages/data-center/src/utils/index.ts deleted file mode 100644 index f986c535ce..0000000000 --- a/packages/data-center/src/utils/index.ts +++ /dev/null @@ -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 { - 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((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)); - }); - } -}; diff --git a/packages/data-center/src/workspace-unit-collection.spec.ts b/packages/data-center/src/workspace-unit-collection.spec.ts deleted file mode 100644 index 32ab139721..0000000000 --- a/packages/data-center/src/workspace-unit-collection.spec.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/packages/data-center/src/workspace-unit-collection.ts b/packages/data-center/src/workspace-unit-collection.ts deleted file mode 100644 index cf1bc2c022..0000000000 --- a/packages/data-center/src/workspace-unit-collection.ts +++ /dev/null @@ -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(); - - 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(); - - 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, - }; - } -} diff --git a/packages/data-center/src/workspace-unit.ts b/packages/data-center/src/workspace-unit.ts deleted file mode 100644 index bd6ffefe15..0000000000 --- a/packages/data-center/src/workspace-unit.ts +++ /dev/null @@ -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 ->; - -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(); - } -} diff --git a/packages/data-center/tsconfig.json b/packages/data-center/tsconfig.json deleted file mode 100644 index 7513033f03..0000000000 --- a/packages/data-center/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["./src"] -} diff --git a/packages/workspace/package.json b/packages/workspace/package.json index c6d5fef397..f513ca8377 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -5,7 +5,8 @@ "./utils": "./src/utils.ts", "./type": "./src/type.ts", "./affine/*": "./src/affine/*.ts", - "./affine/api": "./src/affine/api/index.ts" + "./affine/api": "./src/affine/api/index.ts", + "./affine/sync": "./src/affine/sync.js" }, "dependencies": { "@affine-test/fixtures": "workspace:^", @@ -18,6 +19,7 @@ "jotai": "^2.0.3", "js-base64": "^3.7.5", "ky": "^0.33.3", + "lib0": "^0.2.73", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.21.4" diff --git a/packages/workspace/src/affine/__tests__/api.spec.ts b/packages/workspace/src/affine/__tests__/api.spec.ts index 10559483f9..56ebf48550 100644 --- a/packages/workspace/src/affine/__tests__/api.spec.ts +++ b/packages/workspace/src/affine/__tests__/api.spec.ts @@ -3,7 +3,7 @@ */ import 'fake-indexeddb/auto'; -import { MessageCode } from '@affine/datacenter'; +import { MessageCode } from '@affine/env/constant'; import userA from '@affine-test/fixtures/userA.json'; import { assertExists } from '@blocksuite/global/utils'; import { Workspace } from '@blocksuite/store'; diff --git a/packages/workspace/src/affine/api/index.ts b/packages/workspace/src/affine/api/index.ts index 05ef7ca9ba..317a317445 100644 --- a/packages/workspace/src/affine/api/index.ts +++ b/packages/workspace/src/affine/api/index.ts @@ -295,13 +295,14 @@ export function createWorkspaceApis(prefixUrl = '/') { throw new RequestError(MessageCode.acceptInvitingFailed, e); }); }, - uploadBlob: async (params: { blob: Blob }): Promise => { + uploadBlob: async (workspaceId: string, blob: Blob): Promise => { const auth = getLoginStorage(); assertExists(auth); return fetch(prefixUrl + 'api/blob', { method: 'PUT', - body: params.blob, + body: blob, headers: { + 'Content-Type': blob.type, Authorization: auth.token, }, }).then(r => r.text()); diff --git a/packages/data-center/src/provider/affine/sync.js b/packages/workspace/src/affine/sync.js similarity index 100% rename from packages/data-center/src/provider/affine/sync.js rename to packages/workspace/src/affine/sync.js diff --git a/tests/libs/utils.ts b/tests/libs/utils.ts index 2deac2690a..a781f8ca37 100644 --- a/tests/libs/utils.ts +++ b/tests/libs/utils.ts @@ -77,7 +77,7 @@ export async function loginUser( ) { await page.evaluate(async token => { // @ts-ignore - globalThis.AFFINE_APIS.auth.setLogin(token); + globalThis.setLogin(token); }, token); } diff --git a/tsconfig.json b/tsconfig.json index 8017eb0649..e5b1af533e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ "./packages/component/src/components/block-suite-editor/index" ], "@affine/templates/*": ["./packages/templates/src/*"], - "@affine/datacenter": ["./packages/datacenter/src"], "@affine/i18n": ["./packages/i18n/src"], "@affine/debug": ["./packages/debug"], "@affine/env": ["./packages/env"], @@ -42,9 +41,6 @@ { "path": "./apps/desktop" }, - { - "path": "./packages/data-center" - }, { "path": "./packages/component" }, diff --git a/vitest.config.ts b/vitest.config.ts index 262b0ada7b..21ac44591c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,10 +26,6 @@ export default defineConfig({ provider: 'istanbul', // or 'c8' reporter: ['lcov'], reportsDirectory: '.coverage/store', - exclude: [ - // data center will be removed in the future - 'packages/data-center', - ], }, }, }); diff --git a/yarn.lock b/yarn.lock index c73b9a5bdd..217b030578 100644 --- a/yarn.lock +++ b/yarn.lock @@ -110,30 +110,6 @@ __metadata: languageName: unknown linkType: soft -"@affine/datacenter@workspace:*, @affine/datacenter@workspace:packages/data-center": - version: 0.0.0-use.local - resolution: "@affine/datacenter@workspace:packages/data-center" - dependencies: - "@affine/debug": "workspace:*" - "@blocksuite/blocks": 0.5.0-20230324040005-14417c2 - "@blocksuite/global": 0.5.0-20230324040005-14417c2 - "@blocksuite/store": 0.5.0-20230324040005-14417c2 - "@tauri-apps/api": ^1.2.0 - encoding: ^0.1.13 - fake-indexeddb: 4.0.1 - firebase: ^9.18.0 - idb-keyval: ^6.2.0 - js-base64: ^3.7.5 - ky: ^0.33.3 - ky-universal: ^0.11.0 - lib0: ^0.2.69 - lit: ^2.6.1 - typescript: ^5.0.2 - y-protocols: ^1.0.5 - yjs: ^13.5.50 - languageName: unknown - linkType: soft - "@affine/debug@workspace:*, @affine/debug@workspace:packages/debug": version: 0.0.0-use.local resolution: "@affine/debug@workspace:packages/debug" @@ -219,7 +195,6 @@ __metadata: resolution: "@affine/web@workspace:apps/web" dependencies: "@affine/component": "workspace:*" - "@affine/datacenter": "workspace:*" "@affine/debug": "workspace:*" "@affine/env": "workspace:*" "@affine/i18n": "workspace:*" @@ -285,6 +260,7 @@ __metadata: jotai: ^2.0.3 js-base64: ^3.7.5 ky: ^0.33.3 + lib0: ^0.2.73 react: ^18.2.0 react-dom: ^18.2.0 zod: ^3.21.4 @@ -7766,15 +7742,6 @@ __metadata: languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: ^5.0.0 - checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 - languageName: node - linkType: hard - "accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11458,13 +11425,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^5.0.0": - version: 5.0.1 - resolution: "event-target-shim@npm:5.0.1" - checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 - languageName: node - linkType: hard - "events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -15007,22 +14967,6 @@ __metadata: languageName: node linkType: hard -"ky-universal@npm:^0.11.0": - version: 0.11.0 - resolution: "ky-universal@npm:0.11.0" - dependencies: - abort-controller: ^3.0.0 - node-fetch: ^3.2.10 - peerDependencies: - ky: ">=0.31.4" - web-streams-polyfill: ">=3.2.1" - peerDependenciesMeta: - web-streams-polyfill: - optional: true - checksum: 42e4c91551a0d17465d6a117de2d1fa4efdf38c4e29dbd70af0c5b7ac0ee13994bceca9af2a0ac21a943d1cd22557ea664abe79f25e096d30a6baca0a0265a12 - languageName: node - linkType: hard - "ky@npm:^0.33.3": version: 0.33.3 resolution: "ky@npm:0.33.3" @@ -15084,7 +15028,7 @@ __metadata: languageName: node linkType: hard -"lib0@npm:^0.2.35, lib0@npm:^0.2.42, lib0@npm:^0.2.68, lib0@npm:^0.2.69, lib0@npm:^0.2.72": +"lib0@npm:^0.2.35, lib0@npm:^0.2.42, lib0@npm:^0.2.68, lib0@npm:^0.2.69, lib0@npm:^0.2.72, lib0@npm:^0.2.73": version: 0.2.73 resolution: "lib0@npm:0.2.73" dependencies: @@ -16343,17 +16287,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^3.2.10": - version: 3.3.1 - resolution: "node-fetch@npm:3.3.1" - dependencies: - data-uri-to-buffer: ^4.0.0 - fetch-blob: ^3.1.4 - formdata-polyfill: ^4.0.10 - checksum: 62145fd3ba4770a76110bc31fdc0054ab2f5442b5ce96e9c4b39fc9e94a3d305560eec76e1165d9259eab866e02a8eecf9301062bb5dfc9f08a4d08b69d223dd - languageName: node - linkType: hard - "node-gyp-build@npm:^4.2.1": version: 4.6.0 resolution: "node-gyp-build@npm:4.6.0"