コンテンツにスキップ

Blob Upload Stuck

  • .mutate("attach", { attachment: file }) は成功して返った。
  • 行は useLiveQueryFileRef を表示している。
  • outbox に保留中エントリがある。
  • しかし mutation はサーバーへ push されない。Devtools パネルは outboxDepth: 1+ を表示し、それが減らない。

連続プレフィックス push ルール

Section titled “連続プレフィックス push ルール”

plasma の push ループは 連続プレフィックス配信 を保証します。すなわち、 < N のすべての mutation が確認されるか (blob 依存があれば) その blob が アップロードされるまで、mutation N はサーバーに送られません。

mutation 1blob-upload-failed があると、mutation 2 は — たとえ mutation 2 自体に blob 依存がなくても — 永遠に outbox に留まります。

Devtools パネルを開くか、outbox を直接読みます。

// In devtools console:
const idb = await plasma.__internal.idb // via useDevtoolsSnapshot's schema
const tx = idb.transaction("_plasma_blob_uploads", "readonly")
const uploads = await tx.objectStore("_plasma_blob_uploads").getAll()
console.log(uploads)

各 upload レコードには state があります。

  • "queued" — 実行待ち
  • "uploading" — 現在 PUT 中
  • "present" — アップロード成功 (レコードはまもなくクリーンアップされる)
  • "failed" — リトライ枯渇、対応が必要

いずれかの upload が "failed" なら、それが原因です。

失敗が一時的だった場合 (ネットワークの瞬断、R2 が一瞬不調だった等):

await client.retryBlobUpload(mutationID)

upload レコードは新しい試行予算とともに queued に戻ります。

mutation が回復不可能な場合 (ユーザーがタブを閉じた、やはりその添付は 不要だと判断した等):

await client.discardMutation(mutationID)

outbox エントリと upload レコードの両方を削除します。optimistic ビューは 次の rebuildOptimistic で revert します。

onError を配線して mutation ID を公開します。

createPlasmaClient({
...,
onError: (err) => {
if (err.kind === "blob-upload-failed") {
setStuck((prev) => [...prev, err]) // { kind, mutationID, hash, cause }
}
},
})

スタックした各 upload を retry/discard ボタンとともにレンダリングします。

なぜ単にスキップして続行しないのか

Section titled “なぜ単にスキップして続行しないのか”

push の順序が保たれているのには理由があります。mutation 2 が mutation 1 の 効果に依存しているかもしれないからです。もし 1 がノートを insert し、2 が そのノートに画像を添付する場合、2 を先に配信すると auth.write に失敗します (ノートがまだ存在しないため)。

連続プレフィックスルールは、サーバーのビューをクライアントの mutation タイムラインと一貫させ続けます。アップロードされるまで blob をブロックするのは、 その帰結です。

よくある理由:

  • Worker に間違ったバケットが設定されている。 wrangler.jsonc の binding が、 実際に作成したバケットと一致しているか確認してください。
  • maxSize 上限を超えた。 カラムが file({ maxSize: 5 * 1024 * 1024 }) と 宣言されているのに、ユーザーが 20MB の画像を選んだ。クライアント側で desugarFileArgs がこれを捕捉するはずです — 捕捉しなかった場合、サーバー側の PUT が拒否します。
  • mimeAllowList の制約。 カラムが file({ mimeAllowList: ["image/*"] }) と 宣言されているのに、ユーザーが PDF を選んだ。
  • R2 のレート制限。 まれですが、高速にアップロードして R2 のクォータに 当たっている場合、リトライがいずれ解決します。
  • サーバー 500。 sync-handler.ts:handleBlobPut の何かが throw しました。 実際のエラーは wrangler tail で確認してください。