import { create } from "@bufbuild/protobuf"; import { createContext, type ReactNode, useCallback, useContext, useMemo, useRef, 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_NotificationSetting, InstanceSetting_NotificationSettingSchema, 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; notificationSetting: InstanceSetting_NotificationSetting; aiSetting: InstanceSetting_AISetting; initialize: () => Promise; fetchSetting: (key: InstanceSetting_Key) => Promise; fetchSettings: (keys: InstanceSetting_Key[]) => Promise; updateSetting: (setting: InstanceSetting) => Promise; } const InstanceContext = createContext(null); export function InstanceProvider({ children }: { children: ReactNode }) { const [state, setState] = useState({ profile: create(InstanceProfileSchema, {}), settings: [], isInitialized: false, isLoading: true, profileLoaded: false, }); const fetchedSettingsRef = useRef>(new Set()); // 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 notificationSetting = useMemo((): InstanceSetting_NotificationSetting => { const setting = state.settings.find((s) => s.name === `${instanceSettingNamePrefix}NOTIFICATION`); if (setting?.value.case === "notificationSetting") { return setting.value.value; } return create(InstanceSetting_NotificationSettingSchema, {}); }, [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 settingsResponse = await instanceServiceClient.batchGetInstanceSettings({ names: [ buildInstanceSettingName(InstanceSetting_Key.GENERAL), buildInstanceSettingName(InstanceSetting_Key.MEMO_RELATED), buildInstanceSettingName(InstanceSetting_Key.TAGS), ], }); for (const setting of settingsResponse.settings) { fetchedSettingsRef.current.add(setting.name); } setState({ profile, settings: settingsResponse.settings, isInitialized: true, isLoading: false, profileLoaded: true, }); } catch (error) { console.error("Failed to initialize instance:", error); setState((prev) => ({ ...prev, isInitialized: true, isLoading: false, })); } }, []); const fetchSettings = useCallback(async (keys: InstanceSetting_Key[]) => { const names = keys.map(buildInstanceSettingName).filter((name) => !fetchedSettingsRef.current.has(name)); if (names.length === 0) { return; } for (const name of names) { fetchedSettingsRef.current.add(name); } try { const response = await instanceServiceClient.batchGetInstanceSettings({ names }); const fetchedNames = new Set(response.settings.map((setting) => setting.name)); setState((prev) => ({ ...prev, settings: [...prev.settings.filter((setting) => !fetchedNames.has(setting.name)), ...response.settings], })); } catch (error) { for (const name of names) { fetchedSettingsRef.current.delete(name); } throw error; } }, []); const fetchSetting = useCallback(async (key: InstanceSetting_Key) => { const name = buildInstanceSettingName(key); if (fetchedSettingsRef.current.has(name)) { return; } fetchedSettingsRef.current.add(name); try { const setting = await instanceServiceClient.getInstanceSetting({ name }); setState((prev) => ({ ...prev, settings: [...prev.settings.filter((s) => s.name !== setting.name), setting], })); } catch (error) { fetchedSettingsRef.current.delete(name); throw error; } }, []); const updateSetting = useCallback(async (setting: InstanceSetting) => { const updatedSetting = await instanceServiceClient.updateInstanceSetting({ setting }); setState((prev) => ({ ...prev, settings: [...prev.settings.filter((s) => s.name !== updatedSetting.name), updatedSetting], })); }, []); // Memoize context value to prevent unnecessary re-renders of consumers const value = useMemo( () => ({ ...state, generalSetting, memoRelatedSetting, storageSetting, tagsSetting, notificationSetting, aiSetting, initialize, fetchSetting, fetchSettings, updateSetting, }), [ state, generalSetting, memoRelatedSetting, storageSetting, tagsSetting, notificationSetting, aiSetting, initialize, fetchSetting, fetchSettings, updateSetting, ], ); return {children}; } export function useInstance() { const context = useContext(InstanceContext); if (!context) { throw new Error("useInstance must be used within InstanceProvider"); } return context; }