@rebasepro/client 0.10.1-canary.gdaf6ba4 → 0.11.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
@@ -1194,12 +1194,13 @@ function createCollectionClient(transport, slug, ws) {
1194
1194
  throw err;
1195
1195
  }
1196
1196
  },
1197
- async create(data, id) {
1197
+ async create(data, id, options) {
1198
1198
  const body = { ...data };
1199
1199
  if (id !== void 0) body.id = id;
1200
1200
  return await transport.request(basePath, {
1201
1201
  method: "POST",
1202
- body: JSON.stringify(body)
1202
+ body: JSON.stringify(body),
1203
+ ...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
1203
1204
  });
1204
1205
  },
1205
1206
  async createMany(data, options) {
@@ -3196,6 +3197,21 @@ function isRetryableError(error) {
3196
3197
  if (error instanceof RebaseApiError) return error.status !== void 0 && RETRYABLE_STATUSES.has(error.status);
3197
3198
  return false;
3198
3199
  }
3200
+ /**
3201
+ * Did this write fail because the row is already there?
3202
+ *
3203
+ * Matched on the SQLSTATE the server passes through (`23505`, unique_violation)
3204
+ * and on 409, never on the message — a duplicate-key message names the
3205
+ * constraint and the values, so it is neither stable nor safe to parse.
3206
+ *
3207
+ * The queue uses this to recognise its own earlier attempt. A create whose
3208
+ * response was lost is replayed, and for a row carrying an id the SDK generated
3209
+ * the server can only be rejecting it because the first attempt actually landed.
3210
+ */
3211
+ function isDuplicateKeyError(error) {
3212
+ if (!(error instanceof RebaseApiError)) return false;
3213
+ return error.code === "23505" || error.status === 409;
3214
+ }
3199
3215
  var ConnectivityMonitor = class {
3200
3216
  state = "online";
3201
3217
  backoffMs;
@@ -3812,6 +3828,25 @@ var OfflineManager = class {
3812
3828
  collections = /* @__PURE__ */ new Map();
3813
3829
  /** In-memory mirror of the current scope's queue, in replay order. */
3814
3830
  queue = [];
3831
+ /**
3832
+ * The mutation currently on the wire, if any.
3833
+ *
3834
+ * `flush` awaits `replay(op)` with `op` still at the head of `queue`, so for
3835
+ * the whole duration of that request the in-flight op is also the queue's
3836
+ * *tail* whenever it is the only entry. Both shortcuts in `enqueue` reach
3837
+ * for the tail, and neither may touch an op the server is already reading:
3838
+ *
3839
+ * - Coalescing an update into it mutates a payload that has already been
3840
+ * serialized and sent, and `drop` then removes the whole entry on ACK —
3841
+ * so the second edit is neither sent nor kept. A silently lost write.
3842
+ * - Cancelling it out against a delete assumes the server never saw the
3843
+ * create. It is seeing it right now, so the row would be created and the
3844
+ * delete never queued — an orphan row nothing will ever remove.
3845
+ *
3846
+ * Guarding on the id rather than on a boolean keeps this correct if the
3847
+ * flush loop ever sends more than one op at a time.
3848
+ */
3849
+ inFlightId = null;
3815
3850
  queueLoad;
3816
3851
  /** Serializes enqueues so concurrent writes keep the order the app made them. */
3817
3852
  enqueueChain = Promise.resolve();
@@ -4059,7 +4094,7 @@ var OfflineManager = class {
4059
4094
  },
4060
4095
  update: async (id, data) => {
4061
4096
  await this.ensureCollection(slug);
4062
- if (this.connectivity.shouldAttempt()) try {
4097
+ if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
4063
4098
  const row = await inner.update(id, data);
4064
4099
  this.connectivity.markSuccess();
4065
4100
  await this.ingest(slug, [row]);
@@ -4088,7 +4123,7 @@ var OfflineManager = class {
4088
4123
  },
4089
4124
  delete: async (id) => {
4090
4125
  await this.ensureCollection(slug);
4091
- if (this.connectivity.shouldAttempt()) try {
4126
+ if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
4092
4127
  await inner.delete(id);
4093
4128
  this.connectivity.markSuccess();
4094
4129
  this.removeLocalRow(slug, id, true);
@@ -4630,7 +4665,7 @@ var OfflineManager = class {
4630
4665
  await this.ensureQueueLoaded();
4631
4666
  if (mutation.type === "update") {
4632
4667
  const tail = this.queue[this.queue.length - 1];
4633
- if (tail && tail.collection === mutation.collection && (tail.type === "create" || tail.type === "update") && tail.id === mutation.id) {
4668
+ if (tail && tail.mutationId !== this.inFlightId && tail.collection === mutation.collection && (tail.type === "create" || tail.type === "update") && tail.id === mutation.id) {
4634
4669
  tail.data = {
4635
4670
  ...tail.data,
4636
4671
  ...mutation.data,
@@ -4641,8 +4676,8 @@ var OfflineManager = class {
4641
4676
  }
4642
4677
  }
4643
4678
  if (mutation.type === "delete") {
4644
- if (this.queue.some((m) => m.collection === mutation.collection && m.type === "create" && m.id === mutation.id && m.generatedId === true)) {
4645
- const doomed = this.queue.filter((m) => m.collection === mutation.collection && m.id === mutation.id && (m.type === "create" || m.type === "update"));
4679
+ if (this.queue.some((m) => m.collection === mutation.collection && m.type === "create" && m.id === mutation.id && m.generatedId === true && m.mutationId !== this.inFlightId)) {
4680
+ const doomed = this.queue.filter((m) => m.collection === mutation.collection && m.id === mutation.id && (m.type === "create" || m.type === "update") && m.mutationId !== this.inFlightId);
4646
4681
  for (const op of doomed) await this.store.dequeue(this.queueKey(op));
4647
4682
  this.queue = this.queue.filter((m) => !doomed.includes(m));
4648
4683
  this.afterQueueChange();
@@ -4717,27 +4752,32 @@ var OfflineManager = class {
4717
4752
  while (this.queue.length > 0 && !this.disposed) {
4718
4753
  const op = this.queue[0];
4719
4754
  touched.add(op.collection);
4755
+ this.inFlightId = op.mutationId;
4720
4756
  try {
4721
- await this.replay(op);
4722
- } catch (error) {
4723
- if (isNetworkError(error)) {
4724
- this.connectivity.markFailure();
4725
- break;
4726
- }
4727
- op.attempts = (op.attempts ?? 0) + 1;
4728
- op.lastError = error?.message ?? String(error);
4729
- if (isRetryableError(error) && op.attempts < this.maxRetries) {
4730
- await this.store.enqueue(this.queueKey(op), op).catch(() => void 0);
4731
- this.connectivity.deferRetry();
4732
- this.patchStatus({ lastError: op.lastError });
4733
- break;
4757
+ try {
4758
+ await this.replay(op);
4759
+ } catch (error) {
4760
+ if (isNetworkError(error)) {
4761
+ this.connectivity.markFailure();
4762
+ break;
4763
+ }
4764
+ op.attempts = (op.attempts ?? 0) + 1;
4765
+ op.lastError = error?.message ?? String(error);
4766
+ if (isRetryableError(error) && op.attempts < this.maxRetries) {
4767
+ await this.store.enqueue(this.queueKey(op), op).catch(() => void 0);
4768
+ this.connectivity.deferRetry();
4769
+ this.patchStatus({ lastError: op.lastError });
4770
+ break;
4771
+ }
4772
+ await this.rejectMutation(op, error);
4773
+ continue;
4734
4774
  }
4735
- await this.rejectMutation(op, error);
4736
- continue;
4775
+ this.connectivity.markSuccess();
4776
+ await this.drop(op);
4777
+ flushed++;
4778
+ } finally {
4779
+ this.inFlightId = null;
4737
4780
  }
4738
- this.connectivity.markSuccess();
4739
- await this.drop(op);
4740
- flushed++;
4741
4781
  }
4742
4782
  } finally {
4743
4783
  this.patchStatus({ syncing: false });
@@ -4758,7 +4798,14 @@ var OfflineManager = class {
4758
4798
  async replay(op) {
4759
4799
  const inner = this.innerFor(op.collection);
4760
4800
  if (op.type === "create") {
4761
- const row = await inner.create(op.data);
4801
+ let row;
4802
+ try {
4803
+ row = await inner.create(op.data, void 0, { idempotencyKey: op.mutationId });
4804
+ } catch (error) {
4805
+ if (!(op.generatedId === true && isDuplicateKeyError(error))) throw error;
4806
+ row = await inner.findById(op.id).catch(() => void 0);
4807
+ if (!row) return;
4808
+ }
4762
4809
  await this.adoptServerRow(op, op.id, row);
4763
4810
  } else if (op.type === "createMany") {
4764
4811
  const queued = op.data ?? [];