You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
sequenceDiagram
participant Cron as Cron Trigger
participant Orch as Orchestrator Worker
participant D1 as D1 Database
participant Crawler as Crawler Worker
participant RSS as RSS Server
participant R2 as R2 Storage
participant KV as Workers KV
Cron->>Orch: 15分毎に起動
Orch->>D1: SELECT active feeds
D1-->>Orch: フィード一覧(100件)
loop バッチ処理(6件ずつ)
Orch->>Crawler: crawlFeeds(batch)
par 並列取得(6フィード)
Crawler->>RSS: GET /feed.xml<br/>(If-None-Match, If-Modified-Since)
RSS-->>Crawler: 200 OK or 304 Not Modified
alt 200 OK(更新あり)
Crawler->>Crawler: 基本的なXML検証
Crawler->>Crawler: メタデータ抽出<br/>(lastBuildDate, title, description)
Crawler->>R2: PUT feeds/{hash}/feed.xml
R2-->>Crawler: OK
Crawler->>D1: UPDATE feeds<br/>(etag, last_modified, feed_last_updated, title, error_count=0)
Crawler->>KV: DELETE feed:{id}<br/>(キャッシュ無効化)
else 304 Not Modified
Crawler->>D1: UPDATE feeds.last_fetched
else エラー
Crawler->>D1: UPDATE feeds<br/>(error_count++, error_message)
end
end
end
Orch->>Orch: メトリクス収集
Loading
2.2 フロー詳細
Step 1: Cronトリガー発火
// 15分毎に実行crons=["*/15 * * * *"]
Step 2: Orchestratorによるタスク調整
-- アクティブなフィード取得(最終取得日時が古い順)SELECT id, url, etag, last_modified
FROM feeds
WHERE active = TRUE
ORDER BY last_fetched ASCLIMIT100
// フィード一覧取得(超高速、3-5ms)constfeeds=awaitenv.DB.prepare(`SELECT f.id, f.url, f.title, f.description, f.feed_last_updated, f.last_fetched, s.category, s.title as custom_title, frs.last_read_at, CASE WHEN f.feed_last_updated > COALESCE(frs.last_read_at, '1970-01-01') THEN 1 ELSE 0 END as has_unread FROM subscriptions s JOIN feeds f ON s.feed_id = f.id LEFT JOIN feed_read_status frs ON frs.feed_id = f.id AND frs.user_id = ? WHERE s.user_id = ? ORDER BY s.position`).bind(userId).all();returnc.json({feeds: feeds.results});
特徴:
XMLパース不要(Crawlerが抽出したメタデータを使用)
D1クエリのみで完結
レスポンス時間: 3-5ms(GROUP BYが不要で高速)
未読判定: feed_last_updatedとlast_read_atを比較
フィードレベルの既読管理
アイテムごとの既読管理は行わない
フィード全体を既読としてマーク
シンプルで高速な設計
3.2 フィードアイテム取得フロー
GET /api/feeds/:id/items - XMLパース必要
3.2.1 シーケンス図
sequenceDiagram
participant Client as React Client
participant API as API Worker
participant KV as Workers KV
participant R2 as R2 Storage
participant Parser as Parser Service
participant D1 as D1 Database
Client->>API: GET /api/feeds/:id/items
API->>API: JWT認証
API->>KV: GET feed:{id}
alt キャッシュヒット
KV-->>API: パース済みフィード
API->>D1: SELECT feed_read_status<br/>WHERE user_id=? AND feed_id=?
D1-->>API: フィード既読状態
API-->>Client: フィードアイテム + 既読フラグ
else キャッシュミス
KV-->>API: null
API->>D1: SELECT * FROM feeds WHERE id=?
D1-->>API: フィードメタデータ
API->>R2: GET feeds/{hash}/feed.xml
R2-->>API: 生XML
API->>Parser: parseXML(xml, maxItems: 200)
Parser->>Parser: fast-xml-parser実行(最新200件)
Parser->>Parser: RSS/Atom正規化
Parser-->>API: パース済みフィード
API->>KV: PUT feed:{id}<br/>(TTL: 3600秒)
API->>D1: SELECT feed_read_status
D1-->>API: フィード既読状態
API-->>Client: フィードアイテム + 既読フラグ
end
// L2: R2から生XML取得constfeedData=awaitenv.DB.prepare('SELECT * FROM feeds WHERE id = ?').bind(feedId).first();constr2Key=awaitgetLatestR2Key(feedId,env);constfeedObject=awaitenv.FEED_STORAGE.get(r2Key);constcontent=awaitfeedObject.text();constcontentType=feedObject.httpMetadata?.contentType||'';
// フィードの既読状態を取得(1レコードのみ)constreadStatus=awaitenv.DB.prepare(`SELECT last_read_at FROM feed_read_status WHERE user_id = ? AND feed_id = ?`).bind(userId,feedId).first();// フィード全体の未読判定consthasUnread=readStatus===null||(parsedFeed.metadata.lastUpdated&&readStatus.last_read_at<parsedFeed.metadata.lastUpdated);returnc.json({feed: parsedFeed.metadata,items: parsedFeed.items,total: parsedFeed.items.length,hasUnread: hasUnread,lastReadAt: readStatus?.last_read_at||null});
フィードを既読にする
// POST /api/feeds/:id/mark-readawaitenv.DB.prepare(`INSERT INTO feed_read_status (user_id, feed_id, last_read_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, feed_id) DO UPDATE SET last_read_at = CURRENT_TIMESTAMP`).bind(userId,feedId).run();
設計の利点
超高速: 1レコードのみのクエリ(1-2ms)
ストレージ削減: アイテムごとの既読レコード不要
シンプル: フィード単位の管理のみ
Workers CPU時間: ほぼゼロ(既読付与処理が不要)
未読判定
フィード一覧での未読判定は、feed_last_updatedとlast_read_atを比較:
CASE
WHEN f.feed_last_updated> COALESCE(frs.last_read_at, '1970-01-01')
THEN 1 ELSE 0
END as hasUnread
4. APIスキーマ
4.1 エンドポイント一覧
フィード管理
Method
Endpoint
説明
認証
GET
/api/feeds
フィード一覧取得
Required
GET
/api/feeds/updated
更新されたフィード一覧取得
Required
POST
/api/feeds
フィード追加
Required
DELETE
/api/feeds/:id
フィード削除
Required
GET
/api/feeds/:id/items
フィードアイテム取得
Required
既読管理(フィードレベル)
Method
Endpoint
説明
認証
POST
/api/feeds/:id/mark-read
フィードを既読にマーク
Required
Note: アイテムレベルの既読管理は行いません。フィード全体を既読としてマークします。
OPMLインポート/エクスポート
Method
Endpoint
説明
認証
POST
/api/import/opml
OPMLインポート
Required
GET
/api/export/opml
OPMLエクスポート
Required
ユーザー管理
Method
Endpoint
説明
認証
POST
/api/auth/login
ログイン
-
POST
/api/auth/logout
ログアウト
Required
GET
/api/user/preferences
設定取得
Required
PUT
/api/user/preferences
設定更新
Required
4.2 リクエスト/レスポンス例
GET /api/feeds
Request
GET /api/feeds HTTP/1.1Authorization: Bearer {jwt_token}
-- ユーザーの最終既読時刻より後に更新されたフィードSELECTf.id,
f.title,
f.url,
f.feed_last_updated,
MAX(rs.read_at) AS last_checked_by_user,
CASE
WHEN f.feed_last_updated> COALESCE(frs.last_read_at, '1970-01-01')
THEN 1 ELSE 0
END AS hasUnread
FROM subscriptions s
JOIN feeds f ONs.feed_id=f.idLEFT JOIN feed_read_status frs ONfrs.feed_id=f.idANDfrs.user_id= ?
WHEREs.user_id= ?
GROUP BYf.idHAVINGf.feed_last_updated> COALESCE(frs.last_read_at, '1970-01-01')
ORDER BYf.feed_last_updatedDESC;
GET /api/feeds/:id/items
フィードのアイテム一覧を取得します。最大200件を返します。
Request
GET /api/feeds/1/items HTTP/1.1Authorization: Bearer {jwt_token}
SELECTf.id,
f.title,
f.url,
f.feed_last_updated,
s.category,
frs.last_read_at,
CASE
WHEN f.feed_last_updated> COALESCE(frs.last_read_at, '1970-01-01')
THEN 1 ELSE 0
END AS hasUnread
FROM subscriptions s
JOIN feeds f ONs.feed_id=f.idLEFT JOIN feed_read_status frs ONfrs.feed_id=f.idANDfrs.user_id= ?
WHEREs.user_id= ?
GROUP BYf.idORDER BYs.position;
フィードを既読にマーク
INSERT INTO feed_read_status (user_id, feed_id, last_read_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, feed_id)
DO UPDATESET last_read_at =CURRENT_TIMESTAMP;
// OPMLインポートのバッチ処理asyncfunctionimportOPML(userId: string,feeds: Feed[]){conststatements=[];// フィード挿入for(constfeedoffeeds){statements.push(db.prepare('INSERT OR IGNORE INTO feeds (url, title) VALUES (?, ?)').bind(feed.url,feed.title));}// 購読追加for(constfeedoffeeds){statements.push(db.prepare('INSERT OR IGNORE INTO subscriptions (user_id, feed_id) '+'SELECT ?, id FROM feeds WHERE url = ?').bind(userId,feed.url));}// バッチ実行(オールオアナッシング)constresults=awaitdb.batch(statements);returnresults;}
8.2 データクリーンアップ
// 定期クリーンアップジョブasyncfunctioncleanupOldData(env: Env){// 30日以上前の既読記録を削除awaitenv.DB.prepare('DELETE FROM read_status WHERE read_at < datetime("now", "-30 days")').run();// エラー続きのフィードを無効化awaitenv.DB.prepare('UPDATE feeds SET active = FALSE WHERE error_count > 10').run();// 使われていないフィードを削除awaitenv.DB.prepare(`DELETE FROM feeds WHERE id NOT IN (SELECT DISTINCT feed_id FROM subscriptions) AND created_at < datetime("now", "-7 days")`).run();}