@rebasepro/client 0.22.0 → 0.23.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
  /**
@@ -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
@@ -68,6 +68,28 @@ export interface PendingMutation {
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
70
  upsert?: boolean;
71
+ /**
72
+ * The request as it was already sent once, for a write tried online
73
+ * that failed on the network before it was queued. Nothing says whether
74
+ * it reached the server — only the answer may have been lost — so replay
75
+ * keeps its key. For a create, sent with the rows as queued (their minted
76
+ * ids, which later offline writes may reference), the server either takes
77
+ * them, or, having committed the first attempt, refuses the key as held
78
+ * for a different request, and this request is sent again for its stored
79
+ * answer. Replaying under a fresh key wrote the rows a second time.
80
+ *
81
+ * An `update` or `delete` carries only the key: its queued request is the
82
+ * one it sent, since nothing is merged into an op that has `sent`.
83
+ */
84
+ sent?: {
85
+ idempotencyKey: string;
86
+ /** `create`: the row it was called with; `createMany`: the rows. */
87
+ data?: Record<string, unknown> | Record<string, unknown>[];
88
+ /** `create`: the `id` argument it was called with. */
89
+ id?: string | number;
90
+ /** `createMany`: the conflict target the batch was sent with. */
91
+ onConflict?: readonly string[];
92
+ };
71
93
  queuedAt: number;
72
94
  /** How many times replay has been attempted (diagnostics for a stuck queue). */
73
95
  attempts?: number;
