Skip to content

Commit 76480dc

Browse files
committed
Put the Svelte binding on ConnectionManager; add reconnect() for token swaps
# Description of Changes A Svelte app that lets a user start anonymous and then sign in (e.g. via Google) has no clean way to apply the new token. The Svelte provider holds its connection in a module-level singleton that's reused across remounts, so even a `{#key}` swap keeps the old token, and there's no reconnect path — the only thing that actually works is a hard `window.location.reload()`, which throws away all client state and flickers the UI on every sign-in / sign-out. The Svelte binding also lacks the auto-reconnect that the React and Solid bindings already have, so a dropped socket just stays dropped. This rebuilds `createSpacetimeDBProvider` on top of the shared `ConnectionManager` — the same pool React and Solid already use — and surfaces a `reconnect(builder)` method on the context value, closing both gaps at once: - The connection is owned by `ConnectionManager`, keyed by uri + database name. Its reference counting and deferred release absorb rapid mount/unmount cycles (HMR, `{#key}`), replacing the bespoke singleton, and the binding gets the manager's exponential-backoff auto-reconnect on unexpected drops for free. - `ConnectionState` gains `reconnect(builder)` (backed by the new `ConnectionManager.rebuild`): tears down the current connection and reconnects with a fresh builder carrying a new token, no page reload. `useTable` / `useReducer` already react to the connection store, so subscriptions re-bind to the new connection automatically. - `getConnection` is now non-generic (`DbConnectionImpl<any> | null`), matching the React and Solid `ConnectionState`, so the bindings line up. `useTable`, `useReducer` and `useSpacetimeDB` are unchanged. Motivated by #5373; hangs off the reconnect tracking issue #1936. # API and ABI breaking changes Minor, TypeScript-only: `ConnectionState.getConnection` is no longer generic — it returns `DbConnectionImpl<any> | null`, as the React and Solid bindings already do. Call sites that wrote `getConnection<MyConn>()` lose the type argument; `getConnection()` is unchanged. The provider's return type stays `Writable<ConnectionState>`. # Expected complexity level and risk 2. Low. The Svelte provider is now a thin adapter: it mirrors the manager's store into a Svelte store and wires up `getConnection` / `reconnect` / `retain` / `release`. The connection-lifecycle logic that used to be bespoke here — the module-level singleton, deferred cleanup, and reconnect — now lives in the well-tested, shared `ConnectionManager` that the React and Solid bindings already depend on. # Testing - [x] `tsc -p tsconfig.build.json` typechecks; `build:js` emits the Svelte ESM/CJS + browser bundles cleanly; eslint + prettier clean. - [x] Full vitest suite green (225 tests), including the new `rebuild()` unit tests from the previous commit. - [ ] Reviewer: this package has no Svelte component-test harness today (the pre-existing Svelte binding had none), so the provider wiring is covered indirectly through the `ConnectionManager` unit tests. Worth a manual smoke test of an anonymous -> signed-in `reconnect(builder)` flow, and a call on whether to add @testing-library/svelte coverage here.
1 parent 46bdbb4 commit 76480dc

2 files changed

Lines changed: 61 additions & 81 deletions

File tree

crates/bindings-typescript/src/svelte/SpacetimeDBProvider.ts

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,99 +3,75 @@ import { writable, type Writable } from 'svelte/store';
33
import {
44
DbConnectionBuilder,
55
type DbConnectionImpl,
6-
type ErrorContextInterface,
7-
type RemoteModuleOf,
86
} from '../sdk/db_connection_impl';
97
import { ConnectionId } from '../lib/connection_id';
8+
import {
9+
ConnectionManager,
10+
type ConnectionState as ManagerConnectionState,
11+
} from '../sdk/connection_manager';
1012
import {
1113
SPACETIMEDB_CONTEXT_KEY,
1214
type ConnectionState,
1315
} from './connection_state';
1416

