mcpField
S
spacetimedb-typescript

spacetimedb-typescript

github.com/douglance開発038 回インストール39 週間前に更新
リポジトリ

spacetimedb-typescript について

spacetimedb-typescriptは、SpacetimeDBモジュール向けの型安全なTypeScriptクライアントを構築するスキルとリファレンスです。生成バインディング、データ購読、リアクティブな更新、APIの注意点を説明します。

主な機能

  1. 01TypeScriptバインディングの生成
  2. 02SpacetimeDBモジュールへの接続
  3. 03SQLで絞り込んだテーブルデータの購読
  4. 04リアルタイムのテーブル変更イベント処理
  5. 05React hooksとproviderの提供
  6. 06APIの注意点と制約の説明

活用シーン

  1. 01SpacetimeDBデータを使うTypeScriptアプリの構築
  2. 02クライアントのテーブル状態の同期
  3. 03リアクティブなReact画面の作成

インストール

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

$npx skills add https://github.com/douglance/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!
正しいパターン:
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings';  // Generated!
import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react';

// 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, isLoading]
const [items, isLoading] = 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 を指定OPTIONS(第1引数)に indexes を指定"reading 'tag'" エラー
algorithm のないインデックスalgorithm: 'btree'"reading 'tag'" エラー
filter({ ownerId })filter(ownerId)"does not exist in type 'Range'"
unique カラムに .filter()unique カラムに .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()単一カラムのインデックスを使用するPANIC または結果が黙って空になります
ビュー内で .iter() を使用インデックス参照のみを使用するビューはテーブルをスキャンできません
プロシージャ内で ctx.db を使用ctx.withTx(tx => tx.db...)プロシージャには明示的なトランザクションが必要です
プロシージャのトランザクション内で ctx.myTable を使用tx.db.myTableコンテキスト変数が間違っています
クライアント側のエラー
誤り正しい方法エラー
@spacetimedb/sdkspacetimedb404 / サブパスがありません
conn.reducers.foo("val")conn.reducers.foo({ param: "val" })Reducer の構文が間違っています
connectionBuilder をインラインで指定useMemo(() => ..., [])レンダリングごとに再接続されます
const rows = useTable(table)const [rows, isLoading] = useTable(table)タプルの分割代入
楽観的 UI 更新サブスクリプションに状態を反映させる同期ずれの問題
<SpacetimeDBProvider builder={...}>connectionBuilder={...}props 名が間違っています

必須要件

  1. 生成されたバインディングを編集しないでください — spacetime generate で再生成してください
  2. Reducer はトランザクションです — データを返しません
  3. Reducer は決定論的である必要があります — ファイルシステム、ネットワーク、タイマー、乱数は使用できません
  4. Reducer の呼び出しにはオブジェクト構文を使用します — 位置引数ではなく { param: 'value' } を使用してください
  5. DbConnection は ./module_bindings からインポートしてください — spacetimedb からではありません
  6. useTable はタプルを返します — const [rows, isLoading] = useTable(tables.myTable)
  7. connectionBuilder をメモ化してください — 再接続を防ぐため、useMemo(() => ..., []) でラップしてください
  8. ビューで使用できるのはインデックス参照のみです — ビュー内で .iter() は使用できません

インストール

npm install spacetimedb
# or
pnpm add spacetimedb
# or
yarn add spacetimedb

Node.js 18-21 では、ピア依存関係の undici をインストールしてください:

npm install spacetimedb undici

Node.js 22 以降およびブラウザー環境では、そのまま動作します。

型バインディングの生成

SDK を使用する前に、SpacetimeDB モジュールから TypeScript バインディングを生成してください:

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

これにより、次の内容を含む module_bindings ディレクトリが作成されます:

  • index.ts - DbConnection、tables、reducers、query を含むメインのエクスポート
  • 各テーブルの型定義(例:player_table.ts、user_table.ts)
  • 各 Reducer の型定義(例:create_player_reducer.ts)
  • カスタム型の定義(例:point_type.ts)

