You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
memos/web/src/contexts/InstanceContext.tsx

174 lines
6.2 KiB
TypeScript

import { create } from "@bufbuild/protobuf";
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
import { instanceServiceClient } from "@/connect";
import {
InstanceProfile,
InstanceProfileSchema,
InstanceSetting,
InstanceSetting_AISetting,
InstanceSetting_AISettingSchema,
InstanceSetting_GeneralSetting,
InstanceSetting_GeneralSettingSchema,
InstanceSetting_Key,
InstanceSetting_MemoRelatedSetting,
InstanceSetting_MemoRelatedSettingSchema,
InstanceSetting_StorageSetting,
InstanceSetting_StorageSettingSchema,
InstanceSetting_TagsSetting,
InstanceSetting_TagsSettingSchema,
} from "@/types/proto/api/v1/instance_service_pb";
const instanceSettingNamePrefix = "instance/settings/";
const buildInstanceSettingName = (key: InstanceSetting_Key): string => {
const keyName = InstanceSetting_Key[key];
return `${instanceSettingNamePrefix}${keyName}`;
};
interface InstanceState {
profile: InstanceProfile;
settings: InstanceSetting[];
isInitialized: boolean;
isLoading: boolean;
// True only when the profile was successfully fetched from the server.
// Remains false if initialization failed, so consumers can distinguish
// "no admin exists" from "failed to load profile".
profileLoaded: boolean;
}
interface InstanceContextValue extends InstanceState {
generalSetting: InstanceSetting_GeneralSetting;
memoRelatedSetting: InstanceSetting_MemoRelatedSetting;
storageSetting: InstanceSetting_StorageSetting;
tagsSetting: InstanceSetting_TagsSetting;
aiSetting: InstanceSetting_AISetting;
initialize: () => Promise<void>;
fetchSetting: (key: InstanceSetting_Key) => Promise<void>;
updateSetting: (setting: InstanceSetting) => Promise<void>;
}
const InstanceContext = createContext<InstanceContextValue | null>(null);
export function InstanceProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<InstanceState>({
profile: create(InstanceProfileSchema, {}),
settings: [],
isInitialized: false,
isLoading: true,
profileLoaded: false,
});
// Memoize derived settings to prevent unnecessary recalculations
const generalSetting = useMemo((): InstanceSetting_GeneralSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}GENERAL`);
if (setting?.value.case === "generalSetting") {
return setting.value.value;
}
return create(InstanceSetting_GeneralSettingSchema, {});
}, [state.settings]);
const memoRelatedSetting = useMemo((): InstanceSetting_MemoRelatedSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}MEMO_RELATED`);
if (setting?.value.case === "memoRelatedSetting") {
return setting.value.value;
}
return create(InstanceSetting_MemoRelatedSettingSchema, {});
}, [state.settings]);
const storageSetting = useMemo((): InstanceSetting_StorageSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}STORAGE`);
if (setting?.value.case === "storageSetting") {
return setting.value.value;
}
return create(InstanceSetting_StorageSettingSchema, {});
}, [state.settings]);
const tagsSetting = useMemo((): InstanceSetting_TagsSetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}TAGS`);
if (setting?.value.case === "tagsSetting") {
return setting.value.value;
}
return create(InstanceSetting_TagsSettingSchema, {});
}, [state.settings]);
const aiSetting = useMemo((): InstanceSetting_AISetting => {
const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}AI`);
if (setting?.value.case === "aiSetting") {
return setting.value.value;
}
return create(InstanceSetting_AISettingSchema, {});
}, [state.settings]);
const initialize = useCallback(async () => {
setState((prev) => ({ ...prev, isLoading: true }));
try {
const profile = await instanceServiceClient.getInstanceProfile({});
const [generalSetting, memoRelatedSettingResponse, tagsSettingResponse] = await Promise.all([
instanceServiceClient.getInstanceSetting({ name: buildInstanceSettingName(InstanceSetting_Key.GENERAL) }),
instanceServiceClient.getInstanceSetting({ name: buildInstanceSettingName(InstanceSetting_Key.MEMO_RELATED) }),
instanceServiceClient.getInstanceSetting({ name: buildInstanceSettingName(InstanceSetting_Key.TAGS) }),
]);
setState({
profile,
settings: [generalSetting, memoRelatedSettingResponse, tagsSettingResponse],
isInitialized: true,
isLoading: false,
profileLoaded: true,
});
} catch (error) {
console.error("Failed to initialize instance:", error);
setState((prev) => ({
...prev,
isInitialized: true,
isLoading: false,
}));
}
}, []);
const fetchSetting = useCallback(async (key: InstanceSetting_Key) => {
const setting = await instanceServiceClient.getInstanceSetting({
name: buildInstanceSettingName(key),
});
setState((prev) => ({
...prev,
settings: [...prev.settings.filter((s) => s.name !== setting.name), setting],
}));
}, []);
const updateSetting = useCallback(async (setting: InstanceSetting) => {
await instanceServiceClient.updateInstanceSetting({ setting });
setState((prev) => ({
...prev,
settings: [...prev.settings.filter((s) => s.name !== setting.name), setting],
}));
}, []);
// Memoize context value to prevent unnecessary re-renders of consumers
const value = useMemo(
() => ({
...state,
generalSetting,
memoRelatedSetting,
storageSetting,
tagsSetting,
aiSetting,
initialize,
fetchSetting,
updateSetting,
}),
[state, generalSetting, memoRelatedSetting, storageSetting, tagsSetting, aiSetting, initialize, fetchSetting, updateSetting],
);
return <InstanceContext.Provider value={value}>{children}</InstanceContext.Provider>;
}
export function useInstance() {
const context = useContext(InstanceContext);
if (!context) {
throw new Error("useInstance must be used within InstanceProvider");
}
return context;
}