Skip to content

Commit da4edb3

Browse files
committed
Add tests and documentation for ConnectionManager
- Add 33 unit tests covering reference counting, deferred cleanup, React StrictMode simulation, state management, and subscriptions - Add React Integration section to TypeScript reference docs with SpacetimeDBProvider, useSpacetimeDB, and useTable documentation - Add StrictMode compatibility note to SDK README - Add module-level JSDoc to connection_manager.ts explaining the TanStack Query-style pattern for handling React lifecycles
1 parent 19d3b2d commit da4edb3

4 files changed

Lines changed: 968 additions & 1 deletion

File tree

crates/bindings-typescript/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ connection.reducers.createPlayer();
6666

6767
#### React Usage
6868

69-
This module also include React hooks to subscribe to tables under the `spacetimedb/react` subpath. In order to use SpacetimeDB React hooks in your project, first add a `SpacetimeDBProvider` at the top of your component hierarchy:
69+
This module also includes React hooks to subscribe to tables under the `spacetimedb/react` subpath. The React integration is fully compatible with React StrictMode and handles the double-mount behavior correctly (only one WebSocket connection is created).
70+
71+
In order to use SpacetimeDB React hooks in your project, first add a `SpacetimeDBProvider` at the top of your component hierarchy:
7072

7173
```tsx
7274
const connectionBuilder = DbConnection.builder()

crates/bindings-typescript/src/sdk/connection_manager.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
/**
2+
* ConnectionManager - A reference-counted connection manager for SpacetimeDB.
3+
*
4+
* This module implements a TanStack Query-style pattern for managing WebSocket
5+
* connections in React applications. It solves the React StrictMode double-mount
6+
* problem by using reference counting and deferred cleanup.
7+
*
8+
* ## How it works:
9+
*
10+
* 1. **Reference Counting**: Each `retain()` increments a counter, `release()` decrements it.
11+
* The connection is only closed when the count reaches zero.
12+
*
13+
* 2. **Deferred Cleanup**: When refCount hits zero, cleanup is scheduled via `setTimeout(0)`.
14+
* This allows React StrictMode's rapid unmount→remount cycle to cancel the cleanup.
15+
*
16+
* 3. **useSyncExternalStore Integration**: The `subscribe()` and `getSnapshot()` methods
17+
* are designed to work with React's `useSyncExternalStore` hook for tear-free reads.
18+
*
19+
* ## StrictMode Lifecycle:
20+
*
21+
* ```
22+
* Mount → retain() → refCount: 0→1, connection created
23+
* Unmount → release() → refCount: 1→0, cleanup SCHEDULED (not executed)
24+
* Remount → retain() → refCount: 0→1, cleanup CANCELLED
25+
* Result: Single WebSocket survives ✓
26+
* ```
27+
*
28+
* @module connection_manager
29+
*/
130
import type {
231
DbConnectionBuilder,
332
DbConnectionImpl,
@@ -6,6 +35,7 @@ import type {
635
import type { Identity } from '../lib/identity';
736
import { ConnectionId } from '../lib/connection_id';
837

38+
/** Represents the current state of a managed connection. */
939
export type ConnectionState = {
1040
isActive: boolean;
1141
identity?: Identity;
@@ -37,13 +67,19 @@ function defaultState(): ConnectionState {
3767
};
3868
}
3969

70+
/**
71+
* Singleton manager for SpacetimeDB connections.
72+
* Use the exported `ConnectionManager` instance rather than instantiating directly.
73+
*/
4074
class ConnectionManagerImpl {
4175
#connections = new Map<string, ManagedConnection>();
4276

77+
/** Generates a unique key for a connection based on URI and module name. */
4378
static getKey(uri: string, moduleName: string): string {
4479
return `${uri}::${moduleName}`;
4580
}
4681

82+
/** Instance method wrapper for getKey. */
4783
getKey(uri: string, moduleName: string): string {
4884
return ConnectionManagerImpl.getKey(uri, moduleName);
4985
}
@@ -70,6 +106,15 @@ class ConnectionManagerImpl {
70106
}
71107
}
72108

109+
/**
110+
* Retains a connection, incrementing its reference count.
111+
* Creates the connection on first call; returns existing connection on subsequent calls.
112+
* Cancels any pending release if the connection was about to be cleaned up.
113+
*
114+
* @param key - Unique identifier for the connection (use getKey to generate)
115+
* @param builder - Connection builder to create the connection if needed
116+
* @returns The managed connection instance
117+
*/
73118
retain<T extends DbConnectionImpl<any>>(
74119
key: string,
75120
builder: DbConnectionBuilder<T>

0 commit comments

Comments
 (0)