@rebasepro/client 0.25.1 → 0.26.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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ of it resolves too: Node has supported `require(esm)` since 22.12.
34
34
  |---|---|
35
35
  | `createRebaseClient<DB>(options)` | Create a client instance. Generic `DB` parameter enables type-safe `client.data.*` access. |
36
36
  | `CreateRebaseClientResult<DB>` | The client type it returns (`RebaseClient<DB>` from `@rebasepro/types`, narrowed) — includes `auth`, `admin`, `cron`, `functions`, `storage`, `ws`, `data`, `call`, and token management methods. |
37
- | `CreateRebaseClientOptions` | Extends `RebaseClientConfig` with the `auth`, `admin`, `cron`, `apiKeys` and `personalKeys` sub-configs, plus `storageSources`, `collections` and `offline`. |
37
+ | `CreateRebaseClientOptions` | Extends `RebaseClientConfig` with the `auth`, `admin`, `cron`, `apiKeys` and `personalKeys` sub-configs, plus `storageSources`, `collections`, `offline` and `debug`. |
38
38
 
39
39
  ### Config
40
40
 
@@ -49,6 +49,7 @@ of it resolves too: Node has supported `require(esm)` since 22.12.
49
49
  | `realtime` | `boolean` | `true` | Open the WebSocket — `false` lets a one-shot script exit |
50
50
  | `collections` | `Record<string, string>` | — | Maps accessor names to collection slugs |
