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!

// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn);  // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable);          // Use schema({ myTable })
schema(t1, t2, t3);      // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup'  // Use scheduled: () => run_cleanup
.withModuleName('db')     // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify')  // Removed in 2.0
这些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!

// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn);  // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable);          // Use schema({ myTable })
schema(t1, t2, t3);      // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup'  // Use scheduled: () => run_cleanup
.withModuleName('db')     // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify')  // Removed in 2.0

CORRECT PATTERNS:

正确用法示例:

typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings';  // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';

// 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, isReady]
const [items, isReady] = useTable(tables.item);
typescript
// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings';  // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';

// 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, isReady]
const [items, isReady] = 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) of
table()
"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"
Incorrect multi-column
.filter()
range shape
Match index prefix/tuple shapeEmpty results or range/type errors
.iter()
in views
Use index lookups onlyViews can't scan tables
ctx.db
in procedures
ctx.withTx(tx => tx.db...)
Procedures need explicit transactions
错误用法正确用法报错信息
缺失
package.json
新建
package.json
"could not detect language"
缺失
tsconfig.json
新建
tsconfig.json
"TsconfigNotFound"
入口文件不是
src/index.ts
使用
src/index.ts
作为入口
模块无法打包
indexes
写在COLUMNS(第二个参数)中
indexes
写在
table()
的OPTIONS(第一个参数)中
"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...)
存储过程需要显式事务

Client-side errors

客户端错误

WrongRightError
Inline
connectionBuilder
useMemo(() => ..., [])
Reconnects every render
const rows = useTable(table)
const [rows, isReady] = useTable(table)
Tuple destructuring
Optimistic UI updatesLet subscriptions drive stateDesync issues
<SpacetimeDBProvider builder={...}>
connectionBuilder={...}
Wrong prop name

错误用法正确用法报错信息
内联
connectionBuilder
使用
useMemo(() => ..., [])
包裹
每次渲染都会重连
const rows = useTable(table)
const [rows, isReady] = useTable(table)
元组解构错误
手动实现乐观UI更新让订阅驱动状态更新数据不同步问题
<SpacetimeDBProvider builder={...}>
使用
connectionBuilder={...}
属性名错误

Hard Requirements

硬性要求

  1. schema({ table })
    — use a single tables object; optional module settings are allowed as a second argument
  2. Reducer/procedure names from exports
    export const name = spacetimedb.reducer(params, fn)
    ; never
    reducer('name', ...)
  3. Reducer calls use object syntax
    { param: 'value' }
    not positional args
  4. Import
    DbConnection
    from
    ./module_bindings
    — not from
    spacetimedb
  5. DO NOT edit generated bindings — regenerate with
    spacetime generate
  6. Indexes go in OPTIONS (1st arg) — not in COLUMNS (2nd arg) of
    table()
  7. Use BigInt for u64/i64 fields
    0n
    ,
    1n
    , not
    0
    ,
    1
  8. Reducers are transactional — they do not return data
  9. Reducers must be deterministic — no filesystem, network, timers, random
  10. Views should use index lookups
    .iter()
    causes severe performance issues
  11. Procedures need
    ctx.withTx()
    ctx.db
    doesn't exist in procedures
  12. Sum type values — use
    { tag: 'variant', value: payload }
    not
    { variant: payload }
  13. Use
    .withDatabaseName()
    — not
    .withModuleName()
    (2.0)

  1. schema({ table })
    — 传入单个表对象作为参数,第二个参数可传入可选的模块配置
  2. Reducer/存储过程名称来自导出声明 — 写法为
    export const name = spacetimedb.reducer(params, fn)
    ,禁止使用
    reducer('name', ...)
  3. Reducer调用使用对象语法 — 格式为
    { param: 'value' }
    ,不使用位置参数
  4. ./module_bindings
    导入
    DbConnection
    — 不要从
    spacetimedb
    导入
  5. 请勿编辑生成的绑定文件 — 使用
    spacetime generate
    重新生成
  6. 索引写在OPTIONS(第一个参数)中 — 不要写在
    table()
    的COLUMNS(第二个参数)中
  7. u64/i64字段使用BigInt — 写法为
    0n
    1n
    ,不要使用
    0
    1
  8. Reducer是事务性的 — 不返回数据
  9. Reducer必须是确定性的 — 不能访问文件系统、网络、定时器、随机数
  10. 视图应使用索引查询
    .iter()
    会导致严重性能问题
  11. 存储过程需要使用
    ctx.withTx()
    — 存储过程中不存在
    ctx.db
  12. Sum类型值格式 — 使用
    { tag: 'variant', value: payload }
    ,不要使用
    { variant: payload }
  13. 使用
    .withDatabaseName()
    — 2.0版本中已废弃
    .withModuleName()

