コンテンツにスキップ

クイックスタート

plasma では次のように動きます。

  • ブラウザは mutator を IndexedDB に対して optimistic に実行します
  • Worker は同じ mutator を D1 に対して canonical に実行します
  • sync、push、pull、rebase、live query、retry は plasma が担います

書くのは 1 つ の schema と 1 セット の mutation 関数だけです。両方の runtime がそれらを使います。

  1. schema と mutator を一度だけ定義する

    Section titled “schema と mutator を一度だけ定義する”

    ブラウザと Worker の両方が import する共有ファイルを作成します。

    src/schema.ts
    import {
    defineMutators,
    defineSchema,
    eq,
    id,
    int,
    table,
    text,
    } from "@sh1n4ps/plasma-core"
    export const todos = table("todos", {
    id: id(),
    title: text(),
    done: int().default(0),
    updatedAt: int(),
    })
    export const schema = defineSchema({ todos })
    export const SCHEMA_VERSION = "todos-v1"
    interface Ctx { userId: string }
    export const mutators = defineMutators<typeof schema, Ctx>()({
    createTodo: async ({ db, args }) => {
    await db.insert(todos).values({
    id: args.id,
    title: args.title,
    updatedAt: args.updatedAt,
    })
    },
    markDone: async ({ db, args }) => {
    await db.update(todos)
    .set({ done: 1, updatedAt: args.updatedAt })
    .where(eq(todos.id, args.id))
    },
    })

    defineMutators<typeof schema, Ctx>() という形は、schema(db.insert(todos) が型チェックされるように)と ctx 型(各 mutator の中で ctx.userId が使えるように)の両方を捕捉します。

  2. Worker に sync エンドポイントをマウントする

    Section titled “Worker に sync エンドポイントをマウントする”
    worker.ts
    import { createSyncHandler, ensureSchema, fromD1 } from "@sh1n4ps/plasma-server"
    import { schema, mutators, SCHEMA_VERSION } from "./schema"
    interface Env {
    DB: D1Database
    }
    export default {
    async fetch(req: Request, env: Env): Promise<Response> {
    const executor = fromD1(env.DB)
    await ensureSchema({ schema, executor })
    const handler = createSyncHandler({
    schema,
    mutators,
    executor,
    schemaVersion: SCHEMA_VERSION,
    auth: async (req) => ({
    ok: true,
    clientGroupID: req.headers.get("x-group") ?? "demo",
    clientID: req.headers.get("x-client") ?? "anonymous",
    ctx: { userId: req.headers.get("x-user") ?? "demo-user" },
    }),
    })
    return handler(req)
    },
    }

    ensureSchema は追加のみで安全な DDL を実行します。テーブル、change-log トリガー一式、plasma の内部管理テーブルを作成します。auth はすべてのリクエストで呼ばれます。拒否するには { ok: false, reason } を返します。

  3. src/main.tsx
    import { createPlasmaClient } from "@sh1n4ps/plasma-client"
    import { PlasmaProvider } from "@sh1n4ps/plasma-react"
    import { createRoot } from "react-dom/client"
    import { schema, mutators, SCHEMA_VERSION } from "./schema"
    import { TodoList } from "./TodoList"
    export const plasma = createPlasmaClient({
    schema,
    mutators,
    dbName: "todos",
    endpoint: "/sync",
    clientGroupID: "demo",
    schemaVersion: SCHEMA_VERSION,
    getContext: async () => ({ userId: "demo-user" }),
    })
    plasma.start()
    createRoot(document.getElementById("root")!).render(
    <PlasmaProvider client={plasma}>
    <TodoList />
    </PlasmaProvider>,
    )

    plasma.start() は pull ポーリングループを開始します。WebSocket 経由のリアルタイム poke はオプトインです。準備ができたら createPlasmaClientsubscribe: createWebSocketSubscription({...}) を渡してください。endpoint の値はブラウザでは location.origin を基準に解決されます。Node / Workers では絶対 URL を渡す必要があります。

    ここで plasmaexport しているのは、後述の TodoList.tsx が import できるようにするためです。

  4. useLiveQuery で書き込みに反応する

    Section titled “useLiveQuery で書き込みに反応する”
    src/TodoList.tsx
    import { useLiveQuery, useMutation } from "@sh1n4ps/plasma-react"
    import { asc, eq } from "@sh1n4ps/plasma-core"
    import { plasma } from "./main"
    import { mutators, todos } from "./schema"
    export function TodoList() {
    const rows = useLiveQuery(
    () => plasma.db
    .select()
    .from(todos)
    .where(eq(todos.done, 0))
    .orderBy(asc(todos.updatedAt)),
    [],
    )
    const create = useMutation<typeof mutators, "createTodo">("createTodo")
    const markDone = useMutation<typeof mutators, "markDone">("markDone")
    return (
    <div>
    <button
    onClick={() => create.mutate({
    id: plasma.newId(),
    title: "Try plasma",
    updatedAt: Date.now(),
    })}
    >
    Add todo
    </button>
    <ul>
    {rows.map((row) => (
    <li key={row.id}>
    {row.title}
    <button onClick={() => markDone.mutate({
    id: row.id,
    updatedAt: Date.now(),
    })}>
    done
    </button>
    </li>
    ))}
    </ul>
    </div>
    )
    }

    useLiveQuery の結果は、次のいずれかが起きた瞬間に更新されます。

    • ローカルの mutation が発火したとき(optimistic に)、または
    • pull ループがサーバーからの変更を届けたとき(canonical に)。

    明示的に subscribe することはありません。plasma の IVM(Incremental View Maintenance)が、query のウィンドウにどの行が含まれるかを追跡し、diff をコンポーネントに push します。

あなたは schema.ts一度だけ 書きました。

  • ブラウザはそれを型付きのローカル query と optimistic な書き込みに使いました。
  • Worker はそれを D1 への canonical な書き込みに使いました。
  • 同じ createTodo mutator が両方の場所で実行されました。
  • live query はローカルの mutation が反映された瞬間に更新され、その後 sync ループがバックグラウンドでサーバーと整合を取りました。
  • sync、retry、rebase、change log は plasma が処理しました。