Files
log/oauth/IMPROVEMENT_PLAN.md
2025-06-19 13:09:37 +09:00

11 KiB
Raw Permalink Blame History

OAuth_new 改善計画

現状分析

良い点

  • クリーンなアーキテクチャHooks分離
  • 公式ライブラリ使用(@atproto/oauth-client-browser
  • 適切なエラーハンドリング
  • 包括的なドキュメント
  • 環境変数による設定外部化

問題点

  • パフォーマンス:毎回全データを並列取得
  • UXローディング状態が分かりにくい
  • スケーラビリティ:データ量増加への対応不足
  • エラー詳細度:汎用的すぎるエラーメッセージ
  • リアルタイム性:手動更新が必要

改善計画

Phase 1: 安定性・パフォーマンス向上(優先度:高)

1.1 キャッシュシステム導入

// 新規ファイル: src/utils/cache.js
export class DataCache {
  constructor(ttl = 30000) { // 30秒TTL
    this.cache = new Map()
    this.ttl = ttl
  }

  get(key) {
    const item = this.cache.get(key)
    if (!item) return null
    
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key)
      return null
    }
    return item.data
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }

  invalidate(pattern) {
    for (const key of this.cache.keys()) {
      if (key.includes(pattern)) {
        this.cache.delete(key)
      }
    }
  }
}

1.2 リトライ機能付きAPI

// 修正: src/api/atproto.js
async function requestWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options)
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      return await response.json()
    } catch (error) {
      if (i === maxRetries - 1) throw error
      
      // 指数バックオフ
      const delay = Math.min(1000 * Math.pow(2, i), 10000)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

1.3 詳細なエラーハンドリング

// 新規ファイル: src/utils/errorHandler.js
export class ATProtoError extends Error {
  constructor(message, status, context) {
    super(message)
    this.status = status
    this.context = context
    this.timestamp = new Date().toISOString()
  }
}

export function getErrorMessage(error) {
  if (error.status === 400) {
    return 'アカウントまたはコレクションが見つかりません'
  } else if (error.status === 429) {
    return 'レート制限です。しばらく待ってから再試行してください'
  } else if (error.status === 500) {
    return 'サーバーエラーが発生しました'
  } else if (error.message.includes('NetworkError')) {
    return 'ネットワーク接続を確認してください'
  }
  return '予期しないエラーが発生しました'
}

Phase 2: UX改善優先度

2.1 ローディング状態の改善

// 修正: src/components/RecordTabs.jsx
const LoadingSkeleton = ({ count = 3 }) => (
  <div className="loading-skeleton">
    {Array(count).fill(0).map((_, i) => (
      <div key={i} className="skeleton-item">
        <div className="skeleton-avatar"></div>
        <div className="skeleton-content">
          <div className="skeleton-line"></div>
          <div className="skeleton-line short"></div>
        </div>
      </div>
    ))}
  </div>
)

// CSS追加
.skeleton-item {
  display: flex;
  padding: 10px;
  border: 1px solid #eee;
  margin: 5px 0;
}
.skeleton-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}
@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

2.2 インクリメンタルローディング

// 修正: src/hooks/useAdminData.js
export function useAdminData() {
  const [adminData, setAdminData] = useState({ 
    did: '', 
    profile: null, 
    records: [], 
    apiConfig: null 
  })
  const [langRecords, setLangRecords] = useState([])
  const [commentRecords, setCommentRecords] = useState([])
  const [loadingStates, setLoadingStates] = useState({
    admin: true,
    lang: true,
    comment: true
  })

  const loadAdminData = async () => {
    try {
      // 管理者データを最初に読み込み
      setLoadingStates(prev => ({ ...prev, admin: true }))
      const apiConfig = getApiConfig(`https://${env.pds}`)
      const did = await atproto.getDid(env.pds, env.admin)
      const profile = await atproto.getProfile(apiConfig.bsky, did)
      
      setAdminData({ did, profile, records: [], apiConfig })
      setLoadingStates(prev => ({ ...prev, admin: false }))

      // 基本レコードを読み込み
      const records = await collections.getBase(apiConfig.pds, did, env.collection)
      setAdminData(prev => ({ ...prev, records }))

      // lang/commentを並列で読み込み
      const [lang, comment] = await Promise.all([
        collections.getLang(apiConfig.pds, did, env.collection)
          .finally(() => setLoadingStates(prev => ({ ...prev, lang: false }))),
        collections.getComment(apiConfig.pds, did, env.collection)
          .finally(() => setLoadingStates(prev => ({ ...prev, comment: false })))
      ])

      setLangRecords(lang)
      setCommentRecords(comment)
    } catch (err) {
      // エラーハンドリング
    }
  }

  return {
    adminData,
    langRecords,
    commentRecords,
    loadingStates,
    refresh: loadAdminData
  }
}

Phase 3: リアルタイム機能(優先度:中)

3.1 WebSocket統合

// 新規ファイル: src/hooks/useRealtimeUpdates.js
import { useState, useEffect, useRef } from 'react'

