mcpField
S
spacetimedb-typescript

spacetimedb-typescript

github.com/clockworklabs開発25.4k90 回インストール今日更新
リポジトリ

spacetimedb-typescript について

SpacetimeDB でリアルタイム TypeScript クライアントやモジュールを開発するためのスキルガイドです。型付きデータアクセス、サブスクリプション、リアクティブ更新、React 連携、API の誤用例を扱います。

主な機能

  1. 01型付きデータアクセス
  2. 02サブスクリプションとリアクティブ更新
  3. 03React hooks と provider
  4. 04型バインディングの生成
  5. 05モジュール schema と reducer の指針

活用シーン

  1. 01リアルタイムデータ用 TypeScript クライアントの構築
  2. 02データベーステーブルの購読
  3. 03SpacetimeDB と React の連携
  4. 04SDK の使用法やモジュールエラーの解決

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/clockworklabs/spacetimedb --skill spacetimedb-typescript

インストール後、Claude Code で/spacetimedb-typescriptを入力して読み込まれたか確認します。

詳細

SpacetimeDB TypeScript SDK

SpacetimeDB モジュールに直接接続する、リアルタイムの TypeScript クライアントを構築できます。この SDK は、型安全なデータベースアクセス、自動同期、Web アプリ、Node.js、Deno、Bun、その他の JavaScript ランタイム向けのリアクティブな更新を提供します。


存在しない API — 使用禁止

これらの API は存在しません。LLM は頻繁にこれらをでっち上げます。

// WRONG PACKAGE — does not exist
import { SpacetimeDBClient } from "@clockworklabs/spacetimedb-sdk";

// WRONG — these methods don't exist
SpacetimeDBClient.connect(...);
SpacetimeDBClient.call("reducer_name", [...]);
connection.call("reducer_name", [arg1, arg2]);

// WRONG — positional reducer arguments
conn.reducers.doSomething("value");  // WRONG!

// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn);  // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable);          // Use schema({ myTable })
schema(t1, t2, t3);      // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup'  // Use scheduled: () => run_cleanup
.withModuleName('db')     // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify')  // Removed in 2.0
正しいパターン:
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings';  // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';

// CORRECT REDUCER CALLS — object syntax, not positional!
conn.reducers.doSomething({ value: 'test' });
conn.reducers.updateItem({ itemId: 1n, newValue: 42 });

// CORRECT DATA ACCESS — useTable returns [rows, isReady]
const [items, isReady] = useTable(tables.item);
使用禁止:
  • useItems()、useData() のようなフックをでっち上げないでください — useTable(tables.tableName) を使用してください
  • 偽のパッケージからインポートしないでください — spacetimedb、spacetimedb/react、./module_bindings のみを使用してください

よくある間違いの一覧

サーバー側のエラー
誤り正しい方法エラー
package.json がないpackage.json を作成する"could not detect language"
tsconfig.json がないtsconfig.json を作成する"TsconfigNotFound"
エントリーポイントが src/index.ts にないsrc/index.ts を使用するモジュールをバンドルできません
COLUMNS(第 2 引数)に indexes を指定table() の OPTIONS(第 1 引数)に indexes を指定"reading 'tag'" error
algorithm のないインデックスalgorithm: 'btree'"reading 'tag'" error
filter({ ownerId })filter(ownerId)"does not exist in type 'Range'"
一意なカラムに .filter()一意なカラムに .find()TypeError
insert({ ...without id })insert({ id: 0n, ... })"Property 'id' is missing"
const id = table.insert(...)const row = table.insert(...).insert() は ID ではなく ROW を返します
.unique() + 明示的なインデックス.unique() のみを使用"name is used for multiple entities"
index.ts から spacetimedb をインポートschema.ts からインポート"Cannot access before initialization"
複数カラムの .filter() の範囲の形が不正インデックスのプレフィックス/タプルの形に合わせる結果が空になる、または範囲/型エラー
ビュー内で .iter() を使用インデックス検索のみを使用ビューではテーブルを走査できません
プロシージャ内で ctx.db を使用ctx.withTx(tx => tx.db...)プロシージャには明示的なトランザクションが必要です
クライアント側のエラー
誤り正しい方法エラー
connectionBuilder をインラインで指定useMemo(() => ..., [])レンダリングのたびに再接続します
const rows = useTable(table)const [rows, isReady] = useTable(table)タプルの分割代入
楽観的な UI 更新サブスクリプションに状態を反映させる同期ずれの問題
<SpacetimeDBProvider builder={...}>connectionBuilder={...}プロパティ名が誤っています