15-
let connRef: DbConnectionImpl<any> | null = null;
16-
let cleanupTimeoutId: ReturnType<typeof setTimeout> | null = null;
17-
17+
/**
18+
* Establish a SpacetimeDB connection for the current component subtree and make
19+
* it available to `useSpacetimeDB`, `useTable` and `useReducer`.
20+
*
21+
* The connection is owned by the shared `ConnectionManager` (the same pool the
22+
* React and Solid bindings use), keyed by uri + database name. The manager's
23+
* reference counting and deferred cleanup absorb rapid mount/unmount cycles
24+
* (HMR, `{#key}` blocks), and it reconnects automatically with exponential
25+
* backoff if the socket drops unexpectedly.
26+
*
27+
* To swap the connection's auth token without reloading the page (e.g. after a
28+
* sign-in), call `reconnect(builder)` from the context value with a builder
29+
* carrying the new token.
30+
*/
1831
export function createSpacetimeDBProvider<
1932
DbConnection extends DbConnectionImpl<any>,
2033
>(
2134
connectionBuilder: DbConnectionBuilder<DbConnection>
2235
): Writable<ConnectionState> {
23-
const getConnection = () => connRef as DbConnection | null;
36+
const key = ConnectionManager.getKey(
37+
connectionBuilder.getUri(),
38+
connectionBuilder.getModuleName()
39+
);
2440

25-
const store = writable<ConnectionState>({
41+
const fallback: ManagerConnectionState = {
2642
isActive: false,
2743
identity: undefined,
2844
token: undefined,
2945
connectionId: ConnectionId.random(),
3046
connectionError: undefined,
31-
getConnection: getConnection as ConnectionState['getConnection'],
32-
});
33-
34-
if (cleanupTimeoutId) {
35-
clearTimeout(cleanupTimeoutId);
36-
cleanupTimeoutId = null;
37-
}
38-
39-
if (!connRef) {
40-
connRef = connectionBuilder.build();
41-
}
42-
43-
const onConnect = (conn: DbConnection) => {
44-
store.update(s => ({
45-
...s,
46-
isActive: conn.isActive,
47-
identity: conn.identity,
48-
token: conn.token,
49-
connectionId: conn.connectionId,
50-
}));
5147
};
5248

53-
const onDisconnect = (
54-
ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>
55-
) => {
56-
store.update(s => ({
57-
...s,
58-
isActive: ctx.isActive,
59-
}));
49+
const getConnection = () =>
50+
ConnectionManager.getConnection<DbConnection>(key);
51+
const reconnect = (builder: DbConnectionBuilder<DbConnection>): void => {
52+
ConnectionManager.rebuild(key, builder);
6053
};
6154

62-
const onConnectError = (
63-
ctx: ErrorContextInterface<RemoteModuleOf<DbConnection>>,
64-
err: Error
65-
) => {
66-
store.update(s => ({
67-
...s,
68-
isActive: ctx.isActive,
69-
connectionError: err,
70-
}));
71-
};
72-
73-
connectionBuilder.onConnect(onConnect);
74-
connectionBuilder.onDisconnect(onDisconnect);
75-
connectionBuilder.onConnectError(onConnectError);
76-
77-
const conn = connRef;
78-
store.update(s => ({
79-
...s,
80-
isActive: conn.isActive,
81-
identity: conn.identity,
82-
token: conn.token,
83-
connectionId: conn.connectionId,
84-
}));
55+
const snapshot = (): ConnectionState => ({
56+
...(ConnectionManager.getSnapshot(key) ?? fallback),
57+
getConnection,
58+
reconnect,
59+
});
8560

86-
setContext(SPACETIMEDB_CONTEXT_KEY, store);
61+
// Retain for this provider's lifetime, then mirror the manager's external
62+
// store into a Svelte store. `getConnection` / `reconnect` are stable across
63+
// updates; only the plain state fields change.
64+
ConnectionManager.retain(key, connectionBuilder);
65+
const store = writable<ConnectionState>(snapshot());
66+
const unsubscribe = ConnectionManager.subscribe(key, () =>
67+
store.set(snapshot())
68+
);
8769

8870
onDestroy(() => {
89-
connRef?.removeOnConnect(onConnect as any);
90-
connRef?.removeOnDisconnect(onDisconnect as any);
91-
connRef?.removeOnConnectError(onConnectError as any);
92-
93-
cleanupTimeoutId = setTimeout(() => {
94-
connRef?.disconnect();
95-
connRef = null;
96-
cleanupTimeoutId = null;
97-
}, 0);
71+
unsubscribe();
72+
ConnectionManager.release(key);
9873
});
9974

75+
setContext(SPACETIMEDB_CONTEXT_KEY, store);
10076
return store;
10177
}
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import type { ConnectionId } from '../lib/connection_id';
2-
import type { Identity } from '../lib/identity';
3-
import type { DbConnectionImpl } from '../sdk/db_connection_impl';
4-
5-
export type ConnectionState = {
6-
isActive: boolean;
7-
identity?: Identity;
8-
token?: string;
9-
connectionId: ConnectionId;
10-
connectionError?: Error;
11-
getConnection<
12-
DbConnection extends DbConnectionImpl<any>,
13-
>(): DbConnection | null;
14-
};
1+
import type {
2+
DbConnectionBuilder,
3+
DbConnectionImpl,
4+
} from '../sdk/db_connection_impl';
5+
import type { ConnectionState as ManagerConnectionState } from '../sdk/connection_manager';
156

167
export const SPACETIMEDB_CONTEXT_KEY = Symbol('spacetimedb');
8+
9+
export type ConnectionState = ManagerConnectionState & {
10+
/** The live connection, or `null` before it is first established. */
11+
getConnection(): DbConnectionImpl<any> | null;
12+
/**
13+
* Tear down the current connection and reconnect using a fresh builder —
14+
* typically to apply a new auth token after sign-in or sign-out. The builder
15+
* should carry the new token and the same uri + database name. Table and
16+
* reducer subscriptions re-bind automatically once the new connection is
17+
* live, so there is no need to reload the page to swap a token.
18+
*/
19+
reconnect(builder: DbConnectionBuilder<any>): void;
20+
};

0 commit comments

Comments
 (0)