基本的な接続の設定

import { DbConnection } from './module_bindings';

const connection = DbConnection.builder()
  .withUri('ws://localhost:3000')
  .withModuleName('my_database')
  .onConnect((conn, identity, token) => {
    console.log('Connected with identity:', identity.toHexString());

    // Store token for reconnection
    localStorage.setItem('spacetimedb_token', token);

    // Subscribe to tables after connection
    conn.subscriptionBuilder().subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => {
    console.log('Disconnected');
  })
  .onConnectError((ctx, error) => {
    console.error('Connection error:', error);
  })
  .build();

Connection Builder のオプション

DbConnection.builder()
  // Required: SpacetimeDB server URI
  .withUri('ws://localhost:3000')

  // Required: Database module name or address
  .withModuleName('my_database')

  // Optional: Authentication token for reconnection
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)

  // Optional: Enable compression (default: 'gzip')
  .withCompression('gzip')  // or 'none'

  // Optional: Light mode reduces network traffic
  .withLightMode(true)

  // Optional: Wait for durable writes before receiving updates
  .withConfirmedReads(true)

  // Connection lifecycle callbacks
  .onConnect((conn, identity, token) => { /* ... */ })
  .onDisconnect((ctx, error) => { /* ... */ })
  .onConnectError((ctx, error) => { /* ... */ })

  .build();

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

サブスクリプションはテーブルデータをクライアントキャッシュに同期します。SQL クエリを使って、受信するデータを絞り込みます。

基本的なサブスクリプション
connection.subscriptionBuilder()
  .onApplied((ctx) => {
    console.log('Subscription applied, cache is ready');
  })
  .onError((ctx, error) => {
    console.error('Subscription error:', error);
  })
  .subscribe('SELECT * FROM player');
複数のクエリ
connection.subscriptionBuilder()
  .subscribe([
    'SELECT * FROM player',
    'SELECT * FROM game_state',
    'SELECT * FROM message WHERE room_id = 1'
  ]);
型付きクエリビルダー

型安全なクエリには、生成された query オブジェクトを使います。

import { query } from './module_bindings';

// Simple query - selects all rows
connection.subscriptionBuilder()
  .subscribe(query.player.build());

// Query with WHERE clause
connection.subscriptionBuilder()
  .subscribe(
    query.player
      .where(row => row.name.eq('Alice'))
      .build()
  );

// Complex conditions
connection.subscriptionBuilder()
  .subscribe(
    query.player
      .where(row => row.score.gte(100))
      .where(row => row.isActive.eq(true))
      .build()
  );
すべてのテーブルをサブスクライブ

開発環境や小規模なデータセットで使用します。

connection.subscriptionBuilder().subscribeToAllTables();
サブスクライブ解除
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

// Later, unsubscribe
handle.unsubscribe();

// Or with callback when complete
handle.unsubscribeThen((ctx) => {
  console.log('Unsubscribed successfully');
});

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

サブスクリプション後は、connection.db を通じてキャッシュされたデータにアクセスします。

// Iterate all rows
for (const player of connection.db.player.iter()) {
  console.log(player.name, player.score);
}

// Convert to array
const players = Array.from(connection.db.player.iter());

// Count rows
const count = connection.db.player.count();

// Find by primary key (if table has one)
const player = connection.db.player.id.find(42);

// Find by indexed column
const alice = connection.db.player.name.find('Alice');

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

テーブルデータのリアルタイムな変更を監視します。

// Row inserted
connection.db.player.onInsert((ctx, player) => {
  console.log('New player:', player.name);
});

// Row deleted
connection.db.player.onDelete((ctx, player) => {
  console.log('Player left:', player.name);
});

// Row updated (requires primary key on table)
connection.db.player.onUpdate((ctx, oldPlayer, newPlayer) => {
  console.log(`${oldPlayer.name} score: ${oldPlayer.score} -> ${newPlayer.score}`);
});