必須要件

  1. schema({ table }) — テーブルオブジェクトを 1 つ使用してください。オプションのモジュール設定は第 2 引数として指定できます
  2. Reducer/procedure 名にはエクスポート名を使用 — export const name = spacetimedb.reducer(params, fn)。reducer('name', ...) は使用しないでください
  3. Reducer の呼び出しにはオブジェクト構文を使用 — 位置引数ではなく { param: 'value' } を使用してください
  4. DbConnection は ./module_bindings からインポート — spacetimedb からではありません
  5. 生成されたバインディングは編集禁止 — spacetime generate で再生成してください
  6. インデックスは OPTIONS(第 1 引数)に指定 — table() の COLUMNS(第 2 引数)ではありません
  7. u64/i64 フィールドには BigInt を使用 — 0、1 ではなく 0n、1n を使用してください
  8. Reducer はトランザクション処理されます — データは返しません
  9. Reducer は決定論的である必要があります — ファイルシステム、ネットワーク、タイマー、乱数は使用できません
  10. ビューではインデックス検索を使用してください — .iter() は深刻なパフォーマンス問題を引き起こします
  11. プロシージャには ctx.withTx() が必要です — プロシージャでは ctx.db は存在しません
  12. Sum type の値 — { variant: payload } ではなく { tag: 'variant', value: payload } を使用してください
  13. .withDatabaseName() を使用 — .withModuleName() ではありません(2.0)

インストール

npm install spacetimedb

ネイティブの fetch/WebSocket に対応していない Node.js 環境では、undici をインストールしてください。

型バインディングの生成

spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./server

クライアント接続

import { DbConnection } from './module_bindings';

const connection = DbConnection.builder()
  .withUri('ws://localhost:3000')
  .withDatabaseName('my_database')
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
  .onConnect((conn, identity, token) => {
    // identity: your unique Identity for this database
    console.log('Connected as:', identity.toHexString());

    // Save token for reconnection (preserves identity across sessions)
    localStorage.setItem('spacetimedb_token', token);

    conn.subscriptionBuilder()
      .onApplied(() => console.log('Cache ready'))
      .subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => console.log('Disconnected'))
  .onConnectError((ctx, error) => console.error('Connection failed:', error))
  .build();

テーブルのサブスクライブ

// Basic subscription
connection.subscriptionBuilder()
  .onApplied((ctx) => console.log('Cache ready'))
  .subscribe('SELECT * FROM player');

// Multiple queries
connection.subscriptionBuilder()
  .subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);

// Subscribe to all tables (development only — cannot mix with Subscribe)
connection.subscriptionBuilder().subscribeToAllTables();

// Subscription handle for later unsubscribe
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

handle.unsubscribeThen(() => console.log('Unsubscribed'));

テーブルデータへのアクセス

for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);

テーブルイベントのコールバック

connection.db.player.onInsert((ctx, player) => console.log('New:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('Left:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));

Reducer の呼び出し

重要:位置引数ではなく、オブジェクト構文を使用してください。

connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });
snake_case から camelCase への変換
  • サーバー:export const do_something = spacetimedb.reducer(...)
  • クライアント:conn.reducers.doSomething({ ... })

アイデンティティと認証

  • identity と token は onConnect コールバックで提供されます(上記のクライアント接続を参照)
  • 表示またはログ記録には identity.toHexString() を使用します
  • 匿名接続にするには .withToken() を省略します。サーバーが新しいアイデンティティを割り当てます
  • 古い、または無効なトークンを渡すと、サーバーは onConnect で新しいアイデンティティとトークンを発行します

エラー処理

接続レベルのエラー(.onConnectError、.onDisconnect)については、上記のクライアント接続の例を参照してください。

// Subscription error
connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .onError((ctx) => console.error('Subscription error:', ctx.event))
  .subscribe('SELECT * FROM player');

サーバーサイドのモジュール開発

テーブル定義
import { schema, table, t } from 'spacetimedb/server';

