搜尋完整文件

輸入關鍵字,例如 idempotency、proposal lifecycle 或權限。

選擇 開啟
English
瀏覽文件

Activity · 時間軸

排序、fan-out 與重建

Activity 分開儲存 canonical post 與 viewer timeline。Timeline entry 是衍生投影,可能在 post 寫入後才出現,但重試與修復會讓每組 viewer 與 post 收斂成一筆 entry。

Publish transaction
  ├─ canonical Post
  ├─ author self entry
  ├─ initial PostStats
  ├─ fan-out job
  └─ outbox event

Fan-out worker
  └─ follower TimelineEntry rows

Private post 不會建立 fan-out job,但作者自己的 self entry 仍會存在。

Timeline 排序

Home timeline 採用反向時間排序。Activity 依照以下欄位排序:

ORDER BY ranked_at DESC, id DESC

ranked_at 取自來源 post 的 published_at。多筆 entry 使用相同 timestamp 時,timeline entry UUID 是穩定的 tie-breaker。

這是 materialized chronological order,不是 recommendation score。未明確委派給 Activity 的 ranking policy 仍由 host 負責。

Cursor 分頁

第一頁可以帶選填的 limit

GET /api/v1/timeline/home?limit=50

公開契約接受 1 到 100,預設值為 50。還有下一頁時,page.next_cursor 會包含 opaque URL-safe value。

{
  "data": [],
  "page": {
    "next_cursor": "eyJhdCI6IjIwMjYtMDctMTRUMDQ6MTA6MDBaIiwiaWQiOiIuLi4ifQ"
  }
}

下一個請求必須原樣帶入這個值:

GET /api/v1/timeline/home?limit=50&cursor=<next_cursor>

請根據實際送出的 query string 簽章。不要解碼 cursor、修改 padding,或把它用於不同的分頁契約。

無效 cursor 會回傳 400 invalid_cursor。最後一頁則回傳 "next_cursor": null

穩定的分頁邊界

Cursor 同時記錄最後一筆 entry 的排序時間與 UUID。下一頁會選取小於該組合的 entry:

ranked_at < cursor time
or
ranked_at = cursor time and id < cursor id

這個 tie-break 可以避免多筆 entry 使用相同 ranked_at 時,在穩定的結果集合內重複或跳過資料。

Timeline read 不是 snapshot。新 post 或 policy change 可能在兩次請求之間改變 materialized set。Consumer 應把一串 cursor 視為一次向後分頁工作階段,不是永久的 archive position。

Fan-out 收斂

Publish transaction 會建立作者的 self entry,並排入 follower fan-out。Publish response 剛回傳時,follower 可能還看不到該 post。

每筆 follower projection 都使用 upsert,並由以下 database identity 保證唯一:

(tenant_id, viewer_id, post_id)

重複 job 可能執行,但只會更新同一筆 timeline entry,不會建立另一筆。公開保證的是 projection uniqueness,不是 job uniqueness。

Fan-out 前,worker 會讀取目前的 post。Post 已經符合以下任一條件時,worker 會略過 materialization:

  • 已 soft delete
  • Moderation state 不是 visible
  • Visibility 是 private

Worker 選擇 viewer 時,也會使用目前有效的 follow graph 與 block state。

Rebuild 會還原什麼

內部 repair worker 每次重建一位 viewer。它會在同一個 database transaction 內:

  1. 刪除該 viewer 的 following entry。
  2. 依照目前有效的 follow 讀取 canonical post。
  3. 排除 private、deleted、hidden、blocked 與 non-visible post。
  4. 為每筆符合條件的 post 重建一筆 following entry。

Rebuild 不會刪除 viewer 自己的 self entry。重建的 entry 會使用來源 post 的 published_at 作為 ranked_at,因此能保留正常的讀取順序。

目前的 rebuild primitive 是內部 Oban worker,沒有出現在公開 /api/v1 契約裡。Host 現在無法透過 customer HTTP API 要求重建。

保證矩陣

狀況目前行為
Publish 成功Canonical post 與作者的 self entry 已 commit
Follower 立刻讀取新 entry 可能仍在等待 fan-out
Fan-out job 執行兩次只保留一筆 viewer-post entry
Fan-out 前 post 已刪除Worker 略過 materialization
Cursor read 重試重新簽署請求後可安全讀取
Publish timeout不保證 retry 不會建立重複 post
Viewer projection 發生 drift內部 rebuild 可重建 following entry

驗證邊界

Repository test 涵蓋相同 timestamp 的分頁、重複 fan-out 執行、刪除或內容管理後的 stale fan-out、private post 排除,以及 viewer rebuild。FeedCore TLA+ model 會在其建模範圍與 fairness assumption 內檢查 tenant safety、projection uniqueness、visibility safety、idempotent job effect 與 eventual convergence。

這些 proof 不代表已證明 production fan-out latency、queue capacity、recovery time objective 或 public rebuild operation。這些仍是產品化工作。

下一步:套用可見性與 host-owned policy