@rebasepro/client 0.25.0 → 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 +225 -50
- 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 +34 -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`.
|
|
@@ -2150,6 +2165,48 @@ function realtimeLostError() {
|
|
|
2150
2165
|
function realtimeDebugOn() {
|
|
2151
2166
|
return Reflect.get(globalThis, "__REBASE_DEBUG_REALTIME__") === true;
|
|
2152
2167
|
}
|
|
2168
|
+
/**
|
|
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.
|
|
2173
|
+
*/
|
|
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
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* The codes an `AUTHENTICATE` is refused with when the server will not honour
|
|
2194
|
+
* the session: the token does not verify or has expired, the session was
|
|
2195
|
+
* revoked or its account is gone (`SESSION_REVOKED`), the account is disabled
|
|
2196
|
+
* (`ACCOUNT_DISABLED`) — the codes the HTTP routes answer the same token with.
|
|
2197
|
+
* An answer, not a failure: asking again with the same token gets the same one.
|
|
2198
|
+
*/
|
|
2199
|
+
var SESSION_REFUSAL_CODES = /* @__PURE__ */ new Set([
|
|
2200
|
+
"INVALID_TOKEN",
|
|
2201
|
+
"TOKEN_EXPIRED",
|
|
2202
|
+
"JWT_EXPIRED",
|
|
2203
|
+
"SESSION_REVOKED",
|
|
2204
|
+
"ACCOUNT_DISABLED",
|
|
2205
|
+
"SESSION_ENDED"
|
|
2206
|
+
]);
|
|
2207
|
+
function isSessionRefusal(error) {
|
|
2208
|
+
return error instanceof RebaseApiError$1 && typeof error.code === "string" && SESSION_REFUSAL_CODES.has(error.code);
|
|
2209
|
+
}
|
|
2153
2210
|
/** The ceiling of the reconnect backoff. */
|
|
2154
2211
|
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
2155
2212
|
/**
|
|
@@ -2452,11 +2509,28 @@ var RebaseWebSocketClient = class {
|
|
|
2452
2509
|
onUnauthorized;
|
|
2453
2510
|
refreshInProgress = null;
|
|
2454
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
|
+
}
|
|
2455
2528
|
constructor(config) {
|
|
2456
2529
|
this.websocketUrl = config.websocketUrl;
|
|
2457
2530
|
this.getAuthToken = config.getAuthToken;
|
|
2458
2531
|
this.onUnauthorized = config.onUnauthorized;
|
|
2459
2532
|
this.impersonate = config.impersonate;
|
|
2533
|
+
this.debug = config.debug === true;
|
|
2460
2534
|
this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
2461
2535
|
}
|
|
2462
2536
|
/**
|
|
@@ -2495,7 +2569,7 @@ var RebaseWebSocketClient = class {
|
|
|
2495
2569
|
if (this.closedByCaller) return;
|
|
2496
2570
|
if (this.connectionState !== "reconnecting" && this.connectionState !== "disconnected") return;
|
|
2497
2571
|
if (this.ws) return;
|
|
2498
|
-
|
|
2572
|
+
this.connectionDebug(`${reason} — retrying the realtime connection`);
|
|
2499
2573
|
if (this.reconnectTimeout) {
|
|
2500
2574
|
clearTimeout(this.reconnectTimeout);
|
|
2501
2575
|
this.reconnectTimeout = null;
|
|
@@ -2527,6 +2601,39 @@ var RebaseWebSocketClient = class {
|
|
|
2527
2601
|
}
|
|
2528
2602
|
onlineListener = null;
|
|
2529
2603
|
visibilityListener = null;
|
|
2604
|
+
/** The session this socket's server refused, and why. Cleared when a socket opens. */
|
|
2605
|
+
refusedSession = null;
|
|
2606
|
+
/**
|
|
2607
|
+
* Whether `error` is the server refusing the session `token` signs in as —
|
|
2608
|
+
* revoked, its account gone or disabled, a token that does not verify. Said
|
|
2609
|
+
* once per socket and token, as a warning that names the code.
|
|
2610
|
+
*/
|
|
2611
|
+
noteSessionRefused(token, error) {
|
|
2612
|
+
if (!isSessionRefusal(error)) return false;
|
|
2613
|
+
if (this.refusedSession?.token !== token) {
|
|
2614
|
+
this.refusedSession = {
|
|
2615
|
+
token,
|
|
2616
|
+
error
|
|
2617
|
+
};
|
|
2618
|
+
console.warn(`[Rebase] Realtime: the server refused this session (${error.code}: ${error.message}). The socket is connected but not signed in, and what needs an account is refused with that code.`);
|
|
2619
|
+
}
|
|
2620
|
+
return true;
|
|
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
|
+
}
|
|
2530
2637
|
/**
|
|
2531
2638
|
* Authenticate the WebSocket connection
|
|
2532
2639
|
*/
|
|
@@ -2575,10 +2682,10 @@ var RebaseWebSocketClient = class {
|
|
|
2575
2682
|
if (this.isConnected && !this.isAuthenticated && !this.authPromise) this.getAuthToken().then((token) => {
|
|
2576
2683
|
if (!this.ws) return;
|
|
2577
2684
|
if (token) this.authenticate(token).catch((e) => {
|
|
2578
|
-
if (this.ws)
|
|
2685
|
+
if (this.ws) this.connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
|
|
2579
2686
|
});
|
|
2580
2687
|
}).catch((e) => {
|
|
2581
|
-
if (this.ws)
|
|
2688
|
+
if (this.ws) this.connectionDebug("WebSocket auto-auth skipped:", e?.message || e);
|
|
2582
2689
|
});
|
|
2583
2690
|
}
|
|
2584
2691
|
/**
|
|
@@ -2594,6 +2701,7 @@ var RebaseWebSocketClient = class {
|
|
|
2594
2701
|
if (permanent) this.removeNetworkListeners();
|
|
2595
2702
|
this.isAuthenticated = false;
|
|
2596
2703
|
this.authPromise = null;
|
|
2704
|
+
this.refusedSession = null;
|
|
2597
2705
|
if (this.reconnectTimeout) {
|
|
2598
2706
|
clearTimeout(this.reconnectTimeout);
|
|
2599
2707
|
this.reconnectTimeout = null;
|
|
@@ -2628,24 +2736,35 @@ var RebaseWebSocketClient = class {
|
|
|
2628
2736
|
const socket = new this.WebSocketConstructor(withOptIns(this.websocketUrl));
|
|
2629
2737
|
this.ws = socket;
|
|
2630
2738
|
this.ws.onopen = async () => {
|
|
2631
|
-
|
|
2739
|
+
this.connectionDebug("Connected to PostgreSQL backend");
|
|
2632
2740
|
const wasReconnect = this.hadConnection;
|
|
2633
2741
|
this.hadConnection = true;
|
|
2634
2742
|
this.isConnected = true;
|
|
2743
|
+
this.gaveUp = false;
|
|
2744
|
+
if (!this.outageReported) this.setState("connected");
|
|
2745
|
+
this.refusedSession = null;
|
|
2746
|
+
if (this.getAuthToken && !this.isAuthenticated) {
|
|
2747
|
+
let token = null;
|
|
2748
|
+
try {
|
|
2749
|
+
token = await this.getAuthToken();
|
|
2750
|
+
if (token) {
|
|
2751
|
+
await this.authenticate(token);
|
|
2752
|
+
this.connectionDebug("WebSocket auto-authenticated");
|
|
2753
|
+
}
|
|
2754
|
+
} catch (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);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (this.ws !== socket) return;
|
|
2635
2763
|
this.reconnectAttempts = 0;
|
|
2636
2764
|
this.clearOutageTimer();
|
|
2637
2765
|
this.outageReported = false;
|
|
2638
|
-
this.
|
|
2766
|
+
this.socketCeilingWarned = false;
|
|
2639
2767
|
this.setState("connected");
|
|
2640
|
-
if (this.getAuthToken && !this.isAuthenticated) try {
|
|
2641
|
-
const token = await this.getAuthToken();
|
|
2642
|
-
if (token) {
|
|
2643
|
-
await this.authenticate(token);
|
|
2644
|
-
console.debug("WebSocket auto-authenticated");
|
|
2645
|
-
}
|
|
2646
|
-
} catch (error) {
|
|
2647
|
-
console.debug("WebSocket connected without auth:", error?.message || error);
|
|
2648
|
-
}
|
|
2649
2768
|
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
2650
2769
|
this.processMessageQueue(wasReconnect);
|
|
2651
2770
|
if (wasReconnect) this.resubscribeAll();
|
|
@@ -2661,7 +2780,7 @@ var RebaseWebSocketClient = class {
|
|
|
2661
2780
|
}
|
|
2662
2781
|
};
|
|
2663
2782
|
const onClosed = () => {
|
|
2664
|
-
|
|
2783
|
+
this.connectionDebug("Disconnected from PostgreSQL backend");
|
|
2665
2784
|
this.stopLivenessWatch();
|
|
2666
2785
|
this.forgetServerOptIns();
|
|
2667
2786
|
if (this.ws === socket) this.ws = null;
|
|
@@ -2791,7 +2910,7 @@ var RebaseWebSocketClient = class {
|
|
|
2791
2910
|
this.reconnectAttempts = Math.min(this.reconnectAttempts + 1, 30);
|
|
2792
2911
|
const ceiling = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_DELAY_MS);
|
|
2793
2912
|
const delay = MIN_RECONNECT_DELAY_MS + Math.floor(Math.random() * (ceiling - MIN_RECONNECT_DELAY_MS));
|
|
2794
|
-
|
|
2913
|
+
this.connectionDebug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
2795
2914
|
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
2796
2915
|
this.reconnectTimeout = setTimeout(() => {
|
|
2797
2916
|
this.reconnectTimeout = null;
|
|
@@ -2817,11 +2936,11 @@ var RebaseWebSocketClient = class {
|
|
|
2817
2936
|
if (this.outageTimer) clearTimeout(this.outageTimer);
|
|
2818
2937
|
this.outageTimer = null;
|
|
2819
2938
|
}
|
|
2820
|
-
reportConnectionLost() {
|
|
2939
|
+
reportConnectionLost(refusal) {
|
|
2821
2940
|
if (this.outageReported) return;
|
|
2822
2941
|
this.outageReported = true;
|
|
2823
2942
|
this.setState("disconnected");
|
|
2824
|
-
const error = realtimeLostError();
|
|
2943
|
+
const error = realtimeLostError(refusal);
|
|
2825
2944
|
for (const subscription of this.collectionSubscriptions.values()) this.notifyError(subscription.callbacks, error);
|
|
2826
2945
|
for (const subscription of this.singleSubscriptions.values()) this.notifyError(subscription.callbacks, error);
|
|
2827
2946
|
for (const handler of [...this.connectionLostHandlers]) try {
|
|
@@ -3060,31 +3179,36 @@ var RebaseWebSocketClient = class {
|
|
|
3060
3179
|
}
|
|
3061
3180
|
async runAuthentication(retryCount) {
|
|
3062
3181
|
let lastError = null;
|
|
3063
|
-
for (let attempt = 0; attempt < retryCount; attempt++)
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
throw error;
|
|
3075
|
-
|
|
3076
|
-
|
|
3182
|
+
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
3183
|
+
let token = null;
|
|
3184
|
+
try {
|
|
3185
|
+
token = await this.getAuthToken();
|
|
3186
|
+
if (!token) throw new Error("user not logged in");
|
|
3187
|
+
if (this.refusedSession?.token === token) throw this.refusedSession.error;
|
|
3188
|
+
await this.authenticate(token);
|
|
3189
|
+
this.connectionDebug("WebSocket authenticated on demand");
|
|
3190
|
+
return;
|
|
3191
|
+
} catch (error) {
|
|
3192
|
+
lastError = error;
|
|
3193
|
+
if (token && this.noteSessionRefused(token, error)) throw error;
|
|
3194
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
3195
|
+
if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
|
|
3196
|
+
console.warn("WebSocket auth failed: user not logged in");
|
|
3197
|
+
throw error;
|
|
3198
|
+
}
|
|
3199
|
+
if (errMsg.includes("still loading")) {
|
|
3200
|
+
if (attempt < retryCount - 1) {
|
|
3201
|
+
const delay = Math.min(500 * (attempt + 1), 2e3);
|
|
3202
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3077
3206
|
if (attempt < retryCount - 1) {
|
|
3078
|
-
const delay = Math.min(
|
|
3207
|
+
const delay = Math.min(1e3 * (attempt + 1), 3e3);
|
|
3208
|
+
this.connectionDebug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
3079
3209
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3080
|
-
continue;
|
|
3081
3210
|
}
|
|
3082
3211
|
}
|
|
3083
|
-
if (attempt < retryCount - 1) {
|
|
3084
|
-
const delay = Math.min(1e3 * (attempt + 1), 3e3);
|
|
3085
|
-
console.debug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
3086
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3087
|
-
}
|
|
3088
3212
|
}
|
|
3089
3213
|
console.warn("WebSocket on-demand auth failed after retries:", lastError);
|
|
3090
3214
|
throw lastError;
|
|
@@ -3096,7 +3220,7 @@ var RebaseWebSocketClient = class {
|
|
|
3096
3220
|
const token = await this.getAuthToken();
|
|
3097
3221
|
if (!token) throw new Error("user not logged in");
|
|
3098
3222
|
await this.authenticate(token);
|
|
3099
|
-
|
|
3223
|
+
this.connectionDebug("WebSocket reauthenticated successfully");
|
|
3100
3224
|
} catch (error) {
|
|
3101
3225
|
console.error("WebSocket reauthentication failed:", error);
|
|
3102
3226
|
throw error;
|
|
@@ -3170,6 +3294,10 @@ var RebaseWebSocketClient = class {
|
|
|
3170
3294
|
await this.ensureAuthenticated();
|
|
3171
3295
|
} catch (error) {
|
|
3172
3296
|
if (!(isSubscriptionFrame && isNotSignedIn(error))) {
|
|
3297
|
+
if (error instanceof RebaseApiError$1) {
|
|
3298
|
+
reject(error);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3173
3301
|
reject(new RebaseApiError$1(error instanceof Error ? error.message : "Authentication required"));
|
|
3174
3302
|
return;
|
|
3175
3303
|
}
|
|
@@ -3782,7 +3910,7 @@ var RebaseWebSocketClient = class {
|
|
|
3782
3910
|
* we need to re-register everything to resume receiving updates.
|
|
3783
3911
|
*/
|
|
3784
3912
|
resubscribeAll() {
|
|
3785
|
-
|
|
3913
|
+
this.connectionDebug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
|
|
3786
3914
|
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
3787
3915
|
if (sub.subscribeInFlight) continue;
|
|
3788
3916
|
const oldBackendId = sub.backendSubscriptionId;
|
|
@@ -4072,12 +4200,28 @@ var RebaseRealtimeChannel = class {
|
|
|
4072
4200
|
*/
|
|
4073
4201
|
async broadcast(event, payload) {
|
|
4074
4202
|
await this.join();
|
|
4203
|
+
this.rememberOwnSender(this.transport.socketClientId);
|
|
4075
4204
|
await this.send("broadcast", {
|
|
4076
4205
|
event,
|
|
4077
4206
|
payload
|
|
4078
4207
|
});
|
|
4079
4208
|
}
|
|
4080
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
|
+
/**
|
|
4081
4225
|
* Observe refusals about this channel.
|
|
4082
4226
|
*
|
|
4083
4227
|
* `CHANNEL_FORBIDDEN` when an authorizer refuses a join, a broadcast or a
|
|
@@ -4250,7 +4394,7 @@ var RebaseRealtimeChannel = class {
|
|
|
4250
4394
|
for (const entry of entries) {
|
|
4251
4395
|
if (entry.seq <= this.lastSeq) continue;
|
|
4252
4396
|
this.lastSeq = entry.seq;
|
|
4253
|
-
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;
|
|
4254
4398
|
this.deliver({
|
|
4255
4399
|
event: entry.event,
|
|
4256
4400
|
payload: entry.payload,
|
|
@@ -4399,7 +4543,7 @@ var LazyOfflineManager = class {
|
|
|
4399
4543
|
keepsLastUser;
|
|
4400
4544
|
constructor(config, createInner) {
|
|
4401
4545
|
this.keepsLastUser = config.keepLastUser === true;
|
|
4402
|
-
import("./offline-
|
|
4546
|
+
import("./offline-AvuEcg7u.js").then(({ OfflineManager: Engine }) => {
|
|
4403
4547
|
const engine = new Engine(config, createInner);
|
|
4404
4548
|
this.engine = engine;
|
|
4405
4549
|
const queued = this.queued;
|
|
@@ -4474,6 +4618,9 @@ var LazyOfflineManager = class {
|
|
|
4474
4618
|
setScope(uid, options) {
|
|
4475
4619
|
this.run((engine) => engine.setScope(uid, options)).catch(() => void 0);
|
|
4476
4620
|
}
|
|
4621
|
+
onReachedUnconfirmed(listener) {
|
|
4622
|
+
return this.subscribe((engine) => engine.onReachedUnconfirmed(listener));
|
|
4623
|
+
}
|
|
4477
4624
|
async resumeLastUser() {
|
|
4478
4625
|
await await this.run((engine) => engine.resumeLastUser());
|
|
4479
4626
|
}
|
|
@@ -4631,7 +4778,8 @@ function createRebaseClient(options) {
|
|
|
4631
4778
|
websocketUrl: resolvedWsUrl,
|
|
4632
4779
|
getAuthToken: () => transport.resolveToken(),
|
|
4633
4780
|
onUnauthorized: () => onUnauthorized(),
|
|
4634
|
-
impersonate: options.impersonate
|
|
4781
|
+
impersonate: options.impersonate,
|
|
4782
|
+
debug: options.debug
|
|
4635
4783
|
});
|
|
4636
4784
|
let realtimeUid = auth.getSession()?.user?.uid ?? null;
|
|
4637
4785
|
auth.onAuthStateChange((event, session) => {
|
|
@@ -4708,15 +4856,19 @@ function createRebaseClient(options) {
|
|
|
4708
4856
|
});
|
|
4709
4857
|
if (offlineManager.keepsLastUser) {
|
|
4710
4858
|
let confirming = false;
|
|
4711
|
-
|
|
4712
|
-
if (
|
|
4859
|
+
const confirm = () => {
|
|
4860
|
+
if (confirming || auth.getSession()) return;
|
|
4713
4861
|
confirming = true;
|
|
4714
4862
|
auth.refreshSession().catch((error) => {
|
|
4715
4863
|
if (error instanceof RebaseApiError && error.status !== void 0 && error.status >= 400 && error.status < 500 && !auth.getSession()) offlineManager.setScope(void 0);
|
|
4716
4864
|
}).finally(() => {
|
|
4717
4865
|
confirming = false;
|
|
4718
4866
|
});
|
|
4867
|
+
};
|
|
4868
|
+
offlineManager.api.onStatusChange((status) => {
|
|
4869
|
+
if (status.online && status.unverifiedUser) confirm();
|
|
4719
4870
|
});
|
|
4871
|
+
offlineManager.onReachedUnconfirmed(confirm);
|
|
4720
4872
|
}
|
|
4721
4873
|
}
|
|
4722
4874
|
/**
|
|
@@ -4777,11 +4929,12 @@ function createRebaseClient(options) {
|
|
|
4777
4929
|
}
|
|
4778
4930
|
});
|
|
4779
4931
|
}
|
|
4932
|
+
const slug = toSnakeCase(prop);
|
|
4780
4933
|
if (!untypedWarned) {
|
|
4781
4934
|
untypedWarned = true;
|
|
4782
|
-
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>").`);
|
|
4783
4936
|
}
|
|
4784
|
-
return collection(
|
|
4937
|
+
return collection(slug);
|
|
4785
4938
|
},
|
|
4786
4939
|
has(_target, prop) {
|
|
4787
4940
|
if (prop === "collection") return true;
|
|
@@ -4848,6 +5001,28 @@ channel: (name, options) => {
|
|
|
4848
5001
|
onUnauthorized = handler;
|
|
4849
5002
|
},
|
|
4850
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
|
+
},
|
|
4851
5026
|
baseUrl: transport.baseUrl,
|
|
4852
5027
|
apiPath: transport.apiPath,
|
|
4853
5028
|
collection,
|