Merge branch 'feat/cloud-sync-saika' into feat/datacenter-dev

This commit is contained in:
DiamondThree
2023-01-12 14:10:13 +08:00
13 changed files with 351 additions and 298 deletions

View File

@@ -11,7 +11,7 @@ const EDITOR_VERSION = enableDebugLocal
const profileTarget = { const profileTarget = {
ac: '100.85.73.88:12001', ac: '100.85.73.88:12001',
dev: '192.168.65.20:3000', dev: '100.77.180.48:11001',
local: '127.0.0.1:3000', local: '127.0.0.1:3000',
}; };

View File

@@ -96,12 +96,7 @@ export class DataCenter {
'There is no provider. You should add provider first.' 'There is no provider. You should add provider first.'
); );
const workspaceMeta = await this._mainProvider.createWorkspaceInfo(params); const workspaceUnit = await this._mainProvider.createWorkspace(params);
const workspace = createBlocksuiteWorkspace(workspaceMeta.id);
await this._mainProvider.createWorkspace(workspace, workspaceMeta);
const workspaceUnit = this._workspaceUnitCollection.find(workspaceMeta.id);
return workspaceUnit; return workspaceUnit;
} }
@@ -169,7 +164,9 @@ export class DataCenter {
this._workspaceInstances.set(workspaceId, workspace); this._workspaceInstances.set(workspaceId, workspace);
await provider.warpWorkspace(workspace); await provider.warpWorkspace(workspace);
this._workspaceUnitCollection.workspaces.forEach(workspaceUnit => { this._workspaceUnitCollection.workspaces.forEach(workspaceUnit => {
workspaceUnit.setBlocksuiteWorkspace(null); const provider = this.providerMap.get(workspaceUnit.provider);
assert(provider);
provider.closeWorkspace(workspaceUnit.id);
}); });
workspaceUnit.setBlocksuiteWorkspace(workspace); workspaceUnit.setBlocksuiteWorkspace(workspace);
return workspaceUnit; return workspaceUnit;
@@ -322,55 +319,33 @@ export class DataCenter {
} }
} }
private async _transWorkspaceProvider( public async enableProvider(
workspace: BlocksuiteWorkspace, workspaceUnit: WorkspaceUnit,
providerId: string providerId = 'affine'
) { ) {
assert(workspace.room, 'No workspace id'); if (workspaceUnit.provider === providerId) {
const workspaceInfo = this._workspaceUnitCollection.find(workspace.room);
assert(workspaceInfo, 'Workspace not found');
if (workspaceInfo.provider === providerId) {
this._logger('Workspace provider is same'); this._logger('Workspace provider is same');
return; return;
} }
const currentProvider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(providerId);
assert(currentProvider, 'Provider not found'); assert(provider);
const newProvider = this.providerMap.get(providerId); const newWorkspaceUnit = await provider.extendWorkspace(workspaceUnit);
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,
});
assert(newWorkspace, 'Create workspace failed'); // Currently we only allow enable one provider, so after enable new provider,
this._logger( // delete the old workspace from its provider.
`update workspace data from ${workspaceInfo.provider} to ${providerId}` const oldProvider = this.providerMap.get(workspaceUnit.provider);
); assert(oldProvider);
await newProvider.assign(newWorkspace, workspace); await oldProvider.deleteWorkspace(workspaceUnit.id);
assert(newWorkspace, 'Create workspace failed');
await currentProvider.deleteWorkspace(workspace.room); return newWorkspaceUnit;
return newWorkspace.room;
} }
/** /**
* Enable workspace cloud * Enable workspace cloud
* @param {string} id ID of workspace. * @param {string} id ID of workspace.
*/ */
public async enableWorkspaceCloud(workspace: WorkspaceUnit) { public async enableWorkspaceCloud(workspaceUnit: WorkspaceUnit) {
assert(workspace?.id, 'No workspace to enable cloud'); return this.enableProvider(workspaceUnit);
assert(workspace.blocksuiteWorkspace);
return await this._transWorkspaceProvider(
workspace.blocksuiteWorkspace,
'affine'
);
} }
/** /**

View File

@@ -6,7 +6,6 @@ import type {
} from '../base'; } from '../base';
import type { User } from '../../types'; import type { User } from '../../types';
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { BlockSchema } from '@blocksuite/blocks/models';
import { storage } from './storage.js'; import { storage } from './storage.js';
import assert from 'assert'; import assert from 'assert';
import { WebsocketProvider } from './sync.js'; import { WebsocketProvider } from './sync.js';
@@ -14,10 +13,17 @@ import { WebsocketProvider } from './sync.js';
import { getApis, Workspace } from './apis/index.js'; import { getApis, Workspace } from './apis/index.js';
import type { Apis, WorkspaceDetail, Callback } from './apis'; import type { Apis, WorkspaceDetail, Callback } from './apis';
import { setDefaultAvatar } from '../utils.js'; import { setDefaultAvatar } from '../utils.js';
import { MessageCode } from '../../message'; import { MessageCode } from '../../message/index.js';
import { token } from './apis/token.js'; import { token } from './apis/token.js';
import { WebsocketClient } from './channel'; 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 = { type ChannelMessage = {
ws_list: Workspace[]; ws_list: Workspace[];
@@ -31,7 +37,7 @@ export interface AffineProviderConstructorParams
} }
const { const {
Y: { applyUpdate, encodeStateAsUpdate }, Y: { encodeStateAsUpdate },
} = BlocksuiteWorkspace; } = BlocksuiteWorkspace;
export class AffineProvider extends BaseProvider { export class AffineProvider extends BaseProvider {
@@ -95,13 +101,13 @@ export class AffineProvider extends BaseProvider {
); );
} }
this._channel.on('message', (msg: ChannelMessage) => { 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'); 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]; const { name, avatar } = metadata[id];
assert(name); assert(name);
const workspace = { const workspace = {
@@ -121,7 +127,11 @@ export class AffineProvider extends BaseProvider {
if (this._workspaces.get(id)) { if (this._workspaces.get(id)) {
this._workspaces.update(id, workspace); this._workspaces.update(id, workspace);
} else { } 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, blocksuiteWorkspace: BlocksuiteWorkspace,
published = false published = false
) { ) {
const { doc, room: workspaceId } = blocksuiteWorkspace; const { room: workspaceId } = blocksuiteWorkspace;
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
const updates = await this._apis.downloadWorkspace(workspaceId, published); const updates = await this._apis.downloadWorkspace(workspaceId, published);
if (updates && updates.byteLength) { await applyUpdate(blocksuiteWorkspace, new Uint8Array(updates));
await new Promise(resolve => {
doc.once('update', () => {
setTimeout(resolve, 100);
});
BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates));
});
}
} }
override async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) { override async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) {
@@ -194,79 +197,25 @@ export class AffineProvider extends BaseProvider {
return []; return [];
} }
const workspacesList = await this._apis.getWorkspaces(); const workspacesList = await this._apis.getWorkspaces();
const workspaces: WorkspaceMeta0[] = workspacesList.map(w => { const workspaceUnits = await Promise.all(
return { workspacesList.map(w => {
...w, return loadWorkspaceUnit(
published: w.public, {
memberCount: 0, id: w.id,
name: '', name: '',
provider: 'affine', avatar: undefined,
syncMode: 'core', owner: undefined,
}; published: w.public,
}); memberCount: 1,
const workspaceInstances = workspaces.map(({ id }) => { provider: 'affine',
const workspace = syncMode: 'core',
this._workspacesCache.get(id) || },
new BlocksuiteWorkspace({ this._apis
room: id, );
}).register(BlockSchema); })
this._workspacesCache.set(id, workspace); );
if (workspace) { this._workspaces.add(workspaceUnits);
return new Promise<BlocksuiteWorkspace>(resolve => { return workspaceUnits;
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;
} }
override async auth() { override async auth() {
@@ -353,52 +302,29 @@ export class AffineProvider extends BaseProvider {
// return workspace; // return workspace;
} }
public override async createWorkspaceInfo( public override async createWorkspace(
meta: CreateWorkspaceInfoParams meta: CreateWorkspaceInfoParams
): Promise<WorkspaceMeta0> { ): Promise<WorkspaceUnit | undefined> {
const { id } = await this._apis.createWorkspace(meta); const { id } = await this._apis.createWorkspace(meta);
const workspaceInfo: WorkspaceMeta0 = { const workspaceUnit = await createWorkspaceUnit({
id,
name: meta.name, name: meta.name,
id: id, avatar: undefined,
published: false,
avatar: '',
owner: await this.getUserInfo(), 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, published: false,
avatar: '',
owner: undefined,
syncMode: 'core',
memberCount: 1, memberCount: 1,
provider: 'affine', provider: 'affine',
}; syncMode: 'core',
});
if (!blocksuiteWorkspace.meta.avatar) { await syncToCloud(
await setDefaultAvatar(blocksuiteWorkspace); workspaceUnit.blocksuiteWorkspace!,
workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar; this._apis.token.refresh
} );
this._workspaces.add(workspaceInfo); this._workspaces.add(workspaceUnit);
return blocksuiteWorkspace;
return workspaceUnit;
} }
public override async publish(id: string, isPublish: boolean): Promise<void> { public override async publish(id: string, isPublish: boolean): Promise<void> {
@@ -420,22 +346,36 @@ export class AffineProvider extends BaseProvider {
: null; : null;
} }
public override async assign( public override async extendWorkspace(
to: BlocksuiteWorkspace, workspaceUnit: WorkspaceUnit
from: BlocksuiteWorkspace ): Promise<WorkspaceUnit> {
): Promise<BlocksuiteWorkspace> { const { id } = await this._apis.createWorkspace({
assert(to.room, 'Blocksuite Workspace without room(workspaceId).'); name: workspaceUnit.name,
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());
}); });
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> { public override async logout(): Promise<void> {

View 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;
};

View File

@@ -1,8 +1,8 @@
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { MessageCenter } from '../message'; import { MessageCenter } from '../message';
import { Logger, User } from '../types'; import { Logger, User } from '../types';
import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection'; 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'; import { Member } from './affine/apis';
const defaultLogger = () => { const defaultLogger = () => {
@@ -44,12 +44,6 @@ export class BaseProvider {
return; return;
} }
public async createWorkspaceInfo(
params: CreateWorkspaceInfoParams
): Promise<WorkspaceMeta0> {
throw new Error(`provider: ${this.id} createWorkspaceInfo Not implemented`);
}
/** /**
* auth provider * auth provider
*/ */
@@ -87,7 +81,7 @@ export class BaseProvider {
/** /**
* load workspaces * load workspaces
**/ **/
public async loadWorkspaces(): Promise<WorkspaceMeta0[]> { public async loadWorkspaces(): Promise<WorkspaceUnit[]> {
throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`);
} }
@@ -183,13 +177,18 @@ export class BaseProvider {
/** /**
* create workspace by workspace meta * create workspace by workspace meta
* @param {WorkspaceMeta} meta * @param {CreateWorkspaceInfoParams} meta
*/ */
public async createWorkspace( public async createWorkspace(
blocksuiteWorkspace: BlocksuiteWorkspace, meta: CreateWorkspaceInfoParams
meta: WorkspaceMeta0 ): Promise<WorkspaceUnit | undefined> {
): Promise<BlocksuiteWorkspace | undefined> { throw new Error(`provider: ${this.id} createWorkspace not implemented`);
return blocksuiteWorkspace; }
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; return workspace;
} }
/**
* merge one workspaces to another
* @param workspace
* @returns
*/
public async assign(to: BlocksuiteWorkspace, from: BlocksuiteWorkspace) {
from;
return to;
}
/** /**
* get workspace members * get workspace members
* @param {string} workspaceId * @param {string} workspaceId

View File

@@ -1,10 +1,13 @@
import assert from 'assert'; import assert from 'assert';
import * as idb from 'lib0/indexeddb.js'; import * as idb from 'lib0/indexeddb.js';
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; 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; const workspaceId = blocksuiteWorkspace.room;
assert(workspaceId); assert(workspaceId);
await idb.deleteDB(workspaceId); await idb.deleteDB(workspaceId);
@@ -18,3 +21,21 @@ export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => {
await idb.addAutoKey(updatesStore, currState); 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;
};

View File

@@ -16,12 +16,10 @@ test.describe.serial('local provider', () => {
let workspaceId: string | undefined; let workspaceId: string | undefined;
test('create workspace', async () => { test('create workspace', async () => {
const workspaceInfo = await provider.createWorkspaceInfo({ const workspaceUnit = await provider.createWorkspace({
name: workspaceName, name: workspaceName,
}); });
workspaceId = workspaceInfo.id; workspaceId = workspaceUnit?.id;
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceId);
await provider.createWorkspace(blocksuiteWorkspace, workspaceInfo);
expect(workspaceMetaCollection.workspaces.length).toEqual(1); expect(workspaceMetaCollection.workspaces.length).toEqual(1);
expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName); expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName);

View File

@@ -8,9 +8,9 @@ import type {
import { varStorage as storage } from 'lib0/storage'; import { varStorage as storage } from 'lib0/storage';
import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
import { IndexedDBProvider } from './indexeddb/indexeddb.js'; import { IndexedDBProvider } from './indexeddb/indexeddb.js';
import { initStore } from './indexeddb/utils.js';
import assert from 'assert'; 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'; const WORKSPACE_KEY = 'workspaces';
@@ -22,21 +22,12 @@ export class LocalProvider extends BaseProvider {
super(params); super(params);
} }
private _storeWorkspaces(workspaces: WorkspaceMeta0[]) { private _storeWorkspaces(workspaceUnits: WorkspaceUnit[]) {
storage.setItem( storage.setItem(
WORKSPACE_KEY, WORKSPACE_KEY,
JSON.stringify( JSON.stringify(
workspaces.map(w => { workspaceUnits.map(w => {
return { return w.toJSON();
id: w.id,
name: w.name,
avatar: w.avatar,
owner: w.owner,
published: w.published,
memberCount: w.memberCount,
provider: w.provider,
syncMode: w.syncMode,
};
}) })
) )
); );
@@ -61,20 +52,23 @@ export class LocalProvider extends BaseProvider {
return workspace; return workspace;
} }
override loadWorkspaces(): Promise<WorkspaceMeta0[]> { override async loadWorkspaces(): Promise<WorkspaceUnit[]> {
const workspaceStr = storage.getItem(WORKSPACE_KEY); const workspaceStr = storage.getItem(WORKSPACE_KEY);
let workspaces: WorkspaceMeta0[] = [];
if (workspaceStr) { if (workspaceStr) {
try { try {
workspaces = JSON.parse(workspaceStr) as WorkspaceMeta0[]; const workspaceMetas = JSON.parse(workspaceStr) as WorkspaceMeta0[];
workspaces.forEach(workspace => { const workspaceUnits = await Promise.all(
this._workspaces.add(workspace); workspaceMetas.map(meta => {
}); return loadWorkspaceUnit(meta);
})
);
this._workspaces.add(workspaceUnits);
return workspaceUnits;
} catch (error) { } catch (error) {
this._logger(`Failed to parse workspaces from storage`); this._logger(`Failed to parse workspaces from storage`);
} }
} }
return Promise.resolve(workspaces); return [];
} }
public override async deleteWorkspace(id: string): Promise<void> { public override async deleteWorkspace(id: string): Promise<void> {
@@ -96,10 +90,10 @@ export class LocalProvider extends BaseProvider {
this._storeWorkspaces(this._workspaces.list()); this._storeWorkspaces(this._workspaces.list());
} }
public override async createWorkspaceInfo( public override async createWorkspace(
meta: CreateWorkspaceInfoParams meta: CreateWorkspaceInfoParams
): Promise<WorkspaceMeta0> { ): Promise<WorkspaceUnit | undefined> {
const workspaceInfo: WorkspaceMeta0 = { const workspaceUnit = await createWorkspaceUnit({
name: meta.name, name: meta.name,
id: uuidv4(), id: uuidv4(),
published: false, published: false,
@@ -108,35 +102,10 @@ export class LocalProvider extends BaseProvider {
syncMode: 'core', syncMode: 'core',
memberCount: 1, memberCount: 1,
provider: 'local', provider: 'local',
}; });
return Promise.resolve(workspaceInfo); this._workspaces.add(workspaceUnit);
}
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._storeWorkspaces(this._workspaces.list()); this._storeWorkspaces(this._workspaces.list());
return workspaceUnit;
return blocksuiteWorkspace;
} }
public override async clear(): Promise<void> { public override async clear(): Promise<void> {

View 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;
};

View File

@@ -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));
});
}
};

View File

@@ -1,6 +1,7 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { WorkspaceUnitCollection } from './workspace-unit-collection.js'; import { WorkspaceUnitCollection } from './workspace-unit-collection.js';
import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection'; import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection';
import { WorkspaceUnit } from './workspace-unit.js';
test.describe.serial('workspace meta collection observable', () => { test.describe.serial('workspace meta collection observable', () => {
const workspaceUnitCollection = new WorkspaceUnitCollection(); const workspaceUnitCollection = new WorkspaceUnitCollection();
@@ -14,13 +15,15 @@ test.describe.serial('workspace meta collection observable', () => {
expect(event.added?.[0]?.id).toEqual('123'); expect(event.added?.[0]?.id).toEqual('123');
} }
); );
scope.add({ const workspaceUnit = new WorkspaceUnit({
id: '123', id: '123',
name: 'test', name: 'test',
avatar: undefined,
memberCount: 1, memberCount: 1,
provider: '', provider: '',
syncMode: 'core', syncMode: 'core',
}); });
scope.add(workspaceUnit);
}); });
test('list workspace', () => { test('list workspace', () => {

View File

@@ -1,19 +1,18 @@
import { Observable } from 'lib0/observable'; import { Observable } from 'lib0/observable';
import { WorkspaceUnit } from './workspace-unit.js';
import type { import type {
WorkspaceUnitCtorParams, WorkspaceUnit,
UpdateWorkspaceUnitParams, UpdateWorkspaceUnitParams,
} from './workspace-unit'; } from './workspace-unit';
export interface WorkspaceUnitCollectionScope { export interface WorkspaceUnitCollectionScope {
get: (workspaceId: string) => WorkspaceUnit | undefined; get: (workspaceId: string) => WorkspaceUnit | undefined;
list: () => WorkspaceUnit[]; list: () => WorkspaceUnit[];
add: (workspace: WorkspaceUnitCtorParams) => void; add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void;
remove: (workspaceId: string) => boolean; remove: (workspaceId: string) => boolean;
clear: () => void; clear: () => void;
update: ( update: (
workspaceId: string, workspaceId: string,
workspaceMeta: UpdateWorkspaceUnitParams workspaceUnit: UpdateWorkspaceUnitParams
) => void; ) => void;
} }
@@ -59,20 +58,23 @@ export class WorkspaceUnitCollection {
return this._workspaceUnitMap.get(workspaceId); return this._workspaceUnitMap.get(workspaceId);
}; };
const add = (workspace: WorkspaceUnitCtorParams) => { const add = (workspaceUnit: WorkspaceUnit | WorkspaceUnit[]) => {
if (this._workspaceUnitMap.has(workspace.id)) { const workspaceUnits = Array.isArray(workspaceUnit)
// FIXME: multiple add same workspace ? workspaceUnit
return; : [workspaceUnit];
}
const workspaceUnit = new WorkspaceUnit(workspace); workspaceUnits.forEach(workspaceUnit => {
this._workspaceUnitMap.set(workspace.id.toString(), workspaceUnit); if (this._workspaceUnitMap.has(workspaceUnit.id)) {
// FIXME: multiple add same workspace
scopedWorkspaceIds.add(workspace.id); return;
}
this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit);
scopedWorkspaceIds.add(workspaceUnit.id);
});
this._events.emit('change', [ this._events.emit('change', [
{ {
added: [workspaceUnit], added: workspaceUnits,
} as WorkspaceUnitCollectionChangeEvent, } as WorkspaceUnitCollectionChangeEvent,
]); ]);
}; };
@@ -107,10 +109,7 @@ export class WorkspaceUnitCollection {
}); });
}; };
const update = ( const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => {
workspaceId: string,
workspaceMeta: UpdateWorkspaceUnitParams
) => {
if (!scopedWorkspaceIds.has(workspaceId)) { if (!scopedWorkspaceIds.has(workspaceId)) {
return true; return true;
} }
@@ -120,7 +119,7 @@ export class WorkspaceUnitCollection {
return true; return true;
} }
workspaceUnit.update(workspaceMeta); workspaceUnit.update(meta);
this._events.emit('change', [ this._events.emit('change', [
{ {

View File

@@ -37,6 +37,16 @@ export class WorkspaceUnit {
this.update(params); 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() { get blocksuiteWorkspace() {
return this._blocksuiteWorkspace; return this._blocksuiteWorkspace;
} }
@@ -52,13 +62,16 @@ export class WorkspaceUnit {
Object.assign(this, params); Object.assign(this, params);
} }
get isPublish() { toJSON(): Omit<WorkspaceUnitCtorParams, 'blocksuiteWorkspace'> {
console.error('Suggest changing to published'); return {
return this.published; id: this.id,
} name: this.name,
avatar: this.avatar,
get isLocal() { owner: this.owner,
console.error('Suggest changing to syncMode'); published: this.published,
return this.syncMode === 'all'; memberCount: this.memberCount,
provider: this.provider,
syncMode: this.syncMode,
};
} }
} }