Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions frontend/src/app/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ExpoAudioPlayback } from '../features/assistant/data/audio/ExpoAudioPla
import { AuthenticatedVoiceTransport } from '../features/assistant/data/websocket/AuthenticatedVoiceTransport';
import { LocalScheduleWriter } from '../features/assistant/data/local/LocalScheduleWriter';
import { useAuth } from '../features/auth/presentation/AuthProvider';
import type { SqliteLocalScheduleReader, SqliteReminderStateStore } from '../features/reminder';
import { SqliteScheduleClientService } from '../features/schedule/application';
import type { ScheduleLocalRepository } from '../features/schedule/data';
import { RNAppStateProvider } from '../infrastructure/appState/RNAppStateProvider';
Expand All @@ -34,6 +35,8 @@ export function AppRoot({ services: providedServices }: { services?: AppServices
>
<AuthRoute
protectedClient={services.protectedClient}
reminderState={services.reminderState}
scheduleReader={services.schedules}
webSocketClient={services.webSocketClient}
/>
</AppProviders>
Expand All @@ -42,9 +45,13 @@ export function AppRoot({ services: providedServices }: { services?: AppServices

function AuthRoute({
protectedClient,
reminderState,
scheduleReader,
webSocketClient,
}: {
readonly protectedClient: ApiRequest;
readonly reminderState: SqliteReminderStateStore;
readonly scheduleReader: SqliteLocalScheduleReader;
readonly webSocketClient: AuthenticatedWebSocketClient;
}) {
const { retryInitialization, viewState } = useAuth();
Expand Down Expand Up @@ -78,6 +85,8 @@ function AuthRoute({
accountId={viewState.accountId}
key={viewState.accountId}
protectedClient={protectedClient}
reminderState={reminderState}
scheduleReader={scheduleReader}
username={viewState.username}
webSocketClient={webSocketClient}
/>
Expand All @@ -98,11 +107,15 @@ type ScheduleLoadState =
function AuthenticatedScheduleRoute({
accountId,
protectedClient,
reminderState,
scheduleReader,
username,
webSocketClient,
}: {
readonly accountId: string;
readonly protectedClient: ApiRequest;
readonly reminderState: SqliteReminderStateStore;
readonly scheduleReader: SqliteLocalScheduleReader;
readonly username: string;
readonly webSocketClient: AuthenticatedWebSocketClient;
}) {
Expand Down Expand Up @@ -158,6 +171,21 @@ function AuthenticatedScheduleRoute({
[currentLoadState],
);

// The reminder runtime starts as soon as authentication succeeds, before SQLite may be ready.
// Bind its stable adapters to the active account/repository, then refresh the running engine.
useEffect(() => {
if (currentLoadState?.status !== 'ready') {
return;
}
reminderState.attach(currentLoadState.repository, accountId);
scheduleReader.attach(currentLoadState.repository, accountId);
void scheduleReader.refresh();
return () => {
scheduleReader.detach();
reminderState.detach();
};
}, [accountId, currentLoadState, reminderState, scheduleReader]);

// 语音这条连接复用应用唯一的 AuthenticatedWebSocketClient——握手、鉴权失效、
// 断线通知都由它统一处理,这里不再单独持有 access_token/device_id/wsUrl。
// 按住说话和免提通话共用同一批 capture/playback/location/appState 端口
Expand All @@ -174,11 +202,11 @@ function AuthenticatedScheduleRoute({
return {
appState: new RNAppStateProvider(),
capture: new ExpoAudioCapture(),
localScheduleWriter: new LocalScheduleWriter(currentLoadState.repository),
localScheduleWriter: new LocalScheduleWriter(currentLoadState.repository, scheduleReader),
location: new ExpoLocationProvider(),
playback: new ExpoAudioPlayback(),
};
}, [currentLoadState]);
}, [currentLoadState, scheduleReader]);

const pushToTalkApplication = useMemo(() => {
if (assistantDependencies === null) {
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/app/composition/createAppServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import type {
} from '../../features/reminder/application/interfaces';
import { LocalReminderApplication } from '../../features/reminder/application';
import {
MockLocalScheduleReader,
MockReminderDispositionSync,
MockReminderStateStore,
LocalReminderDispositionSync,
SqliteLocalScheduleReader,
SqliteReminderStateStore,
} from '../../features/reminder/data/local';
import { ExpoAudioPlayback } from '../../infrastructure/audio';
import { ExpoLocationMonitor } from '../../infrastructure/location';
Expand All @@ -31,7 +31,9 @@ export type AppServices = {
runtime: AppRuntime;
reminder: ReminderApplicationPort;
reminderPorts: ReminderApplicationDependencies;
reminderState: SqliteReminderStateStore;
scheduleView: ScheduleViewStore;
schedules: SqliteLocalScheduleReader;
webSocketClient: AuthRuntime['webSocketClient'];
};

Expand All @@ -42,8 +44,10 @@ export interface CreateAppServicesOptions {
/** 应用唯一组合根:认证传输、功能服务、生命周期和账号内存清理在此接线。 */
export function createAppServices(options: CreateAppServicesOptions = {}): AppServices {
const auth = createAuthRuntime(options.auth);
const schedules = new SqliteLocalScheduleReader();
const reminderState = new SqliteReminderStateStore();
const reminderPorts: ReminderApplicationDependencies = {
schedules: new MockLocalScheduleReader(),
schedules,
time: new IntervalTimeListener(),
location: new ExpoLocationMonitor(),
alarms: new NativeAlarmScheduler(),
Expand All @@ -55,8 +59,8 @@ export function createAppServices(options: CreateAppServicesOptions = {}): AppSe
popup: new MockPopup(),
vibration: new MockVibration(),
recovery: new MockReminderRecovery(),
state: new MockReminderStateStore(),
dispositionSync: new MockReminderDispositionSync(),
state: reminderState,
dispositionSync: new LocalReminderDispositionSync(),
};
const reminder = new LocalReminderApplication(reminderPorts);
const scheduleView = new ScheduleViewStore();
Expand All @@ -76,7 +80,9 @@ export function createAppServices(options: CreateAppServicesOptions = {}): AppSe
runtime,
reminder,
reminderPorts,
reminderState,
scheduleView,
schedules,
webSocketClient: auth.webSocketClient,
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SCHEDULE_CATEGORIES, type ScheduleCategory } from '../../../../contracts/schedule';
import type { SqliteLocalScheduleReader } from '../../../reminder';
import type {
CloudScheduleRow,
LocalScheduleOccurrenceOverrideRow,
Expand Down Expand Up @@ -33,7 +34,10 @@ import type { AppliedCommand, AppliedOccurrenceOverride } from '../../domain/Con
* message.ack,不会向服务端谎报已落库、也不会在本地留下半吊子状态。
*/
export class LocalScheduleWriter implements LocalScheduleWriterPort {
public constructor(private readonly repository: ScheduleLocalRepository) {}
public constructor(
private readonly repository: ScheduleLocalRepository,
private readonly scheduleReader?: SqliteLocalScheduleReader,
) {}

public async applyCommandResult(accountId: string, command: AppliedCommand): Promise<void> {
if (command.status !== 'applied' || command.operation === 'list_schedules') {
Expand All @@ -60,6 +64,9 @@ export class LocalScheduleWriter implements LocalScheduleWriterPort {
}
}
});
// 提醒引擎读的是 SqliteLocalScheduleReader 的投影,不是这个仓储本身;写完
// 必须主动刷新一次,不然新建/改动的日程要等下次 rebuild 才会被提醒引擎看到。
await this.scheduleReader?.refresh();
}

public applyCategoryUpdate(
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/features/reminder/data/local/LocalReminderAdapters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type {
PopupPort,
PopupReceipt,
PopupRequest,
ReminderDeliveryPort,
ReminderDeliveryReceipt,
ReminderDeliveryRequest,
ReminderDispositionSyncPort,
ReminderDispositionSyncReceipt,
ReminderRecoveryPort,
ReminderRecoveryReceipt,
ReminderConfirmedDisposition,
} from '../../application/interfaces';

/** 送达记账:展示由 systemNotification / presenter 完成,此处只返回回执。 */
export class LocalReminderDelivery implements ReminderDeliveryPort {
async deliver(request: ReminderDeliveryRequest): Promise<ReminderDeliveryReceipt> {
return {
delivery_id: `delivery-${request.schedule_id}-${Date.now()}`,
schedule_id: request.schedule_id,
delivered_at: new Date().toISOString(),
channels: [],
used_fallback_audio: false,
};
}

async dismiss(_scheduleId: string): Promise<void> {
return Promise.resolve();
}
}

/** 弹窗通道占位:提醒页由 ReminderPresenter 承接。 */
export class NoopPopup implements PopupPort {
async show(request: PopupRequest): Promise<PopupReceipt> {
return { popup_id: request.popup_id, visible: false };
}

async dismiss(_popupId: string): Promise<void> {
return Promise.resolve();
}
}

/** 重启恢复占位:后续可接开机广播 / 精确闹钟重挂。 */
export class LocalReminderRecovery implements ReminderRecoveryPort {
async registerForRestart(): Promise<ReminderRecoveryReceipt> {
return { registered: true, recovery_id: `recovery-${Date.now()}` };
}

async restoreAfterRestart(): Promise<ReminderRecoveryReceipt> {
return { registered: true, recovery_id: `recovery-${Date.now()}` };
}
}

/** 确认态本地受理:无网络时也返回 accepted,供后续 sync 替换。 */
export class LocalReminderDispositionSync implements ReminderDispositionSyncPort {
async submitConfirmed(
disposition: ReminderConfirmedDisposition,
): Promise<ReminderDispositionSyncReceipt> {
return {
schedule_id: disposition.schedule_id,
accepted: true,
};
}
}

This file was deleted.

105 changes: 0 additions & 105 deletions frontend/src/features/reminder/data/local/MockReminderApplication.ts

This file was deleted.

This file was deleted.

Loading
Loading