@rebasepro/client 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, SCHEMA_VERSION_HEADER, Vector, hasFieldOperation, isPublicStoragePath, isUnsupported, parseRelationAggregateSort, sortKeyToString, toCanonicalOp, unsupportedMethod } from "@rebasepro/types";
2
2
  import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, mergeIncludeSpecs, normalizeOrderBy, not, or, paginateFind, resolveFindWindow, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
3
- import { toSnakeCase } from "@rebasepro/utils";
3
+ import { toSnakeCase, unref } from "@rebasepro/utils";
4
4
  //#region src/reviver.ts
5
5
  function rebaseReviver(_key, value) {
6
6
  if (value && typeof value === "object" && "__type" in value) {
@@ -2490,6 +2490,23 @@ var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
2490
2490
  "channel_history"
2491
2491
  ]);
2492
2492
  /**
2493
+ * The other direction: the frame types the *server* addresses by channel.
2494
+ *
2495
+ * Not the mirror of {@link CHANNEL_MESSAGE_TYPES} — a request and its answer
2496
+ * have different names (`presence_state` is the one type in both) — so the two
2497
+ * sets are listed separately rather than derived from each other. These are
2498
+ * exactly the members of `ChannelMessage`; adding one here without declaring it
2499
+ * there leaves the handler with a frame it cannot read.
2500
+ */
2501
+ var CHANNEL_FRAME_TYPES = /* @__PURE__ */ new Set([
2502
+ "broadcast",
2503
+ "presence_state",
2504
+ "presence_diff",
2505
+ "channel_history",
2506
+ "error",
2507
+ "ERROR"
2508
+ ]);
2509
+ /**
2493
2510
  * Low-level realtime WebSocket client.
2494
2511
  *
2495
2512
  * @internal Not a stable app-facing API. `createRebaseClient()` constructs and
@@ -2500,6 +2517,28 @@ var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
2500
2517
  * `@rebasepro/client-postgres` was removed; its surface may change without a
2501
2518
  * major bump.
2502
2519
  */