Installation

安装

bash
npm install spacetimedb
For Node.js environments without native fetch/WebSocket support, install
undici
.
bash
npm install spacetimedb
如果Node.js环境没有原生fetch/WebSocket支持,请安装
undici

Generating Type Bindings

生成类型绑定

bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./server
bash
spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./server

Client Connection

客户端连接

typescript
import { DbConnection } from './module_bindings';

const connection = DbConnection.builder()
  .withUri('ws://localhost:3000')
  .withDatabaseName('my_database')
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
  .onConnect((conn, identity, token) => {
    // identity: your unique Identity for this database
    console.log('Connected as:', identity.toHexString());

    // Save token for reconnection (preserves identity across sessions)
    localStorage.setItem('spacetimedb_token', token);

    conn.subscriptionBuilder()
      .onApplied(() => console.log('Cache ready'))
      .subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => console.log('Disconnected'))
  .onConnectError((ctx, error) => console.error('Connection failed:', error))
  .build();
typescript
import { DbConnection } from './module_bindings';

const connection = DbConnection.builder()
  .withUri('ws://localhost:3000')
  .withDatabaseName('my_database')
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
  .onConnect((conn, identity, token) => {
    // identity: 你在该数据库的唯一身份标识
    console.log('Connected as:', identity.toHexString());

    // 保存token用于重连(跨会话保留身份)
    localStorage.setItem('spacetimedb_token', token);

    conn.subscriptionBuilder()
      .onApplied(() => console.log('Cache ready'))
      .subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => console.log('Disconnected'))
  .onConnectError((ctx, error) => console.error('Connection failed:', error))
  .build();

Subscribing to Tables

订阅表数据

typescript
// Basic subscription
connection.subscriptionBuilder()
  .onApplied((ctx) => console.log('Cache ready'))
  .subscribe('SELECT * FROM player');

// Multiple queries
connection.subscriptionBuilder()
  .subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);

// Subscribe to all tables (development only — cannot mix with Subscribe)
connection.subscriptionBuilder().subscribeToAllTables();

// Subscription handle for later unsubscribe
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

handle.unsubscribeThen(() => console.log('Unsubscribed'));
typescript
// 基础订阅
connection.subscriptionBuilder()
  .onApplied((ctx) => console.log('Cache ready'))
  .subscribe('SELECT * FROM player');

// 多查询订阅
connection.subscriptionBuilder()
  .subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);

// 订阅所有表(仅开发环境使用 — 不能和普通Subscribe混用)
connection.subscriptionBuilder().subscribeToAllTables();

// 获取订阅句柄用于后续取消订阅
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

handle.unsubscribeThen(() => console.log('Unsubscribed'));

Accessing Table Data

访问表数据

typescript
for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);
typescript
for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);

Table Event Callbacks

表事件回调

typescript
connection.db.player.onInsert((ctx, player) => console.log('New:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('Left:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));
typescript
connection.db.player.onInsert((ctx, player) => console.log('新增玩家:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('玩家离开:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));

Calling Reducers

调用Reducer

CRITICAL: Use object syntax, not positional arguments.
typescript
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });
重要提示:使用对象语法,不要使用位置参数。
typescript
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });

Snake_case to camelCase conversion

蛇形命名转驼峰命名规则

  • Server:
    export const do_something = spacetimedb.reducer(...)
  • Client:
    conn.reducers.doSomething({ ... })

  • 服务端:
    export const do_something = spacetimedb.reducer(...)
  • 客户端:
    conn.reducers.doSomething({ ... })

Identity and Authentication

身份与认证

  • identity
    and
    token
    are provided in the
    onConnect
    callback (see Client Connection above)
  • identity.toHexString()
    for display or logging
  • Omit
    .withToken()
    for anonymous connection — server assigns a new identity
  • Pass a stale/invalid token: server issues a new identity and token in
    onConnect

  • identity
    token
    会在
    onConnect
    回调中返回(参考上方客户端连接示例)
  • 使用
    identity.toHexString()
    用于展示或日志记录
  • 省略
    .withToken()
    可匿名连接 — 服务端会分配新的身份标识
  • 传入过期/无效的token: 服务端会在
    onConnect
    中返回新的身份标识和token

Error Handling

错误处理

