@rebasepro/client 0.13.0 → 0.13.1-canary.g3660bd5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.es.js +93 -58
- package/dist/index.es.js.map +1 -1
- package/dist/offline-query.d.ts +18 -3
- package/package.json +4 -4
- package/src/auth-listener-errors.test.ts +57 -0
- package/src/auth.ts +10 -1
- package/src/collection-listen-meta.test.ts +105 -0
- package/src/collection-observe.test.ts +138 -0
- package/src/collection.ts +88 -46
- package/src/index.ts +48 -15
- package/src/offline-query.test.ts +28 -2
- package/src/offline-query.ts +20 -7
- package/src/offline.ts +6 -4
- package/src/realtime-optout.test.ts +15 -15
- package/src/realtime-subscription-key.test.ts +92 -0
- package/src/transport-baseurl.test.ts +49 -1
- package/src/transport.ts +22 -0
- package/src/websocket-url.test.ts +97 -0
- package/src/websocket.ts +18 -9
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, Vector, isPublicStoragePath, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
-
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, or, paginateFind, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
2
|
+
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, or, paginateFind, resolveFindWindow, 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) {
|
|
@@ -131,6 +131,13 @@ function resolveBaseUrl(configured) {
|
|
|
131
131
|
function createTransport(config, environment) {
|
|
132
132
|
const fetchFn = config.fetch || globalThis.fetch;
|
|
133
133
|
const apiPath = config.apiPath || "/api";
|
|
134
|
+
for (const field of ["baseUrl", "storageUrlOrigin"]) {
|
|
135
|
+
const value = config[field];
|
|
136
|
+
if (!value || !apiPath) continue;
|
|
137
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
138
|
+
if (!trimmed.endsWith(apiPath)) continue;
|
|
139
|
+
console.warn(`[Rebase] ${field} ${JSON.stringify(value)} already ends with the API path ${JSON.stringify(apiPath)}, which is appended to it — requests will go to ${trimmed}${apiPath}/… and 404. Pass the origin only (${JSON.stringify(trimmed.slice(0, trimmed.length - apiPath.length) || "/")}), or set \`apiPath\` if the server really does mount the API one level deeper.`);
|
|
140
|
+
}
|
|
134
141
|
let token = config.token;
|
|
135
142
|
let tokenGetter;
|
|
136
143
|
let onUnauthorizedHandler = config.onUnauthorized;
|
|
@@ -343,7 +350,9 @@ function createAuth(transport, options) {
|
|
|
343
350
|
function emit(event, session) {
|
|
344
351
|
for (const fn of listeners) try {
|
|
345
352
|
fn(event, session);
|
|
346
|
-
} catch (e) {
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.error("Error in auth state change listener:", e);
|
|
355
|
+
}
|
|
347
356
|
}
|
|
348
357
|
function saveSession(session) {
|
|
349
358
|
if (!persistSession || authFlowMode === "cookie") return;
|
|
@@ -1337,8 +1346,15 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1337
1346
|
},
|
|
1338
1347
|
observe(params, onResult, onError, options) {
|
|
1339
1348
|
let closed = false;
|
|
1340
|
-
|
|
1349
|
+
let liveDelivered = false;
|
|
1350
|
+
let signature;
|
|
1351
|
+
const deliver = (result, fromLive) => {
|
|
1341
1352
|
if (closed) return;
|
|
1353
|
+
if (fromLive) liveDelivered = true;
|
|
1354
|
+
else if (liveDelivered) return;
|
|
1355
|
+
const next = `${result.meta?.total ?? ""}|${JSON.stringify(result.data)}`;
|
|
1356
|
+
if (signature !== void 0 && next === signature) return;
|
|
1357
|
+
signature = next;
|
|
1342
1358
|
onResult({
|
|
1343
1359
|
...result,
|
|
1344
1360
|
fromCache: false,
|
|
@@ -1346,10 +1362,10 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1346
1362
|
partial: false
|
|
1347
1363
|
});
|
|
1348
1364
|
};
|
|
1349
|
-
client.find(params).then(
|
|
1365
|
+
client.find(params).then((result) => deliver(result, false)).catch((error) => {
|
|
1350
1366
|
if (!closed) onError?.(error);
|
|
1351
1367
|
});
|
|
1352
|
-
const live = options?.realtime !== false && client.listen ? client.listen(params,
|
|
1368
|
+
const live = options?.realtime !== false && client.listen ? client.listen(params, (result) => deliver(result, true), onError) : void 0;
|
|
1353
1369
|
return () => {
|
|
1354
1370
|
closed = true;
|
|
1355
1371
|
live?.();
|
|
@@ -1357,17 +1373,24 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1357
1373
|
},
|
|
1358
1374
|
observeById(id, onResult, onError, options) {
|
|
1359
1375
|
let closed = false;
|
|
1360
|
-
|
|
1376
|
+
let liveDelivered = false;
|
|
1377
|
+
let signature;
|
|
1378
|
+
const deliver = (row, fromLive) => {
|
|
1361
1379
|
if (closed) return;
|
|
1380
|
+
if (fromLive) liveDelivered = true;
|
|
1381
|
+
else if (liveDelivered) return;
|
|
1382
|
+
const next = row === void 0 ? "\0missing" : JSON.stringify(row);
|
|
1383
|
+
if (signature !== void 0 && next === signature) return;
|
|
1384
|
+
signature = next;
|
|
1362
1385
|
onResult(row, {
|
|
1363
1386
|
fromCache: false,
|
|
1364
1387
|
hasPendingWrites: false
|
|
1365
1388
|
});
|
|
1366
1389
|
};
|
|
1367
|
-
client.findById(id).then(
|
|
1390
|
+
client.findById(id).then((row) => deliver(row, false)).catch((error) => {
|
|
1368
1391
|
if (!closed) onError?.(error);
|
|
1369
1392
|
});
|
|
1370
|
-
const live = options?.realtime !== false && client.listenById ? client.listenById(id,
|
|
1393
|
+
const live = options?.realtime !== false && client.listenById ? client.listenById(id, (row) => deliver(row, true), onError) : void 0;
|
|
1371
1394
|
return () => {
|
|
1372
1395
|
closed = true;
|
|
1373
1396
|
live?.();
|
|
@@ -1398,51 +1421,43 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1398
1421
|
client.listen = (params, onUpdate, onError) => {
|
|
1399
1422
|
let active = true;
|
|
1400
1423
|
let lastUpdateId = 0;
|
|
1424
|
+
let lastKnownTotal;
|
|
1425
|
+
const window = resolveFindWindow(params);
|
|
1401
1426
|
const unsub = ws.listenCollection({
|
|
1402
1427
|
path: slug,
|
|
1403
1428
|
filter: params?.where,
|
|
1429
|
+
logical: params?.logical,
|
|
1404
1430
|
limit: params?.limit,
|
|
1405
|
-
|
|
1431
|
+
offset: window.driverOffset,
|
|
1406
1432
|
orderBy: params?.orderBy?.[0],
|
|
1407
1433
|
order: params?.orderBy?.[1],
|
|
1408
1434
|
searchString: params?.searchString
|
|
1409
1435
|
}, (incomingRows) => {
|
|
1410
1436
|
const currentUpdateId = ++lastUpdateId;
|
|
1411
|
-
const requestedLimit =
|
|
1412
|
-
const offset =
|
|
1437
|
+
const requestedLimit = window.limit;
|
|
1438
|
+
const offset = window.offset;
|
|
1413
1439
|
const rows = incomingRows;
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
1440
|
+
const emit = (total, hasMore) => {
|
|
1441
|
+
if (!active || currentUpdateId !== lastUpdateId) return;
|
|
1442
|
+
onUpdate({
|
|
1418
1443
|
data: rows,
|
|
1419
1444
|
meta: {
|
|
1420
1445
|
total,
|
|
1421
1446
|
limit: requestedLimit,
|
|
1422
1447
|
offset,
|
|
1423
|
-
hasMore
|
|
1448
|
+
hasMore
|
|
1424
1449
|
}
|
|
1425
1450
|
});
|
|
1451
|
+
};
|
|
1452
|
+
const emitWithoutCount = () => emit(offset + rows.length, rows.length >= requestedLimit);
|
|
1453
|
+
if (client.count) client.count(params).then((total) => {
|
|
1454
|
+
lastKnownTotal = total;
|
|
1455
|
+
emit(total, offset + rows.length < total);
|
|
1426
1456
|
}).catch(() => {
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
meta: {
|
|
1430
|
-
total: heuristicTotal,
|
|
1431
|
-
limit: requestedLimit,
|
|
1432
|
-
offset,
|
|
1433
|
-
hasMore: heuristicHasMore
|
|
1434
|
-
}
|
|
1435
|
-
});
|
|
1436
|
-
});
|
|
1437
|
-
else onUpdate({
|
|
1438
|
-
data: rows,
|
|
1439
|
-
meta: {
|
|
1440
|
-
total: heuristicTotal,
|
|
1441
|
-
limit: requestedLimit,
|
|
1442
|
-
offset,
|
|
1443
|
-
hasMore: heuristicHasMore
|
|
1444
|
-
}
|
|
1457
|
+
if (lastKnownTotal !== void 0) emit(lastKnownTotal, offset + rows.length < lastKnownTotal);
|
|
1458
|
+
else emitWithoutCount();
|
|
1445
1459
|
});
|
|
1460
|
+
else emitWithoutCount();
|
|
1446
1461
|
}, onError);
|
|
1447
1462
|
return () => {
|
|
1448
1463
|
active = false;
|
|
@@ -2833,15 +2848,10 @@ var RebaseWebSocketClient = class {
|
|
|
2833
2848
|
}
|
|
2834
2849
|
}
|
|
2835
2850
|
createCollectionSubscriptionKey(props) {
|
|
2851
|
+
const { collection, ...query } = props;
|
|
2836
2852
|
const key = {
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
limit: props.limit,
|
|
2840
|
-
startAfter: props.startAfter,
|
|
2841
|
-
orderBy: props.orderBy,
|
|
2842
|
-
order: props.order,
|
|
2843
|
-
searchString: props.searchString,
|
|
2844
|
-
collection: props.collection?.name
|
|
2853
|
+
...query,
|
|
2854
|
+
collection: collection?.name
|
|
2845
2855
|
};
|
|
2846
2856
|
return JSON.stringify(key, (_, value) => {
|
|
2847
2857
|
if (value && typeof value === "object" && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, k) => {
|
|
@@ -3903,12 +3913,20 @@ function sortRows(rows, orderBy) {
|
|
|
3903
3913
|
function tiebreak(a, b) {
|
|
3904
3914
|
return compareValues(a.id, b.id) ?? 0;
|
|
3905
3915
|
}
|
|
3906
|
-
/**
|
|
3916
|
+
/**
|
|
3917
|
+
* Resolve `page`/`offset`/`limit` the way the server does.
|
|
3918
|
+
*
|
|
3919
|
+
* It did not: this defaulted an absent limit to 20 while `/api/data` pages by
|
|
3920
|
+
* 50, so the same `observe()` answered with 20 rows from the local database and
|
|
3921
|
+
* 50 from the network — a list that changed length depending on which side
|
|
3922
|
+
* answered, with `page` striding differently on each. Delegated now, so the
|
|
3923
|
+
* sentence above is true by construction rather than by agreement.
|
|
3924
|
+
*/
|
|
3907
3925
|
function resolvePagination(params) {
|
|
3908
|
-
const limit = params
|
|
3926
|
+
const { limit, offset } = resolveFindWindow(params);
|
|
3909
3927
|
return {
|
|
3910
3928
|
limit,
|
|
3911
|
-
offset
|
|
3929
|
+
offset
|
|
3912
3930
|
};
|
|
3913
3931
|
}
|
|
3914
3932
|
/**
|
|
@@ -4536,9 +4554,8 @@ var OfflineManager = class {
|
|
|
4536
4554
|
if (!state) return {
|
|
4537
4555
|
data: [],
|
|
4538
4556
|
meta: {
|
|
4557
|
+
...resolvePagination(params),
|
|
4539
4558
|
total: 0,
|
|
4540
|
-
limit: params?.limit ?? 20,
|
|
4541
|
-
offset: params?.offset ?? 0,
|
|
4542
4559
|
hasMore: false
|
|
4543
4560
|
},
|
|
4544
4561
|
fromCache: true,
|
|
@@ -4730,17 +4747,17 @@ var OfflineManager = class {
|
|
|
4730
4747
|
return row;
|
|
4731
4748
|
}
|
|
4732
4749
|
recordSnapshot(slug, params, result) {
|
|
4750
|
+
const window = resolvePagination(params);
|
|
4733
4751
|
const meta = result.meta ?? {
|
|
4734
4752
|
total: result.data?.length ?? 0,
|
|
4735
|
-
|
|
4736
|
-
offset: 0,
|
|
4753
|
+
...window,
|
|
4737
4754
|
hasMore: false
|
|
4738
4755
|
};
|
|
4739
4756
|
const snapshot = {
|
|
4740
4757
|
ids: (result.data ?? []).map((row) => row.id).filter((id) => id !== void 0),
|
|
4741
4758
|
total: meta.total ?? result.data?.length ?? 0,
|
|
4742
|
-
limit: meta.limit ??
|
|
4743
|
-
offset: meta.offset ??
|
|
4759
|
+
limit: meta.limit ?? window.limit,
|
|
4760
|
+
offset: meta.offset ?? window.offset,
|
|
4744
4761
|
hasMore: meta.hasMore ?? false
|
|
4745
4762
|
};
|
|
4746
4763
|
const state = this.collectionState(slug);
|
|
@@ -5204,23 +5221,37 @@ var OfflineManager = class {
|
|
|
5204
5221
|
/**
|
|
5205
5222
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
5206
5223
|
* `http://` → `ws://`, `https://` → `wss://`.
|
|
5224
|
+
*
|
|
5225
|
+
* A backend mounted under a path is the reason `baseUrl` accepts one, so the
|
|
5226
|
+
* path is kept. It used to be kept for an absolute `baseUrl` and dropped for a
|
|
5227
|
+
* relative one — resolved through `.origin` — so one deployment dialled two
|
|
5228
|
+
* different sockets depending on whether its config said `"/backend"` or
|
|
5229
|
+
* `"https://app.example.com/backend"`.
|
|
5230
|
+
*
|
|
5231
|
+
* Returns `""` when there is nothing to resolve against: a relative `baseUrl`
|
|
5232
|
+
* outside a browser has no origin, and inventing one would dial somewhere
|
|
5233
|
+
* arbitrary. The caller warns rather than leaving that silent.
|
|
5207
5234
|
*/
|
|
5208
5235
|
function deriveWebSocketUrl(baseUrl) {
|
|
5236
|
+
const toWsProtocol = (url) => {
|
|
5237
|
+
const secure = /^(https|wss):/i.test(url);
|
|
5238
|
+
return url.replace(/^https?:\/\//i, secure ? "wss://" : "ws://").replace(/^wss?:\/\//i, secure ? "wss://" : "ws://").replace(/\/$/, "");
|
|
5239
|
+
};
|
|
5209
5240
|
if (typeof window !== "undefined") {
|
|
5210
|
-
let absoluteUrl
|
|
5241
|
+
let absoluteUrl;
|
|
5211
5242
|
if (!baseUrl) absoluteUrl = window.location.origin;
|
|
5212
5243
|
else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) absoluteUrl = baseUrl;
|
|
5213
5244
|
else try {
|
|
5214
|
-
|
|
5245
|
+
const resolved = new URL(baseUrl, window.location.href);
|
|
5246
|
+
absoluteUrl = resolved.origin + resolved.pathname;
|
|
5215
5247
|
} catch {
|
|
5216
5248
|
absoluteUrl = window.location.origin;
|
|
5217
5249
|
}
|
|
5218
|
-
|
|
5219
|
-
return absoluteUrl.replace(/^https?:\/\//i, `${protocol}//`).replace(/^wss?:\/\//i, `${protocol}//`).replace(/\/$/, "");
|
|
5250
|
+
return toWsProtocol(absoluteUrl);
|
|
5220
5251
|
}
|
|
5221
5252
|
if (!baseUrl) return "";
|
|
5222
5253
|
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) return "";
|
|
5223
|
-
return baseUrl
|
|
5254
|
+
return toWsProtocol(baseUrl);
|
|
5224
5255
|
}
|
|
5225
5256
|
function createRebaseClient(options) {
|
|
5226
5257
|
const transport = createTransport(options, { credentialOutOfBand: options.auth?.authFlowMode === "cookie" });
|
|
@@ -5248,7 +5279,11 @@ function createRebaseClient(options) {
|
|
|
5248
5279
|
});
|
|
5249
5280
|
return storageSourcesPromise;
|
|
5250
5281
|
};
|
|
5251
|
-
const
|
|
5282
|
+
const realtimeEnabled = options.realtime !== false;
|
|
5283
|
+
const resolvedWsUrl = realtimeEnabled ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
5284
|
+
const realtimeUnreachable = realtimeEnabled && !resolvedWsUrl;
|
|
5285
|
+
const unreachableReason = `no WebSocket URL could be derived from baseUrl ${JSON.stringify(options.baseUrl ?? null)} — outside a browser there is no page origin to resolve a relative URL against. Pass an absolute \`baseUrl\`, set \`websocketUrl\` explicitly, or pass \`realtime: false\` to say this was intended.`;
|
|
5286
|
+
if (realtimeUnreachable) console.warn(`[Rebase] Realtime is enabled but ${unreachableReason} Live queries will fall back to a single fetch and channels will throw.`);
|
|
5252
5287
|
let ws;
|
|
5253
5288
|
/** One channel object per name — see `realtime.channel`. */
|
|
5254
5289
|
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
@@ -5368,7 +5403,7 @@ function createRebaseClient(options) {
|
|
|
5368
5403
|
* cut off the others.
|
|
5369
5404
|
*/
|
|
5370
5405
|
channel: (name, options) => {
|
|
5371
|
-
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
5406
|
+
if (!ws) throw new RebaseClientError(realtimeUnreachable ? `Realtime is enabled but ${unreachableReason}` : "Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
5372
5407
|
let existing = realtimeChannels.get(name);
|
|
5373
5408
|
if (!existing) {
|
|
5374
5409
|
existing = new RebaseRealtimeChannel(name, ws, options);
|