spacetimedb-typescript

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SpacetimeDB TypeScript SDK

SpacetimeDB TypeScript SDK

Build real-time TypeScript clients that connect directly to SpacetimeDB modules. The SDK provides type-safe database access, automatic synchronization, and reactive updates for web apps, Node.js, Deno, Bun, and other JavaScript runtimes.

构建可直接连接SpacetimeDB模块的实时TypeScript客户端。该SDK为Web应用、Node.js、Deno、Bun及其他JavaScript运行时提供类型安全的数据库访问、自动同步和响应式更新功能。

HALLUCINATED APIs — DO NOT USE

虚构的API — 请勿使用

These APIs DO NOT EXIST. LLMs frequently hallucinate them.
typescript
// 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!
这些API并不存在,是大语言模型经常虚构出来的。
typescript
// 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 PATTERNS:

正确用法示例:

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

DO NOT:

请勿:

  • Invent hooks like
    useItems()
    ,
    useData()
    — use
    useTable(tables.tableName)
  • Import from fake packages — only
    spacetimedb
    ,
    spacetimedb/react
    ,
    ./module_bindings

  • 自定义钩子
    useItems()
    useData()
    — 请使用
    useTable(tables.tableName)
  • 导入不存在的包 — 仅可导入
    spacetimedb
    spacetimedb/react
    ./module_bindings

Common Mistakes Table

常见错误对照表

Server-side errors

服务端错误

WrongRightError
Missing
package.json
Create
package.json
"could not detect language"
Missing
tsconfig.json
Create
tsconfig.json
"TsconfigNotFound"
Entrypoint not at
src/index.ts
Use
src/index.ts
Module won't bundle
indexes
in COLUMNS (2nd arg)
indexes
in OPTIONS (1st arg)
"reading 'tag'" error
Index without
algorithm
algorithm: 'btree'
"reading 'tag'" error
filter({ ownerId })
filter(ownerId)
"does not exist in type 'Range'"
.filter()
on unique column
.find()
on unique column
TypeError
insert({ ...without id })
insert({ id: 0n, ... })
"Property 'id' is missing"
const id = table.insert(...)
const row = table.insert(...)
.insert()
returns ROW, not ID
.unique()
+ explicit index
Just use
.unique()
"name is used for multiple entities"
Import spacetimedb from index.tsImport from schema.ts"Cannot access before initialization"
Multi-column index
.filter()
Use single-column indexPANIC or silent empty results
.iter()
in views
Use index lookups onlyViews can't scan tables
ctx.db
in procedures
ctx.withTx(tx => tx.db...)
Procedures need explicit transactions
ctx.myTable
in procedure tx
tx.db.myTable
Wrong context variable
错误做法正确做法错误信息
缺少
package.json
创建
package.json
"could not detect language"
缺少
tsconfig.json
创建
tsconfig.json
"TsconfigNotFound"
入口文件不在
src/index.ts
使用
src/index.ts
模块无法打包
在COLUMNS(第二个参数)中设置
indexes
在OPTIONS(第一个参数)中设置
indexes
"reading 'tag'" error
索引未指定
algorithm
添加
algorithm: 'btree'
"reading 'tag'" error
使用
filter({ ownerId })
使用
filter(ownerId)
"does not exist in type 'Range'"
在唯一列上调用
.filter()
在唯一列上调用
.find()
TypeError
insert({ ...without id })
insert({ id: 0n, ... })
"Property 'id' is missing"
const id = table.insert(...)
const row = table.insert(...)
.insert()
返回的是行对象,而非ID
同时使用
.unique()
和显式索引
仅使用
.unique()
"name is used for multiple entities"
从index.ts导入spacetimedb从schema.ts导入"Cannot access before initialization"
对多列索引使用
.filter()
使用单列索引程序崩溃或返回空结果
在视图中使用
.iter()
仅使用索引查询视图无法扫描整张表
在过程中使用
ctx.db
使用
ctx.withTx(tx => tx.db...)
过程需要显式事务
在过程事务中使用
ctx.myTable
使用
tx.db.myTable
上下文变量错误

Client-side errors

客户端错误

