# ARCHITECTURE
NoteDeck — マルチサーバー対応 Misskey デッキクライアントのアーキテクチャ。
---
## 目次
- [アーキテクチャ概要](#アーキテクチャ概要)
- [全体像](#全体像)
- [notedeck(GUI アプリ)](#notedeckgui-アプリ)
- [notecli(コアライブラリ)](#notecliコアライブラリ)
- [ノートキャッシュ・キューアーキテクチャ](#ノートキャッシュキューアーキテクチャ)
- [レンダリングパフォーマンス](#レンダリングパフォーマンス)
- [採用状況マトリクス](#採用状況マトリクス)
---
## アーキテクチャ概要
### 全体像
```mermaid
graph TB
subgraph App["NoteDeck Application"]
subgraph Frontend["Frontend (WebView)
Vue 3 + TypeScript + Vite"]
Pinia["Pinia Stores (19個)"]
Router["Vue Router"]
Composables["Composables (58個)
useNoteList, useColumnSetup,
useStreamingBatch, useDeckWindow,
useEmojiResolver, ..."]
Adapter["Server Adapter Layer
(Misskey + フォーク)"]
Pinia --> Composables
end
subgraph Backend["Rust Backend"]
IPC["Tauri IPC Layer
(148 commands)"]
Notecli["notecli Library
(Misskey API)"]
SQLite["SQLite (WAL+FTS5)
refinery マイグレーション"]
Axum["Axum HTTP Server
(localhost:19820)"]
Streaming["Streaming Manager
(WebSocket)"]
ImageCache["Image Cache
(3層: Mem/Disk/Net)"]
OGP["OGP Cache
(SQLite-backed)"]
IPC --> Notecli --> SQLite
end
subgraph TauriCore["Tauri V2 Core"]
Bridge["IPC Bridge"]
Events["Event System (emit/listen)"]
MultiWin["Multi-Window Management"]
PluginSys["Plugin System"]
end
Frontend <-->|"Tauri IPC & Events"| TauriCore
TauriCore <--> Backend
end
Plugins["Plugins: notification, global-shortcut,
autostart, updater, opener, os, process"]
App --- Plugins
```
**技術スタック:**
- **フレームワーク**: Tauri V2
- **フロントエンド**: Vue 3 + TypeScript + Vite(Vapor モード移行予定 [#52](https://github.com/hitalin/notedeck/issues/52))
- **バックエンド**: Rust (Axum, notecli)
- **対応プラットフォーム**: Windows, macOS, Linux, Android (開発中)
**Frontend ↔ Backend の3つの通信パターン:**
```mermaid
sequenceDiagram
participant FE as Frontend
participant Tauri as Tauri V2
participant RS as Rust Backend
participant Ext as External Tool
Note over FE,RS: 1. Tauri IPC (同期的リクエスト/レスポンス)
FE->>Tauri: invoke("api_get_timeline", {...})
Tauri->>RS: Rust command
RS-->>FE: Response
Note over FE,RS: 2. Tauri Event System (非同期・双方向)
RS->>FE: emit("stream-event", payload)
FE->>RS: emit("nd:query-request")
RS-->>FE: emit("nd:query-response-{id}")
Note over Ext,RS: 3. Localhost HTTP API (外部ツール連携)
Ext->>RS: HTTP GET /api/{host}/timeline/home
RS-->>Ext: JSON Response
Ext->>RS: SSE /api/events
RS-->>Ext: リアルタイムイベント受信
```
---
### notedeck(GUI アプリ)
#### A-1. Query Bridge(Rust ↔ フロントエンド双方向クエリ)
**場所**: `query_bridge.rs` + `utils/apiBridge.ts`
外部 HTTP リクエスト → Rust → Tauri Event → Vue/Pinia → Tauri Event → Rust → HTTP レスポンス。
フロントエンドのリアクティブ状態(デッキカラム、コマンド一覧等)を外部ツールから直接取得可能。
---
#### A-2. マルチウィンドウ・デッキ(クロスウィンドウ D&D)
**場所**: `useDeckWindow.ts` + `useColumnDrag.ts`
- カラムを別ウィンドウにポップアウト
- ウィンドウ間でカラムをドラッグ移動
- マルチモニター対応のレイアウト保存・復元
- ウィンドウ閉鎖時の自動カラム回収
---
#### A-2b. PiP ウィンドウ(常前面フローティングカラム)
**場所**: `usePipWindow.ts` + `src/views/PipPage.vue`
- デッキのカラムを常前面フローティングウィンドウとして切り離し
- 375×700px(リサイズ可能)、`alwaysOnTop`、複数同時起動(動的ラベル `pip-*`)
- コマンドパレット / タイトルバー / カラムメニューの 3 経路で起動
---
#### A-3. HTTP API(notecli ルーター共有)
**場所**: `http_server.rs`(notedeck)+ `http_server.rs`(notecli)
notecli の `build_core_routes()` でコア API 16ルートを共有し、notedeck 固有ルート(deck, commands, image proxy, OpenAPI docs)を `.merge()` で追加。SSE イベントストリーム、Scalar UI ドキュメント付き。
---
#### A-4. ストリーミング → マルチ配信ブリッジ
**場所**: `streaming.rs` + `EventBus`
WebSocket 受信 → 1箇所で3つの出力先に同時配信:
1. OS ネイティブ通知(`tauri-plugin-notification`)
2. WebView イベント(`app.emit("stream-event")`)
3. SSE(外部 HTTP クライアント向け)
ストリーミングで受信したノートは `db.cache_note()` で SQLite に非同期保存。
---
#### A-5. 3層画像プロキシキャッシュ
**場所**: `image_cache.rs` + `/proxy/image`
```mermaid
graph LR
Browser["ブラウザ (ETag)"] --> Axum["/proxy/image"]
Axum --> Mem["メモリ LRU (32MB)"]
Axum --> Disk["ディスクキャッシュ
(24h TTL, 20MB max)"]
Axum --> Upstream["アップストリーム
(ストリーミング配信)"]
```
CSP で外部画像を直接ロードせず、Rust 側のプロキシを経由。ETag/304 対応、インフライト重複排除、同時フェッチ20件制限。
---
#### A-6. OGP プラグインシステム(15プラットフォーム対応)
**場所**: `ogp/plugins/` (Twitter, YouTube, Pixiv, Amazon, ニコニコ 等)
URL ごとに専用パーサーが起動し、汎用 OG タグ解析より高精度なプレビューを生成。
3段フォールバック: プラグイン → サーバー API → 直接 HTML パース。
---
#### A-7. グローバルショートカット + ボスキー + システムトレイ
**場所**: `lib.rs`(デスクトップ専用 `#[cfg(not(mobile))]`)
- `Ctrl+Shift+B`: ボスキー(瞬時にウィンドウ非表示)
- `Ctrl+Alt+N`: クイックノート(ウィンドウ表示 + 投稿フォーム起動)
- トレイアイコン: 左クリックで表示切替、右クリックメニュー
- 閉じるボタン: トレイに隠す(終了しない)
---
#### A-8. オフラインファースト(読み取り専用)
**場所**: `useNoteColumn.ts` + `useColumnSetup.ts` + `DeckTimelineColumn.vue`
```mermaid
graph LR
subgraph Online["オンライン"]
WS["WebSocket"] --> Recv["ノート受信"] --> Save["SQLite 保存"] --> Show1["UI 表示"]
end
subgraph Offline["オフライン"]
Restore["SQLite から復元"] --> Show2["UI 表示"]
Write["投稿・リアクション"] --> Block["ブロック"]
end
subgraph Reconnect["復帰時"]
Gap["差分フェッチ"] --> Sync["UI 同期"]
end
```
- **オフライン検出**: WebSocket 切断 (`disconnected`/`reconnecting`) + API fetch 失敗の両方で即座に検出
- **キャッシュ自動切替**: API 失敗時にキャッシュ済みノートを表示し続ける。スクロールで古いノートも SQLite から読み込み
- **書き込みガード**: オフライン時はリアクション・リノート・リプライ・引用・削除・編集・ブックマークをサイレントにブロック
- **自動復帰**: WebSocket 再接続成功 or API fetch 成功で `isOffline` が自動解除
- **UI バナー**: 「オフライン — キャッシュを表示中」をカラム上部に表示
**方針**: 書き込みキューイングは行わない。Misskey はリアルタイム性が重要な SNS であり、オフライン時に蓄積した操作を後から送信しても文脈が失われる。
---
#### A-9. フロントエンド層
**Pinia Stores (19個):**
| Store | 役割 |
|-------|------|
| `accounts` | マルチアカウント管理(ゲスト・ログアウト済みアカウント含む) |
| `deck` | デッキ・カラム・レイアウト・プロファイル管理(40+カラム種別) |
| `streaming` | WebSocket接続状態・購読管理 |
| `notes` | ノートのキャッシュ・正規化 |
| `emojis` | カスタム絵文字管理 |
| `servers` | 接続先サーバー情報 |
| `theme` | テーマ設定 |
| `ui` | UI状態 |
| `keybinds` | キーバインド設定 |
| `windows` | マルチウィンドウ管理 |
| `plugins` | AiScriptプラグイン |
| `pinnedReactions` | ピン留めリアクション |
| `recentEmojis` | 最近使った絵文字 |
| `confirm` | 確認ダイアログ管理 |
| `deckProfile` | デッキプロファイル管理 |
| `deckWallpaper` | デッキ壁紙設定 |
| `performance` | パフォーマンス設定 |
| `themeFileSync` | テーマファイル同期 |
| `toast` | トースト通知 |
**Server Adapter パターン** (`types.ts` → `registry.ts` → `misskey/`):
Misskey 本家および Misskey を名乗り続けるフォークに共通インターフェースで対応。
---
#### A-10. タイムライン DOM 管理
**場所**: `NoteScroller.vue` + `useNoteList.ts` + `useStreamingBatch.ts`
`@tanstack/vue-virtual` による仮想スクロールで、viewport + overscan 分のみ DOM に描画する。
**仮想スクロール:**
| 設定 | 値 | 説明 |
|------|-----|------|
| `noteListMax` | 200(デフォルト) | データ配列の上限(`performanceStore` で設定可能、50〜1000) |
| `overscan` | 7 | viewport 外に余分に描画する件数 |
| `estimateSize` | 動的 | 実測値の移動平均(20件ごとに更新) |
- `NoteScroller.vue` が `useVirtualizer` で仮想化。実 DOM は 30-50 件程度に抑制
- `measureElement` + ResizeObserver で可変高さ(テキストのみ 80px〜画像付き 400px+)を自動追跡
- `near-end` イベントで末尾到達を検知し loadMore を発火
- `scrollToIndex` expose でキーボードナビゲーション(j/k)に対応
**アニメーション:**
`` は使わず、データレイヤーでの ID マーキング + CSS `@keyframes` で新着ノートの slide-in アニメーションを実現。位置指定に `translate` プロパティ、アニメーションに `transform` プロパティを使い、独立プロパティとして干渉なく動作する。Vue Vapor Mode 互換。
**バッファリング:**
- `useStreamingBatch` は RAF バッファリング + pending 2段階で高頻度更新を1フレームにまとめる
- 超過分は末尾から削除。削除されたノートは SQLite に保存済みのため再取得可能
---
### notecli(コアライブラリ)
notecli は notedeck のコア基盤となる Rust クレートであり、**スタンドアロン CLI** と **ライブラリ** の二重の役割を持つ。
#### B-1. デュアルパーパス・クレート設計
| モード | エントリポイント | FrontendEmitter | HTTP サーバー |
|--------|------------------|-----------------|---------------|
| **CLI** | `main.rs` (clap) | `NoopEmitter` | なし |
| **デーモン** | `main.rs --daemon` | `EventBusEmitter` | Axum (16ルート) |
| **notedeck 組込** | `lib.rs` (ライブラリ) | `TauriEmitter` (notedeck側) | 拡張版 Axum (notecli の `build_core_routes()` + notedeck 固有ルート) |
同じビジネスロジック(API呼び出し、DB操作、ストリーミング)が CLI・デーモン・GUI のすべてで共有される。
---
#### B-2. FrontendEmitter トレイトパターン
ストリーミング(WebSocket)からのイベント配信を実行環境ごとに分離する Strategy パターン:
- **CLI**: `NoopEmitter`(何もしない)
- **デーモン**: `EventBusEmitter`(broadcast channel → SSE)
- **Tauri GUI**: `TauriEmitter`(Tauri Event System → Vue)
---
#### B-3. Raw → Normalized モデル変換
Misskey API レスポンスはフォークによってフィールドが異なる問題を2層モデルで解決:
- 既知フィールドは型安全にデシリアライズ
- 未知フィールド(フォーク固有)は `extra: HashMap` に自動収集
- `normalize()` で統一的な `NormalizedNote` に変換
- 新フォーク固有フィールド追加時に **コード変更不要**
セキュリティ: `Account` の `Drop` 実装で `token.zeroize()` を呼び、メモリ残留リスクを最小化。
---
#### B-4. SQLite + FTS5 + refinery マイグレーション
**DB マイグレーション**: refinery による番号付き SQL マイグレーション (`migrations/V1__*.sql`)。`refinery_schema_history` テーブルでバージョンを自動追跡。今後のスキーマ変更は SQL ファイル追加のみで対応可能。
**FTS5 トライグラム検索**:
```sql
CREATE VIRTUAL TABLE notes_fts USING fts5(
text, content=notes_cache, content_rowid=rowid, tokenize='trigram'
);
```
CJK(日本語・中国語・韓国語)の部分文字列検索に対応。CW も検索対象。
---
#### B-5. プラットフォーム・キーチェーン抽象化
条件付きコンパイルで各 OS ネイティブのキーチェーンに対応:
- Android → `AndroidNativeCredentialStore`
- macOS/iOS → `IosKeychain::Authenticated`
- Windows → `WindowsNativeCredentialStore`
- Linux → `LinuxKeyutilsPersistentStore`
クレデンシャル解決: キーチェーン → DB フォールバック → 遅延移行(既存ユーザーの自動移行)。
**ゲスト・ログアウト対応**: `get_credentials_or_anon()` でトークンがなければ `(host, "")` を返し、notecli が公開 API にフォールバック。認証必須 API は従来の `get_credentials()` を使用。
---
#### B-6. ストリーミング・マネージャー
- **指数バックオフ再接続**: 接続断 → 1秒 → 2秒 → ... → 最大30秒。成功時にバックオフリセット + 全サブスクリプション再送信
- **メッセージ処理**: `spawn_blocking` で SQLite 書き込みを非同期タスクからオフロード
- **ノート自動キャッシュ**: ストリーミング受信ノートを SQLite に非同期保存(オフラインファースト基盤)
---
#### B-7. CLI 設計:Unix 哲学の適用
5つの出力フォーマット(Default, JSON, JSONL, IDs, Compact/TSV)でパイプライン処理に対応。
```bash
notecli tl -f compact | fzf | cut -f1 | xargs notecli note
notecli tl -f json | jq '.[].text'
```
---
#### B-8. エラーハンドリング: safe_message() パターン
内部情報(SQLite クエリ、ネットワークトレース、キーチェーン詳細)はフロントエンドに露出させない。`Serialize` 実装で `code` + `safe_message` のペアを自動生成。
---
### Vue Vapor モード移行([#52](https://github.com/hitalin/notedeck/issues/52))
Vue 3.6 で導入予定の Vapor モード(仮想DOMレス)への移行準備が**完了**。
既知の移行ブロッカーはゼロ。Vue 3.6 リリース時にそのまま有効化可能。
**対応済み項目:**
- `