// Remove callbacks
const onInsertCb = (ctx, player) => console.log(player);
connection.db.player.onInsert(onInsertCb);
connection.db.player.removeOnInsert(onInsertCb);
イベントコンテキスト

コールバックは、イベントに関する情報を含む EventContext を受け取ります。

connection.db.player.onInsert((ctx, player) => {
  // Access to database
  const allPlayers = Array.from(ctx.db.player.iter());

  // Check event type
  if (ctx.event.tag === 'Reducer') {
    const { callerIdentity, reducer, status } = ctx.event.value;
    console.log(`Triggered by reducer: ${reducer.name}`);
  }

  // Call other reducers
  ctx.reducers.sendMessage({ playerId: player.id, text: 'Welcome!' });
});

Reducer の呼び出し

Reducer はデータベースを変更するサーバーサイド関数です。重要:位置引数ではなく、オブジェクト構文を使ってください。

// CORRECT: Object syntax
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });

// WRONG: Positional arguments
// connection.reducers.createPlayer('Alice', { x: 0, y: 0 });  // DO NOT DO THIS

// Listen for reducer results
connection.reducers.onCreatePlayer((ctx, args) => {
  const { callerIdentity, status, timestamp, energyConsumed } = ctx.event;

  if (status.tag === 'Committed') {
    console.log('Player created successfully');
  } else if (status.tag === 'Failed') {
    console.error('Failed:', status.value);
  }
});

// Remove reducer callback
connection.reducers.removeOnCreatePlayer(callback);
snake_case から camelCase への変換
  • サーバー:spacetimedb.reducer('do_something', ...)
  • クライアント:conn.reducers.doSomething({ ... })
Reducer フラグ

サーバーによる Reducer 呼び出しの処理方法を制御します。

// NoSuccessNotify: Don't send TransactionUpdate on success (reduces traffic)
connection.setReducerFlags.movePlayer('NoSuccessNotify');

// FullUpdate: Always send full TransactionUpdate (default)
connection.setReducerFlags.movePlayer('FullUpdate');

ビュー

ビューを使用すると、接続中のユーザーに基づいて、非公開テーブルデータにフィルターを適用してアクセスできます。

ViewContext と AnonymousViewContext
// ViewContext — has ctx.sender, result varies per user (computed per-subscriber)
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 (shared, better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => {
  return [...ctx.db.player.by_score.filter(/* top scores */)];
});
重要:ビューではインデックスルックアップのみ使用できます
// WRONG — views cannot use .iter()
spacetimedb.view(
  { name: 'my_data_wrong', public: true },
  t.array(PrivateData.rowType),
  (ctx) => [...ctx.db.privateData.iter()]  // NOT ALLOWED
);

// RIGHT — use index lookup
spacetimedb.view(
  { name: 'my_data', public: true },
  t.array(PrivateData.rowType),
  (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)]
);
ビューのサブスクライブ

ビューを使用するには、明示的なサブスクリプションが必要です。

conn.subscriptionBuilder().subscribe([
  'SELECT * FROM public_table',
  'SELECT * FROM my_data',  // Views need explicit SQL!
]);

プロシージャ(ベータ版)

プロシージャは、Reducer では実行できない副作用(HTTP リクエストなど)に使用します。

プロシージャは現在ベータ版です。API は変更される可能性があります。

プロシージャの定義
spacetimedb.procedure(
  'fetch_external_data',
  { url: t.string() },
  t.string(),  // return type
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    return response.text();
  }
);
重要:プロシージャでのデータベースアクセス

プロシージャには ctx.db がありません。データベースにアクセスするには ctx.withTx() を使用してください。

