@rebasepro/client 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-keys.d.ts +10 -38
- package/dist/auth.d.ts +3 -5
- package/dist/functions.d.ts +2 -8
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +172 -24
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +8 -8
- package/dist/transport.d.ts +35 -1
- package/dist/websocket.d.ts +19 -0
- package/package.json +8 -8
- package/src/anonymous-client-guard.test.ts +190 -0
- package/src/api-keys.ts +21 -44
- package/src/auth.ts +30 -5
- package/src/client-close.test.ts +80 -0
- package/src/collection.test.ts +51 -11
- package/src/collection.ts +21 -1
- package/src/data-proxy.test.ts +22 -6
- package/src/functions.ts +11 -9
- package/src/index.ts +35 -12
- package/src/offline-idb-store.test.ts +58 -4
- package/src/offline-integration.test.ts +5 -0
- package/src/offline.ts +9 -1
- package/src/realtime-channel.ts +8 -8
- package/src/realtime-concurrent-subscribe.test.ts +102 -0
- package/src/realtime-optout.test.ts +38 -4
- package/src/transport.ts +123 -1
- package/src/websocket.ts +82 -12
package/dist/api-keys.d.ts
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
import type { Transport } from "./transport";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
permissions: ApiKeyPermission[];
|
|
13
|
-
admin: boolean;
|
|
14
|
-
rate_limit: number | null;
|
|
15
|
-
created_by: string;
|
|
16
|
-
created_at: string;
|
|
17
|
-
updated_at: string;
|
|
18
|
-
last_used_at: string | null;
|
|
19
|
-
expires_at: string | null;
|
|
20
|
-
revoked_at: string | null;
|
|
21
|
-
}
|
|
22
|
-
/** An API key including the full secret (returned only on creation). */
|
|
23
|
-
export interface ApiKeyWithSecret extends ApiKeyMasked {
|
|
24
|
-
key: string;
|
|
25
|
-
}
|
|
26
|
-
/** Payload for creating a new API key. */
|
|
27
|
-
export interface CreateApiKeyRequest {
|
|
28
|
-
name: string;
|
|
29
|
-
permissions: ApiKeyPermission[];
|
|
30
|
-
rate_limit?: number | null;
|
|
31
|
-
expires_at?: string | null;
|
|
32
|
-
}
|
|
33
|
-
/** Payload for updating an existing API key. */
|
|
34
|
-
export interface UpdateApiKeyRequest {
|
|
35
|
-
name?: string;
|
|
36
|
-
permissions?: ApiKeyPermission[];
|
|
37
|
-
rate_limit?: number | null;
|
|
38
|
-
expires_at?: string | null;
|
|
39
|
-
}
|
|
2
|
+
/**
|
|
3
|
+
* These were re-declared here, under a comment saying they lived in the server
|
|
4
|
+
* package rather than in `@rebasepro/types`. That stopped being true, and the
|
|
5
|
+
* copy drifted: it never gained `admin`, the flag that grants a key the `admin`
|
|
6
|
+
* role — admin routes plus the RLS `default_admin` policies — so the SDK could
|
|
7
|
+
* describe every kind of key except a privileged one, and
|
|
8
|
+
* `createKey({ …, admin: true })` was an excess-property error.
|
|
9
|
+
*/
|
|
10
|
+
export type { ApiKeyPermission, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "@rebasepro/types";
|
|
11
|
+
import type { ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, UpdateApiKeyRequest } from "@rebasepro/types";
|
|
40
12
|
/** Options for the `createApiKeys` factory. */
|
|
41
13
|
export interface CreateApiKeysOptions {
|
|
42
14
|
apiKeysPath?: string;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { Transport } from "./transport";
|
|
2
|
-
import type { AuthChangeEvent, RebaseSession,
|
|
2
|
+
import type { AuthChangeEvent, RebaseSession, DeviceSession, User } from "@rebasepro/types";
|
|
3
3
|
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
4
|
-
/** @deprecated Use `User` from `@rebasepro/types` instead. */
|
|
5
|
-
export type RebaseUser = User;
|
|
6
|
-
/** @deprecated Use `AuthTokens` from `@rebasepro/types` instead. */
|
|
7
|
-
export type RebaseTokens = AuthTokens;
|
|
8
4
|
/** Minimal, non-sensitive user profile returned by {@link findUserByEmail}. */
|
|
9
5
|
export interface PublicUserProfile {
|
|
10
6
|
uid: string;
|
|
@@ -128,6 +124,7 @@ export declare function createAuth(transport: Transport, options?: CreateAuthOpt
|
|
|
128
124
|
refreshToken: string;
|
|
129
125
|
}>;
|
|
130
126
|
signOut: () => Promise<void>;
|
|
127
|
+
stopAutoRefresh: () => void;
|
|
131
128
|
refreshSession: () => Promise<RebaseSession>;
|
|
132
129
|
handleUnauthorized: () => Promise<boolean>;
|
|
133
130
|
getUser: () => Promise<User>;
|
|
@@ -180,6 +177,7 @@ export declare function createAuth(transport: Transport, options?: CreateAuthOpt
|
|
|
180
177
|
getAuthConfig: () => Promise<AuthConfig>;
|
|
181
178
|
getSession: () => RebaseSession | null;
|
|
182
179
|
onAuthStateChange: (callback: (event: AuthChangeEvent, session: RebaseSession | null) => void) => () => boolean;
|
|
180
|
+
canRestoreSession: () => boolean;
|
|
183
181
|
isInitialized: () => Promise<void>;
|
|
184
182
|
};
|
|
185
183
|
export interface CookieStorageOptions {
|
package/dist/functions.d.ts
CHANGED
|
@@ -27,14 +27,8 @@ export interface FunctionsClient {
|
|
|
27
27
|
*/
|
|
28
28
|
invoke<T = unknown>(name: string, payload?: unknown, options?: FunctionInvokeOptions): Promise<T>;
|
|
29
29
|
}
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
33
|
-
/** Sub-path appended after the function name, e.g. `"status/123"`. */
|
|
34
|
-
path?: string;
|
|
35
|
-
/** Extra headers merged into the request (auth is still injected automatically). */
|
|
36
|
-
headers?: Record<string, string>;
|
|
37
|
-
}
|
|
30
|
+
export type { FunctionInvokeOptions } from "@rebasepro/types";
|
|
31
|
+
import type { FunctionInvokeOptions } from "@rebasepro/types";
|
|
38
32
|
/**
|
|
39
33
|
* Create a `FunctionsClient` backed by the given transport.
|
|
40
34
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -15,12 +15,13 @@ export { RebaseClientError } from "./errors";
|
|
|
15
15
|
export type { RebaseClientConfig, FindParams, FindResponse } from "./transport";
|
|
16
16
|
export type { CollectionClient } from "./collection";
|
|
17
17
|
export type { FindResult, SDKCollectionClient, SDKQueryBuilderInterface, PaginationMeta } from "@rebasepro/types";
|
|
18
|
+
export type { IterateParams, FindAllParams, PageWalkOptions, CursorSpec } from "@rebasepro/types";
|
|
19
|
+
export { RebasePaginationError } from "@rebasepro/common";
|
|
20
|
+
export type { PaginationErrorCode } from "@rebasepro/common";
|
|
18
21
|
export { QueryBuilder, or, and, cond } from "@rebasepro/common";
|
|
19
22
|
export { createCookieStorage, createMemoryStorage } from "./auth";
|
|
20
23
|
export type { AuthConfig, AuthStorage, CookieStorageOptions, CreateAuthOptions } from "./auth";
|
|
21
|
-
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
22
|
-
/** @deprecated Import `User` / `AuthTokens` from `@rebasepro/types` instead. */
|
|
23
|
-
export type { RebaseUser, RebaseTokens } from "./auth";
|
|
24
|
+
export type { User, RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
24
25
|
export type { CreateAdminOptions } from "./admin";
|
|
25
26
|
export type { AdminUser } from "./admin";
|
|
26
27
|
export type { CreateCronOptions } from "./cron";
|
|
@@ -117,11 +118,17 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
117
118
|
channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
|
|
118
119
|
};
|
|
119
120
|
/**
|
|
120
|
-
* Release
|
|
121
|
+
* Release everything this client holds that can keep a process alive: the
|
|
122
|
+
* realtime socket and its reconnect timer, channel presence heartbeats, the
|
|
123
|
+
* offline manager, and the scheduled token refresh.
|
|
121
124
|
*
|
|
122
|
-
*
|
|
123
|
-
* call this will not exit
|
|
124
|
-
*
|
|
125
|
+
* Each of those keeps the Node event loop alive on its own, so a script
|
|
126
|
+
* that does not call this will not exit — and, until the refresh timer was
|
|
127
|
+
* included, one that *did* call it still would not if it had signed in.
|
|
128
|
+
*
|
|
129
|
+
* Safe when realtime was never started (`realtime: false`), safe when
|
|
130
|
+
* signed out, and safe to call twice. It does not sign the user out: a
|
|
131
|
+
* persisted session survives for the next client to restore.
|
|
125
132
|
*/
|
|
126
133
|
close: () => void;
|
|
127
134
|
storage: StorageSource;
|
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, Vector, isPublicStoragePath, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
-
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, and, buildCompositeId, cond, or, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
1
|
+
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, Vector, isPublicStoragePath, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
+
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, or, paginateFind, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
@@ -30,6 +30,57 @@ function rebaseReviver(_key, value) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/transport.ts
|
|
33
|
+
/**
|
|
34
|
+
* True when there is no browser to have signed a user in — a Node script, a
|
|
35
|
+
* cron job, an edge worker.
|
|
36
|
+
*
|
|
37
|
+
* Anonymous is an ordinary, correct state in a browser: before sign-in, on a
|
|
38
|
+
* marketing page, for public reads. Warning there would be noise that teaches
|
|
39
|
+
* people to ignore warnings, so the guard is off entirely. This uses the same
|
|
40
|
+
* `typeof window` test as {@link resolveBaseUrl}, and additionally treats a
|
|
41
|
+
* defined `document` as a browser so an SSR shim or test harness that installs
|
|
42
|
+
* only one of the two is still excluded.
|
|
43
|
+
*/
|
|
44
|
+
function isServerLikeEnvironment() {
|
|
45
|
+
return typeof window === "undefined" && typeof document === "undefined";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Emitted once per client. Kept as a constant so the wording is testable and
|
|
49
|
+
* greppable — this is the string a user will paste into a search.
|
|
50
|
+
*/
|
|
51
|
+
var ANONYMOUS_SERVER_CLIENT_WARNING = "[rebase] This client was created outside a browser with no credential — no `token`, no auth token getter, and no cookie auth flow — so every request runs as an anonymous caller. Row-level security will return only publicly readable rows, which is usually nothing and occasionally the wrong thing. Inside a cron or function handler, use the `client` you were handed instead of building a new one: its data plane is already admin-scoped. In a standalone script or job, pass the service key as `token`. If you really do want anonymous access, pass `anonymous: true` to silence this.";
|
|
52
|
+
/**
|
|
53
|
+
* Refuse a filter whose *value* is missing.
|
|
54
|
+
*
|
|
55
|
+
* `where: { status: ["==", undefined] }` used to serialize to the literal
|
|
56
|
+
* string, so `status=eq.undefined` went out on the wire and the server dutifully
|
|
57
|
+
* looked for rows whose status is the four-letter word "undefined". The caller
|
|
58
|
+
* saw an empty page, not an error — the classic shape of a variable that was
|
|
59
|
+
* never set.
|
|
60
|
+
*
|
|
61
|
+
* Dropping the condition instead would be worse than sending it: the query
|
|
62
|
+
* would come back *unfiltered*, which for an ownership or tenant filter means
|
|
63
|
+
* returning rows the caller never asked to see. So this is a hard error, and
|
|
64
|
+
* both correct spellings are named in the message: omit the key to skip the
|
|
65
|
+
* filter, or use `["is-null", null]` to match SQL NULL (which still
|
|
66
|
+
* serializes — `null` is a value, `undefined` is the absence of one).
|
|
67
|
+
*/
|
|
68
|
+
function assertNoUndefinedFilterValues(where) {
|
|
69
|
+
const reject = (field, op) => {
|
|
70
|
+
throw new RebaseClientError$1(`Filter on "${field}" has an undefined value (["${String(op)}", undefined]). Omit "${field}" from \`where\` to skip the filter, or use ["is-null", null] to match SQL NULL.`);
|
|
71
|
+
};
|
|
72
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
73
|
+
if (condition === void 0) continue;
|
|
74
|
+
if (!Array.isArray(condition)) continue;
|
|
75
|
+
const tuples = Array.isArray(condition[0]) ? condition : [condition];
|
|
76
|
+
for (const tuple of tuples) {
|
|
77
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) continue;
|
|
78
|
+
const [op, value] = tuple;
|
|
79
|
+
if (value === void 0) reject(field, op);
|
|
80
|
+
if (Array.isArray(value) && value.some((v) => v === void 0)) reject(field, op);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
33
84
|
function buildQueryString(params) {
|
|
34
85
|
if (!params) return "";
|
|
35
86
|
const parts = [];
|
|
@@ -48,6 +99,7 @@ function buildQueryString(params) {
|
|
|
48
99
|
parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
|
|
49
100
|
}
|
|
50
101
|
if (params.where) {
|
|
102
|
+
assertNoUndefinedFilterValues(params.where);
|
|
51
103
|
const serialized = serializeFilter(params.where);
|
|
52
104
|
for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
|
|
53
105
|
else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
|
|
@@ -76,12 +128,31 @@ function resolveBaseUrl(configured) {
|
|
|
76
128
|
if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
|
|
77
129
|
return "";
|
|
78
130
|
}
|
|
79
|
-
function createTransport(config) {
|
|
131
|
+
function createTransport(config, environment) {
|
|
80
132
|
const fetchFn = config.fetch || globalThis.fetch;
|
|
81
133
|
const apiPath = config.apiPath || "/api";
|
|
82
134
|
let token = config.token;
|
|
83
135
|
let tokenGetter;
|
|
84
136
|
let onUnauthorizedHandler = config.onUnauthorized;
|
|
137
|
+
/** Once per client, never per request — log spam is its own bug. */
|
|
138
|
+
let anonymousWarningIssued = false;
|
|
139
|
+
/**
|
|
140
|
+
* Warn a server-side caller that it built a client that can only ever be
|
|
141
|
+
* anonymous. Deliberately checked at the *first request* rather than at
|
|
142
|
+
* construction: `setToken()` / `setAuthTokenGetter()` and a server-side
|
|
143
|
+
* `auth.signIn…()` (which calls `transport.setToken`) all land after the
|
|
144
|
+
* constructor, and warning at construction would fire on every one of them.
|
|
145
|
+
*/
|
|
146
|
+
function warnIfAnonymousServerClient(activeToken) {
|
|
147
|
+
if (anonymousWarningIssued) return;
|
|
148
|
+
if (activeToken) return;
|
|
149
|
+
if (tokenGetter) return;
|
|
150
|
+
if (config.anonymous) return;
|
|
151
|
+
if (environment?.credentialOutOfBand) return;
|
|
152
|
+
if (!isServerLikeEnvironment()) return;
|
|
153
|
+
anonymousWarningIssued = true;
|
|
154
|
+
console.warn(ANONYMOUS_SERVER_CLIENT_WARNING);
|
|
155
|
+
}
|
|
85
156
|
function getHeaders(activeToken, init) {
|
|
86
157
|
return {
|
|
87
158
|
"Content-Type": "application/json",
|
|
@@ -96,6 +167,7 @@ function createTransport(config) {
|
|
|
96
167
|
const fetched = await tokenGetter();
|
|
97
168
|
if (fetched !== null && fetched !== void 0) activeToken = fetched;
|
|
98
169
|
} catch (e) {}
|
|
170
|
+
warnIfAnonymousServerClient(activeToken);
|
|
99
171
|
const headers = getHeaders(activeToken, init);
|
|
100
172
|
if (init?.body instanceof FormData) delete headers["Content-Type"];
|
|
101
173
|
const res = await fetchFn(url, {
|
|
@@ -379,6 +451,29 @@ function createAuth(transport, options) {
|
|
|
379
451
|
attemptScheduledRefresh(0);
|
|
380
452
|
}, delay);
|
|
381
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Stop the scheduled token refresh, leaving the session itself alone.
|
|
456
|
+
*
|
|
457
|
+
* This is teardown, not sign-out. `scheduleRefresh` arms an ordinary
|
|
458
|
+
* `setTimeout` up to a token lifetime away, and it is not `unref`'d — so on
|
|
459
|
+
* Node it holds the event loop open by itself. `client.close()` promised
|
|
460
|
+
* that "a script that does not call this will not exit on its own", which
|
|
461
|
+
* was true, while the converse it plainly implies was not: a signed-in
|
|
462
|
+
* client that closed its socket still hung, because this timer outlived it.
|
|
463
|
+
* Any script, cron handler or job that signs in hit that.
|
|
464
|
+
*
|
|
465
|
+
* Deliberately does NOT clear the session, touch storage, or emit
|
|
466
|
+
* SIGNED_OUT. Closing a client is not the user signing out — `signOut()`
|
|
467
|
+
* POSTs /logout and revokes the whole sign-in, which is the wrong hammer
|
|
468
|
+
* (see `abandonSessionLocally`) — and a persisted session must still be
|
|
469
|
+
* there for the next client to restore.
|
|
470
|
+
*/
|
|
471
|
+
function stopAutoRefresh() {
|
|
472
|
+
if (refreshTimeout) {
|
|
473
|
+
clearTimeout(refreshTimeout);
|
|
474
|
+
refreshTimeout = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
382
477
|
function handleAuthResponse(data, event) {
|
|
383
478
|
const user = mapRawUser(data.user);
|
|
384
479
|
const session = {
|
|
@@ -849,6 +944,7 @@ function createAuth(transport, options) {
|
|
|
849
944
|
signInWithSlack,
|
|
850
945
|
signInWithSpotify,
|
|
851
946
|
signOut,
|
|
947
|
+
stopAutoRefresh,
|
|
852
948
|
refreshSession,
|
|
853
949
|
handleUnauthorized,
|
|
854
950
|
getUser,
|
|
@@ -868,6 +964,7 @@ function createAuth(transport, options) {
|
|
|
868
964
|
getAuthConfig,
|
|
869
965
|
getSession,
|
|
870
966
|
onAuthStateChange,
|
|
967
|
+
canRestoreSession: () => persistSession || authFlowMode === "cookie",
|
|
871
968
|
isInitialized: () => isInitialized
|
|
872
969
|
};
|
|
873
970
|
}
|
|
@@ -1184,6 +1281,12 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1184
1281
|
meta: raw.meta
|
|
1185
1282
|
};
|
|
1186
1283
|
},
|
|
1284
|
+
iterate(params) {
|
|
1285
|
+
return paginateFind((p) => client.find(p), params, slug);
|
|
1286
|
+
},
|
|
1287
|
+
findAll(params) {
|
|
1288
|
+
return collectAllPages((p) => client.find(p), params, slug);
|
|
1289
|
+
},
|
|
1187
1290
|
async findById(id) {
|
|
1188
1291
|
try {
|
|
1189
1292
|
const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
|
|
@@ -1227,7 +1330,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1227
1330
|
const qs = buildQueryString({
|
|
1228
1331
|
...params,
|
|
1229
1332
|
limit: void 0,
|
|
1230
|
-
offset: void 0
|
|
1333
|
+
offset: void 0,
|
|
1334
|
+
include: void 0
|
|
1231
1335
|
});
|
|
1232
1336
|
return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
|
|
1233
1337
|
},
|
|
@@ -1373,7 +1477,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1373
1477
|
function createFunctionsClient(transport) {
|
|
1374
1478
|
return { async invoke(name, payload, options) {
|
|
1375
1479
|
const method = options?.method ?? "POST";
|
|
1376
|
-
const
|
|
1480
|
+
const rawPath = options?.path;
|
|
1481
|
+
const subPath = rawPath ? /^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}` : "";
|
|
1377
1482
|
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
1378
1483
|
const init = { method };
|
|
1379
1484
|
if (payload !== void 0 && method !== "GET") init.body = JSON.stringify(payload);
|
|
@@ -1587,7 +1692,7 @@ function extractMessageError(message) {
|
|
|
1587
1692
|
* Fire-and-forget (the server sends no response envelope), and exempt from the
|
|
1588
1693
|
* client-side auth gate — a public channel is usable without an account.
|
|
1589
1694
|
*/
|
|
1590
|
-
var CHANNEL_MESSAGE_TYPES = new Set([
|
|
1695
|
+
var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
1591
1696
|
"join_channel",
|
|
1592
1697
|
"leave_channel",
|
|
1593
1698
|
"broadcast",
|
|
@@ -1616,6 +1721,17 @@ var RebaseWebSocketClient = class {
|
|
|
1616
1721
|
/** Set by `close()`. Blocks any later operation from silently redialling. */
|
|
1617
1722
|
closedByCaller = false;
|
|
1618
1723
|
/**
|
|
1724
|
+
* Set when the backoff budget ran out, cleared by anything that earns a
|
|
1725
|
+
* fresh one.
|
|
1726
|
+
*
|
|
1727
|
+
* Unlike {@link closedByCaller} this is not final — nobody *asked* for the
|
|
1728
|
+
* socket to stay down. Five attempts with exponential backoff is about a
|
|
1729
|
+
* minute, which a laptop lid, a wifi handover or a backend rollout all
|
|
1730
|
+
* exceed routinely; treating that as permanent meant realtime silently
|
|
1731
|
+
* stopped for the rest of the page's life, with a reload the only cure.
|
|
1732
|
+
*/
|
|
1733
|
+
gaveUp = false;
|
|
1734
|
+
/**
|
|
1619
1735
|
* Whether a socket exists at all (open or still opening).
|
|
1620
1736
|
*
|
|
1621
1737
|
* Lets callers distinguish "authenticate the live socket" from "there is
|
|
@@ -1688,18 +1804,37 @@ var RebaseWebSocketClient = class {
|
|
|
1688
1804
|
}
|
|
1689
1805
|
return;
|
|
1690
1806
|
}
|
|
1807
|
+
this.installOnlineListener();
|
|
1691
1808
|
if (this.ws || this.reconnectTimeout) return;
|
|
1809
|
+
if (this.gaveUp) {
|
|
1810
|
+
this.gaveUp = false;
|
|
1811
|
+
this.reconnectAttempts = 0;
|
|
1812
|
+
}
|
|
1692
1813
|
this.initWebSocket();
|
|
1693
1814
|
}
|
|
1694
1815
|
/**
|
|
1816
|
+
* The browser says the network is back — the usual reason the budget ran
|
|
1817
|
+
* out in the first place. Registered lazily so a Node client, or a page
|
|
1818
|
+
* that never subscribes, adds no listener.
|
|
1819
|
+
*/
|
|
1820
|
+
installOnlineListener() {
|
|
1821
|
+
if (this.onlineListener || typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
1822
|
+
this.onlineListener = () => {
|
|
1823
|
+
if (this.closedByCaller || !this.gaveUp) return;
|
|
1824
|
+
console.debug("Network is back — retrying the realtime connection");
|
|
1825
|
+
this.ensureConnected();
|
|
1826
|
+
};
|
|
1827
|
+
window.addEventListener("online", this.onlineListener);
|
|
1828
|
+
}
|
|
1829
|
+
onlineListener = null;
|
|
1830
|
+
/**
|
|
1695
1831
|
* Authenticate the WebSocket connection
|
|
1696
1832
|
*/
|
|
1697
1833
|
async authenticate(token) {
|
|
1698
1834
|
return new Promise((resolve, reject) => {
|
|
1699
|
-
const requestId = `auth_${Date.now()}`;
|
|
1835
|
+
const requestId = `auth_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1700
1836
|
const timeout = setTimeout(() => {
|
|
1701
1837
|
this.pendingRequests.delete(requestId);
|
|
1702
|
-
this.authPromise = null;
|
|
1703
1838
|
reject(/* @__PURE__ */ new Error("Authentication timeout"));
|
|
1704
1839
|
}, 3e4);
|
|
1705
1840
|
this.pendingRequests.set(requestId, {
|
|
@@ -1749,6 +1884,10 @@ var RebaseWebSocketClient = class {
|
|
|
1749
1884
|
*/
|
|
1750
1885
|
disconnect(permanent = false) {
|
|
1751
1886
|
if (permanent) this.closedByCaller = true;
|
|
1887
|
+
if (permanent && this.onlineListener && typeof window !== "undefined") {
|
|
1888
|
+
window.removeEventListener("online", this.onlineListener);
|
|
1889
|
+
this.onlineListener = null;
|
|
1890
|
+
}
|
|
1752
1891
|
this.isAuthenticated = false;
|
|
1753
1892
|
this.authPromise = null;
|
|
1754
1893
|
if (this.reconnectTimeout) {
|
|
@@ -1773,7 +1912,8 @@ var RebaseWebSocketClient = class {
|
|
|
1773
1912
|
this.ws = null;
|
|
1774
1913
|
}
|
|
1775
1914
|
try {
|
|
1776
|
-
|
|
1915
|
+
const socket = new this.WebSocketConstructor(this.websocketUrl);
|
|
1916
|
+
this.ws = socket;
|
|
1777
1917
|
this.ws.onopen = async () => {
|
|
1778
1918
|
console.debug("Connected to PostgreSQL backend");
|
|
1779
1919
|
const wasReconnect = this.reconnectAttempts > 0;
|
|
@@ -1803,6 +1943,7 @@ var RebaseWebSocketClient = class {
|
|
|
1803
1943
|
};
|
|
1804
1944
|
this.ws.onclose = () => {
|
|
1805
1945
|
console.debug("Disconnected from PostgreSQL backend");
|
|
1946
|
+
if (this.ws === socket) this.ws = null;
|
|
1806
1947
|
this.isConnected = false;
|
|
1807
1948
|
this.isAuthenticated = false;
|
|
1808
1949
|
this.authPromise = null;
|
|
@@ -1838,6 +1979,7 @@ var RebaseWebSocketClient = class {
|
|
|
1838
1979
|
attemptReconnect() {
|
|
1839
1980
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1840
1981
|
console.error("Max reconnection attempts reached");
|
|
1982
|
+
this.gaveUp = true;
|
|
1841
1983
|
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1842
1984
|
return;
|
|
1843
1985
|
}
|
|
@@ -2080,21 +2222,23 @@ var RebaseWebSocketClient = class {
|
|
|
2080
2222
|
}
|
|
2081
2223
|
async ensureAuthenticated(retryCount = 3) {
|
|
2082
2224
|
if (this.isAuthenticated || !this.getAuthToken) return;
|
|
2083
|
-
if (this.authPromise) {
|
|
2084
|
-
|
|
2085
|
-
|
|
2225
|
+
if (!this.authPromise) {
|
|
2226
|
+
this.authPromise = this.runAuthentication(retryCount);
|
|
2227
|
+
this.authPromise.finally(() => {
|
|
2228
|
+
this.authPromise = null;
|
|
2229
|
+
}).catch(() => void 0);
|
|
2086
2230
|
}
|
|
2231
|
+
await this.authPromise;
|
|
2232
|
+
}
|
|
2233
|
+
async runAuthentication(retryCount) {
|
|
2087
2234
|
let lastError = null;
|
|
2088
2235
|
for (let attempt = 0; attempt < retryCount; attempt++) try {
|
|
2089
2236
|
const token = await this.getAuthToken();
|
|
2090
2237
|
if (!token) throw new Error("user not logged in");
|
|
2091
|
-
|
|
2092
|
-
await this.authPromise;
|
|
2093
|
-
this.authPromise = null;
|
|
2238
|
+
await this.authenticate(token);
|
|
2094
2239
|
console.debug("WebSocket authenticated on demand");
|
|
2095
2240
|
return;
|
|
2096
2241
|
} catch (error) {
|
|
2097
|
-
this.authPromise = null;
|
|
2098
2242
|
lastError = error;
|
|
2099
2243
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
2100
2244
|
if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
|
|
@@ -2380,7 +2524,7 @@ var RebaseWebSocketClient = class {
|
|
|
2380
2524
|
if (this.deepEqual(normCached, normIncoming)) return cachedRow;
|
|
2381
2525
|
else {
|
|
2382
2526
|
const mismatches = {};
|
|
2383
|
-
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2527
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2384
2528
|
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
2385
2529
|
cached: normCached[key],
|
|
2386
2530
|
incoming: normIncoming[key]
|
|
@@ -3183,7 +3327,7 @@ function isNetworkError(error) {
|
|
|
3183
3327
|
* bug the same payload will hit again than a blip, and retrying it forever
|
|
3184
3328
|
* jams every write behind it.
|
|
3185
3329
|
*/
|
|
3186
|
-
var RETRYABLE_STATUSES = new Set([
|
|
3330
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3187
3331
|
408,
|
|
3188
3332
|
425,
|
|
3189
3333
|
429,
|
|
@@ -4008,6 +4152,8 @@ var OfflineManager = class {
|
|
|
4008
4152
|
meta: answer.meta
|
|
4009
4153
|
};
|
|
4010
4154
|
},
|
|
4155
|
+
iterate: (params) => paginateFind((p) => wrapped.find(p), params, slug),
|
|
4156
|
+
findAll: (params) => collectAllPages((p) => wrapped.find(p), params, slug),
|
|
4011
4157
|
findById: async (id) => {
|
|
4012
4158
|
await this.ensureCollection(slug);
|
|
4013
4159
|
if (this.connectivity.shouldAttempt()) try {
|
|
@@ -5077,7 +5223,7 @@ function deriveWebSocketUrl(baseUrl) {
|
|
|
5077
5223
|
return baseUrl.replace(/^https?:\/\//i, (match) => match.toLowerCase() === "https://" ? "wss://" : "ws://").replace(/\/$/, "");
|
|
5078
5224
|
}
|
|
5079
5225
|
function createRebaseClient(options) {
|
|
5080
|
-
const transport = createTransport(options);
|
|
5226
|
+
const transport = createTransport(options, { credentialOutOfBand: options.auth?.authFlowMode === "cookie" });
|
|
5081
5227
|
const auth = createAuth(transport, options.auth);
|
|
5082
5228
|
const admin = createAdmin(transport, options.admin);
|
|
5083
5229
|
const cron = createCron(transport, options.cron);
|
|
@@ -5231,23 +5377,25 @@ channel: (name, options) => {
|
|
|
5231
5377
|
return existing;
|
|
5232
5378
|
} },
|
|
5233
5379
|
/**
|
|
5234
|
-
* Release
|
|
5380
|
+
* Release every handle that can keep a process alive — see the
|
|
5381
|
+
* `close` docblock on the client interface.
|
|
5235
5382
|
*
|
|
5236
|
-
*
|
|
5237
|
-
* and
|
|
5238
|
-
* was never started, and safe to call twice.
|
|
5383
|
+
* Safe to call when realtime was never started, safe when signed out,
|
|
5384
|
+
* and safe to call twice.
|
|
5239
5385
|
*/
|
|
5240
5386
|
close: () => {
|
|
5241
5387
|
for (const channel of realtimeChannels.values()) channel.leave();
|
|
5242
5388
|
realtimeChannels.clear();
|
|
5243
5389
|
ws?.disconnect(true);
|
|
5244
5390
|
offlineManager?.dispose();
|
|
5391
|
+
auth.stopAutoRefresh();
|
|
5245
5392
|
},
|
|
5246
5393
|
setToken: transport.setToken,
|
|
5247
5394
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
5248
5395
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
5249
5396
|
resolveToken: transport.resolveToken,
|
|
5250
5397
|
baseUrl: transport.baseUrl,
|
|
5398
|
+
apiPath: transport.apiPath,
|
|
5251
5399
|
collection,
|
|
5252
5400
|
call: async (endpoint, payload) => {
|
|
5253
5401
|
const prefix = endpoint.startsWith("/") ? "" : "/";
|
|
@@ -5262,6 +5410,6 @@ channel: (name, options) => {
|
|
|
5262
5410
|
};
|
|
5263
5411
|
}
|
|
5264
5412
|
//#endregion
|
|
5265
|
-
export { MemoryOfflineStore, QueryBuilder, RebaseApiError, RebaseClientError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, isOfflineError, or };
|
|
5413
|
+
export { MemoryOfflineStore, QueryBuilder, RebaseApiError, RebaseClientError, RebasePaginationError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, isOfflineError, or };
|
|
5266
5414
|
|
|
5267
5415
|
//# sourceMappingURL=index.es.js.map
|