event-manual / chapter 14

コンテンツストア

@event/core@event/cms

createContentStore@event/core)が content/*.json の書き込みを一手に担う。3リポでほぼ同一だった実装のうち、楽観ロックと GitHub-fresh 読みを備えた ivs-cms 版を正典として採取した。

fs-dev / Octokit-prod の二重書き込み

sha256 楽観ロック protocol(5 ステップ)

「後勝ちで他人の編集を黙って消す」事故を封じる。プロトコルは以下の5段:

  1. hash の定義hash = sha256(直列化 JSON テキスト)。git blob SHA ではない(git 側の実装詳細に依存しない)
  2. 読みreadResourceWithHash(name, fallback){ data, hash } を返す。クライアントはこの hash を ETag として保持する
  3. 書きwriteResource(name, data, { expectedHash })。書き込み直前に現在値を再 hash し、不一致なら { ok: false, code: "conflict" } を返して上書き拒否する
  4. 成功時の hash 返却 — 戻り値の hash は「今書いた内容」の sha256。次の保存の基準はこの値に確実に一致する
  5. 本番の読みは GitHub 最新 — 本番の fs はデプロイ時点で凍結されたバンドルなので、readResourceWithHash は prod では GitHub HEAD を先に読む(失敗時のみ fs fallback)

ステップ 4・5 が本番の重要不変条件「読みの hash 基準と書きの競合判定基準は同じソースであること」を守る。これが無いと、デプロイ後最初の commit の時点で「fs(凍結バンドル)の hash」と「GitHub HEAD の hash」が乖離し、保存が永久に 409 する既知バグになる(第46章)。

conflict 時のユーザー向けメッセージは固定文言で、UI はそのまま表示すればよい:

> 他のセッションが先に更新しました。ページを再読み込みして最新を取得してから再編集してください。

stale sha リトライ

sync 等の連続書き戻しでは、別 commit 直後に stale な git sha で update すると GitHub が 409/422 を返す。expectedHash によるロックをかけていない書き込みに限り、最新 sha を取り直して最大3回リトライする。ロックあり時の 409 は本物の競合なのでリトライしない。

If-Match REST route

admin editor 向けには createCmsRouteHandlers@event/cms)が同じ protocol を HTTP に写像する:

// app/api/admin/[resource]/route.ts
const h = createCmsRouteHandlers({ registry, store, isAuthed, afterWrite });
export const GET = h.GET;
export const PUT = h.PUT;
メソッド挙動
GET{ data, hash, kind, schema } + ETag: <hash>
PUTIf-Match: <sha256> で楽観ロック。conflict は 409、検証エラーは 400、成功時 ETag = 「今コミットした内容」の hash

PUT は body のシェイプを吸収し(list は配列 or {items}、singleton は {value} or 平坦 object)、validatePayloadbeforeWrite(last-writer-wins のタイムスタンプ刻印等)→ writeResourceafterWrite(revalidatePath/Tag・KV ミラー・write-through)の順に通す。next/cache への依存はアプリ側 hook に押し出してあり、package は next 非依存を保つ。

client 対向は putResource(resource, body, expectedHash)@event/cmsclient.ts)で、409 → code: "conflict"、401 → code: "unauthorized" の型付き結果を返す。

KV ミラー

保存反映を rebuild を待たず公開側へ即時化したい場合、afterWrite で KV へミラーする。セッションカード overlay は @event/session-card/persist がこのパターンを内蔵している(第19章)。KV は best-effort であり、失敗しても content には保存済み — リビルドで必ず追いつく。