@rebasepro/client 0.0.1-canary.09e5ec5

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.
@@ -0,0 +1,2277 @@
1
+ import { Vector, GeoPoint, EntityRelation, EntityReference } from "@rebasepro/types";
2
+ import { toSnakeCase } from "@rebasepro/utils";
3
+ function rebaseReviver(_key, value) {
4
+ if (value && typeof value === "object" && "__type" in value) {
5
+ const record = value;
6
+ switch (record.__type) {
7
+ case "date":
8
+ case "Date": {
9
+ if (typeof record.value !== "string") {
10
+ return value;
11
+ }
12
+ const date = new Date(record.value);
13
+ return isNaN(date.getTime()) ? null : date;
14
+ }
15
+ case "reference":
16
+ case "EntityReference":
17
+ return new EntityReference({
18
+ id: String(record.id),
19
+ path: record.path,
20
+ driver: record.driver,
21
+ databaseId: record.databaseId
22
+ });
23
+ case "relation":
24
+ case "EntityRelation":
25
+ return new EntityRelation(
26
+ record.id,
27
+ record.path,
28
+ record.data
29
+ );
30
+ case "GeoPoint":
31
+ return new GeoPoint(record.latitude, record.longitude);
32
+ case "Vector":
33
+ return new Vector(record.value);
34
+ default:
35
+ return value;
36
+ }
37
+ }
38
+ return value;
39
+ }
40
+ class RebaseApiError extends Error {
41
+ status;
42
+ code;
43
+ details;
44
+ constructor(status, message, code, details) {
45
+ super(message);
46
+ this.name = "RebaseApiError";
47
+ this.status = status;
48
+ this.code = code;
49
+ this.details = details;
50
+ }
51
+ }
52
+ const OP_MAP = {
53
+ "==": "eq",
54
+ "!=": "neq",
55
+ ">": "gt",
56
+ ">=": "gte",
57
+ "<": "lt",
58
+ "<=": "lte",
59
+ "not-in": "nin",
60
+ "array-contains": "cs",
61
+ "array-contains-any": "csa"
62
+ };
63
+ function normalizeWhereValue(value) {
64
+ if (value === null) return "eq.null";
65
+ if (typeof value === "boolean") return `eq.${value}`;
66
+ if (typeof value === "number") return String(value);
67
+ if (Array.isArray(value) && value.length === 2) {
68
+ const [rawOp, val] = value;
69
+ const op = OP_MAP[rawOp] ?? rawOp;
70
+ if (val === null) return `${op}.null`;
71
+ if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
72
+ return `${op}.${val}`;
73
+ }
74
+ return String(value);
75
+ }
76
+ function buildQueryString(params) {
77
+ if (!params) return "";
78
+ const parts = [];
79
+ if (params.limit != null) parts.push(`limit=${params.limit}`);
80
+ if (params.offset != null) parts.push(`offset=${params.offset}`);
81
+ if (params.page != null) parts.push(`page=${params.page}`);
82
+ if (params.orderBy) {
83
+ parts.push(`orderBy=${encodeURIComponent(params.orderBy)}`);
84
+ }
85
+ if (params.searchString) {
86
+ parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
87
+ }
88
+ if (params.include && params.include.length > 0) {
89
+ parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
90
+ }
91
+ if (params.where) {
92
+ for (const [field, value] of Object.entries(params.where)) {
93
+ const normalized = normalizeWhereValue(value);
94
+ parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
95
+ }
96
+ }
97
+ return parts.length > 0 ? "?" + parts.join("&") : "";
98
+ }
99
+ function createTransport(config) {
100
+ const fetchFn = config.fetch || globalThis.fetch;
101
+ const apiPath = config.apiPath || "/api";
102
+ let token = config.token;
103
+ let tokenGetter;
104
+ let onUnauthorizedHandler = config.onUnauthorized;
105
+ function getHeaders(activeToken, init) {
106
+ return {
107
+ "Content-Type": "application/json",
108
+ ...activeToken ? { Authorization: `Bearer ${activeToken}` } : {},
109
+ ...init?.headers || {}
110
+ };
111
+ }
112
+ async function request(path, init) {
113
+ const base = config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "";
114
+ const url = base + apiPath + path;
115
+ let activeToken = token;
116
+ if (tokenGetter) {
117
+ try {
118
+ const fetched = await tokenGetter();
119
+ if (fetched !== null && fetched !== void 0) {
120
+ activeToken = fetched;
121
+ }
122
+ } catch (e) {
123
+ }
124
+ }
125
+ const headers = getHeaders(activeToken, init);
126
+ if (init?.body instanceof FormData) {
127
+ delete headers["Content-Type"];
128
+ }
129
+ const res = await fetchFn(url, {
130
+ ...init,
131
+ headers
132
+ });
133
+ if (res.status === 204) return void 0;
134
+ const text = await res.text().catch(() => "");
135
+ let body = {};
136
+ if (text) {
137
+ try {
138
+ body = JSON.parse(text, rebaseReviver);
139
+ } catch (e) {
140
+ }
141
+ }
142
+ if (res.status === 401 && onUnauthorizedHandler) {
143
+ const retried = await onUnauthorizedHandler();
144
+ if (retried) {
145
+ let retryToken = token;
146
+ if (tokenGetter) {
147
+ try {
148
+ const fetched = await tokenGetter();
149
+ if (fetched !== null && fetched !== void 0) {
150
+ retryToken = fetched;
151
+ }
152
+ } catch (e) {
153
+ }
154
+ }
155
+ const retryHeaders = getHeaders(retryToken, init);
156
+ const retryRes = await fetchFn(url, {
157
+ ...init,
158
+ headers: retryHeaders
159
+ });
160
+ if (retryRes.status === 204) return void 0;
161
+ const retryText = await retryRes.text().catch(() => "");
162
+ let retryBody = {};
163
+ if (retryText) {
164
+ try {
165
+ retryBody = JSON.parse(retryText, rebaseReviver);
166
+ } catch (e) {
167
+ }
168
+ }
169
+ if (!retryRes.ok) {
170
+ let fallbackMessage = retryRes.statusText;
171
+ if (retryRes.status === 404 && !fallbackMessage) {
172
+ const method = init?.method || "GET";
173
+ fallbackMessage = `Endpoint not found (${method} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
174
+ }
175
+ throw new RebaseApiError(
176
+ retryRes.status,
177
+ retryBody?.error?.message || retryBody?.message || fallbackMessage || `Request failed with status ${retryRes.status}`,
178
+ retryBody?.error?.code || retryBody?.code,
179
+ retryBody?.error?.details || retryBody?.details
180
+ );
181
+ }
182
+ return retryBody;
183
+ }
184
+ }
185
+ if (!res.ok) {
186
+ let fallbackMessage = res.statusText;
187
+ if (res.status === 404 && !fallbackMessage) {
188
+ const method = init?.method || "GET";
189
+ fallbackMessage = `Endpoint not found (${method} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
190
+ }
191
+ throw new RebaseApiError(
192
+ res.status,
193
+ body?.error?.message || body?.message || fallbackMessage || `Request failed with status ${res.status}`,
194
+ body?.error?.code || body?.code,
195
+ body?.error?.details || body?.details
196
+ );
197
+ }
198
+ return body;
199
+ }
200
+ return {
201
+ request,
202
+ setToken(newToken) {
203
+ token = newToken || void 0;
204
+ },
205
+ setAuthTokenGetter(getter) {
206
+ tokenGetter = getter;
207
+ },
208
+ setOnUnauthorized(handler) {
209
+ onUnauthorizedHandler = handler;
210
+ },
211
+ get baseUrl() {
212
+ return config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "";
213
+ },
214
+ get apiPath() {
215
+ return apiPath;
216
+ },
217
+ get fetchFn() {
218
+ return fetchFn;
219
+ },
220
+ getHeaders: (init) => getHeaders(token, init),
221
+ resolveToken: async () => {
222
+ if (tokenGetter) {
223
+ try {
224
+ const fetched = await tokenGetter();
225
+ if (fetched !== null && fetched !== void 0) {
226
+ return fetched;
227
+ }
228
+ } catch (e) {
229
+ }
230
+ }
231
+ return token || null;
232
+ }
233
+ };
234
+ }
235
+ function createMemoryStorage() {
236
+ const store = {};
237
+ return {
238
+ getItem(key) {
239
+ return store[key] ?? null;
240
+ },
241
+ setItem(key, value) {
242
+ store[key] = value;
243
+ },
244
+ removeItem(key) {
245
+ delete store[key];
246
+ }
247
+ };
248
+ }
249
+ function detectStorage() {
250
+ try {
251
+ if (typeof localStorage !== "undefined") {
252
+ localStorage.setItem("__rebase_test__", "1");
253
+ localStorage.removeItem("__rebase_test__");
254
+ return localStorage;
255
+ }
256
+ } catch (e) {
257
+ }
258
+ return createMemoryStorage();
259
+ }
260
+ function createAuth(transport, options) {
261
+ const opts = options || {};
262
+ const storage = opts.storage || detectStorage();
263
+ const authPath = opts.authPath || "/auth";
264
+ const autoRefresh = opts.autoRefresh !== false;
265
+ const persistSession = opts.persistSession !== false;
266
+ const STORAGE_KEY = "rebase_auth";
267
+ const REFRESH_BUFFER_MS = 12e4;
268
+ let currentSession = null;
269
+ const listeners = /* @__PURE__ */ new Set();
270
+ let refreshTimeout = null;
271
+ function authUrl(endpoint) {
272
+ return transport.baseUrl + transport.apiPath + authPath + endpoint;
273
+ }
274
+ function getFetch() {
275
+ return transport.fetchFn || globalThis.fetch;
276
+ }
277
+ function throwApiError(status, body, statusText) {
278
+ throw new RebaseApiError(
279
+ status,
280
+ body?.error?.message || body?.message || statusText,
281
+ body?.error?.code || body?.code,
282
+ body?.error?.details || body?.details
283
+ );
284
+ }
285
+ function emit(event, session) {
286
+ for (const fn of listeners) {
287
+ try {
288
+ fn(event, session);
289
+ } catch (e) {
290
+ }
291
+ }
292
+ }
293
+ function saveSession(session) {
294
+ if (!persistSession) return;
295
+ try {
296
+ storage.setItem(STORAGE_KEY, JSON.stringify(session));
297
+ } catch (e) {
298
+ }
299
+ }
300
+ function clearStoredSession() {
301
+ try {
302
+ storage.removeItem(STORAGE_KEY);
303
+ } catch (e) {
304
+ }
305
+ }
306
+ function loadStoredSession() {
307
+ try {
308
+ const raw = storage.getItem(STORAGE_KEY);
309
+ if (raw) return JSON.parse(raw);
310
+ } catch (e) {
311
+ }
312
+ return null;
313
+ }
314
+ function scheduleRefresh(expiresAt) {
315
+ if (refreshTimeout) clearTimeout(refreshTimeout);
316
+ if (!autoRefresh) return;
317
+ const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
318
+ if (delay <= 0) {
319
+ refreshSession().catch(() => signOut());
320
+ return;
321
+ }
322
+ refreshTimeout = setTimeout(async () => {
323
+ try {
324
+ await refreshSession();
325
+ } catch (e) {
326
+ signOut();
327
+ }
328
+ }, delay);
329
+ }
330
+ function handleAuthResponse(data, event) {
331
+ const session = {
332
+ accessToken: data.tokens.accessToken,
333
+ refreshToken: data.tokens.refreshToken,
334
+ expiresAt: data.tokens.accessTokenExpiresAt,
335
+ user: data.user
336
+ };
337
+ currentSession = session;
338
+ saveSession(session);
339
+ transport.setToken(session.accessToken);
340
+ scheduleRefresh(session.expiresAt);
341
+ emit(event, session);
342
+ return session;
343
+ }
344
+ async function signInWithEmail(email, password) {
345
+ const fetchFn = getFetch();
346
+ const res = await fetchFn(authUrl("/login"), {
347
+ method: "POST",
348
+ headers: { "Content-Type": "application/json" },
349
+ body: JSON.stringify({
350
+ email,
351
+ password
352
+ })
353
+ });
354
+ const body = await res.json().catch(() => ({}));
355
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
356
+ const session = handleAuthResponse(body, "SIGNED_IN");
357
+ return {
358
+ user: session.user,
359
+ accessToken: session.accessToken,
360
+ refreshToken: session.refreshToken
361
+ };
362
+ }
363
+ async function signUp(email, password, displayName) {
364
+ const fetchFn = getFetch();
365
+ const payload = {
366
+ email,
367
+ password
368
+ };
369
+ if (displayName !== void 0) payload.displayName = displayName;
370
+ const res = await fetchFn(authUrl("/register"), {
371
+ method: "POST",
372
+ headers: { "Content-Type": "application/json" },
373
+ body: JSON.stringify(payload)
374
+ });
375
+ const body = await res.json().catch(() => ({}));
376
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
377
+ const session = handleAuthResponse(body, "SIGNED_IN");
378
+ return {
379
+ user: session.user,
380
+ accessToken: session.accessToken,
381
+ refreshToken: session.refreshToken
382
+ };
383
+ }
384
+ async function signInWithGoogle(idToken) {
385
+ const fetchFn = getFetch();
386
+ const res = await fetchFn(authUrl("/google"), {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/json" },
389
+ body: JSON.stringify({ idToken })
390
+ });
391
+ const body = await res.json().catch(() => ({}));
392
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
393
+ const session = handleAuthResponse(body, "SIGNED_IN");
394
+ return {
395
+ user: session.user,
396
+ accessToken: session.accessToken,
397
+ refreshToken: session.refreshToken
398
+ };
399
+ }
400
+ async function signInWithLinkedin(code, redirectUri) {
401
+ const fetchFn = getFetch();
402
+ const res = await fetchFn(authUrl("/linkedin"), {
403
+ method: "POST",
404
+ headers: { "Content-Type": "application/json" },
405
+ body: JSON.stringify({
406
+ code,
407
+ redirectUri
408
+ })
409
+ });
410
+ const body = await res.json().catch(() => ({}));
411
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
412
+ const session = handleAuthResponse(body, "SIGNED_IN");
413
+ return {
414
+ user: session.user,
415
+ accessToken: session.accessToken,
416
+ refreshToken: session.refreshToken
417
+ };
418
+ }
419
+ async function signInWithOAuth(providerId, payload) {
420
+ const fetchFn = getFetch();
421
+ const res = await fetchFn(authUrl(`/${providerId}`), {
422
+ method: "POST",
423
+ headers: { "Content-Type": "application/json" },
424
+ body: JSON.stringify(payload)
425
+ });
426
+ const body = await res.json().catch(() => ({}));
427
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
428
+ const session = handleAuthResponse(body, "SIGNED_IN");
429
+ return {
430
+ user: session.user,
431
+ accessToken: session.accessToken,
432
+ refreshToken: session.refreshToken
433
+ };
434
+ }
435
+ async function signInWithGitHub(code, redirectUri) {
436
+ return signInWithOAuth("github", {
437
+ code,
438
+ redirectUri
439
+ });
440
+ }
441
+ async function signInWithMicrosoft(code, redirectUri) {
442
+ return signInWithOAuth("microsoft", {
443
+ code,
444
+ redirectUri
445
+ });
446
+ }
447
+ async function signInWithApple(code, redirectUri, user) {
448
+ return signInWithOAuth("apple", {
449
+ code,
450
+ redirectUri,
451
+ user
452
+ });
453
+ }
454
+ async function signInWithFacebook(code, redirectUri) {
455
+ return signInWithOAuth("facebook", {
456
+ code,
457
+ redirectUri
458
+ });
459
+ }
460
+ async function signInWithTwitter(code, redirectUri, codeVerifier) {
461
+ return signInWithOAuth("twitter", {
462
+ code,
463
+ redirectUri,
464
+ codeVerifier
465
+ });
466
+ }
467
+ async function signInWithDiscord(code, redirectUri) {
468
+ return signInWithOAuth("discord", {
469
+ code,
470
+ redirectUri
471
+ });
472
+ }
473
+ async function signInWithGitLab(code, redirectUri) {
474
+ return signInWithOAuth("gitlab", {
475
+ code,
476
+ redirectUri
477
+ });
478
+ }
479
+ async function signInWithBitbucket(code, redirectUri) {
480
+ return signInWithOAuth("bitbucket", {
481
+ code,
482
+ redirectUri
483
+ });
484
+ }
485
+ async function signInWithSlack(code, redirectUri) {
486
+ return signInWithOAuth("slack", {
487
+ code,
488
+ redirectUri
489
+ });
490
+ }
491
+ async function signInWithSpotify(code, redirectUri) {
492
+ return signInWithOAuth("spotify", {
493
+ code,
494
+ redirectUri
495
+ });
496
+ }
497
+ async function signOut() {
498
+ const fetchFn = getFetch();
499
+ try {
500
+ if (currentSession?.refreshToken) {
501
+ await fetchFn(authUrl("/logout"), {
502
+ method: "POST",
503
+ headers: { "Content-Type": "application/json" },
504
+ body: JSON.stringify({ refreshToken: currentSession.refreshToken })
505
+ });
506
+ }
507
+ } catch (e) {
508
+ }
509
+ currentSession = null;
510
+ clearStoredSession();
511
+ if (refreshTimeout) {
512
+ clearTimeout(refreshTimeout);
513
+ refreshTimeout = null;
514
+ }
515
+ transport.setToken(null);
516
+ emit("SIGNED_OUT", null);
517
+ }
518
+ async function refreshSession() {
519
+ if (!currentSession?.refreshToken) {
520
+ throw new Error("No active session to refresh");
521
+ }
522
+ const fetchFn = getFetch();
523
+ const res = await fetchFn(authUrl("/refresh"), {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({ refreshToken: currentSession.refreshToken })
527
+ });
528
+ const body = await res.json().catch(() => ({}));
529
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
530
+ const session = {
531
+ accessToken: body.tokens.accessToken,
532
+ refreshToken: body.tokens.refreshToken,
533
+ expiresAt: body.tokens.accessTokenExpiresAt,
534
+ user: currentSession.user
535
+ };
536
+ currentSession = session;
537
+ saveSession(session);
538
+ transport.setToken(session.accessToken);
539
+ scheduleRefresh(session.expiresAt);
540
+ emit("TOKEN_REFRESHED", session);
541
+ return session;
542
+ }
543
+ async function getUser() {
544
+ const data = await transport.request(authPath + "/me", { method: "GET" });
545
+ return data.user;
546
+ }
547
+ async function updateUser(updates) {
548
+ const data = await transport.request(authPath + "/me", {
549
+ method: "PATCH",
550
+ body: JSON.stringify(updates)
551
+ });
552
+ if (currentSession) {
553
+ currentSession = {
554
+ ...currentSession,
555
+ user: data.user
556
+ };
557
+ saveSession(currentSession);
558
+ emit("USER_UPDATED", currentSession);
559
+ }
560
+ return data.user;
561
+ }
562
+ async function resetPasswordForEmail(email) {
563
+ const fetchFn = getFetch();
564
+ const res = await fetchFn(authUrl("/forgot-password"), {
565
+ method: "POST",
566
+ headers: { "Content-Type": "application/json" },
567
+ body: JSON.stringify({ email })
568
+ });
569
+ const body = await res.json().catch(() => ({}));
570
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
571
+ return body;
572
+ }
573
+ async function resetPassword(token, password) {
574
+ const fetchFn = getFetch();
575
+ const res = await fetchFn(authUrl("/reset-password"), {
576
+ method: "POST",
577
+ headers: { "Content-Type": "application/json" },
578
+ body: JSON.stringify({
579
+ token,
580
+ password
581
+ })
582
+ });
583
+ const body = await res.json().catch(() => ({}));
584
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
585
+ return body;
586
+ }
587
+ async function changePassword(oldPassword, newPassword) {
588
+ return transport.request(authPath + "/change-password", {
589
+ method: "POST",
590
+ body: JSON.stringify({
591
+ oldPassword,
592
+ newPassword
593
+ })
594
+ });
595
+ }
596
+ async function sendVerificationEmail() {
597
+ return transport.request(authPath + "/send-verification", {
598
+ method: "POST"
599
+ });
600
+ }
601
+ async function verifyEmail(token) {
602
+ const fetchFn = getFetch();
603
+ const res = await fetchFn(authUrl("/verify-email?token=" + encodeURIComponent(token)), {
604
+ method: "GET",
605
+ headers: { "Content-Type": "application/json" }
606
+ });
607
+ const body = await res.json().catch(() => ({}));
608
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
609
+ return body;
610
+ }
611
+ async function getSessions() {
612
+ const data = await transport.request(authPath + "/sessions", { method: "GET" });
613
+ return data.sessions;
614
+ }
615
+ async function revokeSession(sessionId) {
616
+ return transport.request(authPath + "/sessions/" + encodeURIComponent(sessionId), {
617
+ method: "DELETE"
618
+ });
619
+ }
620
+ async function revokeAllSessions() {
621
+ const result = await transport.request(authPath + "/sessions", {
622
+ method: "DELETE"
623
+ });
624
+ currentSession = null;
625
+ clearStoredSession();
626
+ if (refreshTimeout) {
627
+ clearTimeout(refreshTimeout);
628
+ refreshTimeout = null;
629
+ }
630
+ transport.setToken(null);
631
+ emit("SIGNED_OUT", null);
632
+ return result;
633
+ }
634
+ async function getAuthConfig() {
635
+ const fetchFn = getFetch();
636
+ const res = await fetchFn(authUrl("/config"), {
637
+ method: "GET",
638
+ headers: { "Content-Type": "application/json" }
639
+ });
640
+ const body = await res.json().catch(() => ({}));
641
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
642
+ return body;
643
+ }
644
+ function getSession() {
645
+ return currentSession;
646
+ }
647
+ function onAuthStateChange(callback) {
648
+ listeners.add(callback);
649
+ return () => listeners.delete(callback);
650
+ }
651
+ if (persistSession) {
652
+ const stored = loadStoredSession();
653
+ if (stored && stored.accessToken && stored.refreshToken) {
654
+ if (stored.expiresAt > Date.now()) {
655
+ currentSession = stored;
656
+ transport.setToken(stored.accessToken);
657
+ scheduleRefresh(stored.expiresAt);
658
+ } else if (stored.refreshToken) {
659
+ currentSession = stored;
660
+ refreshSession().catch(() => {
661
+ currentSession = null;
662
+ clearStoredSession();
663
+ transport.setToken(null);
664
+ });
665
+ }
666
+ }
667
+ }
668
+ return {
669
+ signInWithEmail,
670
+ signUp,
671
+ signInWithGoogle,
672
+ signInWithLinkedin,
673
+ signInWithOAuth,
674
+ signInWithGitHub,
675
+ signInWithMicrosoft,
676
+ signInWithApple,
677
+ signInWithFacebook,
678
+ signInWithTwitter,
679
+ signInWithDiscord,
680
+ signInWithGitLab,
681
+ signInWithBitbucket,
682
+ signInWithSlack,
683
+ signInWithSpotify,
684
+ signOut,
685
+ refreshSession,
686
+ getUser,
687
+ updateUser,
688
+ resetPasswordForEmail,
689
+ resetPassword,
690
+ changePassword,
691
+ sendVerificationEmail,
692
+ verifyEmail,
693
+ getSessions,
694
+ revokeSession,
695
+ revokeAllSessions,
696
+ getAuthConfig,
697
+ getSession,
698
+ onAuthStateChange
699
+ };
700
+ }
701
+ function createAdmin(transport, options) {
702
+ const opts = options || {};
703
+ const adminPath = opts.adminPath || "/admin";
704
+ async function listUsers() {
705
+ return transport.request(adminPath + "/users", { method: "GET" });
706
+ }
707
+ async function listUsersPaginated(options2) {
708
+ const params = new URLSearchParams();
709
+ if (options2?.limit !== void 0) params.set("limit", String(options2.limit));
710
+ if (options2?.offset !== void 0) params.set("offset", String(options2.offset));
711
+ if (options2?.search) params.set("search", options2.search);
712
+ if (options2?.orderBy) params.set("orderBy", options2.orderBy);
713
+ if (options2?.orderDir) params.set("orderDir", options2.orderDir);
714
+ const qs = params.toString();
715
+ return transport.request(
716
+ adminPath + "/users" + (qs ? "?" + qs : ""),
717
+ { method: "GET" }
718
+ );
719
+ }
720
+ async function getUser(userId) {
721
+ return transport.request(adminPath + "/users/" + encodeURIComponent(userId), { method: "GET" });
722
+ }
723
+ async function createUser(data) {
724
+ return transport.request(adminPath + "/users", {
725
+ method: "POST",
726
+ body: JSON.stringify(data)
727
+ });
728
+ }
729
+ async function updateUser(userId, data) {
730
+ return transport.request(adminPath + "/users/" + encodeURIComponent(userId), {
731
+ method: "PUT",
732
+ body: JSON.stringify(data)
733
+ });
734
+ }
735
+ async function deleteUser(userId) {
736
+ return transport.request(adminPath + "/users/" + encodeURIComponent(userId), {
737
+ method: "DELETE"
738
+ });
739
+ }
740
+ async function listRoles() {
741
+ return transport.request(adminPath + "/roles", { method: "GET" });
742
+ }
743
+ async function getRole(roleId) {
744
+ return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), { method: "GET" });
745
+ }
746
+ async function createRole(data) {
747
+ return transport.request(adminPath + "/roles", {
748
+ method: "POST",
749
+ body: JSON.stringify(data)
750
+ });
751
+ }
752
+ async function updateRole(roleId, data) {
753
+ return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), {
754
+ method: "PUT",
755
+ body: JSON.stringify(data)
756
+ });
757
+ }
758
+ async function deleteRole(roleId) {
759
+ return transport.request(adminPath + "/roles/" + encodeURIComponent(roleId), {
760
+ method: "DELETE"
761
+ });
762
+ }
763
+ async function bootstrap() {
764
+ return transport.request(adminPath + "/bootstrap", {
765
+ method: "POST"
766
+ });
767
+ }
768
+ return {
769
+ listUsers,
770
+ listUsersPaginated,
771
+ getUser,
772
+ createUser,
773
+ updateUser,
774
+ deleteUser,
775
+ listRoles,
776
+ getRole,
777
+ createRole,
778
+ updateRole,
779
+ deleteRole,
780
+ bootstrap
781
+ };
782
+ }
783
+ function createCron(transport, options) {
784
+ const cronPath = options?.cronPath || "/cron";
785
+ async function listJobs() {
786
+ return transport.request(cronPath, { method: "GET" });
787
+ }
788
+ async function getJob(jobId) {
789
+ return transport.request(
790
+ cronPath + "/" + encodeURIComponent(jobId),
791
+ { method: "GET" }
792
+ );
793
+ }
794
+ async function triggerJob(jobId) {
795
+ return transport.request(
796
+ cronPath + "/" + encodeURIComponent(jobId) + "/trigger",
797
+ { method: "POST" }
798
+ );
799
+ }
800
+ async function getJobLogs(jobId, options2) {
801
+ const params = new URLSearchParams();
802
+ if (options2?.limit !== void 0) params.set("limit", String(options2.limit));
803
+ const qs = params.toString();
804
+ return transport.request(
805
+ cronPath + "/" + encodeURIComponent(jobId) + "/logs" + (qs ? "?" + qs : ""),
806
+ { method: "GET" }
807
+ );
808
+ }
809
+ async function toggleJob(jobId, enabled) {
810
+ return transport.request(
811
+ cronPath + "/" + encodeURIComponent(jobId),
812
+ {
813
+ method: "PUT",
814
+ body: JSON.stringify({ enabled })
815
+ }
816
+ );
817
+ }
818
+ return {
819
+ listJobs,
820
+ getJob,
821
+ triggerJob,
822
+ getJobLogs,
823
+ toggleJob
824
+ };
825
+ }
826
+ function mapOperator(op) {
827
+ switch (op) {
828
+ case "==":
829
+ return "eq";
830
+ case "!=":
831
+ return "neq";
832
+ case ">":
833
+ return "gt";
834
+ case ">=":
835
+ return "gte";
836
+ case "<":
837
+ return "lt";
838
+ case "<=":
839
+ return "lte";
840
+ case "array-contains":
841
+ return "cs";
842
+ case "array-contains-any":
843
+ return "csa";
844
+ case "not-in":
845
+ return "nin";
846
+ default:
847
+ return op;
848
+ }
849
+ }
850
+ class QueryBuilder {
851
+ constructor(collection) {
852
+ this.collection = collection;
853
+ }
854
+ params = { where: {} };
855
+ /**
856
+ * Add a filter condition to your query.
857
+ * @example
858
+ * client.collection('users').where('age', '>=', 18).find()
859
+ */
860
+ where(column, operator, value) {
861
+ if (!this.params.where) {
862
+ this.params.where = {};
863
+ }
864
+ const mappedOp = mapOperator(operator);
865
+ let formattedValue = value;
866
+ if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
867
+ formattedValue = `(${value.join(",")})`;
868
+ } else if (value === null) {
869
+ formattedValue = "null";
870
+ }
871
+ this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
872
+ return this;
873
+ }
874
+ /**
875
+ * Order the results by a specific column.
876
+ * @example
877
+ * client.collection('users').orderBy('createdAt', 'desc').find()
878
+ */
879
+ orderBy(column, ascending = "asc") {
880
+ this.params.orderBy = `${column}:${ascending}`;
881
+ return this;
882
+ }
883
+ /**
884
+ * Limit the number of results returned.
885
+ */
886
+ limit(count) {
887
+ this.params.limit = count;
888
+ return this;
889
+ }
890
+ /**
891
+ * Skip the first N results.
892
+ */
893
+ offset(count) {
894
+ this.params.offset = count;
895
+ return this;
896
+ }
897
+ /**
898
+ * Set a free-text search string if supported by the backend.
899
+ */
900
+ search(searchString) {
901
+ this.params.searchString = searchString;
902
+ return this;
903
+ }
904
+ /**
905
+ * Include related entities in the response.
906
+ * Relations will be populated with full entity data instead of just IDs.
907
+ *
908
+ * @param relations - Relation names to include, or "*" for all.
909
+ * @example
910
+ * // Include specific relations
911
+ * client.data.posts.include("tags", "author").find()
912
+ *
913
+ * // Include all relations
914
+ * client.data.posts.include("*").find()
915
+ */
916
+ include(...relations) {
917
+ this.params.include = relations;
918
+ return this;
919
+ }
920
+ /**
921
+ * Execute the find query and return the results.
922
+ */
923
+ async find() {
924
+ return this.collection.find(this.params);
925
+ }
926
+ /**
927
+ * Listen to realtime updates matching this query.
928
+ */
929
+ listen(onUpdate, onError) {
930
+ if (!this.collection.listen) {
931
+ throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
932
+ }
933
+ return this.collection.listen(this.params, onUpdate, onError);
934
+ }
935
+ }
936
+ function parseWhereFilter(where) {
937
+ if (!where) return void 0;
938
+ const filters = {};
939
+ for (const [key, rawValue] of Object.entries(where)) {
940
+ if (rawValue === null) {
941
+ filters[key] = ["==", null];
942
+ continue;
943
+ }
944
+ if (typeof rawValue === "boolean") {
945
+ filters[key] = ["==", rawValue];
946
+ continue;
947
+ }
948
+ if (typeof rawValue === "number") {
949
+ filters[key] = ["==", rawValue];
950
+ continue;
951
+ }
952
+ if (Array.isArray(rawValue) && rawValue.length === 2) {
953
+ const [rawOp, val] = rawValue;
954
+ const OP_TO_FILTER = {
955
+ "eq": "==",
956
+ "neq": "!=",
957
+ "gt": ">",
958
+ "gte": ">=",
959
+ "lt": "<",
960
+ "lte": "<=",
961
+ "==": "==",
962
+ "!=": "!=",
963
+ ">": ">",
964
+ ">=": ">=",
965
+ "<": "<",
966
+ "<=": "<=",
967
+ "in": "in",
968
+ "nin": "not-in",
969
+ "not-in": "not-in",
970
+ "cs": "array-contains",
971
+ "csa": "array-contains-any",
972
+ "array-contains": "array-contains",
973
+ "array-contains-any": "array-contains-any"
974
+ };
975
+ filters[key] = [OP_TO_FILTER[rawOp] ?? "==", val];
976
+ continue;
977
+ }
978
+ const value = String(rawValue);
979
+ const dotIndex = value.indexOf(".");
980
+ if (dotIndex > 0) {
981
+ const opStr = value.substring(0, dotIndex);
982
+ const valStr = value.substring(dotIndex + 1);
983
+ let op = "==";
984
+ let val = valStr;
985
+ switch (opStr) {
986
+ case "eq":
987
+ op = "==";
988
+ break;
989
+ case "neq":
990
+ op = "!=";
991
+ break;
992
+ case "gt":
993
+ op = ">";
994
+ break;
995
+ case "gte":
996
+ op = ">=";
997
+ break;
998
+ case "lt":
999
+ op = "<";
1000
+ break;
1001
+ case "lte":
1002
+ op = "<=";
1003
+ break;
1004
+ case "in":
1005
+ op = "in";
1006
+ val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
1007
+ break;
1008
+ case "nin":
1009
+ op = "not-in";
1010
+ val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
1011
+ break;
1012
+ case "cs":
1013
+ op = "array-contains";
1014
+ break;
1015
+ case "csa":
1016
+ op = "array-contains-any";
1017
+ val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
1018
+ break;
1019
+ default:
1020
+ op = "==";
1021
+ val = value;
1022
+ }
1023
+ if (val === "true") val = true;
1024
+ else if (val === "false") val = false;
1025
+ else if (val === "null") val = null;
1026
+ else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && key !== "id" && !key.endsWith("_id")) val = Number(val);
1027
+ filters[key] = [op, val];
1028
+ } else {
1029
+ filters[key] = ["==", value];
1030
+ }
1031
+ }
1032
+ return filters;
1033
+ }
1034
+ function rowToEntity(row, slug) {
1035
+ return {
1036
+ id: row.id,
1037
+ path: slug,
1038
+ values: row
1039
+ };
1040
+ }
1041
+ function createCollectionClient(transport, slug, ws) {
1042
+ const basePath = `/data/${slug}`;
1043
+ const client = {
1044
+ async find(params) {
1045
+ const qs = buildQueryString(params);
1046
+ const raw = await transport.request(basePath + qs, { method: "GET" });
1047
+ return {
1048
+ data: (raw.data || []).map((row) => rowToEntity(row, slug)),
1049
+ meta: raw.meta
1050
+ };
1051
+ },
1052
+ async findById(id) {
1053
+ const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
1054
+ if (!raw) return void 0;
1055
+ return rowToEntity(raw, slug);
1056
+ },
1057
+ async create(data, id) {
1058
+ const body = { ...data };
1059
+ if (id !== void 0) {
1060
+ body.id = id;
1061
+ }
1062
+ const raw = await transport.request(basePath, {
1063
+ method: "POST",
1064
+ body: JSON.stringify(body)
1065
+ });
1066
+ return rowToEntity(raw, slug);
1067
+ },
1068
+ async update(id, data) {
1069
+ const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1070
+ method: "PUT",
1071
+ body: JSON.stringify(data)
1072
+ });
1073
+ return rowToEntity(raw, slug);
1074
+ },
1075
+ async delete(id) {
1076
+ return transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1077
+ method: "DELETE"
1078
+ });
1079
+ },
1080
+ // Fluent builder instantiation
1081
+ where(column, operator, value) {
1082
+ return new QueryBuilder(client).where(column, operator, value);
1083
+ },
1084
+ orderBy(column, ascending) {
1085
+ return new QueryBuilder(client).orderBy(column, ascending);
1086
+ },
1087
+ limit(count) {
1088
+ return new QueryBuilder(client).limit(count);
1089
+ },
1090
+ offset(count) {
1091
+ return new QueryBuilder(client).offset(count);
1092
+ },
1093
+ search(searchString) {
1094
+ return new QueryBuilder(client).search(searchString);
1095
+ },
1096
+ include(...relations) {
1097
+ return new QueryBuilder(client).include(...relations);
1098
+ }
1099
+ };
1100
+ if (ws) {
1101
+ client.listen = (params, onUpdate, onError) => {
1102
+ return ws.listenCollection(
1103
+ {
1104
+ path: slug,
1105
+ filter: parseWhereFilter(params?.where),
1106
+ limit: params?.limit,
1107
+ startAfter: params?.offset ? String(params.offset) : void 0,
1108
+ orderBy: params?.orderBy?.split(":")[0],
1109
+ order: params?.orderBy?.split(":")[1],
1110
+ searchString: params?.searchString
1111
+ },
1112
+ (entities) => {
1113
+ const requestedLimit = params?.limit || 20;
1114
+ onUpdate({
1115
+ data: entities,
1116
+ meta: {
1117
+ total: entities.length,
1118
+ limit: requestedLimit,
1119
+ offset: params?.offset || 0,
1120
+ hasMore: entities.length >= requestedLimit
1121
+ }
1122
+ });
1123
+ },
1124
+ onError
1125
+ );
1126
+ };
1127
+ client.listenById = (id, onUpdate, onError) => {
1128
+ return ws.listenEntity(
1129
+ {
1130
+ path: slug,
1131
+ entityId: String(id)
1132
+ },
1133
+ (entity) => {
1134
+ if (entity) {
1135
+ onUpdate(entity);
1136
+ } else {
1137
+ onUpdate(void 0);
1138
+ }
1139
+ },
1140
+ onError
1141
+ );
1142
+ };
1143
+ }
1144
+ return client;
1145
+ }
1146
+ function rehydrateEntity(entity) {
1147
+ return entity;
1148
+ }
1149
+ function extractMessageError(message) {
1150
+ const payload = message.payload;
1151
+ const errPayload = payload?.error;
1152
+ const errorMessage = typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error";
1153
+ const errorCode = typeof errPayload === "object" ? errPayload.code : payload?.code;
1154
+ return {
1155
+ errorMessage,
1156
+ errorCode
1157
+ };
1158
+ }
1159
+ class ApiError extends Error {
1160
+ code;
1161
+ error;
1162
+ constructor(message, error, code) {
1163
+ super(message);
1164
+ this.name = "ApiError";
1165
+ this.code = code;
1166
+ this.error = error;
1167
+ }
1168
+ }
1169
+ class RebaseWebSocketClient {
1170
+ websocketUrl;
1171
+ ws = null;
1172
+ getAuthToken;
1173
+ subscriptions = /* @__PURE__ */ new Map();
1174
+ listeners = /* @__PURE__ */ new Map();
1175
+ on(event, cb) {
1176
+ if (!this.listeners.has(event)) {
1177
+ this.listeners.set(event, /* @__PURE__ */ new Set());
1178
+ }
1179
+ this.listeners.get(event).add(cb);
1180
+ return () => this.listeners.get(event).delete(cb);
1181
+ }
1182
+ emit(event, ...args) {
1183
+ if (this.listeners.has(event)) {
1184
+ this.listeners.get(event).forEach((cb) => cb(...args));
1185
+ }
1186
+ }
1187
+ // New: Subscription deduplication management with optimizations
1188
+ collectionSubscriptions = /* @__PURE__ */ new Map();
1189
+ entitySubscriptions = /* @__PURE__ */ new Map();
1190
+ // Maps to quickly find subscription by backend subscription ID
1191
+ backendToCollectionKey = /* @__PURE__ */ new Map();
1192
+ backendToEntityKey = /* @__PURE__ */ new Map();
1193
+ pendingRequests = /* @__PURE__ */ new Map();
1194
+ reconnectAttempts = 0;
1195
+ maxReconnectAttempts = 5;
1196
+ isConnected = false;
1197
+ messageQueue = [];
1198
+ reconnectTimeout = null;
1199
+ isAuthenticated = false;
1200
+ authPromise = null;
1201
+ WebSocketConstructor;
1202
+ constructor(config) {
1203
+ this.websocketUrl = config.websocketUrl;
1204
+ this.getAuthToken = config.getAuthToken;
1205
+ this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
1206
+ if (!this.WebSocketConstructor) {
1207
+ console.warn("WebSocket is not defined in this environment. Realtime subscriptions will not work unless you provide a WebSocket implementation in the config.");
1208
+ } else {
1209
+ this.initWebSocket();
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Authenticate the WebSocket connection
1214
+ */
1215
+ async authenticate(token) {
1216
+ return new Promise((resolve, reject) => {
1217
+ const requestId = `auth_${Date.now()}`;
1218
+ const timeout = setTimeout(() => {
1219
+ this.pendingRequests.delete(requestId);
1220
+ this.authPromise = null;
1221
+ reject(new Error("Authentication timeout"));
1222
+ }, 3e4);
1223
+ this.pendingRequests.set(requestId, {
1224
+ resolve: () => {
1225
+ clearTimeout(timeout);
1226
+ this.isAuthenticated = true;
1227
+ resolve();
1228
+ },
1229
+ reject: (error) => {
1230
+ clearTimeout(timeout);
1231
+ reject(error);
1232
+ }
1233
+ });
1234
+ const message = {
1235
+ type: "AUTHENTICATE",
1236
+ requestId,
1237
+ payload: { token }
1238
+ };
1239
+ if (!this.isConnected || !this.ws) {
1240
+ this.messageQueue.unshift(message);
1241
+ } else {
1242
+ this.ws.send(JSON.stringify(message));
1243
+ }
1244
+ });
1245
+ }
1246
+ /**
1247
+ * Set the auth token getter function
1248
+ */
1249
+ setAuthTokenGetter(getAuthToken) {
1250
+ this.getAuthToken = getAuthToken;
1251
+ if (this.isConnected && !this.isAuthenticated && !this.authPromise) {
1252
+ console.log("WebSocket auto-authenticating after token getter set");
1253
+ this.getAuthToken().then((token) => {
1254
+ if (!this.ws) return;
1255
+ if (token) {
1256
+ this.authenticate(token).catch((e) => {
1257
+ if (this.ws) console.warn("WebSocket auto-auth failed:", e);
1258
+ });
1259
+ }
1260
+ }).catch((e) => {
1261
+ if (this.ws) console.warn("WebSocket auto-auth failed:", e);
1262
+ });
1263
+ }
1264
+ }
1265
+ disconnect() {
1266
+ this.isAuthenticated = false;
1267
+ this.authPromise = null;
1268
+ if (this.reconnectTimeout) {
1269
+ clearTimeout(this.reconnectTimeout);
1270
+ this.reconnectTimeout = null;
1271
+ }
1272
+ if (this.ws) {
1273
+ this.ws.onclose = null;
1274
+ this.ws.onerror = null;
1275
+ this.ws.onopen = null;
1276
+ this.ws.onmessage = null;
1277
+ this.ws.close();
1278
+ this.ws = null;
1279
+ }
1280
+ }
1281
+ // Initialize WebSocket connection
1282
+ initWebSocket() {
1283
+ if (!this.WebSocketConstructor) return;
1284
+ if (this.ws?.readyState === this.WebSocketConstructor.OPEN) return;
1285
+ try {
1286
+ this.ws = new this.WebSocketConstructor(this.websocketUrl);
1287
+ this.ws.onopen = async () => {
1288
+ console.log("Connected to PostgreSQL backend");
1289
+ const wasReconnect = this.reconnectAttempts > 0;
1290
+ this.isConnected = true;
1291
+ this.reconnectAttempts = 0;
1292
+ if (this.getAuthToken && !this.isAuthenticated) {
1293
+ try {
1294
+ const token = await this.getAuthToken();
1295
+ if (token) {
1296
+ await this.authenticate(token);
1297
+ console.log("WebSocket auto-authenticated");
1298
+ }
1299
+ } catch (error) {
1300
+ console.warn("WebSocket auto-auth failed, requests may fail:", error);
1301
+ }
1302
+ }
1303
+ this.emit(wasReconnect ? "reconnect" : "connect");
1304
+ this.processMessageQueue();
1305
+ if (wasReconnect) {
1306
+ this.resubscribeAll();
1307
+ }
1308
+ };
1309
+ this.ws.onmessage = (event) => {
1310
+ try {
1311
+ const message = JSON.parse(event.data, rebaseReviver);
1312
+ this.handleWebSocketMessage(message);
1313
+ } catch (error) {
1314
+ console.error("Error parsing WebSocket message:", error);
1315
+ }
1316
+ };
1317
+ this.ws.onclose = () => {
1318
+ console.log("Disconnected from PostgreSQL backend");
1319
+ this.isConnected = false;
1320
+ this.isAuthenticated = false;
1321
+ this.authPromise = null;
1322
+ this.emit("disconnect");
1323
+ for (const [reqId, request] of this.pendingRequests.entries()) {
1324
+ if (reqId.startsWith("auth_")) {
1325
+ request.reject(new Error("Connection closed during authentication"));
1326
+ } else if (request.message) {
1327
+ request.message._queuedResolve = request.resolve;
1328
+ request.message._queuedReject = request.reject;
1329
+ this.messageQueue.push(request.message);
1330
+ } else {
1331
+ request.reject(new ApiError("Connection closed", "Connection closed"));
1332
+ }
1333
+ this.pendingRequests.delete(reqId);
1334
+ }
1335
+ this.attemptReconnect();
1336
+ };
1337
+ this.ws.onerror = (error) => {
1338
+ console.error("WebSocket error:", error);
1339
+ this.isConnected = false;
1340
+ this.emit("error", error);
1341
+ };
1342
+ } catch (error) {
1343
+ console.error("Failed to initialize WebSocket:", error);
1344
+ this.attemptReconnect();
1345
+ }
1346
+ }
1347
+ processMessageQueue() {
1348
+ while (this.messageQueue.length > 0 && this.isConnected) {
1349
+ const message = this.messageQueue.shift();
1350
+ if (message) this.sendMessage(message);
1351
+ }
1352
+ }
1353
+ attemptReconnect() {
1354
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1355
+ console.error("Max reconnection attempts reached");
1356
+ return;
1357
+ }
1358
+ this.reconnectAttempts++;
1359
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
1360
+ console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
1361
+ if (this.reconnectTimeout) {
1362
+ clearTimeout(this.reconnectTimeout);
1363
+ }
1364
+ this.reconnectTimeout = setTimeout(() => {
1365
+ this.reconnectTimeout = null;
1366
+ this.initWebSocket();
1367
+ }, delay);
1368
+ }
1369
+ handleWebSocketMessage(message) {
1370
+ const {
1371
+ type,
1372
+ requestId,
1373
+ subscriptionId
1374
+ } = message;
1375
+ if (requestId && this.pendingRequests.has(requestId)) {
1376
+ const {
1377
+ resolve,
1378
+ reject
1379
+ } = this.pendingRequests.get(requestId);
1380
+ this.pendingRequests.delete(requestId);
1381
+ if (type === "ERROR" || type === "AUTH_ERROR" || message.error) {
1382
+ const { errorMessage, errorCode } = extractMessageError(message);
1383
+ reject(new ApiError(errorMessage, errorMessage, errorCode));
1384
+ } else {
1385
+ resolve(message.payload || message);
1386
+ }
1387
+ return;
1388
+ }
1389
+ if (subscriptionId && type === "collection_update") {
1390
+ const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1391
+ if (subscriptionKey) {
1392
+ const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1393
+ if (collectionSub) {
1394
+ const incomingEntities = (message.entities || []).map((e) => rehydrateEntity(e));
1395
+ const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
1396
+ collectionSub.latestData = entities;
1397
+ collectionSub.lastUpdated = Date.now();
1398
+ collectionSub.isInitialDataReceived = true;
1399
+ collectionSub.callbacks.forEach((callback) => {
1400
+ try {
1401
+ callback.onUpdate(entities);
1402
+ } catch (error) {
1403
+ console.error("Error in collection subscription callback:", error);
1404
+ if (callback.onError) {
1405
+ callback.onError(error instanceof Error ? error : new Error(String(error)));
1406
+ }
1407
+ }
1408
+ });
1409
+ return;
1410
+ }
1411
+ }
1412
+ }
1413
+ if (subscriptionId && type === "collection_entity_patch") {
1414
+ const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1415
+ if (subscriptionKey) {
1416
+ const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1417
+ if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
1418
+ const patchEntity = message.entity ? rehydrateEntity(message.entity) : message.entity;
1419
+ const patchEntityId = message.entityId;
1420
+ let updated;
1421
+ if (patchEntity === null || patchEntity === void 0) {
1422
+ updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1423
+ } else {
1424
+ const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchEntity.id));
1425
+ if (idx >= 0) {
1426
+ updated = [...collectionSub.latestData];
1427
+ updated[idx] = patchEntity;
1428
+ } else {
1429
+ updated = [patchEntity, ...collectionSub.latestData];
1430
+ }
1431
+ }
1432
+ collectionSub.latestData = updated;
1433
+ collectionSub.lastUpdated = Date.now();
1434
+ collectionSub.callbacks.forEach((callback) => {
1435
+ try {
1436
+ callback.onUpdate(updated);
1437
+ } catch (error) {
1438
+ console.error("Error in collection patch callback:", error);
1439
+ if (callback.onError) {
1440
+ callback.onError(error instanceof Error ? error : new Error(String(error)));
1441
+ }
1442
+ }
1443
+ });
1444
+ return;
1445
+ }
1446
+ }
1447
+ }
1448
+ if (subscriptionId && type === "entity_update") {
1449
+ const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
1450
+ if (subscriptionKey) {
1451
+ const entitySub = this.entitySubscriptions.get(subscriptionKey);
1452
+ if (entitySub) {
1453
+ const entity = message.entity ? rehydrateEntity(message.entity) : null;
1454
+ entitySub.latestData = entity;
1455
+ entitySub.lastUpdated = Date.now();
1456
+ entitySub.isInitialDataReceived = true;
1457
+ entitySub.callbacks.forEach((callback) => {
1458
+ try {
1459
+ callback.onUpdate(entity);
1460
+ } catch (error) {
1461
+ console.error("Error in entity subscription callback:", error);
1462
+ if (callback.onError) {
1463
+ callback.onError(error instanceof Error ? error : new Error(String(error)));
1464
+ }
1465
+ }
1466
+ });
1467
+ return;
1468
+ }
1469
+ }
1470
+ }
1471
+ if (subscriptionId && (type === "ERROR" || message.error)) {
1472
+ const collectionKey = this.backendToCollectionKey.get(subscriptionId);
1473
+ if (collectionKey) {
1474
+ const collectionSub = this.collectionSubscriptions.get(collectionKey);
1475
+ if (collectionSub) {
1476
+ const { errorMessage, errorCode } = extractMessageError(message);
1477
+ const error = new ApiError(errorMessage, errorMessage, errorCode);
1478
+ collectionSub.callbacks.forEach((callback) => {
1479
+ if (callback.onError) {
1480
+ callback.onError(error);
1481
+ }
1482
+ });
1483
+ return;
1484
+ }
1485
+ }
1486
+ const entityKey = this.backendToEntityKey.get(subscriptionId);
1487
+ if (entityKey) {
1488
+ const entitySub = this.entitySubscriptions.get(entityKey);
1489
+ if (entitySub) {
1490
+ const { errorMessage, errorCode } = extractMessageError(message);
1491
+ const error = new ApiError(errorMessage, errorMessage, errorCode);
1492
+ entitySub.callbacks.forEach((callback) => {
1493
+ if (callback.onError) {
1494
+ callback.onError(error);
1495
+ }
1496
+ });
1497
+ return;
1498
+ }
1499
+ }
1500
+ }
1501
+ if (subscriptionId && this.subscriptions.has(subscriptionId)) {
1502
+ const callback = this.subscriptions.get(subscriptionId);
1503
+ if (!callback) {
1504
+ throw new Error(`Subscription callback not found for subscriptionId: ${subscriptionId}`);
1505
+ }
1506
+ if (message.type === "ERROR" || message.error) {
1507
+ if (callback.onError) {
1508
+ const { errorMessage, errorCode } = extractMessageError(message);
1509
+ callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
1510
+ }
1511
+ } else {
1512
+ callback.onUpdate(message);
1513
+ }
1514
+ }
1515
+ }
1516
+ async ensureAuthenticated(retryCount = 3) {
1517
+ if (this.isAuthenticated || !this.getAuthToken) return;
1518
+ if (this.authPromise) {
1519
+ await this.authPromise;
1520
+ return;
1521
+ }
1522
+ let lastError = null;
1523
+ for (let attempt = 0; attempt < retryCount; attempt++) {
1524
+ try {
1525
+ const token = await this.getAuthToken();
1526
+ if (!token) throw new Error("user not logged in");
1527
+ this.authPromise = this.authenticate(token);
1528
+ await this.authPromise;
1529
+ this.authPromise = null;
1530
+ console.log("WebSocket authenticated on demand");
1531
+ return;
1532
+ } catch (error) {
1533
+ this.authPromise = null;
1534
+ lastError = error;
1535
+ const errMsg = error instanceof Error ? error.message : String(error);
1536
+ if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
1537
+ console.warn("WebSocket auth failed: user not logged in");
1538
+ throw error;
1539
+ }
1540
+ if (errMsg.includes("still loading")) {
1541
+ if (attempt < retryCount - 1) {
1542
+ const delay = Math.min(500 * (attempt + 1), 2e3);
1543
+ await new Promise((resolve) => setTimeout(resolve, delay));
1544
+ continue;
1545
+ }
1546
+ }
1547
+ if (attempt < retryCount - 1) {
1548
+ const delay = Math.min(1e3 * (attempt + 1), 3e3);
1549
+ console.log(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
1550
+ await new Promise((resolve) => setTimeout(resolve, delay));
1551
+ }
1552
+ }
1553
+ }
1554
+ console.warn("WebSocket on-demand auth failed after retries:", lastError);
1555
+ throw lastError;
1556
+ }
1557
+ /**
1558
+ * Force re-authentication (call after token refresh)
1559
+ */
1560
+ async reauthenticate() {
1561
+ if (!this.getAuthToken) return;
1562
+ this.isAuthenticated = false;
1563
+ try {
1564
+ const token = await this.getAuthToken();
1565
+ await this.authenticate(token);
1566
+ console.log("WebSocket reauthenticated successfully");
1567
+ } catch (error) {
1568
+ console.error("WebSocket reauthentication failed:", error);
1569
+ throw error;
1570
+ }
1571
+ }
1572
+ sendMessage(message) {
1573
+ const queuedMsg = message;
1574
+ if (queuedMsg._queuedResolve && queuedMsg._queuedReject) {
1575
+ return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
1576
+ }
1577
+ if (!this.isConnected || !this.ws) {
1578
+ return new Promise((resolve, reject) => {
1579
+ const queueable = message;
1580
+ queueable._queuedResolve = resolve;
1581
+ queueable._queuedReject = reject;
1582
+ this.messageQueue.push(message);
1583
+ });
1584
+ }
1585
+ return new Promise((resolve, reject) => {
1586
+ this.doSendMessage(message, resolve, reject);
1587
+ });
1588
+ }
1589
+ async doSendMessage(message, resolve, reject) {
1590
+ if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) {
1591
+ try {
1592
+ await this.ensureAuthenticated();
1593
+ } catch (error) {
1594
+ const errorMessage = error instanceof Error ? error.message : "Authentication required";
1595
+ reject(new ApiError(errorMessage, errorMessage));
1596
+ return;
1597
+ }
1598
+ }
1599
+ const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1600
+ message.requestId = requestId;
1601
+ if (!this.pendingRequests.has(requestId)) {
1602
+ this.pendingRequests.set(requestId, {
1603
+ resolve,
1604
+ reject,
1605
+ message
1606
+ });
1607
+ }
1608
+ try {
1609
+ this.ws.send(JSON.stringify(message));
1610
+ } catch (error) {
1611
+ this.pendingRequests.delete(requestId);
1612
+ reject(new ApiError("Failed to send message", error instanceof Error ? error.message : "Unknown error"));
1613
+ }
1614
+ }
1615
+ // Data source methods
1616
+ async fetchCollection(props) {
1617
+ const response = await this.sendMessage({
1618
+ type: "FETCH_COLLECTION",
1619
+ payload: props
1620
+ });
1621
+ return (response.entities || []).map((e) => rehydrateEntity(e));
1622
+ }
1623
+ async fetchEntity(props) {
1624
+ const response = await this.sendMessage({
1625
+ type: "FETCH_ENTITY",
1626
+ payload: props
1627
+ });
1628
+ return response.entity ? rehydrateEntity(response.entity) : void 0;
1629
+ }
1630
+ async saveEntity(props) {
1631
+ const response = await this.sendMessage({
1632
+ type: "SAVE_ENTITY",
1633
+ payload: props
1634
+ });
1635
+ return rehydrateEntity(response.entity);
1636
+ }
1637
+ async deleteEntity(props) {
1638
+ await this.sendMessage({
1639
+ type: "DELETE_ENTITY",
1640
+ payload: props
1641
+ });
1642
+ }
1643
+ async executeSql(sql, options) {
1644
+ const response = await this.sendMessage({
1645
+ type: "EXECUTE_SQL",
1646
+ payload: {
1647
+ sql,
1648
+ options
1649
+ }
1650
+ });
1651
+ return response.result || [];
1652
+ }
1653
+ async fetchAvailableDatabases() {
1654
+ const response = await this.sendMessage({
1655
+ type: "FETCH_DATABASES",
1656
+ payload: {}
1657
+ });
1658
+ return response.databases || [];
1659
+ }
1660
+ async fetchAvailableRoles() {
1661
+ const response = await this.sendMessage({
1662
+ type: "FETCH_ROLES"
1663
+ });
1664
+ return response.roles || [];
1665
+ }
1666
+ async fetchCurrentDatabase() {
1667
+ const response = await this.sendMessage({
1668
+ type: "FETCH_CURRENT_DATABASE"
1669
+ });
1670
+ return response.database;
1671
+ }
1672
+ async checkUniqueField(path, name, value, entityId, collection) {
1673
+ const response = await this.sendMessage({
1674
+ type: "CHECK_UNIQUE_FIELD",
1675
+ payload: {
1676
+ path,
1677
+ name,
1678
+ value,
1679
+ entityId,
1680
+ collection
1681
+ }
1682
+ });
1683
+ return response.isUnique;
1684
+ }
1685
+ async countEntities(props) {
1686
+ const response = await this.sendMessage({
1687
+ type: "COUNT_ENTITIES",
1688
+ payload: props
1689
+ });
1690
+ return response.count;
1691
+ }
1692
+ async fetchUnmappedTables(mappedPaths) {
1693
+ const response = await this.sendMessage({
1694
+ type: "FETCH_UNMAPPED_TABLES",
1695
+ payload: { mappedPaths }
1696
+ });
1697
+ return response.tables || [];
1698
+ }
1699
+ async fetchTableMetadata(tableName) {
1700
+ const response = await this.sendMessage({
1701
+ type: "FETCH_TABLE_METADATA",
1702
+ payload: { tableName }
1703
+ });
1704
+ return response.metadata || {
1705
+ columns: [],
1706
+ foreignKeys: [],
1707
+ junctions: [],
1708
+ policies: []
1709
+ };
1710
+ }
1711
+ async createBranch(name, options) {
1712
+ const response = await this.sendMessage({
1713
+ type: "CREATE_BRANCH",
1714
+ payload: {
1715
+ name,
1716
+ options
1717
+ }
1718
+ });
1719
+ return response.branch;
1720
+ }
1721
+ async deleteBranch(name) {
1722
+ await this.sendMessage({
1723
+ type: "DELETE_BRANCH",
1724
+ payload: { name }
1725
+ });
1726
+ }
1727
+ async listBranches() {
1728
+ const response = await this.sendMessage({
1729
+ type: "LIST_BRANCHES",
1730
+ payload: {}
1731
+ });
1732
+ return response.branches || [];
1733
+ }
1734
+ /**
1735
+ * Recursively compare two values for structural equality.
1736
+ * Handles primitives, null, undefined, Date, RegExp, arrays, and plain objects.
1737
+ */
1738
+ deepEqual(a, b) {
1739
+ if (a === b) return true;
1740
+ if (a === null || b === null || a === void 0 || b === void 0) return false;
1741
+ if (typeof a !== typeof b) return false;
1742
+ if (typeof a !== "object") return false;
1743
+ if (a instanceof Date && b instanceof Date) {
1744
+ return a.getTime() === b.getTime();
1745
+ }
1746
+ if (a instanceof Date || b instanceof Date) return false;
1747
+ if (a instanceof RegExp && b instanceof RegExp) {
1748
+ return a.source === b.source && a.flags === b.flags;
1749
+ }
1750
+ if (a instanceof RegExp || b instanceof RegExp) return false;
1751
+ const aIsArray = Array.isArray(a);
1752
+ const bIsArray = Array.isArray(b);
1753
+ if (aIsArray !== bIsArray) return false;
1754
+ if (aIsArray && bIsArray) {
1755
+ if (a.length !== b.length) return false;
1756
+ for (let i = 0; i < a.length; i++) {
1757
+ if (!this.deepEqual(a[i], b[i])) return false;
1758
+ }
1759
+ return true;
1760
+ }
1761
+ const aObj = a;
1762
+ const bObj = b;
1763
+ const aKeys = Object.keys(aObj);
1764
+ const bKeys = Object.keys(bObj);
1765
+ if (aKeys.length !== bKeys.length) return false;
1766
+ for (const key of aKeys) {
1767
+ if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
1768
+ if (!this.deepEqual(aObj[key], bObj[key])) return false;
1769
+ }
1770
+ return true;
1771
+ }
1772
+ normalizeForComparison(val) {
1773
+ if (!val) return val;
1774
+ if (Array.isArray(val)) {
1775
+ return val.map((item) => this.normalizeForComparison(item));
1776
+ }
1777
+ if (typeof val === "object") {
1778
+ if (val instanceof Date) return val;
1779
+ if (val instanceof RegExp) return val;
1780
+ const obj = val;
1781
+ if (obj.__type === "relation") {
1782
+ const { data, ...rest } = obj;
1783
+ return rest;
1784
+ }
1785
+ const result = {};
1786
+ for (const [k, v] of Object.entries(obj)) {
1787
+ result[k] = this.normalizeForComparison(v);
1788
+ }
1789
+ return result;
1790
+ }
1791
+ return val;
1792
+ }
1793
+ /**
1794
+ * Merge incoming entities with cached data, preserving cached references
1795
+ * for entities whose values haven't changed. This avoids unnecessary
1796
+ * React re-renders when the server refetches all entities but most
1797
+ * haven't actually changed.
1798
+ */
1799
+ mergeEntities(cached, incoming) {
1800
+ if (!cached || cached.length === 0) return incoming;
1801
+ const cachedById = /* @__PURE__ */ new Map();
1802
+ for (const entity of cached) {
1803
+ cachedById.set(entity.id, entity);
1804
+ }
1805
+ return incoming.map((incomingEntity) => {
1806
+ const cachedEntity = cachedById.get(incomingEntity.id);
1807
+ if (!cachedEntity) return incomingEntity;
1808
+ if (cachedEntity.path === incomingEntity.path) {
1809
+ const normCached = this.normalizeForComparison(cachedEntity.values);
1810
+ const normIncoming = this.normalizeForComparison(incomingEntity.values);
1811
+ if (this.deepEqual(normCached, normIncoming)) {
1812
+ return cachedEntity;
1813
+ } else {
1814
+ const mismatches = {};
1815
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
1816
+ for (const key of allKeys) {
1817
+ if (!this.deepEqual(normCached[key], normIncoming[key])) {
1818
+ mismatches[key] = {
1819
+ cached: normCached[key],
1820
+ incoming: normIncoming[key]
1821
+ };
1822
+ }
1823
+ }
1824
+ console.log(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:
1825
+ `, JSON.stringify(mismatches, null, 2));
1826
+ }
1827
+ }
1828
+ return incomingEntity;
1829
+ });
1830
+ }
1831
+ // Subscription methods
1832
+ listenCollection(props, onUpdate, onError) {
1833
+ const subscriptionKey = this.createCollectionSubscriptionKey(props);
1834
+ const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1835
+ const existingSubscription = this.collectionSubscriptions.get(subscriptionKey);
1836
+ if (existingSubscription) {
1837
+ const callbackMap2 = existingSubscription.callbacks;
1838
+ callbackMap2.set(callbackId, {
1839
+ onUpdate,
1840
+ onError
1841
+ });
1842
+ if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) {
1843
+ try {
1844
+ onUpdate(existingSubscription.latestData);
1845
+ } catch (error) {
1846
+ console.error("Error in collection subscription callback:", error);
1847
+ if (onError) {
1848
+ onError(error instanceof Error ? error : new Error(String(error)));
1849
+ }
1850
+ }
1851
+ }
1852
+ return () => {
1853
+ callbackMap2.delete(callbackId);
1854
+ if (callbackMap2.size === 0) {
1855
+ this.collectionSubscriptions.delete(subscriptionKey);
1856
+ this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
1857
+ if (this.isConnected && this.ws) {
1858
+ this.sendMessage({
1859
+ type: "unsubscribe",
1860
+ payload: { subscriptionId: existingSubscription.backendSubscriptionId }
1861
+ }).catch(console.error);
1862
+ }
1863
+ }
1864
+ };
1865
+ }
1866
+ const backendSubscriptionId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1867
+ const callbackMap = /* @__PURE__ */ new Map();
1868
+ callbackMap.set(callbackId, {
1869
+ onUpdate,
1870
+ onError
1871
+ });
1872
+ this.collectionSubscriptions.set(subscriptionKey, {
1873
+ backendSubscriptionId,
1874
+ callbacks: callbackMap,
1875
+ props
1876
+ });
1877
+ this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
1878
+ this.sendMessage({
1879
+ type: "subscribe_collection",
1880
+ payload: {
1881
+ ...props,
1882
+ subscriptionId: backendSubscriptionId
1883
+ }
1884
+ }).catch((error) => {
1885
+ if (onError) onError(error);
1886
+ });
1887
+ return () => {
1888
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
1889
+ if (subscription) {
1890
+ const callbacks = subscription.callbacks;
1891
+ callbacks.delete(callbackId);
1892
+ if (callbacks.size === 0) {
1893
+ this.collectionSubscriptions.delete(subscriptionKey);
1894
+ this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
1895
+ if (this.isConnected && this.ws) {
1896
+ this.sendMessage({
1897
+ type: "unsubscribe",
1898
+ payload: { subscriptionId: subscription.backendSubscriptionId }
1899
+ }).catch(console.error);
1900
+ }
1901
+ }
1902
+ }
1903
+ };
1904
+ }
1905
+ listenEntity(props, onUpdate, onError) {
1906
+ const subscriptionKey = this.createEntitySubscriptionKey(props);
1907
+ const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1908
+ const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
1909
+ if (existingSubscription) {
1910
+ const callbackMap2 = existingSubscription.callbacks;
1911
+ callbackMap2.set(callbackId, {
1912
+ onUpdate,
1913
+ onError
1914
+ });
1915
+ if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) {
1916
+ try {
1917
+ onUpdate(existingSubscription.latestData);
1918
+ } catch (error) {
1919
+ console.error("Error in entity subscription callback:", error);
1920
+ if (onError) {
1921
+ onError(error instanceof Error ? error : new Error(String(error)));
1922
+ }
1923
+ }
1924
+ }
1925
+ return () => {
1926
+ callbackMap2.delete(callbackId);
1927
+ if (callbackMap2.size === 0) {
1928
+ this.entitySubscriptions.delete(subscriptionKey);
1929
+ this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
1930
+ if (this.isConnected && this.ws) {
1931
+ this.sendMessage({
1932
+ type: "unsubscribe",
1933
+ payload: { subscriptionId: existingSubscription.backendSubscriptionId }
1934
+ }).catch(console.error);
1935
+ }
1936
+ }
1937
+ };
1938
+ }
1939
+ const backendSubscriptionId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1940
+ const callbackMap = /* @__PURE__ */ new Map();
1941
+ callbackMap.set(callbackId, {
1942
+ onUpdate,
1943
+ onError
1944
+ });
1945
+ this.entitySubscriptions.set(subscriptionKey, {
1946
+ backendSubscriptionId,
1947
+ callbacks: callbackMap,
1948
+ props
1949
+ });
1950
+ this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
1951
+ this.sendMessage({
1952
+ type: "subscribe_entity",
1953
+ payload: {
1954
+ ...props,
1955
+ subscriptionId: backendSubscriptionId
1956
+ }
1957
+ }).catch((error) => {
1958
+ if (onError) onError(error);
1959
+ });
1960
+ return () => {
1961
+ const subscription = this.entitySubscriptions.get(subscriptionKey);
1962
+ if (subscription) {
1963
+ const callbacks = subscription.callbacks;
1964
+ callbacks.delete(callbackId);
1965
+ if (callbacks.size === 0) {
1966
+ this.entitySubscriptions.delete(subscriptionKey);
1967
+ this.backendToEntityKey.delete(subscription.backendSubscriptionId);
1968
+ if (this.isConnected && this.ws) {
1969
+ this.sendMessage({
1970
+ type: "unsubscribe",
1971
+ payload: { subscriptionId: subscription.backendSubscriptionId }
1972
+ }).catch(console.error);
1973
+ }
1974
+ }
1975
+ }
1976
+ };
1977
+ }
1978
+ /**
1979
+ * Re-send all active subscriptions to the backend after a reconnect.
1980
+ * The server wipes subscription state when a client disconnects, so
1981
+ * we need to re-register everything to resume receiving updates.
1982
+ */
1983
+ resubscribeAll() {
1984
+ console.log(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
1985
+ for (const [key, sub] of this.collectionSubscriptions.entries()) {
1986
+ const oldBackendId = sub.backendSubscriptionId;
1987
+ const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1988
+ sub.backendSubscriptionId = newBackendId;
1989
+ this.backendToCollectionKey.delete(oldBackendId);
1990
+ this.backendToCollectionKey.set(newBackendId, key);
1991
+ this.sendMessage({
1992
+ type: "subscribe_collection",
1993
+ payload: {
1994
+ ...sub.props,
1995
+ subscriptionId: newBackendId
1996
+ }
1997
+ }).catch((error) => {
1998
+ console.error("[WS] Failed to re-subscribe collection:", key, error);
1999
+ });
2000
+ }
2001
+ for (const [key, sub] of this.entitySubscriptions.entries()) {
2002
+ const oldBackendId = sub.backendSubscriptionId;
2003
+ const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2004
+ sub.backendSubscriptionId = newBackendId;
2005
+ this.backendToEntityKey.delete(oldBackendId);
2006
+ this.backendToEntityKey.set(newBackendId, key);
2007
+ this.sendMessage({
2008
+ type: "subscribe_entity",
2009
+ payload: {
2010
+ ...sub.props,
2011
+ subscriptionId: newBackendId
2012
+ }
2013
+ }).catch((error) => {
2014
+ console.error("[WS] Failed to re-subscribe entity:", key, error);
2015
+ });
2016
+ }
2017
+ }
2018
+ createCollectionSubscriptionKey(props) {
2019
+ const key = {
2020
+ path: props.path,
2021
+ filter: props.filter,
2022
+ limit: props.limit,
2023
+ startAfter: props.startAfter,
2024
+ orderBy: props.orderBy,
2025
+ order: props.order,
2026
+ searchString: props.searchString,
2027
+ collection: props.collection?.name
2028
+ };
2029
+ return JSON.stringify(key, (_, value) => {
2030
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2031
+ return Object.keys(value).sort().reduce((sorted, k) => {
2032
+ sorted[k] = value[k];
2033
+ return sorted;
2034
+ }, {});
2035
+ }
2036
+ return value;
2037
+ });
2038
+ }
2039
+ createEntitySubscriptionKey(props) {
2040
+ return `${props.path}|${props.entityId}`;
2041
+ }
2042
+ }
2043
+ function createStorage(transport) {
2044
+ const urlsCache = /* @__PURE__ */ new Map();
2045
+ async function putObject({
2046
+ file,
2047
+ key,
2048
+ metadata,
2049
+ bucket
2050
+ }) {
2051
+ const formData = new FormData();
2052
+ formData.append("file", file);
2053
+ if (key) formData.append("key", key);
2054
+ if (bucket) formData.append("bucket", bucket);
2055
+ if (metadata) {
2056
+ for (const [key2, value] of Object.entries(metadata)) {
2057
+ if (value !== void 0 && value !== null) {
2058
+ formData.append(
2059
+ `metadata_${key2}`,
2060
+ typeof value === "string" ? value : JSON.stringify(value)
2061
+ );
2062
+ }
2063
+ }
2064
+ }
2065
+ const result = await transport.request("/storage/upload", {
2066
+ method: "POST",
2067
+ body: formData,
2068
+ headers: {
2069
+ // transport.request merges headers, so to prevent it setting application/json we can delete it
2070
+ // in transport if body is FormData, or we can explicitly set it to an empty string.
2071
+ // Let's rely on standard behaviour for now and adjust transport if it fails.
2072
+ }
2073
+ });
2074
+ return result.data;
2075
+ }
2076
+ async function getSignedUrl(keyOrUrl, bucket) {
2077
+ const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
2078
+ const cached = urlsCache.get(cacheKey);
2079
+ if (cached) return cached;
2080
+ let filePath = keyOrUrl;
2081
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
2082
+ filePath = filePath.substring(filePath.indexOf("://") + 3);
2083
+ }
2084
+ if (bucket && filePath && !filePath.startsWith(bucket)) {
2085
+ filePath = `${bucket}/${filePath}`;
2086
+ }
2087
+ if (!filePath || filePath.trim() === "" || filePath === "/") {
2088
+ return {
2089
+ url: null,
2090
+ fileNotFound: true
2091
+ };
2092
+ }
2093
+ try {
2094
+ const result = await transport.request(`/storage/metadata/${filePath}`);
2095
+ const activeToken = await transport.resolveToken();
2096
+ const tokenQuery = activeToken ? `?token=${activeToken}` : "";
2097
+ const downloadConfig = {
2098
+ url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
2099
+ metadata: result.data
2100
+ };
2101
+ urlsCache.set(cacheKey, downloadConfig);
2102
+ return downloadConfig;
2103
+ } catch (e) {
2104
+ if (e instanceof Error && "status" in e && e.status === 404) {
2105
+ return {
2106
+ url: null,
2107
+ fileNotFound: true
2108
+ };
2109
+ }
2110
+ throw e;
2111
+ }
2112
+ }
2113
+ async function getObject(key, bucket) {
2114
+ let filePath = key;
2115
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
2116
+ filePath = filePath.substring(filePath.indexOf("://") + 3);
2117
+ }
2118
+ if (bucket && filePath && !filePath.startsWith(bucket)) {
2119
+ filePath = `${bucket}/${filePath}`;
2120
+ }
2121
+ if (!filePath || filePath.trim() === "" || filePath === "/") {
2122
+ return null;
2123
+ }
2124
+ const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
2125
+ const response = await transport.fetchFn(url, {
2126
+ headers: transport.getHeaders ? transport.getHeaders() : {}
2127
+ });
2128
+ if (response.status === 404) return null;
2129
+ if (!response.ok) throw new Error("Failed to get file");
2130
+ const blob = await response.blob();
2131
+ const fileName = filePath.split("/").pop() || "file";
2132
+ return new File([blob], fileName, { type: blob.type });
2133
+ }
2134
+ async function deleteObject(key, bucket) {
2135
+ let filePath = key;
2136
+ if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
2137
+ filePath = filePath.substring(filePath.indexOf("://") + 3);
2138
+ }
2139
+ if (bucket && filePath && !filePath.startsWith(bucket)) {
2140
+ filePath = `${bucket}/${filePath}`;
2141
+ }
2142
+ if (!filePath || filePath.trim() === "" || filePath === "/") {
2143
+ return;
2144
+ }
2145
+ try {
2146
+ await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
2147
+ } catch (e) {
2148
+ if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
2149
+ }
2150
+ urlsCache.delete(bucket ? `${bucket}/${key}` : key);
2151
+ }
2152
+ async function listObjects(prefix, options) {
2153
+ const params = new URLSearchParams();
2154
+ if (prefix) params.set("prefix", prefix);
2155
+ if (options?.bucket) params.set("bucket", options.bucket);
2156
+ if (options?.maxResults) params.set("maxResults", String(options.maxResults));
2157
+ if (options?.pageToken) params.set("pageToken", options.pageToken);
2158
+ const result = await transport.request(`/storage/list?${params.toString()}`);
2159
+ return result.data;
2160
+ }
2161
+ return {
2162
+ putObject,
2163
+ getSignedUrl,
2164
+ getObject,
2165
+ deleteObject,
2166
+ listObjects
2167
+ };
2168
+ }
2169
+ function deriveWebSocketUrl(baseUrl) {
2170
+ if (!baseUrl) {
2171
+ if (typeof window !== "undefined") {
2172
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
2173
+ return `${protocol}//${window.location.host}`;
2174
+ }
2175
+ return "";
2176
+ }
2177
+ return baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://").replace(/\/$/, "");
2178
+ }
2179
+ function createRebaseClient(options) {
2180
+ const transport = createTransport(options);
2181
+ const auth = createAuth(transport, options.auth);
2182
+ const admin = createAdmin(transport, options.admin);
2183
+ const cron = createCron(transport, options.cron);
2184
+ const storage = createStorage(transport);
2185
+ const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2186
+ let ws;
2187
+ if (resolvedWsUrl) {
2188
+ ws = new RebaseWebSocketClient({
2189
+ websocketUrl: resolvedWsUrl,
2190
+ getAuthToken: async () => {
2191
+ const session = await auth.getSession();
2192
+ return session?.accessToken || options.token || "";
2193
+ }
2194
+ });
2195
+ auth.onAuthStateChange((event, session) => {
2196
+ if (!ws) return;
2197
+ if (event === "SIGNED_OUT") {
2198
+ ws.disconnect();
2199
+ } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
2200
+ if (session?.accessToken) {
2201
+ ws.authenticate(session.accessToken).catch(console.warn);
2202
+ }
2203
+ }
2204
+ });
2205
+ }
2206
+ if (!options.onUnauthorized) {
2207
+ transport.setOnUnauthorized(async () => {
2208
+ try {
2209
+ await auth.refreshSession();
2210
+ return true;
2211
+ } catch (e) {
2212
+ return false;
2213
+ }
2214
+ });
2215
+ }
2216
+ const collectionClients = /* @__PURE__ */ new Map();
2217
+ function collection(slug) {
2218
+ if (!collectionClients.has(slug)) {
2219
+ collectionClients.set(slug, createCollectionClient(transport, slug, ws));
2220
+ }
2221
+ return collectionClients.get(slug);
2222
+ }
2223
+ const dataTarget = { collection };
2224
+ const dataProxy = new Proxy(dataTarget, {
2225
+ get(_target, prop) {
2226
+ if (prop === "collection") {
2227
+ return collection;
2228
+ }
2229
+ if (typeof prop === "symbol") return void 0;
2230
+ if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
2231
+ const slug = toSnakeCase(prop);
2232
+ return collection(slug);
2233
+ }
2234
+ return void 0;
2235
+ }
2236
+ });
2237
+ const target = {
2238
+ auth,
2239
+ admin,
2240
+ cron,
2241
+ storage,
2242
+ ws,
2243
+ setToken: transport.setToken,
2244
+ setAuthTokenGetter: transport.setAuthTokenGetter,
2245
+ setOnUnauthorized: transport.setOnUnauthorized,
2246
+ resolveToken: transport.resolveToken,
2247
+ baseUrl: transport.baseUrl,
2248
+ collection,
2249
+ call: async (endpoint, payload) => {
2250
+ const prefix = endpoint.startsWith("/") ? "" : "/";
2251
+ const res = await transport.request(`${prefix}${endpoint}`, {
2252
+ method: "POST",
2253
+ body: payload ? JSON.stringify(payload) : void 0
2254
+ });
2255
+ return res.data ?? res;
2256
+ },
2257
+ data: dataProxy,
2258
+ email: void 0
2259
+ };
2260
+ return target;
2261
+ }
2262
+ export {
2263
+ ApiError,
2264
+ RebaseApiError,
2265
+ RebaseWebSocketClient,
2266
+ buildQueryString,
2267
+ createAdmin,
2268
+ createAuth,
2269
+ createCollectionClient,
2270
+ createCron,
2271
+ createMemoryStorage,
2272
+ createRebaseClient,
2273
+ createStorage,
2274
+ createTransport,
2275
+ rebaseReviver
2276
+ };
2277
+ //# sourceMappingURL=index.es.js.map