@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.
@@ -30,6 +30,14 @@ export declare function isNetworkError(error: unknown): boolean;
30
30
  * server's own message says, and exactly what this SDK used not to do.
31
31
  */
32
32
  export declare function isIdempotencyInProgressError(error: unknown): boolean;
33
+ /**
34
+ * Is this key held for a different request than the one just sent with it?
35
+ *
36
+ * For a create the queue already sent once, that is an answer rather than a
37
+ * failure: the key is live only because the first attempt got through and
38
+ * committed, and only its response went missing.
39
+ */
40
+ export declare function isIdempotencyKeyReusedError(error: unknown): boolean;
33
41
  /** Is this failure worth another attempt later? */
34
42
  export declare function isRetryableError(error: unknown): boolean;
35
43
  /**
@@ -82,6 +90,12 @@ export declare class ConnectivityMonitor {
82
90
  private readonly clearTimer;
83
91
  /** Called when the backoff window expires, to drive an automatic retry. */
84
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;
85
99
  private readonly handleOnline;
86
100
  private readonly handleOffline;
87
101
  constructor(options?: ConnectivityOptions);
@@ -15,7 +15,11 @@ export { DEFAULT_LIST_LIMIT as DEFAULT_PAGE_SIZE } from "@rebasepro/types";
15
15
  * which is how NULL propagates through a comparison.
16
16
  */
17
17
  export declare function compareValues(a: unknown, b: unknown): number | undefined;
18
- /** Equality with the wire's type erasure allowed for, but never across NULL. */
18
+ /**
19
+ * Equality with a string filter value allowed to name a number, a boolean or
20
+ * an instant, but never across NULL. Two strings are equal only when they are
21
+ * the same string, as text is in Postgres.
22
+ */
19
23
  export declare function looseEquals(a: unknown, b: unknown): boolean;
20
24
  /** Evaluate one canonical operator against one row value. */
21
25
  export declare function matchesOperator(rowValue: unknown, op: WhereFilterOp, filterValue: unknown): boolean;
@@ -72,14 +76,14 @@ export declare function isExactlyEvaluable(params?: FindParams): boolean;
72
76
  *
73
77
  * Asked of the rows rather than of the query, because unlike a filter this one
74
78
  * *is* decidable from the data in hand: {@link compareValues} reaches the
75
- * collator only when it cannot read both operands as numbers, and `toComparable`
76
- * has already turned dates and relations into numbers and ids by then. If every
79
+ * collator only when neither operand is a number, and `toComparable` has
80
+ * already turned dates and relations into numbers and ids by then. If every
77
81
  * value on the sort column normalises to a number, the collator is unreachable
78
82
  * and the local order is the server's order.
79
83
  *
80
84
  * A text column is therefore refused — see {@link isExactlyEvaluable} for why
81
- * the two cannot be made to agree — and so is a column this page happens to see
82
- * only as strings, which is the same thing from here.
85
+ * the two cannot be made to agree — including one holding only digits, which
86
+ * Postgres orders as text ("10", "100", "9").
83
87
  *
84
88
  * Nulls are fine either way: they are ordered by an explicit rule (last
85
89
  * ascending, first descending) that matches Postgres and never reaches the
@@ -67,7 +67,37 @@ 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;
79
+ /**
80
+ * The request as it was already sent once, for a write tried online
81
+ * that failed on the network before it was queued. Nothing says whether
82
+ * it reached the server — only the answer may have been lost — so replay
83
+ * keeps its key. For a create, sent with the rows as queued (their minted
84
+ * ids, which later offline writes may reference), the server either takes
85
+ * them, or, having committed the first attempt, refuses the key as held
86
+ * for a different request, and this request is sent again for its stored
87
+ * answer. Replaying under a fresh key wrote the rows a second time.
88
+ *
89
+ * An `update` or `delete` carries only the key: its queued request is the
90
+ * one it sent, since nothing is merged into an op that has `sent`.
91
+ */
92
+ sent?: {
93
+ idempotencyKey: string;
94
+ /** `create`: the row it was called with; `createMany`: the rows. */
95
+ data?: Record<string, unknown> | Record<string, unknown>[];
96
+ /** `create`: the `id` argument it was called with. */
97
+ id?: string | number;
98
+ /** `createMany`: the conflict target the batch was sent with. */
99
+ onConflict?: readonly string[];
100
+ };
71
101
  queuedAt: number;
72
102
  /** How many times replay has been attempted (diagnostics for a stuck queue). */
73
103
  attempts?: number;
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
  *
