メンタルモデル
コードをこれ以上読み進める前に、この 3 つのレイヤーを頭に入れて ください。名前を言えるようになるだけで、このドキュメントの他の ページがすべて理解しやすくなります。
レイヤー 1 — 宣言
Section titled “レイヤー 1 — 宣言”あなたが書くのは次の 2 つだけです。
- schema — テーブル、そのカラム、それぞれの型。
- mutator の集合 — 行を変更する関数。
どちらもごく普通の TypeScript ファイルに置かれ、どちらも 2 つの 異なるランタイムから import されます。
// src/schema.ts — 1 つのファイルを両サイドが読む。export const todos = table("todos", { id: id(), title: text(), done: int().default(0),})
export const mutators = defineMutators<typeof schema, Ctx>()({ markDone: async ({ db, args }) => { await db.update(todos).set({ done: 1 }).where(eq(todos.id, args.id)) },})client 専用の schema も server 専用の schema もありません。client 専用の mutator も server 専用の mutator もありません。それぞれ 1 つ だけです。
レイヤー 2 — 2 つのランタイム
Section titled “レイヤー 2 — 2 つのランタイム”同じファイルが 2 つのエンジン上で動きます。
-
ブラウザエンジン (
@sh1n4ps/plasma-client) — IndexedDB に対して optimistic に読み書きします。client.mutate("markDone", { id: "t1" })を呼ぶと、mutator は ローカルの IDB ストアに対して実行され、即座に返り、次の React tick で UI が更新されます。 -
Worker エンジン (
@sh1n4ps/plasma-server) — ブラウザが mutation をワイヤー越しに push した後、同じ mutator を D1(または Hyperdrive 経由の Postgres)に対して canonical に 実行します。
どちらのエンジンも db.update(todos).set({...}).where(...) を、
それぞれのサイドのネイティブなストレージ層へ変換します。どちらの
エンジンも、前進するために相手がオンラインである必要はありません。
レイヤー 3 — sync ループ
Section titled “レイヤー 3 — sync ループ”3 つ目のレイヤーが 2 つのランタイムを縫い合わせます。これは完全に plasma の責務です。あなたがこれを書くことは一切ありません。
sync ループは次のことを行います。
- サーバーにまだ確認されていないすべての mutation を、ブラウザ 側の outbox に保持します。
- その outbox エントリを
POST /sync/pushへ push します。 Worker エンジンが mutator を canonical に実行します。 GET /sync/pull経由でサーバーの change log を pull します。 client は確認済みの変更を自分の base ストアに適用し、その上に 未確認の outbox エントリを再生します。- ローカルとリモートの両方が同じ行を編集したとき、サーバー権威の 状態を基準に rebase します。
- ネットワークエラー・5xx・429 のときは指数バックオフで retry します。
- 関連する変更が起きるたびに、購読中の live query へ 通知 します。
Push, pull, rebase のページで 各ステップを詳しく解説します。
各レイヤーがどこに置かれるか
Section titled “各レイヤーがどこに置かれるか”┌────────────────────────────────────────────────────────────┐│ Layer 1 — declarations (isomorphic TypeScript) ││ ││ schema.ts ────────────────► @sh1n4ps/plasma-core ││ mutators.ts ────────────────► (no runtime deps) │└─────────────────┬─────────────────────┬────────────────────┘ │ │ ▼ ▼┌───────────────────────────┐ ┌───────────────────────────┐│ Layer 2 — browser engine │ │ Layer 2 — Worker engine ││ @sh1n4ps/plasma-client │ │ @sh1n4ps/plasma-server ││ │ │ ││ IndexedDB (optimistic) │ │ D1 / Postgres (canonical)││ Live queries + IVM │ │ Trigger-emitted change ││ Optimistic outbox │ │ log ││ Blob local cache │ │ R2 blob storage adapter │└───────────────┬───────────┘ └───────────────┬───────────┘ │ │ │ Layer 3 — sync loop │ │ (owned by plasma) │ │ │ └──► POST /sync/push ────────┤ │◄── GET /sync/pull ────────┤ │◄── WebSocket /sync/ws ──────┤ │ PUT /sync/blob/:hash ────┤ │ GET /sync/blob/:hash ────┤plasma とは何か(そして何でないか)
Section titled “plasma とは何か(そして何でないか)”plasma は、アプリの状態とその永続化の間に位置するレイヤーです。 オフライン対応 + リアルタイム sync のために通常支払うことになる コスト、すなわち outbox、rebase、retry ロジック、change log、live query の無効化、ファイルアップロードの状態機械を取り除きます。
plasma は、UI ローカルの一時的な状態(フォーム入力、ホバー、 「ユーザーがモーダルを閉じたか?」)のための状態ライブラリでは ありません。それらは引き続き React の state やお気に入りの atom ライブラリに属します。plasma は、ページリロードを生き延びる べきデータ、そして時にはサーバーへ届くべきデータのためのもの です。
次に読むもの
Section titled “次に読むもの”- Schema と mutator — isomorphic な型が実際にどう流れるか
- Optimistic vs canonical —
mutate()が返った後に何が起きるか - Push, pull, rebase — sync ループの詳細