WrongRightError
@spacetimedb/sdk
spacetimedb
404 / missing subpath
conn.reducers.foo("val")
conn.reducers.foo({ param: "val" })
Wrong reducer syntax
Inline
connectionBuilder
useMemo(() => ..., [])
Reconnects every render
const rows = useTable(table)
const [rows, isLoading] = useTable(table)
Tuple destructuring
Optimistic UI updatesLet subscriptions drive stateDesync issues
<SpacetimeDBProvider builder={...}>
connectionBuilder={...}
Wrong prop name

错误做法正确做法错误信息
使用
@spacetimedb/sdk
使用
spacetimedb
404 / 缺少子路径
conn.reducers.foo("val")
conn.reducers.foo({ param: "val" })
Reducer语法错误
内联
connectionBuilder
使用
useMemo(() => ..., [])
每次渲染都会重新连接
const rows = useTable(table)
const [rows, isLoading] = useTable(table)
需使用元组解构
乐观UI更新由订阅驱动状态数据不一致问题
<SpacetimeDBProvider builder={...}>
<SpacetimeDBProvider connectionBuilder={...}>
属性名称错误

Hard Requirements

硬性要求

  1. DO NOT edit generated bindings — regenerate with
    spacetime generate
  2. Reducers are transactional — they do not return data
  3. Reducers must be deterministic — no filesystem, network, timers, random
  4. Reducer calls use object syntax
    { param: 'value' }
    not positional args
  5. Import
    DbConnection
    from
    ./module_bindings
    — not from
    spacetimedb
  6. useTable returns a tuple
    const [rows, isLoading] = useTable(tables.myTable)
  7. Memoize connectionBuilder — wrap in
    useMemo(() => ..., [])
    to prevent reconnects
  8. Views can only use index lookups
    .iter()
    is not allowed in views

  1. 请勿编辑生成的绑定代码 — 请使用
    spacetime generate
    重新生成
  2. Reducer是事务性的 — 它们不会返回数据
  3. Reducer必须是确定性的 — 不可使用文件系统、网络、定时器或随机数
  4. 调用Reducer必须使用对象语法 — 如
    { param: 'value' }
    ,而非位置参数
  5. ./module_bindings
    导入
    DbConnection
    — 不可从
    spacetimedb
    导入
  6. useTable返回元组
    const [rows, isLoading] = useTable(tables.myTable)
  7. 对connectionBuilder进行记忆化 — 用
    useMemo(() => ..., [])
    包裹以避免重复连接
  8. 视图仅可使用索引查询 — 不允许在视图中使用
    .iter()

Installation

安装步骤

bash
npm install spacetimedb
bash
npm install spacetimedb

or

or

pnpm add spacetimedb
pnpm add spacetimedb

or

or

yarn add spacetimedb

For Node.js 18-21, install the `undici` peer dependency:

```bash
npm install spacetimedb undici
Node.js 22+ and browser environments work out of the box.
yarn add spacetimedb

对于Node.js 18-21版本,需安装`undici` peer依赖:

```bash
npm install spacetimedb undici
Node.js 22+版本及浏览器环境可直接使用,无需额外依赖。

Generating Type Bindings

生成类型绑定

Before using the SDK, generate TypeScript bindings from your SpacetimeDB module:
bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --project-path ./server
This creates a
module_bindings
directory with:
  • index.ts
    - Main exports including
    DbConnection
    ,
    tables
    ,
    reducers
    ,
    query
  • Type definitions for each table (e.g.,
    player_table.ts
    ,
    user_table.ts
    )
  • Type definitions for each reducer (e.g.,
    create_player_reducer.ts
    )
  • Custom type definitions (e.g.,
    point_type.ts
    )
使用SDK前,请从SpacetimeDB模块生成TypeScript绑定:
bash
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

Basic Connection Setup

基础连接配置

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

    // 存储token用于重连
    localStorage.setItem('spacetimedb_token', token);

    // 连接后订阅表
    conn.subscriptionBuilder().subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => {
    console.log('Disconnected');
  })
  .onConnectError((ctx, error) => {
    console.error('Connection error:', error);
  })
  .build();

Connection Builder Options

连接构建器选项

