@rebasepro/client 0.24.0 → 0.24.1-canary.gcb918fc

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.
package/dist/offline.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { RebaseApiError } from "./transport.js";
1
2
  import { SDKCollectionClient } from "@rebasepro/types";
2
3
  import { CollectionClient } from "./collection.js";
3
4
  import { OfflineStore, PendingMutation } from "./offline-store.js";
@@ -81,7 +82,76 @@ export interface OfflineConfig {
81
82
  * Network failures are not errors: those mutations stay queued.
82
83
  */
83
84
  onSyncError?: (error: Error, mutation: PendingMutation) => void;
85
+ /**
86
+ * Detect conflicts when a queued edit replays: an `update` made offline
87
+ * is sent with `If-Match`, the version of the row it was made against,
88
+ * and if someone changed the row on the server since, this decides what
89
+ * happens to the edit:
90
+ *
91
+ * - `"overwrite"`: send it again without the precondition. Last writer
92
+ * wins, as without this option.
93
+ * - `"discard"`: drop it. The row goes back to what it was before the
94
+ * edit, as for a rejection, but it is not reported to `onSyncError`
95
+ * (writes queued on top of it are, as `DEPENDENCY_REJECTED`), and the
96
+ * row is read again from the server.
97
+ *
98
+ * The edit's version is the one the row's last read reported: a
99
+ * `findById`, a `listenById` frame, or the answer to an earlier `update`.
100
+ * A row last seen in a `find()` or `listen()` list has no version, and an
101
+ * edit made on top of another queued write to the same row has none
102
+ * either; those replay without a precondition. A caller's own `ifMatch` is
103
+ * kept on a queued edit too.
104
+ *
105
+ * The queue waits while this runs, so a prompt can ask the user. Without
106
+ * it — the default — replay sends no precondition and the last writer
107
+ * wins, per field.
108
+ */
109
+ onConflict?: (error: RebaseApiError, mutation: PendingMutation) => ConflictResolution | Promise<ConflictResolution>;
110
+ /**
111
+ * Remember, on this device, the last user who signed in, and open their
112
+ * local database when a reload cannot reach the server to restore the
113
+ * session. Off by default.
114
+ *
115
+ * This matters in cookie mode, where nothing about the session is stored
116
+ * on the device: without it, an offline reload has no user, so it shows
117
+ * the signed-out local database and queues the user's writes as the
118
+ * signed-out user's. With it, the reload shows the last user's rows, and
119
+ * writes made meanwhile go into their queue. That queue is not replayed
120
+ * until the session is confirmed — by a refresh once the connection is
121
+ * back, or a sign-in. If the server then refuses the session, or someone
122
+ * else signs in, the device switches users and the queue waits, kept, for
123
+ * its author.
124
+ *
125
+ * The trade-off is the reason it is opt-in: on a shared device, the
126
+ * previous user's cached rows can be read offline without a session.
127
+ * Turn it on for installs that belong to one person. The id is forgotten
128
+ * at sign-out, and when the server refuses the session; it is never used
129
+ * when the server answered that there is no session.
130
+ *
131
+ * {@link OfflineStatus.unverifiedUser} is `true` while the device shows a
132
+ * user whose session it has not confirmed.
133
+ */
134
+ keepLastUser?: boolean;
135
+ /**
136
+ * How many queued creates replay as one request. Defaults to `1`: every
137
+ * queued write is its own request, in order. Raise it (up to 1 000) and a
138
+ * run of consecutive offline creates in one collection drains as one
139
+ * `createMany` — 500 rows typed offline go back in a handful of requests
140
+ * instead of 500 round trips.
141
+ *
142
+ * Only creates whose ids the SDK minted as strings are folded, and never
143
+ * one that references another row of the same run: those are the writes a
144
+ * lost answer cannot duplicate (the server refuses the id the second time,
145
+ * and the create is recognised as its own) and whose values need nothing
146
+ * from the server first. Anything else — and any run the server refuses or
147
+ * defers for any reason but a lost connection — replays one write at a
148
+ * time exactly as with the default, so each write still stands or falls
149
+ * on its own.
150
+ */
151
+ replayBatchSize?: number;
84
152
  }
