@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 +2 -1
- package/dist/admin.d.ts +8 -0
- package/dist/index.d.ts +36 -6
- package/dist/index.es.js +149 -37
- package/dist/index.es.js.map +1 -1
- package/dist/{offline-BrqCRgih.js → offline-AvuEcg7u.js} +122 -36
- package/dist/offline-AvuEcg7u.js.map +1 -0
- package/dist/offline-lazy.d.ts +1 -0
- package/dist/offline-store-BhYXViha.js.map +1 -1
- package/dist/offline-store.d.ts +10 -0
- package/dist/offline.d.ts +26 -1
- package/dist/realtime-channel.d.ts +8 -0
- package/dist/websocket.d.ts +26 -0
- package/package.json +4 -4
- package/dist/offline-BrqCRgih.js.map +0 -1
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 `
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
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
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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 (
|
|
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-
|
|
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
|
-
|
|
4775
|
-
if (
|
|
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
|
|
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(
|
|
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,
|