typescript
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();
typescript
DbConnection.builder()
  // 必填:SpacetimeDB服务器URI
  .withUri('ws://localhost:3000')

  // 必填:数据库模块名称或地址
  .withModuleName('my_database')

  // 可选:用于重连的认证token
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)

  // 可选:启用压缩(默认值:'gzip')
  .withCompression('gzip')  // 或'none'

  // 可选:轻量模式可减少网络流量
  .withLightMode(true)

  // 可选:在接收更新前等待持久化写入完成
  .withConfirmedReads(true)

  // 连接生命周期回调
  .onConnect((conn, identity, token) => { /* ... */ })
  .onDisconnect((ctx, error) => { /* ... */ })
  .onConnectError((ctx, error) => { /* ... */ })

  .build();

Subscribing to Tables

订阅表数据

Subscriptions sync table data to the client cache. Use SQL queries to filter what data you receive.
订阅功能可将表数据同步到客户端缓存。使用SQL查询过滤需要接收的数据。

Basic Subscription

基础订阅

typescript
connection.subscriptionBuilder()
  .onApplied((ctx) => {
    console.log('Subscription applied, cache is ready');
  })
  .onError((ctx, error) => {
    console.error('Subscription error:', error);
  })
  .subscribe('SELECT * FROM player');
typescript
connection.subscriptionBuilder()
  .onApplied((ctx) => {
    console.log('Subscription applied, cache is ready');
  })
  .onError((ctx, error) => {
    console.error('Subscription error:', error);
  })
  .subscribe('SELECT * FROM player');

Multiple Queries

多查询订阅

typescript
connection.subscriptionBuilder()
  .subscribe([
    'SELECT * FROM player',
    'SELECT * FROM game_state',
    'SELECT * FROM message WHERE room_id = 1'
  ]);
typescript
connection.subscriptionBuilder()
  .subscribe([
    'SELECT * FROM player',
    'SELECT * FROM game_state',
    'SELECT * FROM message WHERE room_id = 1'
  ]);

Typed Query Builder

类型安全查询构建器

Use the generated
query
object for type-safe queries:
typescript
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()
  );
使用生成的
query
对象进行类型安全的查询:
typescript
import { query } from './module_bindings';

// 简单查询 - 选择所有行
connection.subscriptionBuilder()
  .subscribe(query.player.build());

// 带WHERE子句的查询
connection.subscriptionBuilder()
  .subscribe(
    query.player
      .where(row => row.name.eq('Alice'))
      .build()
  );

// 复杂条件查询
connection.subscriptionBuilder()
  .subscribe(
    query.player
      .where(row => row.score.gte(100))
      .where(row => row.isActive.eq(true))
      .build()
  );

Subscribe to All Tables

订阅所有表

For development or small datasets:
typescript
connection.subscriptionBuilder().subscribeToAllTables();
适用于开发环境或小型数据集:
typescript
connection.subscriptionBuilder().subscribeToAllTables();

Unsubscribing

取消订阅

typescript
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');
});
typescript
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

// 后续取消订阅
handle.unsubscribe();

// 或在取消完成后执行回调
handle.unsubscribeThen((ctx) => {
  console.log('Unsubscribed successfully');
});

Accessing Table Data

访问表数据

After subscription, access cached data through
connection.db
:
typescript
// 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');
订阅完成后,可通过
connection.db
访问缓存数据:
typescript
// 遍历所有行
for (const player of connection.db.player.iter()) {
  console.log(player.name, player.score);
}

// 转换为数组
const players = Array.from(connection.db.player.iter());

// 统计行数
const count = connection.db.player.count();

// 通过主键查找(如果表有主键)
const player = connection.db.player.id.find(42);

// 通过索引列查找
const alice = connection.db.player.name.find('Alice');

Table Event Callbacks

表事件回调

Listen for real-time changes to table data:
typescript
// 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);
监听表数据的实时变化:
typescript
// 行插入事件
connection.db.player.onInsert((ctx, player) => {
  console.log('New player:', player.name);
});

// 行删除事件
connection.db.player.onDelete((ctx, player) => {
  console.log('Player left:', player.name);
});

// 行更新事件(要求表有主键)
connection.db.player.onUpdate((ctx, oldPlayer, newPlayer) => {
  console.log(`${oldPlayer.name} score: ${oldPlayer.score} -> ${newPlayer.score}`);
});

// 移除回调
const onInsertCb = (ctx, player) => console.log(player);
connection.db.player.onInsert(onInsertCb);
connection.db.player.removeOnInsert(onInsertCb);

Event Context

事件上下文

