@pylonsync/sync 0.3.185 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +32 -17
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.185",
6
+ "version": "0.3.186",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -1598,23 +1598,38 @@ export class SyncEngine {
1598
1598
  this.lastSeenToken = tokenNow;
1599
1599
 
1600
1600
  try {
1601
- const resp = await this.request<PullResponse>(
1602
- "GET",
1603
- `/api/sync/pull?since=${this.cursor.last_seq}`
1604
- );
1605
- // Successful response clear the 410 circuit breaker.
1606
- this.consecutive_410s = 0;
1607
- // Route through the apply queue so concurrent WS messages and
1608
- // pull responses don't race. The queue's monotonic guard skips
1609
- // any seq we've already applied (e.g. WS already landed the
1610
- // events that pull is now redelivering) and only advances the
1611
- // cursor forward. We still advance even when no changes land,
1612
- // because the server's last_seq may have moved past us due to
1613
- // policy-filtered events.
1614
- await this.enqueueApply(resp.changes, resp.cursor);
1615
- // If there are more, pull again immediately.
1616
- if (resp.has_more) {
1617
- await this.pull();
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