本文へ移動

SDK ガイド

多人数で空間を共有するリアルタイムアプリを、path と entry で書きます。

コンセプト

tablebox は、多人数で空間を共有するリアルタイムアプリケーションの実装基盤です。アプリケーションが定義した entry を、その変更規則に従って参加者間で同期します。対象には、参加者の位置のように毎秒何度も更新される entry を含みます。描画と操作画面は、アプリケーションが実装します。

全員が毎秒何度も更新する空間で、全員の変化を全員へ届けると、一人が受け取る量は参加者数に比例します。tablebox では、参加者が watch で購読の範囲と頻度を選び、受け取る量はそれだけで決まります。関心の高い範囲は細かく、低い範囲は粗く同期すれば、参加者が何人いても一人の負荷は一定に収まります。

基本の概念

World
共有空間です。作成時に rules を固定します。
path・entry
path は World の中の場所、entry は path にあるものです。class は entry.State(最新の値)、entry.Stream(追記される record の列。データか、映像・音声)、entry.Blob(バイト列)の3つです。
rules
path のパターンごとに、class、書ける人、読める人、schema、索引、寿命、上限を宣言します。
watch
prefix に一致する entry のうち、範囲・値・並びで選んだものを、件数と頻度の上限付きで受け取り続けます。
利用権
Node を使う資格です。運営者から開発者、Project、参加者の鍵へと範囲を狭めて委譲され、Node が委譲の連鎖を確かめます。

使用例

World の作成は最初の1回だけで、参加は毎回同じ手順で行います。近くのアバターは 10 Hz で100人、遠くは 1 Hz で1,000人まで受け取るので、この参加者が受け取る更新は毎秒2,000件以下です。

TypeScript
import { createClient, entry, t } from "@tablebox/sdk";

const rules = {
  "stage":               { class: entry.State, owner: "world", ttl: "none", write: ["creator", "moderator"], export: true },
  "avatars/{uid}":       {
    class: entry.State, write: "{uid}",
    schema: { name: t.string(), image: t.blob(), team: t.string(), x: t.number(), y: t.number() },
    indexes: [{ grid: ["x", "y"], cell: 20 }, { eq: "team" }],
  },
  "chat/{room}":         { class: entry.Stream, owner: "world", ttl: 3_600_000, create: "creator", write: "any",
                           window: { count: 200, time: 600_000 }, limits: { rate: 1 } },
  "assets/{id}":         { class: entry.Blob, owner: "world", ttl: "none", write: "any", export: true, limits: { size: 16_000_000 } },
  "voices/{room}/{uid}": { class: entry.Stream, kind: "media", read: "members", list: "{room}", write: "{uid}",
                           limits: { bitrate: 64_000 } },
};

// rulesの型。createWorld、findWorld、put、get、watchが型引数なしでこの型になる
declare global {
  interface Tablebox {
    rules: typeof rules;
  }
}

// 作成は最初の1回だけ。参加は毎回同じ手順で行う
const client = await createClient({ endpoints, token: (subject) => myServer.token(subject) });
const worldId = await client.createWorld({ rules, idle: 600_000 });
const world = await client.findWorld(worldId).join();

// 自分のアバター。接続している間はSDKが自動で延長する
const me = world.put(`avatars/${world.me}`, new entry.State({ name: "noname", image: null, team: "red", x: 0, y: 0 }));
onMove((x, y) => me.set({ x, y }));

// 近くは細かく、遠くは粗く受け取る
const near = world.watch("avatars/", { area: { follow: `avatars/${world.me}`, range: 20 }, limit: 100, maxHz: 10 }, render);
const far = world.watch("avatars/", { area: { follow: `avatars/${world.me}`, range: 200 }, limit: 1000, maxHz: 1 }, render);

// チャット
world.put("chat/lobby", new entry.Stream());  // 作成者が部屋を作る
(await world.get("chat/lobby"))?.append({ text: "hello" });

// 抜ける
near.close();
far.close();
await world.leave();

Console

Console は、開発者の token をブラウザーの鍵でプロジェクトへ委譲します。サーバーのないアプリは、公開 token の入った tablebox.json を組み込み、SDK の publicToken が参加者ごとに15分の token を手元で作ります。会員制のアプリは、サーバーの公開鍵へ委譲したサーバー用設定をアプリのサーバーに置き、ログインした本人の鍵へ token を委譲します。