perf(client): defer chat persistence dependencies
This commit is contained in:
@@ -5,7 +5,10 @@ import { shallowEqual } from "@xstate/react";
|
||||
|
||||
import type { LoginStatus } from "@/data/dto/auth";
|
||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||
import { usePaymentDispatch, usePaymentSelector } from "@/stores/payment";
|
||||
import {
|
||||
usePaymentDispatch,
|
||||
usePaymentSelector,
|
||||
} from "@/stores/payment/payment-context";
|
||||
import { useUserSelector } from "@/stores/user/user-context";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { IChatRepository } from "./interfaces/ichat_repository";
|
||||
|
||||
/**
|
||||
* Loads the browser-only chat repository behind an explicit async boundary.
|
||||
*
|
||||
* The repository owns the Dexie-backed history and media stores. Keeping this
|
||||
* loader separate prevents those stores from entering every route's eager
|
||||
* client graph through shared providers.
|
||||
*/
|
||||
export async function loadChatRepository(): Promise<IChatRepository> {
|
||||
const { getChatRepository } = await import("./chat_repository");
|
||||
return getChatRepository();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ChatMediaKind } from "@/data/dto/chat";
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import { Result, type Result as ResultT } from "@/utils/result";
|
||||
|
||||
import { localChatMediaRowToBlob } from "./chat_media_blob";
|
||||
@@ -19,7 +19,8 @@ export interface CacheRemoteChatMediaBlobInput
|
||||
export async function resolveCachedChatMediaBlob(
|
||||
input: ResolveCachedChatMediaBlobInput,
|
||||
): Promise<ResultT<Blob | null>> {
|
||||
const result = await getChatRepository().getCachedMedia(input);
|
||||
const repository = await loadChatRepository();
|
||||
const result = await repository.getCachedMedia(input);
|
||||
if (Result.isErr(result)) return result;
|
||||
if (!result.data) return Result.ok(null);
|
||||
return Result.ok(localChatMediaRowToBlob(result.data));
|
||||
@@ -28,7 +29,8 @@ export async function resolveCachedChatMediaBlob(
|
||||
export async function cacheRemoteChatMediaBlob(
|
||||
input: CacheRemoteChatMediaBlobInput,
|
||||
): Promise<ResultT<Blob>> {
|
||||
const result = await getChatRepository().cacheRemoteMedia(input);
|
||||
const repository = await loadChatRepository();
|
||||
const result = await repository.cacheRemoteMedia(input);
|
||||
if (Result.isErr(result)) return result;
|
||||
const blob = localChatMediaRowToBlob(result.data);
|
||||
if (!blob) return Result.err(new Error("Cached media has no readable bytes."));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import {
|
||||
resolveChatCacheOwnerKey,
|
||||
type ChatCacheIdentityResolver,
|
||||
@@ -44,7 +44,8 @@ export async function loadSplashLatestMessagePreview({
|
||||
export async function fetchSplashLatestMessagePreview(): Promise<
|
||||
ResultT<string | null>
|
||||
> {
|
||||
const result = await getChatRepository().getHistory(1, 0);
|
||||
const repository = await loadChatRepository();
|
||||
const result = await repository.getHistory(1, 0);
|
||||
if (Result.isErr(result)) return Result.err(result.error);
|
||||
|
||||
return Result.ok(getLatestSplashMessagePreview(result.data.messages));
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ChatProvider } from "@/stores/chat/chat-context";
|
||||
import { PaymentProvider } from "@/stores/payment";
|
||||
import {
|
||||
ChatAuthSync,
|
||||
ChatPaymentSuccessSync,
|
||||
PaymentSuccessSync,
|
||||
} from "@/stores/sync";
|
||||
import { PaymentProvider } from "@/stores/payment/payment-context";
|
||||
import { ChatAuthSync } from "@/stores/sync/chat-auth-sync";
|
||||
import { ChatPaymentSuccessSync } from "@/stores/sync/chat-payment-success-sync";
|
||||
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
||||
|
||||
export function ChatRouteProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { PaymentProvider } from "@/stores/payment";
|
||||
import { PaymentSuccessSync } from "@/stores/sync";
|
||||
import { PaymentProvider } from "@/stores/payment/payment-context";
|
||||
import { PaymentSuccessSync } from "@/stores/sync/payment-success-sync";
|
||||
|
||||
export function PaymentRouteProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AppNavigationGuard } from "@/router/app-navigation-guard";
|
||||
import { AuthProvider } from "@/stores/auth/auth-context";
|
||||
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
||||
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
||||
import { UserAuthSync } from "@/stores/sync";
|
||||
import { UserAuthSync } from "@/stores/sync/user-auth-sync";
|
||||
import { UserProvider } from "@/stores/user/user-context";
|
||||
|
||||
export interface RootProvidersProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UiMessage } from "@/data/dto/chat";
|
||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
import { todayString } from "@/utils/date";
|
||||
@@ -47,7 +47,7 @@ export async function resolveHistoryCacheIdentity(): Promise<string | null> {
|
||||
export async function readLocalHistorySnapshot(
|
||||
cacheIdentity: string | null,
|
||||
): Promise<LocalHistorySnapshotOutput> {
|
||||
const chatRepo = getChatRepository();
|
||||
const chatRepo = await loadChatRepository();
|
||||
const greetingMessage = createGreetingMessage();
|
||||
|
||||
const localResult = cacheIdentity
|
||||
@@ -77,7 +77,7 @@ export async function syncNetworkHistory(
|
||||
localCount: number,
|
||||
cacheIdentity: string | null,
|
||||
): Promise<NetworkHistorySyncOutput | null> {
|
||||
const chatRepo = getChatRepository();
|
||||
const chatRepo = await loadChatRepository();
|
||||
const greetingMessage = createGreetingMessage();
|
||||
|
||||
const networkResult = await chatRepo.getHistory(CHAT_HISTORY_LIMIT, 0);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fromCallback, fromPromise } from "xstate";
|
||||
import { ExceptionHandler } from "@/core/errors";
|
||||
import { MessageQueue } from "@/core/net/message-queue";
|
||||
import type { ChatSendResponse } from "@/data/dto/chat";
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
@@ -66,7 +66,7 @@ async function sendMessageViaHttp(content: string): Promise<{
|
||||
response: ChatSendResponse;
|
||||
reply: UiMessage | null;
|
||||
}> {
|
||||
const chatRepo = getChatRepository();
|
||||
const chatRepo = await loadChatRepository();
|
||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||
const result = await chatRepo.sendMessage(content);
|
||||
if (Result.isErr(result)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fromPromise } from "xstate";
|
||||
|
||||
import { getChatRepository } from "@/data/repositories/chat_repository";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import { resolveChatCacheOwnerKey } from "@/data/repositories/chat_cache_identity";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
@@ -23,7 +23,7 @@ export interface UnlockHistoryOutput {
|
||||
}
|
||||
|
||||
export const unlockHistoryActor = fromPromise<UnlockHistoryOutput>(async () => {
|
||||
const chatRepo = getChatRepository();
|
||||
const chatRepo = await loadChatRepository();
|
||||
const unlockResult = await chatRepo.unlockHistory();
|
||||
if (Result.isErr(unlockResult)) {
|
||||
log.error("[chat-machine] unlockHistoryActor failed", {
|
||||
@@ -45,7 +45,7 @@ export const unlockMessageActor = fromPromise<
|
||||
UnlockMessageOutput,
|
||||
UnlockMessageRequest
|
||||
>(async ({ input }) => {
|
||||
const chatRepo = getChatRepository();
|
||||
const chatRepo = await loadChatRepository();
|
||||
const cacheIdentityResult = await resolveChatCacheOwnerKey();
|
||||
const unlockResult = await chatRepo.unlockPrivateMessage({
|
||||
...(input.messageId ? { messageId: input.messageId } : {}),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
/** Payment → Chat bridge, mounted only while the chat route is active. */
|
||||
import { useEffect, useRef } from "react";
|
||||
import { shallowEqual } from "@xstate/react";
|
||||
|
||||
import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session";
|
||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||
import { usePaymentSelector } from "@/stores/payment/payment-context";
|
||||
|
||||
export function ChatPaymentSuccessSync() {
|
||||
const paymentState = usePaymentSelector(
|
||||
(state) => ({
|
||||
currentOrderId: state.context.currentOrderId,
|
||||
isPaid: state.matches("paid"),
|
||||
orderStatus: state.context.orderStatus,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
const chatDispatch = useChatDispatch();
|
||||
const lastPaidKeyRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!paymentState.isPaid || !paymentState.currentOrderId) return;
|
||||
|
||||
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
|
||||
if (lastPaidKeyRef.current === paidKey) return;
|
||||
lastPaidKeyRef.current = paidKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
if (await hasPendingChatUnlock()) return;
|
||||
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
chatDispatch,
|
||||
paymentState.currentOrderId,
|
||||
paymentState.isPaid,
|
||||
paymentState.orderStatus,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./chat-auth-sync";
|
||||
export * from "./chat-payment-success-sync";
|
||||
export * from "./payment-success-sync";
|
||||
export * from "./user-auth-sync";
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { shallowEqual } from "@xstate/react";
|
||||
|
||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||
import { useUserDispatch } from "@/stores/user/user-context";
|
||||
import { hasPendingChatUnlock } from "@/lib/navigation/chat_unlock_session";
|
||||
import {
|
||||
usePaymentDispatch,
|
||||
usePaymentSelector,
|
||||
@@ -54,42 +52,3 @@ export function PaymentSuccessSync() {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Payment → Chat bridge, mounted only while the chat route is active. */
|
||||
export function ChatPaymentSuccessSync() {
|
||||
const paymentState = usePaymentSelector(
|
||||
(state) => ({
|
||||
currentOrderId: state.context.currentOrderId,
|
||||
isPaid: state.matches("paid"),
|
||||
orderStatus: state.context.orderStatus,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
const chatDispatch = useChatDispatch();
|
||||
const lastPaidKeyRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!paymentState.isPaid || !paymentState.currentOrderId) return;
|
||||
|
||||
const paidKey = `${paymentState.currentOrderId}:${paymentState.orderStatus}`;
|
||||
if (lastPaidKeyRef.current === paidKey) return;
|
||||
lastPaidKeyRef.current = paidKey;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
if (await hasPendingChatUnlock()) return;
|
||||
if (!cancelled) chatDispatch({ type: "ChatPaymentSucceeded" });
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
chatDispatch,
|
||||
paymentState.currentOrderId,
|
||||
paymentState.isPaid,
|
||||
paymentState.orderStatus,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user