@rebasepro/client 0.12.0 → 0.12.1-canary.g06f263c
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 +2 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.es.js +75 -9
- package/dist/index.es.js.map +1 -1
- package/dist/websocket.d.ts +18 -0
- package/package.json +8 -8
- package/src/auth.ts +30 -0
- package/src/client-close.test.ts +80 -0
- package/src/functions.ts +9 -1
- package/src/index.ts +21 -8
- package/src/offline-idb-store.test.ts +23 -4
- package/src/websocket.ts +51 -1
package/dist/auth.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ export declare function createAuth(transport: Transport, options?: CreateAuthOpt
|
|
|
128
128
|
refreshToken: string;
|
|
129
129
|
}>;
|
|
130
130
|
signOut: () => Promise<void>;
|
|
131
|
+
stopAutoRefresh: () => void;
|
|
131
132
|
refreshSession: () => Promise<RebaseSession>;
|
|
132
133
|
handleUnauthorized: () => Promise<boolean>;
|
|
133
134
|
getUser: () => Promise<User>;
|
|
@@ -180,6 +181,7 @@ export declare function createAuth(transport: Transport, options?: CreateAuthOpt
|
|
|
180
181
|
getAuthConfig: () => Promise<AuthConfig>;
|
|
181
182
|
getSession: () => RebaseSession | null;
|
|
182
183
|
onAuthStateChange: (callback: (event: AuthChangeEvent, session: RebaseSession | null) => void) => () => boolean;
|
|
184
|
+
canRestoreSession: () => boolean;
|
|
183
185
|
isInitialized: () => Promise<void>;
|
|
184
186
|
};
|
|
185
187
|
export interface CookieStorageOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -120,11 +120,17 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
120
120
|
channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
|
|
121
121
|
};
|
|
122
122
|
/**
|
|
123
|
-
* Release
|
|
123
|
+
* Release everything this client holds that can keep a process alive: the
|
|
124
|
+
* realtime socket and its reconnect timer, channel presence heartbeats, the
|
|
125
|
+
* offline manager, and the scheduled token refresh.
|
|
124
126
|
*
|
|
125
|
-
*
|
|
126
|
-
* call this will not exit
|
|
127
|
-
*
|
|
127
|
+
* Each of those keeps the Node event loop alive on its own, so a script
|
|
128
|
+
* that does not call this will not exit — and, until the refresh timer was
|
|
129
|
+
* included, one that *did* call it still would not if it had signed in.
|
|
130
|
+
*
|
|
131
|
+
* Safe when realtime was never started (`realtime: false`), safe when
|
|
132
|
+
* signed out, and safe to call twice. It does not sign the user out: a
|
|
133
|
+
* persisted session survives for the next client to restore.
|
|
128
134
|
*/
|
|
129
135
|
close: () => void;
|
|
130
136
|
storage: StorageSource;
|
package/dist/index.es.js
CHANGED
|
@@ -418,6 +418,29 @@ function createAuth(transport, options) {
|
|
|
418
418
|
attemptScheduledRefresh(0);
|
|
419
419
|
}, delay);
|
|
420
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Stop the scheduled token refresh, leaving the session itself alone.
|
|
423
|
+
*
|
|
424
|
+
* This is teardown, not sign-out. `scheduleRefresh` arms an ordinary
|
|
425
|
+
* `setTimeout` up to a token lifetime away, and it is not `unref`'d — so on
|
|
426
|
+
* Node it holds the event loop open by itself. `client.close()` promised
|
|
427
|
+
* that "a script that does not call this will not exit on its own", which
|
|
428
|
+
* was true, while the converse it plainly implies was not: a signed-in
|
|
429
|
+
* client that closed its socket still hung, because this timer outlived it.
|
|
430
|
+
* Any script, cron handler or job that signs in hit that.
|
|
431
|
+
*
|
|
432
|
+
* Deliberately does NOT clear the session, touch storage, or emit
|
|
433
|
+
* SIGNED_OUT. Closing a client is not the user signing out — `signOut()`
|
|
434
|
+
* POSTs /logout and revokes the whole sign-in, which is the wrong hammer
|
|
435
|
+
* (see `abandonSessionLocally`) — and a persisted session must still be
|
|
436
|
+
* there for the next client to restore.
|
|
437
|
+
*/
|
|
438
|
+
function stopAutoRefresh() {
|
|
439
|
+
if (refreshTimeout) {
|
|
440
|
+
clearTimeout(refreshTimeout);
|
|
441
|
+
refreshTimeout = null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
421
444
|
function handleAuthResponse(data, event) {
|
|
422
445
|
const user = mapRawUser(data.user);
|
|
423
446
|
const session = {
|
|
@@ -888,6 +911,7 @@ function createAuth(transport, options) {
|
|
|
888
911
|
signInWithSlack,
|
|
889
912
|
signInWithSpotify,
|
|
890
913
|
signOut,
|
|
914
|
+
stopAutoRefresh,
|
|
891
915
|
refreshSession,
|
|
892
916
|
handleUnauthorized,
|
|
893
917
|
getUser,
|
|
@@ -907,6 +931,7 @@ function createAuth(transport, options) {
|
|
|
907
931
|
getAuthConfig,
|
|
908
932
|
getSession,
|
|
909
933
|
onAuthStateChange,
|
|
934
|
+
canRestoreSession: () => persistSession || authFlowMode === "cookie",
|
|
910
935
|
isInitialized: () => isInitialized
|
|
911
936
|
};
|
|
912
937
|
}
|
|
@@ -1418,7 +1443,8 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1418
1443
|
function createFunctionsClient(transport) {
|
|
1419
1444
|
return { async invoke(name, payload, options) {
|
|
1420
1445
|
const method = options?.method ?? "POST";
|
|
1421
|
-
const
|
|
1446
|
+
const rawPath = options?.path;
|
|
1447
|
+
const subPath = rawPath ? /^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}` : "";
|
|
1422
1448
|
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
1423
1449
|
const init = { method };
|
|
1424
1450
|
if (payload !== void 0 && method !== "GET") init.body = JSON.stringify(payload);
|
|
@@ -1632,7 +1658,7 @@ function extractMessageError(message) {
|
|
|
1632
1658
|
* Fire-and-forget (the server sends no response envelope), and exempt from the
|
|
1633
1659
|
* client-side auth gate — a public channel is usable without an account.
|
|
1634
1660
|
*/
|
|
1635
|
-
var CHANNEL_MESSAGE_TYPES = new Set([
|
|
1661
|
+
var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
1636
1662
|
"join_channel",
|
|
1637
1663
|
"leave_channel",
|
|
1638
1664
|
"broadcast",
|
|
@@ -1661,6 +1687,17 @@ var RebaseWebSocketClient = class {
|
|
|
1661
1687
|
/** Set by `close()`. Blocks any later operation from silently redialling. */
|
|
1662
1688
|
closedByCaller = false;
|
|
1663
1689
|
/**
|
|
1690
|
+
* Set when the backoff budget ran out, cleared by anything that earns a
|
|
1691
|
+
* fresh one.
|
|
1692
|
+
*
|
|
1693
|
+
* Unlike {@link closedByCaller} this is not final — nobody *asked* for the
|
|
1694
|
+
* socket to stay down. Five attempts with exponential backoff is about a
|
|
1695
|
+
* minute, which a laptop lid, a wifi handover or a backend rollout all
|
|
1696
|
+
* exceed routinely; treating that as permanent meant realtime silently
|
|
1697
|
+
* stopped for the rest of the page's life, with a reload the only cure.
|
|
1698
|
+
*/
|
|
1699
|
+
gaveUp = false;
|
|
1700
|
+
/**
|
|
1664
1701
|
* Whether a socket exists at all (open or still opening).
|
|
1665
1702
|
*
|
|
1666
1703
|
* Lets callers distinguish "authenticate the live socket" from "there is
|
|
@@ -1733,10 +1770,30 @@ var RebaseWebSocketClient = class {
|
|
|
1733
1770
|
}
|
|
1734
1771
|
return;
|
|
1735
1772
|
}
|
|
1773
|
+
this.installOnlineListener();
|
|
1736
1774
|
if (this.ws || this.reconnectTimeout) return;
|
|
1775
|
+
if (this.gaveUp) {
|
|
1776
|
+
this.gaveUp = false;
|
|
1777
|
+
this.reconnectAttempts = 0;
|
|
1778
|
+
}
|
|
1737
1779
|
this.initWebSocket();
|
|
1738
1780
|
}
|
|
1739
1781
|
/**
|
|
1782
|
+
* The browser says the network is back — the usual reason the budget ran
|
|
1783
|
+
* out in the first place. Registered lazily so a Node client, or a page
|
|
1784
|
+
* that never subscribes, adds no listener.
|
|
1785
|
+
*/
|
|
1786
|
+
installOnlineListener() {
|
|
1787
|
+
if (this.onlineListener || typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
1788
|
+
this.onlineListener = () => {
|
|
1789
|
+
if (this.closedByCaller || !this.gaveUp) return;
|
|
1790
|
+
console.debug("Network is back — retrying the realtime connection");
|
|
1791
|
+
this.ensureConnected();
|
|
1792
|
+
};
|
|
1793
|
+
window.addEventListener("online", this.onlineListener);
|
|
1794
|
+
}
|
|
1795
|
+
onlineListener = null;
|
|
1796
|
+
/**
|
|
1740
1797
|
* Authenticate the WebSocket connection
|
|
1741
1798
|
*/
|
|
1742
1799
|
async authenticate(token) {
|
|
@@ -1794,6 +1851,10 @@ var RebaseWebSocketClient = class {
|
|
|
1794
1851
|
*/
|
|
1795
1852
|
disconnect(permanent = false) {
|
|
1796
1853
|
if (permanent) this.closedByCaller = true;
|
|
1854
|
+
if (permanent && this.onlineListener && typeof window !== "undefined") {
|
|
1855
|
+
window.removeEventListener("online", this.onlineListener);
|
|
1856
|
+
this.onlineListener = null;
|
|
1857
|
+
}
|
|
1797
1858
|
this.isAuthenticated = false;
|
|
1798
1859
|
this.authPromise = null;
|
|
1799
1860
|
if (this.reconnectTimeout) {
|
|
@@ -1818,7 +1879,8 @@ var RebaseWebSocketClient = class {
|
|
|
1818
1879
|
this.ws = null;
|
|
1819
1880
|
}
|
|
1820
1881
|
try {
|
|
1821
|
-
|
|
1882
|
+
const socket = new this.WebSocketConstructor(this.websocketUrl);
|
|
1883
|
+
this.ws = socket;
|
|
1822
1884
|
this.ws.onopen = async () => {
|
|
1823
1885
|
console.debug("Connected to PostgreSQL backend");
|
|
1824
1886
|
const wasReconnect = this.reconnectAttempts > 0;
|
|
@@ -1848,6 +1910,7 @@ var RebaseWebSocketClient = class {
|
|
|
1848
1910
|
};
|
|
1849
1911
|
this.ws.onclose = () => {
|
|
1850
1912
|
console.debug("Disconnected from PostgreSQL backend");
|
|
1913
|
+
if (this.ws === socket) this.ws = null;
|
|
1851
1914
|
this.isConnected = false;
|
|
1852
1915
|
this.isAuthenticated = false;
|
|
1853
1916
|
this.authPromise = null;
|
|
@@ -1883,6 +1946,7 @@ var RebaseWebSocketClient = class {
|
|
|
1883
1946
|
attemptReconnect() {
|
|
1884
1947
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1885
1948
|
console.error("Max reconnection attempts reached");
|
|
1949
|
+
this.gaveUp = true;
|
|
1886
1950
|
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1887
1951
|
return;
|
|
1888
1952
|
}
|
|
@@ -2425,7 +2489,7 @@ var RebaseWebSocketClient = class {
|
|
|
2425
2489
|
if (this.deepEqual(normCached, normIncoming)) return cachedRow;
|
|
2426
2490
|
else {
|
|
2427
2491
|
const mismatches = {};
|
|
2428
|
-
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2492
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2429
2493
|
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
2430
2494
|
cached: normCached[key],
|
|
2431
2495
|
incoming: normIncoming[key]
|
|
@@ -3228,7 +3292,7 @@ function isNetworkError(error) {
|
|
|
3228
3292
|
* bug the same payload will hit again than a blip, and retrying it forever
|
|
3229
3293
|
* jams every write behind it.
|
|
3230
3294
|
*/
|
|
3231
|
-
var RETRYABLE_STATUSES = new Set([
|
|
3295
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3232
3296
|
408,
|
|
3233
3297
|
425,
|
|
3234
3298
|
429,
|
|
@@ -5278,23 +5342,25 @@ channel: (name, options) => {
|
|
|
5278
5342
|
return existing;
|
|
5279
5343
|
} },
|
|
5280
5344
|
/**
|
|
5281
|
-
* Release
|
|
5345
|
+
* Release every handle that can keep a process alive — see the
|
|
5346
|
+
* `close` docblock on the client interface.
|
|
5282
5347
|
*
|
|
5283
|
-
*
|
|
5284
|
-
* and
|
|
5285
|
-
* was never started, and safe to call twice.
|
|
5348
|
+
* Safe to call when realtime was never started, safe when signed out,
|
|
5349
|
+
* and safe to call twice.
|
|
5286
5350
|
*/
|
|
5287
5351
|
close: () => {
|
|
5288
5352
|
for (const channel of realtimeChannels.values()) channel.leave();
|
|
5289
5353
|
realtimeChannels.clear();
|
|
5290
5354
|
ws?.disconnect(true);
|
|
5291
5355
|
offlineManager?.dispose();
|
|
5356
|
+
auth.stopAutoRefresh();
|
|
5292
5357
|
},
|
|
5293
5358
|
setToken: transport.setToken,
|
|
5294
5359
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
5295
5360
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
5296
5361
|
resolveToken: transport.resolveToken,
|
|
5297
5362
|
baseUrl: transport.baseUrl,
|
|
5363
|
+
apiPath: transport.apiPath,
|
|
5298
5364
|
collection,
|
|
5299
5365
|
call: async (endpoint, payload) => {
|
|
5300
5366
|
const prefix = endpoint.startsWith("/") ? "" : "/";
|