Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_SPACETIMEDB_HOST=ws://localhost:3000
VITE_SPACETIMEDB_DB_NAME=mono-chat
Comment on lines +1 to +2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 .env.local committed to version control

.env.local is not listed in .gitignore, so this file is now tracked by git. While the current values (ws://localhost:3000 and mono-chat) are not sensitive, committing environment files is dangerous because:

  1. A developer could later add secrets (tokens, API keys, production URLs) to this file and they would be silently committed.
  2. It overrides other developers' local configurations when they pull the branch.

.env.local should be added to .gitignore and a .env.local.example (with placeholder values) provided instead. The .gitignore currently has no entry for .env* or .env.local.

# .gitignore (add these lines)
.env.local
.env.*.local

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.local is used for example if someone wants to setup app themselves, for production would be used .env

66 changes: 60 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,82 @@ export type PrettyMessage = {
};

function App() {
const { identity, isActive: connected } = useSpacetimeDB();
const setName = useReducer(reducers.setName);
const sendMessage = useReducer(reducers.sendMessage);

const [messages] = useTable(tables.message);

const [newName, setNewName] = useState("");
const [settingName, setSettingName] = useState(false);
const [systemMessages, setSystemMessages] = useState([] as Types.Message[]);
const [newMessage, setNewMessage] = useState("");
Comment on lines 24 to 25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether backend lifecycle hooks exist and whether they persist connect/disconnect events.
rg -n -C3 'clientConnected|clientDisconnected|insert\(|db\.message|send_message|system'

Repository: DLineone/mono-chat

Length of output: 16326


Extend lifecycle hooks to persist connect/disconnect events as shared messages.

systemMessages is local component state that resets on refresh. System messages should be emitted on the backend via the existing spacetimedb.clientConnected() and spacetimedb.clientDisconnected() lifecycle hooks—insert messages with sender: Identity.zero() into the shared message table, then subscribe to all messages on the client. This ensures all clients see the same event history.

The lifecycle hooks already exist in spacetimedb/src/index.ts (lines 68 and 81) but currently only manage the user.online field. Extend them to also insert system messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 24 - 25, Extend the lifecycle hooks clientConnected
and clientDisconnected in spacetimedb to insert a system message row into the
shared "message" table (use sender: Identity.zero(), appropriate text like "User
X connected/disconnected", and any timestamp) instead of keeping systemMessages
only in local state; then update the client component (the useState for
systemMessages and newMessage and any code that pushes local systemMessages) to
subscribe to the shared messages feed (subscribe to all messages from the
"message" table) and derive system messages from that subscription so all
clients see the same event history; reference clientConnected,
clientDisconnected, Identity.zero(), and the shared "message" table when making
these changes.


const onlineUsers: Types.User[] = [];
const offlineUsers: Types.User[] = [];
const [onlineUsers] = useTable(
tables.user.where((r) => r.online.eq(true)),
{
onInsert: (user) => {
const name = user.name || user.identity.toHexString().substring(0, 8);
setSystemMessages((prev) => [
...prev,
{
sender: Identity.zero(),
text: `${name} has connected.`,
sent: Timestamp.now(),
},
]);
},
onDelete: (user) => {
const name = user.name || user.identity.toHexString().substring(0, 8);
setSystemMessages((prev) => [
...prev,
{
sender: Identity.zero(),
text: `${name} has disconnected.`,
sent: Timestamp.now(),
},
]);
},
},
);
const [offlineUsers] = useTable(tables.user.where((r) => r.online.eq(false)));
Comment on lines +27 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

spacetimedb 2.0 useTable where predicate onInsert onDelete callbacks

💡 Result:

In SpacetimeDB 2.0, there is no direct "useTable" hook in the core SDK, but the spacetimedb/react package provides React hooks like useTable for subscribing to tables with real-time updates. These hooks handle subscriptions using table references from generated module_bindings. Key features: - Subscribe to tables or filtered views using tables. from bindings. - useTable(tables.person) returns [rows, isLoading], automatically managing WebSocket subscriptions for inserts, updates, deletes. - Use .where for predicates: tables.person.where(r => r.online.eq(true)) filters rows server-side via indexed columns. - Row callbacks like onInsert, onUpdate, onDelete are available on conn.db.

in non-React code, firing on cache changes. React hooks abstract this. Example from Next.js template (client component): import { useTable, useReducer } from 'spacetimedb/react'; import { tables } from '../module_bindings'; function PersonList({ initialPeople }) { const [people, isLoading] = useTable(tables.person); // Uses server-fetched initial data until subscription applies const displayPeople = isLoading ? initialPeople : people; // ... } For filtered subscriptions: conn.subscriptionBuilder.subscribe( tables.user.where(r => r.online.eq(true)) ); This uses the query builder API with operators like eq, ne, gt, etc., on indexed columns. onInsert/onDelete callbacks are used directly on table handles for event handling: conn.db.damageEvent.onInsert((ctx, event) => { /* handle */ }); Event tables only support onInsert since rows are transient. Wrap app in SpacetimeDBProvider with connection builder. Generate bindings via spacetime generate --lang typescript. Official docs confirm React integration via spacetimedb/react subpath, with hooks like useTable and useReducer for tables and reducers.

Citations:


🌐 Web query:

spacetimedb/react useTable hook signature parameters API documentation

💡 Result:

The useTable hook from spacetimedb/react subscribes to a SpacetimeDB table and returns the rows and loading state for real-time updates in React components. Signature: useTable<DbConnection, RowType>(tableRef: TableRef): [rows: RowType[], isLoading: boolean] Parameters: - tableRef: A TableRef from generated module_bindings, e.g., tables.person or filtered like tables.user.where(r => r.online.eq(true)). Type-safe query builder for subscriptions. Usage requires wrapping the app in . Examples: const { rows: messages } = useTable<DbConnection, Message>('message'); const { rows: people, isLoading } = useTable(tables.person); const { rows: onlineUsers } = useTable('user', where(eq('online', true))); Must be used within a SpacetimeDBProvider. Automatically handles subscriptions and real-time sync. Compatible with React StrictMode.

Citations:


Remove invalid callback parameters from useTable hook.

The useTable hook signature in spacetimedb/react accepts only a single tableRef parameter and returns [rows, isLoading]. The onInsert and onDelete callbacks are not valid parameters for the React hook. These callbacks are available only on server-side conn.db.<table> handles, not on the client-side React hook.

The .where() predicate is correct, but remove the second parameter object:

const [onlineUsers] = useTable(tables.user.where((r) => r.online.eq(true)));
const [offlineUsers] = useTable(tables.user.where((r) => r.online.eq(false)));

For real-time updates on insert/delete events, rely on the subscription mechanism within useTable itself, which automatically syncs table changes. Handle UI updates through useEffect hooks observing the onlineUsers state if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 27 - 54, The useTable hook call is passing an
invalid second parameter with onInsert/onDelete callbacks; remove that object
and call useTable only with the tableRef (e.g., change
useTable(tables.user.where((r) => r.online.eq(true)), { ... }) to
useTable(tables.user.where((r) => r.online.eq(true))) and do the same for
offlineUsers). To produce the same system messages, add a useEffect that watches
the onlineUsers array returned from useTable and uses setSystemMessages along
with Identity.zero() and Timestamp.now() to append connect/disconnect notices
when the list changes (detect additions/removals in the effect).

const users = [...onlineUsers, ...offlineUsers];
const prettyMessages: PrettyMessage[] = [];
const prettyMessages: PrettyMessage[] = messages
.concat(systemMessages)
.sort((a, b) => a.sent.toDate().getTime() - b.sent.toDate().getTime())
.map((message) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const user = users.find((u) => u.identity.toHexString() === message.sender.toHexString());
return {
senderName: user?.name || message.sender.toHexString().substring(0, 8),
text: message.text,
sent: message.sent,
kind: Identity.zero().isEqual(message.sender) ? "system" : "user",
};
});

if (!connected || !identity) {
return (
<div className="App">
<h1>Connecting...</h1>
</div>
);
}

const name = "";
const name = (() => {
const user = users.find((u) => u.identity.isEqual(identity));
return user?.name || identity?.toHexString().substring(0, 8) || "";
})();

const onSubmitNewName = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSettingName(false);
// TODO: Call `setName` reducer
setName({ name: newName.trim() });
};

const onSubmitMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setNewMessage("");
// TODO: Call `sendMessage` reducer
sendMessage({ text: newMessage.trim() });
};

return (
Expand Down
44 changes: 37 additions & 7 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { Identity } from "spacetimedb";
import { SpacetimeDBProvider } from "spacetimedb/react";
import { DbConnection, type ErrorContext } from "./module_bindings/index.ts";

createRoot(document.getElementById('root')!).render(
const HOST = import.meta.env.VITE_SPACETIMEDB_HOST;
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME;
const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`;

Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast when required env vars are missing.

If either env var is absent, the builder is created with invalid values and the token key becomes undefined/..., which leads to confusing connection failures.

Suggested fix
 const HOST = import.meta.env.VITE_SPACETIMEDB_HOST;
 const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME;
+if (!HOST || !DB_NAME) {
+  throw new Error("Missing VITE_SPACETIMEDB_HOST or VITE_SPACETIMEDB_DB_NAME");
+}
 const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`;

Also applies to: 26-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.tsx` around lines 9 - 12, Validate that
import.meta.env.VITE_SPACETIMEDB_HOST and
import.meta.env.VITE_SPACETIMEDB_DB_NAME are present before computing TOKEN_KEY
or constructing the Spacetime DB builder: if either HOST or DB_NAME is falsy,
log a clear error and throw/exit immediately; compute TOKEN_KEY only after
validation (TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`) and apply the same
presence check before any code that constructs the builder using HOST and
DB_NAME so the app fails fast with a clear message instead of producing
undefined-based keys.

const onConnect = (_conn: DbConnection, identity: Identity, token: string) => {
localStorage.setItem(TOKEN_KEY, token);
console.log("Connected to SpacetimeDB with identity:", identity.toHexString());
};

const onDisconnect = () => {
console.log("Disconnected from SpacetimeDB");
};

const onConnectError = (_ctx: ErrorContext, err: Error) => {
console.log("Error connecting to SpacetimeDB:", err);
};
Comment on lines +22 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Connection errors leave users stuck on "Connecting…" indefinitely

onConnectError only logs to the console. Because App.tsx renders <h1>Connecting...</h1> whenever !connected || !identity, a connection failure will leave the user staring at the loading screen forever with no actionable feedback.

Consider surfacing the error to the user, for example by storing the error in module-level state and rendering an error message:

const onConnectError = (_ctx: ErrorContext, err: Error) => {
  console.error("Error connecting to SpacetimeDB:", err);
  // TODO: set an error state and render it in the UI
};


const connectionBuilder = DbConnection.builder()
.withUri(HOST)
.withDatabaseName(DB_NAME)
.withToken(localStorage.getItem(TOKEN_KEY) || undefined)
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(onConnectError);

createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
<App />
</SpacetimeDBProvider>
</StrictMode>,
)
);