@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 +73 -26
- package/dist/index.es.js.map +1 -1
- package/dist/offline-connectivity.d.ts +12 -0
- package/dist/offline.d.ts +19 -0
- package/package.json +4 -4
- package/src/collection.ts +7 -3
- package/src/offline-connectivity.ts +16 -0
- package/src/offline-sync-engine.test.ts +130 -0
- package/src/offline.ts +100 -28
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
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
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
|
-
|
|
4736
|
-
|
|
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
|
-
|
|
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 ?? [];
|