Callbacks receive an
EventContext
with information about the event:
typescript
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!' });
});
回调函数会接收包含事件信息的
EventContext
typescript
connection.db.player.onInsert((ctx, player) => {
  // 访问数据库
  const allPlayers = Array.from(ctx.db.player.iter());

  // 检查事件类型
  if (ctx.event.tag === 'Reducer') {
    const { callerIdentity, reducer, status } = ctx.event.value;
    console.log(`Triggered by reducer: ${reducer.name}`);
  }

  // 调用其他Reducer
  ctx.reducers.sendMessage({ playerId: player.id, text: 'Welcome!' });
});

Calling Reducers

调用Reducer

Reducers are server-side functions that modify the database. CRITICAL: Use object syntax, not positional arguments.
typescript
// 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);
Reducer是用于修改数据库的服务端函数。重要:必须使用对象语法,而非位置参数。
typescript
// 正确:对象语法
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });

// 错误:位置参数
// connection.reducers.createPlayer('Alice', { x: 0, y: 0 });  // 请勿这样做

// 监听Reducer执行结果
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);
  }
});

// 移除Reducer回调
connection.reducers.removeOnCreatePlayer(callback);

Snake_case to camelCase conversion

蛇形命名转驼峰命名

  • Server:
    spacetimedb.reducer('do_something', ...)
  • Client:
    conn.reducers.doSomething({ ... })
  • 服务端:
    spacetimedb.reducer('do_something', ...)
  • 客户端:
    conn.reducers.doSomething({ ... })

Reducer Flags

Reducer标志

Control how the server handles reducer calls:
typescript
// NoSuccessNotify: Don't send TransactionUpdate on success (reduces traffic)
connection.setReducerFlags.movePlayer('NoSuccessNotify');

// FullUpdate: Always send full TransactionUpdate (default)
connection.setReducerFlags.movePlayer('FullUpdate');
控制服务端处理Reducer调用的方式:
typescript
// NoSuccessNotify: 成功时不发送TransactionUpdate(减少流量)
connection.setReducerFlags.movePlayer('NoSuccessNotify');

// FullUpdate: 始终发送完整的TransactionUpdate(默认值)
connection.setReducerFlags.movePlayer('FullUpdate');

Views

视图

Views provide filtered access to private table data based on the connected user.
视图可根据连接用户的身份,提供对私有表数据的过滤访问。

ViewContext vs AnonymousViewContext

ViewContext vs AnonymousViewContext

typescript
// 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 */)];
});
typescript
// ViewContext — 包含ctx.sender,结果因用户而异(每个订阅者单独计算)
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
  return [...ctx.db.item.by_owner.filter(ctx.sender)];
});

// AnonymousViewContext — 无ctx.sender,所有用户结果相同(共享计算,性能更好)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => {
  return [...ctx.db.player.by_score.filter(/* top scores */)];
});

CRITICAL: Views can only use index lookups

重要:视图仅可使用索引查询

typescript
// 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)]
);
typescript
// 错误 — 视图中不能使用.iter()
spacetimedb.view(
  { name: 'my_data_wrong', public: true },
  t.array(PrivateData.rowType),
  (ctx) => [...ctx.db.privateData.iter()]  // 不允许
);

// 正确 — 使用索引查询
spacetimedb.view(
  { name: 'my_data', public: true },
  t.array(PrivateData.rowType),
  (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)]
);

Subscribing to Views

订阅视图

Views require explicit subscription:
typescript
conn.subscriptionBuilder().subscribe([
  'SELECT * FROM public_table',
  'SELECT * FROM my_data',  // Views need explicit SQL!
]);
视图需要显式订阅:
typescript
conn.subscriptionBuilder().subscribe([
  'SELECT * FROM public_table',
  'SELECT * FROM my_data',  // 视图需要显式SQL!
]);

Procedures (Beta)

过程(Beta版)

Procedures are for side effects (HTTP requests, etc.) that reducers can't do.
Procedures are currently in beta. API may change.
过程用于处理Reducer无法完成的副作用(如HTTP请求等)。
目前过程处于Beta阶段,API可能会发生变化。

Defining a procedure

定义过程

typescript
spacetimedb.procedure(
  'fetch_external_data',
  { url: t.string() },
  t.string(),  // return type
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    return response.text();
  }
);
typescript
spacetimedb.procedure(
  'fetch_external_data',
  { url: t.string() },
  t.string(),  // 返回类型
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    return response.text();
  }
);

