Yuki Takei 5 месяцев назад
Родитель
Сommit
db7ab9e4bc
1 измененных файлов с 296 добавлено и 600 удалено
  1. 296 600
      .serena/memories/page-state-hooks-useLatestRevision-degradation.md

+ 296 - 600
.serena/memories/page-state-hooks-useLatestRevision-degradation.md

@@ -1,744 +1,440 @@
-# Page State Hooks Analysis - useLatestRevision Degradation Issue
+# Page State Hooks - useLatestRevision リファクタリング記録
 
-**Date**: 2025-10-30
+**Date**: 2025-10-31
 **Branch**: support/use-jotai
-**Comparison**: master vs support/use-jotai
 
-## 調査対象フック
+## 🎯 実施内容のサマリー
 
-- `useLatestRevision` / `useIsLatestRevision`
-- `useIsRevisionOutdated`
-- `useRemoteRevisionId`
-- `useRemoteRevisionBody`
-- `useRemoteRevisionLastUpdatedAt`
+`support/use-jotai` ブランチで `useLatestRevision` が機能していなかった問題を解決し、リビジョン管理の状態管理を大幅に改善しました。
 
-## 🔴 重大な発見: useLatestRevision のデグレ
+### 主な成果
 
-### master ブランチの実装
+1. ✅ `IPageInfoForEntity.latestRevisionId` を導入
+2. ✅ `useIsLatestRevision` を SWR ベースで実装(Jotai atom から脱却)
+3. ✅ `remoteRevisionIdAtom` を完全削除(状態管理の簡素化)
+4. ✅ `useIsRevisionOutdated` の意味論を改善(「意図的な過去閲覧」を考慮)
+5. ✅ `useRevisionIdFromUrl` で URL パラメータ取得を一元化
 
-**Location**: `/workspace/growi/apps/app/src/stores/page.tsx:51-55`
-
-```typescript
-export const useIsLatestRevision = (
-  initialData?: boolean,
-): SWRResponse<boolean, any> => {
-  return useSWRStatic('isLatestRevision', initialData);
-};
-```
+---
 
-- **SWR ベースの状態管理**
-- SSR で `page.isLatestRevision()` を計算して props 経由で渡される
-- `[[...path]].page.tsx:481` で `props.isLatestRevision` を mutate して更新
+## 📋 実装の要点
 
-#### SSR での判定フロー
+### 1. `IPageInfoForEntity` に `latestRevisionId` を追加
 
-1. URL から `revisionId` パラメータを取得
-2. `page.initLatestRevisionField(revisionId)` を実行
-   - `latestRevision` フィールドに現在の最新リビジョンを保存
-   - `revision` フィールドを URL 指定のリビジョンに上書き
-3. `page.isLatestRevision()` で比較
-   - `latestRevision == revision._id` → `true` (最新版表示中)
-   - `latestRevision != revision._id` → `false` (古いリビジョン表示中)
+**ファイル**: `packages/core/src/interfaces/page.ts`
 
-### support/use-jotai ブランチの実装
+```typescript
+export type IPageInfoForEntity = Omit<IPageInfo, 'isNotFound' | 'isEmpty'> & {
+  // ... existing fields
+  latestRevisionId?: string;  // ✅ 追加
+};
+```
 
-**Location**: `/workspace/growi-use-jotai/apps/app/src/states/page/`
+**ファイル**: `apps/app/src/server/service/page/index.ts:2605`
 
 ```typescript
-// hooks.ts:48
-export const useLatestRevision = () => useAtomValue(latestRevisionAtom);
-
-// internal-atoms.ts:16
-export const latestRevisionAtom = atom(true);
+const infoForEntity: Omit<IPageInfoForEntity, 'bookmarkCount'> = {
+  // ... existing fields
+  latestRevisionId: page.revision != null ? getIdStringForRef(page.revision) : undefined,
+};
 ```
 
-- **Jotai atom: `atom(true)` - 常に true を返す(ハードコード)**
-- **commit `8f34782af0` で `setPageStatusAtom` が削除された**
-- **SSR からの初期化機構が完全に失われている**
-
-### 影響範囲
+**データフロー**: SSR で `constructBasicPageInfo` が自動的に `latestRevisionId` を設定 → `useSWRxPageInfo` で参照
 
-| 使用箇所 | 影響 |
-|---------|------|
-| OldRevisionAlert | 古いリビジョン表示時もアラートが表示されない |
-| DisplaySwitcher | PageEditor と PageEditorReadOnly の切り替えが正しく動作しない |
-| PageEditorReadOnly | 古いリビジョンでも読み取り専用エディタが表示されない |
+---
 
-### 実際の問題
+### 2. `useIsLatestRevision` を SWR ベースで実装
 
-- URL `?revisionId=xxx` で古いリビジョンを表示しても常に「最新版」と誤認される
-- 編集可能/不可の制御が正しく動作しない
-- キャッシュ制御ロジックが機能しない
+**ファイル**: `stores/page.tsx:164-191`
 
