mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 19:16:29 +08:00
refactor(server): standalone runtime module (#9120)
This commit is contained in:
@@ -3,7 +3,6 @@ import { merge } from 'lodash-es';
|
||||
|
||||
import { AFFiNEConfig } from './def';
|
||||
import { Config } from './provider';
|
||||
import { Runtime } from './runtime/service';
|
||||
|
||||
export * from './def';
|
||||
export * from './default';
|
||||
@@ -17,10 +16,10 @@ function createConfigProvider(
|
||||
): FactoryProvider<Config> {
|
||||
return {
|
||||
provide: Config,
|
||||
useFactory: (runtime: Runtime) => {
|
||||
return Object.freeze(merge({}, globalThis.AFFiNE, override, { runtime }));
|
||||
useFactory: () => {
|
||||
return Object.freeze(merge({}, globalThis.AFFiNE, override));
|
||||
},
|
||||
inject: [Runtime],
|
||||
inject: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,10 +30,8 @@ export class ConfigModule {
|
||||
return {
|
||||
global: true,
|
||||
module: ConfigModule,
|
||||
providers: [provider, Runtime],
|
||||
providers: [provider],
|
||||
exports: [provider],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export { Runtime };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApplyType } from '../utils/types';
|
||||
import { AFFiNEConfig } from './def';
|
||||
import type { Runtime } from './runtime/service';
|
||||
|
||||
/**
|
||||
* @example
|
||||
@@ -14,6 +13,4 @@ import type { Runtime } from './runtime/service';
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export class Config extends ApplyType<AFFiNEConfig>() {
|
||||
runtime!: Runtime;
|
||||
}
|
||||
export class Config extends ApplyType<AFFiNEConfig>() {}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { OnEvent } from '../../event';
|
||||
import { Payload } from '../../event/def';
|
||||
import { FlattenedAppRuntimeConfig } from '../types';
|
||||
|
||||
declare module '../../event/def' {
|
||||
interface EventDefinitions {
|
||||
runtimeConfig: {
|
||||
[K in keyof FlattenedAppRuntimeConfig]: {
|
||||
changed: Payload<FlattenedAppRuntimeConfig[K]>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* not implemented yet
|
||||
*/
|
||||
export const OnRuntimeConfigChange_DO_NOT_USE = (
|
||||
nameWithModule: keyof FlattenedAppRuntimeConfig
|
||||
) => {
|
||||
return OnEvent(`runtimeConfig.${nameWithModule}.changed`);
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import {
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { difference, keyBy } from 'lodash-es';
|
||||
|
||||
import { Cache } from '../../cache';
|
||||
import { InvalidRuntimeConfigType, RuntimeConfigNotFound } from '../../error';
|
||||
import { defer } from '../../utils/promise';
|
||||
import { defaultRuntimeConfig, runtimeConfigType } from '../register';
|
||||
import { AppRuntimeConfigModules, FlattenedAppRuntimeConfig } from '../types';
|
||||
|
||||
function validateConfigType<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
key: K,
|
||||
value: any
|
||||
) {
|
||||
const config = defaultRuntimeConfig[key];
|
||||
|
||||
if (!config) {
|
||||
throw new RuntimeConfigNotFound({ key });
|
||||
}
|
||||
|
||||
const want = config.type;
|
||||
const get = runtimeConfigType(value);
|
||||
if (get !== want) {
|
||||
throw new InvalidRuntimeConfigType({
|
||||
key,
|
||||
want,
|
||||
get,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* runtime.fetch(k) // v1
|
||||
* runtime.fetchAll(k1, k2, k3) // [v1, v2, v3]
|
||||
* runtime.set(k, v)
|
||||
* runtime.update(k, (v) => {
|
||||
* v.xxx = 'yyy';
|
||||
* return v
|
||||
* })
|
||||
*/
|
||||
@Injectable()
|
||||
export class Runtime implements OnModuleInit {
|
||||
private readonly logger = new Logger('App:RuntimeConfig');
|
||||
|
||||
constructor(
|
||||
private readonly db: PrismaClient,
|
||||
// circular deps: runtime => cache => redis(maybe) => config => runtime
|
||||
@Inject(forwardRef(() => Cache)) private readonly cache: Cache
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.upgradeDB();
|
||||
}
|
||||
|
||||
async fetch<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
k: K
|
||||
): Promise<FlattenedAppRuntimeConfig[K]> {
|
||||
const cached = await this.loadCache<K>(k);
|
||||
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const dbValue = await this.loadDb<K>(k);
|
||||
|
||||
if (dbValue === undefined) {
|
||||
throw new RuntimeConfigNotFound({ key: k });
|
||||
}
|
||||
|
||||
await this.setCache(k, dbValue);
|
||||
|
||||
return dbValue;
|
||||
}
|
||||
|
||||
async fetchAll<
|
||||
Selector extends { [Key in keyof FlattenedAppRuntimeConfig]?: true },
|
||||
>(
|
||||
selector: Selector
|
||||
): Promise<{
|
||||
// @ts-expect-error allow
|
||||
[Key in keyof Selector]: FlattenedAppRuntimeConfig[Key];
|
||||
}> {
|
||||
const keys = Object.keys(selector);
|
||||
|
||||
if (keys.length === 0) {
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
const records = await this.db.runtimeConfig.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
value: true,
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
in: keys,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
const keyed = keyBy(records, 'id');
|
||||
return keys.reduce((ret, key) => {
|
||||
ret[key] = keyed[key]?.value ?? defaultRuntimeConfig[key].value;
|
||||
return ret;
|
||||
}, {} as any);
|
||||
}
|
||||
|
||||
async list(module?: AppRuntimeConfigModules) {
|
||||
return await this.db.runtimeConfig.findMany({
|
||||
where: module ? { module, deletedAt: null } : { deletedAt: null },
|
||||
});
|
||||
}
|
||||
|
||||
async set<
|
||||
K extends keyof FlattenedAppRuntimeConfig,
|
||||
V = FlattenedAppRuntimeConfig[K],
|
||||
>(key: K, value: V) {
|
||||
validateConfigType(key, value);
|
||||
const config = await this.db.runtimeConfig.update({
|
||||
where: {
|
||||
id: key,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: {
|
||||
value: value as any,
|
||||
},
|
||||
});
|
||||
|
||||
await this.setCache(key, config.value as FlattenedAppRuntimeConfig[K]);
|
||||
return config;
|
||||
}
|
||||
|
||||
async update<
|
||||
K extends keyof FlattenedAppRuntimeConfig,
|
||||
V = FlattenedAppRuntimeConfig[K],
|
||||
>(k: K, modifier: (v: V) => V | Promise<V>) {
|
||||
const data = await this.fetch<K>(k);
|
||||
|
||||
const updated = await modifier(data as V);
|
||||
|
||||
await this.set(k, updated);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async loadDb<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
k: K
|
||||
): Promise<FlattenedAppRuntimeConfig[K] | undefined> {
|
||||
const v = await this.db.runtimeConfig.findFirst({
|
||||
where: {
|
||||
id: k,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (v) {
|
||||
return v.value as FlattenedAppRuntimeConfig[K];
|
||||
} else {
|
||||
const record = await this.db.runtimeConfig.create({
|
||||
data: defaultRuntimeConfig[k],
|
||||
});
|
||||
|
||||
return record.value as any;
|
||||
}
|
||||
}
|
||||
|
||||
async loadCache<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
k: K
|
||||
): Promise<FlattenedAppRuntimeConfig[K] | undefined> {
|
||||
return this.cache.get<FlattenedAppRuntimeConfig[K]>(`SERVER_RUNTIME:${k}`);
|
||||
}
|
||||
|
||||
async setCache<K extends keyof FlattenedAppRuntimeConfig>(
|
||||
k: K,
|
||||
v: FlattenedAppRuntimeConfig[K]
|
||||
): Promise<boolean> {
|
||||
return this.cache.set<FlattenedAppRuntimeConfig[K]>(
|
||||
`SERVER_RUNTIME:${k}`,
|
||||
v,
|
||||
{ ttl: 60 * 1000 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the DB with latest runtime configs
|
||||
*/
|
||||
private async upgradeDB() {
|
||||
const existingConfig = await this.db.runtimeConfig.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
const defined = Object.keys(defaultRuntimeConfig);
|
||||
const existing = existingConfig.map(c => c.id);
|
||||
const newConfigs = difference(defined, existing);
|
||||
const deleteConfigs = difference(existing, defined);
|
||||
|
||||
if (!newConfigs.length && !deleteConfigs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found runtime config changes, upgrading...`);
|
||||
const acquired = await this.cache.setnx('runtime:upgrade', 1, {
|
||||
ttl: 10 * 60 * 1000,
|
||||
});
|
||||
await using _ = defer(async () => {
|
||||
await this.cache.delete('runtime:upgrade');
|
||||
});
|
||||
|
||||
if (acquired) {
|
||||
for (const key of newConfigs) {
|
||||
await this.db.runtimeConfig.upsert({
|
||||
create: defaultRuntimeConfig[key],
|
||||
// old deleted setting should be restored
|
||||
update: {
|
||||
...defaultRuntimeConfig[key],
|
||||
deletedAt: null,
|
||||
},
|
||||
where: {
|
||||
id: key,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.runtimeConfig.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: deleteConfigs,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('Upgrade completed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user