@rebasepro/client 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- import { DeleteProps, CollectionConfig, FetchCollectionProps, ListenCollectionProps, FetchOneProps, SaveProps, CollectionUpdateMeta, ChannelMessage, TableMetadata, BranchInfo } from "@rebasepro/types";
1
+ import { DeleteProps, CollectionConfig, FetchCollectionProps, ListenCollectionProps, FetchOneProps, SaveProps, CollectionUpdateMeta, ChannelMessage, TableMetadata, BranchInfo, RebaseApiError, type RealtimeConnectionState } from "@rebasepro/types";
2
+ import type { SqlScriptResult } from "@rebasepro/types";
2
3
  export interface RebaseWebSocketConfig {
3
4
  websocketUrl: string;
4
5
  /** Optional auth token getter for WebSocket authentication */
@@ -7,6 +8,11 @@ export interface RebaseWebSocketConfig {
7
8
  WebSocket?: typeof WebSocket;
8
9
  /** Callback to handle unauthorized requests or token expiration (refreshes auth session) */
9
10
  onUnauthorized?: () => Promise<boolean>;
11
+ /**
12
+ * Sign in as this user rather than as the token's own — the socket half of
13
+ * `RebaseClientConfig.impersonate`. Sent with every `AUTHENTICATE`.
14
+ */
15
+ impersonate?: string;
10
16
  }
11
17
  export declare class RebaseWebSocketClient {
12
18
  private websocketUrl;
@@ -18,16 +24,41 @@ export declare class RebaseWebSocketClient {
18
24
  /** Set by `close()`. Blocks any later operation from silently redialling. */
19
25
  private closedByCaller;
20
26
  /**
21
- * Set when the backoff budget ran out, cleared by anything that earns a
22
- * fresh one.
27
+ * Set when the backoff budget ran out with nothing registered, cleared by
28
+ * anything that earns a fresh one.
23
29
  *
24
- * Unlike {@link closedByCaller} this is not final — nobody *asked* for the
25
- * socket to stay down. Five attempts with exponential backoff is about a
26
- * minute, which a laptop lid, a wifi handover or a backend rollout all
27
- * exceed routinely; treating that as permanent meant realtime silently
28
- * stopped for the rest of the page's life, with a reload the only cure.
30
+ * Only reachable with nothing registered: while a subscription or a joined
31
+ * channel exists the client never stops retrying (see `attemptReconnect`).
32
+ * And it is not final like {@link closedByCaller} — nobody *asked* for the
33
+ * socket to stay down, so the next subscribe dials again.
29
34
  */
30
35
  private gaveUp;
36
+ /** See {@link state}. */
37
+ private connectionState;
38
+ private stateListeners;
39
+ /**
40
+ * Whether the current outage has been reported to the registrations as
41
+ * `CONNECTION_LOST`. Once per outage: cleared when a socket opens.
42
+ */
43
+ private outageReported;
44
+ /**
45
+ * Where the connection is.
46
+ *
47
+ * - `idle` — no socket, and none wanted yet (the connection is lazy), or
48
+ * the last one was dropped by a sign-out.
49
+ * - `connecting` — dialling, with no outage in progress.
50
+ * - `connected` — the socket is open.
51
+ * - `reconnecting` — the socket dropped and the client is redialling. A
52
+ * blip, so far: nothing has been reported.
53
+ * - `disconnected` — the outage has outlasted the blip budget. Every
54
+ * subscription and joined channel has been told `CONNECTION_LOST` once;
55
+ * the client keeps redialling (every 30 s at most) while any exists.
56
+ * - `closed` — `client.close()`. Final.
57
+ */
58
+ get state(): RealtimeConnectionState;
59
+ /** Called on every change of {@link state}. Returns the unsubscribe. */
60
+ onStateChange(listener: (state: RealtimeConnectionState) => void): () => void;
61
+ private setState;
31
62
  /**
32
63
  * Whether a socket exists at all (open or still opening).
33
64
  *
@@ -41,6 +72,13 @@ export declare class RebaseWebSocketClient {
41
72
  onChannelMessage(channel: string, handler: (message: ChannelMessage) => void): () => void;
42
73
  /** Notified after the socket comes back, so channels can re-join. */
43
74
  onReconnect(handler: () => void): () => void;
75
+ /**
76
+ * Notified once per outage that outlasted a blip, with the
77
+ * `CONNECTION_LOST` error — so a joined channel can tell its `onError`
78
+ * handlers, as subscriptions are told through theirs.
79
+ */
80
+ onConnectionLost(handler: (error: RebaseApiError) => void): () => void;
81
+ private connectionLostHandlers;
44
82
  on(event: "connect" | "disconnect" | "reconnect" | "error", cb: (...args: unknown[]) => void): () => boolean;
45
83
  private emit;
46
84
  private collectionSubscriptions;
@@ -49,7 +87,29 @@ export declare class RebaseWebSocketClient {
49
87
  private backendToEntityKey;
50
88
  private pendingRequests;
51
89
  private reconnectAttempts;
90
+ /**
91
+ * With nothing registered, how many failed redials before the client stops
92
+ * trying until something asks for the connection. With a registration it
93
+ * never stops.
94
+ */
52
95
  private maxReconnectAttempts;
96
+ /**
97
+ * How many failed redials make a blip an outage: the point at which every
98
+ * registration is told `CONNECTION_LOST` and {@link state} reads
99
+ * `disconnected`. Three is 2 + 4 + 8 s of backoff, about 14 s down.
100
+ */
101
+ private lostAfterAttempts;
102
+ /**
103
+ * Whether a socket of this client has ever opened — what makes the next
104
+ * open a *reconnect*, whose server has forgotten every subscription and
105
+ * channel membership and must be told them again.
106
+ *
107
+ * Not `reconnectAttempts > 0`: that counter is a backoff budget, and a
108
+ * fresh one (after giving up, or after a sign-out dropped the socket)
109
+ * starts at zero, so the socket that finally came back looked like a first
110
+ * connect and nothing was re-sent.
111
+ */
112
+ private hadConnection;
53
113
  private isConnected;
54
114
  private messageQueue;
55
115
  private requestTimeoutMs;
@@ -60,6 +120,7 @@ export declare class RebaseWebSocketClient {
60
120
  private WebSocketConstructor;
61
121
  onUnauthorized?: () => Promise<boolean>;
62
122
  private refreshInProgress;
123
+ private readonly impersonate?;
63
124
  constructor(config: RebaseWebSocketConfig);
64
125
  /**
65
126
  * Open the socket if it is not open (or opening) already.
@@ -70,12 +131,20 @@ export declare class RebaseWebSocketClient {
70
131
  */
71
132
  ensureConnected(): void;
72
133
  /**
73
- * The browser says the network is back — the usual reason the budget ran
74
- * out in the first place. Registered lazily so a Node client, or a page
75
- * that never subscribes, adds no listener.
134
+ * Dial now rather than at the next backoff step — the browser says the
135
+ * network is back, or the tab is in front of the user again. Both are the
136
+ * usual end of an outage; neither fires for a server restart, which the
137
+ * backoff covers.
138
+ */
139
+ private retryNow;
140
+ /**
141
+ * `online` and `visibilitychange`. Registered lazily so a Node client, or a
142
+ * page that never subscribes, adds no listener.
76
143
  */
77
- private installOnlineListener;
144
+ private installNetworkListeners;
145
+ private removeNetworkListeners;
78
146
  private onlineListener;
147
+ private visibilityListener;
79
148
  /**
80
149
  * Authenticate the WebSocket connection
81
150
  */
@@ -95,7 +164,43 @@ export declare class RebaseWebSocketClient {
95
164
  disconnect(permanent?: boolean): void;
96
165
  private initWebSocket;
97
166
  private processMessageQueue;
167
+ /**
168
+ * Whether a frame that waited in the queue should go out on the socket
169
+ * that just opened. Only a subscribe can be refused, and one is exactly
170
+ * when sending it would give the server a subscription this side will not
171
+ * read:
172
+ *
173
+ * - Its registration is gone, or has been given a new id, while it waited.
174
+ * A listener that unmounted before the socket opened (a StrictMode double
175
+ * mount does exactly this) sends no `unsubscribe`, because there was no
176
+ * socket to send it on, so the server would keep that one forever.
177
+ * - This open is a reconnect. `resubscribeAll` sends every registration
178
+ * under a fresh id right after the queue drains, so the queued copy would
179
+ * be the second of two. It is handed back to that pass instead.
180
+ */
181
+ private shouldSendQueued;
98
182
  private attemptReconnect;
183
+ /**
184
+ * Whether anything depends on the socket coming back: a subscription, a
185
+ * joined channel, or a request whose caller is still waiting for a socket.
186
+ */
187
+ private hasRegistrations;
188
+ /**
189
+ * Tell every subscription and joined channel, once per outage, that the
190
+ * connection is down and their data is not updating.
191
+ *
192
+ * Registrations are kept — that is the difference from a subscribe that
193
+ * failed. When the socket is back they are re-subscribed, and the fresh
194
+ * `onUpdate` is the recovery.
195
+ */
196
+ private reportConnectionLost;
197
+ private notifyError;
198
+ /**
199
+ * A listener attaching while an outage is being reported is told at once:
200
+ * its subscribe waits for a socket that may be minutes away, and nothing
201
+ * else would say so.
202
+ */
203
+ private reportOutageTo;
99
204
  private isAuthError;
100
205
  private handleAuthFailure;
101
206
  /**
@@ -112,6 +217,15 @@ export declare class RebaseWebSocketClient {
112
217
  * Not part of the stable surface — prefer `client.realtime.channel(name)`.
113
218
  */
114
219
  sendMessage(message: Record<string, unknown>): Promise<unknown>;
220
+ /**
221
+ * Frames whose caller has already been answered — timed out, or rejected
222
+ * by a close. One can still be on its way to the socket (awaiting
223
+ * authentication), and must not be written after its caller was told it
224
+ * failed.
225
+ */
226
+ private abandonedFrames;
227
+ /** Whether the server answers this frame with a response envelope. */
228
+ private expectsResponse;
115
229
  private doSendMessage;
116
230
  fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
117
231
  fetchOne<M extends Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;
@@ -121,6 +235,16 @@ export declare class RebaseWebSocketClient {
121
235
  database?: string;
122
236
  role?: string;
123
237
  }): Promise<Record<string, unknown>[]>;
238
+ /**
239
+ * Run a script a person wrote — the Studio console — and get back what its
240
+ * last statement returned, every value as the database's text, with the
241
+ * table column each result column was read from. See
242
+ * `SQLAdmin.runSqlScript`.
243
+ */
244
+ runSqlScript(sql: string, options?: {
245
+ database?: string;
246
+ role?: string;
247
+ }): Promise<SqlScriptResult>;
124
248
  fetchAvailableDatabases(): Promise<string[]>;
125
249
  fetchAvailableRoles(): Promise<string[]>;
126
250
  fetchApplicationRoles(): Promise<string[]>;
@@ -191,6 +315,14 @@ export declare class RebaseWebSocketClient {
191
315
  * firing mid-reconnect would tear down healthy subscriptions.
192
316
  */
193
317
  private suspendSubscribeWatchdogs;
318
+ /**
319
+ * Drop every registration's cached rows, keeping the registrations.
320
+ *
321
+ * A registration without rows counts as not loaded, so the next listener
322
+ * to attach asks the server (or waits for the resubscribe) instead of
323
+ * being handed what is cached.
324
+ */
325
+ private forgetCachedData;
194
326
  /**
195
327
  * Arm watchdogs for subscribes that were requested while offline and have
196
328
  * just been flushed to the socket. Their timers were deliberately not set at
@@ -199,11 +331,6 @@ export declare class RebaseWebSocketClient {
199
331
  private armPendingSubscribeWatchdogs;
200
332
  private sendCollectionSubscribeWatchdog;
201
333
  private sendEntitySubscribeWatchdog;
202
- /**
203
- * Fail every subscription that never received data. Called when reconnection
204
- * is given up on, so views surface an error instead of spinning forever.
205
- */
206
- private failAllPendingSubscriptions;
207
334
  /**
208
335
  * Re-send all active subscriptions to the backend after a reconnect.
209
336
  * The server wipes subscription state when a client disconnects, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/client",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "HTTP SDK client for the Rebase custom backend",
5
5
  "keywords": [
6
6
  "rebase",
@@ -41,9 +41,9 @@
41
41
  "./package.json": "./package.json"
42
42
  },
43
43
  "dependencies": {
44
- "@rebasepro/common": "0.22.0",
45
- "@rebasepro/types": "0.22.0",
46
- "@rebasepro/utils": "0.22.0"
44
+ "@rebasepro/common": "0.24.0",
45
+ "@rebasepro/utils": "0.24.0",
46
+ "@rebasepro/types": "0.24.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@jest/globals": "^30.4.1",