搜尋完整文件

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

選擇 開啟
English
瀏覽文件

Activity · 第一個請求

發佈貼文並讀取時間軸

這條路徑會為簽署請求的 actor 建立一筆 post,再讀取同一位 actor 的 materialized home timeline。請在能安全存取 gateway secret 的 host server 執行。

1. 序列化並簽署 post

const path = "/api/v1/posts";
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
  idempotency_key: "match:match_1842:published",
  verb: "matched",
  object_ref: { type: "match", id: "match_1842" },
  body: "Won the summer qualifier",
  metadata: { season: "2026-summer" },
  render_refs: {},
  visibility: "followers"
});

const signature = signActivityRequest({
  secret: process.env.ACTIVITY_GATEWAY_SECRET,
  timestamp,
  method: "POST",
  path,
  tenantId,
  actorId,
  body
});

verb 是必填欄位。每次邏輯發佈都應設定穩定的 idempotency_keyobject_refmetadatarender_refs 預設為空 object。visibility 可以是 publicfollowersprivate

2. 發佈 post

const response = await fetch(`${activityUrl}${path}`, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-tenant-id": tenantId,
    "x-actor-id": actorId,
    "x-feedaas-timestamp": timestamp,
    "x-feedaas-signature": signature
  },
  body
});

if (!response.ok) throw new Error(await response.text());
const { post } = await response.json();

成功寫入會回傳 201 Created。Activity 從簽署 header 取得 actor_id,建立來源 post 與作者自己的 self timeline entry,並在 visibility 允許時排入 follower fan-out。

3. 讀取首頁時間軸

讀取請求必須單獨簽署。它的 method、path、query string、body hash 與 timestamp 都和發佈請求不同。

const path = "/api/v1/timeline/home";
const query = "limit=20";
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = signActivityRequest({
  secret: process.env.ACTIVITY_GATEWAY_SECRET,
  timestamp,
  method: "GET",
  path,
  query,
  tenantId,
  actorId,
  body: ""
});

const response = await fetch(`${activityUrl}${path}?${query}`, {
  headers: {
    "x-tenant-id": tenantId,
    "x-actor-id": actorId,
    "x-feedaas-timestamp": timestamp,
    "x-feedaas-signature": signature
  }
});

if (!response.ok) throw new Error(await response.text());
const timeline = await response.json();

Response 會包含 materialized entry 與 opaque next cursor:

{
  "data": [
    {
      "id": "timeline-entry-uuid",
      "viewer_id": "player_42",
      "reason": "self",
      "ranked_at": "2026-07-14T04:10:00Z",
      "item": {
        "post": {
          "id": "post-uuid",
          "actor_id": "player_42",
          "verb": "matched",
          "visibility": "followers"
        },
        "stats": {
          "like_count": 0,
          "comment_count": 0,
          "reply_count": 0,
          "repost_count": 0,
          "share_count": 0
        },
        "viewer": {
          "has_liked": false,
          "follows_author": true,
          "can_delete": true
        }
      }
    }
  ],
  "page": { "next_cursor": null }
}

實際的 postviewer object 會包含 OpenAPI 契約定義的完整欄位。這個範例只保留用來說明 response layer 的部分。

同步完成的部分

作者自己的 self timeline entry 會和 post 一起建立。Follower fan-out 會非同步執行,並在 job 重試後收斂。Post 成功回傳,不代表每一位 follower 的時間軸都已更新。

PostStats 同樣是非同步產生的衍生資料,不能作為 engagement 的 canonical record。

重試邊界

重試同一次邏輯發佈時,請沿用相同的 idempotency_key。在同一個 tenant 與 actor 範圍內,Activity 會回傳原本的 post,不會再次建立 timeline entry、fan-out job 或 outbox event。新的發佈必須使用新的 key。

Timeline read 可以用新簽章安全重試。page.next_cursor 必須視為 opaque value,並將完全相同的 encoded value 放入 request URL 與簽署的 query string。

下一步:理解排序、fan-out、cursor 與重建