@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 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 the realtime socket and its reconnect timer.
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
- * An open socket keeps the Node event loop alive, so a script that does not
126
- * call this will not exit on its own. Safe when realtime was never started
127
- * (`realtime: false`), and safe to call twice.
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 subPath = options?.path ? `/${options.path.replace(/^\//, "")}` : "";
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
- this.ws = new this.WebSocketConstructor(this.websocketUrl);
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 the realtime socket and its reconnect timer.
5345
+ * Release every handle that can keep a process alive — see the
5346
+ * `close` docblock on the client interface.
5282
5347
  *
5283
- * Until this returns, the open socket keeps the Node event loop alive
5284
- * and the process will not exit on its own. Safe to call when realtime
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("/") ? "" : "/";