typescript-client

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SpacetimeDB TypeScript Client

SpacetimeDB TypeScript 客户端

Generated bindings convert snake_case names to camelCase, including row fields: a server column
trip_id
is
tripId
on client rows.
生成的绑定会将 snake_case 名称转换为 camelCase,包括行字段:服务器列
trip_id
在客户端行中为
tripId
。

React: main.tsx

React: main.tsx

typescript
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 />);
typescript
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

React: App.tsx

typescript
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>
  );
}
typescript
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 (non-React)

原生(非 React)

typescript
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));
typescript
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));

Submodule Accessors and View Arguments

子模块访问器与视图参数

Generated bindings preserve submodule accessors as nested members. For a namespace accessor
myAuth
pinned to canonical
auth_data
:
typescript
conn.subscriptionBuilder().subscribe([tables.myAuth.users]);
// Rows: conn.db.myAuth.users
// Reducers: conn.reducers.myAuth.verifyToken({ token: 'example' })
// React: useTable(tables.myAuth.users), useReducer(reducers.myAuth.verifyToken)
Use generated accessors in SDK calls; they encode canonical names automatically. Raw SQL/HTTP/CLI calls instead use canonical qualified names such as
auth_data.users
or
auth_data.verify_token
. Namespaces share the connection and cache; they are not separate database connections.
For views with arguments (2.13+), use a raw SQL subscription; TypeScript has no typed argument-bearing view query builder yet:
typescript
conn.subscriptionBuilder().subscribe('SELECT * FROM by_id(42)');
Use explicit queries for every table on that connection, not subscribe-all, which skips views with arguments. Only
SELECT * FROM view_name(literal_args)
is supported. All argument values share one cache table; unsubscribe from obsolete arguments to avoid combining their rows. Validate/encode dynamic values as SQL literals rather than concatenating untrusted input. In React, raw SQL subscriptions need explicit lifetime management and cleanup;
useTable
does not supply an argument-bearing view builder.
生成的绑定会将子模块访问器保留为嵌套成员。对于绑定到规范名称
auth_data
的命名空间访问器
myAuth
:
typescript
conn.subscriptionBuilder().subscribe([tables.myAuth.users]);
// Rows: conn.db.myAuth.users
// Reducers: conn.reducers.myAuth.verifyToken({ token: 'example' })
// React: useTable(tables.myAuth.users), useReducer(reducers.myAuth.verifyToken)
在 SDK 调用中使用生成的访问器;它们会自动编码规范名称。相比之下,原始 SQL/HTTP/CLI 调用应使用规范限定名称,例如
auth_data.users
或
auth_data.verify_token
。命名空间共享同一个连接和缓存;它们并非独立的数据库连接。
对于带参数的视图(2.13+),请使用原始 SQL 订阅;TypeScript 目前还没有支持带参数视图的类型化查询构建器:
typescript
conn.subscriptionBuilder().subscribe('SELECT * FROM by_id(42)');
对于该连接上的每张表,请使用显式查询,而不是 subscribe-all,因为 subscribe-all 会跳过带参数的视图。仅支持
SELECT * FROM view_name(literal_args)
。所有参数值共享一个缓存表;请取消对不再使用的参数的订阅,以避免混合对应的行。请将动态值验证并编码为 SQL 字面量,而不要拼接不可信输入。在 React 中,原始 SQL 订阅需要显式的生命周期管理和清理;
useTable
不提供支持参数的视图构建器。

Automatic Reconnect and Token Refresh

自动重连与 Token 刷新

