# 通知設計書 20-フォールバック書き込みエラー

## 概要

本ドキュメントは、Actualアプリケーションにおける「フォールバック書き込みエラー」エラー通知の設計仕様を定義する。この通知は、ブラウザで複数タブが開かれている場合に表示され、変更が保存されないことを警告する。

### 本通知の処理概要

この通知は、ブラウザ環境で同じ予算ファイルを複数のタブで開いた場合に発行される。ブラウザのIndexedDBは複数タブ間での同時書き込みをサポートしていないため、後から開いたタブでは変更を保存できなくなる。

**業務上の目的・背景**：ブラウザのWeb版Actualは、データ永続化にIndexedDBを使用している。IndexedDBはタブ間での排他制御をサポートしていないため、複数タブで同じファイルを開くとデータ競合が発生する可能性がある。この通知は、そのような状況を検出し、ユーザーにタブを閉じることを促すことで、データの整合性を保護する目的がある。

**通知の送信タイミング**：サーバー側から`fallback-write-error`イベントが発火された場合に発行される。これは、IndexedDBへの書き込み時に、別のタブが同じデータベースを使用していることを検出した場合に発生する。

**通知の受信者**：現在アプリケーションを操作しているユーザー（後から開いたタブ）。通知はアプリ内のUIに直接表示されるため、該当タブのユーザーが受信者となる。

**通知内容の概要**：ブラウザが同時に1つのタブでのみ使用可能であること、別のタブがアプリを開いていること、このタブでの変更は保存されないことを説明する。

**期待されるアクション**：ユーザーは現在のタブを閉じて、既に開いているタブで作業を続けることが期待される。

## 通知種別

アプリ内通知（In-App Notification）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（イベントリスナー経由） |
| 優先度 | 高（error） |
| リトライ | 無し |

### 送信先決定ロジック

通知は後から開かれたタブに表示される。最初に開いたタブには影響がない。

## 通知テンプレート

### アプリ内通知の場合

| 項目 | 内容 |
|-----|------|
| 通知タイプ | error |
| タイトル | Unable to save changes |
| sticky | true（手動で閉じるまで表示継続） |
| 識別ID | なし（自動生成） |

### 本文テンプレート

```
This browser only supports using the app in one tab at a time, and another tab has opened the app.
No changes will be saved from this tab; please close it and continue working in the other one.
```

### ボタン

ボタンなし

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| 該当なし | 固定テキストのため変数なし | - | - |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | fallback-write-error | 複数タブ検出 | 同じファイルが別タブで開かれている |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| デスクトップアプリ | Electronアプリでは発生しない（IndexedDB非使用） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーが新しいタブでアプリを開く] --> B[予算ファイルを開く]
    B --> C{IndexedDB書き込み試行}
    C -->|成功| D[通常の動作]
    C -->|別タブ検出| E[fallback-write-error イベント発火]
    E --> F[global-events.ts がキャッチ]
    F --> G[通知オブジェクト作成]
    G --> H[addNotification ディスパッチ]
    H --> I[UI に通知表示]
    I --> J{ユーザーアクション}
    J -->|タブを閉じる| K[正常終了]
    J -->|無視して作業続行| L[変更が保存されない]
    J -->|閉じるボタン| M[通知削除 警告継続]
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| IndexedDB | 予算データの永続化 | ブラウザのローカルストレージ |

### 更新テーブル一覧

このエラーでは更新が失敗するため、実際の更新は行われない（後から開いたタブ）。

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 書き込み競合 | 複数タブで同時アクセス | タブを閉じる |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 自動リトライなし |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

時間帯制限なし（イベント発生時に即座に表示）

## セキュリティ考慮事項

- データ損失防止のため、書き込みができないことを明確に警告
- 別タブの存在を通知することでユーザーの意図しないデータ競合を防止

## 備考

- この通知はstickyフラグがtrueのため、ユーザーが手動で閉じるまで表示が継続される
- ブラウザ版のみで発生する（デスクトップElectronアプリでは発生しない）
- ボタンが提供されないため、ユーザーは手動でタブを閉じる必要がある
- 通知を閉じても、変更が保存されない状態は継続する
- 国際化対応されていない（英語固定）

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: イベント型を理解する

fallback-write-errorイベントの型定義を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | server-events.ts | `packages/loot-core/src/types/server-events.ts` | FallbackWriteErrorEvent型（53行目）、ServerEvents型（70-86行目） |

**読解のコツ**: FallbackWriteErrorEventはundefined型で、ペイロードを持たないシンプルなイベントである。

#### Step 2: イベントリスナーを理解する

global-events.tsでのイベント処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | global-events.ts | `packages/desktop-client/src/global-events.ts` | unlistenFallbackWriteError（108-122行目） |

**主要処理フロー**:
1. **108行目**: listen('fallback-write-error', ...)でリスナー登録
2. **109-121行目**: 通知オブジェクト作成とディスパッチ
3. **113行目**: タイトル「Unable to save changes」
4. **114行目**: sticky: true
5. **115-118行目**: メッセージ内容

#### Step 3: イベント発火箇所を理解する（オプション）

サーバー側でfallback-write-errorが発火される箇所を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | （プラットフォーム固有） | `packages/loot-core/src/platform/server/...` | IndexedDB書き込み処理 |

**主要処理フロー**:
- IndexedDBへの書き込み時に競合を検出
- connection.send('fallback-write-error')でイベント発火

### プログラム呼び出し階層図

```
[サーバー側]
IndexedDB書き込み処理
    │
    └─ [競合検出時]
           │
           └─ connection.send('fallback-write-error')

    ▼ イベント送信

[クライアント側]
handleGlobalEvents (global-events.ts:19)
    │
    └─ listen('fallback-write-error') (global-events.ts:108)
           │
           └─ addNotification dispatch
                  │
                  └─ Notifications.tsx 表示
```

### データフロー図

```
[入力]                       [処理]                          [出力]

新しいタブでアプリ起動    IndexedDB書き込み試行           イベント発火
───▶ ファイルオープン ───▶ [競合検出] ───▶ fallback-write-error
                                                │
                                                ▼
                                         global-events.ts
                                                │
                                                └─ addNotification ───▶ UI表示
                                                                            │
                                                                            ▼
                                                                    [ユーザー選択]
                                                                            │
                                                                    ├─ タブを閉じる（推奨）
                                                                    └─ 作業続行（変更保存されず）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| global-events.ts | `packages/desktop-client/src/global-events.ts` | ソース | fallback-write-errorリスナー、通知発行 |
| notificationsSlice.ts | `packages/desktop-client/src/notifications/notificationsSlice.ts` | ソース | 通知状態管理 |
| Notifications.tsx | `packages/desktop-client/src/components/Notifications.tsx` | ソース | 通知UI表示 |
| server-events.ts | `packages/loot-core/src/types/server-events.ts` | 型定義 | FallbackWriteErrorEvent型定義 |
| connection.ts | `packages/loot-core/src/platform/server/connection.ts` | ソース | イベント送信インフラ |