-## ✅ 正常動作: useIsRevisionOutdated
+```typescript
+export const useIsLatestRevision = (): SWRResponse<boolean, Error> => {
+  const currentPage = useCurrentPageData();
+  const pageId = currentPage?._id;
+  const shareLinkId = useShareLinkId();
+  const { data: pageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
-**両ブランチで正常動作**
+  const latestRevisionId = pageInfo && 'latestRevisionId' in pageInfo
+    ? pageInfo.latestRevisionId
+    : undefined;
 
-```typescript
-// master: stores/page.tsx:416-430
-export const useIsRevisionOutdated = (): SWRResponse<boolean, Error> => {
-  const { data: currentPage } = useSWRxCurrentPage();
-  const { data: remoteRevisionId } = useRemoteRevisionId();
-  const currentRevisionId = currentPage?.revision?._id;
+  const key = useMemo(() => {
+    if (currentPage?.revision?._id == null) {
+      return null;
+    }
+    return ['isLatestRevision', currentPage.revision._id, latestRevisionId ?? null];
+  }, [currentPage?.revision?._id, latestRevisionId]);
 
   return useSWRImmutable(
-    currentRevisionId != null && remoteRevisionId != null
-      ? ['useIsRevisionOutdated', currentRevisionId, remoteRevisionId]
-      : null,
-    ([, remoteRevisionId, currentRevisionId]) => {
-      return remoteRevisionId !== currentRevisionId;
+    key,
+    ([, currentRevisionId, latestRevisionId]) => {
+      if (latestRevisionId == null) {
+        return true;  // Assume latest if not available
+      }
+      return latestRevisionId === currentRevisionId;
     },
   );
 };
-
-// support/use-jotai: states/page/internal-atoms.ts:76-85
-export const isRevisionOutdatedAtom = atom((get) => {
-  const currentRevisionId = get(currentRevisionIdAtom);
-  const remoteRevisionId = get(remoteRevisionIdAtom);
-
-  if (currentRevisionId == null || remoteRevisionId == null) {
-    return false;
-  }
-
-  return remoteRevisionId !== currentRevisionId;
-});
 ```
 
-**用途**: リモートで他のユーザーがページを更新したかを検出(編集コンフリクト検出)
-
-## 🟡 重複実装の発見
-
-### PageStatusAlert での重複ロジック
-
-**Location**: `/workspace/growi-use-jotai/apps/app/src/client/components/PageStatusAlert.tsx:37-38`
-
-```typescript
-const currentRevisionId = pageData?.revision?._id;
-const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null)
-  && currentRevisionId !== remoteRevisionId;
-```
-
-この実装は `useIsRevisionOutdated()` と完全に重複している。
-
-**master ブランチでも同じ重複が存在**: `/workspace/growi/apps/app/src/client/components/PageStatusAlert.tsx:37-38`
-
-## Remote 系フックの使用状況
+**使用箇所**: OldRevisionAlert, DisplaySwitcher, PageEditorReadOnly
 
-| フック | master 使用箇所数 | support/use-jotai 使用箇所数 |
-|--------|------------------|----------------------------|
-| `useRemoteRevisionId` | 5箇所 | 2箇所 |
-| `useRemoteRevisionBody` | 2箇所 | 1箇所 |
-| `useRemoteRevisionLastUpdateUser` | 2箇所 | 2箇所 |
-| `useRemoteRevisionLastUpdatedAt` | 2箇所 | 1箇所 |
+**判定**: `.data !== false` で「古いリビジョン」を検出
 
-**master での追加使用箇所**:
-- `[[...path]].page.tsx` で初期化に使用
+---
 
-## 修正提案
+### 3. `remoteRevisionIdAtom` の完全削除
 
-### 🔴 優先度 1: useLatestRevision のデグレ修正(必須)
+**削除理由**:
+- `useSWRxPageInfo.data.latestRevisionId` で代替可能
+- 「Socket.io 更新検知」と「最新リビジョン保持」の用途が混在していた
+- 状態管理が複雑化していた
 
-1. **`setPageStatusAtom` を復活**
-   ```typescript
-   export const setPageStatusAtom = atom(
-     null,
-     (get, set, status: { isNotFound?: boolean; isLatestRevision?: boolean }) => {
-       if (status.isNotFound !== undefined) {
-         set(pageNotFoundAtom, status.isNotFound);
-       }
-       if (status.isLatestRevision !== undefined) {
-         set(latestRevisionAtom, status.isLatestRevision);
-       }
-     },
-   );
-   ```
+**重要**: `RemoteRevisionData.remoteRevisionId` は型定義に残した
+→ コンフリクト解決時に「どのリビジョンに対して保存するか」の情報として必要
 
-2. **SSR からの初期化を実装**
-   - `[[...path]].page.tsx` で `setPageStatusAtom` を使用
-   - `props.isLatestRevision` を atom に反映
+---
 
-3. **命名を `useIsLatestRevision` に統一**
-   - master ブランチと一貫性を保つ
-   - `is` プレフィックスで boolean を明示
+### 4. `useIsRevisionOutdated` の意味論的改善
 
-### 🟡 優先度 2: 重複ロジックの削除(推奨)
+**改善前**: 単純に「現在のリビジョン ≠ 最新リビジョン」を判定
+**問題**: URL `?revisionId=xxx` で意図的に過去を見ている場合も `true` を返していた
 
-- `PageStatusAlert` の独自実装を `useIsRevisionOutdated()` に置き換え
+**改善後**: 「ユーザーが意図的に過去リビジョンを見ているか」を考慮
 
-### 🟢 優先度 3: Remote 系フックの統合(オプション)
+**ファイル**: `states/context.ts:82-100`
 
 ```typescript
