mcpField
T
typescript-client

typescript-client

github.com/clockworklabs开发25.4k91 次安装今天更新
代码仓库

关于 typescript-client

面向 SpacetimeDB TypeScript 客户端开发的参考资料,涵盖 React 与原生连接、订阅恢复和令牌刷新,帮助开发者处理重连及类型化数据行。

主要功能

  1. 01React Provider 与表格 Hook
  2. 02原生 TypeScript 连接
  3. 03自动重连与退避策略
  4. 04重连时刷新令牌
  5. 05订阅重放与就绪状态
  6. 06类型化数据行与 Reducer

适用场景

  1. 01构建连接 SpacetimeDB 的 React 应用
  2. 02创建非 React 的直接客户端连接
  3. 03网络中断后恢复订阅
  4. 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)}。

评论

支持 Markdown