spacetimedb.procedure('save_fetched_data', { url: t.string() }, t.unit(), (ctx, { url }) => {
  // Fetch external data (outside transaction)
  const response = ctx.http.fetch(url);
  const data = response.text();

  // WRONG — ctx.db doesn't exist in procedures
  // ctx.db.myTable.insert({ ... });

  // RIGHT — use ctx.withTx() for database access
  ctx.withTx(tx => {
    tx.db.myTable.insert({
      id: 0n,
      content: data,
      fetchedAt: tx.timestamp,
      fetchedBy: tx.sender,
    });
  });

  return {};
});
Reducer との主な違い
Reducerプロシージャ
ctx.db を直接利用可能ctx.withTx(tx => tx.db...) を使用する必要がある
自動トランザクション手動でのトランザクション管理
HTTP/ネットワーク不可ctx.http.fetch() を利用可能
呼び出し元に戻り値を返さない呼び出し元にデータを返せる

ID と認証

import { Identity } from 'spacetimedb';

// Get current identity
const identity = connection.identity;
console.log(identity?.toHexString());

// Compare identities
if (identity?.isEqual(otherIdentity)) {
  console.log('Same user');
}

// Create from hex string
const parsed = Identity.fromString('0x1234...');

// Zero identity
const zero = Identity.zero();

// Compare identities using toHexString()
const isOwner = row.ownerId.toHexString() === myIdentity.toHexString();
認証情報の永続化
// On connect, save the token
.onConnect((conn, identity, token) => {
  localStorage.setItem('auth_token', token);
  localStorage.setItem('identity', identity.toHexString());
})

// On reconnect, use saved token
.withToken(localStorage.getItem('auth_token') ?? undefined)
古いトークンの処理
const onConnectError = (_ctx: ErrorContext, err: Error) => {
  if (err.message?.includes('Unauthorized') || err.message?.includes('401')) {
    localStorage.removeItem('auth_token');
    window.location.reload();
  }
};

React 連携

SDK には、リアクティブな UI 更新のための React フックが含まれています。

プロバイダーの設定
import React, { useMemo } from 'react';
import ReactDOM from 'react-dom/client';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection, query } from './module_bindings';
import App from './App';

function Root() {
  // CRITICAL: Memoize to prevent reconnects on every render
  const connectionBuilder = useMemo(() =>
    DbConnection.builder()
      .withUri('ws://localhost:3000')
      .withModuleName('my_game')
      .withToken(localStorage.getItem('auth_token') || undefined)
      .onConnect((conn, identity, token) => {
        console.log('Connected:', identity.toHexString());
        localStorage.setItem('auth_token', token);
        conn.subscriptionBuilder().subscribe(query.player.build());
      })
      .onDisconnect(() => console.log('Disconnected'))
      .onConnectError((ctx, err) => console.error('Error:', err)),
    []  // Empty deps - only create once
  );

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

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Root />
  </React.StrictMode>
);
useSpacetimeDB フック

接続状態にアクセスします。

import { useSpacetimeDB } from 'spacetimedb/react';

function ConnectionStatus() {
  const { isActive, identity, token, connectionId, connectionError } = useSpacetimeDB();

  if (connectionError) {
    return <div>Error: {connectionError.message}</div>;
  }

  if (!isActive) {
    return <div>Connecting...</div>;
  }

  return <div>Connected as {identity?.toHexString()}</div>;
}
useTable フック

リアクティブな更新を伴うテーブルデータをサブスクライブします。重要:[rows, isLoading] のタプルを返します。

import { useTable, where, eq } from 'spacetimedb/react';
import { tables } from './module_bindings';

function PlayerList() {
  // CORRECT: Tuple destructuring
  const [players, isLoading] = useTable(tables.player);

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {players.map(player => (
        <li key={player.id}>{player.name}: {player.score}</li>
      ))}
    </ul>
  );
}

function FilteredPlayerList() {
  // Filtered players with callbacks
  const [activePlayers, isLoading] = useTable(
    tables.player,
    where(eq('isActive', true)),
    {
      onInsert: (player) => console.log('Player joined:', player.name),
      onDelete: (player) => console.log('Player left:', player.name),
      onUpdate: (oldPlayer, newPlayer) => {
        console.log(`${oldPlayer.name} updated`);
      },
    }
  );

  return (
    <ul>
      {activePlayers.map(player => (
        <li key={player.id}>{player.name}</li>
      ))}
    </ul>
  );
}
useReducer フック

