refactor progress
This commit is contained in:
250
packages/api/src/instance/InstanceConfigRepository.tsx
Normal file
250
packages/api/src/instance/InstanceConfigRepository.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Config} from '@fluxer/api/src/Config';
|
||||
import {sanitizeLimitConfigForInstance} from '@fluxer/api/src/constants/LimitConfig';
|
||||
import {fetchMany, fetchOne, upsertOne} from '@fluxer/api/src/database/Cassandra';
|
||||
import type {InstanceConfigurationRow} from '@fluxer/api/src/database/types/InstanceConfigTypes';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
import {InstanceConfiguration} from '@fluxer/api/src/Tables';
|
||||
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
|
||||
|
||||
const FETCH_CONFIG_QUERY = InstanceConfiguration.selectCql({
|
||||
where: InstanceConfiguration.where.eq('key'),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const FETCH_ALL_CONFIG_QUERY = InstanceConfiguration.selectCql();
|
||||
|
||||
export interface InstanceConfig {
|
||||
manualReviewEnabled: boolean;
|
||||
manualReviewScheduleEnabled: boolean;
|
||||
manualReviewScheduleStartHourUtc: number;
|
||||
manualReviewScheduleEndHourUtc: number;
|
||||
registrationAlertsWebhookUrl: string | null;
|
||||
systemAlertsWebhookUrl: string | null;
|
||||
}
|
||||
|
||||
export interface InstanceSsoConfig {
|
||||
enabled: boolean;
|
||||
displayName: string | null;
|
||||
issuer: string | null;
|
||||
authorizationUrl: string | null;
|
||||
tokenUrl: string | null;
|
||||
userInfoUrl: string | null;
|
||||
jwksUrl: string | null;
|
||||
clientId: string | null;
|
||||
clientSecret?: string | null;
|
||||
clientSecretSet?: boolean;
|
||||
scope: string | null;
|
||||
allowedEmailDomains: Array<string>;
|
||||
autoProvision: boolean;
|
||||
redirectUri: string | null;
|
||||
}
|
||||
|
||||
export class InstanceConfigRepository {
|
||||
async getConfig(key: string): Promise<string | null> {
|
||||
const row = await fetchOne<InstanceConfigurationRow>(FETCH_CONFIG_QUERY, {key});
|
||||
return row?.value ?? null;
|
||||
}
|
||||
|
||||
async getAllConfigs(): Promise<Map<string, string>> {
|
||||
const rows = await fetchMany<InstanceConfigurationRow>(FETCH_ALL_CONFIG_QUERY, {});
|
||||
const configs = new Map<string, string>();
|
||||
for (const row of rows) {
|
||||
if (row.value != null) {
|
||||
configs.set(row.key, row.value);
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
async setConfig(key: string, value: string): Promise<void> {
|
||||
await upsertOne(
|
||||
InstanceConfiguration.upsertAll({
|
||||
key,
|
||||
value,
|
||||
updated_at: new Date(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getInstanceConfig(): Promise<InstanceConfig> {
|
||||
const configs = await this.getAllConfigs();
|
||||
|
||||
return {
|
||||
manualReviewEnabled: configs.get('manual_review_enabled') === 'true',
|
||||
manualReviewScheduleEnabled: configs.get('manual_review_schedule_enabled') === 'true',
|
||||
manualReviewScheduleStartHourUtc: Number.parseInt(
|
||||
configs.get('manual_review_schedule_start_hour_utc') ?? '0',
|
||||
10,
|
||||
),
|
||||
manualReviewScheduleEndHourUtc: Number.parseInt(configs.get('manual_review_schedule_end_hour_utc') ?? '23', 10),
|
||||
registrationAlertsWebhookUrl: configs.get('registration_alerts_webhook_url') ?? null,
|
||||
systemAlertsWebhookUrl: configs.get('system_alerts_webhook_url') ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async setManualReviewEnabled(enabled: boolean): Promise<void> {
|
||||
await this.setConfig('manual_review_enabled', enabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
async setManualReviewSchedule(scheduleEnabled: boolean, startHourUtc: number, endHourUtc: number): Promise<void> {
|
||||
await this.setConfig('manual_review_schedule_enabled', scheduleEnabled ? 'true' : 'false');
|
||||
await this.setConfig('manual_review_schedule_start_hour_utc', String(startHourUtc));
|
||||
await this.setConfig('manual_review_schedule_end_hour_utc', String(endHourUtc));
|
||||
}
|
||||
|
||||
async setRegistrationAlertsWebhookUrl(url: string | null): Promise<void> {
|
||||
if (url) {
|
||||
await this.setConfig('registration_alerts_webhook_url', url);
|
||||
} else {
|
||||
await this.setConfig('registration_alerts_webhook_url', '');
|
||||
}
|
||||
}
|
||||
|
||||
async setSystemAlertsWebhookUrl(url: string | null): Promise<void> {
|
||||
if (url) {
|
||||
await this.setConfig('system_alerts_webhook_url', url);
|
||||
} else {
|
||||
await this.setConfig('system_alerts_webhook_url', '');
|
||||
}
|
||||
}
|
||||
|
||||
isManualReviewActiveNow(config: InstanceConfig): boolean {
|
||||
if (!config.manualReviewEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.manualReviewScheduleEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nowUtc = new Date();
|
||||
const currentHour = nowUtc.getUTCHours();
|
||||
|
||||
const start = config.manualReviewScheduleStartHourUtc;
|
||||
const end = config.manualReviewScheduleEndHourUtc;
|
||||
|
||||
if (start <= end) {
|
||||
return currentHour >= start && currentHour <= end;
|
||||
}
|
||||
return currentHour >= start || currentHour <= end;
|
||||
}
|
||||
|
||||
async hasLimitConfig(): Promise<boolean> {
|
||||
const raw = await this.getConfig('limit_config');
|
||||
return raw !== null;
|
||||
}
|
||||
|
||||
async getLimitConfig(): Promise<LimitConfigSnapshot | null> {
|
||||
const raw = await this.getConfig('limit_config');
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: LimitConfigSnapshot = JSON.parse(raw);
|
||||
return sanitizeLimitConfigForInstance(parsed, {selfHosted: Config.instance.selfHosted});
|
||||
} catch (error) {
|
||||
Logger.warn({error}, 'Invalid limit config JSON, returning null');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setLimitConfig(config: LimitConfigSnapshot): Promise<void> {
|
||||
await this.setConfig('limit_config', JSON.stringify(config));
|
||||
}
|
||||
|
||||
async getSsoConfig(options?: {includeSecret?: boolean}): Promise<InstanceSsoConfig> {
|
||||
const configs = await this.getAllConfigs();
|
||||
|
||||
const read = (key: string): string | null => {
|
||||
const v = configs.get(key);
|
||||
if (!v) return null;
|
||||
const trimmed = v.trim();
|
||||
return trimmed.length === 0 ? null : trimmed;
|
||||
};
|
||||
|
||||
const allowedDomainsRaw = configs.get('sso_allowed_domains');
|
||||
let allowedDomains: Array<string> = [];
|
||||
if (allowedDomainsRaw) {
|
||||
try {
|
||||
const parsed = JSON.parse(allowedDomainsRaw);
|
||||
if (Array.isArray(parsed)) {
|
||||
allowedDomains = parsed.map((item) => String(item)).filter((item) => item.length > 0);
|
||||
}
|
||||
} catch {
|
||||
allowedDomains = allowedDomainsRaw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
const clientSecret = read('sso_client_secret');
|
||||
|
||||
return {
|
||||
enabled: configs.get('sso_enabled') === 'true',
|
||||
displayName: read('sso_display_name'),
|
||||
issuer: read('sso_issuer'),
|
||||
authorizationUrl: read('sso_authorization_url'),
|
||||
tokenUrl: read('sso_token_url'),
|
||||
userInfoUrl: read('sso_userinfo_url'),
|
||||
jwksUrl: read('sso_jwks_url'),
|
||||
clientId: read('sso_client_id'),
|
||||
clientSecret: options?.includeSecret ? clientSecret : undefined,
|
||||
clientSecretSet: Boolean(clientSecret),
|
||||
scope: read('sso_scope'),
|
||||
allowedEmailDomains: allowedDomains,
|
||||
autoProvision: configs.get('sso_auto_provision') !== 'false',
|
||||
redirectUri: read('sso_redirect_uri'),
|
||||
};
|
||||
}
|
||||
|
||||
async setSsoConfig(config: Partial<InstanceSsoConfig>): Promise<InstanceSsoConfig> {
|
||||
const current = await this.getSsoConfig({includeSecret: true});
|
||||
const next: InstanceSsoConfig = {
|
||||
...current,
|
||||
...config,
|
||||
clientSecret: config.clientSecret !== undefined ? config.clientSecret : current.clientSecret,
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
this.setConfig('sso_enabled', next.enabled ? 'true' : 'false'),
|
||||
this.setConfig('sso_display_name', next.displayName ?? ''),
|
||||
this.setConfig('sso_issuer', next.issuer ?? ''),
|
||||
this.setConfig('sso_authorization_url', next.authorizationUrl ?? ''),
|
||||
this.setConfig('sso_token_url', next.tokenUrl ?? ''),
|
||||
this.setConfig('sso_userinfo_url', next.userInfoUrl ?? ''),
|
||||
this.setConfig('sso_jwks_url', next.jwksUrl ?? ''),
|
||||
this.setConfig('sso_client_id', next.clientId ?? ''),
|
||||
this.setConfig('sso_scope', next.scope ?? ''),
|
||||
this.setConfig('sso_allowed_domains', JSON.stringify(next.allowedEmailDomains ?? [])),
|
||||
this.setConfig('sso_auto_provision', next.autoProvision ? 'true' : 'false'),
|
||||
this.setConfig('sso_redirect_uri', next.redirectUri ?? ''),
|
||||
]);
|
||||
|
||||
if (config.clientSecret !== undefined) {
|
||||
await this.setConfig('sso_client_secret', config.clientSecret ?? '');
|
||||
}
|
||||
|
||||
return this.getSsoConfig({includeSecret: true});
|
||||
}
|
||||
}
|
||||
131
packages/api/src/instance/InstanceController.tsx
Normal file
131
packages/api/src/instance/InstanceController.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Config} from '@fluxer/api/src/Config';
|
||||
import {getKeyManager} from '@fluxer/api/src/federation/KeyManager';
|
||||
import type {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
|
||||
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {API_CODE_VERSION} from '@fluxer/constants/src/AppConstants';
|
||||
import {FEDERATION_PROTOCOL_VERSION} from '@fluxer/constants/src/Federation';
|
||||
import {WellKnownFluxerResponse} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import type {Hono} from 'hono';
|
||||
|
||||
export function InstanceController(app: Hono<HonoEnv>) {
|
||||
app.get(
|
||||
'/.well-known/fluxer',
|
||||
RateLimitMiddleware(RateLimitConfigs.INSTANCE_INFO),
|
||||
OpenAPI({
|
||||
operationId: 'get_well_known_fluxer',
|
||||
summary: 'Get instance discovery document',
|
||||
responseSchema: WellKnownFluxerResponse,
|
||||
statusCode: 200,
|
||||
security: [],
|
||||
tags: ['Instance'],
|
||||
description:
|
||||
'Returns the instance discovery document including API endpoints, feature flags, limits, and federation capabilities. This is the canonical discovery endpoint for all Fluxer clients.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
ctx.header('Access-Control-Allow-Origin', '*');
|
||||
|
||||
const limitConfigService = ctx.get('limitConfigService') as LimitConfigService | undefined;
|
||||
const limits = limitConfigService?.getConfigWireFormat();
|
||||
|
||||
const instanceConfigRepository = ctx.get('instanceConfigRepository') as InstanceConfigRepository | undefined;
|
||||
const instanceConfig = await instanceConfigRepository?.getInstanceConfig();
|
||||
|
||||
const apiClientEndpoint = Config.endpoints.apiClient;
|
||||
const apiPublicEndpoint = Config.endpoints.apiPublic;
|
||||
|
||||
const sso = await ctx.get('ssoService').getPublicStatus();
|
||||
|
||||
const response: Record<string, unknown> = {
|
||||
api_code_version: API_CODE_VERSION,
|
||||
endpoints: {
|
||||
api: apiClientEndpoint,
|
||||
api_client: apiClientEndpoint,
|
||||
api_public: apiPublicEndpoint,
|
||||
gateway: Config.endpoints.gateway,
|
||||
media: Config.endpoints.media,
|
||||
static_cdn: Config.endpoints.staticCdn,
|
||||
marketing: Config.endpoints.marketing,
|
||||
admin: Config.endpoints.admin,
|
||||
invite: Config.endpoints.invite,
|
||||
gift: Config.endpoints.gift,
|
||||
webapp: Config.endpoints.webApp,
|
||||
},
|
||||
captcha: {
|
||||
provider: Config.captcha.provider,
|
||||
hcaptcha_site_key: Config.captcha.hcaptcha?.siteKey ?? null,
|
||||
turnstile_site_key: Config.captcha.turnstile?.siteKey ?? null,
|
||||
},
|
||||
features: {
|
||||
sms_mfa_enabled: Config.dev.testModeEnabled || Config.sms.enabled,
|
||||
voice_enabled: Config.voice.enabled,
|
||||
stripe_enabled: Config.stripe.enabled,
|
||||
self_hosted: Config.instance.selfHosted,
|
||||
manual_review_enabled: instanceConfig?.manualReviewEnabled ?? false,
|
||||
},
|
||||
gif: {
|
||||
provider: Config.gif.provider,
|
||||
},
|
||||
sso,
|
||||
limits,
|
||||
push: {
|
||||
public_vapid_key: Config.push.publicVapidKey ?? null,
|
||||
},
|
||||
app_public: {
|
||||
sentry_dsn: Config.appPublic.sentryDsn,
|
||||
sentry_proxy_path: Config.appPublic.sentryProxyPath,
|
||||
sentry_report_host: Config.appPublic.sentryReportHost,
|
||||
sentry_project_id: Config.appPublic.sentryProjectId,
|
||||
sentry_public_key: Config.appPublic.sentryPublicKey,
|
||||
},
|
||||
};
|
||||
|
||||
if (Config.federation?.enabled) {
|
||||
const keyManager = getKeyManager();
|
||||
const instanceDomain = Config.domain.baseDomain;
|
||||
|
||||
response.federation = {
|
||||
enabled: true,
|
||||
version: FEDERATION_PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
response.public_key = {
|
||||
id: `https://${instanceDomain}/.well-known/fluxer#main-key`,
|
||||
algorithm: 'x25519' as const,
|
||||
public_key_base64: keyManager.getPublicKeyBase64(),
|
||||
};
|
||||
|
||||
response.oauth2 = {
|
||||
authorization_endpoint: `${apiClientEndpoint}/oauth2/authorize`,
|
||||
token_endpoint: `${apiClientEndpoint}/oauth2/token`,
|
||||
userinfo_endpoint: `${apiClientEndpoint}/oauth2/userinfo`,
|
||||
scopes_supported: ['identify', 'guilds', 'guilds.join', 'messages.read', 'messages.write', 'voice'],
|
||||
};
|
||||
}
|
||||
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
}
|
||||
81
packages/api/src/instance/SnowflakeReservationRepository.tsx
Normal file
81
packages/api/src/instance/SnowflakeReservationRepository.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SNOWFLAKE_RESERVATION_KEY_PREFIX} from '@fluxer/api/src/constants/InstanceConfig';
|
||||
import {deleteOneOrMany, fetchMany, upsertOne} from '@fluxer/api/src/database/Cassandra';
|
||||
import type {InstanceConfigurationRow} from '@fluxer/api/src/database/types/InstanceConfigTypes';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
import {InstanceConfiguration} from '@fluxer/api/src/Tables';
|
||||
|
||||
const FETCH_ALL_CONFIG_QUERY = InstanceConfiguration.selectCql();
|
||||
|
||||
export interface SnowflakeReservationConfig {
|
||||
emailKey: string;
|
||||
snowflake: bigint;
|
||||
updatedAt: Date | null;
|
||||
}
|
||||
|
||||
export class SnowflakeReservationRepository {
|
||||
async listReservations(): Promise<Array<SnowflakeReservationConfig>> {
|
||||
const rows = await fetchMany<InstanceConfigurationRow>(FETCH_ALL_CONFIG_QUERY, {});
|
||||
const reservations: Array<SnowflakeReservationConfig> = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row.key.startsWith(SNOWFLAKE_RESERVATION_KEY_PREFIX) || row.value == null || row.value.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const emailKey = row.key.slice(SNOWFLAKE_RESERVATION_KEY_PREFIX.length);
|
||||
if (!emailKey) continue;
|
||||
|
||||
const snowflakeString = row.value.trim();
|
||||
try {
|
||||
const snowflake = BigInt(snowflakeString);
|
||||
reservations.push({
|
||||
emailKey,
|
||||
snowflake,
|
||||
updatedAt: row.updated_at ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.warn({key: row.key, value: row.value, error}, 'Skipping invalid snowflake reservation value');
|
||||
}
|
||||
}
|
||||
|
||||
return reservations;
|
||||
}
|
||||
|
||||
async setReservation(emailKey: string, snowflake: bigint): Promise<void> {
|
||||
await upsertOne(
|
||||
InstanceConfiguration.upsertAll({
|
||||
key: `${SNOWFLAKE_RESERVATION_KEY_PREFIX}${emailKey}`,
|
||||
value: snowflake.toString(),
|
||||
updated_at: new Date(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteReservation(emailKey: string): Promise<void> {
|
||||
await deleteOneOrMany(
|
||||
InstanceConfiguration.deleteCql({
|
||||
where: InstanceConfiguration.where.eq('key'),
|
||||
}),
|
||||
{key: `${SNOWFLAKE_RESERVATION_KEY_PREFIX}${emailKey}`},
|
||||
);
|
||||
}
|
||||
}
|
||||
107
packages/api/src/instance/SnowflakeReservationService.tsx
Normal file
107
packages/api/src/instance/SnowflakeReservationService.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SNOWFLAKE_RESERVATION_REFRESH_CHANNEL} from '@fluxer/api/src/constants/InstanceConfig';
|
||||
import type {
|
||||
SnowflakeReservationConfig,
|
||||
SnowflakeReservationRepository,
|
||||
} from '@fluxer/api/src/instance/SnowflakeReservationRepository';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
import type {IKVProvider, IKVSubscription} from '@fluxer/kv_client/src/IKVProvider';
|
||||
|
||||
export class SnowflakeReservationService {
|
||||
private reservations = new Map<string, bigint>();
|
||||
private initialized = false;
|
||||
private reloadPromise: Promise<void> | null = null;
|
||||
private kvSubscription: IKVSubscription | null = null;
|
||||
|
||||
constructor(
|
||||
private repository: SnowflakeReservationRepository,
|
||||
private kvClient: IKVProvider | null,
|
||||
) {}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.reload();
|
||||
this.initialized = true;
|
||||
|
||||
if (this.kvClient) {
|
||||
try {
|
||||
const subscription = this.kvClient.duplicate();
|
||||
this.kvSubscription = subscription;
|
||||
await subscription.connect();
|
||||
await subscription.subscribe(SNOWFLAKE_RESERVATION_REFRESH_CHANNEL);
|
||||
subscription.on('message', (channel) => {
|
||||
if (channel === SNOWFLAKE_RESERVATION_REFRESH_CHANNEL) {
|
||||
this.reload().catch((error) => {
|
||||
Logger.error({error}, 'Failed to reload snowflake reservations');
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error({error}, 'Failed to subscribe to snowflake reservation refresh channel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
if (this.reloadPromise) {
|
||||
return this.reloadPromise;
|
||||
}
|
||||
|
||||
this.reloadPromise = (async () => {
|
||||
const entries = await this.repository.listReservations();
|
||||
this.reservations = this.buildLookup(entries);
|
||||
})()
|
||||
.catch((error) => {
|
||||
Logger.error({error}, 'Failed to reload snowflake reservations from the database');
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
this.reloadPromise = null;
|
||||
});
|
||||
|
||||
return this.reloadPromise;
|
||||
}
|
||||
|
||||
getReservedSnowflake(emailKey: string | null): bigint | null {
|
||||
if (!emailKey) {
|
||||
return null;
|
||||
}
|
||||
return this.reservations.get(emailKey) ?? null;
|
||||
}
|
||||
|
||||
private buildLookup(entries: Array<SnowflakeReservationConfig>): Map<string, bigint> {
|
||||
const lookup = new Map<string, bigint>();
|
||||
for (const entry of entries) {
|
||||
lookup.set(entry.emailKey, entry.snowflake);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
if (this.kvSubscription) {
|
||||
this.kvSubscription.disconnect();
|
||||
this.kvSubscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user