@rebasepro/client 0.0.1-canary.eae7889 → 0.1.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 +6 -1
- package/dist/collection.d.ts +2 -1
- package/dist/collection.test.d.ts +1 -0
- package/dist/functions.d.ts +49 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +46 -23
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +46 -23
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -4
- package/src/auth.ts +20 -8
- package/src/collection.test.ts +159 -0
- package/src/collection.ts +8 -0
- package/src/functions.ts +80 -0
- package/src/index.ts +6 -0
- package/src/websocket.ts +17 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "HTTP SDK client for the Rebase custom backend",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"./package.json": "./package.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@rebasepro/types": "0.
|
|
34
|
-
"@rebasepro/utils": "0.
|
|
33
|
+
"@rebasepro/types": "0.1.0",
|
|
34
|
+
"@rebasepro/utils": "0.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@jest/globals": "^29.7.0",
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"jest": "^29.7.0",
|
|
42
42
|
"npm-run-all": "^4.1.5",
|
|
43
43
|
"ts-jest": "^29.3.1",
|
|
44
|
-
"ts-node": "^10.9.2",
|
|
45
44
|
"tsd": "^0.31.2",
|
|
46
45
|
"typescript": "^5.8.3",
|
|
47
46
|
"vite": "^5.4.17"
|
package/src/auth.ts
CHANGED
|
@@ -193,19 +193,31 @@ accessToken: session.accessToken,
|
|
|
193
193
|
refreshToken: session.refreshToken };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Sign in with Google.
|
|
198
|
+
*
|
|
199
|
+
* Supports two invocation styles:
|
|
200
|
+
* - `signInWithGoogle({ idToken })` — ID-token flow (One Tap / Sign In button)
|
|
201
|
+
* - `signInWithGoogle({ accessToken })` — Access-token flow (popup)
|
|
202
|
+
* - `signInWithGoogle({ code, redirectUri })` — Authorization code flow (most secure)
|
|
203
|
+
* - `signInWithGoogle(idToken)` — Legacy shorthand for ID-token flow
|
|
204
|
+
*/
|
|
205
|
+
async function signInWithGoogle(
|
|
206
|
+
tokenOrPayload: string | { idToken?: string; accessToken?: string; code?: string; redirectUri?: string }
|
|
207
|
+
) {
|
|
197
208
|
const fetchFn = getFetch();
|
|
209
|
+
const body = typeof tokenOrPayload === "string"
|
|
210
|
+
? { idToken: tokenOrPayload }
|
|
211
|
+
: tokenOrPayload;
|
|
198
212
|
const res = await fetchFn(authUrl("/google"), {
|
|
199
213
|
method: "POST",
|
|
200
214
|
headers: { "Content-Type": "application/json" },
|
|
201
|
-
body: JSON.stringify(
|
|
215
|
+
body: JSON.stringify(body)
|
|
202
216
|
});
|
|
203
|
-
const
|
|
204
|
-
if (!res.ok) throwApiError(res.status,
|
|
205
|
-
const session = handleAuthResponse(
|
|
206
|
-
return { user: session.user,
|
|
207
|
-
accessToken: session.accessToken,
|
|
208
|
-
refreshToken: session.refreshToken };
|
|
217
|
+
const responseBody = await res.json().catch(() => ({}));
|
|
218
|
+
if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
|
|
219
|
+
const session = handleAuthResponse(responseBody, "SIGNED_IN");
|
|
220
|
+
return { user: session.user, accessToken: session.accessToken, refreshToken: session.refreshToken };
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
async function signInWithLinkedin(code: string, redirectUri: string) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createCollectionClient } from "./collection";
|
|
3
|
+
import type { Transport } from "./transport";
|
|
4
|
+
|
|
5
|
+
function createMockTransport(): Transport {
|
|
6
|
+
return {
|
|
7
|
+
request: jest.fn<any>().mockResolvedValue({}),
|
|
8
|
+
setToken: jest.fn(),
|
|
9
|
+
setAuthTokenGetter: jest.fn(),
|
|
10
|
+
setOnUnauthorized: jest.fn(),
|
|
11
|
+
baseUrl: "http://localhost:3000",
|
|
12
|
+
apiPath: "/api",
|
|
13
|
+
fetchFn: globalThis.fetch,
|
|
14
|
+
getHeaders: () => ({}),
|
|
15
|
+
resolveToken: jest.fn<any>().mockResolvedValue(null),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createCollectionClient", () => {
|
|
20
|
+
let transport: Transport;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
transport = createMockTransport();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("count()", () => {
|
|
27
|
+
it("should call the /count endpoint and return the count", async () => {
|
|
28
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 42 });
|
|
29
|
+
|
|
30
|
+
const client = createCollectionClient(transport, "products");
|
|
31
|
+
const result = await client.count();
|
|
32
|
+
|
|
33
|
+
expect(transport.request).toHaveBeenCalledWith(
|
|
34
|
+
"/data/products/count",
|
|
35
|
+
{ method: "GET" }
|
|
36
|
+
);
|
|
37
|
+
expect(result).toBe(42);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should forward where filters to the count endpoint", async () => {
|
|
41
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 7 });
|
|
42
|
+
|
|
43
|
+
const client = createCollectionClient(transport, "products");
|
|
44
|
+
const result = await client.count({
|
|
45
|
+
where: { status: "eq.published" }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(transport.request).toHaveBeenCalledWith(
|
|
49
|
+
expect.stringContaining("/data/products/count?"),
|
|
50
|
+
{ method: "GET" }
|
|
51
|
+
);
|
|
52
|
+
// Verify the query string includes the filter
|
|
53
|
+
const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
|
|
54
|
+
expect(calledUrl).toContain("status=");
|
|
55
|
+
expect(result).toBe(7);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should forward orderBy to the count endpoint", async () => {
|
|
59
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 15 });
|
|
60
|
+
|
|
61
|
+
const client = createCollectionClient(transport, "items");
|
|
62
|
+
const result = await client.count({
|
|
63
|
+
orderBy: "created_at:desc"
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
|
|
67
|
+
expect(calledUrl).toContain("/data/items/count?");
|
|
68
|
+
expect(calledUrl).toContain("orderBy=");
|
|
69
|
+
expect(result).toBe(15);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should NOT include limit or offset in the count request", async () => {
|
|
73
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 100 });
|
|
74
|
+
|
|
75
|
+
const client = createCollectionClient(transport, "users");
|
|
76
|
+
// Even if the caller passes limit/offset in params, they should be stripped
|
|
77
|
+
await client.count({ limit: 50, offset: 10 });
|
|
78
|
+
|
|
79
|
+
const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
|
|
80
|
+
expect(calledUrl).not.toContain("limit=");
|
|
81
|
+
expect(calledUrl).not.toContain("offset=");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return 0 when meta.count is missing", async () => {
|
|
85
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({});
|
|
86
|
+
|
|
87
|
+
const client = createCollectionClient(transport, "empty");
|
|
88
|
+
const result = await client.count();
|
|
89
|
+
|
|
90
|
+
expect(result).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should forward searchString to the count endpoint", async () => {
|
|
94
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 3 });
|
|
95
|
+
|
|
96
|
+
const client = createCollectionClient(transport, "articles");
|
|
97
|
+
const result = await client.count({
|
|
98
|
+
searchString: "hello world"
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
|
|
102
|
+
expect(calledUrl).toContain("searchString=");
|
|
103
|
+
expect(calledUrl).toContain("hello");
|
|
104
|
+
expect(result).toBe(3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should forward multiple where filters correctly", async () => {
|
|
108
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({ count: 5 });
|
|
109
|
+
|
|
110
|
+
const client = createCollectionClient(transport, "orders");
|
|
111
|
+
await client.count({
|
|
112
|
+
where: {
|
|
113
|
+
status: "eq.active",
|
|
114
|
+
total: [">=", 100],
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const calledUrl = (transport.request as ReturnType<typeof jest.fn>).mock.calls[0][0] as string;
|
|
119
|
+
expect(calledUrl).toContain("status=");
|
|
120
|
+
expect(calledUrl).toContain("total=");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("find()", () => {
|
|
125
|
+
it("should call the list endpoint and return entities", async () => {
|
|
126
|
+
(transport.request as ReturnType<typeof jest.fn>).mockResolvedValue({
|
|
127
|
+
data: [{ id: "1", name: "Product A" }],
|
|
128
|
+
meta: { total: 1, limit: 20, offset: 0, hasMore: false }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const client = createCollectionClient(transport, "products");
|
|
132
|
+
const result = await client.find();
|
|
133
|
+
|
|
134
|
+
expect(transport.request).toHaveBeenCalledWith(
|
|
135
|
+
"/data/products",
|
|
136
|
+
{ method: "GET" }
|
|
137
|
+
);
|
|
138
|
+
expect(result.data).toHaveLength(1);
|
|
139
|
+
expect(result.data[0].id).toBe("1");
|
|
140
|
+
expect(result.data[0].path).toBe("products");
|
|
141
|
+
expect(result.meta.total).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("count() is defined", () => {
|
|
146
|
+
it("should have count as a defined function on the accessor", () => {
|
|
147
|
+
const client = createCollectionClient(transport, "products");
|
|
148
|
+
expect(typeof client.count).toBe("function");
|
|
149
|
+
expect(client.count).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should pass the accessor.count truthiness check (used by EntitiesCount component)", () => {
|
|
153
|
+
const client = createCollectionClient(transport, "products");
|
|
154
|
+
// The EntitiesCount component does `if (accessor.count) { ... }`
|
|
155
|
+
// This verifies that check would pass
|
|
156
|
+
expect(!!client.count).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/collection.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface CollectionClient<M extends Record<string, unknown> = Record<str
|
|
|
133
133
|
offset(count: number): QueryBuilder<M>;
|
|
134
134
|
search(searchString: string): QueryBuilder<M>;
|
|
135
135
|
include(...relations: string[]): QueryBuilder<M>;
|
|
136
|
+
count(params?: FindParams): Promise<number>;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
export function createCollectionClient<M extends Record<string, unknown> = Record<string, unknown>>(transport: Transport, slug: string, ws?: RebaseWebSocketClient): CollectionClient<M> {
|
|
@@ -180,6 +181,13 @@ export function createCollectionClient<M extends Record<string, unknown> = Recor
|
|
|
180
181
|
});
|
|
181
182
|
},
|
|
182
183
|
|
|
184
|
+
async count(params?: FindParams): Promise<number> {
|
|
185
|
+
const countParams: FindParams = { ...params, limit: undefined, offset: undefined };
|
|
186
|
+
const qs = buildQueryString(countParams);
|
|
187
|
+
const raw = await transport.request<{ count: number }>(basePath + "/count" + qs, { method: "GET" });
|
|
188
|
+
return raw.count ?? 0;
|
|
189
|
+
},
|
|
190
|
+
|
|
183
191
|
// Fluent builder instantiation
|
|
184
192
|
where(column: keyof M & string, operator: FilterOperator, value: unknown) {
|
|
185
193
|
return new QueryBuilder<M>(client).where(column, operator, value);
|
package/src/functions.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Transport } from "./transport";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client interface for invoking custom backend functions.
|
|
5
|
+
*
|
|
6
|
+
* Custom functions are Hono route files auto-mounted by the Rebase backend
|
|
7
|
+
* at `/api/functions/{name}`. The `FunctionsClient` wraps the shared
|
|
8
|
+
* transport so callers never need to manually construct URLs or inject
|
|
9
|
+
* auth tokens.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const result = await client.functions.invoke<{ job: Job }>('extract-job', {
|
|
14
|
+
* url: 'https://example.com/posting',
|
|
15
|
+
* html: htmlContent,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export interface FunctionsClient {
|
|
20
|
+
/**
|
|
21
|
+
* Invoke a custom backend function by name.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - Expected shape of the response payload.
|
|
24
|
+
* @param name - Function name (the filename without extension, e.g. `"extract-job"`).
|
|
25
|
+
* @param payload - Optional JSON-serialisable body sent as `POST`.
|
|
26
|
+
* @param options - Optional overrides (HTTP method, sub-path, extra headers).
|
|
27
|
+
* @returns The parsed JSON response from the function.
|
|
28
|
+
*/
|
|
29
|
+
invoke<T = unknown>(
|
|
30
|
+
name: string,
|
|
31
|
+
payload?: unknown,
|
|
32
|
+
options?: FunctionInvokeOptions,
|
|
33
|
+
): Promise<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FunctionInvokeOptions {
|
|
37
|
+
/** HTTP method — defaults to `"POST"`. */
|
|
38
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
39
|
+
/** Sub-path appended after the function name, e.g. `"status/123"`. */
|
|
40
|
+
path?: string;
|
|
41
|
+
/** Extra headers merged into the request (auth is still injected automatically). */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a `FunctionsClient` backed by the given transport.
|
|
47
|
+
*
|
|
48
|
+
* The transport already handles:
|
|
49
|
+
* - Base URL resolution
|
|
50
|
+
* - JWT injection via `Authorization: Bearer`
|
|
51
|
+
* - 401 retry / `onUnauthorized` flow
|
|
52
|
+
* - Consistent error throwing via `RebaseApiError`
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export function createFunctionsClient(transport: Transport): FunctionsClient {
|
|
57
|
+
return {
|
|
58
|
+
async invoke<T = unknown>(
|
|
59
|
+
name: string,
|
|
60
|
+
payload?: unknown,
|
|
61
|
+
options?: FunctionInvokeOptions,
|
|
62
|
+
): Promise<T> {
|
|
63
|
+
const method = options?.method ?? "POST";
|
|
64
|
+
const subPath = options?.path ? `/${options.path.replace(/^\//, "")}` : "";
|
|
65
|
+
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
66
|
+
|
|
67
|
+
const init: RequestInit = { method };
|
|
68
|
+
|
|
69
|
+
if (payload !== undefined && method !== "GET") {
|
|
70
|
+
init.body = JSON.stringify(payload);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options?.headers) {
|
|
74
|
+
init.headers = options.headers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return transport.request<T>(routePath, init);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { createAuth, CreateAuthOptions } from "./auth";
|
|
|
3
3
|
import { createAdmin, CreateAdminOptions } from "./admin";
|
|
4
4
|
import { createCron, CreateCronOptions } from "./cron";
|
|
5
5
|
import { createCollectionClient, CollectionClient } from "./collection";
|
|
6
|
+
import { createFunctionsClient } from "./functions";
|
|
7
|
+
import type { FunctionsClient } from "./functions";
|
|
6
8
|
|
|
7
9
|
export * from "./transport";
|
|
8
10
|
export * from "./auth";
|
|
@@ -12,6 +14,7 @@ export * from "./collection";
|
|
|
12
14
|
export * from "./websocket";
|
|
13
15
|
export * from "./storage";
|
|
14
16
|
export * from "./reviver";
|
|
17
|
+
export * from "./functions";
|
|
15
18
|
|
|
16
19
|
export interface CreateRebaseClientOptions extends RebaseClientConfig {
|
|
17
20
|
auth?: CreateAuthOptions;
|
|
@@ -31,6 +34,7 @@ export type RebaseClient<DB = Record<string, unknown>> = BaseRebaseClient<DB> &
|
|
|
31
34
|
auth: ReturnType<typeof createAuth>;
|
|
32
35
|
admin: ReturnType<typeof createAdmin>;
|
|
33
36
|
cron: ReturnType<typeof createCron>;
|
|
37
|
+
functions: FunctionsClient;
|
|
34
38
|
ws?: RebaseWebSocketClient;
|
|
35
39
|
storage?: StorageSource;
|
|
36
40
|
call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
|
|
@@ -72,6 +76,7 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
72
76
|
const admin = createAdmin(transport, options.admin);
|
|
73
77
|
const cron = createCron(transport, options.cron);
|
|
74
78
|
const storage = createStorage(transport);
|
|
79
|
+
const functions = createFunctionsClient(transport);
|
|
75
80
|
|
|
76
81
|
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
77
82
|
|
|
@@ -143,6 +148,7 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
|
|
|
143
148
|
auth,
|
|
144
149
|
admin,
|
|
145
150
|
cron,
|
|
151
|
+
functions,
|
|
146
152
|
storage,
|
|
147
153
|
ws,
|
|
148
154
|
setToken: transport.setToken,
|
package/src/websocket.ts
CHANGED
|
@@ -190,16 +190,18 @@ export class RebaseWebSocketClient {
|
|
|
190
190
|
this.getAuthToken = getAuthToken;
|
|
191
191
|
// Auto-authenticate if we are already connected but didn't have the token getter yet
|
|
192
192
|
if (this.isConnected && !this.isAuthenticated && !this.authPromise) {
|
|
193
|
-
console.
|
|
193
|
+
console.debug("WebSocket auto-authenticating after token getter set");
|
|
194
194
|
this.getAuthToken().then(token => {
|
|
195
195
|
if (!this.ws) return; // Prevent memory leaks / actions after disconnect
|
|
196
196
|
if (token) {
|
|
197
197
|
this.authenticate(token).catch(e => {
|
|
198
|
-
if (this.ws) console.
|
|
198
|
+
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
}).catch(e => {
|
|
202
|
-
|
|
202
|
+
// User not logged in or auth still loading — this is expected,
|
|
203
|
+
// the WebSocket will authenticate on-demand when a request is made.
|
|
204
|
+
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
203
205
|
});
|
|
204
206
|
}
|
|
205
207
|
}
|
|
@@ -230,7 +232,7 @@ export class RebaseWebSocketClient {
|
|
|
230
232
|
this.ws = new this.WebSocketConstructor(this.websocketUrl);
|
|
231
233
|
|
|
232
234
|
this.ws!.onopen = async () => {
|
|
233
|
-
console.
|
|
235
|
+
console.debug("Connected to PostgreSQL backend");
|
|
234
236
|
const wasReconnect = this.reconnectAttempts > 0;
|
|
235
237
|
this.isConnected = true;
|
|
236
238
|
this.reconnectAttempts = 0;
|
|
@@ -241,10 +243,12 @@ export class RebaseWebSocketClient {
|
|
|
241
243
|
const token = await this.getAuthToken();
|
|
242
244
|
if (token) {
|
|
243
245
|
await this.authenticate(token);
|
|
244
|
-
console.
|
|
246
|
+
console.debug("WebSocket auto-authenticated");
|
|
245
247
|
}
|
|
246
248
|
} catch (error) {
|
|
247
|
-
|
|
249
|
+
// User not logged in or auth still loading — this is expected.
|
|
250
|
+
// Authentication will happen on-demand when the user logs in.
|
|
251
|
+
console.debug("WebSocket connected without auth:", (error as Error)?.message || error);
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
254
|
|
|
@@ -269,7 +273,7 @@ export class RebaseWebSocketClient {
|
|
|
269
273
|
};
|
|
270
274
|
|
|
271
275
|
this.ws!.onclose = () => {
|
|
272
|
-
console.
|
|
276
|
+
console.debug("Disconnected from PostgreSQL backend");
|
|
273
277
|
this.isConnected = false;
|
|
274
278
|
this.isAuthenticated = false;
|
|
275
279
|
this.authPromise = null;
|
|
@@ -319,7 +323,7 @@ export class RebaseWebSocketClient {
|
|
|
319
323
|
this.reconnectAttempts++;
|
|
320
324
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
321
325
|
|
|
322
|
-
console.
|
|
326
|
+
console.debug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
323
327
|
|
|
324
328
|
if (this.reconnectTimeout) {
|
|
325
329
|
clearTimeout(this.reconnectTimeout);
|
|
@@ -534,7 +538,7 @@ export class RebaseWebSocketClient {
|
|
|
534
538
|
this.authPromise = this.authenticate(token);
|
|
535
539
|
await this.authPromise;
|
|
536
540
|
this.authPromise = null;
|
|
537
|
-
console.
|
|
541
|
+
console.debug("WebSocket authenticated on demand");
|
|
538
542
|
return; // Success
|
|
539
543
|
} catch (error: unknown) {
|
|
540
544
|
this.authPromise = null;
|
|
@@ -560,7 +564,7 @@ export class RebaseWebSocketClient {
|
|
|
560
564
|
// For other errors, retry with backoff
|
|
561
565
|
if (attempt < retryCount - 1) {
|
|
562
566
|
const delay = Math.min(1000 * (attempt + 1), 3000);
|
|
563
|
-
console.
|
|
567
|
+
console.debug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
564
568
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
565
569
|
}
|
|
566
570
|
}
|
|
@@ -580,7 +584,7 @@ export class RebaseWebSocketClient {
|
|
|
580
584
|
try {
|
|
581
585
|
const token = await this.getAuthToken();
|
|
582
586
|
await this.authenticate(token);
|
|
583
|
-
console.
|
|
587
|
+
console.debug("WebSocket reauthenticated successfully");
|
|
584
588
|
} catch (error) {
|
|
585
589
|
console.error("WebSocket reauthentication failed:", error);
|
|
586
590
|
throw error;
|
|
@@ -891,7 +895,7 @@ options }
|
|
|
891
895
|
incoming: normIncoming[key] };
|
|
892
896
|
}
|
|
893
897
|
}
|
|
894
|
-
console.
|
|
898
|
+
console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
895
899
|
}
|
|
896
900
|
}
|
|
897
901
|
return incomingEntity;
|
|
@@ -1101,7 +1105,7 @@ onError });
|
|
|
1101
1105
|
* we need to re-register everything to resume receiving updates.
|
|
1102
1106
|
*/
|
|
1103
1107
|
private resubscribeAll(): void {
|
|
1104
|
-
console.
|
|
1108
|
+
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
|
|
1105
1109
|
|
|
1106
1110
|
// Re-subscribe collection subscriptions
|
|
1107
1111
|
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|