diff --git a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md
index 487b962f4ad..10ba131faef 100644
--- a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md
+++ b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md
@@ -345,9 +345,18 @@ ctx.procedures.makeRequest().then(
-C# modules currently cannot define procedures. Support for defining procedures in C# modules will be released shortly.
+C# modules can define procedures:
-A C# [client](#client) can call a procedure defined by a Rust or TypeScript module:
+```csharp
+[SpacetimeDB.Procedure]
+public static string MakeRequest(ProcedureContext ctx)
+{
+ // ...
+ return "result";
+}
+```
+
+A C# [client](#client) can call a procedure defined by a module:
```csharp
void Main()
@@ -384,7 +393,7 @@ Because procedures are unstable, Rust modules that define them must opt in to th
```toml
[dependencies]
-spacetimedb = { version = "1.x", features = ["unstable"] }
+spacetimedb = { version = "2.*", features = ["unstable"] }
```
Then, that module can define a procedure:
@@ -436,7 +445,7 @@ Use the other tabs (TypeScript/C#/Rust/Unreal C++/Blueprint) for client call exa
-An Unreal C++ [client](#client) can call a procedure defined by a Rust or TypeScript module:
+An Unreal C++ [client](#client) can call a procedure defined by a module:
```cpp
{
@@ -468,7 +477,7 @@ void AGameManager::OnMakeRequestComplete(const FProcedureEventContext& Context,
-An Unreal [client](#client) can call a procedure defined by a Rust or TypeScript module:
+An Unreal [client](#client) can call a procedure defined by a module:

diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
index 96ed9018dfd..954fb903712 100644
--- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
+++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
@@ -2042,7 +2042,10 @@ void PrintMessage(RemoteTables tables, Message message)
#### Warn if our name was rejected
-We can also register callbacks to run each time a reducer is invoked. We register these callbacks using the `OnReducerEvent` method of the `Reducer` namespace, which is automatically implemented for each reducer by `spacetime generate`.
+We can also register callbacks for reducer results. We register these callbacks
+using generated events on `conn.Reducers`, such as `conn.Reducers.OnSetName`
+and `conn.Reducers.OnSendMessage`, which are automatically implemented for
+each reducer by `spacetime generate`.
Each reducer callback takes one fixed argument:
@@ -2061,7 +2064,11 @@ These callbacks will be invoked in one of two cases:
Note that a status of `Failed` or `OutOfEnergy` implies that the caller identity is our own identity.
-We already handle successful `SetName` invocations using our `User.OnUpdate` callback, but if the module rejects a user's chosen name, we'd like that user's client to let them know. We define a function `Reducer_OnSetNameEvent` as a `Reducer.OnSetNameEvent` callback which checks if the reducer failed, and if it did, prints an error message including the rejected name.
+We already handle successful `SetName` invocations using our `User.OnUpdate`
+callback, but if the module rejects a user's chosen name, we'd like that user's
+client to let them know. We define a function `Reducer_OnSetNameEvent` and
+register it with `conn.Reducers.OnSetName`; the callback checks if the reducer
+failed, and if it did, prints an error message including the rejected name.
We'll test both that our identity matches the sender and that the status is `Failed`, even though the latter implies the former, for demonstration purposes.
diff --git a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00400-part-3.md b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00400-part-3.md
index fc5b6ae572c..01a99bcfc91 100644
--- a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00400-part-3.md
+++ b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00400-part-3.md
@@ -721,7 +721,7 @@ const START_PLAYER_MASS: i32 = 15;
#[spacetimedb::reducer]
pub fn enter_game(ctx: &ReducerContext, name: String) -> Result<(), String> {
log::info!("Creating player with name {}", name);
- let mut player: Player = ctx.db.player().identity().find(ctx.sender).ok_or("")?;
+ let mut player: Player = ctx.db.player().identity().find(ctx.sender()).ok_or("")?;
let player_id = player.player_id;
player.name = name;
ctx.db.player().identity().update(player);
@@ -786,11 +786,11 @@ pub fn disconnect(ctx: &ReducerContext) -> Result<(), String> {
.db
.player()
.identity()
- .find(&ctx.sender)
+ .find(&ctx.sender())
.ok_or("Player not found")?;
let player_id = player.player_id;
ctx.db.logged_out_player().insert(player);
- ctx.db.player().identity().delete(&ctx.sender);
+ ctx.db.player().identity().delete(&ctx.sender());
// Remove any circles from the arena
for circle in ctx.db.circle().player_id().filter(&player_id) {
diff --git a/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00300-part-2.md b/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00300-part-2.md
index a592e63bfda..c3113c50bf4 100644
--- a/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00300-part-2.md
+++ b/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00300-part-2.md
@@ -655,7 +655,7 @@ spacetime generate --lang unrealcpp --uproject-dir .. --unreal-module-name black
This will generate a set of files in the `blackholio/Source/blackholio/Private/ModuleBindings` and `blackholio/Source/blackholio/Public/ModuleBindings` directories which contain the code generated types and reducer functions that are defined in your module, but usable on the client.
:::note
-`--uproject-dir` is straightforward as the path to the .uproject file. `--unreal-module-name` is the name of the Unreal module which in most projects is the name of the project, in this case `blackholio`.
+`--uproject-dir` is the path to the Unreal project directory that contains the `.uproject` file. `--unreal-module-name` is the name of the Unreal module, which in most projects is the name of the project, in this case `blackholio`.
:::
:::warning
diff --git a/docs/docs/00100-intro/00300-tutorials/00500-godot-tutorial/00400-part-3.md b/docs/docs/00100-intro/00300-tutorials/00500-godot-tutorial/00400-part-3.md
index 0a4586a5216..b5e3e4451fa 100644
--- a/docs/docs/00100-intro/00300-tutorials/00500-godot-tutorial/00400-part-3.md
+++ b/docs/docs/00100-intro/00300-tutorials/00500-godot-tutorial/00400-part-3.md
@@ -721,7 +721,7 @@ const START_PLAYER_MASS: i32 = 15;
#[spacetimedb::reducer]
pub fn enter_game(ctx: &ReducerContext, name: String) -> Result<(), String> {
log::info!("Creating player with name {}", name);
- let mut player: Player = ctx.db.player().identity().find(ctx.sender).ok_or("")?;
+ let mut player: Player = ctx.db.player().identity().find(ctx.sender()).ok_or("")?;
let player_id = player.player_id;
player.name = name;
ctx.db.player().identity().update(player);
@@ -786,11 +786,11 @@ pub fn disconnect(ctx: &ReducerContext) -> Result<(), String> {
.db
.player()
.identity()
- .find(&ctx.sender)
+ .find(&ctx.sender())
.ok_or("Player not found")?;
let player_id = player.player_id;
ctx.db.logged_out_player().insert(player);
- ctx.db.player().identity().delete(&ctx.sender);
+ ctx.db.player().identity().delete(&ctx.sender());
// Remove any circles from the arena
for circle in ctx.db.circle().player_id().filter(&player_id) {
diff --git a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
index 55b95444cff..5ecd5421564 100644
--- a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
+++ b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
@@ -775,7 +775,7 @@ SPACETIMEDB_VIEW(std::optional, player_count, Public, AnonymousView
```typescript
ctx.db // Database access
ctx.sender // Identity of caller
-ctx.connectionId // ConnectionId | undefined
+ctx.connectionId // ConnectionId | null
ctx.timestamp // Timestamp
ctx.databaseIdentity // Module's identity
```
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
index 4180d28ba6d..6238cf8eab4 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
@@ -546,7 +546,7 @@ import { ScheduleAt } from 'spacetimedb';
import { schema, t, table } from 'spacetimedb/server';
// Define a schedule table for the procedure
-const fetchSchedule = table(
+const fetch_schedule = table(
{ name: 'fetch_schedule', scheduled: (): any => fetch_external_data },
{
scheduled_id: t.u64().primaryKey().autoInc(),
@@ -555,12 +555,12 @@ const fetchSchedule = table(
}
);
-const spacetimedb = schema({ fetchSchedule });
+const spacetimedb = schema({ fetch_schedule });
export default spacetimedb;
// The procedure to be scheduled
export const fetch_external_data = spacetimedb.procedure(
- { arg: fetchSchedule.rowType },
+ { arg: fetch_schedule.rowType },
t.unit(),
(ctx, { arg }) => {
const response = ctx.http.fetch(arg.url);
@@ -571,7 +571,7 @@ export const fetch_external_data = spacetimedb.procedure(
// From a reducer, schedule the procedure by inserting into the schedule table
export const queueFetch = spacetimedb.reducer({ url: t.string() }, (ctx, { url }) => {
- ctx.db.fetchSchedule.insert({
+ ctx.db.fetch_schedule.insert({
scheduled_id: 0n,
scheduled_at: ScheduleAt.interval(0n), // Run immediately
url,
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
index 06e30fb7b2a..8a0e40d64ae 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
@@ -131,7 +131,7 @@ Every reducer invocation has an associated caller identity.
```typescript
-import { schema, table, t, type Identity } from 'spacetimedb/server';
+import { schema, table, t } from 'spacetimedb/server';
const player = table(
{ name: 'player', public: true },
@@ -257,7 +257,7 @@ SPACETIMEDB_REDUCER(update_score, ReducerContext ctx, uint32_t new_score) {
The connection ID identifies the specific client connection that invoked the reducer. This is useful for tracking sessions or implementing per-connection state.
:::note
-The connection ID may be `None`/`null`/`undefined` for reducers invoked by the system (such as scheduled reducers or lifecycle reducers) or when called via the CLI without specifying a connection.
+The connection ID may be absent for reducers invoked by the system (such as scheduled reducers or lifecycle reducers) or when called via the CLI without specifying a connection. In TypeScript modules, `ctx.connectionId` is `ConnectionId | null`.
:::
### Timestamp
@@ -324,7 +324,7 @@ Scheduled reducers and procedures are private by default in SpacetimeDB 2.x, so
```typescript
import { schema, table, t } from 'spacetimedb/server';
-const scheduledTask = table(
+const scheduled_task = table(
{ name: 'scheduled_task', scheduled: (): any => send_reminder },
{
taskId: t.u64().primaryKey().autoInc(),
@@ -333,10 +333,10 @@ const scheduledTask = table(
}
);
-const spacetimedb = schema({ scheduledTask });
+const spacetimedb = schema({ scheduled_task });
export default spacetimedb;
-export const send_reminder = spacetimedb.reducer({ arg: scheduledTask.rowType }, (_ctx, { arg }) => {
+export const send_reminder = spacetimedb.reducer({ arg: scheduled_task.rowType }, (_ctx, { arg }) => {
console.log(`Reminder: ${arg.message}`);
});
```
@@ -349,20 +349,20 @@ using SpacetimeDB;
public static partial class Module
{
- [SpacetimeDB.Table(Accessor = "ScheduledTask", Scheduled = nameof(SendReminder))]
+ [SpacetimeDB.Table(Accessor = "ScheduledTask", Scheduled = nameof(SendReminder), ScheduledAt = nameof(ScheduledAt))]
public partial struct ScheduledTask
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
- public ulong taskId;
- public ScheduleAt scheduledAt;
- public string message;
+ public ulong TaskId;
+ public ScheduleAt ScheduledAt;
+ public string Message;
}
[SpacetimeDB.Reducer]
public static void SendReminder(ReducerContext _ctx, ScheduledTask task)
{
- Log.Info($"Reminder: {task.message}");
+ Log.Info($"Reminder: {task.Message}");
}
}
```
@@ -426,7 +426,7 @@ SPACETIMEDB_REDUCER(send_reminder, ReducerContext _ctx, ScheduledTask task) {
| `db` | `DbView` | Access to the module's database tables |
| `sender` | `Identity` | Identity of the caller |
| `senderAuth` | `AuthCtx` | Authorization context for the caller (includes JWT claims and internal call detection) |
-| `connectionId` | `ConnectionId \| undefined`| Connection ID of the caller, if available |
+| `connectionId` | `ConnectionId \| null` | Connection ID of the caller, if available |
| `timestamp` | `Timestamp` | Time when the reducer was invoked |
| `random` | `Random` | Random number generator (deterministic, seeded by SpacetimeDB) |
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
index b679d90ef82..a7a13cc3e5d 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
@@ -385,5 +385,5 @@ Reducers can be triggered at specific times using schedule tables. See [Schedule
:::info Scheduled Reducer Context
Scheduled reducer calls originate from SpacetimeDB itself, not from a client. Therefore:
- `ctx.sender()` will be the module's own identity
-- `ctx.connection_id()` will be `None`/`null`/`undefined`
+- The connection ID will be absent (`null` in TypeScript, `null` in C#, `None` in Rust, and `std::nullopt` in C++)
:::
diff --git a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
index 32589a22a88..f96b8e238ec 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
@@ -128,7 +128,7 @@ This means there's no `ctx.db` field to access the database.
Instead, procedure code must manage transactions explicitly with `ProcedureCtx.withTx`.
```typescript
-const myTable = table(
+const my_table = table(
{ name: "my_table" },
{
a: t.u32(),
@@ -136,12 +136,12 @@ const myTable = table(
},
)
-const spacetimedb = schema({ myTable });
+const spacetimedb = schema({ my_table });
export default spacetimedb;
export const insert_a_value = spacetimedb.procedure({ a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) => {
ctx.withTx(ctx => {
- ctx.db.myTable.insert({ a, b });
+ ctx.db.my_table.insert({ a, b });
});
return {};
})
@@ -328,7 +328,7 @@ export const maybe_insert_a_value = spacetimedb.procedure({ a: t.u32(), b: t.str
if (a < 10) {
throw new SenderError("a is less than 10!");
}
- ctx.db.myTable.insert({ a, b });
+ ctx.db.my_table.insert({ a, b });
});
})
```
@@ -1190,7 +1190,7 @@ A common use case for procedures is integrating with external APIs like OpenAI's
import { schema, t, table, SenderError } from 'spacetimedb/server';
import { TimeDuration } from 'spacetimedb';
-const aiMessage = table(
+const ai_message = table(
{ name: 'ai_message', public: true },
{
user: t.identity(),
@@ -1200,7 +1200,7 @@ const aiMessage = table(
}
);
-const spacetimedb = schema({ aiMessage });
+const spacetimedb = schema({ ai_message });
export default spacetimedb;
export const ask_ai = spacetimedb.procedure(
@@ -1235,7 +1235,7 @@ export const ask_ai = spacetimedb.procedure(
// Store the conversation in the database
ctx.withTx(txCtx => {
- txCtx.db.aiMessage.insert({
+ txCtx.db.ai_message.insert({
user: txCtx.sender,
prompt,
response: aiResponse,
diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md
index 89c2e1c2931..d86af13564c 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md
@@ -40,7 +40,7 @@ const players = table(
}
);
-const playerLevels = table(
+const player_levels = table(
{ name: 'player_levels', public: true },
{
player_id: t.u64().unique(),
@@ -48,7 +48,7 @@ const playerLevels = table(
}
);
-const spacetimedb = schema({ players, playerLevels });
+const spacetimedb = schema({ players, player_levels });
export default spacetimedb;
// At-most-one row: return Option via t.option(...)
@@ -75,7 +75,7 @@ export const players_for_level = spacetimedb.anonymousView(
t.array(playerAndLevelRow),
(ctx) => {
const out: Array<{ id: bigint; name: string; level: bigint }> = [];
- for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
+ for (const playerLevel of ctx.db.player_levels.level.filter(2n)) {
const p = ctx.db.players.id.find(playerLevel.player_id);
if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
}
@@ -167,8 +167,7 @@ Views must be static methods and can return either a single row (`T?`) or multip
Use the `#[spacetimedb::view]` macro on a function:
```rust
-use spacetimedb::{view, ViewContext, AnonymousViewContext, table, SpacetimeType};
-use spacetimedb_lib::Identity;
+use spacetimedb::{view, ViewContext, AnonymousViewContext, table, SpacetimeType, Identity};
#[spacetimedb::table(accessor = player)]
pub struct Player {
@@ -494,7 +493,7 @@ const entity = table(
);
// Track which chunks each player is subscribed to
-const playerChunk = table(
+const player_chunk = table(
{ name: 'player_chunk', public: true },
{
playerId: t.u64().primaryKey(),
@@ -522,7 +521,7 @@ export const entities_in_my_chunk = spacetimedb.view(
const player = ctx.db.players.identity.find(ctx.sender);
if (!player) return [];
- const chunk = ctx.db.playerChunk.playerId.find(player.id);
+ const chunk = ctx.db.player_chunk.playerId.find(player.id);
if (!chunk) return [];
return Array.from(ctx.db.entity.chunkX.filter(chunk.chunkX))
@@ -1068,7 +1067,7 @@ const players = table(
}
);
-const playerLevels = table(
+const player_levels = table(
{ name: 'player_levels', public: true },
{
player_id: t.u64().unique(),
@@ -1076,7 +1075,7 @@ const playerLevels = table(
}
);
-const spacetimedb = schema({ players, playerLevels });
+const spacetimedb = schema({ players, player_levels });
export default spacetimedb;
export const all_players = spacetimedb.anonymousView(
@@ -1087,8 +1086,8 @@ export const all_players = spacetimedb.anonymousView(
export const all_player_levels = spacetimedb.anonymousView(
{ name: 'all_player_levels', public: true },
- t.array(playerLevels.rowType),
- (ctx) => ctx.from.playerLevels
+ t.array(player_levels.rowType),
+ (ctx) => ctx.from.player_levels
);
```
@@ -1343,17 +1342,17 @@ export const players_with_levels = spacetimedb.anonymousView(
t.array(players.rowType),
(ctx) => {
return ctx.from.players
- .leftSemijoin(ctx.from.playerLevels, (p, pl) => p.id.eq(pl.player_id));
+ .leftSemijoin(ctx.from.player_levels, (p, pl) => p.id.eq(pl.player_id));
}
);
export const levels_for_high_scorers = spacetimedb.anonymousView(
{ name: 'levels_for_high_scorers', public: true },
- t.array(playerLevels.rowType),
+ t.array(player_levels.rowType),
(ctx) => {
return ctx.from.players
.where(p => p.score.gte(1000n))
- .rightSemijoin(ctx.from.playerLevels, (p, pl) => p.id.eq(pl.player_id))
+ .rightSemijoin(ctx.from.player_levels, (p, pl) => p.id.eq(pl.player_id))
.where(pl => pl.level.gte(10n));
}
);
diff --git a/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md b/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md
index c8bc481d338..9696462542d 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md
@@ -7,7 +7,7 @@ import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
HTTP handlers allow a SpacetimeDB database to expose an HTTP API.
-External clients can make HTTP requests to routes nested under [`/v1/database/:name_or_address/route`](../../00300-resources/00200-reference/00200-http-api/00300-database.md#any-v1databasename_or_identityroutepath); these requests are resolved to routes defined by the database and then passed to the corresponding HTTP handler.
+External clients can make HTTP requests to routes nested under [`/v1/database/:name_or_identity/route`](../../00300-resources/00200-reference/00200-http-api/00300-database.md#any-v1databasename_or_identityroutepath); these requests are resolved to routes defined by the database and then passed to the corresponding HTTP handler.
:::warning
***HTTP handlers are currently in beta, and their API may change in upcoming SpacetimeDB releases.***
@@ -225,4 +225,4 @@ SpacetimeDB uses strict routing, meaning that a request must match a path exactl
## Sending Requests
-Routes defined by a SpacetimeDB database are exposed under the prefix `/v1/database/:name/route`. To access the `say-hello` route above, send a request to `$SPACETIMEDB_URI/v1/database/$DATABASE/route/say-hello`, where `$SPACETIMEDB_URI` is the SpacetimeDB host (usually `https://maincloud.spacetimedb.com`), and `$DATABASE` is the name of the database.
+Routes defined by a SpacetimeDB database are exposed under the prefix `/v1/database/:name_or_identity/route`. To access the `say-hello` route above, send a request to `$SPACETIMEDB_URI/v1/database/$DATABASE/route/say-hello`, where `$SPACETIMEDB_URI` is the SpacetimeDB host (usually `https://maincloud.spacetimedb.com`), and `$DATABASE` is the name or identity of the database.
diff --git a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md
index 86c1589ea46..68087bcdda9 100644
--- a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md
+++ b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md
@@ -20,7 +20,7 @@ Store binary data using `Vec` (Rust), `List` (C#), `std::vector {
// Delete existing avatar if present
- ctx.db.userAvatar.userId.delete(userId);
+ ctx.db.user_avatar.userId.delete(userId);
// Insert new avatar
- ctx.db.userAvatar.insert({
+ ctx.db.user_avatar.insert({
userId,
mimeType,
data,
@@ -332,7 +332,7 @@ SPACETIMEDB_REDUCER(register_document, ReducerContext ctx,
std::string filename, std::string mime_type, uint64_t size_bytes, std::string storage_url) {
ctx.db[document].insert(Document{
.id = 0, // auto-increment
- .owner_id = ctx.sender,
+ .owner_id = ctx.sender(),
.filename = filename,
.mime_type = mime_type,
.size_bytes = size_bytes,
@@ -405,7 +405,7 @@ export const upload_to_s3 = spacetimedb.procedure(
t.string(), // Returns the S3 key
(ctx, { filename, contentType, data, s3Bucket, s3Region }) => {
// Generate a unique S3 key
- const s3Key = `uploads/${Date.now()}-${filename}`;
+ const s3Key = `uploads/${ctx.timestamp.microsSinceUnixEpoch}-${filename}`;
const url = `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${s3Key}`;
// Upload to S3 (simplified - add AWS4 signature in production)
@@ -579,7 +579,7 @@ pub fn upload_to_s3(
ctx.with_tx(|tx_ctx| {
tx_ctx.db.document().insert(Document {
id: 0,
- owner_id: tx_ctx.sender,
+ owner_id: tx_ctx.sender(),
filename: filename_clone.clone(),
s3_key: s3_key_clone.clone(),
uploaded_at: tx_ctx.timestamp,
@@ -610,7 +610,7 @@ export const get_upload_url = spacetimedb.procedure(
{ filename: t.string(), contentType: t.string() },
t.object('UploadInfo', { uploadUrl: t.string(), s3Key: t.string() }),
(ctx, { filename, contentType }) => {
- const s3Key = `uploads/${Date.now()}-${filename}`;
+ const s3Key = `uploads/${ctx.timestamp.microsSinceUnixEpoch}-${filename}`;
// Generate pre-signed URL (requires AWS credentials and signing logic)
const uploadUrl = generatePresignedUrl(s3Key, contentType);
@@ -719,7 +719,7 @@ pub fn get_upload_url(
pub fn confirm_upload(ctx: &ReducerContext, filename: String, s3_key: String) {
ctx.db.document().insert(Document {
id: 0,
- owner_id: ctx.sender,
+ owner_id: ctx.sender(),
filename,
s3_key,
uploaded_at: ctx.timestamp,
diff --git a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md
index 36005fa6c3f..666b68b5960 100644
--- a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md
+++ b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md
@@ -568,7 +568,7 @@ Use views to return a custom type that omits sensitive columns. The view reads f
import {schema, t, table} from 'spacetimedb/server';
// Private table with sensitive data
-const userAccount = table(
+const user_account = table(
{ name: 'user_account' }, // Private by default
{
id: t.u64().primaryKey().autoInc(),
@@ -581,7 +581,7 @@ const userAccount = table(
}
);
-const spacetimedb = schema({ userAccount });
+const spacetimedb = schema({ user_account });
export default spacetimedb;
// Public type without sensitive columns
@@ -597,7 +597,7 @@ export const my_profile = spacetimedb.view(
t.option(publicUserProfile),
(ctx) => {
// Look up the caller's account by their identity (unique index)
- const user = ctx.db.userAccount.identity.find(ctx.sender);
+ const user = ctx.db.user_account.identity.find(ctx.sender);
if (!user) return null;
return {
id: user.id,
diff --git a/docs/docs/00200-core-concepts/00300-tables/00550-event-tables.md b/docs/docs/00200-core-concepts/00300-tables/00550-event-tables.md
index 3f2936ca7ea..306fa92272c 100644
--- a/docs/docs/00200-core-concepts/00300-tables/00550-event-tables.md
+++ b/docs/docs/00200-core-concepts/00300-tables/00550-event-tables.md
@@ -21,7 +21,8 @@ To declare a table as an event table, add the `event` attribute to the table def
```typescript
-const damageEvent = table({
+const damage_event = table({
+ name: 'damage_event',
public: true,
event: true,
}, {
@@ -31,7 +32,7 @@ const damageEvent = table({
});
const spacetimedb = schema({
- damageEvent,
+ damage_event,
});
export default spacetimedb;
```
@@ -97,7 +98,7 @@ export const attack = spacetimedb.reducer(
// Game logic...
// Publish the event
- ctx.db.damageEvent.insert({
+ ctx.db.damage_event.insert({
entity_id: target_id,
damage,
source: "melee_attack",
@@ -174,7 +175,7 @@ This behavior follows naturally from the fact that event table rows are never me
## Subscribing to Events
-On the client side, event tables are subscribed to in the same way as regular tables. The important difference is that event table rows are never stored in the client cache. Calling `count()` on an event table always returns 0, and `iter()` always yields no rows. Instead, you observe events through `on_insert` callbacks, which fire for each row that was inserted during the transaction.
+On the client side, event tables are subscribed to with explicit queries, just like regular tables. Subscribe-all helpers such as `subscribeToAllTables`, `SubscribeToAllTables`, and `subscribe_to_all_tables` do not include event tables. Once subscribed, event table rows are never stored in the client cache. Calling `count()` on an event table always returns 0, and `iter()` always yields no rows. Instead, you observe events through `on_insert` callbacks, which fire for each row that was inserted during the transaction.
Because event table rows are ephemeral, only `on_insert` callbacks are available. There are no `on_delete`, `on_update`, or `on_before_delete` callbacks, since rows are never present in the client state to be deleted or updated.
diff --git a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
index f380a574a68..acc6e7ef221 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
@@ -57,10 +57,10 @@ Replace **PATH-TO-MODULE-DIRECTORY** with the path to your module's directory, w
```bash
mkdir -p src/module_bindings
-spacetime generate --lang rust --out-dir client/src/module_bindings --module-path PATH-TO-MODULE-DIRECTORY
+spacetime generate --lang rust --out-dir src/module_bindings --module-path PATH-TO-MODULE-DIRECTORY
```
-This generates Rust files in `client/src/module_bindings/`. Import them in your client with:
+This generates Rust files in `src/module_bindings/`. Import them in your client with:
```rust
mod module_bindings;
diff --git a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md
index f1ad4c73d42..e5378e4eddc 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md
@@ -397,11 +397,11 @@ Conn->Disconnect();
### Reconnection Behavior
-:::note[Current Limitation]
+:::note[Reconnection behavior]
-Automatic reconnection behavior is inconsistently implemented across SDKs. If your connection is interrupted, you may need to create a new `DbConnection` to re-establish connectivity.
+Lower-level `DbConnection` objects do not reconnect themselves. If you create a `DbConnection` directly and the connection is interrupted, create a new `DbConnection` to re-establish connectivity.
-We recommend implementing reconnection logic in your application if reliable connectivity is critical.
+The TypeScript React, Solid, and Svelte providers manage their connections through the SDK's shared connection manager. While a provider is mounted, that manager automatically rebuilds unexpectedly closed connections with exponential backoff and re-checks connection liveness when the page becomes visible, regains focus, returns online, or is restored from the back-forward cache.
:::
diff --git a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md
index 0db7e730c91..e5cd26b9363 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md
@@ -306,14 +306,14 @@ interface IRemoteDbContext
```
`Reducers` will have methods to invoke each reducer defined by the module,
-plus methods for adding and removing callbacks on each of those reducers.
+plus events for observing the result of reducer calls made by this connection.
##### Example
```csharp
var conn = ConnectToDB();
-// Register a callback to be run every time the SendMessage reducer is invoked
+// Register a callback to observe the result of SendMessage calls made by this connection.
conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent;
```
@@ -425,7 +425,7 @@ class SubscriptionBuilder
}
```
-Subscribe to all rows from all public tables. This method is provided as a convenience for simple clients. The subscription initiated by `SubscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
+Subscribe to all rows from all public non-event tables. Event tables are excluded and must be subscribed to with explicit queries. This method is provided as a convenience for simple clients. The subscription initiated by `SubscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
#### Type `TypedSubscriptionBuilder`
@@ -711,9 +711,9 @@ record Event
}
```
-Event when we are notified that a reducer ran in the remote database. The [`ReducerEvent`](#record-reducerevent) contains metadata about the reducer run, including its arguments and termination [`Status`](#record-status).
+Event when we are notified of the result of a reducer call made by this connection. The [`ReducerEvent`](#record-reducerevent) contains metadata about the reducer run, including its arguments and termination [`Status`](#record-status).
-This event is passed to row callbacks resulting from modifications by the reducer.
+For changes caused by other clients' reducer calls, use table row callbacks or event tables rather than reducer callbacks. The server does not broadcast reducer arguments globally.
#### Variant `SubscribeApplied`
@@ -1084,13 +1084,14 @@ int CountPlayersAtLevel(RemoteTables tables, uint level) => tables.Player.Level.
## Observe and invoke reducers
-All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have a `.Reducers` property, which in turn has methods for invoking reducers defined by the module and registering callbacks on it.
+All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have a `.Reducers` property. Generated module bindings expose one invoke method and one result event for each reducer.
-Each reducer defined by the module has three methods on the `.Reducers`:
+For a reducer named `send_message`, generated C# bindings use PascalCase names:
-- An invoke method, whose name is the reducer's name converted to snake case, like `set_name`. This requests that the module run the reducer.
-- A callback registation method, whose name is prefixed with `on_`, like `on_set_name`. This registers a callback to run whenever we are notified that the reducer ran, including successfully committed runs and runs we requested which failed. This method returns a callback id, which can be passed to the callback remove method.
-- A callback remove method, whose name is prefixed with `remove_on_`, like `remove_on_set_name`. This cancels a callback previously registered via the callback registration method.
+- An invoke method, like `SendMessage(...)`. This requests that the module run the reducer.
+- A result event, like `OnSendMessage`. This event fires on the calling connection when SpacetimeDB reports that reducer call's result, including committed, failed, and out-of-energy statuses.
+
+Reducer result events are not global notifications. They are for reducer calls made by this connection. To notify other clients that something happened, write to a public table or event table and subscribe to it.
## Identify a client
diff --git a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
index 6f1b6eeecad..657ff8397b3 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
@@ -384,7 +384,7 @@ class SubscriptionBuilder {
}
```
-Subscribe to all rows from all public tables. This method is provided as a convenience for simple clients. The subscription initiated by `subscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
+Subscribe to all rows from all public non-event tables. Event tables are excluded and must be subscribed to with explicit queries. This method is provided as a convenience for simple clients. The subscription initiated by `subscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions.
## Query Builder API
@@ -1034,7 +1034,7 @@ The SpacetimeDB TypeScript SDK includes React bindings under the `spacetimedb/re
The React integration is fully compatible with React StrictMode and correctly handles the double-mount behavior (only one WebSocket connection is created).
-While a `SpacetimeDBProvider` is mounted, the React connection manager also replaces the managed `DbConnection` if the underlying WebSocket closes or reports a connection error. Reconnect attempts use exponential backoff, starting at 1 second and doubling after each consecutive failure up to a 30 second maximum; the backoff resets after a successful connection. Hooks such as `useTable` observe the provider state, receive the fresh connection, and establish their subscriptions again; while the replacement connection is being established, `useTable` reports `isReady` as `false` until its subscription is applied on the new connection. This provider-level recovery does not change the lower-level `DbConnection` contract: applications that create a `DbConnection` directly are still responsible for creating a new connection if they need reconnection behavior.
+While a `SpacetimeDBProvider` is mounted, the shared connection manager also replaces the managed `DbConnection` if the underlying WebSocket closes or reports a connection error. Reconnect attempts use exponential backoff, starting at 1 second and doubling after each consecutive failure up to a 30 second maximum; the backoff resets after a successful connection. In browser environments, the manager also re-checks connection liveness when the page becomes visible, regains focus, returns online, or is restored from the back-forward cache, so a stalled reconnect or silently closed socket can be rebuilt promptly after a suspended tab resumes. Hooks such as `useTable` observe the provider state, receive the fresh connection, and establish their subscriptions again; while the replacement connection is being established, `useTable` reports `isReady` as `false` until its subscription is applied on the new connection. This provider-level recovery does not change the lower-level `DbConnection` contract: applications that create a `DbConnection` directly are still responsible for creating a new connection if they need reconnection behavior.
| Name | Description |
| ----------------------------------------------------------- | --------------------------------------------------------- |
diff --git a/docs/docs/00300-resources/00100-how-to/00600-migrating-to-2.0.md b/docs/docs/00300-resources/00100-how-to/00600-migrating-to-2.0.md
index 8f70388d274..9069559e07d 100644
--- a/docs/docs/00300-resources/00100-how-to/00600-migrating-to-2.0.md
+++ b/docs/docs/00300-resources/00100-how-to/00600-migrating-to-2.0.md
@@ -107,9 +107,9 @@ conn.Reducers.OnDealDamage += (ctx, _, _) =>
{
Console.WriteLine("Reducer succeeded");
}
- else if (ctx.Event.Status is Status.Failed failed)
+ else if (ctx.Event.Status is Status.Failed(var reason))
{
- Console.WriteLine($"Reducer failed: {failed}");
+ Console.WriteLine($"Reducer failed: {reason}");
}
else if (ctx.Event.Status is Status.OutOfEnergy)
{
@@ -190,15 +190,15 @@ spacetimedb.reducer('deal_damage', { target: t.identity(), amount: t.u32() }, (c
**Server (module) -- after:**
```typescript
// 2.0 server -- explicitly publish events via an event table
-const damageEvent = table({ event: true }, {
+const damage_event = table({ name: 'damage_event', event: true }, {
target: t.identity(),
amount: t.u32(),
})
-// schema() takes an object: schema({ damageEvent }), never schema(damageEvent)
-const spacetimedb = schema({ damageEvent });
+// schema() takes an object: schema({ damage_event }), never schema(damage_event)
+const spacetimedb = schema({ damage_event });
export const dealDamage = spacetimedb.reducer({ target: t.identity(), amount: t.u32() }, (ctx, { target, amount }) => {
- ctx.db.damageEvent.insert({ target, amount });
+ ctx.db.damage_event.insert({ target, amount });
});
```
@@ -1084,14 +1084,14 @@ SPACETIMEDB_REDUCER(my_reducer, ReducerContext ctx) {
In 2.0 modules, only columns with a `.primaryKey()` constraint expose an `update` method, whereas previously, `.unique()` constraints also provided that method. The previous behavior led to confusion, as only updates which preserved the value in the primary key column resulted in `onUpdate` callbacks being invoked on the client.
```typescript
-const myTable = table({ name: 'my_table' }, {
+const my_table = table({ name: 'my_table' }, {
id: t.u32().unique(),
name: t.string(),
})
// 1.0 -- REMOVED in 2.0
spacetimedb.reducer('my_reducer', ctx => {
- ctx.db.myTable.id.update({
+ ctx.db.my_table.id.update({
id: 1,
name: "Foobar",
});
@@ -1100,8 +1100,8 @@ spacetimedb.reducer('my_reducer', ctx => {
// 2.0 -- Perform a delete followed by an insert
// OR change the `.unique()` constraint into `.primaryKey()` constraint
spacetimedb.reducer(ctx => {
- ctx.db.myTable.id.delete(1);
- ctx.db.myTable.insert({
+ ctx.db.my_table.id.delete(1);
+ ctx.db.my_table.insert({
id: 1,
name: "Foobar"
});
@@ -1366,14 +1366,14 @@ spacetimedb.reducer('runMyTimer', myTimer.rowType, (ctx, timer) => {
```
```typescript
-const myTimer = table({ scheduled: () => runMyTimer }, {
+const my_timer = table({ name: 'my_timer', scheduled: (): any => runMyTimer }, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
});
-const spacetimedb = schema({ myTimer }); // schema({ table }), never schema(table)
+const spacetimedb = schema({ my_timer }); // schema({ table }), never schema(table)
// 2.0 -- Can only be called by the database
-export const runMyTimer = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
+export const runMyTimer = spacetimedb.reducer({ arg: my_timer.rowType }, (ctx, { arg }) => {
// Do stuff
})
```
@@ -1479,17 +1479,17 @@ In the rare event that you have a reducer or procedure which is intended to be i
```typescript
-const myTimer = table({ scheduled: () => runMyTimerPrivate }, {
+const my_timer = table({ name: 'my_timer', scheduled: (): any => runMyTimerPrivate }, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
});
-const spacetimedb = schema({ myTimer }); // schema({ table }), never schema(table)
+const spacetimedb = schema({ my_timer }); // schema({ table }), never schema(table)
-export const runMyTimerPrivate = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
+export const runMyTimerPrivate = spacetimedb.reducer({ arg: my_timer.rowType }, (ctx, { arg }) => {
// Do stuff...
});
-export const runMyTimer = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
+export const runMyTimer = spacetimedb.reducer({ arg: my_timer.rowType }, (ctx, { arg }) => {
// Same logic as runMyTimerPrivate — extract to a helper if needed
});
```
diff --git a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00300-spacetime-json.md b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00300-spacetime-json.md
index f14340c3337..0e90d315c80 100644
--- a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00300-spacetime-json.md
+++ b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00300-spacetime-json.md
@@ -211,7 +211,7 @@ These apply to all selected databases:
- `--server`: target server
- `--break-clients`: allow breaking changes
-- `--delete-data`: clear database data
+- `--delete-data=`: clear database data (`always`, `on-conflict`, or `never`)
- `--yes` / `--force`: skip confirmation prompts
### Per-database overrides
diff --git a/skills/cli/SKILL.md b/skills/cli/SKILL.md
index 534ad7ee4ec..f5cd3479128 100644
--- a/skills/cli/SKILL.md
+++ b/skills/cli/SKILL.md
@@ -63,6 +63,10 @@ spacetime publish my-database --server local --yes
spacetime publish my-database --delete-data=always --yes
```
+Bare `--delete-data` defaults to `always`. Keep simple interactive docs examples
+bare, and use `--delete-data=always` for scripted/non-interactive examples that
+also pass `--yes`.
+
### Database Interaction
```bash
diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md
index 2e9326b3150..a870f680cbb 100644
--- a/skills/typescript-server/SKILL.md
+++ b/skills/typescript-server/SKILL.md
@@ -52,7 +52,8 @@ import { ScheduleAt } from 'spacetimedb'; // for scheduled tables only
## Tables
-`table(OPTIONS, COLUMNS)` takes two arguments. The `name` field MUST be snake_case:
+`table(OPTIONS, COLUMNS)` takes two arguments. The `name` field is optional;
+when present, it overrides the canonical SQL name and should be snake_case:
```typescript
const entity = table(
@@ -65,9 +66,13 @@ const entity = table(
);
```
-Options: `name` (snake_case, recommended), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`
+Options: `name` (optional canonical SQL name override), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`
-`ctx.db` accessors are the keys passed to `schema({...})`, verbatim: `schema({ score_record })` → `ctx.db.score_record`. Use snake_case keys matching the table `name`. Client codegen converts case; server `ctx.db` does not.
+`ctx.db` accessors are the keys passed to `schema({ ... })`, verbatim:
+`schema({ score_record })` -> `ctx.db.score_record`. The optional `name`
+field overrides the canonical SQL name; it does not change the server
+`ctx.db` accessor. Use snake_case keys matching the table `name`. Client
+codegen converts case; server `ctx.db` does not.
## Column Types