コンポーネントから Reducer を呼び出します。

import { useReducer } from 'spacetimedb/react';
import { reducers } from './module_bindings';

function CreatePlayerForm() {
  const createPlayer = useReducer(reducers.createPlayer);
  const [name, setName] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    // CORRECT: Object syntax
    createPlayer({ name, location: { x: 0, y: 0 } });
    setName('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button type="submit">Create Player</button>
    </form>
  );
}

Vue 連携

SDK には Vue コンポーザブルが含まれています。

import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/vue';

使い方は React フックと似ています。

Svelte 連携

SDK には Svelte ストアが含まれています。

import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/svelte';

サーバーサイドでの使用(Node.js、Deno、Bun)

SDK はサーバーサイドの JavaScript ランタイムで動作します。

import { DbConnection } from './module_bindings';

async function main() {
  const connection = DbConnection.builder()
    .withUri('ws://localhost:3000')
    .withModuleName('my_database')
    .onConnect((conn, identity, token) => {
      console.log('Connected:', identity.toHexString());

      conn.subscriptionBuilder()
        .onApplied(() => {
          // Process data
          for (const player of conn.db.player.iter()) {
            console.log(player);
          }
        })
        .subscribe('SELECT * FROM player');
    })
    .build();
}

main();

タイムスタンプ

サーバーサイド
import { Timestamp, ScheduleAt } from 'spacetimedb';

// Current time
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });

// Future time (add microseconds)
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;  // 5 minutes
クライアントサイド(重要)

タイムスタンプは数値ではなく、オブジェクトです。

// WRONG
const date = new Date(row.createdAt);
const date = new Date(Number(row.createdAt / 1000n));

// RIGHT
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
クライアントでの ScheduleAt
// ScheduleAt is a tagged union
if (scheduleAt.tag === 'Time') {
  const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n));
}

スケジュールテーブル

// Scheduled table MUST use scheduledId and scheduledAt columns
export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: 'run_cleanup'  // reducer name
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),  // Your custom data
});

// Scheduled reducer receives full row as arg
spacetimedb.reducer('run_cleanup', { arg: CleanupJob.rowType }, (ctx, { arg }) => {
  // arg.scheduledId, arg.targetId available
  // Row is auto-deleted after reducer completes
});

// Schedule a job
import { ScheduleAt } from 'spacetimedb';
const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60 seconds
ctx.db.cleanupJob.insert({
  scheduledId: 0n,
  scheduledAt: ScheduleAt.time(futureTime),
  targetId: someId
});

// Cancel a job by deleting the row
ctx.db.cleanupJob.scheduledId.delete(jobId);

エラー処理

接続エラー
DbConnection.builder()
  .onConnectError((ctx, error) => {
    console.error('Failed to connect:', error.message);

    // Implement retry logic
    setTimeout(() => {
      // Rebuild connection
    }, 5000);
  })
  .build();
サブスクリプションエラー
connection.subscriptionBuilder()
  .onError((ctx, error) => {
    console.error('Subscription failed:', error.message);
  })
  .subscribe('SELECT * FROM player');
Reducer エラー
connection.reducers.onCreatePlayer((ctx, args) => {
  const { status } = ctx.event;

  switch (status.tag) {
    case 'Committed':
      console.log('Success');
      break;
    case 'Failed':
      console.error('Reducer failed:', status.value);
      break;
    case 'OutOfEnergy':
      console.error('Out of energy');
      break;
  }
});

切断

// Gracefully disconnect
connection.disconnect();

型リファレンス

