mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
Merge branch 'feat/cloud-sync-saika' into feat/datacenter-dev
This commit is contained in:
@@ -96,12 +96,7 @@ export class DataCenter {
|
||||
'There is no provider. You should add provider first.'
|
||||
);
|
||||
|
||||
const workspaceMeta = await this._mainProvider.createWorkspaceInfo(params);
|
||||
|
||||
const workspace = createBlocksuiteWorkspace(workspaceMeta.id);
|
||||
|
||||
await this._mainProvider.createWorkspace(workspace, workspaceMeta);
|
||||
const workspaceUnit = this._workspaceUnitCollection.find(workspaceMeta.id);
|
||||
const workspaceUnit = await this._mainProvider.createWorkspace(params);
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
@@ -169,7 +164,9 @@ export class DataCenter {
|
||||
this._workspaceInstances.set(workspaceId, workspace);
|
||||
await provider.warpWorkspace(workspace);
|
||||
this._workspaceUnitCollection.workspaces.forEach(workspaceUnit => {
|
||||
workspaceUnit.setBlocksuiteWorkspace(null);
|
||||
const provider = this.providerMap.get(workspaceUnit.provider);
|
||||
assert(provider);
|
||||
provider.closeWorkspace(workspaceUnit.id);
|
||||
});
|
||||
workspaceUnit.setBlocksuiteWorkspace(workspace);
|
||||
return workspaceUnit;
|
||||
@@ -322,55 +319,33 @@ export class DataCenter {
|
||||
}
|
||||
}
|
||||
|
||||
private async _transWorkspaceProvider(
|
||||
workspace: BlocksuiteWorkspace,
|
||||
providerId: string
|
||||
public async enableProvider(
|
||||
workspaceUnit: WorkspaceUnit,
|
||||
providerId = 'affine'
|
||||
) {
|
||||
assert(workspace.room, 'No workspace id');
|
||||
const workspaceInfo = this._workspaceUnitCollection.find(workspace.room);
|
||||
assert(workspaceInfo, 'Workspace not found');
|
||||
if (workspaceInfo.provider === providerId) {
|
||||
if (workspaceUnit.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(providerId);
|
||||
assert(newProvider, `provide '${providerId}' is not registered`);
|
||||
this._logger(`create ${providerId} workspace: `, workspaceInfo.name);
|
||||
const newWorkspaceInfo = await newProvider.createWorkspaceInfo({
|
||||
name: workspaceInfo.name,
|
||||
// avatar: workspaceInfo.avatar,
|
||||
});
|
||||
const newWorkspace = createBlocksuiteWorkspace(newWorkspaceInfo.id);
|
||||
// TODO optimize this function
|
||||
await newProvider.createWorkspace(newWorkspace, {
|
||||
...newWorkspaceInfo,
|
||||
name: workspaceInfo.name,
|
||||
avatar: workspaceInfo.avatar,
|
||||
});
|
||||
const provider = this.providerMap.get(providerId);
|
||||
assert(provider);
|
||||
const newWorkspaceUnit = await provider.extendWorkspace(workspaceUnit);
|
||||
|
||||
assert(newWorkspace, 'Create workspace failed');
|
||||
this._logger(
|
||||
`update workspace data from ${workspaceInfo.provider} to ${providerId}`
|
||||
);
|
||||
await newProvider.assign(newWorkspace, workspace);
|
||||
assert(newWorkspace, 'Create workspace failed');
|
||||
await currentProvider.deleteWorkspace(workspace.room);
|
||||
return newWorkspace.room;
|
||||
// Currently we only allow enable one provider, so after enable new provider,
|
||||
// delete the old workspace from its provider.
|
||||
const oldProvider = this.providerMap.get(workspaceUnit.provider);
|
||||
assert(oldProvider);
|
||||
await oldProvider.deleteWorkspace(workspaceUnit.id);
|
||||
|
||||
return newWorkspaceUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable workspace cloud
|
||||
* @param {string} id ID of workspace.
|
||||
*/
|
||||
public async enableWorkspaceCloud(workspace: WorkspaceUnit) {
|
||||
assert(workspace?.id, 'No workspace to enable cloud');
|
||||
assert(workspace.blocksuiteWorkspace);
|
||||
return await this._transWorkspaceProvider(
|
||||
workspace.blocksuiteWorkspace,
|
||||
'affine'
|
||||
);
|
||||
public async enableWorkspaceCloud(workspaceUnit: WorkspaceUnit) {
|
||||
return this.enableProvider(workspaceUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
} from '../base';
|
||||
import type { User } from '../../types';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||
import { storage } from './storage.js';
|
||||
import assert from 'assert';
|
||||
import { WebsocketProvider } from './sync.js';
|
||||
@@ -14,10 +13,17 @@ import { WebsocketProvider } from './sync.js';
|
||||
import { getApis, Workspace } from './apis/index.js';
|
||||
import type { Apis, WorkspaceDetail, Callback } from './apis';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
import { MessageCode } from '../../message';
|
||||
import { MessageCode } from '../../message/index.js';
|
||||
import { token } from './apis/token.js';
|
||||
import { WebsocketClient } from './channel';
|
||||
import { SyncMode } from '../../workspace-unit';
|
||||
import {
|
||||
loadWorkspaceUnit,
|
||||
createWorkspaceUnit,
|
||||
syncToCloud,
|
||||
} from './utils.js';
|
||||
import { WorkspaceUnit } from '../../workspace-unit.js';
|
||||
import { createBlocksuiteWorkspace, applyUpdate } from '../../utils/index.js';
|
||||
import type { SyncMode } from '../../workspace-unit';
|
||||
|
||||
type ChannelMessage = {
|
||||
ws_list: Workspace[];
|
||||
@@ -31,7 +37,7 @@ export interface AffineProviderConstructorParams
|
||||
}
|
||||
|
||||
const {
|
||||
Y: { applyUpdate, encodeStateAsUpdate },
|
||||
Y: { encodeStateAsUpdate },
|
||||
} = BlocksuiteWorkspace;
|
||||
|
||||
export class AffineProvider extends BaseProvider {
|
||||
@@ -95,13 +101,13 @@ export class AffineProvider extends BaseProvider {
|
||||
);
|
||||
}
|
||||
this._channel.on('message', (msg: ChannelMessage) => {
|
||||
this.handlerAffineListMessage(msg);
|
||||
this._handlerAffineListMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
private handlerAffineListMessage({ ws_details, metadata }: ChannelMessage) {
|
||||
private _handlerAffineListMessage({ ws_details, metadata }: ChannelMessage) {
|
||||
this._logger('receive server message');
|
||||
Object.entries(ws_details).forEach(([id, detail]) => {
|
||||
Object.entries(ws_details).forEach(async ([id, detail]) => {
|
||||
const { name, avatar } = metadata[id];
|
||||
assert(name);
|
||||
const workspace = {
|
||||
@@ -121,7 +127,11 @@ export class AffineProvider extends BaseProvider {
|
||||
if (this._workspaces.get(id)) {
|
||||
this._workspaces.update(id, workspace);
|
||||
} else {
|
||||
this._workspaces.add({ id, ...workspace });
|
||||
const workspaceUnit = await loadWorkspaceUnit(
|
||||
{ id, ...workspace },
|
||||
this._apis
|
||||
);
|
||||
this._workspaces.add(workspaceUnit);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,17 +157,10 @@ export class AffineProvider extends BaseProvider {
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
published = false
|
||||
) {
|
||||
const { doc, room: workspaceId } = blocksuiteWorkspace;
|
||||
const { room: workspaceId } = blocksuiteWorkspace;
|
||||
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
|
||||
const updates = await this._apis.downloadWorkspace(workspaceId, published);
|
||||
if (updates && updates.byteLength) {
|
||||
await new Promise(resolve => {
|
||||
doc.once('update', () => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates));
|
||||
});
|
||||
}
|
||||
await applyUpdate(blocksuiteWorkspace, new Uint8Array(updates));
|
||||
}
|
||||
|
||||
override async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
|
||||
@@ -194,79 +197,25 @@ export class AffineProvider extends BaseProvider {
|
||||
return [];
|
||||
}
|
||||
const workspacesList = await this._apis.getWorkspaces();
|
||||
const workspaces: WorkspaceMeta0[] = workspacesList.map(w => {
|
||||
return {
|
||||
...w,
|
||||
published: w.public,
|
||||
memberCount: 0,
|
||||
name: '',
|
||||
provider: 'affine',
|
||||
syncMode: 'core',
|
||||
};
|
||||
});
|
||||
const workspaceInstances = workspaces.map(({ id }) => {
|
||||
const workspace =
|
||||
this._workspacesCache.get(id) ||
|
||||
new BlocksuiteWorkspace({
|
||||
room: id,
|
||||
}).register(BlockSchema);
|
||||
this._workspacesCache.set(id, workspace);
|
||||
if (workspace) {
|
||||
return new Promise<BlocksuiteWorkspace>(resolve => {
|
||||
this._apis.downloadWorkspace(id).then(data => {
|
||||
applyUpdate(workspace.doc, new Uint8Array(data));
|
||||
resolve(workspace);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
(await Promise.all(workspaceInstances)).forEach((workspace, i) => {
|
||||
if (workspace) {
|
||||
workspaces[i] = {
|
||||
...workspaces[i],
|
||||
name: workspace.meta.name,
|
||||
avatar: workspace.meta.avatar,
|
||||
};
|
||||
}
|
||||
});
|
||||
const getDetailList = workspacesList.map(w => {
|
||||
const { id } = w;
|
||||
return new Promise<{ id: string; detail: WorkspaceDetail | null }>(
|
||||
resolve => {
|
||||
this._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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workspaces.forEach(workspace => {
|
||||
this._workspaces.add(workspace);
|
||||
});
|
||||
|
||||
return workspaces;
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspacesList.map(w => {
|
||||
return loadWorkspaceUnit(
|
||||
{
|
||||
id: w.id,
|
||||
name: '',
|
||||
avatar: undefined,
|
||||
owner: undefined,
|
||||
published: w.public,
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
syncMode: 'core',
|
||||
},
|
||||
this._apis
|
||||
);
|
||||
})
|
||||
);
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
}
|
||||
|
||||
override async auth() {
|
||||
@@ -353,52 +302,29 @@ export class AffineProvider extends BaseProvider {
|
||||
// return workspace;
|
||||
}
|
||||
|
||||
public override async createWorkspaceInfo(
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceMeta0> {
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
const { id } = await this._apis.createWorkspace(meta);
|
||||
|
||||
const workspaceInfo: WorkspaceMeta0 = {
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
id,
|
||||
name: meta.name,
|
||||
id: id,
|
||||
published: false,
|
||||
avatar: '',
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
};
|
||||
return workspaceInfo;
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
meta: WorkspaceMeta0
|
||||
): Promise<BlocksuiteWorkspace | undefined> {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
|
||||
this._logger('Creating affine workspace');
|
||||
|
||||
this._applyCloudUpdates(blocksuiteWorkspace);
|
||||
this.linkLocal(blocksuiteWorkspace);
|
||||
|
||||
const workspaceInfo: WorkspaceMeta0 = {
|
||||
name: meta.name,
|
||||
id: workspaceId,
|
||||
published: false,
|
||||
avatar: '',
|
||||
owner: undefined,
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
};
|
||||
syncMode: 'core',
|
||||
});
|
||||
|
||||
if (!blocksuiteWorkspace.meta.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar;
|
||||
}
|
||||
this._workspaces.add(workspaceInfo);
|
||||
return blocksuiteWorkspace;
|
||||
await syncToCloud(
|
||||
workspaceUnit.blocksuiteWorkspace!,
|
||||
this._apis.token.refresh
|
||||
);
|
||||
this._workspaces.add(workspaceUnit);
|
||||
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
public override async publish(id: string, isPublish: boolean): Promise<void> {
|
||||
@@ -420,22 +346,36 @@ export class AffineProvider extends BaseProvider {
|
||||
: null;
|
||||
}
|
||||
|
||||
public override async assign(
|
||||
to: BlocksuiteWorkspace,
|
||||
from: BlocksuiteWorkspace
|
||||
): Promise<BlocksuiteWorkspace> {
|
||||
assert(to.room, 'Blocksuite Workspace without room(workspaceId).');
|
||||
const ws = this._getWebsocketProvider(to);
|
||||
applyUpdate(to.doc, encodeStateAsUpdate(from.doc));
|
||||
// TODO: upload blobs and make sure doc is synced
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ws.once('synced', () => {
|
||||
setTimeout(() => resolve(), 1000);
|
||||
});
|
||||
ws.once('lost-connection', () => reject());
|
||||
ws.once('connection-error', () => reject());
|
||||
public override async extendWorkspace(
|
||||
workspaceUnit: WorkspaceUnit
|
||||
): Promise<WorkspaceUnit> {
|
||||
const { id } = await this._apis.createWorkspace({
|
||||
name: workspaceUnit.name,
|
||||
});
|
||||
return to;
|
||||
const newWorkspaceUnit = new WorkspaceUnit({
|
||||
id,
|
||||
name: workspaceUnit.name,
|
||||
avatar: undefined,
|
||||
owner: await this.getUserInfo(),
|
||||
published: false,
|
||||
memberCount: 1,
|
||||
provider: 'affine',
|
||||
syncMode: 'core',
|
||||
});
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(id);
|
||||
assert(workspaceUnit.blocksuiteWorkspace);
|
||||
await applyUpdate(
|
||||
blocksuiteWorkspace,
|
||||
encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
await syncToCloud(blocksuiteWorkspace, this._apis.token.refresh);
|
||||
|
||||
newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
this._workspaces.add(newWorkspaceUnit);
|
||||
return newWorkspaceUnit;
|
||||
}
|
||||
|
||||
public override async logout(): Promise<void> {
|
||||
|
||||
90
packages/data-center/src/provider/affine/utils.ts
Normal file
90
packages/data-center/src/provider/affine/utils.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import assert from 'assert';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { WorkspaceUnit } from '../../workspace-unit.js';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { createBlocksuiteWorkspace } from '../../utils/index.js';
|
||||
import type { Apis } from './apis';
|
||||
import { WebsocketProvider } from './sync.js';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
import { applyUpdate } from '../../utils/index.js';
|
||||
|
||||
export const loadWorkspaceUnit = async (
|
||||
params: WorkspaceUnitCtorParams,
|
||||
apis: Apis
|
||||
) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id);
|
||||
|
||||
const updates = await apis.downloadWorkspace(
|
||||
workspaceUnit.id,
|
||||
params.published
|
||||
);
|
||||
applyUpdate(blocksuiteWorkspace, new Uint8Array(updates));
|
||||
|
||||
const details = await apis.getWorkspaceDetail({ id: workspaceUnit.id });
|
||||
const owner = details?.owner;
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
workspaceUnit.update({
|
||||
name: blocksuiteWorkspace.meta.name,
|
||||
avatar: blocksuiteWorkspace.meta.avatar,
|
||||
memberCount: details?.member_count || 1,
|
||||
owner: owner
|
||||
? {
|
||||
id: owner.id,
|
||||
name: owner.name,
|
||||
avatar: owner.avatar_url,
|
||||
email: owner.email,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
export const syncToCloud = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
refreshToken: string
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId, 'Blocksuite workspace without room(workspaceId).');
|
||||
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||
window.location.host
|
||||
}/api/sync/`;
|
||||
|
||||
const ws = new WebsocketProvider(
|
||||
wsUrl,
|
||||
workspaceId,
|
||||
blocksuiteWorkspace.doc,
|
||||
{
|
||||
params: { token: refreshToken },
|
||||
}
|
||||
);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.once('synced', () => {
|
||||
// FIXME: we don't when send local data to cloud successfully, so hack to wait 1s.
|
||||
// Server will support this by add a new api.
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
ws.once('lost-connection', () => reject());
|
||||
ws.once('connection-error', () => reject());
|
||||
});
|
||||
};
|
||||
|
||||
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id);
|
||||
|
||||
blocksuiteWorkspace.meta.setName(workspaceUnit.name);
|
||||
if (!workspaceUnit.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar });
|
||||
}
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { MessageCenter } from '../message';
|
||||
import { Logger, User } from '../types';
|
||||
import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection';
|
||||
import type { WorkspaceUnitCtorParams } from '../workspace-unit';
|
||||
import type { WorkspaceUnitCtorParams, WorkspaceUnit } from '../workspace-unit';
|
||||
import { Member } from './affine/apis';
|
||||
|
||||
const defaultLogger = () => {
|
||||
@@ -44,12 +44,6 @@ export class BaseProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
public async createWorkspaceInfo(
|
||||
params: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceMeta0> {
|
||||
throw new Error(`provider: ${this.id} createWorkspaceInfo Not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
* auth provider
|
||||
*/
|
||||
@@ -87,7 +81,7 @@ export class BaseProvider {
|
||||
/**
|
||||
* load workspaces
|
||||
**/
|
||||
public async loadWorkspaces(): Promise<WorkspaceMeta0[]> {
|
||||
public async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`);
|
||||
}
|
||||
|
||||
@@ -183,13 +177,18 @@ export class BaseProvider {
|
||||
|
||||
/**
|
||||
* create workspace by workspace meta
|
||||
* @param {WorkspaceMeta} meta
|
||||
* @param {CreateWorkspaceInfoParams} meta
|
||||
*/
|
||||
public async createWorkspace(
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
meta: WorkspaceMeta0
|
||||
): Promise<BlocksuiteWorkspace | undefined> {
|
||||
return blocksuiteWorkspace;
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
throw new Error(`provider: ${this.id} createWorkspace not implemented`);
|
||||
}
|
||||
|
||||
public async extendWorkspace(
|
||||
workspaceUnit: WorkspaceUnit
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
throw new Error(`provider: ${this.id} extendWorkspace not implemented`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,16 +213,6 @@ export class BaseProvider {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* merge one workspaces to another
|
||||
* @param workspace
|
||||
* @returns
|
||||
*/
|
||||
public async assign(to: BlocksuiteWorkspace, from: BlocksuiteWorkspace) {
|
||||
from;
|
||||
return to;
|
||||
}
|
||||
|
||||
/**
|
||||
* get workspace members
|
||||
* @param {string} workspaceId
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import * as idb from 'lib0/indexeddb.js';
|
||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||
import { applyUpdate } from '../../../utils/index.js';
|
||||
|
||||
const { encodeStateAsUpdate } = BlocksuiteWorkspace.Y;
|
||||
const { encodeStateAsUpdate, mergeUpdates } = BlocksuiteWorkspace.Y;
|
||||
|
||||
export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => {
|
||||
export const writeUpdatesToLocal = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId);
|
||||
await idb.deleteDB(workspaceId);
|
||||
@@ -18,3 +21,21 @@ export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => {
|
||||
await idb.addAutoKey(updatesStore, currState);
|
||||
}
|
||||
};
|
||||
|
||||
export const applyLocalUpdates = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace
|
||||
) => {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId, 'Blocksuite workspace without room(workspaceId).');
|
||||
const db = await idb.openDB(workspaceId, db =>
|
||||
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||
);
|
||||
|
||||
const [updatesStore] = idb.transact(db, ['updates']); // , 'readonly')
|
||||
if (updatesStore) {
|
||||
const updates = await idb.getAll(updatesStore);
|
||||
const mergedUpdates = mergeUpdates(updates);
|
||||
await applyUpdate(blocksuiteWorkspace, mergedUpdates);
|
||||
}
|
||||
return blocksuiteWorkspace;
|
||||
};
|
||||
|
||||
@@ -16,12 +16,10 @@ test.describe.serial('local provider', () => {
|
||||
let workspaceId: string | undefined;
|
||||
|
||||
test('create workspace', async () => {
|
||||
const workspaceInfo = await provider.createWorkspaceInfo({
|
||||
const workspaceUnit = await provider.createWorkspace({
|
||||
name: workspaceName,
|
||||
});
|
||||
workspaceId = workspaceInfo.id;
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceId);
|
||||
await provider.createWorkspace(blocksuiteWorkspace, workspaceInfo);
|
||||
workspaceId = workspaceUnit?.id;
|
||||
|
||||
expect(workspaceMetaCollection.workspaces.length).toEqual(1);
|
||||
expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName);
|
||||
|
||||
@@ -8,9 +8,9 @@ import type {
|
||||
import { varStorage as storage } from 'lib0/storage';
|
||||
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
|
||||
import { IndexedDBProvider } from './indexeddb/indexeddb.js';
|
||||
import { initStore } from './indexeddb/utils.js';
|
||||
import assert from 'assert';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js';
|
||||
import type { WorkspaceUnit } from '../../workspace-unit';
|
||||
|
||||
const WORKSPACE_KEY = 'workspaces';
|
||||
|
||||
@@ -22,21 +22,12 @@ export class LocalProvider extends BaseProvider {
|
||||
super(params);
|
||||
}
|
||||
|
||||
private _storeWorkspaces(workspaces: WorkspaceMeta0[]) {
|
||||
private _storeWorkspaces(workspaceUnits: WorkspaceUnit[]) {
|
||||
storage.setItem(
|
||||
WORKSPACE_KEY,
|
||||
JSON.stringify(
|
||||
workspaces.map(w => {
|
||||
return {
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
avatar: w.avatar,
|
||||
owner: w.owner,
|
||||
published: w.published,
|
||||
memberCount: w.memberCount,
|
||||
provider: w.provider,
|
||||
syncMode: w.syncMode,
|
||||
};
|
||||
workspaceUnits.map(w => {
|
||||
return w.toJSON();
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -61,20 +52,23 @@ export class LocalProvider extends BaseProvider {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
override loadWorkspaces(): Promise<WorkspaceMeta0[]> {
|
||||
override async loadWorkspaces(): Promise<WorkspaceUnit[]> {
|
||||
const workspaceStr = storage.getItem(WORKSPACE_KEY);
|
||||
let workspaces: WorkspaceMeta0[] = [];
|
||||
if (workspaceStr) {
|
||||
try {
|
||||
workspaces = JSON.parse(workspaceStr) as WorkspaceMeta0[];
|
||||
workspaces.forEach(workspace => {
|
||||
this._workspaces.add(workspace);
|
||||
});
|
||||
const workspaceMetas = JSON.parse(workspaceStr) as WorkspaceMeta0[];
|
||||
const workspaceUnits = await Promise.all(
|
||||
workspaceMetas.map(meta => {
|
||||
return loadWorkspaceUnit(meta);
|
||||
})
|
||||
);
|
||||
this._workspaces.add(workspaceUnits);
|
||||
return workspaceUnits;
|
||||
} catch (error) {
|
||||
this._logger(`Failed to parse workspaces from storage`);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(workspaces);
|
||||
return [];
|
||||
}
|
||||
|
||||
public override async deleteWorkspace(id: string): Promise<void> {
|
||||
@@ -96,10 +90,10 @@ export class LocalProvider extends BaseProvider {
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
}
|
||||
|
||||
public override async createWorkspaceInfo(
|
||||
public override async createWorkspace(
|
||||
meta: CreateWorkspaceInfoParams
|
||||
): Promise<WorkspaceMeta0> {
|
||||
const workspaceInfo: WorkspaceMeta0 = {
|
||||
): Promise<WorkspaceUnit | undefined> {
|
||||
const workspaceUnit = await createWorkspaceUnit({
|
||||
name: meta.name,
|
||||
id: uuidv4(),
|
||||
published: false,
|
||||
@@ -108,35 +102,10 @@ export class LocalProvider extends BaseProvider {
|
||||
syncMode: 'core',
|
||||
memberCount: 1,
|
||||
provider: 'local',
|
||||
};
|
||||
return Promise.resolve(workspaceInfo);
|
||||
}
|
||||
|
||||
public override async createWorkspace(
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
meta: WorkspaceMeta0
|
||||
): Promise<BlocksuiteWorkspace | undefined> {
|
||||
const workspaceId = blocksuiteWorkspace.room;
|
||||
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
|
||||
this._logger('Creating affine workspace');
|
||||
|
||||
const workspaceInfo: WorkspaceMeta0 = {
|
||||
...meta,
|
||||
};
|
||||
|
||||
blocksuiteWorkspace.meta.setName(meta.name);
|
||||
|
||||
if (!meta.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar;
|
||||
}
|
||||
|
||||
await initStore(blocksuiteWorkspace);
|
||||
|
||||
this._workspaces.add(workspaceInfo);
|
||||
});
|
||||
this._workspaces.add(workspaceUnit);
|
||||
this._storeWorkspaces(this._workspaces.list());
|
||||
|
||||
return blocksuiteWorkspace;
|
||||
return workspaceUnit;
|
||||
}
|
||||
|
||||
public override async clear(): Promise<void> {
|
||||
|
||||
34
packages/data-center/src/provider/local/utils.ts
Normal file
34
packages/data-center/src/provider/local/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { WorkspaceUnit } from '../../workspace-unit.js';
|
||||
import type { WorkspaceUnitCtorParams } from '../../workspace-unit';
|
||||
import { createBlocksuiteWorkspace } from '../../utils/index.js';
|
||||
import { applyLocalUpdates, writeUpdatesToLocal } from './indexeddb/utils.js';
|
||||
import { setDefaultAvatar } from '../utils.js';
|
||||
|
||||
export const loadWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id);
|
||||
|
||||
await applyLocalUpdates(blocksuiteWorkspace);
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
|
||||
export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => {
|
||||
const workspaceUnit = new WorkspaceUnit(params);
|
||||
|
||||
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id);
|
||||
blocksuiteWorkspace.meta.setName(workspaceUnit.name);
|
||||
if (!workspaceUnit.avatar) {
|
||||
await setDefaultAvatar(blocksuiteWorkspace);
|
||||
workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar });
|
||||
}
|
||||
|
||||
await writeUpdatesToLocal(blocksuiteWorkspace);
|
||||
|
||||
workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace);
|
||||
|
||||
return workspaceUnit;
|
||||
};
|
||||
@@ -45,3 +45,25 @@ export async function getDefaultHeadImgBlob(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const applyUpdate = async (
|
||||
blocksuiteWorkspace: BlocksuiteWorkspace,
|
||||
updates: Uint8Array
|
||||
) => {
|
||||
if (updates && updates.byteLength) {
|
||||
await new Promise(resolve => {
|
||||
// FIXME: if we merge two empty doc, there will no update event.
|
||||
// So we set a timer to cancel update listener.
|
||||
const doc = blocksuiteWorkspace.doc;
|
||||
const timer = setTimeout(() => {
|
||||
doc.off('update', resolve);
|
||||
resolve(undefined);
|
||||
}, 1000);
|
||||
doc.once('update', () => {
|
||||
clearTimeout(timer);
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WorkspaceUnitCollection } from './workspace-unit-collection.js';
|
||||
import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection';
|
||||
import { WorkspaceUnit } from './workspace-unit.js';
|
||||
|
||||
test.describe.serial('workspace meta collection observable', () => {
|
||||
const workspaceUnitCollection = new WorkspaceUnitCollection();
|
||||
@@ -14,13 +15,15 @@ test.describe.serial('workspace meta collection observable', () => {
|
||||
expect(event.added?.[0]?.id).toEqual('123');
|
||||
}
|
||||
);
|
||||
scope.add({
|
||||
const workspaceUnit = new WorkspaceUnit({
|
||||
id: '123',
|
||||
name: 'test',
|
||||
avatar: undefined,
|
||||
memberCount: 1,
|
||||
provider: '',
|
||||
syncMode: 'core',
|
||||
});
|
||||
scope.add(workspaceUnit);
|
||||
});
|
||||
|
||||
test('list workspace', () => {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Observable } from 'lib0/observable';
|
||||
import { WorkspaceUnit } from './workspace-unit.js';
|
||||
import type {
|
||||
WorkspaceUnitCtorParams,
|
||||
WorkspaceUnit,
|
||||
UpdateWorkspaceUnitParams,
|
||||
} from './workspace-unit';
|
||||
|
||||
export interface WorkspaceUnitCollectionScope {
|
||||
get: (workspaceId: string) => WorkspaceUnit | undefined;
|
||||
list: () => WorkspaceUnit[];
|
||||
add: (workspace: WorkspaceUnitCtorParams) => void;
|
||||
add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void;
|
||||
remove: (workspaceId: string) => boolean;
|
||||
clear: () => void;
|
||||
update: (
|
||||
workspaceId: string,
|
||||
workspaceMeta: UpdateWorkspaceUnitParams
|
||||
workspaceUnit: UpdateWorkspaceUnitParams
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -59,20 +58,23 @@ export class WorkspaceUnitCollection {
|
||||
return this._workspaceUnitMap.get(workspaceId);
|
||||
};
|
||||
|
||||
const add = (workspace: WorkspaceUnitCtorParams) => {
|
||||
if (this._workspaceUnitMap.has(workspace.id)) {
|
||||
// FIXME: multiple add same workspace
|
||||
return;
|
||||
}
|
||||
const add = (workspaceUnit: WorkspaceUnit | WorkspaceUnit[]) => {
|
||||
const workspaceUnits = Array.isArray(workspaceUnit)
|
||||
? workspaceUnit
|
||||
: [workspaceUnit];
|
||||
|
||||
const workspaceUnit = new WorkspaceUnit(workspace);
|
||||
this._workspaceUnitMap.set(workspace.id.toString(), workspaceUnit);
|
||||
|
||||
scopedWorkspaceIds.add(workspace.id);
|
||||
workspaceUnits.forEach(workspaceUnit => {
|
||||
if (this._workspaceUnitMap.has(workspaceUnit.id)) {
|
||||
// FIXME: multiple add same workspace
|
||||
return;
|
||||
}
|
||||
this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit);
|
||||
scopedWorkspaceIds.add(workspaceUnit.id);
|
||||
});
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
added: [workspaceUnit],
|
||||
added: workspaceUnits,
|
||||
} as WorkspaceUnitCollectionChangeEvent,
|
||||
]);
|
||||
};
|
||||
@@ -107,10 +109,7 @@ export class WorkspaceUnitCollection {
|
||||
});
|
||||
};
|
||||
|
||||
const update = (
|
||||
workspaceId: string,
|
||||
workspaceMeta: UpdateWorkspaceUnitParams
|
||||
) => {
|
||||
const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => {
|
||||
if (!scopedWorkspaceIds.has(workspaceId)) {
|
||||
return true;
|
||||
}
|
||||
@@ -120,7 +119,7 @@ export class WorkspaceUnitCollection {
|
||||
return true;
|
||||
}
|
||||
|
||||
workspaceUnit.update(workspaceMeta);
|
||||
workspaceUnit.update(meta);
|
||||
|
||||
this._events.emit('change', [
|
||||
{
|
||||
|
||||
@@ -37,6 +37,16 @@ export class WorkspaceUnit {
|
||||
this.update(params);
|
||||
}
|
||||
|
||||
get isPublish() {
|
||||
console.error('Suggest changing to published');
|
||||
return this.published;
|
||||
}
|
||||
|
||||
get isLocal() {
|
||||
console.error('Suggest changing to syncMode');
|
||||
return this.syncMode === 'all';
|
||||
}
|
||||
|
||||
get blocksuiteWorkspace() {
|
||||
return this._blocksuiteWorkspace;
|
||||
}
|
||||
@@ -52,13 +62,16 @@ export class WorkspaceUnit {
|
||||
Object.assign(this, params);
|
||||
}
|
||||
|
||||
get isPublish() {
|
||||
console.error('Suggest changing to published');
|
||||
return this.published;
|
||||
}
|
||||
|
||||
get isLocal() {
|
||||
console.error('Suggest changing to syncMode');
|
||||
return this.syncMode === 'all';
|
||||
toJSON(): Omit<WorkspaceUnitCtorParams, 'blocksuiteWorkspace'> {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
avatar: this.avatar,
|
||||
owner: this.owner,
|
||||
published: this.published,
|
||||
memberCount: this.memberCount,
|
||||
provider: this.provider,
|
||||
syncMode: this.syncMode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user