CRITICAL: Database access in procedures

重要:过程中的数据库访问

Procedures don't have
ctx.db
. Use
ctx.withTx()
for database access.
typescript
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 {};
});
过程中没有
ctx.db
,请使用
ctx.withTx()
进行数据库访问。
typescript
spacetimedb.procedure('save_fetched_data', { url: t.string() }, t.unit(), (ctx, { url }) => {
  // 外部数据获取(事务外)
  const response = ctx.http.fetch(url);
  const data = response.text();

  // 错误 — 过程中不存在ctx.db
  // ctx.db.myTable.insert({ ... });

  // 正确 — 使用ctx.withTx()进行数据库访问
  ctx.withTx(tx => {
    tx.db.myTable.insert({
      id: 0n,
      content: data,
      fetchedAt: tx.timestamp,
      fetchedBy: tx.sender,
    });
  });

  return {};
});

Key differences from reducers

与Reducer的主要区别

ReducersProcedures
ctx.db
available directly
Must use
ctx.withTx(tx => tx.db...)
Automatic transactionManual transaction management
No HTTP/network
ctx.http.fetch()
available
No return values to callerCan return data to caller
ReducersProcedures
可直接使用
ctx.db
必须使用
ctx.withTx(tx => tx.db...)
自动事务管理手动事务管理
不支持HTTP/网络操作支持
ctx.http.fetch()
无法向调用者返回值可向调用者返回数据

Identity and Authentication

身份与认证

typescript
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();
typescript
import { Identity } from 'spacetimedb';

// 获取当前身份
const identity = connection.identity;
console.log(identity?.toHexString());

// 比较身份
if (identity?.isEqual(otherIdentity)) {
  console.log('Same user');
}

// 从十六进制字符串创建
const parsed = Identity.fromString('0x1234...');

// 零值身份
const zero = Identity.zero();

// 使用toHexString()比较身份
const isOwner = row.ownerId.toHexString() === myIdentity.toHexString();

Persisting Authentication

持久化认证信息

typescript
// 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)
typescript
// 连接成功时保存token
.onConnect((conn, identity, token) => {
  localStorage.setItem('auth_token', token);
  localStorage.setItem('identity', identity.toHexString());
})

// 重连时使用保存的token
.withToken(localStorage.getItem('auth_token') ?? undefined)

Stale token handling

过期token处理

typescript
const onConnectError = (_ctx: ErrorContext, err: Error) => {
  if (err.message?.includes('Unauthorized') || err.message?.includes('401')) {
    localStorage.removeItem('auth_token');
    window.location.reload();
  }
};
typescript
const onConnectError = (_ctx: ErrorContext, err: Error) => {
  if (err.message?.includes('Unauthorized') || err.message?.includes('401')) {
    localStorage.removeItem('auth_token');
    window.location.reload();
  }
};

React Integration

React集成

The SDK includes React hooks for reactive UI updates.
SDK包含用于响应式UI更新的React钩子。

Provider Setup

Provider配置

tsx
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>
);
tsx
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() {
  // 重要:记忆化以避免每次渲染都重新连接
  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)),
    []  // 空依赖数组 - 仅创建一次
  );

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

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

useSpacetimeDB Hook

useSpacetimeDB钩子

Access connection state:
tsx
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>;
}
访问连接状态:
tsx
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 Hook

useTable钩子

Subscribe to table data with reactive updates. CRITICAL: Returns a tuple
[rows, isLoading]
.
tsx
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>
  );
}
订阅表数据并实现响应式更新。重要:返回元组
[rows, isLoading]
tsx
import { useTable, where, eq } from 'spacetimedb/react';
import { tables } from './module_bindings';

function PlayerList() {
  // 正确:元组解构
  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() {
  // 带回调的过滤玩家列表
  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 Hook

useReducer钩子

Call reducers from components:
tsx
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>
  );
}
在组件中调用Reducer:
tsx
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();
    // 正确:对象语法
    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 Integration

Vue集成

The SDK includes Vue composables:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/vue';
Usage is similar to React hooks.
SDK包含Vue组合式函数:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/vue';
用法与React钩子类似。

Svelte Integration

Svelte集成

