typescript-client について
SpacetimeDB の TypeScript クライアント開発向けリファレンスです。React と通常の接続、サブスクリプションの復元、トークン更新を扱い、再接続や型付きデータ行の実装に役立ちます。
主な機能
- 01React プロバイダーとテーブルフック
- 02通常の TypeScript 接続
- 03自動再接続とバックオフ
- 04再接続時のトークン更新
- 05サブスクリプション再生と準備状態
- 06型付きデータ行と Reducer
活用シーン
- 01SpacetimeDB に接続する React アプリの構築
- 02React を使わない直接接続の作成
- 03ネットワーク障害後のサブスクリプション復元
- 04再接続時に期限付き認証情報を使用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill typescript-clientインストール後、Claude Code で/typescript-clientを入力して読み込まれたか確認します。
詳細
SpacetimeDB TypeScript クライアント
生成されたバインディングでは、行のフィールドも含め、snake_case の名前が camelCase に変換されます。サーバーの列 trip_id は、クライアントの行では tripId になります。
React:main.tsx
import React, { useMemo } from 'react';
import ReactDOM from 'react-dom/client';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection } from './module_bindings';
import { MODULE_NAME, SPACETIMEDB_URI } from './config';
import App from './App';
const TOKEN_KEY = `${SPACETIMEDB_URI}/${MODULE_NAME}/auth_token`;
function Root() {
const connectionBuilder = useMemo(() =>
DbConnection.builder()
.withUri(SPACETIMEDB_URI)
.withDatabaseName(MODULE_NAME)
.withToken(localStorage.getItem(TOKEN_KEY) ?? undefined)
.withAutomaticReconnect()
.onConnect((_conn, _identity, token) => localStorage.setItem(TOKEN_KEY, token)),
[]
);
return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<Root />);
React:App.tsx
import { useTable, useSpacetimeDB } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';
export default function App() {
const { isActive, identity: myIdentity, getConnection } = useSpacetimeDB();
const conn = getConnection() as DbConnection | null;
// useTable owns subscriptions and reports readiness after replay.
const [entities, entitiesReady] = useTable(tables.entity);
const [records, recordsReady] = useTable(tables.record);
const [onlineUsers] = useTable(
tables.entity.where(r => r.active.eq(true)),
{
onInsert: user => console.log('User connected:', user.name),
onDelete: user => console.log('User disconnected:', user.name),
onUpdate: (oldUser, newUser) => console.log('Updated:', newUser.name),
}
);
const addRecord = (data: string) => {
if (!conn || !isActive) return;
void conn.reducers.addRecord({ data }).catch(console.error);
};
const ownsEntity = entities.some(
row => row.owner.toHexString() === myIdentity?.toHexString()
);
return (
<main>
<p>{isActive ? 'Connected' : 'Not connected'}</p>
<p>{entitiesReady && recordsReady ? 'Data ready' : 'Waiting for current data'}</p>
<p>{onlineUsers.length} users online, {records.length} records</p>
<p>{ownsEntity ? 'You own an entity' : 'No owned entity'}</p>
<button disabled={!isActive} onClick={() => addRecord('Hello!')}>
Add record
</button>
</main>
);
}
Vanilla(React 以外)
import { DbConnection, tables } from './module_bindings';
const HOST = 'wss://maincloud.spacetimedb.com';
const DATABASE = 'my_module';
const TOKEN_KEY = `${HOST}/${DATABASE}/auth_token`;
const conn = DbConnection.builder()
.withUri(HOST)
.withDatabaseName(DATABASE)
.withToken(localStorage.getItem(TOKEN_KEY) ?? undefined)
.withAutomaticReconnect()
.onConnect((_conn, identity, token) => {
localStorage.setItem(TOKEN_KEY, token);
console.log('Connected as:', identity.toHexString());
})
.onDisconnect((_ctx, error, nextAttempt, delayMs) => {
if (nextAttempt !== undefined) {
console.warn(`Reconnect attempt ${nextAttempt} in ${delayMs} ms`, error);
} else {
console.log('Connection ended', error);
}
})
.onConnectError((_ctx, error, nextAttempt, delayMs) => {
console.error('Connection failed:', error);
if (nextAttempt !== undefined) {
console.log(`Retry ${nextAttempt} in ${delayMs} ms`);
}
})
.build();
// Register once; the SDK replays this subscription after reconnecting.
const subscription = conn.subscriptionBuilder()
.onApplied(() => console.log('Ready'))
.subscribe([tables.user, tables.message]);
// Row callbacks
conn.db.user.onInsert((ctx, user) => console.log('Joined:', user.name));
conn.db.user.onDelete((ctx, user) => console.log('Left:', user.name));
conn.db.user.onUpdate((ctx, oldUser, newUser) => console.log('Updated:', newUser.name));
自動再接続とトークンの更新
React プロバイダーの接続マネージャーでは、自動再接続が有効になっています。上記の .withAutomaticReconnect() は、直接接続で使用する設定も示しています。この設定がない場合、直接接続は自動的に復旧しません。直接接続では、初回接続の失敗は再試行されません。接続が確立された後に切断された場合は、disconnect() が呼び出されるか、回復不能な失敗が発生するまで、minDelayMs(デフォルトは 1 s)から maxDelayMs(デフォルトは 30 s)まで、指数バックオフとジッターを使用して再試行します。.withAutomaticReconnect({ minDelayMs, maxDelayMs }) でこれらの値を調整できます。500 ms と 1 s の下限より小さい値は警告とともに引き上げられ、再試行するクライアントがデータベースに過剰な負荷をかけないようにします。React プロバイダーがマウントされている間は、別個の接続マネージャーによる再試行も維持されます。コア接続が再試行しない初回または回復不能な失敗の後に、代わりの接続を作成します。明示的な disconnect() は尊重されます。
同じ接続、ID、テーブルハンドル、サブスクリプション、行コールバックが復旧後も維持されます。試行ごとに新しい接続 ID が割り当てられます。onConnect はサブスクリプションの再適用前に再度実行されるため、サブスクリプションと行コールバックは、このコールバックの外で一度だけ登録してください。SDK はサブスクリプションを 1 つのバッチで再適用し、停止中も読み取り可能ですが古くなったキャッシュ済みの行を保持し、整合性の回復後に行の差分を通知します。サブスクリプションの onApplied は再適用後に再度実行されるため、一度だけ行う初期設定は分けてください。
React では、useTable にサブスクリプションを管理させてください。同じクエリに対して 2 つ目のサブスクリプション用エフェクトを追加したり、isActive が変わるたびにサブスクリプションを作り直したりしないでください。データの準備状況には、フックの isReady の結果を使用してください。再接続のハンドシェイクが成功しても、再適用が完了したことにはなりません。リデューサーはレンダリング中ではなく、イベントハンドラーから呼び出してください。手動で作成したサブスクリプションはハンドルを保持し、不要になったとき(停止中を含む)に unsubscribe() を呼び出してください。
直接接続では、conn.isReconnecting により、次のハンドシェイクが成功する前に復旧中であることを確認できます。onDisconnect と onConnectError には (ctx, error, nextReconnectAttempt, nextReconnectDelayMs) が渡されます。再試行が予定されていない場合、最後の 2 つの引数は undefined です。自動復旧が保留中の間は、代わりの接続を作成したり、独自の再試行タイマーを動かしたりしないでください。明示的な conn.disconnect() は、保留中のトークン更新結果も含めて、復旧を停止します。
有効期限のある認証情報では、.withTokenProvider(() => refreshTokenAsync()) も呼び出してください。認証連携で refreshTokenAsync(): Promise<string> を用意します。初期トークンは .withToken(initialToken) で指定してください。プロバイダーが使用されるのは、再接続時のみです。プロバイダーは同じ ID に対する空でないトークンを返す必要があります。SDK は、残りの有効期間が 30 秒または元の有効期間の 5% のいずれか大きい方以下になったとき、有効期限を読み取れないとき、または再利用したトークンが拒否された後に、プロバイダーを呼び出します。プロバイダーの失敗は再試行されますが、新たに提供されたトークンが拒否された場合は回復不能な失敗となります。接続中に定期的な更新は行われず、切断してもプロバイダー自身の非同期処理はキャンセルされません。
切断中に行われた呼び出しは、ただちに失敗します。結果を受け取る前に接続が失われた場合、保留中のリデューサーとプロシージャーの Promise は、spacetimedb からエクスポートされる UnknownCallResultError で拒否されます。サーバーでは操作が実行されている可能性がありますが、SDK が再実行することはありません。このエラーが発生した場合、べき等でない呼び出しを自動的に再試行しないでください。
注意点
useTableの行はreadonlyです。 ソートや変更を行う前にコピーしてください。そうしないと型チェックに失敗します:const [rows] = useTable(tables.message); const sorted = [...rows].sort(...).- JSX 内の bigint。
t.u64()/t.i64()列の ID や件数はbigintであり、React ではレンダリングできません。次のようにラップしてください:{Number(row.id)}または{String(count)}。