diff --git a/apps/web/src/blocksuite/index.ts b/apps/web/src/blocksuite/index.ts index cc33651cf2..a2d296c32f 100644 --- a/apps/web/src/blocksuite/index.ts +++ b/apps/web/src/blocksuite/index.ts @@ -1,12 +1,13 @@ import { config } from '@affine/env'; +import { + createIndexedDBProvider, + createLocalProviders, +} from '@affine/workspace/providers'; +import { createBroadCastChannelProvider } from '@affine/workspace/providers'; import type { Provider } from '@affine/workspace/type'; import type { BlockSuiteWorkspace } from '../shared'; -import { - createAffineWebSocketProvider, - createBroadCastChannelProvider, - createIndexedDBProvider, -} from './providers'; +import { createAffineWebSocketProvider } from './providers'; import { createAffineDownloadProvider } from './providers/affine'; export const createAffineProviders = ( @@ -24,15 +25,4 @@ export const createAffineProviders = ( ).filter(v => Boolean(v)); }; -export const createLocalProviders = ( - blockSuiteWorkspace: BlockSuiteWorkspace -): Provider[] => { - return ( - [ - config.enableBroadCastChannelProvider && - createBroadCastChannelProvider(blockSuiteWorkspace), - config.enableIndexedDBProvider && - createIndexedDBProvider(blockSuiteWorkspace), - ] as any[] - ).filter(v => Boolean(v)); -}; +export { createLocalProviders }; diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts index 30fafed8c0..75964a8c49 100644 --- a/apps/web/src/blocksuite/providers/index.ts +++ b/apps/web/src/blocksuite/providers/index.ts @@ -1,15 +1,10 @@ import { KeckProvider } from '@affine/workspace/affine/keck'; import { getLoginStorage } from '@affine/workspace/affine/login'; -import type { - AffineWebSocketProvider, - LocalIndexedDBProvider, -} from '@affine/workspace/type'; +import type { AffineWebSocketProvider } from '@affine/workspace/type'; import { assertExists } from '@blocksuite/store'; -import { IndexeddbPersistence } from 'y-indexeddb'; import type { BlockSuiteWorkspace } from '../../shared'; import { providerLogger } from '../logger'; -import { createBroadCastChannelProvider } from './broad-cast-channel'; const createAffineWebSocketProvider = ( blockSuiteWorkspace: BlockSuiteWorkspace @@ -52,46 +47,4 @@ const createAffineWebSocketProvider = ( }; }; -const createIndexedDBProvider = ( - blockSuiteWorkspace: BlockSuiteWorkspace -): LocalIndexedDBProvider => { - let indexeddbProvider: IndexeddbPersistence | null = null; - const callbacks = new Set<() => void>(); - return { - flavour: 'local-indexeddb', - callbacks, - // fixme: remove background long polling - background: true, - cleanup: () => { - assertExists(indexeddbProvider); - indexeddbProvider.clearData(); - callbacks.clear(); - indexeddbProvider = null; - }, - connect: () => { - providerLogger.info('connect indexeddb provider', blockSuiteWorkspace.id); - indexeddbProvider = new IndexeddbPersistence( - blockSuiteWorkspace.id, - blockSuiteWorkspace.doc - ); - indexeddbProvider.whenSynced.then(() => { - callbacks.forEach(cb => cb()); - }); - }, - disconnect: () => { - assertExists(indexeddbProvider); - providerLogger.info( - 'disconnect indexeddb provider', - blockSuiteWorkspace.id - ); - indexeddbProvider.destroy(); - indexeddbProvider = null; - }, - }; -}; - -export { - createAffineWebSocketProvider, - createBroadCastChannelProvider, - createIndexedDBProvider, -}; +export { createAffineWebSocketProvider }; diff --git a/apps/web/src/pages/_debug/broadcast.dev.tsx b/apps/web/src/pages/_debug/broadcast.dev.tsx index 9356c3a61e..ee29429ba6 100644 --- a/apps/web/src/pages/_debug/broadcast.dev.tsx +++ b/apps/web/src/pages/_debug/broadcast.dev.tsx @@ -1,5 +1,6 @@ import { Button } from '@affine/component'; import { DebugLogger } from '@affine/debug'; +import { createBroadCastChannelProvider } from '@affine/workspace/providers'; import type { BroadCastChannelProvider } from '@affine/workspace/type'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { nanoid } from '@blocksuite/store'; @@ -7,7 +8,6 @@ import { Typography } from '@mui/material'; import type React from 'react'; import { useEffect, useMemo, useState } from 'react'; -import { createBroadCastChannelProvider } from '../../blocksuite/providers'; import PageList from '../../components/blocksuite/block-suite-page-list/page-list'; import { StyledPage, StyledWrapper } from '../../layouts/styles'; import { toast } from '../../utils'; diff --git a/apps/web/src/plugins/local/index.tsx b/apps/web/src/plugins/local/index.tsx index 4788c7ff3e..e567876d7d 100644 --- a/apps/web/src/plugins/local/index.tsx +++ b/apps/web/src/plugins/local/index.tsx @@ -1,97 +1,18 @@ -import type { LocalWorkspace } from '@affine/workspace/type'; +import { CRUD } from '@affine/workspace/local/crud'; import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type'; -import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; -import { nanoid } from '@blocksuite/store'; -import { createJSONStorage } from 'jotai/utils'; import React from 'react'; -import { IndexeddbPersistence } from 'y-indexeddb'; -import { z } from 'zod'; -import { createLocalProviders } from '../../blocksuite'; import { PageNotFoundError } from '../../components/affine/affine-error-eoundary'; import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { PageDetailEditor } from '../../components/page-detail-editor'; -import { BlockSuiteWorkspace } from '../../shared'; import { initPage } from '../../utils'; import type { WorkspacePlugin } from '..'; -const getStorage = () => createJSONStorage(() => localStorage); - -export const kStoreKey = 'affine-local-workspace'; -const schema = z.array(z.string()); - export const LocalPlugin: WorkspacePlugin = { flavour: WorkspaceFlavour.LOCAL, loadPriority: LoadPriority.LOW, - CRUD: { - get: async workspaceId => { - const storage = getStorage(); - !Array.isArray(storage.getItem(kStoreKey)) && - storage.setItem(kStoreKey, []); - const data = storage.getItem(kStoreKey) as z.infer; - const id = data.find(id => id === workspaceId); - if (!id) { - return null; - } - const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( - id, - (_: string) => undefined - ); - const workspace: LocalWorkspace = { - id, - flavour: WorkspaceFlavour.LOCAL, - blockSuiteWorkspace: blockSuiteWorkspace, - providers: [...createLocalProviders(blockSuiteWorkspace)], - }; - return workspace; - }, - create: async ({ doc }) => { - const storage = getStorage(); - !Array.isArray(storage.getItem(kStoreKey)) && - storage.setItem(kStoreKey, []); - const data = storage.getItem(kStoreKey) as z.infer; - const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc); - const id = nanoid(); - const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( - id, - (_: string) => undefined - ); - BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary); - const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc); - await persistence.whenSynced.then(() => { - persistence.destroy(); - }); - storage.setItem(kStoreKey, [...data, id]); - console.log('create', id, storage.getItem(kStoreKey)); - return id; - }, - delete: async workspace => { - const storage = getStorage(); - !Array.isArray(storage.getItem(kStoreKey)) && - storage.setItem(kStoreKey, []); - const data = storage.getItem(kStoreKey) as z.infer; - const idx = data.findIndex(id => id === workspace.id); - if (idx === -1) { - throw new Error('workspace not found'); - } - data.splice(idx, 1); - storage.setItem(kStoreKey, [...data]); - }, - list: async () => { - const storage = getStorage(); - !Array.isArray(storage.getItem(kStoreKey)) && - storage.setItem(kStoreKey, []); - const data = ( - await Promise.all( - (storage.getItem(kStoreKey) as z.infer).map(id => - LocalPlugin.CRUD.get(id) - ) - ) - ).filter(item => item !== null) as LocalWorkspace[]; - return data; - }, - }, + CRUD, UI: { PageDetail: ({ currentWorkspace, currentPageId }) => { const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId); diff --git a/packages/workspace/package.json b/packages/workspace/package.json index ec5ea5a2a8..30f65ceebb 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -5,6 +5,8 @@ "./atom": "./src/atom.ts", "./utils": "./src/utils.ts", "./type": "./src/type.ts", + "./local/crud": "./src/local/crud.ts", + "./providers": "./src/providers/index.ts", "./affine/*": "./src/affine/*.ts", "./affine/api": "./src/affine/api/index.ts", "./affine/sync": "./src/affine/sync.js", @@ -24,6 +26,7 @@ "lib0": "^0.2.73", "react": "^18.2.0", "react-dom": "^18.2.0", + "y-indexeddb": "^9.0.10", "y-protocols": "^1.0.5", "yjs": "^13.5.51", "zod": "^3.21.4" diff --git a/packages/workspace/src/local/crud.ts b/packages/workspace/src/local/crud.ts new file mode 100644 index 0000000000..eeab0597fb --- /dev/null +++ b/packages/workspace/src/local/crud.ts @@ -0,0 +1,82 @@ +import { nanoid, Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; +import { createJSONStorage } from 'jotai/utils'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import { z } from 'zod'; + +import { createLocalProviders } from '../providers'; +import type { LocalWorkspace, WorkspaceCRUD } from '../type'; +import { WorkspaceFlavour } from '../type'; +import { createEmptyBlockSuiteWorkspace } from '../utils'; + +const getStorage = () => createJSONStorage(() => localStorage); + +const kStoreKey = 'affine-local-workspace'; +const schema = z.array(z.string()); + +export const CRUD: WorkspaceCRUD = { + get: async workspaceId => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + const data = storage.getItem(kStoreKey) as z.infer; + const id = data.find(id => id === workspaceId); + if (!id) { + return null; + } + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + id, + (_: string) => undefined + ); + const workspace: LocalWorkspace = { + id, + flavour: WorkspaceFlavour.LOCAL, + blockSuiteWorkspace: blockSuiteWorkspace, + providers: [...createLocalProviders(blockSuiteWorkspace)], + }; + return workspace; + }, + create: async ({ doc }) => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + const data = storage.getItem(kStoreKey) as z.infer; + const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc); + const id = nanoid(); + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( + id, + (_: string) => undefined + ); + BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary); + const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc); + await persistence.whenSynced.then(() => { + persistence.destroy(); + }); + storage.setItem(kStoreKey, [...data, id]); + console.log('create', id, storage.getItem(kStoreKey)); + return id; + }, + delete: async workspace => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + const data = storage.getItem(kStoreKey) as z.infer; + const idx = data.findIndex(id => id === workspace.id); + if (idx === -1) { + throw new Error('workspace not found'); + } + data.splice(idx, 1); + storage.setItem(kStoreKey, [...data]); + }, + list: async () => { + const storage = getStorage(); + !Array.isArray(storage.getItem(kStoreKey)) && + storage.setItem(kStoreKey, []); + return ( + await Promise.all( + (storage.getItem(kStoreKey) as z.infer).map(id => + CRUD.get(id) + ) + ) + ).filter(item => item !== null) as LocalWorkspace[]; + }, +}; diff --git a/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts b/packages/workspace/src/providers/broad-cast-channel/index.ts similarity index 92% rename from apps/web/src/blocksuite/providers/broad-cast-channel/index.ts rename to packages/workspace/src/providers/broad-cast-channel/index.ts index e850093595..69d31c0c24 100644 --- a/apps/web/src/blocksuite/providers/broad-cast-channel/index.ts +++ b/packages/workspace/src/providers/broad-cast-channel/index.ts @@ -1,4 +1,4 @@ -import type { BroadCastChannelProvider } from '@affine/workspace/type'; +import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store'; import type { Awareness } from 'y-protocols/awareness'; import { @@ -6,8 +6,8 @@ import { encodeAwarenessUpdate, } from 'y-protocols/awareness'; -import { BlockSuiteWorkspace } from '../../../shared'; -import { providerLogger } from '../../logger'; +import type { BroadCastChannelProvider } from '../../type'; +import { localProviderLogger } from '../logger'; import type { AwarenessChanges, BroadcastChannelMessageEvent, @@ -86,7 +86,10 @@ export const createBroadCastChannelProvider = ( onmessage: handleBroadcastChannelMessage, } ); - providerLogger.info('connect broadcast channel', blockSuiteWorkspace.id); + localProviderLogger.info( + 'connect broadcast channel', + blockSuiteWorkspace.id + ); const docDiff = Y.encodeStateVector(doc); broadcastChannel.postMessage(['doc:diff', docDiff, awareness.clientID]); const docUpdateV2 = Y.encodeStateAsUpdate(doc); @@ -101,7 +104,7 @@ export const createBroadCastChannelProvider = ( }, disconnect: () => { assertExists(broadcastChannel); - providerLogger.info( + localProviderLogger.info( 'disconnect broadcast channel', blockSuiteWorkspace.id ); diff --git a/apps/web/src/blocksuite/providers/broad-cast-channel/type.ts b/packages/workspace/src/providers/broad-cast-channel/type.ts similarity index 100% rename from apps/web/src/blocksuite/providers/broad-cast-channel/type.ts rename to packages/workspace/src/providers/broad-cast-channel/type.ts diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts new file mode 100644 index 0000000000..69e3d37adb --- /dev/null +++ b/packages/workspace/src/providers/index.ts @@ -0,0 +1,115 @@ +import { config } from '@affine/env'; +import { KeckProvider } from '@affine/workspace/affine/keck'; +import { getLoginStorage } from '@affine/workspace/affine/login'; +import type { Provider } from '@affine/workspace/type'; +import type { + AffineWebSocketProvider, + LocalIndexedDBProvider, +} from '@affine/workspace/type'; +import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; +import { assertExists } from '@blocksuite/store'; +import { IndexeddbPersistence } from 'y-indexeddb'; + +import { createBroadCastChannelProvider } from './broad-cast-channel'; +import { localProviderLogger } from './logger'; + +const createAffineWebSocketProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): AffineWebSocketProvider => { + let webSocketProvider: KeckProvider | null = null; + return { + flavour: 'affine-websocket', + background: false, + cleanup: () => { + assertExists(webSocketProvider); + webSocketProvider.destroy(); + webSocketProvider = null; + }, + connect: () => { + const wsUrl = `${ + window.location.protocol === 'https:' ? 'wss' : 'ws' + }://${window.location.host}/api/sync/`; + webSocketProvider = new KeckProvider( + wsUrl, + blockSuiteWorkspace.id, + blockSuiteWorkspace.doc, + { + params: { token: getLoginStorage()?.token ?? '' }, + // @ts-expect-error ignore the type + awareness: blockSuiteWorkspace.awarenessStore.awareness, + // we maintain broadcast channel by ourselves + disableBc: true, + connect: false, + } + ); + localProviderLogger.info('connect', webSocketProvider.url); + webSocketProvider.connect(); + }, + disconnect: () => { + assertExists(webSocketProvider); + localProviderLogger.info('disconnect', webSocketProvider.url); + webSocketProvider.destroy(); + webSocketProvider = null; + }, + }; +}; + +const createIndexedDBProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): LocalIndexedDBProvider => { + let indexeddbProvider: IndexeddbPersistence | null = null; + const callbacks = new Set<() => void>(); + return { + flavour: 'local-indexeddb', + callbacks, + // fixme: remove background long polling + background: true, + cleanup: () => { + assertExists(indexeddbProvider); + indexeddbProvider.clearData(); + callbacks.clear(); + indexeddbProvider = null; + }, + connect: () => { + localProviderLogger.info( + 'connect indexeddb provider', + blockSuiteWorkspace.id + ); + indexeddbProvider = new IndexeddbPersistence( + blockSuiteWorkspace.id, + blockSuiteWorkspace.doc + ); + indexeddbProvider.whenSynced.then(() => { + callbacks.forEach(cb => cb()); + }); + }, + disconnect: () => { + assertExists(indexeddbProvider); + localProviderLogger.info( + 'disconnect indexeddb provider', + blockSuiteWorkspace.id + ); + indexeddbProvider.destroy(); + indexeddbProvider = null; + }, + }; +}; + +export { + createAffineWebSocketProvider, + createBroadCastChannelProvider, + createIndexedDBProvider, +}; + +export const createLocalProviders = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): Provider[] => { + return ( + [ + config.enableBroadCastChannelProvider && + createBroadCastChannelProvider(blockSuiteWorkspace), + config.enableIndexedDBProvider && + createIndexedDBProvider(blockSuiteWorkspace), + ] as any[] + ).filter(v => Boolean(v)); +}; diff --git a/packages/workspace/src/providers/logger.ts b/packages/workspace/src/providers/logger.ts new file mode 100644 index 0000000000..b9d2b1dd68 --- /dev/null +++ b/packages/workspace/src/providers/logger.ts @@ -0,0 +1,3 @@ +import { DebugLogger } from '@affine/debug'; + +export const localProviderLogger = new DebugLogger('local-provider'); diff --git a/yarn.lock b/yarn.lock index d62d67a7eb..bd51230c57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -254,6 +254,7 @@ __metadata: react: ^18.2.0 react-dom: ^18.2.0 ws: ^8.13.0 + y-indexeddb: ^9.0.10 y-protocols: ^1.0.5 yjs: ^13.5.51 zod: ^3.21.4