-export const useRemoteRevision = () => {
-  const id = useAtomValue(remoteRevisionIdAtom);
-  const body = useAtomValue(remoteRevisionBodyAtom);
-  const lastUpdateUser = useAtomValue(remoteRevisionLastUpdateUserAtom);
-  const lastUpdatedAt = useAtomValue(remoteRevisionLastUpdatedAtAtom);
-  return { id, body, lastUpdateUser, lastUpdatedAt };
+export const useRevisionIdFromUrl = (): string | undefined => {
+  const router = useRouter();
+  const revisionId = router.query.revisionId;
+  return typeof revisionId === 'string' ? revisionId : undefined;
 };
-```
-
-既存の個別フックは後方互換のため残す。
-
-## 関連ファイル
-
-### master ブランチ
-- `/workspace/growi/apps/app/src/stores/page.tsx`
-- `/workspace/growi/apps/app/src/stores/remote-latest-page.ts`
-- `/workspace/growi/apps/app/src/pages/[[...path]].page.tsx`
-- `/workspace/growi/apps/app/src/server/models/obsolete-page.js`
-
-### support/use-jotai ブランチ
-- `/workspace/growi-use-jotai/apps/app/src/states/page/hooks.ts`
-- `/workspace/growi-use-jotai/apps/app/src/states/page/internal-atoms.ts`
-- `/workspace/growi-use-jotai/apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx`
-- `/workspace/growi-use-jotai/apps/app/src/client/components/Page/DisplaySwitcher.tsx`
-- `/workspace/growi-use-jotai/apps/app/src/client/components/PageEditor/PageEditorReadOnly.tsx`
-
-## まとめ
 
-| 項目 | 状態 | 対応 |
-|-----|------|------|
-| **useLatestRevision** | 🔴 機能デグレ | 必須修正 |
-| **useIsRevisionOutdated** | ✅ 正常動作 | 対応不要 |
-| **PageStatusAlert 重複** | 🟡 要リファクタ | 推奨 |
-| **Remote 系フック** | ✅ 正常動作 | 統合はオプション |
-
-最も重要な問題は **useLatestRevision が完全に機能していない** ことです。これは古いリビジョン表示時の UI 制御に影響するため、早急な修正が必要です。
-
----
-
-## ✅ 修正完了 (2025-10-30)
-
-### 実装内容
-
-**グローバル state を削減する方針で修正を実施**
-
-#### 1. `latestRevisionAtom` を削除し、computed atom に置き換え
+export const useIsViewingSpecificRevision = (): boolean => {
+  const revisionId = useRevisionIdFromUrl();
+  return revisionId != null;
+};
+```
 
-**変更**: `src/states/page/internal-atoms.ts`
+**ファイル**: `stores/page.tsx:193-219`
 
 ```typescript
-// ❌ 削除: ハードコードされた state
-// export const latestRevisionAtom = atom(true);
+export const useIsRevisionOutdated = (): boolean => {
+  const { data: isLatestRevision } = useIsLatestRevision();
+  const isViewingSpecificRevision = useIsViewingSpecificRevision();
 
-// ✅ 追加: currentPageData から導出する computed atom
-export const isLatestRevisionAtom = atom((get) => {
-  const currentPage = get(currentPageDataAtom);
-
-  if (currentPage == null) {
-    return true;
+  // If user intentionally views a specific revision, don't show "outdated" alert
+  if (isViewingSpecificRevision) {
+    return false;
   }
 
-  if (currentPage.latestRevision == null || currentPage.revision?._id == null) {
-    return true;
+  if (isLatestRevision == null) {
+    return false;
   }
 
-  // Compare IDs using utility function for type safety
-  return (
-    getIdStringForRef(currentPage.latestRevision) === currentPage.revision._id
-  );
-});
-```
-
-**利点:**
-- ✅ **グローバル state を1つ削減** (`latestRevisionAtom` が不要に)
-- ✅ **SSR からの初期化不要** (`initLatestRevisionField` が実行されていれば自動的に動作)
-- ✅ **master の `isLatestRevision()` メソッドと同じロジック**
-
-#### 2. フック名を `useIsLatestRevision` に統一
-
-**変更**: `src/states/page/hooks.ts`
-
-```typescript
-// ❌ 削除
-// export const useLatestRevision = () => useAtomValue(latestRevisionAtom);
-
-// ✅ 追加: master と命名を統一
-export const useIsLatestRevision = (): boolean =>
-  useAtomValue(isLatestRevisionAtom);
+  // User expects latest, but it's not latest = outdated
+  return !isLatestRevision;
+};
 ```
 
-#### 3. 使用箇所を更新
-
-- `src/components/PageView/PageAlerts/OldRevisionAlert.tsx`
-- `src/client/components/Page/DisplaySwitcher.tsx`
-- `src/client/components/PageEditor/PageEditorReadOnly.tsx`
-
-全て `useIsLatestRevision()` を使用するように変更。
-
-#### 4. hydration ロジックを簡素化
-
-**変更**: `src/states/page/hydrate.ts`
-
-- `latestRevisionAtom` への hydration を削除
-- `isLatestRevision` オプションを削除
-- computed atom なので hydration 不要
-
-### 検証結果
-
-✅ **TypeScript 型チェック**: `latestRevisionAtom` 関連のエラーなし
-✅ **SSR での動作**: `page.initLatestRevisionField(revisionId)` が実行されることを確認
-✅ **データフロー**: `currentPageData.latestRevision` と `currentPageData.revision._id` の比較で正しく動作
-
-### 技術的詳細
-
-**データの流れ:**
-
-1. **SSR (page-data-props.ts:202)**
-   ```typescript
-   page.initLatestRevisionField(revisionId);
-   // → page.latestRevision に最新リビジョンの ObjectId を設定
-   // → revisionId が指定されていれば page.revision を上書き
-   ```
-
-2. **Hydration (hydrate.ts)**
-   ```typescript
-   [currentPageDataAtom, page ?? undefined]
-   // → page オブジェクトが atom に格納される
-   ```
-
-3. **Computed (isLatestRevisionAtom)**
-   ```typescript
-   getIdStringForRef(currentPage.latestRevision) === currentPage.revision._id
-   // → 最新リビジョンかどうかを自動判定
-   ```
-
-### 副次的な改善
-
-- **型安全性の向上**: `getIdStringForRef` を使用して ObjectId と string を安全に比較
-- **コードの簡潔化**: hydration オプションが1つ減少
-- **保守性の向上**: master と同じロジックを使用することでバグの可能性を低減
-
-### 残課題
-
-1. 🟡 **PageStatusAlert の重複ロジック** (L37-38)
-   - `useIsRevisionOutdated()` で置き換え可能
-   - 優先度: 低
-
-2. 🟢 **Remote 系フックの統合** (オプション)
-   - 統合フックを追加して ConflictDiffModal を簡潔化
-   - 優先度: 低
-
 ---
 
-## 🔴 問題発覚: 実装が動作しない (2025-10-30)
-
-### 現象
-
-`http://localhost:3000/68fb8ec144f3c32fc54fd386?revisionId=68fb8ec744f3c32fc54fd456` で古いリビジョンにアクセスしても:
-- OldRevisionAlert が表示されない
-- PageEditorReadOnly にならない
-- `isLatestRevisionAtom` が常に `true` を返す
-
-### 根本原因
+## 🎭 動作例
 
-**`latestRevision` フィールドがクライアント側に届いていない**
+| 状況 | isLatestRevision | isViewingSpecificRevision | isRevisionOutdated | 意味 |
+|------|------------------|---------------------------|---------------------|------|
+| 最新を表示中 | true | false | false | 正常 |
+| Socket.io更新を受信 | false | false | **true** | 「再fetchせよ」 |
+| URL `?revisionId=old` で過去を閲覧 | false | true | false | 「意図的な過去閲覧」 |
 
-デバッグログの結果:
-```
-[isLatestRevisionAtom] Missing data, returning true
-Object { hasLatestRevision: false, hasRevisionId: true }
-```
+---
 
-#### 原因の詳細
-
-1. **Schema に `latestRevision` フィールドが定義されていない**
-   - `src/server/models/page.ts` の schema に `latestRevision` フィールドがない
-   - `latestRevisionBodyLength` はあるが、`latestRevision` 自体は未定義
-
-2. **Virtual フィールドは自動的にシリアライズされない**
-   - `initLatestRevisionField()` で動的に追加される `latestRevision` フィールド
-   - Mongoose の `.toJSON()` や `.toObject()` でデフォルト除外される
-   - クライアントに届かない
-
-3. **SSR で設定しても無駄**
-   ```javascript
-   page.initLatestRevisionField(revisionId);
-   // ↓
-   this.latestRevision = this.revision;  // 設定される
-   // ↓
-   await page.populateDataToShowRevision(skipSSR);
-   // ↓ しかし
-   // JSON シリアライズで latestRevision が消える
-   ```
+## 🔄 現状の remoteRevision 系 atom と useSetRemoteLatestPageData
 
-### 検討した解決策
+### 削除済み
+- ✅ `remoteRevisionIdAtom` - 完全削除(`useSWRxPageInfo.data.latestRevisionId` で代替)
 
-#### ❌ 案1: Schema に `latestRevision` を追加
-- 既存の動作に影響する可能性が高い
-- Migration 必要
-- リスク大
+### 残存している atom(未整理)
+- ⚠️ `remoteRevisionBodyAtom` - ConflictDiffModal で使用
+- ⚠️ `remoteRevisionLastUpdateUserAtom` - ConflictDiffModal, PageStatusAlert で使用
+- ⚠️ `remoteRevisionLastUpdatedAtAtom` - ConflictDiffModal で使用
 
-#### ❌ 案2: `toJSON` オプションで virtual を含める
-- 全ての場所に影響
-- 予期しない副作用の可能性
+### `useSetRemoteLatestPageData` の役割
 
-#### ✅ 案3: `remoteRevisionId` を活用(推奨)
+**定義**: `states/page/use-set-remote-latest-page-data.ts`
 
-**着眼点**: `remoteRevisionId` は既に「最新リビジョン」として使われている
+```typescript
+export type RemoteRevisionData = {
+  remoteRevisionId: string;      // 型には含むが atom には保存しない
+  remoteRevisionBody: string;
+  remoteRevisionLastUpdateUser?: IUserHasId;
+  remoteRevisionLastUpdatedAt: Date;
+};
 
-#### 既存の `remoteRevisionId` の用途
+export const useSetRemoteLatestPageData = (): SetRemoteLatestPageData => {
+  // remoteRevisionBodyAtom, remoteRevisionLastUpdateUserAtom, remoteRevisionLastUpdatedAtAtom を更新
+  // remoteRevisionId は atom に保存しない(コンフリクト解決時のパラメータとしてのみ使用)
+};
+```
 
-1. **SSR での初期化** (`src/states/page/hydrate.ts`)
-   ```typescript
-   [remoteRevisionIdAtom, page?.revision?._id]
-   ```
+**使用箇所**(6箇所):
 
-2. **Socket での更新** (`src/client/services/side-effects/page-updated.ts:26-33`)
+1. **`page-updated.ts`** - Socket.io でページ更新受信時
    ```typescript
-   const remoteData: RemoteRevisionData = {
+   // 他のユーザーがページを更新したときに最新リビジョン情報を保存
+   setRemoteLatestPageData({
      remoteRevisionId: s2cMessagePageUpdated.revisionId,
      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
-   };
-   setRemoteLatestPageData(remoteData);
+   });
    ```
 
-3. **既存の `useIsRevisionOutdated` でも使用**
+2. **`page-operation.ts`** - 自分がページ保存した後(`useUpdateStateAfterSave`)
    ```typescript
-   export const isRevisionOutdatedAtom = atom((get) => {
-     const currentRevisionId = get(currentRevisionIdAtom);
-     const remoteRevisionId = get(remoteRevisionIdAtom);
-     return remoteRevisionId !== currentRevisionId;
+   // 自分が保存した後の最新リビジョン情報を保存
+   setRemoteLatestPageData({
+     remoteRevisionId: updatedPage.revision._id,
+     remoteRevisionBody: updatedPage.revision.body,
+     remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
+     remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
    });
    ```
 
-### 新しい実装方針
-
-**`isLatestRevisionAtom` を `remoteRevisionId` ベースに変更**
-
-```typescript
-export const isLatestRevisionAtom = atom((get) => {
-  const currentPage = get(currentPageDataAtom);
-  const remoteRevisionId = get(remoteRevisionIdAtom);
-
-  if (currentPage?.revision?._id == null || remoteRevisionId == null) {
-    return true;  // デフォルトは最新版とみなす
-  }
-
-  // remote (最新) と current (表示中) を比較
-  return remoteRevisionId === currentPage.revision._id;
-});
-```
-
-### メリット
+3. **`conflict.tsx`** - コンフリクト解決時(`useConflictResolver`)
+   ```typescript
+   // コンフリクト発生時にリモートリビジョン情報を保存
+   setRemoteLatestPageData(remoteRevidsionData);
+   ```
 
-1. ✅ **Schema 変更不要**
-2. ✅ **既存のデータフローを活用**
-   - SSR で `remoteRevisionId` に最新リビジョン ID が設定される
-   - Socket で更新時も自動的に反映される
-3. ✅ **`useIsRevisionOutdated` と同じ比較ロジック**
-   - 整合性が高い
-   - 保守しやすい
-4. ✅ **`initLatestRevisionField()` 不要**
-   - `latestRevision` フィールドが不要になる
-   - コードがシンプルになる
+4. **`drawio-modal-launcher-for-view.ts`** - Drawio 編集でコンフリクト発生時
+5. **`handsontable-modal-launcher-for-view.ts`** - Handsontable 編集でコンフリクト発生時
+6. **定義ファイル自体**
 
-### 動作フロー
+### 現在のデータフロー
 
 ```
-┌─────────────────────────────────────────────────────────┐
-│ 1. SSR (page-data-props.ts)                             │
-│    URL: /page?revisionId=old_revision_id                │
-├─────────────────────────────────────────────────────────┤
-│ page.revision = old_revision_id  (URL で指定)           │
-│ remoteRevisionIdAtom = page.revision._id (最新)         │
-│                                                          │
-│ ※ initLatestRevisionField() は不要になる               │
-└─────────────────────────────────────────────────────────┘
-                          ↓
-┌─────────────────────────────────────────────────────────┐
-│ 2. Hydration                                             │
-├─────────────────────────────────────────────────────────┤
-│ currentPageDataAtom ← page (old_revision_id)            │
-│ remoteRevisionIdAtom ← latest_revision_id               │
-└─────────────────────────────────────────────────────────┘
-                          ↓
-┌─────────────────────────────────────────────────────────┐
-│ 3. isLatestRevisionAtom (Client)                        │
-├─────────────────────────────────────────────────────────┤
-│ remoteRevisionId === currentPage.revision._id           │
-│ → latest_revision_id === old_revision_id                │
-│ → false                                                  │
-│                                                          │
-│ ∴ OldRevisionAlert 表示                                 │
-│ ∴ PageEditorReadOnly に切り替え                         │
-└─────────────────────────────────────────────────────────┘
+┌─────────────────────────────────────────────────────┐
+│ Socket.io / 保存処理 / コンフリクト                  │
+└─────────────────────────────────────────────────────┘
+                    ↓
+┌─────────────────────────────────────────────────────┐
+│ useSetRemoteLatestPageData                          │
+│  ├─ remoteRevisionBodyAtom ← body                   │
+│  ├─ remoteRevisionLastUpdateUserAtom ← user         │
+│  └─ remoteRevisionLastUpdatedAtAtom ← date          │
+│  (remoteRevisionId は保存しない)                    │
+└─────────────────────────────────────────────────────┘
+                    ↓
+┌─────────────────────────────────────────────────────┐
+│ 使用箇所                                             │
+│  ├─ ConflictDiffModal: body, user, date を表示     │
+│  └─ PageStatusAlert: user を表示                    │
+└─────────────────────────────────────────────────────┘
 ```
 
-### 懸念事項と対応
+### 問題点
 
-#### Q1: SSR で `remoteRevisionId` に何を設定するか?
+1. **PageInfo (latestRevisionId) との同期がない**:
+   - Socket.io 更新時に `remoteRevision*` atom は更新される
+   - しかし `useSWRxPageInfo.data.latestRevisionId` は更新されない
+   - → `useIsLatestRevision()` と `useIsRevisionOutdated()` がリアルタイム更新を検知できない
 
-**A**: `page.revision._id` を設定する(現在のまま)
+2. **用途が限定的**:
+   - 主に ConflictDiffModal でリモートリビジョンの詳細を表示するために使用
+   - PageStatusAlert でも使用しているが、本来は `useIsRevisionOutdated()` で十分
 
-- URL に `?revisionId=xxx` がある場合:
-  - `page.revision` は古いリビジョンを指す
-  - **問題**: `remoteRevisionId` にも古い ID が入ってしまう?
+3. **データの二重管理**:
+   - リビジョン ID: `useSWRxPageInfo.data.latestRevisionId` で管理
+   - リビジョン詳細 (body, user, date): atom で管理
+   - 一貫性のないデータ管理
 
-**解決**: SSR の hydration ロジックを修正
+---
 
-```typescript
-// Before: hydrate.ts
-[remoteRevisionIdAtom, page?.revision?._id]
+## 🎯 次に取り組むべきタスク
 
-// After: 最新のリビジョン ID を取得する必要がある
-// ↓ この時点で page.revision は URL の revisionId で上書きされている
-// ↓ 元の最新リビジョン ID を別途渡す必要がある
-```
+### PageInfo (useSWRxPageInfo) の mutate が必要な3つのタイミング
 
-#### Q2: `initLatestRevisionField()` の代替方法は?
+#### 1. 🔴 SSR時の optimistic update
 
-**A**: SSR で props に `latestRevisionId` を追加
+**問題**:
+- SSR で `pageWithMeta.meta` (IPageInfoForEntity) が取得されているが、`useSWRxPageInfo` のキャッシュに入っていない
+- クライアント初回レンダリング時に PageInfo が未取得状態になる
 
+**実装方針**:
 ```typescript
-// page-data-props.ts
-const latestRevisionId = page.revision?._id;  // revisionId 上書き前に保存
-
-// revisionId が指定されていれば page.revision を上書き
-if (revisionId != null) {
-  page.revision = revisionId;
-}
-
-return {
-  props: {
-    pageWithMeta: { data: populatedPage, meta },
-    latestRevisionId,  // ← 追加
-  }
-};
-```
+// [[...path]]/index.page.tsx または適切な場所
+const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
-```typescript
-// hydrate.ts
-useHydratePageAtoms(pageWithMeta?.data, pageMeta, {
-  latestRevisionId: props.latestRevisionId,  // ← remoteRevisionIdAtom に設定
-});
+useEffect(() => {
+  if (pageWithMeta?.meta) {
+    mutatePageInfo(pageWithMeta.meta, { revalidate: false });
+  }
+}, [pageWithMeta?.meta, mutatePageInfo]);
 ```
 
-### サーバー側の `isLatestRevision()` メソッドについて
-
-**結論: 削除できない**
-
-使用箇所(サーバー側のみ):
-- `getLatestRevisionBodyLength()` - page.ts:1194
-- `calculateAndUpdateLatestRevisionBodyLength()` - page.ts:1209
-
-これらは「最新リビジョンの場合のみ body length を計算する」という内部ロジックで使用されており、クライアント側とは無関係。
-
-### 次のステップ
-
-1. SSR で `latestRevisionId` を props に追加
-2. `isLatestRevisionAtom` を `remoteRevisionId` ベースに変更
-3. Hydration ロジックを更新
-4. `initLatestRevisionField()` の呼び出しを削除(オプション)
-5. デバッグログで動作確認
-6. 動作したらログを削除
+**Note**:
+- Jotai の hydrate とは別レイヤー(Jotai は atom、これは SWR のキャッシュ)
+- `useSWRxPageInfo` は既に `initialData` パラメータを持っているが、呼び出し側で渡していない
+- **重要**: `mutatePageInfo` は bound mutate(hook から返されるもの)を使う
 
 ---
 
-## 🟢 新しいアプローチ: IPageInfoForEntity に latestRevisionId を追加 (2025-10-31)
-
-### アプローチの概要
-
-**方針**: `IPageInfoForEntity` に `latestRevisionId` 属性を追加し、`constructBasicPageInfo` で導出する
+#### 2. 🔴 same route 遷移時の mutate
 
-**データフロー**:
-1. `constructBasicPageInfo` で `page.revision._id` から `latestRevisionId` を導出
-2. SSR で `findPageAndMetaDataByViewer` の返す `meta` にデータが含まれる
-3. クライアント側で `useSWRxPageInfo` を通してデータを参照
-4. `isLatestRevisionAtom` で `pageInfo.latestRevisionId` と `currentPage.revision._id` を比較
+**問題**:
+- `[[...path]]` ルート内での遷移(例: `/pageA` → `/pageB`)時に PageInfo が更新されない
+- `useFetchCurrentPage` が新しいページを取得しても PageInfo は古いまま
 
-### タイミングの検証
-
-#### ✅ 重要な発見: `constructBasicPageInfo` は `initLatestRevisionField` より前に呼ばれる
-
-**`page-data-props.ts` のフロー**:
-```typescript
-// L157: findPageAndMetaDataByViewer を呼び出し(ここで meta が生成される)
-const pageWithMeta = await pageService.findPageAndMetaDataByViewer(
-  pageId,
-  resolvedPagePath,
-  user,
-);
-
-// L202: この後に initLatestRevisionField(page.revision が上書きされる)
-page.initLatestRevisionField(revisionId);
-```
-
-**`findPageAndMetaDataByViewer` の内部 (server/service/page/index.ts:406-441)**:
+**実装方針**:
 ```typescript
-// L421: ページ取得(この時点で page.revision は最新版)
-page = await Page.findByIdAndViewer(pageId, user, null, true);
+// states/page/use-fetch-current-page.ts
+export const useFetchCurrentPage = () => {
+  const shareLinkId = useAtomValue(shareLinkIdAtom);
+  const revisionIdFromUrl = useRevisionIdFromUrl();
 
-// L441: meta 生成(initLatestRevisionField より前!)
-const basicPageInfo = this.constructBasicPageInfo(page, isGuestUser);
-```
+  // ✅ 追加: PageInfo の mutate 関数を取得
+  const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPageId, shareLinkId);
 
