mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 04:51:49 +08:00
refactor(server): indexer & worker & sync perf (#15504)
This commit is contained in:
@@ -3,10 +3,27 @@ import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
assertSupportedServerVersion,
|
||||
getSyncProtocol,
|
||||
isBatchSyncServerVersion,
|
||||
MIN_SUPPORTED_SERVER_VERSION,
|
||||
} from './server-config';
|
||||
|
||||
describe('server config version guard', () => {
|
||||
test('selects batch sync from server version', () => {
|
||||
expect(isBatchSyncServerVersion('0.27.4')).toBe(false);
|
||||
expect(isBatchSyncServerVersion('0.27.5')).toBe(true);
|
||||
expect(isBatchSyncServerVersion('0.27.5-beta.1')).toBe(true);
|
||||
expect(isBatchSyncServerVersion('2026.8.20-canary.15')).toBe(true);
|
||||
expect(isBatchSyncServerVersion('0.28.0')).toBe(true);
|
||||
});
|
||||
|
||||
test('does not select a route before server version is verified', () => {
|
||||
expect(() => getSyncProtocol()).toThrow(UserFriendlyError);
|
||||
expect(() => getSyncProtocol('0.26.9')).toThrow(UserFriendlyError);
|
||||
expect(getSyncProtocol('0.27.4')).toBe('legacy');
|
||||
expect(getSyncProtocol('0.27.5')).toBe('batch');
|
||||
});
|
||||
|
||||
test('accepts supported server versions', () => {
|
||||
expect(() => assertSupportedServerVersion('0.27.0')).not.toThrow();
|
||||
expect(() => assertSupportedServerVersion('0.27.0-beta.5')).not.toThrow();
|
||||
|
||||
@@ -14,6 +14,7 @@ export type ServerConfigType = ServerConfigQuery['serverConfig'] &
|
||||
OauthProvidersQuery['serverConfig'];
|
||||
|
||||
export const MIN_SUPPORTED_SERVER_VERSION = '0.27.0';
|
||||
export const BATCH_SYNC_SERVER_VERSION = '0.27.5';
|
||||
|
||||
const NETWORK_ERROR_PATTERNS = [
|
||||
/failed to fetch/i,
|
||||
@@ -63,6 +64,21 @@ export function assertSupportedServerVersion(version?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isBatchSyncServerVersion(version?: string | null) {
|
||||
const normalized = version && semver.valid(version, { loose: true });
|
||||
return (
|
||||
!!normalized &&
|
||||
semver.gte(normalized, `${BATCH_SYNC_SERVER_VERSION}-0`, {
|
||||
loose: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function getSyncProtocol(version?: string | null) {
|
||||
assertSupportedServerVersion(version);
|
||||
return isBatchSyncServerVersion(version) ? 'batch' : 'legacy';
|
||||
}
|
||||
|
||||
function mapServerConfigError(error: unknown) {
|
||||
const userFriendlyError = UserFriendlyError.fromAny(error);
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Framework, LiveData } from '@toeverything/infra';
|
||||
import { Subject } from 'rxjs';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { AuthService } from '../../cloud/services/auth';
|
||||
import { NbstoreService } from '../../storage/services/nbstore';
|
||||
import { NotificationStore } from '../stores/notification';
|
||||
import { NotificationCountService } from './count';
|
||||
|
||||
function createCountService() {
|
||||
const events$ = new Subject<{ type: 'ready' } | { count: number }>();
|
||||
const request = vi.fn().mockResolvedValue({ count: 1 });
|
||||
const cache = new LiveData(0);
|
||||
const setNotificationCountCache = vi.fn((count: number) =>
|
||||
cache.setValue(count)
|
||||
);
|
||||
const store = {
|
||||
watchNotificationCountCache: () => cache,
|
||||
setNotificationCountCache,
|
||||
} as unknown as NotificationStore;
|
||||
const auth = {
|
||||
session: {
|
||||
status$: new LiveData<'authenticated' | 'unauthenticated'>(
|
||||
'authenticated'
|
||||
),
|
||||
},
|
||||
} as unknown as AuthService;
|
||||
const nbstore = {
|
||||
realtime: {
|
||||
request,
|
||||
subscribe: () => events$,
|
||||
},
|
||||
} as unknown as NbstoreService;
|
||||
|
||||
const framework = new Framework();
|
||||
framework.service(AuthService, auth);
|
||||
framework.store(NotificationStore, store);
|
||||
framework.service(NbstoreService, nbstore);
|
||||
framework.service(NotificationCountService, [
|
||||
NotificationStore,
|
||||
AuthService,
|
||||
NbstoreService,
|
||||
]);
|
||||
|
||||
return {
|
||||
events$,
|
||||
request,
|
||||
service: framework.provider().get(NotificationCountService),
|
||||
setNotificationCountCache,
|
||||
};
|
||||
}
|
||||
|
||||
describe('NotificationCountService', () => {
|
||||
test('uses snapshots for reconnects and applies realtime count changes', async () => {
|
||||
const { events$, request, service, setNotificationCountCache } =
|
||||
createCountService();
|
||||
|
||||
expect(service.loggedIn$.value).toBe(true);
|
||||
service.handleServerStarted();
|
||||
events$.next({ type: 'ready' });
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalled());
|
||||
await vi.waitFor(() => expect(service.count$.value).toBe(1));
|
||||
|
||||
events$.next({ count: 3 });
|
||||
expect(service.count$.value).toBe(3);
|
||||
expect(setNotificationCountCache).toHaveBeenLastCalledWith(3);
|
||||
|
||||
const requestsBeforeFocus = request.mock.calls.length;
|
||||
service.handleApplicationFocused();
|
||||
events$.next({ type: 'ready' });
|
||||
await vi.waitFor(() =>
|
||||
expect(request.mock.calls.length).toBeGreaterThan(requestsBeforeFocus)
|
||||
);
|
||||
await vi.waitFor(() => expect(service.count$.value).toBe(1));
|
||||
service.dispose();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LiveData, OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import { AccountChanged, type AuthService } from '../../cloud';
|
||||
import { AccountChanged } from '../../cloud/events/account-changed';
|
||||
import { ServerStarted } from '../../cloud/events/server-started';
|
||||
import { RealtimeLiveQuery } from '../../cloud/realtime/live-query';
|
||||
import type { AuthService } from '../../cloud/services/auth';
|
||||
import { ApplicationFocused } from '../../lifecycle';
|
||||
import type { NbstoreService } from '../../storage';
|
||||
import type { NotificationStore } from '../stores/notification';
|
||||
@@ -19,7 +20,10 @@ export class NotificationCountService extends Service {
|
||||
super();
|
||||
}
|
||||
|
||||
loggedIn$ = this.authService.session.status$.map(v => v === 'authenticated');
|
||||
loggedIn$ = LiveData.from(
|
||||
this.authService.session.status$.map(v => v === 'authenticated'),
|
||||
this.authService.session.status$.value === 'authenticated'
|
||||
);
|
||||
|
||||
readonly count$ = LiveData.from(this.store.watchNotificationCountCache(), 0);
|
||||
readonly isLoading$ = new LiveData(false);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { StoreClient } from '@affine/nbstore/worker/client';
|
||||
import { Entity } from '@toeverything/infra';
|
||||
|
||||
import type { ServerService } from '../../cloud';
|
||||
import { getSyncProtocol } from '../../cloud/stores/server-config';
|
||||
import type { NbstoreService } from '../../storage';
|
||||
|
||||
export class UserDBEngine extends Entity<{
|
||||
@@ -64,6 +65,9 @@ export class UserDBEngine extends Entity<{
|
||||
opts: {
|
||||
id: this.userId,
|
||||
serverBaseUrl: serverService.server.baseUrl,
|
||||
syncProtocol: getSyncProtocol(
|
||||
serverService.server.config$.value.version
|
||||
),
|
||||
type: 'userspace',
|
||||
isSelfHosted:
|
||||
serverService.server.config$.value.type ===
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
GraphQLService,
|
||||
WorkspaceServerService,
|
||||
} from '../../cloud';
|
||||
import { getSyncProtocol } from '../../cloud/stores/server-config';
|
||||
import { type GlobalState, NbstoreService } from '../../storage';
|
||||
import type {
|
||||
Workspace,
|
||||
@@ -519,6 +520,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
type: 'workspace',
|
||||
id: workspaceId,
|
||||
serverBaseUrl: this.server.serverMetadata.baseUrl,
|
||||
syncProtocol: getSyncProtocol(this.server.config$.value.version),
|
||||
isSelfHosted:
|
||||
this.server.config$.value.type ===
|
||||
ServerDeploymentType.Selfhosted,
|
||||
@@ -537,6 +539,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
type: 'workspace',
|
||||
id: workspaceId,
|
||||
serverBaseUrl: this.server.serverMetadata.baseUrl,
|
||||
syncProtocol: getSyncProtocol(this.server.config$.value.version),
|
||||
isSelfHosted:
|
||||
this.server.config$.value.type ===
|
||||
ServerDeploymentType.Selfhosted,
|
||||
|
||||
Reference in New Issue
Block a user