@@ -221,6 +296,19 @@ export declare class OfflineManager {
221
296
  * query the server has never answered here.
222
297
  */
223
298
  private answer;
299
+ /**
300
+ * Answer a projected query (`fields`, `distinct`) from the server's own
301
+ * answer to it.
302
+ *
303
+ * The server's rows are the answer. The local database contributes only
304
+ * what it knows better: a row with unsynced writes shows them, narrowed to
305
+ * the columns the server sent, or drops out if it was deleted here or
306
+ * edited out of a filter this side can evaluate; and rows created here
307
+ * join the first page when there is somewhere to put them. A distinct
308
+ * query, or an order on a column the projection does not carry, leaves
309
+ * them out, and the result says it is partial.
310
+ */
311
+ private projectedAnswer;
224
312
  private localFind;
225
313
  /**
226
314
  * A temporary id of the type this collection's ids actually have.
@@ -256,6 +344,12 @@ export declare class OfflineManager {
256
344
  * Rows that came back unchanged keep their identity and revision, so a
257
345
  * refetch that changed nothing does not re-render every live query that
258
346
  * touches them — or rewrite them all to disk.
347
+ *
348
+ * A `projection` (see {@link isProjection}) only refreshes the columns it
349
+ * carries on rows already held, and never creates one. It is not a row:
350
+ * stored as one, it replaced the full row every other query reads and
351
+ * wrote the narrowed copy to disk, so a list lost its columns because a
352
+ * dropdown elsewhere asked for titles.
259
353
  */
260
354
  private ingest;
261
355
  /**
@@ -292,6 +386,17 @@ export declare class OfflineManager {
292
386
  }>;
293
387
  private flush;
294
388
  private replay;
389
+ /**
390
+ * Send a queued write under its key.
391
+ *
392
+ * A write the queue sent once already, before it was queued, carries that
393
+ * attempt's key and is sent under it again, as queued. If the server holds
394
+ * the key for a different request, the difference is the ids minted here
395
+ * — so the first attempt committed and only its answer was lost, and
396
+ * sending that request again returns the stored answer instead of a
397
+ * second copy of the rows.
398
+ */
399
+ private sendQueuedWrite;
295
400
  /**
296
401
  * Take the server's version of a row the client created offline.
297
402
  *
@@ -323,6 +428,15 @@ export declare class OfflineManager {
323
428
  * silently lose writes the server would have accepted.
324
429
  */
325
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;
326
440
  /** Every row id a mutation writes to. */
327
441
  private idsOf;
328
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;
@@ -133,6 +138,27 @@ export declare class RebaseRealtimeChannel {
133
138
  */
134
139
  private pendingLive;
135
140
  private catchUpInFlight;
141
+ /**
142
+ * Whether {@link lastSeq} is a position this client reached, rather than
143
+ * the zero of a handle that has not caught up yet.
144
+ *
145
+ * A client joining for the first time asks for whatever is retained, so
146
+ * history pruned before it ever joined is not something it missed. Only a
147
+ * client with a position can have a hole in what it saw.
148
+ */
149
+ private positioned;
150
+ /** The page size the catch-up in flight asked for, reused for its later pages. */
151
+ private catchUpLimit;
152
+ /**
153
+ * `latestSeq` of an empty page that left this client behind, while a
154
+ * second request confirms it.
155
+ *
156
+ * The server reads the retained messages and then the cursor, so a message
157
+ * committed between the two shows up in `latestSeq` and not in the page.
158
+ * The second request returns it if it exists; only a second empty page
159
+ * means the messages up to this seq are gone.
160
+ */
161
+ private emptyTailSeq;
136
162
  /**
137
163
  * Deadline for a catch-up response.
138
164
  *
@@ -234,6 +260,12 @@ export declare class RebaseRealtimeChannel {
234
260
  * retention cannot be served; `CHANNEL_BUS_PAYLOAD_TOO_LARGE` when a
235
261
  * broadcast on an ephemeral channel is too big to cross the bus.
236
262
  *
263
+ * `CHANNEL_HISTORY_GAP` is raised by the client: a catch-up found that the
264
+ * server no longer retains messages this client never received, so its
265
+ * state has a hole a replay cannot fill. `details` is `{ from, to }`, the
266
+ * inclusive range of sequence numbers missed. Resync the channel's state
267
+ * from its source of truth.
268
+ *
237
269
  * These used to be dropped on the floor — there is no promise to reject on
238
270
  * a fire-and-forget frame, so a forbidden broadcast looked exactly like a
239
271
  * delivered one. With no handler attached they are logged as a warning,
@@ -271,6 +303,9 @@ export declare class RebaseRealtimeChannel {
271
303
  private stopHeartbeat;
272
304
  /** Fold an incoming frame into the roster and fan it out. */
273
305
  private handle;
306
+ /** Tell the app that retention dropped messages it never received. */
307
+ private reportHistoryGap;
308
+ private emitError;
274
309
  /** Deliver everything held back during a catch-up, in sequence order. */
275
310
  private flushPendingLive;
276
311
  private deliver;
@@ -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,8 +137,25 @@ 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>>;
142
+ /**
143
+ * Refuse a filter whose *value* is missing.
144
+ *
145
+ * `where: { status: ["==", undefined] }` used to serialize to the literal
146
+ * string, so `status=eq.undefined` went out on the wire and the server dutifully
147
+ * looked for rows whose status is the four-letter word "undefined". The caller
148
+ * saw an empty page, not an error — the classic shape of a variable that was
149
+ * never set.
150
+ *
151
+ * Dropping the condition instead would be worse than sending it: the query
152
+ * would come back *unfiltered*, which for an ownership or tenant filter means
153
+ * returning rows the caller never asked to see. So this is a hard error, and
154
+ * both correct spellings are named in the message: omit the key to skip the
155
+ * filter, or use `["is-null", null]` to match SQL NULL (which still
156
+ * serializes — `null` is a value, `undefined` is the absence of one).
157
+ */
158
+ export declare function assertNoUndefinedFilterValues(where: Record<string, unknown>): void;
129
159
  export declare function buildQueryString(params?: FindParams): string;
130
160
  /**
131
161
  * The query string for `GET /<collection>/aggregate`.