@pylonsync/sync 0.3.184 → 0.3.186
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/package.json +1 -1
- package/src/index.ts +48 -17
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1598,23 +1598,38 @@ export class SyncEngine {
|
|
|
1598
1598
|
this.lastSeenToken = tokenNow;
|
|
1599
1599
|
|
|
1600
1600
|
try {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
//
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1601
|
+
// Snapshot pagination: when the cursor is 0 and the server's
|
|
1602
|
+
// table is larger than a single batch, the response carries
|
|
1603
|
+
// `snapshot_after` for the next page. Loop until exhausted
|
|
1604
|
+
// BEFORE returning so a fresh client always observes a
|
|
1605
|
+
// consistent full snapshot, not a 1k-row prefix it mistakes
|
|
1606
|
+
// for the whole replica.
|
|
1607
|
+
let snapshotAfter: string | undefined;
|
|
1608
|
+
let firstPass = true;
|
|
1609
|
+
while (firstPass || snapshotAfter) {
|
|
1610
|
+
firstPass = false;
|
|
1611
|
+
const params = new URLSearchParams();
|
|
1612
|
+
params.set("since", String(this.cursor.last_seq));
|
|
1613
|
+
if (snapshotAfter) {
|
|
1614
|
+
params.set("snapshot_after", snapshotAfter);
|
|
1615
|
+
}
|
|
1616
|
+
const resp = await this.request<
|
|
1617
|
+
PullResponse & { snapshot_after?: string | null }
|
|
1618
|
+
>("GET", `/api/sync/pull?${params.toString()}`);
|
|
1619
|
+
this.consecutive_410s = 0;
|
|
1620
|
+
await this.enqueueApply(resp.changes, resp.cursor);
|
|
1621
|
+
// `snapshot_after` is only set when the server is mid-snapshot.
|
|
1622
|
+
// Continue paginating in the same loop iteration so we don't
|
|
1623
|
+
// leave a fresh client with a partial replica.
|
|
1624
|
+
snapshotAfter = resp.snapshot_after ?? undefined;
|
|
1625
|
+
// The change-log tail also paginates via `has_more` — handle
|
|
1626
|
+
// that one recursively after the snapshot loop completes so
|
|
1627
|
+
// backpressure on the change-log path uses the existing
|
|
1628
|
+
// tail-pull semantics.
|
|
1629
|
+
if (!snapshotAfter && resp.has_more) {
|
|
1630
|
+
await this.pull();
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1618
1633
|
}
|
|
1619
1634
|
} catch (err) {
|
|
1620
1635
|
// Swallow network + transient errors so the poll/reconnect loop
|
|
@@ -1735,6 +1750,13 @@ export class SyncEngine {
|
|
|
1735
1750
|
// bypass the tombstone — re-creation server-side still propagates.
|
|
1736
1751
|
const tombstoneSeq = this.cursor.last_seq;
|
|
1737
1752
|
for (const entity of names) {
|
|
1753
|
+
// Capture cursor BEFORE the fetch so we can detect drift mid-
|
|
1754
|
+
// reconcile. If a WS event lands while this entity is being
|
|
1755
|
+
// pulled, our snapshot is already stale — applying it would
|
|
1756
|
+
// overwrite a newer authoritative row. Skip apply in that case
|
|
1757
|
+
// and rely on the WS event (which has the correct seq) plus the
|
|
1758
|
+
// next reconcile trigger to converge. Codex P1.
|
|
1759
|
+
const cursorBeforeFetch = this.cursor.last_seq;
|
|
1738
1760
|
let serverRows: Row[];
|
|
1739
1761
|
try {
|
|
1740
1762
|
serverRows = await this.fetchEntityRows(entity);
|
|
@@ -1750,6 +1772,15 @@ export class SyncEngine {
|
|
|
1750
1772
|
}
|
|
1751
1773
|
continue;
|
|
1752
1774
|
}
|
|
1775
|
+
if (this.cursor.last_seq !== cursorBeforeFetch) {
|
|
1776
|
+
// Cursor moved during fetch — at least one WS event for this
|
|
1777
|
+
// (or another) entity landed and might have a fresher value
|
|
1778
|
+
// for a row our snapshot just captured. Bail out for this
|
|
1779
|
+
// entity; reconcile() is triggered again on visibility-change
|
|
1780
|
+
// and reconnect, and the WS event already carried the latest
|
|
1781
|
+
// state for the affected row.
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1753
1784
|
await this.applyEntityReconcile(entity, serverRows, tombstoneSeq);
|
|
1754
1785
|
}
|
|
1755
1786
|
}
|