153
+ /** What {@link OfflineConfig.onConflict} does with an edit the row has moved past. */
154
+ export type ConflictResolution = "overwrite" | "discard";
85
155
  /** A snapshot of the engine's state, for a status indicator. */
86
156
  export interface OfflineStatus {
87
157
  /** False once a request has failed to reach the server, until one does. */
@@ -94,8 +164,23 @@ export interface OfflineStatus {
94
164
  lastSyncedAt?: number;
95
165
  /** The last replay rejection, if any. */
96
166
  lastError?: string;
167
+ /**
168
+ * Whether the browser agreed to keep the offline database through storage
169
+ * pressure (`navigator.storage.persist()`), asked when the first write is
170
+ * queued. `false` means it may be evicted — on Safari, after seven days
171
+ * without a visit — and unsent writes with it; a UI can warn. Unset until
172
+ * asked, and where there is nothing to ask.
173
+ */
174
+ persisted?: boolean;
175
+ /**
176
+ * `true` while the local data shown is the last signed-in user's, opened
177
+ * without a confirmed session ({@link OfflineConfig.keepLastUser}): their
178
+ * writes queue, and none replays until the session is confirmed. Unset
179
+ * otherwise.
180
+ */
181
+ unverifiedUser?: boolean;
97
182
  }
98
- export type { LiveResult, ObserveOptions, RowSnapshotMeta } from "./collection.js";
183
+ export type { LiveResult, ObserveByIdOptions, ObserveOptions, RowSnapshotMeta } from "./collection.js";
99
184
  /** Which queue {@link OfflineApi.pending} and {@link OfflineApi.clear} act on. */
100
185
  export interface OfflineQueueOptions {
101
186
  /**
@@ -145,15 +230,7 @@ export interface OfflineApi {
145
230
  /** Subscribe to queue-size changes (for a "pending changes" badge). */
146
231
  onQueueChange(listener: (count: number) => void): () => void;
147
232
  }
148
- /**
149
- * True when a read failed because there was neither network nor local data.
150
- *
151
- * The code is `OFFLINE`. It was `"offline"`, the one lowercase code in a
152
- * surface whose every other code is `SCREAMING_SNAKE_CASE` — including the
153
- * `NETWORK_ERROR` it sits next to — so `e.code === "OFFLINE"`, which is what
154
- * anyone writes, never matched.
155
- */
156
- export declare function isOfflineError(error: unknown): boolean;
233
+ export { isOfflineError } from "./offline-errors.js";
157
234
  type AnyRow = Record<string, unknown>;
158
235
  type InnerFactory = (slug: string) => SDKCollectionClient<AnyRow>;
159
236
  export declare class OfflineManager {
@@ -162,10 +239,20 @@ export declare class OfflineManager {
162
239
  private readonly maxCachedRows;
163
240
  private readonly maxRetries;
164
241
  private readonly onSyncError?;
242
+ private readonly onConflict?;
165
243
  private readonly createInner;
166
244
  private readonly inners;
167
245
  private readonly connectivity;
246
+ private readonly keepLastUser;
247
+ private readonly replayBatchSize;
168
248
  private scope;
249
+ /**
250
+ * The scope is the last signed-in user's, taken without a confirmed
251
+ * session ({@link resumeLastUser}). Nothing replays while it is set: the
252
+ * queue is sent under the credentials of whoever is signed in, and nobody
253
+ * is yet.
254
+ */
255
+ private unverified;
169
256
  /**
170
257
  * Set while the manager does not know whose data it holds: from
171
258
  * {@link holdScope} until the next {@link setScope}. `scope` is not
@@ -189,9 +276,21 @@ export declare class OfflineManager {
189
276
  /** The local database: normalized rows and query snapshots per collection. */
190
277
  private collections;
191
278
  /** In-memory mirror of the current scope's queue, in replay order. */
192
- private queue;
279
+ private queueEntries;
280
+ private get queue();
281
+ private set queue(value);
193
282
  /**
194
- * The mutation currently on the wire, if any.
283
+ * Per collection, the row ids the queue writes to and the ones it creates
284
+ * — {@link hasPending} and {@link isLocallyCreated}, which answering a
285
+ * page asks once or twice per row. Each used to scan the whole queue: a
286
+ * 500-row page against 200 queued writes read 80,000 queue entries, on
287
+ * every emit. Built on first use after the queue changes; whatever changes
288
+ * it in place calls {@link queueChangedInPlace}.
289
+ */
290
+ private pendingIndex?;
291
+ /**
292
+ * The mutations currently on the wire: one, or the members of a folded
293
+ * run (`replayBatchSize`).
195
294
  *
196
295
  * `flush` awaits `replay(op)` with `op` still at the head of `queue`, so for
197
296
  * the whole duration of that request the in-flight op is also the queue's
@@ -205,10 +304,10 @@ export declare class OfflineManager {
205
304
  * create. It is seeing it right now, so the row would be created and the
206
305
  * delete never queued — an orphan row nothing will ever remove.
207
306
  *
208
- * Guarding on the id rather than on a boolean keeps this correct if the
209
- * flush loop ever sends more than one op at a time.
307
+ * Guarding on ids rather than on a boolean is what keeps this correct
308
+ * when a folded run sends several at once, its last member the tail.
210
309
  */
211
- private inFlightId;
310
+ private readonly inFlight;
212
311
  private queueLoad?;
213
312
  /** Serializes enqueues so concurrent writes keep the order the app made them. */
214
313
  private enqueueChain;
@@ -219,6 +318,8 @@ export declare class OfflineManager {
219
318
  private refreshPending;
220
319
  private revCounter;
221
320
  private disposed;
321
+ /** Whether {@link requestPersistence} has asked. */
322
+ private persistenceRequested;
222
323
  private currentStatus;
223
324
  private readonly channel?;
224
325
  private readonly tabId;
@@ -230,7 +331,27 @@ export declare class OfflineManager {
230
331
  * replay under the credentials that made them — so neither may ever leak
231
332
  * across a sign-out/sign-in on a shared browser.
232
333
  */
233
- setScope(uid: string | undefined): void;
334
+ setScope(uid: string | undefined, options?: {
335
+ unverified?: boolean;
336
+ }): void;
337
+ /** Whether the manager was created with {@link OfflineConfig.keepLastUser}. */
338
+ get keepsLastUser(): boolean;
339
+ /**
340
+ * Take the scope of the last user who signed in on this device, without a
341
+ * session — for a load whose session restore could not reach the server.
342
+ * See {@link OfflineConfig.keepLastUser}. With no user remembered (or the
343
+ * option off) this is the signed-out scope, as it would have been anyway.
344
+ *
345
+ * Only while the scope is still held: an auth event that settled it while
346
+ * the id was being read is newer news.
347
+ */
348
+ resumeLastUser(): Promise<void>;
349
+ /**
350
+ * Keep the signed-in user's id for {@link resumeLastUser}, or forget it at
351
+ * sign-out. Forgetting does not depend on the option, so turning it off
352
+ * does not leave the last id behind for good.
353
+ */
354
+ private rememberUser;
234
355
  /**
235
356
  * Forget whose data this is until the next {@link setScope}.
236
357
  *
@@ -273,6 +394,21 @@ export declare class OfflineManager {
273
394
  /** Release listeners, timers and the cross-tab channel (client.close()). */
274
395
  dispose(): void;
275
396
  wrap<M extends AnyRow>(slug: string, inner: CollectionClient<M>): CollectionClient<M>;
397
+ /**
398
+ * `client.data.<slug>.trash` on an offline client: every call goes to the
399
+ * server, and none is cached or queued.
400
+ *
401
+ * The local database holds live rows only — a trashed row read into it
402
+ * would show up in every live query that matches it — so it cannot answer
403
+ * a trash read, and a restore or a purge queued offline would act on a row
404
+ * this device has never seen. Each of them rejects with `OFFLINE` instead.
405
+ *
406
+ * What the server answers does reach the local database: a restored row
407
+ * is live again and joins it, and a purged row leaves it. A row with a
408
+ * write still queued is synced first, so a purge never overtakes the
409
+ * queued delete it follows; if the queue cannot drain, the call is refused.
410
+ */
411
+ private wrapTrash;
276
412
  private observe;
277
413
  private observeById;
278
414
  private observersFor;
@@ -325,6 +461,13 @@ export declare class OfflineManager {
325
461
  * the difference. Reading one row first is enough to fix it.
326
462
  */
327
463
  private mintOfflineId;
464
+ /**
465
+ * Keep the version a server row came with, or forget the one held when it
466
+ * came with none: a row from a list carries no version, and may be newer
467
+ * than the one held. Only with `onConflict`, the one reader of it.
468
+ */
469
+ private noteETag;
470
+ private etagFor;
328
471
  private rawLocalRow;
329
472
  private localRow;
330
473
  private setLocalRow;
@@ -372,9 +515,35 @@ export declare class OfflineManager {
372
515
  */
373
516
  private scheduleRefresh;
374
517
  private evictRows;
518
+ /**
519
+ * Tombstones are tiny but unbounded — every row the app ever asked for and
520
+ * did not find leaves one. Capped against the row budget, oldest first,
521
+ * whenever one is added: the cap used to sit behind the row cache's own
522
+ * early return, so it only applied once the rows were over theirs, and
523
+ * looking up missing ids grew the set without limit.
524
+ */
525
+ private capTombstones;
375
526
  private evictSnapshots;
376
527
  private ensureQueueLoaded;
377
528
  private enqueue;
529
+ /**
530
+ * Ask the browser to keep the offline database, once, when there is first
531
+ * something in it worth keeping. Without it IndexedDB is best-effort
532
+ * storage, evicted under pressure (and by Safari after seven days without
533
+ * a visit) with the unsent writes in it.
534
+ */
535
+ private requestPersistence;
536
+ /** The queue's row ids for one collection — see {@link pendingIndex}. */
537
+ private pendingIds;
538
+ /**
539
+ * Did the queued writes to this row bring it into the query's filter? The
540
+ * row as it was before the first of them is that write's rollback; one
541
+ * that did not match then is in no server page of the query, so only
542
+ * this side can put it on screen.
543
+ */
544
+ private movedIntoFilter;
545
+ /** The queue's entries were changed without replacing the array. */
546
+ private queueChangedInPlace;
378
547
  private hasPending;
379
548
  /** Is this row one the server has never been told about? */
380
549
  private isLocallyCreated;
@@ -385,6 +554,38 @@ export declare class OfflineManager {
385
554
  remaining: number;
386
555
  }>;
387
556
  private flush;
557
+ /**
558
+ * The creates at the head of the queue that may replay as one request —
559
+ * see {@link OfflineConfig.replayBatchSize}. Fewer than two means the head
560
+ * replays on its own.
561
+ *
562
+ * Consecutive, so the queue's order holds; one collection, since a bulk
563
+ * write is one collection's. Each is a create never sent (so its request
564
+ * is still its own to choose) with an id the SDK minted as a string — a
565
+ * lost answer then cannot duplicate it, because the server refuses that
566
+ * id the second time and the single replay recognises the row as its
567
+ * own. A create whose values name another member's minted id waits: the
568
+ * server may give that row a different id, which only a replay one at a
569
+ * time can carry over.
570
+ */
571
+ private foldableRun;
572
+ /**
573
+ * Replay a {@link foldableRun} as one `createMany`.
574
+ *
575
+ * - `"done"`: every member is acknowledged, adopted and dropped.
576
+ * - `"offline"`: the connection failed; the members stay queued, as one
577
+ * write would.
578
+ * - `"unbatch"`: anything else — a refusal, a busy server. Nothing was
579
+ * written (a bulk write is one transaction), and the caller replays the
580
+ * members one at a time, where each refusal, retry budget and recovery
581
+ * applies to its own write.
582
+ *
583
+ * The key names the run by its first and last member and its length,
584
+ * which fixes its composition: a run re-sent after a lost answer is the
585
+ * same request under the same key, and is answered from the server's
586
+ * record of it.
587
+ */
588
+ private replayRun;
388
589
  private replay;
389
590
  /**
390
591
  * Send a queued write under its key.
@@ -428,6 +629,13 @@ export declare class OfflineManager {
428
629
  * silently lose writes the server would have accepted.
429
630
  */
430
631
  private rejectMutation;
632
+ /**
633
+ * An edit replayed with `If-Match` and refused with a 412: ask
634
+ * {@link OfflineConfig.onConflict}, and do what it says. A callback that
635
+ * throws, or none, is "overwrite" — what replay did before it knew of
636
+ * conflicts.
637
+ */
638
+ private resolveConflict;
431
639
  /**
432
640
  * Put the separate edits a coalesced `update` was made of back in the
433
641
  * queue, in its place and in their order, each with the row as it stood
@@ -455,6 +663,10 @@ export declare class OfflineManager {
455
663
  private rowKey;
456
664
  private absentKey;
457
665
  private queueKey;
666
+ /** Queue a write in the store, its rows encoded as the cache's are. */
667
+ private persistMutation;
668
+ /** The store's queue under `prefix`, its rows revived. */
669
+ private readQueue;
458
670
  private readCache;
459
671
  private writeCache;
460
672
  private deleteCache;
@@ -23,7 +23,7 @@
23
23
  * generated SDK — where `M` is a concrete row type — the documented call was a
24
24
  * compile error. Nothing in this repo noticed; a downstream application did.
25
25
  */
26
- import type { FindParams, FindResult, SDKQueryBuilderInterface, WhereFilterOp } from "@rebasepro/types";
26
+ import type { AggregateParams, FindParams, FindResult, LogicalCondition, SDKCollectionClient, SDKQueryBuilderInterface, UpdateValues, WhereFilterOp } from "@rebasepro/types";
27
27
  /**
28
28
  * A row shaped the way a **generated** SDK shapes one: a type alias with a
29
29
  * finite key set.
@@ -46,6 +46,10 @@ type ContractRow = {
46
46
  tags: string[];
47
47
  age: number;
48
48
  deleted_at: string | null;
49
+ /** A `vector` property. */
50
+ embedding: number[];
51
+ /** An `enum` property, which codegen emits as a union of literals. */
52
+ status: "draft" | "published";
49
53
  };
50
54
  /** A to-many relation, which codegen emits as `Array<TargetRow>`. */
51
55
  type TagRow = {
@@ -118,6 +122,21 @@ export declare const fluentRange: (qb: SDKQueryBuilderInterface<ContractRow>) =>
118
122
  export declare const paramsArrayContains: FindParams<ContractRow>;
119
123
  /** …and the array-of-tuples form the builder produces from two `.where()` calls. */
120
124
  export declare const paramsRange: FindParams<ContractRow>;
125
+ /**
126
+ * `cond` cannot know the row on its own; the place the group is used can.
127
+ * `logical` and `.where(group)` check every condition in the tree against `M`,
128
+ * as the object-form `where` does, so a typo'd column is a compile error, not
129
+ * the server's 400.
130
+ */
131
+ export declare const logicalOk: FindParams<ContractRow>;
132
+ export declare const logicalTypo: FindParams<ContractRow>;
133
+ export declare const logicalWrongValue: FindParams<ContractRow>;
134
+ export declare const logicalEnumTypo: FindParams<ContractRow>;
135
+ export declare const logicalArrayContainsList: FindParams<ContractRow>;
136
+ export declare const fluentLogicalTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
137
+ export declare const fluentLogicalOk: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
138
+ /** A group built at runtime keeps working where the row is untyped. */
139
+ export declare const untypedLogical: (qb: SDKQueryBuilderInterface, group: LogicalCondition) => SDKQueryBuilderInterface<Record<string, unknown>, import("@rebasepro/types").IncludeSpec>;
121
140
  /**
122
141
  * Sorting by relevance and then being unable to read it was the other half of
123
142
  * the same bug — the e2e cast around it, which should have been the tell.
@@ -171,4 +190,37 @@ export declare const fluentAggregateSortTypo: (qb: SDKQueryBuilderInterface<Post
171
190
  export declare const paramsRelationPath: FindParams<PostRow>;
172
191
  export declare const paramsJsonPath: FindParams<ContractRow>;
173
192
  export declare const paramsRelationAggregateSort: FindParams<PostRow>;
193
+ /**
194
+ * `FilterValues` typed the key and left the value `unknown`, so the shape the
195
+ * fluent form was fixed to refuse compiled in `find({ where })` — the form
196
+ * `listen`, `observe`, `count` and `iterate` all take.
197
+ */
198
+ export declare const paramsArrayContainsWholeArray: FindParams<ContractRow>;
199
+ export declare const paramsWrongValueType: FindParams<ContractRow>;
200
+ /** A path that is not a column has nothing to check against, and stays open. */
201
+ export declare const paramsRelationPathTyped: FindParams<ContractRow>;
202
+ /** An untyped row keeps the object form exactly as permissive as it was. */
203
+ export declare const paramsUntyped: FindParams;
204
+ /** `fields` is a projection of columns; a typo was `UNKNOWN_FIELD` at runtime. */
205
+ export declare const fieldsColumn: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
206
+ export declare const fieldsTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
207
+ /** `vectorSearch` names a column. */
208
+ export declare const vectorTypo: (qb: SDKQueryBuilderInterface<ContractRow>) => SDKQueryBuilderInterface<ContractRow, import("@rebasepro/types").IncludeSpec>;
209
+ /** `sum` and `avg` add numbers up; `min`/`max` order anything. */
210
+ export declare const aggregateSum: AggregateParams<ContractRow>;
211
+ export declare const aggregateSumText: AggregateParams<ContractRow>;
212
+ /** A field operation fits its column: `$inc` a number, `$push` an array. */
213
+ export declare const updateOps: UpdateValues<Partial<ContractRow>>;
214
+ export declare const updateIncText: UpdateValues<Partial<ContractRow>>;
215
+ /** An untyped row keeps every operation on every key. */
216
+ export declare const updateUntyped: UpdateValues<Partial<Record<string, unknown>>>;
217
+ /**
218
+ * A typed client is still a client: a helper written against the wide
219
+ * `SDKCollectionClient` (an export, a paginator, a seeder) takes
220
+ * `rebase.data.posts`. The column checks above must narrow what a typed
221
+ * client's methods accept without making the typed client a different kind
222
+ * of thing — `NumericKeyOf` as a conditional over `keyof M` made tsc measure
223
+ * `AggregateParams` invariant, and this line stopped compiling.
224
+ */
225
+ export declare const typedClientWidens: (posts: SDKCollectionClient<ContractRow>) => SDKCollectionClient;
174
226
  export {};
@@ -103,6 +103,14 @@ export interface ChannelTransport {
103
103
  * transport need not implement it.
104
104
  */
105
105
  onConnectionLost?(handler: (error: RebaseApiError) => void): () => void;
106
+ /**
107
+ * Whether the server agreed, for the current socket, to `name` (see the
108
+ * socket's `hello`). A transport without it is one whose server agreed to
109
+ * nothing.
110
+ */
111
+ hasServerOptIn?(name: string): boolean;
112
+ /** The current socket's id on the server: the `senderId` of what it broadcasts. */
113
+ readonly socketClientId?: string;
106
114
  }
107
115
  export declare class RebaseRealtimeChannel {
108
116
  readonly name: string;
@@ -303,6 +311,23 @@ export declare class RebaseRealtimeChannel {
303
311
  private stopHeartbeat;
304
312
  /** Fold an incoming frame into the roster and fan it out. */
305
313
  private handle;
314
+ /**
315
+ * Whether a jump in the sequence means a missed message: only when the
316
+ * server numbers this client's own broadcasts for it (`channel_seq`), so
317
+ * its own messages leave no hole. Against a server that does not, a hole
318
+ * is as likely to be the client's own message, and the old behaviour —
319
+ * deliver, advance — stands.
320
+ */
321
+ private watchesForGaps;
322
+ /**
323
+ * Hold `event` back and fetch what lies between it and {@link lastSeq},
324
+ * when it is past a hole. A frame can go missing between instances — a
325
+ * dropped bus connection, a bus that does not order — and the watermark
326
+ * would otherwise step over it for good.
327
+ *
328
+ * @returns Whether it was held back.
329
+ */
330
+ private fillsGap;
306
331
  /** Tell the app that retention dropped messages it never received. */
307
332
  private reportHistoryGap;
308
333
  private emitError;
package/dist/reviver.d.ts CHANGED
@@ -1 +1,19 @@
1
1
  export declare function rebaseReviver(_key: string, value: unknown): unknown;
2
+ /**
3
+ * `JSON.parse(text, rebaseReviver)`, without the reviver's cost where it has
4
+ * nothing to do.
5
+ *
6
+ * Handing `JSON.parse` a reviver takes V8 off its fast path and calls back for
7
+ * every key and every value, primitives included: about 7× a plain parse on a
8
+ * 1,000-row page, 10 ms of main thread per 600 KB. The reviver only ever
9
+ * changes objects carrying `__type`, and a REST page of flat rows has none. So
10
+ * this parses plainly and walks the result only when the text can hold one —
11
+ * and the walk visits objects, never primitives.
12
+ *
13
+ * Same result, instance for instance: the same values revived, bottom-up, as
14
+ * own data properties, and nothing deleted (the reviver never answers
15
+ * `undefined`).
16
+ *
17
+ * @throws SyntaxError exactly where `JSON.parse` does.
18
+ */
19
+ export declare function parseRebaseJson<T = unknown>(text: string): T;
@@ -0,0 +1,2 @@
1
+ import { uploadResumable } from "@rebasepro/common";
2
+ export { uploadResumable };
@@ -0,0 +1 @@
1
+ export { uploadResumable } from "@rebasepro/common";
@@ -19,6 +19,11 @@ export interface RebaseClientConfig {
19
19
  * service key instead, which resolves to `{ uid: "service", roles: ["admin"] }`
20
20
  * and **bypasses RLS**: there is no user to constrain those queries, so scope
21
21
  * them explicitly.
22
+ *
23
+ * Read each time a request or the realtime socket needs a token, until
24
+ * `setToken()` or a sign-in replaces it — so it may be declared as a getter.
25
+ * A token getter installed with `setAuthTokenGetter()` comes first; this is
26
+ * what is sent when it answers nothing.
22
27
  */
23
28
  token?: string;
24
29
  /**
@@ -156,7 +161,25 @@ export type FindResponse<T> = TypesFindResponse<T extends Record<string, unknown
156
161
  * serializes — `null` is a value, `undefined` is the absence of one).
157
162
  */
158
163
  export declare function assertNoUndefinedFilterValues(where: Record<string, unknown>): void;
159
- export declare function buildQueryString(params?: FindParams): string;
164
+ /**
165
+ * `?deleted=` — which soft-deleted rows a read covers: `"only"` the trashed
166
+ * ones, `"include"` live and trashed together. Absent, the server hides them.
167
+ *
168
+ * Not a {@link FindParams} field: `FindParams` is also what `listen()` and the
169
+ * offline cache take, and neither can answer for a trashed row — a
170
+ * subscription has no `withDeleted`, and the local database never holds one.
171
+ * Only the trash accessor (`client.data.<slug>.trash`) passes it.
172
+ */
173
+ export type SoftDeletedRows = "include" | "only";
174
+ export declare function buildQueryString(params?: FindParams, deleted?: SoftDeletedRows): string;
175
+ /**
176
+ * The body of `POST /data/<collection>/query`: the parameters
177
+ * {@link buildQueryString} puts in the URL, spelled as the URL spells them —
178
+ * each value a string, and a parameter the URL repeats (a filter with two
179
+ * conditions on one field) a list of them. The server reads it back into the
180
+ * same parameters and runs the same listing.
181
+ */
182
+ export declare function buildQueryBody(params?: FindParams, deleted?: SoftDeletedRows): Record<string, string | string[]>;
160
183
  /**
161
184
  * The query string for `GET /<collection>/aggregate`.
162
185
  *
@@ -166,11 +189,12 @@ export declare function buildQueryString(params?: FindParams): string;
166
189
  * by exactly the same code a `find()` uses, so "revenue by status, this month"
167
190
  * narrows the same rows whichever call is asking.
168
191
  *
169
- * `orderBy`, `include` and the page are deliberately not here: an aggregate has
170
- * no rows to sort, no relations to load and no page to continue. `limit` is,
171
- * and bounds the number of *groups*.
192
+ * `include` and a cursor are deliberately not here: an aggregate has no
193
+ * relations to load and no row to continue after. `limit`, `offset` and
194
+ * `orderBy` are, and page and sort the *groups* — `orderBy` by a group column or
195
+ * an aggregate's alias, in the spelling a `find()` sends.
172
196
  */
173
- export declare function buildAggregateQueryString(params: AggregateParams): string;
197
+ export declare function buildAggregateQueryString<M extends Record<string, unknown>>(params: AggregateParams<M>): string;
174
198
  /**
175
199
  * Response metadata a caller can ask for, filled in by `request`.
176
200
  *
@@ -191,12 +215,40 @@ export interface ResponseMeta {
191
215
  etag?: string;
192
216
  /** The HTTP status, for a caller that has to tell 200 from 204. */
193
217
  status?: number;
218
+ /**
219
+ * The response's headers, for a protocol that answers in them — a
220
+ * resumable upload's `Location` and `Upload-Offset`.
221
+ */
222
+ headers?: {
223
+ get(name: string): string | null;
224
+ };
194
225
  }
226
+ /** See {@link Transport.setTokenRefresher}. */
227
+ export type TokenRefresher = () => Promise<string | undefined> | undefined;
195
228
  export interface Transport {
196
229
  request: <T = unknown>(path: string, init?: RequestInit, meta?: ResponseMeta) => Promise<T>;
230
+ /**
231
+ * Like `request`, but the body of a success is left unread: the response
232
+ * comes back as it arrived, for a caller that wants text, a Blob or a
233
+ * stream. Auth, the refresh-and-resend and the error envelope are the same.
234
+ * Optional so a hand-written transport need not have it.
235
+ */
236
+ requestRaw?: (path: string, init?: RequestInit, meta?: ResponseMeta) => Promise<Response>;
197
237
  setToken: (newToken: string | null) => void;
198
238
  setAuthTokenGetter: (getter: () => Promise<string | null>) => void;
199
239
  setOnUnauthorized: (handler: () => Promise<boolean>) => void;
240
+ /**
241
+ * Install the hook asked, before each request that carries this
242
+ * transport's own token, whether that token is known to be spent. It
243
+ * answers `undefined` to send the token as held, or a promise for the one
244
+ * to send instead — the auth module's refresh, so a request made while a
245
+ * session is being restored, or after its token expired unnoticed, goes
246
+ * out once with a good token rather than with none or a dead one.
247
+ *
248
+ * Not consulted when an auth token getter is installed: the getter owns
249
+ * the token then. Optional so a hand-written transport need not have it.
250
+ */
251
+ setTokenRefresher?: (refresher: TokenRefresher | null) => void;
200
252
  readonly baseUrl: string;
201
253
  readonly apiPath: string;
202
254
  /** See {@link RebaseClientConfig.storageUrlOrigin}. Undefined = use `baseUrl`. */
@@ -205,4 +257,32 @@ export interface Transport {
205
257
  getHeaders: (init?: RequestInit) => Record<string, string>;
206
258
  resolveToken: () => Promise<string | null>;
207
259
  }
260
+ /**
261
+ * The base every request and every caller-built URL resolves against.
262
+ *
263
+ * `baseUrl` is optional because the common production shape is a Rebase
264
+ * backend serving its own SPA, where the API is simply the page's origin.
265
+ * Leaving it unset is therefore the *correct* configuration there — and the
266
+ * one that keeps working when a second hostname (a custom domain) points at
267
+ * the same app.
268
+ *
269
+ * When unset in a browser this resolves to the page origin rather than "".
270
+ * Requests behave identically either way, but the empty string is a trap for
271
+ * anything that builds a URL from `client.baseUrl`: `new URL("" + path)`
272
+ * throws, so apps "fixed" it by baking an absolute host into their bundle —
273
+ * which is exactly what breaks the day a custom domain is added, and which no
274
+ * amount of CORS configuration repairs, because a SameSite=Lax auth cookie is
275
+ * not sent cross-site either.
276
+ */
277
+ /**
278
+ * The code an error carries when the response did not name one: a proxy's 413,
279
+ * a load balancer's 502, a crash page's 500 — anything without the server's
280
+ * envelope. Without it `e.code` was `undefined` and the documented
281
+ * `switch (e.code)` fell through to `default` on exactly the failures that are
282
+ * not Rebase's own. Only codes the backend itself answers with, so a switch
283
+ * written against the error reference catches both.
284
+ *
285
+ * @internal
286
+ */
287
+ export declare function defaultCodeForStatus(status: number): string | undefined;
208
288
  export declare function createTransport(config: RebaseClientConfig, environment?: TransportEnvironment): Transport;