Tauri IPCの戻り値型を変える時は全callerを同時に更新する
Tauri commandの戻り値型は、Rust側だけ正しく変更してもアプリが壊れます。
危険なのは小さな変更です。bool が String になる、単純な値が詳細なstatusになる。Rustはcompileされ、TypeScriptもcompileされます。しかしruntimeでは、JavaScriptのtruthinessにより、空でない文字列が true として扱われます。
IPC return typeの変更は、全callerを同時に変えるmigrationとして扱います。
症状: expiredなのにtrue扱いになる
もともとbooleanを返していたcommandがあるとします。
#[tauri::command]
fn check_pro_status() -> bool {
true
}
frontendはこう呼びます。
const isPro = await invoke<boolean>("check_pro_status");
if (isPro) {
unlockProFeatures();
}
後で詳細statusが必要になり、backendを変えます。
#[tauri::command]
fn check_pro_status() -> String {
"expired".to_string()
}
callerを1つでも見落とすと、runtimeで "expired" はtruthyです。期限切れなのにPro機能が開く、という逆の挙動になります。
型ではなくcommand名で検索する
Rust commandを変える前に、frontend callerをすべて列挙します。
rg "check_pro_status|checkProStatus|invoke<.*check_pro_status" src
見る場所です。
- raw
invoke("command") - typed
invoke<T>("command") - service wrapper
- store initialization
- test mock
boolean だけで検索してはいけません。wrapperや推論の中に隠れているcallerがあります。
booleanではなく明示的な状態にする
backendが複数状態を返すなら、frontendもそれを型にします。
type ProStatus = "active" | "trial" | "expired" | "none" | "network_error";
const status = await invoke<ProStatus>("check_pro_status");
if (status === "active" || status === "trial") {
unlockProFeatures();
}
これは避けます。
if (status) {
unlockProFeatures();
}
許可する状態を明示的に比較します。
mockとtestも同時に更新する
test mockもIPC contractの一部です。
vi.mock("@tauri-apps/api/core", () => ({
invoke: async (command: string) => {
if (command === "check_pro_status") return "expired";
throw new Error(`unknown command ${command}`);
},
}));
truthinessの再発を防ぐtestを入れます。
it("does not unlock pro features when status is expired", async () => {
const status: ProStatus = "expired";
expect(status === "active" || status === "trial").toBe(false);
});
単純ですが、将来のrefactorで if (status) に戻るのを防げます。
store更新はatomicにする
Zustand storeが status、isPro、isInitialized を持つなら、1回の set() で更新します。
set({
status,
isPro: status === "active" || status === "trial",
isInitialized: true,
});
分けると、途中状態を1renderだけUIが読めます。
set({ status });
set({ isPro });
set({ isInitialized: true });
これがflicker、locked contentの一瞬表示、逆に一瞬unlockされる問題につながります。
検証チェック
- command名で全
invokecallerをgrepする - wrapper return typeを更新する
- truthiness checkを明示比較に変える
- mockを更新する
- 全statusのtestを入れる
- store更新に中間状態がないか見る
- 通常のwrapper patternから外すなら短い理由を書く