关于 typescript-client
面向 SpacetimeDB TypeScript 客户端开发的参考资料,涵盖 React 与原生连接、订阅恢复和令牌刷新,帮助开发者处理重连及类型化数据行。
主要功能
- 01React Provider 与表格 Hook
- 02原生 TypeScript 连接
- 03自动重连与退避策略
- 04重连时刷新令牌
- 05订阅重放与就绪状态
- 06类型化数据行与 Reducer
适用场景
- 01构建连接 SpacetimeDB 的 React 应用
- 02创建非 React 的直接客户端连接
- 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() 也展示了直接连接时的设置。如果不启用此功能,直接连接断开后不会自动恢复。对于直接连接,初始连接失败不会重试。已建立的连接断开后,会采用指数退避和抖动进行重试,延迟从 minDelayMs(默认 1 s)逐渐增加至 maxDelayMs(默认 30 s),直到调用 disconnect() 或发生终止性故障。可使用 .withAutomaticReconnect({ minDelayMs, maxDelayMs }) 调整这些值;低于 500 ms 和 1 s 下限的值会被上调,并发出警告,以免重试的客户端压垮数据库。React 提供程序处于挂载状态时,还会保留其独立的连接管理器重试机制:当核心连接因初始故障或终止性故障而不会重试时,该机制会创建替代连接。显式调用 disconnect() 后,此机制会停止。
恢复后,同一连接、身份、表句柄、订阅和行回调都会保留。每次尝试都会获取新的连接 ID。订阅重放前会再次运行 onConnect,因此请在该回调之外注册订阅和行回调,只注册一次。SDK 会将订阅批量重放,在中断期间保留可读取但已过时的缓存行,并在协调完成后发出净行变更。重放后会再次运行订阅的 onApplied;请将一次性设置逻辑分开处理。
在 React 中,让 useTable 管理自己的订阅。不要针对相同查询添加第二个订阅 effect,也不要在 isActive 发生变化时重新创建订阅。使用钩子的 isReady 结果判断数据是否就绪:重新连接握手成功并不意味着订阅重放已经完成。请从事件处理程序中调用 reducer,而不是在渲染期间调用。对于手动创建的订阅,请保留其句柄,并在不再需要时调用 unsubscribe(),包括连接中断期间。
对于直接连接,conn.isReconnecting 会在下一次成功握手前报告正在恢复。onDisconnect 和 onConnectError 会收到 (ctx, error, nextReconnectAttempt, nextReconnectDelayMs)。未安排重试时,最后两个参数为 undefined。自动恢复正在进行时,不要创建替代连接,也不要运行自己的重试计时器。显式调用 conn.disconnect() 会停止恢复,包括尚未完成的令牌刷新。
对于会过期的凭据,还要调用 .withTokenProvider(() => refreshTokenAsync()),其中身份验证集成会提供 refreshTokenAsync(): Promise<string>。使用 .withToken(initialToken) 提供初始令牌;仅在重新连接时才会使用令牌提供程序。它必须返回与相同身份对应的非空令牌。当剩余有效期不超过 30 秒或原始有效期的 5%(取两者中的较大值)、无法读取到期时间,或复用的令牌被拒绝时,SDK 会调用该提供程序。提供程序失败时会重试;新提供的令牌被拒绝则属于终止性故障。连接期间不会定期刷新令牌,断开连接也不会取消提供程序自身的异步任务。
断开连接时发起的调用会立即失败。如果连接在收到结果前丢失,待处理的 reducer 和过程调用 Promise 会以 UnknownCallResultError(从 spacetimedb 导出)拒绝。服务器可能已经执行了该操作;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)}。