From 46792e5757ac1d03a7bdc936945c66fe8d746017 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Thu, 5 Jan 2023 22:20:40 +0800 Subject: [PATCH 01/32] feat: data center business apis --- packages/data-center/src/apis/business.ts | 115 ++++++++++++++++++ packages/data-center/src/apis/index.ts | 10 +- packages/data-center/src/apis/workspace.ts | 2 +- packages/data-center/src/datacenter.ts | 8 +- packages/data-center/src/style/index.ts | 24 ++++ packages/data-center/src/workspaces/index.ts | 1 + .../data-center/src/workspaces/workspaces.ts | 113 +++++++++++++++++ 7 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 packages/data-center/src/apis/business.ts create mode 100644 packages/data-center/src/style/index.ts create mode 100644 packages/data-center/src/workspaces/index.ts create mode 100644 packages/data-center/src/workspaces/workspaces.ts diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/apis/business.ts new file mode 100644 index 0000000000..ab5b258d29 --- /dev/null +++ b/packages/data-center/src/apis/business.ts @@ -0,0 +1,115 @@ +import { uuidv4 } from '@blocksuite/store'; +import { getDataCenter } from 'src'; +import { DataCenter } from 'src/datacenter'; +import { Workspace, WorkspaceMeta, WorkspaceType } from '../style'; +import { token } from './token'; + +export class Business { + private _dc: DataCenter | undefined; + + private async _getDc() { + if (!this._dc) { + this._dc = await getDataCenter(); + } + return this._dc; + } + + async createWorkspace( + name: string + ): Promise> { + let id = ''; + let type = WorkspaceType.local; + if (token.isLogin) { + // TODO: add default avatar + const data = await this._dc?.apis.createWorkspace({ name, avatar: '' }); + id = data?.id || ''; + type = WorkspaceType.cloud; + this._dc?.load(id, { providerId: 'affine' }); + } else { + this._dc?.load(uuidv4(), { providerId: 'local' }); + } + const newWorkspaces = (await this.getWorkspaces()).find(w => w.id === id); + return { + id: newWorkspaces?.id || '', + name, + avatar: '', + type, + }; + } + + // updateWorkspaceMeta( + // id: string, + // meta: { name?: string; avatar: Partial } + // ) {} + + async getWorkspaces(focusUpdated?: boolean): Promise { + const dc = await this._getDc(); + if (focusUpdated) { + await dc.workspacesList.refreshWorkspaceList(); + } + return dc.workspacesList.getWorkspaces(); + } + + async onWorkspaceChange(cb: (workspaces: Workspace[]) => void) { + const dc = await this._getDc(); + dc.workspacesList.on('change', cb); + } + + async deleteWorkspace(id: string) { + const dc = await this._getDc(); + const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); + if (workspace?.type === WorkspaceType.cloud) { + dc.apis.deleteWorkspace({ id }); + } + dc.delete(id); + } + + async leaveWorkspace(id: string) { + const dc = await this._getDc(); + const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); + if (workspace?.type === WorkspaceType.cloud) { + dc.apis.leaveWorkspace({ id }); + dc.delete(id); + } + } + + setWorkspacePublish(id: string, isPublish: boolean): boolean { + return isPublish; + } + + async getWorkspaceById(id: string) { + const dc = await this._getDc(); + const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); + if (workspace?.type === WorkspaceType.cloud) { + return dc.load(id, { providerId: 'affine' }); + } else { + return dc.load(id, { providerId: 'local' }); + } + } + + // getMembers(id: string): any {} + + // inviteMember(id: string, email: string) {} + + async acceptInvitation(invitingCode: string) { + const dc = await this._getDc(); + dc.apis.acceptInviting({ invitingCode }); + } + + // getUserInfo(): any {} + + async login() { + const dc = await this._getDc(); + await dc.auth('affine'); + } + + // logout() {} + + // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} + + // importWorkspace(file: File) {} + + // exportWorkspace(id: string) {} + + // enableWorkspaceCloud(id: string) {} +} diff --git a/packages/data-center/src/apis/index.ts b/packages/data-center/src/apis/index.ts index de62a9fcb1..57c003d8fd 100644 --- a/packages/data-center/src/apis/index.ts +++ b/packages/data-center/src/apis/index.ts @@ -4,16 +4,24 @@ export type { Callback } from './token.js'; import { getAuthorizer } from './token.js'; import * as user from './user.js'; import * as workspace from './workspace.js'; +import * as business from './business.js'; export type Apis = typeof user & typeof workspace & { + business: typeof business; signInWithGoogle: ReturnType[0]; onAuthStateChanged: ReturnType[1]; }; export const getApis = (): Apis => { const [signInWithGoogle, onAuthStateChanged] = getAuthorizer(); - return { ...user, ...workspace, signInWithGoogle, onAuthStateChanged }; + return { + ...user, + ...workspace, + business, + signInWithGoogle, + onAuthStateChanged, + }; }; export type { AccessTokenMessage } from './token'; diff --git a/packages/data-center/src/apis/workspace.ts b/packages/data-center/src/apis/workspace.ts index 396aa46f8e..0c145928d5 100644 --- a/packages/data-center/src/apis/workspace.ts +++ b/packages/data-center/src/apis/workspace.ts @@ -84,7 +84,7 @@ export interface CreateWorkspaceParams { export async function createWorkspace( params: CreateWorkspaceParams -): Promise { +): Promise<{ id: string }> { return client.post('api/workspace', { json: params }).json(); } diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 3e31e9f1d8..dd01e7785f 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -7,6 +7,7 @@ import { getApis, Apis } from './apis/index.js'; import { AffineProvider, BaseProvider } from './provider/index.js'; import { LocalProvider } from './provider/index.js'; import { getKVConfigure } from './store.js'; +import { Workspaces } from './workspaces'; // load workspace's config type LoadConfig = { @@ -33,6 +34,7 @@ export class DataCenter { private readonly _workspaces = new Map>(); private readonly _config; private readonly _logger; + public readonly workspacesList = new Workspaces(this); readonly signals = { listAdd: new Signal(), @@ -62,6 +64,7 @@ export class DataCenter { this.signals.listRemove.on(workspace => { this._config.delete(`list:${workspace}`); }); + this.workspacesList.init(); } get apis(): Readonly { @@ -195,10 +198,11 @@ export class DataCenter { * data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist */ async list(): Promise>> { + const listString = 'list:'; const entries: [string, WorkspaceItem][] = await this._config.entries(); return entries.reduce((acc, [k, i]) => { - if (k.startsWith('list:')) { - const key = k.slice(5); + if (k.startsWith(listString)) { + const key = k.slice(listString.length); acc[key] = acc[key] || {}; acc[key][i.provider] = i.locally; } diff --git a/packages/data-center/src/style/index.ts b/packages/data-center/src/style/index.ts new file mode 100644 index 0000000000..522946a099 --- /dev/null +++ b/packages/data-center/src/style/index.ts @@ -0,0 +1,24 @@ +export enum WorkspaceType { + local = 'local', + cloud = 'cloud', +} + +export type Workspace = { + name: string; + id: string; + isPublish?: boolean; + avatar?: string; + type: WorkspaceType; + owner?: User; + isLocal?: boolean; + memberCount: number; +}; + +export type User = { + name: string; + id: string; + email: string; + avatar: string; +}; + +export type WorkspaceMeta = Pick; diff --git a/packages/data-center/src/workspaces/index.ts b/packages/data-center/src/workspaces/index.ts new file mode 100644 index 0000000000..05a26f8e2b --- /dev/null +++ b/packages/data-center/src/workspaces/index.ts @@ -0,0 +1 @@ +export { Workspaces } from './workspaces'; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts new file mode 100644 index 0000000000..2c2211614d --- /dev/null +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -0,0 +1,113 @@ +import { Workspace } from '@blocksuite/store'; +import { Observable } from 'lib0/observable'; +import { WorkspaceDetail } from 'src/apis/workspace'; +import { DataCenter } from 'src/datacenter'; +import { User, Workspace as Wp, WorkspaceType } from 'src/style'; + +function getProvider(providerList: Record) { + return Object.keys(providerList)[0]; +} + +function isCloudWorkspace(provider: string) { + return provider === 'affine'; +} + +export class Workspaces extends Observable { + private _workspaces: Wp[]; + private readonly workspaceInstances: Map = new Map(); + // cache cloud workspace owner + _dc: DataCenter; + + constructor(dataCenter: DataCenter) { + super(); + this._workspaces = []; + this.workspaceInstances = new Map(); + this._dc = dataCenter; + } + + init() { + // IMP: init local providers + this._dc.auth('local'); + // add listener on list change + this._dc.signals.listAdd.on(e => { + this.refreshWorkspaceList(); + }); + this._dc.signals.listRemove.on(e => { + this.refreshWorkspaceList(); + }); + } + + async refreshWorkspaceList() { + const workspaceList = await this._dc.list(); + + const workspaceMap = Object.keys(workspaceList).map(([id]) => { + return this._dc.load(id).then(w => { + return { id, workspace: w, provider: getProvider(workspaceList[id]) }; + }); + }); + + const workspaces = (await Promise.all(workspaceMap)).map(w => { + const { id, workspace, provider } = w; + if (workspace && !this.workspaceInstances.has(id)) { + this.workspaceInstances.set(id, workspace); + } + return { + id, + name: (workspace?.doc?.meta.name as string) || '', + avatar: (workspace?.meta.avatar as string) || '', + type: isCloudWorkspace(provider) + ? WorkspaceType.cloud + : WorkspaceType.local, + isLocal: false, + isPublish: false, + owner: undefined, + memberCount: 1, + } as Wp; + }); + const getDetailList = (await Promise.all(workspaceMap)).map(w => { + const { id, provider } = w; + if (provider === 'workspaces') { + return new Promise<{ id: string; detail: WorkspaceDetail | null }>( + resolve => { + this._dc.apis.getWorkspaceDetail({ id }).then(data => { + resolve({ id, detail: data || null }); + }); + } + ); + } + }); + const ownerList = await Promise.all(getDetailList); + (await Promise.all(ownerList)).forEach(detail => { + if (detail) { + const { id, detail: workspaceDetail } = detail; + if (workspaceDetail) { + const { owner, member_count } = workspaceDetail; + const currentWorkspace = workspaces.find(w => w.id === id); + if (currentWorkspace) { + currentWorkspace.owner = { + id: owner.id, + name: owner.name, + avatar: owner.avatar_url, + email: owner.email, + }; + currentWorkspace.memberCount = member_count; + } + } + } + }); + this._updateWorkspaces(workspaces); + } + + getWorkspaces() { + return this._workspaces; + } + + _updateWorkspaces(workspaces: Wp[]) { + this._workspaces = workspaces; + this.emit('change', this._workspaces); + } + + onWorkspaceChange(cb: (workspace: Wp) => void) { + this.on('change', cb); + } +} From 27aaf2de1a6fc391aacfe4777744a87f8396a737 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Thu, 5 Jan 2023 22:55:41 +0800 Subject: [PATCH 02/32] feat: add api state --- packages/data-center/src/apis/business.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/apis/business.ts index ab5b258d29..a2586d444c 100644 --- a/packages/data-center/src/apis/business.ts +++ b/packages/data-center/src/apis/business.ts @@ -37,6 +37,7 @@ export class Business { }; } + // not think out a good way to update workspace meta // updateWorkspaceMeta( // id: string, // meta: { name?: string; avatar: Partial } @@ -87,6 +88,7 @@ export class Business { } } + // no time // getMembers(id: string): any {} // inviteMember(id: string, email: string) {} @@ -96,20 +98,27 @@ export class Business { dc.apis.acceptInviting({ invitingCode }); } + // check with dark sky // getUserInfo(): any {} + // TODO check with dark sky async login() { const dc = await this._getDc(); await dc.auth('affine'); } + // just has no time // logout() {} + // need discuss // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} + // need discuss // importWorkspace(file: File) {} + // need discuss may be not in apis // exportWorkspace(id: string) {} + // need discuss // enableWorkspaceCloud(id: string) {} } From e2293ffc44357164f11ea3694bac3f1ebc6b88b6 Mon Sep 17 00:00:00 2001 From: tzhangchi Date: Thu, 5 Jan 2023 23:58:17 +0800 Subject: [PATCH 03/32] feat: add api names --- packages/data-center/src/apis/business.ts | 90 ++++++++++++++++--- .../data-center/src/workspaces/workspaces.ts | 4 +- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/apis/business.ts index a2586d444c..e364f01a60 100644 --- a/packages/data-center/src/apis/business.ts +++ b/packages/data-center/src/apis/business.ts @@ -1,6 +1,6 @@ import { uuidv4 } from '@blocksuite/store'; -import { getDataCenter } from 'src'; -import { DataCenter } from 'src/datacenter'; +import { getDataCenter } from './../index'; +import { DataCenter } from './../datacenter'; import { Workspace, WorkspaceMeta, WorkspaceType } from '../style'; import { token } from './token'; @@ -51,6 +51,18 @@ export class Business { return dc.workspacesList.getWorkspaces(); } + /** + * Get page list by workspace id + * @param {string} id ID of workspace. + */ + getPagesByWorkspaceId(id: string) { + return []; + } + + /** + * Observe the update of the workspace + * @param {function} callback({Workspace[]}). + */ async onWorkspaceChange(cb: (workspaces: Workspace[]) => void) { const dc = await this._getDc(); dc.workspacesList.on('change', cb); @@ -65,6 +77,10 @@ export class Business { dc.delete(id); } + /** + * The member of the workspace go to leave workspace + * @param {string} id ID of workspace. + */ async leaveWorkspace(id: string) { const dc = await this._getDc(); const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); @@ -74,10 +90,19 @@ export class Business { } } + /** + * Let the workspace to be public + * @param {string} id ID of workspace. + * @param {string} isPublish publish flag of workspace. + */ setWorkspacePublish(id: string, isPublish: boolean): boolean { return isPublish; } + /** + * Get workspace by workspace id + * @param {string} id ID of workspace. + */ async getWorkspaceById(id: string) { const dc = await this._getDc(); const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); @@ -89,9 +114,21 @@ export class Business { } // no time - // getMembers(id: string): any {} - - // inviteMember(id: string, email: string) {} + /** + * Get the members of the workspace + * @param {string} id ID of workspace. + */ + getMembers(id: string): any { + void 0; + } + /** + * Add a new member to the workspace + * @param {string} id ID of workspace. + * @param {string} email new member email. + */ + inviteMember(id: string, email: string) { + void 0; + } async acceptInvitation(invitingCode: string) { const dc = await this._getDc(); @@ -99,7 +136,12 @@ export class Business { } // check with dark sky - // getUserInfo(): any {} + /** + * Get login user info + */ + getUserInfo() { + void 0; + } // TODO check with dark sky async login() { @@ -108,17 +150,45 @@ export class Business { } // just has no time - // logout() {} + /** + * Logout and clear login session + */ + logout() { + void 0; + } // need discuss + /** + * Create a connection between local and cloud, sync cloud data to local + * @param {string} id ID of workspace. + * @param {string} id type of workspace. + */ // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} // need discuss - // importWorkspace(file: File) {} + /** + * Select a file to import the workspace + * @param {File} file file of workspace. + */ + importWorkspace(file: File) { + void 0; + } // need discuss may be not in apis - // exportWorkspace(id: string) {} + // /** + // * Generate a file ,and export it to local file system + // * @param {string} id ID of workspace. + // */ + exportWorkspace(id: string) { + void 0; + } // need discuss - // enableWorkspaceCloud(id: string) {} + // /** + // * Enable workspace cloud flag + // * @param {string} id ID of workspace. + // */ + enableWorkspaceCloud(id: string) { + void 0; + } } diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index 2c2211614d..72348e79d7 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,8 +1,8 @@ import { Workspace } from '@blocksuite/store'; import { Observable } from 'lib0/observable'; import { WorkspaceDetail } from 'src/apis/workspace'; -import { DataCenter } from 'src/datacenter'; -import { User, Workspace as Wp, WorkspaceType } from 'src/style'; +import { DataCenter } from './../datacenter'; +import { User, Workspace as Wp, WorkspaceType } from './../style'; function getProvider(providerList: Record) { return Object.keys(providerList)[0]; From 1da129378934c55902bfe8d3a0f5437f1ab3fcea Mon Sep 17 00:00:00 2001 From: tzhangchi Date: Fri, 6 Jan 2023 00:18:43 +0800 Subject: [PATCH 04/32] feat: add removeMember api --- packages/data-center/src/apis/business.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/apis/business.ts index e364f01a60..ababd0e28a 100644 --- a/packages/data-center/src/apis/business.ts +++ b/packages/data-center/src/apis/business.ts @@ -130,6 +130,19 @@ export class Business { void 0; } + /** + * remove the new member to the workspace + * @param {string} workspaceId ID of workspace. + * @param {string} memberId ID of member + */ + removeMember(workspaceId: string, memberId: string) { + void 0; + } + + /** + * A new member click the invite link, finish to join the workspace + * @param {string} inviteCode token for invitation. + */ async acceptInvitation(invitingCode: string) { const dc = await this._getDc(); dc.apis.acceptInviting({ invitingCode }); From f4b7e5f474ee250095bfa0bfe2543943707a2aae Mon Sep 17 00:00:00 2001 From: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Date: Fri, 6 Jan 2023 13:06:00 +0800 Subject: [PATCH 05/32] feat: add padding for editor --- packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx index 971e950f8b..619f8e037e 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -19,6 +19,7 @@ import { usePageHelper } from '@/hooks/use-page-helper'; const StyledEditorContainer = styled('div')(() => { return { height: 'calc(100vh - 60px)', + padding: '0 32px', }; }); From 449b08c9c207e76edb23c4bbbd0bea81bab97e18 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Fri, 6 Jan 2023 13:26:10 +0800 Subject: [PATCH 06/32] feat: compat with surface block structure --- packages/app/src/components/import/index.tsx | 5 +++-- packages/app/src/hooks/use-props-updated.ts | 2 +- packages/app/src/pages/playground/templates.tsx | 5 +++-- .../app/src/pages/workspace/[workspaceId]/[pageId].tsx | 9 +++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/import/index.tsx b/packages/app/src/components/import/index.tsx index 7387cb5de6..96a383dad2 100644 --- a/packages/app/src/components/import/index.tsx +++ b/packages/app/src/components/import/index.tsx @@ -31,9 +31,10 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => { setTimeout(() => { const editor = document.querySelector('editor-container'); if (editor) { - const groupId = page.addBlock({ flavour: 'affine:group' }, pageId); + page.addBlock({ flavour: 'affine:surface' }, null); + const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId); // TODO blocksuite should offer a method to import markdown from store - editor.clipboard.importMarkdown(template.source, `${groupId}`); + editor.clipboard.importMarkdown(template.source, `${frameId}`); page.resetHistory(); editor.requestUpdate(); } diff --git a/packages/app/src/hooks/use-props-updated.ts b/packages/app/src/hooks/use-props-updated.ts index d76ada43d2..ad6fc2525a 100644 --- a/packages/app/src/hooks/use-props-updated.ts +++ b/packages/app/src/hooks/use-props-updated.ts @@ -17,7 +17,7 @@ export const usePropsUpdated: UsePropsUpdated = () => { return; } setTimeout(() => { - editor.model?.propsUpdated.on(() => { + editor.pageBlockModel?.propsUpdated.on(() => { callbackQueue.current.forEach(callback => { callback(editor); }); diff --git a/packages/app/src/pages/playground/templates.tsx b/packages/app/src/pages/playground/templates.tsx index 0517498878..d32cbaa3f5 100644 --- a/packages/app/src/pages/playground/templates.tsx +++ b/packages/app/src/pages/playground/templates.tsx @@ -46,9 +46,10 @@ const All = () => { setTimeout(() => { const editor = document.querySelector('editor-container'); if (editor) { - const groupId = page.addBlock({ flavour: 'affine:group' }, pageId); + page.addBlock({ flavour: 'affine:surface' }, null); + const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId); // TODO blocksuite should offer a method to import markdown from store - editor.clipboard.importMarkdown(template.source, `${groupId}`); + editor.clipboard.importMarkdown(template.source, `${frameId}`); page.resetHistory(); editor.requestUpdate(); } diff --git a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx index 619f8e037e..998ec29941 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -55,14 +55,15 @@ const Page: NextPageWithLayout = () => { flavour: 'affine:page', title, }); - const groupId = currentPage!.addBlock( - { flavour: 'affine:group' }, + currentPage!.addBlock({ flavour: 'affine:surface' }, null); + const frameId = currentPage!.addBlock( + { flavour: 'affine:frame' }, pageId ); - currentPage!.addBlock({ flavour: 'affine:group' }, pageId); + currentPage!.addBlock({ flavour: 'affine:frame' }, pageId); // If this is a first page in workspace, init an introduction markdown if (isFirstPage) { - editor.clipboard.importMarkdown(exampleMarkdown, `${groupId}`); + editor.clipboard.importMarkdown(exampleMarkdown, `${frameId}`); currentWorkspace!.setPageMeta(currentPage!.id, { title }); } currentPage!.resetHistory(); From cc13bf8265255564b5668320afe070f5cf351803 Mon Sep 17 00:00:00 2001 From: himself65 Date: Fri, 6 Jan 2023 14:37:21 +0800 Subject: [PATCH 07/32] chore: bump version --- packages/app/package.json | 6 ++-- pnpm-lock.yaml | 69 +++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index ab9571a953..9dac686ae9 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -11,10 +11,10 @@ }, "dependencies": { "@affine/datacenter": "workspace:*", - "@blocksuite/blocks": "0.3.1", - "@blocksuite/editor": "0.3.1", + "@blocksuite/blocks": "=0.3.1-20230106060050-1aad55d", + "@blocksuite/editor": "=0.3.1-20230106060050-1aad55d", "@blocksuite/icons": "^2.0.2", - "@blocksuite/store": "0.3.1", + "@blocksuite/store": "=0.3.1-20230106060050-1aad55d", "@emotion/css": "^11.10.0", "@emotion/react": "^11.10.4", "@emotion/server": "^11.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbf408c5db..4ccb8a6299 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,10 +45,10 @@ importers: packages/app: specifiers: '@affine/datacenter': workspace:* - '@blocksuite/blocks': 0.3.1 - '@blocksuite/editor': 0.3.1 + '@blocksuite/blocks': '=0.3.1-20230106060050-1aad55d' + '@blocksuite/editor': '=0.3.1-20230106060050-1aad55d' '@blocksuite/icons': ^2.0.2 - '@blocksuite/store': 0.3.1 + '@blocksuite/store': '=0.3.1-20230106060050-1aad55d' '@emotion/css': ^11.10.0 '@emotion/react': ^11.10.4 '@emotion/server': ^11.10.0 @@ -87,10 +87,10 @@ importers: yjs: ^13.5.44 dependencies: '@affine/datacenter': link:../data-center - '@blocksuite/blocks': 0.3.1_yjs@13.5.44 - '@blocksuite/editor': 0.3.1_yjs@13.5.44 + '@blocksuite/blocks': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 + '@blocksuite/editor': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 '@blocksuite/icons': 2.0.4_w5j4k42lgipnm43s3brx6h3c34 - '@blocksuite/store': 0.3.1_yjs@13.5.44 + '@blocksuite/store': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 '@emotion/css': 11.10.0 '@emotion/react': 11.10.4_w5j4k42lgipnm43s3brx6h3c34 '@emotion/server': 11.10.0_@emotion+css@11.10.0 @@ -1496,6 +1496,26 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@blocksuite/blocks/0.3.1-20230106060050-1aad55d_yjs@13.5.44: + resolution: {integrity: sha512-qRNXmhjw+GAGsV1mI2XXPxYTlHfsFHv9ttTCNQ6IIcxvc5Hh6lWmdwVibxvlpYUkgEc1zv3/GxOEsR/ngpZXzQ==} + dependencies: + '@blocksuite/phasor': 0.3.1_yjs@13.5.44 + '@blocksuite/store': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 + '@tldraw/intersect': 1.8.0 + autosize: 5.0.2 + highlight.js: 11.7.0 + hotkeys-js: 3.10.1 + lit: 2.5.0 + perfect-freehand: 1.2.0 + quill: 1.3.7 + quill-cursors: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - yjs + dev: false + /@blocksuite/blocks/0.3.1_yjs@13.5.44: resolution: {integrity: sha512-b0dGz2MG4yIgngJPRumaMY58wAsd2FEVSZl3tpCXlagK9I0HD165Bq70PxcaRHVjBSV1Gf29ZVHUF6BVTYogPw==} dependencies: @@ -1515,11 +1535,11 @@ packages: - yjs dev: false - /@blocksuite/editor/0.3.1_yjs@13.5.44: - resolution: {integrity: sha512-ycKcyvPW6R8R2GZOFneGH1xNi5gJBx5WtWjW9YwcQslFzXVWMCCBips1Bud2uL4kkbWQoodyua6k2vsXxGAKLw==} + /@blocksuite/editor/0.3.1-20230106060050-1aad55d_yjs@13.5.44: + resolution: {integrity: sha512-wSlAF9XVxIkHFJ1qCzn7oQ/gwXybFYMrzRl35UTJV509D+DuWZefRZWvpdIDCOUJ24uQscr1HxwsON11ltfWgA==} dependencies: - '@blocksuite/blocks': 0.3.1_yjs@13.5.44 - '@blocksuite/store': 0.3.1_yjs@13.5.44 + '@blocksuite/blocks': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 + '@blocksuite/store': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 lit: 2.5.0 marked: 4.2.5 turndown: 7.1.1 @@ -1540,6 +1560,35 @@ packages: react: 18.2.0 dev: false + /@blocksuite/phasor/0.3.1_yjs@13.5.44: + resolution: {integrity: sha512-aJmAQn2qoF6HxFZWgq7xa/pWVyzg3MmD6dynIHAKdfN7rBdKk3PNA+lRX919QkD2e270N/zgHEGFFQI1Nj5xrA==} + peerDependencies: + yjs: ^13 + dependencies: + yjs: 13.5.44 + dev: false + + /@blocksuite/store/0.3.1-20230106060050-1aad55d_yjs@13.5.44: + resolution: {integrity: sha512-dRy+YzlWMwiYq0Im9NogK/NTkV+NKK+lgejYq56m6nH2m16/G9AMODqP0oQy/XeYFevUpL9i9RdV0rHsJ2gc0Q==} + peerDependencies: + yjs: ^13 + dependencies: + '@types/flexsearch': 0.7.3 + '@types/quill': 1.3.10 + buffer: 6.0.3 + flexsearch: 0.7.21 + idb-keyval: 6.2.0 + ky: 0.33.1 + lib0: 0.2.58 + y-protocols: 1.0.5 + y-webrtc: 10.2.3 + yjs: 13.5.44 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@blocksuite/store/0.3.1_yjs@13.5.44: resolution: {integrity: sha512-kynVTDfNCSChz2JI2rtGHxRIV2YrLzvAgVajcbfDVCuXKG0siBoEjLasG1a0kvevbvW/FabrNAj+xaIplklioA==} peerDependencies: From d43bdb7aa91e5075a111d09df9fb017268ae5669 Mon Sep 17 00:00:00 2001 From: QiShaoXuan Date: Fri, 6 Jan 2023 14:50:12 +0800 Subject: [PATCH 08/32] chore: change default outline workspace name --- packages/app/src/hooks/use-ensure-workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/hooks/use-ensure-workspace.ts b/packages/app/src/hooks/use-ensure-workspace.ts index a340b78a48..d20c63b05f 100644 --- a/packages/app/src/hooks/use-ensure-workspace.ts +++ b/packages/app/src/hooks/use-ensure-workspace.ts @@ -1,8 +1,8 @@ import { useState, useEffect } from 'react'; import { useAppState } from '@/providers/app-state-provider'; import { useRouter } from 'next/router'; -const defaultOutLineWorkspaceId = - 'local-first-' + '85b4ca0b9081421d903bbc2501ea280f'; +const defaultOutLineWorkspaceId = 'affine'; +// 'local-first-' + '85b4ca0b9081421d903bbc2501ea280f'; // It is a fully effective hook // Cause it not just ensure workspace loaded, but also have router change. export const useEnsureWorkspace = () => { From f01c6e12d2efcc197a0807d8931cd869ea4b30a6 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 6 Jan 2023 15:03:54 +0800 Subject: [PATCH 09/32] chore: cleanup actions --- .changeset/config.json | 2 +- .github/workflows/changlog.yml | 3 +- .github/workflows/temp_test.yml | 128 -------------------------------- 3 files changed, 2 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/temp_test.yml diff --git a/.changeset/config.json b/.changeset/config.json index 1892e1a308..6ce3b1a2ce 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,7 +5,7 @@ "fixed": [], "linked": [], "access": "restricted", - "baseBranch": "feat/cloud-sync", + "baseBranch": "feat/master", "updateInternalDependencies": "patch", "ignore": [] } diff --git a/.github/workflows/changlog.yml b/.github/workflows/changlog.yml index 6a4ced3e20..d91559cf50 100644 --- a/.github/workflows/changlog.yml +++ b/.github/workflows/changlog.yml @@ -1,8 +1,7 @@ name: Pathfinder changelog on: - push: - branches: [feat/cloud-sync, master] + workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. # See https://docs.github.com/en/actions/using-jobs/using-concurrency diff --git a/.github/workflows/temp_test.yml b/.github/workflows/temp_test.yml deleted file mode 100644 index a1fa93bdf0..0000000000 --- a/.github/workflows/temp_test.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Pathfinder Check - -on: - push: - branches: [feat/cloud-sync] - pull_request: - branches: [feat/cloud-sync] - -# Cancels all previous workflow runs for pull requests that have not completed. -# See https://docs.github.com/en/actions/using-jobs/using-concurrency -concurrency: - # The concurrency group contains the workflow name and the branch name for - # pull requests or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - build: - name: Build on Pull Request - if: github.ref != 'refs/heads/master' - runs-on: self-hosted - environment: development - - steps: - - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2 - with: - version: 'latest' - - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: 18.x - registry-url: https://npm.pkg.github.com - scope: '@toeverything' - cache: 'pnpm' - - - run: node scripts/module-resolve/ci.js - - - name: Restore cache - uses: actions/cache@v3 - with: - path: | - .next/cache - # Generate a new cache whenever packages or source files change. - key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} - # If source files changed but packages didn't, rebuild from a prior cache. - restore-keys: | - ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }} - - - name: Build - run: pnpm build - env: - NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} - NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} - NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} - - - name: Export - run: pnpm export - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - path: ./packages/app/.next - - lint: - name: Lint and E2E Test - runs-on: ubuntu-latest - environment: development - needs: build - - steps: - - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2 - with: - version: 'latest' - - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: 18.x - cache: 'pnpm' - - - name: Restore cache - uses: actions/cache@v3 - with: - path: | - .next/cache - # Generate a new cache whenever packages or source files change. - key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} - # If source files changed but packages didn't, rebuild from a prior cache. - restore-keys: | - ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- - - - name: Install dependencies - run: pnpm install - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }} - - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: artifact - path: packages/app/.next/ - - - name: Lint & E2E Test - run: | - pnpm lint --max-warnings=0 - PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium - PLAYWRIGHT_BROWSERS_PATH=0 pnpm test - PLAYWRIGHT_BROWSERS_PATH=0 pnpm test:dc - env: - NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} - NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} - NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} - NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} - NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} - NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} - NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} From b1a39fa2e77a02c7f9b7a29a43df66c0b734b20c Mon Sep 17 00:00:00 2001 From: x1a0t <405028157@qq.com> Date: Fri, 6 Jan 2023 15:03:42 +0800 Subject: [PATCH 10/32] feat: compatiable with async importMarkdown() --- packages/app/src/hooks/use-props-updated.ts | 2 +- packages/app/src/pages/playground/templates.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/hooks/use-props-updated.ts b/packages/app/src/hooks/use-props-updated.ts index ad6fc2525a..f0df6ffd49 100644 --- a/packages/app/src/hooks/use-props-updated.ts +++ b/packages/app/src/hooks/use-props-updated.ts @@ -26,7 +26,7 @@ export const usePropsUpdated: UsePropsUpdated = () => { return () => { callbackQueue.current = []; - editor?.model?.propsUpdated.dispose(); + editor?.model?.propsUpdated?.dispose(); }; }, [editor]); diff --git a/packages/app/src/pages/playground/templates.tsx b/packages/app/src/pages/playground/templates.tsx index d32cbaa3f5..a83d25e516 100644 --- a/packages/app/src/pages/playground/templates.tsx +++ b/packages/app/src/pages/playground/templates.tsx @@ -43,13 +43,16 @@ const All = () => { if (page) { currentWorkspace?.setPageMeta(page.id, { title }); if (page && page.root === null) { - setTimeout(() => { + setTimeout(async () => { const editor = document.querySelector('editor-container'); if (editor) { page.addBlock({ flavour: 'affine:surface' }, null); const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId); // TODO blocksuite should offer a method to import markdown from store - editor.clipboard.importMarkdown(template.source, `${frameId}`); + await editor.clipboard.importMarkdown( + template.source, + `${frameId}` + ); page.resetHistory(); editor.requestUpdate(); } From 274075d7745797742b607614c3e9476a67e02ab6 Mon Sep 17 00:00:00 2001 From: himself65 Date: Fri, 6 Jan 2023 15:29:30 +0800 Subject: [PATCH 11/32] fix: code --- packages/app/src/components/header/editor-header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/header/editor-header.tsx b/packages/app/src/components/header/editor-header.tsx index c1b52ec464..7f433de467 100644 --- a/packages/app/src/components/header/editor-header.tsx +++ b/packages/app/src/components/header/editor-header.tsx @@ -22,14 +22,14 @@ export const EditorHeader = () => { useEffect(() => { onPropsUpdated(editor => { - setTitle(editor.model?.title || 'Untitled'); + setTitle(editor.pageBlockModel?.title || 'Untitled'); }); }, [onPropsUpdated]); useEffect(() => { setTimeout(() => { // If first time in, need to wait for editor to be inserted into DOM - setTitle(editor?.model?.title || 'Untitled'); + setTitle(editor?.pageBlockModel?.title || 'Untitled'); }, 300); }, [editor]); From 49fdaf4a18e1605431158eddf1352c1bf2951b05 Mon Sep 17 00:00:00 2001 From: x1a0t <405028157@qq.com> Date: Fri, 6 Jan 2023 16:07:15 +0800 Subject: [PATCH 12/32] fix: build error --- packages/app/src/hooks/use-props-updated.ts | 2 +- packages/data-center/package.json | 4 +- pnpm-lock.yaml | 48 ++------------------- 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/packages/app/src/hooks/use-props-updated.ts b/packages/app/src/hooks/use-props-updated.ts index f0df6ffd49..1bbd8d2cd4 100644 --- a/packages/app/src/hooks/use-props-updated.ts +++ b/packages/app/src/hooks/use-props-updated.ts @@ -26,7 +26,7 @@ export const usePropsUpdated: UsePropsUpdated = () => { return () => { callbackQueue.current = []; - editor?.model?.propsUpdated?.dispose(); + editor?.pageBlockModel?.propsUpdated?.dispose(); }; }, [editor]); diff --git a/packages/data-center/package.json b/packages/data-center/package.json index 5d0ebc843f..fe639b306e 100644 --- a/packages/data-center/package.json +++ b/packages/data-center/package.json @@ -26,8 +26,8 @@ "typescript": "^4.8.4" }, "dependencies": { - "@blocksuite/blocks": "^0.3.1", - "@blocksuite/store": "^0.3.1", + "@blocksuite/blocks": "=0.3.1-20230106060050-1aad55d", + "@blocksuite/store": "=0.3.1-20230106060050-1aad55d", "debug": "^4.3.4", "encoding": "^0.1.13", "firebase": "^9.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ccb8a6299..26f0cce6b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,8 +131,8 @@ importers: packages/data-center: specifiers: - '@blocksuite/blocks': ^0.3.1 - '@blocksuite/store': ^0.3.1 + '@blocksuite/blocks': '=0.3.1-20230106060050-1aad55d' + '@blocksuite/store': '=0.3.1-20230106060050-1aad55d' '@playwright/test': ^1.29.1 '@types/debug': ^4.1.7 debug: ^4.3.4 @@ -148,8 +148,8 @@ importers: y-protocols: ^1.0.5 yjs: ^13.5.44 dependencies: - '@blocksuite/blocks': 0.3.1_yjs@13.5.44 - '@blocksuite/store': 0.3.1_yjs@13.5.44 + '@blocksuite/blocks': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 + '@blocksuite/store': 0.3.1-20230106060050-1aad55d_yjs@13.5.44 debug: 4.3.4 encoding: 0.1.13 firebase: 9.15.0_encoding@0.1.13 @@ -1516,25 +1516,6 @@ packages: - yjs dev: false - /@blocksuite/blocks/0.3.1_yjs@13.5.44: - resolution: {integrity: sha512-b0dGz2MG4yIgngJPRumaMY58wAsd2FEVSZl3tpCXlagK9I0HD165Bq70PxcaRHVjBSV1Gf29ZVHUF6BVTYogPw==} - dependencies: - '@blocksuite/store': 0.3.1_yjs@13.5.44 - '@tldraw/intersect': 1.8.0 - autosize: 5.0.2 - highlight.js: 11.7.0 - hotkeys-js: 3.10.1 - lit: 2.5.0 - perfect-freehand: 1.2.0 - quill: 1.3.7 - quill-cursors: 4.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - yjs - dev: false - /@blocksuite/editor/0.3.1-20230106060050-1aad55d_yjs@13.5.44: resolution: {integrity: sha512-wSlAF9XVxIkHFJ1qCzn7oQ/gwXybFYMrzRl35UTJV509D+DuWZefRZWvpdIDCOUJ24uQscr1HxwsON11ltfWgA==} dependencies: @@ -1589,27 +1570,6 @@ packages: - utf-8-validate dev: false - /@blocksuite/store/0.3.1_yjs@13.5.44: - resolution: {integrity: sha512-kynVTDfNCSChz2JI2rtGHxRIV2YrLzvAgVajcbfDVCuXKG0siBoEjLasG1a0kvevbvW/FabrNAj+xaIplklioA==} - peerDependencies: - yjs: ^13 - dependencies: - '@types/flexsearch': 0.7.3 - '@types/quill': 1.3.10 - buffer: 6.0.3 - flexsearch: 0.7.21 - idb-keyval: 6.2.0 - ky: 0.33.1 - lib0: 0.2.58 - y-protocols: 1.0.5 - y-webrtc: 10.2.3 - yjs: 13.5.44 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@changesets/apply-release-plan/6.1.3: resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} dependencies: From 82c4a35648a824a1a93a868d57766ff5a8c45a3f Mon Sep 17 00:00:00 2001 From: x1a0t <405028157@qq.com> Date: Fri, 6 Jan 2023 16:31:27 +0800 Subject: [PATCH 13/32] fix: unit test --- tests/quick-search.spec.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/quick-search.spec.ts b/tests/quick-search.spec.ts index 242e09bc51..1fd1b1efb9 100644 --- a/tests/quick-search.spec.ts +++ b/tests/quick-search.spec.ts @@ -7,10 +7,13 @@ loadPage(); const openQuickSearchByShortcut = async (page: Page) => await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 })); -async function assertTitleTexts(page: Page, texts: string[]) { - const actual = await page - .locator('.affine-default-page-block-title') - .allTextContents(); +async function assertTitleTexts(page: Page, texts: string) { + const actual = await page.evaluate(() => { + const titleElement = ( + document.querySelector('.affine-default-page-block-title') + ); + return titleElement.value; + }); expect(actual).toEqual(texts); } async function assertResultList(page: Page, texts: string[]) { @@ -55,7 +58,7 @@ test.describe('Add new page in quick search', () => { const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]'); await addNewPage.click(); await page.waitForTimeout(200); - await assertTitleTexts(page, ['']); + await assertTitleTexts(page, ''); }); test('Create a new page with keyword', async ({ page }) => { @@ -65,7 +68,7 @@ test.describe('Add new page in quick search', () => { const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]'); await addNewPage.click(); await page.waitForTimeout(200); - await assertTitleTexts(page, ['test123456']); + await assertTitleTexts(page, 'test123456'); }); }); @@ -81,6 +84,6 @@ test.describe('Search and select', () => { await page.keyboard.insertText('test123456'); await assertResultList(page, ['test123456']); await page.keyboard.press('Enter', { delay: 50 }); - await assertTitleTexts(page, ['test123456']); + await assertTitleTexts(page, 'test123456'); }); }); From 9b278d29d5b2c9b74f4f0c00376c5f9261e851dd Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 6 Jan 2023 17:28:35 +0800 Subject: [PATCH 14/32] feat: self-hosted provider --- .vscode/settings.json | 9 ++++++++- packages/app/next.config.js | 8 ++++++++ .../providers/app-state-provider/dynamic-blocksuite.tsx | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a366193c02..166b0206c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,12 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.formatOnSaveMode": "file", - "cSpell.words": ["blocksuite", "datacenter", "livedemo", "pnpm", "testid"] + "cSpell.words": [ + "blocksuite", + "datacenter", + "livedemo", + "pnpm", + "selfhosted", + "testid" + ] } diff --git a/packages/app/next.config.js b/packages/app/next.config.js index 6d7225864c..10da796485 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -46,6 +46,10 @@ const nextConfig = { source: '/api/:path*', destination: destinationAC, }, + { + source: '/collaboration/:path*', + destination: 'http://localhost:3000/collaboration/:path*', + }, ]; } else { let destinationStandard = 'http://100.77.180.48:11001/api/:path*'; @@ -58,6 +62,10 @@ const nextConfig = { source: '/api/:path*', destination: destinationStandard, }, + { + source: '/collaboration/:path*', + destination: 'http://localhost:3000/collaboration/:path*', + }, ]; } }, diff --git a/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx b/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx index 3a8fb7671c..1875a35557 100644 --- a/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx +++ b/packages/app/src/providers/app-state-provider/dynamic-blocksuite.tsx @@ -18,7 +18,7 @@ const DynamicBlocksuite = ({ const openWorkspace: LoadWorkspaceHandler = async (workspaceId: string) => { if (workspaceId) { const dc = await getDataCenter(); - return dc.load(workspaceId, { providerId: 'affine' }); + return dc.load(workspaceId, { providerId: 'selfhosted' }); } else { return null; } From bc767ed526969f7b781c1964025439b6d219c253 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 6 Jan 2023 17:32:34 +0800 Subject: [PATCH 15/32] feat: provider & ci --- .github/workflows/publish.yml | 114 ++++ packages/data-center/src/datacenter.ts | 10 +- packages/data-center/src/provider/index.ts | 1 + .../src/provider/selfhosted/index.ts | 63 +++ .../src/provider/selfhosted/sync.js | 508 ++++++++++++++++++ 5 files changed, 694 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 packages/data-center/src/provider/selfhosted/index.ts create mode 100644 packages/data-center/src/provider/selfhosted/sync.js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..09ec455e77 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,114 @@ +name: Build Pathfinder Self-hosted + +on: + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +# See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + # The concurrency group contains the workflow name and the branch name for + # pull requests or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + build-self-hosted: + name: Build Community + if: github.ref == 'refs/heads/master' + runs-on: self-hosted + environment: development + + steps: + - uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: 'latest' + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + cache: 'pnpm' + + - name: Restore cache + uses: actions/cache@v3 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Install dependencies + run: pnpm install + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }} + + - name: Build + run: pnpm build + env: + NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} + NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} + + - name: Export + run: pnpm export + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + path: ./packages/app/out + + publish-self-hosted: + name: Push Community Image + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + needs: build-self-hosted + + permissions: + contents: read + packages: write + + env: + REGISTRY: ghcr.io + IMAGE_NAME: 'toeverything/affine-static' + IMAGE_TAG_LATEST: abbey-wood + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: artifact + path: packages/app/out/ + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: ${{ env.IMAGE_TAG_LATEST }} + + - name: Build Docker image + uses: docker/build-push-action@v3 + with: + context: . + push: true + file: ./.github/deployment/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 3e31e9f1d8..59f72b6afc 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -4,8 +4,13 @@ import { Workspace, Signal } from '@blocksuite/store'; import { getLogger } from './index.js'; import { getApis, Apis } from './apis/index.js'; -import { AffineProvider, BaseProvider } from './provider/index.js'; -import { LocalProvider } from './provider/index.js'; +import { + AffineProvider, + BaseProvider, + LocalProvider, + SelfHostedProvider, +} from './provider/index.js'; + import { getKVConfigure } from './store.js'; // load workspace's config @@ -43,6 +48,7 @@ export class DataCenter { const dc = new DataCenter(debug); dc.addProvider(AffineProvider); dc.addProvider(LocalProvider); + dc.addProvider(SelfHostedProvider); return dc; } diff --git a/packages/data-center/src/provider/index.ts b/packages/data-center/src/provider/index.ts index 5718b7eef0..dc4667af7c 100644 --- a/packages/data-center/src/provider/index.ts +++ b/packages/data-center/src/provider/index.ts @@ -20,3 +20,4 @@ export type { Apis, ConfigStore, DataCenterSignals, Workspace }; export type { BaseProvider } from './base.js'; export { AffineProvider } from './affine/index.js'; export { LocalProvider } from './local/index.js'; +export { SelfHostedProvider } from './selfhosted/index.js'; diff --git a/packages/data-center/src/provider/selfhosted/index.ts b/packages/data-center/src/provider/selfhosted/index.ts new file mode 100644 index 0000000000..48632480ef --- /dev/null +++ b/packages/data-center/src/provider/selfhosted/index.ts @@ -0,0 +1,63 @@ +import assert from 'assert'; + +import { LocalProvider } from '../local/index.js'; +import { WebsocketProvider } from './sync.js'; + +export class SelfHostedProvider extends LocalProvider { + static id = 'selfhosted'; + private _ws?: WebsocketProvider; + + constructor() { + super(); + } + + async destroy() { + this._ws?.disconnect(); + } + + async initData() { + const databases = await indexedDB.databases(); + await super.initData( + // set locally to true if exists a same name db + databases + .map(db => db.name) + .filter(v => v) + .includes(this._workspace.room) + ); + + const workspace = this._workspace; + const doc = workspace.doc; + + if (workspace.room) { + try { + // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later + this._ws = new WebsocketProvider(this.host, workspace.room, doc); + await new Promise((resolve, reject) => { + // TODO: synced will also be triggered on reconnection after losing sync + // There needs to be an event mechanism to emit the synchronization state to the upper layer + assert(this._ws); + this._ws.once('synced', () => resolve()); + this._ws.once('lost-connection', () => resolve()); + this._ws.once('connection-error', () => reject()); + }); + this._signals.listAdd.emit({ + workspace: workspace.room, + provider: this.id, + locally: true, + }); + } catch (e) { + this._logger('Failed to init cloud workspace', e); + } + } + + // if after update, the space:meta is empty + // then we need to get map with doc + // just a workaround for yjs + doc.getMap('space:meta'); + } + + private get host() { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${location.host}/collaboration/`; + } +} diff --git a/packages/data-center/src/provider/selfhosted/sync.js b/packages/data-center/src/provider/selfhosted/sync.js new file mode 100644 index 0000000000..54c8dec628 --- /dev/null +++ b/packages/data-center/src/provider/selfhosted/sync.js @@ -0,0 +1,508 @@ +/* eslint-disable no-undef */ +/** + * @module provider/websocket + */ + +/* eslint-env browser */ + +// import * as Y from 'yjs'; // eslint-disable-line +import * as bc from 'lib0/broadcastchannel'; +import * as time from 'lib0/time'; +import * as encoding from 'lib0/encoding'; +import * as decoding from 'lib0/decoding'; +import * as syncProtocol from 'y-protocols/sync'; +import * as authProtocol from 'y-protocols/auth'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import { Observable } from 'lib0/observable'; +import * as math from 'lib0/math'; +import * as url from 'lib0/url'; + +export const messageSync = 0; +export const messageQueryAwareness = 3; +export const messageAwareness = 1; +export const messageAuth = 2; + +/** + * encoder, decoder, provider, emitSynced, messageType + * @type {Array} + */ +const messageHandlers = []; + +messageHandlers[messageSync] = ( + encoder, + decoder, + provider, + emitSynced, + _messageType +) => { + encoding.writeVarUint(encoder, messageSync); + const syncMessageType = syncProtocol.readSyncMessage( + decoder, + encoder, + provider.doc, + provider + ); + if ( + emitSynced && + syncMessageType === syncProtocol.messageYjsSyncStep2 && + !provider.synced + ) { + provider.synced = true; + } +}; + +messageHandlers[messageQueryAwareness] = ( + encoder, + _decoder, + provider, + _emitSynced, + _messageType +) => { + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate( + provider.awareness, + Array.from(provider.awareness.getStates().keys()) + ) + ); +}; + +messageHandlers[messageAwareness] = ( + _encoder, + decoder, + provider, + _emitSynced, + _messageType +) => { + awarenessProtocol.applyAwarenessUpdate( + provider.awareness, + decoding.readVarUint8Array(decoder), + provider + ); +}; + +messageHandlers[messageAuth] = ( + _encoder, + decoder, + provider, + _emitSynced, + _messageType +) => { + authProtocol.readAuthMessage(decoder, provider.doc, (_ydoc, reason) => + permissionDeniedHandler(provider, reason) + ); +}; + +// @todo - this should depend on awareness.outdatedTime +const messageReconnectTimeout = 30000; + +/** + * @param {WebsocketProvider} provider + * @param {string} reason + */ +const permissionDeniedHandler = (provider, reason) => + console.warn(`Permission denied to access ${provider.url}.\n${reason}`); + +/** + * @param {WebsocketProvider} provider + * @param {Uint8Array} buf + * @param {boolean} emitSynced + * @return {encoding.Encoder} + */ +const readMessage = (provider, buf, emitSynced) => { + const decoder = decoding.createDecoder(buf); + const encoder = encoding.createEncoder(); + const messageType = decoding.readVarUint(decoder); + const messageHandler = provider.messageHandlers[messageType]; + if (/** @type {any} */ (messageHandler)) { + messageHandler(encoder, decoder, provider, emitSynced, messageType); + } else { + console.error('Unable to compute message'); + } + return encoder; +}; + +/** + * @param {WebsocketProvider} provider + */ +const setupWS = provider => { + if (provider.shouldConnect && provider.ws === null) { + const websocket = new provider._WS(provider.url, 'AFFiNE'); + websocket.binaryType = 'arraybuffer'; + provider.ws = websocket; + provider.wsconnecting = true; + provider.wsconnected = false; + provider.synced = false; + + websocket.onmessage = event => { + provider.wsLastMessageReceived = time.getUnixTime(); + const encoder = readMessage(provider, new Uint8Array(event.data), true); + if (encoding.length(encoder) > 1) { + websocket.send(encoding.toUint8Array(encoder)); + } + }; + websocket.onerror = event => { + provider.emit('connection-error', [event, provider]); + }; + websocket.onclose = event => { + provider.emit('connection-close', [event, provider]); + provider.ws = null; + provider.wsconnecting = false; + if (provider.wsconnected) { + provider.wsconnected = false; + provider.synced = false; + // update awareness (all users except local left) + awarenessProtocol.removeAwarenessStates( + provider.awareness, + Array.from(provider.awareness.getStates().keys()).filter( + client => client !== provider.doc.clientID + ), + provider + ); + provider.emit('status', [ + { + status: 'disconnected', + }, + ]); + } else { + provider.wsUnsuccessfulReconnects++; + } + // Start with no reconnect timeout and increase timeout by + // using exponential backoff starting with 100ms + setTimeout( + setupWS, + math.min( + math.pow(2, provider.wsUnsuccessfulReconnects) * 100, + provider.maxBackoffTime + ), + provider + ); + }; + websocket.onopen = () => { + provider.wsLastMessageReceived = time.getUnixTime(); + provider.wsconnecting = false; + provider.wsconnected = true; + provider.wsUnsuccessfulReconnects = 0; + provider.emit('status', [ + { + status: 'connected', + }, + ]); + // always send sync step 1 when connected + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, provider.doc); + websocket.send(encoding.toUint8Array(encoder)); + // broadcast local awareness state + if (provider.awareness.getLocalState() !== null) { + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [ + provider.doc.clientID, + ]) + ); + websocket.send(encoding.toUint8Array(encoderAwarenessState)); + } + }; + + provider.emit('status', [ + { + status: 'connecting', + }, + ]); + } +}; + +/** + * @param {WebsocketProvider} provider + * @param {ArrayBuffer} buf + */ +const broadcastMessage = (provider, buf) => { + if (provider.wsconnected) { + /** @type {WebSocket} */ (provider.ws).send(buf); + } + if (provider.bcconnected) { + bc.publish(provider.bcChannel, buf, provider); + } +}; + +/** + * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. + * The document name is attached to the provided url. I.e. the following example + * creates a websocket connection to http://localhost:1234/my-document-name + * + * @example + * import * as Y from 'yjs' + * import { WebsocketProvider } from 'y-websocket' + * const doc = new Y.Doc() + * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) + * + * @extends {Observable} + */ +export class WebsocketProvider extends Observable { + /** + * @param {string} serverUrl + * @param {string} roomname + * @param {Y.Doc} doc + * @param {object} [opts] + * @param {boolean} [opts.connect] + * @param {awarenessProtocol.Awareness} [opts.awareness] + * @param {Object} [opts.params] + * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill + * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds + * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) + * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication + */ + constructor( + serverUrl, + roomname, + doc, + { + connect = true, + awareness = new awarenessProtocol.Awareness(doc), + params = {}, + WebSocketPolyfill = WebSocket, + resyncInterval = -1, + maxBackoffTime = 2500, + disableBc = false, + } = {} + ) { + super(); + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + const encodedParams = url.encodeQueryParams(params); + this.maxBackoffTime = maxBackoffTime; + this.bcChannel = serverUrl + '/' + roomname; + this.url = + serverUrl + + '/' + + roomname + + (encodedParams.length === 0 ? '' : '?' + encodedParams); + this.roomname = roomname; + this.doc = doc; + this._WS = WebSocketPolyfill; + this.awareness = awareness; + this.wsconnected = false; + this.wsconnecting = false; + this.bcconnected = false; + this.disableBc = disableBc; + this.wsUnsuccessfulReconnects = 0; + this.messageHandlers = messageHandlers.slice(); + /** + * @type {boolean} + */ + this._synced = false; + /** + * @type {WebSocket?} + */ + this.ws = null; + this.wsLastMessageReceived = 0; + /** + * Whether to connect to other peers or not + * @type {boolean} + */ + this.shouldConnect = connect; + + /** + * @type {number} + */ + this._resyncInterval = 0; + if (resyncInterval > 0) { + this._resyncInterval = /** @type {any} */ ( + setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + // resend sync step 1 + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, doc); + this.ws.send(encoding.toUint8Array(encoder)); + } + }, resyncInterval) + ); + } + + /** + * @param {ArrayBuffer} data + * @param {any} origin + */ + this._bcSubscriber = (data, origin) => { + if (origin !== this) { + const encoder = readMessage(this, new Uint8Array(data), false); + if (encoding.length(encoder) > 1) { + bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this); + } + } + }; + /** + * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) + * @param {Uint8Array} update + * @param {any} origin + */ + this._updateHandler = (update, origin) => { + if (origin !== this) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeUpdate(encoder, update); + broadcastMessage(this, encoding.toUint8Array(encoder)); + } + }; + this.doc.on('update', this._updateHandler); + /** + * @param {any} changed + * @param {any} _origin + */ + this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => { + const changedClients = added.concat(updated).concat(removed); + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients) + ); + broadcastMessage(this, encoding.toUint8Array(encoder)); + }; + this._unloadHandler = () => { + awarenessProtocol.removeAwarenessStates( + this.awareness, + [doc.clientID], + 'window unload' + ); + }; + if (typeof window !== 'undefined') { + window.addEventListener('unload', this._unloadHandler); + } else if (typeof process !== 'undefined') { + process.on('exit', this._unloadHandler); + } + awareness.on('update', this._awarenessUpdateHandler); + this._checkInterval = /** @type {any} */ ( + setInterval(() => { + if ( + this.wsconnected && + messageReconnectTimeout < + time.getUnixTime() - this.wsLastMessageReceived + ) { + // no message received in a long time - not even your own awareness + // updates (which are updated every 15 seconds) + /** @type {WebSocket} */ (this.ws).close(); + } + }, messageReconnectTimeout / 10) + ); + if (connect) { + this.connect(); + } + } + + /** + * @type {boolean} + */ + get synced() { + return this._synced; + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state; + this.emit('synced', [state]); + this.emit('sync', [state]); + } + } + + destroy() { + if (this._resyncInterval !== 0) { + clearInterval(this._resyncInterval); + } + clearInterval(this._checkInterval); + this.disconnect(); + if (typeof window !== 'undefined') { + window.removeEventListener('unload', this._unloadHandler); + } else if (typeof process !== 'undefined') { + process.off('exit', this._unloadHandler); + } + this.awareness.off('update', this._awarenessUpdateHandler); + this.doc.off('update', this._updateHandler); + super.destroy(); + } + + connectBc() { + if (this.disableBc) { + return; + } + if (!this.bcconnected) { + bc.subscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = true; + } + // send sync step1 to bc + // write sync step 1 + const encoderSync = encoding.createEncoder(); + encoding.writeVarUint(encoderSync, messageSync); + syncProtocol.writeSyncStep1(encoderSync, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this); + // broadcast local state + const encoderState = encoding.createEncoder(); + encoding.writeVarUint(encoderState, messageSync); + syncProtocol.writeSyncStep2(encoderState, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this); + // write queryAwareness + const encoderAwarenessQuery = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); + bc.publish( + this.bcChannel, + encoding.toUint8Array(encoderAwarenessQuery), + this + ); + // broadcast local awareness state + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, [ + this.doc.clientID, + ]) + ); + bc.publish( + this.bcChannel, + encoding.toUint8Array(encoderAwarenessState), + this + ); + } + + disconnectBc() { + // broadcast message with local awareness state set to null (indicating disconnect) + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate( + this.awareness, + [this.doc.clientID], + new Map() + ) + ); + broadcastMessage(this, encoding.toUint8Array(encoder)); + if (this.bcconnected) { + bc.unsubscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = false; + } + } + + disconnect() { + this.shouldConnect = false; + this.disconnectBc(); + if (this.ws !== null) { + this.ws.close(); + } + } + + connect() { + this.shouldConnect = true; + if (!this.wsconnected && this.ws === null) { + setupWS(this); + this.connectBc(); + } + } +} From 6e8f33dc2c5c1c6c26bf0d45613ac340e06562bd Mon Sep 17 00:00:00 2001 From: x1a0t <405028157@qq.com> Date: Fri, 6 Jan 2023 18:16:58 +0800 Subject: [PATCH 16/32] feat: update landing page --- .../pages/workspace/[workspaceId]/[pageId].tsx | 17 +++++++++++------ .../templates/Welcome-to-AFFiNE-Alpha-v2.0.md | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx index 998ec29941..05c2c17eba 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/[pageId].tsx @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { - useRef, - useEffect, - useState, - ReactElement, PropsWithChildren, + ReactElement, + useEffect, + useRef, + useState, } from 'react'; import { styled } from '@/styles'; import { EditorHeader } from '@/components/header'; @@ -16,6 +16,7 @@ import type { NextPageWithLayout } from '../..//_app'; import WorkspaceLayout from '@/components/workspace-layout'; import { useRouter } from 'next/router'; import { usePageHelper } from '@/hooks/use-page-helper'; + const StyledEditorContainer = styled('div')(() => { return { height: 'calc(100vh - 60px)', @@ -63,8 +64,12 @@ const Page: NextPageWithLayout = () => { currentPage!.addBlock({ flavour: 'affine:frame' }, pageId); // If this is a first page in workspace, init an introduction markdown if (isFirstPage) { - editor.clipboard.importMarkdown(exampleMarkdown, `${frameId}`); - currentWorkspace!.setPageMeta(currentPage!.id, { title }); + editor.clipboard + .importMarkdown(exampleMarkdown, `${frameId}`) + .then(() => { + currentWorkspace!.setPageMeta(currentPage!.id, { title }); + currentPage!.resetHistory(); + }); } currentPage!.resetHistory(); } diff --git a/packages/app/src/templates/Welcome-to-AFFiNE-Alpha-v2.0.md b/packages/app/src/templates/Welcome-to-AFFiNE-Alpha-v2.0.md index e0fc661340..6495135903 100644 --- a/packages/app/src/templates/Welcome-to-AFFiNE-Alpha-v2.0.md +++ b/packages/app/src/templates/Welcome-to-AFFiNE-Alpha-v2.0.md @@ -10,6 +10,11 @@ Let us know what you think of this latest version. 2. More complete Markdown support and improved keyboard shortcuts; 3. New features such as dark mode; Switch between view styles using the ☀ and 🌙. 4. Clean and modern UI/UX design. +5. You can self-host locally with Docker. + +```basic +docker run -d -v [YOUR_PATH]:/app/data -p 3000:3000 ghcr.io/toeverything/affine-self-hosted:alpha-abbey-wood +``` **Looking for Markdown syntax or keyboard shortcuts?** @@ -23,15 +28,21 @@ Let us know what you think of this latest version. - Copy and paste **images** into your pages, resize them and add captions - Add horizontal line dividers to your text with `---` and `***` - Changes are saved **locally**, but we still recommend you export your data to avoid data loss -- Insert code blocks with syntax highlighting support using ``` +- Insert code blocks with syntax highlighting support using ````` ### Playground: [] Try a horizontal line: `---` -[] What about a code block? ``` +[] What about a code block? ````` -        console.log('Hello world'); +```javascript +console.log('Hello world'); +``` + +[] Can you resize this image? + +![](https://cdn.affine.pro/694fdbab78e0da3ed7922eba7d506dcf12f57308e1904dd694f53eb2.jpg) **How about page management?** From f99bf74499ce6a1e2c1bdddd159ce5b67daf5fe7 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 6 Jan 2023 22:03:43 +0800 Subject: [PATCH 17/32] feat: hide selector --- .../app/src/components/workspace-slider-bar/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/workspace-slider-bar/index.tsx b/packages/app/src/components/workspace-slider-bar/index.tsx index 4954b5cd31..61e81cb3ed 100644 --- a/packages/app/src/components/workspace-slider-bar/index.tsx +++ b/packages/app/src/components/workspace-slider-bar/index.tsx @@ -4,14 +4,14 @@ import { StyledArrowButton, StyledLink, StyledListItem, - StyledListItemForWorkspace, + // StyledListItemForWorkspace, StyledNewPageButton, StyledSliderBar, StyledSliderBarWrapper, StyledSubListItem, } from './style'; import { Arrow } from './icons'; -import { WorkspaceSelector } from './WorkspaceSelector'; +// import { WorkspaceSelector } from './WorkspaceSelector'; import Collapse from '@mui/material/Collapse'; import { ArrowDownIcon, @@ -109,9 +109,9 @@ export const WorkSpaceSliderBar = () => { - + {/* - + */} Date: Sat, 7 Jan 2023 17:01:10 +0800 Subject: [PATCH 18/32] feat: improve scripts --- package.json | 3 +- packages/app/next.config.js | 57 +++++++++++++++++-------------------- packages/app/package.json | 1 - 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 1abcf66a9b..7c9ac38d11 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "MPL-2.0", "scripts": { "dev": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev", - "dev:ac": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev:ac", + "dev:ac": "pnpm --filter=!@affine/app build && NODE_API_SERVER=ac pnpm --filter @affine/app dev", + "dev:local": "pnpm --filter=!@affine/app build && NODE_API_SERVER=local pnpm --filter @affine/app dev", "build": " pnpm --filter=!@affine/app build && pnpm --filter!=@affine/datacenter -r build", "export": "pnpm --filter @affine/app export", "start": "pnpm --filter @affine/app start", diff --git a/packages/app/next.config.js b/packages/app/next.config.js index 10da796485..df91a70108 100644 --- a/packages/app/next.config.js +++ b/packages/app/next.config.js @@ -9,6 +9,27 @@ const EDITOR_VERSION = enableDebugLocal ? 'local-version' : dependencies['@blocksuite/editor']; +const profileTarget = { + ac: '100.85.73.88:12001', + dev: '100.85.73.88:12001', + local: '127.0.0.1:3000', +}; + +const getRedirectConfig = profile => { + const target = profileTarget[profile || 'dev'] || profileTarget['dev']; + + return [ + [ + { source: '/api/:path*', destination: `http://${target}/api/:path*` }, + { + source: '/collaboration/:path*', + destination: `http://${target}/collaboration/:path*`, + }, + ], + target, + ]; +}; + /** @type {import('next').NextConfig} */ const nextConfig = { productionBrowserSourceMaps: true, @@ -36,38 +57,12 @@ const nextConfig = { images: { unoptimized: true, }, - // XXX not test yet rewrites: async () => { - if (process.env.NODE_API_SERVER === 'ac') { - let destinationAC = 'http://100.85.73.88:12001/api/:path*'; - printer.info('API request proxy to [AC Server] ' + destinationAC); - return [ - { - source: '/api/:path*', - destination: destinationAC, - }, - { - source: '/collaboration/:path*', - destination: 'http://localhost:3000/collaboration/:path*', - }, - ]; - } else { - let destinationStandard = 'http://100.77.180.48:11001/api/:path*'; - printer.info( - 'API request proxy to [Standard Server] ' + destinationStandard - ); - - return [ - { - source: '/api/:path*', - destination: destinationStandard, - }, - { - source: '/collaboration/:path*', - destination: 'http://localhost:3000/collaboration/:path*', - }, - ]; - } + const [profile, desc] = getRedirectConfig(process.env.NODE_API_SERVER); + printer.info( + `API request proxy to [${process.env.NODE_API_SERVER} Server]: ` + desc + ); + return profile; }, basePath: process.env.BASE_PATH, }; diff --git a/packages/app/package.json b/packages/app/package.json index 9dac686ae9..3ba74e2712 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -3,7 +3,6 @@ "version": "0.3.1", "scripts": { "dev": "next dev -p 8080", - "dev:ac": "NODE_API_SERVER=ac next dev -p 8080", "build": "next build", "export": "next export", "start": "next start", From 6518aaccc28f6507fe267612414874e0c03e572f Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sat, 7 Jan 2023 21:44:10 +0800 Subject: [PATCH 19/32] feat: data center change --- package.json | 3 +- packages/data-center/src/apis/business.ts | 361 ++++++------- packages/data-center/src/apis/token.ts | 7 +- packages/data-center/src/datacenter.bk.ts | 234 ++++++++ packages/data-center/src/datacenter.ts | 413 +++++++------- packages/data-center/src/index.ts | 6 +- .../data-center/src/provider/affine/affine.ts | 238 ++++++++ .../data-center/src/provider/affine/index.ts | 176 +----- packages/data-center/src/provider/base.ts | 163 ++++-- packages/data-center/src/provider/index.ts | 23 +- .../data-center/src/provider/indexeddb.ts | 203 +++++++ .../data-center/src/provider/local/index.ts | 74 +-- .../data-center/src/provider/local/local.ts | 81 +++ .../src/provider_bk/affine/index.ts | 175 ++++++ .../src/provider_bk/affine/sync.js | 508 ++++++++++++++++++ packages/data-center/src/provider_bk/base.ts | 79 +++ .../src/provider_bk/baseProvider.ts | 0 packages/data-center/src/provider_bk/index.ts | 22 + .../src/provider_bk/local/index.ts | 73 +++ .../local/indexeddb.ts | 0 .../data-center/src/{style => types}/index.ts | 9 +- .../data-center/src/workspaces/workspaces.ts | 176 +++--- packages/data-center/tests/local/init.spec.ts | 60 +-- .../data-center/tests/local/search.spec.ts | 27 +- .../data-center/tests/local/workspace.spec.ts | 82 ++- packages/data-center/tsconfig.json | 2 +- 26 files changed, 2302 insertions(+), 893 deletions(-) create mode 100644 packages/data-center/src/datacenter.bk.ts create mode 100644 packages/data-center/src/provider/affine/affine.ts create mode 100644 packages/data-center/src/provider/indexeddb.ts create mode 100644 packages/data-center/src/provider/local/local.ts create mode 100644 packages/data-center/src/provider_bk/affine/index.ts create mode 100644 packages/data-center/src/provider_bk/affine/sync.js create mode 100644 packages/data-center/src/provider_bk/base.ts create mode 100644 packages/data-center/src/provider_bk/baseProvider.ts create mode 100644 packages/data-center/src/provider_bk/index.ts create mode 100644 packages/data-center/src/provider_bk/local/index.ts rename packages/data-center/src/{provider => provider_bk}/local/indexeddb.ts (100%) rename packages/data-center/src/{style => types}/index.ts (75%) diff --git a/package.json b/package.json index 1abcf66a9b..9f4028cb70 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "reportUnusedDisableDirectives": true, "ignorePatterns": [ "src/**/*.test.ts", - "package/**/dist/*" + "package/**/dist/*", + "package/**/sync.js" ] } } diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/apis/business.ts index ababd0e28a..2612fedd9f 100644 --- a/packages/data-center/src/apis/business.ts +++ b/packages/data-center/src/apis/business.ts @@ -1,207 +1,198 @@ import { uuidv4 } from '@blocksuite/store'; -import { getDataCenter } from './../index'; -import { DataCenter } from './../datacenter'; -import { Workspace, WorkspaceMeta, WorkspaceType } from '../style'; -import { token } from './token'; +import { getDataCenter } from 'src'; +import { DataCenter } from 'src/datacenter'; +import { Workspace } from '../types'; -export class Business { - private _dc: DataCenter | undefined; +// export class Business { +// private _dc: DataCenter | undefined; - private async _getDc() { - if (!this._dc) { - this._dc = await getDataCenter(); - } - return this._dc; - } +// private async _getDc() { +// if (!this._dc) { +// this._dc = await getDataCenter(); +// } +// return this._dc; +// } - async createWorkspace( - name: string - ): Promise> { - let id = ''; - let type = WorkspaceType.local; - if (token.isLogin) { - // TODO: add default avatar - const data = await this._dc?.apis.createWorkspace({ name, avatar: '' }); - id = data?.id || ''; - type = WorkspaceType.cloud; - this._dc?.load(id, { providerId: 'affine' }); - } else { - this._dc?.load(uuidv4(), { providerId: 'local' }); - } - const newWorkspaces = (await this.getWorkspaces()).find(w => w.id === id); - return { - id: newWorkspaces?.id || '', - name, - avatar: '', - type, - }; - } +// // async createWorkspace( +// // name: string +// // ): Promise> { +// // let id = ''; +// // let type = WorkspaceType.local; +// // this._dc?.load(uuidv4()); +// // const newWorkspaces = (await this.getWorkspaces()).find(w => w.id === id); +// // return { +// // id: newWorkspaces?.id || '', +// // name, +// // avatar: '', +// // type, +// // }; +// // } - // not think out a good way to update workspace meta - // updateWorkspaceMeta( - // id: string, - // meta: { name?: string; avatar: Partial } - // ) {} +// // not think out a good way to update workspace meta +// // updateWorkspaceMeta( +// // id: string, +// // meta: { name?: string; avatar: Partial } +// // ) {} - async getWorkspaces(focusUpdated?: boolean): Promise { - const dc = await this._getDc(); - if (focusUpdated) { - await dc.workspacesList.refreshWorkspaceList(); - } - return dc.workspacesList.getWorkspaces(); - } +// // async getWorkspaces(focusUpdated?: boolean): Promise { +// // const dc = await this._getDc(); +// // if (focusUpdated) { +// // await dc.workspacesList.refreshWorkspaceList(); +// // } +// // return dc.workspacesList.getWorkspaces(); +// // } - /** - * Get page list by workspace id - * @param {string} id ID of workspace. - */ - getPagesByWorkspaceId(id: string) { - return []; - } +// /** +// * Get page list by workspace id +// * @param {string} id ID of workspace. +// */ +// getPagesByWorkspaceId(id: string) { +// return []; +// } - /** - * Observe the update of the workspace - * @param {function} callback({Workspace[]}). - */ - async onWorkspaceChange(cb: (workspaces: Workspace[]) => void) { - const dc = await this._getDc(); - dc.workspacesList.on('change', cb); - } +// /** +// * Observe the update of the workspace +// * @param {function} callback({Workspace[]}). +// */ +// async onWorkspaceChange(cb: (workspaces: Workspace[]) => void) { +// const dc = await this._getDc(); +// dc.workspacesList.on('change', cb); +// } - async deleteWorkspace(id: string) { - const dc = await this._getDc(); - const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); - if (workspace?.type === WorkspaceType.cloud) { - dc.apis.deleteWorkspace({ id }); - } - dc.delete(id); - } +// async deleteWorkspace(id: string) { +// const dc = await this._getDc(); +// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); +// if (workspace?.type === WorkspaceType.cloud) { +// dc.apis.deleteWorkspace({ id }); +// } +// dc.delete(id); +// } - /** - * The member of the workspace go to leave workspace - * @param {string} id ID of workspace. - */ - async leaveWorkspace(id: string) { - const dc = await this._getDc(); - const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); - if (workspace?.type === WorkspaceType.cloud) { - dc.apis.leaveWorkspace({ id }); - dc.delete(id); - } - } +// /** +// * The member of the workspace go to leave workspace +// * @param {string} id ID of workspace. +// */ +// async leaveWorkspace(id: string) { +// const dc = await this._getDc(); +// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); +// if (workspace?.type === WorkspaceType.cloud) { +// dc.apis.leaveWorkspace({ id }); +// dc.delete(id); +// } +// } - /** - * Let the workspace to be public - * @param {string} id ID of workspace. - * @param {string} isPublish publish flag of workspace. - */ - setWorkspacePublish(id: string, isPublish: boolean): boolean { - return isPublish; - } +// /** +// * Let the workspace to be public +// * @param {string} id ID of workspace. +// * @param {string} isPublish publish flag of workspace. +// */ +// setWorkspacePublish(id: string, isPublish: boolean): boolean { +// return isPublish; +// } - /** - * Get workspace by workspace id - * @param {string} id ID of workspace. - */ - async getWorkspaceById(id: string) { - const dc = await this._getDc(); - const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); - if (workspace?.type === WorkspaceType.cloud) { - return dc.load(id, { providerId: 'affine' }); - } else { - return dc.load(id, { providerId: 'local' }); - } - } +// /** +// * Get workspace by workspace id +// * @param {string} id ID of workspace. +// */ +// async getWorkspaceById(id: string) { +// const dc = await this._getDc(); +// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); +// if (workspace?.type === WorkspaceType.cloud) { +// return dc.load(id, { providerId: 'affine' }); +// } else { +// return dc.load(id, { providerId: 'local' }); +// } +// } - // no time - /** - * Get the members of the workspace - * @param {string} id ID of workspace. - */ - getMembers(id: string): any { - void 0; - } - /** - * Add a new member to the workspace - * @param {string} id ID of workspace. - * @param {string} email new member email. - */ - inviteMember(id: string, email: string) { - void 0; - } +// // no time +// /** +// * Get the members of the workspace +// * @param {string} id ID of workspace. +// */ +// getMembers(id: string): any { +// void 0; +// } +// /** +// * Add a new member to the workspace +// * @param {string} id ID of workspace. +// * @param {string} email new member email. +// */ +// inviteMember(id: string, email: string) { +// void 0; +// } - /** - * remove the new member to the workspace - * @param {string} workspaceId ID of workspace. - * @param {string} memberId ID of member - */ - removeMember(workspaceId: string, memberId: string) { - void 0; - } +// /** +// * remove the new member to the workspace +// * @param {string} workspaceId ID of workspace. +// * @param {string} memberId ID of member +// */ +// removeMember(workspaceId: string, memberId: string) { +// void 0; +// } - /** - * A new member click the invite link, finish to join the workspace - * @param {string} inviteCode token for invitation. - */ - async acceptInvitation(invitingCode: string) { - const dc = await this._getDc(); - dc.apis.acceptInviting({ invitingCode }); - } +// /** +// * A new member click the invite link, finish to join the workspace +// * @param {string} inviteCode token for invitation. +// */ +// async acceptInvitation(invitingCode: string) { +// const dc = await this._getDc(); +// dc.apis.acceptInviting({ invitingCode }); +// } - // check with dark sky - /** - * Get login user info - */ - getUserInfo() { - void 0; - } +// // check with dark sky +// /** +// * Get login user info +// */ +// getUserInfo() { +// void 0; +// } - // TODO check with dark sky - async login() { - const dc = await this._getDc(); - await dc.auth('affine'); - } +// // TODO check with dark sky +// async login() { +// const dc = await this._getDc(); +// await dc.auth('affine'); +// } - // just has no time - /** - * Logout and clear login session - */ - logout() { - void 0; - } +// // just has no time +// /** +// * Logout and clear login session +// */ +// logout() { +// void 0; +// } - // need discuss - /** - * Create a connection between local and cloud, sync cloud data to local - * @param {string} id ID of workspace. - * @param {string} id type of workspace. - */ - // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} +// // need discuss +// /** +// * Create a connection between local and cloud, sync cloud data to local +// * @param {string} id ID of workspace. +// * @param {string} id type of workspace. +// */ +// // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} - // need discuss - /** - * Select a file to import the workspace - * @param {File} file file of workspace. - */ - importWorkspace(file: File) { - void 0; - } +// // need discuss +// /** +// * Select a file to import the workspace +// * @param {File} file file of workspace. +// */ +// importWorkspace(file: File) { +// void 0; +// } - // need discuss may be not in apis - // /** - // * Generate a file ,and export it to local file system - // * @param {string} id ID of workspace. - // */ - exportWorkspace(id: string) { - void 0; - } +// // need discuss may be not in apis +// // /** +// // * Generate a file ,and export it to local file system +// // * @param {string} id ID of workspace. +// // */ +// exportWorkspace(id: string) { +// void 0; +// } - // need discuss - // /** - // * Enable workspace cloud flag - // * @param {string} id ID of workspace. - // */ - enableWorkspaceCloud(id: string) { - void 0; - } -} +// // need discuss +// // /** +// // * Enable workspace cloud flag +// // * @param {string} id ID of workspace. +// // */ +// enableWorkspaceCloud(id: string) { +// void 0; +// } +// } diff --git a/packages/data-center/src/apis/token.ts b/packages/data-center/src/apis/token.ts index c509da0f33..3bd1dd65aa 100644 --- a/packages/data-center/src/apis/token.ts +++ b/packages/data-center/src/apis/token.ts @@ -168,14 +168,15 @@ export const getAuthorizer = () => { const signInWithGoogle = async () => { const idToken = await getToken(); + let loginUser: AccessTokenMessage | null = null; if (idToken) { - await token.initToken(idToken); + loginUser = await token.initToken(idToken); } else { const user = await signInWithPopup(firebaseAuth, googleAuthProvider); const idToken = await user.user.getIdToken(); - await token.initToken(idToken); + loginUser = await token.initToken(idToken); } - return firebaseAuth.currentUser; + return loginUser; }; const onAuthStateChanged = (callback: (user: User | null) => void) => { diff --git a/packages/data-center/src/datacenter.bk.ts b/packages/data-center/src/datacenter.bk.ts new file mode 100644 index 0000000000..dd01e7785f --- /dev/null +++ b/packages/data-center/src/datacenter.bk.ts @@ -0,0 +1,234 @@ +import assert from 'assert'; +import { BlockSchema } from '@blocksuite/blocks/models'; +import { Workspace, Signal } from '@blocksuite/store'; + +import { getLogger } from './index.js'; +import { getApis, Apis } from './apis/index.js'; +import { AffineProvider, BaseProvider } from './provider/index.js'; +import { LocalProvider } from './provider/index.js'; +import { getKVConfigure } from './store.js'; +import { Workspaces } from './workspaces'; + +// load workspace's config +type LoadConfig = { + // use witch provider load data + providerId?: string; + // provider config + config?: Record; +}; + +export type DataCenterSignals = DataCenter['signals']; +type WorkspaceItem = { + // provider id + provider: string; + // data exists locally + locally: boolean; +}; +type WorkspaceLoadEvent = WorkspaceItem & { + workspace: string; +}; + +export class DataCenter { + private readonly _apis: Apis; + private readonly _providers = new Map(); + private readonly _workspaces = new Map>(); + private readonly _config; + private readonly _logger; + public readonly workspacesList = new Workspaces(this); + + readonly signals = { + listAdd: new Signal(), + listRemove: new Signal(), + }; + + static async init(debug: boolean): Promise { + const dc = new DataCenter(debug); + dc.addProvider(AffineProvider); + dc.addProvider(LocalProvider); + + return dc; + } + + private constructor(debug: boolean) { + this._apis = getApis(); + this._config = getKVConfigure('sys'); + this._logger = getLogger('dc'); + this._logger.enabled = debug; + + this.signals.listAdd.on(e => { + this._config.set(`list:${e.workspace}`, { + provider: e.provider, + locally: e.locally, + }); + }); + this.signals.listRemove.on(workspace => { + this._config.delete(`list:${workspace}`); + }); + this.workspacesList.init(); + } + + get apis(): Readonly { + return this._apis; + } + + private addProvider(provider: typeof BaseProvider) { + this._providers.set(provider.id, provider); + } + + private async _getProvider( + id: string, + providerId = 'local' + ): Promise { + const providerKey = `${id}:provider`; + if (this._providers.has(providerId)) { + await this._config.set(providerKey, providerId); + return providerId; + } else { + const providerValue = await this._config.get(providerKey); + if (providerValue) return providerValue; + } + throw Error(`Provider ${providerId} not found`); + } + + private async _getWorkspace( + id: string, + params: LoadConfig + ): Promise { + this._logger(`Init workspace ${id} with ${params.providerId}`); + + const providerId = await this._getProvider(id, params.providerId); + + // init workspace & register block schema + const workspace = new Workspace({ room: id }).register(BlockSchema); + + const Provider = this._providers.get(providerId); + assert(Provider); + + // initial configurator + const config = getKVConfigure(`workspace:${id}`); + // set workspace configs + const values = Object.entries(params.config || {}); + if (values.length) await config.setMany(values); + + // init data by provider + const provider = new Provider(); + await provider.init({ + apis: this._apis, + config, + debug: this._logger.enabled, + logger: this._logger.extend(`${Provider.id}:${id}`), + signals: this.signals, + workspace, + }); + await provider.initData(); + this._logger(`Workspace ${id} loaded`); + + return provider; + } + + async auth(providerId: string, globalConfig?: Record) { + const Provider = this._providers.get(providerId); + if (Provider) { + // initial configurator + const config = getKVConfigure(`provider:${providerId}`); + // set workspace configs + const values = Object.entries(globalConfig || {}); + if (values.length) await config.setMany(values); + + const logger = this._logger.extend(`auth:${providerId}`); + logger.enabled = this._logger.enabled; + await Provider.auth(config, logger, this.signals); + } + } + + /** + * load workspace data to memory + * @param workspaceId workspace id + * @param config.providerId provider id + * @param config.config provider config + * @returns Workspace instance + */ + async load( + workspaceId: string, + params: LoadConfig = {} + ): Promise { + if (workspaceId) { + if (!this._workspaces.has(workspaceId)) { + this._workspaces.set( + workspaceId, + this._getWorkspace(workspaceId, params) + ); + } + const workspace = this._workspaces.get(workspaceId); + assert(workspace); + return workspace.then(w => w.workspace); + } + return null; + } + + /** + * destroy workspace's instance in memory + * @param workspaceId workspace id + */ + async destroy(workspaceId: string) { + const provider = await this._workspaces.get(workspaceId); + if (provider) { + this._workspaces.delete(workspaceId); + await provider.destroy(); + } + } + + /** + * reload new workspace instance to memory to refresh config + * @param workspaceId workspace id + * @param config.providerId provider id + * @param config.config provider config + * @returns Workspace instance + */ + async reload( + workspaceId: string, + config: LoadConfig = {} + ): Promise { + await this.destroy(workspaceId); + return this.load(workspaceId, config); + } + + /** + * get workspace list,return a map of workspace id and data state + * data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist + */ + async list(): Promise>> { + const listString = 'list:'; + const entries: [string, WorkspaceItem][] = await this._config.entries(); + return entries.reduce((acc, [k, i]) => { + if (k.startsWith(listString)) { + const key = k.slice(listString.length); + acc[key] = acc[key] || {}; + acc[key][i.provider] = i.locally; + } + return acc; + }, {} as Record>); + } + + /** + * delete local workspace's data + * @param workspaceId workspace id + */ + async delete(workspaceId: string) { + await this._config.delete(`${workspaceId}:provider`); + const provider = await this._workspaces.get(workspaceId); + if (provider) { + this._workspaces.delete(workspaceId); + // clear workspace data implement by provider + await provider.clear(); + } + } + + /** + * clear all local workspace's data + */ + async clear() { + const workspaces = await this.list(); + await Promise.all(Object.keys(workspaces).map(id => this.delete(id))); + } +} diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index dd01e7785f..d782c98bc0 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -1,234 +1,275 @@ -import assert from 'assert'; -import { BlockSchema } from '@blocksuite/blocks/models'; -import { Workspace, Signal } from '@blocksuite/store'; - -import { getLogger } from './index.js'; -import { getApis, Apis } from './apis/index.js'; -import { AffineProvider, BaseProvider } from './provider/index.js'; -import { LocalProvider } from './provider/index.js'; -import { getKVConfigure } from './store.js'; import { Workspaces } from './workspaces'; +import { Workspace } from '@blocksuite/store'; +import { BaseProvider } from './provider/base'; +import { LocalProvider } from './provider/local/local'; +import { AffineProvider } from './provider'; +import { Workspace as WS, WorkspaceMeta } from 'src/types'; +import assert from 'assert'; +import { getLogger } from 'src'; +import { BlockSchema } from '@blocksuite/blocks/models'; +import { applyUpdate, encodeStateAsUpdate } from 'yjs'; -// load workspace's config -type LoadConfig = { - // use witch provider load data - providerId?: string; - // provider config - config?: Record; -}; - -export type DataCenterSignals = DataCenter['signals']; -type WorkspaceItem = { - // provider id - provider: string; - // data exists locally - locally: boolean; -}; -type WorkspaceLoadEvent = WorkspaceItem & { - workspace: string; -}; - +/** + * @class DataCenter + * @classdesc DataCenter is a data center, it can manage different providers for business + */ export class DataCenter { - private readonly _apis: Apis; - private readonly _providers = new Map(); - private readonly _workspaces = new Map>(); - private readonly _config; - private readonly _logger; - public readonly workspacesList = new Workspaces(this); + private readonly workspaces = new Workspaces(this); + private currentWorkspace: Workspace | null = null; + private readonly _logger = getLogger('dc'); + providerMap: Map = new Map(); - readonly signals = { - listAdd: new Signal(), - listRemove: new Signal(), - }; + constructor(debug: boolean) { + this._logger.enabled = debug; + } static async init(debug: boolean): Promise { const dc = new DataCenter(debug); - dc.addProvider(AffineProvider); - dc.addProvider(LocalProvider); + // TODO: switch different provider + dc.registerProvider(new LocalProvider()); + dc.registerProvider(new AffineProvider()); + dc.workspaces.init(); return dc; } - private constructor(debug: boolean) { - this._apis = getApis(); - this._config = getKVConfigure('sys'); - this._logger = getLogger('dc'); - this._logger.enabled = debug; - - this.signals.listAdd.on(e => { - this._config.set(`list:${e.workspace}`, { - provider: e.provider, - locally: e.locally, - }); - }); - this.signals.listRemove.on(workspace => { - this._config.delete(`list:${workspace}`); - }); - this.workspacesList.init(); + registerProvider(provider: BaseProvider) { + // inject data in provider + provider.inject({ logger: this._logger, workspaces: this.workspaces }); + provider.init(); + this.providerMap.set(provider.id, provider); } - get apis(): Readonly { - return this._apis; + get providers() { + return Array.from(this.providerMap.values()); } - private addProvider(provider: typeof BaseProvider) { - this._providers.set(provider.id, provider); + /** + * create new workspace , new workspace is a local workspace + * @param {string} name workspace name + * @returns + */ + public async createWorkspace(name: string) { + const workspaceInfo = this.workspaces.addLocalWorkspace(name); + return workspaceInfo; } - private async _getProvider( - id: string, - providerId = 'local' - ): Promise { - const providerKey = `${id}:provider`; - if (this._providers.has(providerId)) { - await this._config.set(providerKey, providerId); - return providerId; - } else { - const providerValue = await this._config.get(providerKey); - if (providerValue) return providerValue; - } - throw Error(`Provider ${providerId} not found`); - } - - private async _getWorkspace( - id: string, - params: LoadConfig - ): Promise { - this._logger(`Init workspace ${id} with ${params.providerId}`); - - const providerId = await this._getProvider(id, params.providerId); - - // init workspace & register block schema - const workspace = new Workspace({ room: id }).register(BlockSchema); - - const Provider = this._providers.get(providerId); - assert(Provider); - - // initial configurator - const config = getKVConfigure(`workspace:${id}`); - // set workspace configs - const values = Object.entries(params.config || {}); - if (values.length) await config.setMany(values); - - // init data by provider - const provider = new Provider(); - await provider.init({ - apis: this._apis, - config, - debug: this._logger.enabled, - logger: this._logger.extend(`${Provider.id}:${id}`), - signals: this.signals, - workspace, - }); - await provider.initData(); - this._logger(`Workspace ${id} loaded`); - - return provider; - } - - async auth(providerId: string, globalConfig?: Record) { - const Provider = this._providers.get(providerId); - if (Provider) { - // initial configurator - const config = getKVConfigure(`provider:${providerId}`); - // set workspace configs - const values = Object.entries(globalConfig || {}); - if (values.length) await config.setMany(values); - - const logger = this._logger.extend(`auth:${providerId}`); - logger.enabled = this._logger.enabled; - await Provider.auth(config, logger, this.signals); + /** + * delete workspace by id + * @param {string} workspaceId workspace id + */ + public async deleteWorkspace(workspaceId: string) { + const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); + if (provider && this.workspaces.hasWorkspace(workspaceId)) { + await provider.delete(workspaceId); + // may be refresh all workspaces + this.workspaces.delete(workspaceId); } } /** - * load workspace data to memory - * @param workspaceId workspace id - * @param config.providerId provider id - * @param config.config provider config - * @returns Workspace instance + * get a new workspace only has room id + * @param {string} workspaceId workspace id */ - async load( - workspaceId: string, - params: LoadConfig = {} - ): Promise { - if (workspaceId) { - if (!this._workspaces.has(workspaceId)) { - this._workspaces.set( - workspaceId, - this._getWorkspace(workspaceId, params) - ); - } - const workspace = this._workspaces.get(workspaceId); - assert(workspace); - return workspace.then(w => w.workspace); - } - return null; + private _getWorkspace(workspaceId: string) { + return new Workspace({ + room: workspaceId, + }).register(BlockSchema); } /** - * destroy workspace's instance in memory - * @param workspaceId workspace id + * login to all providers, it will default run all auth , + * maybe need a params to control which provider to auth */ - async destroy(workspaceId: string) { - const provider = await this._workspaces.get(workspaceId); + public async login() { + this.providers.forEach(p => { + // TODO: may be add params of auth + p.auth(); + }); + } + + /** + * logout from all providers + */ + public async logout() { + this.providers.forEach(p => { + p.logout(); + }); + } + + /** + * load workspace instance by id + * @param {string} workspaceId workspace id + */ + public async loadWorkspace(workspaceId: string) { + const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + assert(workspaceInfo, 'Workspace not found'); + const currentProvider = this.providerMap.get(workspaceInfo.provider); + if (currentProvider) { + currentProvider.close(workspaceId); + } + const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - this._workspaces.delete(workspaceId); - await provider.destroy(); + this._logger(`Loading ${provider} workspace: `, workspaceId); + const workspace = this._getWorkspace(workspaceId); + this.currentWorkspace = await provider.warpWorkspace(workspace); + return this.currentWorkspace; } + return workspaceInfo; } /** - * reload new workspace instance to memory to refresh config - * @param workspaceId workspace id - * @param config.providerId provider id - * @param config.config provider config - * @returns Workspace instance + * get user info by provider id + * @param {string} providerId the provider name of workspace */ - async reload( - workspaceId: string, - config: LoadConfig = {} - ): Promise { - await this.destroy(workspaceId); - return this.load(workspaceId, config); + public async getUserInfo(providerId = 'affine') { + // XXX: maybe return all user info + const provider = this.providerMap.get(providerId); + assert(provider, 'Provider not found'); + return provider.getUserInfo(); } /** - * get workspace list,return a map of workspace id and data state - * data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist + * listen workspaces list change */ - async list(): Promise>> { - const listString = 'list:'; - const entries: [string, WorkspaceItem][] = await this._config.entries(); - return entries.reduce((acc, [k, i]) => { - if (k.startsWith(listString)) { - const key = k.slice(listString.length); - acc[key] = acc[key] || {}; - acc[key][i.provider] = i.locally; - } - return acc; - }, {} as Record>); + public async onWorkspacesChange(callback: (workspaces: WS[]) => void) { + this.workspaces.onWorkspacesChange(callback); } /** - * delete local workspace's data - * @param workspaceId workspace id + * change workspaces meta + * @param {WorkspaceMeta} workspaceMeta workspace meta + * @param {Workspace} workspace workspace instance */ - async delete(workspaceId: string) { - await this._config.delete(`${workspaceId}:provider`); - const provider = await this._workspaces.get(workspaceId); + public async resetWorkspaceMeta( + { name, avatar }: WorkspaceMeta, + workspace?: Workspace + ) { + const w = workspace ?? this.currentWorkspace; + assert(w?.room, 'No workspace to set meta'); + const update: Partial = {}; + if (name) { + w.doc.meta.setName(name); + update.name = name; + } + if (avatar) { + w.doc.meta.setAvatar(avatar); + update.avatar = avatar; + } + this.workspaces.updateWorkspaceMeta(w.room, update); + } + + /** + * + * leave workspace by id + * @param id workspace id + */ + public async leaveWorkspace(workspaceId: string) { + const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - this._workspaces.delete(workspaceId); - // clear workspace data implement by provider - await provider.clear(); + provider.close(workspaceId); + provider.leave(workspaceId); + } + } + + public async setWorkspacePublish() { + // TODO: set workspace publish + } + + public async inviteMember(id: string, email: string) { + const workspaceInfo = this.workspaces.getWorkspace(id); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); + if (provider) { + provider.invite(id, email); } } /** - * clear all local workspace's data + * remove the new member to the workspace + * @param {number} permissionId permission id */ - async clear() { - const workspaces = await this.list(); - await Promise.all(Object.keys(workspaces).map(id => this.delete(id))); + public async removeMember(workspaceId: string, permissionId: number) { + const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); + if (provider) { + provider.removeMember(permissionId); + } + } + + /** + * + * do close current workspace + */ + public async closeCurrentWorkspace() { + assert(this.currentWorkspace?.room, 'No workspace to close'); + const currentWorkspace = this.workspaces.getWorkspace( + this.currentWorkspace.room + ); + assert(currentWorkspace, 'Workspace not found'); + const provider = this.providerMap.get(currentWorkspace.provider); + assert(provider, 'Provider not found'); + provider.close(currentWorkspace.id); + } + + private async _transWorkspaceProvider( + workspace: Workspace, + provider: string + ) { + assert(workspace.room, 'No workspace'); + const workspaceInfo = this.workspaces.getWorkspace(workspace.room); + assert(workspaceInfo, 'Workspace not found'); + if (workspaceInfo.provider === provider) { + this._logger('Workspace provider is same'); + return; + } + const currentProvider = this.providerMap.get(workspaceInfo.provider); + assert(currentProvider, 'Provider not found'); + const newProvider = this.providerMap.get(provider); + assert(newProvider, 'AffineProvider is not registered'); + const newWorkspace = await newProvider.createWorkspace({ + name: workspaceInfo.name, + avatar: workspaceInfo.avatar, + }); + assert(newWorkspace, 'Create workspace failed'); + // load doc to another workspace + applyUpdate(newWorkspace.doc, encodeStateAsUpdate(workspace.doc)); + assert(newWorkspace, 'Create workspace failed'); + currentProvider.delete(workspace.room); + this.workspaces.refreshWorkspaces(); + } + + /** + * Enable workspace cloud flag + * @param {string} id ID of workspace. + */ + public async enableWorkspaceCloud( + workspace: Workspace | null = this.currentWorkspace + ) { + assert(workspace?.room, 'No workspace to enable cloud'); + return await this._transWorkspaceProvider(workspace, 'affine'); + } + + /** + * Select a file to import the workspace + * @param {File} file file of workspace. + */ + public async importWorkspace(file: File) { + file; + return; + } + + /** + * Generate a file ,and export it to local file system + * @param {string} id ID of workspace. + */ + public async exportWorkspace(id: string) { + id; + return; } } diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts index 0ca8f1cfee..fb7e2f0445 100644 --- a/packages/data-center/src/index.ts +++ b/packages/data-center/src/index.ts @@ -1,5 +1,5 @@ import debug from 'debug'; -import { DataCenter } from './datacenter.js'; +import { DataCenter } from './dataCenter'; const _initializeDataCenter = () => { let _dataCenterInstance: Promise; @@ -32,5 +32,5 @@ export function getLogger(namespace: string) { return logger; } -export type { AccessTokenMessage, Member, Workspace } from './apis'; -export { WorkspaceType } from './apis/index.js'; +export type { AccessTokenMessage } from './apis'; +export type { Workspace } from './types'; diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts new file mode 100644 index 0000000000..29fcd4683c --- /dev/null +++ b/packages/data-center/src/provider/affine/affine.ts @@ -0,0 +1,238 @@ +import { + getWorkspaces, + getWorkspaceDetail, + WorkspaceDetail, + downloadWorkspace, + deleteWorkspace, + leaveWorkspace, + inviteMember, + removeMember, + createWorkspace, +} from 'src/apis/workspace'; +import { BaseProvider } from '../base'; +import { User, Workspace as WS, WorkspaceMeta } from 'src/types'; +import { Workspace } from '@blocksuite/store'; +import { BlockSchema } from '@blocksuite/blocks/models'; +import { applyUpdate } from 'yjs'; +import { token, Callback } from 'src/apis'; +import { varStorage as storage } from 'lib0/storage'; +import assert from 'assert'; +import { getAuthorizer } from 'src/apis/token'; +import { WebsocketProvider } from 'src/provider/sync'; +import { IndexedDBProvider } from '../indexeddb'; + +export class AffineProvider extends BaseProvider { + public id = 'affine'; + private _workspacesCache: Map = new Map(); + private _onTokenRefresh?: Callback = undefined; + private readonly _authorizer = getAuthorizer(); + private _user: User | undefined = undefined; + private _wsMap: Map = new Map(); + private _idbMap: Map = new Map(); + + constructor() { + super(); + } + + override async init() { + this._onTokenRefresh = () => { + if (token.refresh) { + storage.setItem('token', token.refresh); + } + }; + + token.onChange(this._onTokenRefresh); + + // initial login token + if (token.isExpired) { + try { + const refreshToken = storage.getItem('token'); + await token.refreshToken(refreshToken); + + if (token.refresh) { + storage.set('token', token.refresh); + } + + assert(token.isLogin); + } catch (_) { + // this._logger('Authorization failed, fallback to local mode'); + } + } else { + storage.setItem('token', token.refresh); + } + } + + override async warpWorkspace(workspace: Workspace) { + const { doc, room } = workspace; + assert(room); + this._initWorkspaceDb(workspace); + const updates = await downloadWorkspace(room); + if (updates) { + await new Promise(resolve => { + doc.once('update', resolve); + applyUpdate(doc, new Uint8Array(updates)); + }); + } + const ws = new WebsocketProvider('/', room, doc); + this._wsMap.set(room, ws); + await new Promise((resolve, reject) => { + // TODO: synced will also be triggered on reconnection after losing sync + // There needs to be an event mechanism to emit the synchronization state to the upper layer + assert(ws); + ws.once('synced', () => resolve()); + ws.once('lost-connection', () => resolve()); + ws.once('connection-error', () => reject()); + }); + return workspace; + } + + override async loadWorkspaces() { + if (!token.isLogin) { + return []; + } + const workspacesList = await getWorkspaces(); + const workspaces: WS[] = workspacesList.map(w => { + return { + ...w, + memberCount: 0, + name: '', + provider: 'affine', + }; + }); + const workspaceInstances = workspaces.map(({ id }) => { + const workspace = + this._workspacesCache.get(id) || + new Workspace({ + room: id, + }).register(BlockSchema); + this._workspacesCache.set(id, workspace); + if (workspace) { + return new Promise(resolve => { + downloadWorkspace(id).then(data => { + applyUpdate(workspace.doc, new Uint8Array(data)); + resolve(workspace); + }); + }); + } else { + return Promise.resolve(null); + } + }); + await ( + await Promise.all(workspaceInstances) + ).forEach((workspace, i) => { + if (workspace) { + workspaces[i] = { + ...workspaces[i], + name: workspace.doc.meta.name, + avatar: workspace.doc.meta.avatar, + }; + } + }); + const getDetailList = workspacesList.map(w => { + const { id } = w; + return new Promise<{ id: string; detail: WorkspaceDetail | null }>( + resolve => { + getWorkspaceDetail({ id }).then(data => { + resolve({ id, detail: data || null }); + }); + } + ); + }); + const ownerList = await Promise.all(getDetailList); + (await Promise.all(ownerList)).forEach(detail => { + if (detail) { + const { id, detail: workspaceDetail } = detail; + if (workspaceDetail) { + const { owner, member_count } = workspaceDetail; + const currentWorkspace = workspaces.find(w => w.id === id); + if (currentWorkspace) { + currentWorkspace.owner = { + id: owner.id, + name: owner.name, + avatar: owner.avatar_url, + email: owner.email, + }; + currentWorkspace.memberCount = member_count; + } + } + } + }); + return workspaces; + } + + override async auth() { + const refreshToken = await storage.getItem('token'); + if (refreshToken) { + await token.refreshToken(refreshToken); + if (token.isLogin && !token.isExpired) { + // login success + return; + } + } + const user = await this._authorizer[0]?.(); + assert(user); + this._user = { + id: user.id, + name: user.name, + avatar: user.avatar_url, + email: user.email, + }; + } + + public override async getUserInfo(): Promise { + return this._user; + } + + public override async delete(id: string): Promise { + // TODO delete workspace all local data + await deleteWorkspace({ id }); + } + + public override async clear(): Promise { + // TODO: clear all workspaces source + this._workspacesCache.clear(); + } + + public override async close(id: string) { + const idb = this._idbMap.get(id); + idb?.destroy(); + const ws = this._wsMap.get(id); + ws?.disconnect(); + } + + public override async leave(id: string): Promise { + await leaveWorkspace({ id }); + } + + public override async invite(id: string, email: string): Promise { + return await inviteMember({ id, email }); + } + + public override async removeMember(permissionId: number): Promise { + return await removeMember({ permissionId }); + } + + private async _initWorkspaceDb(workspace: Workspace) { + assert(workspace.room); + let idb = this._idbMap.get(workspace.room); + idb?.destroy(); + idb = new IndexedDBProvider(workspace.room, workspace.doc); + this._idbMap.set(workspace.room, idb); + await idb.whenSynced; + return idb; + } + + public override async createWorkspace( + meta: WorkspaceMeta + ): Promise { + assert(meta.name, 'Workspace name is required'); + meta.avatar ?? (meta.avatar = ''); + const { id } = await createWorkspace(meta as Required); + const nw = new Workspace({ + room: id, + }).register(BlockSchema); + this._initWorkspaceDb(nw); + this._logger('Local data loaded'); + return nw; + } +} diff --git a/packages/data-center/src/provider/affine/index.ts b/packages/data-center/src/provider/affine/index.ts index d2323f4ae1..8f9dc079c0 100644 --- a/packages/data-center/src/provider/affine/index.ts +++ b/packages/data-center/src/provider/affine/index.ts @@ -1,175 +1 @@ -import assert from 'assert'; -import { applyUpdate, Doc } from 'yjs'; - -import type { - ConfigStore, - DataCenterSignals, - InitialParams, - Logger, -} from '../index.js'; -import { token, Callback, getApis } from '../../apis/index.js'; -import { LocalProvider } from '../local/index.js'; - -import { WebsocketProvider } from './sync.js'; -import { IndexedDBProvider } from '../local/indexeddb.js'; - -export class AffineProvider extends LocalProvider { - static id = 'affine'; - private _onTokenRefresh?: Callback = undefined; - private _ws?: WebsocketProvider; - - constructor() { - super(); - } - - async init(params: InitialParams) { - super.init(params); - - this._onTokenRefresh = () => { - if (token.refresh) { - this._config.set('token', token.refresh); - } - }; - assert(this._onTokenRefresh); - - token.onChange(this._onTokenRefresh); - - // initial login token - if (token.isExpired) { - try { - const refreshToken = await this._config.get('token'); - await token.refreshToken(refreshToken); - - if (token.refresh) { - this._config.set('token', token.refresh); - } - - assert(token.isLogin); - } catch (_) { - this._logger('Authorization failed, fallback to local mode'); - } - } else { - this._config.set('token', token.refresh); - } - } - - async destroy() { - if (this._onTokenRefresh) { - token.offChange(this._onTokenRefresh); - } - this._ws?.disconnect(); - } - - async initData() { - const databases = await indexedDB.databases(); - await super.initData( - // set locally to true if exists a same name db - databases - .map(db => db.name) - .filter(v => v) - .includes(this._workspace.room) - ); - - const workspace = this._workspace; - const doc = workspace.doc; - - this._logger(`Login: ${token.isLogin}`); - - if (workspace.room && token.isLogin) { - try { - // init data from cloud - await AffineProvider._initCloudDoc( - workspace.room, - doc, - this._logger, - this._signals - ); - - // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later - this._ws = new WebsocketProvider('/', workspace.room, doc); - await new Promise((resolve, reject) => { - // TODO: synced will also be triggered on reconnection after losing sync - // There needs to be an event mechanism to emit the synchronization state to the upper layer - assert(this._ws); - this._ws.once('synced', () => resolve()); - this._ws.once('lost-connection', () => resolve()); - this._ws.once('connection-error', () => reject()); - }); - this._signals.listAdd.emit({ - workspace: workspace.room, - provider: this.id, - locally: true, - }); - } catch (e) { - this._logger('Failed to init cloud workspace', e); - } - } - - // if after update, the space:meta is empty - // then we need to get map with doc - // just a workaround for yjs - doc.getMap('space:meta'); - } - - private static async _initCloudDoc( - workspace: string, - doc: Doc, - logger: Logger, - signals: DataCenterSignals - ) { - const apis = getApis(); - logger(`Loading ${workspace}...`); - const updates = await apis.downloadWorkspace(workspace); - if (updates) { - await new Promise(resolve => { - doc.once('update', resolve); - applyUpdate(doc, new Uint8Array(updates)); - }); - logger(`Loaded: ${workspace}`); - - // only add to list as online workspace - signals.listAdd.emit({ - workspace, - provider: this.id, - // at this time we always download full workspace - // but after we support sub doc, we can only download metadata - locally: false, - }); - } - } - - static async auth( - config: Readonly>, - logger: Logger, - signals: DataCenterSignals - ) { - const refreshToken = await config.get('token'); - if (refreshToken) { - await token.refreshToken(refreshToken); - if (token.isLogin && !token.isExpired) { - logger('check login success'); - // login success - return; - } - } - - logger('start login'); - // login with google - const apis = getApis(); - assert(apis.signInWithGoogle); - const user = await apis.signInWithGoogle(); - assert(user); - logger(`login success: ${user.displayName}`); - - // TODO: refresh local workspace data - const workspaces = await apis.getWorkspaces(); - await Promise.all( - workspaces.map(async ({ id }) => { - const doc = new Doc(); - const idb = new IndexedDBProvider(id, doc); - await idb.whenSynced; - await this._initCloudDoc(id, doc, logger, signals); - }) - ); - } -} +export * from './affine'; diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index bd8d8a4e03..95c98fe892 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,79 +1,134 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import type { Workspace } from '@blocksuite/store'; - -import type { - Apis, - DataCenterSignals, - Logger, - InitialParams, - ConfigStore, -} from './index'; +import { BlobStorage, Workspace } from '@blocksuite/store'; +import { Logger, User, Workspace as WS, WorkspaceMeta } from 'src/types'; +import { Workspaces } from 'src/workspaces'; export class BaseProvider { - static id = 'base'; - protected _apis!: Readonly; - protected _config!: Readonly; + public readonly id: string = 'base'; + protected _workspaces!: Workspaces; protected _logger!: Logger; - protected _signals!: DataCenterSignals; - protected _workspace!: Workspace; + protected _blobs!: BlobStorage; - constructor() { - // Nothing to do here + public inject({ + logger, + workspaces, + }: { + logger: Logger; + workspaces: Workspaces; + }) { + this._logger = logger; + this._workspaces = workspaces; } - get id(): string { - return (this.constructor as any).id; + /** + * hook after provider registered + */ + public async init() { + return; } - async init(params: InitialParams) { - this._apis = params.apis; - this._config = params.config; - this._logger = params.logger; - this._signals = params.signals; - this._workspace = params.workspace; - this._logger.enabled = params.debug; + /** + * auth provider + */ + public async auth() { + return; } + /** + * logout provider + */ + public async logout() { + return; + } + + /** + * warp workspace with workspace functions + * @param workspace + * @returns + */ + public async warpWorkspace(workspace: Workspace): Promise { + return workspace; + } + + /** + * load workspaces + **/ + public async loadWorkspaces(): Promise { + throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); + } + + /** + * get auth user info + * @returns + */ + public async getUserInfo(): Promise { + return; + } + + async getBlob(id: string): Promise { + return this._blobs.get(id); + } + + async setBlob(blob: Blob): Promise { + return this._blobs.set(blob); + } + + /** + * clear all local data in provider + */ async clear() { - await this.destroy(); - await this._config.clear(); + this._blobs.clear(); } - async destroy() { - // Nothing to do here + /** + * delete workspace include all data + * @param id workspace id + */ + public async delete(id: string): Promise { + id; + return; } - async initData() { - throw Error('Not implemented: initData'); + /** + * leave workspace by workspace id + * @param id workspace id + */ + public async leave(id: string): Promise { + id; + return; } - // should return a blob url - async getBlob(_id: string): Promise { - throw Error('Not implemented: getBlob'); + /** + * close db link and websocket connection and other resources + * @param id workspace id + */ + public async close(id: string) { + id; + return; } - // should return a blob unique id - async setBlob(_blob: Blob): Promise { - throw Error('Not implemented: setBlob'); + /** + * invite workspace member + * @param id workspace id + */ + public async invite(id: string, email: string): Promise { + id; + email; + return; } - get workspace() { - return this._workspace; + /** + * remove workspace member by permission id + * @param permissionId + */ + public async removeMember(permissionId: number): Promise { + permissionId; + return; } - static async auth( - _config: Readonly, - logger: Logger, - _signals: DataCenterSignals - ) { - logger("This provider doesn't require authentication"); - } - - // get workspace list,return a map of workspace id and boolean - // if value is true, it exists locally, otherwise it does not exist locally - static async list( - _config: Readonly - ): Promise | undefined> { - throw Error('Not implemented: list'); + public async createWorkspace( + meta: WorkspaceMeta + ): Promise { + meta; + return; } } diff --git a/packages/data-center/src/provider/index.ts b/packages/data-center/src/provider/index.ts index 5718b7eef0..abfae4ce71 100644 --- a/packages/data-center/src/provider/index.ts +++ b/packages/data-center/src/provider/index.ts @@ -1,22 +1 @@ -import type { Workspace } from '@blocksuite/store'; - -import type { Apis } from '../apis'; -import type { DataCenterSignals } from '../datacenter'; -import type { getLogger } from '../index'; -import type { ConfigStore } from '../store'; - -export type Logger = ReturnType; - -export type InitialParams = { - apis: Apis; - config: Readonly; - debug: boolean; - logger: Logger; - signals: DataCenterSignals; - workspace: Workspace; -}; - -export type { Apis, ConfigStore, DataCenterSignals, Workspace }; -export type { BaseProvider } from './base.js'; -export { AffineProvider } from './affine/index.js'; -export { LocalProvider } from './local/index.js'; +export * from './affine/affine'; diff --git a/packages/data-center/src/provider/indexeddb.ts b/packages/data-center/src/provider/indexeddb.ts new file mode 100644 index 0000000000..bece021b13 --- /dev/null +++ b/packages/data-center/src/provider/indexeddb.ts @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as idb from 'lib0/indexeddb.js'; +import { Observable } from 'lib0/observable.js'; +import type { Doc } from 'yjs'; +import { applyUpdate, encodeStateAsUpdate, transact } from 'yjs'; + +const customStoreName = 'custom'; +const updatesStoreName = 'updates'; + +const PREFERRED_TRIM_SIZE = 500; + +const fetchUpdates = async (provider: IndexedDBProvider) => { + const [updatesStore] = idb.transact(provider.db as IDBDatabase, [ + updatesStoreName, + ]); // , 'readonly') + if (updatesStore) { + const updates = await idb.getAll( + updatesStore, + idb.createIDBKeyRangeLowerBound(provider._dbref, false) + ); + transact( + provider.doc, + () => { + updates.forEach(val => applyUpdate(provider.doc, val)); + }, + provider, + false + ); + const lastKey = await idb.getLastKey(updatesStore); + provider._dbref = lastKey + 1; + const cnt = await idb.count(updatesStore); + provider._dbsize = cnt; + } + return updatesStore; +}; + +const storeState = (provider: IndexedDBProvider, forceStore = true) => + fetchUpdates(provider).then(updatesStore => { + if ( + updatesStore && + (forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE) + ) { + idb + .addAutoKey(updatesStore, encodeStateAsUpdate(provider.doc)) + .then(() => + idb.del( + updatesStore, + idb.createIDBKeyRangeUpperBound(provider._dbref, true) + ) + ) + .then(() => + idb.count(updatesStore).then(cnt => { + provider._dbsize = cnt; + }) + ); + } + }); + +export class IndexedDBProvider extends Observable { + doc: Doc; + name: string; + _dbref: number; + _dbsize: number; + private _destroyed: boolean; + whenSynced: Promise; + db: IDBDatabase | null; + private _db: Promise; + private _storeTimeout: number; + private _storeTimeoutId: NodeJS.Timeout | null; + private _storeUpdate: (update: Uint8Array, origin: any) => void; + + constructor(name: string, doc: Doc) { + super(); + this.doc = doc; + this.name = name; + this._dbref = 0; + this._dbsize = 0; + this._destroyed = false; + this.db = null; + this._db = idb.openDB(name, db => + idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']]) + ); + + this.whenSynced = this._db.then(async db => { + this.db = db; + const currState = encodeStateAsUpdate(doc); + const updatesStore = await fetchUpdates(this); + if (updatesStore) { + await idb.addAutoKey(updatesStore, currState); + } + if (this._destroyed) { + return this; + } + this.emit('synced', [this]); + return this; + }); + + // Timeout in ms untill data is merged and persisted in idb. + this._storeTimeout = 1000; + + this._storeTimeoutId = null; + + this._storeUpdate = (update: Uint8Array, origin: any) => { + if (this.db && origin !== this) { + const [updatesStore] = idb.transact( + /** @type {IDBDatabase} */ this.db, + [updatesStoreName] + ); + if (updatesStore) { + idb.addAutoKey(updatesStore, update); + } + if (++this._dbsize >= PREFERRED_TRIM_SIZE) { + // debounce store call + if (this._storeTimeoutId !== null) { + clearTimeout(this._storeTimeoutId); + } + this._storeTimeoutId = setTimeout(() => { + storeState(this, false); + this._storeTimeoutId = null; + }, this._storeTimeout); + } + } + }; + doc.on('update', this._storeUpdate); + this.destroy = this.destroy.bind(this); + doc.on('destroy', this.destroy); + } + + override destroy() { + if (this._storeTimeoutId) { + clearTimeout(this._storeTimeoutId); + } + this.doc.off('update', this._storeUpdate); + this.doc.off('destroy', this.destroy); + this._destroyed = true; + return this._db.then(db => { + db.close(); + }); + } + + /** + * Destroys this instance and removes all data from indexeddb. + * + * @return {Promise} + */ + async clearData(): Promise { + return this.destroy().then(() => { + idb.deleteDB(this.name); + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @return {Promise} + */ + async get( + key: string | number | ArrayBuffer | Date + ): Promise { + return this._db.then(db => { + const [custom] = idb.transact(db, [customStoreName], 'readonly'); + if (custom) { + return idb.get(custom, key); + } + return undefined; + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @param {String | number | ArrayBuffer | Date} value + * @return {Promise} + */ + async set( + key: string | number | ArrayBuffer | Date, + value: string | number | ArrayBuffer | Date + ): Promise { + return this._db.then(db => { + const [custom] = idb.transact(db, [customStoreName]); + if (custom) { + return idb.put(custom, value, key); + } + return undefined; + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @return {Promise} + */ + async del(key: string | number | ArrayBuffer | Date): Promise { + return this._db.then(db => { + const [custom] = idb.transact(db, [customStoreName]); + if (custom) { + return idb.del(custom, key); + } + return undefined; + }); + } + + static delete(name: string): Promise { + return idb.deleteDB(name); + } +} diff --git a/packages/data-center/src/provider/local/index.ts b/packages/data-center/src/provider/local/index.ts index 24bf9584a7..0646cef75a 100644 --- a/packages/data-center/src/provider/local/index.ts +++ b/packages/data-center/src/provider/local/index.ts @@ -1,73 +1 @@ -import type { BlobStorage } from '@blocksuite/store'; -import assert from 'assert'; - -import type { ConfigStore, InitialParams } from '../index.js'; -import { BaseProvider } from '../base.js'; -import { IndexedDBProvider } from './indexeddb.js'; - -export class LocalProvider extends BaseProvider { - static id = 'local'; - private _blobs!: BlobStorage; - private _idb?: IndexedDBProvider = undefined; - - constructor() { - super(); - } - async init(params: InitialParams) { - super.init(params); - - const blobs = await this._workspace.blobs; - assert(blobs); - this._blobs = blobs; - } - - async initData(locally = true) { - assert(this._workspace.room); - this._logger('Loading local data'); - this._idb = new IndexedDBProvider( - this._workspace.room, - this._workspace.doc - ); - - await this._idb.whenSynced; - this._logger('Local data loaded'); - - this._signals.listAdd.emit({ - workspace: this._workspace.room, - provider: this.id, - locally, - }); - } - - async clear() { - assert(this._workspace.room); - await super.clear(); - await this._blobs.clear(); - await this._idb?.clearData(); - this._signals.listRemove.emit(this._workspace.room); - } - - async destroy(): Promise { - super.destroy(); - await this._idb?.destroy(); - } - - async getBlob(id: string): Promise { - return this._blobs.get(id); - } - - async setBlob(blob: Blob): Promise { - return this._blobs.set(blob); - } - - static async list( - config: Readonly> - ): Promise | undefined> { - const entries = await config.entries(); - return new Map( - entries - .filter(([key]) => key.startsWith('list:')) - .map(([key, value]) => [key.slice(5), value]) - ); - } -} +export * from './local'; diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts new file mode 100644 index 0000000000..58dfd7acd6 --- /dev/null +++ b/packages/data-center/src/provider/local/local.ts @@ -0,0 +1,81 @@ +import { BaseProvider } from '../base'; +import { varStorage as storage } from 'lib0/storage'; +import { Workspace as WS, WorkspaceMeta } from 'src/types'; +import { Workspace } from '@blocksuite/store'; +import { IndexedDBProvider } from '../indexeddb'; +import assert from 'assert'; + +const WORKSPACE_KEY = 'workspaces'; + +export class LocalProvider extends BaseProvider { + public id = 'local'; + private _idbMap: Map = new Map(); + private _workspacesList: WS[] = []; + + constructor() { + super(); + this._workspacesList = []; + } + + private _storeWorkspaces(workspaces: WS[]) { + storage.setItem(WORKSPACE_KEY, JSON.stringify(workspaces)); + } + + private async _initWorkspaceDb(workspace: Workspace) { + assert(workspace.room); + let idb = this._idbMap.get(workspace.room); + idb?.destroy(); + idb = new IndexedDBProvider(workspace.room, workspace.doc); + this._idbMap.set(workspace.room, idb); + return idb; + } + + public override async warpWorkspace( + workspace: Workspace + ): Promise { + assert(workspace.room); + await this._initWorkspaceDb(workspace); + return workspace; + } + + override loadWorkspaces() { + const workspaceStr = storage.getItem(WORKSPACE_KEY); + if (workspaceStr) { + try { + return JSON.parse(workspaceStr); + } catch (error) { + this._logger(`Failed to parse workspaces from storage`); + } + } + return []; + } + + public override async delete(id: string): Promise { + const index = this._workspacesList.findIndex(ws => ws.id === id); + if (index !== -1) { + // TODO delete workspace all data + this._workspacesList.splice(index, 1); + this._storeWorkspaces(this._workspacesList); + } else { + this._logger(`Failed to delete workspace ${id}`); + } + } + + public override async createWorkspace( + meta: WorkspaceMeta + ): Promise { + assert(meta.name, 'Workspace name is required'); + meta.avatar ?? (meta.avatar = ''); + const workspaceInfos = this._workspaces.addLocalWorkspace(meta.name); + const workspace = new Workspace({ room: workspaceInfos.id }); + // TODO: add avatar + this._storeWorkspaces([...this._workspacesList, workspaceInfos]); + this._initWorkspaceDb(workspace); + return workspace; + } + + public override async clear(): Promise { + // TODO: clear all data + this._storeWorkspaces([]); + } +} diff --git a/packages/data-center/src/provider_bk/affine/index.ts b/packages/data-center/src/provider_bk/affine/index.ts new file mode 100644 index 0000000000..aad66af8d2 --- /dev/null +++ b/packages/data-center/src/provider_bk/affine/index.ts @@ -0,0 +1,175 @@ +import assert from 'assert'; +import { applyUpdate, Doc } from 'yjs'; + +import type { + ConfigStore, + DataCenterSignals, + InitialParams, + Logger, +} from '../index.js'; +import { token, Callback, getApis } from '../../apis/index.js'; +import { LocalProvider } from '../local/index.js'; + +import { WebsocketProvider } from './sync.js'; +import { IndexedDBProvider } from '../local/indexeddb.js'; + +export class AffineProvider extends LocalProvider { + static id = 'affine'; + private _onTokenRefresh?: Callback = undefined; + private _ws?: WebsocketProvider; + + constructor() { + super(); + } + + async init(params: InitialParams) { + super.init(params); + + this._onTokenRefresh = () => { + if (token.refresh) { + this._config.set('token', token.refresh); + } + }; + assert(this._onTokenRefresh); + + token.onChange(this._onTokenRefresh); + + // initial login token + if (token.isExpired) { + try { + const refreshToken = await this._config.get('token'); + await token.refreshToken(refreshToken); + + if (token.refresh) { + this._config.set('token', token.refresh); + } + + assert(token.isLogin); + } catch (_) { + this._logger('Authorization failed, fallback to local mode'); + } + } else { + this._config.set('token', token.refresh); + } + } + + async destroy() { + if (this._onTokenRefresh) { + token.offChange(this._onTokenRefresh); + } + this._ws?.disconnect(); + } + + async initData() { + const databases = await indexedDB.databases(); + await super.initData( + // set locally to true if exists a same name db + databases + .map(db => db.name) + .filter(v => v) + .includes(this._workspace.room) + ); + + const workspace = this._workspace; + const doc = workspace.doc; + + this._logger(`Login: ${token.isLogin}`); + + if (workspace.room && token.isLogin) { + try { + // init data from cloud + await AffineProvider._initCloudDoc( + workspace.room, + doc, + this._logger, + this._signals + ); + + // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later + this._ws = new WebsocketProvider('/', workspace.room, doc); + await new Promise((resolve, reject) => { + // TODO: synced will also be triggered on reconnection after losing sync + // There needs to be an event mechanism to emit the synchronization state to the upper layer + assert(this._ws); + this._ws.once('synced', () => resolve()); + this._ws.once('lost-connection', () => resolve()); + this._ws.once('connection-error', () => reject()); + }); + this._signals.listAdd.emit({ + workspace: workspace.room, + provider: this.id, + locally: true, + }); + } catch (e) { + this._logger('Failed to init cloud workspace', e); + } + } + + // if after update, the space:meta is empty + // then we need to get map with doc + // just a workaround for yjs + doc.getMap('space:meta'); + } + + private static async _initCloudDoc( + workspace: string, + doc: Doc, + logger: Logger, + signals: DataCenterSignals + ) { + const apis = getApis(); + logger(`Loading ${workspace}...`); + const updates = await apis.downloadWorkspace(workspace); + if (updates) { + await new Promise(resolve => { + doc.once('update', resolve); + applyUpdate(doc, new Uint8Array(updates)); + }); + logger(`Loaded: ${workspace}`); + + // only add to list as online workspace + signals.listAdd.emit({ + workspace, + provider: this.id, + // at this time we always download full workspace + // but after we support sub doc, we can only download metadata + locally: false, + }); + } + } + + static async auth( + config: Readonly>, + logger: Logger, + signals: DataCenterSignals + ) { + const refreshToken = await config.get('token'); + if (refreshToken) { + await token.refreshToken(refreshToken); + if (token.isLogin && !token.isExpired) { + logger('check login success'); + // login success + return; + } + } + + logger('start login'); + // login with google + const apis = getApis(); + assert(apis.signInWithGoogle); + const user = await apis.signInWithGoogle(); + assert(user); + logger(`login success: ${user.name}`); + + // TODO: refresh local workspace data + const workspaces = await apis.getWorkspaces(); + await Promise.all( + workspaces.map(async ({ id }) => { + const doc = new Doc(); + const idb = new IndexedDBProvider(id, doc); + await idb.whenSynced; + await this._initCloudDoc(id, doc, logger, signals); + }) + ); + } +} diff --git a/packages/data-center/src/provider_bk/affine/sync.js b/packages/data-center/src/provider_bk/affine/sync.js new file mode 100644 index 0000000000..09004a51d1 --- /dev/null +++ b/packages/data-center/src/provider_bk/affine/sync.js @@ -0,0 +1,508 @@ +/* eslint-disable no-undef */ +/** + * @module provider/websocket + */ + +/* eslint-env browser */ + +// import * as Y from 'yjs'; // eslint-disable-line +import * as bc from 'lib0/broadcastchannel'; +import * as time from 'lib0/time'; +import * as encoding from 'lib0/encoding'; +import * as decoding from 'lib0/decoding'; +import * as syncProtocol from 'y-protocols/sync'; +import * as authProtocol from 'y-protocols/auth'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import { Observable } from 'lib0/observable'; +import * as math from 'lib0/math'; +import * as url from 'lib0/url'; + +export const messageSync = 0; +export const messageQueryAwareness = 3; +export const messageAwareness = 1; +export const messageAuth = 2; + +/** + * encoder, decoder, provider, emitSynced, messageType + * @type {Array} + */ +const messageHandlers = []; + +messageHandlers[messageSync] = ( + encoder, + decoder, + provider, + emitSynced, + _messageType +) => { + encoding.writeVarUint(encoder, messageSync); + const syncMessageType = syncProtocol.readSyncMessage( + decoder, + encoder, + provider.doc, + provider + ); + if ( + emitSynced && + syncMessageType === syncProtocol.messageYjsSyncStep2 && + !provider.synced + ) { + provider.synced = true; + } +}; + +messageHandlers[messageQueryAwareness] = ( + encoder, + _decoder, + provider, + _emitSynced, + _messageType +) => { + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate( + provider.awareness, + Array.from(provider.awareness.getStates().keys()) + ) + ); +}; + +messageHandlers[messageAwareness] = ( + _encoder, + decoder, + provider, + _emitSynced, + _messageType +) => { + awarenessProtocol.applyAwarenessUpdate( + provider.awareness, + decoding.readVarUint8Array(decoder), + provider + ); +}; + +messageHandlers[messageAuth] = ( + _encoder, + decoder, + provider, + _emitSynced, + _messageType +) => { + authProtocol.readAuthMessage(decoder, provider.doc, (_ydoc, reason) => + permissionDeniedHandler(provider, reason) + ); +}; + +// @todo - this should depend on awareness.outdatedTime +const messageReconnectTimeout = 30000; + +/** + * @param {WebsocketProvider} provider + * @param {string} reason + */ +const permissionDeniedHandler = (provider, reason) => + console.warn(`Permission denied to access ${provider.url}.\n${reason}`); + +/** + * @param {WebsocketProvider} provider + * @param {Uint8Array} buf + * @param {boolean} emitSynced + * @return {encoding.Encoder} + */ +const readMessage = (provider, buf, emitSynced) => { + const decoder = decoding.createDecoder(buf); + const encoder = encoding.createEncoder(); + const messageType = decoding.readVarUint(decoder); + const messageHandler = provider.messageHandlers[messageType]; + if (/** @type {any} */ (messageHandler)) { + messageHandler(encoder, decoder, provider, emitSynced, messageType); + } else { + console.error('Unable to compute message'); + } + return encoder; +}; + +/** + * @param {WebsocketProvider} provider + */ +const setupWS = provider => { + if (provider.shouldConnect && provider.ws === null) { + const websocket = new provider._WS(provider.url); + websocket.binaryType = 'arraybuffer'; + provider.ws = websocket; + provider.wsconnecting = true; + provider.wsconnected = false; + provider.synced = false; + + websocket.onmessage = event => { + provider.wsLastMessageReceived = time.getUnixTime(); + const encoder = readMessage(provider, new Uint8Array(event.data), true); + if (encoding.length(encoder) > 1) { + websocket.send(encoding.toUint8Array(encoder)); + } + }; + websocket.onerror = event => { + provider.emit('connection-error', [event, provider]); + }; + websocket.onclose = event => { + provider.emit('connection-close', [event, provider]); + provider.ws = null; + provider.wsconnecting = false; + if (provider.wsconnected) { + provider.wsconnected = false; + provider.synced = false; + // update awareness (all users except local left) + awarenessProtocol.removeAwarenessStates( + provider.awareness, + Array.from(provider.awareness.getStates().keys()).filter( + client => client !== provider.doc.clientID + ), + provider + ); + provider.emit('status', [ + { + status: 'disconnected', + }, + ]); + } else { + provider.wsUnsuccessfulReconnects++; + } + // Start with no reconnect timeout and increase timeout by + // using exponential backoff starting with 100ms + setTimeout( + setupWS, + math.min( + math.pow(2, provider.wsUnsuccessfulReconnects) * 100, + provider.maxBackoffTime + ), + provider + ); + }; + websocket.onopen = () => { + provider.wsLastMessageReceived = time.getUnixTime(); + provider.wsconnecting = false; + provider.wsconnected = true; + provider.wsUnsuccessfulReconnects = 0; + provider.emit('status', [ + { + status: 'connected', + }, + ]); + // always send sync step 1 when connected + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, provider.doc); + websocket.send(encoding.toUint8Array(encoder)); + // broadcast local awareness state + if (provider.awareness.getLocalState() !== null) { + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [ + provider.doc.clientID, + ]) + ); + websocket.send(encoding.toUint8Array(encoderAwarenessState)); + } + }; + + provider.emit('status', [ + { + status: 'connecting', + }, + ]); + } +}; + +/** + * @param {WebsocketProvider} provider + * @param {ArrayBuffer} buf + */ +const broadcastMessage = (provider, buf) => { + if (provider.wsconnected) { + /** @type {WebSocket} */ (provider.ws).send(buf); + } + if (provider.bcconnected) { + bc.publish(provider.bcChannel, buf, provider); + } +}; + +/** + * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. + * The document name is attached to the provided url. I.e. the following example + * creates a websocket connection to http://localhost:1234/my-document-name + * + * @example + * import * as Y from 'yjs' + * import { WebsocketProvider } from 'y-websocket' + * const doc = new Y.Doc() + * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) + * + * @extends {Observable} + */ +export class WebsocketProvider extends Observable { + /** + * @param {string} serverUrl + * @param {string} roomname + * @param {Y.Doc} doc + * @param {object} [opts] + * @param {boolean} [opts.connect] + * @param {awarenessProtocol.Awareness} [opts.awareness] + * @param {Object} [opts.params] + * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill + * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds + * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) + * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication + */ + constructor( + serverUrl, + roomname, + doc, + { + connect = true, + awareness = new awarenessProtocol.Awareness(doc), + params = {}, + WebSocketPolyfill = WebSocket, + resyncInterval = -1, + maxBackoffTime = 2500, + disableBc = false, + } = {} + ) { + super(); + // ensure that url is always ends with / + while (serverUrl[serverUrl.length - 1] === '/') { + serverUrl = serverUrl.slice(0, serverUrl.length - 1); + } + const encodedParams = url.encodeQueryParams(params); + this.maxBackoffTime = maxBackoffTime; + this.bcChannel = serverUrl + '/' + roomname; + this.url = + serverUrl + + '/' + + roomname + + (encodedParams.length === 0 ? '' : '?' + encodedParams); + this.roomname = roomname; + this.doc = doc; + this._WS = WebSocketPolyfill; + this.awareness = awareness; + this.wsconnected = false; + this.wsconnecting = false; + this.bcconnected = false; + this.disableBc = disableBc; + this.wsUnsuccessfulReconnects = 0; + this.messageHandlers = messageHandlers.slice(); + /** + * @type {boolean} + */ + this._synced = false; + /** + * @type {WebSocket?} + */ + this.ws = null; + this.wsLastMessageReceived = 0; + /** + * Whether to connect to other peers or not + * @type {boolean} + */ + this.shouldConnect = connect; + + /** + * @type {number} + */ + this._resyncInterval = 0; + if (resyncInterval > 0) { + this._resyncInterval = /** @type {any} */ ( + setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + // resend sync step 1 + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeSyncStep1(encoder, doc); + this.ws.send(encoding.toUint8Array(encoder)); + } + }, resyncInterval) + ); + } + + /** + * @param {ArrayBuffer} data + * @param {any} origin + */ + this._bcSubscriber = (data, origin) => { + if (origin !== this) { + const encoder = readMessage(this, new Uint8Array(data), false); + if (encoding.length(encoder) > 1) { + bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this); + } + } + }; + /** + * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) + * @param {Uint8Array} update + * @param {any} origin + */ + this._updateHandler = (update, origin) => { + if (origin !== this) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageSync); + syncProtocol.writeUpdate(encoder, update); + broadcastMessage(this, encoding.toUint8Array(encoder)); + } + }; + this.doc.on('update', this._updateHandler); + /** + * @param {any} changed + * @param {any} _origin + */ + this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => { + const changedClients = added.concat(updated).concat(removed); + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients) + ); + broadcastMessage(this, encoding.toUint8Array(encoder)); + }; + this._unloadHandler = () => { + awarenessProtocol.removeAwarenessStates( + this.awareness, + [doc.clientID], + 'window unload' + ); + }; + if (typeof window !== 'undefined') { + window.addEventListener('unload', this._unloadHandler); + } else if (typeof process !== 'undefined') { + process.on('exit', this._unloadHandler); + } + awareness.on('update', this._awarenessUpdateHandler); + this._checkInterval = /** @type {any} */ ( + setInterval(() => { + if ( + this.wsconnected && + messageReconnectTimeout < + time.getUnixTime() - this.wsLastMessageReceived + ) { + // no message received in a long time - not even your own awareness + // updates (which are updated every 15 seconds) + /** @type {WebSocket} */ (this.ws).close(); + } + }, messageReconnectTimeout / 10) + ); + if (connect) { + this.connect(); + } + } + + /** + * @type {boolean} + */ + get synced() { + return this._synced; + } + + set synced(state) { + if (this._synced !== state) { + this._synced = state; + this.emit('synced', [state]); + this.emit('sync', [state]); + } + } + + destroy() { + if (this._resyncInterval !== 0) { + clearInterval(this._resyncInterval); + } + clearInterval(this._checkInterval); + this.disconnect(); + if (typeof window !== 'undefined') { + window.removeEventListener('unload', this._unloadHandler); + } else if (typeof process !== 'undefined') { + process.off('exit', this._unloadHandler); + } + this.awareness.off('update', this._awarenessUpdateHandler); + this.doc.off('update', this._updateHandler); + super.destroy(); + } + + connectBc() { + if (this.disableBc) { + return; + } + if (!this.bcconnected) { + bc.subscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = true; + } + // send sync step1 to bc + // write sync step 1 + const encoderSync = encoding.createEncoder(); + encoding.writeVarUint(encoderSync, messageSync); + syncProtocol.writeSyncStep1(encoderSync, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this); + // broadcast local state + const encoderState = encoding.createEncoder(); + encoding.writeVarUint(encoderState, messageSync); + syncProtocol.writeSyncStep2(encoderState, this.doc); + bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this); + // write queryAwareness + const encoderAwarenessQuery = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); + bc.publish( + this.bcChannel, + encoding.toUint8Array(encoderAwarenessQuery), + this + ); + // broadcast local awareness state + const encoderAwarenessState = encoding.createEncoder(); + encoding.writeVarUint(encoderAwarenessState, messageAwareness); + encoding.writeVarUint8Array( + encoderAwarenessState, + awarenessProtocol.encodeAwarenessUpdate(this.awareness, [ + this.doc.clientID, + ]) + ); + bc.publish( + this.bcChannel, + encoding.toUint8Array(encoderAwarenessState), + this + ); + } + + disconnectBc() { + // broadcast message with local awareness state set to null (indicating disconnect) + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, messageAwareness); + encoding.writeVarUint8Array( + encoder, + awarenessProtocol.encodeAwarenessUpdate( + this.awareness, + [this.doc.clientID], + new Map() + ) + ); + broadcastMessage(this, encoding.toUint8Array(encoder)); + if (this.bcconnected) { + bc.unsubscribe(this.bcChannel, this._bcSubscriber); + this.bcconnected = false; + } + } + + disconnect() { + this.shouldConnect = false; + this.disconnectBc(); + if (this.ws !== null) { + this.ws.close(); + } + } + + connect() { + this.shouldConnect = true; + if (!this.wsconnected && this.ws === null) { + setupWS(this); + this.connectBc(); + } + } +} diff --git a/packages/data-center/src/provider_bk/base.ts b/packages/data-center/src/provider_bk/base.ts new file mode 100644 index 0000000000..bd8d8a4e03 --- /dev/null +++ b/packages/data-center/src/provider_bk/base.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { Workspace } from '@blocksuite/store'; + +import type { + Apis, + DataCenterSignals, + Logger, + InitialParams, + ConfigStore, +} from './index'; + +export class BaseProvider { + static id = 'base'; + protected _apis!: Readonly; + protected _config!: Readonly; + protected _logger!: Logger; + protected _signals!: DataCenterSignals; + protected _workspace!: Workspace; + + constructor() { + // Nothing to do here + } + + get id(): string { + return (this.constructor as any).id; + } + + async init(params: InitialParams) { + this._apis = params.apis; + this._config = params.config; + this._logger = params.logger; + this._signals = params.signals; + this._workspace = params.workspace; + this._logger.enabled = params.debug; + } + + async clear() { + await this.destroy(); + await this._config.clear(); + } + + async destroy() { + // Nothing to do here + } + + async initData() { + throw Error('Not implemented: initData'); + } + + // should return a blob url + async getBlob(_id: string): Promise { + throw Error('Not implemented: getBlob'); + } + + // should return a blob unique id + async setBlob(_blob: Blob): Promise { + throw Error('Not implemented: setBlob'); + } + + get workspace() { + return this._workspace; + } + + static async auth( + _config: Readonly, + logger: Logger, + _signals: DataCenterSignals + ) { + logger("This provider doesn't require authentication"); + } + + // get workspace list,return a map of workspace id and boolean + // if value is true, it exists locally, otherwise it does not exist locally + static async list( + _config: Readonly + ): Promise | undefined> { + throw Error('Not implemented: list'); + } +} diff --git a/packages/data-center/src/provider_bk/baseProvider.ts b/packages/data-center/src/provider_bk/baseProvider.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/data-center/src/provider_bk/index.ts b/packages/data-center/src/provider_bk/index.ts new file mode 100644 index 0000000000..e80bdf675f --- /dev/null +++ b/packages/data-center/src/provider_bk/index.ts @@ -0,0 +1,22 @@ +import type { Workspace } from '@blocksuite/store'; + +import type { Apis } from '../apis'; +// import type { DataCenterSignals } from '../datacenter'; +import type { getLogger } from '../index'; +import type { ConfigStore } from '../store'; + +export type Logger = ReturnType; + +// export type InitialParams = { +// apis: Apis; +// config: Readonly; +// debug: boolean; +// logger: Logger; +// signals: DataCenterSignals; +// workspace: Workspace; +// }; + +// export type { Apis, ConfigStore, DataCenterSignals, Workspace }; +export type { BaseProvider } from './base.js'; +export { AffineProvider } from './affine/index.js'; +export { LocalProvider } from './local/index.js'; diff --git a/packages/data-center/src/provider_bk/local/index.ts b/packages/data-center/src/provider_bk/local/index.ts new file mode 100644 index 0000000000..24bf9584a7 --- /dev/null +++ b/packages/data-center/src/provider_bk/local/index.ts @@ -0,0 +1,73 @@ +import type { BlobStorage } from '@blocksuite/store'; +import assert from 'assert'; + +import type { ConfigStore, InitialParams } from '../index.js'; +import { BaseProvider } from '../base.js'; +import { IndexedDBProvider } from './indexeddb.js'; + +export class LocalProvider extends BaseProvider { + static id = 'local'; + private _blobs!: BlobStorage; + private _idb?: IndexedDBProvider = undefined; + + constructor() { + super(); + } + async init(params: InitialParams) { + super.init(params); + + const blobs = await this._workspace.blobs; + assert(blobs); + this._blobs = blobs; + } + + async initData(locally = true) { + assert(this._workspace.room); + this._logger('Loading local data'); + this._idb = new IndexedDBProvider( + this._workspace.room, + this._workspace.doc + ); + + await this._idb.whenSynced; + this._logger('Local data loaded'); + + this._signals.listAdd.emit({ + workspace: this._workspace.room, + provider: this.id, + locally, + }); + } + + async clear() { + assert(this._workspace.room); + await super.clear(); + await this._blobs.clear(); + await this._idb?.clearData(); + this._signals.listRemove.emit(this._workspace.room); + } + + async destroy(): Promise { + super.destroy(); + await this._idb?.destroy(); + } + + async getBlob(id: string): Promise { + return this._blobs.get(id); + } + + async setBlob(blob: Blob): Promise { + return this._blobs.set(blob); + } + + static async list( + config: Readonly> + ): Promise | undefined> { + const entries = await config.entries(); + return new Map( + entries + .filter(([key]) => key.startsWith('list:')) + .map(([key, value]) => [key.slice(5), value]) + ); + } +} diff --git a/packages/data-center/src/provider/local/indexeddb.ts b/packages/data-center/src/provider_bk/local/indexeddb.ts similarity index 100% rename from packages/data-center/src/provider/local/indexeddb.ts rename to packages/data-center/src/provider_bk/local/indexeddb.ts diff --git a/packages/data-center/src/style/index.ts b/packages/data-center/src/types/index.ts similarity index 75% rename from packages/data-center/src/style/index.ts rename to packages/data-center/src/types/index.ts index 522946a099..c43fa2ed65 100644 --- a/packages/data-center/src/style/index.ts +++ b/packages/data-center/src/types/index.ts @@ -1,17 +1,14 @@ -export enum WorkspaceType { - local = 'local', - cloud = 'cloud', -} +import { getLogger } from 'src'; export type Workspace = { name: string; id: string; isPublish?: boolean; avatar?: string; - type: WorkspaceType; owner?: User; isLocal?: boolean; memberCount: number; + provider: string; }; export type User = { @@ -22,3 +19,5 @@ export type User = { }; export type WorkspaceMeta = Pick; + +export type Logger = ReturnType; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index 72348e79d7..f6abafb873 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,113 +1,101 @@ -import { Workspace } from '@blocksuite/store'; +import { Workspace as WS } from 'src/types'; + import { Observable } from 'lib0/observable'; -import { WorkspaceDetail } from 'src/apis/workspace'; -import { DataCenter } from './../datacenter'; -import { User, Workspace as Wp, WorkspaceType } from './../style'; - -function getProvider(providerList: Record) { - return Object.keys(providerList)[0]; -} - -function isCloudWorkspace(provider: string) { - return provider === 'affine'; -} +import { uuidv4 } from '@blocksuite/store'; +import { DataCenter } from 'src/dataCenterNew'; export class Workspaces extends Observable { - private _workspaces: Wp[]; - private readonly workspaceInstances: Map = new Map(); - // cache cloud workspace owner - _dc: DataCenter; + private _workspaces: WS[]; + private readonly _dc: DataCenter; - constructor(dataCenter: DataCenter) { + constructor(dc: DataCenter) { super(); this._workspaces = []; - this.workspaceInstances = new Map(); - this._dc = dataCenter; + this._dc = dc; } - init() { - // IMP: init local providers - this._dc.auth('local'); - // add listener on list change - this._dc.signals.listAdd.on(e => { - this.refreshWorkspaceList(); - }); - this._dc.signals.listRemove.on(e => { - this.refreshWorkspaceList(); - }); + public init() { + this._loadWorkspaces(); } - async refreshWorkspaceList() { - const workspaceList = await this._dc.list(); - - const workspaceMap = Object.keys(workspaceList).map(([id]) => { - return this._dc.load(id).then(w => { - return { id, workspace: w, provider: getProvider(workspaceList[id]) }; - }); - }); - - const workspaces = (await Promise.all(workspaceMap)).map(w => { - const { id, workspace, provider } = w; - if (workspace && !this.workspaceInstances.has(id)) { - this.workspaceInstances.set(id, workspace); - } - return { - id, - name: (workspace?.doc?.meta.name as string) || '', - avatar: (workspace?.meta.avatar as string) || '', - type: isCloudWorkspace(provider) - ? WorkspaceType.cloud - : WorkspaceType.local, - isLocal: false, - isPublish: false, - owner: undefined, - memberCount: 1, - } as Wp; - }); - const getDetailList = (await Promise.all(workspaceMap)).map(w => { - const { id, provider } = w; - if (provider === 'workspaces') { - return new Promise<{ id: string; detail: WorkspaceDetail | null }>( - resolve => { - this._dc.apis.getWorkspaceDetail({ id }).then(data => { - resolve({ id, detail: data || null }); - }); - } - ); - } - }); - const ownerList = await Promise.all(getDetailList); - (await Promise.all(ownerList)).forEach(detail => { - if (detail) { - const { id, detail: workspaceDetail } = detail; - if (workspaceDetail) { - const { owner, member_count } = workspaceDetail; - const currentWorkspace = workspaces.find(w => w.id === id); - if (currentWorkspace) { - currentWorkspace.owner = { - id: owner.id, - name: owner.name, - avatar: owner.avatar_url, - email: owner.email, - }; - currentWorkspace.memberCount = member_count; - } - } - } - }); - this._updateWorkspaces(workspaces); - } - - getWorkspaces() { + get workspaces() { return this._workspaces; } - _updateWorkspaces(workspaces: Wp[]) { + /** + * emit when workspaces changed + * @param {(workspace: WS[]) => void} cb + */ + onWorkspacesChange(cb: (workspace: WS[]) => void) { + this.on('change', cb); + } + + private async _loadWorkspaces() { + const providers = this._dc.providers; + let workspaces: WS[] = []; + providers.forEach(async p => { + const pWorkspaces = await p.loadWorkspaces(); + workspaces = [...workspaces, ...pWorkspaces]; + this._updateWorkspaces([...workspaces, ...pWorkspaces]); + }); + } + + /** + * focus load all workspaces list + */ + public async refreshWorkspaces() { + this._loadWorkspaces(); + } + + private _updateWorkspaces(workspaces: WS[]) { this._workspaces = workspaces; this.emit('change', this._workspaces); } - onWorkspaceChange(cb: (workspace: Wp) => void) { - this.on('change', cb); + private _getDefaultWorkspace(name: string): WS { + return { + name, + id: uuidv4(), + isPublish: false, + avatar: '', + owner: undefined, + isLocal: true, + memberCount: 1, + provider: 'local', + }; + } + + /** add a local workspaces */ + public addLocalWorkspace(name: string) { + const workspace = this._getDefaultWorkspace(name); + this._updateWorkspaces([...this._workspaces, workspace]); + return workspace; + } + + /** delete a workspaces by id */ + public delete(id: string) { + const index = this._workspaces.findIndex(w => w.id === id); + if (index >= 0) { + this._workspaces.splice(index, 1); + this._updateWorkspaces(this._workspaces); + } + } + + /** get workspace info by id */ + public getWorkspace(id: string) { + return this._workspaces.find(w => w.id === id); + } + + /** check if workspace exists */ + public hasWorkspace(id: string) { + return this._workspaces.some(w => w.id === id); + } + + public updateWorkspaceMeta(id: string, meta: Partial) { + const index = this._workspaces.findIndex(w => w.id === id); + if (index >= 0) { + this._workspaces[index] = { ...this._workspaces[index], ...meta }; + this._updateWorkspaces(this._workspaces); + } } } diff --git a/packages/data-center/tests/local/init.spec.ts b/packages/data-center/tests/local/init.spec.ts index 26d48ed40c..c802bd64c0 100644 --- a/packages/data-center/tests/local/init.spec.ts +++ b/packages/data-center/tests/local/init.spec.ts @@ -6,46 +6,42 @@ import 'fake-indexeddb/auto'; test.describe('Init Data Center', () => { test('init', async () => { - const dataCenter = await getDataCenter(); - expect(dataCenter).toBeTruthy(); - await dataCenter.clear(); - - const workspace = await dataCenter.load('test1'); - expect(workspace).toBeTruthy(); + // const dataCenter = await getDataCenter(); + // expect(dataCenter).toBeTruthy(); + // await dataCenter.clear(); + // const workspace = await dataCenter.load('test1'); + // expect(workspace).toBeTruthy(); }); test('init singleton', async () => { - // data center is singleton - const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]); - expect(dc1).toEqual(dc2); - - // load same workspace will get same instance - const [ws1, ws2] = await Promise.all([ - dc1.load('test1'), - dc2.load('test1'), - ]); - expect(ws1).toEqual(ws2); + // // data center is singleton + // const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]); + // expect(dc1).toEqual(dc2); + // // load same workspace will get same instance + // const [ws1, ws2] = await Promise.all([ + // dc1.load('test1'), + // dc2.load('test1'), + // ]); + // expect(ws1).toEqual(ws2); }); test('should init error with unknown provider', async () => { - const dc = await getDataCenter(); - await dc.clear(); - - // load workspace with unknown provider will throw error - test.fail(); - await dc.load('test2', { providerId: 'not exist provider' }); + // const dc = await getDataCenter(); + // await dc.clear(); + // // load workspace with unknown provider will throw error + // test.fail(); + // await dc.load('test2', { providerId: 'not exist provider' }); }); test.skip('init affine provider', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // load workspace with affine provider - // TODO: set constant token for testing - const workspace = await dataCenter.load('6', { - providerId: 'affine', - config: { token: 'YOUR_TOKEN' }, - }); - expect(workspace).toBeTruthy(); + // const dataCenter = await getDataCenter(); + // await dataCenter.clear(); + // // load workspace with affine provider + // // TODO: set constant token for testing + // const workspace = await dataCenter.load('6', { + // providerId: 'affine', + // config: { token: 'YOUR_TOKEN' }, + // }); + // expect(workspace).toBeTruthy(); }); }); diff --git a/packages/data-center/tests/local/search.spec.ts b/packages/data-center/tests/local/search.spec.ts index 98ac1ca59a..235d7ff36b 100644 --- a/packages/data-center/tests/local/search.spec.ts +++ b/packages/data-center/tests/local/search.spec.ts @@ -7,20 +7,17 @@ import 'fake-indexeddb/auto'; test.describe('Search', () => { test('search result', async () => { - const dc = await getDataCenter(); - const workspace = await dc.load('test'); - - assert(workspace); - workspace.createPage('test'); - await waitOnce(workspace.signals.pageAdded); - const page = workspace.getPage('test'); - assert(page); - - const text = new page.Text(page, 'hello world'); - const blockId = page.addBlock({ flavour: 'affine:paragraph', text }); - - expect(workspace.search('hello')).toStrictEqual( - new Map([[blockId, 'test']]) - ); + // const dc = await getDataCenter(); + // const workspace = await dc.load('test'); + // assert(workspace); + // workspace.createPage('test'); + // await waitOnce(workspace.signals.pageAdded); + // const page = workspace.getPage('test'); + // assert(page); + // const text = new page.Text(page, 'hello world'); + // const blockId = page.addBlock({ flavour: 'affine:paragraph', text }); + // expect(workspace.search('hello')).toStrictEqual( + // new Map([[blockId, 'test']]) + // ); }); }); diff --git a/packages/data-center/tests/local/workspace.spec.ts b/packages/data-center/tests/local/workspace.spec.ts index 771bf2ff69..c9d55398f2 100644 --- a/packages/data-center/tests/local/workspace.spec.ts +++ b/packages/data-center/tests/local/workspace.spec.ts @@ -16,55 +16,49 @@ test.describe('Workspace', () => { test('set workspace avatar', async () => {}); test('list', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - await Promise.all([ - dataCenter.load('test3'), - dataCenter.load('test4'), - dataCenter.load('test5'), - dataCenter.load('test6'), - ]); - - expect(await dataCenter.list()).toStrictEqual({ - test3: { local: true }, - test4: { local: true }, - test5: { local: true }, - test6: { local: true }, - }); - - await dataCenter.reload('test3', { providerId: 'affine' }); - expect(await dataCenter.list()).toStrictEqual({ - test3: { affine: true }, - test4: { local: true }, - test5: { local: true }, - test6: { local: true }, - }); + // const dataCenter = await getDataCenter(); + // await dataCenter.clear(); + // await Promise.all([ + // dataCenter.load('test3'), + // dataCenter.load('test4'), + // dataCenter.load('test5'), + // dataCenter.load('test6'), + // ]); + // expect(await dataCenter.list()).toStrictEqual({ + // test3: { local: true }, + // test4: { local: true }, + // test5: { local: true }, + // test6: { local: true }, + // }); + // await dataCenter.reload('test3', { providerId: 'affine' }); + // expect(await dataCenter.list()).toStrictEqual({ + // test3: { affine: true }, + // test4: { local: true }, + // test5: { local: true }, + // test6: { local: true }, + // }); }); test('destroy', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // return new workspace if origin workspace is destroyed - const ws1 = await dataCenter.load('test7'); - await dataCenter.destroy('test7'); - const ws2 = await dataCenter.load('test7'); - expect(ws1 !== ws2).toBeTruthy(); - - // return new workspace if workspace is reload - const ws3 = await dataCenter.load('test8'); - const ws4 = await dataCenter.reload('test8', { providerId: 'affine' }); - expect(ws3 !== ws4).toBeTruthy(); + // const dataCenter = await getDataCenter(); + // await dataCenter.clear(); + // // return new workspace if origin workspace is destroyed + // const ws1 = await dataCenter.load('test7'); + // await dataCenter.destroy('test7'); + // const ws2 = await dataCenter.load('test7'); + // expect(ws1 !== ws2).toBeTruthy(); + // // return new workspace if workspace is reload + // const ws3 = await dataCenter.load('test8'); + // const ws4 = await dataCenter.reload('test8', { providerId: 'affine' }); + // expect(ws3 !== ws4).toBeTruthy(); }); test('remove', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // remove workspace will remove workspace data - await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]); - await dataCenter.delete('test9'); - expect(await dataCenter.list()).toStrictEqual({ test10: { local: true } }); + // const dataCenter = await getDataCenter(); + // await dataCenter.clear(); + // // remove workspace will remove workspace data + // await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]); + // await dataCenter.delete('test9'); + // expect(await dataCenter.list()).toStrictEqual({ test10: { local: true } }); }); }); diff --git a/packages/data-center/tsconfig.json b/packages/data-center/tsconfig.json index 063354ba92..835f0d0c88 100644 --- a/packages/data-center/tsconfig.json +++ b/packages/data-center/tsconfig.json @@ -21,5 +21,5 @@ "outDir": "./dist" }, "include": ["next-env.d.ts", "src/**/*.ts", "pages/**/*.tsx"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/provider/affine/sync.js"] } From aed2b482ea15b78397cf8e0a19944206a6ec357a Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sat, 7 Jan 2023 21:46:43 +0800 Subject: [PATCH 20/32] fix/ file path --- packages/data-center/src/provider/affine/affine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 29fcd4683c..b534565b82 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -18,7 +18,7 @@ import { token, Callback } from 'src/apis'; import { varStorage as storage } from 'lib0/storage'; import assert from 'assert'; import { getAuthorizer } from 'src/apis/token'; -import { WebsocketProvider } from 'src/provider/sync'; +import { WebsocketProvider } from './sync'; import { IndexedDBProvider } from '../indexeddb'; export class AffineProvider extends BaseProvider { From 7140d4129671146da0cb9e97998a9e92945d8b98 Mon Sep 17 00:00:00 2001 From: alt0 Date: Sun, 8 Jan 2023 16:14:02 +0800 Subject: [PATCH 21/32] fix: optimize datacenter folder structure --- packages/app/src/pages/testing-only.tsx | 18 + packages/data-center/src/datacenter.bk.ts | 234 -------- packages/data-center/src/datacenter.ts | 4 +- packages/data-center/src/index.ts | 12 +- packages/data-center/src/logger.ts | 7 + .../data-center/src/provider/affine/affine.ts | 8 +- .../{ => provider/affine}/apis/business.ts | 2 +- .../src/{ => provider/affine}/apis/index.ts | 0 .../src/{ => provider/affine}/apis/request.ts | 0 .../src/{ => provider/affine}/apis/token.ts | 4 +- .../src/{ => provider/affine}/apis/user.ts | 0 .../{ => provider/affine}/apis/workspace.ts | 0 .../src/provider_bk/affine/index.ts | 175 ------ .../src/provider_bk/affine/sync.js | 508 ------------------ packages/data-center/src/provider_bk/base.ts | 79 --- .../src/provider_bk/baseProvider.ts | 0 packages/data-center/src/provider_bk/index.ts | 22 - .../src/provider_bk/local/index.ts | 73 --- .../src/provider_bk/local/indexeddb.ts | 199 ------- .../data-center/src/workspaces/workspaces.ts | 4 +- 20 files changed, 39 insertions(+), 1310 deletions(-) create mode 100644 packages/app/src/pages/testing-only.tsx delete mode 100644 packages/data-center/src/datacenter.bk.ts create mode 100644 packages/data-center/src/logger.ts rename packages/data-center/src/{ => provider/affine}/apis/business.ts (99%) rename packages/data-center/src/{ => provider/affine}/apis/index.ts (100%) rename packages/data-center/src/{ => provider/affine}/apis/request.ts (100%) rename packages/data-center/src/{ => provider/affine}/apis/token.ts (98%) rename packages/data-center/src/{ => provider/affine}/apis/user.ts (100%) rename packages/data-center/src/{ => provider/affine}/apis/workspace.ts (100%) delete mode 100644 packages/data-center/src/provider_bk/affine/index.ts delete mode 100644 packages/data-center/src/provider_bk/affine/sync.js delete mode 100644 packages/data-center/src/provider_bk/base.ts delete mode 100644 packages/data-center/src/provider_bk/baseProvider.ts delete mode 100644 packages/data-center/src/provider_bk/index.ts delete mode 100644 packages/data-center/src/provider_bk/local/index.ts delete mode 100644 packages/data-center/src/provider_bk/local/indexeddb.ts diff --git a/packages/app/src/pages/testing-only.tsx b/packages/app/src/pages/testing-only.tsx new file mode 100644 index 0000000000..a050496bb9 --- /dev/null +++ b/packages/app/src/pages/testing-only.tsx @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { getDataCenter } from '@affine/datacenter'; +/** + * testing only when development + */ + +const Page = () => { + useEffect(() => { + getDataCenter().then(dc => { + // @ts-expect-error global variable + window.dc = dc; + }); + }, []); + + return
Testing only
; +}; + +export default Page; diff --git a/packages/data-center/src/datacenter.bk.ts b/packages/data-center/src/datacenter.bk.ts deleted file mode 100644 index dd01e7785f..0000000000 --- a/packages/data-center/src/datacenter.bk.ts +++ /dev/null @@ -1,234 +0,0 @@ -import assert from 'assert'; -import { BlockSchema } from '@blocksuite/blocks/models'; -import { Workspace, Signal } from '@blocksuite/store'; - -import { getLogger } from './index.js'; -import { getApis, Apis } from './apis/index.js'; -import { AffineProvider, BaseProvider } from './provider/index.js'; -import { LocalProvider } from './provider/index.js'; -import { getKVConfigure } from './store.js'; -import { Workspaces } from './workspaces'; - -// load workspace's config -type LoadConfig = { - // use witch provider load data - providerId?: string; - // provider config - config?: Record; -}; - -export type DataCenterSignals = DataCenter['signals']; -type WorkspaceItem = { - // provider id - provider: string; - // data exists locally - locally: boolean; -}; -type WorkspaceLoadEvent = WorkspaceItem & { - workspace: string; -}; - -export class DataCenter { - private readonly _apis: Apis; - private readonly _providers = new Map(); - private readonly _workspaces = new Map>(); - private readonly _config; - private readonly _logger; - public readonly workspacesList = new Workspaces(this); - - readonly signals = { - listAdd: new Signal(), - listRemove: new Signal(), - }; - - static async init(debug: boolean): Promise { - const dc = new DataCenter(debug); - dc.addProvider(AffineProvider); - dc.addProvider(LocalProvider); - - return dc; - } - - private constructor(debug: boolean) { - this._apis = getApis(); - this._config = getKVConfigure('sys'); - this._logger = getLogger('dc'); - this._logger.enabled = debug; - - this.signals.listAdd.on(e => { - this._config.set(`list:${e.workspace}`, { - provider: e.provider, - locally: e.locally, - }); - }); - this.signals.listRemove.on(workspace => { - this._config.delete(`list:${workspace}`); - }); - this.workspacesList.init(); - } - - get apis(): Readonly { - return this._apis; - } - - private addProvider(provider: typeof BaseProvider) { - this._providers.set(provider.id, provider); - } - - private async _getProvider( - id: string, - providerId = 'local' - ): Promise { - const providerKey = `${id}:provider`; - if (this._providers.has(providerId)) { - await this._config.set(providerKey, providerId); - return providerId; - } else { - const providerValue = await this._config.get(providerKey); - if (providerValue) return providerValue; - } - throw Error(`Provider ${providerId} not found`); - } - - private async _getWorkspace( - id: string, - params: LoadConfig - ): Promise { - this._logger(`Init workspace ${id} with ${params.providerId}`); - - const providerId = await this._getProvider(id, params.providerId); - - // init workspace & register block schema - const workspace = new Workspace({ room: id }).register(BlockSchema); - - const Provider = this._providers.get(providerId); - assert(Provider); - - // initial configurator - const config = getKVConfigure(`workspace:${id}`); - // set workspace configs - const values = Object.entries(params.config || {}); - if (values.length) await config.setMany(values); - - // init data by provider - const provider = new Provider(); - await provider.init({ - apis: this._apis, - config, - debug: this._logger.enabled, - logger: this._logger.extend(`${Provider.id}:${id}`), - signals: this.signals, - workspace, - }); - await provider.initData(); - this._logger(`Workspace ${id} loaded`); - - return provider; - } - - async auth(providerId: string, globalConfig?: Record) { - const Provider = this._providers.get(providerId); - if (Provider) { - // initial configurator - const config = getKVConfigure(`provider:${providerId}`); - // set workspace configs - const values = Object.entries(globalConfig || {}); - if (values.length) await config.setMany(values); - - const logger = this._logger.extend(`auth:${providerId}`); - logger.enabled = this._logger.enabled; - await Provider.auth(config, logger, this.signals); - } - } - - /** - * load workspace data to memory - * @param workspaceId workspace id - * @param config.providerId provider id - * @param config.config provider config - * @returns Workspace instance - */ - async load( - workspaceId: string, - params: LoadConfig = {} - ): Promise { - if (workspaceId) { - if (!this._workspaces.has(workspaceId)) { - this._workspaces.set( - workspaceId, - this._getWorkspace(workspaceId, params) - ); - } - const workspace = this._workspaces.get(workspaceId); - assert(workspace); - return workspace.then(w => w.workspace); - } - return null; - } - - /** - * destroy workspace's instance in memory - * @param workspaceId workspace id - */ - async destroy(workspaceId: string) { - const provider = await this._workspaces.get(workspaceId); - if (provider) { - this._workspaces.delete(workspaceId); - await provider.destroy(); - } - } - - /** - * reload new workspace instance to memory to refresh config - * @param workspaceId workspace id - * @param config.providerId provider id - * @param config.config provider config - * @returns Workspace instance - */ - async reload( - workspaceId: string, - config: LoadConfig = {} - ): Promise { - await this.destroy(workspaceId); - return this.load(workspaceId, config); - } - - /** - * get workspace list,return a map of workspace id and data state - * data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist - */ - async list(): Promise>> { - const listString = 'list:'; - const entries: [string, WorkspaceItem][] = await this._config.entries(); - return entries.reduce((acc, [k, i]) => { - if (k.startsWith(listString)) { - const key = k.slice(listString.length); - acc[key] = acc[key] || {}; - acc[key][i.provider] = i.locally; - } - return acc; - }, {} as Record>); - } - - /** - * delete local workspace's data - * @param workspaceId workspace id - */ - async delete(workspaceId: string) { - await this._config.delete(`${workspaceId}:provider`); - const provider = await this._workspaces.get(workspaceId); - if (provider) { - this._workspaces.delete(workspaceId); - // clear workspace data implement by provider - await provider.clear(); - } - } - - /** - * clear all local workspace's data - */ - async clear() { - const workspaces = await this.list(); - await Promise.all(Object.keys(workspaces).map(id => this.delete(id))); - } -} diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index d782c98bc0..28de94043c 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -5,7 +5,7 @@ import { LocalProvider } from './provider/local/local'; import { AffineProvider } from './provider'; import { Workspace as WS, WorkspaceMeta } from 'src/types'; import assert from 'assert'; -import { getLogger } from 'src'; +import { getLogger } from './logger'; import { BlockSchema } from '@blocksuite/blocks/models'; import { applyUpdate, encodeStateAsUpdate } from 'yjs'; @@ -231,7 +231,7 @@ export class DataCenter { const currentProvider = this.providerMap.get(workspaceInfo.provider); assert(currentProvider, 'Provider not found'); const newProvider = this.providerMap.get(provider); - assert(newProvider, 'AffineProvider is not registered'); + assert(newProvider, `${provider} provider is not registered`); const newWorkspace = await newProvider.createWorkspace({ name: workspaceInfo.name, avatar: workspaceInfo.avatar, diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts index fb7e2f0445..6fbb0e45db 100644 --- a/packages/data-center/src/index.ts +++ b/packages/data-center/src/index.ts @@ -1,5 +1,4 @@ -import debug from 'debug'; -import { DataCenter } from './dataCenter'; +import { DataCenter } from './datacenter'; const _initializeDataCenter = () => { let _dataCenterInstance: Promise; @@ -26,11 +25,6 @@ const _initializeDataCenter = () => { export const getDataCenter = _initializeDataCenter(); -export function getLogger(namespace: string) { - const logger = debug(namespace); - logger.log = console.log.bind(console); - return logger; -} - -export type { AccessTokenMessage } from './apis'; +export type { AccessTokenMessage } from './provider/affine/apis'; export type { Workspace } from './types'; +export { getLogger } from './logger'; diff --git a/packages/data-center/src/logger.ts b/packages/data-center/src/logger.ts new file mode 100644 index 0000000000..83d68b932e --- /dev/null +++ b/packages/data-center/src/logger.ts @@ -0,0 +1,7 @@ +import debug from 'debug'; + +export function getLogger(namespace: string) { + const logger = debug(namespace); + logger.log = console.log.bind(console); + return logger; +} diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index b534565b82..c3ec22ca99 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -8,16 +8,16 @@ import { inviteMember, removeMember, createWorkspace, -} from 'src/apis/workspace'; +} from './apis/workspace'; import { BaseProvider } from '../base'; -import { User, Workspace as WS, WorkspaceMeta } from 'src/types'; +import { User, Workspace as WS, WorkspaceMeta } from '../../types'; import { Workspace } from '@blocksuite/store'; import { BlockSchema } from '@blocksuite/blocks/models'; import { applyUpdate } from 'yjs'; -import { token, Callback } from 'src/apis'; +import { token, Callback } from './apis'; import { varStorage as storage } from 'lib0/storage'; import assert from 'assert'; -import { getAuthorizer } from 'src/apis/token'; +import { getAuthorizer } from './apis/token'; import { WebsocketProvider } from './sync'; import { IndexedDBProvider } from '../indexeddb'; diff --git a/packages/data-center/src/apis/business.ts b/packages/data-center/src/provider/affine/apis/business.ts similarity index 99% rename from packages/data-center/src/apis/business.ts rename to packages/data-center/src/provider/affine/apis/business.ts index 2612fedd9f..a43cac2418 100644 --- a/packages/data-center/src/apis/business.ts +++ b/packages/data-center/src/provider/affine/apis/business.ts @@ -1,7 +1,7 @@ import { uuidv4 } from '@blocksuite/store'; import { getDataCenter } from 'src'; import { DataCenter } from 'src/datacenter'; -import { Workspace } from '../types'; +// import { Workspace } from '../types'; // export class Business { // private _dc: DataCenter | undefined; diff --git a/packages/data-center/src/apis/index.ts b/packages/data-center/src/provider/affine/apis/index.ts similarity index 100% rename from packages/data-center/src/apis/index.ts rename to packages/data-center/src/provider/affine/apis/index.ts diff --git a/packages/data-center/src/apis/request.ts b/packages/data-center/src/provider/affine/apis/request.ts similarity index 100% rename from packages/data-center/src/apis/request.ts rename to packages/data-center/src/provider/affine/apis/request.ts diff --git a/packages/data-center/src/apis/token.ts b/packages/data-center/src/provider/affine/apis/token.ts similarity index 98% rename from packages/data-center/src/apis/token.ts rename to packages/data-center/src/provider/affine/apis/token.ts index 3bd1dd65aa..d7a2c34f87 100644 --- a/packages/data-center/src/apis/token.ts +++ b/packages/data-center/src/provider/affine/apis/token.ts @@ -2,8 +2,8 @@ import { initializeApp } from 'firebase/app'; import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import type { User } from 'firebase/auth'; -import { getLogger } from '../index.js'; -import { bareClient } from './request.js'; +import { getLogger } from '../../../logger'; +import { bareClient } from './request'; export interface AccessTokenMessage { create_at: number; diff --git a/packages/data-center/src/apis/user.ts b/packages/data-center/src/provider/affine/apis/user.ts similarity index 100% rename from packages/data-center/src/apis/user.ts rename to packages/data-center/src/provider/affine/apis/user.ts diff --git a/packages/data-center/src/apis/workspace.ts b/packages/data-center/src/provider/affine/apis/workspace.ts similarity index 100% rename from packages/data-center/src/apis/workspace.ts rename to packages/data-center/src/provider/affine/apis/workspace.ts diff --git a/packages/data-center/src/provider_bk/affine/index.ts b/packages/data-center/src/provider_bk/affine/index.ts deleted file mode 100644 index aad66af8d2..0000000000 --- a/packages/data-center/src/provider_bk/affine/index.ts +++ /dev/null @@ -1,175 +0,0 @@ -import assert from 'assert'; -import { applyUpdate, Doc } from 'yjs'; - -import type { - ConfigStore, - DataCenterSignals, - InitialParams, - Logger, -} from '../index.js'; -import { token, Callback, getApis } from '../../apis/index.js'; -import { LocalProvider } from '../local/index.js'; - -import { WebsocketProvider } from './sync.js'; -import { IndexedDBProvider } from '../local/indexeddb.js'; - -export class AffineProvider extends LocalProvider { - static id = 'affine'; - private _onTokenRefresh?: Callback = undefined; - private _ws?: WebsocketProvider; - - constructor() { - super(); - } - - async init(params: InitialParams) { - super.init(params); - - this._onTokenRefresh = () => { - if (token.refresh) { - this._config.set('token', token.refresh); - } - }; - assert(this._onTokenRefresh); - - token.onChange(this._onTokenRefresh); - - // initial login token - if (token.isExpired) { - try { - const refreshToken = await this._config.get('token'); - await token.refreshToken(refreshToken); - - if (token.refresh) { - this._config.set('token', token.refresh); - } - - assert(token.isLogin); - } catch (_) { - this._logger('Authorization failed, fallback to local mode'); - } - } else { - this._config.set('token', token.refresh); - } - } - - async destroy() { - if (this._onTokenRefresh) { - token.offChange(this._onTokenRefresh); - } - this._ws?.disconnect(); - } - - async initData() { - const databases = await indexedDB.databases(); - await super.initData( - // set locally to true if exists a same name db - databases - .map(db => db.name) - .filter(v => v) - .includes(this._workspace.room) - ); - - const workspace = this._workspace; - const doc = workspace.doc; - - this._logger(`Login: ${token.isLogin}`); - - if (workspace.room && token.isLogin) { - try { - // init data from cloud - await AffineProvider._initCloudDoc( - workspace.room, - doc, - this._logger, - this._signals - ); - - // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later - this._ws = new WebsocketProvider('/', workspace.room, doc); - await new Promise((resolve, reject) => { - // TODO: synced will also be triggered on reconnection after losing sync - // There needs to be an event mechanism to emit the synchronization state to the upper layer - assert(this._ws); - this._ws.once('synced', () => resolve()); - this._ws.once('lost-connection', () => resolve()); - this._ws.once('connection-error', () => reject()); - }); - this._signals.listAdd.emit({ - workspace: workspace.room, - provider: this.id, - locally: true, - }); - } catch (e) { - this._logger('Failed to init cloud workspace', e); - } - } - - // if after update, the space:meta is empty - // then we need to get map with doc - // just a workaround for yjs - doc.getMap('space:meta'); - } - - private static async _initCloudDoc( - workspace: string, - doc: Doc, - logger: Logger, - signals: DataCenterSignals - ) { - const apis = getApis(); - logger(`Loading ${workspace}...`); - const updates = await apis.downloadWorkspace(workspace); - if (updates) { - await new Promise(resolve => { - doc.once('update', resolve); - applyUpdate(doc, new Uint8Array(updates)); - }); - logger(`Loaded: ${workspace}`); - - // only add to list as online workspace - signals.listAdd.emit({ - workspace, - provider: this.id, - // at this time we always download full workspace - // but after we support sub doc, we can only download metadata - locally: false, - }); - } - } - - static async auth( - config: Readonly>, - logger: Logger, - signals: DataCenterSignals - ) { - const refreshToken = await config.get('token'); - if (refreshToken) { - await token.refreshToken(refreshToken); - if (token.isLogin && !token.isExpired) { - logger('check login success'); - // login success - return; - } - } - - logger('start login'); - // login with google - const apis = getApis(); - assert(apis.signInWithGoogle); - const user = await apis.signInWithGoogle(); - assert(user); - logger(`login success: ${user.name}`); - - // TODO: refresh local workspace data - const workspaces = await apis.getWorkspaces(); - await Promise.all( - workspaces.map(async ({ id }) => { - const doc = new Doc(); - const idb = new IndexedDBProvider(id, doc); - await idb.whenSynced; - await this._initCloudDoc(id, doc, logger, signals); - }) - ); - } -} diff --git a/packages/data-center/src/provider_bk/affine/sync.js b/packages/data-center/src/provider_bk/affine/sync.js deleted file mode 100644 index 09004a51d1..0000000000 --- a/packages/data-center/src/provider_bk/affine/sync.js +++ /dev/null @@ -1,508 +0,0 @@ -/* eslint-disable no-undef */ -/** - * @module provider/websocket - */ - -/* eslint-env browser */ - -// import * as Y from 'yjs'; // eslint-disable-line -import * as bc from 'lib0/broadcastchannel'; -import * as time from 'lib0/time'; -import * as encoding from 'lib0/encoding'; -import * as decoding from 'lib0/decoding'; -import * as syncProtocol from 'y-protocols/sync'; -import * as authProtocol from 'y-protocols/auth'; -import * as awarenessProtocol from 'y-protocols/awareness'; -import { Observable } from 'lib0/observable'; -import * as math from 'lib0/math'; -import * as url from 'lib0/url'; - -export const messageSync = 0; -export const messageQueryAwareness = 3; -export const messageAwareness = 1; -export const messageAuth = 2; - -/** - * encoder, decoder, provider, emitSynced, messageType - * @type {Array} - */ -const messageHandlers = []; - -messageHandlers[messageSync] = ( - encoder, - decoder, - provider, - emitSynced, - _messageType -) => { - encoding.writeVarUint(encoder, messageSync); - const syncMessageType = syncProtocol.readSyncMessage( - decoder, - encoder, - provider.doc, - provider - ); - if ( - emitSynced && - syncMessageType === syncProtocol.messageYjsSyncStep2 && - !provider.synced - ) { - provider.synced = true; - } -}; - -messageHandlers[messageQueryAwareness] = ( - encoder, - _decoder, - provider, - _emitSynced, - _messageType -) => { - encoding.writeVarUint(encoder, messageAwareness); - encoding.writeVarUint8Array( - encoder, - awarenessProtocol.encodeAwarenessUpdate( - provider.awareness, - Array.from(provider.awareness.getStates().keys()) - ) - ); -}; - -messageHandlers[messageAwareness] = ( - _encoder, - decoder, - provider, - _emitSynced, - _messageType -) => { - awarenessProtocol.applyAwarenessUpdate( - provider.awareness, - decoding.readVarUint8Array(decoder), - provider - ); -}; - -messageHandlers[messageAuth] = ( - _encoder, - decoder, - provider, - _emitSynced, - _messageType -) => { - authProtocol.readAuthMessage(decoder, provider.doc, (_ydoc, reason) => - permissionDeniedHandler(provider, reason) - ); -}; - -// @todo - this should depend on awareness.outdatedTime -const messageReconnectTimeout = 30000; - -/** - * @param {WebsocketProvider} provider - * @param {string} reason - */ -const permissionDeniedHandler = (provider, reason) => - console.warn(`Permission denied to access ${provider.url}.\n${reason}`); - -/** - * @param {WebsocketProvider} provider - * @param {Uint8Array} buf - * @param {boolean} emitSynced - * @return {encoding.Encoder} - */ -const readMessage = (provider, buf, emitSynced) => { - const decoder = decoding.createDecoder(buf); - const encoder = encoding.createEncoder(); - const messageType = decoding.readVarUint(decoder); - const messageHandler = provider.messageHandlers[messageType]; - if (/** @type {any} */ (messageHandler)) { - messageHandler(encoder, decoder, provider, emitSynced, messageType); - } else { - console.error('Unable to compute message'); - } - return encoder; -}; - -/** - * @param {WebsocketProvider} provider - */ -const setupWS = provider => { - if (provider.shouldConnect && provider.ws === null) { - const websocket = new provider._WS(provider.url); - websocket.binaryType = 'arraybuffer'; - provider.ws = websocket; - provider.wsconnecting = true; - provider.wsconnected = false; - provider.synced = false; - - websocket.onmessage = event => { - provider.wsLastMessageReceived = time.getUnixTime(); - const encoder = readMessage(provider, new Uint8Array(event.data), true); - if (encoding.length(encoder) > 1) { - websocket.send(encoding.toUint8Array(encoder)); - } - }; - websocket.onerror = event => { - provider.emit('connection-error', [event, provider]); - }; - websocket.onclose = event => { - provider.emit('connection-close', [event, provider]); - provider.ws = null; - provider.wsconnecting = false; - if (provider.wsconnected) { - provider.wsconnected = false; - provider.synced = false; - // update awareness (all users except local left) - awarenessProtocol.removeAwarenessStates( - provider.awareness, - Array.from(provider.awareness.getStates().keys()).filter( - client => client !== provider.doc.clientID - ), - provider - ); - provider.emit('status', [ - { - status: 'disconnected', - }, - ]); - } else { - provider.wsUnsuccessfulReconnects++; - } - // Start with no reconnect timeout and increase timeout by - // using exponential backoff starting with 100ms - setTimeout( - setupWS, - math.min( - math.pow(2, provider.wsUnsuccessfulReconnects) * 100, - provider.maxBackoffTime - ), - provider - ); - }; - websocket.onopen = () => { - provider.wsLastMessageReceived = time.getUnixTime(); - provider.wsconnecting = false; - provider.wsconnected = true; - provider.wsUnsuccessfulReconnects = 0; - provider.emit('status', [ - { - status: 'connected', - }, - ]); - // always send sync step 1 when connected - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageSync); - syncProtocol.writeSyncStep1(encoder, provider.doc); - websocket.send(encoding.toUint8Array(encoder)); - // broadcast local awareness state - if (provider.awareness.getLocalState() !== null) { - const encoderAwarenessState = encoding.createEncoder(); - encoding.writeVarUint(encoderAwarenessState, messageAwareness); - encoding.writeVarUint8Array( - encoderAwarenessState, - awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [ - provider.doc.clientID, - ]) - ); - websocket.send(encoding.toUint8Array(encoderAwarenessState)); - } - }; - - provider.emit('status', [ - { - status: 'connecting', - }, - ]); - } -}; - -/** - * @param {WebsocketProvider} provider - * @param {ArrayBuffer} buf - */ -const broadcastMessage = (provider, buf) => { - if (provider.wsconnected) { - /** @type {WebSocket} */ (provider.ws).send(buf); - } - if (provider.bcconnected) { - bc.publish(provider.bcChannel, buf, provider); - } -}; - -/** - * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. - * The document name is attached to the provided url. I.e. the following example - * creates a websocket connection to http://localhost:1234/my-document-name - * - * @example - * import * as Y from 'yjs' - * import { WebsocketProvider } from 'y-websocket' - * const doc = new Y.Doc() - * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) - * - * @extends {Observable} - */ -export class WebsocketProvider extends Observable { - /** - * @param {string} serverUrl - * @param {string} roomname - * @param {Y.Doc} doc - * @param {object} [opts] - * @param {boolean} [opts.connect] - * @param {awarenessProtocol.Awareness} [opts.awareness] - * @param {Object} [opts.params] - * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill - * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds - * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) - * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication - */ - constructor( - serverUrl, - roomname, - doc, - { - connect = true, - awareness = new awarenessProtocol.Awareness(doc), - params = {}, - WebSocketPolyfill = WebSocket, - resyncInterval = -1, - maxBackoffTime = 2500, - disableBc = false, - } = {} - ) { - super(); - // ensure that url is always ends with / - while (serverUrl[serverUrl.length - 1] === '/') { - serverUrl = serverUrl.slice(0, serverUrl.length - 1); - } - const encodedParams = url.encodeQueryParams(params); - this.maxBackoffTime = maxBackoffTime; - this.bcChannel = serverUrl + '/' + roomname; - this.url = - serverUrl + - '/' + - roomname + - (encodedParams.length === 0 ? '' : '?' + encodedParams); - this.roomname = roomname; - this.doc = doc; - this._WS = WebSocketPolyfill; - this.awareness = awareness; - this.wsconnected = false; - this.wsconnecting = false; - this.bcconnected = false; - this.disableBc = disableBc; - this.wsUnsuccessfulReconnects = 0; - this.messageHandlers = messageHandlers.slice(); - /** - * @type {boolean} - */ - this._synced = false; - /** - * @type {WebSocket?} - */ - this.ws = null; - this.wsLastMessageReceived = 0; - /** - * Whether to connect to other peers or not - * @type {boolean} - */ - this.shouldConnect = connect; - - /** - * @type {number} - */ - this._resyncInterval = 0; - if (resyncInterval > 0) { - this._resyncInterval = /** @type {any} */ ( - setInterval(() => { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - // resend sync step 1 - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageSync); - syncProtocol.writeSyncStep1(encoder, doc); - this.ws.send(encoding.toUint8Array(encoder)); - } - }, resyncInterval) - ); - } - - /** - * @param {ArrayBuffer} data - * @param {any} origin - */ - this._bcSubscriber = (data, origin) => { - if (origin !== this) { - const encoder = readMessage(this, new Uint8Array(data), false); - if (encoding.length(encoder) > 1) { - bc.publish(this.bcChannel, encoding.toUint8Array(encoder), this); - } - } - }; - /** - * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) - * @param {Uint8Array} update - * @param {any} origin - */ - this._updateHandler = (update, origin) => { - if (origin !== this) { - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageSync); - syncProtocol.writeUpdate(encoder, update); - broadcastMessage(this, encoding.toUint8Array(encoder)); - } - }; - this.doc.on('update', this._updateHandler); - /** - * @param {any} changed - * @param {any} _origin - */ - this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => { - const changedClients = added.concat(updated).concat(removed); - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageAwareness); - encoding.writeVarUint8Array( - encoder, - awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients) - ); - broadcastMessage(this, encoding.toUint8Array(encoder)); - }; - this._unloadHandler = () => { - awarenessProtocol.removeAwarenessStates( - this.awareness, - [doc.clientID], - 'window unload' - ); - }; - if (typeof window !== 'undefined') { - window.addEventListener('unload', this._unloadHandler); - } else if (typeof process !== 'undefined') { - process.on('exit', this._unloadHandler); - } - awareness.on('update', this._awarenessUpdateHandler); - this._checkInterval = /** @type {any} */ ( - setInterval(() => { - if ( - this.wsconnected && - messageReconnectTimeout < - time.getUnixTime() - this.wsLastMessageReceived - ) { - // no message received in a long time - not even your own awareness - // updates (which are updated every 15 seconds) - /** @type {WebSocket} */ (this.ws).close(); - } - }, messageReconnectTimeout / 10) - ); - if (connect) { - this.connect(); - } - } - - /** - * @type {boolean} - */ - get synced() { - return this._synced; - } - - set synced(state) { - if (this._synced !== state) { - this._synced = state; - this.emit('synced', [state]); - this.emit('sync', [state]); - } - } - - destroy() { - if (this._resyncInterval !== 0) { - clearInterval(this._resyncInterval); - } - clearInterval(this._checkInterval); - this.disconnect(); - if (typeof window !== 'undefined') { - window.removeEventListener('unload', this._unloadHandler); - } else if (typeof process !== 'undefined') { - process.off('exit', this._unloadHandler); - } - this.awareness.off('update', this._awarenessUpdateHandler); - this.doc.off('update', this._updateHandler); - super.destroy(); - } - - connectBc() { - if (this.disableBc) { - return; - } - if (!this.bcconnected) { - bc.subscribe(this.bcChannel, this._bcSubscriber); - this.bcconnected = true; - } - // send sync step1 to bc - // write sync step 1 - const encoderSync = encoding.createEncoder(); - encoding.writeVarUint(encoderSync, messageSync); - syncProtocol.writeSyncStep1(encoderSync, this.doc); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync), this); - // broadcast local state - const encoderState = encoding.createEncoder(); - encoding.writeVarUint(encoderState, messageSync); - syncProtocol.writeSyncStep2(encoderState, this.doc); - bc.publish(this.bcChannel, encoding.toUint8Array(encoderState), this); - // write queryAwareness - const encoderAwarenessQuery = encoding.createEncoder(); - encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness); - bc.publish( - this.bcChannel, - encoding.toUint8Array(encoderAwarenessQuery), - this - ); - // broadcast local awareness state - const encoderAwarenessState = encoding.createEncoder(); - encoding.writeVarUint(encoderAwarenessState, messageAwareness); - encoding.writeVarUint8Array( - encoderAwarenessState, - awarenessProtocol.encodeAwarenessUpdate(this.awareness, [ - this.doc.clientID, - ]) - ); - bc.publish( - this.bcChannel, - encoding.toUint8Array(encoderAwarenessState), - this - ); - } - - disconnectBc() { - // broadcast message with local awareness state set to null (indicating disconnect) - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, messageAwareness); - encoding.writeVarUint8Array( - encoder, - awarenessProtocol.encodeAwarenessUpdate( - this.awareness, - [this.doc.clientID], - new Map() - ) - ); - broadcastMessage(this, encoding.toUint8Array(encoder)); - if (this.bcconnected) { - bc.unsubscribe(this.bcChannel, this._bcSubscriber); - this.bcconnected = false; - } - } - - disconnect() { - this.shouldConnect = false; - this.disconnectBc(); - if (this.ws !== null) { - this.ws.close(); - } - } - - connect() { - this.shouldConnect = true; - if (!this.wsconnected && this.ws === null) { - setupWS(this); - this.connectBc(); - } - } -} diff --git a/packages/data-center/src/provider_bk/base.ts b/packages/data-center/src/provider_bk/base.ts deleted file mode 100644 index bd8d8a4e03..0000000000 --- a/packages/data-center/src/provider_bk/base.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import type { Workspace } from '@blocksuite/store'; - -import type { - Apis, - DataCenterSignals, - Logger, - InitialParams, - ConfigStore, -} from './index'; - -export class BaseProvider { - static id = 'base'; - protected _apis!: Readonly; - protected _config!: Readonly; - protected _logger!: Logger; - protected _signals!: DataCenterSignals; - protected _workspace!: Workspace; - - constructor() { - // Nothing to do here - } - - get id(): string { - return (this.constructor as any).id; - } - - async init(params: InitialParams) { - this._apis = params.apis; - this._config = params.config; - this._logger = params.logger; - this._signals = params.signals; - this._workspace = params.workspace; - this._logger.enabled = params.debug; - } - - async clear() { - await this.destroy(); - await this._config.clear(); - } - - async destroy() { - // Nothing to do here - } - - async initData() { - throw Error('Not implemented: initData'); - } - - // should return a blob url - async getBlob(_id: string): Promise { - throw Error('Not implemented: getBlob'); - } - - // should return a blob unique id - async setBlob(_blob: Blob): Promise { - throw Error('Not implemented: setBlob'); - } - - get workspace() { - return this._workspace; - } - - static async auth( - _config: Readonly, - logger: Logger, - _signals: DataCenterSignals - ) { - logger("This provider doesn't require authentication"); - } - - // get workspace list,return a map of workspace id and boolean - // if value is true, it exists locally, otherwise it does not exist locally - static async list( - _config: Readonly - ): Promise | undefined> { - throw Error('Not implemented: list'); - } -} diff --git a/packages/data-center/src/provider_bk/baseProvider.ts b/packages/data-center/src/provider_bk/baseProvider.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/data-center/src/provider_bk/index.ts b/packages/data-center/src/provider_bk/index.ts deleted file mode 100644 index e80bdf675f..0000000000 --- a/packages/data-center/src/provider_bk/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Workspace } from '@blocksuite/store'; - -import type { Apis } from '../apis'; -// import type { DataCenterSignals } from '../datacenter'; -import type { getLogger } from '../index'; -import type { ConfigStore } from '../store'; - -export type Logger = ReturnType; - -// export type InitialParams = { -// apis: Apis; -// config: Readonly; -// debug: boolean; -// logger: Logger; -// signals: DataCenterSignals; -// workspace: Workspace; -// }; - -// export type { Apis, ConfigStore, DataCenterSignals, Workspace }; -export type { BaseProvider } from './base.js'; -export { AffineProvider } from './affine/index.js'; -export { LocalProvider } from './local/index.js'; diff --git a/packages/data-center/src/provider_bk/local/index.ts b/packages/data-center/src/provider_bk/local/index.ts deleted file mode 100644 index 24bf9584a7..0000000000 --- a/packages/data-center/src/provider_bk/local/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { BlobStorage } from '@blocksuite/store'; -import assert from 'assert'; - -import type { ConfigStore, InitialParams } from '../index.js'; -import { BaseProvider } from '../base.js'; -import { IndexedDBProvider } from './indexeddb.js'; - -export class LocalProvider extends BaseProvider { - static id = 'local'; - private _blobs!: BlobStorage; - private _idb?: IndexedDBProvider = undefined; - - constructor() { - super(); - } - async init(params: InitialParams) { - super.init(params); - - const blobs = await this._workspace.blobs; - assert(blobs); - this._blobs = blobs; - } - - async initData(locally = true) { - assert(this._workspace.room); - this._logger('Loading local data'); - this._idb = new IndexedDBProvider( - this._workspace.room, - this._workspace.doc - ); - - await this._idb.whenSynced; - this._logger('Local data loaded'); - - this._signals.listAdd.emit({ - workspace: this._workspace.room, - provider: this.id, - locally, - }); - } - - async clear() { - assert(this._workspace.room); - await super.clear(); - await this._blobs.clear(); - await this._idb?.clearData(); - this._signals.listRemove.emit(this._workspace.room); - } - - async destroy(): Promise { - super.destroy(); - await this._idb?.destroy(); - } - - async getBlob(id: string): Promise { - return this._blobs.get(id); - } - - async setBlob(blob: Blob): Promise { - return this._blobs.set(blob); - } - - static async list( - config: Readonly> - ): Promise | undefined> { - const entries = await config.entries(); - return new Map( - entries - .filter(([key]) => key.startsWith('list:')) - .map(([key, value]) => [key.slice(5), value]) - ); - } -} diff --git a/packages/data-center/src/provider_bk/local/indexeddb.ts b/packages/data-center/src/provider_bk/local/indexeddb.ts deleted file mode 100644 index db7fab29d7..0000000000 --- a/packages/data-center/src/provider_bk/local/indexeddb.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as idb from 'lib0/indexeddb.js'; -import { Observable } from 'lib0/observable.js'; -import type { Doc } from 'yjs'; -import { applyUpdate, encodeStateAsUpdate, transact } from 'yjs'; - -const customStoreName = 'custom'; -const updatesStoreName = 'updates'; - -const PREFERRED_TRIM_SIZE = 500; - -const fetchUpdates = async (provider: IndexedDBProvider) => { - const [updatesStore] = idb.transact(provider.db as IDBDatabase, [ - updatesStoreName, - ]); // , 'readonly') - if (updatesStore) { - const updates = await idb.getAll( - updatesStore, - idb.createIDBKeyRangeLowerBound(provider._dbref, false) - ); - transact( - provider.doc, - () => { - updates.forEach(val => applyUpdate(provider.doc, val)); - }, - provider, - false - ); - const lastKey = await idb.getLastKey(updatesStore); - provider._dbref = lastKey + 1; - const cnt = await idb.count(updatesStore); - provider._dbsize = cnt; - } - return updatesStore; -}; - -const storeState = (provider: IndexedDBProvider, forceStore = true) => - fetchUpdates(provider).then(updatesStore => { - if ( - updatesStore && - (forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE) - ) { - idb - .addAutoKey(updatesStore, encodeStateAsUpdate(provider.doc)) - .then(() => - idb.del( - updatesStore, - idb.createIDBKeyRangeUpperBound(provider._dbref, true) - ) - ) - .then(() => - idb.count(updatesStore).then(cnt => { - provider._dbsize = cnt; - }) - ); - } - }); - -export class IndexedDBProvider extends Observable { - doc: Doc; - name: string; - _dbref: number; - _dbsize: number; - private _destroyed: boolean; - whenSynced: Promise; - db: IDBDatabase | null; - private _db: Promise; - private _storeTimeout: number; - private _storeTimeoutId: NodeJS.Timeout | null; - private _storeUpdate: (update: Uint8Array, origin: any) => void; - - constructor(name: string, doc: Doc) { - super(); - this.doc = doc; - this.name = name; - this._dbref = 0; - this._dbsize = 0; - this._destroyed = false; - this.db = null; - this._db = idb.openDB(name, db => - idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']]) - ); - - this.whenSynced = this._db.then(async db => { - this.db = db; - const currState = encodeStateAsUpdate(doc); - const updatesStore = await fetchUpdates(this); - if (updatesStore) { - await idb.addAutoKey(updatesStore, currState); - } - if (this._destroyed) { - return this; - } - this.emit('synced', [this]); - return this; - }); - - // Timeout in ms untill data is merged and persisted in idb. - this._storeTimeout = 1000; - - this._storeTimeoutId = null; - - this._storeUpdate = (update: Uint8Array, origin: any) => { - if (this.db && origin !== this) { - const [updatesStore] = idb.transact( - /** @type {IDBDatabase} */ this.db, - [updatesStoreName] - ); - if (updatesStore) { - idb.addAutoKey(updatesStore, update); - } - if (++this._dbsize >= PREFERRED_TRIM_SIZE) { - // debounce store call - if (this._storeTimeoutId !== null) { - clearTimeout(this._storeTimeoutId); - } - this._storeTimeoutId = setTimeout(() => { - storeState(this, false); - this._storeTimeoutId = null; - }, this._storeTimeout); - } - } - }; - doc.on('update', this._storeUpdate); - this.destroy = this.destroy.bind(this); - doc.on('destroy', this.destroy); - } - - override destroy() { - if (this._storeTimeoutId) { - clearTimeout(this._storeTimeoutId); - } - this.doc.off('update', this._storeUpdate); - this.doc.off('destroy', this.destroy); - this._destroyed = true; - return this._db.then(db => { - db.close(); - }); - } - - /** - * Destroys this instance and removes all data from indexeddb. - * - * @return {Promise} - */ - async clearData(): Promise { - return this.destroy().then(() => { - idb.deleteDB(this.name); - }); - } - - /** - * @param {String | number | ArrayBuffer | Date} key - * @return {Promise} - */ - async get( - key: string | number | ArrayBuffer | Date - ): Promise { - return this._db.then(db => { - const [custom] = idb.transact(db, [customStoreName], 'readonly'); - if (custom) { - return idb.get(custom, key); - } - return undefined; - }); - } - - /** - * @param {String | number | ArrayBuffer | Date} key - * @param {String | number | ArrayBuffer | Date} value - * @return {Promise} - */ - async set( - key: string | number | ArrayBuffer | Date, - value: string | number | ArrayBuffer | Date - ): Promise { - return this._db.then(db => { - const [custom] = idb.transact(db, [customStoreName]); - if (custom) { - return idb.put(custom, value, key); - } - return undefined; - }); - } - - /** - * @param {String | number | ArrayBuffer | Date} key - * @return {Promise} - */ - async del(key: string | number | ArrayBuffer | Date): Promise { - return this._db.then(db => { - const [custom] = idb.transact(db, [customStoreName]); - if (custom) { - return idb.del(custom, key); - } - return undefined; - }); - } -} diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index f6abafb873..b8ee7f5df8 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,8 +1,8 @@ -import { Workspace as WS } from 'src/types'; +import { Workspace as WS } from '../types'; import { Observable } from 'lib0/observable'; import { uuidv4 } from '@blocksuite/store'; -import { DataCenter } from 'src/dataCenterNew'; +import { DataCenter } from '../datacenter'; export class Workspaces extends Observable { private _workspaces: WS[]; From 153150cd3b5974c6da00c8198cda0e4d2fe2b281 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sun, 8 Jan 2023 16:48:28 +0800 Subject: [PATCH 22/32] feat/add get workspaces --- packages/data-center/src/datacenter.ts | 48 ++++++++++++------- packages/data-center/src/provider/base.ts | 2 +- .../data-center/src/workspaces/workspaces.ts | 4 +- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 28de94043c..032f96b89a 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -44,13 +44,27 @@ export class DataCenter { return Array.from(this.providerMap.values()); } + /** + * get workspaces list if focusUpdate is true, it will refresh workspaces + * @param {string} name workspace name + * @returns {Promise} + */ + public async getWorkspaces(focusUpdate = false) { + if (focusUpdate) { + this.workspaces.refreshWorkspaces(); + } + return this.workspaces.workspaces; + } + /** * create new workspace , new workspace is a local workspace * @param {string} name workspace name - * @returns + * @returns {Promise} */ public async createWorkspace(name: string) { const workspaceInfo = this.workspaces.addLocalWorkspace(name); + // IMP: create workspace in local provider + this.providerMap.get('local')?.createWorkspace({ name }); return workspaceInfo; } @@ -102,6 +116,7 @@ export class DataCenter { /** * load workspace instance by id * @param {string} workspaceId workspace id + * @returns {Promise} */ public async loadWorkspace(workspaceId: string) { const workspaceInfo = this.workspaces.getWorkspace(workspaceId); @@ -111,13 +126,11 @@ export class DataCenter { currentProvider.close(workspaceId); } const provider = this.providerMap.get(workspaceInfo.provider); - if (provider) { - this._logger(`Loading ${provider} workspace: `, workspaceId); - const workspace = this._getWorkspace(workspaceId); - this.currentWorkspace = await provider.warpWorkspace(workspace); - return this.currentWorkspace; - } - return workspaceInfo; + assert(provider, `provide '${workspaceInfo.provider}' is not registered`); + this._logger(`Loading ${workspaceInfo.provider} workspace: `, workspaceId); + const workspace = this._getWorkspace(workspaceId); + this.currentWorkspace = await provider.warpWorkspace(workspace); + return this.currentWorkspace; } /** @@ -127,7 +140,7 @@ export class DataCenter { public async getUserInfo(providerId = 'affine') { // XXX: maybe return all user info const provider = this.providerMap.get(providerId); - assert(provider, 'Provider not found'); + assert(provider, `provide '${providerId}' is not registered`); return provider.getUserInfo(); } @@ -219,28 +232,31 @@ export class DataCenter { private async _transWorkspaceProvider( workspace: Workspace, - provider: string + providerId: string ) { - assert(workspace.room, 'No workspace'); + assert(workspace.room, 'No workspace id'); const workspaceInfo = this.workspaces.getWorkspace(workspace.room); assert(workspaceInfo, 'Workspace not found'); - if (workspaceInfo.provider === provider) { + if (workspaceInfo.provider === providerId) { this._logger('Workspace provider is same'); return; } const currentProvider = this.providerMap.get(workspaceInfo.provider); assert(currentProvider, 'Provider not found'); - const newProvider = this.providerMap.get(provider); - assert(newProvider, `${provider} provider is not registered`); + const newProvider = this.providerMap.get(providerId); + assert(newProvider, `provide '${providerId}' is not registered`); + this._logger(`create ${providerId} workspace: `, workspaceInfo.name); const newWorkspace = await newProvider.createWorkspace({ name: workspaceInfo.name, avatar: workspaceInfo.avatar, }); assert(newWorkspace, 'Create workspace failed'); - // load doc to another workspace + this._logger( + `update workspace data from ${workspaceInfo.provider} to ${providerId}` + ); applyUpdate(newWorkspace.doc, encodeStateAsUpdate(workspace.doc)); assert(newWorkspace, 'Create workspace failed'); - currentProvider.delete(workspace.room); + await currentProvider.delete(workspace.room); this.workspaces.refreshWorkspaces(); } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index 95c98fe892..e9c568fa3c 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -41,7 +41,7 @@ export class BaseProvider { } /** - * warp workspace with workspace functions + * warp workspace with provider functions * @param workspace * @returns */ diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index b8ee7f5df8..ec107dbe69 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,8 +1,8 @@ -import { Workspace as WS } from '../types'; +import { Workspace as WS } from 'src/types'; import { Observable } from 'lib0/observable'; import { uuidv4 } from '@blocksuite/store'; -import { DataCenter } from '../datacenter'; +import { DataCenter } from 'src/datacenter'; export class Workspaces extends Observable { private _workspaces: WS[]; From d154a28ad182669afd123a25c860c080dec47daf Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sun, 8 Jan 2023 17:09:45 +0800 Subject: [PATCH 23/32] feat: add publish workspace --- packages/data-center/src/datacenter.ts | 28 ++++++++++++------- .../data-center/src/provider/affine/affine.ts | 5 ++++ packages/data-center/src/provider/base.ts | 6 ++++ .../data-center/src/workspaces/workspaces.ts | 4 +-- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 032f96b89a..a065228670 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -3,7 +3,7 @@ import { Workspace } from '@blocksuite/store'; import { BaseProvider } from './provider/base'; import { LocalProvider } from './provider/local/local'; import { AffineProvider } from './provider'; -import { Workspace as WS, WorkspaceMeta } from 'src/types'; +import type { Workspace as WS, WorkspaceMeta } from 'src/types'; import assert from 'assert'; import { getLogger } from './logger'; import { BlockSchema } from '@blocksuite/blocks/models'; @@ -136,6 +136,7 @@ export class DataCenter { /** * get user info by provider id * @param {string} providerId the provider name of workspace + * @returns {Promise} */ public async getUserInfo(providerId = 'affine') { // XXX: maybe return all user info @@ -146,6 +147,7 @@ export class DataCenter { /** * listen workspaces list change + * @param {Function} callback callback function */ public async onWorkspacesChange(callback: (workspaces: WS[]) => void) { this.workspaces.onWorkspacesChange(callback); @@ -171,7 +173,7 @@ export class DataCenter { w.doc.meta.setAvatar(avatar); update.avatar = avatar; } - this.workspaces.updateWorkspaceMeta(w.room, update); + this.workspaces.updateWorkspaceInfo(w.room, update); } /** @@ -184,13 +186,19 @@ export class DataCenter { assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - provider.close(workspaceId); - provider.leave(workspaceId); + await provider.close(workspaceId); + await provider.leave(workspaceId); } } - public async setWorkspacePublish() { - // TODO: set workspace publish + public async setWorkspacePublish(workspaceId: string, isPublish: boolean) { + const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); + if (provider) { + await provider.publish(workspaceId, isPublish); + this.workspaces.updateWorkspaceInfo(workspaceId, { isPublish }); + } } public async inviteMember(id: string, email: string) { @@ -198,7 +206,7 @@ export class DataCenter { assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - provider.invite(id, email); + await provider.invite(id, email); } } @@ -211,7 +219,7 @@ export class DataCenter { assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - provider.removeMember(permissionId); + await provider.removeMember(permissionId); } } @@ -227,7 +235,7 @@ export class DataCenter { assert(currentWorkspace, 'Workspace not found'); const provider = this.providerMap.get(currentWorkspace.provider); assert(provider, 'Provider not found'); - provider.close(currentWorkspace.id); + await provider.close(currentWorkspace.id); } private async _transWorkspaceProvider( @@ -261,7 +269,7 @@ export class DataCenter { } /** - * Enable workspace cloud flag + * Enable workspace cloud * @param {string} id ID of workspace. */ public async enableWorkspaceCloud( diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index c3ec22ca99..c1caa4048d 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -8,6 +8,7 @@ import { inviteMember, removeMember, createWorkspace, + updateWorkspace, } from './apis/workspace'; import { BaseProvider } from '../base'; import { User, Workspace as WS, WorkspaceMeta } from '../../types'; @@ -235,4 +236,8 @@ export class AffineProvider extends BaseProvider { this._logger('Local data loaded'); return nw; } + + public override async publish(id: string, isPublish: boolean): Promise { + await updateWorkspace({ id, public: isPublish }); + } } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index e9c568fa3c..904b04d067 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -125,6 +125,12 @@ export class BaseProvider { return; } + public async publish(id: string, isPublish: boolean): Promise { + id; + isPublish; + return; + } + public async createWorkspace( meta: WorkspaceMeta ): Promise { diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index ec107dbe69..4dc704e8b7 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -91,10 +91,10 @@ export class Workspaces extends Observable { return this._workspaces.some(w => w.id === id); } - public updateWorkspaceMeta(id: string, meta: Partial) { + public updateWorkspaceInfo(id: string, info: Partial) { const index = this._workspaces.findIndex(w => w.id === id); if (index >= 0) { - this._workspaces[index] = { ...this._workspaces[index], ...meta }; + this._workspaces[index] = { ...this._workspaces[index], ...info }; this._updateWorkspaces(this._workspaces); } } From 512c0a791e66a937ad062f812b60b0894160ff85 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sun, 8 Jan 2023 17:43:42 +0800 Subject: [PATCH 24/32] feat: add clear all workspaces --- packages/data-center/src/datacenter.ts | 10 ++++++++++ packages/data-center/src/provider/affine/affine.ts | 13 +++++++++++-- packages/data-center/src/provider/local/local.ts | 12 +++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index a065228670..d88762b436 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -279,6 +279,16 @@ export class DataCenter { return await this._transWorkspaceProvider(workspace, 'affine'); } + /** + * @deprecated + * clear all workspaces and data + */ + public async clear() { + for (const provider of this.providerMap.values()) { + await provider.clear(); + } + } + /** * Select a file to import the workspace * @param {File} file file of workspace. diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index c1caa4048d..c2bea657db 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -185,12 +185,21 @@ export class AffineProvider extends BaseProvider { } public override async delete(id: string): Promise { - // TODO delete workspace all local data + await this.close(id); + IndexedDBProvider.delete(id); await deleteWorkspace({ id }); } public override async clear(): Promise { - // TODO: clear all workspaces source + for (const w of this._workspacesCache.values()) { + if (w.room) { + try { + await this.delete(w.room); + } catch (e) { + this._logger('has a problem of delete workspace ', e); + } + } + } this._workspacesCache.clear(); } diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 58dfd7acd6..20ed420ba1 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -38,22 +38,23 @@ export class LocalProvider extends BaseProvider { return workspace; } - override loadWorkspaces() { + override loadWorkspaces(): Promise { const workspaceStr = storage.getItem(WORKSPACE_KEY); + let workspaces: WS[] = []; if (workspaceStr) { try { - return JSON.parse(workspaceStr); + workspaces = JSON.parse(workspaceStr) as WS[]; } catch (error) { this._logger(`Failed to parse workspaces from storage`); } } - return []; + return Promise.resolve(workspaces); } public override async delete(id: string): Promise { const index = this._workspacesList.findIndex(ws => ws.id === id); if (index !== -1) { - // TODO delete workspace all data + IndexedDBProvider.delete(id); this._workspacesList.splice(index, 1); this._storeWorkspaces(this._workspacesList); } else { @@ -75,7 +76,8 @@ export class LocalProvider extends BaseProvider { } public override async clear(): Promise { - // TODO: clear all data + const workspaces = await this.loadWorkspaces(); + workspaces.forEach(ws => IndexedDBProvider.delete(ws.id)); this._storeWorkspaces([]); } } From 375b5197da6867c3ef22daf9167aa02b92cc7e71 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Sun, 8 Jan 2023 18:42:35 +0800 Subject: [PATCH 25/32] feat: add default head img --- packages/data-center/src/datacenter.ts | 6 +++ .../data-center/src/provider/affine/affine.ts | 12 +++++- packages/data-center/src/provider/base.ts | 19 +++++++++- .../data-center/src/provider/local/local.ts | 27 ++++++++++++- packages/data-center/src/utils/index.ts | 38 +++++++++++++++++++ 5 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 packages/data-center/src/utils/index.ts diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index d88762b436..bec4794df4 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -173,6 +173,12 @@ export class DataCenter { w.doc.meta.setAvatar(avatar); update.avatar = avatar; } + // may run for change workspace meta + const workspaceInfo = this.workspaces.getWorkspace(w.room); + assert(workspaceInfo, 'Workspace not found'); + const provider = this.providerMap.get(workspaceInfo.provider); + provider?.updateWorkspaceMeta(w.room, update); + // update workspace list directly this.workspaces.updateWorkspaceInfo(w.room, update); } diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index c2bea657db..ae986cdb7c 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -21,6 +21,7 @@ import assert from 'assert'; import { getAuthorizer } from './apis/token'; import { WebsocketProvider } from './sync'; import { IndexedDBProvider } from '../indexeddb'; +import { getDefaultHeadImgBlob } from 'src/utils'; export class AffineProvider extends BaseProvider { public id = 'affine'; @@ -229,6 +230,7 @@ export class AffineProvider extends BaseProvider { idb = new IndexedDBProvider(workspace.room, workspace.doc); this._idbMap.set(workspace.room, idb); await idb.whenSynced; + this._logger('Local data loaded'); return idb; } @@ -236,13 +238,19 @@ export class AffineProvider extends BaseProvider { meta: WorkspaceMeta ): Promise { assert(meta.name, 'Workspace name is required'); - meta.avatar ?? (meta.avatar = ''); + if (!meta.avatar) { + // set default avatar + const blob = await getDefaultHeadImgBlob(meta.name); + meta.avatar = (await this.setBlob(blob)) || ''; + } const { id } = await createWorkspace(meta as Required); + this._logger('Creating affine workspace'); const nw = new Workspace({ room: id, }).register(BlockSchema); + nw.meta.setName(meta.name); + nw.meta.setAvatar(meta.avatar); this._initWorkspaceDb(nw); - this._logger('Local data loaded'); return nw; } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index 904b04d067..77cd9fdd3f 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -65,11 +65,11 @@ export class BaseProvider { } async getBlob(id: string): Promise { - return this._blobs.get(id); + return await this._blobs.get(id); } async setBlob(blob: Blob): Promise { - return this._blobs.set(blob); + return await this._blobs.set(blob); } /** @@ -131,6 +131,21 @@ export class BaseProvider { return; } + /** + * change workspace meta by workspace id , work for cached list in different provider + * @param id + * @param meta + * @returns + */ + public async updateWorkspaceMeta( + id: string, + meta: Partial + ): Promise { + id; + meta; + return; + } + public async createWorkspace( meta: WorkspaceMeta ): Promise { diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 20ed420ba1..1acc6db7dd 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -4,6 +4,7 @@ import { Workspace as WS, WorkspaceMeta } from 'src/types'; import { Workspace } from '@blocksuite/store'; import { IndexedDBProvider } from '../indexeddb'; import assert from 'assert'; +import { getDefaultHeadImgBlob } from 'src/utils'; const WORKSPACE_KEY = 'workspaces'; @@ -27,6 +28,7 @@ export class LocalProvider extends BaseProvider { idb?.destroy(); idb = new IndexedDBProvider(workspace.room, workspace.doc); this._idbMap.set(workspace.room, idb); + this._logger('Local data loaded'); return idb; } @@ -62,14 +64,35 @@ export class LocalProvider extends BaseProvider { } } + public override async updateWorkspaceMeta( + id: string, + meta: Partial + ) { + const index = this._workspacesList.findIndex(ws => ws.id === id); + if (index !== -1) { + const workspace = this._workspacesList[index]; + meta.name && (workspace.name = meta.name); + meta.avatar && (workspace.avatar = meta.avatar); + this._storeWorkspaces(this._workspacesList); + } else { + this._logger(`Failed to update workspace ${id}`); + } + } + public override async createWorkspace( meta: WorkspaceMeta ): Promise { assert(meta.name, 'Workspace name is required'); - meta.avatar ?? (meta.avatar = ''); + if (!meta.avatar) { + // set default avatar + const blob = await getDefaultHeadImgBlob(meta.name); + meta.avatar = (await this.setBlob(blob)) || ''; + } const workspaceInfos = this._workspaces.addLocalWorkspace(meta.name); + this._logger('Creating affine workspace'); const workspace = new Workspace({ room: workspaceInfos.id }); - // TODO: add avatar + workspace.meta.setName(meta.name); + workspace.meta.setAvatar(meta.avatar); this._storeWorkspaces([...this._workspacesList, workspaceInfos]); this._initWorkspaceDb(workspace); return workspace; diff --git a/packages/data-center/src/utils/index.ts b/packages/data-center/src/utils/index.ts new file mode 100644 index 0000000000..d1040b0246 --- /dev/null +++ b/packages/data-center/src/utils/index.ts @@ -0,0 +1,38 @@ +const DefaultHeadImgColors = [ + ['#C6F2F3', '#0C6066'], + ['#FFF5AB', '#896406'], + ['#FFCCA7', '#8F4500'], + ['#FFCECE', '#AF1212'], + ['#E3DEFF', '#511AAB'], +]; + +export async function getDefaultHeadImgBlob( + workspaceName: string +): Promise { + const canvas = document.createElement('canvas'); + canvas.height = 100; + canvas.width = 100; + const ctx = canvas.getContext('2d'); + return new Promise((resolve, reject) => { + if (ctx) { + const randomNumber = Math.floor(Math.random() * 5); + const randomColor = DefaultHeadImgColors[randomNumber]; + ctx.fillStyle = randomColor[0]; + ctx.fillRect(0, 0, 100, 100); + ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'"; + ctx.fillStyle = randomColor[1]; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(workspaceName[0], 50, 50); + canvas.toBlob(blob => { + if (blob) { + resolve(blob); + } else { + reject(); + } + }, 'image/png'); + } else { + reject(); + } + }); +} From d75e5fff926158831c6db18d9b67fca9b64b4b0c Mon Sep 17 00:00:00 2001 From: alt0 Date: Sun, 8 Jan 2023 19:14:21 +0800 Subject: [PATCH 26/32] fix: module path --- packages/data-center/src/datacenter.ts | 2 +- packages/data-center/src/provider/affine/affine.ts | 2 +- packages/data-center/src/provider/affine/apis/business.ts | 4 ++-- packages/data-center/src/provider/base.ts | 4 ++-- packages/data-center/src/provider/local/local.ts | 4 ++-- packages/data-center/src/workspaces/workspaces.ts | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index bec4794df4..bce67be90f 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -3,7 +3,7 @@ import { Workspace } from '@blocksuite/store'; import { BaseProvider } from './provider/base'; import { LocalProvider } from './provider/local/local'; import { AffineProvider } from './provider'; -import type { Workspace as WS, WorkspaceMeta } from 'src/types'; +import type { Workspace as WS, WorkspaceMeta } from './types'; import assert from 'assert'; import { getLogger } from './logger'; import { BlockSchema } from '@blocksuite/blocks/models'; diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index ae986cdb7c..25516386d0 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -21,7 +21,7 @@ import assert from 'assert'; import { getAuthorizer } from './apis/token'; import { WebsocketProvider } from './sync'; import { IndexedDBProvider } from '../indexeddb'; -import { getDefaultHeadImgBlob } from 'src/utils'; +import { getDefaultHeadImgBlob } from '../../utils'; export class AffineProvider extends BaseProvider { public id = 'affine'; diff --git a/packages/data-center/src/provider/affine/apis/business.ts b/packages/data-center/src/provider/affine/apis/business.ts index a43cac2418..11a4720851 100644 --- a/packages/data-center/src/provider/affine/apis/business.ts +++ b/packages/data-center/src/provider/affine/apis/business.ts @@ -1,6 +1,6 @@ import { uuidv4 } from '@blocksuite/store'; -import { getDataCenter } from 'src'; -import { DataCenter } from 'src/datacenter'; +// import { getDataCenter } from '.'; +// import { DataCenter } from './datacenter'; // import { Workspace } from '../types'; // export class Business { diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index 77cd9fdd3f..d1568dbbc6 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,6 +1,6 @@ import { BlobStorage, Workspace } from '@blocksuite/store'; -import { Logger, User, Workspace as WS, WorkspaceMeta } from 'src/types'; -import { Workspaces } from 'src/workspaces'; +import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types'; +import { Workspaces } from '../workspaces'; export class BaseProvider { public readonly id: string = 'base'; diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 1acc6db7dd..c741b9da06 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -1,10 +1,10 @@ import { BaseProvider } from '../base'; import { varStorage as storage } from 'lib0/storage'; -import { Workspace as WS, WorkspaceMeta } from 'src/types'; +import { Workspace as WS, WorkspaceMeta } from '../../types'; import { Workspace } from '@blocksuite/store'; import { IndexedDBProvider } from '../indexeddb'; import assert from 'assert'; -import { getDefaultHeadImgBlob } from 'src/utils'; +import { getDefaultHeadImgBlob } from '../../utils'; const WORKSPACE_KEY = 'workspaces'; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index 4dc704e8b7..f813f6b16d 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,8 +1,8 @@ -import { Workspace as WS } from 'src/types'; +import { Workspace as WS } from '../types'; import { Observable } from 'lib0/observable'; import { uuidv4 } from '@blocksuite/store'; -import { DataCenter } from 'src/datacenter'; +import { DataCenter } from '../datacenter'; export class Workspaces extends Observable { private _workspaces: WS[]; From 055b63382b2ce911565cc0f2fad063e6c5c3d416 Mon Sep 17 00:00:00 2001 From: alt0 Date: Mon, 9 Jan 2023 00:41:29 +0800 Subject: [PATCH 27/32] refactor: workspace crud --- packages/data-center/src/datacenter.ts | 105 +++++----- .../data-center/src/provider/affine/affine.ts | 35 +++- packages/data-center/src/provider/base.ts | 12 +- .../data-center/src/provider/local/local.ts | 52 +++-- packages/data-center/src/workspaces/index.ts | 1 + .../data-center/src/workspaces/workspaces.ts | 196 ++++++++++-------- .../src/workspaces/workspaces.ts.bak | 101 +++++++++ 7 files changed, 335 insertions(+), 167 deletions(-) create mode 100644 packages/data-center/src/workspaces/workspaces.ts.bak diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index bce67be90f..7db16c1ca0 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -1,4 +1,5 @@ import { Workspaces } from './workspaces'; +import type { WorkspacesChangeEvent } from './workspaces'; import { Workspace } from '@blocksuite/store'; import { BaseProvider } from './provider/base'; import { LocalProvider } from './provider/local/local'; @@ -14,9 +15,10 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs'; * @classdesc DataCenter is a data center, it can manage different providers for business */ export class DataCenter { - private readonly workspaces = new Workspaces(this); + private readonly _workspaces = new Workspaces(); private currentWorkspace: Workspace | null = null; private readonly _logger = getLogger('dc'); + private _defaultProvider?: BaseProvider; providerMap: Map = new Map(); constructor(debug: boolean) { @@ -28,32 +30,43 @@ export class DataCenter { // TODO: switch different provider dc.registerProvider(new LocalProvider()); dc.registerProvider(new AffineProvider()); - dc.workspaces.init(); return dc; } + /** + * Register provider. + * We will automatically set the first provider to default provider. + */ registerProvider(provider: BaseProvider) { + if (!this._defaultProvider) { + this._defaultProvider = provider; + } // inject data in provider - provider.inject({ logger: this._logger, workspaces: this.workspaces }); + provider.inject({ + logger: this._logger, + workspaces: this._workspaces.createScope(), + }); provider.init(); this.providerMap.set(provider.id, provider); } + setDefaultProvider(providerId: string) { + this._defaultProvider = this.providerMap.get(providerId); + } + get providers() { return Array.from(this.providerMap.values()); } - /** - * get workspaces list if focusUpdate is true, it will refresh workspaces - * @param {string} name workspace name - * @returns {Promise} - */ - public async getWorkspaces(focusUpdate = false) { - if (focusUpdate) { - this.workspaces.refreshWorkspaces(); - } - return this.workspaces.workspaces; + public get workspaces() { + return this._workspaces.workspaces; + } + + public async refreshWorkspaces() { + return Promise.allSettled( + Object.values(this.providerMap).map(provider => provider.loadWorkspaces()) + ); } /** @@ -61,11 +74,16 @@ export class DataCenter { * @param {string} name workspace name * @returns {Promise} */ - public async createWorkspace(name: string) { - const workspaceInfo = this.workspaces.addLocalWorkspace(name); - // IMP: create workspace in local provider - this.providerMap.get('local')?.createWorkspace({ name }); - return workspaceInfo; + public async createWorkspace(workspaceMeta: WorkspaceMeta) { + assert( + this._defaultProvider, + 'There is no provider. You should add provider first.' + ); + + const workspace = await this._defaultProvider.createWorkspace( + workspaceMeta + ); + return workspace; } /** @@ -73,14 +91,11 @@ export class DataCenter { * @param {string} workspaceId workspace id */ public async deleteWorkspace(workspaceId: string) { - const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + const workspaceInfo = this._workspaces.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); - if (provider && this.workspaces.hasWorkspace(workspaceId)) { - await provider.delete(workspaceId); - // may be refresh all workspaces - this.workspaces.delete(workspaceId); - } + assert(provider, `Workspace exists, but we couldn't find its provider.`); + await provider.deleteWorkspace(workspaceId); } /** @@ -119,11 +134,11 @@ export class DataCenter { * @returns {Promise} */ public async loadWorkspace(workspaceId: string) { - const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + const workspaceInfo = this._workspaces.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const currentProvider = this.providerMap.get(workspaceInfo.provider); if (currentProvider) { - currentProvider.close(workspaceId); + currentProvider.closeWorkspace(workspaceId); } const provider = this.providerMap.get(workspaceInfo.provider); assert(provider, `provide '${workspaceInfo.provider}' is not registered`); @@ -149,8 +164,10 @@ export class DataCenter { * listen workspaces list change * @param {Function} callback callback function */ - public async onWorkspacesChange(callback: (workspaces: WS[]) => void) { - this.workspaces.onWorkspacesChange(callback); + public async onWorkspacesChange( + callback: (workspaces: WorkspacesChangeEvent) => void + ) { + this._workspaces.on('change', callback); } /** @@ -166,20 +183,18 @@ export class DataCenter { assert(w?.room, 'No workspace to set meta'); const update: Partial = {}; if (name) { - w.doc.meta.setName(name); + w.meta.setName(name); update.name = name; } if (avatar) { - w.doc.meta.setAvatar(avatar); + w.meta.setAvatar(avatar); update.avatar = avatar; } // may run for change workspace meta - const workspaceInfo = this.workspaces.getWorkspace(w.room); + const workspaceInfo = this._workspaces.find(w.room); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); provider?.updateWorkspaceMeta(w.room, update); - // update workspace list directly - this.workspaces.updateWorkspaceInfo(w.room, update); } /** @@ -188,27 +203,26 @@ export class DataCenter { * @param id workspace id */ public async leaveWorkspace(workspaceId: string) { - const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + const workspaceInfo = this._workspaces.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { - await provider.close(workspaceId); - await provider.leave(workspaceId); + await provider.closeWorkspace(workspaceId); + await provider.leaveWorkspace(workspaceId); } } public async setWorkspacePublish(workspaceId: string, isPublish: boolean) { - const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + const workspaceInfo = this._workspaces.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { await provider.publish(workspaceId, isPublish); - this.workspaces.updateWorkspaceInfo(workspaceId, { isPublish }); } } public async inviteMember(id: string, email: string) { - const workspaceInfo = this.workspaces.getWorkspace(id); + const workspaceInfo = this._workspaces.find(id); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -221,7 +235,7 @@ export class DataCenter { * @param {number} permissionId permission id */ public async removeMember(workspaceId: string, permissionId: number) { - const workspaceInfo = this.workspaces.getWorkspace(workspaceId); + const workspaceInfo = this._workspaces.find(workspaceId); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); if (provider) { @@ -235,13 +249,11 @@ export class DataCenter { */ public async closeCurrentWorkspace() { assert(this.currentWorkspace?.room, 'No workspace to close'); - const currentWorkspace = this.workspaces.getWorkspace( - this.currentWorkspace.room - ); + const currentWorkspace = this._workspaces.find(this.currentWorkspace.room); assert(currentWorkspace, 'Workspace not found'); const provider = this.providerMap.get(currentWorkspace.provider); assert(provider, 'Provider not found'); - await provider.close(currentWorkspace.id); + await provider.closeWorkspace(currentWorkspace.id); } private async _transWorkspaceProvider( @@ -249,7 +261,7 @@ export class DataCenter { providerId: string ) { assert(workspace.room, 'No workspace id'); - const workspaceInfo = this.workspaces.getWorkspace(workspace.room); + const workspaceInfo = this._workspaces.find(workspace.room); assert(workspaceInfo, 'Workspace not found'); if (workspaceInfo.provider === providerId) { this._logger('Workspace provider is same'); @@ -270,8 +282,7 @@ export class DataCenter { ); applyUpdate(newWorkspace.doc, encodeStateAsUpdate(workspace.doc)); assert(newWorkspace, 'Create workspace failed'); - await currentProvider.delete(workspace.room); - this.workspaces.refreshWorkspaces(); + await currentProvider.deleteWorkspace(workspace.room); } /** diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 25516386d0..1651cba12d 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -119,9 +119,8 @@ export class AffineProvider extends BaseProvider { return Promise.resolve(null); } }); - await ( - await Promise.all(workspaceInstances) - ).forEach((workspace, i) => { + + (await Promise.all(workspaceInstances)).forEach((workspace, i) => { if (workspace) { workspaces[i] = { ...workspaces[i], @@ -159,6 +158,11 @@ export class AffineProvider extends BaseProvider { } } }); + + workspaces.forEach(workspace => { + this._workspaces.add(workspace); + }); + return workspaces; } @@ -185,17 +189,19 @@ export class AffineProvider extends BaseProvider { return this._user; } - public override async delete(id: string): Promise { - await this.close(id); + public override async deleteWorkspace(id: string): Promise { + await this.closeWorkspace(id); IndexedDBProvider.delete(id); await deleteWorkspace({ id }); + this._workspaces.remove(id); } public override async clear(): Promise { for (const w of this._workspacesCache.values()) { if (w.room) { try { - await this.delete(w.room); + await this.deleteWorkspace(w.room); + this._workspaces.remove(w.room); } catch (e) { this._logger('has a problem of delete workspace ', e); } @@ -204,14 +210,14 @@ export class AffineProvider extends BaseProvider { this._workspacesCache.clear(); } - public override async close(id: string) { + public override async closeWorkspace(id: string) { const idb = this._idbMap.get(id); idb?.destroy(); const ws = this._wsMap.get(id); ws?.disconnect(); } - public override async leave(id: string): Promise { + public override async leaveWorkspace(id: string): Promise { await leaveWorkspace({ id }); } @@ -251,6 +257,19 @@ export class AffineProvider extends BaseProvider { nw.meta.setName(meta.name); nw.meta.setAvatar(meta.avatar); this._initWorkspaceDb(nw); + + const workspaceInfo: WS = { + name: meta.name, + id, + isPublish: false, + avatar: '', + owner: undefined, + isLocal: true, + memberCount: 1, + provider: 'local', + }; + + this._workspaces.add(workspaceInfo); return nw; } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index d1568dbbc6..dbf24df431 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,10 +1,10 @@ import { BlobStorage, Workspace } from '@blocksuite/store'; import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types'; -import { Workspaces } from '../workspaces'; +import type { WorkspacesScope } from '../workspaces'; export class BaseProvider { public readonly id: string = 'base'; - protected _workspaces!: Workspaces; + protected _workspaces!: WorkspacesScope; protected _logger!: Logger; protected _blobs!: BlobStorage; @@ -13,7 +13,7 @@ export class BaseProvider { workspaces, }: { logger: Logger; - workspaces: Workspaces; + workspaces: WorkspacesScope; }) { this._logger = logger; this._workspaces = workspaces; @@ -83,7 +83,7 @@ export class BaseProvider { * delete workspace include all data * @param id workspace id */ - public async delete(id: string): Promise { + public async deleteWorkspace(id: string): Promise { id; return; } @@ -92,7 +92,7 @@ export class BaseProvider { * leave workspace by workspace id * @param id workspace id */ - public async leave(id: string): Promise { + public async leaveWorkspace(id: string): Promise { id; return; } @@ -101,7 +101,7 @@ export class BaseProvider { * close db link and websocket connection and other resources * @param id workspace id */ - public async close(id: string) { + public async closeWorkspace(id: string) { id; return; } diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index c741b9da06..0a383a05bd 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -1,7 +1,7 @@ import { BaseProvider } from '../base'; import { varStorage as storage } from 'lib0/storage'; import { Workspace as WS, WorkspaceMeta } from '../../types'; -import { Workspace } from '@blocksuite/store'; +import { Workspace, uuidv4 } from '@blocksuite/store'; import { IndexedDBProvider } from '../indexeddb'; import assert from 'assert'; import { getDefaultHeadImgBlob } from '../../utils'; @@ -11,11 +11,10 @@ const WORKSPACE_KEY = 'workspaces'; export class LocalProvider extends BaseProvider { public id = 'local'; private _idbMap: Map = new Map(); - private _workspacesList: WS[] = []; constructor() { super(); - this._workspacesList = []; + this.loadWorkspaces(); } private _storeWorkspaces(workspaces: WS[]) { @@ -46,6 +45,9 @@ export class LocalProvider extends BaseProvider { if (workspaceStr) { try { workspaces = JSON.parse(workspaceStr) as WS[]; + workspaces.forEach(workspace => { + this._workspaces.add(workspace); + }); } catch (error) { this._logger(`Failed to parse workspaces from storage`); } @@ -53,12 +55,12 @@ export class LocalProvider extends BaseProvider { return Promise.resolve(workspaces); } - public override async delete(id: string): Promise { - const index = this._workspacesList.findIndex(ws => ws.id === id); - if (index !== -1) { + public override async deleteWorkspace(id: string): Promise { + const workspace = this._workspaces.get(id); + if (workspace) { IndexedDBProvider.delete(id); - this._workspacesList.splice(index, 1); - this._storeWorkspaces(this._workspacesList); + this._workspaces.remove(id); + this._storeWorkspaces(this._workspaces.list()); } else { this._logger(`Failed to delete workspace ${id}`); } @@ -68,15 +70,8 @@ export class LocalProvider extends BaseProvider { id: string, meta: Partial ) { - const index = this._workspacesList.findIndex(ws => ws.id === id); - if (index !== -1) { - const workspace = this._workspacesList[index]; - meta.name && (workspace.name = meta.name); - meta.avatar && (workspace.avatar = meta.avatar); - this._storeWorkspaces(this._workspacesList); - } else { - this._logger(`Failed to update workspace ${id}`); - } + this._workspaces.update(id, meta); + this._storeWorkspaces(this._workspaces.list()); } public override async createWorkspace( @@ -88,13 +83,27 @@ export class LocalProvider extends BaseProvider { const blob = await getDefaultHeadImgBlob(meta.name); meta.avatar = (await this.setBlob(blob)) || ''; } - const workspaceInfos = this._workspaces.addLocalWorkspace(meta.name); this._logger('Creating affine workspace'); - const workspace = new Workspace({ room: workspaceInfos.id }); + + const workspaceInfo: WS = { + name: meta.name, + id: uuidv4(), + isPublish: false, + avatar: '', + owner: undefined, + isLocal: true, + memberCount: 1, + provider: 'local', + }; + + const workspace = new Workspace({ room: workspaceInfo.id }); + this._initWorkspaceDb(workspace); workspace.meta.setName(meta.name); workspace.meta.setAvatar(meta.avatar); - this._storeWorkspaces([...this._workspacesList, workspaceInfos]); - this._initWorkspaceDb(workspace); + + this._workspaces.add(workspaceInfo); + this._storeWorkspaces(this._workspaces.list()); + return workspace; } @@ -102,5 +111,6 @@ export class LocalProvider extends BaseProvider { const workspaces = await this.loadWorkspaces(); workspaces.forEach(ws => IndexedDBProvider.delete(ws.id)); this._storeWorkspaces([]); + this._workspaces.clear(); } } diff --git a/packages/data-center/src/workspaces/index.ts b/packages/data-center/src/workspaces/index.ts index 05a26f8e2b..41419a6eb4 100644 --- a/packages/data-center/src/workspaces/index.ts +++ b/packages/data-center/src/workspaces/index.ts @@ -1 +1,2 @@ export { Workspaces } from './workspaces'; +export type { WorkspacesScope, WorkspacesChangeEvent } from './workspaces'; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index f813f6b16d..2d8661b2bb 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -1,101 +1,127 @@ -import { Workspace as WS } from '../types'; - import { Observable } from 'lib0/observable'; -import { uuidv4 } from '@blocksuite/store'; -import { DataCenter } from '../datacenter'; +import type { Workspace, WorkspaceMeta } from '../types'; -export class Workspaces extends Observable { - private _workspaces: WS[]; - private readonly _dc: DataCenter; +export interface WorkspacesScope { + get: (workspaceId: string) => Workspace | undefined; + list: () => Workspace[]; + add: (workspace: Workspace) => void; + remove: (workspaceId: string) => boolean; + clear: () => void; + update: (workspaceId: string, workspaceMeta: Partial) => void; +} - constructor(dc: DataCenter) { - super(); - this._workspaces = []; - this._dc = dc; +export interface WorkspacesChangeEvent { + added?: Workspace; + deleted?: Workspace; + updated?: Workspace; +} + +export class Workspaces extends Observable<'change'> { + private _workspacesMap = new Map(); + + get workspaces(): Workspace[] { + return Object.values(this._workspacesMap); } - public init() { - this._loadWorkspaces(); + find(workspaceId: string) { + return this._workspacesMap.get(workspaceId); } - get workspaces() { - return this._workspaces; - } + createScope(): WorkspacesScope { + const scopedWorkspaceIds = new Set(); - /** - * emit when workspaces changed - * @param {(workspace: WS[]) => void} cb - */ - onWorkspacesChange(cb: (workspace: WS[]) => void) { - this.on('change', cb); - } + const get = (workspaceId: string) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return; + } + return this._workspacesMap.get(workspaceId); + }; - private async _loadWorkspaces() { - const providers = this._dc.providers; - let workspaces: WS[] = []; - providers.forEach(async p => { - const pWorkspaces = await p.loadWorkspaces(); - workspaces = [...workspaces, ...pWorkspaces]; - this._updateWorkspaces([...workspaces, ...pWorkspaces]); - }); - } + const add = (workspace: Workspace) => { + if (this._workspacesMap.has(workspace.id)) { + throw new Error(`Duplicate workspace id.`); + } + this._workspacesMap.set(workspace.id, workspace); + scopedWorkspaceIds.add(workspace.id); - /** - * focus load all workspaces list - */ - public async refreshWorkspaces() { - this._loadWorkspaces(); - } + this.emit('change', [ + { + added: workspace, + } as WorkspacesChangeEvent, + ]); + }; - private _updateWorkspaces(workspaces: WS[]) { - this._workspaces = workspaces; - this.emit('change', this._workspaces); - } + const remove = (workspaceId: string) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return true; + } + + const workspace = this._workspacesMap.get(workspaceId); + if (workspace) { + const ret = this._workspacesMap.delete(workspaceId); + // If deletion failed, return. + if (!ret) { + return ret; + } + + scopedWorkspaceIds.delete(workspaceId); + + this.emit('change', [ + { + deleted: workspace, + } as WorkspacesChangeEvent, + ]); + } + return true; + }; + + const clear = () => { + scopedWorkspaceIds.forEach(id => { + remove(id); + }); + }; + + const update = ( + workspaceId: string, + workspaceMeta: Partial + ) => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return true; + } + + const workspace = this._workspacesMap.get(workspaceId); + if (!workspace) { + return true; + } + + this._workspacesMap.set(workspaceId, { ...workspace, ...workspaceMeta }); + + this.emit('change', [ + { + updated: this._workspacesMap.get(workspaceId), + } as WorkspacesChangeEvent, + ]); + }; + + // TODO: need to optimize + const list = () => { + const workspaces: Workspace[] = []; + scopedWorkspaceIds.forEach(id => { + const workspace = this._workspacesMap.get(id); + if (workspace) { + workspaces.push(workspace); + } + }); + return workspaces; + }; - private _getDefaultWorkspace(name: string): WS { return { - name, - id: uuidv4(), - isPublish: false, - avatar: '', - owner: undefined, - isLocal: true, - memberCount: 1, - provider: 'local', + get, + list, + add, + remove, + clear, + update, }; } - - /** add a local workspaces */ - public addLocalWorkspace(name: string) { - const workspace = this._getDefaultWorkspace(name); - this._updateWorkspaces([...this._workspaces, workspace]); - return workspace; - } - - /** delete a workspaces by id */ - public delete(id: string) { - const index = this._workspaces.findIndex(w => w.id === id); - if (index >= 0) { - this._workspaces.splice(index, 1); - this._updateWorkspaces(this._workspaces); - } - } - - /** get workspace info by id */ - public getWorkspace(id: string) { - return this._workspaces.find(w => w.id === id); - } - - /** check if workspace exists */ - public hasWorkspace(id: string) { - return this._workspaces.some(w => w.id === id); - } - - public updateWorkspaceInfo(id: string, info: Partial) { - const index = this._workspaces.findIndex(w => w.id === id); - if (index >= 0) { - this._workspaces[index] = { ...this._workspaces[index], ...info }; - this._updateWorkspaces(this._workspaces); - } - } } diff --git a/packages/data-center/src/workspaces/workspaces.ts.bak b/packages/data-center/src/workspaces/workspaces.ts.bak new file mode 100644 index 0000000000..f813f6b16d --- /dev/null +++ b/packages/data-center/src/workspaces/workspaces.ts.bak @@ -0,0 +1,101 @@ +import { Workspace as WS } from '../types'; + +import { Observable } from 'lib0/observable'; +import { uuidv4 } from '@blocksuite/store'; +import { DataCenter } from '../datacenter'; + +export class Workspaces extends Observable { + private _workspaces: WS[]; + private readonly _dc: DataCenter; + + constructor(dc: DataCenter) { + super(); + this._workspaces = []; + this._dc = dc; + } + + public init() { + this._loadWorkspaces(); + } + + get workspaces() { + return this._workspaces; + } + + /** + * emit when workspaces changed + * @param {(workspace: WS[]) => void} cb + */ + onWorkspacesChange(cb: (workspace: WS[]) => void) { + this.on('change', cb); + } + + private async _loadWorkspaces() { + const providers = this._dc.providers; + let workspaces: WS[] = []; + providers.forEach(async p => { + const pWorkspaces = await p.loadWorkspaces(); + workspaces = [...workspaces, ...pWorkspaces]; + this._updateWorkspaces([...workspaces, ...pWorkspaces]); + }); + } + + /** + * focus load all workspaces list + */ + public async refreshWorkspaces() { + this._loadWorkspaces(); + } + + private _updateWorkspaces(workspaces: WS[]) { + this._workspaces = workspaces; + this.emit('change', this._workspaces); + } + + private _getDefaultWorkspace(name: string): WS { + return { + name, + id: uuidv4(), + isPublish: false, + avatar: '', + owner: undefined, + isLocal: true, + memberCount: 1, + provider: 'local', + }; + } + + /** add a local workspaces */ + public addLocalWorkspace(name: string) { + const workspace = this._getDefaultWorkspace(name); + this._updateWorkspaces([...this._workspaces, workspace]); + return workspace; + } + + /** delete a workspaces by id */ + public delete(id: string) { + const index = this._workspaces.findIndex(w => w.id === id); + if (index >= 0) { + this._workspaces.splice(index, 1); + this._updateWorkspaces(this._workspaces); + } + } + + /** get workspace info by id */ + public getWorkspace(id: string) { + return this._workspaces.find(w => w.id === id); + } + + /** check if workspace exists */ + public hasWorkspace(id: string) { + return this._workspaces.some(w => w.id === id); + } + + public updateWorkspaceInfo(id: string, info: Partial) { + const index = this._workspaces.findIndex(w => w.id === id); + if (index >= 0) { + this._workspaces[index] = { ...this._workspaces[index], ...info }; + this._updateWorkspaces(this._workspaces); + } + } +} From e1e1d0c9647fe59dcf49d920773b27acb15d3a96 Mon Sep 17 00:00:00 2001 From: alt0 Date: Mon, 9 Jan 2023 00:48:54 +0800 Subject: [PATCH 28/32] test: add local provider test --- jest.config.js | 19 - package.json | 20 +- .../app/scripts/__tests__/printer.spec.ts | 2 +- .../src/utils/__tests__/get-is-mobile.spec.ts | 2 +- packages/data-center/src/datacenter.ts | 20 +- .../data-center/src/provider/affine/affine.ts | 5 +- packages/data-center/src/provider/base.ts | 19 +- .../src/provider/local/local.spec.ts | 49 + .../data-center/src/provider/local/local.ts | 5 +- packages/data-center/src/types/index.ts | 2 +- .../data-center/src/workspaces/workspaces.ts | 2 +- packages/data-center/tests/cloud/auth.spec.ts | 15 - .../tests/cloud/collaborate.spec.ts | 15 - .../tests/cloud/permission.spec.ts | 17 - .../data-center/tests/cloud/share.spec.ts | 15 - packages/data-center/tests/cloud/sync.spec.ts | 23 - .../tests/local/attachment.spec.ts | 13 - .../tests/local/import-export.spec.ts | 11 - packages/data-center/tests/local/init.spec.ts | 47 - .../data-center/tests/local/search.spec.ts | 23 - .../data-center/tests/local/workspace.spec.ts | 64 - packages/data-center/tests/utils.ts | 9 - pnpm-lock.yaml | 1667 +++++------------ vitest.config.ts | 7 + 24 files changed, 565 insertions(+), 1506 deletions(-) delete mode 100644 jest.config.js create mode 100644 packages/data-center/src/provider/local/local.spec.ts delete mode 100644 packages/data-center/tests/cloud/auth.spec.ts delete mode 100644 packages/data-center/tests/cloud/collaborate.spec.ts delete mode 100644 packages/data-center/tests/cloud/permission.spec.ts delete mode 100644 packages/data-center/tests/cloud/share.spec.ts delete mode 100644 packages/data-center/tests/cloud/sync.spec.ts delete mode 100644 packages/data-center/tests/local/attachment.spec.ts delete mode 100644 packages/data-center/tests/local/import-export.spec.ts delete mode 100644 packages/data-center/tests/local/init.spec.ts delete mode 100644 packages/data-center/tests/local/search.spec.ts delete mode 100644 packages/data-center/tests/local/workspace.spec.ts delete mode 100644 packages/data-center/tests/utils.ts create mode 100644 vitest.config.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 4423f5ad0b..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - projects: ['/packages/app'], - preset: 'ts-jest', - testEnvironment: 'node', - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - transform: { - // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` - // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` - '^.+\\.tsx?$': [ - 'ts-jest', - { - useESM: true, - }, - ], - }, -}; diff --git a/package.json b/package.json index 9f4028cb70..624a14bee0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test": "playwright test", "test:dc": "pnpm --filter @affine/datacenter test", "test:e2e:codegen": "npx playwright codegen http://localhost:8080", - "test:unit": "jest", + "test:unit": "vitest run", + "test:unit:watch": "vitest", "postinstall": "husky install", "notify": "node --experimental-modules scripts/notify.mjs", "check:ci": "pnpm lint & pnpm test" @@ -24,24 +25,23 @@ "*.{ts,tsx,js,jsx}": "npx eslint --cache --fix" }, "devDependencies": { - "@jest/globals": "^29.3.1", + "@changesets/cli": "^2.26.0", "@playwright/test": "^1.29.1", - "@typescript-eslint/eslint-plugin": "^5.47.0", - "@typescript-eslint/parser": "^5.47.0", "@types/eslint": "^8.4.10", "@types/node": "^18.11.17", + "@typescript-eslint/eslint-plugin": "^5.47.0", + "@typescript-eslint/parser": "^5.47.0", "eslint": "^8.30.0", "eslint-config-next": "12.3.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "fake-indexeddb": "4.0.1", "got": "^12.5.3", - "jest": "^29.3.1", - "prettier": "^2.7.1", - "ts-jest": "^29.0.3", - "typescript": "^4.9.3", - "lint-staged": "^13.1.0", "husky": "^8.0.2", - "@changesets/cli": "^2.26.0" + "lint-staged": "^13.1.0", + "prettier": "^2.7.1", + "typescript": "^4.9.3", + "vitest": "^0.26.3" }, "eslintConfig": { "root": true, diff --git a/packages/app/scripts/__tests__/printer.spec.ts b/packages/app/scripts/__tests__/printer.spec.ts index beb14114e6..7f67223c1d 100644 --- a/packages/app/scripts/__tests__/printer.spec.ts +++ b/packages/app/scripts/__tests__/printer.spec.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from '@jest/globals'; +import { describe, test, expect } from 'vitest'; import { printer } from './../printer'; const chalk = require('chalk'); describe('printer', () => { diff --git a/packages/app/src/utils/__tests__/get-is-mobile.spec.ts b/packages/app/src/utils/__tests__/get-is-mobile.spec.ts index bef77b03ba..c5df2c6a63 100644 --- a/packages/app/src/utils/__tests__/get-is-mobile.spec.ts +++ b/packages/app/src/utils/__tests__/get-is-mobile.spec.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from '@jest/globals'; +import { describe, test, expect } from 'vitest'; import { isMobile } from '../get-is-mobile'; describe('get-is-mobile', () => { diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 7db16c1ca0..c859ad7d02 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -28,8 +28,18 @@ export class DataCenter { static async init(debug: boolean): Promise { const dc = new DataCenter(debug); // TODO: switch different provider - dc.registerProvider(new LocalProvider()); - dc.registerProvider(new AffineProvider()); + dc.registerProvider( + new LocalProvider({ + logger: dc._logger, + workspaces: dc._workspaces.createScope(), + }) + ); + dc.registerProvider( + new AffineProvider({ + logger: dc._logger, + workspaces: dc._workspaces.createScope(), + }) + ); return dc; } @@ -42,11 +52,7 @@ export class DataCenter { if (!this._defaultProvider) { this._defaultProvider = provider; } - // inject data in provider - provider.inject({ - logger: this._logger, - workspaces: this._workspaces.createScope(), - }); + provider.init(); this.providerMap.set(provider.id, provider); } diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 1651cba12d..b9f168a328 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -11,6 +11,7 @@ import { updateWorkspace, } from './apis/workspace'; import { BaseProvider } from '../base'; +import type { ProviderConstructorParams } from '../base'; import { User, Workspace as WS, WorkspaceMeta } from '../../types'; import { Workspace } from '@blocksuite/store'; import { BlockSchema } from '@blocksuite/blocks/models'; @@ -32,8 +33,8 @@ export class AffineProvider extends BaseProvider { private _wsMap: Map = new Map(); private _idbMap: Map = new Map(); - constructor() { - super(); + constructor(params: ProviderConstructorParams) { + super(params); } override async init() { diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index dbf24df431..40572a136a 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -2,20 +2,23 @@ import { BlobStorage, Workspace } from '@blocksuite/store'; import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types'; import type { WorkspacesScope } from '../workspaces'; +const defaultLogger = () => { + return; +}; + +export interface ProviderConstructorParams { + logger?: Logger; + workspaces: WorkspacesScope; +} + export class BaseProvider { public readonly id: string = 'base'; protected _workspaces!: WorkspacesScope; protected _logger!: Logger; protected _blobs!: BlobStorage; - public inject({ - logger, - workspaces, - }: { - logger: Logger; - workspaces: WorkspacesScope; - }) { - this._logger = logger; + public constructor({ logger, workspaces }: ProviderConstructorParams) { + this._logger = (logger || defaultLogger) as Logger; this._workspaces = workspaces; } diff --git a/packages/data-center/src/provider/local/local.spec.ts b/packages/data-center/src/provider/local/local.spec.ts new file mode 100644 index 0000000000..442263c070 --- /dev/null +++ b/packages/data-center/src/provider/local/local.spec.ts @@ -0,0 +1,49 @@ +import { describe, test, expect } from 'vitest'; +import { Workspaces } from '../../workspaces'; +import { LocalProvider } from './local'; +import 'fake-indexeddb/auto'; + +describe('local provider', () => { + const workspaces = new Workspaces(); + const provider = new LocalProvider({ + workspaces: workspaces.createScope(), + }); + + const workspaceName = 'workspace-test'; + let workspaceId: string | undefined; + + test('create workspace', async () => { + const w = await provider.createWorkspace({ + name: workspaceName, + avatar: 'avatar-url-test', + }); + workspaceId = w?.room; + + expect(workspaces.workspaces.length).toEqual(1); + expect(workspaces.workspaces[0].name).toEqual(workspaceName); + }); + + test('workspace list cache', async () => { + const workspaces1 = new Workspaces(); + const provider1 = new LocalProvider({ + workspaces: workspaces1.createScope(), + }); + await provider1.loadWorkspaces(); + expect(workspaces1.workspaces.length).toEqual(1); + expect(workspaces1.workspaces[0].name).toEqual(workspaceName); + expect(workspaces1.workspaces[0].id).toEqual(workspaceId); + }); + + test('update workspace', async () => { + await provider.updateWorkspaceMeta(workspaceId!, { + name: '1111', + }); + expect(workspaces.workspaces[0].name).toEqual('1111'); + }); + + test('delete workspace', async () => { + expect(workspaces.workspaces.length).toEqual(1); + await provider.deleteWorkspace(workspaces.workspaces[0].id); + expect(workspaces.workspaces.length).toEqual(0); + }); +}); diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 0a383a05bd..a01d516a35 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -1,4 +1,5 @@ import { BaseProvider } from '../base'; +import type { ProviderConstructorParams } from '../base'; import { varStorage as storage } from 'lib0/storage'; import { Workspace as WS, WorkspaceMeta } from '../../types'; import { Workspace, uuidv4 } from '@blocksuite/store'; @@ -12,8 +13,8 @@ export class LocalProvider extends BaseProvider { public id = 'local'; private _idbMap: Map = new Map(); - constructor() { - super(); + constructor(params: ProviderConstructorParams) { + super(params); this.loadWorkspaces(); } diff --git a/packages/data-center/src/types/index.ts b/packages/data-center/src/types/index.ts index c43fa2ed65..61bc12be81 100644 --- a/packages/data-center/src/types/index.ts +++ b/packages/data-center/src/types/index.ts @@ -1,4 +1,4 @@ -import { getLogger } from 'src'; +import { getLogger } from '../logger'; export type Workspace = { name: string; diff --git a/packages/data-center/src/workspaces/workspaces.ts b/packages/data-center/src/workspaces/workspaces.ts index 2d8661b2bb..90b1a5e9ea 100644 --- a/packages/data-center/src/workspaces/workspaces.ts +++ b/packages/data-center/src/workspaces/workspaces.ts @@ -20,7 +20,7 @@ export class Workspaces extends Observable<'change'> { private _workspacesMap = new Map(); get workspaces(): Workspace[] { - return Object.values(this._workspacesMap); + return Array.from(this._workspacesMap.values()); } find(workspaceId: string) { diff --git a/packages/data-center/tests/cloud/auth.spec.ts b/packages/data-center/tests/cloud/auth.spec.ts deleted file mode 100644 index f896dc99e6..0000000000 --- a/packages/data-center/tests/cloud/auth.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Auth', () => { - test('sign in', async () => {}); - - test('sign out', async () => {}); - - test('isLogin', async () => {}); - - test('getUserInfo', async () => {}); -}); diff --git a/packages/data-center/tests/cloud/collaborate.spec.ts b/packages/data-center/tests/cloud/collaborate.spec.ts deleted file mode 100644 index abe771109c..0000000000 --- a/packages/data-center/tests/cloud/collaborate.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Collaborate', () => { - test('collaborate editor content', async () => {}); - - test('collaborate workspace name', async () => {}); - - test('collaborate workspace avatar', async () => {}); - - test('collaborate workspace list', async () => {}); -}); diff --git a/packages/data-center/tests/cloud/permission.spec.ts b/packages/data-center/tests/cloud/permission.spec.ts deleted file mode 100644 index 69f40ad8f3..0000000000 --- a/packages/data-center/tests/cloud/permission.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Permission', () => { - test('get the public of workspace', async () => {}); - - test('make workspace public', async () => {}); - - test('make workspace private', async () => {}); - - test('un-login user open the public workspace ', async () => {}); - - test('un-login user open the private workspace ', async () => {}); -}); diff --git a/packages/data-center/tests/cloud/share.spec.ts b/packages/data-center/tests/cloud/share.spec.ts deleted file mode 100644 index 1698e29eab..0000000000 --- a/packages/data-center/tests/cloud/share.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Share', () => { - test('add(invite) member by email', async () => {}); - - test('accept invite member link', async () => {}); - - test('members list', async () => {}); - - test('delete member', async () => {}); -}); diff --git a/packages/data-center/tests/cloud/sync.spec.ts b/packages/data-center/tests/cloud/sync.spec.ts deleted file mode 100644 index f81cde5758..0000000000 --- a/packages/data-center/tests/cloud/sync.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Sync', () => { - test('get cloud the sync flag of workspace', async () => {}); - - test('enable [cloud sync feature]', async () => {}); - - test('close [cloud sync feature]', async () => {}); - - test('editor cloud storage', async () => {}); - - test('cloud sync is in-progress', async () => {}); - - test('cloud sync is completed', async () => {}); - - test('cloud sync is error', async () => {}); - - test('cloud storage is right', async () => {}); -}); diff --git a/packages/data-center/tests/local/attachment.spec.ts b/packages/data-center/tests/local/attachment.spec.ts deleted file mode 100644 index 3a017080b6..0000000000 --- a/packages/data-center/tests/local/attachment.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Attachment', () => { - test('upload blob', async () => {}); - - test('get blob', async () => {}); - - test('remove blob', async () => {}); -}); diff --git a/packages/data-center/tests/local/import-export.spec.ts b/packages/data-center/tests/local/import-export.spec.ts deleted file mode 100644 index 14d8a8a4f6..0000000000 --- a/packages/data-center/tests/local/import-export.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Import/Export Workspace', () => { - test('import workspace', async () => {}); - - test('export workspace', async () => {}); -}); diff --git a/packages/data-center/tests/local/init.spec.ts b/packages/data-center/tests/local/init.spec.ts deleted file mode 100644 index c802bd64c0..0000000000 --- a/packages/data-center/tests/local/init.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Init Data Center', () => { - test('init', async () => { - // const dataCenter = await getDataCenter(); - // expect(dataCenter).toBeTruthy(); - // await dataCenter.clear(); - // const workspace = await dataCenter.load('test1'); - // expect(workspace).toBeTruthy(); - }); - - test('init singleton', async () => { - // // data center is singleton - // const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]); - // expect(dc1).toEqual(dc2); - // // load same workspace will get same instance - // const [ws1, ws2] = await Promise.all([ - // dc1.load('test1'), - // dc2.load('test1'), - // ]); - // expect(ws1).toEqual(ws2); - }); - - test('should init error with unknown provider', async () => { - // const dc = await getDataCenter(); - // await dc.clear(); - // // load workspace with unknown provider will throw error - // test.fail(); - // await dc.load('test2', { providerId: 'not exist provider' }); - }); - - test.skip('init affine provider', async () => { - // const dataCenter = await getDataCenter(); - // await dataCenter.clear(); - // // load workspace with affine provider - // // TODO: set constant token for testing - // const workspace = await dataCenter.load('6', { - // providerId: 'affine', - // config: { token: 'YOUR_TOKEN' }, - // }); - // expect(workspace).toBeTruthy(); - }); -}); diff --git a/packages/data-center/tests/local/search.spec.ts b/packages/data-center/tests/local/search.spec.ts deleted file mode 100644 index 235d7ff36b..0000000000 --- a/packages/data-center/tests/local/search.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import assert from 'assert'; -import { test, expect } from '@playwright/test'; - -import { getDataCenter, waitOnce } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Search', () => { - test('search result', async () => { - // const dc = await getDataCenter(); - // const workspace = await dc.load('test'); - // assert(workspace); - // workspace.createPage('test'); - // await waitOnce(workspace.signals.pageAdded); - // const page = workspace.getPage('test'); - // assert(page); - // const text = new page.Text(page, 'hello world'); - // const blockId = page.addBlock({ flavour: 'affine:paragraph', text }); - // expect(workspace.search('hello')).toStrictEqual( - // new Map([[blockId, 'test']]) - // ); - }); -}); diff --git a/packages/data-center/tests/local/workspace.spec.ts b/packages/data-center/tests/local/workspace.spec.ts deleted file mode 100644 index c9d55398f2..0000000000 --- a/packages/data-center/tests/local/workspace.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from '../utils.js'; - -import 'fake-indexeddb/auto'; - -test.describe('Workspace', () => { - test('create', async () => {}); - - test('load', async () => {}); - - test('get workspace name', async () => {}); - test('set workspace name', async () => {}); - - test('get workspace avatar', async () => {}); - test('set workspace avatar', async () => {}); - - test('list', async () => { - // const dataCenter = await getDataCenter(); - // await dataCenter.clear(); - // await Promise.all([ - // dataCenter.load('test3'), - // dataCenter.load('test4'), - // dataCenter.load('test5'), - // dataCenter.load('test6'), - // ]); - // expect(await dataCenter.list()).toStrictEqual({ - // test3: { local: true }, - // test4: { local: true }, - // test5: { local: true }, - // test6: { local: true }, - // }); - // await dataCenter.reload('test3', { providerId: 'affine' }); - // expect(await dataCenter.list()).toStrictEqual({ - // test3: { affine: true }, - // test4: { local: true }, - // test5: { local: true }, - // test6: { local: true }, - // }); - }); - - test('destroy', async () => { - // const dataCenter = await getDataCenter(); - // await dataCenter.clear(); - // // return new workspace if origin workspace is destroyed - // const ws1 = await dataCenter.load('test7'); - // await dataCenter.destroy('test7'); - // const ws2 = await dataCenter.load('test7'); - // expect(ws1 !== ws2).toBeTruthy(); - // // return new workspace if workspace is reload - // const ws3 = await dataCenter.load('test8'); - // const ws4 = await dataCenter.reload('test8', { providerId: 'affine' }); - // expect(ws3 !== ws4).toBeTruthy(); - }); - - test('remove', async () => { - // const dataCenter = await getDataCenter(); - // await dataCenter.clear(); - // // remove workspace will remove workspace data - // await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]); - // await dataCenter.delete('test9'); - // expect(await dataCenter.list()).toStrictEqual({ test10: { local: true } }); - }); -}); diff --git a/packages/data-center/tests/utils.ts b/packages/data-center/tests/utils.ts deleted file mode 100644 index 1f4f128333..0000000000 --- a/packages/data-center/tests/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Signal } from '@blocksuite/store'; - -export const getDataCenter = async () => { - const dataCenter = await import('../src/index.js'); - return await dataCenter.getDataCenter(false); -}; - -export const waitOnce = (signal: Signal) => - new Promise(resolve => signal.once(val => resolve(val))); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a5e883aae..35c82751c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,6 @@ importers: .: specifiers: '@changesets/cli': ^2.26.0 - '@jest/globals': ^29.3.1 '@playwright/test': ^1.29.1 '@types/eslint': ^8.4.10 '@types/node': ^18.11.17 @@ -15,16 +14,15 @@ importers: eslint-config-next: 12.3.1 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 + fake-indexeddb: 4.0.1 got: ^12.5.3 husky: ^8.0.2 - jest: ^29.3.1 lint-staged: ^13.1.0 prettier: ^2.7.1 - ts-jest: ^29.0.3 typescript: ^4.9.3 + vitest: ^0.26.3 devDependencies: '@changesets/cli': 2.26.0 - '@jest/globals': 29.3.1 '@playwright/test': 1.29.1 '@types/eslint': 8.4.10 '@types/node': 18.11.18 @@ -34,13 +32,13 @@ importers: eslint-config-next: 12.3.1_p4cjf2r47dnfiqufepc5hp43sq eslint-config-prettier: 8.5.0_eslint@8.31.0 eslint-plugin-prettier: 4.2.1_vkmhlldugy3fnk7p2bqhsfnaiq + fake-indexeddb: 4.0.1 got: 12.5.3 husky: 8.0.2 - jest: 29.3.1_@types+node@18.11.18 lint-staged: 13.1.0 prettier: 2.7.1 - ts-jest: 29.0.3_4f6uxrzmuwipl5rr3bcogf6k74 typescript: 4.9.3 + vitest: 0.26.3 packages/app: specifiers: @@ -755,15 +753,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.20.5: - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.5: resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: @@ -811,15 +800,6 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true - /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.20.5: - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.5: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -838,16 +818,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: false - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.20.5: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.5: resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -922,16 +892,6 @@ packages: '@babel/helper-plugin-utils': 7.19.0 dev: true - /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.20.5: - resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.20.5 - '@babel/helper-plugin-utils': 7.19.0 - dev: true - /@babel/plugin-transform-arrow-functions/7.20.7_@babel+core@7.20.5: resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} engines: {node: '>=6.9.0'} @@ -1466,15 +1426,6 @@ packages: - supports-color dev: true - /@babel/types/7.19.4: - resolution: {integrity: sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - dev: true - /@babel/types/7.20.5: resolution: {integrity: sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==} engines: {node: '>=6.9.0'} @@ -1492,10 +1443,6 @@ packages: to-fast-properties: 2.0.0 dev: true - /@bcoe/v8-coverage/0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true - /@blocksuite/blocks/0.3.1_yjs@13.5.44: resolution: {integrity: sha512-b0dGz2MG4yIgngJPRumaMY58wAsd2FEVSZl3tpCXlagK9I0HD165Bq70PxcaRHVjBSV1Gf29ZVHUF6BVTYogPw==} dependencies: @@ -1900,6 +1847,204 @@ packages: resolution: {integrity: sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==} dev: false + /@esbuild/android-arm/0.16.15: + resolution: {integrity: sha512-JsJtmadyWcR+DEtHLixM7bAQsfi1s0Xotv9kVOoXbCLyhKPOHvMEyh3kJBuTbCPSE4c2jQkQVmarwc9Mg9k3bA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64/0.16.15: + resolution: {integrity: sha512-OdbkUv7468dSsgoFtHIwTaYAuI5lDEv/v+dlfGBUbVa2xSDIIuSOHXawynw5N9+5lygo/JdXa5/sgGjiEU18gQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64/0.16.15: + resolution: {integrity: sha512-dPUOBiNNWAm+/bxoA75o7R7qqqfcEzXaYlb5uJk2xGHmUMNKSAnDCtRYLgx9/wfE4sXyn8H948OrDyUAHhPOuA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64/0.16.15: + resolution: {integrity: sha512-AksarYV85Hxgwh5/zb6qGl4sYWxIXPQGBAZ+jUro1ZpINy3EWumK+/4DPOKUBPnsrOIvnNXy7Rq4mTeCsMQDNA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64/0.16.15: + resolution: {integrity: sha512-qqrKJxoohceZGGP+sZ5yXkzW9ZiyFZJ1gWSEfuYdOWzBSL18Uy3w7s/IvnDYHo++/cxwqM0ch3HQVReSZy7/4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64/0.16.15: + resolution: {integrity: sha512-LBWaep6RvJm5KnsKkocdVEzuwnGMjz54fcRVZ9d3R7FSEWOtPBxMhuxeA1n98JVbCLMkTPFmKN6xSnfhnM9WXQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64/0.16.15: + resolution: {integrity: sha512-LE8mKC6JPR04kPLRP9A6k7ZmG0k2aWF4ru79Sde6UeWCo7yDby5f48uJNFQ2pZqzUUkLrHL8xNdIHerJeZjHXg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm/0.16.15: + resolution: {integrity: sha512-+1sGlqtMJTOnJUXwLUGnDhPaGRKqxT0UONtYacS+EjdDOrSgpQ/1gUXlnze45Z/BogwYaswQM19Gu1YD1T19/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64/0.16.15: + resolution: {integrity: sha512-mRYpuQGbzY+XLczy3Sk7fMJ3DRKLGDIuvLKkkUkyecDGQMmil6K/xVKP9IpKO7JtNH477qAiMjjX7jfKae8t4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32/0.16.15: + resolution: {integrity: sha512-puXVFvY4m8EB6/fzu3LdgjiNnEZ3gZMSR7NmKoQe51l3hyQalvTjab3Dt7aX4qGf+8Pj7dsCOBNzNzkSlr/4Aw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64/0.16.15: + resolution: {integrity: sha512-ATMGb3eg8T6ZTGZFldlGeFEcevBiVq6SBHvRAO04HMfUjZWneZ/U+JJb3YzlNZxuscJ4Tmzq+JrYxlk7ro4dRg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el/0.16.15: + resolution: {integrity: sha512-3SEA4L82OnoSATW+Ve8rPgLaKjC8WMt8fnx7De9kvi/NcVbkj8W+J7qnu/tK2P9pUPQP7Au/0sjPEqZtFeyKQQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64/0.16.15: + resolution: {integrity: sha512-8PgbeX+N6vmqeySzyxO0NyDOltCEW13OS5jUHTvCHmCgf4kNXZtAWJ+zEfJxjRGYhVezQ1FdIm7WfN1R27uOyg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64/0.16.15: + resolution: {integrity: sha512-U+coqH+89vbPVoU30no1Fllrn6gvEeO5tfEArBhjYZ+dQ3Gv7ciQXYf5nrT1QdlIFwEjH4Is1U1iiaGWW+tGpQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x/0.16.15: + resolution: {integrity: sha512-M0nKLFMdyFGBoitxG42kq6Xap0CPeDC6gfF9lg7ZejzGF6kqYUGT+pQGl2QCQoxJBeat/LzTma1hG8C3dq2ocg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64/0.16.15: + resolution: {integrity: sha512-t7/fOXBUKfigvhJLGKZ9TPHHgqNgpIpYaAbcXQk1X+fPeUG7x0tpAbXJ2wST9F/gJ02+CLETPMnhG7Tra2wqsQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64/0.16.15: + resolution: {integrity: sha512-0k0Nxi6DOJmTnLtKD/0rlyqOPpcqONXY53vpkoAsue8CfyhNPWtwzba1ICFNCfCY1dqL3Ho/xEzujJhmdXq1rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64/0.16.15: + resolution: {integrity: sha512-3SkckazfIbdSjsGpuIYT3d6n2Hx0tck3MS1yVsbahhWiLvdy4QozTpvlbjqO3GmvtvhxY4qdyhFOO2wiZKeTAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64/0.16.15: + resolution: {integrity: sha512-8PNvBC+O8X5EnyIGqE8St2bOjjrXMR17NOLenIrzolvwWnJXvwPo0tE/ahOeiAJmTOS/eAcN8b4LAZcn17Uj7w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64/0.16.15: + resolution: {integrity: sha512-YPaSgm/mm7kNcATB53OxVGVfn6rDNbImTn330ZlF3hKej1e9ktCaljGjn2vH08z2dlHEf3kdt57tNjE6zs8SzA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32/0.16.15: + resolution: {integrity: sha512-0movUXbSNrTeNf5ZXT0avklEvlJD0hNGZsrrXHfsp9z4tK5xC+apCqmUEZeE9mqrb84Z8XbgGr/MS9LqafTP2A==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64/0.16.15: + resolution: {integrity: sha512-27h5GCcbfomVAqAnMJWvR1LqEY0dFqIq4vTe5nY3becnZNu0SX8F0+gTk3JPvgWQHzaGc6VkPzlOiMkdSUunUA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint/eslintrc/1.3.2: resolution: {integrity: sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2470,235 +2615,6 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@istanbuljs/load-nyc-config/1.1.0: - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - dev: true - - /@istanbuljs/schema/0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - dev: true - - /@jest/console/29.3.1: - resolution: {integrity: sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - chalk: 4.1.2 - jest-message-util: 29.3.1 - jest-util: 29.3.1 - slash: 3.0.0 - dev: true - - /@jest/core/29.3.1: - resolution: {integrity: sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/console': 29.3.1 - '@jest/reporters': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.7.0 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 29.2.0 - jest-config: 29.3.1_@types+node@18.11.18 - jest-haste-map: 29.3.1 - jest-message-util: 29.3.1 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-resolve-dependencies: 29.3.1 - jest-runner: 29.3.1 - jest-runtime: 29.3.1 - jest-snapshot: 29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 - jest-watcher: 29.3.1 - micromatch: 4.0.5 - pretty-format: 29.3.1 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - - /@jest/environment/29.3.1: - resolution: {integrity: sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/fake-timers': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - jest-mock: 29.3.1 - dev: true - - /@jest/expect-utils/29.3.1: - resolution: {integrity: sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - jest-get-type: 29.2.0 - dev: true - - /@jest/expect/29.3.1: - resolution: {integrity: sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - expect: 29.3.1 - jest-snapshot: 29.3.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@jest/fake-timers/29.3.1: - resolution: {integrity: sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - '@sinonjs/fake-timers': 9.1.2 - '@types/node': 18.11.18 - jest-message-util: 29.3.1 - jest-mock: 29.3.1 - jest-util: 29.3.1 - dev: true - - /@jest/globals/29.3.1: - resolution: {integrity: sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/environment': 29.3.1 - '@jest/expect': 29.3.1 - '@jest/types': 29.3.1 - jest-mock: 29.3.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@jest/reporters/29.3.1: - resolution: {integrity: sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 - '@types/node': 18.11.18 - chalk: 4.1.2 - collect-v8-coverage: 1.0.1 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.1 - istanbul-lib-report: 3.0.0 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 - jest-message-util: 29.3.1 - jest-util: 29.3.1 - jest-worker: 29.3.1 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.0.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@jest/schemas/29.0.0: - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.24.51 - dev: true - - /@jest/source-map/29.2.0: - resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jridgewell/trace-mapping': 0.3.17 - callsites: 3.1.0 - graceful-fs: 4.2.10 - dev: true - - /@jest/test-result/29.3.1: - resolution: {integrity: sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/console': 29.3.1 - '@jest/types': 29.3.1 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 - dev: true - - /@jest/test-sequencer/29.3.1: - resolution: {integrity: sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/test-result': 29.3.1 - graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - slash: 3.0.0 - dev: true - - /@jest/transform/29.3.1: - resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/core': 7.20.5 - '@jest/types': 29.3.1 - '@jridgewell/trace-mapping': 0.3.17 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 - micromatch: 4.0.5 - pirates: 4.0.5 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@jest/types/29.3.1: - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.0.0 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.18 - '@types/yargs': 17.0.15 - chalk: 4.1.2 - dev: true - /@jridgewell/gen-mapping/0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} @@ -3501,27 +3417,11 @@ packages: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true - /@sinclair/typebox/0.24.51: - resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - dev: true - /@sindresorhus/is/5.3.0: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} dev: true - /@sinonjs/commons/1.8.6: - resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} - dependencies: - type-detect: 4.0.8 - dev: true - - /@sinonjs/fake-timers/9.1.2: - resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} - dependencies: - '@sinonjs/commons': 1.8.6 - dev: true - /@surma/rollup-plugin-off-main-thread/2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} dependencies: @@ -3559,33 +3459,14 @@ packages: resolution: {integrity: sha512-GiS5Df3CzXY/fPBFcM0CKFERZfI4Cg1X33VPZX+NLo7Fwm/h9zu/aU24N1mG75Q9LuMnwKm7woxKr8BiUXGYCg==} dev: false - /@types/babel__core/7.1.20: - resolution: {integrity: sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==} + /@types/chai-subset/1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 - '@types/babel__generator': 7.6.4 - '@types/babel__template': 7.4.1 - '@types/babel__traverse': 7.18.3 + '@types/chai': 4.3.4 dev: true - /@types/babel__generator/7.6.4: - resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} - dependencies: - '@babel/types': 7.20.5 - dev: true - - /@types/babel__template/7.4.1: - resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} - dependencies: - '@babel/parser': 7.20.5 - '@babel/types': 7.20.5 - dev: true - - /@types/babel__traverse/7.18.3: - resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} - dependencies: - '@babel/types': 7.20.5 + /@types/chai/4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} dev: true /@types/debug/4.1.7: @@ -3616,12 +3497,6 @@ packages: '@types/node': 18.7.18 dev: true - /@types/graceful-fs/4.1.5: - resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} - dependencies: - '@types/node': 18.11.18 - dev: true - /@types/http-cache-semantics/4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: true @@ -3632,22 +3507,6 @@ packages: ci-info: 3.7.0 dev: true - /@types/istanbul-lib-coverage/2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true - - /@types/istanbul-lib-report/3.0.0: - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - dev: true - - /@types/istanbul-reports/3.0.1: - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - dependencies: - '@types/istanbul-lib-report': 3.0.0 - dev: true - /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -3691,10 +3550,6 @@ packages: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: false - /@types/prettier/2.7.1: - resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} - dev: true - /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -3754,10 +3609,6 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true - /@types/stack-utils/2.0.1: - resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} - dev: true - /@types/trusted-types/2.0.2: resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} @@ -3765,16 +3616,6 @@ packages: resolution: {integrity: sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==} dev: true - /@types/yargs-parser/21.0.0: - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - dev: true - - /@types/yargs/17.0.15: - resolution: {integrity: sha512-ZHc4W2dnEQPfhn06TBEdWaiUHEZAocYaiVMfwOipY5jcJt/251wVrKCBWBetGZWO5CF8tdb7L3DmdxVlZ2BOIg==} - dependencies: - '@types/yargs-parser': 21.0.0 - dev: true - /@typescript-eslint/eslint-plugin/5.48.0_wxmz6cyorqfuzhhglktlvr5lve: resolution: {integrity: sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3960,11 +3801,22 @@ packages: acorn: 8.8.0 dev: true + /acorn-walk/8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + /acorn/8.8.0: resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} engines: {node: '>=0.4.0'} dev: true + /acorn/8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /aggregate-error/3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -4032,24 +3884,11 @@ packages: dependencies: color-convert: 2.0.1 - /ansi-styles/5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - /ansi-styles/6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} dev: true - /anymatch/3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - /argparse/1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -4136,6 +3975,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /ast-types-flow/0.0.7: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true @@ -4175,24 +4018,6 @@ packages: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} dev: true - /babel-jest/29.3.1_@babel+core@7.20.5: - resolution: {integrity: sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.20.5 - '@jest/transform': 29.3.1 - '@types/babel__core': 7.1.20 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.2.0_@babel+core@7.20.5 - chalk: 4.1.2 - graceful-fs: 4.2.10 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /babel-loader/8.3.0: resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} @@ -4206,29 +4031,6 @@ packages: schema-utils: 2.7.1 dev: true - /babel-plugin-istanbul/6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - dependencies: - '@babel/helper-plugin-utils': 7.19.0 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /babel-plugin-jest-hoist/29.2.0: - resolution: {integrity: sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.20.5 - '@types/babel__core': 7.1.20 - '@types/babel__traverse': 7.18.3 - dev: true - /babel-plugin-macros/3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -4274,37 +4076,6 @@ packages: - supports-color dev: true - /babel-preset-current-node-syntax/1.0.1_@babel+core@7.20.5: - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.20.5 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.5 - '@babel/plugin-syntax-bigint': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.5 - '@babel/plugin-syntax-import-meta': 7.10.4_@babel+core@7.20.5 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.5 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.5 - dev: true - - /babel-preset-jest/29.2.0_@babel+core@7.20.5: - resolution: {integrity: sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.20.5 - babel-plugin-jest-hoist: 29.2.0 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.5 - dev: true - /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -4366,19 +4137,6 @@ packages: update-browserslist-db: 1.0.10_browserslist@4.21.4 dev: true - /bs-logger/0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 - dev: true - - /bser/2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - dependencies: - node-int64: 0.4.0 - dev: true - /buffer-from/0.1.2: resolution: {integrity: sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==} dev: false @@ -4441,14 +4199,22 @@ packages: engines: {node: '>=6'} dev: true - /camelcase/6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - dev: true - /caniuse-lite/1.0.30001419: resolution: {integrity: sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==} + /chai/4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk-next/6.1.5: resolution: {integrity: sha512-OAx9F3vSk18qpfCohk0849/j3GyaoIpv8eXjmpdbmLZt+5+sWYq8xwt3B5ue25irLcxFcLL2hAbxxHSsBxupbw==} engines: {node: '>=10'} @@ -4477,24 +4243,19 @@ packages: supports-color: 7.2.0 dev: true - /char-regex/1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - dev: true - /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + /ci-info/3.7.0: resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==} engines: {node: '>=8'} dev: true - /cjs-module-lexer/1.2.2: - resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} - dev: true - /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -4589,15 +4350,6 @@ packages: - '@types/react' dev: false - /co/4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - dev: true - - /collect-v8-coverage/1.0.1: - resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} - dev: true - /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -4648,10 +4400,6 @@ packages: /convert-source-map/1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - /convert-source-map/2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true - /core-js-compat/3.27.1: resolution: {integrity: sha512-Dg91JFeCDA17FKnneN7oCMz4BkQ4TcffkgHP4OWwp9yx3pi7ubqMDXXSacfNak1PQqjc95skyt+YBLHQJnkJwA==} dependencies: @@ -4797,8 +4545,11 @@ packages: mimic-response: 3.1.0 dev: true - /dedent/0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + /deep-eql/4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 dev: true /deep-equal/1.1.1: @@ -4857,20 +4608,10 @@ packages: engines: {node: '>=8'} dev: true - /detect-newline/3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - dev: true - /detect-node-es/1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false - /diff-sequences/29.3.1: - resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4931,11 +4672,6 @@ packages: resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==} dev: true - /emittery/0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - dev: true - /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5014,6 +4750,36 @@ packages: is-symbol: 1.0.4 dev: true + /esbuild/0.16.15: + resolution: {integrity: sha512-v+3ozjy9wyj8cOElzx3//Lsb4TCxPfZxRmdsfm0YaEkvZu7y6rKH7Zi1UpDx4JI7dSQui+U1Qxhfij9KBbHfrA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.16.15 + '@esbuild/android-arm64': 0.16.15 + '@esbuild/android-x64': 0.16.15 + '@esbuild/darwin-arm64': 0.16.15 + '@esbuild/darwin-x64': 0.16.15 + '@esbuild/freebsd-arm64': 0.16.15 + '@esbuild/freebsd-x64': 0.16.15 + '@esbuild/linux-arm': 0.16.15 + '@esbuild/linux-arm64': 0.16.15 + '@esbuild/linux-ia32': 0.16.15 + '@esbuild/linux-loong64': 0.16.15 + '@esbuild/linux-mips64el': 0.16.15 + '@esbuild/linux-ppc64': 0.16.15 + '@esbuild/linux-riscv64': 0.16.15 + '@esbuild/linux-s390x': 0.16.15 + '@esbuild/linux-x64': 0.16.15 + '@esbuild/netbsd-x64': 0.16.15 + '@esbuild/openbsd-x64': 0.16.15 + '@esbuild/sunos-x64': 0.16.15 + '@esbuild/win32-arm64': 0.16.15 + '@esbuild/win32-ia32': 0.16.15 + '@esbuild/win32-x64': 0.16.15 + dev: true + /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -5022,11 +4788,6 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - /escape-string-regexp/2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - dev: true - /escape-string-regexp/4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -5606,21 +5367,6 @@ packages: resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} dev: false - /execa/5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - /execa/6.1.0: resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5636,22 +5382,6 @@ packages: strip-final-newline: 3.0.0 dev: true - /exit/0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - dev: true - - /expect/29.3.1: - resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/expect-utils': 29.3.1 - jest-get-type: 29.2.0 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-util: 29.3.1 - dev: true - /extend/3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false @@ -5719,12 +5449,6 @@ packages: websocket-driver: 0.7.4 dev: false - /fb-watchman/2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - dependencies: - bser: 2.1.1 - dev: true - /fetch-blob/3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -5937,6 +5661,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + /get-intrinsic/1.1.3: resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} dependencies: @@ -5953,11 +5681,6 @@ packages: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} dev: true - /get-package-type/0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - dev: true - /get-stream/6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -6132,10 +5855,6 @@ packages: resolution: {integrity: sha512-mshqjgTqx8ee0qryHvRgZaZDxTwxam/2yTQmQlqAWS3+twnq1jsY9Yng9zB7lWq6WRrjTbTOc7knNwccXQiAjQ==} dev: false - /html-escaper/2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true - /html-parse-stringify/3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: @@ -6172,11 +5891,6 @@ packages: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} dev: true - /human-signals/2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true - /human-signals/3.0.1: resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} engines: {node: '>=12.20.0'} @@ -6238,15 +5952,6 @@ packages: parent-module: 1.0.1 resolve-from: 4.0.0 - /import-local/3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} - engines: {node: '>=8'} - hasBin: true - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - dev: true - /imurmurhash/0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -6344,11 +6049,6 @@ packages: engines: {node: '>=12'} dev: true - /is-generator-fn/2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - dev: true - /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -6487,52 +6187,6 @@ packages: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} dev: false - /istanbul-lib-coverage/3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} - dev: true - - /istanbul-lib-instrument/5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - dependencies: - '@babel/core': 7.20.5 - '@babel/parser': 7.20.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /istanbul-lib-report/3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} - dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 - supports-color: 7.2.0 - dev: true - - /istanbul-lib-source-maps/4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - dependencies: - debug: 4.3.4 - istanbul-lib-coverage: 3.2.0 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - dev: true - - /istanbul-reports/3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 - dev: true - /jake/10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} engines: {node: '>=10'} @@ -6544,385 +6198,6 @@ packages: minimatch: 3.1.2 dev: true - /jest-changed-files/29.2.0: - resolution: {integrity: sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - execa: 5.1.1 - p-limit: 3.1.0 - dev: true - - /jest-circus/29.3.1: - resolution: {integrity: sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/environment': 29.3.1 - '@jest/expect': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - chalk: 4.1.2 - co: 4.6.0 - dedent: 0.7.0 - is-generator-fn: 2.1.0 - jest-each: 29.3.1 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-runtime: 29.3.1 - jest-snapshot: 29.3.1 - jest-util: 29.3.1 - p-limit: 3.1.0 - pretty-format: 29.3.1 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-cli/29.3.1_@types+node@18.11.18: - resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.3.1_@types+node@18.11.18 - jest-util: 29.3.1 - jest-validate: 29.3.1 - prompts: 2.4.2 - yargs: 17.6.2 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - - /jest-config/29.3.1_@types+node@18.11.18: - resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.20.5 - '@jest/test-sequencer': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - babel-jest: 29.3.1_@babel+core@7.20.5 - chalk: 4.1.2 - ci-info: 3.7.0 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.3.1 - jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-runner: 29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.3.1 - slash: 3.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-diff/29.3.1: - resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 - dev: true - - /jest-docblock/29.2.0: - resolution: {integrity: sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - detect-newline: 3.1.0 - dev: true - - /jest-each/29.3.1: - resolution: {integrity: sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - chalk: 4.1.2 - jest-get-type: 29.2.0 - jest-util: 29.3.1 - pretty-format: 29.3.1 - dev: true - - /jest-environment-node/29.3.1: - resolution: {integrity: sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/environment': 29.3.1 - '@jest/fake-timers': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - jest-mock: 29.3.1 - jest-util: 29.3.1 - dev: true - - /jest-get-type/29.2.0: - resolution: {integrity: sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - - /jest-haste-map/29.3.1: - resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - '@types/graceful-fs': 4.1.5 - '@types/node': 18.11.18 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.10 - jest-regex-util: 29.2.0 - jest-util: 29.3.1 - jest-worker: 29.3.1 - micromatch: 4.0.5 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /jest-leak-detector/29.3.1: - resolution: {integrity: sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - jest-get-type: 29.2.0 - pretty-format: 29.3.1 - dev: true - - /jest-matcher-utils/29.3.1: - resolution: {integrity: sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - jest-diff: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 - dev: true - - /jest-message-util/29.3.1: - resolution: {integrity: sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/code-frame': 7.18.6 - '@jest/types': 29.3.1 - '@types/stack-utils': 2.0.1 - chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 - pretty-format: 29.3.1 - slash: 3.0.0 - stack-utils: 2.0.6 - dev: true - - /jest-mock/29.3.1: - resolution: {integrity: sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - jest-util: 29.3.1 - dev: true - - /jest-pnp-resolver/1.2.3_jest-resolve@29.3.1: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - dependencies: - jest-resolve: 29.3.1 - dev: true - - /jest-regex-util/29.2.0: - resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - - /jest-resolve-dependencies/29.3.1: - resolution: {integrity: sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - jest-regex-util: 29.2.0 - jest-snapshot: 29.3.1 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-resolve/29.3.1: - resolution: {integrity: sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - jest-pnp-resolver: 1.2.3_jest-resolve@29.3.1 - jest-util: 29.3.1 - jest-validate: 29.3.1 - resolve: 1.22.1 - resolve.exports: 1.1.0 - slash: 3.0.0 - dev: true - - /jest-runner/29.3.1: - resolution: {integrity: sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/console': 29.3.1 - '@jest/environment': 29.3.1 - '@jest/test-result': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.10 - jest-docblock: 29.2.0 - jest-environment-node: 29.3.1 - jest-haste-map: 29.3.1 - jest-leak-detector: 29.3.1 - jest-message-util: 29.3.1 - jest-resolve: 29.3.1 - jest-runtime: 29.3.1 - jest-util: 29.3.1 - jest-watcher: 29.3.1 - jest-worker: 29.3.1 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-runtime/29.3.1: - resolution: {integrity: sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/environment': 29.3.1 - '@jest/fake-timers': 29.3.1 - '@jest/globals': 29.3.1 - '@jest/source-map': 29.2.0 - '@jest/test-result': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-haste-map: 29.3.1 - jest-message-util: 29.3.1 - jest-mock: 29.3.1 - jest-regex-util: 29.2.0 - jest-resolve: 29.3.1 - jest-snapshot: 29.3.1 - jest-util: 29.3.1 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-snapshot/29.3.1: - resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/core': 7.20.5 - '@babel/generator': 7.20.5 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5 - '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.5 - '@babel/traverse': 7.20.5 - '@babel/types': 7.19.4 - '@jest/expect-utils': 29.3.1 - '@jest/transform': 29.3.1 - '@jest/types': 29.3.1 - '@types/babel__traverse': 7.18.3 - '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.5 - chalk: 4.1.2 - expect: 29.3.1 - graceful-fs: 4.2.10 - jest-diff: 29.3.1 - jest-get-type: 29.2.0 - jest-haste-map: 29.3.1 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.3.1 - jest-util: 29.3.1 - natural-compare: 1.4.0 - pretty-format: 29.3.1 - semver: 7.3.7 - transitivePeerDependencies: - - supports-color - dev: true - - /jest-util/29.3.1: - resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - chalk: 4.1.2 - ci-info: 3.7.0 - graceful-fs: 4.2.10 - picomatch: 2.3.1 - dev: true - - /jest-validate/29.3.1: - resolution: {integrity: sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/types': 29.3.1 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.2.0 - leven: 3.1.0 - pretty-format: 29.3.1 - dev: true - - /jest-watcher/29.3.1: - resolution: {integrity: sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/test-result': 29.3.1 - '@jest/types': 29.3.1 - '@types/node': 18.11.18 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.3.1 - string-length: 4.0.2 - dev: true - /jest-worker/26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -6941,36 +6216,6 @@ packages: supports-color: 8.1.1 dev: true - /jest-worker/29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@types/node': 18.11.18 - jest-util: 29.3.1 - merge-stream: 2.0.0 - supports-color: 8.1.1 - dev: true - - /jest/29.3.1_@types+node@18.11.18: - resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.3.1 - '@jest/types': 29.3.1 - import-local: 3.1.0 - jest-cli: 29.3.1_@types+node@18.11.18 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} dev: true @@ -7039,6 +6284,10 @@ packages: hasBin: true dev: true + /jsonc-parser/3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + /jsonfile/4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} optionalDependencies: @@ -7077,11 +6326,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /kleur/3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - dev: true - /kleur/4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -7248,6 +6492,11 @@ packages: json5: 2.2.1 dev: true + /local-pkg/0.4.2: + resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==} + engines: {node: '>=14'} + dev: true + /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -7270,10 +6519,6 @@ packages: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: true - /lodash.memoize/4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true - /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -7312,6 +6557,12 @@ packages: dependencies: js-tokens: 4.0.0 + /loupe/2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lowercase-keys/3.0.0: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7350,16 +6601,6 @@ packages: semver: 6.3.0 dev: true - /make-error/1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true - - /makeerror/1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - dependencies: - tmpl: 1.0.5 - dev: true - /map-obj/1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -7465,6 +6706,15 @@ packages: engines: {node: '>= 8.0.0'} dev: true + /mlly/1.0.0: + resolution: {integrity: sha512-QL108Hwt+u9bXdWgOI0dhzZfACovn5Aen4Xvc8Jasd9ouRH4NjnrXEiyP3nVvJo91zPlYjVRckta0Nt2zfoR6g==} + dependencies: + acorn: 8.8.1 + pathe: 1.0.0 + pkg-types: 1.0.1 + ufo: 1.0.1 + dev: true + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -7637,10 +6887,6 @@ packages: formdata-polyfill: 4.0.10 dev: false - /node-int64/0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - dev: true - /node-releases/2.0.6: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} dev: true @@ -7664,13 +6910,6 @@ packages: engines: {node: '>=14.16'} dev: true - /npm-run-path/4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - /npm-run-path/5.1.0: resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7894,6 +7133,18 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pathe/0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + dev: true + + /pathe/1.0.0: + resolution: {integrity: sha512-nPdMG0Pd09HuSsr7QOKUXO2Jr9eqaDiZvDwdyIhNG5SHYujkQHYKDfGQkulBxvbDHz8oHLsTgKN86LSwYzSHAg==} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /perfect-freehand/1.2.0: resolution: {integrity: sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==} dev: false @@ -7934,11 +7185,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /pirates/4.0.5: - resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} - engines: {node: '>= 6'} - dev: true - /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -7946,6 +7192,14 @@ packages: find-up: 4.1.0 dev: true + /pkg-types/1.0.1: + resolution: {integrity: sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.0.0 + pathe: 1.0.0 + dev: true + /playwright-core/1.29.1: resolution: {integrity: sha512-20Ai3d+lMkWpI9YZYlxk8gxatfgax5STW8GaMozAHwigLiyiKQrdkt7gaoT9UQR8FIVDg6qVXs9IoZUQrDjIIg==} engines: {node: '>=14'} @@ -7960,6 +7214,15 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /preferred-pm/3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} @@ -7991,27 +7254,10 @@ packages: engines: {node: '>=6'} dev: true - /pretty-format/29.3.1: - resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.0.0 - ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: true - /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false - /prompts/2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - dev: true - /prop-types/15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: @@ -8153,6 +7399,7 @@ packages: /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false /react-remove-scroll-bar/2.3.4_w5j4k42lgipnm43s3brx6h3c34: resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} @@ -8377,13 +7624,6 @@ packages: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: true - /resolve-cwd/3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - dependencies: - resolve-from: 5.0.0 - dev: true - /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -8393,11 +7633,6 @@ packages: engines: {node: '>=8'} dev: true - /resolve.exports/1.1.0: - resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} - engines: {node: '>=10'} - dev: true - /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} dependencies: @@ -8471,6 +7706,14 @@ packages: fsevents: 2.3.2 dev: true + /rollup/3.9.1: + resolution: {integrity: sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -8604,10 +7847,6 @@ packages: - supports-color dev: false - /sisteransi/1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true - /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -8660,13 +7899,6 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - /source-map-support/0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - /source-map-support/0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -8729,13 +7961,6 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true - /stack-utils/2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - dependencies: - escape-string-regexp: 2.0.0 - dev: true - /stream-transform/2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} dependencies: @@ -8747,14 +7972,6 @@ packages: engines: {node: '>=0.6.19'} dev: true - /string-length/4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - dev: true - /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -8844,21 +8061,11 @@ packages: engines: {node: '>=4'} dev: true - /strip-bom/4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - dev: true - /strip-comments/2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} dev: true - /strip-final-newline/2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true - /strip-final-newline/3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -8876,6 +8083,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal/1.0.0: + resolution: {integrity: sha512-5o4LsH1lzBzO9UFH63AJ2ad2/S2AVx6NtjOcaz+VTT2h1RiRvbipW72z8M/lxEhcPHDBQwpDrnTF7sXy/7OwCQ==} + dependencies: + acorn: 8.8.1 + dev: true + /styled-jsx/5.0.7_react@18.2.0: resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} @@ -8999,15 +8212,6 @@ packages: source-map-support: 0.5.21 dev: true - /test-exclude/6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true - /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -9022,6 +8226,20 @@ packages: xtend: 2.1.2 dev: false + /tinybench/2.3.1: + resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==} + dev: true + + /tinypool/0.3.0: + resolution: {integrity: sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy/1.0.2: + resolution: {integrity: sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==} + engines: {node: '>=14.0.0'} + dev: true + /tmp/0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -9029,10 +8247,6 @@ packages: os-tmpdir: 1.0.2 dev: true - /tmpl/1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - dev: true - /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -9066,39 +8280,6 @@ packages: engines: {node: '>=8'} dev: true - /ts-jest/29.0.3_4f6uxrzmuwipl5rr3bcogf6k74: - resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.3.1_@types+node@18.11.18 - jest-util: 29.3.1 - json5: 2.2.1 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.7 - typescript: 4.9.3 - yargs-parser: 21.1.1 - dev: true - /tsconfig-paths/3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: @@ -9229,6 +8410,10 @@ packages: engines: {node: '>=0.1.14'} dev: true + /ufo/1.0.1: + resolution: {integrity: sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==} + dev: true + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -9353,15 +8538,6 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true - /v8-to-istanbul/9.0.1: - resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} - engines: {node: '>=10.12.0'} - dependencies: - '@jridgewell/trace-mapping': 0.3.17 - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - dev: true - /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -9369,17 +8545,112 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vite-node/0.26.3_@types+node@18.11.18: + resolution: {integrity: sha512-Te2bq0Bfvq6XiO718I+1EinMjpNYKws6SNHKOmVbILAQimKoZKDd+IZLlkaYcBXPpK3HFe2U80k8Zw+m3w/a2w==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + debug: 4.3.4 + mlly: 1.0.0 + pathe: 0.2.0 + source-map: 0.6.1 + source-map-support: 0.5.21 + vite: 4.0.4_@types+node@18.11.18 + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite/4.0.4_@types+node@18.11.18: + resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.11.18 + esbuild: 0.16.15 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.9.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest/0.26.3: + resolution: {integrity: sha512-FmHxU9aUCxTi23keF3vxb/Qp0lYXaaJ+jRLGOUmMS3qVTOJvgGE+f1VArupA6pEhaG2Ans4X+zV9dqM5WISMbg==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 + '@types/node': 18.11.18 + acorn: 8.8.1 + acorn-walk: 8.2.0 + chai: 4.3.7 + debug: 4.3.4 + local-pkg: 0.4.2 + source-map: 0.6.1 + strip-literal: 1.0.0 + tinybench: 2.3.1 + tinypool: 0.3.0 + tinyspy: 1.0.2 + vite: 4.0.4_@types+node@18.11.18 + vite-node: 0.26.3_@types+node@18.11.18 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /void-elements/3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} dev: false - /walker/1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - dependencies: - makeerror: 1.0.12 - dev: true - /wcwidth/1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: @@ -9673,14 +8944,6 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /write-file-atomic/4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - dev: true - /ws/7.5.9: resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} engines: {node: '>=8.3.0'} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..4c6feaa4dd --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, +}); From 59fc7731747a7d56a3b181a3b57eb8103a5cc35a Mon Sep 17 00:00:00 2001 From: alt0 Date: Mon, 9 Jan 2023 01:03:10 +0800 Subject: [PATCH 29/32] test: add workspaces unit test --- .../src/workspaces/workspaces.spec.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/data-center/src/workspaces/workspaces.spec.ts diff --git a/packages/data-center/src/workspaces/workspaces.spec.ts b/packages/data-center/src/workspaces/workspaces.spec.ts new file mode 100644 index 0000000000..859cbabfb0 --- /dev/null +++ b/packages/data-center/src/workspaces/workspaces.spec.ts @@ -0,0 +1,50 @@ +import { describe, test, expect } from 'vitest'; +import { Workspaces } from './workspaces'; +import type { WorkspacesChangeEvent } from './workspaces'; + +describe('workspaces observable', () => { + const workspaces = new Workspaces(); + + const scope = workspaces.createScope(); + + test('add workspace', () => { + workspaces.once('change', (event: WorkspacesChangeEvent) => { + expect(event.added?.id).toEqual('123'); + }); + scope.add({ + id: '123', + name: 'test', + memberCount: 1, + provider: '', + }); + }); + + test('list workspace', () => { + const list = scope.list(); + expect(list.length).toEqual(1); + expect(list[0].id).toEqual('123'); + }); + + test('get workspace', () => { + expect(scope.get('123')?.id).toEqual('123'); + }); + + test('update workspace', () => { + workspaces.once('change', (event: WorkspacesChangeEvent) => { + expect(event.updated?.name).toEqual('demo'); + }); + scope.update('123', { name: 'demo' }); + }); + + test('get workspace form other scope', () => { + const scope1 = workspaces.createScope(); + expect(scope1.get('123')).toBeFalsy(); + }); + + test('delete workspace', () => { + workspaces.once('change', (event: WorkspacesChangeEvent) => { + expect(event.deleted?.id).toEqual('123'); + }); + scope.remove('123'); + }); +}); From c8101dfa4c056fc697ba92e7f4cda36a5ebe5a77 Mon Sep 17 00:00:00 2001 From: alt0 Date: Mon, 9 Jan 2023 01:07:09 +0800 Subject: [PATCH 30/32] refactor: rename defaultProvider -> mainProvider --- packages/data-center/src/datacenter.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index c859ad7d02..410c60366b 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -18,7 +18,10 @@ export class DataCenter { private readonly _workspaces = new Workspaces(); private currentWorkspace: Workspace | null = null; private readonly _logger = getLogger('dc'); - private _defaultProvider?: BaseProvider; + /** + * A mainProvider must exist as the only data trustworthy source. + */ + private _mainProvider?: BaseProvider; providerMap: Map = new Map(); constructor(debug: boolean) { @@ -49,16 +52,16 @@ export class DataCenter { * We will automatically set the first provider to default provider. */ registerProvider(provider: BaseProvider) { - if (!this._defaultProvider) { - this._defaultProvider = provider; + if (!this._mainProvider) { + this._mainProvider = provider; } provider.init(); this.providerMap.set(provider.id, provider); } - setDefaultProvider(providerId: string) { - this._defaultProvider = this.providerMap.get(providerId); + setMainProvider(providerId: string) { + this._mainProvider = this.providerMap.get(providerId); } get providers() { @@ -82,13 +85,11 @@ export class DataCenter { */ public async createWorkspace(workspaceMeta: WorkspaceMeta) { assert( - this._defaultProvider, + this._mainProvider, 'There is no provider. You should add provider first.' ); - const workspace = await this._defaultProvider.createWorkspace( - workspaceMeta - ); + const workspace = await this._mainProvider.createWorkspace(workspaceMeta); return workspace; } From 1124d14055dfc3d2db3f239c4b4fc34124d6d62d Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Mon, 9 Jan 2023 10:37:40 +0800 Subject: [PATCH 31/32] feat: remove current workspaces --- packages/data-center/src/datacenter.ts | 36 +++++++------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 410c60366b..5714fe98fa 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -16,7 +16,6 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs'; */ export class DataCenter { private readonly _workspaces = new Workspaces(); - private currentWorkspace: Workspace | null = null; private readonly _logger = getLogger('dc'); /** * A mainProvider must exist as the only data trustworthy source. @@ -150,9 +149,8 @@ export class DataCenter { const provider = this.providerMap.get(workspaceInfo.provider); assert(provider, `provide '${workspaceInfo.provider}' is not registered`); this._logger(`Loading ${workspaceInfo.provider} workspace: `, workspaceId); - const workspace = this._getWorkspace(workspaceId); - this.currentWorkspace = await provider.warpWorkspace(workspace); - return this.currentWorkspace; + + return await provider.warpWorkspace(this._getWorkspace(workspaceId)); } /** @@ -184,24 +182,23 @@ export class DataCenter { */ public async resetWorkspaceMeta( { name, avatar }: WorkspaceMeta, - workspace?: Workspace + workspace: Workspace ) { - const w = workspace ?? this.currentWorkspace; - assert(w?.room, 'No workspace to set meta'); + assert(workspace?.room, 'No workspace to set meta'); const update: Partial = {}; if (name) { - w.meta.setName(name); + workspace.doc.meta.setName(name); update.name = name; } if (avatar) { - w.meta.setAvatar(avatar); + workspace.doc.meta.setAvatar(avatar); update.avatar = avatar; } // may run for change workspace meta - const workspaceInfo = this._workspaces.find(w.room); + const workspaceInfo = this._workspaces.find(workspace.room); assert(workspaceInfo, 'Workspace not found'); const provider = this.providerMap.get(workspaceInfo.provider); - provider?.updateWorkspaceMeta(w.room, update); + provider?.updateWorkspaceMeta(workspace.room, update); } /** @@ -250,19 +247,6 @@ export class DataCenter { } } - /** - * - * do close current workspace - */ - public async closeCurrentWorkspace() { - assert(this.currentWorkspace?.room, 'No workspace to close'); - const currentWorkspace = this._workspaces.find(this.currentWorkspace.room); - assert(currentWorkspace, 'Workspace not found'); - const provider = this.providerMap.get(currentWorkspace.provider); - assert(provider, 'Provider not found'); - await provider.closeWorkspace(currentWorkspace.id); - } - private async _transWorkspaceProvider( workspace: Workspace, providerId: string @@ -296,9 +280,7 @@ export class DataCenter { * Enable workspace cloud * @param {string} id ID of workspace. */ - public async enableWorkspaceCloud( - workspace: Workspace | null = this.currentWorkspace - ) { + public async enableWorkspaceCloud(workspace: Workspace) { assert(workspace?.room, 'No workspace to enable cloud'); return await this._transWorkspaceProvider(workspace, 'affine'); } From dd529f80355aa6134c1ec143f7b60e316ea77587 Mon Sep 17 00:00:00 2001 From: MingLiang Wang Date: Mon, 9 Jan 2023 10:53:18 +0800 Subject: [PATCH 32/32] feat: change new data center --- .../src/components/invite-members/index.tsx | 46 ++-- .../login-modal/LoginOptionButton.tsx | 16 +- .../general/delete/delete.tsx | 10 +- .../workspace-setting/general/general.tsx | 26 +-- .../workspace-setting/general/leave/leave.tsx | 4 +- .../workspace-setting/workspace-setting.tsx | 68 +++--- .../SelectorPopperContent.tsx | 6 +- .../workspace-create/workspace-create.tsx | 42 ++-- .../app/src/pages/invite/[invite_code].tsx | 26 +-- .../src/provider/affine/apis/business.ts | 198 ------------------ 10 files changed, 122 insertions(+), 320 deletions(-) delete mode 100644 packages/data-center/src/provider/affine/apis/business.ts diff --git a/packages/app/src/components/invite-members/index.tsx b/packages/app/src/components/invite-members/index.tsx index f06674cb6b..22a72220ee 100644 --- a/packages/app/src/components/invite-members/index.tsx +++ b/packages/app/src/components/invite-members/index.tsx @@ -60,19 +60,19 @@ export const InviteMembers = ({ setShowTip(false); debounce( () => { - getDataCenter() - .then(dc => - dc.apis.getUserByEmail({ - email: value, - workspace_id: workspaceId, - }) - ) - .then(data => { - if (data?.name) { - setUserData(data); - setShowTip(false); - } - }); + // getDataCenter() + // .then(dc => + // dc.apis.getUserByEmail({ + // email: value, + // workspace_id: workspaceId, + // }) + // ) + // .then(data => { + // if (data?.name) { + // setUserData(data); + // setShowTip(false); + // } + // }); }, 300, true @@ -134,16 +134,16 @@ export const InviteMembers = ({ shape="circle" type="primary" onClick={() => { - getDataCenter() - .then(dc => dc.apis.inviteMember({ id: workspaceId, email })) - .then(() => { - onClose(); - onInviteSuccess && onInviteSuccess(); - }) - .catch(err => { - // toast('Invite failed'); - console.log(err); - }); + // getDataCenter() + // .then(dc => dc.apis.inviteMember({ id: workspaceId, email })) + // .then(() => { + // onClose(); + // onInviteSuccess && onInviteSuccess(); + // }) + // .catch(err => { + // // toast('Invite failed'); + // console.log(err); + // }); }} > Invite diff --git a/packages/app/src/components/login-modal/LoginOptionButton.tsx b/packages/app/src/components/login-modal/LoginOptionButton.tsx index e04d8fecc7..f8e3cdafce 100644 --- a/packages/app/src/components/login-modal/LoginOptionButton.tsx +++ b/packages/app/src/components/login-modal/LoginOptionButton.tsx @@ -9,14 +9,14 @@ export const GoogleLoginButton = () => { return ( { - getDataCenter() - .then(dc => dc.apis.signInWithGoogle?.()) - .then(() => { - triggerLoginModal(); - }) - .catch(error => { - console.log('sign google error', error); - }); + // getDataCenter() + // .then(dc => dc.apis.signInWithGoogle?.()) + // .then(() => { + // triggerLoginModal(); + // }) + // .catch(error => { + // console.log('sign google error', error); + // }); }} > diff --git a/packages/app/src/components/workspace-setting/general/delete/delete.tsx b/packages/app/src/components/workspace-setting/general/delete/delete.tsx index 7fd14ef818..5ba8b120f4 100644 --- a/packages/app/src/components/workspace-setting/general/delete/delete.tsx +++ b/packages/app/src/components/workspace-setting/general/delete/delete.tsx @@ -39,11 +39,11 @@ export const WorkspaceDelete = ({ }; const handleDelete = async () => { - const dc = await getDataCenter(); - await dc.apis.deleteWorkspace({ id: workspaceId }); - router.push(`/workspace/${nextWorkSpaceId}`); - refreshWorkspacesMeta(); - onClose(); + // const dc = await getDataCenter(); + // await dc.apis.deleteWorkspace({ id: workspaceId }); + // router.push(`/workspace/${nextWorkSpaceId}`); + // refreshWorkspacesMeta(); + // onClose(); }; return ( diff --git a/packages/app/src/components/workspace-setting/general/general.tsx b/packages/app/src/components/workspace-setting/general/general.tsx index cef2daccb7..fc3c4cacfd 100644 --- a/packages/app/src/components/workspace-setting/general/general.tsx +++ b/packages/app/src/components/workspace-setting/general/general.tsx @@ -72,19 +72,19 @@ export const GeneralPage = ({ }; const fileChange = async (file: File) => { - setUploading(true); - const blob = new Blob([file], { type: file.type }); - const blobId = await getDataCenter() - .then(dc => dc.apis.uploadBlob({ blob })) - .finally(() => { - setUploading(false); - }); - if (blobId) { - currentWorkspace?.meta.setAvatar(blobId); - workspaces[workspace.id]?.meta.setAvatar(blobId); - setUploading(false); - debouncedRefreshWorkspacesMeta(); - } + // setUploading(true); + // const blob = new Blob([file], { type: file.type }); + // const blobId = await getDataCenter() + // .then(dc => dc.apis.uploadBlob({ blob })) + // .finally(() => { + // setUploading(false); + // }); + // if (blobId) { + // currentWorkspace?.meta.setAvatar(blobId); + // workspaces[workspace.id]?.meta.setAvatar(blobId); + // setUploading(false); + // debouncedRefreshWorkspacesMeta(); + // } }; return workspace ? ( diff --git a/packages/app/src/components/workspace-setting/general/leave/leave.tsx b/packages/app/src/components/workspace-setting/general/leave/leave.tsx index 3c2cf8ce22..baea054915 100644 --- a/packages/app/src/components/workspace-setting/general/leave/leave.tsx +++ b/packages/app/src/components/workspace-setting/general/leave/leave.tsx @@ -28,8 +28,8 @@ export const WorkspaceLeave = ({ const router = useRouter(); const { refreshWorkspacesMeta } = useAppState(); const handleLeave = async () => { - const dc = await getDataCenter(); - await dc.apis.leaveWorkspace({ id: workspaceId }); + // const dc = await getDataCenter(); + // await dc.apis.leaveWorkspace({ id: workspaceId }); router.push(`/workspace/${nextWorkSpaceId}`); refreshWorkspacesMeta(); onClose(); diff --git a/packages/app/src/components/workspace-setting/workspace-setting.tsx b/packages/app/src/components/workspace-setting/workspace-setting.tsx index c1b07e2441..fc11df64bb 100644 --- a/packages/app/src/components/workspace-setting/workspace-setting.tsx +++ b/packages/app/src/components/workspace-setting/workspace-setting.tsx @@ -163,18 +163,18 @@ const MembersPage = ({ workspace }: { workspace: Workspace }) => { const [isInviteModalShow, setIsInviteModalShow] = useState(false); const [members, setMembers] = useState([]); const refreshMembers = useCallback(() => { - getDataCenter() - .then(dc => - dc.apis.getWorkspaceMembers({ - id: workspace.id, - }) - ) - .then(data => { - setMembers(data); - }) - .catch(err => { - console.log(err); - }); + // getDataCenter() + // .then(dc => + // dc.apis.getWorkspaceMembers({ + // id: workspace.id, + // }) + // ) + // .then(data => { + // setMembers(data); + // }) + // .catch(err => { + // console.log(err); + // }); }, [workspace.id]); useEffect(() => { refreshMembers(); @@ -233,17 +233,17 @@ const MembersPage = ({ workspace }: { workspace: Workspace }) => { // confirmText: 'Delete', // confirmType: 'danger', // }).then(confirm => { - getDataCenter() - .then(dc => - dc.apis.removeMember({ - permissionId: member.id, - }) - ) - .then(() => { - // console.log('data: ', data); - toast('Moved to Trash'); - refreshMembers(); - }); + // getDataCenter() + // .then(dc => + // dc.apis.removeMember({ + // permissionId: member.id, + // }) + // ) + // .then(() => { + // // console.log('data: ', data); + // toast('Moved to Trash'); + // refreshMembers(); + // }); // }); }} icon={} @@ -298,17 +298,17 @@ const PublishPage = ({ workspace }: { workspace: Workspace }) => { workspace.public ); const togglePublic = (flag: boolean) => { - getDataCenter() - .then(dc => - dc.apis.updateWorkspace({ - id: workspace.id, - public: flag, - }) - ) - .then(data => { - setPublicStatus(data?.public); - toast('Updated Public Status Success'); - }); + // getDataCenter() + // .then(dc => + // dc.apis.updateWorkspace({ + // id: workspace.id, + // public: flag, + // }) + // ) + // .then(data => { + // setPublicStatus(data?.public); + // toast('Updated Public Status Success'); + // }); }; const copyUrl = () => { navigator.clipboard.writeText(shareUrl); diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx index 8e101307bb..5a35becfcf 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent.tsx @@ -54,9 +54,9 @@ export const SelectorPopperContent = ({ if (type === WorkspaceType.Private) { return { id, member_count: 1, owner: user }; } else { - const dc = await getDataCenter(); - const data = await dc.apis.getWorkspaceDetail({ id }); - return { id, ...data } || { id, member_count: 0, owner: user }; + // const dc = await getDataCenter(); + // const data = await dc.apis.getWorkspaceDetail({ id }); + // return { id, ...data } || { id, member_count: 0, owner: user }; } } }) diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/CreateWorkspaceItem/workspace-create/workspace-create.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/CreateWorkspaceItem/workspace-create/workspace-create.tsx index b07fdeecac..23434c28d2 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/CreateWorkspaceItem/workspace-create/workspace-create.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/CreateWorkspaceItem/workspace-create/workspace-create.tsx @@ -52,10 +52,10 @@ export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => { ctx.fillText(workspaceName[0], 50, 50); canvas.toBlob(blob => { if (blob) { - const blobId = getDataCenter().then(dc => - dc.apis.uploadBlob({ blob }) - ); - resolve(blobId); + // const blobId = getDataCenter().then(dc => + // dc.apis.uploadBlob({ blob }) + // ); + // resolve(blobId); } else { reject(); } @@ -71,23 +71,23 @@ export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => { setCreating(false); }); if (blobId) { - getDataCenter() - .then(dc => - dc.apis.createWorkspace({ name: workspaceName, avatar: blobId }) - ) - .then(async data => { - await refreshWorkspacesMeta(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - router.push(`/workspace/${data.id}`); - onClose(); - }) - .catch(err => { - console.log(err, 'err'); - }) - .finally(() => { - setCreating(false); - }); + // getDataCenter() + // .then(dc => + // dc.apis.createWorkspace({ name: workspaceName, avatar: blobId }) + // ) + // .then(async data => { + // await refreshWorkspacesMeta(); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // router.push(`/workspace/${data.id}`); + // onClose(); + // }) + // .catch(err => { + // console.log(err, 'err'); + // }) + // .finally(() => { + // setCreating(false); + // }); } }; return ( diff --git a/packages/app/src/pages/invite/[invite_code].tsx b/packages/app/src/pages/invite/[invite_code].tsx index c6ef0b6b4e..91488a7397 100644 --- a/packages/app/src/pages/invite/[invite_code].tsx +++ b/packages/app/src/pages/invite/[invite_code].tsx @@ -24,19 +24,19 @@ export default function DevPage() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const [inviteData, setInviteData] = useState(null); useEffect(() => { - getDataCenter() - .then(dc => - dc.apis.acceptInviting({ - invitingCode: router.query.invite_code as string, - }) - ) - .then(data => { - setSuccessInvited(true); - setInviteData(data); - }) - .catch(err => { - console.log('err: ', err); - }); + // getDataCenter() + // .then(dc => + // dc.apis.acceptInviting({ + // invitingCode: router.query.invite_code as string, + // }) + // ) + // .then(data => { + // setSuccessInvited(true); + // setInviteData(data); + // }) + // .catch(err => { + // console.log('err: ', err); + // }); }, [router.query.invite_code]); return ( diff --git a/packages/data-center/src/provider/affine/apis/business.ts b/packages/data-center/src/provider/affine/apis/business.ts deleted file mode 100644 index 11a4720851..0000000000 --- a/packages/data-center/src/provider/affine/apis/business.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { uuidv4 } from '@blocksuite/store'; -// import { getDataCenter } from '.'; -// import { DataCenter } from './datacenter'; -// import { Workspace } from '../types'; - -// export class Business { -// private _dc: DataCenter | undefined; - -// private async _getDc() { -// if (!this._dc) { -// this._dc = await getDataCenter(); -// } -// return this._dc; -// } - -// // async createWorkspace( -// // name: string -// // ): Promise> { -// // let id = ''; -// // let type = WorkspaceType.local; -// // this._dc?.load(uuidv4()); -// // const newWorkspaces = (await this.getWorkspaces()).find(w => w.id === id); -// // return { -// // id: newWorkspaces?.id || '', -// // name, -// // avatar: '', -// // type, -// // }; -// // } - -// // not think out a good way to update workspace meta -// // updateWorkspaceMeta( -// // id: string, -// // meta: { name?: string; avatar: Partial } -// // ) {} - -// // async getWorkspaces(focusUpdated?: boolean): Promise { -// // const dc = await this._getDc(); -// // if (focusUpdated) { -// // await dc.workspacesList.refreshWorkspaceList(); -// // } -// // return dc.workspacesList.getWorkspaces(); -// // } - -// /** -// * Get page list by workspace id -// * @param {string} id ID of workspace. -// */ -// getPagesByWorkspaceId(id: string) { -// return []; -// } - -// /** -// * Observe the update of the workspace -// * @param {function} callback({Workspace[]}). -// */ -// async onWorkspaceChange(cb: (workspaces: Workspace[]) => void) { -// const dc = await this._getDc(); -// dc.workspacesList.on('change', cb); -// } - -// async deleteWorkspace(id: string) { -// const dc = await this._getDc(); -// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); -// if (workspace?.type === WorkspaceType.cloud) { -// dc.apis.deleteWorkspace({ id }); -// } -// dc.delete(id); -// } - -// /** -// * The member of the workspace go to leave workspace -// * @param {string} id ID of workspace. -// */ -// async leaveWorkspace(id: string) { -// const dc = await this._getDc(); -// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); -// if (workspace?.type === WorkspaceType.cloud) { -// dc.apis.leaveWorkspace({ id }); -// dc.delete(id); -// } -// } - -// /** -// * Let the workspace to be public -// * @param {string} id ID of workspace. -// * @param {string} isPublish publish flag of workspace. -// */ -// setWorkspacePublish(id: string, isPublish: boolean): boolean { -// return isPublish; -// } - -// /** -// * Get workspace by workspace id -// * @param {string} id ID of workspace. -// */ -// async getWorkspaceById(id: string) { -// const dc = await this._getDc(); -// const workspace = dc.workspacesList.getWorkspaces().find(w => w.id === id); -// if (workspace?.type === WorkspaceType.cloud) { -// return dc.load(id, { providerId: 'affine' }); -// } else { -// return dc.load(id, { providerId: 'local' }); -// } -// } - -// // no time -// /** -// * Get the members of the workspace -// * @param {string} id ID of workspace. -// */ -// getMembers(id: string): any { -// void 0; -// } -// /** -// * Add a new member to the workspace -// * @param {string} id ID of workspace. -// * @param {string} email new member email. -// */ -// inviteMember(id: string, email: string) { -// void 0; -// } - -// /** -// * remove the new member to the workspace -// * @param {string} workspaceId ID of workspace. -// * @param {string} memberId ID of member -// */ -// removeMember(workspaceId: string, memberId: string) { -// void 0; -// } - -// /** -// * A new member click the invite link, finish to join the workspace -// * @param {string} inviteCode token for invitation. -// */ -// async acceptInvitation(invitingCode: string) { -// const dc = await this._getDc(); -// dc.apis.acceptInviting({ invitingCode }); -// } - -// // check with dark sky -// /** -// * Get login user info -// */ -// getUserInfo() { -// void 0; -// } - -// // TODO check with dark sky -// async login() { -// const dc = await this._getDc(); -// await dc.auth('affine'); -// } - -// // just has no time -// /** -// * Logout and clear login session -// */ -// logout() { -// void 0; -// } - -// // need discuss -// /** -// * Create a connection between local and cloud, sync cloud data to local -// * @param {string} id ID of workspace. -// * @param {string} id type of workspace. -// */ -// // setWorkspaceSyncType(id: string, type: 'local' | 'cloud') {} - -// // need discuss -// /** -// * Select a file to import the workspace -// * @param {File} file file of workspace. -// */ -// importWorkspace(file: File) { -// void 0; -// } - -// // need discuss may be not in apis -// // /** -// // * Generate a file ,and export it to local file system -// // * @param {string} id ID of workspace. -// // */ -// exportWorkspace(id: string) { -// void 0; -// } - -// // need discuss -// // /** -// // * Enable workspace cloud flag -// // * @param {string} id ID of workspace. -// // */ -// enableWorkspaceCloud(id: string) { -// void 0; -// } -// }