The React provider's connection manager enables automatic reconnect; the explicit
.withAutomaticReconnect()
above also shows the setting to use for direct connections. Without it, a direct connection does not recover automatically. For direct connections, initial failures are not retried. After an established connection drops, retries use exponential backoff and jitter from
minDelayMs
(default 1 s) up to
maxDelayMs
(default 30 s), until
disconnect()
or a terminal failure. Tune these with
.withAutomaticReconnect({ minDelayMs, maxDelayMs })
; values below the 500 ms and 1 s floors are raised with a warning, so that retrying clients cannot overwhelm the database. While mounted, the React provider also preserves its separate connection-manager retries: it builds a replacement connection after an initial or terminal failure that the core connection will not retry. It respects an explicit
disconnect()
.
The same connection, identity, table handles, subscriptions, and row callbacks survive recovery. Each attempt gets a fresh connection ID.
onConnect
runs again before subscription replay, so register subscriptions and row callbacks once, outside that callback. The SDK replays subscriptions in one batch, retains readable but stale cached rows during outages, and emits net row changes after reconciliation. Subscription
onApplied
runs again after replay; keep one-time setup separate.
In React, let
useTable
manage its own subscriptions. Do not add a second subscription effect for the same queries or recreate subscriptions whenever
isActive
changes. Use the hook's
isReady
result for data readiness: a successful reconnect handshake does not mean replay has completed. Invoke reducers from event handlers, not during rendering. For a manually created subscription, retain its handle and call
unsubscribe()
when it is no longer needed, including during an outage.
For direct connections,
conn.isReconnecting
reports recovery before the next successful handshake.
onDisconnect
and
onConnectError
receive
(ctx, error, nextReconnectAttempt, nextReconnectDelayMs)
. The last two arguments are
undefined
when no retry is scheduled. Do not build a replacement connection or run your own retry timer while automatic recovery is pending. An explicit
conn.disconnect()
stops recovery, including a pending token refresh result.
For expiring credentials, also call
.withTokenProvider(() => refreshTokenAsync())
, where your authentication integration supplies
refreshTokenAsync(): Promise<string>
. Supply the initial token with
.withToken(initialToken)
; the provider is used only for reconnect attempts. It must return a non-empty token for the same identity. The SDK calls it when remaining validity is at most 30 seconds or 5% of the original lifetime, whichever is greater, when expiry cannot be read, or after a reused token is rejected. Provider failures retry; rejection of a freshly provided token is terminal. No periodic refresh runs while connected, and disconnecting does not cancel the provider's own asynchronous work.
Calls made while disconnected fail immediately. Pending reducer and procedure promises reject with
UnknownCallResultError
(exported from
spacetimedb
) when the connection is lost before a result arrives. The server may have executed the operation; the SDK never replays it. Do not automatically retry non-idempotent calls on that error.
React provider 的连接管理器会启用自动重连;上面的显式
.withAutomaticReconnect()
也展示了直接连接所需的设置。如果不启用该设置,直接连接将无法自动恢复。对于直接连接,初始连接失败不会重试。已建立的连接中断后,重试会采用从
minDelayMs
(默认 1 s)到
maxDelayMs
(默认 30 s)的指数退避和抖动策略,直至调用
disconnect()
或遇到不可恢复的失败。可通过
.withAutomaticReconnect({ minDelayMs, maxDelayMs })
调整这些值;小于 500 ms 和 1 s 下限的值会触发警告并被上调,从而防止重试客户端压垮数据库。React provider 挂载期间,还会保留其独立的连接管理器重试机制:如果初始连接失败或遇到核心连接不会重试的终止性失败,它会构建替代连接。它会遵循显式调用
disconnect()
。
恢复后,同一连接、身份、表句柄、订阅和行回调都会保留。每次尝试都会获得新的连接 ID。
onConnect
会在订阅重放之前再次运行,因此只需在该回调之外注册一次订阅和行回调。SDK 会批量重放订阅,在服务中断期间保留可读取但已过期的缓存行,并在数据协调后发出净行变更。订阅的
onApplied
会在重放后再次运行;应将一次性初始化逻辑单独处理。
在 React 中,应让
useTable
管理自己的订阅。不要为相同的查询再添加第二个订阅 effect,也不要在
isActive
变化时重新创建订阅。使用 hook 返回的
isReady
判断数据是否就绪:重连握手成功并不表示重放已完成。请在事件处理器中调用 reducer,而不是在渲染期间调用。对于手动创建的订阅,应保留其句柄,并在不再需要时调用
unsubscribe()
,包括服务中断期间。
对于直接连接,
conn.isReconnecting
会在下一次成功握手之前指示恢复进程。
onDisconnect
和
onConnectError
接收
(ctx, error, nextReconnectAttempt, nextReconnectDelayMs)
。未安排重试时,最后两个参数为
undefined
。自动恢复待处理期间,不要构建替代连接或运行自己的重试定时器。显式调用
conn.disconnect()
会停止恢复,包括尚未完成的 Token 刷新结果。
对于会过期的凭据,还应调用
.withTokenProvider(() => refreshTokenAsync())
,其中身份验证集成提供
refreshTokenAsync(): Promise<string>
。使用
.withToken(initialToken)
提供初始 token;token provider 仅在重连尝试时使用。它必须为同一身份返回非空 token。当剩余有效期达到“原始有效期的 5%”与“30 秒”二者中的较大阈值、无法读取过期时间,或者重复使用的 token 被拒绝时,SDK 会调用它。provider 调用失败时会重试;新提供的 token 仍被拒绝则视为终止性失败。连接期间不会定期刷新,断开连接也不会取消 provider 自身的异步工作。
断开连接期间发起的调用会立即失败。如果在结果返回前连接丢失,待决的 reducer 和 procedure Promise 会以
UnknownCallResultError
(从
spacetimedb
导出)拒绝。服务端可能已经执行了该操作;SDK 绝不会重放它。出现该错误时,不要自动重试非幂等调用。

Gotchas

常见陷阱

  • useTable
    rows are
    readonly
    .
    Copy before sorting/mutating, or it fails to type-check:
    const [rows] = useTable(tables.message); const sorted = [...rows].sort(...)
    .
  • bigint in JSX. ids/counts from
    t.u64()
    /
    t.i64()
    columns are
    bigint
    , which React cannot render. Wrap it:
    {Number(row.id)}
    or
    {String(count)}
    .
  • 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)}
    。