The SDK includes Svelte stores:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/svelte';
SDK包含Svelte存储:
typescript
import { SpacetimeDBProvider, useSpacetimeDB, useTable, useReducer } from 'spacetimedb/svelte';

Server-Side Usage (Node.js, Deno, Bun)

服务端使用(Node.js, Deno, Bun)

The SDK works in server-side JavaScript runtimes:
typescript
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();
SDK可在服务端JavaScript运行时中使用:
typescript
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(() => {
          // 处理数据
          for (const player of conn.db.player.iter()) {
            console.log(player);
          }
        })
        .subscribe('SELECT * FROM player');
    })
    .build();
}

main();

Timestamps

时间戳

Server-side

服务端

typescript
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
typescript
import { Timestamp, ScheduleAt } from 'spacetimedb';

// 当前时间
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });

// 未来时间(添加微秒)
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;  // 5分钟

Client-side (CRITICAL)

客户端(重要)

Timestamps are objects, not numbers:
typescript
// 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));
时间戳是对象,而非数字:
typescript
// 错误
const date = new Date(row.createdAt);
const date = new Date(Number(row.createdAt / 1000n));

// 正确
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

ScheduleAt on client

客户端的ScheduleAt

typescript
// ScheduleAt is a tagged union
if (scheduleAt.tag === 'Time') {
  const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n));
}
typescript
// ScheduleAt是标签联合类型
if (scheduleAt.tag === 'Time') {
  const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n));
}

Scheduled Tables

调度表

typescript
// 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);
typescript
// 调度表必须包含scheduledId和scheduledAt列
export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: 'run_cleanup'  // reducer名称
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),  // 自定义数据
});

// 调度Reducer会接收完整行作为参数
spacetimedb.reducer('run_cleanup', { arg: CleanupJob.rowType }, (ctx, { arg }) => {
  // 可访问arg.scheduledId、arg.targetId
  // Reducer执行完成后该行会自动删除
});

// 调度任务
import { ScheduleAt } from 'spacetimedb';
const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60秒
ctx.db.cleanupJob.insert({
  scheduledId: 0n,
  scheduledAt: ScheduleAt.time(futureTime),
  targetId: someId
});

// 通过删除行取消任务
ctx.db.cleanupJob.scheduledId.delete(jobId);

Error Handling

错误处理

Connection Errors

连接错误

typescript
DbConnection.builder()
  .onConnectError((ctx, error) => {
    console.error('Failed to connect:', error.message);

    // Implement retry logic
    setTimeout(() => {
      // Rebuild connection
    }, 5000);
  })
  .build();
typescript
DbConnection.builder()
  .onConnectError((ctx, error) => {
    console.error('Failed to connect:', error.message);

    // 实现重试逻辑
    setTimeout(() => {
      // 重建连接
    }, 5000);
  })
  .build();

Subscription Errors

订阅错误

typescript
connection.subscriptionBuilder()
  .onError((ctx, error) => {
    console.error('Subscription failed:', error.message);
  })
  .subscribe('SELECT * FROM player');
typescript
connection.subscriptionBuilder()
  .onError((ctx, error) => {
    console.error('Subscription failed:', error.message);
  })
  .subscribe('SELECT * FROM player');

Reducer Errors

Reducer错误

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

Disconnecting

断开连接

typescript
// Gracefully disconnect
connection.disconnect();
typescript
// 优雅断开连接
connection.disconnect();

Type Reference

类型参考

Core Types

核心类型

typescript
import {
  Identity,           // User identity (256-bit)
  ConnectionId,       // Connection identifier
  Timestamp,          // SpacetimeDB timestamp
  TimeDuration,       // Duration type
  Uuid,               // UUID type
} from 'spacetimedb';
typescript
import {
  Identity,           // 用户身份(256位)
  ConnectionId,       // 连接标识符
  Timestamp,          // SpacetimeDB时间戳
  TimeDuration,       // 时长类型
  Uuid,               // UUID类型
} from 'spacetimedb';

Generated Types

生成的类型

typescript
// 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';
typescript
// 来自module_bindings
import {
  DbConnection,        // 连接类
  DbConnectionBuilder, // 构建器类
  SubscriptionBuilder, // 订阅构建器
  SubscriptionHandle,  // 订阅句柄
  EventContext,        // 事件回调上下文
  ReducerEventContext, // Reducer回调上下文
  ErrorContext,        // 错误回调上下文
  tables,              // 用于useTable的表访问器
  reducers,            // 用于useReducer的Reducer定义
  query,               // 类型安全查询构建器

  // 自定义类型
  Player,
  Point,
  // ... 其他
} from './module_bindings';