51
51
  | `offline` | `boolean \| OfflineConfig` | `false` | Local-first sync — see [the docs](https://rebase.pro/docs/sdk/offline) |
52
+ | `debug` | `boolean` | `false` | Log the realtime connection's progress with `console.debug`. One browser can turn it on with `localStorage.setItem("rebase_debug", "1")` — see [Connection logging](https://rebase.pro/docs/sdk#connection-logging) |
52
53
 
53
54
  ### Tokens
54
55
 
package/dist/admin.d.ts CHANGED
@@ -11,6 +11,14 @@ export interface ListUsersOptions {
11
11
  offset?: number;
12
12
  orderBy?: string;
13
13
  orderDir?: "asc" | "desc";
14
+ /** Only the users holding this role id (`?role=`). */
15
+ role?: string;
16
+ /**
17
+ * Resolve exactly these users in one round trip (`?ids=`), for turning
18
+ * stored ids into names. The window, search and role are ignored when it
19
+ * is given, and ids that name no user are left out of the answer.
20
+ */
21
+ ids?: readonly string[];
14
22
  }
15
23
  export declare function createAdmin(transport: Transport, options?: CreateAdminOptions): {
16
24
  listUsers: (options?: ListUsersOptions) => Promise<{
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ import { createFunctionsClient } from "./functions.js";
9
9
  import { RebaseWebSocketClient } from "./websocket.js";
10
10
  import { RebaseRealtimeChannel, type ChannelOptions } from "./realtime-channel.js";
11
11
  import type { OfflineApi, OfflineConfig } from "./offline.js";
12
- import { type BatchOperation, type BatchResult, type IncludeOf, InsertOf, RebaseClient, RebaseSdkData, RowOf, StorageSource, StorageSourceDefinition, StorageSourceRegistry, UpdateOf, type WriteOptions } from "@rebasepro/types";
12
+ import { type AccessSimulation, type BatchOperation, type BatchResult, type IncludeOf, InsertOf, RebaseClient, RebaseSdkData, RowOf, StorageSource, StorageSourceDefinition, StorageSourceRegistry, UpdateOf, type WriteOptions } from "@rebasepro/types";
13
13
  export { RebaseApiError } from "./transport.js";
14
14
  export { RebaseClientError } from "./errors.js";
15
15
  export type { RebaseErrorCode } from "@rebasepro/types";
@@ -57,10 +57,17 @@ export interface CreateRebaseClientOptions extends RebaseClientConfig {
57
57
  */
58
58
  storageSources?: StorageSourceDefinition[];
59
59
  /**
60
- * Maps camelCase property names / safe identifiers to the actual
61
- * collection slugs on the server (e.g. `{ companyMembers: "company-members" }`).
62
- * If provided, the data layer proxy will resolve property accessors to their
63
- * correct slugs via this map before falling back to automatic snake_casing.
60
+ * Maps each `client.data` accessor to the collection slug the server
61
+ * serves (e.g. `{ companyMembers: "company-members" }`): the generated
62
+ * `collectionsDictionary`.
63
+ *
64
+ * **Required for a typed client** (`createRebaseClient<Database>`). The
65
+ * types offer every accessor, but without the map the proxy sends
66
+ * `client.data.companyMembers` to `/data/company_members`, so a kebab-case
67
+ * slug or a renamed accessor (`collectionCollection`) answers 404; a
68
+ * one-time warning names the accessor. With the map, an unknown accessor
69
+ * throws `UNKNOWN_COLLECTION` with a suggestion. A later version will make
70
+ * it required in the types when `Database` is supplied.
64
71
  */
65
72
  collections?: Record<string, string>;
66
73
  /**
@@ -78,6 +85,17 @@ export interface CreateRebaseClientOptions extends RebaseClientConfig {
78
85
  * shared across tabs. Off by default.
79
86
  */
80
87
  offline?: boolean | OfflineConfig;
88
+ /**
89
+ * Log the realtime connection's progress — opened, signed in, closed,
90
+ * redialling — with `console.debug`. Off by default, so a public page says
91
+ * nothing about its backend in a visitor's console; warnings and errors
92
+ * are written either way.
93
+ *
94
+ * Without a code change, one browser can turn it on from its console with
95
+ * `localStorage.setItem("rebase_debug", "1")` (remove the key to stop), and
96
+ * any runtime with `globalThis.__REBASE_DEBUG__ = true`.
97
+ */
98
+ debug?: boolean;
81
99
  }
82
100
  type KebabToCamelCase<S extends string> = S extends `${infer T}-${infer U}` ? `${T}${Capitalize<KebabToCamelCase<U>>}` : S;
83
101
  /** The accessor whose generated entry declares `Slug: S`, or `never`. */
@@ -101,7 +119,19 @@ type TypedDataLayer<DB> = {
101
119
  * capabilities populated and the `data` layer narrowed to provide
102
120
  * typed collection accessors when a `DB` schema generic is supplied.
103
121
  */
104
- export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<RebaseClient<DB>, "data" | "email"> & {
122
+ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<RebaseClient<DB>, "data" | "email" | "simulate"> & {
123
+ /**
124
+ * A client whose data requests run with simulated access — as a role, a
125
+ * uid holding a role, or a service key — for an administrator checking
126
+ * what row-level security lets that identity read. See
127
+ * {@link RebaseClient.simulate} for what the server honours and refuses.
128
+ *
129
+ * Always present here, typed by the same `Database` (it is built with the
130
+ * same `collections` dictionary), and closable: it is a client of its own,
131
+ * so release it with `close()` when done. On `RebaseClient` it is optional
132
+ * and untyped, because a server-side client has none.
133
+ */
134
+ simulate: (simulation: AccessSimulation) => CreateRebaseClientResult<DB>;
105
135
  /**
106
136
  * Send this token from now on, over HTTP and on the realtime socket. An
107
137
  * open socket is rebuilt to authenticate with it. A token getter, when one
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as invalidBulkBody, d as RebaseApiError, h as parseRebaseJson, i as isOfflineError, l as SDKQueryBuilder, m as defaultCodeForStatus, n as MemoryOfflineStore, o as createCollectionClient, p as createTransport, s as etagOf, u as RebaseClientError } from "./offline-store-BhYXViha.js";
2
- import { DEFAULT_STORAGE_SOURCE_KEY, PUBLIC_STORAGE_PREFIX, RebaseApiError as RebaseApiError$1, RebaseClientError as RebaseClientError$1, STORAGE_REFERENCE_SCHEME, isPublicStoragePath, isStorageReference, isUnsupported, parseStorageReference, resolveStorageReferences, storageObjectPath, storageReference, storageReferencesForDownloadUrls, storageTargetOfDownloadUrl, storageTargetsInText } from "@rebasepro/types";
2
+ import { DEFAULT_STORAGE_SOURCE_KEY, PUBLIC_STORAGE_PREFIX, RebaseApiError as RebaseApiError$1, RebaseClientError as RebaseClientError$1, STORAGE_REFERENCE_SCHEME, isPublicStoragePath, isStorageReference, isUnsupported, parseStorageReference, resolveStorageReferences, simulationHeaders, storageObjectPath, storageReference, storageReferencesForDownloadUrls, storageTargetOfDownloadUrl, storageTargetsInText } from "@rebasepro/types";
3
3
  import { COMPOSITE_ID_SEPARATOR, DOWNLOAD_URL_CONCURRENCY, RESUMABLE_UPLOAD_THRESHOLD, RebasePaginationError, and, buildCompositeId, cond, createBatchLoader, createConcurrencyLimit, createDownloadUrlCache, not, or, warnDeprecated, withETag } from "@rebasepro/common";
4
4
  import { toSnakeCase, unref } from "@rebasepro/utils";
5
5
  //#region src/auth.ts
@@ -1364,6 +1364,8 @@ function createAdmin(transport, options) {
1364
1364
  if (options?.search) params.set("search", options.search);
1365
1365
  if (options?.orderBy) params.set("orderBy", options.orderBy);
1366
1366
  if (options?.orderDir) params.set("orderDir", options.orderDir);
1367
+ if (options?.role) params.set("role", options.role);
1368
+ if (options?.ids) params.set("ids", options.ids.join(","));
1367
1369
  const qs = params.toString();
1368
1370
  return transport.request(adminPath + "/users" + (qs ? "?" + qs : ""), { method: "GET" });
1369
1371
  }
@@ -2139,10 +2141,23 @@ function requestTimeoutError(written) {
2139
2141
  * connection has been down for longer than a blip. The registration is kept and
2140
2142
  * resumes when the socket is back.
2141
2143
  */
2142
- function realtimeLostError() {
2144
+ function realtimeLostError(refusal) {
2145
+ if (refusal) return new RebaseApiError$1(`The server refused this realtime connection (${refusal.code}: ${refusal.message}), so live data is not updating. The client keeps retrying, more slowly each time, and refreshes this subscription once it is let in.`, {
2146
+ code: "CONNECTION_LOST",
2147
+ details: { reason: refusal.code },
2148
+ cause: refusal
2149
+ });
2143
2150
  return new RebaseApiError$1("The realtime connection is down, so live data is not updating. The client keeps reconnecting and refreshes this subscription when the connection is back.", { code: "CONNECTION_LOST" });
2144
2151
  }
2145
2152
  /**
2153
+ * Whether `error` is the server refusing this socket for its per-user ceiling
2154
+ * (`REALTIME_MAX_SOCKETS_PER_USER`): it answers AUTHENTICATE with
2155
+ * `TOO_MANY_SOCKETS` and closes the socket.
2156
+ */
2157
+ function isSocketCeilingRefusal(error) {
2158
+ return error instanceof RebaseApiError$1 && error.code === "TOO_MANY_SOCKETS";
2159
+ }
2160
+ /**
2146
2161
  * Whether realtime debugging is on: `globalThis.__REBASE_DEBUG_REALTIME__ = true`,
2147
2162
  * set from a console or a test. While it is, each refetched row that differs
2148
2163
  * from the one held is explained key by key with `console.debug`.
@@ -2151,17 +2166,28 @@ function realtimeDebugOn() {
2151
2166
  return Reflect.get(globalThis, "__REBASE_DEBUG_REALTIME__") === true;
2152
2167
  }
2153
2168
  /**
2154
- * Connection chatter: the socket opened, signed in, closed, is redialling.
2155
- *
2156
- * `console.debug` where that is a debug level — a browser's console hides it
2157
- * unless Verbose is asked for. Not in Node, where `console.debug` is
2158
- * `console.log`: a script's `listen()` opened its stdout with "Connected to
2159
- * PostgreSQL backend" and "WebSocket auto-authenticated", ahead of what the
2160
- * script prints. There it is written only while realtime debugging is on.
2169
+ * The `localStorage` key that turns connection logging on for one browser,
2170
+ * without a code change or a rebuild: `localStorage.setItem("rebase_debug", "1")`
2171
+ * in its console. For a page that loads the client as a plain script, where
2172
+ * `createRebaseClient({ debug: true })` means editing and redeploying the site.
2161
2173
  */
2162
- function connectionDebug(...args) {
2163
- if (!(typeof window !== "undefined" || typeof document !== "undefined") && !realtimeDebugOn()) return;
2164
- console.debug(...args);
2174
+ var DEBUG_STORAGE_KEY = "rebase_debug";
2175
+ /**
2176
+ * Whether connection logging was switched on from outside the code:
2177
+ * `globalThis.__REBASE_DEBUG__`, `globalThis.__REBASE_DEBUG_REALTIME__` (which
2178
+ * also explains refetched rows), or {@link DEBUG_STORAGE_KEY} in this
2179
+ * browser's `localStorage`. Read at each line, so it can be switched on in a
2180
+ * console without reloading a page whose socket is already open.
2181
+ */
2182
+ function debugSwitchedOn() {
2183
+ if (Reflect.get(globalThis, "__REBASE_DEBUG__") === true || realtimeDebugOn()) return true;
2184
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return false;
2185
+ try {
2186
+ const value = localStorage.getItem(DEBUG_STORAGE_KEY);
2187
+ return value === "1" || value === "true";
2188
+ } catch {
2189
+ return false;
2190
+ }
2165
2191
  }
2166
2192
  /**
2167
2193
  * The codes an `AUTHENTICATE` is refused with when the server will not honour
@@ -2483,11 +2509,28 @@ var RebaseWebSocketClient = class {
2483
2509
  onUnauthorized;
2484
2510
  refreshInProgress = null;
2485
2511
  impersonate;
2512
+ debug;
2513
+ /**
2514
+ * Connection chatter: the socket opened, signed in, closed, is redialling.
2515
+ *
2516
+ * Silent unless asked for, with the `debug` option or
2517
+ * {@link debugSwitchedOn}. It used to go to `console.debug` in every
2518
+ * browser: a public page printed "Connected to PostgreSQL backend" and
2519
+ * "WebSocket auto-authenticated" to each visitor's console, naming the
2520
+ * backend on every load. In Node, where `console.debug` is `console.log`,
2521
+ * it opened a script's stdout ahead of what the script prints. Warnings and
2522
+ * errors are not chatter and are always written.
2523
+ */
2524
+ connectionDebug(...args) {
2525
+ if (!this.debug && !debugSwitchedOn()) return;
2526
+ console.debug(...args);
2527
+ }
2486
2528
  constructor(config) {
2487
2529
  this.websocketUrl = config.websocketUrl;
2488
2530
  this.getAuthToken = config.getAuthToken;
2489
2531
  this.onUnauthorized = config.onUnauthorized;
2490
2532
  this.impersonate = config.impersonate;
2533
+ this.debug = config.debug === true;
2491
2534
  this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
2492
2535
  }
2493
2536
  /**
@@ -2526,7 +2569,7 @@ var RebaseWebSocketClient = class {
2526
2569
  if (this.closedByCaller) return;
2527
2570
  if (this.connectionState !== "reconnecting" && this.connectionState !== "disconnected") return;
2528
2571
  if (this.ws) return;
2529
- connectionDebug(`${reason} — retrying the realtime connection`);
2572
+ this.connectionDebug(`${reason} — retrying the realtime connection`);
2530
2573
  if (this.reconnectTimeout) {
2531
2574
  clearTimeout(this.reconnectTimeout);
2532
2575
  this.reconnectTimeout = null;
@@ -2576,6 +2619,21 @@ var RebaseWebSocketClient = class {
2576
2619
  }
2577
2620
  return true;
2578
2621
  }
2622
+ /** Whether the current run of ceiling refusals has been said in the console. */
2623
+ socketCeilingWarned = false;
2624
+ /**
2625
+ * The server refused this socket for its per-user ceiling. Said once per
2626
+ * run of refusals, as a warning, and reported to every registration at
2627
+ * once as an outage — a refusal is not a blip that the next dial fixes.
2628
+ */
2629
+ noteSocketCeiling(error) {
2630
+ if (!this.socketCeilingWarned) {
2631
+ this.socketCeilingWarned = true;
2632
+ console.warn(`[Rebase] Realtime: the server refused this connection (${error.code}: ${error.message}). Live data is not updating; the client retries, more slowly each time, until another connection closes.`);
2633
+ }
2634
+ this.clearOutageTimer();
2635
+ this.reportConnectionLost(error);
2636
+ }
2579
2637
  /**
2580
2638
  * Authenticate the WebSocket connection
2581
2639
  */
@@ -2624,10 +2682,10 @@ var RebaseWebSocketClient = class {
2624
2682
  if (this.isConnected && !this.isAuthenticated && !this.authPromise) this.getAuthToken().then((token) => {
2625
2683
  if (!this.ws) return;
2626
2684
  if (token) this.authenticate(token).catch((e) => {
2627
- if (this.ws) connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
2685
+ if (this.ws) this.connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
2628
2686
  });
2629
2687
  }).catch((e) => {
2630
- if (this.ws) connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
2688
+ if (this.ws) this.connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
2631
2689
  });
2632
2690
  }
2633
2691
  /**
@@ -2678,15 +2736,12 @@ var RebaseWebSocketClient = class {
2678
2736
  const socket = new this.WebSocketConstructor(withOptIns(this.websocketUrl));
2679
2737
  this.ws = socket;
2680
2738
  this.ws.onopen = async () => {
2681
- connectionDebug("Connected to PostgreSQL backend");
2739
+ this.connectionDebug("Connected to PostgreSQL backend");
2682
2740
  const wasReconnect = this.hadConnection;
2683
2741
  this.hadConnection = true;
2684
2742
  this.isConnected = true;
2685
- this.reconnectAttempts = 0;
2686
- this.clearOutageTimer();
2687
- this.outageReported = false;
2688
2743
  this.gaveUp = false;
2689
- this.setState("connected");
2744
+ if (!this.outageReported) this.setState("connected");
2690
2745
  this.refusedSession = null;
2691
2746
  if (this.getAuthToken && !this.isAuthenticated) {
2692
2747
  let token = null;
@@ -2694,12 +2749,22 @@ var RebaseWebSocketClient = class {
2694
2749
  token = await this.getAuthToken();
2695
2750
  if (token) {
2696
2751
  await this.authenticate(token);
2697
- connectionDebug("WebSocket auto-authenticated");
2752
+ this.connectionDebug("WebSocket auto-authenticated");
2698
2753
  }
2699
2754
  } catch (error) {
2700
- if (!(token && this.noteSessionRefused(token, error))) connectionDebug("WebSocket connected, not signed in yet:", error?.message || error);
2755
+ if (isSocketCeilingRefusal(error)) {
2756
+ this.noteSocketCeiling(error);
2757
+ return;
2758
+ }
2759
+ if (!(token && this.noteSessionRefused(token, error))) this.connectionDebug("WebSocket connected, not signed in yet:", error?.message || error);
2701
2760
  }
2702
2761
  }
2762
+ if (this.ws !== socket) return;
2763
+ this.reconnectAttempts = 0;
2764
+ this.clearOutageTimer();
2765
+ this.outageReported = false;
2766
+ this.socketCeilingWarned = false;
2767
+ this.setState("connected");
2703
2768
  this.emit(wasReconnect ? "reconnect" : "connect");
2704
2769
  this.processMessageQueue(wasReconnect);
2705
2770
  if (wasReconnect) this.resubscribeAll();
@@ -2715,7 +2780,7 @@ var RebaseWebSocketClient = class {
2715
2780
  }
2716
2781
  };
2717
2782
  const onClosed = () => {
2718
- connectionDebug("Disconnected from PostgreSQL backend");
2783
+ this.connectionDebug("Disconnected from PostgreSQL backend");
2719
2784
  this.stopLivenessWatch();
2720
2785
  this.forgetServerOptIns();
2721
2786
  if (this.ws === socket) this.ws = null;
@@ -2845,7 +2910,7 @@ var RebaseWebSocketClient = class {
2845
2910
  this.reconnectAttempts = Math.min(this.reconnectAttempts + 1, 30);
2846
2911
  const ceiling = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_DELAY_MS);
2847
2912
  const delay = MIN_RECONNECT_DELAY_MS + Math.floor(Math.random() * (ceiling - MIN_RECONNECT_DELAY_MS));
2848
- connectionDebug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
2913
+ this.connectionDebug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
2849
2914
  if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
2850
2915
  this.reconnectTimeout = setTimeout(() => {
2851
2916
  this.reconnectTimeout = null;
@@ -2871,11 +2936,11 @@ var RebaseWebSocketClient = class {
2871
2936
  if (this.outageTimer) clearTimeout(this.outageTimer);
2872
2937
  this.outageTimer = null;
2873
2938
  }
2874
- reportConnectionLost() {
2939
+ reportConnectionLost(refusal) {
2875
2940
  if (this.outageReported) return;
2876
2941
  this.outageReported = true;
2877
2942
  this.setState("disconnected");
2878
- const error = realtimeLostError();
2943
+ const error = realtimeLostError(refusal);
2879
2944
  for (const subscription of this.collectionSubscriptions.values()) this.notifyError(subscription.callbacks, error);
2880
2945
  for (const subscription of this.singleSubscriptions.values()) this.notifyError(subscription.callbacks, error);
2881
2946
  for (const handler of [...this.connectionLostHandlers]) try {
@@ -3121,7 +3186,7 @@ var RebaseWebSocketClient = class {
3121
3186
  if (!token) throw new Error("user not logged in");
3122
3187
  if (this.refusedSession?.token === token) throw this.refusedSession.error;
3123
3188
  await this.authenticate(token);
3124
- connectionDebug("WebSocket authenticated on demand");
3189
+ this.connectionDebug("WebSocket authenticated on demand");
3125
3190
  return;
3126
3191
  } catch (error) {
3127
3192
  lastError = error;
@@ -3140,7 +3205,7 @@ var RebaseWebSocketClient = class {
3140
3205
  }
3141
3206
  if (attempt < retryCount - 1) {
3142
3207
  const delay = Math.min(1e3 * (attempt + 1), 3e3);
3143
- connectionDebug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
3208
+ this.connectionDebug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
3144
3209
  await new Promise((resolve) => setTimeout(resolve, delay));
3145
3210
  }
3146
3211
  }
@@ -3155,7 +3220,7 @@ var RebaseWebSocketClient = class {
3155
3220
  const token = await this.getAuthToken();
3156
3221
  if (!token) throw new Error("user not logged in");
3157
3222
  await this.authenticate(token);
3158
- connectionDebug("WebSocket reauthenticated successfully");
3223
+ this.connectionDebug("WebSocket reauthenticated successfully");
3159
3224
  } catch (error) {
3160
3225
  console.error("WebSocket reauthentication failed:", error);
3161
3226
  throw error;
@@ -3845,7 +3910,7 @@ var RebaseWebSocketClient = class {
3845
3910
  * we need to re-register everything to resume receiving updates.
3846
3911
  */
3847
3912
  resubscribeAll() {
3848
- connectionDebug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
3913
+ this.connectionDebug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
3849
3914
  for (const [key, sub] of this.collectionSubscriptions.entries()) {
3850
3915
  if (sub.subscribeInFlight) continue;
3851
3916
  const oldBackendId = sub.backendSubscriptionId;
@@ -4135,12 +4200,28 @@ var RebaseRealtimeChannel = class {
4135
4200
  */
4136
4201
  async broadcast(event, payload) {
4137
4202
  await this.join();
4203
+ this.rememberOwnSender(this.transport.socketClientId);
4138
4204
  await this.send("broadcast", {
4139
4205
  event,
4140
4206
  payload
4141
4207
  });
4142
4208
  }
4143
4209
  /**
4210
+ * The socket ids this channel has broadcast from — the current one and the
4211
+ * ones before a reconnect. A message in history from any of them is this
4212
+ * client's own, and a sender is never sent its own messages: replayed after
4213
+ * a reconnect whose `broadcast_ack` was lost, it was applied twice.
4214
+ */
4215
+ ownSenderIds = /* @__PURE__ */ new Set();
4216
+ rememberOwnSender(id) {
4217
+ if (id === void 0 || this.ownSenderIds.has(id)) return;
4218
+ this.ownSenderIds.add(id);
4219
+ if (this.ownSenderIds.size > 16) {
4220
+ const oldest = this.ownSenderIds.values().next().value;
4221
+ if (oldest !== void 0) this.ownSenderIds.delete(oldest);
4222
+ }
4223
+ }
4224
+ /**
4144
4225
  * Observe refusals about this channel.
4145
4226
  *
4146
4227
  * `CHANNEL_FORBIDDEN` when an authorizer refuses a join, a broadcast or a
@@ -4313,7 +4394,7 @@ var RebaseRealtimeChannel = class {
4313
4394
  for (const entry of entries) {
4314
4395
  if (entry.seq <= this.lastSeq) continue;
4315
4396
  this.lastSeq = entry.seq;
4316
- if (ownId !== void 0 && entry.senderId === ownId) continue;
4397
+ if (ownId !== void 0 && (entry.senderId === ownId || entry.senderId !== void 0 && this.ownSenderIds.has(entry.senderId))) continue;
4317
4398
  this.deliver({
4318
4399
  event: entry.event,
4319
4400
  payload: entry.payload,
@@ -4462,7 +4543,7 @@ var LazyOfflineManager = class {
4462
4543
  keepsLastUser;
4463
4544
  constructor(config, createInner) {
4464
4545
  this.keepsLastUser = config.keepLastUser === true;
4465
- import("./offline-BrqCRgih.js").then(({ OfflineManager: Engine }) => {
4546
+ import("./offline-AvuEcg7u.js").then(({ OfflineManager: Engine }) => {
4466
4547
  const engine = new Engine(config, createInner);
4467
4548
  this.engine = engine;
4468
4549
  const queued = this.queued;
@@ -4537,6 +4618,9 @@ var LazyOfflineManager = class {
4537
4618
  setScope(uid, options) {
4538
4619
  this.run((engine) => engine.setScope(uid, options)).catch(() => void 0);
4539
4620
  }
4621
+ onReachedUnconfirmed(listener) {
4622
+ return this.subscribe((engine) => engine.onReachedUnconfirmed(listener));
4623
+ }
4540
4624
  async resumeLastUser() {
4541
4625
  await await this.run((engine) => engine.resumeLastUser());
4542
4626
  }
@@ -4694,7 +4778,8 @@ function createRebaseClient(options) {
4694
4778
  websocketUrl: resolvedWsUrl,
4695
4779
  getAuthToken: () => transport.resolveToken(),
4696
4780
  onUnauthorized: () => onUnauthorized(),
4697
- impersonate: options.impersonate
4781
+ impersonate: options.impersonate,
4782
+ debug: options.debug
4698
4783
  });
4699
4784
  let realtimeUid = auth.getSession()?.user?.uid ?? null;
4700
4785
  auth.onAuthStateChange((event, session) => {
@@ -4771,15 +4856,19 @@ function createRebaseClient(options) {
4771
4856
  });
4772
4857
  if (offlineManager.keepsLastUser) {
4773
4858
  let confirming = false;
4774
- offlineManager.api.onStatusChange((status) => {
4775
- if (!status.online || !status.unverifiedUser || confirming || auth.getSession()) return;
4859
+ const confirm = () => {
4860
+ if (confirming || auth.getSession()) return;
4776
4861
  confirming = true;
4777
4862
  auth.refreshSession().catch((error) => {
4778
4863
  if (error instanceof RebaseApiError && error.status !== void 0 && error.status >= 400 && error.status < 500 && !auth.getSession()) offlineManager.setScope(void 0);
4779
4864
  }).finally(() => {
4780
4865
  confirming = false;
4781
4866
  });
4867
+ };
4868
+ offlineManager.api.onStatusChange((status) => {
4869
+ if (status.online && status.unverifiedUser) confirm();
4782
4870
  });
4871
+ offlineManager.onReachedUnconfirmed(confirm);
4783
4872
  }
4784
4873
  }
4785
4874
  /**
@@ -4840,11 +4929,12 @@ function createRebaseClient(options) {
4840
4929
  }
4841
4930
  });
4842
4931
  }
4932
+ const slug = toSnakeCase(prop);
4843
4933
  if (!untypedWarned) {
4844
4934
  untypedWarned = true;
4845
- console.warn(`[Rebase] Untyped data access detected (client.data.${prop}). Collection names are resolved via snake_case conversion, which may cause silent 404s at request time. Pass a \`collections\` dictionary to createRebaseClient() or use the generated SDK for type-safe access.`);
4935
+ console.warn(`[Rebase] Untyped data access detected: client.data.${prop} was sent to /data/${slug}, its name in snake_case, because this client has no \`collections\` dictionary. A collection whose slug is not that (a kebab-case slug, a renamed accessor) answers 404. With generated types, pass \`collections: collectionsDictionary\` (from generated/sdk/database.types) to createRebaseClient(); a typed client needs it. Otherwise use client.data.collection("<slug>").`);
4846
4936
  }
4847
- return collection(toSnakeCase(prop));
4937
+ return collection(slug);
4848
4938
  },
4849
4939
  has(_target, prop) {
4850
4940
  if (prop === "collection") return true;
@@ -4911,6 +5001,28 @@ channel: (name, options) => {
4911
5001
  onUnauthorized = handler;
4912
5002
  },
4913
5003
  resolveToken: transport.resolveToken,
5004
+ simulate: (simulation) => {
5005
+ const derived = createRebaseClient({
5006
+ baseUrl: options.baseUrl,
5007
+ apiPath: options.apiPath,
5008
+ fetch: options.fetch,
5009
+ collections: options.collections,
5010
+ schemaVersion: options.schemaVersion,
5011
+ headers: {
5012
+ ...options.headers ?? {},
5013
+ ...simulationHeaders(simulation)
5014
+ },
5015
+ realtime: false,
5016
+ auth: {
5017
+ storage: createMemoryStorage(),
5018
+ persistSession: false,
5019
+ autoRefresh: false
5020
+ }
5021
+ });
5022
+ derived.setAuthTokenGetter(() => transport.resolveToken());
5023
+ derived.setOnUnauthorized(() => onUnauthorized());
5024
+ return derived;
5025
+ },
4914
5026
  baseUrl: transport.baseUrl,
4915
5027
  apiPath: transport.apiPath,
4916
5028
  collection,