package/dist/offline.d.ts CHANGED
@@ -221,6 +221,19 @@ export declare class OfflineManager {
221
221
  * query the server has never answered here.
222
222
  */
223
223
  private answer;
224
+ /**
225
+ * Answer a projected query (`fields`, `distinct`) from the server's own
226
+ * answer to it.
227
+ *
228
+ * The server's rows are the answer. The local database contributes only
229
+ * what it knows better: a row with unsynced writes shows them, narrowed to
230
+ * the columns the server sent, or drops out if it was deleted here or
231
+ * edited out of a filter this side can evaluate; and rows created here
232
+ * join the first page when there is somewhere to put them. A distinct
233
+ * query, or an order on a column the projection does not carry, leaves
234
+ * them out, and the result says it is partial.
235
+ */
236
+ private projectedAnswer;
224
237
  private localFind;
225
238
  /**
226
239
  * A temporary id of the type this collection's ids actually have.
@@ -256,6 +269,12 @@ export declare class OfflineManager {
256
269
  * Rows that came back unchanged keep their identity and revision, so a
257
270
  * refetch that changed nothing does not re-render every live query that
258
271
  * touches them — or rewrite them all to disk.
272
+ *
273
+ * A `projection` (see {@link isProjection}) only refreshes the columns it
274
+ * carries on rows already held, and never creates one. It is not a row:
275
+ * stored as one, it replaced the full row every other query reads and
276
+ * wrote the narrowed copy to disk, so a list lost its columns because a
277
+ * dropdown elsewhere asked for titles.
259
278
  */
260
279
  private ingest;
261
280
  /**
@@ -292,6 +311,17 @@ export declare class OfflineManager {
292
311
  }>;
293
312
  private flush;
294
313
  private replay;
314
+ /**
315
+ * Send a queued write under its key.
316
+ *
317
+ * A write the queue sent once already, before it was queued, carries that
318
+ * attempt's key and is sent under it again, as queued. If the server holds
319
+ * the key for a different request, the difference is the ids minted here
320
+ * — so the first attempt committed and only its answer was lost, and
321
+ * sending that request again returns the stored answer instead of a
322
+ * second copy of the rows.
323
+ */
324
+ private sendQueuedWrite;
295
325
  /**
296
326
  * Take the server's version of a row the client created offline.
297
327
  *
@@ -133,6 +133,27 @@ export declare class RebaseRealtimeChannel {
133
133
  */
134
134
  private pendingLive;
135
135
  private catchUpInFlight;
136
+ /**
137
+ * Whether {@link lastSeq} is a position this client reached, rather than
138
+ * the zero of a handle that has not caught up yet.
139
+ *
140
+ * A client joining for the first time asks for whatever is retained, so
141
+ * history pruned before it ever joined is not something it missed. Only a
142
+ * client with a position can have a hole in what it saw.
143
+ */
144
+ private positioned;
145
+ /** The page size the catch-up in flight asked for, reused for its later pages. */
146
+ private catchUpLimit;
147
+ /**
148
+ * `latestSeq` of an empty page that left this client behind, while a
149
+ * second request confirms it.
150
+ *
151
+ * The server reads the retained messages and then the cursor, so a message
152
+ * committed between the two shows up in `latestSeq` and not in the page.
153
+ * The second request returns it if it exists; only a second empty page
154
+ * means the messages up to this seq are gone.
155
+ */
156
+ private emptyTailSeq;
136
157
  /**
137
158
  * Deadline for a catch-up response.
138
159
  *
@@ -234,6 +255,12 @@ export declare class RebaseRealtimeChannel {
234
255
  * retention cannot be served; `CHANNEL_BUS_PAYLOAD_TOO_LARGE` when a
235
256
  * broadcast on an ephemeral channel is too big to cross the bus.
236
257
  *
258
+ * `CHANNEL_HISTORY_GAP` is raised by the client: a catch-up found that the
259
+ * server no longer retains messages this client never received, so its
260
+ * state has a hole a replay cannot fill. `details` is `{ from, to }`, the
261
+ * inclusive range of sequence numbers missed. Resync the channel's state
262
+ * from its source of truth.
263
+ *
237
264
  * These used to be dropped on the floor — there is no promise to reject on
238
265
  * a fire-and-forget frame, so a forbidden broadcast looked exactly like a
239
266
  * delivered one. With no handler attached they are logged as a warning,
@@ -271,6 +298,9 @@ export declare class RebaseRealtimeChannel {
271
298
  private stopHeartbeat;
272
299
  /** Fold an incoming frame into the roster and fan it out. */
273
300
  private handle;
301
+ /** Tell the app that retention dropped messages it never received. */
302
+ private reportHistoryGap;
303
+ private emitError;
274
304
  /** Deliver everything held back during a catch-up, in sequence order. */
275
305
  private flushPendingLive;
276
306
  private deliver;
@@ -126,6 +126,23 @@ export declare const ANONYMOUS_SERVER_CLIENT_WARNING: string;
126
126
  */
127
127
  export type FindParams<M extends Record<string, unknown> = Record<string, unknown>> = TypesFindParams<M>;
128
128
  export type FindResponse<T> = TypesFindResponse<T extends Record<string, unknown> ? T : Record<string, unknown>>;
129
+ /**
130
+ * Refuse a filter whose *value* is missing.
131
+ *
132
+ * `where: { status: ["==", undefined] }` used to serialize to the literal
133
+ * string, so `status=eq.undefined` went out on the wire and the server dutifully
134
+ * looked for rows whose status is the four-letter word "undefined". The caller
135
+ * saw an empty page, not an error — the classic shape of a variable that was
136
+ * never set.
137
+ *
138
+ * Dropping the condition instead would be worse than sending it: the query
139
+ * would come back *unfiltered*, which for an ownership or tenant filter means
140
+ * returning rows the caller never asked to see. So this is a hard error, and
141
+ * both correct spellings are named in the message: omit the key to skip the
142
+ * filter, or use `["is-null", null]` to match SQL NULL (which still
143
+ * serializes — `null` is a value, `undefined` is the absence of one).
144
+ */
145
+ export declare function assertNoUndefinedFilterValues(where: Record<string, unknown>): void;
129
146
  export declare function buildQueryString(params?: FindParams): string;
130
147
  /**
131
148
  * The query string for `GET /<collection>/aggregate`.
@@ -50,6 +50,17 @@ export declare class RebaseWebSocketClient {
50
50
  private pendingRequests;
51
51
  private reconnectAttempts;
52
52
  private maxReconnectAttempts;
53
+ /**
54
+ * Whether a socket of this client has ever opened — what makes the next
55
+ * open a *reconnect*, whose server has forgotten every subscription and
56
+ * channel membership and must be told them again.
57
+ *
58
+ * Not `reconnectAttempts > 0`: that counter is a backoff budget, and a
59
+ * fresh one (after giving up, or after a sign-out dropped the socket)
60
+ * starts at zero, so the socket that finally came back looked like a first
61
+ * connect and nothing was re-sent.
62
+ */
63
+ private hadConnection;
53
64
  private isConnected;
54
65
  private messageQueue;
55
66
  private requestTimeoutMs;
@@ -95,6 +106,21 @@ export declare class RebaseWebSocketClient {
95
106
  disconnect(permanent?: boolean): void;
96
107
  private initWebSocket;
97
108
  private processMessageQueue;
109
+ /**
110
+ * Whether a frame that waited in the queue should go out on the socket
111
+ * that just opened. Only a subscribe can be refused, and one is exactly
112
+ * when sending it would give the server a subscription this side will not
113
+ * read:
114
+ *
115
+ * - Its registration is gone, or has been given a new id, while it waited.
116
+ * A listener that unmounted before the socket opened (a StrictMode double
117
+ * mount does exactly this) sends no `unsubscribe`, because there was no
118
+ * socket to send it on, so the server would keep that one forever.
119
+ * - This open is a reconnect. `resubscribeAll` sends every registration
120
+ * under a fresh id right after the queue drains, so the queued copy would
121
+ * be the second of two. It is handed back to that pass instead.
122
+ */
123
+ private shouldSendQueued;
98
124
  private attemptReconnect;
99
125
  private isAuthError;
100
126
  private handleAuthFailure;
@@ -191,6 +217,14 @@ export declare class RebaseWebSocketClient {
191
217
  * firing mid-reconnect would tear down healthy subscriptions.
192
218
  */
193
219
  private suspendSubscribeWatchdogs;
220
+ /**
221
+ * Drop every registration's cached rows, keeping the registrations.
222
+ *
223
+ * A registration without rows counts as not loaded, so the next listener
224
+ * to attach asks the server (or waits for the resubscribe) instead of
225
+ * being handed what is cached.
226
+ */
227
+ private forgetCachedData;
194
228
  /**
195
229
  * Arm watchdogs for subscribes that were requested while offline and have
196
230
  * just been flushed to the socket. Their timers were deliberately not set at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/client",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "HTTP SDK client for the Rebase custom backend",
5
5
  "keywords": [
6
6
  "rebase",
@@ -41,9 +41,9 @@
41
41
  "./package.json": "./package.json"
42
42
  },
43
43
  "dependencies": {
44
- "@rebasepro/common": "0.22.0",
45
- "@rebasepro/types": "0.22.0",
46
- "@rebasepro/utils": "0.22.0"
44
+ "@rebasepro/common": "0.23.0",
45
+ "@rebasepro/utils": "0.23.0",
46
+ "@rebasepro/types": "0.23.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@jest/globals": "^30.4.1",