-
Notifications
You must be signed in to change notification settings - Fork 0
feat: client db connection #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.
The lifecycle hooks already exist in 🤖 Prompt for AI Agents |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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:
💡 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 The The 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 🤖 Prompt for AI Agents |
||
| 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) => { | ||
|
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 ( | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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>, | ||
| ) | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.localcommitted to version control.env.localis not listed in.gitignore, so this file is now tracked by git. While the current values (ws://localhost:3000andmono-chat) are not sensitive, committing environment files is dangerous because:.env.localshould be added to.gitignoreand a.env.local.example(with placeholder values) provided instead. The.gitignorecurrently has no entry for.env*or.env.local.There was a problem hiding this comment.
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