mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-27 18:12:54 +03:00
refactor: workspace provider (#2218)
This commit is contained in:
parent
ec39c23fb7
commit
9096ac2960
71
apps/web/src/atoms/__tests__/atom.spec.ts
Normal file
71
apps/web/src/atoms/__tests__/atom.spec.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
import { initPage } from '@affine/env/blocksuite';
|
||||||
|
import {
|
||||||
|
rootCurrentWorkspaceIdAtom,
|
||||||
|
rootWorkspacesMetadataAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
|
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||||
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
|
import {
|
||||||
|
_cleanupBlockSuiteWorkspaceCache,
|
||||||
|
createEmptyBlockSuiteWorkspace,
|
||||||
|
} from '@affine/workspace/utils';
|
||||||
|
import type { ParagraphBlockModel } from '@blocksuite/blocks/models';
|
||||||
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import { createStore } from 'jotai';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { WorkspacePlugins } from '../../plugins';
|
||||||
|
import { rootCurrentWorkspaceAtom } from '../root';
|
||||||
|
|
||||||
|
describe('currentWorkspace atom', () => {
|
||||||
|
test('should be defined', async () => {
|
||||||
|
const store = createStore();
|
||||||
|
let id: string;
|
||||||
|
{
|
||||||
|
const workspace = createEmptyBlockSuiteWorkspace(
|
||||||
|
'test',
|
||||||
|
WorkspaceFlavour.LOCAL
|
||||||
|
);
|
||||||
|
const page = workspace.createPage('page0');
|
||||||
|
initPage(page);
|
||||||
|
const frameId = page.getBlockByFlavour('affine:frame').at(0)
|
||||||
|
?.id as string;
|
||||||
|
id = page.addBlock(
|
||||||
|
'affine:paragraph',
|
||||||
|
{
|
||||||
|
text: new page.Text('test 1'),
|
||||||
|
},
|
||||||
|
frameId
|
||||||
|
);
|
||||||
|
const provider = createIndexedDBDownloadProvider(workspace);
|
||||||
|
provider.sync();
|
||||||
|
await provider.whenReady;
|
||||||
|
const workspaceId = await WorkspacePlugins[
|
||||||
|
WorkspaceFlavour.LOCAL
|
||||||
|
].CRUD.create(workspace);
|
||||||
|
store.set(rootWorkspacesMetadataAtom, [
|
||||||
|
{
|
||||||
|
id: workspaceId,
|
||||||
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
_cleanupBlockSuiteWorkspaceCache();
|
||||||
|
}
|
||||||
|
store.set(
|
||||||
|
rootCurrentWorkspaceIdAtom,
|
||||||
|
store.get(rootWorkspacesMetadataAtom)[0].id
|
||||||
|
);
|
||||||
|
const workspace = await store.get(rootCurrentWorkspaceAtom);
|
||||||
|
expect(workspace).toBeDefined();
|
||||||
|
const page = workspace.blockSuiteWorkspace.getPage('page0') as Page;
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
const paragraphBlock = page.getBlockById(id) as ParagraphBlockModel;
|
||||||
|
expect(paragraphBlock).not.toBeNull();
|
||||||
|
expect(paragraphBlock.text.toString()).toBe('test 1');
|
||||||
|
});
|
||||||
|
});
|
@ -5,6 +5,10 @@ import {
|
|||||||
rootCurrentWorkspaceIdAtom,
|
rootCurrentWorkspaceIdAtom,
|
||||||
rootWorkspacesMetadataAtom,
|
rootWorkspacesMetadataAtom,
|
||||||
} from '@affine/workspace/atom';
|
} from '@affine/workspace/atom';
|
||||||
|
import type {
|
||||||
|
NecessaryProvider,
|
||||||
|
WorkspaceRegistry,
|
||||||
|
} from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
@ -37,18 +41,38 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
|||||||
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
|
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
|
||||||
assertExists(plugin);
|
assertExists(plugin);
|
||||||
const { CRUD } = plugin;
|
const { CRUD } = plugin;
|
||||||
return CRUD.get(workspace.id);
|
return CRUD.get(workspace.id).then(workspace => {
|
||||||
|
if (workspace === null) {
|
||||||
|
console.warn(
|
||||||
|
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return workspace;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
).then(workspaces =>
|
||||||
|
workspaces.filter(
|
||||||
|
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
|
||||||
|
workspace !== null
|
||||||
|
)
|
||||||
);
|
);
|
||||||
logger.info('workspaces', workspaces);
|
const workspaceProviders = workspaces.map(workspace =>
|
||||||
workspaces.forEach(workspace => {
|
workspace.providers.filter(
|
||||||
if (workspace === null) {
|
(provider): provider is NecessaryProvider =>
|
||||||
console.warn(
|
'necessary' in provider && provider.necessary
|
||||||
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
|
)
|
||||||
);
|
);
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
for (const providers of workspaceProviders) {
|
||||||
|
for (const provider of providers) {
|
||||||
|
provider.sync();
|
||||||
|
promises.push(provider.whenReady);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
|
// we will wait for all the necessary providers to be ready
|
||||||
|
await Promise.all(promises);
|
||||||
|
logger.info('workspaces', workspaces);
|
||||||
|
return workspaces;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +101,15 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
|
|||||||
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
|
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const providers = workspace.providers.filter(
|
||||||
|
(provider): provider is NecessaryProvider =>
|
||||||
|
'necessary' in provider && provider.necessary === true
|
||||||
|
);
|
||||||
|
for (const provider of providers) {
|
||||||
|
provider.sync();
|
||||||
|
// we will wait for the necessary providers to be ready
|
||||||
|
await provider.whenReady;
|
||||||
|
}
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import 'fake-indexeddb/auto';
|
|
||||||
|
|
||||||
import { beforeEach, describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { BlockSuiteWorkspace } from '../../shared';
|
|
||||||
import { createAffineProviders, createLocalProviders } from '..';
|
|
||||||
|
|
||||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blocksuite providers', () => {
|
|
||||||
test('should be valid provider', () => {
|
|
||||||
[createLocalProviders, createAffineProviders].forEach(createProviders => {
|
|
||||||
createProviders(blockSuiteWorkspace).forEach(provider => {
|
|
||||||
expect(provider).toBeTypeOf('object');
|
|
||||||
expect(provider).toHaveProperty('flavour');
|
|
||||||
expect(provider).toHaveProperty('connect');
|
|
||||||
expect(provider.connect).toBeTypeOf('function');
|
|
||||||
expect(provider).toHaveProperty('disconnect');
|
|
||||||
expect(provider.disconnect).toBeTypeOf('function');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,13 +1,15 @@
|
|||||||
import { config } from '@affine/env';
|
import { config } from '@affine/env';
|
||||||
import {
|
import {
|
||||||
createIndexedDBProvider,
|
createIndexedDBDownloadProvider,
|
||||||
createLocalProviders,
|
createLocalProviders,
|
||||||
} from '@affine/workspace/providers';
|
} from '@affine/workspace/providers';
|
||||||
import { createBroadCastChannelProvider } from '@affine/workspace/providers';
|
import {
|
||||||
|
createAffineWebSocketProvider,
|
||||||
|
createBroadCastChannelProvider,
|
||||||
|
} from '@affine/workspace/providers';
|
||||||
import type { Provider } from '@affine/workspace/type';
|
import type { Provider } from '@affine/workspace/type';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../shared';
|
import type { BlockSuiteWorkspace } from '../shared';
|
||||||
import { createAffineWebSocketProvider } from './providers';
|
|
||||||
import { createAffineDownloadProvider } from './providers/affine';
|
import { createAffineDownloadProvider } from './providers/affine';
|
||||||
|
|
||||||
export const createAffineProviders = (
|
export const createAffineProviders = (
|
||||||
@ -19,7 +21,7 @@ export const createAffineProviders = (
|
|||||||
createAffineWebSocketProvider(blockSuiteWorkspace),
|
createAffineWebSocketProvider(blockSuiteWorkspace),
|
||||||
config.enableBroadCastChannelProvider &&
|
config.enableBroadCastChannelProvider &&
|
||||||
createBroadCastChannelProvider(blockSuiteWorkspace),
|
createBroadCastChannelProvider(blockSuiteWorkspace),
|
||||||
createIndexedDBProvider(blockSuiteWorkspace),
|
createIndexedDBDownloadProvider(blockSuiteWorkspace),
|
||||||
] as any[]
|
] as any[]
|
||||||
).filter(v => Boolean(v));
|
).filter(v => Boolean(v));
|
||||||
};
|
};
|
||||||
|
@ -12,9 +12,15 @@ export const createAffineDownloadProvider = (
|
|||||||
): AffineDownloadProvider => {
|
): AffineDownloadProvider => {
|
||||||
assertExists(blockSuiteWorkspace.id);
|
assertExists(blockSuiteWorkspace.id);
|
||||||
const id = blockSuiteWorkspace.id;
|
const id = blockSuiteWorkspace.id;
|
||||||
|
let connected = false;
|
||||||
|
const callbacks = new Set<() => void>();
|
||||||
return {
|
return {
|
||||||
flavour: 'affine-download',
|
flavour: 'affine-download',
|
||||||
background: true,
|
background: true,
|
||||||
|
get connected() {
|
||||||
|
return connected;
|
||||||
|
},
|
||||||
|
callbacks,
|
||||||
connect: () => {
|
connect: () => {
|
||||||
providerLogger.info('connect download provider', id);
|
providerLogger.info('connect download provider', id);
|
||||||
if (hashMap.has(id)) {
|
if (hashMap.has(id)) {
|
||||||
@ -23,6 +29,7 @@ export const createAffineDownloadProvider = (
|
|||||||
blockSuiteWorkspace.doc,
|
blockSuiteWorkspace.doc,
|
||||||
new Uint8Array(hashMap.get(id) as ArrayBuffer)
|
new Uint8Array(hashMap.get(id) as ArrayBuffer)
|
||||||
);
|
);
|
||||||
|
connected = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
affineApis
|
affineApis
|
||||||
@ -41,6 +48,7 @@ export const createAffineDownloadProvider = (
|
|||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
providerLogger.info('disconnect download provider', id);
|
providerLogger.info('disconnect download provider', id);
|
||||||
|
connected = false;
|
||||||
},
|
},
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
hashMap.delete(id);
|
hashMap.delete(id);
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { websocketPrefixUrl } from '@affine/env';
|
|
||||||
import { KeckProvider } from '@affine/workspace/affine/keck';
|
|
||||||
import { getLoginStorage } from '@affine/workspace/affine/login';
|
|
||||||
import type { AffineWebSocketProvider } from '@affine/workspace/type';
|
|
||||||
import { assertExists } from '@blocksuite/store';
|
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../../shared';
|
|
||||||
import { providerLogger } 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: () => {
|
|
||||||
webSocketProvider = new KeckProvider(
|
|
||||||
websocketPrefixUrl + '/api/sync/',
|
|
||||||
blockSuiteWorkspace.id,
|
|
||||||
blockSuiteWorkspace.doc,
|
|
||||||
{
|
|
||||||
params: { token: getLoginStorage()?.token ?? '' },
|
|
||||||
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
|
||||||
// we maintain broadcast channel by ourselves
|
|
||||||
// @ts-expect-error
|
|
||||||
disableBc: true,
|
|
||||||
connect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
providerLogger.info('connect', webSocketProvider.url);
|
|
||||||
webSocketProvider.connect();
|
|
||||||
},
|
|
||||||
disconnect: () => {
|
|
||||||
assertExists(webSocketProvider);
|
|
||||||
providerLogger.info('disconnect', webSocketProvider.url);
|
|
||||||
webSocketProvider.destroy();
|
|
||||||
webSocketProvider = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { createAffineWebSocketProvider };
|
|
@ -22,6 +22,7 @@ export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
|
|||||||
}
|
}
|
||||||
const exist = metadata.find(m => m.id === currentWorkspaceId);
|
const exist = metadata.find(m => m.id === currentWorkspaceId);
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
|
console.warn('workspace not exist, redirect to first one');
|
||||||
// clean up
|
// clean up
|
||||||
setCurrentWorkspaceId(null);
|
setCurrentWorkspaceId(null);
|
||||||
setCurrentPageId(null);
|
setCurrentPageId(null);
|
||||||
|
@ -45,8 +45,9 @@ export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
|
|||||||
window.apis?.onWorkspaceChange(targetWorkspace.id);
|
window.apis?.onWorkspaceChange(targetWorkspace.id);
|
||||||
}
|
}
|
||||||
void router.push({
|
void router.push({
|
||||||
pathname: '/workspace/[workspaceId]/all',
|
pathname: router.pathname,
|
||||||
query: {
|
query: {
|
||||||
|
...router.query,
|
||||||
workspaceId: targetWorkspace.id,
|
workspaceId: targetWorkspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -56,8 +57,9 @@ export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
|
|||||||
console.log('set workspace id', workspaceId);
|
console.log('set workspace id', workspaceId);
|
||||||
setCurrentWorkspaceId(targetWorkspace.id);
|
setCurrentWorkspaceId(targetWorkspace.id);
|
||||||
void router.push({
|
void router.push({
|
||||||
pathname: '/workspace/[workspaceId]/all',
|
pathname: router.pathname,
|
||||||
query: {
|
query: {
|
||||||
|
...router.query,
|
||||||
workspaceId: targetWorkspace.id,
|
workspaceId: targetWorkspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
|
import { DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
|
||||||
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
|
import { initPage } from '@affine/env/blocksuite';
|
||||||
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
||||||
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
|
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
|
||||||
import {
|
import {
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
rootStore,
|
rootStore,
|
||||||
rootWorkspacesMetadataAtom,
|
rootWorkspacesMetadataAtom,
|
||||||
} from '@affine/workspace/atom';
|
} from '@affine/workspace/atom';
|
||||||
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
|
import type { BackgroundProvider } from '@affine/workspace/type';
|
||||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||||
@ -17,14 +17,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||||
import {
|
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||||
lazy,
|
|
||||||
Suspense,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
||||||
import {
|
import {
|
||||||
@ -127,7 +120,10 @@ export const AllWorkspaceContext = ({
|
|||||||
// ignore current workspace
|
// ignore current workspace
|
||||||
.filter(workspace => workspace.id !== currentWorkspaceId)
|
.filter(workspace => workspace.id !== currentWorkspaceId)
|
||||||
.flatMap(workspace =>
|
.flatMap(workspace =>
|
||||||
workspace.providers.filter(provider => provider.background)
|
workspace.providers.filter(
|
||||||
|
(provider): provider is BackgroundProvider =>
|
||||||
|
'background' in provider && provider.background
|
||||||
|
)
|
||||||
);
|
);
|
||||||
providers.forEach(provider => {
|
providers.forEach(provider => {
|
||||||
provider.connect();
|
provider.connect();
|
||||||
@ -260,69 +256,48 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { jumpToPage } = useRouterHelper(router);
|
const { jumpToPage } = useRouterHelper(router);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.info('currentWorkspace: ', currentWorkspace);
|
logger.info('currentWorkspace: ', currentWorkspace);
|
||||||
|
globalThis.currentWorkspace = currentWorkspace;
|
||||||
}, [currentWorkspace]);
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
useEffect(() => {
|
//#region init workspace
|
||||||
if (currentWorkspace) {
|
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
|
||||||
globalThis.currentWorkspace = currentWorkspace;
|
// this is a new workspace, so we should redirect to the new page
|
||||||
|
const pageId = nanoid();
|
||||||
|
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
|
||||||
|
assertEquals(page.id, pageId);
|
||||||
|
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
|
||||||
|
init: true,
|
||||||
|
});
|
||||||
|
initPage(page);
|
||||||
|
if (!router.query.pageId) {
|
||||||
|
setCurrentPageId(pageId);
|
||||||
|
void jumpToPage(currentWorkspace.id, pageId);
|
||||||
}
|
}
|
||||||
}, [currentWorkspace]);
|
}
|
||||||
|
|
||||||
|
// fixme: pinboard has been removed,
|
||||||
|
// the related code should be removed in the future.
|
||||||
|
// no matter the workspace is empty, ensure the root pinboard exists
|
||||||
|
// ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentWorkspace) {
|
const backgroundProviders = currentWorkspace.providers.filter(
|
||||||
currentWorkspace.providers.forEach(provider => {
|
(provider): provider is BackgroundProvider => 'background' in provider
|
||||||
provider.connect();
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
currentWorkspace.providers.forEach(provider => {
|
|
||||||
provider.disconnect();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currentWorkspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const localProvider = currentWorkspace.providers.find(
|
|
||||||
provider => provider.flavour === 'local-indexeddb'
|
|
||||||
);
|
);
|
||||||
if (localProvider && localProvider.flavour === 'local-indexeddb') {
|
backgroundProviders.forEach(provider => {
|
||||||
const provider = localProvider as LocalIndexedDBProvider;
|
provider.connect();
|
||||||
const callback = () => {
|
});
|
||||||
setIsLoading(false);
|
return () => {
|
||||||
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
|
backgroundProviders.forEach(provider => {
|
||||||
// this is a new workspace, so we should redirect to the new page
|
provider.disconnect();
|
||||||
const pageId = nanoid();
|
});
|
||||||
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
|
};
|
||||||
assertEquals(page.id, pageId);
|
}, [currentWorkspace]);
|
||||||
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
|
|
||||||
init: true,
|
|
||||||
});
|
|
||||||
initPage(page);
|
|
||||||
if (!router.query.pageId) {
|
|
||||||
setCurrentPageId(pageId);
|
|
||||||
void jumpToPage(currentWorkspace.id, pageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// no matter the workspace is empty, ensure the root pinboard exists
|
|
||||||
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
|
|
||||||
};
|
|
||||||
provider.callbacks.add(callback);
|
|
||||||
return () => {
|
|
||||||
provider.callbacks.delete(callback);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [currentWorkspace, jumpToPage, router, setCurrentPageId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
@ -395,11 +370,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
<MainContainerWrapper>
|
<MainContainerWrapper>
|
||||||
<MainContainer className="main-container">
|
<MainContainer className="main-container">
|
||||||
<Suspense fallback={<PageLoading text={t('Page is Loading')} />}>
|
<Suspense fallback={<PageLoading text={t('Page is Loading')} />}>
|
||||||
{isLoading ? (
|
{children}
|
||||||
<PageLoading text={t('Page is Loading')} />
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<StyledToolWrapper>
|
<StyledToolWrapper>
|
||||||
{/* fixme(himself65): remove this */}
|
{/* fixme(himself65): remove this */}
|
||||||
|
@ -3,12 +3,12 @@ import {
|
|||||||
DEFAULT_HELLO_WORLD_PAGE_ID,
|
DEFAULT_HELLO_WORLD_PAGE_ID,
|
||||||
DEFAULT_WORKSPACE_NAME,
|
DEFAULT_WORKSPACE_NAME,
|
||||||
} from '@affine/env';
|
} from '@affine/env';
|
||||||
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
|
import { initPage } from '@affine/env/blocksuite';
|
||||||
import {
|
import {
|
||||||
CRUD,
|
CRUD,
|
||||||
saveWorkspaceToLocalStorage,
|
saveWorkspaceToLocalStorage,
|
||||||
} from '@affine/workspace/local/crud';
|
} from '@affine/workspace/local/crud';
|
||||||
import { createIndexedDBProvider } from '@affine/workspace/providers';
|
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
|
||||||
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
@ -40,12 +40,11 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
|
|||||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||||
jumpOnce: true,
|
jumpOnce: true,
|
||||||
});
|
});
|
||||||
const provider = createIndexedDBProvider(blockSuiteWorkspace);
|
const provider = createIndexedDBBackgroundProvider(blockSuiteWorkspace);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
provider.callbacks.add(() => {
|
provider.callbacks.add(() => {
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
});
|
});
|
||||||
ensureRootPinboard(blockSuiteWorkspace);
|
|
||||||
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
|
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
|
||||||
logger.debug('create first workspace');
|
logger.debug('create first workspace');
|
||||||
return [blockSuiteWorkspace.id];
|
return [blockSuiteWorkspace.id];
|
||||||
|
@ -17,7 +17,7 @@ export type RootWorkspaceMetadata = {
|
|||||||
/**
|
/**
|
||||||
* root workspaces atom
|
* root workspaces atom
|
||||||
* this atom stores the metadata of all workspaces,
|
* this atom stores the metadata of all workspaces,
|
||||||
* which is `id` and `flavour`, that is enough to load the real workspace data
|
* which is `id` and `flavor`, that is enough to load the real workspace data
|
||||||
*/
|
*/
|
||||||
export const rootWorkspacesMetadataAtom = atomWithSyncStorage<
|
export const rootWorkspacesMetadataAtom = atomWithSyncStorage<
|
||||||
RootWorkspaceMetadata[]
|
RootWorkspaceMetadata[]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { CallbackSet } from '@affine/workspace/utils';
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import type { Awareness } from 'y-protocols/awareness';
|
import type { Awareness } from 'y-protocols/awareness';
|
||||||
@ -23,6 +24,7 @@ export const createBroadCastChannelProvider = (
|
|||||||
const awareness = blockSuiteWorkspace.awarenessStore
|
const awareness = blockSuiteWorkspace.awarenessStore
|
||||||
.awareness as unknown as Awareness;
|
.awareness as unknown as Awareness;
|
||||||
let broadcastChannel: TypedBroadcastChannel | null = null;
|
let broadcastChannel: TypedBroadcastChannel | null = null;
|
||||||
|
const callbacks = new CallbackSet();
|
||||||
const handleBroadcastChannelMessage = (
|
const handleBroadcastChannelMessage = (
|
||||||
event: BroadcastChannelMessageEvent
|
event: BroadcastChannelMessageEvent
|
||||||
) => {
|
) => {
|
||||||
@ -56,6 +58,9 @@ export const createBroadCastChannelProvider = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (callbacks.ready) {
|
||||||
|
callbacks.forEach(cb => cb());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleDocUpdate = (updateV1: Uint8Array, origin: any) => {
|
const handleDocUpdate = (updateV1: Uint8Array, origin: any) => {
|
||||||
if (origin === broadcastChannel) {
|
if (origin === broadcastChannel) {
|
||||||
@ -77,7 +82,11 @@ export const createBroadCastChannelProvider = (
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
flavour: 'broadcast-channel',
|
flavour: 'broadcast-channel',
|
||||||
background: false,
|
background: true,
|
||||||
|
get connected() {
|
||||||
|
return callbacks.ready;
|
||||||
|
},
|
||||||
|
callbacks,
|
||||||
connect: () => {
|
connect: () => {
|
||||||
assertExists(blockSuiteWorkspace.id);
|
assertExists(blockSuiteWorkspace.id);
|
||||||
broadcastChannel = Object.assign(
|
broadcastChannel = Object.assign(
|
||||||
@ -101,6 +110,7 @@ export const createBroadCastChannelProvider = (
|
|||||||
broadcastChannel.postMessage(['awareness:update', awarenessUpdate]);
|
broadcastChannel.postMessage(['awareness:update', awarenessUpdate]);
|
||||||
doc.on('update', handleDocUpdate);
|
doc.on('update', handleDocUpdate);
|
||||||
awareness.on('update', handleAwarenessUpdate);
|
awareness.on('update', handleAwarenessUpdate);
|
||||||
|
callbacks.ready = true;
|
||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
assertExists(broadcastChannel);
|
assertExists(broadcastChannel);
|
||||||
@ -111,6 +121,7 @@ export const createBroadCastChannelProvider = (
|
|||||||
doc.off('update', handleDocUpdate);
|
doc.off('update', handleDocUpdate);
|
||||||
awareness.off('update', handleAwarenessUpdate);
|
awareness.off('update', handleAwarenessUpdate);
|
||||||
broadcastChannel.close();
|
broadcastChannel.close();
|
||||||
|
callbacks.ready = false;
|
||||||
},
|
},
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
assertExists(broadcastChannel);
|
assertExists(broadcastChannel);
|
||||||
|
@ -4,16 +4,22 @@ import {
|
|||||||
getLoginStorage,
|
getLoginStorage,
|
||||||
storageChangeSlot,
|
storageChangeSlot,
|
||||||
} from '@affine/workspace/affine/login';
|
} from '@affine/workspace/affine/login';
|
||||||
import type { Provider, SQLiteProvider } from '@affine/workspace/type';
|
|
||||||
import type {
|
import type {
|
||||||
AffineWebSocketProvider,
|
AffineWebSocketProvider,
|
||||||
LocalIndexedDBProvider,
|
LocalIndexedDBBackgroundProvider,
|
||||||
|
LocalIndexedDBDownloadProvider,
|
||||||
|
Provider,
|
||||||
|
SQLiteProvider,
|
||||||
} from '@affine/workspace/type';
|
} from '@affine/workspace/type';
|
||||||
|
import { CallbackSet } from '@affine/workspace/utils';
|
||||||
import type { BlobManager, Disposable } from '@blocksuite/store';
|
import type { BlobManager, Disposable } from '@blocksuite/store';
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import {
|
||||||
import { assertExists } from '@blocksuite/store';
|
assertExists,
|
||||||
|
Workspace as BlockSuiteWorkspace,
|
||||||
|
} from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
createIndexedDBProvider as create,
|
createIndexedDBProvider as create,
|
||||||
|
downloadBinary,
|
||||||
EarlyDisconnectError,
|
EarlyDisconnectError,
|
||||||
} from '@toeverything/y-indexeddb';
|
} from '@toeverything/y-indexeddb';
|
||||||
|
|
||||||
@ -27,9 +33,15 @@ const createAffineWebSocketProvider = (
|
|||||||
): AffineWebSocketProvider => {
|
): AffineWebSocketProvider => {
|
||||||
let webSocketProvider: KeckProvider | null = null;
|
let webSocketProvider: KeckProvider | null = null;
|
||||||
let dispose: Disposable | undefined = undefined;
|
let dispose: Disposable | undefined = undefined;
|
||||||
|
const callbacks = new CallbackSet();
|
||||||
|
const cb = () => callbacks.forEach(cb => cb());
|
||||||
const apis: AffineWebSocketProvider = {
|
const apis: AffineWebSocketProvider = {
|
||||||
flavour: 'affine-websocket',
|
flavour: 'affine-websocket',
|
||||||
background: false,
|
background: true,
|
||||||
|
get connected() {
|
||||||
|
return callbacks.ready;
|
||||||
|
},
|
||||||
|
callbacks,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
assertExists(webSocketProvider);
|
assertExists(webSocketProvider);
|
||||||
webSocketProvider.destroy();
|
webSocketProvider.destroy();
|
||||||
@ -48,20 +60,19 @@ const createAffineWebSocketProvider = (
|
|||||||
{
|
{
|
||||||
params: { token: getLoginStorage()?.token ?? '' },
|
params: { token: getLoginStorage()?.token ?? '' },
|
||||||
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
||||||
// we maintain broadcast channel by ourselves
|
// we maintain a broadcast channel by ourselves
|
||||||
// @ts-expect-error
|
|
||||||
disableBc: true,
|
|
||||||
connect: false,
|
connect: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.info('connect', webSocketProvider.url);
|
logger.info('connect', webSocketProvider.url);
|
||||||
|
webSocketProvider.on('synced', cb);
|
||||||
webSocketProvider.connect();
|
webSocketProvider.connect();
|
||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
assertExists(webSocketProvider);
|
assertExists(webSocketProvider);
|
||||||
logger.info('disconnect', webSocketProvider.url);
|
logger.info('disconnect', webSocketProvider.url);
|
||||||
webSocketProvider.destroy();
|
webSocketProvider.disconnect();
|
||||||
webSocketProvider = null;
|
webSocketProvider.off('synced', cb);
|
||||||
dispose?.dispose();
|
dispose?.dispose();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -69,52 +80,21 @@ const createAffineWebSocketProvider = (
|
|||||||
return apis;
|
return apis;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CallbackSet extends Set<() => void> {
|
const createIndexedDBBackgroundProvider = (
|
||||||
#ready = false;
|
|
||||||
|
|
||||||
get ready(): boolean {
|
|
||||||
return this.#ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
set ready(v: boolean) {
|
|
||||||
this.#ready = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
add(cb: () => void) {
|
|
||||||
if (this.ready) {
|
|
||||||
cb();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (this.has(cb)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
return super.add(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(cb: () => void) {
|
|
||||||
if (this.has(cb)) {
|
|
||||||
return super.delete(cb);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createIndexedDBProvider = (
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||||
): LocalIndexedDBProvider => {
|
): LocalIndexedDBBackgroundProvider => {
|
||||||
const indexeddbProvider = create(
|
const indexeddbProvider = create(
|
||||||
blockSuiteWorkspace.id,
|
blockSuiteWorkspace.id,
|
||||||
blockSuiteWorkspace.doc
|
blockSuiteWorkspace.doc
|
||||||
);
|
);
|
||||||
const callbacks = new CallbackSet();
|
const callbacks = new CallbackSet();
|
||||||
return {
|
return {
|
||||||
flavour: 'local-indexeddb',
|
flavour: 'local-indexeddb-background',
|
||||||
// fixme: remove callbacks
|
|
||||||
callbacks,
|
|
||||||
// fixme: remove whenSynced
|
|
||||||
whenSynced: indexeddbProvider.whenSynced,
|
|
||||||
// fixme: remove background long polling
|
|
||||||
background: true,
|
background: true,
|
||||||
|
get connected() {
|
||||||
|
return callbacks.ready;
|
||||||
|
},
|
||||||
|
callbacks,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
// todo: cleanup data
|
// todo: cleanup data
|
||||||
},
|
},
|
||||||
@ -127,6 +107,7 @@ const createIndexedDBProvider = (
|
|||||||
callbacks.forEach(cb => cb());
|
callbacks.forEach(cb => cb());
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
callbacks.ready = false;
|
||||||
if (error instanceof EarlyDisconnectError) {
|
if (error instanceof EarlyDisconnectError) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@ -143,6 +124,40 @@ const createIndexedDBProvider = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createIndexedDBDownloadProvider = (
|
||||||
|
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||||
|
): LocalIndexedDBDownloadProvider => {
|
||||||
|
let _resolve: () => void;
|
||||||
|
let _reject: (error: unknown) => void;
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
_resolve = resolve;
|
||||||
|
_reject = reject;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
flavour: 'local-indexeddb',
|
||||||
|
necessary: true,
|
||||||
|
get whenReady() {
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
cleanup: () => {
|
||||||
|
// todo: cleanup data
|
||||||
|
},
|
||||||
|
sync: () => {
|
||||||
|
logger.info('connect indexeddb provider', blockSuiteWorkspace.id);
|
||||||
|
downloadBinary(blockSuiteWorkspace.id)
|
||||||
|
.then(binary => {
|
||||||
|
if (binary !== false) {
|
||||||
|
Y.applyUpdate(blockSuiteWorkspace.doc, binary);
|
||||||
|
}
|
||||||
|
_resolve();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
_reject(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const createSQLiteProvider = (
|
const createSQLiteProvider = (
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||||
): SQLiteProvider => {
|
): SQLiteProvider => {
|
||||||
@ -166,18 +181,20 @@ const createSQLiteProvider = (
|
|||||||
const keysToPersist = allKeys.filter(k => !persistedKeys.includes(k));
|
const keysToPersist = allKeys.filter(k => !persistedKeys.includes(k));
|
||||||
|
|
||||||
logger.info('persisting blobs', keysToPersist, 'to sqlite');
|
logger.info('persisting blobs', keysToPersist, 'to sqlite');
|
||||||
keysToPersist.forEach(async k => {
|
return Promise.all(
|
||||||
const blob = await bs.get(k);
|
keysToPersist.map(async k => {
|
||||||
if (!blob) {
|
const blob = await bs.get(k);
|
||||||
logger.warn('blob not found for', k);
|
if (!blob) {
|
||||||
return;
|
logger.warn('blob not found for', k);
|
||||||
}
|
return;
|
||||||
window.apis.db.addBlob(
|
}
|
||||||
blockSuiteWorkspace.id,
|
return window.apis.db.addBlob(
|
||||||
k,
|
blockSuiteWorkspace.id,
|
||||||
new Uint8Array(await blob.arrayBuffer())
|
k,
|
||||||
);
|
new Uint8Array(await blob.arrayBuffer())
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncUpdates() {
|
async function syncUpdates() {
|
||||||
@ -202,16 +219,23 @@ const createSQLiteProvider = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let unsubscribe = () => {};
|
let unsubscribe = () => {};
|
||||||
|
let connected = false;
|
||||||
|
const callbacks = new CallbackSet();
|
||||||
|
|
||||||
const provider = {
|
return {
|
||||||
flavour: 'sqlite',
|
flavour: 'sqlite',
|
||||||
background: true,
|
background: true,
|
||||||
|
callbacks,
|
||||||
|
get connected(): boolean {
|
||||||
|
return connected;
|
||||||
|
},
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
},
|
},
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
logger.info('connecting sqlite provider', blockSuiteWorkspace.id);
|
logger.info('connecting sqlite provider', blockSuiteWorkspace.id);
|
||||||
await syncUpdates();
|
await syncUpdates();
|
||||||
|
connected = true;
|
||||||
|
|
||||||
blockSuiteWorkspace.doc.on('update', handleUpdate);
|
blockSuiteWorkspace.doc.on('update', handleUpdate);
|
||||||
|
|
||||||
@ -223,6 +247,7 @@ const createSQLiteProvider = (
|
|||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error ignore the type
|
// @ts-expect-error ignore the type
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
syncUpdates();
|
syncUpdates();
|
||||||
@ -237,16 +262,16 @@ const createSQLiteProvider = (
|
|||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
blockSuiteWorkspace.doc.off('update', handleUpdate);
|
blockSuiteWorkspace.doc.off('update', handleUpdate);
|
||||||
|
connected = false;
|
||||||
},
|
},
|
||||||
} satisfies SQLiteProvider;
|
};
|
||||||
|
|
||||||
return provider;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createAffineWebSocketProvider,
|
createAffineWebSocketProvider,
|
||||||
createBroadCastChannelProvider,
|
createBroadCastChannelProvider,
|
||||||
createIndexedDBProvider,
|
createIndexedDBBackgroundProvider,
|
||||||
|
createIndexedDBDownloadProvider,
|
||||||
createSQLiteProvider,
|
createSQLiteProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -257,7 +282,8 @@ export const createLocalProviders = (
|
|||||||
[
|
[
|
||||||
config.enableBroadCastChannelProvider &&
|
config.enableBroadCastChannelProvider &&
|
||||||
createBroadCastChannelProvider(blockSuiteWorkspace),
|
createBroadCastChannelProvider(blockSuiteWorkspace),
|
||||||
createIndexedDBProvider(blockSuiteWorkspace),
|
createIndexedDBBackgroundProvider(blockSuiteWorkspace),
|
||||||
|
createIndexedDBDownloadProvider(blockSuiteWorkspace),
|
||||||
environment.isDesktop && createSQLiteProvider(blockSuiteWorkspace),
|
environment.isDesktop && createSQLiteProvider(blockSuiteWorkspace),
|
||||||
] as any[]
|
] as any[]
|
||||||
).filter(v => Boolean(v));
|
).filter(v => Boolean(v));
|
||||||
|
@ -9,44 +9,76 @@ export type JotaiStore = ReturnType<typeof createStore>;
|
|||||||
|
|
||||||
export type BaseProvider = {
|
export type BaseProvider = {
|
||||||
flavour: string;
|
flavour: string;
|
||||||
// if this is true, we will connect the provider on the background
|
|
||||||
background: boolean;
|
|
||||||
connect: () => void;
|
|
||||||
disconnect: () => void;
|
|
||||||
// cleanup data when workspace is removed
|
// cleanup data when workspace is removed
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* If a provider is marked as a background provider,
|
||||||
|
* we will connect it in the `useEffect` in React.js.
|
||||||
|
*
|
||||||
|
* This means that the data might be stale when you use it.
|
||||||
|
*/
|
||||||
export interface BackgroundProvider extends BaseProvider {
|
export interface BackgroundProvider extends BaseProvider {
|
||||||
|
// if this is true,
|
||||||
|
// we will connect the provider on the background
|
||||||
background: true;
|
background: true;
|
||||||
|
get connected(): boolean;
|
||||||
|
connect(): void;
|
||||||
|
disconnect(): void;
|
||||||
callbacks: Set<() => void>;
|
callbacks: Set<() => void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffineDownloadProvider extends BaseProvider {
|
/**
|
||||||
|
* @description
|
||||||
|
* If a provider is marked as a necessary provider,
|
||||||
|
* we will connect it once you read the workspace.
|
||||||
|
*
|
||||||
|
* This means that the data will be fresh when you use it.
|
||||||
|
*
|
||||||
|
* Currently, there is only on necessary provider: `local-indexeddb`.
|
||||||
|
*/
|
||||||
|
export interface NecessaryProvider extends Omit<BaseProvider, 'disconnect'> {
|
||||||
|
// if this is true,
|
||||||
|
// we will ensure that the provider is connected before you can use it
|
||||||
|
necessary: true;
|
||||||
|
sync(): void;
|
||||||
|
get whenReady(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffineDownloadProvider extends BackgroundProvider {
|
||||||
flavour: 'affine-download';
|
flavour: 'affine-download';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BroadCastChannelProvider extends BaseProvider {
|
/**
|
||||||
|
* Download the first binary from local indexeddb
|
||||||
|
*/
|
||||||
|
export interface BroadCastChannelProvider extends BackgroundProvider {
|
||||||
flavour: 'broadcast-channel';
|
flavour: 'broadcast-channel';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalIndexedDBProvider extends BackgroundProvider {
|
/**
|
||||||
flavour: 'local-indexeddb';
|
* Long polling provider with local indexeddb
|
||||||
whenSynced: Promise<void>;
|
*/
|
||||||
|
export interface LocalIndexedDBBackgroundProvider extends BackgroundProvider {
|
||||||
|
flavour: 'local-indexeddb-background';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SQLiteProvider extends BaseProvider {
|
export interface SQLiteProvider extends BackgroundProvider {
|
||||||
flavour: 'sqlite';
|
flavour: 'sqlite';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffineWebSocketProvider extends BaseProvider {
|
export interface LocalIndexedDBDownloadProvider extends NecessaryProvider {
|
||||||
|
flavour: 'local-indexeddb';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AffineWebSocketProvider extends BackgroundProvider {
|
||||||
flavour: 'affine-websocket';
|
flavour: 'affine-websocket';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Provider =
|
export type Provider = BackgroundProvider | NecessaryProvider;
|
||||||
| LocalIndexedDBProvider
|
|
||||||
| AffineWebSocketProvider
|
|
||||||
| BroadCastChannelProvider;
|
|
||||||
|
|
||||||
export interface AffineWorkspace extends RemoteWorkspace {
|
export interface AffineWorkspace extends RemoteWorkspace {
|
||||||
flavour: WorkspaceFlavour.AFFINE;
|
flavour: WorkspaceFlavour.AFFINE;
|
||||||
|
@ -16,6 +16,11 @@ export function cleanupWorkspace(flavour: WorkspaceFlavour) {
|
|||||||
|
|
||||||
const hashMap = new Map<string, Workspace>();
|
const hashMap = new Map<string, Workspace>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal test only
|
||||||
|
*/
|
||||||
|
export const _cleanupBlockSuiteWorkspaceCache = () => hashMap.clear();
|
||||||
|
|
||||||
export function createEmptyBlockSuiteWorkspace(
|
export function createEmptyBlockSuiteWorkspace(
|
||||||
id: string,
|
id: string,
|
||||||
flavour: WorkspaceFlavour.AFFINE,
|
flavour: WorkspaceFlavour.AFFINE,
|
||||||
@ -83,3 +88,33 @@ export function createEmptyBlockSuiteWorkspace(
|
|||||||
hashMap.set(cacheKey, workspace);
|
hashMap.set(cacheKey, workspace);
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CallbackSet extends Set<() => void> {
|
||||||
|
#ready = false;
|
||||||
|
|
||||||
|
get ready(): boolean {
|
||||||
|
return this.#ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
set ready(v: boolean) {
|
||||||
|
this.#ready = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(cb: () => void) {
|
||||||
|
if (this.ready) {
|
||||||
|
cb();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (this.has(cb)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return super.add(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(cb: () => void) {
|
||||||
|
if (this.has(cb)) {
|
||||||
|
return super.delete(cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
# @toeverything/y-indexeddb
|
# @toeverything/y-indexeddb
|
||||||
|
|
||||||
> This package haven't been published yet.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createIndexedDBProvider } from '@toeverything/y-indexeddb';
|
import { createIndexedDBProvider, downloadBinary } from '@toeverything/y-indexeddb';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc();
|
||||||
|
|
||||||
|
// sync yDoc with indexedDB
|
||||||
const provider = createIndexedDBProvider('docName', yDoc);
|
const provider = createIndexedDBProvider('docName', yDoc);
|
||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced.then(() => {
|
await provider.whenSynced.then(() => {
|
||||||
console.log('synced');
|
console.log('synced');
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dowload binary data from indexedDB for once
|
||||||
|
downloadBinary('docName').then(blob => {
|
||||||
|
if (blob !== false) {
|
||||||
|
Y.applyUpdate(yDoc, blob);
|
||||||
|
}
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
@ -340,7 +340,10 @@ describe('utils', () => {
|
|||||||
provider.connect();
|
provider.connect();
|
||||||
await provider.whenSynced;
|
await provider.whenSynced;
|
||||||
provider.disconnect();
|
provider.disconnect();
|
||||||
const update = await downloadBinary(workspace.id, rootDBName);
|
const update = (await downloadBinary(
|
||||||
|
workspace.id,
|
||||||
|
rootDBName
|
||||||
|
)) as Uint8Array;
|
||||||
expect(update).toBeInstanceOf(Uint8Array);
|
expect(update).toBeInstanceOf(Uint8Array);
|
||||||
const newWorkspace = new Workspace({
|
const newWorkspace = new Workspace({
|
||||||
id,
|
id,
|
||||||
|
@ -129,7 +129,7 @@ export async function tryMigrate(
|
|||||||
export async function downloadBinary(
|
export async function downloadBinary(
|
||||||
id: string,
|
id: string,
|
||||||
dbName = DEFAULT_DB_NAME
|
dbName = DEFAULT_DB_NAME
|
||||||
): Promise<UpdateMessage['update']> {
|
): Promise<UpdateMessage['update'] | false> {
|
||||||
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
const dbPromise = openDB<BlockSuiteBinaryDB>(dbName, dbVersion, {
|
||||||
upgrade: upgradeDB,
|
upgrade: upgradeDB,
|
||||||
});
|
});
|
||||||
@ -137,7 +137,7 @@ export async function downloadBinary(
|
|||||||
const t = db.transaction('workspace', 'readonly').objectStore('workspace');
|
const t = db.transaction('workspace', 'readonly').objectStore('workspace');
|
||||||
const doc = await t.get(id);
|
const doc = await t.get(id);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return new Uint8Array(0);
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return mergeUpdates(doc.updates.map(({ update }) => update));
|
return mergeUpdates(doc.updates.map(({ update }) => update));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user