From f034454ec9e9ff24e8ab5364a409237a9ac244dd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 12 Apr 2026 18:36:21 +0300 Subject: [PATCH] refactor(backup): constructor-based dependency injection for options --- .../src/lightweight/backup_provider.ts | 6 ++- .../src/local-server-worker.ts | 2 +- apps/client-standalone/src/test_setup.ts | 4 +- apps/desktop/src/main.ts | 4 +- apps/server/spec/setup.ts | 4 +- apps/server/src/backup_provider.ts | 6 ++- apps/server/src/main.ts | 4 +- packages/trilium-core/src/index.ts | 4 +- packages/trilium-core/src/services/backup.ts | 45 +++++++------------ 9 files changed, 36 insertions(+), 43 deletions(-) diff --git a/apps/client-standalone/src/lightweight/backup_provider.ts b/apps/client-standalone/src/lightweight/backup_provider.ts index d29bf77d8a..2e5c9464bc 100644 --- a/apps/client-standalone/src/lightweight/backup_provider.ts +++ b/apps/client-standalone/src/lightweight/backup_provider.ts @@ -1,5 +1,5 @@ import type { DatabaseBackup } from "@triliumnext/commons"; -import { BackupService, getSql } from "@triliumnext/core"; +import { BackupOptionsService, BackupService, getSql } from "@triliumnext/core"; const BACKUP_DIR_NAME = "backups"; const BACKUP_FILE_PATTERN = /^backup-.*\.db$/; @@ -13,6 +13,10 @@ export default class StandaloneBackupService extends BackupService { private backupDir: FileSystemDirectoryHandle | null = null; private opfsAvailable: boolean | null = null; + constructor(getOptions: () => BackupOptionsService) { + super(getOptions); + } + private isOpfsAvailable(): boolean { if (this.opfsAvailable === null) { this.opfsAvailable = typeof navigator !== "undefined" diff --git a/apps/client-standalone/src/local-server-worker.ts b/apps/client-standalone/src/local-server-worker.ts index 08fd86bb7b..6d49db3d1d 100644 --- a/apps/client-standalone/src/local-server-worker.ts +++ b/apps/client-standalone/src/local-server-worker.ts @@ -176,7 +176,7 @@ async function initialize(): Promise { request: new FetchRequestProvider(), platform: new StandalonePlatformProvider(queryString), log: logService, - backup: new StandaloneBackupService(), + backup: new StandaloneBackupService(() => coreModule!.options), translations: translationProvider, schema: schemaModule.default, getDemoArchive: async () => { diff --git a/apps/client-standalone/src/test_setup.ts b/apps/client-standalone/src/test_setup.ts index 1191d785ab..47d087a5de 100644 --- a/apps/client-standalone/src/test_setup.ts +++ b/apps/client-standalone/src/test_setup.ts @@ -2,7 +2,7 @@ import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { initializeCore } from "@triliumnext/core"; +import { initializeCore, options } from "@triliumnext/core"; import schemaSql from "@triliumnext/core/src/assets/schema.sql?raw"; import HappyDomHtmlParser from "happy-dom/lib/html-parser/HTMLParser.js"; import serverEnTranslations from "../../server/src/assets/translations/en/server.json"; @@ -130,7 +130,7 @@ beforeAll(async () => { }); }, platform: new StandalonePlatformProvider(""), - backup: new StandaloneBackupService(), + backup: new StandaloneBackupService(() => options), schema: schemaSql, dbConfig: { provider: sqlProvider, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index db14d63645..69830488cd 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -4,7 +4,7 @@ import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js"; import { loadCoreSchema } from "@triliumnext/server/src/core_assets.js"; import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js"; import dataDirs from "@triliumnext/server/src/services/data_dir.js"; -import options from "@triliumnext/server/src/services/options.js"; +import { options } from "@triliumnext/core"; import port from "@triliumnext/server/src/services/port.js"; import NodeRequestProvider from "@triliumnext/server/src/services/request.js"; import { RESOURCE_DIR } from "@triliumnext/server/src/services/resource_dir.js"; @@ -151,7 +151,7 @@ async function main() { // both source and bundled-production modes. getDemoArchive: async () => fs.readFileSync(path.join(RESOURCE_DIR, "db", "demo.zip")), inAppHelp: new NodejsInAppHelpProvider(), - backup: new ServerBackupService(), + backup: new ServerBackupService(() => options), image: (await import("@triliumnext/server/src/services/image_provider.js")).serverImageProvider, extraAppInfo: { nodeVersion: process.version, diff --git a/apps/server/spec/setup.ts b/apps/server/spec/setup.ts index 77dcf98871..ba449361ac 100644 --- a/apps/server/spec/setup.ts +++ b/apps/server/spec/setup.ts @@ -1,7 +1,7 @@ import { beforeAll } from "vitest"; import { readFileSync } from "fs"; import { join } from "path"; -import { initializeCore } from "@triliumnext/core"; +import { initializeCore, options } from "@triliumnext/core"; import { serverZipExportProviderFactory } from "../src/services/export/zip/factory.js"; import ServerBackupService from "../src/backup_provider.js"; import ClsHookedExecutionContext from "../src/cls_provider.js"; @@ -44,6 +44,6 @@ beforeAll(async () => { platform: new ServerPlatformProvider(), translations: initializeTranslationsWithParams, inAppHelp: new NodejsInAppHelpProvider(), - backup: new ServerBackupService() + backup: new ServerBackupService(() => options) }); }); diff --git a/apps/server/src/backup_provider.ts b/apps/server/src/backup_provider.ts index 3af14dceec..08a8d86712 100644 --- a/apps/server/src/backup_provider.ts +++ b/apps/server/src/backup_provider.ts @@ -1,5 +1,5 @@ import type { DatabaseBackup } from "@triliumnext/commons"; -import { BackupService, sync_mutex as syncMutexService } from "@triliumnext/core"; +import { BackupOptionsService, BackupService, sync_mutex as syncMutexService } from "@triliumnext/core"; import fs from "fs"; import path from "path"; @@ -8,6 +8,10 @@ import log from "./services/log.js"; import sql from "./services/sql.js"; export default class ServerBackupService extends BackupService { + constructor(getOptions: () => BackupOptionsService) { + super(getOptions); + } + override getExistingBackups(): DatabaseBackup[] { if (!fs.existsSync(dataDir.BACKUP_DIR)) { return []; diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index ac3b1624fc..b83e529513 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -3,7 +3,7 @@ * are loaded later and will result in an empty string. */ -import { getLog, initializeCore, sql_init } from "@triliumnext/core"; +import { getLog, initializeCore, options, sql_init } from "@triliumnext/core"; import fs from "fs"; import { t } from "i18next"; import path from "path"; @@ -81,7 +81,7 @@ async function startApplication() { // both source and bundled-production modes. getDemoArchive: async () => fs.readFileSync(path.join(RESOURCE_DIR, "db", "demo.zip")), inAppHelp: new NodejsInAppHelpProvider(), - backup: new ServerBackupService(), + backup: new ServerBackupService(() => options), image: (await import("./services/image_provider.js")).serverImageProvider, extraAppInfo: { nodeVersion: process.version, diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index dfb4bf4bf3..4520c5998a 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -1,7 +1,7 @@ import { ExecutionContext, initContext } from "./services/context"; import { CryptoProvider, initCrypto } from "./services/encryption/crypto"; import LogService, { getLog, initLog } from "./services/log"; -import BackupService, { initBackup } from "./services/backup"; +import BackupService, { initBackup, type BackupOptionsService } from "./services/backup"; import { initSql } from "./services/sql/index"; import { SqlService, SqlServiceParams } from "./services/sql/sql"; import { initMessaging, MessagingProvider } from "./services/messaging/index"; @@ -17,7 +17,7 @@ import { type ImageProvider, initImageProvider } from "./services/image_provider export { default as LogService, getLog } from "./services/log"; export { default as FileBasedLogService, type LogFileInfo } from "./services/file_based_log"; -export { default as BackupService, getBackup, initBackup } from "./services/backup"; +export { default as BackupService, getBackup, initBackup, type BackupOptionsService } from "./services/backup"; export type * from "./services/sql/types"; export * from "./services/sql/index"; export { default as sql_init } from "./services/sql_init"; diff --git a/packages/trilium-core/src/services/backup.ts b/packages/trilium-core/src/services/backup.ts index 9323134aef..ceda255334 100644 --- a/packages/trilium-core/src/services/backup.ts +++ b/packages/trilium-core/src/services/backup.ts @@ -1,17 +1,13 @@ -import type { DatabaseBackup, OptionNames } from "@triliumnext/commons"; +import type { DatabaseBackup, FilterOptionsByType, OptionNames } from "@triliumnext/commons"; import { getContext } from "./context.js"; import dateUtils from "./utils/date.js"; type BackupType = "daily" | "weekly" | "monthly"; -// Lazy-loaded to avoid circular dependency (options -> becca -> entities) -let optionsModule: Awaited["default"] | null = null; - -async function getOptions() { - if (!optionsModule) { - optionsModule = (await import("./options.js")).default; - } - return optionsModule!; +export interface BackupOptionsService { + getOption(name: OptionNames): string; + getOptionBool(name: FilterOptionsByType): boolean; + setOption(name: OptionNames, value: string): void; } /** @@ -19,6 +15,8 @@ async function getOptions() { * Platform-specific implementations must extend this class. */ export default abstract class BackupService { + constructor(protected readonly getOptions: () => BackupOptionsService) {} + /** * Create a backup with the given name. * Returns the backup file path/name. @@ -32,7 +30,6 @@ export default abstract class BackupService { */ regularBackup(): void { getContext().init(() => { - // Fire and forget - the async work runs in background this.runScheduledBackups().catch(err => { console.error("[Backup] Error running scheduled backups:", err); }); @@ -46,7 +43,6 @@ export default abstract class BackupService { /** * Run the scheduled backup checks for daily, weekly, and monthly backups. - * Can be overridden by subclasses if they need custom behavior. */ protected async runScheduledBackups(): Promise { await this.periodBackup("lastDailyBackupDate", "daily", 24 * 3600); @@ -57,22 +53,13 @@ export default abstract class BackupService { /** * Check if a specific backup type is enabled via options. */ - protected async isBackupEnabled(backupType: BackupType): Promise { - const options = await getOptions(); - let optionName: OptionNames; - switch (backupType) { - case "daily": - optionName = "dailyBackupEnabled"; - break; - case "weekly": - optionName = "weeklyBackupEnabled"; - break; - case "monthly": - optionName = "monthlyBackupEnabled"; - break; - } + protected isBackupEnabled(backupType: BackupType): boolean { + const optionName: FilterOptionsByType = + backupType === "daily" ? "dailyBackupEnabled" : + backupType === "weekly" ? "weeklyBackupEnabled" : + "monthlyBackupEnabled"; - return options.getOptionBool(optionName); + return this.getOptions().getOptionBool(optionName); } /** @@ -83,12 +70,11 @@ export default abstract class BackupService { backupType: BackupType, periodInSeconds: number ): Promise { - if (!(await this.isBackupEnabled(backupType))) { + if (!this.isBackupEnabled(backupType)) { return; } - const options = await getOptions(); - + const options = this.getOptions(); const now = new Date(); const lastBackupDate = dateUtils.parseDateTime(options.getOption(optionName)); @@ -103,7 +89,6 @@ let backupService: BackupService | undefined; /** * Get the current backup service instance. - * Throws if no provider has been initialized. */ export function getBackup(): BackupService { if (!backupService) {