refactor(infra): migrate to new infra (#5565)

This commit is contained in:
EYHN 2024-01-30 07:16:39 +00:00
parent 1e3499c323
commit 329fc19852
No known key found for this signature in database
GPG Key ID: 46C9E26A75AB276C
170 changed files with 2007 additions and 4354 deletions

View File

@ -64,7 +64,7 @@ const allPackages = [
'packages/frontend/i18n', 'packages/frontend/i18n',
'packages/frontend/native', 'packages/frontend/native',
'packages/frontend/templates', 'packages/frontend/templates',
'packages/frontend/workspace', 'packages/frontend/workspace-impl',
'packages/common/debug', 'packages/common/debug',
'packages/common/env', 'packages/common/env',
'packages/common/infra', 'packages/common/infra',

5
.github/labeler.yml vendored
View File

@ -29,11 +29,6 @@ mod:plugin-cli:
- any-glob-to-any-file: - any-glob-to-any-file:
- 'tools/plugin-cli/**/*' - 'tools/plugin-cli/**/*'
mod:workspace:
- changed-files:
- any-glob-to-any-file:
- 'packages/common/workspace/**/*'
mod:workspace-impl: mod:workspace-impl:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:

View File

@ -203,6 +203,25 @@ export class ServiceCollection {
this.services.set(normalizedScope, services); this.services.set(normalizedScope, services);
} }
remove(identifier: ServiceIdentifierValue, scope: ServiceScope = ROOT_SCOPE) {
const normalizedScope = stringifyScope(scope);
const normalizedIdentifier = parseIdentifier(identifier);
const normalizedVariant =
normalizedIdentifier.variant ?? DEFAULT_SERVICE_VARIANT;
const services = this.services.get(normalizedScope);
if (!services) {
return;
}
const variants = services.get(normalizedIdentifier.identifierName);
if (!variants) {
return;
}
variants.delete(normalizedVariant);
}
/** /**
* Create a service provider from the collection. * Create a service provider from the collection.
* *
@ -365,7 +384,7 @@ class ServiceCollectionEditor {
*/ */
override = < override = <
Arg1 extends ServiceIdentifier<any>, Arg1 extends ServiceIdentifier<any>,
Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait, Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait | null,
Trait = ServiceIdentifierType<Arg1>, Trait = ServiceIdentifierType<Arg1>,
Deps extends Arg2 extends Type<Trait> Deps extends Arg2 extends Type<Trait>
? TypesToDeps<ConstructorParameters<Arg2>> ? TypesToDeps<ConstructorParameters<Arg2>>
@ -378,7 +397,10 @@ class ServiceCollectionEditor {
arg2: Arg2, arg2: Arg2,
...[arg3]: Arg3 extends [] ? [] : [Arg3] ...[arg3]: Arg3 extends [] ? [] : [Arg3]
): this => { ): this => {
if (arg2 instanceof Function) { if (arg2 === null) {
this.collection.remove(identifier, this.currentScope);
return this;
} else if (arg2 instanceof Function) {
this.collection.addFactory<any>( this.collection.addFactory<any>(
identifier, identifier,
dependenciesToFactory(arg2, arg3 as any[]), dependenciesToFactory(arg2, arg3 as any[]),

View File

@ -26,6 +26,6 @@ export function configureInfraServices(services: ServiceCollection) {
export function configureTestingInfraServices(services: ServiceCollection) { export function configureTestingInfraServices(services: ServiceCollection) {
configureTestingWorkspaceServices(services); configureTestingWorkspaceServices(services);
services.addImpl(GlobalCache, MemoryMemento); services.override(GlobalCache, MemoryMemento);
services.addImpl(GlobalState, MemoryMemento); services.override(GlobalState, MemoryMemento);
} }

View File

@ -17,7 +17,7 @@ export function configurePageServices(services: ServiceCollection) {
services services
.scope(WorkspaceScope) .scope(WorkspaceScope)
.add(PageListService, [Workspace]) .add(PageListService, [Workspace])
.add(PageManager, [Workspace, ServiceProvider]); .add(PageManager, [Workspace, PageListService, ServiceProvider]);
services services
.scope(PageScope) .scope(PageScope)
.add(CleanupService) .add(CleanupService)

View File

@ -2,7 +2,7 @@ import type { PageMeta } from '@blocksuite/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { LiveData } from '../livedata'; import { LiveData } from '../livedata';
import type { Workspace } from '../workspace'; import { SyncEngineStep, type Workspace } from '../workspace';
export class PageListService { export class PageListService {
constructor(private readonly workspace: Workspace) {} constructor(private readonly workspace: Workspace) {}
@ -25,4 +25,26 @@ export class PageListService {
}), }),
[] []
); );
public readonly isReady = LiveData.from<boolean>(
new Observable(subscriber => {
subscriber.next(
this.workspace.engine.status.sync.step === SyncEngineStep.Synced
);
const dispose = this.workspace.engine.onStatusChange.on(() => {
subscriber.next(
this.workspace.engine.status.sync.step === SyncEngineStep.Synced
);
}).dispose;
return () => {
dispose();
};
}),
false
);
public getPageMetaById(id: string) {
return this.pages.value.find(page => page.id === id);
}
} }

View File

@ -1,9 +1,10 @@
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import type { ServiceProvider } from '../di'; import type { ServiceProvider } from '../di';
import { ObjectPool, type RcRef } from '../utils/object-pool'; import { ObjectPool } from '../utils/object-pool';
import type { Workspace } from '../workspace'; import type { Workspace } from '../workspace';
import { configurePageContext } from './context'; import { configurePageContext } from './context';
import type { PageListService } from './list';
import { Page } from './page'; import { Page } from './page';
import { PageScope } from './service-scope'; import { PageScope } from './service-scope';
@ -12,10 +13,20 @@ export class PageManager {
constructor( constructor(
private readonly workspace: Workspace, private readonly workspace: Workspace,
private readonly pageList: PageListService,
private readonly serviceProvider: ServiceProvider private readonly serviceProvider: ServiceProvider
) {} ) {}
open(pageMeta: PageMeta): RcRef<Page> { openByPageId(pageId: string) {
const pageMeta = this.pageList.getPageMetaById(pageId);
if (!pageMeta) {
throw new Error('Page not found');
}
return this.open(pageMeta);
}
open(pageMeta: PageMeta) {
const blockSuitePage = this.workspace.blockSuiteWorkspace.getPage( const blockSuitePage = this.workspace.blockSuiteWorkspace.getPage(
pageMeta.id pageMeta.id
); );
@ -25,7 +36,7 @@ export class PageManager {
const exists = this.pool.get(pageMeta.id); const exists = this.pool.get(pageMeta.id);
if (exists) { if (exists) {
return exists; return { page: exists.obj, release: exists.release };
} }
const serviceCollection = this.serviceProvider.collection const serviceCollection = this.serviceProvider.collection
@ -41,6 +52,8 @@ export class PageManager {
const page = provider.get(Page); const page = provider.get(Page);
return this.pool.put(pageMeta.id, page); const { obj, release } = this.pool.put(pageMeta.id, page);
return { page: obj, release };
} }
} }

View File

@ -11,10 +11,6 @@ export const AwarenessProvider =
export class AwarenessEngine { export class AwarenessEngine {
constructor(public readonly providers: AwarenessProvider[]) {} constructor(public readonly providers: AwarenessProvider[]) {}
static get EMPTY() {
return new AwarenessEngine([]);
}
connect() { connect() {
this.providers.forEach(provider => provider.connect()); this.providers.forEach(provider => provider.connect());
} }

View File

@ -54,10 +54,6 @@ export class BlobEngine {
private readonly remotes: BlobStorage[] private readonly remotes: BlobStorage[]
) {} ) {}
static get EMPTY() {
return new BlobEngine(createEmptyBlobStorage(), []);
}
start() { start() {
if (this.abort || this._status.isStorageOverCapacity) { if (this.abort || this._status.isStorageOverCapacity) {
return; return;
@ -222,21 +218,19 @@ export class BlobEngine {
} }
} }
export function createEmptyBlobStorage() { export const EmptyBlobStorage: BlobStorage = {
return { name: 'empty',
name: 'empty', readonly: true,
readonly: true, async get(_key: string) {
async get(_key: string) { return null;
return null; },
}, async set(_key: string, _value: Blob) {
async set(_key: string, _value: Blob) { throw new Error('not supported');
throw new Error('not supported'); },
}, async delete(_key: string) {
async delete(_key: string) { throw new Error('not supported');
throw new Error('not supported'); },
}, async list() {
async list() { return [];
return []; },
}, };
} satisfies BlobStorage;
}

View File

@ -23,3 +23,22 @@ export interface SyncStorage {
disconnect: (reason: string) => void disconnect: (reason: string) => void
): Promise<() => void>; ): Promise<() => void>;
} }
export const EmptySyncStorage: SyncStorage = {
name: 'empty',
pull: async () => null,
push: async () => {},
subscribe: async () => () => {},
};
export const ReadonlyMappingSyncStorage = (map: {
[key: string]: Uint8Array;
}): SyncStorage => ({
name: 'map',
pull: async (id: string) => {
const data = map[id];
return data ? { data } : null;
},
push: async () => {},
subscribe: async () => () => {},
});

View File

@ -74,8 +74,14 @@ export function configureWorkspaceServices(services: ServiceCollection) {
export function configureTestingWorkspaceServices(services: ServiceCollection) { export function configureTestingWorkspaceServices(services: ServiceCollection) {
services services
.addImpl(WorkspaceListProvider, TestingLocalWorkspaceListProvider, [ .override(WorkspaceListProvider('affine-cloud'), null)
.override(WorkspaceFactory('affine-cloud'), null)
.override(
WorkspaceListProvider('local'),
TestingLocalWorkspaceListProvider,
[GlobalState]
)
.override(WorkspaceFactory('local'), TestingLocalWorkspaceFactory, [
GlobalState, GlobalState,
]) ]);
.addImpl(WorkspaceFactory, TestingLocalWorkspaceFactory, [GlobalState]);
} }

View File

@ -5,7 +5,7 @@ import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { fixWorkspaceVersion } from '../blocksuite'; import { fixWorkspaceVersion } from '../blocksuite';
import type { ServiceProvider } from '../di'; import type { ServiceCollection, ServiceProvider } from '../di';
import { ObjectPool } from '../utils/object-pool'; import { ObjectPool } from '../utils/object-pool';
import { configureWorkspaceContext } from './context'; import { configureWorkspaceContext } from './context';
import type { BlobStorage } from './engine'; import type { BlobStorage } from './engine';
@ -90,6 +90,9 @@ export class WorkspaceManager {
} }
const workspace = this.instantiate(metadata); const workspace = this.instantiate(metadata);
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
this.list.getInformation(metadata).syncWithWorkspace(workspace);
const ref = this.pool.put(workspace.meta.id, workspace); const ref = this.pool.put(workspace.meta.id, workspace);
return { return {
@ -164,14 +167,21 @@ export class WorkspaceManager {
return factory.getWorkspaceBlob(metadata.id, blobKey); return factory.getWorkspaceBlob(metadata.id, blobKey);
} }
private instantiate(metadata: WorkspaceMetadata) { instantiate(
metadata: WorkspaceMetadata,
configureWorkspace?: (serviceCollection: ServiceCollection) => void
) {
logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `); logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `);
const factory = this.factories.find(x => x.name === metadata.flavour);
if (!factory) {
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
}
const serviceCollection = this.serviceProvider.collection.clone(); const serviceCollection = this.serviceProvider.collection.clone();
factory.configureWorkspace(serviceCollection); if (configureWorkspace) {
configureWorkspace(serviceCollection);
} else {
const factory = this.factories.find(x => x.name === metadata.flavour);
if (!factory) {
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
}
factory.configureWorkspace(serviceCollection);
}
configureWorkspaceContext(serviceCollection, metadata); configureWorkspaceContext(serviceCollection, metadata);
const provider = serviceCollection.provider( const provider = serviceCollection.provider(
WorkspaceScope, WorkspaceScope,
@ -179,9 +189,6 @@ export class WorkspaceManager {
); );
const workspace = provider.get(Workspace); const workspace = provider.get(Workspace);
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
this.list.getInformation(metadata).syncWithWorkspace(workspace);
// apply compatibility fix // apply compatibility fix
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc); fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);

View File

@ -1 +0,0 @@
lib

View File

@ -1,24 +0,0 @@
{
"name": "@affine/workspace",
"private": true,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"@blocksuite/blocks": "*",
"@blocksuite/global": "*",
"@blocksuite/store": "*"
},
"dependencies": {
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@toeverything/infra": "workspace:*",
"lodash-es": "^4.17.21",
"yjs": "^13.6.10"
},
"devDependencies": {
"vitest": "1.1.3"
},
"version": "0.12.0"
}

View File

@ -1,4 +0,0 @@
export interface AwarenessProvider {
connect(): void;
disconnect(): void;
}

View File

@ -1,233 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
import { difference } from 'lodash-es';
import { BlobStorageOverCapacity } from './error';
const logger = new DebugLogger('affine:blob-engine');
export interface BlobStatus {
isStorageOverCapacity: boolean;
}
/**
* # BlobEngine
*
* sync blobs between storages in background.
*
* all operations priority use local, then use remote.
*/
export class BlobEngine {
private abort: AbortController | null = null;
private _status: BlobStatus = { isStorageOverCapacity: false };
onStatusChange = new Slot<BlobStatus>();
singleBlobSizeLimit: number = 100 * 1024 * 1024;
onAbortLargeBlob = new Slot<Blob>();
private set status(s: BlobStatus) {
logger.debug('status change', s);
this._status = s;
this.onStatusChange.emit(s);
}
get status() {
return this._status;
}
constructor(
private readonly local: BlobStorage,
private readonly remotes: BlobStorage[]
) {}
start() {
if (this.abort) {
return;
}
this.abort = new AbortController();
const abortSignal = this.abort.signal;
const sync = () => {
if (abortSignal.aborted) {
return;
}
this.sync()
.catch(error => {
logger.error('sync blob error', error);
})
.finally(() => {
// sync every 1 minute
setTimeout(sync, 60000);
});
};
sync();
}
stop() {
this.abort?.abort();
this.abort = null;
}
get storages() {
return [this.local, ...this.remotes];
}
async sync() {
if (this.local.readonly || this._status.isStorageOverCapacity) {
return;
}
logger.debug('start syncing blob...');
for (const remote of this.remotes) {
let localList: string[] = [];
let remoteList: string[] = [];
if (!remote.readonly) {
try {
localList = await this.local.list();
remoteList = await remote.list();
} catch (err) {
logger.error(`error when sync`, err);
continue;
}
const needUpload = difference(localList, remoteList);
for (const key of needUpload) {
try {
const data = await this.local.get(key);
if (data) {
await remote.set(key, data);
}
} catch (err) {
if (err instanceof BlobStorageOverCapacity) {
this.status = {
isStorageOverCapacity: true,
};
}
logger.error(
`error when sync ${key} from [${this.local.name}] to [${remote.name}]`,
err
);
}
}
}
const needDownload = difference(remoteList, localList);
for (const key of needDownload) {
try {
const data = await remote.get(key);
if (data) {
await this.local.set(key, data);
}
} catch (err) {
logger.error(
`error when sync ${key} from [${remote.name}] to [${this.local.name}]`,
err
);
}
}
}
logger.debug('finish syncing blob');
}
async get(key: string) {
logger.debug('get blob', key);
for (const storage of this.storages) {
const data = await storage.get(key);
if (data) {
return data;
}
}
return null;
}
async set(key: string, value: Blob) {
if (this.local.readonly) {
throw new Error('local peer is readonly');
}
if (value.size > this.singleBlobSizeLimit) {
this.onAbortLargeBlob.emit(value);
logger.error('blob over limit, abort set');
return key;
}
// await upload to the local peer
await this.local.set(key, value);
// uploads to other peers in the background
Promise.allSettled(
this.remotes
.filter(r => !r.readonly)
.map(peer =>
peer.set(key, value).catch(err => {
logger.error('Error when uploading to peer', err);
})
)
)
.then(result => {
if (result.some(({ status }) => status === 'rejected')) {
logger.error(
`blob ${key} update finish, but some peers failed to update`
);
} else {
logger.debug(`blob ${key} update finish`);
}
})
.catch(() => {
// Promise.allSettled never reject
});
return key;
}
async delete(_key: string) {
// not supported
}
async list() {
const blobList = new Set<string>();
for (const peer of this.storages) {
const list = await peer.list();
if (list) {
for (const blob of list) {
blobList.add(blob);
}
}
}
return Array.from(blobList);
}
}
export interface BlobStorage {
name: string;
readonly: boolean;
get: (key: string) => Promise<Blob | null>;
set: (key: string, value: Blob) => Promise<string>;
delete: (key: string) => Promise<void>;
list: () => Promise<string[]>;
}
export function createMemoryBlobStorage() {
const map = new Map<string, Blob>();
return {
name: 'memory',
readonly: false,
async get(key: string) {
return map.get(key) ?? null;
},
async set(key: string, value: Blob) {
map.set(key, value);
return key;
},
async delete(key: string) {
map.delete(key);
},
async list() {
return Array.from(map.keys());
},
} satisfies BlobStorage;
}

View File

@ -1,5 +0,0 @@
export class BlobStorageOverCapacity extends Error {
constructor(public originError?: any) {
super('Blob storage over capacity.');
}
}

View File

@ -1,84 +0,0 @@
import { Slot } from '@blocksuite/global/utils';
import { throwIfAborted } from '../utils/throw-if-aborted';
import type { AwarenessProvider } from './awareness';
import type { BlobEngine, BlobStatus } from './blob';
import type { SyncEngine, SyncEngineStatus } from './sync';
export interface WorkspaceEngineStatus {
sync: SyncEngineStatus;
blob: BlobStatus;
}
/**
* # WorkspaceEngine
*
* sync ydoc, blob, awareness together
*/
export class WorkspaceEngine {
_status: WorkspaceEngineStatus;
onStatusChange = new Slot<WorkspaceEngineStatus>();
get status() {
return this._status;
}
set status(status: WorkspaceEngineStatus) {
this._status = status;
this.onStatusChange.emit(status);
}
constructor(
public blob: BlobEngine,
public sync: SyncEngine,
public awareness: AwarenessProvider[]
) {
this._status = {
sync: sync.status,
blob: blob.status,
};
sync.onStatusChange.on(status => {
this.status = {
sync: status,
blob: blob.status,
};
});
blob.onStatusChange.on(status => {
this.status = {
sync: sync.status,
blob: status,
};
});
}
start() {
this.sync.start();
for (const awareness of this.awareness) {
awareness.connect();
}
this.blob.start();
}
canGracefulStop() {
return this.sync.canGracefulStop();
}
async waitForGracefulStop(abort?: AbortSignal) {
await this.sync.waitForGracefulStop(abort);
throwIfAborted(abort);
this.forceStop();
}
forceStop() {
this.sync.forceStop();
for (const awareness of this.awareness) {
awareness.disconnect();
}
this.blob.stop();
}
}
export * from './awareness';
export * from './blob';
export * from './error';
export * from './sync';

View File

@ -1,15 +0,0 @@
export enum SyncEngineStep {
Stopped = 0,
Syncing = 1,
Synced = 2,
}
export enum SyncPeerStep {
Stopped = 0,
Retrying = 1,
LoadingRootDoc = 2,
LoadingSubDoc = 3,
Loaded = 4.5,
Syncing = 5,
Synced = 6,
}

View File

@ -1,278 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
import type { Doc } from 'yjs';
import { SharedPriorityTarget } from '../../utils/async-queue';
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
import { SyncEngineStep, SyncPeerStep } from './consts';
import { SyncPeer, type SyncPeerStatus } from './peer';
import type { SyncStorage } from './storage';
export interface SyncEngineStatus {
step: SyncEngineStep;
local: SyncPeerStatus | null;
remotes: (SyncPeerStatus | null)[];
retrying: boolean;
}
/**
* # SyncEngine
*
* ```
*
* SyncEngine
*
*
*
*
* SyncPeer
* local
*
*
*
*
* SyncPeer SyncPeer SyncPeer
* Remote Remote Remote
*
* ```
*
* Sync engine manage sync peers
*
* Sync steps:
* 1. start local sync
* 2. wait for local sync complete
* 3. start remote sync
* 4. continuously sync local and remote
*/
export class SyncEngine {
get rootDocId() {
return this.rootDoc.guid;
}
logger = new DebugLogger('affine:sync-engine:' + this.rootDocId);
private _status: SyncEngineStatus;
onStatusChange = new Slot<SyncEngineStatus>();
private set status(s: SyncEngineStatus) {
this.logger.debug('status change', s);
this._status = s;
this.onStatusChange.emit(s);
}
priorityTarget = new SharedPriorityTarget();
get status() {
return this._status;
}
private abort = new AbortController();
constructor(
private readonly rootDoc: Doc,
private readonly local: SyncStorage,
private readonly remotes: SyncStorage[]
) {
this._status = {
step: SyncEngineStep.Stopped,
local: null,
remotes: remotes.map(() => null),
retrying: false,
};
}
start() {
if (this.status.step !== SyncEngineStep.Stopped) {
this.forceStop();
}
this.abort = new AbortController();
this.sync(this.abort.signal).catch(err => {
// should never reach here
this.logger.error(err);
});
}
canGracefulStop() {
return !!this.status.local && this.status.local.pendingPushUpdates === 0;
}
async waitForGracefulStop(abort?: AbortSignal) {
await Promise.race([
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
new Promise<void>(resolve => {
this.onStatusChange.on(() => {
if (this.canGracefulStop()) {
resolve();
}
});
}),
]);
throwIfAborted(abort);
this.forceStop();
}
forceStop() {
this.abort.abort(MANUALLY_STOP);
this._status = {
step: SyncEngineStep.Stopped,
local: null,
remotes: this.remotes.map(() => null),
retrying: false,
};
}
// main sync process, should never return until abort
async sync(signal: AbortSignal) {
const state: {
localPeer: SyncPeer | null;
remotePeers: (SyncPeer | null)[];
} = {
localPeer: null,
remotePeers: this.remotes.map(() => null),
};
const cleanUp: (() => void)[] = [];
try {
// Step 1: start local sync peer
state.localPeer = new SyncPeer(
this.rootDoc,
this.local,
this.priorityTarget
);
cleanUp.push(
state.localPeer.onStatusChange.on(() => {
if (!signal.aborted)
this.updateSyncingState(state.localPeer, state.remotePeers);
}).dispose
);
this.updateSyncingState(state.localPeer, state.remotePeers);
// Step 2: wait for local sync complete
await state.localPeer.waitForLoaded(signal);
// Step 3: start remote sync peer
state.remotePeers = this.remotes.map(remote => {
const peer = new SyncPeer(this.rootDoc, remote, this.priorityTarget);
cleanUp.push(
peer.onStatusChange.on(() => {
if (!signal.aborted)
this.updateSyncingState(state.localPeer, state.remotePeers);
}).dispose
);
return peer;
});
this.updateSyncingState(state.localPeer, state.remotePeers);
// Step 4: continuously sync local and remote
// wait for abort
await new Promise((_, reject) => {
if (signal.aborted) {
reject(signal.reason);
}
signal.addEventListener('abort', () => {
reject(signal.reason);
});
});
} catch (error) {
if (error === MANUALLY_STOP || signal.aborted) {
return;
}
throw error;
} finally {
// stop peers
state.localPeer?.stop();
for (const remotePeer of state.remotePeers) {
remotePeer?.stop();
}
for (const clean of cleanUp) {
clean();
}
}
}
updateSyncingState(local: SyncPeer | null, remotes: (SyncPeer | null)[]) {
let step = SyncEngineStep.Synced;
const allPeer = [local, ...remotes];
for (const peer of allPeer) {
if (!peer || peer.status.step !== SyncPeerStep.Synced) {
step = SyncEngineStep.Syncing;
break;
}
}
this.status = {
step,
local: local?.status ?? null,
remotes: remotes.map(peer => peer?.status ?? null),
retrying: allPeer.some(
peer => peer?.status.step === SyncPeerStep.Retrying
),
};
}
async waitForSynced(abort?: AbortSignal) {
if (this.status.step === SyncEngineStep.Synced) {
return;
} else {
return Promise.race([
new Promise<void>(resolve => {
this.onStatusChange.on(status => {
if (status.step === SyncEngineStep.Synced) {
resolve();
}
});
}),
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
}
}
async waitForLoadedRootDoc(abort?: AbortSignal) {
function isLoadedRootDoc(status: SyncEngineStatus) {
return ![status.local, ...status.remotes].some(
peer => !peer || peer.step <= SyncPeerStep.LoadingRootDoc
);
}
if (isLoadedRootDoc(this.status)) {
return;
} else {
return Promise.race([
new Promise<void>(resolve => {
this.onStatusChange.on(status => {
if (isLoadedRootDoc(status)) {
resolve();
}
});
}),
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
}
}
setPriorityRule(target: ((id: string) => boolean) | null) {
this.priorityTarget.priorityRule = target;
}
}

View File

@ -1,20 +0,0 @@
/**
*
* **SyncEngine**
*
* Manages one local storage and multiple remote storages.
*
* Responsible for creating SyncPeers for synchronization, following the local-first strategy.
*
* **SyncPeer**
*
* Responsible for synchronizing a single storage with Y.Doc.
*
* Carries the main synchronization logic.
*
*/
export * from './consts';
export * from './engine';
export * from './peer';
export * from './storage';

View File

@ -1,444 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
import { isEqual } from '@blocksuite/global/utils';
import type { Doc } from 'yjs';
import { applyUpdate, encodeStateAsUpdate, encodeStateVector } from 'yjs';
import {
PriorityAsyncQueue,
SharedPriorityTarget,
} from '../../utils/async-queue';
import { mergeUpdates } from '../../utils/merge-updates';
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
import { SyncPeerStep } from './consts';
import type { SyncStorage } from './storage';
export interface SyncPeerStatus {
step: SyncPeerStep;
totalDocs: number;
loadedDocs: number;
pendingPullUpdates: number;
pendingPushUpdates: number;
}
/**
* # SyncPeer
* A SyncPeer is responsible for syncing one Storage with one Y.Doc and its subdocs.
*
* ```
*
* Start
*
*
*
* listenpull rootdoc peer
*
* onLoad()
*
* listenpull subdocs subscribe
*
* onReady()
*
* queueapply updatesqueue
*
* ```
*
* listen: listen for updates from ydoc, typically from user modifications.
* subscribe: listen for updates from storage, typically from other users.
*
*/
export class SyncPeer {
private _status: SyncPeerStatus = {
step: SyncPeerStep.LoadingRootDoc,
totalDocs: 1,
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
};
onStatusChange = new Slot<SyncPeerStatus>();
readonly abort = new AbortController();
get name() {
return this.storage.name;
}
logger = new DebugLogger('affine:sync-peer:' + this.name);
constructor(
private readonly rootDoc: Doc,
private readonly storage: SyncStorage,
private readonly priorityTarget = new SharedPriorityTarget()
) {
this.logger.debug('peer start');
this.syncRetryLoop(this.abort.signal).catch(err => {
// should not reach here
console.error(err);
});
}
private set status(s: SyncPeerStatus) {
if (!isEqual(s, this._status)) {
this.logger.debug('status change', s);
this._status = s;
this.onStatusChange.emit(s);
}
}
get status() {
return this._status;
}
/**
* stop sync
*
* SyncPeer is one-time use, this peer should be discarded after call stop().
*/
stop() {
this.logger.debug('peer stop');
this.abort.abort(MANUALLY_STOP);
}
/**
* auto retry after 5 seconds if sync failed
*/
async syncRetryLoop(abort: AbortSignal) {
while (abort.aborted === false) {
try {
await this.sync(abort);
} catch (err) {
if (err === MANUALLY_STOP || abort.aborted) {
return;
}
this.logger.error('sync error', err);
}
try {
this.logger.error('retry after 5 seconds');
this.status = {
step: SyncPeerStep.Retrying,
totalDocs: 1,
loadedDocs: 0,
pendingPullUpdates: 0,
pendingPushUpdates: 0,
};
await Promise.race([
new Promise<void>(resolve => {
setTimeout(resolve, 5 * 1000);
}),
new Promise((_, reject) => {
// exit if manually stopped
if (abort.aborted) {
reject(abort.reason);
}
abort.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
} catch (err) {
if (err === MANUALLY_STOP || abort.aborted) {
return;
}
// should never reach here
throw err;
}
}
}
private readonly state: {
connectedDocs: Map<string, Doc>;
pushUpdatesQueue: PriorityAsyncQueue<{
id: string;
data: Uint8Array[];
}>;
pushingUpdate: boolean;
pullUpdatesQueue: PriorityAsyncQueue<{
id: string;
data: Uint8Array;
}>;
subdocLoading: boolean;
subdocsLoadQueue: PriorityAsyncQueue<{ id: string; doc: Doc }>;
} = {
connectedDocs: new Map(),
pushUpdatesQueue: new PriorityAsyncQueue([], this.priorityTarget),
pushingUpdate: false,
pullUpdatesQueue: new PriorityAsyncQueue([], this.priorityTarget),
subdocLoading: false,
subdocsLoadQueue: new PriorityAsyncQueue([], this.priorityTarget),
};
initState() {
this.state.connectedDocs.clear();
this.state.pushUpdatesQueue.clear();
this.state.pullUpdatesQueue.clear();
this.state.subdocsLoadQueue.clear();
this.state.pushingUpdate = false;
this.state.subdocLoading = false;
}
/**
* main synchronization logic
*/
async sync(abortOuter: AbortSignal) {
this.initState();
const abortInner = new AbortController();
abortOuter.addEventListener('abort', reason => {
abortInner.abort(reason);
});
let dispose: (() => void) | null = null;
try {
this.reportSyncStatus();
// start listen storage updates
dispose = await this.storage.subscribe(
this.handleStorageUpdates,
reason => {
// abort if storage disconnect, should trigger retry loop
abortInner.abort('subscribe disconnect:' + reason);
}
);
throwIfAborted(abortInner.signal);
// Step 1: load root doc
await this.connectDoc(this.rootDoc, abortInner.signal);
// Step 2: load subdocs
this.state.subdocsLoadQueue.push(
...Array.from(this.rootDoc.getSubdocs()).map(doc => ({
id: doc.guid,
doc,
}))
);
this.reportSyncStatus();
this.rootDoc.on('subdocs', this.handleSubdocsUpdate);
// Finally: start sync
await Promise.all([
// load subdocs
(async () => {
while (throwIfAborted(abortInner.signal)) {
const subdoc = await this.state.subdocsLoadQueue.next(
abortInner.signal
);
this.state.subdocLoading = true;
this.reportSyncStatus();
await this.connectDoc(subdoc.doc, abortInner.signal);
this.state.subdocLoading = false;
this.reportSyncStatus();
}
})(),
// pull updates
(async () => {
while (throwIfAborted(abortInner.signal)) {
const { id, data } = await this.state.pullUpdatesQueue.next(
abortInner.signal
);
// don't apply empty data or Uint8Array([0, 0])
if (
!(
data.byteLength === 0 ||
(data.byteLength === 2 && data[0] === 0 && data[1] === 0)
)
) {
const subdoc = this.state.connectedDocs.get(id);
if (subdoc) {
applyUpdate(subdoc, data, this.name);
}
}
this.reportSyncStatus();
}
})(),
// push updates
(async () => {
while (throwIfAborted(abortInner.signal)) {
const { id, data } = await this.state.pushUpdatesQueue.next(
abortInner.signal
);
this.state.pushingUpdate = true;
this.reportSyncStatus();
const merged = mergeUpdates(data);
// don't push empty data or Uint8Array([0, 0])
if (
!(
merged.byteLength === 0 ||
(merged.byteLength === 2 && merged[0] === 0 && merged[1] === 0)
)
) {
await this.storage.push(id, merged);
}
this.state.pushingUpdate = false;
this.reportSyncStatus();
}
})(),
]);
} finally {
dispose?.();
for (const docs of this.state.connectedDocs.values()) {
this.disconnectDoc(docs);
}
this.rootDoc.off('subdocs', this.handleSubdocsUpdate);
}
}
async connectDoc(doc: Doc, abort: AbortSignal) {
const { data: docData, state: inStorageState } =
(await this.storage.pull(doc.guid, encodeStateVector(doc))) ?? {};
throwIfAborted(abort);
if (docData) {
applyUpdate(doc, docData, 'load');
}
// diff root doc and in-storage, save updates to pendingUpdates
this.state.pushUpdatesQueue.push({
id: doc.guid,
data: [encodeStateAsUpdate(doc, inStorageState)],
});
this.state.connectedDocs.set(doc.guid, doc);
// start listen root doc changes
doc.on('update', this.handleYDocUpdates);
// mark rootDoc as loaded
doc.emit('sync', [true]);
this.reportSyncStatus();
}
disconnectDoc(doc: Doc) {
doc.off('update', this.handleYDocUpdates);
this.state.connectedDocs.delete(doc.guid);
this.reportSyncStatus();
}
// handle updates from ydoc
handleYDocUpdates = (update: Uint8Array, origin: string, doc: Doc) => {
// don't push updates from storage
if (origin === this.name) {
return;
}
const exist = this.state.pushUpdatesQueue.find(({ id }) => id === doc.guid);
if (exist) {
exist.data.push(update);
} else {
this.state.pushUpdatesQueue.push({
id: doc.guid,
data: [update],
});
}
this.reportSyncStatus();
};
// handle subdocs changes, append new subdocs to queue, remove subdocs from queue
handleSubdocsUpdate = ({
added,
removed,
}: {
added: Set<Doc>;
removed: Set<Doc>;
}) => {
for (const subdoc of added) {
this.state.subdocsLoadQueue.push({ id: subdoc.guid, doc: subdoc });
}
for (const subdoc of removed) {
this.disconnectDoc(subdoc);
this.state.subdocsLoadQueue.remove(doc => doc.doc === subdoc);
}
this.reportSyncStatus();
};
// handle updates from storage
handleStorageUpdates = (id: string, data: Uint8Array) => {
this.state.pullUpdatesQueue.push({
id,
data,
});
this.reportSyncStatus();
};
reportSyncStatus() {
let step;
if (this.state.connectedDocs.size === 0) {
step = SyncPeerStep.LoadingRootDoc;
} else if (this.state.subdocsLoadQueue.length || this.state.subdocLoading) {
step = SyncPeerStep.LoadingSubDoc;
} else if (
this.state.pullUpdatesQueue.length ||
this.state.pushUpdatesQueue.length ||
this.state.pushingUpdate
) {
step = SyncPeerStep.Syncing;
} else {
step = SyncPeerStep.Synced;
}
this.status = {
step: step,
totalDocs:
this.state.connectedDocs.size + this.state.subdocsLoadQueue.length,
loadedDocs: this.state.connectedDocs.size,
pendingPullUpdates:
this.state.pullUpdatesQueue.length + (this.state.subdocLoading ? 1 : 0),
pendingPushUpdates:
this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0),
};
}
async waitForSynced(abort?: AbortSignal) {
if (this.status.step >= SyncPeerStep.Synced) {
return;
} else {
return Promise.race([
new Promise<void>(resolve => {
this.onStatusChange.on(status => {
if (status.step >= SyncPeerStep.Synced) {
resolve();
}
});
}),
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
}
}
async waitForLoaded(abort?: AbortSignal) {
if (this.status.step > SyncPeerStep.Loaded) {
return;
} else {
return Promise.race([
new Promise<void>(resolve => {
this.onStatusChange.on(status => {
if (status.step > SyncPeerStep.Loaded) {
resolve();
}
});
}),
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
}
}
}

View File

@ -1,25 +0,0 @@
export interface SyncStorage {
/**
* for debug
*/
name: string;
pull(
docId: string,
state: Uint8Array
): Promise<{ data: Uint8Array; state?: Uint8Array } | null>;
push(docId: string, data: Uint8Array): Promise<void>;
/**
* Subscribe to updates from peer
*
* @param cb callback to handle updates
* @param disconnect callback to handle disconnect, reason can be something like 'network-error'
*
* @returns unsubscribe function
*/
subscribe(
cb: (docId: string, data: Uint8Array) => void,
disconnect: (reason: string) => void
): Promise<() => void>;
}

View File

@ -1,13 +0,0 @@
import type { WorkspaceMetadata } from './metadata';
import type { Workspace } from './workspace';
export interface WorkspaceFactory {
name: string;
openWorkspace(metadata: WorkspaceMetadata): Workspace;
/**
* get blob without open workspace
*/
getWorkspaceBlob(id: string, blobKey: string): Promise<Blob | null>;
}

View File

@ -1,6 +0,0 @@
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { Schema } from '@blocksuite/store';
export const globalBlockSuiteSchema = new Schema();
globalBlockSuiteSchema.register(AffineSchemas).register(__unstableSchemas);

View File

@ -1,7 +0,0 @@
export * from './engine';
export * from './factory';
export * from './global-schema';
export * from './list';
export * from './manager';
export * from './metadata';
export * from './workspace';

View File

@ -1,21 +0,0 @@
import { type WorkspaceMetadata } from '../metadata';
const CACHE_STORAGE_KEY = 'jotai-workspaces';
export function readWorkspaceListCache() {
const metadata = localStorage.getItem(CACHE_STORAGE_KEY);
if (metadata) {
try {
const items = JSON.parse(metadata) as WorkspaceMetadata[];
return [...items];
} catch (e) {
console.error('cannot parse worksapce', e);
}
return [];
}
return [];
}
export function writeWorkspaceListCache(metadata: WorkspaceMetadata[]) {
localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(metadata));
}

View File

@ -1,300 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { Slot } from '@blocksuite/global/utils';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { differenceWith } from 'lodash-es';
import type { BlobStorage } from '../engine';
import type { WorkspaceMetadata } from '../metadata';
import { readWorkspaceListCache, writeWorkspaceListCache } from './cache';
import { type WorkspaceInfo, WorkspaceInformation } from './information';
export * from './information';
const logger = new DebugLogger('affine:workspace:list');
export interface WorkspaceListProvider {
name: WorkspaceFlavour;
/**
* get workspaces list
*/
getList(): Promise<WorkspaceMetadata[]>;
/**
* delete workspace by id
*/
delete(workspaceId: string): Promise<void>;
/**
* create workspace
* @param initial callback to put initial data to workspace
*/
create(
initial: (
workspace: BlockSuiteWorkspace,
blobStorage: BlobStorage
) => Promise<void>
): Promise<string>;
/**
* Start subscribe workspaces list
*
* @returns unsubscribe function
*/
subscribe(
callback: (changed: {
added?: WorkspaceMetadata[];
deleted?: WorkspaceMetadata[];
}) => void
): () => void;
/**
* get workspace avatar and name by id
*
* @param id workspace id
*/
getInformation(id: string): Promise<WorkspaceInfo | undefined>;
}
export interface WorkspaceListStatus {
/**
* is workspace list doing first loading.
* if false, UI can display workspace not found page.
*/
loading: boolean;
workspaceList: WorkspaceMetadata[];
}
/**
* # WorkspaceList
*
* manage multiple workspace metadata list providers.
* provide a __cache-first__ and __offline useable__ workspace list.
*/
export class WorkspaceList {
private readonly abortController = new AbortController();
private readonly workspaceInformationList = new Map<
string,
WorkspaceInformation
>();
onStatusChanged = new Slot<WorkspaceListStatus>();
private _status: Readonly<WorkspaceListStatus> = {
loading: true,
workspaceList: [],
};
get status() {
return this._status;
}
set status(status) {
this._status = status;
// update cache
writeWorkspaceListCache(status.workspaceList);
this.onStatusChanged.emit(this._status);
}
get workspaceList() {
return this.status.workspaceList;
}
constructor(private readonly providers: WorkspaceListProvider[]) {
// initialize workspace list from cache
const cache = readWorkspaceListCache();
const workspaceList = cache;
this.status = {
...this.status,
workspaceList,
};
// start first load
this.startLoad();
}
/**
* create workspace
* @param flavour workspace flavour
* @param initial callback to put initial data to workspace
* @returns workspace id
*/
async create(
flavour: WorkspaceFlavour,
initial: (
workspace: BlockSuiteWorkspace,
blobStorage: BlobStorage
) => Promise<void>
) {
const provider = this.providers.find(x => x.name === flavour);
if (!provider) {
throw new Error(`Unknown workspace flavour: ${flavour}`);
}
const id = await provider.create(initial);
const metadata = {
id,
flavour,
};
// update workspace list
this.status = this.addWorkspace(this.status, metadata);
return id;
}
/**
* delete workspace
* @param workspaceMetadata
*/
async delete(workspaceMetadata: WorkspaceMetadata) {
logger.info(
`delete workspace [${workspaceMetadata.flavour}] ${workspaceMetadata.id}`
);
const provider = this.providers.find(
x => x.name === workspaceMetadata.flavour
);
if (!provider) {
throw new Error(
`Unknown workspace flavour: ${workspaceMetadata.flavour}`
);
}
await provider.delete(workspaceMetadata.id);
// delete workspace from list
this.status = this.deleteWorkspace(this.status, workspaceMetadata);
}
/**
* add workspace to list
*/
private addWorkspace(
status: WorkspaceListStatus,
workspaceMetadata: WorkspaceMetadata
) {
if (status.workspaceList.some(x => x.id === workspaceMetadata.id)) {
return status;
}
return {
...status,
workspaceList: status.workspaceList.concat(workspaceMetadata),
};
}
/**
* delete workspace from list
*/
private deleteWorkspace(
status: WorkspaceListStatus,
workspaceMetadata: WorkspaceMetadata
) {
if (!status.workspaceList.some(x => x.id === workspaceMetadata.id)) {
return status;
}
return {
...status,
workspaceList: status.workspaceList.filter(
x => x.id !== workspaceMetadata.id
),
};
}
/**
* callback for subscribe workspaces list
*/
private handleWorkspaceChange(changed: {
added?: WorkspaceMetadata[];
deleted?: WorkspaceMetadata[];
}) {
let status = this.status;
for (const added of changed.added ?? []) {
status = this.addWorkspace(status, added);
}
for (const deleted of changed.deleted ?? []) {
status = this.deleteWorkspace(status, deleted);
}
this.status = status;
}
/**
* start first load workspace list
*/
private startLoad() {
for (const provider of this.providers) {
// subscribe workspace list change
const unsubscribe = provider.subscribe(changed => {
this.handleWorkspaceChange(changed);
});
// unsubscribe when abort
if (this.abortController.signal.aborted) {
unsubscribe();
return;
}
this.abortController.signal.addEventListener('abort', () => {
unsubscribe();
});
}
this.revalidate()
.catch(error => {
logger.error('load workspace list error: ' + error);
})
.finally(() => {
this.status = {
...this.status,
loading: false,
};
});
}
async revalidate() {
await Promise.allSettled(
this.providers.map(async provider => {
try {
const list = await provider.getList();
const oldList = this.workspaceList.filter(
w => w.flavour === provider.name
);
this.handleWorkspaceChange({
added: differenceWith(list, oldList, (a, b) => a.id === b.id),
deleted: differenceWith(oldList, list, (a, b) => a.id === b.id),
});
} catch (error) {
logger.error('load workspace list error: ' + error);
}
})
);
}
/**
* get workspace information, if not exists, create it.
*/
getInformation(meta: WorkspaceMetadata) {
const exists = this.workspaceInformationList.get(meta.id);
if (exists) {
return exists;
}
return this.createInformation(meta);
}
private createInformation(workspaceMetadata: WorkspaceMetadata) {
const provider = this.providers.find(
x => x.name === workspaceMetadata.flavour
);
if (!provider) {
throw new Error(
`Unknown workspace flavour: ${workspaceMetadata.flavour}`
);
}
const information = new WorkspaceInformation(workspaceMetadata, provider);
information.fetch();
this.workspaceInformationList.set(workspaceMetadata.id, information);
return information;
}
dispose() {
this.abortController.abort();
}
}

View File

@ -1,97 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
import type { WorkspaceMetadata } from '../metadata';
import type { Workspace } from '../workspace';
import type { WorkspaceListProvider } from './index';
const logger = new DebugLogger('affine:workspace:list:information');
const WORKSPACE_INFORMATION_CACHE_KEY = 'workspace-information:';
export interface WorkspaceInfo {
avatar?: string;
name?: string;
}
/**
* # WorkspaceInformation
*
* This class take care of workspace avatar and name
*
* The class will try to get from 3 places:
* - local cache
* - fetch from `WorkspaceListProvider`, which will fetch from database or server
* - sync with active workspace
*/
export class WorkspaceInformation {
private _info: WorkspaceInfo = {};
public set info(info: WorkspaceInfo) {
if (info.avatar !== this._info.avatar || info.name !== this._info.name) {
localStorage.setItem(
WORKSPACE_INFORMATION_CACHE_KEY + this.meta.id,
JSON.stringify(info)
);
this._info = info;
this.onUpdated.emit(info);
}
}
public get info() {
return this._info;
}
public onUpdated = new Slot<WorkspaceInfo>();
constructor(
public meta: WorkspaceMetadata,
public provider: WorkspaceListProvider
) {
const cached = this.getCachedInformation();
// init with cached information
this.info = { ...cached };
}
/**
* sync information with workspace
*/
syncWithWorkspace(workspace: Workspace) {
this.info = {
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
};
workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
this.info = {
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
};
});
}
getCachedInformation() {
const cache = localStorage.getItem(
WORKSPACE_INFORMATION_CACHE_KEY + this.meta.id
);
if (cache) {
return JSON.parse(cache) as WorkspaceInfo;
}
return null;
}
/**
* fetch information from provider
*/
fetch() {
this.provider
.getInformation(this.meta.id)
.then(info => {
if (info) {
this.info = info;
}
})
.catch(err => {
logger.warn('get workspace information error: ' + err);
});
}
}

View File

@ -1,169 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { assertEquals } from '@blocksuite/global/utils';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { fixWorkspaceVersion } from '@toeverything/infra/blocksuite';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { WorkspaceFactory } from './factory';
import type { BlobStorage } from './index';
import type { WorkspaceList } from './list';
import type { WorkspaceMetadata } from './metadata';
import { WorkspacePool } from './pool';
import type { Workspace } from './workspace';
const logger = new DebugLogger('affine:workspace-manager');
/**
* # `WorkspaceManager`
*
* This class acts as the central hub for managing various aspects of workspaces.
* It is structured as follows:
*
* ```
*
* Workspace
* Manager
*
*
*
* List Pool Factories
*
* ```
*
* Manage every about workspace
*
* # List
*
* The `WorkspaceList` component stores metadata for all workspaces, also include workspace avatar and custom name.
*
* # Factories
*
* This class contains a collection of `WorkspaceFactory`,
* We utilize `metadata.flavour` to identify the appropriate factory for opening a workspace.
* Once opened, workspaces are stored in the `WorkspacePool`.
*
* # Pool
*
* The `WorkspacePool` use reference counting to manage active workspaces.
* Calling `use()` to create a reference to the workspace. Calling `release()` to release the reference.
* When the reference count is 0, it will close the workspace.
*
*/
export class WorkspaceManager {
pool: WorkspacePool = new WorkspacePool();
constructor(
public list: WorkspaceList,
public factories: WorkspaceFactory[]
) {}
/**
* get workspace reference by metadata.
*
* You basically don't need to call this function directly, use the react hook `useWorkspace(metadata)` instead.
*
* @returns the workspace reference and a release function, don't forget to call release function when you don't
* need the workspace anymore.
*/
use(metadata: WorkspaceMetadata): {
workspace: Workspace;
release: () => void;
} {
const exist = this.pool.get(metadata.id);
if (exist) {
return exist;
}
const workspace = this.open(metadata);
const ref = this.pool.put(workspace);
return ref;
}
createWorkspace(
flavour: WorkspaceFlavour,
initial: (
workspace: BlockSuiteWorkspace,
blobStorage: BlobStorage
) => Promise<void>
): Promise<string> {
logger.info(`create workspace [${flavour}]`);
return this.list.create(flavour, initial);
}
/**
* delete workspace by metadata, same as `WorkspaceList.deleteWorkspace`
*/
async deleteWorkspace(metadata: WorkspaceMetadata) {
await this.list.delete(metadata);
}
/**
* helper function to transform local workspace to cloud workspace
*/
async transformLocalToCloud(local: Workspace): Promise<WorkspaceMetadata> {
assertEquals(local.flavour, WorkspaceFlavour.LOCAL);
await local.engine.sync.waitForSynced();
const newId = await this.list.create(
WorkspaceFlavour.AFFINE_CLOUD,
async (ws, bs) => {
applyUpdate(ws.doc, encodeStateAsUpdate(local.blockSuiteWorkspace.doc));
for (const subdoc of local.blockSuiteWorkspace.doc.getSubdocs()) {
for (const newSubdoc of ws.doc.getSubdocs()) {
if (newSubdoc.guid === subdoc.guid) {
applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc));
}
}
}
const blobList = await local.engine.blob.list();
for (const blobKey of blobList) {
const blob = await local.engine.blob.get(blobKey);
if (blob) {
await bs.set(blobKey, blob);
}
}
}
);
await this.list.delete(local.meta);
return {
id: newId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
};
}
/**
* helper function to get blob without open workspace, its be used for download workspace avatars.
*/
getWorkspaceBlob(metadata: WorkspaceMetadata, blobKey: string) {
const factory = this.factories.find(x => x.name === metadata.flavour);
if (!factory) {
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
}
return factory.getWorkspaceBlob(metadata.id, blobKey);
}
private open(metadata: WorkspaceMetadata) {
logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `);
const factory = this.factories.find(x => x.name === metadata.flavour);
if (!factory) {
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
}
const workspace = factory.openWorkspace(metadata);
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
this.list.getInformation(metadata).syncWithWorkspace(workspace);
// apply compatibility fix
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
return workspace;
}
}

View File

@ -1,3 +0,0 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
export type WorkspaceMetadata = { id: string; flavour: WorkspaceFlavour };

View File

@ -1,86 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Unreachable } from '@affine/env/constant';
import type { Workspace } from './workspace';
const logger = new DebugLogger('affine:workspace-manager:pool');
/**
* Collection of opened workspaces. use reference counting to manage active workspaces.
*/
export class WorkspacePool {
openedWorkspaces = new Map<string, { workspace: Workspace; rc: number }>();
timeoutToGc: NodeJS.Timeout | null = null;
get(workspaceId: string): {
workspace: Workspace;
release: () => void;
} | null {
const exist = this.openedWorkspaces.get(workspaceId);
if (exist) {
exist.rc++;
let released = false;
return {
workspace: exist.workspace,
release: () => {
// avoid double release
if (released) {
return;
}
released = true;
exist.rc--;
this.requestGc();
},
};
}
return null;
}
put(workspace: Workspace) {
const ref = { workspace, rc: 0 };
this.openedWorkspaces.set(workspace.meta.id, ref);
const r = this.get(workspace.meta.id);
if (!r) {
throw new Unreachable();
}
return r;
}
private requestGc() {
if (this.timeoutToGc) {
clearInterval(this.timeoutToGc);
}
// do gc every 1s
this.timeoutToGc = setInterval(() => {
this.gc();
}, 1000);
}
private gc() {
for (const [id, { workspace, rc }] of new Map(
this.openedWorkspaces /* clone the map, because the origin will be modified during iteration */
)) {
if (rc === 0 && workspace.canGracefulStop()) {
// we can safely close the workspace
logger.info(`close workspace [${workspace.flavour}] ${workspace.id}`);
workspace.forceStop();
this.openedWorkspaces.delete(id);
}
}
for (const [_, { rc }] of this.openedWorkspaces) {
if (rc === 0) {
return;
}
}
// if all workspaces has referrer, stop gc
if (this.timeoutToGc) {
clearInterval(this.timeoutToGc);
}
}
}

View File

@ -1,148 +0,0 @@
import { Unreachable } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Slot } from '@blocksuite/global/utils';
import {
checkWorkspaceCompatibility,
MigrationPoint,
} from '@toeverything/infra/blocksuite';
import {
forceUpgradePages,
upgradeV1ToV2,
} from '@toeverything/infra/blocksuite';
import { migrateGuidCompatibility } from '@toeverything/infra/blocksuite';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import type { WorkspaceManager } from '..';
import type { Workspace } from '../workspace';
export interface WorkspaceUpgradeStatus {
needUpgrade: boolean;
upgrading: boolean;
}
export class WorkspaceUpgradeController {
_status: Readonly<WorkspaceUpgradeStatus> = {
needUpgrade: false,
upgrading: false,
};
readonly onStatusChange = new Slot<WorkspaceUpgradeStatus>();
get status() {
return this._status;
}
set status(value) {
if (
value.needUpgrade !== this._status.needUpgrade ||
value.upgrading !== this._status.upgrading
) {
this._status = value;
this.onStatusChange.emit(value);
}
}
constructor(private readonly workspace: Workspace) {
workspace.blockSuiteWorkspace.doc.on('update', () => {
this.checkIfNeedUpgrade();
});
}
checkIfNeedUpgrade() {
const needUpgrade = !!checkWorkspaceCompatibility(
this.workspace.blockSuiteWorkspace,
this.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD
);
this.status = {
...this.status,
needUpgrade,
};
return needUpgrade;
}
async upgrade(workspaceManager: WorkspaceManager): Promise<string | null> {
if (this.status.upgrading) {
return null;
}
this.status = { ...this.status, upgrading: true };
try {
await this.workspace.engine.sync.waitForSynced();
const step = checkWorkspaceCompatibility(
this.workspace.blockSuiteWorkspace,
this.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD
);
if (!step) {
return null;
}
// Clone a new doc to prevent change events.
const clonedDoc = new YDoc({
guid: this.workspace.blockSuiteWorkspace.doc.guid,
});
applyDoc(clonedDoc, this.workspace.blockSuiteWorkspace.doc);
if (step === MigrationPoint.SubDoc) {
const newWorkspace = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL,
async (workspace, blobStorage) => {
await upgradeV1ToV2(clonedDoc, workspace.doc);
migrateGuidCompatibility(clonedDoc);
await forceUpgradePages(
workspace.doc,
this.workspace.blockSuiteWorkspace.schema
);
const blobList =
await this.workspace.blockSuiteWorkspace.blob.list();
for (const blobKey of blobList) {
const blob =
await this.workspace.blockSuiteWorkspace.blob.get(blobKey);
if (blob) {
await blobStorage.set(blobKey, blob);
}
}
}
);
await workspaceManager.deleteWorkspace(this.workspace.meta);
return newWorkspace;
} else if (step === MigrationPoint.GuidFix) {
migrateGuidCompatibility(clonedDoc);
await forceUpgradePages(
clonedDoc,
this.workspace.blockSuiteWorkspace.schema
);
applyDoc(this.workspace.blockSuiteWorkspace.doc, clonedDoc);
await this.workspace.engine.sync.waitForSynced();
return null;
} else if (step === MigrationPoint.BlockVersion) {
await forceUpgradePages(
clonedDoc,
this.workspace.blockSuiteWorkspace.schema
);
applyDoc(this.workspace.blockSuiteWorkspace.doc, clonedDoc);
await this.workspace.engine.sync.waitForSynced();
return null;
} else {
throw new Unreachable();
}
} finally {
this.status = { ...this.status, upgrading: false };
}
}
}
function applyDoc(target: YDoc, result: YDoc) {
applyUpdate(target, encodeStateAsUpdate(result));
for (const targetSubDoc of target.subdocs.values()) {
const resultSubDocs = Array.from(result.subdocs.values());
const resultSubDoc = resultSubDocs.find(
item => item.guid === targetSubDoc.guid
);
if (resultSubDoc) {
applyDoc(targetSubDoc, resultSubDoc);
}
}
}

View File

@ -1,45 +0,0 @@
import { describe, expect, test, vi } from 'vitest';
import { AsyncQueue } from '../async-queue';
describe('async-queue', () => {
test('push & pop', async () => {
const queue = new AsyncQueue();
queue.push(1, 2, 3);
expect(queue.length).toBe(3);
expect(await queue.next()).toBe(1);
expect(await queue.next()).toBe(2);
expect(await queue.next()).toBe(3);
expect(queue.length).toBe(0);
});
test('await', async () => {
const queue = new AsyncQueue<number>();
queue.push(1, 2);
expect(await queue.next()).toBe(1);
expect(await queue.next()).toBe(2);
let v = -1;
// setup 2 pop tasks
queue.next().then(next => {
v = next;
});
queue.next().then(next => {
v = next;
});
// Wait for 100ms
await new Promise(resolve => setTimeout(resolve, 100));
// v should not be changed
expect(v).toBe(-1);
// push 3, should trigger the first pop task
queue.push(3);
await vi.waitFor(() => v === 3);
// push 4, should trigger the second pop task
queue.push(4);
await vi.waitFor(() => v === 4);
});
});

View File

@ -1,13 +0,0 @@
import { describe, expect, test } from 'vitest';
import { throwIfAborted } from '../throw-if-aborted';
describe('throw-if-aborted', () => {
test('basic', async () => {
const abortController = new AbortController();
const abortSignal = abortController.signal;
expect(throwIfAborted(abortSignal)).toBe(true);
abortController.abort('TEST_ABORT');
expect(() => throwIfAborted(abortSignal)).toThrowError('TEST_ABORT');
});
});

View File

@ -1,101 +0,0 @@
export class AsyncQueue<T> {
private _queue: T[];
private _resolveUpdate: (() => void) | null = null;
private _waitForUpdate: Promise<void> | null = null;
constructor(init: T[] = []) {
this._queue = init;
}
get length() {
return this._queue.length;
}
async next(
abort?: AbortSignal,
dequeue: (arr: T[]) => T | undefined = a => a.shift()
): Promise<T> {
const update = dequeue(this._queue);
if (update) {
return update;
} else {
if (!this._waitForUpdate) {
this._waitForUpdate = new Promise(resolve => {
this._resolveUpdate = resolve;
});
}
await Promise.race([
this._waitForUpdate,
new Promise((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]);
return this.next(abort, dequeue);
}
}
push(...updates: T[]) {
this._queue.push(...updates);
if (this._resolveUpdate) {
const resolve = this._resolveUpdate;
this._resolveUpdate = null;
this._waitForUpdate = null;
resolve();
}
}
remove(predicate: (update: T) => boolean) {
const index = this._queue.findIndex(predicate);
if (index !== -1) {
this._queue.splice(index, 1);
}
}
find(predicate: (update: T) => boolean) {
return this._queue.find(predicate);
}
clear() {
this._queue = [];
}
}
export class PriorityAsyncQueue<
T extends { id: string },
> extends AsyncQueue<T> {
constructor(
init: T[] = [],
public readonly priorityTarget: SharedPriorityTarget = new SharedPriorityTarget()
) {
super(init);
}
override next(abort?: AbortSignal | undefined): Promise<T> {
return super.next(abort, arr => {
if (this.priorityTarget.priorityRule !== null) {
const index = arr.findIndex(
update => this.priorityTarget.priorityRule?.(update.id)
);
if (index !== -1) {
return arr.splice(index, 1)[0];
}
}
return arr.shift();
});
}
}
/**
* Shared priority target can be shared by multiple queues.
*/
export class SharedPriorityTarget {
public priorityRule: ((id: string) => boolean) | null = null;
}

View File

@ -1,17 +0,0 @@
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
export function mergeUpdates(updates: Uint8Array[]) {
if (updates.length === 0) {
return new Uint8Array();
}
if (updates.length === 1) {
return updates[0];
}
const doc = new Doc();
doc.transact(() => {
updates.forEach(update => {
applyUpdate(doc, update);
});
});
return encodeStateAsUpdate(doc);
}

View File

@ -1,9 +0,0 @@
// because AbortSignal.throwIfAborted is not available in abortcontroller-polyfill
export function throwIfAborted(abort?: AbortSignal) {
if (abort?.aborted) {
throw new Error(abort.reason);
}
return true;
}
export const MANUALLY_STOP = 'manually-stop';

View File

@ -1,137 +0,0 @@
import { DebugLogger } from '@affine/debug';
import { Slot } from '@blocksuite/global/utils';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { WorkspaceEngine, WorkspaceEngineStatus } from './engine';
import type { WorkspaceMetadata } from './metadata';
import {
WorkspaceUpgradeController,
type WorkspaceUpgradeStatus,
} from './upgrade';
const logger = new DebugLogger('affine:workspace');
export type WorkspaceStatus = {
mode: 'ready' | 'closed';
engine: WorkspaceEngineStatus;
upgrade: WorkspaceUpgradeStatus;
};
/**
* # Workspace
*
* ```
*
* Workspace
*
*
*
*
*
*
* Upgrade blocksuite Engine
*
*
*
*
*
* sync blob awareness
*
* ```
*
* This class contains all the components needed to run a workspace.
*/
export class Workspace {
get id() {
return this.meta.id;
}
get flavour() {
return this.meta.flavour;
}
private _status: WorkspaceStatus;
upgrade: WorkspaceUpgradeController;
/**
* event on workspace stop, workspace is one-time use, so it will be triggered only once
*/
onStop = new Slot();
onStatusChange = new Slot<WorkspaceStatus>();
get status() {
return this._status;
}
set status(status: WorkspaceStatus) {
this._status = status;
this.onStatusChange.emit(status);
}
constructor(
public meta: WorkspaceMetadata,
public engine: WorkspaceEngine,
public blockSuiteWorkspace: BlockSuiteWorkspace
) {
this.upgrade = new WorkspaceUpgradeController(this);
this._status = {
mode: 'closed',
engine: engine.status,
upgrade: this.upgrade.status,
};
this.engine.onStatusChange.on(status => {
this.status = {
...this.status,
engine: status,
};
});
this.upgrade.onStatusChange.on(status => {
this.status = {
...this.status,
upgrade: status,
};
});
this.start();
}
/**
* workspace start when create and workspace is one-time use
*/
private start() {
if (this.status.mode === 'ready') {
return;
}
logger.info('start workspace', this.id);
this.engine.start();
this.status = {
...this.status,
mode: 'ready',
engine: this.engine.status,
};
}
canGracefulStop() {
return this.engine.canGracefulStop() && !this.status.upgrade.upgrading;
}
forceStop() {
if (this.status.mode === 'closed') {
return;
}
logger.info('stop workspace', this.id);
this.engine.forceStop();
this.status = {
...this.status,
mode: 'closed',
engine: this.engine.status,
};
this.onStop.emit();
}
// same as `WorkspaceEngine.sync.setPriorityRule`
setPriorityRule(target: ((id: string) => boolean) | null) {
this.engine.sync.setPriorityRule(target);
}
}

View File

@ -1,16 +0,0 @@
{
"extends": "../../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"noEmit": false,
"outDir": "lib"
},
"references": [
{ "path": "../../../tests/fixtures" },
{ "path": "../../common/env" },
{ "path": "../../common/debug" },
{ "path": "../../common/infra" },
{ "path": "../../frontend/graphql" },
{ "path": "../../frontend/electron-api" }
]
}

View File

@ -1,4 +0,0 @@
{
"extends": ["../../../typedoc.base.json"],
"entryPoints": ["src/index.ts"]
}

View File

@ -24,7 +24,6 @@
"@affine/electron-api": "workspace:*", "@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@affine/workspace": "workspace:*",
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "^8.0.0",

View File

@ -1,8 +1,8 @@
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons'; import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Avatar } from '../../../ui/avatar'; import { Avatar } from '../../../ui/avatar';

View File

@ -1,4 +1,3 @@
import type { WorkspaceMetadata } from '@affine/workspace';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { import {
DndContext, DndContext,
@ -11,6 +10,7 @@ import {
restrictToVerticalAxis, restrictToVerticalAxis,
} from '@dnd-kit/modifiers'; } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import type { WorkspaceMetadata } from '@toeverything/infra';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';

View File

@ -14,7 +14,9 @@
{ {
"path": "../../frontend/electron-api" "path": "../../frontend/electron-api"
}, },
{ "path": "../../common/workspace" }, {
"path": "../../frontend/graphql"
},
{ {
"path": "../../common/debug" "path": "../../common/debug"
}, },

View File

@ -24,7 +24,6 @@
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*", "@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@affine/workspace-impl": "workspace:*", "@affine/workspace-impl": "workspace:*",
"@blocksuite/block-std": "0.12.0-nightly-202401290223-b6302df", "@blocksuite/block-std": "0.12.0-nightly-202401290223-b6302df",
"@blocksuite/blocks": "0.12.0-nightly-202401290223-b6302df", "@blocksuite/blocks": "0.12.0-nightly-202401290223-b6302df",

View File

@ -8,14 +8,17 @@ import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n'; import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { ServiceCollection } from '@toeverything/infra/di';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense } from 'react'; import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { GlobalScopeProvider } from './modules/infra-web/global-scope';
import { CloudSessionProvider } from './providers/session-provider'; import { CloudSessionProvider } from './providers/session-provider';
import { router } from './router'; import { router } from './router';
import { performanceLogger, performanceRenderLogger } from './shared'; import { performanceLogger, performanceRenderLogger } from './shared';
import createEmotionCache from './utils/create-emotion-cache'; import createEmotionCache from './utils/create-emotion-cache';
import { configureWebServices } from './web';
const performanceI18nLogger = performanceLogger.namespace('i18n'); const performanceI18nLogger = performanceLogger.namespace('i18n');
const cache = createEmotionCache(); const cache = createEmotionCache();
@ -52,6 +55,10 @@ async function loadLanguage() {
let languageLoadingPromise: Promise<void> | null = null; let languageLoadingPromise: Promise<void> | null = null;
const services = new ServiceCollection();
configureWebServices(services);
const serviceProvider = services.provider();
export const App = memo(function App() { export const App = memo(function App() {
performanceRenderLogger.info('App'); performanceRenderLogger.info('App');
@ -60,20 +67,26 @@ export const App = memo(function App() {
} }
return ( return (
<CacheProvider value={cache}> <Suspense>
<AffineContext store={getCurrentStore()}> <GlobalScopeProvider provider={serviceProvider}>
<CloudSessionProvider> <CacheProvider value={cache}>
<DebugProvider> <AffineContext store={getCurrentStore()}>
<GlobalLoading /> <CloudSessionProvider>
{runtimeConfig.enableNotificationCenter && <NotificationCenter />} <DebugProvider>
<RouterProvider <GlobalLoading />
fallbackElement={<WorkspaceFallback key="RouterFallback" />} {runtimeConfig.enableNotificationCenter && (
router={router} <NotificationCenter />
future={future} )}
/> <RouterProvider
</DebugProvider> fallbackElement={<WorkspaceFallback key="RouterFallback" />}
</CloudSessionProvider> router={router}
</AffineContext> future={future}
</CacheProvider> />
</DebugProvider>
</CloudSessionProvider>
</AffineContext>
</CacheProvider>
</GlobalScopeProvider>
</Suspense>
); );
}); });

View File

@ -1,231 +0,0 @@
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/core/modules/workspace';
import type { Collection, DeprecatedCollection } from '@affine/env/filter';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { type DBSchema, openDB } from 'idb';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { Observable, of } from 'rxjs';
import type {
CollectionsCRUD,
CollectionsCRUDAtom,
} from '../components/page-list';
import { getUserSetting } from '../utils/user-setting';
import { getWorkspaceSetting } from '../utils/workspace-setting';
import { sessionAtom } from './cloud-user';
/**
* @deprecated
*/
export interface PageCollectionDBV1 extends DBSchema {
view: {
key: DeprecatedCollection['id'];
value: DeprecatedCollection;
};
}
/**
* @deprecated
*/
export interface StorageCRUD<Value> {
get: (key: string) => Promise<Value | null>;
set: (key: string, value: Value) => Promise<string>;
delete: (key: string) => Promise<void>;
list: () => Promise<string[]>;
}
/**
* @deprecated
*/
const collectionDBAtom = atom(
openDB<PageCollectionDBV1>('page-view', 1, {
upgrade(database) {
database.createObjectStore('view', {
keyPath: 'id',
});
},
})
);
/**
* @deprecated
*/
const localCollectionCRUDAtom = atom(get => ({
get: async (key: string) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view').objectStore('view');
return (await t.get(key)) ?? null;
},
set: async (key: string, value: DeprecatedCollection) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.put(value);
return key;
},
delete: async (key: string) => {
const db = await get(collectionDBAtom);
const t = db.transaction('view', 'readwrite').objectStore('view');
await t.delete(key);
},
list: async () => {
const db = await get(collectionDBAtom);
const t = db.transaction('view').objectStore('view');
return t.getAllKeys();
},
}));
/**
* @deprecated
*/
const getCollections = async (
storage: StorageCRUD<DeprecatedCollection>
): Promise<DeprecatedCollection[]> => {
return storage
.list()
.then(async keys => {
return await Promise.all(keys.map(key => storage.get(key))).then(v =>
v.filter((v): v is DeprecatedCollection => v !== null)
);
})
.catch(error => {
console.error('Failed to load collections', error);
return [];
});
};
type BaseCollectionsDataType = {
loading: boolean;
collections: Collection[];
};
export const pageCollectionBaseAtom =
atomWithObservable<BaseCollectionsDataType>(
get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
return of({ loading: true, collections: [] });
}
const session = get(sessionAtom);
const userId = session?.data?.user.id ?? null;
const migrateCollectionsFromIdbData = async (
workspace: Workspace
): Promise<Collection[]> => {
const localCRUD = get(localCollectionCRUDAtom);
const collections = await getCollections(localCRUD);
const result = collections.filter(v => v.workspaceId === workspace.id);
Promise.all(
result.map(collection => {
return localCRUD.delete(collection.id);
})
).catch(error => {
console.error('Failed to delete collections from indexeddb', error);
});
return result.map(v => {
return {
id: v.id,
name: v.name,
filterList: v.filterList,
allowList: v.allowList ?? [],
};
});
};
const migrateCollectionsFromUserData = async (
workspace: Workspace
): Promise<Collection[]> => {
if (userId == null) {
return [];
}
const userSetting = getUserSetting(workspace, userId);
await userSetting.loaded;
const view = userSetting.view;
if (view) {
const collections: Omit<DeprecatedCollection, 'workspaceId'>[] = [
...view.values(),
];
//delete collections
view.clear();
return collections.map(v => {
return {
id: v.id,
name: v.name,
filterList: v.filterList,
allowList: v.allowList ?? [],
};
});
}
return [];
};
return new Observable<BaseCollectionsDataType>(subscriber => {
const group = new DisposableGroup();
const workspaceSetting = getWorkspaceSetting(
currentWorkspace.blockSuiteWorkspace
);
migrateCollectionsFromIdbData(currentWorkspace.blockSuiteWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
migrateCollectionsFromUserData(currentWorkspace.blockSuiteWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
if (group.disposed) {
return;
}
const fn = () => {
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.setting.observeDeep(fn);
group.add(() => {
workspaceSetting.setting.unobserveDeep(fn);
});
return () => {
group.dispose();
};
});
},
{ initialValue: { loading: true, collections: [] } }
);
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return {
addCollection: (...collections) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).addCollection(
...collections
);
},
collections: get(pageCollectionBaseAtom).collections,
updateCollection: (id, updater) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).updateCollection(
id,
updater
);
},
deleteCollection: (info, ...ids) => {
getWorkspaceSetting(workspace.blockSuiteWorkspace).deleteCollection(
info,
...ids
);
},
} satisfies CollectionsCRUD;
});

View File

@ -1,7 +1,7 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant'; import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { workspaceManager } from '@affine/workspace-impl'; import type { WorkspaceManager } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { import {
buildShowcaseWorkspace, buildShowcaseWorkspace,
@ -12,12 +12,12 @@ import { setPageModeAtom } from '../atoms';
const logger = new DebugLogger('affine:first-app-data'); const logger = new DebugLogger('affine:first-app-data');
export async function createFirstAppData() { export async function createFirstAppData(workspaceManager: WorkspaceManager) {
if (localStorage.getItem('is-first-open') !== null) { if (localStorage.getItem('is-first-open') !== null) {
return; return;
} }
localStorage.setItem('is-first-open', 'false'); localStorage.setItem('is-first-open', 'false');
const workspaceId = await workspaceManager.createWorkspace( const workspaceMetadata = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,
async workspace => { async workspace => {
workspace.meta.setName(DEFAULT_WORKSPACE_NAME); workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
@ -38,6 +38,6 @@ export async function createFirstAppData() {
logger.debug('create first workspace'); logger.debug('create first workspace');
} }
); );
console.info('create first workspace', workspaceId); console.info('create first workspace', workspaceMetadata);
return workspaceId; return workspaceMetadata;
} }

View File

@ -1,12 +1,12 @@
import { import { WorkspaceListService } from '@toeverything/infra';
currentWorkspaceAtom, import { useService } from '@toeverything/infra/di';
workspaceListAtom, import { useLiveData } from '@toeverything/infra/livedata';
} from '@affine/core/modules/workspace';
import { useAtomValue } from 'jotai/react'; import { useAtomValue } from 'jotai/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { currentPageIdAtom } from '../../../../atoms/mode'; import { currentPageIdAtom } from '../../../../atoms/mode';
import { CurrentWorkspaceService } from '../../../../modules/workspace/current-workspace';
export interface DumpInfoProps { export interface DumpInfoProps {
error: any; error: any;
@ -14,8 +14,10 @@ export interface DumpInfoProps {
export const DumpInfo = (_props: DumpInfoProps) => { export const DumpInfo = (_props: DumpInfoProps) => {
const location = useLocation(); const location = useLocation();
const workspaceList = useAtomValue(workspaceListAtom); const workspaceList = useService(WorkspaceListService);
const currentWorkspace = useAtomValue(currentWorkspaceAtom); const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const currentPageId = useAtomValue(currentPageIdAtom); const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname; const path = location.pathname;
const query = useParams(); const query = useParams();

View File

@ -1,13 +1,16 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai'; import { useLiveData } from '@toeverything/infra/livedata';
import { Suspense, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { useCurrentUser } from '../../../hooks/affine/use-current-user'; import { useCurrentUser } from '../../../hooks/affine/use-current-user';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
const SyncAwarenessInnerLoggedIn = () => { const SyncAwarenessInnerLoggedIn = () => {
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
useEffect(() => { useEffect(() => {
if (currentUser && currentWorkspace) { if (currentUser && currentWorkspace) {

View File

@ -5,18 +5,18 @@ import {
Modal, Modal,
} from '@affine/component/ui/modal'; } from '@affine/component/ui/modal';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api'; import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { _addLocalWorkspace } from '@affine/workspace-impl'; import { _addLocalWorkspace } from '@affine/workspace-impl';
import { WorkspaceManager } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { import {
buildShowcaseWorkspace, buildShowcaseWorkspace,
initEmptyPage, initEmptyPage,
} from '@toeverything/infra/blocksuite'; } from '@toeverything/infra/blocksuite';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra/di';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@ -101,7 +101,7 @@ export const CreateWorkspaceModal = ({
}: ModalProps) => { }: ModalProps) => {
const [step, setStep] = useState<CreateWorkspaceStep>(); const [step, setStep] = useState<CreateWorkspaceStep>();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
// todo: maybe refactor using xstate? // todo: maybe refactor using xstate?
useLayoutEffect(() => { useLayoutEffect(() => {
@ -148,7 +148,7 @@ export const CreateWorkspaceModal = ({
async (name: string) => { async (name: string) => {
// this will be the last step for web for now // this will be the last step for web for now
// fix me later // fix me later
const id = await workspaceManager.createWorkspace( const { id } = await workspaceManager.createWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,
async workspace => { async workspace => {
workspace.meta.setName(name); workspace.meta.setName(name);

View File

@ -8,10 +8,10 @@ import {
listHistoryQuery, listHistoryQuery,
recoverDocMutation, recoverDocMutation,
} from '@affine/graphql'; } from '@affine/graphql';
import { globalBlockSuiteSchema } from '@affine/workspace'; import { AffineCloudBlobStorage } from '@affine/workspace-impl';
import { createAffineCloudBlobStorage } from '@affine/workspace-impl';
import { assertEquals } from '@blocksuite/global/utils'; import { assertEquals } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { globalBlockSuiteSchema } from '@toeverything/infra';
import { revertUpdate } from '@toeverything/y-indexeddb'; import { revertUpdate } from '@toeverything/y-indexeddb';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import useSWRImmutable from 'swr/immutable'; import useSWRImmutable from 'swr/immutable';
@ -108,7 +108,7 @@ const workspaceMap = new Map<string, Workspace>();
const getOrCreateShellWorkspace = (workspaceId: string) => { const getOrCreateShellWorkspace = (workspaceId: string) => {
let workspace = workspaceMap.get(workspaceId); let workspace = workspaceMap.get(workspaceId);
if (!workspace) { if (!workspace) {
const blobStorage = createAffineCloudBlobStorage(workspaceId); const blobStorage = new AffineCloudBlobStorage(workspaceId);
workspace = new Workspace({ workspace = new Workspace({
id: workspaceId, id: workspaceId,
providerCreators: [], providerCreators: [],

View File

@ -7,14 +7,17 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota'; import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { timestampToLocalTime } from '@affine/core/utils';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons'; import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
import type { Page, Workspace } from '@blocksuite/store'; import {
type Page,
type Workspace as BlockSuiteWorkspace,
} from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import type { DialogContentProps } from '@radix-ui/react-dialog'; import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { import {
Fragment, Fragment,
@ -29,6 +32,7 @@ import { encodeStateAsUpdate } from 'yjs';
import { currentModeAtom } from '../../../atoms/mode'; import { currentModeAtom } from '../../../atoms/mode';
import { pageHistoryModalAtom } from '../../../atoms/page-history'; import { pageHistoryModalAtom } from '../../../atoms/page-history';
import { timestampToLocalTime } from '../../../utils';
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor'; import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style'; import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
import { import {
@ -48,7 +52,7 @@ import * as styles from './styles.css';
export interface PageHistoryModalProps { export interface PageHistoryModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
workspace: Workspace; workspace: BlockSuiteWorkspace;
pageId: string; pageId: string;
} }
@ -153,13 +157,12 @@ const HistoryEditorPreview = ({
const planPromptClosedAtom = atom(false); const planPromptClosedAtom = atom(false);
const PlanPrompt = () => { const PlanPrompt = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const workspaceQuota = useWorkspaceQuota(workspace.id);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
const isProWorkspace = useMemo(() => { const isProWorkspace = useMemo(() => {
return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free'; return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free';
}, [workspaceQuota]); }, [workspaceQuota]);
const isOwner = useIsWorkspaceOwner(workspace.meta);
const setSettingModalAtom = useSetAtom(openSettingModalAtom); const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom); const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
@ -412,7 +415,7 @@ const PageHistoryManager = ({
pageId, pageId,
onClose, onClose,
}: { }: {
workspace: Workspace; workspace: BlockSuiteWorkspace;
pageId: string; pageId: string;
onClose: () => void; onClose: () => void;
}) => { }) => {
@ -536,8 +539,7 @@ export const PageHistoryModal = ({
export const GlobalPageHistoryModal = () => { export const GlobalPageHistoryModal = () => {
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom); const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const handleOpenChange = useCallback( const handleOpenChange = useCallback(
(open: boolean) => { (open: boolean) => {
setState(prev => ({ setState(prev => ({

View File

@ -3,15 +3,15 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useUserQuota } from '@affine/core/hooks/use-quota'; import { useUserQuota } from '@affine/core/hooks/use-quota';
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota'; import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useService, Workspace } from '@toeverything/infra';
import bytes from 'bytes'; import bytes from 'bytes';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
export const CloudQuotaModal = () => { export const CloudQuotaModal = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const [open, setOpen] = useAtom(openQuotaModalAtom); const [open, setOpen] = useAtom(openQuotaModalAtom);
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id); const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta); const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);

View File

@ -1,13 +1,13 @@
import { ConfirmModal } from '@affine/component/ui/modal'; import { ConfirmModal } from '@affine/component/ui/modal';
import { openQuotaModalAtom } from '@affine/core/atoms'; import { openQuotaModalAtom } from '@affine/core/atoms';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom, useAtomValue } from 'jotai'; import { useService, Workspace } from '@toeverything/infra';
import { useAtom } from 'jotai';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
export const LocalQuotaModal = () => { export const LocalQuotaModal = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const [open, setOpen] = useAtom(openQuotaModalAtom); const [open, setOpen] = useAtom(openQuotaModalAtom);
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {

View File

@ -1,8 +1,8 @@
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components'; import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
import { Modal, type ModalProps } from '@affine/component/ui/modal'; import { Modal, type ModalProps } from '@affine/component/ui/modal';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
import { ContactWithUsIcon } from '@blocksuite/icons'; import { ContactWithUsIcon } from '@blocksuite/icons';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react'; import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';

View File

@ -8,17 +8,19 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob'; import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { useWorkspaceAvailableFeatures } from '@affine/core/hooks/use-workspace-features'; import { useWorkspaceAvailableFeatures } from '@affine/core/hooks/use-workspace-features';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info'; import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import {
waitForCurrentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import {
Workspace,
WorkspaceManager,
type WorkspaceMetadata,
} from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai/react'; import { useAtom } from 'jotai/react';
import { type ReactElement, Suspense, useCallback, useMemo } from 'react'; import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
import { authAtom } from '../../../../atoms'; import { authAtom } from '../../../../atoms';
@ -188,7 +190,9 @@ export const WorkspaceList = ({
selectedWorkspaceId: string | null; selectedWorkspaceId: string | null;
activeSubTab: WorkspaceSubTab; activeSubTab: WorkspaceSubTab;
}) => { }) => {
const workspaces = useAtomValue(workspaceListAtom); const workspaces = useLiveData(
useService(WorkspaceManager).list.workspaceList
);
return ( return (
<> <>
{workspaces.map(workspace => { {workspaces.map(workspace => {
@ -236,7 +240,7 @@ const WorkspaceListItem = ({
const information = useWorkspaceInfo(meta); const information = useWorkspaceInfo(meta);
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar); const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
const name = information?.name ?? UNTITLED_WORKSPACE_NAME; const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const isCurrent = currentWorkspace.id === meta.id; const isCurrent = currentWorkspace.id === meta.id;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(meta); const isOwner = useIsWorkspaceOwner(meta);

View File

@ -8,7 +8,7 @@ import {
} from '@affine/core/hooks/use-workspace-features'; } from '@affine/core/hooks/use-workspace-features';
import { FeatureType } from '@affine/graphql'; import { FeatureType } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
import { Suspense, useCallback, useState } from 'react'; import { Suspense, useCallback, useState } from 'react';

View File

@ -1,4 +1,4 @@
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import { ExperimentalFeatures } from './experimental-features'; import { ExperimentalFeatures } from './experimental-features';

View File

@ -8,7 +8,7 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import * as styles from './style.css'; import * as styles from './style.css';

View File

@ -2,15 +2,12 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { ConfirmModal } from '@affine/component/ui/modal'; import { ConfirmModal } from '@affine/component/ui/modal';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import {
currentWorkspaceAtom,
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai'; import { Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { openSettingModalAtom } from '../../../../../../atoms'; import { openSettingModalAtom } from '../../../../../../atoms';
@ -18,6 +15,7 @@ import {
RouteLogic, RouteLogic,
useNavigateHelper, useNavigateHelper,
} from '../../../../../../hooks/use-navigate-helper'; } from '../../../../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../../../../shared';
import type { WorkspaceSettingDetailProps } from '../types'; import type { WorkspaceSettingDetailProps } from '../types';
import { WorkspaceDeleteModal } from './delete'; import { WorkspaceDeleteModal } from './delete';
@ -35,9 +33,9 @@ export const DeleteLeaveWorkspace = ({
const [showLeave, setShowLeave] = useState(false); const [showLeave, setShowLeave] = useState(false);
const setSettingModal = useSetAtom(openSettingModalAtom); const setSettingModal = useSetAtom(openSettingModalAtom);
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
const workspaceList = useAtomValue(workspaceListAtom); const workspaceList = useLiveData(workspaceManager.list.workspaceList);
const currentWorkspace = useAtomValue(currentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const pushNotification = useSetAtom(pushNotificationAtom); const pushNotification = useSetAtom(pushNotificationAtom);
const onLeaveOrDelete = useCallback(() => { const onLeaveOrDelete = useCallback(() => {

View File

@ -2,17 +2,17 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info'; import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace'; import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai'; import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { openSettingModalAtom } from '../../../../../atoms'; import { openSettingModalAtom } from '../../../../../atoms';
import { useNavigateHelper } from '../../../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../../../shared';
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal'; import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal'; import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
import type { WorkspaceSettingDetailProps } from './types'; import type { WorkspaceSettingDetailProps } from './types';
@ -29,7 +29,7 @@ export const EnableCloudPanel = ({
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
const workspaceInfo = useWorkspaceInfo(workspaceMetadata); const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
const setSettingModal = useSetAtom(openSettingModalAtom); const setSettingModal = useSetAtom(openSettingModalAtom);

View File

@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { apis } from '@affine/electron-api'; import { apis } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace, WorkspaceMetadata } from '@affine/workspace'; import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';

View File

@ -9,9 +9,9 @@ import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import { validateAndReduceImage } from '@affine/core/utils/reduce-image'; import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { SyncPeerStep } from '@affine/workspace';
import { CameraIcon } from '@blocksuite/icons'; import { CameraIcon } from '@blocksuite/icons';
import type { Workspace } from '@toeverything/infra';
import { SyncPeerStep } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { import {
type KeyboardEvent, type KeyboardEvent,

View File

@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';

View File

@ -1,4 +1,4 @@
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
export interface WorkspaceSettingDetailProps { export interface WorkspaceSettingDetailProps {
isOwner: boolean; isOwner: boolean;

View File

@ -1,9 +1,8 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Workspace } from '@affine/workspace';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
@ -25,7 +24,7 @@ export const SharePageButton = ({
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
const handleConfirm = useAsyncCallback(async () => { const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) { if (workspace.flavour !== WorkspaceFlavour.LOCAL) {

View File

@ -3,9 +3,9 @@ import { Divider } from '@affine/component/ui/divider';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import { WebIcon } from '@blocksuite/icons'; import { WebIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import type { WorkspaceMetadata } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page'; import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';

View File

@ -1,11 +1,11 @@
import { FavoriteTag } from '@affine/core/components/page-list'; import { FavoriteTag } from '@affine/core/components/page-list';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { toast } from '@affine/core/utils'; import { toast } from '@affine/core/utils';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { useAtomValue } from 'jotai'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react'; import { useCallback } from 'react';
export interface FavoriteButtonProps { export interface FavoriteButtonProps {
@ -14,7 +14,7 @@ export interface FavoriteButtonProps {
export const useFavorite = (pageId: string) => { export const useFavorite = (pageId: string) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId); const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage); assertExists(currentPage);

View File

@ -1,3 +1,4 @@
import { toast } from '@affine/component';
import { import {
Menu, Menu,
MenuIcon, MenuIcon,
@ -11,8 +12,6 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui
import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { toast } from '@affine/core/utils';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
@ -26,6 +25,7 @@ import {
ImportIcon, ImportIcon,
PageIcon, PageIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useService, Workspace } from '@toeverything/infra';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@ -46,8 +46,7 @@ export const PageHeaderMenuButton = ({
}: PageMenuProps) => { }: PageMenuProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
// fixme(himself65): remove these hooks ASAP const workspace = useService(Workspace);
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId); const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage); assertExists(currentPage);

View File

@ -3,57 +3,46 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import type { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { atom } from 'jotai'; import { LiveData } from '@toeverything/infra';
import { atomWithObservable } from 'jotai/utils';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { expect, test } from 'vitest'; import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars'; import { createDefaultFilter, vars } from '../filter/vars';
import { import { useCollectionManager } from '../use-collection-manager';
type CollectionsCRUD,
useCollectionManager,
} from '../use-collection-manager';
const defaultMeta = { tags: { options: [] } }; const defaultMeta = { tags: { options: [] } };
const collectionsSubject = new BehaviorSubject<Collection[]>([]); const collectionsSubject = new BehaviorSubject<Collection[]>([]);
const baseAtom = atomWithObservable<Collection[]>(
() => {
return collectionsSubject;
},
{
initialValue: [],
}
);
const mockAtom = atom(get => { const mockWorkspaceCollectionService = {
return { collections: LiveData.from(collectionsSubject, []),
collections: get(baseAtom), addCollection: (...collections) => {
addCollection: (...collections) => { const prev = collectionsSubject.value;
const prev = collectionsSubject.value; collectionsSubject.next([...collections, ...prev]);
collectionsSubject.next([...collections, ...prev]); },
}, deleteCollection: (...ids) => {
deleteCollection: (...ids) => { const prev = collectionsSubject.value;
const prev = collectionsSubject.value; collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
collectionsSubject.next(prev.filter(v => !ids.includes(v.id))); },
}, updateCollection: (id, updater) => {
updateCollection: (id, updater) => { const prev = collectionsSubject.value;
const prev = collectionsSubject.value; collectionsSubject.next(
collectionsSubject.next( prev.map(v => {
prev.map(v => { if (v.id === id) {
if (v.id === id) { return updater(v);
return updater(v); }
} return v;
return v; })
}) );
); },
}, } as CollectionService;
} satisfies CollectionsCRUD;
});
test('useAllPageSetting', async () => { test('useAllPageSetting', async () => {
const settingHook = renderHook(() => useCollectionManager(mockAtom)); const settingHook = renderHook(() =>
useCollectionManager(mockWorkspaceCollectionService)
);
const prevCollection = settingHook.result.current.currentCollection; const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]); expect(settingHook.result.current.savedCollections).toEqual([]);
settingHook.result.current.updateCollection({ settingHook.result.current.updateCollection({

View File

@ -1,9 +1,8 @@
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info'; import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { import {
type ReactElement, type ReactElement,
useCallback, useCallback,
@ -12,6 +11,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { CollectionService } from '../../../modules/collection';
import { ListFloatingToolbar } from '../components/list-floating-toolbar'; import { ListFloatingToolbar } from '../components/list-floating-toolbar';
import { collectionHeaderColsDef } from '../header-col-def'; import { collectionHeaderColsDef } from '../header-col-def';
import { CollectionOperationCell } from '../operation-cell'; import { CollectionOperationCell } from '../operation-cell';
@ -69,8 +69,8 @@ export const VirtualizedCollectionList = ({
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>( const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(
[] []
); );
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const info = useDeleteCollectionInfo(); const info = useDeleteCollectionInfo();
const collectionOperations = useCollectionOperationsRenderer({ const collectionOperations = useCollectionOperationsRenderer({

View File

@ -1,13 +1,14 @@
import { Button } from '@affine/component'; import { Button } from '@affine/component';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import type { Collection, Tag } from '@affine/env/filter'; import type { Collection, Tag } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ViewLayersIcon } from '@blocksuite/icons'; import { ViewLayersIcon } from '@blocksuite/icons';
import { useService } from '@toeverything/infra/di';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { CollectionService } from '../../../modules/collection';
import { createTagFilter } from '../filter/utils'; import { createTagFilter } from '../filter/utils';
import { import {
createEmptyCollection, createEmptyCollection,
@ -24,7 +25,7 @@ import { PageListNewPageButton } from './page-list-new-page-button';
export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => { export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper(); const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => { const handleJumpToCollections = useCallback(() => {
@ -74,14 +75,16 @@ export const CollectionPageListHeader = ({
workspaceId: string; workspaceId: string;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper(); const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => { const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId); jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]); }, [jumpToCollections, workspaceId]);
const { updateCollection } = useCollectionManager(collectionsCRUDAtom); const { updateCollection } = useCollectionManager(
useService(CollectionService)
);
const { node, open } = useEditCollection(config); const { node, open } = useEditCollection(config);
const handleAddPage = useAsyncCallback(async () => { const handleAddPage = useAsyncCallback(async () => {
@ -121,7 +124,7 @@ export const TagPageListHeader = ({
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { jumpToTags, jumpToCollection } = useNavigateHelper(); const { jumpToTags, jumpToCollection } = useNavigateHelper();
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const { open, node } = useEditCollectionName({ const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.saveCollection'](), title: t['com.affine.editCollection.saveCollection'](),
showTips: true, showTips: true,

View File

@ -1,5 +1,5 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { Workspace } from '@toeverything/infra';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra/di';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
@ -16,7 +16,7 @@ export const PageListNewPageButton = ({
size?: 'small' | 'default'; size?: 'small' | 'default';
testId?: string; testId?: string;
}>) => { }>) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const { importFile, createEdgeless, createPage } = usePageHelper( const { importFile, createEdgeless, createPage } = usePageHelper(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );

View File

@ -2,12 +2,12 @@ import { toast } from '@affine/component';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta, Tag } from '@blocksuite/store'; import type { PageMeta, Tag } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
@ -27,7 +27,7 @@ import {
} from './page-list-header'; } from './page-list-header';
const usePageOperationsRenderer = () => { const usePageOperationsRenderer = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const { setTrashModal } = useTrashModalHelper( const { setTrashModal } = useTrashModalHelper(
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );
@ -89,7 +89,7 @@ export const VirtualizedPageList = ({
const listRef = useRef<ItemListHandle>(null); const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]); const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const pageOperations = usePageOperationsRenderer(); const pageOperations = usePageOperationsRenderer();
const { isPreferredEdgeless } = usePageHelper( const { isPreferredEdgeless } = usePageHelper(

View File

@ -1,7 +1,7 @@
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import type { Tag } from '@blocksuite/store'; import type { Tag } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { ListFloatingToolbar } from '../components/list-floating-toolbar'; import { ListFloatingToolbar } from '../components/list-floating-toolbar';
@ -26,7 +26,7 @@ export const VirtualizedTagList = ({
const listRef = useRef<ItemListHandle>(null); const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]); const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const filteredSelectedTagIds = useMemo(() => { const filteredSelectedTagIds = useMemo(() => {
const ids = tags.map(tag => tag.id); const ids = tags.map(tag => tag.id);

View File

@ -1,11 +1,8 @@
import type { import type { CollectionService } from '@affine/core/modules/collection';
Collection, import type { Collection, Filter, VariableMap } from '@affine/env/filter';
DeleteCollectionInfo,
Filter,
VariableMap,
} from '@affine/env/filter';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { type Atom, useAtom, useAtomValue } from 'jotai'; import { useLiveData } from '@toeverything/infra/livedata';
import { useAtom, useAtomValue } from 'jotai';
import { atomWithReset } from 'jotai/utils'; import { atomWithReset } from 'jotai/utils';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { NIL } from 'uuid'; import { NIL } from 'uuid';
@ -32,47 +29,28 @@ export const currentCollectionAtom = atomWithReset<string>(NIL);
export type Updater<T> = (value: T) => T; export type Updater<T> = (value: T) => T;
export type CollectionUpdater = Updater<Collection>; export type CollectionUpdater = Updater<Collection>;
export type CollectionsCRUD = {
addCollection: (...collections: Collection[]) => void;
collections: Collection[];
updateCollection: (id: string, updater: CollectionUpdater) => void;
deleteCollection: (info: DeleteCollectionInfo, ...ids: string[]) => void;
};
export type CollectionsCRUDAtom = Atom<
Promise<CollectionsCRUD> | CollectionsCRUD
>;
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => { export const useSavedCollections = (collectionService: CollectionService) => {
const [{ collections, addCollection, deleteCollection, updateCollection }] =
useAtom(collectionAtom);
const addPage = useCallback( const addPage = useCallback(
(collectionId: string, pageId: string) => { (collectionId: string, pageId: string) => {
updateCollection(collectionId, old => { collectionService.updateCollection(collectionId, old => {
return { return {
...old, ...old,
allowList: [pageId, ...(old.allowList ?? [])], allowList: [pageId, ...(old.allowList ?? [])],
}; };
}); });
}, },
[updateCollection] [collectionService]
); );
return { return {
collections, collectionService,
addCollection,
updateCollection,
deleteCollection,
addPage, addPage,
}; };
}; };
export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => { export const useCollectionManager = (collectionService: CollectionService) => {
const { const collections = useLiveData(collectionService.collections);
collections, const { addPage } = useSavedCollections(collectionService);
updateCollection,
addCollection,
deleteCollection,
addPage,
} = useSavedCollections(collectionsAtom);
const currentCollectionId = useAtomValue(currentCollectionAtom); const currentCollectionId = useAtomValue(currentCollectionAtom);
const [defaultCollection, updateDefaultCollection] = useAtom( const [defaultCollection, updateDefaultCollection] = useAtom(
defaultCollectionAtom defaultCollectionAtom
@ -82,10 +60,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
if (collection.id === NIL) { if (collection.id === NIL) {
updateDefaultCollection(collection); updateDefaultCollection(collection);
} else { } else {
updateCollection(collection.id, () => collection); collectionService.updateCollection(collection.id, () => collection);
} }
}, },
[updateDefaultCollection, updateCollection] [updateDefaultCollection, collectionService]
); );
const setTemporaryFilter = useCallback( const setTemporaryFilter = useCallback(
(filterList: Filter[]) => { (filterList: Filter[]) => {
@ -108,9 +86,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
isDefault: currentCollectionId === NIL, isDefault: currentCollectionId === NIL,
// actions // actions
createCollection: addCollection, createCollection: collectionService.addCollection.bind(collectionService),
updateCollection: update, updateCollection: update,
deleteCollection, deleteCollection:
collectionService.deleteCollection.bind(collectionService),
addPage, addPage,
setTemporaryFilter, setTemporaryFilter,
}; };

View File

@ -1,8 +1,9 @@
import { allPageModeSelectAtom } from '@affine/core/atoms'; import { allPageModeSelectAtom } from '@affine/core/atoms';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { CollectionService } from '@affine/core/modules/collection';
import type { BlockSuiteWorkspace } from '@affine/core/shared'; import type { BlockSuiteWorkspace } from '@affine/core/shared';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -19,8 +20,9 @@ export const useFilteredPageMetas = (
) => { ) => {
const { isPreferredEdgeless } = usePageHelper(workspace); const { isPreferredEdgeless } = usePageHelper(workspace);
const pageMode = useAtomValue(allPageModeSelectAtom); const pageMode = useAtomValue(allPageModeSelectAtom);
const { currentCollection, isDefault } = const { currentCollection, isDefault } = useCollectionManager(
useCollectionManager(collectionsCRUDAtom); useService(CollectionService)
);
const filteredPageMetas = useMemo( const filteredPageMetas = useMemo(
() => () =>

View File

@ -1,4 +1,5 @@
import { Button, Tooltip } from '@affine/component'; import { Button, Tooltip } from '@affine/component';
import type { CollectionService } from '@affine/core/modules/collection';
import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter'; import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info'; import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
@ -6,10 +7,7 @@ import { ViewLayersIcon } from '@blocksuite/icons';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { import { useCollectionManager } from '../use-collection-manager';
type CollectionsCRUDAtom,
useCollectionManager,
} from '../use-collection-manager';
import * as styles from './collection-bar.css'; import * as styles from './collection-bar.css';
import { import {
type AllPageListConfig, type AllPageListConfig,
@ -20,16 +18,16 @@ import { useActions } from './use-action';
interface CollectionBarProps { interface CollectionBarProps {
getPageInfo: GetPageInfoById; getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta; propertiesMeta: PropertiesMeta;
collectionsAtom: CollectionsCRUDAtom; collectionService: CollectionService;
backToAll: () => void; backToAll: () => void;
allPageListConfig: AllPageListConfig; allPageListConfig: AllPageListConfig;
info: DeleteCollectionInfo; info: DeleteCollectionInfo;
} }
export const CollectionBar = (props: CollectionBarProps) => { export const CollectionBar = (props: CollectionBarProps) => {
const { collectionsAtom } = props; const { collectionService } = props;
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionsAtom); const setting = useCollectionManager(collectionService);
const collection = setting.currentCollection; const collection = setting.currentCollection;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const actions = useActions({ const actions = useActions({

View File

@ -3,15 +3,11 @@ import {
useBlockSuitePageMeta, useBlockSuitePageMeta,
usePageMetaHelper, usePageMetaHelper,
} from '@affine/core/hooks/use-block-suite-page-meta'; } from '@affine/core/hooks/use-block-suite-page-meta';
import {
currentWorkspaceAtom,
waitForCurrentWorkspaceAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons'; import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom'; import { getCurrentStore } from '@toeverything/infra/atom';
import { import {
type AffineCommand, type AffineCommand,
@ -19,19 +15,17 @@ import {
type CommandCategory, type CommandCategory,
PreconditionStrategy, PreconditionStrategy,
} from '@toeverything/infra/command'; } from '@toeverything/infra/command';
import { useService } from '@toeverything/infra/di';
import { commandScore } from 'cmdk'; import { commandScore } from 'cmdk';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { import { pageSettingsAtom, recentPageIdsBaseAtom } from '../../../atoms';
openQuickSearchModalAtom,
pageSettingsAtom,
recentPageIdsBaseAtom,
} from '../../../atoms';
import { collectionsCRUDAtom } from '../../../atoms/collections';
import { currentPageIdAtom } from '../../../atoms/mode'; import { currentPageIdAtom } from '../../../atoms/mode';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CollectionService } from '../../../modules/collection';
import { WorkspaceSubPath } from '../../../shared';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import type { CMDKCommand, CommandContext } from './types'; import type { CMDKCommand, CommandContext } from './types';
@ -47,41 +41,6 @@ export function removeDoubleQuotes(str?: string): string | undefined {
export const cmdkQueryAtom = atom(''); export const cmdkQueryAtom = atom('');
export const cmdkValueAtom = atom(''); export const cmdkValueAtom = atom('');
// like currentWorkspaceAtom, but not throw error
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
return;
}
const currentPageId = get(currentPageIdAtom);
if (!currentPageId) {
return;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
return;
}
if (!page.loaded) {
await page.waitForLoaded();
}
return page;
});
export const commandContextAtom = atom<Promise<CommandContext>>(async get => {
const currentPage = await get(safeCurrentPageAtom);
const pageSettings = get(pageSettingsAtom);
return {
currentPage,
pageMode: currentPage ? pageSettings[currentPage.id]?.mode : undefined,
};
});
function filterCommandByContext( function filterCommandByContext(
command: AffineCommand, command: AffineCommand,
context: CommandContext context: CommandContext
@ -96,7 +55,7 @@ function filterCommandByContext(
return context.pageMode === 'page'; return context.pageMode === 'page';
} }
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) { if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
return !!context.currentPage; return !!context.pageMode;
} }
if (command.preconditionStrategy === PreconditionStrategy.Never) { if (command.preconditionStrategy === PreconditionStrategy.Never) {
return false; return false;
@ -107,27 +66,16 @@ function filterCommandByContext(
return true; return true;
} }
let quickSearchOpenCounter = 0; function getAllCommand(context: CommandContext) {
const openCountAtom = atom(get => {
if (get(openQuickSearchModalAtom)) {
quickSearchOpenCounter++;
}
return quickSearchOpenCounter;
});
export const filteredAffineCommands = atom(async get => {
const context = await get(commandContextAtom);
// reset when modal open
get(openCountAtom);
const commands = AffineCommandRegistry.getAll(); const commands = AffineCommandRegistry.getAll();
return commands.filter(command => { return commands.filter(command => {
return filterCommandByContext(command, context); return filterCommandByContext(command, context);
}); });
}); }
const useWorkspacePages = () => { const useWorkspacePages = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pages = useBlockSuitePageMeta(workspace.blockSuiteWorkspace);
return pages; return pages;
}; };
@ -153,6 +101,7 @@ export const pageToCommand = (
store: ReturnType<typeof getCurrentStore>, store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>, navigationHelper: ReturnType<typeof useNavigateHelper>,
t: ReturnType<typeof useAFFiNEI18N>, t: ReturnType<typeof useAFFiNEI18N>,
workspace: Workspace,
label?: { label?: {
title: string; title: string;
subTitle?: string; subTitle?: string;
@ -160,7 +109,6 @@ export const pageToCommand = (
blockId?: string blockId?: string
): CMDKCommand => { ): CMDKCommand => {
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode; const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
const currentWorkspace = store.get(currentWorkspaceAtom);
const title = page.title || t['Untitled'](); const title = page.title || t['Untitled']();
const commandLabel = label || { const commandLabel = label || {
@ -186,18 +134,14 @@ export const pageToCommand = (
originalValue: title, originalValue: title,
category: category, category: category,
run: () => { run: () => {
if (!currentWorkspace) { if (!workspace) {
console.error('current workspace not found'); console.error('current workspace not found');
return; return;
} }
if (blockId) { if (blockId) {
return navigationHelper.jumpToPageBlock( return navigationHelper.jumpToPageBlock(workspace.id, page.id, blockId);
currentWorkspace.id,
page.id,
blockId
);
} }
return navigationHelper.jumpToPage(currentWorkspace.id, page.id); return navigationHelper.jumpToPage(workspace.id, page.id);
}, },
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />, icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
timestamp: page.updatedDate, timestamp: page.updatedDate,
@ -212,7 +156,7 @@ export const usePageCommands = () => {
const recentPages = useRecentPages(); const recentPages = useRecentPages();
const pages = useWorkspacePages(); const pages = useWorkspacePages();
const store = getCurrentStore(); const store = getCurrentStore();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace); const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace); const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
const query = useAtomValue(cmdkQueryAtom); const query = useAtomValue(cmdkQueryAtom);
@ -241,7 +185,14 @@ export const usePageCommands = () => {
let results: CMDKCommand[] = []; let results: CMDKCommand[] = [];
if (query.trim() === '') { if (query.trim() === '') {
results = recentPages.map(page => { results = recentPages.map(page => {
return pageToCommand('affine:recent', page, store, navigationHelper, t); return pageToCommand(
'affine:recent',
page,
store,
navigationHelper,
t,
workspace
);
}); });
} else { } else {
// queried pages that has matched contents // queried pages that has matched contents
@ -283,6 +234,7 @@ export const usePageCommands = () => {
store, store,
navigationHelper, navigationHelper,
t, t,
workspace,
label, label,
blockId blockId
); );
@ -334,27 +286,26 @@ export const usePageCommands = () => {
} }
return results; return results;
}, [ }, [
pageHelper, searchTime,
pageMetaHelper,
navigationHelper,
pages,
query, query,
recentPages, recentPages,
store, store,
navigationHelper,
t, t,
workspace.blockSuiteWorkspace, workspace,
searchTime, pages,
pageHelper,
pageMetaHelper,
]); ]);
}; };
export const collectionToCommand = ( export const collectionToCommand = (
collection: Collection, collection: Collection,
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>, navigationHelper: ReturnType<typeof useNavigateHelper>,
selectCollection: (id: string) => void, selectCollection: (id: string) => void,
t: ReturnType<typeof useAFFiNEI18N> t: ReturnType<typeof useAFFiNEI18N>,
workspace: Workspace
): CMDKCommand => { ): CMDKCommand => {
const currentWorkspace = store.get(currentWorkspaceAtom);
const label = collection.name || t['Untitled'](); const label = collection.name || t['Untitled']();
const category = 'affine:collections'; const category = 'affine:collections';
return { return {
@ -372,11 +323,7 @@ export const collectionToCommand = (
originalValue: label, originalValue: label,
category: category, category: category,
run: () => { run: () => {
if (!currentWorkspace) { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
console.error('current workspace not found');
return;
}
navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
selectCollection(collection.id); selectCollection(collection.id);
}, },
icon: <ViewLayersIcon />, icon: <ViewLayersIcon />,
@ -385,12 +332,13 @@ export const collectionToCommand = (
export const useCollectionsCommands = () => { export const useCollectionsCommands = () => {
// todo: considering collections for searching pages // todo: considering collections for searching pages
const { savedCollections } = useCollectionManager(collectionsCRUDAtom); const { savedCollections } = useCollectionManager(
const store = getCurrentStore(); useService(CollectionService)
);
const query = useAtomValue(cmdkQueryAtom); const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper(); const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const selectCollection = useCallback( const selectCollection = useCallback(
(id: string) => { (id: string) => {
navigationHelper.jumpToCollection(workspace.id, id); navigationHelper.jumpToCollection(workspace.id, id);
@ -405,22 +353,39 @@ export const useCollectionsCommands = () => {
results = savedCollections.map(collection => { results = savedCollections.map(collection => {
const command = collectionToCommand( const command = collectionToCommand(
collection, collection,
store,
navigationHelper, navigationHelper,
selectCollection, selectCollection,
t t,
workspace
); );
return command; return command;
}); });
return results; return results;
} }
}, [query, savedCollections, store, navigationHelper, selectCollection, t]); }, [
query,
savedCollections,
navigationHelper,
selectCollection,
t,
workspace,
]);
}; };
export const useCMDKCommandGroups = () => { export const useCMDKCommandGroups = () => {
const pageCommands = usePageCommands(); const pageCommands = usePageCommands();
const collectionCommands = useCollectionsCommands(); const collectionCommands = useCollectionsCommands();
const affineCommands = useAtomValue(filteredAffineCommands);
const currentPageId = useAtomValue(currentPageIdAtom);
const pageSettings = useAtomValue(pageSettingsAtom);
const currentPageMode = currentPageId
? pageSettings[currentPageId]?.mode
: undefined;
const affineCommands = useMemo(() => {
return getAllCommand({
pageMode: currentPageMode,
});
}, [currentPageMode]);
return useMemo(() => { return useMemo(() => {
const commands = [ const commands = [

View File

@ -1,8 +1,6 @@
import type { Page } from '@blocksuite/store';
import type { CommandCategory } from '@toeverything/infra/command'; import type { CommandCategory } from '@toeverything/infra/command';
export interface CommandContext { export interface CommandContext {
currentPage: Page | undefined;
pageMode: 'page' | 'edgeless' | undefined; pageMode: 'page' | 'edgeless' | undefined;
} }

View File

@ -2,22 +2,25 @@ import { Button } from '@affine/component/ui/button';
import { ConfirmModal } from '@affine/component/ui/modal'; import { ConfirmModal } from '@affine/component/ui/modal';
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon, ResetIcon } from '@blocksuite/icons'; import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
import { useAtomValue } from 'jotai'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
import { WorkspaceSubPath } from '../../../shared';
import { toast } from '../../../utils'; import { toast } from '../../../utils';
import * as styles from './styles.css'; import * as styles from './styles.css';
export const TrashPageFooter = ({ pageId }: { pageId: string }) => { export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
assertExists(workspace); assertExists(workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(

View File

@ -7,21 +7,22 @@ import {
filterPage, filterPage,
stopPropagation, stopPropagation,
useCollectionManager, useCollectionManager,
useSavedCollections,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { CollectionService } from '@affine/core/modules/collection';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons'; import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store'; import type { PageMeta, Workspace } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra/livedata';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { collectionsCRUDAtom } from '../../../../atoms/collections';
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config'; import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag'; import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
import { useBlockSuitePageMeta } from '../../../../hooks/use-block-suite-page-meta';
import type { CollectionsListProps } from '../index'; import type { CollectionsListProps } from '../index';
import { Page } from './page'; import { Page } from './page';
import * as styles from './styles.css'; import * as styles from './styles.css';
@ -39,7 +40,7 @@ const CollectionRenderer = ({
}) => { }) => {
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const dragItemId = getDropItemId('collections', collection.id); const dragItemId = getDropItemId('collections', collection.id);
@ -168,7 +169,7 @@ export const CollectionsList = ({
onCreate, onCreate,
}: CollectionsListProps) => { }: CollectionsListProps) => {
const metas = useBlockSuitePageMeta(workspace); const metas = useBlockSuitePageMeta(workspace);
const { collections } = useSavedCollections(collectionsCRUDAtom); const collections = useLiveData(useService(CollectionService).collections);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
if (collections.length === 0) { if (collections.length === 0) {
return ( return (

View File

@ -1,13 +1,12 @@
import { Divider } from '@affine/component/ui/divider'; import { Divider } from '@affine/component/ui/divider';
import { MenuItem } from '@affine/component/ui/menu'; import { MenuItem } from '@affine/component/ui/menu';
import {
workspaceListAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Logo1Icon } from '@blocksuite/icons'; import { Logo1Icon } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai'; import { WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
@ -85,9 +84,8 @@ export const UserWithWorkspaceList = ({
onEventEnd?.(); onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]); }, [onEventEnd, setOpenCreateWorkspaceModal]);
const workspaces = useAtomValue(workspaceListAtom); const workspaceManager = useService(WorkspaceManager);
const workspaces = useLiveData(workspaceManager.list.workspaceList);
const workspaceManager = useAtomValue(workspaceManagerAtom);
// revalidate workspace list when mounted // revalidate workspace list when mounted
useEffect(() => { useEffect(() => {

View File

@ -5,16 +5,13 @@ import {
useWorkspaceAvatar, useWorkspaceAvatar,
useWorkspaceName, useWorkspaceName,
} from '@affine/core/hooks/use-workspace-info'; } from '@affine/core/hooks/use-workspace-info';
import {
currentWorkspaceAtom,
workspaceListAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { WorkspaceMetadata } from '@affine/workspace';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
import { useAtomValue, useSetAtom } from 'jotai'; import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata';
import { useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@ -23,6 +20,8 @@ import {
openCreateWorkspaceModalAtom, openCreateWorkspaceModalAtom,
openSettingModalAtom, openSettingModalAtom,
} from '../../../../../atoms'; } from '../../../../../atoms';
import { CurrentWorkspaceService } from '../../../../../modules/workspace/current-workspace';
import { WorkspaceSubPath } from '../../../../../shared';
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
import * as styles from './index.css'; import * as styles from './index.css';
@ -106,13 +105,17 @@ export const AFFiNEWorkspaceList = ({
}: { }: {
onEventEnd?: () => void; onEventEnd?: () => void;
}) => { }) => {
const workspaces = useAtomValue(workspaceListAtom); const workspaces = useLiveData(
useService(WorkspaceManager).list.workspaceList
);
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom); const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const { jumpToSubPath } = useNavigateHelper(); const { jumpToSubPath } = useNavigateHelper();
const currentWorkspace = useAtomValue(currentWorkspaceAtom); const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace
);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);

View File

@ -6,11 +6,9 @@ import { openSettingModalAtom } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob'; import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info'; import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
import { import {
CloudWorkspaceIcon, CloudWorkspaceIcon,
InformationFillDuotoneIcon, InformationFillDuotoneIcon,
@ -18,7 +16,13 @@ import {
NoNetworkIcon, NoNetworkIcon,
UnsyncIcon, UnsyncIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { useAtomValue, useSetAtom } from 'jotai'; import {
type SyncEngineStatus,
SyncEngineStep,
Workspace,
} from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { debounce, mean } from 'lodash-es'; import { debounce, mean } from 'lodash-es';
import { import {
forwardRef, forwardRef,
@ -97,7 +101,7 @@ const useSyncEngineSyncProgress = () => {
useState<SyncEngineStatus | null>(null); useState<SyncEngineStatus | null>(null);
const [isOverCapacity, setIsOverCapacity] = useState(false); const [isOverCapacity, setIsOverCapacity] = useState(false);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta); const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
const setSettingModalAtom = useSetAtom(openSettingModalAtom); const setSettingModalAtom = useSetAtom(openSettingModalAtom);
@ -250,7 +254,7 @@ export const WorkspaceCard = forwardRef<
HTMLDivElement, HTMLDivElement,
HTMLAttributes<HTMLDivElement> HTMLAttributes<HTMLDivElement>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const information = useWorkspaceInfo(currentWorkspace.meta); const information = useWorkspaceInfo(currentWorkspace.meta);

View File

@ -12,15 +12,14 @@ import {
SidebarScrollableContainer, SidebarScrollableContainer,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { Menu } from '@affine/component/ui/menu'; import { Menu } from '@affine/component/ui/menu';
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { WorkspaceSubPath } from '@affine/core/shared'; import { CollectionService } from '@affine/core/modules/collection';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace';
import { FolderIcon, SettingsIcon } from '@blocksuite/icons'; import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
import { type Page } from '@blocksuite/store'; import { type Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { useService, type Workspace } from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { HTMLAttributes, ReactElement } from 'react'; import type { HTMLAttributes, ReactElement } from 'react';
@ -35,6 +34,7 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands'; import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../shared';
import { import {
createEmptyCollection, createEmptyCollection,
MoveToTrash, MoveToTrash,
@ -177,7 +177,7 @@ export const RootAppSidebar = ({
useRegisterBrowserHistoryCommands(router.back, router.forward); useRegisterBrowserHistoryCommands(router.back, router.forward);
const userInfo = useDeleteCollectionInfo(); const userInfo = useDeleteCollectionInfo();
const setting = useCollectionManager(collectionsCRUDAtom); const setting = useCollectionManager(useService(CollectionService));
const { node, open } = useEditCollectionName({ const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](), title: t['com.affine.editCollection.createCollection'](),
showTips: true, showTips: true,

View File

@ -1,18 +1,18 @@
import { BrowserWarning } from '@affine/component/affine-banner'; import { BrowserWarning } from '@affine/component/affine-banner';
import { LocalDemoTips } from '@affine/component/affine-banner'; import { LocalDemoTips } from '@affine/component/affine-banner';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@affine/workspace'; import { type Workspace, WorkspaceManager } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai'; import { useService } from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { authAtom } from '../atoms'; import { authAtom } from '../atoms';
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal'; import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
const minimumChromeVersion = 106; const minimumChromeVersion = 106;
@ -77,7 +77,7 @@ export const TopTip = ({
}, [setAuthModal]); }, [setAuthModal]);
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
const handleConfirm = useAsyncCallback(async () => { const handleConfirm = useAsyncCallback(async () => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) { if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
return; return;

View File

@ -3,15 +3,12 @@ import { AffineShapeIcon } from '@affine/core/components/page-list'; // TODO: im
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status'; import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
import {
waitForCurrentWorkspaceAtom,
workspaceManagerAtom,
} from '@affine/core/modules/workspace';
import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtomValue } from 'jotai'; import { Workspace, WorkspaceManager } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useState } from 'react'; import { useState } from 'react';
import { WorkspaceSubPath } from '../../shared';
import * as styles from './upgrade.css'; import * as styles from './upgrade.css';
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon'; import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
@ -20,8 +17,8 @@ import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
*/ */
export const WorkspaceUpgrade = function WorkspaceUpgrade() { export const WorkspaceUpgrade = function WorkspaceUpgrade() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const workspaceManager = useAtomValue(workspaceManagerAtom); const workspaceManager = useService(WorkspaceManager);
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade); const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -32,10 +29,10 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade() {
} }
try { try {
const newWorkspaceId = const newWorkspace =
await currentWorkspace.upgrade.upgrade(workspaceManager); await currentWorkspace.upgrade.upgrade(workspaceManager);
if (newWorkspaceId) { if (newWorkspace) {
openPage(newWorkspaceId, WorkspaceSubPath.ALL); openPage(newWorkspace.id, WorkspaceSubPath.ALL);
} else { } else {
// blocksuite may enter an incorrect state, reload to reset it. // blocksuite may enter an incorrect state, reload to reset it.
location.reload(); location.reload();

View File

@ -3,101 +3,71 @@
*/ */
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import { import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
currentWorkspaceAtom,
WorkspacePropertiesAdapter,
} from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Workspace } from '@affine/workspace/workspace';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { assertExists } from '@blocksuite/global/utils';
import { type Page, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { Schema } from '@blocksuite/store';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { Workspace } from '@toeverything/infra';
import { ServiceProviderContext, useService } from '@toeverything/infra/di';
import { createStore, Provider } from 'jotai'; import { createStore, Provider } from 'jotai';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { describe, expect, test, vi } from 'vitest'; import { describe, expect, test, vi } from 'vitest';
import { beforeEach } from 'vitest'; import { beforeEach } from 'vitest';
import { configureTestingEnvironment } from '../../testing';
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title'; import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
let blockSuiteWorkspace: BlockSuiteWorkspace;
const store = createStore(); const store = createStore();
const schema = new Schema();
schema.register(AffineSchemas).register(__unstableSchemas);
const Component = () => { const Component = () => {
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0'); const workspace = useService(Workspace);
const title = useBlockSuiteWorkspacePageTitle(
workspace.blockSuiteWorkspace,
'page0'
);
return <div>title: {title}</div>; return <div>title: {title}</div>;
}; };
// todo: this module has some side-effects that will break the tests
vi.mock('@affine/workspace-impl', () => ({
default: {},
}));
beforeEach(async () => { beforeEach(async () => {
vi.useFakeTimers({ toFake: ['requestIdleCallback'] }); vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
const workspace = {
blockSuiteWorkspace,
flavour: WorkspaceFlavour.LOCAL,
} as Workspace;
store.set(currentWorkspaceAtom, workspace);
blockSuiteWorkspace = workspace.blockSuiteWorkspace;
blockSuiteWorkspace.doc.emit('sync', []);
const initPage = async (page: Page) => {
await page.waitForLoaded();
expect(page).not.toBeNull();
assertExists(page);
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
const frameId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
}); });
describe('useBlockSuiteWorkspacePageTitle', () => { describe('useBlockSuiteWorkspacePageTitle', () => {
test('basic', async () => { test('basic', async () => {
const { workspace, page } = await configureTestingEnvironment();
const { findByText, rerender } = render( const { findByText, rerender } = render(
<Provider store={store}> <ServiceProviderContext.Provider value={page.services}>
<Suspense fallback="loading"> <Provider store={store}>
<Component /> <Suspense fallback="loading">
</Suspense> <Component />
</Provider> </Suspense>
</Provider>
</ServiceProviderContext.Provider>
); );
expect(await findByText('title: Untitled')).toBeDefined(); expect(await findByText('title: Untitled')).toBeDefined();
blockSuiteWorkspace.setPageMeta('page0', { title: '1' }); workspace.blockSuiteWorkspace.setPageMeta(page.id, { title: '1' });
rerender( rerender(
<Provider store={store}> <ServiceProviderContext.Provider value={page.services}>
<Suspense fallback="loading"> <Provider store={store}>
<Component /> <Suspense fallback="loading">
</Suspense> <Component />
</Provider> </Suspense>
</Provider>
</ServiceProviderContext.Provider>
); );
expect(await findByText('title: 1')).toBeDefined(); expect(await findByText('title: 1')).toBeDefined();
}); });
test('journal', async () => { test('journal', async () => {
const adapter = new WorkspacePropertiesAdapter(blockSuiteWorkspace); const { workspace, page } = await configureTestingEnvironment();
adapter.setJournalPageDateString('page0', '2021-01-01'); const adapter = workspace.services.get(WorkspacePropertiesAdapter);
adapter.setJournalPageDateString(page.id, '2021-01-01');
const { findByText } = render( const { findByText } = render(
<Provider store={store}> <ServiceProviderContext.Provider value={page.services}>
<Suspense fallback="loading"> <Provider store={store}>
<Component /> <Suspense fallback="loading">
</Suspense> <Component />
</Provider> </Suspense>
</Provider>
</ServiceProviderContext.Provider>
); );
expect(await findByText('title: Jan 1, 2021')).toBeDefined(); expect(await findByText('title: Jan 1, 2021')).toBeDefined();
}); });

View File

@ -4,17 +4,17 @@ import {
FavoriteTag, FavoriteTag,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export const useAllPageListConfig = () => { export const useAllPageListConfig = () => {
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const workspace = currentWorkspace.blockSuiteWorkspace; const workspace = currentWorkspace.blockSuiteWorkspace;
const pageMetas = useBlockSuitePageMeta(workspace); const pageMetas = useBlockSuitePageMeta(workspace);
const { isPreferredEdgeless } = usePageHelper(workspace); const { isPreferredEdgeless } = usePageHelper(workspace);

View File

@ -4,6 +4,8 @@ import {
usePageMetaHelper, usePageMetaHelper,
} from '@affine/core/hooks/use-block-suite-page-meta'; } from '@affine/core/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { CollectionService } from '@affine/core/modules/collection';
import { useService } from '@toeverything/infra';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
@ -11,7 +13,6 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { setPageModeAtom } from '../../atoms'; import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode'; import { currentModeAtom } from '../../atoms/mode';
import type { BlockSuiteWorkspace } from '../../shared'; import type { BlockSuiteWorkspace } from '../../shared';
import { getWorkspaceSetting } from '../../utils/workspace-setting';
import { useNavigateHelper } from '../use-navigate-helper'; import { useNavigateHelper } from '../use-navigate-helper';
import { useReferenceLinkHelper } from './use-reference-link-helper'; import { useReferenceLinkHelper } from './use-reference-link-helper';
@ -26,6 +27,7 @@ export function useBlockSuiteMetaHelper(
const currentMode = useAtomValue(currentModeAtom); const currentMode = useAtomValue(currentModeAtom);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const collectionService = useService(CollectionService);
const switchToPageMode = useCallback( const switchToPageMode = useCallback(
(pageId: string) => { (pageId: string) => {
@ -89,9 +91,9 @@ export function useBlockSuiteMetaHelper(
trashRelate: isRoot ? parentMeta?.id : undefined, trashRelate: isRoot ? parentMeta?.id : undefined,
}); });
setPageReadonly(pageId, true); setPageReadonly(pageId, true);
getWorkspaceSetting(blockSuiteWorkspace).deletePages([pageId]); collectionService.deletePagesFromCollections([pageId]);
}, },
[blockSuiteWorkspace, getPageMeta, metas, setPageMeta, setPageReadonly] [collectionService, getPageMeta, metas, setPageMeta, setPageReadonly]
); );
const restoreFromTrash = useCallback( const restoreFromTrash = useCallback(

View File

@ -1,6 +1,6 @@
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { getIsOwnerQuery } from '@affine/graphql'; import { getIsOwnerQuery } from '@affine/graphql';
import type { WorkspaceMetadata } from '@affine/workspace/metadata'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { useQueryImmutable } from '../use-query'; import { useQueryImmutable } from '../use-query';

View File

@ -1,15 +1,16 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons'; import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { Workspace } from '@toeverything/infra';
import { import {
PreconditionStrategy, PreconditionStrategy,
registerAffineCommand, registerAffineCommand,
} from '@toeverything/infra/command'; } from '@toeverything/infra/command';
import { useAtomValue, useSetAtom } from 'jotai'; import { useService } from '@toeverything/infra/di';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { pageHistoryModalAtom } from '../../atoms/page-history'; import { pageHistoryModalAtom } from '../../atoms/page-history';
@ -22,7 +23,7 @@ export function useRegisterBlocksuiteEditorCommands(
mode: 'page' | 'edgeless' mode: 'page' | 'edgeless'
) { ) {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const workspace = useAtomValue(waitForCurrentWorkspaceAtom); const workspace = useService(Workspace);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace); const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId); const currentPage = blockSuiteWorkspace.getPage(pageId);

View File

@ -1,10 +1,10 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import type { DraggableTitleCellData } from '@affine/core/components/page-list'; import type { DraggableTitleCellData } from '@affine/core/components/page-list';
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core'; import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import { useAtomValue } from 'jotai'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
@ -69,7 +69,7 @@ export function getDragItemId(
export const useSidebarDrag = () => { export const useSidebarDrag = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
const workspace = currentWorkspace.blockSuiteWorkspace; const workspace = currentWorkspace.blockSuiteWorkspace;
const { setTrashModal } = useTrashModalHelper(workspace); const { setTrashModal } = useTrashModalHelper(workspace);
const { addToFavorite, removeFromFavorite } = const { addToFavorite, removeFromFavorite } =

View File

@ -1,15 +1,15 @@
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { currentPageIdAtom } from '../../atoms/mode'; import { currentPageIdAtom } from '../../atoms/mode';
export const useCurrentPage = () => { export const useCurrentPage = () => {
const currentPageId = useAtomValue(currentPageIdAtom); const currentPageId = useAtomValue(currentPageIdAtom);
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom); const currentWorkspace = useService(Workspace);
return useBlockSuiteWorkspacePage( return useBlockSuiteWorkspacePage(
currentWorkspace?.blockSuiteWorkspace, currentWorkspace.blockSuiteWorkspace,
currentPageId currentPageId
); );
}; };

View File

@ -1,12 +1,7 @@
import type { Workspace } from '@blocksuite/store'; import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { WorkspacePropertiesAdapter } from '../modules/workspace/properties'; import { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
import {
currentWorkspacePropertiesAdapterAtom,
workspaceAdapterAtomFamily,
} from '../modules/workspace/properties';
function getProxy<T extends object>(obj: T) { function getProxy<T extends object>(obj: T) {
return new Proxy(obj, {}); return new Proxy(obj, {});
@ -31,11 +26,6 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
}; };
export function useCurrentWorkspacePropertiesAdapter() { export function useCurrentWorkspacePropertiesAdapter() {
const adapter = useAtomValue(currentWorkspacePropertiesAdapterAtom); const adapter = useService(WorkspacePropertiesAdapter);
return useReactiveAdapter(adapter);
}
export function useWorkspacePropertiesAdapter(workspace: Workspace) {
const adapter = useAtomValue(workspaceAdapterAtomFamily(workspace));
return useReactiveAdapter(adapter); return useReactiveAdapter(adapter);
} }

View File

@ -1,15 +0,0 @@
import { useCallback, useSyncExternalStore } from 'react';
import type { DataSourceAdapter, Status } from 'y-provider';
type UIStatus =
| Status
| {
type: 'unknown';
};
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
return useSyncExternalStore(
provider.subscribeStatusChange,
useCallback(() => provider.status, [provider])
);
}

View File

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import type { BlockSuiteWorkspace } from '../shared'; import type { BlockSuiteWorkspace } from '../shared';
import { timestampToLocalDate } from '../utils'; import { timestampToLocalDate } from '../utils';
import { useWorkspacePropertiesAdapter } from './use-affine-adapter'; import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter';
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
import { useNavigateHelper } from './use-navigate-helper'; import { useNavigateHelper } from './use-navigate-helper';
@ -24,7 +24,7 @@ function toDayjs(j?: string | false) {
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => { export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace); const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
const adapter = useWorkspacePropertiesAdapter(workspace); const adapter = useCurrentWorkspacePropertiesAdapter();
/** /**
* @internal * @internal

Some files were not shown because too many files have changed in this diff Show More