2520
+ /**
2521
+ * Narrow a frame to the collection-update shape. See its use below.
2522
+ *
2523
+ * `WebSocketMessage.type` is `string`, so the message types that extend it do
2524
+ * not form a discriminated union and `type === "collection_update"` narrows
2525
+ * nothing on its own.
2526
+ */
2527
+ function isCollectionUpdate(message) {
2528
+ return message.type === "collection_update";
2529
+ }
2530
+ /**
2531
+ * Narrow a frame to one of the channel-addressed shapes.
2532
+ *
2533
+ * Both halves of the test are load-bearing. The type says which member of
2534
+ * {@link ChannelMessage} it is; `channel` is what a channel frame is *addressed
2535
+ * by*, and an `ERROR` from a server older than the change that names the
2536
+ * channel has none — it is a generic error, and falls through to the catch-all
2537
+ * warning rather than being delivered to a channel that was never named.
2538
+ */
2539
+ function isChannelMessage(message) {
2540
+ return typeof message.channel === "string" && CHANNEL_FRAME_TYPES.has(message.type);
2541
+ }
2503
2542
  var RebaseWebSocketClient = class {
2504
2543
  websocketUrl;
2505
2544
  ws = null;
@@ -2683,6 +2722,7 @@ var RebaseWebSocketClient = class {
2683
2722
  clearTimeout(this.reconnectTimeout);
2684
2723
  this.reconnectTimeout = null;
2685
2724
  }
2725
+ this.suspendSubscribeWatchdogs();
2686
2726
  if (this.ws) {
2687
2727
  this.ws.onclose = null;
2688
2728
  this.ws.onerror = null;
@@ -2864,7 +2904,7 @@ var RebaseWebSocketClient = class {
2864
2904
  }
2865
2905
  return;
2866
2906
  }
2867
- if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff" || type === "channel_history" || type === "ERROR" || type === "error")) {
2907
+ if (isChannelMessage(message)) {
2868
2908
  const handlers = this.channelHandlers.get(message.channel);
2869
2909
  if (handlers) for (const handler of [...handlers]) try {
2870
2910
  handler(message);
@@ -2873,15 +2913,16 @@ var RebaseWebSocketClient = class {
2873
2913
  }
2874
2914
  return;
2875
2915
  }
2876
- if (subscriptionId && type === "collection_update") {
2916
+ if (subscriptionId && isCollectionUpdate(message)) {
2917
+ const collectionUpdate = message;
2877
2918
  const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
2878
2919
  if (subscriptionKey) {
2879
2920
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
2880
2921
  if (collectionSub) {
2881
2922
  const incomingRows = message.rows || [];
2882
- const updatePks = message.pks;
2923
+ const updatePks = collectionUpdate.pks;
2883
2924
  if (updatePks) collectionSub.pks = updatePks;
2884
- const updateMeta = message.meta;
2925
+ const updateMeta = collectionUpdate.meta;
2885
2926
  if (updateMeta) collectionSub.latestMeta = updateMeta;
2886
2927
  const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
2887
2928
  collectionSub.latestData = rows;
@@ -3747,7 +3788,7 @@ var RebaseRealtimeChannel = class {
3747
3788
  this.catchUpInFlight = true;
3748
3789
  if (this.catchUpTimeout) clearTimeout(this.catchUpTimeout);
3749
3790
  this.catchUpTimeout = setTimeout(() => this.abandonCatchUp(), CATCH_UP_TIMEOUT_MS);
3750
- this.catchUpTimeout.unref?.();
3791
+ unref(this.catchUpTimeout);
3751
3792
  try {
3752
3793
  await this.send("channel_history", {
3753
3794
  sinceSeq: this.lastSeq,
@@ -3793,7 +3834,7 @@ var RebaseRealtimeChannel = class {
3793
3834
  if (!this.trackedState) return;
3794
3835
  this.send("presence_track", { state: this.trackedState }).catch(() => {});
3795
3836
  }, PRESENCE_HEARTBEAT_MS);
3796
- this.heartbeat.unref?.();
3837
+ unref(this.heartbeat);
3797
3838
  }
3798
3839
  }
3799
3840
  /** Stop publishing presence, without leaving the channel. */
@@ -3921,12 +3962,11 @@ var RebaseRealtimeChannel = class {
3921
3962
  handle(message) {
3922
3963
  switch (message.type) {
3923
3964
  case "presence_state":
3924
- this.presences = message.presences ?? {};
3965
+ this.presences = message.presences;
3925
3966
  this.emitPresence();
3926
3967
  break;
3927
3968
  case "presence_diff": {
3928
- const joins = message.joins ?? {};
3929
- const leaves = message.leaves ?? {};
3969
+ const { joins, leaves } = message;
3930
3970
  for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
3931
3971
  for (const id of Object.keys(leaves)) delete this.presences[id];
3932
3972
  this.emitPresence({
@@ -3936,7 +3976,7 @@ var RebaseRealtimeChannel = class {
3936
3976
  break;
3937
3977
  }
3938
3978
  case "broadcast": {
3939
- const seq = typeof message.seq === "number" ? message.seq : void 0;
3979
+ const seq = message.seq;
3940
3980
  const event = {
3941
3981
  event: message.event,
3942
3982
  payload: message.payload,
@@ -3961,9 +4001,7 @@ var RebaseRealtimeChannel = class {
3961
4001
  clearTimeout(this.catchUpTimeout);
3962
4002
  this.catchUpTimeout = null;
3963
4003
  }
3964
- const entries = message.messages ?? [];
3965
- const retained = message.retained === true;
3966
- const latestSeq = typeof message.latestSeq === "number" ? message.latestSeq : void 0;
4004
+ const { messages: entries, retained, latestSeq } = message;
3967
4005
  for (const resolve of this.historyWaiters.splice(0)) resolve({
3968
4006
  messages: entries,
3969
4007
  retained,
@@ -3985,8 +4023,8 @@ var RebaseRealtimeChannel = class {
3985
4023
  case "ERROR":
3986
4024
  case "error": {
3987
4025
  const raw = message.payload?.error;
3988
- const asObject = typeof raw === "object" && raw !== null ? raw : void 0;
3989
- const text = asObject?.message ?? (typeof raw === "string" ? raw : void 0) ?? (typeof message.error === "string" ? message.error : void 0) ?? "The server refused a channel operation.";
4026
+ const asObject = typeof raw === "object" ? raw : void 0;
4027
+ const text = asObject?.message ?? (typeof raw === "string" ? raw : void 0) ?? message.error ?? "The server refused a channel operation.";
3990
4028
  const error = new RebaseApiError$1(text, { ...asObject?.code ? { code: asObject.code } : {} });
3991
4029
  if (this.errorHandlers.size === 0) {
3992
4030
  console.warn(`[Rebase] Channel "${this.name}" error${asObject?.code ? ` (${asObject.code})` : ""}: ${text}. Attach \`channel.onError(...)\` to handle it.`);
@@ -4285,7 +4323,7 @@ var ConnectivityMonitor = class {
4285
4323
  this.timer = void 0;
4286
4324
  this.onRetryDue?.();
4287
4325
  }, delay);
4288
- this.timer.unref?.();
4326
+ unref(this.timer);
4289
4327
  }
4290
4328
  clearPendingTimer() {
4291
4329
  if (this.timer !== void 0) {
@@ -4948,6 +4986,31 @@ function generateOfflineNumericId() {
4948
4986
  lastOfflineNumericId = lastOfflineNumericId === 0 ? candidate : Math.min(candidate, lastOfflineNumericId - 1);
4949
4987
  return lastOfflineNumericId;
4950
4988
  }
4989
+ /**
4990
+ * The row an optimistic write puts in the cache before the server has seen it.
4991
+ *
4992
+ * It is NOT an `M`, and saying so is the point of collecting this in one place.
4993
+ * `M` is the row as the database has it — every server default, every generated
4994
+ * column, every `afterRead` transform. What an optimistic write has is the
4995
+ * fields the caller supplied (plus, on an update, whatever was already cached)
4996
+ * and an id, which may itself be a locally-minted placeholder.
4997
+ *
4998
+ * The two are reconciled when the queued write drains and the server's row
4999
+ * replaces this one. Until then the SDK hands the caller this, under `M`,
5000
+ * because that is what the offline API's return type promises — and there is
5001
+ * nothing in this file that can make that promise true.
5002
+ *
5003
+ * So: one deliberate assertion, named, rather than five spread across the
5004
+ * create / createMany / update / upsert paths, each free to assemble the fields
5005
+ * differently. Fixing it properly means the offline surface saying what it
5006
+ * actually returns, which is a change to its public types.
5007
+ */
5008
+ function optimisticRow(fields, id) {
5009
+ return {
5010
+ ...fields,
5011
+ id
5012
+ };
5013
+ }
4951
5014
  var MISSING = "\0missing";
4952
5015
  /**
4953
5016
  * Replays to spend on a mutation whose idempotency key the server is still
@@ -5034,7 +5097,7 @@ var OfflineManager = class {
5034
5097
  if ((config.crossTab ?? this.store instanceof IndexedDBOfflineStore) && typeof BroadcastChannel !== "undefined") try {
5035
5098
  this.channel = new BroadcastChannel("rebase-offline");
5036
5099
  this.channel.onmessage = (event) => this.onBroadcast(event.data);
5037
- this.channel.unref?.();
5100
+ unref(this.channel);
5038
5101
  } catch {}
5039
5102
  this.api = {
5040
5103
  sync: () => this.sync(),
@@ -5197,10 +5260,7 @@ var OfflineManager = class {
5197
5260
  }
5198
5261
  const providedId = id ?? data.id;
5199
5262
  const rowId = providedId ?? this.mintOfflineId(slug);
5200
- const row = {
5201
- ...data,
5202
- id: rowId
5203
- };
5263
+ const row = optimisticRow(data, rowId);
5204
5264
  await this.enqueue({
5205
5265
  collection: slug,
5206
5266
  type: "create",
@@ -5228,10 +5288,7 @@ var OfflineManager = class {
5228
5288
  if (!isNetworkError(error)) throw error;
5229
5289
  this.connectivity.markFailure();
5230
5290
  }
5231
- const rows = data.map((r) => ({
5232
- ...r,
5233
- id: r.id ?? this.mintOfflineId(slug)
5234
- }));
5291
+ const rows = data.map((r) => optimisticRow(r, r.id ?? this.mintOfflineId(slug)));
5235
5292
  const rollback = {};
5236
5293
  for (const row of rows) {
5237
5294
  const key = String(row.id);
@@ -5274,10 +5331,7 @@ var OfflineManager = class {
5274
5331
  }
5275
5332
  const rowId = data.id;
5276
5333
  if (options?.onConflict?.length || rowId === void 0) throw new RebaseClientError(`Cannot upsert into "${slug}" while offline: the row it would replace can only be found by the server. Upserting on the primary key, with the key in the row, is queued; upserting on a natural key is not.`, { code: "OFFLINE_UPSERT_UNSUPPORTED" });
5277
- const row = {
5278
- ...data,
5279
- id: rowId
5280
- };
5334
+ const row = optimisticRow(data, rowId);
5281
5335
  await this.enqueue({
5282
5336
  collection: slug,
5283
5337
  type: "createMany",
@@ -5309,11 +5363,10 @@ var OfflineManager = class {
5309
5363
  for (const { id, data } of updates) {
5310
5364
  const base = this.rawLocalRow(slug, id);
5311
5365
  rollback[String(id)] = base ?? null;
5312
- optimistic.push({
5366
+ optimistic.push(optimisticRow({
5313
5367
  ...base ?? {},
5314
- ...data,
5315
- id
5316
- });
5368
+ ...data
5369
+ }, id));
5317
5370
  }
5318
5371
  await this.enqueue({
5319
5372
  collection: slug,
@@ -5376,11 +5429,10 @@ var OfflineManager = class {
5376
5429
  data,
5377
5430
  rollback: { rows: { [String(id)]: base ?? null } }
5378
5431
  });
5379
- const optimistic = {
5432
+ const optimistic = optimisticRow({
5380
5433
  ...base ?? {},
5381
- ...data,
5382
- id
5383
- };
5434
+ ...data
5435
+ }, id);
5384
5436
  this.setLocalRow(slug, id, optimistic);
5385
5437
  this.notifyCollection(slug);
5386
5438
  return optimistic;