export const Task = table({
  name: 'task',
  public: true,
  indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
  id: t.u64().primaryKey().autoInc(),
  ownerId: t.identity(),
  title: t.string(),
  createdAt: t.timestamp(),
});
カラム型
t.identity()           // User identity
t.u64()                // Unsigned 64-bit integer (use for IDs)
t.string()             // Text
t.bool()               // Boolean
t.timestamp()          // Timestamp
t.scheduleAt()         // For scheduled tables only
t.object('Name', {})   // Product types (nested objects)
t.enum('Name', {})     // Sum types (tagged unions)
t.string().optional()  // Nullable

BigInt の構文:すべての u64/i64 フィールドでは、0、1 ではなく 0n、1n を使用します。

スキーマのエクスポート
const spacetimedb = schema({ Task, Player });
export default spacetimedb;
リデューサーの定義(2.0)

名前はエクスポートから決まります。文字列引数ではありません。

import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';

export const create_task = spacetimedb.reducer(
  { title: t.string() },
  (ctx, { title }) => {
    if (!title) throw new SenderError('title required');
    ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
  }
);
更新パターン
const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });
ライフサイクルフック
spacetimedb.clientConnected((ctx) => { /* ctx.sender is the connecting identity */ });
spacetimedb.clientDisconnected((ctx) => { /* clean up */ });

イベントテーブル(2.0)

リデューサーのコールバックは 2.0 で削除されました。代わりにイベントテーブルと onInsert を使用します。

export const DamageEvent = table(
  { name: 'damage_event', public: true, event: true },
  { target: t.identity(), amount: t.u32() }
);

export const deal_damage = spacetimedb.reducer(
  { target: t.identity(), amount: t.u32() },
  (ctx, { target, amount }) => {
    ctx.db.damageEvent.insert({ target, amount });
  }
);

クライアントはサブスクライブして onInsert を使用します:

conn.db.damageEvent.onInsert((ctx, evt) => {
  playDamageAnimation(evt.target, evt.amount);
});

イベントテーブルは明示的にサブスクライブする必要があります。subscribeToAllTables() では除外されます。


ビュー

ViewContext と AnonymousViewContext
// ViewContext — has ctx.sender, result varies per user
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
  return [...ctx.db.item.by_owner.filter(ctx.sender)];
});

// AnonymousViewContext — no ctx.sender, same result for everyone (better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(Player.rowType), (ctx) => {
  return ctx.from.player.where(p => p.score.gt(1000));
});

ビューで使用できるのはインデックス検索のみです。.iter() は使用できません。


スケジュールテーブル

export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: () => run_cleanup  // function returning the exported reducer
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),
});

export const run_cleanup = spacetimedb.reducer(
  { arg: CleanupJob.rowType },
  (ctx, { arg }) => { /* arg.scheduledId, arg.targetId available */ }
);

// Schedule a job
import { ScheduleAt } from 'spacetimedb';
ctx.db.cleanupJob.insert({
  scheduledId: 0n,
  scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
  targetId: someId
});
クライアントでの ScheduleAt
// ScheduleAt is a tagged union on the client
// { tag: 'Time', value: Timestamp } or { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
  const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}

タイムスタンプ

サーバーサイド
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;
クライアントサイド
// Timestamps are objects with BigInt, not numbers
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

プロシージャ(ベータ)

export const fetch_data = spacetimedb.procedure(
  { url: t.string() }, t.string(),
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
    return response.text();
  }
);

プロシージャには ctx.db がありません。ctx.withTx(tx => tx.db...) を使用します。


React との統合

import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';

function Root() {
  const connectionBuilder = useMemo(() =>
    DbConnection.builder()
      .withUri('ws://localhost:3000')
      .withDatabaseName('my_game')
      .withToken(localStorage.getItem('auth_token') || undefined)
      .onConnect((conn, identity, token) => {
        localStorage.setItem('auth_token', token);
        conn.subscriptionBuilder().subscribe(tables.player);
      }),
    []
  );

  return (
    <SpacetimeDBProvider connectionBuilder={connectionBuilder}>
      <App />
    </SpacetimeDBProvider>
  );
}

function PlayerList() {
  const [players, isReady] = useTable(tables.player);
  if (!isReady) return <div>Loading...</div>;
  return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

プロジェクト構成

サーバー(backend/spacetimedb/)
src/schema.ts   -> Tables, export spacetimedb
src/index.ts    -> Reducers, lifecycle, import schema
package.json    -> { "type": "module", "dependencies": { "spacetimedb": "^2.0.0" } }
tsconfig.json   -> Standard config
クライアント(client/)
src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx         -> Provider, connection setup
src/App.tsx          -> UI components

コマンド

spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>

コメント