@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/websocket.d.ts
CHANGED
|
@@ -27,6 +27,17 @@ export declare class RebaseWebSocketClient {
|
|
|
27
27
|
private channelHandlers;
|
|
28
28
|
/** Set by `close()`. Blocks any later operation from silently redialling. */
|
|
29
29
|
private closedByCaller;
|
|
30
|
+
/**
|
|
31
|
+
* Set when the backoff budget ran out, cleared by anything that earns a
|
|
32
|
+
* fresh one.
|
|
33
|
+
*
|
|
34
|
+
* Unlike {@link closedByCaller} this is not final — nobody *asked* for the
|
|
35
|
+
* socket to stay down. Five attempts with exponential backoff is about a
|
|
36
|
+
* minute, which a laptop lid, a wifi handover or a backend rollout all
|
|
37
|
+
* exceed routinely; treating that as permanent meant realtime silently
|
|
38
|
+
* stopped for the rest of the page's life, with a reload the only cure.
|
|
39
|
+
*/
|
|
40
|
+
private gaveUp;
|
|
30
41
|
/**
|
|
31
42
|
* Whether a socket exists at all (open or still opening).
|
|
32
43
|
*
|
|
@@ -68,6 +79,13 @@ export declare class RebaseWebSocketClient {
|
|
|
68
79
|
* re-entrant, since the reconnect path has always called it.
|
|
69
80
|
*/
|
|
70
81
|
ensureConnected(): void;
|
|
82
|
+
/**
|
|
83
|
+
* The browser says the network is back — the usual reason the budget ran
|
|
84
|
+
* out in the first place. Registered lazily so a Node client, or a page
|
|
85
|
+
* that never subscribes, adds no listener.
|
|
86
|
+
*/
|
|
87
|
+
private installOnlineListener;
|
|
88
|
+
private onlineListener;
|
|
71
89
|
/**
|
|
72
90
|
* Authenticate the WebSocket connection
|
|
73
91
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.1-canary.g06f263c",
|
|
5
5
|
"description": "HTTP SDK client for the Rebase custom backend",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -29,23 +29,23 @@
|
|
|
29
29
|
"./package.json": "./package.json"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@rebasepro/common": "0.12.
|
|
33
|
-
"@rebasepro/
|
|
34
|
-
"@rebasepro/
|
|
32
|
+
"@rebasepro/common": "0.12.1-canary.g06f263c",
|
|
33
|
+
"@rebasepro/utils": "0.12.1-canary.g06f263c",
|
|
34
|
+
"@rebasepro/types": "0.12.1-canary.g06f263c"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@jest/globals": "^30.4.1",
|
|
38
38
|
"@types/jest": "^30.0.0",
|
|
39
|
-
"@types/node": "^
|
|
39
|
+
"@types/node": "^26.1.2",
|
|
40
40
|
"@types/ws": "^8.18.1",
|
|
41
41
|
"cross-env": "^10.1.0",
|
|
42
42
|
"fake-indexeddb": "^6.2.5",
|
|
43
43
|
"jest": "^30.4.2",
|
|
44
|
-
"ts-jest": "^29.4.
|
|
44
|
+
"ts-jest": "^29.4.12",
|
|
45
45
|
"tsd": "^0.33.0",
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
|
-
"vite": "^8.
|
|
48
|
-
"ws": "^8.21.
|
|
47
|
+
"vite": "^8.1.5",
|
|
48
|
+
"ws": "^8.21.1"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
"dist",
|
package/src/auth.ts
CHANGED
|
@@ -268,6 +268,30 @@ export function createAuth(transport: Transport, options?: CreateAuthOptions) {
|
|
|
268
268
|
refreshTimeout = setTimeout(() => { void attemptScheduledRefresh(0); }, delay);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Stop the scheduled token refresh, leaving the session itself alone.
|
|
273
|
+
*
|
|
274
|
+
* This is teardown, not sign-out. `scheduleRefresh` arms an ordinary
|
|
275
|
+
* `setTimeout` up to a token lifetime away, and it is not `unref`'d — so on
|
|
276
|
+
* Node it holds the event loop open by itself. `client.close()` promised
|
|
277
|
+
* that "a script that does not call this will not exit on its own", which
|
|
278
|
+
* was true, while the converse it plainly implies was not: a signed-in
|
|
279
|
+
* client that closed its socket still hung, because this timer outlived it.
|
|
280
|
+
* Any script, cron handler or job that signs in hit that.
|
|
281
|
+
*
|
|
282
|
+
* Deliberately does NOT clear the session, touch storage, or emit
|
|
283
|
+
* SIGNED_OUT. Closing a client is not the user signing out — `signOut()`
|
|
284
|
+
* POSTs /logout and revokes the whole sign-in, which is the wrong hammer
|
|
285
|
+
* (see `abandonSessionLocally`) — and a persisted session must still be
|
|
286
|
+
* there for the next client to restore.
|
|
287
|
+
*/
|
|
288
|
+
function stopAutoRefresh() {
|
|
289
|
+
if (refreshTimeout) {
|
|
290
|
+
clearTimeout(refreshTimeout);
|
|
291
|
+
refreshTimeout = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
271
295
|
function handleAuthResponse(data: { tokens: AuthTokens, user: Record<string, unknown> }, event?: AuthChangeEvent): RebaseSession {
|
|
272
296
|
const user: User = mapRawUser(data.user);
|
|
273
297
|
const session: RebaseSession = {
|
|
@@ -796,6 +820,7 @@ refreshToken: session.refreshToken };
|
|
|
796
820
|
signInWithSlack,
|
|
797
821
|
signInWithSpotify,
|
|
798
822
|
signOut,
|
|
823
|
+
stopAutoRefresh,
|
|
799
824
|
refreshSession,
|
|
800
825
|
handleUnauthorized,
|
|
801
826
|
getUser,
|
|
@@ -815,6 +840,11 @@ refreshToken: session.refreshToken };
|
|
|
815
840
|
getAuthConfig,
|
|
816
841
|
getSession,
|
|
817
842
|
onAuthStateChange,
|
|
843
|
+
// A client that neither persists sessions nor uses cookie auth has
|
|
844
|
+
// nowhere to restore one from, so "no session in memory" is the final
|
|
845
|
+
// answer rather than a reason to ask the server. See the docblock on
|
|
846
|
+
// `AuthClient.canRestoreSession`.
|
|
847
|
+
canRestoreSession: () => persistSession || authFlowMode === "cookie",
|
|
818
848
|
isInitialized: () => isInitialized
|
|
819
849
|
};
|
|
820
850
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { createRebaseClient } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `close()` has one job: leave nothing behind that can keep a process alive.
|
|
6
|
+
*
|
|
7
|
+
* The realtime socket is the obvious such handle and was the only one this
|
|
8
|
+
* method released. The scheduled token refresh is the other, and it is easy to
|
|
9
|
+
* miss because it only exists once somebody signs in: `scheduleRefresh` arms a
|
|
10
|
+
* plain `setTimeout` for roughly the access token's lifetime, and it is not
|
|
11
|
+
* `unref`'d. So a signed-in Node client — a seed script, a cron handler, a job,
|
|
12
|
+
* an SSR request — released its socket, called `close()` exactly as documented,
|
|
13
|
+
* and still hung until the token would have expired.
|
|
14
|
+
*
|
|
15
|
+
* Found by `e2e/tests/client-sdk-e2e.ts`, which reported every check green and
|
|
16
|
+
* then never exited. Pinned here rather than only there because that suite needs
|
|
17
|
+
* Docker and a real Postgres, and this costs nothing.
|
|
18
|
+
*
|
|
19
|
+
* The assertion is the pending timer count, not a spy on `stopAutoRefresh`:
|
|
20
|
+
* what matters is that no handle survives, not which internal call removed it.
|
|
21
|
+
*/
|
|
22
|
+
describe("client.close()", () => {
|
|
23
|
+
const realFetch = globalThis.fetch;
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
globalThis.fetch = realFetch;
|
|
27
|
+
jest.useRealTimers();
|
|
28
|
+
jest.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** A login response shaped exactly as `handleAuthResponse` reads it. */
|
|
32
|
+
function loginResponse(accessTokenExpiresAt: number) {
|
|
33
|
+
return new Response(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
tokens: { accessToken: "access-token",
|
|
36
|
+
refreshToken: "refresh-token",
|
|
37
|
+
accessTokenExpiresAt },
|
|
38
|
+
user: { uid: "user-1",
|
|
39
|
+
email: "user@example.test" }
|
|
40
|
+
}),
|
|
41
|
+
{ status: 200,
|
|
42
|
+
headers: { "content-type": "application/json" } }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("clears the scheduled token refresh, so a signed-in script can exit", async () => {
|
|
47
|
+
jest.useFakeTimers();
|
|
48
|
+
|
|
49
|
+
// An hour out, so the refresh is scheduled rather than fired
|
|
50
|
+
// immediately — `scheduleRefresh` runs the refresh inline when the
|
|
51
|
+
// delay has already elapsed, which would arm no timer and let a broken
|
|
52
|
+
// `close()` pass this test.
|
|
53
|
+
const expiresAt = Date.now() + 60 * 60 * 1000;
|
|
54
|
+
globalThis.fetch = jest.fn(async () => loginResponse(expiresAt)) as unknown as typeof fetch;
|
|
55
|
+
|
|
56
|
+
// `realtime: false` so the socket cannot account for a pending handle:
|
|
57
|
+
// the only thing this test may observe is the refresh timer.
|
|
58
|
+
const client = createRebaseClient({ baseUrl: "http://localhost:9999",
|
|
59
|
+
realtime: false });
|
|
60
|
+
|
|
61
|
+
await client.auth.signInWithEmail("user@example.test", "password");
|
|
62
|
+
expect(jest.getTimerCount()).toBeGreaterThan(0);
|
|
63
|
+
|
|
64
|
+
client.close();
|
|
65
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("is safe on a client that never signed in, and safe to call twice", () => {
|
|
69
|
+
jest.useFakeTimers();
|
|
70
|
+
|
|
71
|
+
const client = createRebaseClient({ baseUrl: "http://localhost:9999",
|
|
72
|
+
realtime: false });
|
|
73
|
+
|
|
74
|
+
expect(() => {
|
|
75
|
+
client.close();
|
|
76
|
+
client.close();
|
|
77
|
+
}).not.toThrow();
|
|
78
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/functions.ts
CHANGED
|
@@ -55,7 +55,15 @@ export function createFunctionsClient(transport: Transport): FunctionsClient {
|
|
|
55
55
|
options?: FunctionInvokeOptions
|
|
56
56
|
): Promise<T> {
|
|
57
57
|
const method = options?.method ?? "POST";
|
|
58
|
-
|
|
58
|
+
// A `path` that starts the query or fragment is appended as-is. Only a
|
|
59
|
+
// real sub-path gets a separator: inserting one before `?days=30` asks
|
|
60
|
+
// for `/functions/dashboard-stats/?days=30`, and the trailing slash
|
|
61
|
+
// misses the route, so a function that exists answers 404 — and the
|
|
62
|
+
// caller sees it as the backend being down rather than as a bad URL.
|
|
63
|
+
const rawPath = options?.path;
|
|
64
|
+
const subPath = rawPath
|
|
65
|
+
? (/^[?#]/.test(rawPath) ? rawPath : `/${rawPath.replace(/^\//, "")}`)
|
|
66
|
+
: "";
|
|
59
67
|
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
60
68
|
|
|
61
69
|
const init: RequestInit = { method };
|
package/src/index.ts
CHANGED
|
@@ -208,11 +208,17 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
208
208
|
channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
|
|
209
209
|
};
|
|
210
210
|
/**
|
|
211
|
-
* Release
|
|
211
|
+
* Release everything this client holds that can keep a process alive: the
|
|
212
|
+
* realtime socket and its reconnect timer, channel presence heartbeats, the
|
|
213
|
+
* offline manager, and the scheduled token refresh.
|
|
212
214
|
*
|
|
213
|
-
*
|
|
214
|
-
* call this will not exit
|
|
215
|
-
*
|
|
215
|
+
* Each of those keeps the Node event loop alive on its own, so a script
|
|
216
|
+
* that does not call this will not exit — and, until the refresh timer was
|
|
217
|
+
* included, one that *did* call it still would not if it had signed in.
|
|
218
|
+
*
|
|
219
|
+
* Safe when realtime was never started (`realtime: false`), safe when
|
|
220
|
+
* signed out, and safe to call twice. It does not sign the user out: a
|
|
221
|
+
* persisted session survives for the next client to restore.
|
|
216
222
|
*/
|
|
217
223
|
close: () => void;
|
|
218
224
|
storage: StorageSource;
|
|
@@ -547,11 +553,11 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
547
553
|
}
|
|
548
554
|
},
|
|
549
555
|
/**
|
|
550
|
-
* Release
|
|
556
|
+
* Release every handle that can keep a process alive — see the
|
|
557
|
+
* `close` docblock on the client interface.
|
|
551
558
|
*
|
|
552
|
-
*
|
|
553
|
-
* and
|
|
554
|
-
* was never started, and safe to call twice.
|
|
559
|
+
* Safe to call when realtime was never started, safe when signed out,
|
|
560
|
+
* and safe to call twice.
|
|
555
561
|
*/
|
|
556
562
|
close: () => {
|
|
557
563
|
// Channels hold presence heartbeat timers, which would otherwise
|
|
@@ -565,12 +571,19 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
565
571
|
// The offline retry timer is unref'd but the `online` listener is
|
|
566
572
|
// not, and neither should outlive the client.
|
|
567
573
|
offlineManager?.dispose();
|
|
574
|
+
// The scheduled token refresh is a plain setTimeout up to a token
|
|
575
|
+
// lifetime away, and not unref'd — so on Node it holds the event
|
|
576
|
+
// loop open all by itself. Without this, closing a SIGNED-IN client
|
|
577
|
+
// released the socket and the process still never exited, which is
|
|
578
|
+
// the opposite of what this method exists to guarantee.
|
|
579
|
+
auth.stopAutoRefresh();
|
|
568
580
|
},
|
|
569
581
|
setToken: transport.setToken,
|
|
570
582
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
571
583
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
572
584
|
resolveToken: transport.resolveToken,
|
|
573
585
|
baseUrl: transport.baseUrl,
|
|
586
|
+
apiPath: transport.apiPath,
|
|
574
587
|
collection,
|
|
575
588
|
call: async <T = unknown>(endpoint: string, payload?: unknown): Promise<T> => {
|
|
576
589
|
const prefix = endpoint.startsWith("/") ? "" : "/";
|
|
@@ -189,9 +189,28 @@ async function clearOfflineQueue(): Promise<void> {
|
|
|
189
189
|
describe("two tabs over one database", () => {
|
|
190
190
|
beforeEach(clearOfflineQueue);
|
|
191
191
|
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Wait until the other tab has caught up.
|
|
194
|
+
*
|
|
195
|
+
* BroadcastChannel delivery is asynchronous and the receiving tab then does
|
|
196
|
+
* its own IndexedDB work, so there is no fixed number of turns that is
|
|
197
|
+
* both enough and not wasteful. Draining three macrotasks was enough on an
|
|
198
|
+
* idle machine and not enough when the other package suites run alongside
|
|
199
|
+
* this one — which is how this file failed under `pnpm -r test` while
|
|
200
|
+
* passing every time on its own.
|
|
201
|
+
*
|
|
202
|
+
* Polling a condition is what the test actually means, and it costs one
|
|
203
|
+
* turn in the common case rather than three.
|
|
204
|
+
*/
|
|
205
|
+
const settleUntil = async (done: () => boolean | Promise<boolean>, deadlineMs = 5_000) => {
|
|
206
|
+
const started = Date.now();
|
|
207
|
+
for (;;) {
|
|
208
|
+
if (await done()) return;
|
|
209
|
+
if (Date.now() - started > deadlineMs) {
|
|
210
|
+
throw new Error(`condition never held within ${deadlineMs}ms`);
|
|
211
|
+
}
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
213
|
+
}
|
|
195
214
|
};
|
|
196
215
|
|
|
197
216
|
function createServer() {
|
|
@@ -252,7 +271,7 @@ describe("two tabs over one database", () => {
|
|
|
252
271
|
|
|
253
272
|
server.state.online = false;
|
|
254
273
|
await postsA.create({ title: "from tab A" }, "xt1");
|
|
255
|
-
await
|
|
274
|
+
await settleUntil(async () => (await postsB.find()).data.some((r) => r.id === "xt1"));
|
|
256
275
|
|
|
257
276
|
// Without cross-tab propagation this tab would be showing a list that
|
|
258
277
|
// is already wrong, and would keep showing it until a refetch.
|
package/src/websocket.ts
CHANGED
|
@@ -97,6 +97,18 @@ export class RebaseWebSocketClient {
|
|
|
97
97
|
/** Set by `close()`. Blocks any later operation from silently redialling. */
|
|
98
98
|
private closedByCaller = false;
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Set when the backoff budget ran out, cleared by anything that earns a
|
|
102
|
+
* fresh one.
|
|
103
|
+
*
|
|
104
|
+
* Unlike {@link closedByCaller} this is not final — nobody *asked* for the
|
|
105
|
+
* socket to stay down. Five attempts with exponential backoff is about a
|
|
106
|
+
* minute, which a laptop lid, a wifi handover or a backend rollout all
|
|
107
|
+
* exceed routinely; treating that as permanent meant realtime silently
|
|
108
|
+
* stopped for the rest of the page's life, with a reload the only cure.
|
|
109
|
+
*/
|
|
110
|
+
private gaveUp = false;
|
|
111
|
+
|
|
100
112
|
/**
|
|
101
113
|
* Whether a socket exists at all (open or still opening).
|
|
102
114
|
*
|
|
@@ -249,10 +261,35 @@ export class RebaseWebSocketClient {
|
|
|
249
261
|
}
|
|
250
262
|
return;
|
|
251
263
|
}
|
|
264
|
+
this.installOnlineListener();
|
|
252
265
|
if (this.ws || this.reconnectTimeout) return;
|
|
266
|
+
// A caller asking for a connection is a fresh reason to try, so it also
|
|
267
|
+
// buys a fresh backoff budget. Without this, the first `subscribe`
|
|
268
|
+
// after a give-up would exhaust the counter on attempt one.
|
|
269
|
+
if (this.gaveUp) {
|
|
270
|
+
this.gaveUp = false;
|
|
271
|
+
this.reconnectAttempts = 0;
|
|
272
|
+
}
|
|
253
273
|
this.initWebSocket();
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The browser says the network is back — the usual reason the budget ran
|
|
278
|
+
* out in the first place. Registered lazily so a Node client, or a page
|
|
279
|
+
* that never subscribes, adds no listener.
|
|
280
|
+
*/
|
|
281
|
+
private installOnlineListener() {
|
|
282
|
+
if (this.onlineListener || typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
283
|
+
this.onlineListener = () => {
|
|
284
|
+
if (this.closedByCaller || !this.gaveUp) return;
|
|
285
|
+
console.debug("Network is back — retrying the realtime connection");
|
|
286
|
+
this.ensureConnected();
|
|
287
|
+
};
|
|
288
|
+
window.addEventListener("online", this.onlineListener);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private onlineListener: (() => void) | null = null;
|
|
292
|
+
|
|
256
293
|
/**
|
|
257
294
|
* Authenticate the WebSocket connection
|
|
258
295
|
*/
|
|
@@ -325,6 +362,10 @@ export class RebaseWebSocketClient {
|
|
|
325
362
|
*/
|
|
326
363
|
public disconnect(permanent = false): void {
|
|
327
364
|
if (permanent) this.closedByCaller = true;
|
|
365
|
+
if (permanent && this.onlineListener && typeof window !== "undefined") {
|
|
366
|
+
window.removeEventListener("online", this.onlineListener);
|
|
367
|
+
this.onlineListener = null;
|
|
368
|
+
}
|
|
328
369
|
this.isAuthenticated = false;
|
|
329
370
|
this.authPromise = null;
|
|
330
371
|
if (this.reconnectTimeout) {
|
|
@@ -354,7 +395,10 @@ export class RebaseWebSocketClient {
|
|
|
354
395
|
}
|
|
355
396
|
|
|
356
397
|
try {
|
|
357
|
-
|
|
398
|
+
// Captured so each handler can tell "my socket" from a later one:
|
|
399
|
+
// a close arriving after a redial must not clear the new socket.
|
|
400
|
+
const socket = new this.WebSocketConstructor(this.websocketUrl);
|
|
401
|
+
this.ws = socket;
|
|
358
402
|
|
|
359
403
|
this.ws!.onopen = async () => {
|
|
360
404
|
console.debug("Connected to PostgreSQL backend");
|
|
@@ -403,6 +447,11 @@ export class RebaseWebSocketClient {
|
|
|
403
447
|
|
|
404
448
|
this.ws!.onclose = () => {
|
|
405
449
|
console.debug("Disconnected from PostgreSQL backend");
|
|
450
|
+
// Release the dead socket. `ensureConnected` returns early
|
|
451
|
+
// while `this.ws` is set, so holding a closed one made the
|
|
452
|
+
// "give up after N attempts" state permanent: nothing could
|
|
453
|
+
// ever redial, not even a fresh `subscribe`.
|
|
454
|
+
if (this.ws === socket) this.ws = null;
|
|
406
455
|
this.isConnected = false;
|
|
407
456
|
this.isAuthenticated = false;
|
|
408
457
|
this.authPromise = null;
|
|
@@ -451,6 +500,7 @@ export class RebaseWebSocketClient {
|
|
|
451
500
|
console.error("Max reconnection attempts reached");
|
|
452
501
|
// Nothing will re-subscribe now, so stop every subscription that
|
|
453
502
|
// never loaded from spinning forever.
|
|
503
|
+
this.gaveUp = true;
|
|
454
504
|
this.failAllPendingSubscriptions(
|
|
455
505
|
new RebaseApiError("Connection lost", { code: "CONNECTION_LOST" })
|
|
456
506
|
);
|