From fdf45972cf0e64bb7f3a592f834ed67cc517e68a Mon Sep 17 00:00:00 2001 From: Ilya Zhidkov Date: Fri, 13 Mar 2026 07:42:00 +0300 Subject: [PATCH] feat(ts-sdk): add reconnect() method to DbConnectionImpl Add a reconnect() method that reopens the WebSocket on the same connection object, preserving the client cache. This allows applications to handle token refresh and network recovery without losing table data or causing UI flashes. Use case: long-lived browser tabs (e.g. games) where the WS may drop due to network changes, sleep, or token expiration. --- .../src/sdk/db_connection_impl.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/crates/bindings-typescript/src/sdk/db_connection_impl.ts b/crates/bindings-typescript/src/sdk/db_connection_impl.ts index c2c26123bfe..a16fa9d6c22 100644 --- a/crates/bindings-typescript/src/sdk/db_connection_impl.ts +++ b/crates/bindings-typescript/src/sdk/db_connection_impl.ts @@ -176,6 +176,15 @@ export class DbConnectionImpl private ws?: WebsocketAdapter; private wsPromise: Promise; + private wsConfig?: { + url: URL; + nameOrAddress: string; + createWSFn: typeof WebsocketDecompressAdapter.createWebSocketFn; + compression: 'gzip' | 'none'; + lightMode: boolean; + confirmedReads?: boolean; + }; + constructor({ uri, nameOrAddress, @@ -243,6 +252,8 @@ export class DbConnectionImpl this.reducers = this.#makeReducers(remoteModule); this.procedures = this.#makeProcedures(remoteModule); + this.wsConfig = { url, nameOrAddress, createWSFn, compression, lightMode, confirmedReads }; + this.wsPromise = createWSFn({ url, nameOrAddress, @@ -980,6 +991,69 @@ export class DbConnectionImpl this.wsPromise.then(ws => ws?.close()); } + /** + * Reconnect to SpacetimeDB with an optional new authentication token. + * + * Preserves the client cache — existing table data stays intact while + * subscriptions are re-established on the new connection. This avoids + * the data gap that occurs when creating a new connection object. + * + * @param newToken - Optional new auth token. If not provided, reuses the existing token. + * + * @example + * + * ```ts + * connection.onDisconnect(() => { + * const freshToken = await refreshAuthToken(); + * connection.reconnect(freshToken); + * }); + * ``` + */ + reconnect(newToken?: string): void { + if (newToken !== undefined) { + this.token = newToken; + } + stdbLogger('info', 'Reconnecting to SpacetimeDB WS...'); + if (this.ws) { + this.ws.onclose = null as any; + this.ws.onerror = null as any; + this.ws.onopen = null as any; + this.ws.onmessage = null as any; + this.ws.close(); + this.ws = undefined; + } + this.isActive = false; + const cfg = this.wsConfig!; + this.wsPromise = cfg.createWSFn({ + url: cfg.url, + nameOrAddress: cfg.nameOrAddress, + wsProtocol: 'v2.bsatn.spacetimedb', + authToken: this.token, + compression: cfg.compression, + lightMode: cfg.lightMode, + confirmedReads: cfg.confirmedReads, + }) + .then(v => { + this.ws = v; + this.ws.onclose = () => { + this.#emitter.emit('disconnect', this); + this.isActive = false; + }; + this.ws.onerror = (e: ErrorEvent) => { + this.#emitter.emit('connectError', this, e); + this.isActive = false; + }; + this.ws.onopen = this.#handleOnOpen.bind(this); + this.ws.onmessage = this.#handleOnMessage.bind(this); + return v; + }) + .catch(e => { + stdbLogger('error', 'Error reconnecting to SpacetimeDB WS'); + this.#emitter.emit('connectError', this, e); + return undefined; + }); + } + private on( eventName: ConnectionEvent, callback: (ctx: DbConnectionImpl, ...args: any[]) => void