-**結論**: `constructBasicPageInfo` が呼ばれる時点で `page.revision` は最新版を指している ✅
+  const fetchCurrentPage = useAtomCallback(
+    useCallback(async (get, set, args) => {
+      // ... 既存のフェッチ処理 ...
 
-### 実装すべき変更
+      const { data } = await apiv3Get('/page', params);
+      const { page: newData } = data;
 
-#### 1. 型定義の追加
+      set(currentPageDataAtom, newData);
+      set(currentPageIdAtom, newData._id);
 
-**ファイル**: `packages/core/src/interfaces/page.ts:103-113`
+      // ✅ 追加: PageInfo を再フェッチ
+      mutatePageInfo();  // 引数なし = revalidate (再フェッチ)
 
-```typescript
-export type IPageInfoForEntity = Omit<IPageInfo, 'isNotFound' | 'isEmpty'> & {
-  isNotFound: false;
-  isEmpty: false;
-  sumOfLikers: number;
-  likerIds: string[];
-  sumOfSeenUsers: number;
-  seenUserIds: string[];
-  contentAge: number;
-  descendantCount: number;
-  commentCount: number;
-  latestRevisionId?: string;  // ← 追加(optional)
+      return newData;
+    }, [shareLinkId, revisionIdFromUrl, mutatePageInfo])
+  );
 };
 ```
 
-#### 2. `constructBasicPageInfo` の更新
+**Note**:
+- `mutatePageInfo()` を引数なしで呼ぶと SWR が再フェッチする
+- `/page` API からは meta が取得できないため、再フェッチが必要
 
-**ファイル**: `apps/app/src/server/service/page/index.ts:2590`
-
-```typescript
-const infoForEntity: Omit<IPageInfoForEntity, 'bookmarkCount'> = {
-  isNotFound: false,
-  isV5Compatible: isTopPage(page.path) || page.parent != null,
-  isEmpty: false,
-  sumOfLikers: page.liker.length,
-  likerIds: this.extractStringIds(likers),
-  seenUserIds: this.extractStringIds(seenUsers),
-  sumOfSeenUsers: page.seenUsers.length,
-  isMovable,
-  isDeletable,
-  isAbleToDeleteCompletely: false,
-  isRevertible: isTrashPage(page.path),
-  contentAge: page.getContentAge(),
-  descendantCount: page.descendantCount,
-  commentCount: page.commentCount,
-  latestRevisionId: getIdStringForRef(page.revision),  // ← 追加
-};
-```
-
-**注意**: `page.revision` は ObjectId(未 populate)の可能性があるが、`getIdStringForRef` で文字列に変換可能。
+---
 
-#### 3. `isLatestRevisionAtom` の実装
+#### 3. 🔴 Socket.io 更新時の mutate
 
-**ファイル**: `apps/app/src/states/page/internal-atoms.ts`
+**問題**:
+- Socket.io で他のユーザーがページを更新したとき、`useSWRxPageInfo` のキャッシュが更新されない
+- `latestRevisionId` が古いままになる
+- **重要**: `useIsLatestRevision()` と `useIsRevisionOutdated()` が正しく動作しない
 
+**実装方針**:
 ```typescript
-export const isLatestRevisionAtom = atom((get) => {
-  const currentPage = get(currentPageDataAtom);
-  const pageInfo = get(pageInfoAtom);  // useSWRxPageInfo から取得
-
-  // データが揃っていない場合はデフォルトで true
-  if (currentPage?.revision?._id == null || pageInfo?.latestRevisionId == null) {
-    return true;
+// client/services/side-effects/page-updated.ts
+const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id, shareLinkId);
+
+const remotePageDataUpdateHandler = useCallback((data) => {
+  const { s2cMessagePageUpdated } = data;
+
+  // 既存: remoteRevision atom を更新
+  setRemoteLatestPageData(remoteData);
+
+  // ✅ 追加: PageInfo の latestRevisionId を optimistic update
+  if (currentPage?._id != null) {
+    mutatePageInfo((currentPageInfo) => {
+      if (currentPageInfo && 'latestRevisionId' in currentPageInfo) {
+        return {
+          ...currentPageInfo,
+          latestRevisionId: s2cMessagePageUpdated.revisionId,
+        };
+      }
+      return currentPageInfo;
+    }, { revalidate: false });
   }
-
-  // 最新リビジョン ID と現在表示中のリビジョン ID を比較
-  return pageInfo.latestRevisionId === currentPage.revision._id;
-});
+}, [currentPage?._id, mutatePageInfo, setRemoteLatestPageData]);
 ```
 
-#### 4. `pageInfoAtom` の追加
+**Note**:
+- 引数に updater 関数を渡して既存データを部分更新
+- `revalidate: false` で再フェッチを抑制(optimistic update のみ)
 
-**ファイル**: `apps/app/src/states/page/internal-atoms.ts`
+---
 
-`useSWRxPageInfo` のデータを Jotai atom として扱うための atom を追加する必要があります。
+### SWR の mutate の仕組み
 
+**Bound mutate** (推奨):
 ```typescript
-// SWR のデータを Jotai で参照するための atom
-export const pageInfoAtom = atom<IPageInfoForEntity | null>(null);
+const { data, mutate } = useSWRxPageInfo(pageId, shareLinkId);
+mutate(newData, options);  // 自動的に key に紐付いている
 ```
 
-**代替案**: `useSWRxPageInfo` を直接使う方法もあります:
-
+**グローバル mutate**:
 ```typescript
-// hooks.ts
-export const useIsLatestRevision = (): boolean => {
-  const currentPage = useCurrentPageData();
-  const pageId = currentPage?._id;
-  const { data: pageInfo } = useSWRxPageInfo(pageId);
-
-  if (currentPage?.revision?._id == null || pageInfo?.latestRevisionId == null) {
-    return true;
-  }
-
-  return pageInfo.latestRevisionId === currentPage.revision._id;
-};
+import { mutate } from 'swr';
+mutate(['/page/info', pageId, shareLinkId, isGuestUser], newData, options);
 ```
 
-### メリット
-
-1. ✅ **データフローがクリーン**: SSR で自然に `meta` にデータが含まれる
-2. ✅ **既存の仕組みを活用**: `useSWRxPageInfo` の optimistic update を利用できる
-3. ✅ **最小限の変更**: 型定義に optional フィールドを追加するだけ
-4. ✅ **明示的**: `latestRevisionId` という名前で用途が明確
-5. ✅ **型安全**: TypeScript で厳密に型付けされる
-6. ✅ **スケーラブル**: 他の場所でも `pageInfo.latestRevisionId` を参照可能
-
-### 懸念点と解決
-
-#### 1. Core パッケージの型変更
-
-**懸念**: `@growi/core` の型定義を変更する影響範囲
-
-**解決**: `latestRevisionId?: string` (optional) にすることで、既存コードとの互換性を保つ
+**optimistic update のオプション**:
+- `{ revalidate: false }` - 再フェッチせず、キャッシュのみ更新
+- `mutate()` (引数なし) - 再フェッチ
+- `mutate(updater, options)` - updater 関数で部分更新
 
-#### 2. `useSWRxPageInfo` への依存
-
-**懸念**: 新しい依存関係が増える
-
-**解決**: `useSWRxPageInfo` は既に多くの場所で使用されており、標準的なパターン。追加の依存として問題なし。
+---
 
-#### 3. `pageInfoAtom` の実装方法
+### 🟡 優先度 中: PageStatusAlert の重複ロジック削除
 
-**懸念**: SWR と Jotai の橋渡しが複雑になる可能性
+**ファイル**: `src/client/components/PageStatusAlert.tsx`
 
-**解決案 A**: Hook 内で直接 `useSWRxPageInfo` を使う(シンプル)
-**解決案 B**: `pageInfoAtom` を作成して hydration する(一貫性)
+**現状**: 独自に `isRevisionOutdated` を計算している
+**提案**: `useIsRevisionOutdated()` を使用
 
-→ **推奨**: 解決案 A(シンプルさを優先)
+---
 
-### 削除できるコード
+### 🟢 優先度 低
 
-以下のコードは不要になる可能性があります:
+- テストコードの更新
+- `initLatestRevisionField` の役割ドキュメント化
 
-1. **`remoteRevisionId` への依存を削除**(オプション)
-   - `isLatestRevision` の判定に `remoteRevisionId` を使わなくなる
-   - ただし `useIsRevisionOutdated` では引き続き使用するため、完全削除はできない
+---
 
-2. **`initLatestRevisionField` の呼び出し**(一部のみ)
-   - `page-data-props.ts:202` の `initLatestRevisionField(revisionId)` は引き続き必要
-   - 理由: `?revisionId=xxx` の場合に `page.revision` を上書きするため
+## 📊 アーキテクチャの改善
 
-### 実装の優先順位
+### Before (問題のある状態)
 
-1. **Phase 1**: 型定義と `constructBasicPageInfo` の更新
-2. **Phase 2**: `useIsLatestRevision` の実装(hook 内で `useSWRxPageInfo` を使用)
-3. **Phase 3**: 動作確認とテスト
-4. **Phase 4**: デバッグログの削除と cleanup
+```
+┌─────────────────────┐
+│ latestRevisionAtom  │ ← atom(true) でハードコード(機能せず)
+└─────────────────────┘
+┌─────────────────────┐
+│ remoteRevisionIdAtom│ ← 複数の用途で混在(Socket.io更新 + 最新リビジョン保持)
+└─────────────────────┘
+```
 
-### 他のアプローチとの比較
+### After (改善後)
 
-| 項目 | `IPageInfoForEntity` に追加 | `remoteRevisionId` 活用 | `latestRevision` フィールド |
-|------|---------------------------|----------------------|---------------------------|
-| Schema 変更 | 不要 | 不要 | 必要 |
-| 型変更の影響 | 小(optional フィールド) | なし | なし |
-| データフロー | 既存の `meta` を活用 | 既存の hydration を活用 | 新規フィールド |
-| SSR での設定 | 自動(`constructBasicPageInfo`) | 手動(hydration) | 手動(`initLatestRevisionField`) |
-| クライアント側でのアクセス | `useSWRxPageInfo` | `remoteRevisionIdAtom` | 不可能(シリアライズされない) |
-| 保守性 | 高(明示的) | 中(既存の用途と混在) | 低(シリアライズ問題) |
-| 推奨度 | 🟢 **推奨** | 🟡 次点 | ❌ 不可 |
+```
+┌──────────────────────────────┐
+│ useSWRxPageInfo              │
+│  └─ data.latestRevisionId    │ ← SSR で自動設定、SWR でキャッシュ管理
+└──────────────────────────────┘
+        ↓
+┌──────────────────────────────┐
+│ useIsLatestRevision()        │ ← SWR ベース、汎用的な状態確認
+└──────────────────────────────┘
+        ↓
+┌──────────────────────────────┐
+│ useIsRevisionOutdated()      │ ← 「再fetch推奨」のメッセージ性
+│  + useIsViewingSpecificRevision│ ← URL パラメータを考慮
+└──────────────────────────────┘
+```
 
-### まとめ
+---
 
-**`IPageInfoForEntity` に `latestRevisionId` を追加するアプローチが最適**
+## ✅ メリット
 
-- データフローが自然で保守性が高い
-- 既存の設計パターンに沿っている
-- 実装の複雑さが最小限
-- 将来の拡張性がある
+1. **状態管理の簡素化**: Jotai atom を削減、SWR の既存インフラを活用
+2. **データフローの明確化**: SSR → SWR → hooks という一貫した流れ
+3. **意味論の改善**: `useIsRevisionOutdated` が「再fetch推奨」を正確に表現
+4. **保守性の向上**: URL パラメータ取得を `useRevisionIdFromUrl` に集約
+5. **型安全性**: `IPageInfoForEntity` で厳密に型付け