feat: init affine blob storage (#2045)

This commit is contained in:
Himself65
2023-04-20 03:23:41 -05:00
committed by GitHub
parent c08c587efb
commit 63f7b2556e
25 changed files with 828 additions and 448 deletions

View File

@@ -17,11 +17,11 @@
"@affine/jotai": "workspace:*", "@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*", "@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*", "@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/blocks": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/editor": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/editor": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/global": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/global": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/icons": "^2.1.10", "@blocksuite/icons": "^2.1.10",
"@blocksuite/store": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/store": "0.0.0-20230420070759-dbe39fdf-nightly",
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2", "@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7", "@emotion/cache": "^11.10.7",

View File

@@ -1,4 +1,3 @@
import { getLoginStorage } from '@affine/workspace/affine/login';
import type { AffinePublicWorkspace } from '@affine/workspace/type'; import type { AffinePublicWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -14,10 +13,9 @@ function createPublicWorkspace(
): AffinePublicWorkspace { ): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId, workspaceId,
(k: string) => WorkspaceFlavour.AFFINE,
// fixme: token could be expired
({ api: `api/workspace`, token: getLoginStorage()?.token }[k]),
{ {
workspaceApis: affineApis,
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''), cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
} }
); );

View File

@@ -1,3 +1,4 @@
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
@@ -9,7 +10,7 @@ import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'test', 'test',
_ => undefined, WorkspaceFlavour.LOCAL,
{ {
idGenerator: Generator.AutoIncrement, idGenerator: Generator.AutoIncrement,
} }

View File

@@ -1,41 +1,50 @@
import type { BlobStorage } from '@blocksuite/store'; import type { BlobManager } from '@blocksuite/store';
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../shared'; import type { BlockSuiteWorkspace } from '../shared';
export function useWorkspaceBlob( export function useWorkspaceBlob(
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
): BlobStorage | null { ): BlobManager {
const [blobStorage, setBlobStorage] = useState<BlobStorage | null>(null); return useMemo(() => blockSuiteWorkspace.blobs, [blockSuiteWorkspace.blobs]);
useEffect(() => {
blockSuiteWorkspace.blobs.then(blobStorage => {
setBlobStorage(blobStorage);
});
}, [blockSuiteWorkspace]);
return blobStorage;
} }
export function useWorkspaceBlobImage( export function useWorkspaceBlobImage(
key: string | null, key: string | null,
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
) { ) {
const blobStorage = useWorkspaceBlob(blockSuiteWorkspace); const blobManager = useWorkspaceBlob(blockSuiteWorkspace);
const [imageURL, setImageURL] = useState<string | null>(null); const [blob, setBlob] = useState<Blob | null>(null);
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
if (key === null) { if (key === null) {
setImageURL(null); setBlob(null);
return; return;
} }
blobStorage?.get(key).then(blob => { blobManager?.get(key).then(blob => {
if (controller.signal.aborted) { if (controller.signal.aborted) {
return; return;
} }
setImageURL(blob); if (blob) {
setBlob(blob);
}
}); });
return () => { return () => {
controller.abort(); controller.abort();
}; };
}, [blobStorage, key]); }, [blobManager, key]);
return imageURL; const [url, setUrl] = useState<string | null>(null);
const ref = useRef<string | null>(null);
useEffect(() => {
if (ref.current) {
URL.revokeObjectURL(ref.current);
}
if (blob) {
const url = URL.createObjectURL(blob);
setUrl(url);
ref.current = url;
}
}, [blob]);
return url;
} }

View File

@@ -43,7 +43,7 @@ export function useAppHelper() {
async (name: string): Promise<string> => { async (name: string): Promise<string> => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(), nanoid(),
_ => undefined WorkspaceFlavour.LOCAL
); );
blockSuiteWorkspace.meta.setName(name); blockSuiteWorkspace.meta.setName(name);
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace); const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);

View File

