コンテンツにスキップ

メンタルモデル

コードをこれ以上読み進める前に、この 3 つのレイヤーを頭に入れて ください。名前を言えるようになるだけで、このドキュメントの他の ページがすべて理解しやすくなります。

あなたが書くのは次の 2 つだけです。

  1. schema — テーブル、そのカラム、それぞれの型。
  2. 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 つ目のレイヤーが 2 つのランタイムを縫い合わせます。これは完全に plasma の責務です。あなたがこれを書くことは一切ありません。

sync ループは次のことを行います。

  1. サーバーにまだ確認されていないすべての mutation を、ブラウザ 側の outbox に保持します。
  2. その outbox エントリを POST /sync/pushpush します。 Worker エンジンが mutator を canonical に実行します。
  3. GET /sync/pull 経由でサーバーの change log を pull します。 client は確認済みの変更を自分の base ストアに適用し、その上に 未確認の outbox エントリを再生します。
  4. ローカルとリモートの両方が同じ行を編集したとき、サーバー権威の 状態を基準に rebase します。
  5. ネットワークエラー・5xx・429 のときは指数バックオフで retry します。
  6. 関連する変更が起きるたびに、購読中の 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 は、ページリロードを生き延びる べきデータ、そして時にはサーバーへ届くべきデータのためのもの です。