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/00200-quickstarts/00100-react.md b/docs/docs/00100-intro/00200-quickstarts/00100-react.md
index fb16ce69ff8..6ae206e872f 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00100-react.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00100-react.md
@@ -47,7 +47,7 @@ spacetime dev --template react-ts
Your project contains both server and client code.
- Edit `spacetimedb/src/index.ts` to add tables and reducers. Edit `client/src/App.tsx` to build your UI.
+ Edit `spacetimedb/src/index.ts` to add tables and reducers. Edit `src/App.tsx` to build your UI.
```
@@ -55,10 +55,11 @@ my-spacetime-app/
├── spacetimedb/ # Your SpacetimeDB module
│ └── src/
│ └── index.ts # Server-side logic
-├── client/ # React frontend
-│ └── src/
-│ ├── App.tsx
-│ └── module_bindings/ # Auto-generated types
+├── src/ # React frontend
+│ ├── App.tsx
+│ └── module_bindings/ # Auto-generated types
+├── spacetime.json # Shared SpacetimeDB dev config
+├── spacetime.local.json # Local database name
└── package.json
```
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..3b6321b140f 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:
@@ -2054,14 +2057,15 @@ The `ReducerEventContext` of the callback, which contains an `Event` that contai
It also takes a variable amount of additional arguments that match the reducer's arguments.
-These callbacks will be invoked in one of two cases:
+These callbacks are invoked for reducer calls made by this connection, whether the reducer commits successfully or fails.
-1. If the reducer was successful and altered any of our subscribed rows.
-2. If we requested an invocation which failed.
+Note that the caller identity is our own identity for these callbacks.
-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/00200-part-1.md b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00200-part-1.md
index c0dfa43d476..ff383b8acd9 100644
--- a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00200-part-1.md
+++ b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00200-part-1.md
@@ -74,6 +74,8 @@ The `SpacetimeDBNetworkManager` is a simple script which hooks into the Unity `U
When you build a new connection to SpacetimeDB, that connection will be added to and managed by the `SpacetimeDBNetworkManager` automatically.
+The SDK also resets its own internal static Unity state when entering Play Mode, including projects that disable Domain Reloading in Unity's Enter Play Mode Options. Your own static fields and singleton references are still your responsibility.
+
Click on the `GameManager` object in the scene and click **Add Component**. Search for and select the `SpacetimeDBNetworkManager` to add it to your `GameManager` object.
Our Unity project is all set up! If you press play, it will show a blank screen, but it should start the game without any errors. Now we're ready to get started on our SpacetimeDB server module, so we have something to connect to!
diff --git a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00300-part-2.md b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00300-part-2.md
index 033bd7ed2ea..3d9f4347e5e 100644
--- a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00300-part-2.md
+++ b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00300-part-2.md
@@ -701,7 +701,7 @@ Replace the implementation of the `GameManager` class with the following.
public class GameManager : MonoBehaviour
{
const string SERVER_URL = "http://127.0.0.1:3000";
- const string MODULE_NAME = "blackholio";
+ const string DATABASE_NAME = "blackholio";
public static event Action OnConnected;
public static event Action OnSubscriptionApplied;
@@ -719,13 +719,13 @@ public class GameManager : MonoBehaviour
Application.targetFrameRate = 60;
// In order to build a connection to SpacetimeDB we need to register
- // our callbacks and specify a SpacetimeDB server URI and module name.
+ // our callbacks and specify a SpacetimeDB server URI and database name.
var builder = DbConnection.Builder()
.OnConnect(HandleConnect)
.OnConnectError(HandleConnectError)
.OnDisconnect(HandleDisconnect)
.WithUri(SERVER_URL)
- .WithDatabaseName(MODULE_NAME);
+ .WithDatabaseName(DATABASE_NAME);
// If the user has a SpacetimeDB auth token stored in the Unity PlayerPrefs,
// we can use it to authenticate the connection.
@@ -803,7 +803,7 @@ public class GameManager : MonoBehaviour
> Unity WebGL needs one extra precaution here. Browser WebSocket APIs cannot set an `Authorization` header, so reconnecting with a saved server-issued token may yield a short-lived WebSocket token in `HandleConnect`. The `#if UNITY_WEBGL` guard keeps the original saved token instead of overwriting it during reconnect.
-Here we configure the connection to the database, by passing it some callbacks in addition to providing the `SERVER_URI` and `MODULE_NAME` to the connection. When the client connects, the SpacetimeDB SDK will call the `HandleConnect` method, allowing us to start up the game.
+Here we configure the connection to the database, by passing it some callbacks in addition to providing the `SERVER_URL` and `DATABASE_NAME` to the connection. When the client connects, the SpacetimeDB SDK will call the `HandleConnect` method, allowing us to start up the game.
In our `HandleConnect` callback we build a subscription and call `Subscribe`, subscribing to all data in the database. This causes SpacetimeDB to synchronize the state of all your tables with your Unity client's SDK client cache.
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..612042afcb3 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) {
@@ -1554,7 +1554,7 @@ At this point, after publishing our module we can press the play button to see t
- If you get an error when running the generate command, make sure you have an empty subfolder in your Unity project Assets folder called `module_bindings`
-- If you get an error in your Unity console when starting the game, double check that you have published your module and you have the correct module name specified in your `GameManager`.
+- If you get an error in your Unity console when starting the game, double check that you have published your module and you have the correct database name specified in your `GameManager`.
### Next Steps
diff --git a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00500-part-4.md b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00500-part-4.md
index bfc58fc0ded..40aee6fd84c 100644
--- a/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00500-part-4.md
+++ b/docs/docs/00100-intro/00300-tutorials/00300-unity-tutorial/00500-part-4.md
@@ -864,13 +864,13 @@ Notice that the food automatically respawns as you vaccuum them up. This is beca
- Publish to Maincloud `spacetime publish --server maincloud --delete-data`
- `` This name should be unique and cannot contain any special characters other than internal hyphens (`-`). You will have to update the database name in `blackholio-server/spacetime.local.json` to match.
- Update the URL in the Unity project to: `https://maincloud.spacetimedb.com`
-- Update the module name in the Unity project to ``.
+- Update the database name in the Unity project to ``.
- Clear the PlayerPrefs in Start() within `GameManager.cs`
- Your `GameManager.cs` should look something like this:
```csharp
const string SERVER_URL = "https://maincloud.spacetimedb.com";
-const string MODULE_NAME = "";
+const string DATABASE_NAME = "";
...
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..2c16629631c 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
@@ -873,7 +873,7 @@ void AGameManager::HandleSubscriptionApplied(FSubscriptionEventContext& Context)
}
```
-Here we configure the connection to the database, by passing it some callbacks in addition to providing the `SERVER_URI` and `MODULE_NAME` to the connection. When the client connects, the SpacetimeDB SDK will call the `HandleConnect` method, allowing us to start up the game.
+Here we configure the connection to the database, by passing it some callbacks in addition to providing the `ServerUri` and `DatabaseName` to the connection. When the client connects, the SpacetimeDB SDK will call the `HandleConnect` method, allowing us to start up the game.
In our `HandleConnect` callback we build a subscription and are calling `Subscribe` and subscribing to all data in the database. This will cause SpacetimeDB to synchronize the state of all your tables with your Unreal client's SpacetimeDB SDK's "client cache". You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](../../../00300-resources/00200-reference/00400-sql-reference.md) enumerates the operations that are accepted in our SQL syntax.
@@ -955,7 +955,7 @@ Update the **Event EndPlay** and **Event Tick** to the following:
Update the **OnConnect_Event**:

