@rebasepro/client 0.8.0 → 0.9.1-canary.09aaf62
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/README.md +11 -11
- package/dist/admin.d.ts +1 -0
- package/dist/auth.d.ts +35 -38
- package/dist/backups.d.ts +13 -0
- package/dist/collection.d.ts +9 -13
- package/dist/errors.d.ts +9 -0
- package/dist/index.d.ts +38 -22
- package/dist/index.es.js +985 -271
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +89 -0
- package/dist/sdk_query_builder.d.ts +63 -0
- package/dist/transport.d.ts +36 -6
- package/dist/websocket.d.ts +92 -22
- package/package.json +8 -9
- package/src/admin.ts +1 -1
- package/src/api-keys.ts +1 -1
- package/src/auth.ts +188 -64
- package/src/backups.ts +40 -0
- package/src/collection.test.ts +92 -3
- package/src/collection.ts +88 -75
- package/src/data-proxy.test.ts +167 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +211 -28
- package/src/realtime-channel.test.ts +241 -0
- package/src/realtime-channel.ts +238 -0
- package/src/realtime-optout.test.ts +119 -0
- package/src/realtime-row-identity.test.ts +254 -0
- package/src/reviver.ts +2 -2
- package/src/sdk_query_builder.ts +141 -0
- package/src/storage.ts +64 -32
- package/src/transport.ts +59 -25
- package/src/websocket.ts +522 -193
- package/dist/collection.test.d.ts +0 -1
- package/dist/cron.test.d.ts +0 -1
- package/dist/index.umd.js +0 -2215
- package/dist/index.umd.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, Vector, isPublicStoragePath } from "@rebasepro/types";
|
|
2
|
+
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, and, buildCompositeId, cond, or, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
@@ -30,25 +30,16 @@ function rebaseReviver(_key, value) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/transport.ts
|
|
33
|
-
var RebaseApiError = class extends Error {
|
|
34
|
-
status;
|
|
35
|
-
code;
|
|
36
|
-
details;
|
|
37
|
-
constructor(status, message, code, details) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "RebaseApiError";
|
|
40
|
-
this.status = status;
|
|
41
|
-
this.code = code;
|
|
42
|
-
this.details = details;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
33
|
function buildQueryString(params) {
|
|
46
34
|
if (!params) return "";
|
|
47
35
|
const parts = [];
|
|
48
36
|
if (params.limit != null) parts.push(`limit=${params.limit}`);
|
|
49
37
|
if (params.offset != null) parts.push(`offset=${params.offset}`);
|
|
50
38
|
if (params.page != null) parts.push(`page=${params.page}`);
|
|
51
|
-
if (params.orderBy)
|
|
39
|
+
if (params.orderBy) {
|
|
40
|
+
const wire = serializeOrderBy(params.orderBy);
|
|
41
|
+
if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
|
|
42
|
+
}
|
|
52
43
|
if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
|
|
53
44
|
if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
|
|
54
45
|
if (params.logical) {
|
|
@@ -97,8 +88,7 @@ function createTransport(config) {
|
|
|
97
88
|
} catch (e) {}
|
|
98
89
|
const getErrorField = (obj, field) => {
|
|
99
90
|
const err = obj?.error;
|
|
100
|
-
if (err && typeof err === "object" && err !== null
|
|
101
|
-
return obj?.[field];
|
|
91
|
+
if (err && typeof err === "object" && err !== null) return err[field];
|
|
102
92
|
};
|
|
103
93
|
if (res.status === 401 && onUnauthorizedHandler) {
|
|
104
94
|
if (await onUnauthorizedHandler()) {
|
|
@@ -121,7 +111,11 @@ function createTransport(config) {
|
|
|
121
111
|
if (!retryRes.ok) {
|
|
122
112
|
let fallbackMessage = retryRes.statusText;
|
|
123
113
|
if (retryRes.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${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.`;
|
|
124
|
-
throw new RebaseApiError(
|
|
114
|
+
throw new RebaseApiError$1(String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), {
|
|
115
|
+
status: retryRes.status,
|
|
116
|
+
code: getErrorField(retryBody, "code"),
|
|
117
|
+
details: getErrorField(retryBody, "details")
|
|
118
|
+
});
|
|
125
119
|
}
|
|
126
120
|
return retryBody;
|
|
127
121
|
}
|
|
@@ -129,7 +123,11 @@ function createTransport(config) {
|
|
|
129
123
|
if (!res.ok) {
|
|
130
124
|
let fallbackMessage = res.statusText;
|
|
131
125
|
if (res.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${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.`;
|
|
132
|
-
throw new RebaseApiError(
|
|
126
|
+
throw new RebaseApiError$1(String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), {
|
|
127
|
+
status: res.status,
|
|
128
|
+
code: getErrorField(body, "code"),
|
|
129
|
+
details: getErrorField(body, "details")
|
|
130
|
+
});
|
|
133
131
|
}
|
|
134
132
|
return body;
|
|
135
133
|
}
|
|
@@ -165,6 +163,29 @@ function createTransport(config) {
|
|
|
165
163
|
}
|
|
166
164
|
//#endregion
|
|
167
165
|
//#region src/auth.ts
|
|
166
|
+
/** Map a raw user object from an auth response (`/login`, `/refresh`, `/me`) to a `User`. */
|
|
167
|
+
function mapRawUser(raw) {
|
|
168
|
+
return {
|
|
169
|
+
uid: raw.uid,
|
|
170
|
+
email: raw.email ?? null,
|
|
171
|
+
displayName: raw.displayName ?? null,
|
|
172
|
+
photoURL: raw.photoURL ?? null,
|
|
173
|
+
providerId: raw.providerId ?? "password",
|
|
174
|
+
isAnonymous: raw.isAnonymous ?? false,
|
|
175
|
+
emailVerified: raw.emailVerified,
|
|
176
|
+
roles: raw.roles,
|
|
177
|
+
metadata: raw.metadata
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Placeholder user, used only as a last resort when none can be resolved. */
|
|
181
|
+
var EMPTY_USER = {
|
|
182
|
+
uid: "",
|
|
183
|
+
email: null,
|
|
184
|
+
displayName: null,
|
|
185
|
+
photoURL: null,
|
|
186
|
+
providerId: "password",
|
|
187
|
+
isAnonymous: false
|
|
188
|
+
};
|
|
168
189
|
function createMemoryStorage() {
|
|
169
190
|
const store = {};
|
|
170
191
|
return {
|
|
@@ -195,11 +216,20 @@ function createAuth(transport, options) {
|
|
|
195
216
|
const authPath = opts.authPath || "/auth";
|
|
196
217
|
const autoRefresh = opts.autoRefresh !== false;
|
|
197
218
|
const persistSession = opts.persistSession !== false;
|
|
219
|
+
const authFlowMode = opts.authFlowMode || "json";
|
|
198
220
|
const STORAGE_KEY = "rebase_auth";
|
|
199
221
|
const REFRESH_BUFFER_MS = 12e4;
|
|
222
|
+
const MAX_REFRESH_RETRIES = 5;
|
|
223
|
+
const REFRESH_RETRY_BASE_MS = 1e3;
|
|
224
|
+
const REFRESH_RETRY_MAX_MS = 3e4;
|
|
200
225
|
let currentSession = null;
|
|
201
226
|
const listeners = /* @__PURE__ */ new Set();
|
|
202
227
|
let refreshTimeout = null;
|
|
228
|
+
let inFlightRefresh = null;
|
|
229
|
+
let resolveInitialized;
|
|
230
|
+
const isInitialized = new Promise((resolve) => {
|
|
231
|
+
resolveInitialized = resolve;
|
|
232
|
+
});
|
|
203
233
|
function authUrl(endpoint) {
|
|
204
234
|
return transport.baseUrl + transport.apiPath + authPath + endpoint;
|
|
205
235
|
}
|
|
@@ -207,7 +237,11 @@ function createAuth(transport, options) {
|
|
|
207
237
|
return transport.fetchFn || globalThis.fetch;
|
|
208
238
|
}
|
|
209
239
|
function throwApiError(status, body, statusText) {
|
|
210
|
-
throw new RebaseApiError(
|
|
240
|
+
throw new RebaseApiError(body?.error?.message || body?.message || statusText, {
|
|
241
|
+
status,
|
|
242
|
+
code: body?.error?.code || body?.code,
|
|
243
|
+
details: body?.error?.details || body?.details
|
|
244
|
+
});
|
|
211
245
|
}
|
|
212
246
|
function emit(event, session) {
|
|
213
247
|
for (const fn of listeners) try {
|
|
@@ -215,7 +249,7 @@ function createAuth(transport, options) {
|
|
|
215
249
|
} catch (e) {}
|
|
216
250
|
}
|
|
217
251
|
function saveSession(session) {
|
|
218
|
-
if (!persistSession) return;
|
|
252
|
+
if (!persistSession || authFlowMode === "cookie") return;
|
|
219
253
|
try {
|
|
220
254
|
storage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
221
255
|
} catch (e) {}
|
|
@@ -232,28 +266,53 @@ function createAuth(transport, options) {
|
|
|
232
266
|
} catch (e) {}
|
|
233
267
|
return null;
|
|
234
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* A refresh failure is only fatal if the refresh token itself is rejected
|
|
271
|
+
* (expired / invalid / forbidden). Network blips, timeouts, and 5xx (e.g. a
|
|
272
|
+
* backend restart mid-session) are transient and must NOT log the user out.
|
|
273
|
+
*/
|
|
274
|
+
function isFatalRefreshError(err) {
|
|
275
|
+
if (!(err instanceof RebaseApiError)) return false;
|
|
276
|
+
if (err.code === "INVALID_TOKEN" || err.code === "TOKEN_EXPIRED") return true;
|
|
277
|
+
return err.status === 401 || err.status === 403;
|
|
278
|
+
}
|
|
279
|
+
async function attemptScheduledRefresh(attempt) {
|
|
280
|
+
try {
|
|
281
|
+
await refreshSession();
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (isFatalRefreshError(err)) {
|
|
284
|
+
signOut();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (attempt >= MAX_REFRESH_RETRIES) {
|
|
288
|
+
signOut();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const backoff = Math.min(REFRESH_RETRY_BASE_MS * 2 ** attempt, REFRESH_RETRY_MAX_MS);
|
|
292
|
+
refreshTimeout = setTimeout(() => {
|
|
293
|
+
attemptScheduledRefresh(attempt + 1);
|
|
294
|
+
}, backoff);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
235
297
|
function scheduleRefresh(expiresAt) {
|
|
236
298
|
if (refreshTimeout) clearTimeout(refreshTimeout);
|
|
237
299
|
if (!autoRefresh) return;
|
|
238
300
|
const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
|
|
239
301
|
if (delay <= 0) {
|
|
240
|
-
|
|
302
|
+
attemptScheduledRefresh(0);
|
|
241
303
|
return;
|
|
242
304
|
}
|
|
243
|
-
refreshTimeout = setTimeout(
|
|
244
|
-
|
|
245
|
-
await refreshSession();
|
|
246
|
-
} catch (e) {
|
|
247
|
-
signOut();
|
|
248
|
-
}
|
|
305
|
+
refreshTimeout = setTimeout(() => {
|
|
306
|
+
attemptScheduledRefresh(0);
|
|
249
307
|
}, delay);
|
|
250
308
|
}
|
|
251
309
|
function handleAuthResponse(data, event) {
|
|
310
|
+
const user = mapRawUser(data.user);
|
|
252
311
|
const session = {
|
|
253
312
|
accessToken: data.tokens.accessToken,
|
|
254
|
-
refreshToken: data.tokens.refreshToken,
|
|
313
|
+
refreshToken: data.tokens.refreshToken || currentSession?.refreshToken || "",
|
|
255
314
|
expiresAt: data.tokens.accessTokenExpiresAt,
|
|
256
|
-
user
|
|
315
|
+
user
|
|
257
316
|
};
|
|
258
317
|
currentSession = session;
|
|
259
318
|
saveSession(session);
|
|
@@ -269,7 +328,8 @@ function createAuth(transport, options) {
|
|
|
269
328
|
body: JSON.stringify({
|
|
270
329
|
email,
|
|
271
330
|
password
|
|
272
|
-
})
|
|
331
|
+
}),
|
|
332
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
273
333
|
});
|
|
274
334
|
const body = await res.json().catch(() => ({}));
|
|
275
335
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -290,7 +350,8 @@ function createAuth(transport, options) {
|
|
|
290
350
|
const res = await fetchFn(authUrl("/register"), {
|
|
291
351
|
method: "POST",
|
|
292
352
|
headers: { "Content-Type": "application/json" },
|
|
293
|
-
body: JSON.stringify(payload)
|
|
353
|
+
body: JSON.stringify(payload),
|
|
354
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
294
355
|
});
|
|
295
356
|
const body = await res.json().catch(() => ({}));
|
|
296
357
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -313,7 +374,8 @@ function createAuth(transport, options) {
|
|
|
313
374
|
const res = await getFetch()(authUrl("/google"), {
|
|
314
375
|
method: "POST",
|
|
315
376
|
headers: { "Content-Type": "application/json" },
|
|
316
|
-
body: JSON.stringify(payload)
|
|
377
|
+
body: JSON.stringify(payload),
|
|
378
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
317
379
|
});
|
|
318
380
|
const responseBody = await res.json().catch(() => ({}));
|
|
319
381
|
if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
|
|
@@ -331,7 +393,8 @@ function createAuth(transport, options) {
|
|
|
331
393
|
body: JSON.stringify({
|
|
332
394
|
code,
|
|
333
395
|
redirectUri
|
|
334
|
-
})
|
|
396
|
+
}),
|
|
397
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
335
398
|
});
|
|
336
399
|
const body = await res.json().catch(() => ({}));
|
|
337
400
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -350,7 +413,8 @@ function createAuth(transport, options) {
|
|
|
350
413
|
const res = await getFetch()(authUrl(`/${providerId}`), {
|
|
351
414
|
method: "POST",
|
|
352
415
|
headers: { "Content-Type": "application/json" },
|
|
353
|
-
body: JSON.stringify(payload)
|
|
416
|
+
body: JSON.stringify(payload),
|
|
417
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
354
418
|
});
|
|
355
419
|
const body = await res.json().catch(() => ({}));
|
|
356
420
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -426,10 +490,11 @@ function createAuth(transport, options) {
|
|
|
426
490
|
async function signOut() {
|
|
427
491
|
const fetchFn = getFetch();
|
|
428
492
|
try {
|
|
429
|
-
if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
|
|
493
|
+
if (authFlowMode === "cookie" || currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
|
|
430
494
|
method: "POST",
|
|
431
495
|
headers: { "Content-Type": "application/json" },
|
|
432
|
-
body: JSON.stringify({ refreshToken: currentSession
|
|
496
|
+
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
497
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
433
498
|
});
|
|
434
499
|
} catch (e) {}
|
|
435
500
|
currentSession = null;
|
|
@@ -441,20 +506,35 @@ function createAuth(transport, options) {
|
|
|
441
506
|
transport.setToken(null);
|
|
442
507
|
emit("SIGNED_OUT", null);
|
|
443
508
|
}
|
|
444
|
-
|
|
445
|
-
if (
|
|
509
|
+
function refreshSession() {
|
|
510
|
+
if (inFlightRefresh) return inFlightRefresh;
|
|
511
|
+
inFlightRefresh = doRefreshSession().finally(() => {
|
|
512
|
+
inFlightRefresh = null;
|
|
513
|
+
});
|
|
514
|
+
return inFlightRefresh;
|
|
515
|
+
}
|
|
516
|
+
async function doRefreshSession() {
|
|
517
|
+
if (authFlowMode !== "cookie" && !currentSession?.refreshToken) throw new Error("No active session to refresh");
|
|
446
518
|
const res = await getFetch()(authUrl("/refresh"), {
|
|
447
519
|
method: "POST",
|
|
448
520
|
headers: { "Content-Type": "application/json" },
|
|
449
|
-
body: JSON.stringify({ refreshToken: currentSession
|
|
521
|
+
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
522
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
450
523
|
});
|
|
451
524
|
const body = await res.json().catch(() => ({}));
|
|
452
525
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
526
|
+
const accessToken = body.tokens.accessToken;
|
|
527
|
+
transport.setToken(accessToken);
|
|
528
|
+
let user = currentSession?.user;
|
|
529
|
+
if (body.user && typeof body.user.uid === "string") user = mapRawUser(body.user);
|
|
530
|
+
else if (!user || !user.uid) try {
|
|
531
|
+
user = await getUser();
|
|
532
|
+
} catch {}
|
|
453
533
|
const session = {
|
|
454
|
-
accessToken
|
|
455
|
-
refreshToken: body.tokens.refreshToken,
|
|
534
|
+
accessToken,
|
|
535
|
+
refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
|
|
456
536
|
expiresAt: body.tokens.accessTokenExpiresAt,
|
|
457
|
-
user:
|
|
537
|
+
user: user ?? EMPTY_USER
|
|
458
538
|
};
|
|
459
539
|
currentSession = session;
|
|
460
540
|
saveSession(session);
|
|
@@ -466,6 +546,18 @@ function createAuth(transport, options) {
|
|
|
466
546
|
async function getUser() {
|
|
467
547
|
return (await transport.request(authPath + "/me", { method: "GET" })).user;
|
|
468
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Resolve an email to a minimal public profile (`uid`, `displayName`,
|
|
551
|
+
* `photoURL`) for invite-by-email flows. Returns `null` when no account
|
|
552
|
+
* matches. Requires the backend to opt in via `auth.allowUserLookup`;
|
|
553
|
+
* otherwise the endpoint is absent and this rejects.
|
|
554
|
+
*/
|
|
555
|
+
async function findUserByEmail(email) {
|
|
556
|
+
return (await transport.request(authPath + "/find-user", {
|
|
557
|
+
method: "POST",
|
|
558
|
+
body: JSON.stringify({ email })
|
|
559
|
+
})).user;
|
|
560
|
+
}
|
|
469
561
|
async function updateUser(updates) {
|
|
470
562
|
const data = await transport.request(authPath + "/me", {
|
|
471
563
|
method: "PATCH",
|
|
@@ -539,7 +631,8 @@ function createAuth(transport, options) {
|
|
|
539
631
|
const res = await getFetch()(authUrl("/magic-link/verify"), {
|
|
540
632
|
method: "POST",
|
|
541
633
|
headers: { "Content-Type": "application/json" },
|
|
542
|
-
body: JSON.stringify({ token })
|
|
634
|
+
body: JSON.stringify({ token }),
|
|
635
|
+
credentials: authFlowMode === "cookie" ? "include" : void 0
|
|
543
636
|
});
|
|
544
637
|
const body = await res.json().catch(() => ({}));
|
|
545
638
|
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
@@ -586,21 +679,29 @@ function createAuth(transport, options) {
|
|
|
586
679
|
}
|
|
587
680
|
if (persistSession) {
|
|
588
681
|
const stored = loadStoredSession();
|
|
589
|
-
if (stored && stored.accessToken
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
682
|
+
if (stored && stored.accessToken) if (stored.expiresAt > Date.now()) {
|
|
683
|
+
currentSession = stored;
|
|
684
|
+
transport.setToken(stored.accessToken);
|
|
685
|
+
scheduleRefresh(stored.expiresAt);
|
|
686
|
+
resolveInitialized();
|
|
687
|
+
} else if (authFlowMode === "cookie" || stored.refreshToken) {
|
|
688
|
+
currentSession = stored;
|
|
689
|
+
refreshSession().then(() => {
|
|
690
|
+
resolveInitialized();
|
|
691
|
+
}).catch(() => {
|
|
692
|
+
currentSession = null;
|
|
693
|
+
clearStoredSession();
|
|
694
|
+
transport.setToken(null);
|
|
695
|
+
resolveInitialized();
|
|
696
|
+
});
|
|
697
|
+
} else resolveInitialized();
|
|
698
|
+
else if (authFlowMode === "cookie") refreshSession().then(() => {
|
|
699
|
+
resolveInitialized();
|
|
700
|
+
}).catch(() => {
|
|
701
|
+
resolveInitialized();
|
|
702
|
+
});
|
|
703
|
+
else resolveInitialized();
|
|
704
|
+
} else resolveInitialized();
|
|
604
705
|
return {
|
|
605
706
|
signInWithEmail,
|
|
606
707
|
signUp,
|
|
@@ -620,6 +721,7 @@ function createAuth(transport, options) {
|
|
|
620
721
|
signOut,
|
|
621
722
|
refreshSession,
|
|
622
723
|
getUser,
|
|
724
|
+
findUserByEmail,
|
|
623
725
|
updateUser,
|
|
624
726
|
resetPasswordForEmail,
|
|
625
727
|
resetPassword,
|
|
@@ -633,7 +735,8 @@ function createAuth(transport, options) {
|
|
|
633
735
|
revokeAllSessions,
|
|
634
736
|
getAuthConfig,
|
|
635
737
|
getSession,
|
|
636
|
-
onAuthStateChange
|
|
738
|
+
onAuthStateChange,
|
|
739
|
+
isInitialized: () => isInitialized
|
|
637
740
|
};
|
|
638
741
|
}
|
|
639
742
|
function createCookieStorage(options = {}) {
|
|
@@ -766,6 +869,32 @@ function createCron(transport, options) {
|
|
|
766
869
|
};
|
|
767
870
|
}
|
|
768
871
|
//#endregion
|
|
872
|
+
//#region src/backups.ts
|
|
873
|
+
function createBackups(transport, options) {
|
|
874
|
+
const backupsPath = options?.backupsPath || "/admin/backups";
|
|
875
|
+
async function list() {
|
|
876
|
+
return transport.request(backupsPath, { method: "GET" });
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Download a backup's bytes. Uses an authenticated fetch (not the JSON
|
|
880
|
+
* transport) so the octet-stream response comes back as a Blob.
|
|
881
|
+
*/
|
|
882
|
+
async function download(key) {
|
|
883
|
+
const token = await transport.resolveToken();
|
|
884
|
+
const url = `${transport.baseUrl}${transport.apiPath}${backupsPath}/download?key=${encodeURIComponent(key)}`;
|
|
885
|
+
const res = await fetch(url, {
|
|
886
|
+
method: "GET",
|
|
887
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
888
|
+
});
|
|
889
|
+
if (!res.ok) throw new Error(`Failed to download backup (${res.status})`);
|
|
890
|
+
return res.blob();
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
list,
|
|
894
|
+
download
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
//#endregion
|
|
769
898
|
//#region src/api-keys.ts
|
|
770
899
|
/**
|
|
771
900
|
* Creates a client for managing API keys via the admin routes.
|
|
@@ -810,20 +939,108 @@ function createApiKeys(transport, options) {
|
|
|
810
939
|
};
|
|
811
940
|
}
|
|
812
941
|
//#endregion
|
|
813
|
-
//#region src/
|
|
942
|
+
//#region src/sdk_query_builder.ts
|
|
814
943
|
/**
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
944
|
+
* SDK Query Builder — returns flat rows (`FindResult<M>`) instead of
|
|
945
|
+
* Entity-wrapped results (`FindResponse<M>`).
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* const { data } = await rebase.data.posts
|
|
949
|
+
* .where("status", "==", "published")
|
|
950
|
+
* .orderBy("created_at", "desc")
|
|
951
|
+
* .limit(10)
|
|
952
|
+
* .find();
|
|
953
|
+
*
|
|
954
|
+
* console.log(data[0].title); // flat access
|
|
819
955
|
*/
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
956
|
+
var SDKQueryBuilder = class {
|
|
957
|
+
collection;
|
|
958
|
+
params = { where: {} };
|
|
959
|
+
constructor(collection) {
|
|
960
|
+
this.collection = collection;
|
|
961
|
+
}
|
|
962
|
+
where(columnOrCondition, operator, value) {
|
|
963
|
+
if (typeof columnOrCondition === "object" && columnOrCondition !== null && "type" in columnOrCondition) {
|
|
964
|
+
this.params.logical = columnOrCondition;
|
|
965
|
+
return this;
|
|
966
|
+
}
|
|
967
|
+
if (!this.params.where) this.params.where = {};
|
|
968
|
+
const column = columnOrCondition;
|
|
969
|
+
const condition = [operator, value];
|
|
970
|
+
const existing = this.params.where[column];
|
|
971
|
+
if (existing === void 0) this.params.where[column] = condition;
|
|
972
|
+
else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) this.params.where[column].push(condition);
|
|
973
|
+
else {
|
|
974
|
+
let firstCondition;
|
|
975
|
+
if (Array.isArray(existing) && existing.length === 2 && typeof existing[0] === "string") firstCondition = existing;
|
|
976
|
+
else firstCondition = ["==", existing];
|
|
977
|
+
this.params.where[column] = [firstCondition, condition];
|
|
978
|
+
}
|
|
979
|
+
return this;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Order the results by a specific column.
|
|
983
|
+
*/
|
|
984
|
+
orderBy(column, direction = "asc") {
|
|
985
|
+
this.params.orderBy = [column, direction];
|
|
986
|
+
return this;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Limit the number of results returned.
|
|
990
|
+
*/
|
|
991
|
+
limit(count) {
|
|
992
|
+
this.params.limit = count;
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Skip the first N results.
|
|
997
|
+
*/
|
|
998
|
+
offset(count) {
|
|
999
|
+
this.params.offset = count;
|
|
1000
|
+
return this;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Set a free-text search string if supported by the backend.
|
|
1004
|
+
*/
|
|
1005
|
+
search(searchString) {
|
|
1006
|
+
this.params.searchString = searchString;
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Include related entities in the response.
|
|
1011
|
+
* Relations will be populated with full data instead of just IDs.
|
|
1012
|
+
*
|
|
1013
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
1014
|
+
* @example
|
|
1015
|
+
* client.data.posts.include("tags", "author").find()
|
|
1016
|
+
*/
|
|
1017
|
+
include(...relations) {
|
|
1018
|
+
this.params.include = relations;
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Execute the find query and return the results as flat rows.
|
|
1023
|
+
*/
|
|
1024
|
+
async find() {
|
|
1025
|
+
return this.collection.find(this.params);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Count the records matching this query.
|
|
1029
|
+
*/
|
|
1030
|
+
async count() {
|
|
1031
|
+
if (!this.collection.count) throw new Error("count() is not supported by this collection client.");
|
|
1032
|
+
return this.collection.count(this.params);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Listen to realtime updates matching this query.
|
|
1036
|
+
*/
|
|
1037
|
+
listen(onUpdate, onError) {
|
|
1038
|
+
if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl, and not when it was created with realtime: false.");
|
|
1039
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/collection.ts
|
|
827
1044
|
function createCollectionClient(transport, slug, ws) {
|
|
828
1045
|
const basePath = `/data/${slug}`;
|
|
829
1046
|
const client = {
|
|
@@ -831,7 +1048,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
831
1048
|
const qs = buildQueryString(params);
|
|
832
1049
|
const raw = await transport.request(basePath + qs, { method: "GET" });
|
|
833
1050
|
return {
|
|
834
|
-
data:
|
|
1051
|
+
data: raw.data || [],
|
|
835
1052
|
meta: raw.meta
|
|
836
1053
|
};
|
|
837
1054
|
},
|
|
@@ -839,7 +1056,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
839
1056
|
try {
|
|
840
1057
|
const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
|
|
841
1058
|
if (!raw) return void 0;
|
|
842
|
-
return
|
|
1059
|
+
return raw;
|
|
843
1060
|
} catch (err) {
|
|
844
1061
|
if (err instanceof RebaseApiError && err.status === 404) return;
|
|
845
1062
|
throw err;
|
|
@@ -848,19 +1065,30 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
848
1065
|
async create(data, id) {
|
|
849
1066
|
const body = { ...data };
|
|
850
1067
|
if (id !== void 0) body.id = id;
|
|
851
|
-
return
|
|
1068
|
+
return await transport.request(basePath, {
|
|
852
1069
|
method: "POST",
|
|
853
1070
|
body: JSON.stringify(body)
|
|
854
|
-
})
|
|
1071
|
+
});
|
|
1072
|
+
},
|
|
1073
|
+
async createMany(data, options) {
|
|
1074
|
+
if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
|
|
1075
|
+
if (data.length === 0) return [];
|
|
1076
|
+
return (await transport.request(`${basePath}/bulk`, {
|
|
1077
|
+
method: "POST",
|
|
1078
|
+
body: JSON.stringify({
|
|
1079
|
+
rows: data,
|
|
1080
|
+
...options?.upsert ? { upsert: true } : {}
|
|
1081
|
+
})
|
|
1082
|
+
})).data || [];
|
|
855
1083
|
},
|
|
856
1084
|
async update(id, data) {
|
|
857
|
-
return
|
|
1085
|
+
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
858
1086
|
method: "PUT",
|
|
859
1087
|
body: JSON.stringify(data)
|
|
860
|
-
})
|
|
1088
|
+
});
|
|
861
1089
|
},
|
|
862
1090
|
async delete(id) {
|
|
863
|
-
|
|
1091
|
+
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
|
|
864
1092
|
},
|
|
865
1093
|
async count(params) {
|
|
866
1094
|
const qs = buildQueryString({
|
|
@@ -871,24 +1099,24 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
871
1099
|
return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
|
|
872
1100
|
},
|
|
873
1101
|
where(columnOrCondition, operator, value) {
|
|
874
|
-
const builder = new
|
|
1102
|
+
const builder = new SDKQueryBuilder(client);
|
|
875
1103
|
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
876
1104
|
return builder.where(columnOrCondition, operator, value);
|
|
877
1105
|
},
|
|
878
1106
|
orderBy(column, direction) {
|
|
879
|
-
return new
|
|
1107
|
+
return new SDKQueryBuilder(client).orderBy(column, direction);
|
|
880
1108
|
},
|
|
881
1109
|
limit(count) {
|
|
882
|
-
return new
|
|
1110
|
+
return new SDKQueryBuilder(client).limit(count);
|
|
883
1111
|
},
|
|
884
1112
|
offset(count) {
|
|
885
|
-
return new
|
|
1113
|
+
return new SDKQueryBuilder(client).offset(count);
|
|
886
1114
|
},
|
|
887
1115
|
search(searchString) {
|
|
888
|
-
return new
|
|
1116
|
+
return new SDKQueryBuilder(client).search(searchString);
|
|
889
1117
|
},
|
|
890
1118
|
include(...relations) {
|
|
891
|
-
return new
|
|
1119
|
+
return new SDKQueryBuilder(client).include(...relations);
|
|
892
1120
|
}
|
|
893
1121
|
};
|
|
894
1122
|
if (ws) {
|
|
@@ -900,33 +1128,46 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
900
1128
|
filter: params?.where,
|
|
901
1129
|
limit: params?.limit,
|
|
902
1130
|
startAfter: params?.offset ? String(params.offset) : void 0,
|
|
903
|
-
orderBy: params?.orderBy?.
|
|
904
|
-
order: params?.orderBy?.
|
|
1131
|
+
orderBy: params?.orderBy?.[0],
|
|
1132
|
+
order: params?.orderBy?.[1],
|
|
905
1133
|
searchString: params?.searchString
|
|
906
|
-
}, (
|
|
1134
|
+
}, (incomingRows) => {
|
|
907
1135
|
const currentUpdateId = ++lastUpdateId;
|
|
908
1136
|
const requestedLimit = params?.limit || 20;
|
|
909
1137
|
const offset = params?.offset || 0;
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
total: entities.length,
|
|
914
|
-
limit: requestedLimit,
|
|
915
|
-
offset,
|
|
916
|
-
hasMore: entities.length >= requestedLimit
|
|
917
|
-
}
|
|
918
|
-
});
|
|
1138
|
+
const rows = incomingRows;
|
|
1139
|
+
const heuristicTotal = rows.length;
|
|
1140
|
+
const heuristicHasMore = rows.length >= requestedLimit;
|
|
919
1141
|
if (client.count) client.count(params).then((total) => {
|
|
920
1142
|
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
921
|
-
data:
|
|
1143
|
+
data: rows,
|
|
922
1144
|
meta: {
|
|
923
1145
|
total,
|
|
924
1146
|
limit: requestedLimit,
|
|
925
1147
|
offset,
|
|
926
|
-
hasMore: offset +
|
|
1148
|
+
hasMore: offset + rows.length < total
|
|
927
1149
|
}
|
|
928
1150
|
});
|
|
929
|
-
}).catch(() => {
|
|
1151
|
+
}).catch(() => {
|
|
1152
|
+
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
1153
|
+
data: rows,
|
|
1154
|
+
meta: {
|
|
1155
|
+
total: heuristicTotal,
|
|
1156
|
+
limit: requestedLimit,
|
|
1157
|
+
offset,
|
|
1158
|
+
hasMore: heuristicHasMore
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
else onUpdate({
|
|
1163
|
+
data: rows,
|
|
1164
|
+
meta: {
|
|
1165
|
+
total: heuristicTotal,
|
|
1166
|
+
limit: requestedLimit,
|
|
1167
|
+
offset,
|
|
1168
|
+
hasMore: heuristicHasMore
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
930
1171
|
}, onError);
|
|
931
1172
|
return () => {
|
|
932
1173
|
active = false;
|
|
@@ -934,11 +1175,11 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
934
1175
|
};
|
|
935
1176
|
};
|
|
936
1177
|
client.listenById = (id, onUpdate, onError) => {
|
|
937
|
-
return ws.
|
|
1178
|
+
return ws.listenOne({
|
|
938
1179
|
path: slug,
|
|
939
|
-
|
|
940
|
-
}, (
|
|
941
|
-
if (
|
|
1180
|
+
id: String(id)
|
|
1181
|
+
}, (row) => {
|
|
1182
|
+
if (row) onUpdate(row);
|
|
942
1183
|
else onUpdate(void 0);
|
|
943
1184
|
}, onError);
|
|
944
1185
|
};
|
|
@@ -986,10 +1227,12 @@ function createStorage(transport, storageId) {
|
|
|
986
1227
|
if (!storageId) return path;
|
|
987
1228
|
return `${path}${path.includes("?") ? "&" : "?"}storageId=${encodeURIComponent(storageId)}`;
|
|
988
1229
|
};
|
|
989
|
-
async function putObject({ file, key, metadata, bucket }) {
|
|
1230
|
+
async function putObject({ file, key, metadata, bucket, public: isPublic }) {
|
|
990
1231
|
const formData = new FormData();
|
|
991
1232
|
formData.append("file", file);
|
|
992
|
-
|
|
1233
|
+
let effectiveKey = key;
|
|
1234
|
+
if (isPublic && effectiveKey && !isPublicStoragePath(effectiveKey)) effectiveKey = `${PUBLIC_STORAGE_PREFIX}${effectiveKey.replace(/^\/+/, "")}`;
|
|
1235
|
+
if (effectiveKey) formData.append("key", effectiveKey);
|
|
993
1236
|
if (bucket) formData.append("bucket", bucket);
|
|
994
1237
|
if (storageId) formData.append("storageId", storageId);
|
|
995
1238
|
if (metadata) {
|
|
@@ -1003,8 +1246,11 @@ function createStorage(transport, storageId) {
|
|
|
1003
1246
|
}
|
|
1004
1247
|
async function getSignedUrl(keyOrUrl, bucket) {
|
|
1005
1248
|
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1249
|
+
const cachedEntry = urlsCache.get(cacheKey);
|
|
1250
|
+
if (cachedEntry) {
|
|
1251
|
+
if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) return cachedEntry.config;
|
|
1252
|
+
urlsCache.delete(cacheKey);
|
|
1253
|
+
}
|
|
1008
1254
|
let filePath = keyOrUrl;
|
|
1009
1255
|
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://") || filePath.startsWith("gs://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
1010
1256
|
if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
|
|
@@ -1012,15 +1258,32 @@ function createStorage(transport, storageId) {
|
|
|
1012
1258
|
url: null,
|
|
1013
1259
|
fileNotFound: true
|
|
1014
1260
|
};
|
|
1261
|
+
if (isPublicStoragePath(filePath)) {
|
|
1262
|
+
const publicConfig = { url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`) };
|
|
1263
|
+
urlsCache.set(cacheKey, { config: publicConfig });
|
|
1264
|
+
return publicConfig;
|
|
1265
|
+
}
|
|
1015
1266
|
try {
|
|
1016
1267
|
const result = await transport.request(withStorageId(`/storage/metadata/${filePath}`));
|
|
1017
|
-
|
|
1018
|
-
|
|
1268
|
+
if (result.data.public) {
|
|
1269
|
+
const publicConfig = {
|
|
1270
|
+
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`),
|
|
1271
|
+
metadata: result.data
|
|
1272
|
+
};
|
|
1273
|
+
urlsCache.set(cacheKey, { config: publicConfig });
|
|
1274
|
+
return publicConfig;
|
|
1275
|
+
}
|
|
1276
|
+
const scopedToken = result.data.token;
|
|
1277
|
+
const tokenQuery = scopedToken ? `?token=${scopedToken}` : "";
|
|
1019
1278
|
const downloadConfig = {
|
|
1020
1279
|
url: withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`),
|
|
1021
1280
|
metadata: result.data
|
|
1022
1281
|
};
|
|
1023
|
-
|
|
1282
|
+
const expiresAt = result.data.tokenExpiresIn ? Date.now() + (result.data.tokenExpiresIn - 10) * 1e3 : void 0;
|
|
1283
|
+
urlsCache.set(cacheKey, {
|
|
1284
|
+
config: downloadConfig,
|
|
1285
|
+
expiresAt
|
|
1286
|
+
});
|
|
1024
1287
|
return downloadConfig;
|
|
1025
1288
|
} catch (e) {
|
|
1026
1289
|
if (e instanceof Error && "status" in e && e.status === 404) return {
|
|
@@ -1031,16 +1294,13 @@ function createStorage(transport, storageId) {
|
|
|
1031
1294
|
}
|
|
1032
1295
|
}
|
|
1033
1296
|
async function getObject(key, bucket) {
|
|
1034
|
-
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") return null;
|
|
1038
|
-
const url = withStorageId(`${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`);
|
|
1039
|
-
const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
|
|
1297
|
+
const downloadConfig = await getSignedUrl(key, bucket);
|
|
1298
|
+
if (downloadConfig.fileNotFound || !downloadConfig.url) return null;
|
|
1299
|
+
const response = await transport.fetchFn(downloadConfig.url, { headers: {} });
|
|
1040
1300
|
if (response.status === 404) return null;
|
|
1041
1301
|
if (!response.ok) throw new Error("Failed to get file");
|
|
1042
1302
|
const blob = await response.blob();
|
|
1043
|
-
const fileName =
|
|
1303
|
+
const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
|
|
1044
1304
|
return new File([blob], fileName, { type: blob.type });
|
|
1045
1305
|
}
|
|
1046
1306
|
async function deleteObject(key, bucket) {
|
|
@@ -1137,27 +1397,45 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
|
|
|
1137
1397
|
function extractMessageError(message) {
|
|
1138
1398
|
const payload = message.payload;
|
|
1139
1399
|
const errPayload = payload?.error;
|
|
1400
|
+
const errorMessage = typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error";
|
|
1401
|
+
const errorCode = typeof errPayload === "object" ? errPayload.code : payload?.code;
|
|
1140
1402
|
return {
|
|
1141
|
-
errorMessage: typeof
|
|
1142
|
-
errorCode
|
|
1403
|
+
errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
|
|
1404
|
+
errorCode
|
|
1143
1405
|
};
|
|
1144
1406
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
};
|
|
1407
|
+
/**
|
|
1408
|
+
* Low-level realtime WebSocket client.
|
|
1409
|
+
*
|
|
1410
|
+
* @internal Not a stable app-facing API. `createRebaseClient()` constructs and
|
|
1411
|
+
* manages this internally (exposed as `client.ws`, typed by the minimal
|
|
1412
|
+
* `RebaseWebSocket` contract in `@rebasepro/types`). It is re-exported from the
|
|
1413
|
+
* package root only because the `@rebasepro/client-postgres` driver
|
|
1414
|
+
* instantiates it directly; its surface may change without a major bump.
|
|
1415
|
+
*/
|
|
1155
1416
|
var RebaseWebSocketClient = class {
|
|
1156
1417
|
websocketUrl;
|
|
1157
1418
|
ws = null;
|
|
1158
1419
|
getAuthToken;
|
|
1159
1420
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1160
1421
|
listeners = /* @__PURE__ */ new Map();
|
|
1422
|
+
/** Channel-name → handlers, for broadcast and presence frames. */
|
|
1423
|
+
channelHandlers = /* @__PURE__ */ new Map();
|
|
1424
|
+
/** Subscribe to broadcast/presence frames for one channel. */
|
|
1425
|
+
onChannelMessage(channel, handler) {
|
|
1426
|
+
if (!this.channelHandlers.has(channel)) this.channelHandlers.set(channel, /* @__PURE__ */ new Set());
|
|
1427
|
+
this.channelHandlers.get(channel).add(handler);
|
|
1428
|
+
return () => {
|
|
1429
|
+
const handlers = this.channelHandlers.get(channel);
|
|
1430
|
+
if (!handlers) return;
|
|
1431
|
+
handlers.delete(handler);
|
|
1432
|
+
if (handlers.size === 0) this.channelHandlers.delete(channel);
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
/** Notified after the socket comes back, so channels can re-join. */
|
|
1436
|
+
onReconnect(handler) {
|
|
1437
|
+
return this.on("reconnect", handler);
|
|
1438
|
+
}
|
|
1161
1439
|
on(event, cb) {
|
|
1162
1440
|
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1163
1441
|
this.listeners.get(event).add(cb);
|
|
@@ -1167,7 +1445,7 @@ var RebaseWebSocketClient = class {
|
|
|
1167
1445
|
if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
|
|
1168
1446
|
}
|
|
1169
1447
|
collectionSubscriptions = /* @__PURE__ */ new Map();
|
|
1170
|
-
|
|
1448
|
+
singleSubscriptions = /* @__PURE__ */ new Map();
|
|
1171
1449
|
backendToCollectionKey = /* @__PURE__ */ new Map();
|
|
1172
1450
|
backendToEntityKey = /* @__PURE__ */ new Map();
|
|
1173
1451
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
@@ -1176,6 +1454,7 @@ var RebaseWebSocketClient = class {
|
|
|
1176
1454
|
isConnected = false;
|
|
1177
1455
|
messageQueue = [];
|
|
1178
1456
|
requestTimeoutMs = 3e4;
|
|
1457
|
+
subscriptionTimeoutMs = 3e4;
|
|
1179
1458
|
reconnectTimeout = null;
|
|
1180
1459
|
isAuthenticated = false;
|
|
1181
1460
|
authPromise = null;
|
|
@@ -1281,6 +1560,7 @@ var RebaseWebSocketClient = class {
|
|
|
1281
1560
|
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
1282
1561
|
this.processMessageQueue();
|
|
1283
1562
|
if (wasReconnect) this.resubscribeAll();
|
|
1563
|
+
this.armPendingSubscribeWatchdogs();
|
|
1284
1564
|
};
|
|
1285
1565
|
this.ws.onmessage = (event) => {
|
|
1286
1566
|
try {
|
|
@@ -1295,6 +1575,7 @@ var RebaseWebSocketClient = class {
|
|
|
1295
1575
|
this.isConnected = false;
|
|
1296
1576
|
this.isAuthenticated = false;
|
|
1297
1577
|
this.authPromise = null;
|
|
1578
|
+
this.suspendSubscribeWatchdogs();
|
|
1298
1579
|
this.emit("disconnect");
|
|
1299
1580
|
for (const [reqId, request] of this.pendingRequests.entries()) {
|
|
1300
1581
|
if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
|
|
@@ -1302,7 +1583,7 @@ var RebaseWebSocketClient = class {
|
|
|
1302
1583
|
request.message._queuedResolve = request.resolve;
|
|
1303
1584
|
request.message._queuedReject = request.reject;
|
|
1304
1585
|
this.messageQueue.push(request.message);
|
|
1305
|
-
} else request.reject(new
|
|
1586
|
+
} else request.reject(new RebaseApiError$1("Connection closed"));
|
|
1306
1587
|
this.pendingRequests.delete(reqId);
|
|
1307
1588
|
}
|
|
1308
1589
|
this.attemptReconnect();
|
|
@@ -1326,6 +1607,7 @@ var RebaseWebSocketClient = class {
|
|
|
1326
1607
|
attemptReconnect() {
|
|
1327
1608
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1328
1609
|
console.error("Max reconnection attempts reached");
|
|
1610
|
+
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1329
1611
|
return;
|
|
1330
1612
|
}
|
|
1331
1613
|
this.reconnectAttempts++;
|
|
@@ -1369,7 +1651,7 @@ var RebaseWebSocketClient = class {
|
|
|
1369
1651
|
}
|
|
1370
1652
|
}
|
|
1371
1653
|
/**
|
|
1372
|
-
* Shared logic for re-subscribing a collection or
|
|
1654
|
+
* Shared logic for re-subscribing a collection or row subscription
|
|
1373
1655
|
* after an auth error is resolved by refreshing credentials.
|
|
1374
1656
|
*/
|
|
1375
1657
|
resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
|
|
@@ -1380,29 +1662,18 @@ var RebaseWebSocketClient = class {
|
|
|
1380
1662
|
subscription.backendSubscriptionId = newBackendId;
|
|
1381
1663
|
backendKeyMap.delete(oldBackendId);
|
|
1382
1664
|
backendKeyMap.set(newBackendId, subscriptionKey);
|
|
1383
|
-
this.
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
...subscription.props,
|
|
1387
|
-
subscriptionId: newBackendId
|
|
1388
|
-
}
|
|
1389
|
-
}).catch((error) => {
|
|
1390
|
-
console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
|
|
1391
|
-
subscription.callbacks.forEach((callback) => {
|
|
1392
|
-
if (callback.onError) callback.onError(error);
|
|
1393
|
-
});
|
|
1394
|
-
});
|
|
1395
|
-
} else {
|
|
1396
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1397
|
-
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1398
|
-
subscription.callbacks.forEach((callback) => {
|
|
1399
|
-
if (callback.onError) callback.onError(error);
|
|
1400
|
-
});
|
|
1665
|
+
if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
|
|
1666
|
+
else this.sendEntitySubscribe(subscriptionKey);
|
|
1667
|
+
return;
|
|
1401
1668
|
}
|
|
1669
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1670
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1671
|
+
if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
|
|
1672
|
+
else this.failEntitySubscription(subscriptionKey, error);
|
|
1402
1673
|
}).catch((err) => {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1674
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1675
|
+
if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
|
|
1676
|
+
else this.failEntitySubscription(subscriptionKey, error);
|
|
1406
1677
|
});
|
|
1407
1678
|
}
|
|
1408
1679
|
handleWebSocketMessage(message) {
|
|
@@ -1415,7 +1686,7 @@ var RebaseWebSocketClient = class {
|
|
|
1415
1686
|
if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
|
|
1416
1687
|
else {
|
|
1417
1688
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1418
|
-
pendingReq.reject(new
|
|
1689
|
+
pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1419
1690
|
}
|
|
1420
1691
|
}).catch((err) => {
|
|
1421
1692
|
pendingReq.reject(err);
|
|
@@ -1423,7 +1694,7 @@ var RebaseWebSocketClient = class {
|
|
|
1423
1694
|
} else {
|
|
1424
1695
|
this.pendingRequests.delete(requestId);
|
|
1425
1696
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1426
|
-
pendingReq.reject(new
|
|
1697
|
+
pendingReq.reject(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1427
1698
|
}
|
|
1428
1699
|
else {
|
|
1429
1700
|
this.pendingRequests.delete(requestId);
|
|
@@ -1431,19 +1702,33 @@ var RebaseWebSocketClient = class {
|
|
|
1431
1702
|
}
|
|
1432
1703
|
return;
|
|
1433
1704
|
}
|
|
1705
|
+
if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff")) {
|
|
1706
|
+
const handlers = this.channelHandlers.get(message.channel);
|
|
1707
|
+
if (handlers) for (const handler of [...handlers]) try {
|
|
1708
|
+
handler(message);
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.error("Error in channel handler:", error);
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1434
1714
|
if (subscriptionId && type === "collection_update") {
|
|
1435
1715
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1436
1716
|
if (subscriptionKey) {
|
|
1437
1717
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1438
1718
|
if (collectionSub) {
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
collectionSub.
|
|
1719
|
+
const incomingRows = message.rows || [];
|
|
1720
|
+
const updatePks = message.pks;
|
|
1721
|
+
if (updatePks) collectionSub.pks = updatePks;
|
|
1722
|
+
const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
|
|
1723
|
+
collectionSub.latestData = rows;
|
|
1442
1724
|
collectionSub.lastUpdated = Date.now();
|
|
1443
1725
|
collectionSub.isInitialDataReceived = true;
|
|
1726
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1727
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1728
|
+
collectionSub.subscribeInFlight = false;
|
|
1444
1729
|
collectionSub.callbacks.forEach((callback) => {
|
|
1445
1730
|
try {
|
|
1446
|
-
callback.onUpdate(
|
|
1731
|
+
callback.onUpdate(rows);
|
|
1447
1732
|
} catch (error) {
|
|
1448
1733
|
console.error("Error in collection subscription callback:", error);
|
|
1449
1734
|
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -1453,21 +1738,24 @@ var RebaseWebSocketClient = class {
|
|
|
1453
1738
|
}
|
|
1454
1739
|
}
|
|
1455
1740
|
}
|
|
1456
|
-
if (subscriptionId && type === "
|
|
1741
|
+
if (subscriptionId && type === "collection_patch") {
|
|
1457
1742
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1458
1743
|
if (subscriptionKey) {
|
|
1459
1744
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1460
1745
|
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
1461
|
-
const
|
|
1462
|
-
const
|
|
1746
|
+
const patchWireEntity = message.row ?? null;
|
|
1747
|
+
const patchMessage = message;
|
|
1748
|
+
const patchEntityId = patchMessage.id;
|
|
1749
|
+
if (patchMessage.pks) collectionSub.pks = patchMessage.pks;
|
|
1750
|
+
const patchRow = patchWireEntity ? patchWireEntity : null;
|
|
1463
1751
|
let updated;
|
|
1464
|
-
if (
|
|
1752
|
+
if (patchRow === null) updated = collectionSub.latestData.filter((e) => this.rowAddress(e, collectionSub.pks) !== String(patchEntityId));
|
|
1465
1753
|
else {
|
|
1466
|
-
const idx = collectionSub.latestData.findIndex((e) =>
|
|
1754
|
+
const idx = collectionSub.latestData.findIndex((e) => this.rowAddress(e, collectionSub.pks) === String(patchEntityId));
|
|
1467
1755
|
if (idx >= 0) {
|
|
1468
1756
|
updated = [...collectionSub.latestData];
|
|
1469
|
-
updated[idx] =
|
|
1470
|
-
} else updated = [
|
|
1757
|
+
updated[idx] = patchRow;
|
|
1758
|
+
} else updated = [patchRow, ...collectionSub.latestData];
|
|
1471
1759
|
}
|
|
1472
1760
|
collectionSub.latestData = updated;
|
|
1473
1761
|
collectionSub.lastUpdated = Date.now();
|
|
@@ -1483,20 +1771,24 @@ var RebaseWebSocketClient = class {
|
|
|
1483
1771
|
}
|
|
1484
1772
|
}
|
|
1485
1773
|
}
|
|
1486
|
-
if (subscriptionId && type === "
|
|
1774
|
+
if (subscriptionId && type === "single_update") {
|
|
1487
1775
|
const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
|
|
1488
1776
|
if (subscriptionKey) {
|
|
1489
|
-
const entitySub = this.
|
|
1777
|
+
const entitySub = this.singleSubscriptions.get(subscriptionKey);
|
|
1490
1778
|
if (entitySub) {
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1779
|
+
const wireEntity = message.row ?? null;
|
|
1780
|
+
const row = wireEntity ? wireEntity : null;
|
|
1781
|
+
entitySub.latestData = row;
|
|
1493
1782
|
entitySub.lastUpdated = Date.now();
|
|
1494
1783
|
entitySub.isInitialDataReceived = true;
|
|
1784
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1785
|
+
entitySub.subscribeTimeout = void 0;
|
|
1786
|
+
entitySub.subscribeInFlight = false;
|
|
1495
1787
|
entitySub.callbacks.forEach((callback) => {
|
|
1496
1788
|
try {
|
|
1497
|
-
callback.onUpdate(
|
|
1789
|
+
callback.onUpdate(row);
|
|
1498
1790
|
} catch (error) {
|
|
1499
|
-
console.error("Error in
|
|
1791
|
+
console.error("Error in row subscription callback:", error);
|
|
1500
1792
|
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1501
1793
|
}
|
|
1502
1794
|
});
|
|
@@ -1513,8 +1805,11 @@ var RebaseWebSocketClient = class {
|
|
|
1513
1805
|
this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
|
|
1514
1806
|
return;
|
|
1515
1807
|
}
|
|
1808
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1809
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1810
|
+
collectionSub.subscribeInFlight = false;
|
|
1516
1811
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1517
|
-
const error = new
|
|
1812
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1518
1813
|
collectionSub.callbacks.forEach((callback) => {
|
|
1519
1814
|
if (callback.onError) callback.onError(error);
|
|
1520
1815
|
});
|
|
@@ -1523,14 +1818,17 @@ var RebaseWebSocketClient = class {
|
|
|
1523
1818
|
}
|
|
1524
1819
|
const entityKey = this.backendToEntityKey.get(subscriptionId);
|
|
1525
1820
|
if (entityKey) {
|
|
1526
|
-
const entitySub = this.
|
|
1821
|
+
const entitySub = this.singleSubscriptions.get(entityKey);
|
|
1527
1822
|
if (entitySub) {
|
|
1528
1823
|
if (this.isAuthError(message)) {
|
|
1529
|
-
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "
|
|
1824
|
+
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
|
|
1530
1825
|
return;
|
|
1531
1826
|
}
|
|
1827
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1828
|
+
entitySub.subscribeTimeout = void 0;
|
|
1829
|
+
entitySub.subscribeInFlight = false;
|
|
1532
1830
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1533
|
-
const error = new
|
|
1831
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1534
1832
|
entitySub.callbacks.forEach((callback) => {
|
|
1535
1833
|
if (callback.onError) callback.onError(error);
|
|
1536
1834
|
});
|
|
@@ -1544,7 +1842,7 @@ var RebaseWebSocketClient = class {
|
|
|
1544
1842
|
if (message.type === "ERROR" || message.error) {
|
|
1545
1843
|
if (callback.onError) {
|
|
1546
1844
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1547
|
-
callback.onError(new
|
|
1845
|
+
callback.onError(new RebaseApiError$1(errorMessage, { code: errorCode }));
|
|
1548
1846
|
}
|
|
1549
1847
|
} else callback.onUpdate(message);
|
|
1550
1848
|
}
|
|
@@ -1601,6 +1899,10 @@ var RebaseWebSocketClient = class {
|
|
|
1601
1899
|
throw error;
|
|
1602
1900
|
}
|
|
1603
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Public because `RebaseRealtimeChannel` sends channel frames through it.
|
|
1904
|
+
* Not part of the stable surface — prefer `client.realtime.channel(name)`.
|
|
1905
|
+
*/
|
|
1604
1906
|
sendMessage(message) {
|
|
1605
1907
|
const queuedMsg = message;
|
|
1606
1908
|
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
@@ -1618,15 +1920,14 @@ var RebaseWebSocketClient = class {
|
|
|
1618
1920
|
if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
|
|
1619
1921
|
await this.ensureAuthenticated();
|
|
1620
1922
|
} catch (error) {
|
|
1621
|
-
|
|
1622
|
-
reject(new ApiError(errorMessage, errorMessage));
|
|
1923
|
+
reject(new RebaseApiError$1(error instanceof Error ? error.message : "Authentication required"));
|
|
1623
1924
|
return;
|
|
1624
1925
|
}
|
|
1625
1926
|
const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1626
1927
|
message.requestId = requestId;
|
|
1627
1928
|
const expectsResponse = ![
|
|
1628
1929
|
"subscribe_collection",
|
|
1629
|
-
"
|
|
1930
|
+
"subscribe_one",
|
|
1630
1931
|
"unsubscribe",
|
|
1631
1932
|
"join_channel",
|
|
1632
1933
|
"leave_channel",
|
|
@@ -1639,7 +1940,7 @@ var RebaseWebSocketClient = class {
|
|
|
1639
1940
|
const timeoutHandle = setTimeout(() => {
|
|
1640
1941
|
if (this.pendingRequests.has(requestId)) {
|
|
1641
1942
|
this.pendingRequests.delete(requestId);
|
|
1642
|
-
reject(new
|
|
1943
|
+
reject(new RebaseApiError$1("Request timed out"));
|
|
1643
1944
|
}
|
|
1644
1945
|
}, this.requestTimeoutMs);
|
|
1645
1946
|
this.pendingRequests.set(requestId, {
|
|
@@ -1659,30 +1960,30 @@ var RebaseWebSocketClient = class {
|
|
|
1659
1960
|
if (!expectsResponse) resolve(void 0);
|
|
1660
1961
|
} catch (error) {
|
|
1661
1962
|
if (expectsResponse) this.pendingRequests.delete(requestId);
|
|
1662
|
-
reject(new
|
|
1963
|
+
reject(new RebaseApiError$1("Failed to send message", { cause: error }));
|
|
1663
1964
|
}
|
|
1664
1965
|
}
|
|
1665
1966
|
async fetchCollection(props) {
|
|
1666
1967
|
return (await this.sendMessage({
|
|
1667
1968
|
type: "FETCH_COLLECTION",
|
|
1668
1969
|
payload: props
|
|
1669
|
-
})).
|
|
1970
|
+
})).rows || [];
|
|
1670
1971
|
}
|
|
1671
|
-
async
|
|
1972
|
+
async fetchOne(props) {
|
|
1672
1973
|
return (await this.sendMessage({
|
|
1673
|
-
type: "
|
|
1974
|
+
type: "FETCH_ONE",
|
|
1674
1975
|
payload: props
|
|
1675
|
-
})).
|
|
1976
|
+
})).row ?? void 0;
|
|
1676
1977
|
}
|
|
1677
|
-
async
|
|
1978
|
+
async save(props) {
|
|
1678
1979
|
return (await this.sendMessage({
|
|
1679
|
-
type: "
|
|
1980
|
+
type: "SAVE",
|
|
1680
1981
|
payload: props
|
|
1681
|
-
})).
|
|
1982
|
+
})).row;
|
|
1682
1983
|
}
|
|
1683
|
-
async
|
|
1984
|
+
async delete(props) {
|
|
1684
1985
|
await this.sendMessage({
|
|
1685
|
-
type: "
|
|
1986
|
+
type: "DELETE",
|
|
1686
1987
|
payload: props
|
|
1687
1988
|
});
|
|
1688
1989
|
}
|
|
@@ -1707,21 +2008,21 @@ var RebaseWebSocketClient = class {
|
|
|
1707
2008
|
async fetchCurrentDatabase() {
|
|
1708
2009
|
return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
|
|
1709
2010
|
}
|
|
1710
|
-
async checkUniqueField(path, name, value,
|
|
2011
|
+
async checkUniqueField(path, name, value, id, collection) {
|
|
1711
2012
|
return (await this.sendMessage({
|
|
1712
2013
|
type: "CHECK_UNIQUE_FIELD",
|
|
1713
2014
|
payload: {
|
|
1714
2015
|
path,
|
|
1715
2016
|
name,
|
|
1716
2017
|
value,
|
|
1717
|
-
|
|
2018
|
+
id,
|
|
1718
2019
|
collection
|
|
1719
2020
|
}
|
|
1720
2021
|
})).isUnique;
|
|
1721
2022
|
}
|
|
1722
|
-
async
|
|
2023
|
+
async count(props) {
|
|
1723
2024
|
return (await this.sendMessage({
|
|
1724
|
-
type: "
|
|
2025
|
+
type: "COUNT",
|
|
1725
2026
|
payload: props
|
|
1726
2027
|
})).count;
|
|
1727
2028
|
}
|
|
@@ -1813,33 +2114,53 @@ var RebaseWebSocketClient = class {
|
|
|
1813
2114
|
return val;
|
|
1814
2115
|
}
|
|
1815
2116
|
/**
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1818
|
-
*
|
|
2117
|
+
* The address of a row, for matching it against another copy of itself.
|
|
2118
|
+
*
|
|
2119
|
+
* A row is exactly its columns and carries no address, so it is derived
|
|
2120
|
+
* from the key columns the server named — including the ordinary case where
|
|
2121
|
+
* that key is `id`, which the server reports like any other.
|
|
2122
|
+
*
|
|
2123
|
+
* Undefined when there are no keys, which means the server could not
|
|
2124
|
+
* resolve any: such rows genuinely cannot be recognised, and guessing at a
|
|
2125
|
+
* column called `id` would be inventing an identity for a table that has
|
|
2126
|
+
* none.
|
|
2127
|
+
*/
|
|
2128
|
+
rowAddress(row, pks) {
|
|
2129
|
+
if (!pks || pks.length === 0) return void 0;
|
|
2130
|
+
const address = buildCompositeId(row, pks);
|
|
2131
|
+
if (!address || address.split(COMPOSITE_ID_SEPARATOR).every((part) => part === "")) return void 0;
|
|
2132
|
+
return address;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Merge incoming rows with cached data, preserving cached references
|
|
2136
|
+
* for rows whose values haven't changed. This avoids unnecessary
|
|
2137
|
+
* React re-renders when the server refetches all rows but most
|
|
1819
2138
|
* haven't actually changed.
|
|
1820
2139
|
*/
|
|
1821
|
-
|
|
2140
|
+
mergeRows(cached, incoming, pks) {
|
|
1822
2141
|
if (!cached || cached.length === 0) return incoming;
|
|
1823
2142
|
const cachedById = /* @__PURE__ */ new Map();
|
|
1824
|
-
for (const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2143
|
+
for (const row of cached) {
|
|
2144
|
+
const address = this.rowAddress(row, pks);
|
|
2145
|
+
if (address !== void 0) cachedById.set(address, row);
|
|
2146
|
+
}
|
|
2147
|
+
return incoming.map((incomingRow) => {
|
|
2148
|
+
const address = this.rowAddress(incomingRow, pks);
|
|
2149
|
+
const cachedRow = address === void 0 ? void 0 : cachedById.get(address);
|
|
2150
|
+
if (!cachedRow) return incomingRow;
|
|
2151
|
+
const normCached = this.normalizeForComparison(cachedRow);
|
|
2152
|
+
const normIncoming = this.normalizeForComparison(incomingRow);
|
|
2153
|
+
if (this.deepEqual(normCached, normIncoming)) return cachedRow;
|
|
2154
|
+
else {
|
|
2155
|
+
const mismatches = {};
|
|
2156
|
+
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
2157
|
+
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
2158
|
+
cached: normCached[key],
|
|
2159
|
+
incoming: normIncoming[key]
|
|
2160
|
+
};
|
|
2161
|
+
console.debug(`[RebaseWS] Row ${address} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1841
2162
|
}
|
|
1842
|
-
return
|
|
2163
|
+
return incomingRow;
|
|
1843
2164
|
});
|
|
1844
2165
|
}
|
|
1845
2166
|
listenCollection(props, onUpdate, onError) {
|
|
@@ -1858,9 +2179,12 @@ var RebaseWebSocketClient = class {
|
|
|
1858
2179
|
console.error("Error in collection subscription callback:", error);
|
|
1859
2180
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
1860
2181
|
}
|
|
2182
|
+
else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
|
|
1861
2183
|
return () => {
|
|
1862
2184
|
callbackMap.delete(callbackId);
|
|
1863
2185
|
if (callbackMap.size === 0) {
|
|
2186
|
+
if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2187
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
1864
2188
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1865
2189
|
this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
|
|
1866
2190
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -1882,21 +2206,14 @@ var RebaseWebSocketClient = class {
|
|
|
1882
2206
|
props
|
|
1883
2207
|
});
|
|
1884
2208
|
this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
|
|
1885
|
-
this.
|
|
1886
|
-
type: "subscribe_collection",
|
|
1887
|
-
payload: {
|
|
1888
|
-
...props,
|
|
1889
|
-
subscriptionId: backendSubscriptionId
|
|
1890
|
-
}
|
|
1891
|
-
}).catch((error) => {
|
|
1892
|
-
if (onError) onError(error);
|
|
1893
|
-
});
|
|
2209
|
+
this.sendCollectionSubscribe(subscriptionKey);
|
|
1894
2210
|
return () => {
|
|
1895
2211
|
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
1896
2212
|
if (subscription) {
|
|
1897
2213
|
const callbacks = subscription.callbacks;
|
|
1898
2214
|
callbacks.delete(callbackId);
|
|
1899
2215
|
if (callbacks.size === 0) {
|
|
2216
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
1900
2217
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1901
2218
|
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
1902
2219
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -1907,10 +2224,10 @@ var RebaseWebSocketClient = class {
|
|
|
1907
2224
|
}
|
|
1908
2225
|
};
|
|
1909
2226
|
}
|
|
1910
|
-
|
|
1911
|
-
const subscriptionKey = this.
|
|
2227
|
+
listenOne(props, onUpdate, onError) {
|
|
2228
|
+
const subscriptionKey = this.createSingleSubscriptionKey(props);
|
|
1912
2229
|
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1913
|
-
const existingSubscription = this.
|
|
2230
|
+
const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1914
2231
|
if (existingSubscription) {
|
|
1915
2232
|
const callbackMap = existingSubscription.callbacks;
|
|
1916
2233
|
callbackMap.set(callbackId, {
|
|
@@ -1920,13 +2237,16 @@ var RebaseWebSocketClient = class {
|
|
|
1920
2237
|
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
|
|
1921
2238
|
onUpdate(existingSubscription.latestData);
|
|
1922
2239
|
} catch (error) {
|
|
1923
|
-
console.error("Error in
|
|
2240
|
+
console.error("Error in row subscription callback:", error);
|
|
1924
2241
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
1925
2242
|
}
|
|
2243
|
+
else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
|
|
1926
2244
|
return () => {
|
|
1927
2245
|
callbackMap.delete(callbackId);
|
|
1928
2246
|
if (callbackMap.size === 0) {
|
|
1929
|
-
this.
|
|
2247
|
+
if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2248
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
2249
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1930
2250
|
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
1931
2251
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
1932
2252
|
type: "unsubscribe",
|
|
@@ -1941,28 +2261,20 @@ var RebaseWebSocketClient = class {
|
|
|
1941
2261
|
onUpdate,
|
|
1942
2262
|
onError
|
|
1943
2263
|
});
|
|
1944
|
-
this.
|
|
2264
|
+
this.singleSubscriptions.set(subscriptionKey, {
|
|
1945
2265
|
backendSubscriptionId,
|
|
1946
2266
|
callbacks: callbackMap,
|
|
1947
2267
|
props
|
|
1948
2268
|
});
|
|
1949
2269
|
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
1950
|
-
this.
|
|
1951
|
-
type: "subscribe_entity",
|
|
1952
|
-
payload: {
|
|
1953
|
-
...props,
|
|
1954
|
-
subscriptionId: backendSubscriptionId
|
|
1955
|
-
}
|
|
1956
|
-
}).catch((error) => {
|
|
1957
|
-
if (onError) onError(error);
|
|
1958
|
-
});
|
|
2270
|
+
this.sendEntitySubscribe(subscriptionKey);
|
|
1959
2271
|
return () => {
|
|
1960
|
-
const subscription = this.
|
|
2272
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
1961
2273
|
if (subscription) {
|
|
1962
2274
|
const callbacks = subscription.callbacks;
|
|
1963
2275
|
callbacks.delete(callbackId);
|
|
1964
2276
|
if (callbacks.size === 0) {
|
|
1965
|
-
this.
|
|
2277
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
1966
2278
|
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
1967
2279
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
1968
2280
|
type: "unsubscribe",
|
|
@@ -1973,43 +2285,178 @@ var RebaseWebSocketClient = class {
|
|
|
1973
2285
|
};
|
|
1974
2286
|
}
|
|
1975
2287
|
/**
|
|
2288
|
+
* Send a `subscribe_collection` for an already-registered subscription and
|
|
2289
|
+
* arm its watchdog.
|
|
2290
|
+
*
|
|
2291
|
+
* Every path that registers a collection subscription goes through here, so
|
|
2292
|
+
* that a subscribe which never lands — a rejected send, or a server that
|
|
2293
|
+
* never answers — always ends up in `failCollectionSubscription` rather than
|
|
2294
|
+
* leaving the entry parked with `isInitialDataReceived === false` forever.
|
|
2295
|
+
*/
|
|
2296
|
+
sendCollectionSubscribe(subscriptionKey) {
|
|
2297
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2298
|
+
if (!subscription) return;
|
|
2299
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2300
|
+
subscription.subscribeInFlight = true;
|
|
2301
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2302
|
+
subscription.subscribeTimeout = void 0;
|
|
2303
|
+
if (this.isConnected) this.sendCollectionSubscribeWatchdog(subscriptionKey);
|
|
2304
|
+
this.sendMessage({
|
|
2305
|
+
type: "subscribe_collection",
|
|
2306
|
+
payload: {
|
|
2307
|
+
...subscription.props,
|
|
2308
|
+
subscriptionId: backendSubscriptionId
|
|
2309
|
+
}
|
|
2310
|
+
}).catch((error) => {
|
|
2311
|
+
const current = this.collectionSubscriptions.get(subscriptionKey);
|
|
2312
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2313
|
+
this.failCollectionSubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
/** The `listenOne` counterpart of {@link sendCollectionSubscribe}. */
|
|
2317
|
+
sendEntitySubscribe(subscriptionKey) {
|
|
2318
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2319
|
+
if (!subscription) return;
|
|
2320
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2321
|
+
subscription.subscribeInFlight = true;
|
|
2322
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2323
|
+
subscription.subscribeTimeout = void 0;
|
|
2324
|
+
if (this.isConnected) this.sendEntitySubscribeWatchdog(subscriptionKey);
|
|
2325
|
+
this.sendMessage({
|
|
2326
|
+
type: "subscribe_one",
|
|
2327
|
+
payload: {
|
|
2328
|
+
...subscription.props,
|
|
2329
|
+
subscriptionId: backendSubscriptionId
|
|
2330
|
+
}
|
|
2331
|
+
}).catch((error) => {
|
|
2332
|
+
const current = this.singleSubscriptions.get(subscriptionKey);
|
|
2333
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2334
|
+
this.failEntitySubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Report a subscribe failure to every listener and drop the registration.
|
|
2339
|
+
*
|
|
2340
|
+
* Dropping it is the point: the callbacks stay live (their components are
|
|
2341
|
+
* still mounted and have been told), but the next `listenCollection` for
|
|
2342
|
+
* these params finds no entry and issues a fresh subscribe instead of
|
|
2343
|
+
* silently attaching to a dead one.
|
|
2344
|
+
*/
|
|
2345
|
+
failCollectionSubscription(subscriptionKey, error) {
|
|
2346
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2347
|
+
if (!subscription) return;
|
|
2348
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2349
|
+
subscription.subscribeInFlight = false;
|
|
2350
|
+
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2351
|
+
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
2352
|
+
subscription.callbacks.forEach((callback) => {
|
|
2353
|
+
if (callback.onError) try {
|
|
2354
|
+
callback.onError(error);
|
|
2355
|
+
} catch (callbackError) {
|
|
2356
|
+
console.error("Error in collection subscription error callback:", callbackError);
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
/** The `listenOne` counterpart of {@link failCollectionSubscription}. */
|
|
2361
|
+
failEntitySubscription(subscriptionKey, error) {
|
|
2362
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2363
|
+
if (!subscription) return;
|
|
2364
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2365
|
+
subscription.subscribeInFlight = false;
|
|
2366
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
2367
|
+
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
2368
|
+
subscription.callbacks.forEach((callback) => {
|
|
2369
|
+
if (callback.onError) try {
|
|
2370
|
+
callback.onError(error);
|
|
2371
|
+
} catch (callbackError) {
|
|
2372
|
+
console.error("Error in row subscription error callback:", callbackError);
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Stop the watchdogs without failing anything — used when the socket drops,
|
|
2378
|
+
* since the reconnect path re-subscribes everything anyway and a watchdog
|
|
2379
|
+
* firing mid-reconnect would tear down healthy subscriptions.
|
|
2380
|
+
*/
|
|
2381
|
+
suspendSubscribeWatchdogs() {
|
|
2382
|
+
for (const sub of this.collectionSubscriptions.values()) {
|
|
2383
|
+
if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
|
|
2384
|
+
sub.subscribeTimeout = void 0;
|
|
2385
|
+
sub.subscribeInFlight = false;
|
|
2386
|
+
}
|
|
2387
|
+
for (const sub of this.singleSubscriptions.values()) {
|
|
2388
|
+
if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
|
|
2389
|
+
sub.subscribeTimeout = void 0;
|
|
2390
|
+
sub.subscribeInFlight = false;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Arm watchdogs for subscribes that were requested while offline and have
|
|
2395
|
+
* just been flushed to the socket. Their timers were deliberately not set at
|
|
2396
|
+
* request time, so without this they would have no timeout at all.
|
|
2397
|
+
*/
|
|
2398
|
+
armPendingSubscribeWatchdogs() {
|
|
2399
|
+
for (const [key, sub] of this.collectionSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendCollectionSubscribeWatchdog(key);
|
|
2400
|
+
for (const [key, sub] of this.singleSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendEntitySubscribeWatchdog(key);
|
|
2401
|
+
}
|
|
2402
|
+
sendCollectionSubscribeWatchdog(subscriptionKey) {
|
|
2403
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2404
|
+
if (!subscription) return;
|
|
2405
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2406
|
+
subscription.subscribeTimeout = setTimeout(() => {
|
|
2407
|
+
const current = this.collectionSubscriptions.get(subscriptionKey);
|
|
2408
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2409
|
+
if (!current.subscribeInFlight) return;
|
|
2410
|
+
this.failCollectionSubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
|
|
2411
|
+
}, this.subscriptionTimeoutMs);
|
|
2412
|
+
}
|
|
2413
|
+
sendEntitySubscribeWatchdog(subscriptionKey) {
|
|
2414
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2415
|
+
if (!subscription) return;
|
|
2416
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2417
|
+
subscription.subscribeTimeout = setTimeout(() => {
|
|
2418
|
+
const current = this.singleSubscriptions.get(subscriptionKey);
|
|
2419
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2420
|
+
if (!current.subscribeInFlight) return;
|
|
2421
|
+
this.failEntitySubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
|
|
2422
|
+
}, this.subscriptionTimeoutMs);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Fail every subscription that never received data. Called when reconnection
|
|
2426
|
+
* is given up on, so views surface an error instead of spinning forever.
|
|
2427
|
+
*/
|
|
2428
|
+
failAllPendingSubscriptions(error) {
|
|
2429
|
+
for (const key of [...this.collectionSubscriptions.keys()]) {
|
|
2430
|
+
const sub = this.collectionSubscriptions.get(key);
|
|
2431
|
+
if (sub && !sub.isInitialDataReceived) this.failCollectionSubscription(key, error);
|
|
2432
|
+
}
|
|
2433
|
+
for (const key of [...this.singleSubscriptions.keys()]) {
|
|
2434
|
+
const sub = this.singleSubscriptions.get(key);
|
|
2435
|
+
if (sub && !sub.isInitialDataReceived) this.failEntitySubscription(key, error);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
1976
2439
|
* Re-send all active subscriptions to the backend after a reconnect.
|
|
1977
2440
|
* The server wipes subscription state when a client disconnects, so
|
|
1978
2441
|
* we need to re-register everything to resume receiving updates.
|
|
1979
2442
|
*/
|
|
1980
2443
|
resubscribeAll() {
|
|
1981
|
-
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.
|
|
2444
|
+
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.singleSubscriptions.size} row(ies)`);
|
|
1982
2445
|
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
1983
2446
|
const oldBackendId = sub.backendSubscriptionId;
|
|
1984
2447
|
const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1985
2448
|
sub.backendSubscriptionId = newBackendId;
|
|
1986
2449
|
this.backendToCollectionKey.delete(oldBackendId);
|
|
1987
2450
|
this.backendToCollectionKey.set(newBackendId, key);
|
|
1988
|
-
this.
|
|
1989
|
-
type: "subscribe_collection",
|
|
1990
|
-
payload: {
|
|
1991
|
-
...sub.props,
|
|
1992
|
-
subscriptionId: newBackendId
|
|
1993
|
-
}
|
|
1994
|
-
}).catch((error) => {
|
|
1995
|
-
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
1996
|
-
});
|
|
2451
|
+
this.sendCollectionSubscribe(key);
|
|
1997
2452
|
}
|
|
1998
|
-
for (const [key, sub] of this.
|
|
2453
|
+
for (const [key, sub] of this.singleSubscriptions.entries()) {
|
|
1999
2454
|
const oldBackendId = sub.backendSubscriptionId;
|
|
2000
2455
|
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2001
2456
|
sub.backendSubscriptionId = newBackendId;
|
|
2002
2457
|
this.backendToEntityKey.delete(oldBackendId);
|
|
2003
2458
|
this.backendToEntityKey.set(newBackendId, key);
|
|
2004
|
-
this.
|
|
2005
|
-
type: "subscribe_entity",
|
|
2006
|
-
payload: {
|
|
2007
|
-
...sub.props,
|
|
2008
|
-
subscriptionId: newBackendId
|
|
2009
|
-
}
|
|
2010
|
-
}).catch((error) => {
|
|
2011
|
-
console.error("[WS] Failed to re-subscribe entity:", key, error);
|
|
2012
|
-
});
|
|
2459
|
+
this.sendEntitySubscribe(key);
|
|
2013
2460
|
}
|
|
2014
2461
|
}
|
|
2015
2462
|
createCollectionSubscriptionKey(props) {
|
|
@@ -2031,8 +2478,192 @@ var RebaseWebSocketClient = class {
|
|
|
2031
2478
|
return value;
|
|
2032
2479
|
});
|
|
2033
2480
|
}
|
|
2034
|
-
|
|
2035
|
-
return `${props.path}|${props.
|
|
2481
|
+
createSingleSubscriptionKey(props) {
|
|
2482
|
+
return `${props.path}|${props.id}`;
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
//#endregion
|
|
2486
|
+
//#region src/realtime-channel.ts
|
|
2487
|
+
/**
|
|
2488
|
+
* Re-send presence comfortably inside the server's 30s expiry.
|
|
2489
|
+
*
|
|
2490
|
+
* Two-thirds of the window: one lost heartbeat still leaves time for the next
|
|
2491
|
+
* before the entry is reaped, so a single dropped frame is not a disappearance.
|
|
2492
|
+
*/
|
|
2493
|
+
var PRESENCE_HEARTBEAT_MS = 2e4;
|
|
2494
|
+
var RebaseRealtimeChannel = class {
|
|
2495
|
+
name;
|
|
2496
|
+
transport;
|
|
2497
|
+
presenceHandlers = /* @__PURE__ */ new Set();
|
|
2498
|
+
broadcastHandlers = /* @__PURE__ */ new Set();
|
|
2499
|
+
unsubscribers = [];
|
|
2500
|
+
/** Last known roster, kept so handlers always get a full picture. */
|
|
2501
|
+
presences = {};
|
|
2502
|
+
/** What this client last tracked, replayed on reconnect and heartbeat. */
|
|
2503
|
+
trackedState = null;
|
|
2504
|
+
heartbeat = null;
|
|
2505
|
+
joined = false;
|
|
2506
|
+
constructor(name, transport) {
|
|
2507
|
+
this.name = name;
|
|
2508
|
+
this.transport = transport;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Join the channel and ask for the current roster.
|
|
2512
|
+
*
|
|
2513
|
+
* Called automatically by `track`, `broadcast`, `onPresence` and
|
|
2514
|
+
* `onBroadcast`; calling it directly is only needed to start receiving
|
|
2515
|
+
* before there is anything to send.
|
|
2516
|
+
*/
|
|
2517
|
+
async join() {
|
|
2518
|
+
if (this.joined) return;
|
|
2519
|
+
this.joined = true;
|
|
2520
|
+
this.unsubscribers.push(this.transport.onChannelMessage(this.name, (message) => this.handle(message)));
|
|
2521
|
+
this.unsubscribers.push(this.transport.onReconnect(() => {
|
|
2522
|
+
this.rejoin();
|
|
2523
|
+
}));
|
|
2524
|
+
await this.transport.sendMessage({
|
|
2525
|
+
type: "join_channel",
|
|
2526
|
+
channel: this.name
|
|
2527
|
+
});
|
|
2528
|
+
await this.transport.sendMessage({
|
|
2529
|
+
type: "presence_state",
|
|
2530
|
+
channel: this.name
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
async rejoin() {
|
|
2534
|
+
try {
|
|
2535
|
+
await this.transport.sendMessage({
|
|
2536
|
+
type: "join_channel",
|
|
2537
|
+
channel: this.name
|
|
2538
|
+
});
|
|
2539
|
+
await this.transport.sendMessage({
|
|
2540
|
+
type: "presence_state",
|
|
2541
|
+
channel: this.name
|
|
2542
|
+
});
|
|
2543
|
+
if (this.trackedState) await this.transport.sendMessage({
|
|
2544
|
+
type: "presence_track",
|
|
2545
|
+
channel: this.name,
|
|
2546
|
+
state: this.trackedState
|
|
2547
|
+
});
|
|
2548
|
+
} catch {}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Publish this client's presence state, and keep publishing it.
|
|
2552
|
+
*
|
|
2553
|
+
* Calling `track` again replaces the state (and restarts the heartbeat),
|
|
2554
|
+
* which is how you update e.g. a cursor position.
|
|
2555
|
+
*/
|
|
2556
|
+
async track(state) {
|
|
2557
|
+
await this.join();
|
|
2558
|
+
this.trackedState = state;
|
|
2559
|
+
await this.transport.sendMessage({
|
|
2560
|
+
type: "presence_track",
|
|
2561
|
+
channel: this.name,
|
|
2562
|
+
state
|
|
2563
|
+
});
|
|
2564
|
+
if (!this.heartbeat) {
|
|
2565
|
+
this.heartbeat = setInterval(() => {
|
|
2566
|
+
if (!this.trackedState) return;
|
|
2567
|
+
this.transport.sendMessage({
|
|
2568
|
+
type: "presence_track",
|
|
2569
|
+
channel: this.name,
|
|
2570
|
+
state: this.trackedState
|
|
2571
|
+
}).catch(() => {});
|
|
2572
|
+
}, PRESENCE_HEARTBEAT_MS);
|
|
2573
|
+
this.heartbeat.unref?.();
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
/** Stop publishing presence, without leaving the channel. */
|
|
2577
|
+
async untrack() {
|
|
2578
|
+
this.stopHeartbeat();
|
|
2579
|
+
this.trackedState = null;
|
|
2580
|
+
if (this.joined) await this.transport.sendMessage({
|
|
2581
|
+
type: "presence_untrack",
|
|
2582
|
+
channel: this.name
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Observe the roster. The handler fires immediately with what is already
|
|
2587
|
+
* known, then on every change.
|
|
2588
|
+
*/
|
|
2589
|
+
onPresence(handler) {
|
|
2590
|
+
this.presenceHandlers.add(handler);
|
|
2591
|
+
this.join();
|
|
2592
|
+
if (Object.keys(this.presences).length > 0) handler({ ...this.presences });
|
|
2593
|
+
return () => this.presenceHandlers.delete(handler);
|
|
2594
|
+
}
|
|
2595
|
+
/** Send a broadcast. The sender does not receive its own message. */
|
|
2596
|
+
async broadcast(event, payload) {
|
|
2597
|
+
await this.join();
|
|
2598
|
+
await this.transport.sendMessage({
|
|
2599
|
+
type: "broadcast",
|
|
2600
|
+
channel: this.name,
|
|
2601
|
+
event,
|
|
2602
|
+
payload
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
onBroadcast(eventOrHandler, maybeHandler) {
|
|
2606
|
+
const wrapped = typeof eventOrHandler === "string" ? (e) => {
|
|
2607
|
+
if (e.event === eventOrHandler) maybeHandler(e.payload);
|
|
2608
|
+
} : eventOrHandler;
|
|
2609
|
+
this.broadcastHandlers.add(wrapped);
|
|
2610
|
+
this.join();
|
|
2611
|
+
return () => this.broadcastHandlers.delete(wrapped);
|
|
2612
|
+
}
|
|
2613
|
+
/** Leave the channel and release every listener and timer. */
|
|
2614
|
+
async leave() {
|
|
2615
|
+
this.stopHeartbeat();
|
|
2616
|
+
this.trackedState = null;
|
|
2617
|
+
this.presences = {};
|
|
2618
|
+
this.presenceHandlers.clear();
|
|
2619
|
+
this.broadcastHandlers.clear();
|
|
2620
|
+
for (const off of this.unsubscribers) off();
|
|
2621
|
+
this.unsubscribers = [];
|
|
2622
|
+
if (this.joined) {
|
|
2623
|
+
this.joined = false;
|
|
2624
|
+
await this.transport.sendMessage({
|
|
2625
|
+
type: "leave_channel",
|
|
2626
|
+
channel: this.name
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
stopHeartbeat() {
|
|
2631
|
+
if (this.heartbeat) {
|
|
2632
|
+
clearInterval(this.heartbeat);
|
|
2633
|
+
this.heartbeat = null;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
/** Fold an incoming frame into the roster and fan it out. */
|
|
2637
|
+
handle(message) {
|
|
2638
|
+
switch (message.type) {
|
|
2639
|
+
case "presence_state":
|
|
2640
|
+
this.presences = message.presences ?? {};
|
|
2641
|
+
this.emitPresence();
|
|
2642
|
+
break;
|
|
2643
|
+
case "presence_diff": {
|
|
2644
|
+
const joins = message.joins ?? {};
|
|
2645
|
+
const leaves = message.leaves ?? {};
|
|
2646
|
+
for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
|
|
2647
|
+
for (const id of Object.keys(leaves)) delete this.presences[id];
|
|
2648
|
+
this.emitPresence({
|
|
2649
|
+
joins,
|
|
2650
|
+
leaves
|
|
2651
|
+
});
|
|
2652
|
+
break;
|
|
2653
|
+
}
|
|
2654
|
+
case "broadcast": {
|
|
2655
|
+
const event = {
|
|
2656
|
+
event: message.event,
|
|
2657
|
+
payload: message.payload
|
|
2658
|
+
};
|
|
2659
|
+
for (const handler of this.broadcastHandlers) handler(event);
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
emitPresence(diff) {
|
|
2665
|
+
const snapshot = { ...this.presences };
|
|
2666
|
+
for (const handler of this.presenceHandlers) handler(snapshot, diff);
|
|
2036
2667
|
}
|
|
2037
2668
|
};
|
|
2038
2669
|
//#endregion
|
|
@@ -2063,6 +2694,7 @@ function createRebaseClient(options) {
|
|
|
2063
2694
|
const auth = createAuth(transport, options.auth);
|
|
2064
2695
|
const admin = createAdmin(transport, options.admin);
|
|
2065
2696
|
const cron = createCron(transport, options.cron);
|
|
2697
|
+
const backups = createBackups(transport);
|
|
2066
2698
|
const apiKeys = createApiKeys(transport, options.apiKeys);
|
|
2067
2699
|
const storage = createStorage(transport);
|
|
2068
2700
|
const functions = createFunctionsClient(transport);
|
|
@@ -2083,8 +2715,10 @@ function createRebaseClient(options) {
|
|
|
2083
2715
|
});
|
|
2084
2716
|
return storageSourcesPromise;
|
|
2085
2717
|
};
|
|
2086
|
-
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
2718
|
+
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
2087
2719
|
let ws;
|
|
2720
|
+
/** One channel object per name — see `realtime.channel`. */
|
|
2721
|
+
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
2088
2722
|
if (resolvedWsUrl) {
|
|
2089
2723
|
ws = new RebaseWebSocketClient({
|
|
2090
2724
|
websocketUrl: resolvedWsUrl,
|
|
@@ -2120,7 +2754,45 @@ function createRebaseClient(options) {
|
|
|
2120
2754
|
return false;
|
|
2121
2755
|
}
|
|
2122
2756
|
});
|
|
2757
|
+
/**
|
|
2758
|
+
* Suggest the closest known collection key for a mistyped accessor.
|
|
2759
|
+
* Uses edit-distance-1 and prefix matching — no external dependency.
|
|
2760
|
+
*/
|
|
2761
|
+
function suggestCollection(prop, knownKeys) {
|
|
2762
|
+
const prefixMatch = knownKeys.find((k) => k.startsWith(prop) || prop.startsWith(k));
|
|
2763
|
+
if (prefixMatch) return prefixMatch;
|
|
2764
|
+
for (const key of knownKeys) {
|
|
2765
|
+
if (Math.abs(key.length - prop.length) > 1) continue;
|
|
2766
|
+
let diffs = 0;
|
|
2767
|
+
const longer = key.length >= prop.length ? key : prop;
|
|
2768
|
+
const shorter = key.length >= prop.length ? prop : key;
|
|
2769
|
+
if (longer.length === shorter.length) for (let i = 0; i < longer.length; i++) {
|
|
2770
|
+
if (longer[i] !== shorter[i]) {
|
|
2771
|
+
if (i + 1 < longer.length && longer[i] === shorter[i + 1] && longer[i + 1] === shorter[i]) {
|
|
2772
|
+
diffs++;
|
|
2773
|
+
i++;
|
|
2774
|
+
if (diffs > 1) break;
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
diffs++;
|
|
2778
|
+
}
|
|
2779
|
+
if (diffs > 1) break;
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
let li = 0;
|
|
2783
|
+
let si = 0;
|
|
2784
|
+
while (li < longer.length) {
|
|
2785
|
+
if (si < shorter.length && longer[li] === shorter[si]) si++;
|
|
2786
|
+
else diffs++;
|
|
2787
|
+
li++;
|
|
2788
|
+
if (diffs > 1) break;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (diffs <= 1) return key;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2123
2794
|
const collectionClients = /* @__PURE__ */ new Map();
|
|
2795
|
+
let untypedWarned = false;
|
|
2124
2796
|
function collection(slug) {
|
|
2125
2797
|
if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
|
|
2126
2798
|
return collectionClients.get(slug);
|
|
@@ -2129,7 +2801,19 @@ function createRebaseClient(options) {
|
|
|
2129
2801
|
if (prop === "collection") return collection;
|
|
2130
2802
|
if (typeof prop === "symbol") return void 0;
|
|
2131
2803
|
if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
|
|
2132
|
-
if (options.collections
|
|
2804
|
+
if (options.collections) {
|
|
2805
|
+
if (prop in options.collections) return collection(options.collections[prop]);
|
|
2806
|
+
const knownKeys = Object.keys(options.collections);
|
|
2807
|
+
const suggestion = suggestCollection(prop, knownKeys);
|
|
2808
|
+
let msg = `Unknown collection accessor "${prop}". Known collections: ${knownKeys.join(", ")}.`;
|
|
2809
|
+
if (suggestion) msg += ` Did you mean "${suggestion}"?`;
|
|
2810
|
+
msg += ` Use data.collection("<slug>") for dynamic slugs.`;
|
|
2811
|
+
throw new RebaseClientError(msg);
|
|
2812
|
+
}
|
|
2813
|
+
if (!untypedWarned) {
|
|
2814
|
+
untypedWarned = true;
|
|
2815
|
+
console.warn(`[Rebase] Untyped data access detected (client.data.${prop}). Collection names are resolved via snake_case conversion, which may cause silent 404s at request time. Pass a \`collections\` dictionary to createRebaseClient() or use the generated SDK for type-safe access.`);
|
|
2816
|
+
}
|
|
2133
2817
|
return collection(toSnakeCase(prop));
|
|
2134
2818
|
}
|
|
2135
2819
|
} });
|
|
@@ -2137,6 +2821,7 @@ function createRebaseClient(options) {
|
|
|
2137
2821
|
auth,
|
|
2138
2822
|
admin,
|
|
2139
2823
|
cron,
|
|
2824
|
+
backups,
|
|
2140
2825
|
apiKeys,
|
|
2141
2826
|
functions,
|
|
2142
2827
|
storage,
|
|
@@ -2144,6 +2829,36 @@ function createRebaseClient(options) {
|
|
|
2144
2829
|
createStorageSource,
|
|
2145
2830
|
fetchStorageSources,
|
|
2146
2831
|
ws,
|
|
2832
|
+
realtime: {
|
|
2833
|
+
/**
|
|
2834
|
+
* Join a broadcast/presence channel.
|
|
2835
|
+
*
|
|
2836
|
+
* Repeated calls with the same name return the same channel, so
|
|
2837
|
+
* separate components can attach handlers without each opening its
|
|
2838
|
+
* own membership — and `leave()` from one would otherwise silently
|
|
2839
|
+
* cut off the others.
|
|
2840
|
+
*/
|
|
2841
|
+
channel: (name) => {
|
|
2842
|
+
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
2843
|
+
let existing = realtimeChannels.get(name);
|
|
2844
|
+
if (!existing) {
|
|
2845
|
+
existing = new RebaseRealtimeChannel(name, ws);
|
|
2846
|
+
realtimeChannels.set(name, existing);
|
|
2847
|
+
}
|
|
2848
|
+
return existing;
|
|
2849
|
+
} },
|
|
2850
|
+
/**
|
|
2851
|
+
* Release the realtime socket and its reconnect timer.
|
|
2852
|
+
*
|
|
2853
|
+
* Until this returns, the open socket keeps the Node event loop alive
|
|
2854
|
+
* and the process will not exit on its own. Safe to call when realtime
|
|
2855
|
+
* was never started, and safe to call twice.
|
|
2856
|
+
*/
|
|
2857
|
+
close: () => {
|
|
2858
|
+
for (const channel of realtimeChannels.values()) channel.leave();
|
|
2859
|
+
realtimeChannels.clear();
|
|
2860
|
+
ws?.disconnect();
|
|
2861
|
+
},
|
|
2147
2862
|
setToken: transport.setToken,
|
|
2148
2863
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
2149
2864
|
setOnUnauthorized: transport.setOnUnauthorized,
|
|
@@ -2158,11 +2873,10 @@ function createRebaseClient(options) {
|
|
|
2158
2873
|
});
|
|
2159
2874
|
return res.data ?? res;
|
|
2160
2875
|
},
|
|
2161
|
-
data: dataProxy
|
|
2162
|
-
email: void 0
|
|
2876
|
+
data: dataProxy
|
|
2163
2877
|
};
|
|
2164
2878
|
}
|
|
2165
2879
|
//#endregion
|
|
2166
|
-
export {
|
|
2880
|
+
export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
|
|
2167
2881
|
|
|
2168
2882
|
//# sourceMappingURL=index.es.js.map
|