|
|
@@ -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` で厳密に型付け
|