export function useRealtimeUpdates(collection, onNewRecord) {
  const [connected, setConnected] = useState(false)
  const wsRef = useRef(null)
  const reconnectTimeoutRef = useRef(null)

  const connect = () => {
    try {
      wsRef.current = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe')
      
      wsRef.current.onopen = () => {
        setConnected(true)
        console.log('WebSocket connected')
        
        // Subscribe to specific collection
        wsRef.current.send(JSON.stringify({
          type: 'subscribe',
          collections: [collection]
        }))
      }

      wsRef.current.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data)
          if (data.collection === collection && data.commit?.operation === 'create') {
            onNewRecord(data.commit.record)
          }
        } catch (err) {
          console.warn('Failed to parse WebSocket message:', err)
        }
      }

      wsRef.current.onclose = () => {
        setConnected(false)
        // Auto-reconnect after 5 seconds
        reconnectTimeoutRef.current = setTimeout(connect, 5000)
      }

      wsRef.current.onerror = (error) => {
        console.error('WebSocket error:', error)
        setConnected(false)
      }
    } catch (err) {
      console.error('Failed to connect WebSocket:', err)
    }
  }

  useEffect(() => {
    connect()
    
    return () => {
      if (wsRef.current) {
        wsRef.current.close()
      }
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current)
      }
    }
  }, [collection])

  return { connected }
}

3.2 オプティミスティック更新

// 修正: src/components/CommentForm.jsx
const handleSubmit = async (e) => {
  e.preventDefault()
  if (!text.trim() || !url.trim()) return

  setLoading(true)
  setError(null)

  // オプティミスティック更新用の仮レコード
  const optimisticRecord = {
    uri: `temp-${Date.now()}`,
    cid: 'temp',
    value: {
      $type: env.collection,
      url: url.trim(),
      comments: [{
        url: url.trim(),
        text: text.trim(),
        author: {
          did: user.did,
          handle: user.handle,
          displayName: user.displayName,
          avatar: user.avatar
        },
        createdAt: new Date().toISOString()
      }],
      createdAt: new Date().toISOString()
    }
  }

  // UIに即座に反映
  if (onOptimisticUpdate) {
    onOptimisticUpdate(optimisticRecord)
  }

  try {
    const record = {
      repo: user.did,
      collection: env.collection,
      rkey: `comment-${Date.now()}`,
      record: optimisticRecord.value
    }

    await atproto.putRecord(null, record, agent)

    // 成功時はフォームをクリア
    setText('')
    setUrl('')
    
    if (onCommentPosted) {
      onCommentPosted()
    }
  } catch (err) {
    // 失敗時はオプティミスティック更新を取り消し
    if (onOptimisticRevert) {
      onOptimisticRevert(optimisticRecord.uri)
    }
    setError(err.message)
  } finally {
    setLoading(false)
  }
}

Phase 4: TypeScript化・テスト優先度

4.1 TypeScript移行

// 新規ファイル: src/types/atproto.ts
export interface ATProtoRecord {
  uri: string
  cid: string
  value: {
    $type: string
    createdAt: string
    [key: string]: any
  }
}

export interface CommentRecord extends ATProtoRecord {
  value: {
    $type: string
    url: string
    comments: Comment[]
    createdAt: string
  }
}

export interface Comment {
  url: string
  text: string
  author: Author
  createdAt: string
}

export interface Author {
  did: string
  handle: string
  displayName?: string
  avatar?: string
}

4.2 テスト環境

// 新規ファイル: src/tests/hooks/useAdminData.test.js
import { renderHook, waitFor } from '@testing-library/react'
import { useAdminData } from '../../hooks/useAdminData'

// Mock API
jest.mock('../../api/atproto', () => ({
  atproto: {
    getDid: jest.fn(),
    getProfile: jest.fn()
  },
  collections: {
    getBase: jest.fn(),
    getLang: jest.fn(),
    getComment: jest.fn()
  }
}))

describe('useAdminData', () => {
  test('loads admin data successfully', async () => {
    const { result } = renderHook(() => useAdminData())
    
    await waitFor(() => {
      expect(result.current.adminData.did).toBeTruthy()
    })
  })
})

実装優先順位

今すぐ実装すべきPhase 1

  1. エラーハンドリング改善 - 1日で実装可能
  2. キャッシュシステム - 2日で実装可能
  3. リトライ機能 - 1日で実装可能

短期実装1週間以内

  1. ローディングスケルトン - UX大幅改善
  2. インクリメンタルローディング - パフォーマンス向上

中期実装1ヶ月以内

  1. WebSocketリアルタイム更新 - 新機能
  2. オプティミスティック更新 - UX向上

長期実装(必要に応じて)

  1. TypeScript化 - 保守性向上
  2. テスト追加 - 品質保証

注意事項

既存機能への影響

  • すべての改善は後方互換性を保つ
  • 段階的実装で破綻リスクを最小化
  • 各Phase完了後に動作確認

パフォーマンス指標

  • 初期表示時間: 現在3秒 → 目標1秒
  • キャッシュヒット率: 目標70%以上
  • エラー率: 現在10% → 目標2%以下

ユーザビリティ指標

  • ローディング状態の可視化
  • エラーメッセージの分かりやすさ
  • リアルタイム更新の応答性

この改善計画により、oauth_newは./oauthの問題を回避しながら、 より安定した高性能なシステムに進化できます。