@rebasepro/client 0.12.1-canary.gf5f1d39 → 0.13.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/dist/auth.d.ts +3 -5
- package/dist/index.d.ts +11 -7
- package/dist/index.es.js +121 -20
- package/dist/index.es.js.map +1 -1
- package/dist/websocket.d.ts +19 -0
- package/package.json +8 -8
- package/src/auth.ts +30 -5
- package/src/client-close.test.ts +80 -0
- package/src/collection.test.ts +50 -10
- package/src/collection.ts +7 -1
- package/src/data-proxy.test.ts +22 -6
- package/src/functions.ts +9 -1
- package/src/index.ts +25 -11
- package/src/offline-idb-store.test.ts +23 -4
- package/src/realtime-concurrent-subscribe.test.ts +102 -0
- package/src/realtime-optout.test.ts +38 -4
- package/src/transport.ts +43 -0
- package/src/websocket.ts +82 -12
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/index.d.ts
CHANGED
|
@@ -21,9 +21,7 @@ export type { PaginationErrorCode } from "@rebasepro/common";
|
|
|
21
21
|
export { QueryBuilder, or, and, cond } from "@rebasepro/common";
|
|
22
22
|
export { createCookieStorage, createMemoryStorage } from "./auth";
|
|
23
23
|
export type { AuthConfig, AuthStorage, CookieStorageOptions, CreateAuthOptions } from "./auth";
|
|
24
|
-
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
25
|
-
/** @deprecated Import `User` / `AuthTokens` from `@rebasepro/types` instead. */
|
|
26
|
-
export type { RebaseUser, RebaseTokens } from "./auth";
|
|
24
|
+
export type { User, RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
27
25
|
export type { CreateAdminOptions } from "./admin";
|
|
28
26
|
export type { AdminUser } from "./admin";
|
|
29
27
|
export type { CreateCronOptions } from "./cron";
|
|
@@ -120,11 +118,17 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
120
118
|
channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
|
|
121
119
|
};
|
|
122
120
|
/**
|
|
123
|
-
* 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.
|
|
124
124
|
*
|
|
125
|
-
*
|
|
126
|
-
* call this will not exit
|
|
127
|
-
*
|
|
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.
|
|
128
132
|
*/
|
|
129
133
|
close: () => void;
|
|
130
134
|
storage: StorageSource;
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, Vector, isPublicStoragePath, toCanonicalOp } from "@rebasepro/types";
|
|
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
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
|
|
@@ -49,6 +49,38 @@ function isServerLikeEnvironment() {
|
|
|
49
49
|
* greppable — this is the string a user will paste into a search.
|
|
50
50
|
*/
|
|
51
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
|
+
}
|
|
52
84
|
function buildQueryString(params) {
|
|
53
85
|
if (!params) return "";
|
|
54
86
|
const parts = [];
|
|
@@ -67,6 +99,7 @@ function buildQueryString(params) {
|
|
|
67
99
|
parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
|
|
68
100
|
}
|
|
69
101
|
if (params.where) {
|
|
102
|
+
assertNoUndefinedFilterValues(params.where);
|
|
70
103
|
const serialized = serializeFilter(params.where);
|
|
71
104
|
for (const [field, value] of Object.entries(serialized)) if (Array.isArray(value)) for (const v of value) parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(v)}`);
|
|
72
105
|
else parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(value)}`);
|
|
@@ -418,6 +451,29 @@ function createAuth(transport, options) {
|
|
|
418
451
|
attemptScheduledRefresh(0);
|
|
419
452
|
}, delay);
|
|
420
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
|
+
}
|
|
421
477
|
function handleAuthResponse(data, event) {
|
|
422
478
|
const user = mapRawUser(data.user);
|
|
423
479
|
const session = {
|
|
@@ -888,6 +944,7 @@ function createAuth(transport, options) {
|
|
|
888
944
|
signInWithSlack,
|
|
889
945
|
signInWithSpotify,
|
|
890
946
|
signOut,
|
|
947
|
+
stopAutoRefresh,
|
|
891
948
|
refreshSession,
|
|
892
949
|
handleUnauthorized,
|
|
893
950
|
getUser,
|
|
@@ -907,6 +964,7 @@ function createAuth(transport, options) {
|
|
|
907
964
|
getAuthConfig,
|
|
908
965
|
getSession,
|
|
909
966
|
onAuthStateChange,
|
|
967
|
+
canRestoreSession: () => persistSession || authFlowMode === "cookie",
|
|
910
968
|
isInitialized: () => isInitialized
|
|
911
969
|
};
|
|
912
970
|
}
|
|
@@ -1272,7 +1330,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1272
1330
|
const qs = buildQueryString({
|
|
1273
1331
|
...params,
|
|
1274
1332
|
limit: void 0,
|
|
1275
|
-
offset: void 0
|
|
1333
|
+
offset: void 0,
|
|
1334
|
+
include: void 0
|
|
1276
1335
|
});
|
|
1277
1336
|
return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
|
|
1278
1337
|
},
|
|
@@ -1418,7 +1477,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1418
1477
|
function createFunctionsClient(transport) {
|
|
1419
1478
|
return { async invoke(name, payload, options) {
|
|
1420
1479
|
const method = options?.method ?? "POST";
|
|
1421
|
-
const
|
|
1480
|
+
const rawPath = options?.path;
|
|
1481
|
+
const subPath = rawPath ? /^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}` : "";
|
|
1422
1482
|
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
1423
1483
|
const init = { method };
|
|
1424
1484
|
if (payload !== void 0 && method !== "GET") init.body = JSON.stringify(payload);
|
|
@@ -1632,7 +1692,7 @@ function extractMessageError(message) {
|
|
|
1632
1692
|
* Fire-and-forget (the server sends no response envelope), and exempt from the
|
|
1633
1693
|
* client-side auth gate — a public channel is usable without an account.
|
|
1634
1694
|
*/
|
|
1635
|
-
var CHANNEL_MESSAGE_TYPES = new Set([
|
|
1695
|
+
var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
1636
1696
|
"join_channel",
|
|
1637
1697
|
"leave_channel",
|
|
1638
1698
|
"broadcast",
|
|
@@ -1661,6 +1721,17 @@ var RebaseWebSocketClient = class {
|
|
|
1661
1721
|
/** Set by `close()`. Blocks any later operation from silently redialling. */
|
|
1662
1722
|
closedByCaller = false;
|
|
1663
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
|
+
/**
|
|
1664
1735
|
* Whether a socket exists at all (open or still opening).
|
|
1665
1736
|
*
|
|
1666
1737
|
* Lets callers distinguish "authenticate the live socket" from "there is
|
|
@@ -1733,18 +1804,37 @@ var RebaseWebSocketClient = class {
|
|
|
1733
1804
|
}
|
|
1734
1805
|
return;
|
|
1735
1806
|
}
|
|
1807
|
+
this.installOnlineListener();
|
|
1736
1808
|
if (this.ws || this.reconnectTimeout) return;
|
|
1809
|
+
if (this.gaveUp) {
|
|
1810
|
+
this.gaveUp = false;
|
|
1811
|
+
this.reconnectAttempts = 0;
|
|
1812
|
+
}
|
|
1737
1813
|
this.initWebSocket();
|
|
1738
1814
|
}
|
|
1739
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
|
+
/**
|
|
1740
1831
|
* Authenticate the WebSocket connection
|
|
1741
1832
|
*/
|
|
1742
1833
|
async authenticate(token) {
|
|
1743
1834
|
return new Promise((resolve, reject) => {
|
|
1744
|
-
const requestId = `auth_${Date.now()}`;
|
|
1835
|
+
const requestId = `auth_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1745
1836
|
const timeout = setTimeout(() => {
|
|
1746
1837
|
this.pendingRequests.delete(requestId);
|
|
1747
|
-
this.authPromise = null;
|
|
1748
1838
|
reject(/* @__PURE__ */ new Error("Authentication timeout"));
|
|
1749
1839
|
}, 3e4);
|
|
1750
1840
|
this.pendingRequests.set(requestId, {
|
|
@@ -1794,6 +1884,10 @@ var RebaseWebSocketClient = class {
|
|
|
1794
1884
|
*/
|
|
1795
1885
|
disconnect(permanent = false) {
|
|
1796
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
|
+
}
|
|
1797
1891
|
this.isAuthenticated = false;
|
|
1798
1892
|
this.authPromise = null;
|
|
1799
1893
|
if (this.reconnectTimeout) {
|
|
@@ -1818,7 +1912,8 @@ var RebaseWebSocketClient = class {
|
|
|
1818
1912
|
this.ws = null;
|
|
1819
1913
|
}
|
|
1820
1914
|
try {
|
|
1821
|
-
|
|
1915
|
+
const socket = new this.WebSocketConstructor(this.websocketUrl);
|
|
1916
|
+
this.ws = socket;
|
|
1822
1917
|
this.ws.onopen = async () => {
|
|
1823
1918
|
console.debug("Connected to PostgreSQL backend");
|
|
1824
1919
|
const wasReconnect = this.reconnectAttempts > 0;
|
|
@@ -1848,6 +1943,7 @@ var RebaseWebSocketClient = class {
|
|
|
1848
1943
|
};
|
|
1849
1944
|
this.ws.onclose = () => {
|
|
1850
1945
|
console.debug("Disconnected from PostgreSQL backend");
|
|
1946
|
+
if (this.ws === socket) this.ws = null;
|
|
1851
1947
|
this.isConnected = false;
|
|
1852
1948
|
this.isAuthenticated = false;
|
|
1853
1949
|
this.authPromise = null;
|
|
@@ -1883,6 +1979,7 @@ var RebaseWebSocketClient = class {
|
|
|
1883
1979
|
attemptReconnect() {
|
|
1884
1980
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1885
1981
|
console.error("Max reconnection attempts reached");
|
|
1982
|
+
this.gaveUp = true;
|
|
1886
1983
|
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1887
1984
|
return;
|
|
1888
1985
|
}
|
|
@@ -2125,21 +2222,23 @@ var RebaseWebSocketClient = class {
|
|
|
2125
2222
|
}
|
|
2126
2223
|
async ensureAuthenticated(retryCount = 3) {
|
|
2127
2224
|
if (this.isAuthenticated || !this.getAuthToken) return;
|
|
2128
|
-
if (this.authPromise) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2225
|
+
if (!this.authPromise) {
|
|
2226
|
+
this.authPromise = this.runAuthentication(retryCount);
|
|
2227
|
+
this.authPromise.finally(() => {
|
|
2228
|
+
this.authPromise = null;
|
|
2229
|
+
}).catch(() => void 0);
|
|
2131
2230
|
}
|
|
2231
|
+
await this.authPromise;
|
|
2232
|
+
}
|
|
2233
|
+
async runAuthentication(retryCount) {
|
|
2132
2234
|
let lastError = null;
|
|
2133
2235
|
for (let attempt = 0; attempt < retryCount; attempt++) try {
|
|
2134
2236
|
const token = await this.getAuthToken();
|
|
2135
2237
|
if (!token) throw new Error("user not logged in");
|
|
2136
|
-
|
|
2137
|
-
await this.authPromise;
|
|
2138
|
-
this.authPromise = null;
|
|
2238
|
+
await this.authenticate(token);
|
|
2139
2239
|
console.debug("WebSocket authenticated on demand");
|
|
2140
2240
|
return;
|
|
2141
2241
|
} catch (error) {
|
|
2142
|
-
this.authPromise = null;
|
|
2143
2242
|
lastError = error;
|
|
2144
2243
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
2145
2244
|
if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
|
|
@@ -2425,7 +2524,7 @@ var RebaseWebSocketClient = class {
|
|
|
2425
2524
|
if (this.deepEqual(normCached, normIncoming)) return cachedRow;
|
|
2426
2525
|
else {
|
|
2427
2526
|
const mismatches = {};
|
|
2428
|
-
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2527
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2429
2528
|
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
2430
2529
|
cached: normCached[key],
|
|
2431
2530
|
incoming: normIncoming[key]
|
|
@@ -3228,7 +3327,7 @@ function isNetworkError(error) {
|
|
|
3228
3327
|
* bug the same payload will hit again than a blip, and retrying it forever
|
|
3229
3328
|
* jams every write behind it.
|
|
3230
3329
|
*/
|
|
3231
|
-
var RETRYABLE_STATUSES = new Set([
|
|
3330
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3232
3331
|
408,
|
|
3233
3332
|
425,
|
|
3234
3333
|
429,
|
|
@@ -5278,23 +5377,25 @@ channel: (name, options) => {
|
|
|
5278
5377
|
return existing;
|
|
5279
5378
|
} },
|
|
5280
5379
|
/**
|
|
5281
|
-
* Release
|
|
5380
|
+
* Release every handle that can keep a process alive — see the
|
|
5381
|
+
* `close` docblock on the client interface.
|
|
5282
5382
|
*
|
|
5283
|
-
*
|
|
5284
|
-
* and
|
|
5285
|
-
* 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.
|
|
5286
5385
|
*/
|
|
5287
5386
|
close: () => {
|
|
5288
5387
|
for (const channel of realtimeChannels.values()) channel.leave();
|
|
5289
5388
|
realtimeChannels.clear();
|
|
5290
5389
|
ws?.disconnect(true);
|
|
5291
5390
|
offlineManager?.dispose();
|
|
5391
|
+
auth.stopAutoRefresh();
|
|
5292
5392
|
},
|
|
5293
5393
|
setToken: transport.setToken,
|
|
5294
5394
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
5295
5395
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
5296
5396
|
resolveToken: transport.resolveToken,
|
|
5297
5397
|
baseUrl: transport.baseUrl,
|
|
5398
|
+
apiPath: transport.apiPath,
|
|
5298
5399
|
collection,
|
|
5299
5400
|
call: async (endpoint, payload) => {
|
|
5300
5401
|
const prefix = endpoint.startsWith("/") ? "" : "/";
|