Commands

命令

bash
undefined
bash
undefined

Start local server

启动本地服务器

spacetime start
spacetime start

Publish module

发布模块

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

Clear database and republish

清空数据库并重新发布

spacetime publish <module-name> --clear-database -y --project-path <backend-dir>
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>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --project-path <backend-dir>

View logs

查看日志

spacetime logs <module-name>
undefined
spacetime logs <module-name>
undefined

Best Practices

最佳实践

  1. Store auth tokens: Save the token from
    onConnect
    for seamless reconnection.
  2. Subscribe after connect: Set up subscriptions in the
    onConnect
    callback.
  3. Use typed queries: Prefer the
    query
    builder over raw SQL strings for type safety.
  4. Handle all connection states: Implement
    onConnect
    ,
    onDisconnect
    , and
    onConnectError
    .
  5. Use light mode for high-frequency updates: Enable
    .withLightMode(true)
    for games or real-time apps.
  6. Unsubscribe when done: Clean up subscriptions when components unmount or data is no longer needed.
  7. Use primary keys: Define primary keys on tables to enable
    onUpdate
    callbacks.
  8. Memoize connectionBuilder: Always wrap in
    useMemo()
    to prevent reconnects.
  9. Let subscriptions drive state: Avoid optimistic updates; let the server be the source of truth.
  1. 存储认证token:保存
    onConnect
    返回的token以实现无缝重连。
  2. 连接后再订阅:在
    onConnect
    回调中设置订阅。
  3. 使用类型安全查询:优先使用
    query
    构建器而非原始SQL字符串以保证类型安全。
  4. 处理所有连接状态:实现
    onConnect
    onDisconnect
    onConnectError
    回调。
  5. 高频更新场景使用轻量模式:游戏或实时应用中启用
    .withLightMode(true)
  6. 不再需要时取消订阅:组件卸载或不再需要数据时清理订阅。
  7. 使用主键:为表定义主键以启用
    onUpdate
    回调。
  8. 对connectionBuilder进行记忆化:始终用
    useMemo()
    包裹以避免重复连接。
  9. 由订阅驱动状态:避免乐观更新,以服务端作为数据的唯一来源。

Common Patterns

常见模式

Reconnection Logic

重连逻辑

typescript
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();
}
typescript
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(() => {
      // 延迟后重连
      setTimeout(() => {
        createConnection(localStorage.getItem('token') ?? undefined);
      }, 3000);
    })
    .build();
}

Optimistic Updates

乐观更新

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

  // 与实际数据同步
  useEffect(() => {
    setOptimisticScore(player.score);
  }, [player.score]);

  return <div onClick={handleClick}>Score: {optimisticScore}</div>;
}

Filtering with Multiple Conditions

多条件过滤

typescript
// 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);
typescript
// 使用查询构建器
query.player
  .where(row => row.team.eq('red'))
  .where(row => row.score.gte(100))
  .build();

// 使用React钩子
const [redTeamHighScorers] = useTable(
  tables.player,
  where(eq('team', 'red')),  // 客户端额外过滤
);
const filtered = redTeamHighScorers.filter(p => p.score >= 100);

Project Structure

项目结构

Server (
backend/spacetimedb/
)

服务端(
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
src/schema.ts   -> 定义表结构并导出spacetimedb
src/index.ts    -> 定义Reducer、生命周期,导入schema
package.json    -> { "type": "module", "dependencies": { "spacetimedb": "^1.11.0" } }
tsconfig.json   -> 标准配置

Avoiding circular imports

避免循环导入

schema.ts -> defines tables AND exports spacetimedb
index.ts  -> imports spacetimedb from ./schema, defines reducers
schema.ts -> 定义表结构并导出spacetimedb
index.ts  -> 从./schema导入spacetimedb,定义Reducer

Client (
client/
)

客户端(
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
src/module_bindings/ -> 生成的绑定代码(spacetime generate)
src/main.tsx         -> Provider、连接配置
src/App.tsx          -> UI组件
src/config.ts        -> MODULE_NAME、SPACETIMEDB_URI