refactor: support suspense mode in workspaces (#1304)

This commit is contained in:
Himself65
2023-03-04 20:11:15 -06:00
committed by GitHub
parent dd6bee68cb
commit 9a199eb9a1
27 changed files with 713 additions and 652 deletions

View File

@@ -1,4 +1,4 @@
import { assertEquals } from '@blocksuite/store';
import { createJSONStorage } from 'jotai/utils';
import React from 'react';
import { preload } from 'swr';
import { z } from 'zod';
@@ -19,6 +19,7 @@ import { createEmptyBlockSuiteWorkspace } from '../../utils';
import { WorkspacePlugin } from '..';
import { fetcher, QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
const kAffineLocal = 'affine-local-storage-v2';
const schema = z.object({
id: z.string(),
@@ -31,33 +32,48 @@ const schema = z.object({
export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
flavour: RemWorkspaceFlavour.AFFINE,
loadPriority: LoadPriority.HIGH,
createWorkspace: async (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
blockSuiteWorkspace.doc
);
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
return id;
},
deleteWorkspace: async workspace => {
await apis.deleteWorkspace({
id: workspace.id,
});
workspace.providers.forEach(p => p.cleanup());
},
prefetchData: async dataCenter => {
if (localStorage.getItem(kAffineLocal)) {
const localData = JSON.parse(localStorage.getItem(kAffineLocal) || '[]');
if (Array.isArray(localData)) {
const workspacesDump = localData
.map((item: any) => {
const result = schema.safeParse(item);
if (result.success) {
return result.data;
}
return null;
})
.filter(Boolean) as z.infer<typeof schema>[];
const workspaces = workspacesDump.map(workspace => {
CRUD: {
create: async blockSuiteWorkspace => {
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
blockSuiteWorkspace.doc
);
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
return id;
},
delete: async workspace => {
await apis.deleteWorkspace({
id: workspace.id,
});
},
get: async workspaceId => {
const workspaces: AffineWorkspace[] = await preload(
QueryKey.getWorkspaces,
fetcher
);
const workspace = workspaces.find(
workspace => workspace.id === workspaceId
);
const dump = workspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
create_at: workspace.create_at,
} satisfies z.infer<typeof schema>;
});
storage.setItem(kAffineLocal, dump);
if (!workspace) {
return null;
}
return workspace;
},
list: async () => {
// fixme: refactor auth check
if (!apis.auth.isLogin) return [];
return await apis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
(k: string) =>
@@ -66,75 +82,14 @@ export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
);
const affineWorkspace: AffineWorkspace = {
...workspace,
flavour: RemWorkspaceFlavour.AFFINE,
blockSuiteWorkspace,
providers: [...createAffineProviders(blockSuiteWorkspace)],
flavour: RemWorkspaceFlavour.AFFINE,
};
return affineWorkspace;
});
// fixme: refactor to a function
workspaces.forEach(workspace => {
const exist = dataCenter.workspaces.findIndex(
ws => ws.id === workspace.id
);
if (exist !== -1) {
dataCenter.workspaces.splice(exist, 1, workspace);
dataCenter.workspaces = [...dataCenter.workspaces];
} else {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
}
});
dataCenter.callbacks.forEach(cb => cb());
} else {
localStorage.removeItem(kAffineLocal);
}
}
const promise: Promise<AffineWorkspace[]> = preload(
QueryKey.getWorkspaces,
fetcher
);
return promise
.then(async workspaces => {
const promises = workspaces.map(workspace => {
assertEquals(workspace.flavour, RemWorkspaceFlavour.AFFINE);
return workspace;
});
return Promise.all(promises)
.then(workspaces => {
workspaces.forEach(workspace => {
if (workspace === null) {
return;
}
const exist = dataCenter.workspaces.findIndex(
ws => ws.id === workspace.id
);
if (exist !== -1) {
dataCenter.workspaces.splice(exist, 1, workspace);
dataCenter.workspaces = [...dataCenter.workspaces];
} else {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
}
});
return workspaces;
})
.then(ws => {
const workspaces = ws.filter(Boolean) as AffineWorkspace[];
const dump = workspaces.map(workspace => {
return {
id: workspace.id,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
create_at: workspace.create_at,
} satisfies z.infer<typeof schema>;
});
localStorage.setItem(kAffineLocal, JSON.stringify(dump));
});
})
.catch(error => {
console.error(error);
});
},
},
PageDetail: ({ currentWorkspace, currentPageId }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);

View File

@@ -1,12 +1,10 @@
import { assertExists } from '@blocksuite/store';
import React from 'react';
import { refreshDataCenter } from '../hooks/use-workspaces';
import { jotaiStore, jotaiWorkspacesAtom } from '../atoms';
import {
BlockSuiteWorkspace,
FlavourToWorkspace,
LoadPriority,
RemWorkspace,
RemWorkspaceFlavour,
SettingPanel,
} from '../shared';
@@ -45,19 +43,14 @@ export interface WorkspacePlugin<Flavour extends RemWorkspaceFlavour> {
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
// Fetch necessary data for the first render
prefetchData: (
dataCenter: {
workspaces: RemWorkspace[];
callbacks: Set<() => void>;
},
signal?: AbortSignal
) => Promise<void>;
createWorkspace: (
blockSuiteWorkspace: BlockSuiteWorkspace
) => Promise<string>;
deleteWorkspace: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
CRUD: {
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
delete: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
get: (workspaceId: string) => Promise<FlavourToWorkspace[Flavour] | null>;
// not supported yet
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
list: () => Promise<FlavourToWorkspace[Flavour][]>;
};
//#region UI
PageDetail: React.FC<PageDetailProps<Flavour>>;
@@ -73,18 +66,26 @@ export const WorkspacePlugins = {
[Key in RemWorkspaceFlavour]: WorkspacePlugin<Key>;
};
/**
* Transform workspace from one flavour to another
*
* The logic here is to delete the old workspace and create a new one.
*/
export async function transformWorkspace<
From extends RemWorkspaceFlavour,
To extends RemWorkspaceFlavour
>(from: From, to: To, workspace: FlavourToWorkspace[From]): Promise<string> {
// fixme: type cast
await WorkspacePlugins[from].deleteWorkspace(workspace as any);
const newId = await WorkspacePlugins[to].createWorkspace(
await WorkspacePlugins[from].CRUD.delete(workspace as any);
const newId = await WorkspacePlugins[to].CRUD.create(
workspace.blockSuiteWorkspace
);
// refresh the data center
dataCenter.workspaces = [];
await refreshDataCenter();
assertExists(dataCenter.workspaces.some(w => w.id === newId));
const workspaces = jotaiStore.get(jotaiWorkspacesAtom);
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, {
id: newId,
flavour: to,
});
jotaiStore.set(jotaiWorkspacesAtom, [...workspaces]);
return newId;
}

View File

@@ -1,8 +1,9 @@
import { DebugLogger } from '@affine/debug';
import { config, DEFAULT_WORKSPACE_NAME } from '@affine/env';
import { assertEquals, nanoid } from '@blocksuite/store';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
import { nanoid } from '@blocksuite/store';
import { createJSONStorage } from 'jotai/utils';
import React from 'react';
import { IndexeddbPersistence } from 'y-indexeddb';
import { z } from 'zod';
import { createLocalProviders } from '../../blocksuite';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
@@ -18,139 +19,90 @@ import {
import { createEmptyBlockSuiteWorkspace } from '../../utils';
import { WorkspacePlugin } from '..';
const logger = new DebugLogger('local-plugin');
const getStorage = () => createJSONStorage(() => localStorage);
export const kStoreKey = 'affine-local-workspace';
// fixme(himself65): this is a hacking that first workspace will disappear somehow
const hashMap = new Map<string, BlockSuiteWorkspace>();
const schema = z.array(z.string());
export const LocalPlugin: WorkspacePlugin<RemWorkspaceFlavour.LOCAL> = {
flavour: RemWorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW,
createWorkspace: async blockSuiteWorkspace => {
let ids: string[] = [];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
CRUD: {
get: async workspaceId => {
const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []);
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
const id = data.find(id => id === workspaceId);
if (!id) {
return null;
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
const id = nanoid();
const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc);
await persistence.whenSynced.then(() => {
persistence.destroy();
});
ids.push(id);
localStorage.setItem(kStoreKey, JSON.stringify(ids));
return id;
},
deleteWorkspace: async workspace => {
const id = workspace.id;
let ids: string[];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
const idx = ids.findIndex(x => x === id);
if (idx === -1) {
throw new Error('cannot find local workspace from localStorage');
}
workspace.providers.forEach(p => p.cleanup());
ids.splice(idx, 1);
assertEquals(
ids.every(id => typeof id === 'string'),
true
);
localStorage.setItem(kStoreKey, JSON.stringify(ids));
},
prefetchData: async (dataCenter, signal) => {
if (typeof window === 'undefined') {
// SSR mode, no local data
return;
}
if (signal?.aborted) {
return;
}
let ids: string[];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
if (config.enableIndexedDBProvider) {
const workspaces = await Promise.all(
ids.map(id => {
const blockSuiteWorkspace = hashMap.has(id)
? (hashMap.get(id) as BlockSuiteWorkspace)
: createEmptyBlockSuiteWorkspace(id, (_: string) => undefined);
hashMap.set(id, blockSuiteWorkspace);
const workspace: LocalWorkspace = {
id,
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace: blockSuiteWorkspace,
providers: [...createLocalProviders(blockSuiteWorkspace)],
};
return workspace;
})
);
workspaces.forEach(workspace => {
if (workspace) {
const exist = dataCenter.workspaces.findIndex(
w => w.id === workspace.id
);
if (exist === -1) {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
} else {
dataCenter.workspaces[exist] = workspace;
dataCenter.workspaces = [...dataCenter.workspaces];
}
}
});
}
if (dataCenter.workspaces.length === 0) {
if (signal?.aborted) {
return;
}
logger.info('no local workspace found, create a new one');
const workspaceId = nanoid();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
id,
(_: string) => undefined
);
hashMap.set(workspaceId, blockSuiteWorkspace);
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
localStorage.setItem(kStoreKey, JSON.stringify([workspaceId]));
blockSuiteWorkspace.createPage(nanoid());
const workspace: LocalWorkspace = {
id: workspaceId,
id,
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace: blockSuiteWorkspace,
providers: [...createLocalProviders(blockSuiteWorkspace)],
};
const persistence = new IndexeddbPersistence(
blockSuiteWorkspace.room as string,
blockSuiteWorkspace.doc
return workspace;
},
create: async ({ doc }) => {
const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []);
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc);
const id = nanoid();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id,
(_: string) => undefined
);
BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary);
const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc);
await persistence.whenSynced.then(() => {
persistence.destroy();
});
dataCenter.workspaces = [workspace];
}
storage.setItem(kStoreKey, [...data, id]);
console.log('create', id, storage.getItem(kStoreKey));
return id;
},
delete: async workspace => {
const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []);
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
const idx = data.findIndex(id => id === workspace.id);
if (idx === -1) {
throw new Error('workspace not found');
}
data.splice(idx, 1);
storage.setItem(kStoreKey, [...data]);
},
list: async () => {
const storage = getStorage();
!Array.isArray(storage.getItem(kStoreKey)) &&
storage.setItem(kStoreKey, []);
const data = (
await Promise.all(
(storage.getItem(kStoreKey) as z.infer<typeof schema>).map(id =>
LocalPlugin.CRUD.get(id)
)
)
).filter(item => item !== null) as LocalWorkspace[];
if (data.length === 0) {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(),
(_: string) => undefined
);
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
await LocalPlugin.CRUD.create(blockSuiteWorkspace);
return LocalPlugin.CRUD.list();
}
return data;
},
},
PageDetail: ({ currentWorkspace, currentPageId }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);