← ./articles-ja

Tauri IPCの戻り値型を変える時は全callerを同時に更新する

Tauri commandの戻り値型は、Rust側だけ正しく変更してもアプリが壊れます。

危険なのは小さな変更です。boolString になる、単純な値が詳細な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が statusisProisInitialized を持つなら、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名で全 invoke callerをgrepする
  • wrapper return typeを更新する
  • truthiness checkを明示比較に変える
  • mockを更新する
  • 全statusのtestを入れる
  • store更新に中間状態がないか見る
  • 通常のwrapper patternから外すなら短い理由を書く

参考