@rebasepro/client 0.23.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.
@@ -90,6 +90,12 @@ export declare class ConnectivityMonitor {
90
90
  private readonly clearTimer;
91
91
  /** Called when the backoff window expires, to drive an automatic retry. */
92
92
  onRetryDue?: () => void;
93
+ /**
94
+ * Called when the browser says the connection is back. Its own hook rather
95
+ * than {@link onRetryDue}: a client with automatic retries off sets no retry
96
+ * callback, and the `online` event has to wake it all the same.
97
+ */
98
+ onOnline?: () => void;
93
99
  private readonly handleOnline;
94
100
  private readonly handleOffline;
95
101
  constructor(options?: ConnectivityOptions);
@@ -67,6 +67,14 @@ export interface PendingMutation {
67
67
  generatedId?: boolean;
68
68
  /** The payload: a row for create/update, an array of rows for createMany. */
69
69
  data?: Record<string, unknown> | Record<string, unknown>[];
70
+ /**
71
+ * For an `update` that consecutive edits were coalesced into: each edit,
72
+ * in the order the app made them. `data` is their merge, and is what is
73
+ * sent. Kept so a refusal of the merge over one field can be told apart
74
+ * from a refusal of the rest: the merge is split back into these and each
75
+ * replayed on its own, so only the edit the server refuses is undone.
76
+ */
77
+ parts?: Record<string, unknown>[];
70
78
  upsert?: boolean;
71
79
  /**
72
80
  * The request as it was already sent once, for a write tried online
package/dist/offline.d.ts CHANGED
@@ -96,6 +96,24 @@ export interface OfflineStatus {
96
96
  lastError?: string;
97
97
  }
98
98
  export type { LiveResult, ObserveOptions, RowSnapshotMeta } from "./collection.js";
99
+ /** Which queue {@link OfflineApi.pending} and {@link OfflineApi.clear} act on. */
100
+ export interface OfflineQueueOptions {
101
+ /**
102
+ * The writes queued while nobody was signed in, instead of the current
103
+ * user's.
104
+ *
105
+ * While someone is signed in these are held back: they were not made by
106
+ * that user, so they are never replayed under their credentials, and never
107
+ * moved into their queue. `pending({ orphaned: true })` lists them so the
108
+ * app can decide — re-issue them as the signed-in user, or discard them
109
+ * with `clear({ orphaned: true })`. Left alone, they replay the next time
110
+ * nobody is signed in.
111
+ *
112
+ * With nobody signed in they are the current queue, and are not orphaned:
113
+ * `pending({ orphaned: true })` lists nothing.
114
+ */
115
+ orphaned?: boolean;
116
+ }
99
117
  /** What `client.offline` exposes to the app. */
100
118
  export interface OfflineApi {
101
119
  /** Replay the queue now. Resolves with what was flushed and what remains. */
@@ -103,8 +121,12 @@ export interface OfflineApi {
103
121
  flushed: number;
104
122
  remaining: number;
105
123
  }>;
106
- /** The queued mutations for the current user, oldest first. */
107
- pending(): Promise<PendingMutation[]>;
124
+ /**
125
+ * The queued mutations for the current user, oldest first — or, with
126
+ * `{ orphaned: true }`, the ones queued while nobody was signed in (see
127
+ * {@link OfflineQueueOptions.orphaned}).
128
+ */
129
+ pending(options?: OfflineQueueOptions): Promise<PendingMutation[]>;
108
130
  /** The current engine state — connectivity, queue depth, last sync. */
109
131
  status(): OfflineStatus;
110
132
  /** Subscribe to {@link OfflineStatus} changes (for a sync indicator). */
@@ -114,8 +136,12 @@ export interface OfflineApi {
114
136
  * Destructive: queued writes are lost, not replayed. For "discard my
115
137
  * offline changes" flows, not for sign-out (scoping already isolates
116
138
  * users).
139
+ *
140
+ * With `{ orphaned: true }`, drop the writes queued while nobody was
141
+ * signed in, and the local rows they were applied to, instead — the
142
+ * current user's are left alone.
117
143
  */
118
- clear(): Promise<void>;
144
+ clear(options?: OfflineQueueOptions): Promise<void>;
119
145
  /** Subscribe to queue-size changes (for a "pending changes" badge). */
120
146
  onQueueChange(listener: (count: number) => void): () => void;
121
147
  }
@@ -140,6 +166,26 @@ export declare class OfflineManager {
140
166
  private readonly inners;
141
167
  private readonly connectivity;
142
168
  private scope;
169
+ /**
170
+ * Set while the manager does not know whose data it holds: from
171
+ * {@link holdScope} until the next {@link setScope}. `scope` is not
172
+ * consulted meanwhile — no operation gets a {@link ScopeTicket}, and
173
+ * nothing reads or writes the store.
174
+ */
175
+ private scopeHold?;
176
+ /**
177
+ * Bumped by every change of user, and by `clear()`.
178
+ *
179
+ * The load paths always re-checked the scope after their awaits; the write
180
+ * paths did not. A `find` sent for user A that answered after B had signed
181
+ * in wrote A's RLS-filtered rows into B's local database, under B's keys,
182
+ * and showed them to B's observers; a replay acknowledged after the switch
183
+ * dequeued a key under B's scope that did not exist, so A's write stayed
184
+ * queued and was sent again when A came back. Every write now carries the
185
+ * {@link ScopeTicket} its operation started under, and one that is stale
186
+ * touches nothing that belongs to the new user.
187
+ */
188
+ private scopeEpoch;
143
189
  /** The local database: normalized rows and query snapshots per collection. */
144
190
  private collections;
145
191
  /** In-memory mirror of the current scope's queue, in replay order. */
@@ -185,6 +231,35 @@ export declare class OfflineManager {
185
231
  * across a sign-out/sign-in on a shared browser.
186
232
  */
187
233
  setScope(uid: string | undefined): void;
234
+ /**
235
+ * Forget whose data this is until the next {@link setScope}.
236
+ *
237
+ * For the time before the client knows who is signed in — a session being
238
+ * restored on load. Taking that time for "signed out" would put the user's
239
+ * own writes in the signed-out queue, which no sign-in replays, and show
240
+ * them the signed-out local database instead of theirs.
241
+ *
242
+ * Meanwhile the manager has no scope at all. Every operation — a read, a
243
+ * write, a replay, `pending()`, `clear()` — waits for one and then runs
244
+ * for whoever it turns out to be, and nothing reads or writes the store.
245
+ * A realtime frame arriving in between is handed on as the server sent it
246
+ * and kept nowhere.
247
+ */
248
+ holdScope(): void;
249
+ /** Let go of the current user's rows and queue: in memory, and on screen. */
250
+ private leaveScope;
251
+ /** Settles once the manager knows whose data it holds — see {@link holdScope}. */
252
+ private scopeKnown;
253
+ /**
254
+ * The user an operation is running for — see {@link scopeEpoch} — or
255
+ * nothing while that is not known ({@link holdScope}). An operation takes
256
+ * it as `this.ticket() ?? await this.nextTicket()`, before its first await.
257
+ */
258
+ private ticket;
259
+ /** The ticket of whoever the held scope turns out to be. */
260
+ private nextTicket;
261
+ /** Whether an operation's user is still the signed-in one. */
262
+ private isCurrent;
188
263
  /**
189
264
  * Throw away every local row, for a scope change or an explicit clear.
190
265
  *
@@ -353,6 +428,15 @@ export declare class OfflineManager {
353
428
  * silently lose writes the server would have accepted.
354
429
  */
355
430
  private rejectMutation;
431
+ /**
432
+ * Put the separate edits a coalesced `update` was made of back in the
433
+ * queue, in its place and in their order, each with the row as it stood
434
+ * before it as its rollback — so a refusal undoes that edit alone.
435
+ *
436
+ * Their ids extend the merged op's, which sorts them after it and before
437
+ * whatever was queued next, so the queue's order survives a reload.
438
+ */
439
+ private splitCoalesced;
356
440
  /** Every row id a mutation writes to. */
357
441
  private idsOf;
358
442
  private drop;
@@ -67,11 +67,11 @@ export declare const orderByColumn: FindParams<ContractRow>;
67
67
  * turning every typo into an unsorted 200 in production.
68
68
  */
69
69
  export declare const orderByTypo: FindParams<ContractRow>;
70
- export declare const fluentScore: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
71
- export declare const fluentColumn: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
72
- export declare const fluentTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
70
+ export declare const fluentScore: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
71
+ export declare const fluentColumn: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
72
+ export declare const fluentTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
73
73
  /** Vector search must be reachable from the builder, and chain. */
74
- export declare const fluentVector: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
74
+ export declare const fluentVector: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
75
75
  /**
76
76
  * `array-contains` takes an **element** of the column, not the column.
77
77
  *
@@ -82,38 +82,38 @@ export declare const fluentVector: (qb: SDKQueryBuilderInterface<ContractRow>) =
82
82
  * did compile, `["featured"]`, builds `@> ARRAY[$1]` with the whole array bound
83
83
  * as the single element and matches nothing, forever, with no error anywhere.
84
84
  */
85
- export declare const fluentArrayContains: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
86
- export declare const fluentArrayContainsWrapped: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
85
+ export declare const fluentArrayContains: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
86
+ export declare const fluentArrayContainsWrapped: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
87
87
  /**
88
88
  * Same defect, and the case the relation compiler was specifically built for:
89
89
  * a to-many relation is emitted as `Array<TargetRow>`, and the compiler answers
90
90
  * `array-contains` on it by comparing **ids**. So the id must be accepted even
91
91
  * though it is not the element type.
92
92
  */
93
- export declare const fluentRelationContains: (qb: SDKQueryBuilderInterface<PostRow>, tagId: string) => SDKQueryBuilderInterface<PostRow>;
94
- export declare const fluentRelationIn: (qb: SDKQueryBuilderInterface<PostRow>, tagIds: string[]) => SDKQueryBuilderInterface<PostRow>;
95
- export declare const fluentRelationTypo: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow>;
93
+ export declare const fluentRelationContains: (qb: SDKQueryBuilderInterface<PostRow>, tagId: string) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
94
+ export declare const fluentRelationIn: (qb: SDKQueryBuilderInterface<PostRow>, tagIds: string[]) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
95
+ export declare const fluentRelationTypo: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
96
96
  /** The list operators take a list of elements — or one, read as a one-element list. */
97
- export declare const fluentInList: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
98
- export declare const fluentInScalar: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
99
- export declare const fluentInNested: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
97
+ export declare const fluentInList: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
98
+ export declare const fluentInScalar: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
99
+ export declare const fluentInNested: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
100
100
  /** A comparison takes one value. `eq(column, ["a","b"])` is not a query anyone meant. */
101
- export declare const fluentEqArray: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
101
+ export declare const fluentEqArray: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
102
102
  /**
103
103
  * A pattern is a string on every column type. The driver casts, so refusing
104
104
  * `"%3%"` on a numeric column was the type being stricter than the runtime.
105
105
  */
106
- export declare const fluentLikeOnNumber: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
106
+ export declare const fluentLikeOnNumber: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
107
107
  /** The null operators ignore their value; `null` is the conventional spelling. */
108
- export declare const fluentIsNull: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
108
+ export declare const fluentIsNull: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
109
109
  /**
110
110
  * A caller holding an unnarrowed operator — a dynamic filter UI — must keep
111
111
  * compiling. `WhereValueFor` distributes over the operator, so this is the
112
112
  * union of every branch rather than a `never`.
113
113
  */
114
- export declare const fluentDynamicOp: (qb: SDKQueryBuilderInterface<ContractRow>, op: WhereFilterOp) => SDKQueryBuilderInterface<ContractRow>;
114
+ export declare const fluentDynamicOp: (qb: SDKQueryBuilderInterface<ContractRow>, op: WhereFilterOp) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
115
115
  /** Two conditions on one column: the shape the Mongo compiler used to drop. */
116
- export declare const fluentRange: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
116
+ export declare const fluentRange: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
117
117
  /** The object form must accept exactly what the fluent form accepts. */
118
118
  export declare const paramsArrayContains: FindParams<ContractRow>;
119
119
  /** …and the array-of-tuples form the builder produces from two `.where()` calls. */
@@ -150,11 +150,11 @@ export declare const rowStaysIndexable: (result: FindResult<ContractRow>) => Rec
150
150
  * which is to say: on every project that took the trouble to be typed.
151
151
  */
152
152
  /** 1. A relation path. `find({ where: { "author.name": … } })` always compiled. */
153
- export declare const fluentRelationPath: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow>;
153
+ export declare const fluentRelationPath: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
154
154
  /** 2. A JSON path into a `jsonb` column — `?metadata->>tier=eq.gold` on the wire. */
155
- export declare const fluentJsonPath: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
155
+ export declare const fluentJsonPath: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
156
156
  /** 3. An aggregate over a to-many relation, which `FindParams.orderBy` takes. */
157
- export declare const fluentRelationAggregateSort: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow>;
157
+ export declare const fluentRelationAggregateSort: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
158
158
  /** 4. Paging past the `limit` ceiling, which the client can do and the builder could not. */
159
159
  export declare const fluentIterate: (qb: SDKQueryBuilderInterface<ContractRow>) => AsyncIterableIterator<ContractRow>;
160
160
  export declare const fluentFindAll: (qb: SDKQueryBuilderInterface<ContractRow>) => Promise<ContractRow[]>;
@@ -164,9 +164,9 @@ export declare const fluentFindAll: (qb: SDKQueryBuilderInterface<ContractRow>)
164
164
  * `unknown` — turning every one of the assertions above into a silent pass.
165
165
  * These are the same refusals as before.
166
166
  */
167
- export declare const fluentPathTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
168
- export declare const fluentPathDoesNotLoosenValues: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow>;
169
- export declare const fluentAggregateSortTypo: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow>;
167
+ export declare const fluentPathTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
168
+ export declare const fluentPathDoesNotLoosenValues: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
169
+ export declare const fluentAggregateSortTypo: (qb: SDKQueryBuilderInterface<PostRow>) => SDKQueryBuilderInterface<PostRow, import("@rebasepro/types").IncludeSpec>;
170
170
  /** And the object form still takes all four, unchanged. */
171
171
  export declare const paramsRelationPath: FindParams<PostRow>;
172
172
  export declare const paramsJsonPath: FindParams<ContractRow>;
@@ -98,6 +98,11 @@ export interface ChannelTransport {
98
98
  sendMessage(message: Record<string, unknown>): Promise<unknown>;
99
99
  onChannelMessage(channel: string, handler: (message: ChannelMessage) => void): () => void;
100
100
  onReconnect(handler: () => void): () => void;
101
+ /**
102
+ * Notified once per outage that outlasted a blip. Optional so a test
103
+ * transport need not implement it.
104
+ */
105
+ onConnectionLost?(handler: (error: RebaseApiError) => void): () => void;
101
106
  }
102
107
  export declare class RebaseRealtimeChannel {
103
108
  readonly name: string;
@@ -1,4 +1,4 @@
1
- import { FindResult, LogicalCondition, PageWalkOptions, RelationAggregateSort, SDKCollectionClient, SDKQueryBuilderInterface, WhereFilterOp, WhereValueFor, type AggregateParams, type AggregateRow, type ComputedSortField, type FieldPath, type IncludeSpec, type NonColumnFieldPath, type NullsPlacement } from "@rebasepro/types";
1
+ import { FindResult, LogicalCondition, PageWalkOptions, RelationAggregateSort, SDKCollectionClient, SDKQueryBuilderInterface, WhereFilterOp, WhereValueFor, type AggregateParams, type AggregateResult, type ComputedSortField, type FieldPath, type IncludeSpec, type NonColumnFieldPath, type NullsPlacement } from "@rebasepro/types";
2
2
  /**
3
3
  * SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
4
4
  * Entity-wrapped results (`FindResponse<M>`).
@@ -173,7 +173,7 @@ export declare class SDKQueryBuilder<M extends Record<string, unknown> = Record<
173
173
  * // [{ status: "paid", sum_total: 41822.5 }, …]
174
174
  * ```
175
175
  */
176
- aggregate(params: Omit<AggregateParams<M>, "where" | "logical" | "searchString">): Promise<AggregateRow[]>;
176
+ aggregate(params: Omit<AggregateParams<M>, "where" | "logical" | "searchString">): Promise<AggregateResult>;
177
177
  /**
178
178
  * Page through everything this query matches, one row at a time.
179
179
  *
@@ -0,0 +1,10 @@
1
+ import type { SqlScriptResult } from "@rebasepro/types";
2
+ /**
3
+ * Read an `EXECUTE_SQL_SUCCESS` payload for a script run as a
4
+ * {@link SqlScriptResult}, checking every field rather than trusting the frame.
5
+ *
6
+ * A server from before scripts existed answers `{ result }` alone, with parsed
7
+ * values. Its rows are kept, as text, and no column is said to come from
8
+ * anywhere — the console edits no cell of a result it cannot trace.
9
+ */
10
+ export declare function readSqlScriptResult(payload: unknown): SqlScriptResult;
@@ -1,4 +1,4 @@
1
- import { AggregateParams, FindParams as TypesFindParams, FindResponse as TypesFindResponse } from "@rebasepro/types";
1
+ import { AggregateParams, FindParams as TypesFindParams, FindResponse as TypesFindResponse, type IncludeSpec } from "@rebasepro/types";
2
2
  export { RebaseApiError } from "@rebasepro/types";
3
3
  export type { RebaseErrorInit } from "@rebasepro/types";
4
4
  export interface RebaseClientConfig {
@@ -95,6 +95,19 @@ export interface RebaseClientConfig {
95
95
  * is not settable here; it always comes from the token.
96
96
  */
97
97
  headers?: Record<string, string>;
98
+ /**
99
+ * Run every request as this user, named by uid: sent as
100
+ * `x-rebase-impersonate` on each HTTP request, and with the realtime
101
+ * socket's sign-in.
102
+ *
103
+ * For checking what one user can see and do against the real row-level
104
+ * security, as Studio's API explorer and JS editor do. Only an
105
+ * administrator's own session may — the server refuses it for anyone
106
+ * else, and always for an API key or the service key. The data API,
107
+ * custom functions and realtime run as the named user; every other route
108
+ * refuses the request rather than answer as the administrator.
109
+ */
110
+ impersonate?: string;
98
111
  }
99
112
  /**
100
113
  * Facts about the surrounding client that the transport cannot read off its own
@@ -124,7 +137,7 @@ export declare const ANONYMOUS_SERVER_CLIENT_WARNING: string;
124
137
  * `orderBy` went back to accepting any column name — the alias, not the
125
138
  * definition, was where the typing was lost.
126
139
  */
127
- export type FindParams<M extends Record<string, unknown> = Record<string, unknown>> = TypesFindParams<M>;
140
+ export type FindParams<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> = TypesFindParams<M, Inc>;
128
141
  export type FindResponse<T> = TypesFindResponse<T extends Record<string, unknown> ? T : Record<string, unknown>>;
129
142
  /**
130
143
  * Refuse a filter whose *value* is missing.
@@ -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,18 @@ 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;
53
102
  /**
54
103
  * Whether a socket of this client has ever opened — what makes the next
55
104
  * open a *reconnect*, whose server has forgotten every subscription and
@@ -71,6 +120,7 @@ export declare class RebaseWebSocketClient {
71
120
  private WebSocketConstructor;
72
121
  onUnauthorized?: () => Promise<boolean>;
73
122
  private refreshInProgress;
123
+ private readonly impersonate?;
74
124
  constructor(config: RebaseWebSocketConfig);
75
125
  /**
76
126
  * Open the socket if it is not open (or opening) already.
@@ -81,12 +131,20 @@ export declare class RebaseWebSocketClient {
81
131
  */
82
132
  ensureConnected(): void;
83
133
  /**
84
- * The browser says the network is back — the usual reason the budget ran
85
- * out in the first place. Registered lazily so a Node client, or a page
86
- * 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.
87
143
  */
88
- private installOnlineListener;
144
+ private installNetworkListeners;
145
+ private removeNetworkListeners;
89
146
  private onlineListener;
147
+ private visibilityListener;
90
148
  /**
91
149
  * Authenticate the WebSocket connection
92
150
  */
@@ -122,6 +180,27 @@ export declare class RebaseWebSocketClient {
122
180
  */
123
181
  private shouldSendQueued;
124
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;
125
204
  private isAuthError;
126
205
  private handleAuthFailure;
127
206
  /**
@@ -138,6 +217,15 @@ export declare class RebaseWebSocketClient {
138
217
  * Not part of the stable surface — prefer `client.realtime.channel(name)`.
139
218
  */
140
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;
141
229
  private doSendMessage;
142
230
  fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
143
231
  fetchOne<M extends Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;
@@ -147,6 +235,16 @@ export declare class RebaseWebSocketClient {
147
235
  database?: string;
148
236
  role?: string;
149
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>;
150
248
  fetchAvailableDatabases(): Promise<string[]>;
151
249
  fetchAvailableRoles(): Promise<string[]>;
152
250
  fetchApplicationRoles(): Promise<string[]>;
@@ -233,11 +331,6 @@ export declare class RebaseWebSocketClient {
233
331
  private armPendingSubscribeWatchdogs;
234
332
  private sendCollectionSubscribeWatchdog;
235
333
  private sendEntitySubscribeWatchdog;
236
- /**
237
- * Fail every subscription that never received data. Called when reconnection
238
- * is given up on, so views surface an error instead of spinning forever.
239
- */
240
- private failAllPendingSubscriptions;
241
334
  /**
242
335
  * Re-send all active subscriptions to the backend after a reconnect.
243
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.23.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.23.0",
45
- "@rebasepro/utils": "0.23.0",
46
- "@rebasepro/types": "0.23.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",