Connection-level errors (
.onConnectError
,
.onDisconnect
) are shown in the Client Connection example above.
typescript
// Subscription error
connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .onError((ctx) => console.error('Subscription error:', ctx.event))
  .subscribe('SELECT * FROM player');

连接级错误(
.onConnectError
.onDisconnect
)的用法已在上方客户端连接示例中展示。
typescript
// 订阅错误处理
connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .onError((ctx) => console.error('Subscription error:', ctx.event))
  .subscribe('SELECT * FROM player');

Server-Side Module Development

服务端模块开发

Table Definition

表定义

typescript
import { schema, table, t } from 'spacetimedb/server';

export const Task = table({
  name: 'task',
  public: true,
  indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
  id: t.u64().primaryKey().autoInc(),
  ownerId: t.identity(),
  title: t.string(),
  createdAt: t.timestamp(),
});
typescript
import { schema, table, t } from 'spacetimedb/server';

export const Task = table({
  name: 'task',
  public: true,
  indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
  id: t.u64().primaryKey().autoInc(),
  ownerId: t.identity(),
  title: t.string(),
  createdAt: t.timestamp(),
});

Column types

列类型

typescript
t.identity()           // User identity
t.u64()                // Unsigned 64-bit integer (use for IDs)
t.string()             // Text
t.bool()               // Boolean
t.timestamp()          // Timestamp
t.scheduleAt()         // For scheduled tables only
t.object('Name', {})   // Product types (nested objects)
t.enum('Name', {})     // Sum types (tagged unions)
t.string().optional()  // Nullable
BigInt syntax: All
u64
/
i64
fields use
0n
,
1n
, not
0
,
1
.
typescript
t.identity()           // 用户身份标识
t.u64()                // 无符号64位整数(用于ID)
t.string()             // 文本
t.bool()               // 布尔值
t.timestamp()          // 时间戳
t.scheduleAt()         // 仅用于定时表
t.object('Name', {})   // 产品类型(嵌套对象)
t.enum('Name', {})     // Sum类型(标记联合)
t.string().optional()  // 可空字段
BigInt语法规则:所有
u64
/
i64
字段使用
0n
1n
,不要使用
0
1

Schema export

Schema导出

typescript
const spacetimedb = schema({ Task, Player });
export default spacetimedb;
typescript
const spacetimedb = schema({ Task, Player });
export default spacetimedb;

Reducer Definition (2.0)

Reducer定义(2.0版本)

Name comes from the export — NOT from a string argument.
typescript
import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';

export const create_task = spacetimedb.reducer(
  { title: t.string() },
  (ctx, { title }) => {
    if (!title) throw new SenderError('title required');
    ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
  }
);
名称来自导出声明 — 不要通过字符串参数指定。
typescript
import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';

export const create_task = spacetimedb.reducer(
  { title: t.string() },
  (ctx, { title }) => {
    if (!title) throw new SenderError('title required');
    ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
  }
);

Update Pattern

更新模式

typescript
const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });
typescript
const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });

Lifecycle Hooks

生命周期钩子

typescript
spacetimedb.clientConnected((ctx) => { /* ctx.sender is the connecting identity */ });
spacetimedb.clientDisconnected((ctx) => { /* clean up */ });

typescript
spacetimedb.clientConnected((ctx) => { /* ctx.sender是连接的用户身份 */ });
spacetimedb.clientDisconnected((ctx) => { /* 清理逻辑 */ });

Event Tables (2.0)

事件表(2.0版本)

Reducer callbacks are removed in 2.0. Use event tables +
onInsert
instead.
typescript
export const DamageEvent = table(
  { name: 'damage_event', public: true, event: true },
  { target: t.identity(), amount: t.u32() }
);

export const deal_damage = spacetimedb.reducer(
  { target: t.identity(), amount: t.u32() },
  (ctx, { target, amount }) => {
    ctx.db.damageEvent.insert({ target, amount });
  }
);
Client subscribes and uses
onInsert
:
typescript
conn.db.damageEvent.onInsert((ctx, evt) => {
  playDamageAnimation(evt.target, evt.amount);
});
Event tables must be subscribed explicitly — they are excluded from
subscribeToAllTables()
.

2.0版本已移除Reducer回调,请使用事件表+
onInsert
替代。
typescript
export const DamageEvent = table(
  { name: 'damage_event', public: true, event: true },
  { target: t.identity(), amount: t.u32() }
);

