@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 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
- const emit = (result) => {
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(emit).catch((error) => {
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, emit, onError) : void 0;
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
- const emit = (row) => {
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(emit).catch((error) => {
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, emit, onError) : void 0;
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
- startAfter: params?.offset ? String(params.offset) : void 0,
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 = params?.limit || 20;
1412
- const offset = params?.offset || 0;
1437
+ const requestedLimit = window.limit;
1438
+ const offset = window.offset;
1413
1439
  const rows = incomingRows;
1414
- const heuristicTotal = rows.length;
1415
- const heuristicHasMore = rows.length >= requestedLimit;
1416
- if (client.count) client.count(params).then((total) => {
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: offset + rows.length < total
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 (active && currentUpdateId === lastUpdateId) onUpdate({
1428
- data: rows,
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
- path: props.path,
2838
- filter: props.filter,
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
- /** Resolve `page`/`offset`/`limit` the way the server does. */
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?.limit ?? 20;
3926
+ const { limit, offset } = resolveFindWindow(params);
3909
3927
  return {
3910
3928
  limit,
3911
- offset: params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0
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
- limit: 20,
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 ?? params?.limit ?? 20,
4743
- offset: meta.offset ?? params?.offset ?? 0,
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
- absoluteUrl = new URL(baseUrl, window.location.href).origin;
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
- const protocol = absoluteUrl.startsWith("https:") || absoluteUrl.startsWith("wss:") ? "wss:" : "ws:";
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.replace(/^https?:\/\//i, (match) => match.toLowerCase() === "https://" ? "wss://" : "ws://").replace(/\/$/, "");
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 resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
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);