mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: init affine blob storage (#2045)
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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' : ''),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
106
packages/workspace/src/blob/index.ts
Normal file
106
packages/workspace/src/blob/index.ts
Normal 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];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user