import { DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, SaveProps, TableMetadata, BranchInfo } from "@rebasepro/types"; export interface RebaseWebSocketConfig { websocketUrl: string; /** Optional auth token getter for WebSocket authentication */ getAuthToken?: () => Promise; /** Optional WebSocket constructor to override globalThis.WebSocket (e.g. for Node environments) */ WebSocket?: typeof WebSocket; /** Callback to handle unauthorized requests or token expiration (refreshes auth session) */ onUnauthorized?: () => Promise; } /** * Low-level realtime WebSocket client. * * @internal Not a stable app-facing API. `createRebaseClient()` constructs and * manages this internally (exposed as `client.ws`, typed by the minimal * `RebaseWebSocket` contract in `@rebasepro/types`). It stays exported from the * package root for the same reason it always was — a data-source driver may * instantiate it directly — but nothing in this repo does since * `@rebasepro/client-postgres` was removed; its surface may change without a * major bump. */ export declare class RebaseWebSocketClient { private websocketUrl; private ws; getAuthToken?: () => Promise; private subscriptions; private listeners; /** Channel-name → handlers, for broadcast and presence frames. */ private channelHandlers; /** Set by `close()`. Blocks any later operation from silently redialling. */ private closedByCaller; /** * Set when the backoff budget ran out, cleared by anything that earns a * fresh one. * * Unlike {@link closedByCaller} this is not final — nobody *asked* for the * socket to stay down. Five attempts with exponential backoff is about a * minute, which a laptop lid, a wifi handover or a backend rollout all * exceed routinely; treating that as permanent meant realtime silently * stopped for the rest of the page's life, with a reload the only cure. */ private gaveUp; /** * Whether a socket exists at all (open or still opening). * * Lets callers distinguish "authenticate the live socket" from "there is * nothing to authenticate yet", without that question forcing a dial. */ get hasSocket(): boolean; /** So the "no WebSocket in this environment" warning is said once, not per call. */ private warnedNoWebSocket; /** Subscribe to broadcast/presence frames for one channel. */ onChannelMessage(channel: string, handler: (message: Record) => void): () => void; /** Notified after the socket comes back, so channels can re-join. */ onReconnect(handler: () => void): () => void; on(event: "connect" | "disconnect" | "reconnect" | "error", cb: (...args: unknown[]) => void): () => boolean; private emit; private collectionSubscriptions; private singleSubscriptions; private backendToCollectionKey; private backendToEntityKey; private pendingRequests; private reconnectAttempts; private maxReconnectAttempts; private isConnected; private messageQueue; private requestTimeoutMs; private subscriptionTimeoutMs; private reconnectTimeout; private isAuthenticated; private authPromise; private WebSocketConstructor; onUnauthorized?: () => Promise; private refreshInProgress; constructor(config: RebaseWebSocketConfig); /** * Open the socket if it is not open (or opening) already. * * Idempotent, synchronous, and safe to call on every operation that needs a * live socket — `initWebSocket` already no-ops on an open socket and is * re-entrant, since the reconnect path has always called it. */ ensureConnected(): void; /** * The browser says the network is back — the usual reason the budget ran * out in the first place. Registered lazily so a Node client, or a page * that never subscribes, adds no listener. */ private installOnlineListener; private onlineListener; /** * Authenticate the WebSocket connection */ authenticate(token: string): Promise; /** * Set the auth token getter function */ setAuthTokenGetter(getAuthToken: () => Promise): void; /** * Drop the socket. * * `permanent` distinguishes the two callers. Signing out drops the socket * but the client stays usable — a later subscribe should reconnect * anonymously. `client.close()` is the caller saying they are done, and * must not be undone by a stray queued frame. */ disconnect(permanent?: boolean): void; private initWebSocket; private processMessageQueue; private attemptReconnect; private isAuthError; private handleAuthFailure; /** * Shared logic for re-subscribing a collection or row subscription * after an auth error is resolved by refreshing credentials. */ private resubscribeAfterAuthRefresh; private handleWebSocketMessage; private ensureAuthenticated; private runAuthentication; reauthenticate(): Promise; /** * Public because `RebaseRealtimeChannel` sends channel frames through it. * Not part of the stable surface — prefer `client.realtime.channel(name)`. */ sendMessage(message: Record): Promise; private doSendMessage; fetchCollection>(props: FetchCollectionProps): Promise[]>; fetchOne>(props: FetchOneProps): Promise | undefined>; save>(props: SaveProps): Promise>; delete>(props: DeleteProps): Promise; executeSql(sql: string, options?: { database?: string; role?: string; }): Promise[]>; fetchAvailableDatabases(): Promise; fetchAvailableRoles(): Promise; fetchApplicationRoles(): Promise; fetchCurrentDatabase(): Promise; checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise; count>(props: FetchCollectionProps): Promise; fetchUnmappedTables(mappedPaths?: string[]): Promise; fetchTableMetadata(tableName: string): Promise; createBranch(name: string, options?: { source?: string; }): Promise; deleteBranch(name: string): Promise; listBranches(): Promise; /** * Recursively compare two values for structural equality. * Handles primitives, null, undefined, Date, RegExp, arrays, and plain objects. */ private deepEqual; private normalizeForComparison; /** * The address of a row, for matching it against another copy of itself. * * A row is exactly its columns and carries no address, so it is derived * from the key columns the server named — including the ordinary case where * that key is `id`, which the server reports like any other. * * Undefined when there are no keys, which means the server could not * resolve any: such rows genuinely cannot be recognised, and guessing at a * column called `id` would be inventing an identity for a table that has * none. */ private rowAddress; /** * Merge incoming rows with cached data, preserving cached references * for rows whose values haven't changed. This avoids unnecessary * React re-renders when the server refetches all rows but most * haven't actually changed. */ private mergeRows; listenCollection>(props: FetchCollectionProps, onUpdate: (rows: Record[]) => void, onError?: (error: Error) => void): () => void; listenOne>(props: FetchOneProps, onUpdate: (row: Record | null) => void, onError?: (error: Error) => void): () => void; /** * Send a `subscribe_collection` for an already-registered subscription and * arm its watchdog. * * Every path that registers a collection subscription goes through here, so * that a subscribe which never lands — a rejected send, or a server that * never answers — always ends up in `failCollectionSubscription` rather than * leaving the entry parked with `isInitialDataReceived === false` forever. */ private sendCollectionSubscribe; /** The `listenOne` counterpart of {@link sendCollectionSubscribe}. */ private sendEntitySubscribe; /** * Report a subscribe failure to every listener and drop the registration. * * Dropping it is the point: the callbacks stay live (their components are * still mounted and have been told), but the next `listenCollection` for * these params finds no entry and issues a fresh subscribe instead of * silently attaching to a dead one. */ private failCollectionSubscription; /** The `listenOne` counterpart of {@link failCollectionSubscription}. */ private failEntitySubscription; /** * Stop the watchdogs without failing anything — used when the socket drops, * since the reconnect path re-subscribes everything anyway and a watchdog * firing mid-reconnect would tear down healthy subscriptions. */ private suspendSubscribeWatchdogs; /** * Arm watchdogs for subscribes that were requested while offline and have * just been flushed to the socket. Their timers were deliberately not set at * request time, so without this they would have no timeout at all. */ private armPendingSubscribeWatchdogs; private sendCollectionSubscribeWatchdog; private sendEntitySubscribeWatchdog; /** * Fail every subscription that never received data. Called when reconnection * is given up on, so views surface an error instead of spinning forever. */ private failAllPendingSubscriptions; /** * Re-send all active subscriptions to the backend after a reconnect. * The server wipes subscription state when a client disconnects, so * we need to re-register everything to resume receiving updates. */ private resubscribeAll; private createCollectionSubscriptionKey; private createSingleSubscriptionKey; }