コア型
import {
  Identity,           // User identity (256-bit)
  ConnectionId,       // Connection identifier
  Timestamp,          // SpacetimeDB timestamp
  TimeDuration,       // Duration type
  Uuid,               // UUID type
} from 'spacetimedb';
生成された型
// From your module_bindings
import {
  DbConnection,        // Connection class
  DbConnectionBuilder, // Builder class
  SubscriptionBuilder, // Subscription builder
  SubscriptionHandle,  // Subscription handle
  EventContext,        // Event callback context
  ReducerEventContext, // Reducer callback context
  ErrorContext,        // Error callback context
  tables,              // Table accessors for useTable
  reducers,            // Reducer definitions for useReducer
  query,               // Typed query builder

  // Your custom types
  Player,
  Point,
  // ... etc
} from './module_bindings';

コマンド

# Start local server
spacetime start

# Publish module
spacetime publish <module-name> --project-path <backend-dir>

# Clear database and republish
spacetime publish <module-name> --clear-database -y --project-path <backend-dir>

# Generate bindings
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --project-path <backend-dir>

# View logs
spacetime logs <module-name>

ベストプラクティス

  1. 認証トークンを保存する: onConnect から取得したトークンを保存し、シームレスに再接続できるようにします。

  2. 接続後にサブスクライブする: onConnect コールバックでサブスクリプションを設定します。

  3. 型付きクエリを使用する: 型安全性を確保するため、生の SQL 文字列よりも query ビルダーを優先します。

  4. すべての接続状態を処理する: onConnect、onDisconnect、onConnectError を実装します。

  5. 高頻度の更新にはライトモードを使用する: ゲームやリアルタイムアプリでは .withLightMode(true) を有効にします。

  6. 不要になったらサブスクライブを解除する: コンポーネントのアンマウント時やデータが不要になったときに、サブスクリプションをクリーンアップします。

  7. 主キーを使用する: onUpdate コールバックを有効にするため、テーブルに主キーを定義します。

  8. connectionBuilder をメモ化する: 再接続を防ぐため、必ず useMemo() でラップします。

  9. サブスクリプションで状態を駆動する: 楽観的更新は避け、サーバーを信頼できる唯一の情報源とします。

よくあるパターン

再接続ロジック
function createConnection(token?: string) {
  return DbConnection.builder()
    .withUri('ws://localhost:3000')
    .withModuleName('my_database')
    .withToken(token)
    .onConnect((conn, identity, newToken) => {
      localStorage.setItem('token', newToken);
      setupSubscriptions(conn);
    })
    .onDisconnect(() => {
      // Reconnect after delay
      setTimeout(() => {
        createConnection(localStorage.getItem('token') ?? undefined);
      }, 3000);
    })
    .build();
}
楽観的更新
function PlayerScore({ player }) {
  const updateScore = useReducer(reducers.updateScore);
  const [optimisticScore, setOptimisticScore] = useState(player.score);

  const handleClick = () => {
    setOptimisticScore(prev => prev + 1);
    updateScore({ playerId: player.id, delta: 1 });
  };

  // Sync with actual data
  useEffect(() => {
    setOptimisticScore(player.score);
  }, [player.score]);

  return <div onClick={handleClick}>Score: {optimisticScore}</div>;
}
複数の条件によるフィルタリング
// Using query builder
query.player
  .where(row => row.team.eq('red'))
  .where(row => row.score.gte(100))
  .build();

// Using React hooks
const [redTeamHighScorers] = useTable(
  tables.player,
  where(eq('team', 'red')),  // Additional filtering in client
);
const filtered = redTeamHighScorers.filter(p => p.score >= 100);

プロジェクト構成

サーバー(backend/spacetimedb/)
src/schema.ts   -> Tables, export spacetimedb
src/index.ts    -> Reducers, lifecycle, import schema
package.json    -> { "type": "module", "dependencies": { "spacetimedb": "^1.11.0" } }
tsconfig.json   -> Standard config
循環インポートの回避
schema.ts -> defines tables AND exports spacetimedb
index.ts  -> imports spacetimedb from ./schema, defines reducers
クライアント(client/)
src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx         -> Provider, connection setup
src/App.tsx          -> UI components
src/config.ts        -> MODULE_NAME, SPACETIMEDB_URI

コメント