From a72ce0a9449e2225fe1e4c072a5827f133dab820 Mon Sep 17 00:00:00 2001 From: geasd <15225349+gexs04812@user.noreply.gitee.com> Date: Mon, 10 Aug 2026 11:08:56 +0800 Subject: [PATCH 1/3] feat(reminder): wire createAppServices and AppRuntime Compose mock reminder ports and coordinate runtime start/stop with rollback. Closes #177 --- .../src/app/composition/createAppServices.ts | 62 +++++++++++++++++++ frontend/src/app/orchestration/AppRuntime.ts | 38 ++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frontend/src/app/composition/createAppServices.ts create mode 100644 frontend/src/app/orchestration/AppRuntime.ts diff --git a/frontend/src/app/composition/createAppServices.ts b/frontend/src/app/composition/createAppServices.ts new file mode 100644 index 0000000..1e4b20f --- /dev/null +++ b/frontend/src/app/composition/createAppServices.ts @@ -0,0 +1,62 @@ +import { AppRuntime } from '../orchestration/AppRuntime'; +import type { + ReminderApplicationDependencies, + ReminderApplicationPort, +} from '../../features/reminder/application/interfaces'; +import { + MockLocalScheduleReader, + MockReminderApplication, + MockReminderDispositionSync, + MockReminderStateStore, +} from '../../features/reminder/data/local'; +import { MockAudioPlayback } from '../../infrastructure/audio'; +import { MockLocationMonitor } from '../../infrastructure/location'; +import { + MockAlarmScheduler, + MockDeviceCapability, + MockPopup, + MockReminderRecovery, + MockReminderDelivery, + MockSystemNotification, + MockVibration, +} from '../../infrastructure/notifications'; +import { MockTimeListener } from '../../shared/time'; +import { MockReminderPresenter } from '../../features/reminder/presentation'; + +export type AppServices = { + runtime: AppRuntime; + reminder: ReminderApplicationPort; + reminderPorts: ReminderApplicationDependencies; +}; + +/** 提醒端口的组合根;当前所有具体适配器都是确定性的模拟实现。 */ +export function createAppServices(): AppServices { + const reminderPorts: ReminderApplicationDependencies = { + schedules: new MockLocalScheduleReader(), + time: new MockTimeListener(), + location: new MockLocationMonitor(), + alarms: new MockAlarmScheduler(), + delivery: new MockReminderDelivery(), + audio: new MockAudioPlayback(), + device: new MockDeviceCapability(), + presenter: new MockReminderPresenter(), + systemNotification: new MockSystemNotification(), + popup: new MockPopup(), + vibration: new MockVibration(), + recovery: new MockReminderRecovery(), + state: new MockReminderStateStore(), + dispositionSync: new MockReminderDispositionSync(), + }; + const reminder = new MockReminderApplication(reminderPorts); + + return { + runtime: new AppRuntime([ + { + start: () => reminder.start(), + stop: () => reminder.stop(), + }, + ]), + reminder, + reminderPorts, + }; +} diff --git a/frontend/src/app/orchestration/AppRuntime.ts b/frontend/src/app/orchestration/AppRuntime.ts new file mode 100644 index 0000000..e8c7246 --- /dev/null +++ b/frontend/src/app/orchestration/AppRuntime.ts @@ -0,0 +1,38 @@ +export type RuntimeModule = { + start(): Promise | void; + stop(): Promise | void; +}; + +/** 协调应用生命周期模块,不在应用层放置业务规则。 */ +export class AppRuntime { + private started = false; + + constructor(private readonly modules: readonly RuntimeModule[] = []) {} + + async start(): Promise { + if (this.started) return; + + const startedModules: RuntimeModule[] = []; + try { + for (const module of this.modules) { + await module.start(); + startedModules.push(module); + } + this.started = true; + } catch (error) { + for (const module of [...startedModules].reverse()) { + await module.stop(); + } + throw error; + } + } + + async stop(): Promise { + if (!this.started) return; + + for (const module of [...this.modules].reverse()) { + await module.stop(); + } + this.started = false; + } +} From 1d027e2fd81f495fbcc9fa72dca6bc530be4e9fd Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 11 Aug 2026 16:35:39 +0800 Subject: [PATCH 2/3] ci(reminder): retrigger checks after marking PR ready From b8abf49150f48d200d96deddc44590f055c337ad Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 11 Aug 2026 16:43:54 +0800 Subject: [PATCH 3/3] fix(app): serialize AppRuntime lifecycle and best-effort stop Queue start/stop on a shared promise chain, and always attempt to stop every started module on rollback or shutdown while preserving/aggregating errors. --- frontend/src/app/orchestration/AppRuntime.ts | 75 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/orchestration/AppRuntime.ts b/frontend/src/app/orchestration/AppRuntime.ts index e8c7246..86978b8 100644 --- a/frontend/src/app/orchestration/AppRuntime.ts +++ b/frontend/src/app/orchestration/AppRuntime.ts @@ -6,10 +6,30 @@ export type RuntimeModule = { /** 协调应用生命周期模块,不在应用层放置业务规则。 */ export class AppRuntime { private started = false; + /** 串行化 start/stop,避免并发重入与半开状态。 */ + private lifecycle: Promise = Promise.resolve(); constructor(private readonly modules: readonly RuntimeModule[] = []) {} async start(): Promise { + const run = this.lifecycle.then(() => this.startInternal()); + this.lifecycle = run.then( + () => undefined, + () => undefined, + ); + return run; + } + + async stop(): Promise { + const run = this.lifecycle.then(() => this.stopInternal()); + this.lifecycle = run.then( + () => undefined, + () => undefined, + ); + return run; + } + + private async startInternal(): Promise { if (this.started) return; const startedModules: RuntimeModule[] = []; @@ -20,19 +40,60 @@ export class AppRuntime { } this.started = true; } catch (error) { - for (const module of [...startedModules].reverse()) { - await module.stop(); - } - throw error; + const cleanupErrors = await stopAllBestEffort([...startedModules].reverse()); + throw withCleanupErrors(error, cleanupErrors, 'AppRuntime start failed'); } } - async stop(): Promise { + private async stopInternal(): Promise { if (!this.started) return; - for (const module of [...this.modules].reverse()) { + const stopErrors = await stopAllBestEffort([...this.modules].reverse()); + // 无论个别 stop 是否失败,都视为已退出 started,避免半关闭后重试重复 stop。 + this.started = false; + if (stopErrors.length > 0) { + throw withCleanupErrors(undefined, stopErrors, 'AppRuntime stop failed'); + } + } +} + +async function stopAllBestEffort(modules: readonly RuntimeModule[]): Promise { + const errors: unknown[] = []; + for (const module of modules) { + try { await module.stop(); + } catch (error) { + errors.push(error); } - this.started = false; } + return errors; +} + +function withCleanupErrors( + primary: unknown | undefined, + cleanupErrors: readonly unknown[], + message: string, +): Error { + if (cleanupErrors.length === 0) { + return primary instanceof Error ? primary : new Error(String(primary)); + } + + const parts: unknown[] = []; + if (primary !== undefined) { + parts.push(primary); + } + parts.push(...cleanupErrors); + + if (typeof AggregateError === 'function') { + return new AggregateError(parts, message); + } + + const detail = parts + .map((error) => (error instanceof Error ? error.message : String(error))) + .join('; '); + const fallback = new Error(`${message}: ${detail}`); + if (primary instanceof Error) { + fallback.cause = primary; + } + return fallback; }