refactor(server): config system (#11081)
This commit is contained in:
@@ -6,8 +6,6 @@ import ava, { TestFn } from 'ava';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
|
||||
import { AppModule } from '../../../app.module';
|
||||
import { ConfigModule } from '../../../base/config';
|
||||
import { Models } from '../../../models';
|
||||
import { WorkspaceBlobStorage } from '../../storage/wrappers/blob';
|
||||
import { DocReader, PgWorkspaceDocStorageAdapter } from '..';
|
||||
@@ -22,9 +20,7 @@ const test = ava as TestFn<{
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [ConfigModule.forRoot(), AppModule],
|
||||
});
|
||||
const app = await createTestingApp();
|
||||
|
||||
t.context.models = app.get(Models);
|
||||
t.context.docReader = app.get(DocReader);
|
||||
|
||||
@@ -6,9 +6,8 @@ import ava, { TestFn } from 'ava';
|
||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||
|
||||
import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
|
||||
import { AppModule } from '../../../app.module';
|
||||
import { Config, UserFriendlyError } from '../../../base';
|
||||
import { ConfigModule } from '../../../base/config';
|
||||
import { UserFriendlyError } from '../../../base';
|
||||
import { ConfigFactory } from '../../../base/config';
|
||||
import { Models } from '../../../models';
|
||||
import { DatabaseDocReader, DocReader, PgWorkspaceDocStorageAdapter } from '..';
|
||||
import { RpcDocReader } from '../reader';
|
||||
@@ -16,40 +15,45 @@ import { RpcDocReader } from '../reader';
|
||||
const test = ava as TestFn<{
|
||||
models: Models;
|
||||
app: TestingApp;
|
||||
docApp: TestingApp;
|
||||
docReader: DocReader;
|
||||
databaseDocReader: DatabaseDocReader;
|
||||
adapter: PgWorkspaceDocStorageAdapter;
|
||||
config: Config;
|
||||
config: ConfigFactory;
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
flavor: {
|
||||
doc: false,
|
||||
},
|
||||
docService: {
|
||||
endpoint: '',
|
||||
},
|
||||
}),
|
||||
AppModule,
|
||||
],
|
||||
});
|
||||
// test key
|
||||
process.env.AFFINE_PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgS3IAkshQuSmFWGpe
|
||||
rGTg2vwaC3LdcvBQlYHHMBYJZMyhRANCAAQXdT/TAh4neNEpd4UqpDIEqWv0XvFo
|
||||
BRJxGsC5I/fetqObdx1+KEjcm8zFU2xLaUTw9IZCu8OslloOjQv4ur0a
|
||||
-----END PRIVATE KEY-----`;
|
||||
// @ts-expect-error testing
|
||||
env.FLAVOR = 'renderer';
|
||||
const notDocApp = await createTestingApp();
|
||||
// @ts-expect-error testing
|
||||
env.FLAVOR = 'doc';
|
||||
const docApp = await createTestingApp();
|
||||
|
||||
t.context.models = app.get(Models);
|
||||
t.context.docReader = app.get(DocReader);
|
||||
t.context.databaseDocReader = app.get(DatabaseDocReader);
|
||||
t.context.adapter = app.get(PgWorkspaceDocStorageAdapter);
|
||||
t.context.config = app.get(Config);
|
||||
t.context.app = app;
|
||||
t.context.models = notDocApp.get(Models);
|
||||
t.context.docReader = notDocApp.get(DocReader);
|
||||
t.context.databaseDocReader = docApp.get(DatabaseDocReader);
|
||||
t.context.adapter = docApp.get(PgWorkspaceDocStorageAdapter);
|
||||
t.context.config = notDocApp.get(ConfigFactory);
|
||||
t.context.app = notDocApp;
|
||||
t.context.docApp = docApp;
|
||||
});
|
||||
|
||||
let user: User;
|
||||
let workspace: Workspace;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
t.context.config.docService.endpoint = t.context.app.url();
|
||||
t.context.config.override({
|
||||
docService: {
|
||||
endpoint: t.context.docApp.url(),
|
||||
},
|
||||
});
|
||||
await t.context.app.initTestingDB();
|
||||
user = await t.context.models.user.create({
|
||||
email: 'test@affine.pro',
|
||||
@@ -63,6 +67,7 @@ test.afterEach.always(() => {
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
await t.context.docApp.close();
|
||||
});
|
||||
|
||||
test('should return null when doc not found', async t => {
|
||||
@@ -113,7 +118,11 @@ test('should throw error when doc service internal error', async t => {
|
||||
|
||||
test('should fallback to database doc reader when endpoint network error', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.context.config.docService.endpoint = 'http://localhost:13010';
|
||||
t.context.config.override({
|
||||
docService: {
|
||||
endpoint: 'http://localhost:13010',
|
||||
},
|
||||
});
|
||||
const docId = randomUUID();
|
||||
const timestamp = Date.now();
|
||||
await t.context.models.doc.createUpdates([
|
||||
@@ -223,7 +232,11 @@ test('should return doc diff', async t => {
|
||||
|
||||
test('should get doc diff fallback to database doc reader when endpoint network error', async t => {
|
||||
const { docReader } = t.context;
|
||||
t.context.config.docService.endpoint = 'http://localhost:13010';
|
||||
t.context.config.override({
|
||||
docService: {
|
||||
endpoint: 'http://localhost:13010',
|
||||
},
|
||||
});
|
||||
const docId = randomUUID();
|
||||
const timestamp = Date.now();
|
||||
let updates: Buffer[] = [];
|
||||
|
||||
@@ -1,44 +1,25 @@
|
||||
import {
|
||||
defineRuntimeConfig,
|
||||
defineStartupConfig,
|
||||
ModuleConfig,
|
||||
} from '../../base/config';
|
||||
import { defineModuleConfig } from '../../base';
|
||||
|
||||
interface DocStartupConfigurations {
|
||||
history: {
|
||||
/**
|
||||
* How long the buffer time of creating a new history snapshot when doc get updated.
|
||||
*
|
||||
* in {ms}
|
||||
*/
|
||||
interval: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DocRuntimeConfigurations {
|
||||
/**
|
||||
* Use `y-octo` to merge updates at the same time when merging using Yjs.
|
||||
*
|
||||
* This is an experimental feature, and aimed to check the correctness of JwstCodec.
|
||||
*/
|
||||
experimentalMergeWithYOcto: boolean;
|
||||
}
|
||||
|
||||
declare module '../../base/config' {
|
||||
interface AppConfig {
|
||||
doc: ModuleConfig<DocStartupConfigurations, DocRuntimeConfigurations>;
|
||||
declare global {
|
||||
interface AppConfigSchema {
|
||||
doc: {
|
||||
history: {
|
||||
interval: number;
|
||||
};
|
||||
experimental: {
|
||||
yocto: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
defineStartupConfig('doc', {
|
||||
history: {
|
||||
interval: 1000 * 60 * 10 /* 10 mins */,
|
||||
},
|
||||
});
|
||||
|
||||
defineRuntimeConfig('doc', {
|
||||
experimentalMergeWithYOcto: {
|
||||
defineModuleConfig('doc', {
|
||||
'experimental.yocto': {
|
||||
desc: 'Use `y-octo` to merge updates at the same time when merging using Yjs.',
|
||||
default: false,
|
||||
},
|
||||
'history.interval': {
|
||||
desc: 'The minimum time interval in milliseconds of creating a new history snapshot when doc get updated.',
|
||||
default: 1000 * 60 * 10 /* 10 mins */,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,13 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { chunk } from 'lodash-es';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import {
|
||||
CallMetric,
|
||||
Config,
|
||||
mergeUpdatesInApplyWay as yotcoMergeUpdates,
|
||||
metrics,
|
||||
Runtime,
|
||||
} from '../../base';
|
||||
import { CallMetric, Config, metrics } from '../../base';
|
||||
import { mergeUpdatesInApplyWay as yoctoMergeUpdates } from '../../native';
|
||||
import { QuotaService } from '../quota';
|
||||
import { DocStorageOptions as IDocStorageOptions } from './storage';
|
||||
|
||||
@@ -35,7 +30,6 @@ export class DocStorageOptions implements IDocStorageOptions {
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly runtime: Runtime,
|
||||
private readonly quota: QuotaService
|
||||
) {}
|
||||
|
||||
@@ -43,19 +37,17 @@ export class DocStorageOptions implements IDocStorageOptions {
|
||||
const doc = await this.recoverDoc(updates);
|
||||
const yjsResult = Buffer.from(Y.encodeStateAsUpdate(doc));
|
||||
|
||||
const useYocto = await this.runtime.fetch('doc/experimentalMergeWithYOcto');
|
||||
|
||||
if (useYocto) {
|
||||
if (this.config.doc.experimental.yocto) {
|
||||
metrics.jwst.counter('codec_merge_counter').add(1);
|
||||
let log = false;
|
||||
let yoctoResult: Buffer | null = null;
|
||||
try {
|
||||
yoctoResult = yotcoMergeUpdates(updates.map(Buffer.from));
|
||||
yoctoResult = yoctoMergeUpdates(updates.map(Buffer.from));
|
||||
if (!compare(yjsResult, yoctoResult)) {
|
||||
metrics.jwst.counter('codec_not_match').add(1);
|
||||
this.logger.warn(`yocto codec result doesn't match yjs codec result`);
|
||||
log = true;
|
||||
if (this.config.node.dev) {
|
||||
if (env.dev) {
|
||||
this.logger.warn(`Expected:\n ${yjsResult.toString('hex')}`);
|
||||
this.logger.warn(`Result:\n ${yoctoResult.toString('hex')}`);
|
||||
}
|
||||
@@ -66,14 +58,14 @@ export class DocStorageOptions implements IDocStorageOptions {
|
||||
log = true;
|
||||
}
|
||||
|
||||
if (log && this.config.node.dev) {
|
||||
if (log && env.dev) {
|
||||
this.logger.warn(
|
||||
`Updates: ${updates.map(u => Buffer.from(u).toString('hex')).join('\n')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.config.affine.canary &&
|
||||
env.namespaces.canary &&
|
||||
yoctoResult &&
|
||||
yoctoResult.length > 2 /* simple test for non-empty yjs binary */
|
||||
) {
|
||||
|
||||
@@ -402,11 +402,11 @@ export class RpcDocReader extends DatabaseDocReader {
|
||||
|
||||
export const DocReaderProvider: FactoryProvider = {
|
||||
provide: DocReader,
|
||||
useFactory: (config: Config, ref: ModuleRef) => {
|
||||
if (config.flavor.doc) {
|
||||
useFactory: (ref: ModuleRef) => {
|
||||
if (env.flavors.doc) {
|
||||
return ref.create(DatabaseDocReader);
|
||||
}
|
||||
return ref.create(RpcDocReader);
|
||||
},
|
||||
inject: [Config, ModuleRef],
|
||||
inject: [ModuleRef],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user