@@ -2,6 +2,7 @@ import { Button } from '@affine/component';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { createBroadCastChannelProvider } from '@affine/workspace/providers'; import { createBroadCastChannelProvider } from '@affine/workspace/providers';
import type { BroadCastChannelProvider } from '@affine/workspace/type'; import type { BroadCastChannelProvider } from '@affine/workspace/type';
import { 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';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
@@ -22,10 +23,7 @@ declare global {
const BroadcastPage: React.FC = () => { const BroadcastPage: React.FC = () => {
const blockSuiteWorkspace = useMemo( const blockSuiteWorkspace = useMemo(
() => () =>
createEmptyBlockSuiteWorkspace( createEmptyBlockSuiteWorkspace('broadcast-test', WorkspaceFlavour.LOCAL),
'broadcast-test',
(_: string) => undefined
),
[] []
); );
const [provider, setProvider] = useState<BroadCastChannelProvider | null>( const [provider, setProvider] = useState<BroadCastChannelProvider | null>(

View File

@@ -1,4 +1,3 @@
import { getLoginStorage } from '@affine/workspace/affine/login';
import { rootStore } from '@affine/workspace/atom'; import { rootStore } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type'; import type { AffineWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type'; import { WorkspaceFlavour } from '@affine/workspace/type';
@@ -66,9 +65,10 @@ export const fetcher = async (
return workspaces.map(workspace => { return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id, workspace.id,
(k: string) => WorkspaceFlavour.AFFINE,
// fixme: token could be expired {
({ api: '/api/workspace', token: getLoginStorage()?.token }[k]) workspaceApis: affineApis,
}
); );
const remWorkspace: AffineWorkspace = { const remWorkspace: AffineWorkspace = {
...workspace, ...workspace,

View File

@@ -51,12 +51,10 @@ const getPersistenceAllWorkspace = () => {
...items.map((item: z.infer<typeof schema>) => { ...items.map((item: z.infer<typeof schema>) => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
item.id, item.id,
(k: string) => WorkspaceFlavour.AFFINE,
// fixme: token could be expired {
({ workspaceApis: affineApis,
api: prefixUrl + 'api/workspace', }
token: getLoginStorage()?.token,
}[k])
); );
const affineWorkspace: AffineWorkspace = { const affineWorkspace: AffineWorkspace = {
...item, ...item,
@@ -116,19 +114,15 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
const newWorkspaceId = id; const newWorkspaceId = id;
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
const blobs = await blockSuiteWorkspace.blobs; const blobManager = blockSuiteWorkspace.blobs;
if (blobs) { for (const id of await blobManager.list()) {
const ids = await blobs.blobs; const blob = await blobManager.get(id);
for (const id of ids) { if (blob) {
const url = await blobs.get(id); await affineApis.uploadBlob(
if (url) { newWorkspaceId,
const blob = await fetch(url).then(res => res.blob()); await blob.arrayBuffer(),
await affineApis.uploadBlob( blob.type
newWorkspaceId, );
await blob.arrayBuffer(),
blob.type
);
}
} }
} }
@@ -182,9 +176,10 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
return workspaces.map(workspace => { return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id, workspace.id,
(k: string) => WorkspaceFlavour.AFFINE,
// fixme: token could be expired {
({ api: '/api/workspace', token: getLoginStorage()?.token }[k]) workspaceApis: affineApis,
}
); );
const dump = workspaces.map(workspace => { const dump = workspaces.map(workspace => {
return { return {

View File

@@ -29,7 +29,7 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
'app:init': () => { 'app:init': () => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(), nanoid(),
(_: string) => undefined WorkspaceFlavour.LOCAL
); );
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME); blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID); const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID);

View File

@@ -4,7 +4,7 @@
"main": "./src/index.ts", "main": "./src/index.ts",
"scripts": { "scripts": {
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "build-storybook": "NODE_OPTIONS=--max_old_space_size=4096 storybook build",
"test-storybook": "test-storybook" "test-storybook": "test-storybook"
}, },
"exports": { "exports": {
@@ -48,11 +48,11 @@
"react-is": "^18.2.0" "react-is": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@blocksuite/blocks": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/blocks": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/editor": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/editor": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/global": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/global": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/icons": "^2.1.10", "@blocksuite/icons": "^2.1.10",
"@blocksuite/store": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/store": "0.0.0-20230420070759-dbe39fdf-nightly",
"@storybook/addon-actions": "^7.0.5", "@storybook/addon-actions": "^7.0.5",
"@storybook/addon-coverage": "^0.0.8", "@storybook/addon-coverage": "^0.0.8",
"@storybook/addon-essentials": "^7.0.5", "@storybook/addon-essentials": "^7.0.5",

View File

@@ -2,7 +2,7 @@
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { Workspace } from '@blocksuite/store'; import { createMemoryStorage, Workspace } from '@blocksuite/store';
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react'; import { useState } from 'react';
@@ -29,8 +29,9 @@ function initPage(page: Page): void {
const blockSuiteWorkspace = new Workspace({ const blockSuiteWorkspace = new Workspace({
id: 'test', id: 'test',
blobOptionsGetter: () => void 0, blobStorages: [createMemoryStorage],
}); });
blockSuiteWorkspace.register(AffineSchemas).register(__unstableSchemas); blockSuiteWorkspace.register(AffineSchemas).register(__unstableSchemas);
const page = blockSuiteWorkspace.createPage('page0'); const page = blockSuiteWorkspace.createPage('page0');
initPage(page); initPage(page);

View File

@@ -18,19 +18,28 @@ export const Default = () => {
{ {
id: '1', id: '1',
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('1'), blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
'1',
WorkspaceFlavour.LOCAL
),
providers: [], providers: [],
}, },
{ {
id: '2', id: '2',
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('2'), blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
'2',
WorkspaceFlavour.LOCAL
),
providers: [], providers: [],
}, },
{ {
id: '3', id: '3',
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace('3'), blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
'3',
WorkspaceFlavour.LOCAL
),
providers: [], providers: [],
}, },
] satisfies WorkspaceListProps['items']; ] satisfies WorkspaceListProps['items'];

View File

@@ -31,7 +31,10 @@ function initPage(page: Page): void {
page.resetHistory(); page.resetHistory();
} }
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace('test-workspace'); const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'test-workspace',
WorkspaceFlavour.LOCAL
);
initPage(blockSuiteWorkspace.createPage('page0')); initPage(blockSuiteWorkspace.createPage('page0'));
initPage(blockSuiteWorkspace.createPage('page1')); initPage(blockSuiteWorkspace.createPage('page1'));

View File

@@ -21,7 +21,6 @@ export default {
const basicBlockSuiteWorkspace = new Workspace({ const basicBlockSuiteWorkspace = new Workspace({
id: 'blocksuite-local', id: 'blocksuite-local',
blobOptionsGetter: (_: string) => undefined,
}); });
basicBlockSuiteWorkspace.meta.setName('Hello World'); basicBlockSuiteWorkspace.meta.setName('Hello World');
@@ -46,19 +45,17 @@ Basic.args = {
const avatarBlockSuiteWorkspace = new Workspace({ const avatarBlockSuiteWorkspace = new Workspace({
id: 'blocksuite-local', id: 'blocksuite-local',
blobOptionsGetter: (_: string) => undefined,
}); });
avatarBlockSuiteWorkspace.meta.setName('Hello World'); avatarBlockSuiteWorkspace.meta.setName('Hello World');
avatarBlockSuiteWorkspace.blobs.then(async blobs => { fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
if (blobs) { .then(res => res.arrayBuffer())
const buffer = await ( .then(async buffer => {
await fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url)) const id = await avatarBlockSuiteWorkspace.blobs.set(
).arrayBuffer(); new Blob([buffer], { type: 'image/png' })
const id = await blobs.set(new Blob([buffer], { type: 'image/png' })); );
avatarBlockSuiteWorkspace.meta.setAvatar(id); avatarBlockSuiteWorkspace.meta.setAvatar(id);
} });
});
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => { export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
return ( return (

View File

@@ -4,7 +4,7 @@
"main": "./src/index.ts", "main": "./src/index.ts",
"module": "./src/index.ts", "module": "./src/index.ts",
"devDependencies": { "devDependencies": {
"@blocksuite/global": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/global": "0.0.0-20230420070759-dbe39fdf-nightly",
"next": "=13.2.3", "next": "=13.2.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -14,8 +14,9 @@ export function useBlockSuiteWorkspaceAvatarUrl(
fetcher: async avatar => { fetcher: async avatar => {
assertExists(blockSuiteWorkspace); assertExists(blockSuiteWorkspace);
const blobs = await blockSuiteWorkspace.blobs; const blobs = await blockSuiteWorkspace.blobs;
if (blobs) { const blob = await blobs.get(avatar);
return blobs.get(avatar); if (blob) {
return URL.createObjectURL(blob);
} }
return null; return null;
}, },
@@ -27,7 +28,6 @@ export function useBlockSuiteWorkspaceAvatarUrl(
assertExists(blockSuiteWorkspace); assertExists(blockSuiteWorkspace);
const blob = new Blob([file], { type: file.type }); const blob = new Blob([file], { type: file.type });
const blobs = await blockSuiteWorkspace.blobs; const blobs = await blockSuiteWorkspace.blobs;
assertExists(blobs);
const blobId = await blobs.set(blob); const blobId = await blobs.set(blob);
blockSuiteWorkspace.meta.setAvatar(blobId); blockSuiteWorkspace.meta.setAvatar(blobId);
await mutate(blobId); await mutate(blobId);

View File

@@ -7,10 +7,10 @@
"jotai": "^2.0.4" "jotai": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@blocksuite/blocks": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/blocks": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/editor": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/editor": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/global": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/global": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/store": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/store": "0.0.0-20230420070759-dbe39fdf-nightly",
"lottie-web": "^5.11.0" "lottie-web": "^5.11.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -3,6 +3,7 @@
"private": true, "private": true,
"exports": { "exports": {
"./atom": "./src/atom.ts", "./atom": "./src/atom.ts",
"./blob": "./src/blob/index.ts",
"./utils": "./src/utils.ts", "./utils": "./src/utils.ts",
"./type": "./src/type.ts", "./type": "./src/type.ts",
"./local/crud": "./src/local/crud.ts", "./local/crud": "./src/local/crud.ts",

View File

@@ -8,6 +8,7 @@ import { readFile } from 'node:fs/promises';
import { MessageCode } from '@affine/env/constant'; import { MessageCode } from '@affine/env/constant';
import { createStatusApis } from '@affine/workspace/affine/api/status'; import { createStatusApis } from '@affine/workspace/affine/api/status';
import { KeckProvider } from '@affine/workspace/affine/keck'; import { KeckProvider } from '@affine/workspace/affine/keck';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import user1 from '@affine-test/fixtures/built-in-user1.json'; import user1 from '@affine-test/fixtures/built-in-user1.json';
import user2 from '@affine-test/fixtures/built-in-user2.json'; import user2 from '@affine-test/fixtures/built-in-user2.json';
@@ -119,7 +120,7 @@ async function createWorkspace(
): Promise<string> { ): Promise<string> {
const workspace = createEmptyBlockSuiteWorkspace( const workspace = createEmptyBlockSuiteWorkspace(
faker.datatype.uuid(), faker.datatype.uuid(),
_ => undefined WorkspaceFlavour.LOCAL
); );
if (callback) { if (callback) {
callback(workspace); callback(workspace);
@@ -408,9 +409,15 @@ describe('api', () => {
); );
}); });
const binary = await workspaceApis.downloadWorkspace(id, false); const binary = await workspaceApis.downloadWorkspace(id, false);
const workspace = createEmptyBlockSuiteWorkspace(id, () => undefined); const workspace = createEmptyBlockSuiteWorkspace(
id,
WorkspaceFlavour.LOCAL
);
Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(binary)); Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(binary));
const workspace2 = createEmptyBlockSuiteWorkspace(id, () => undefined); const workspace2 = createEmptyBlockSuiteWorkspace(
id,
WorkspaceFlavour.LOCAL
);
{ {
const wsUrl = `ws://127.0.0.1:3000/api/sync/`; const wsUrl = `ws://127.0.0.1:3000/api/sync/`;
const provider = new KeckProvider(wsUrl, workspace.id, workspace.doc, { const provider = new KeckProvider(wsUrl, workspace.id, workspace.doc, {
@@ -459,7 +466,7 @@ describe('api', () => {
); );
const publicWorkspace = createEmptyBlockSuiteWorkspace( const publicWorkspace = createEmptyBlockSuiteWorkspace(
id, id,
() => undefined WorkspaceFlavour.LOCAL
); );
Workspace.Y.applyUpdate(publicWorkspace.doc, new Uint8Array(binary)); Workspace.Y.applyUpdate(publicWorkspace.doc, new Uint8Array(binary));
const publicPage = publicWorkspace.getPage(pageId) as Page; const publicPage = publicWorkspace.getPage(pageId) as Page;

View File

@@ -1,3 +1,8 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import type { Workspace } from '@affine/workspace/affine/api'; import type { Workspace } from '@affine/workspace/affine/api';
import { import {
createWorkspaceApis, createWorkspaceApis,
@@ -6,6 +11,7 @@ import {
import { KeckProvider } from '@affine/workspace/affine/keck'; import { KeckProvider } from '@affine/workspace/affine/keck';
import type { LoginResponse } from '@affine/workspace/affine/login'; import type { LoginResponse } from '@affine/workspace/affine/login';
import { loginResponseSchema } from '@affine/workspace/affine/login'; import { loginResponseSchema } from '@affine/workspace/affine/login';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import user1 from '@affine-test/fixtures/built-in-user1.json'; import user1 from '@affine-test/fixtures/built-in-user1.json';
import user2 from '@affine-test/fixtures/built-in-user2.json'; import user2 from '@affine-test/fixtures/built-in-user2.json';
@@ -80,11 +86,17 @@ describe('ydoc sync', () => {
const binary = await workspaceApis.downloadWorkspace(root.id); const binary = await workspaceApis.downloadWorkspace(root.id);
const workspace1 = createEmptyBlockSuiteWorkspace( const workspace1 = createEmptyBlockSuiteWorkspace(
root.id, root.id,
(k: string) => ({ api: '/api/workspace', token: user1Token.token }[k]) WorkspaceFlavour.AFFINE,
{
workspaceApis,
}
); );
const workspace2 = createEmptyBlockSuiteWorkspace( const workspace2 = createEmptyBlockSuiteWorkspace(
root.id, root.id,
(k: string) => ({ api: '/api/workspace', token: user2Token.token }[k]) WorkspaceFlavour.AFFINE,
{
workspaceApis,
}
); );
BlockSuiteWorkspace.Y.applyUpdate(workspace1.doc, new Uint8Array(binary)); BlockSuiteWorkspace.Y.applyUpdate(workspace1.doc, new Uint8Array(binary));
BlockSuiteWorkspace.Y.applyUpdate(workspace2.doc, new Uint8Array(binary)); BlockSuiteWorkspace.Y.applyUpdate(workspace2.doc, new Uint8Array(binary));

View File

@@ -0,0 +1,106 @@
import type { BlobStorage } from '@blocksuite/store';
import { createIndexeddbStorage } from '@blocksuite/store';
import { openDB } from 'idb';
import type { DBSchema } from 'idb/build/entry';
import type { createWorkspaceApis } from '../affine/api';
type UploadingBlob = {
key: string;
arrayBuffer: ArrayBuffer;
type: string;
};
interface AffineBlob extends DBSchema {
uploading: {
key: string;
value: UploadingBlob;
};
// todo: migrate blob storage from `createIndexeddbStorage`
}
export const createAffineBlobStorage = (
workspaceId: string,
workspaceApis: ReturnType<typeof createWorkspaceApis>
): BlobStorage => {
const storage = createIndexeddbStorage(workspaceId);
const dbPromise = openDB<AffineBlob>('affine-blob', 1, {
upgrade(db) {
db.createObjectStore('uploading', { keyPath: 'key' });
},
});
dbPromise.then(async db => {
const t = db.transaction('uploading', 'readwrite').objectStore('uploading');
await t.getAll().then(blobs =>
blobs.map(({ arrayBuffer, type }) =>
workspaceApis.uploadBlob(workspaceId, arrayBuffer, type).then(key => {
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
return t.delete(key);
})
)
);
});
return {
crud: {
get: async key => {
const blob = await storage.crud.get(key);
if (!blob) {
const buffer = await workspaceApis.getBlob(workspaceId, key);
return new Blob([buffer]);
} else {
return blob;
}
},
set: async (key, value) => {
const db = await dbPromise;
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
let uploaded = false;
t.put({
key,
arrayBuffer: await value.arrayBuffer(),
type: value.type,
}).then(() => {
// delete the uploading blob after uploaded
if (uploaded) {
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
t.delete(key);
}
});
await Promise.all([
storage.crud.set(key, value),
workspaceApis
.uploadBlob(workspaceId, await value.arrayBuffer(), value.type)
.then(async () => {
uploaded = true;
const t = db
.transaction('uploading', 'readwrite')
.objectStore('uploading');
// delete the uploading blob after uploaded
if (await t.get(key)) {
await t.delete(key);
}
}),
]);
return key;
},
delete: async (key: string) => {
await Promise.all([
storage.crud.delete(key),
// we don't support deleting a blob in API?
// workspaceApis.deleteBlob(workspaceId, key)
]);
},
list: async () => {
const blobs = await storage.crud.list();
// we don't support listing blobs in API?
return [...blobs];
},
},
};
};

View File

@@ -43,7 +43,7 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
} }
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id, id,
(_: string) => undefined WorkspaceFlavour.LOCAL
); );
const workspace: LocalWorkspace = { const workspace: LocalWorkspace = {
id, id,
@@ -62,7 +62,7 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
const id = nanoid(); const id = nanoid();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace( const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id, id,
(_: string) => undefined WorkspaceFlavour.LOCAL
); );
BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary); BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary);
const persistence = createIndexedDBProvider(id, blockSuiteWorkspace.doc); const persistence = createIndexedDBProvider(id, blockSuiteWorkspace.doc);

View File

@@ -1,16 +1,47 @@
import type { createWorkspaceApis } from '@affine/workspace/affine/api';
import { createAffineBlobStorage } from '@affine/workspace/blob';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { BlobOptionsGetter, Generator } from '@blocksuite/store'; import type { Generator } from '@blocksuite/store';
import { Workspace } from '@blocksuite/store'; import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
import { WorkspaceFlavour } from './type';
const hashMap = new Map<string, Workspace>(); const hashMap = new Map<string, Workspace>();
export const createEmptyBlockSuiteWorkspace = (
export function createEmptyBlockSuiteWorkspace(
id: string, id: string,
blobOptionsGetter?: BlobOptionsGetter, flavour: WorkspaceFlavour.AFFINE,
config?: { config: {
workspaceApis: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string; cachePrefix?: string;
idGenerator?: Generator; idGenerator?: Generator;
} }
): Workspace => { ): Workspace;
export function createEmptyBlockSuiteWorkspace(
id: string,
flavour: WorkspaceFlavour.LOCAL,
config?: {
workspaceApis?: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string;
idGenerator?: Generator;
}
): Workspace;
export function createEmptyBlockSuiteWorkspace(
id: string,
flavour: WorkspaceFlavour,
config?: {
workspaceApis?: ReturnType<typeof createWorkspaceApis>;
cachePrefix?: string;
idGenerator?: Generator;
}
): Workspace {
if (
flavour === WorkspaceFlavour.AFFINE &&
!config?.workspaceApis?.getBlob &&
!config?.workspaceApis?.uploadBlob
) {
throw new Error('workspaceApis is required for affine flavour');
}
const prefix: string = config?.cachePrefix ?? ''; const prefix: string = config?.cachePrefix ?? '';
const cacheKey = `${prefix}${id}`; const cacheKey = `${prefix}${id}`;
if (hashMap.has(cacheKey)) { if (hashMap.has(cacheKey)) {
@@ -20,11 +51,16 @@ export const createEmptyBlockSuiteWorkspace = (
const workspace = new Workspace({ const workspace = new Workspace({
id, id,
isSSR: typeof window === 'undefined', isSSR: typeof window === 'undefined',
blobOptionsGetter, blobStorages:
flavour === WorkspaceFlavour.AFFINE
? [id => createAffineBlobStorage(id, config!.workspaceApis!)]
: typeof window === 'undefined'
? []
: [createIndexeddbStorage],
idGenerator, idGenerator,
}) })
.register(AffineSchemas) .register(AffineSchemas)
.register(__unstableSchemas); .register(__unstableSchemas);
hashMap.set(cacheKey, workspace); hashMap.set(cacheKey, workspace);
return workspace; return workspace;
}; }

View File

@@ -26,8 +26,8 @@
"idb": "^7.1.1" "idb": "^7.1.1"
}, },
"devDependencies": { "devDependencies": {
"@blocksuite/blocks": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/blocks": "0.0.0-20230420070759-dbe39fdf-nightly",
"@blocksuite/store": "0.0.0-20230416194015-c6ae6f0f-nightly", "@blocksuite/store": "0.0.0-20230420070759-dbe39fdf-nightly",
"vite": "^4.2.1", "vite": "^4.2.1",
"vite-plugin-dts": "^2.2.0", "vite-plugin-dts": "^2.2.0",
"y-indexeddb": "^9.0.10" "y-indexeddb": "^9.0.10"

893
yarn.lock

File diff suppressed because it is too large Load Diff