-Here we configure the connection to the database, by passing it some callbacks in addition to providing the `SERVER_URI` and `MODULE_NAME` to the connection. When the client connects, the SpacetimeDB SDK will call the `OnConnect_Event` method, allowing us to start up the game.
+Here we configure the connection to the database, by passing it some callbacks in addition to providing the `ServerUri` and `DatabaseName` to the connection. When the client connects, the SpacetimeDB SDK will call the `OnConnect_Event` method, allowing us to start up the game.
In our `OnConnect_Event` callback we build a subscription and are calling `Subscribe` and subscribing to all data in the database. This will cause SpacetimeDB to synchronize the state of all your tables with your Unreal client's SpacetimeDB SDK's "client cache". You can also subscribe to specific tables using SQL syntax, e.g. `SELECT * FROM my_table`. Our [SQL documentation](../../../00300-resources/00200-reference/00400-sql-reference.md) enumerates the operations that are accepted in our SQL syntax.
diff --git a/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00500-part-4.md b/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00500-part-4.md
index 4cb494183c2..11233e3bcbc 100644
--- a/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00500-part-4.md
+++ b/docs/docs/00100-intro/00300-tutorials/00400-unreal-tutorial/00500-part-4.md
@@ -920,7 +920,7 @@ Notice that the food automatically respawns as you vaccuum them up. This is beca
- Publish to Maincloud `spacetime publish --server maincloud --delete-data`
- `` This name should be unique and cannot contain any special characters other than internal hyphens (`-`).
- Update the URL in the Unreal project to: `https://maincloud.spacetimedb.com`
-- Update the module name in the Unreal project to ``.
+- Update the database name in the Unreal project to ``.
- Your `BP_GameManager` should look something like this:

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..46291fe7202 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) {
@@ -1639,7 +1639,7 @@ After publishing our module we can press the play button to see the fruits of ou
- If you get an error when running the generate command, make sure you have an empty subfolder in your Godot project Assets folder called `module_bindings`
-- If you get an error in your Godot console when starting the game, double check that you have published your module and you have the correct module name specified in your `GameManager`.
+- If you get an error in your Godot console when starting the game, double check that you have published your module and you have the correct database name specified in your `GameManager`.
### Next Steps
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/00100-databases/00600-submodules.md b/docs/docs/00200-core-concepts/00100-databases/00600-submodules.md
index 242888f333d..c7b39909143 100644
--- a/docs/docs/00200-core-concepts/00100-databases/00600-submodules.md
+++ b/docs/docs/00200-core-concepts/00100-databases/00600-submodules.md
@@ -53,7 +53,7 @@ export const sessionCount = spacetimedb.procedure(
);
export const activeSessions = spacetimedb.anonymousView(
- { name: 'activeSessions', public: true },
+ { name: 'active_sessions', public: true },
t.array(sessions.rowType),
(ctx) => [...ctx.db.sessions.iter()]
);
@@ -114,7 +114,7 @@ Submodule tables appear under a namespace field on `ctx.db`. The field matches t
```typescript
-spacetimedb.reducer('example', {}, (ctx) => {
+export const example = spacetimedb.reducer((ctx) => {
// Consumer's own tables (no namespace)
for (const player of ctx.db.players.iter()) { /* ... */ }
@@ -145,7 +145,7 @@ export function sessionCountHelper(ctx: ReducerContext): num
}
// my-database: call submodule reducer and helper from a consumer reducer
-spacetimedb.reducer('onLogin', { token: t.string() }, (ctx, { token }) => {
+export const onLogin = spacetimedb.reducer({ token: t.string() }, (ctx, { token }) => {
// call a submodule reducer
authLib.verifyToken(ctx.as.myauth, { token });
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..9f8f0fec51c 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
@@ -22,6 +22,18 @@ Use the `spacetimedb.reducer` function:
```typescript
import { schema, table, t } from 'spacetimedb/server';
+const user = table(
+ { name: 'user', public: true },
+ {
+ id: t.u64().primaryKey().autoInc(),
+ name: t.string().index('btree'),
+ email: t.string().unique(),
+ }
+);
+
+const spacetimedb = schema({ user });
+export default spacetimedb;
+
export const create_user = spacetimedb.reducer({ name: t.string(), email: t.string() }, (ctx, { name, email }) => {
// Validate input
if (name === '') {
@@ -69,7 +81,7 @@ public static partial class Module
}
```
-Reducers must be static methods with `ReducerContext` as the first parameter. Additional parameters must be types marked with `[SpacetimeDB.Type]`. Reducers should return `void`.
+Reducers must be static methods with `ReducerContext` as the first parameter. Additional parameters may be built-in SpacetimeDB types, primitive types, or custom types marked with `[SpacetimeDB.Type]`. Reducers should return `void`.
@@ -470,7 +482,7 @@ console.log(`Total users: ${total}`);
```csharp
-var total = ctx.Db.User.Count();
+var total = ctx.Db.User.Count;
Log.Info($"Total users: {total}");
```
@@ -547,10 +559,10 @@ import { schema, t, table } from 'spacetimedb/server';
// Define a schedule table for the procedure
const fetchSchedule = table(
- { name: 'fetch_schedule', scheduled: (): any => fetch_external_data },
+ { name: 'fetch_schedule', scheduled: (): any => fetchExternalData },
{
- scheduled_id: t.u64().primaryKey().autoInc(),
- scheduled_at: t.scheduleAt(),
+ scheduledId: t.u64().primaryKey().autoInc(),
+ scheduledAt: t.scheduleAt(),
url: t.string(),
}
);
@@ -559,7 +571,7 @@ const spacetimedb = schema({ fetchSchedule });
export default spacetimedb;
// The procedure to be scheduled
-export const fetch_external_data = spacetimedb.procedure(
+export const fetchExternalData = spacetimedb.procedure(
{ arg: fetchSchedule.rowType },
t.unit(),
(ctx, { arg }) => {
@@ -572,8 +584,8 @@ 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({
- scheduled_id: 0n,
- scheduled_at: ScheduleAt.interval(0n), // Run immediately
+ scheduledId: 0n,
+ scheduledAt: 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..3274f75fc57 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
@@ -325,7 +325,7 @@ Scheduled reducers and procedures are private by default in SpacetimeDB 2.x, so
import { schema, table, t } from 'spacetimedb/server';
const scheduledTask = table(
- { name: 'scheduled_task', scheduled: (): any => send_reminder },
+ { name: 'scheduled_task', scheduled: (): any => sendReminder },
{
taskId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
@@ -336,7 +336,7 @@ const scheduledTask = table(
const spacetimedb = schema({ scheduledTask });
export default spacetimedb;
-export const send_reminder = spacetimedb.reducer({ arg: scheduledTask.rowType }, (_ctx, { arg }) => {
+export const sendReminder = spacetimedb.reducer({ arg: scheduledTask.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..615db33c756 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
@@ -139,7 +139,7 @@ const myTable = table(
const spacetimedb = schema({ myTable });
export default spacetimedb;
-export const insert_a_value = spacetimedb.procedure({ a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) => {
+export const insertAValue = spacetimedb.procedure({ a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) => {
ctx.withTx(ctx => {
ctx.db.myTable.insert({ a, b });
});
@@ -323,7 +323,7 @@ Avoid capturing mutable state within functions passed to `with_tx`.
For fallible database operations, you can throw an error inside the transaction function:
```typescript
-export const maybe_insert_a_value = spacetimedb.procedure({ a: t.u32(), b: t.string() }, t.unit(), (ctx, { a, b }) => {
+export const maybeInsertAValue = spacetimedb.procedure({ a: t.u32(), b: t.string() }, t.unit(), (ctx, { a, b }) => {
ctx.withTx(ctx => {
if (a < 10) {
throw new SenderError("a is less than 10!");
@@ -1203,7 +1203,7 @@ const aiMessage = table(
const spacetimedb = schema({ aiMessage });
export default spacetimedb;
-export const ask_ai = spacetimedb.procedure(
+export const askAi = spacetimedb.procedure(
{ prompt: t.string(), apiKey: t.string() },
t.string(),
(ctx, { prompt, apiKey }) => {
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..7e97e0078e3 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md
@@ -43,7 +43,7 @@ const players = table(
const playerLevels = table(
{ name: 'player_levels', public: true },
{
- player_id: t.u64().unique(),
+ playerId: t.u64().unique(),
level: t.u64().index('btree'),
}
);
@@ -53,7 +53,7 @@ export default spacetimedb;
// At-most-one row: return Option via t.option(...)
// Your function may return the row or null
-export const my_player = spacetimedb.view(
+export const myPlayer = spacetimedb.view(
{ name: 'my_player', public: true },
t.option(players.rowType),
(ctx) => {
@@ -70,13 +70,13 @@ const playerAndLevelRow = t.row('PlayerAndLevel', {
});
// Multiple rows: return an array of rows via t.array(...)
-export const players_for_level = spacetimedb.anonymousView(
+export const playersForLevel = spacetimedb.anonymousView(
{ name: 'players_for_level', public: true },
t.array(playerAndLevelRow),
(ctx) => {
const out: Array<{ id: bigint; name: string; level: bigint }> = [];
for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
- const p = ctx.db.players.id.find(playerLevel.player_id);
+ const p = ctx.db.players.id.find(playerLevel.playerId);
if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
}
return out;
@@ -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 {
@@ -328,7 +327,7 @@ This view returns the caller's own player data. Each connected client sees diffe
```typescript
// Per-user: each client sees their own player
-export const my_player = spacetimedb.view(
+export const myPlayer = spacetimedb.view(
{ name: 'my_player', public: true },
t.option(players.rowType),
(ctx) => {
@@ -394,7 +393,7 @@ const spacetimedb = schema({ players });
export default spacetimedb;
// Shared: same high scorers for all clients
-export const high_scorers = spacetimedb.anonymousView(
+export const highScorers = spacetimedb.anonymousView(
{ name: 'high_scorers', public: true },
t.array(players.rowType),
(ctx) => {
@@ -504,7 +503,7 @@ const playerChunk = table(
);
// Shared: all players in chunk (0,0) share this view
-export const entities_in_origin_chunk = spacetimedb.anonymousView(
+export const entitiesInOriginChunk = spacetimedb.anonymousView(
{ name: 'entities_in_origin_chunk', public: true },
t.array(entity.rowType),
(ctx) => {
@@ -515,7 +514,7 @@ export const entities_in_origin_chunk = spacetimedb.anonymousView(
);
// Per-user: returns entities in the chunk the player is currently in
-export const entities_in_my_chunk = spacetimedb.view(
+export const entitiesInMyChunk = spacetimedb.view(
{ name: 'entities_in_my_chunk', public: true },
t.array(entity.rowType),
(ctx) => {
@@ -736,7 +735,7 @@ const playerCountRow = t.row('PlayerCountRow', {
count: t.u64(),
});
-export const player_count = spacetimedb.anonymousView(
+export const playerCount = spacetimedb.anonymousView(
{ name: 'player_count', public: true },
t.array(playerCountRow),
(ctx) => [{ count: ctx.db.players.count() }]
@@ -858,7 +857,7 @@ since it pushes work into the query engine, which can optimize and evaluate the
```typescript
-export const high_scorers = spacetimedb.anonymousView(
+export const highScorers = spacetimedb.anonymousView(
{ name: 'high_scorers', public: true },
t.array(players.rowType),
(ctx) => {
@@ -936,24 +935,24 @@ including reordering the join and starting from `moderators` if it thinks that w
```typescript
// Procedural: row-by-row join in module code.
-export const high_scoring_moderators_procedural = spacetimedb.anonymousView(
+export const highScoringModeratorsProcedural = spacetimedb.anonymousView(
{ name: 'high_scoring_moderators_procedural', public: true },
t.array(players.rowType),
(ctx) => {
return Array.from(ctx.db.players.score.filter({ gte: 100n }))
- .filter(p => ctx.db.moderators.player_id.find(p.id) != null);
+ .filter(p => ctx.db.moderators.playerId.find(p.id) != null);
}
);
// Query builder: equivalent logic pushed to the query engine.
// The engine can reorder this join if it decides the smaller side is a better starting point.
-export const high_scoring_moderators_declarative = spacetimedb.anonymousView(
+export const highScoringModeratorsDeclarative = spacetimedb.anonymousView(
{ name: 'high_scoring_moderators_declarative', public: true },
t.array(players.rowType),
(ctx) => {
return ctx.from.players
.where(p => p.score.gte(100n))
- .leftSemijoin(ctx.from.moderators, (p, m) => p.id.eq(m.player_id));
+ .leftSemijoin(ctx.from.moderators, (p, m) => p.id.eq(m.playerId));
}
);
```
@@ -1071,7 +1070,7 @@ const players = table(
const playerLevels = table(
{ name: 'player_levels', public: true },
{
- player_id: t.u64().unique(),
+ playerId: t.u64().unique(),
level: t.u64().index('btree'),
}
);
@@ -1079,13 +1078,13 @@ const playerLevels = table(
const spacetimedb = schema({ players, playerLevels });
export default spacetimedb;
-export const all_players = spacetimedb.anonymousView(
+export const allPlayers = spacetimedb.anonymousView(
{ name: 'all_players', public: true },
t.array(players.rowType),
(ctx) => ctx.from.players
);
-export const all_player_levels = spacetimedb.anonymousView(
+export const allPlayerLevels = spacetimedb.anonymousView(
{ name: 'all_player_levels', public: true },
t.array(playerLevels.rowType),
(ctx) => ctx.from.playerLevels
@@ -1222,7 +1221,7 @@ Use `where` to apply predicates. Chaining multiple filters combines them with lo
```typescript
-export const high_scorers = spacetimedb.anonymousView(
+export const highScorers = spacetimedb.anonymousView(
{ name: 'high_scorers', public: true },
t.array(players.rowType),
(ctx) => {
@@ -1338,22 +1337,22 @@ Semijoins keep rows from one side when a matching row exists on the other side.
```typescript
-export const players_with_levels = spacetimedb.anonymousView(
+export const playersWithLevels = spacetimedb.anonymousView(
{ name: 'players_with_levels', public: true },
t.array(players.rowType),
(ctx) => {
return ctx.from.players
- .leftSemijoin(ctx.from.playerLevels, (p, pl) => p.id.eq(pl.player_id));
+ .leftSemijoin(ctx.from.playerLevels, (p, pl) => p.id.eq(pl.playerId));
}
);
-export const levels_for_high_scorers = spacetimedb.anonymousView(
+export const levelsForHighScorers = spacetimedb.anonymousView(
{ name: 'levels_for_high_scorers', public: true },
t.array(playerLevels.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.playerLevels, (p, pl) => p.id.eq(pl.playerId))
.where(pl => pl.level.gte(10n));
}
);
@@ -1459,7 +1458,7 @@ const players = table(
const spacetimedb = schema({ players });
export default spacetimedb;
-export const my_players = spacetimedb.view(
+export const myPlayers = spacetimedb.view(
{ name: 'my_players', public: true },
t.array(players.rowType),
(ctx) => Array.from(ctx.db.players.owner.filter(ctx.sender))
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..0e29291d1fe 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
@@ -33,7 +33,7 @@ const userAvatar = table(
const spacetimedb = schema({ userAvatar });
export default spacetimedb;
-export const upload_avatar = spacetimedb.reducer({
+export const uploadAvatar = spacetimedb.reducer({
userId: t.u64(),
mimeType: t.string(),
data: t.array(t.u8()),
@@ -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/00250-default-values.md b/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md
index 0a45eed057c..c7591ab17c8 100644
--- a/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md
+++ b/docs/docs/00200-core-concepts/00300-tables/00250-default-values.md
@@ -77,6 +77,8 @@ pub struct Player {
score: u32,
#[default(true)]
is_active: bool,
+ #[default("")]
+ bio: String,
}
```
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..50bcc4abc22 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
@@ -592,7 +592,7 @@ const publicUserProfile = t.row('PublicUserProfile', {
});
// Public view that returns the caller's profile without sensitive data
-export const my_profile = spacetimedb.view(
+export const myProfile = spacetimedb.view(
{ name: 'my_profile', public: true },
t.option(publicUserProfile),
(ctx) => {
diff --git a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md
index 60d0b677c8d..24c2a99284b 100644
--- a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md
+++ b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md
@@ -25,17 +25,17 @@ The table attribute uses `scheduled` (with a "d") because it refers to the **sch
```typescript
const reminder = table(
- { name: 'reminder', scheduled: (): any => send_reminder },
+ { name: 'reminder', scheduled: (): any => sendReminder },
{
- scheduled_id: t.u64().primaryKey().autoInc(),
- scheduled_at: t.scheduleAt(),
+ scheduledId: t.u64().primaryKey().autoInc(),
+ scheduledAt: t.scheduleAt(),
message: t.string(),
}
);
-export const send_reminder = spacetimedb.reducer({ arg: reminder.rowType }, (_ctx, { arg }) => {
+export const sendReminder = spacetimedb.reducer({ arg: reminder.rowType }, (_ctx, { arg }) => {
// Invoked automatically by the scheduler
- // arg.message, arg.scheduled_at, arg.scheduled_id
+ // arg.message, arg.scheduledAt, arg.scheduledId
});
```
@@ -56,7 +56,7 @@ public static partial class Module
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
- public ulong Id;
+ public ulong ScheduledId;
public uint UserId;
public string Message;
public ScheduleAt ScheduledAt;
@@ -81,7 +81,7 @@ use std::time::Duration;
pub struct Reminder {
#[primary_key]
#[auto_inc]
- id: u64,
+ scheduled_id: u64,
user_id: u32,
message: String,
scheduled_at: ScheduleAt,
@@ -96,7 +96,7 @@ fn send_reminder(ctx: &ReducerContext, reminder: Reminder) -> Result<(), String>
#[reducer(init)]
fn init(ctx: &ReducerContext) {
ctx.db.reminder_schedule().insert(Reminder {
- id: 0,
+ scheduled_id: 0,
user_id: 0,
message: "Game tick".to_string(),
scheduled_at: ScheduleAt::Interval(Duration::from_millis(50).into()),
@@ -157,18 +157,18 @@ import { schema } from 'spacetimedb/server';
const spacetimedb = schema({ reminder }); // reminder table defined above
export default spacetimedb;
-export const schedule_periodic_tasks = spacetimedb.reducer((ctx) => {
+export const schedulePeriodicTasks = spacetimedb.reducer((ctx) => {
// Schedule to run every 5 seconds (5,000,000 microseconds)
ctx.db.reminder.insert({
- scheduled_id: 0n,
- scheduled_at: ScheduleAt.interval(5_000_000n),
+ scheduledId: 0n,
+ scheduledAt: ScheduleAt.interval(5_000_000n),
message: "Check for updates",
});
// Schedule to run every 100 milliseconds
ctx.db.reminder.insert({
- scheduled_id: 0n,
- scheduled_at: ScheduleAt.interval(100_000n), // 100ms in microseconds
+ scheduledId: 0n,
+ scheduledAt: ScheduleAt.interval(100_000n), // 100ms in microseconds
message: "Game tick",
});
});
@@ -186,6 +186,7 @@ public static partial class Module
// Schedule to run every 5 seconds
ctx.Db.Reminder.Insert(new Reminder
{
+ ScheduledId = 0,
Message = "Check for updates",
ScheduledAt = new ScheduleAt.Interval(TimeSpan.FromSeconds(5))
});
@@ -193,6 +194,7 @@ public static partial class Module
// Schedule to run every 100 milliseconds
ctx.Db.Reminder.Insert(new Reminder
{
+ ScheduledId = 0,
Message = "Game tick",
ScheduledAt = new ScheduleAt.Interval(TimeSpan.FromMilliseconds(100))
});
@@ -211,14 +213,14 @@ use std::time::Duration;
fn schedule_periodic_tasks(ctx: &ReducerContext) {
// Schedule to run every 5 seconds
ctx.db.reminder().insert(Reminder {
- id: 0,
+ scheduled_id: 0,
message: "Check for updates".to_string(),
scheduled_at: ScheduleAt::Interval(Duration::from_secs(5).into()),
});
// Schedule to run every 100 milliseconds
ctx.db.reminder().insert(Reminder {
- id: 0,
+ scheduled_id: 0,
message: "Game tick".to_string(),
scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()),
});
@@ -232,14 +234,14 @@ fn schedule_periodic_tasks(ctx: &ReducerContext) {
// Schedule to run every 5 seconds
ctx.db[reminder].insert(Reminder{
0,
- ScheduleAt::interval(TimeDuration::from_seconds(5)),
+ ScheduleAt(TimeDuration::from_seconds(5)),
"Check for updates"
});
// Schedule to run every 100 milliseconds
ctx.db[reminder].insert(Reminder{
0,
- ScheduleAt::interval(TimeDuration::from_millis(100)),
+ ScheduleAt(TimeDuration::from_millis(100)),
"Game tick"
});
```
@@ -260,20 +262,20 @@ import { schema } from 'spacetimedb/server';
const spacetimedb = schema({ reminder }); // reminder table defined above
export default spacetimedb;
-export const schedule_timed_tasks = spacetimedb.reducer((ctx) => {
+export const scheduleTimedTasks = spacetimedb.reducer((ctx) => {
// Schedule for 10 seconds from now
const tenSecondsFromNow = ctx.timestamp.microsSinceUnixEpoch + 10_000_000n;
ctx.db.reminder.insert({
- scheduled_id: 0n,
- scheduled_at: ScheduleAt.time(tenSecondsFromNow),
+ scheduledId: 0n,
+ scheduledAt: ScheduleAt.time(tenSecondsFromNow),
message: "Your auction has ended",
});
// Schedule for a specific Unix timestamp (microseconds since epoch)
const targetTime = 1735689600_000_000n; // Jan 1, 2025 00:00:00 UTC
ctx.db.reminder.insert({
- scheduled_id: 0n,
- scheduled_at: ScheduleAt.time(targetTime),
+ scheduledId: 0n,
+ scheduledAt: ScheduleAt.time(targetTime),
message: "Happy New Year!",
});
});
@@ -294,6 +296,7 @@ public static partial class Module
var tenSecondsFromNow = ctx.Timestamp + new TimeDuration(10_000_000);
ctx.Db.Reminder.Insert(new Reminder
{
+ ScheduledId = 0,
Message = "Your auction has ended",
ScheduledAt = new ScheduleAt.Time(tenSecondsFromNow)
});
@@ -302,6 +305,7 @@ public static partial class Module
var targetTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
ctx.Db.Reminder.Insert(new Reminder
{
+ ScheduledId = 0,
Message = "Happy New Year!",
ScheduledAt = new ScheduleAt.Time(targetTime)
});
@@ -321,14 +325,14 @@ fn schedule_timed_tasks(ctx: &ReducerContext) {
// Schedule for 10 seconds from now
let ten_seconds_from_now = ctx.timestamp + Duration::from_secs(10);
ctx.db.reminder().insert(Reminder {
- id: 0,
+ scheduled_id: 0,
message: "Your auction has ended".to_string(),
scheduled_at: ScheduleAt::Time(ten_seconds_from_now),
});
// Schedule for immediate execution (current timestamp)
ctx.db.reminder().insert(Reminder {
- id: 0,
+ scheduled_id: 0,
message: "Process now".to_string(),
scheduled_at: ScheduleAt::Time(ctx.timestamp.clone()),
});
@@ -343,14 +347,14 @@ fn schedule_timed_tasks(ctx: &ReducerContext) {
Timestamp tenSecondsFromNow = ctx.timestamp + TimeDuration::from_seconds(10);
ctx.db[reminder].insert(Reminder{
0,
- ScheduleAt::time(tenSecondsFromNow),
+ ScheduleAt(tenSecondsFromNow),
"Your auction has ended"
});
// Schedule for immediate execution (current timestamp)
ctx.db[reminder].insert(Reminder{
0,
- ScheduleAt::time(ctx.timestamp),
+ ScheduleAt(ctx.timestamp),
"Process now"
});
```
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..e109d892ef4 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
@@ -22,10 +22,11 @@ To declare a table as an event table, add the `event` attribute to the table def
```typescript
const damageEvent = table({
+ name: 'damage_event',
public: true,
event: true,
}, {
- entity_id: t.identity(),
+ entityId: t.identity(),
damage: t.u32(),
source: t.string(),
});
@@ -92,13 +93,13 @@ To publish an event, simply insert a row into the event table from within a redu
```typescript
export const attack = spacetimedb.reducer(
- { target_id: t.identity(), damage: t.u32() },
- (ctx, { target_id, damage }) => {
+ { targetId: t.identity(), damage: t.u32() },
+ (ctx, { targetId, damage }) => {
// Game logic...
// Publish the event
ctx.db.damageEvent.insert({
- entity_id: target_id,
+ entityId: targetId,
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 like regular tables: either through subscribe-all helpers such as `subscribeToAllTables`, `SubscribeToAllTables`, and `subscribe_to_all_tables`, or through explicit typed queries. 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/00500-authentication/00400-BetterAuth.md b/docs/docs/00200-core-concepts/00500-authentication/00400-BetterAuth.md
index e9ac54ec7c0..b27e9df79fc 100644
--- a/docs/docs/00200-core-concepts/00500-authentication/00400-BetterAuth.md
+++ b/docs/docs/00200-core-concepts/00500-authentication/00400-BetterAuth.md
@@ -58,7 +58,7 @@ The examples below use placeholder URLs:
Better Auth issuer: https://app.example.com/api/auth
OAuth client ID:
SpacetimeDB URL:
-Module name:
+Database name:
```
Use the exact same issuer value everywhere. The issuer must match the token's
@@ -272,7 +272,7 @@ const token = await getBetterAuthOidcToken();
const conn = DbConnection.builder()
.withUri('')
- .withDatabaseName('')
+ .withDatabaseName('')
.withToken(token)
.onConnect((_conn, identity) => {
console.log(
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..a1c9950a5e9 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;
@@ -234,9 +234,7 @@ conn.reducers().on_create_user(|ctx, name, email| {
Context.Reducers->CreateUser(TEXT("Alice"), TEXT("alice@example.com"));
// Register a callback to observe reducer invocations
-FOnCreateUserDelegate Callback;
-BIND_DELEGATE_SAFE(Callback, this, AMyActor, OnCreateUser);
-Context.Reducers->OnCreateUser(Callback);
+Context.Reducers->OnCreateUser.AddDynamic(this, &AMyActor::OnCreateUser);
// Callback function (must be UFUNCTION)
UFUNCTION()
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..009053f2aaf 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md
@@ -319,13 +319,13 @@ let conn = DbConnection::builder()
```cpp
// Create delegates
FOnConnectDelegate ConnectDelegate;
-ConnectDelegate.BindDynamic(this, &AMyActor::OnConnected);
+BIND_DELEGATE_SAFE(ConnectDelegate, this, AMyActor, OnConnected);
FOnConnectErrorDelegate ErrorDelegate;
-ErrorDelegate.BindDynamic(this, &AMyActor::OnConnectError);
+BIND_DELEGATE_SAFE(ErrorDelegate, this, AMyActor, OnConnectError);
FOnDisconnectDelegate DisconnectDelegate;
-DisconnectDelegate.BindDynamic(this, &AMyActor::OnDisconnected);
+BIND_DELEGATE_SAFE(DisconnectDelegate, this, AMyActor, OnDisconnected);
// Build connection with callbacks
UDbConnection* Conn = UDbConnection::Builder()
@@ -351,9 +351,9 @@ void OnConnectError(const FString& Error)
}
UFUNCTION()
-void OnDisconnected()
+void OnDisconnected(UDbConnection* Connection, const FString& Error)
{
- UE_LOG(LogTemp, Warning, TEXT("Disconnected from SpacetimeDB"));
+ UE_LOG(LogTemp, Warning, TEXT("Disconnected from SpacetimeDB: %s"), *Error);
}
```
@@ -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.
-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..e6de19c91d7 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
@@ -67,6 +67,8 @@ https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git
(See also the [Unity Tutorial](../../00100-intro/00300-tutorials/00300-unity-tutorial/00200-part-1.md))
+The Unity package includes a `SpacetimeDBNetworkManager` component. Add one instance to a scene GameObject if you want the SDK to advance active connections from Unity's `Update` loop automatically. If you do not use the manager, call [`FrameTick`](#method-frametick) yourself every frame.
+
## Generate module bindings
Each SpacetimeDB client depends on some bindings specific to your module. Create a `module_bindings` directory in your project's directory and generate the C# interface files using the Spacetime CLI. From your project directory, run:
@@ -220,6 +222,8 @@ class DbConnection {
`FrameTick` will advance the connection until no work remains or until it is disconnected, then return rather than blocking. Games might arrange for this message to be called every frame.
+In Unity projects, a `SpacetimeDBNetworkManager` component can call `FrameTick` for active connections automatically. Use either the manager or your own update loop; without one of them, callbacks will not be invoked.
+
It is not advised to run `FrameTick` on a background thread, since it modifies [`dbConnection.Db`](#property-db). If main thread code is also accessing the `Db`, it may observe data races when `FrameTick` runs on another thread.
(Note that the SDK already does most of the work for parsing messages on a background thread. `FrameTick()` does the minimal amount of work needed to apply updates to the `Db`.)
@@ -306,14 +310,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 +429,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 tables, including public event 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.
#### Type `TypedSubscriptionBuilder`
@@ -711,9 +715,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 +1088,16 @@ 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.
+
+For a reducer named `send_message`, generated C# bindings use PascalCase names:
+
+- 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.
-Each reducer defined by the module has three methods on the `.Reducers`:
+Subscribe to reducer result events with `+=` and unsubscribe with `-=`, as with any C# event.
-- 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.
+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..4980dc1b9f5 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 tables, including public event 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.
## 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/00200-core-concepts/00600-clients/00800-unreal-reference.md b/docs/docs/00200-core-concepts/00600-clients/00800-unreal-reference.md
index 5888d3bf08f..9bc2ff53530 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00800-unreal-reference.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00800-unreal-reference.md
@@ -871,10 +871,10 @@ void AMyActor::BeginPlay()
// Setup connection callbacks
FOnConnectDelegate ConnectDelegate;
- ConnectDelegate.BindDynamic(this, &AMyActor::OnConnected);
+ BIND_DELEGATE_SAFE(ConnectDelegate, this, AMyActor, OnConnected);
FOnDisconnectDelegate DisconnectDelegate;
- DisconnectDelegate.BindDynamic(this, &AMyActor::OnDisconnected);
+ BIND_DELEGATE_SAFE(DisconnectDelegate, this, AMyActor, OnDisconnected);
// Build and connect
Conn = UDbConnection::Builder()
@@ -933,6 +933,11 @@ void AMyActor::SendMessage(const FString& Text)
Conn->Reducers->SendMessage(Text);
}
}
+
+void AMyActor::OnDisconnected(UDbConnection* Connection, const FString& Error)
+{
+ UE_LOG(LogTemp, Warning, TEXT("Disconnected from SpacetimeDB: %s"), *Error);
+}
```
For small modules or quick debugging sessions, you can still subscribe to every public table:
diff --git a/docs/docs/00300-resources/00100-how-to/00050-troubleshooting.md b/docs/docs/00300-resources/00100-how-to/00050-troubleshooting.md
index 35b03022b61..9502eff90fb 100644
--- a/docs/docs/00300-resources/00100-how-to/00050-troubleshooting.md
+++ b/docs/docs/00300-resources/00100-how-to/00050-troubleshooting.md
@@ -66,7 +66,8 @@ You may need to advance your connection by calling one of the following methods:
| Rust (browser only) | `conn.run_background_task()` | Spawn a task to continuously advance the connection. |
| Rust | `conn.run_async()` | A `Future` which you can `await` or poll to advance the connection. |
| Rust | `conn.frame_tick()` | In single-threaded games, call this every frame to advance the connection. |
-| C# | `Conn.FrameTick()` | Call this from your game or application update loop. If you use a separate loop, keep `Conn.Db` access on that same thread or synchronize access. |
+| C# | `Conn.FrameTick()` | In native C# clients, call this from your game or application update loop. If you use a separate loop, keep `Conn.Db` access on that same thread or synchronize access. |
+| Unity | `SpacetimeDBNetworkManager` or `Conn.FrameTick()` | The Unity package can advance connections automatically when a single `SpacetimeDBNetworkManager` component is present in the scene. If you manage the loop yourself, call `FrameTick()` every frame. |
| Unreal | `Conn->FrameTick()` or `Conn->SetAutoTicking(true)` | Call `FrameTick()` every frame, or enable auto-ticking once after building the connection. |
| TypeScript | N/a | The TypeScript client SDK advances connections automatically. |
diff --git a/docs/docs/00300-resources/00100-how-to/00100-deploy/00100-maincloud.md b/docs/docs/00300-resources/00100-how-to/00100-deploy/00100-maincloud.md
index 53d2503338c..0f7238dd3cc 100644
--- a/docs/docs/00300-resources/00100-how-to/00100-deploy/00100-maincloud.md
+++ b/docs/docs/00300-resources/00100-how-to/00100-deploy/00100-maincloud.md
@@ -46,7 +46,7 @@ spacetime publish my-database --server maincloud --delete-data
## Connecting Clients to Maincloud
-To connect your client application to a module running on Maincloud, use `https://maincloud.spacetimedb.com` as the host URL and your database name as the module name:
+To connect your client application to a module running on Maincloud, use `https://maincloud.spacetimedb.com` as the host URL and pass the database name to `withDatabaseName` / `WithDatabaseName`:
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..ad5fa1ef966 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,7 +190,7 @@ 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 damageEvent = table({ name: 'damage_event', event: true }, {
target: t.identity(),
amount: t.u32(),
})
@@ -424,7 +424,7 @@ Conn->SubscriptionBuilder()
- On the client, `count()` always returns 0 and `iter()` is always empty.
- Only `on_insert` callbacks are generated (no `on_delete` or `on_update`).
- The `event` keyword in `#[table(..., event)]` marks the table as transient.
-- Event tables must be subscribed to explicitly (they are excluded from `subscribeToAllTables` / `SubscribeToAllTables` / `subscribe_to_all_tables`).
+- Event tables can be subscribed to with subscribe-all helpers or explicit typed queries, but clients observe them only through insert callbacks.
## Event Type Changes
@@ -605,13 +605,13 @@ Unreal 2.0 now supports typed query-builder subscriptions in C++. Use `AddQuery(
-Note that subscribing to event tables requires an explicit query:
+Use explicit queries when you want to subscribe to event tables without subscribing to every public table:
```typescript
-// Event tables are excluded from subscribe_to_all_tables(), so subscribe explicitly:
+// Subscribe explicitly to an event table:
import { tables } from "./module_bindings";
ctx.subscriptionBuilder()
.onApplied((ctx) => { /* ... */ })
@@ -633,7 +633,7 @@ conn.SubscriptionBuilder()
```rust
-// Event tables are excluded from subscribe_to_all_tables(), so subscribe explicitly:
+// Subscribe explicitly to an event table:
ctx.subscription_builder()
.on_applied(|ctx| { /* ... */ })
.add_query(|q| q.from.damage_event())
@@ -644,7 +644,7 @@ ctx.subscription_builder()
```cpp
-// Event tables are excluded from SubscribeToAllTables(), so subscribe explicitly:
+// Subscribe explicitly to an event table:
Conn->SubscriptionBuilder()
->OnApplied(OnAppliedDelegate)
->OnError(OnErrorDelegate)
@@ -1366,7 +1366,7 @@ spacetimedb.reducer('runMyTimer', myTimer.rowType, (ctx, timer) => {
```
```typescript
-const myTimer = table({ scheduled: () => runMyTimer }, {
+const myTimer = table({ name: 'my_timer', scheduled: (): any => runMyTimer }, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
});
@@ -1479,7 +1479,7 @@ In the rare event that you have a reducer or procedure which is intended to be i
```typescript
-const myTimer = table({ scheduled: () => runMyTimerPrivate }, {
+const myTimer = table({ name: 'my_timer', scheduled: (): any => runMyTimerPrivate }, {
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
});
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