mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
refactor: move local workspace CRUD (#1778)
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<WorkspaceFlavour.LOCAL> = {
|
||||
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<typeof schema>;
|
||||
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<typeof schema>;
|
||||
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<typeof schema>;
|
||||
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<typeof schema>).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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<WorkspaceFlavour.LOCAL> = {
|
||||
get: async workspaceId => {
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
|
||||
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<typeof schema>;
|
||||
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<typeof schema>;
|
||||
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<typeof schema>).map(id =>
|
||||
CRUD.get(id)
|
||||
)
|
||||
)
|
||||
).filter(item => item !== null) as LocalWorkspace[];
|
||||
},
|
||||
};
|
||||
+8
-5
@@ -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
|
||||
);
|
||||
@@ -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));
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
export const localProviderLogger = new DebugLogger('local-provider');
|
||||
Reference in New Issue
Block a user