export const deal_damage = spacetimedb.reducer(
  { target: t.identity(), amount: t.u32() },
  (ctx, { target, amount }) => {
    ctx.db.damageEvent.insert({ target, amount });
  }
);
客户端订阅后使用
onInsert
监听:
typescript
conn.db.damageEvent.onInsert((ctx, evt) => {
  playDamageAnimation(evt.target, evt.amount);
});
事件表需要显式订阅 — 不会被
subscribeToAllTables()
包含。

Views

视图

ViewContext vs AnonymousViewContext

ViewContext与AnonymousViewContext

typescript
// ViewContext — has ctx.sender, result varies per user
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 (better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(Player.rowType), (ctx) => {
  return ctx.from.player.where(p => p.score.gt(1000));
});
Views can only use index lookups —
.iter()
is NOT allowed.

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(Player.rowType), (ctx) => {
  return ctx.from.player.where(p => p.score.gt(1000));
});
视图仅可使用索引查询 — 禁止使用
.iter()

Scheduled Tables

定时表

typescript
export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: () => run_cleanup  // function returning the exported reducer
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),
});

export const run_cleanup = spacetimedb.reducer(
  { arg: CleanupJob.rowType },
  (ctx, { arg }) => { /* arg.scheduledId, arg.targetId available */ }
);

// Schedule a job
import { ScheduleAt } from 'spacetimedb';
ctx.db.cleanupJob.insert({
  scheduledId: 0n,
  scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
  targetId: someId
});
typescript
export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: () => run_cleanup  // 返回导出的Reducer的函数
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),
});

export const run_cleanup = spacetimedb.reducer(
  { arg: CleanupJob.rowType },
  (ctx, { arg }) => { /* 可访问arg.scheduledId, arg.targetId */ }
);

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

ScheduleAt on Client

客户端使用ScheduleAt

typescript
// ScheduleAt is a tagged union on the client
// { tag: 'Time', value: Timestamp } or { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
  const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}

typescript
// ScheduleAt在客户端是标记联合类型
// 格式为{ tag: 'Time', value: Timestamp } 或 { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
  const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}

Timestamps

时间戳

Server-side

服务端

typescript
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;
typescript
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;

Client-side

客户端

typescript
// Timestamps are objects with BigInt, not numbers
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

typescript
// 时间戳是包含BigInt的对象,不是数字
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

Procedures (Beta)

存储过程(Beta)

typescript
export const fetch_data = spacetimedb.procedure(
  { url: t.string() }, t.string(),
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
    return response.text();
  }
);
Procedures don't have
ctx.db
— use
ctx.withTx(tx => tx.db...)
.

typescript
export const fetch_data = spacetimedb.procedure(
  { url: t.string() }, t.string(),
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
    return response.text();
  }
);
存储过程中没有
ctx.db
— 请使用
ctx.withTx(tx => tx.db...)

React Integration

React集成

tsx
import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';

function Root() {
  const connectionBuilder = useMemo(() =>
    DbConnection.builder()
      .withUri('ws://localhost:3000')
      .withDatabaseName('my_game')
      .withToken(localStorage.getItem('auth_token') || undefined)
      .onConnect((conn, identity, token) => {
        localStorage.setItem('auth_token', token);
        conn.subscriptionBuilder().subscribe(tables.player);
      }),
    []
  );

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

function PlayerList() {
  const [players, isReady] = useTable(tables.player);
  if (!isReady) return <div>Loading...</div>;
  return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

tsx
import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';

function Root() {
  const connectionBuilder = useMemo(() =>
    DbConnection.builder()
      .withUri('ws://localhost:3000')
      .withDatabaseName('my_game')
      .withToken(localStorage.getItem('auth_token') || undefined)
      .onConnect((conn, identity, token) => {
        localStorage.setItem('auth_token', token);
        conn.subscriptionBuilder().subscribe(tables.player);
      }),
    []
  );

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

function PlayerList() {
  const [players, isReady] = useTable(tables.player);
  if (!isReady) return <div>加载中...</div>;
  return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

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": "^2.0.0" } }
tsconfig.json   -> Standard config
src/schema.ts   -> 表定义,导出spacetimedb
src/index.ts    -> Reducer、生命周期逻辑,导入schema
package.json    -> { "type": "module", "dependencies": { "spacetimedb": "^2.0.0" } }
tsconfig.json   -> 标准配置

Client (
client/
)

客户端(
client/

src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx         -> Provider, connection setup
src/App.tsx          -> UI components

src/module_bindings/ -> 生成的绑定文件(通过spacetime generate生成)
src/main.tsx         -> Provider、连接配置
src/App.tsx          -> UI组件

Commands

常用命令

bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>
bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>