@pylonsync/sync 0.3.195 → 0.3.197
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 +31 -15
- package/src/local-store.ts +23 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -758,7 +758,15 @@ export class SyncEngine {
|
|
|
758
758
|
typeof msg.entity === "string" &&
|
|
759
759
|
typeof msg.row_id === "string"
|
|
760
760
|
) {
|
|
761
|
-
|
|
761
|
+
// Server includes its current high-water seq when known —
|
|
762
|
+
// use it as the tombstone seq so an in-flight stale frame
|
|
763
|
+
// with `seq <= server_seq` is filtered locally. A
|
|
764
|
+
// legitimate re-grant at a higher seq still lands.
|
|
765
|
+
const revokeSeq =
|
|
766
|
+
typeof msg.seq === "number" && msg.seq > 0
|
|
767
|
+
? msg.seq
|
|
768
|
+
: this.cursor.last_seq;
|
|
769
|
+
this.handleRowRevocation(msg.entity, msg.row_id, revokeSeq);
|
|
762
770
|
return;
|
|
763
771
|
}
|
|
764
772
|
|
|
@@ -1491,25 +1499,28 @@ export class SyncEngine {
|
|
|
1491
1499
|
* Drop a row from the local replica because the server signaled
|
|
1492
1500
|
* that the current subscriber's read policy was revoked for it.
|
|
1493
1501
|
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1502
|
+
* `tombstoneSeq` is the server's high-water seq at the time of
|
|
1503
|
+
* revocation (from the envelope). Stale in-flight WS frames with
|
|
1504
|
+
* `seq <= tombstoneSeq` are filtered locally; legitimate
|
|
1505
|
+
* re-grant + re-insert at higher seqs still land. Also fires a
|
|
1506
|
+
* catch-up pull on revocation so any frame with `seq >
|
|
1507
|
+
* tombstoneSeq` that arrives before the next legitimate event
|
|
1508
|
+
* gets reconciled against server truth.
|
|
1509
|
+
*
|
|
1510
|
+
* Uses `LocalStore.revokeRow` (not `reconcileRemove`) so the
|
|
1511
|
+
* tombstone is recorded even for CRDT-only consumers whose row
|
|
1512
|
+
* was never materialized into `tables`.
|
|
1500
1513
|
*
|
|
1501
1514
|
* Also notifies row-eviction listeners so external row-bound
|
|
1502
1515
|
* resources (LoroDoc registries, etc.) can unmount.
|
|
1503
1516
|
*/
|
|
1504
|
-
private handleRowRevocation(
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
);
|
|
1517
|
+
private handleRowRevocation(
|
|
1518
|
+
entity: string,
|
|
1519
|
+
rowId: string,
|
|
1520
|
+
tombstoneSeq: number,
|
|
1521
|
+
): void {
|
|
1522
|
+
const removed = this.store.revokeRow(entity, rowId, tombstoneSeq);
|
|
1510
1523
|
if (removed) {
|
|
1511
|
-
// Persist the deletion through the same pipe as a real Delete
|
|
1512
|
-
// event so on-disk replica + in-memory replica stay aligned.
|
|
1513
1524
|
if (this.persistence) {
|
|
1514
1525
|
void this.persistence.deleteRow(entity, rowId).catch(() => {
|
|
1515
1526
|
/* best-effort */
|
|
@@ -1520,6 +1531,11 @@ export class SyncEngine {
|
|
|
1520
1531
|
for (const listener of this.rowEvictionListeners) {
|
|
1521
1532
|
listener(entity, rowId);
|
|
1522
1533
|
}
|
|
1534
|
+
// Fire a catch-up pull so any in-flight frame with seq above
|
|
1535
|
+
// the revocation tombstone is resolved against server truth.
|
|
1536
|
+
// Fire-and-forget — pull is internally serialized so concurrent
|
|
1537
|
+
// triggers coalesce.
|
|
1538
|
+
void this.pull();
|
|
1523
1539
|
}
|
|
1524
1540
|
|
|
1525
1541
|
/**
|
package/src/local-store.ts
CHANGED
|
@@ -80,6 +80,29 @@ export class LocalStore {
|
|
|
80
80
|
return true;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Per-subscriber row revocation: the server signaled that the
|
|
85
|
+
* current client lost read access to a specific row. ALWAYS
|
|
86
|
+
* records the tombstone (even when the row isn't in memory), so
|
|
87
|
+
* a stale insert/update event that arrives after the revocation
|
|
88
|
+
* can't resurrect the row.
|
|
89
|
+
*
|
|
90
|
+
* `reconcileRemove` returns early when the row is absent — that's
|
|
91
|
+
* the right behavior for "did anything change?" reconcile passes,
|
|
92
|
+
* but wrong here: a CRDT-only consumer using `useLoroDoc` without
|
|
93
|
+
* a JSON row never had the row in `tables` to begin with, and
|
|
94
|
+
* without the tombstone any future server-issued insert
|
|
95
|
+
* (legitimate re-grant or a slow stale frame) would land.
|
|
96
|
+
*
|
|
97
|
+
* Returns true if the row was present + removed; the tombstone
|
|
98
|
+
* is recorded regardless.
|
|
99
|
+
*/
|
|
100
|
+
revokeRow(entity: string, id: string, tombstoneSeq: number): boolean {
|
|
101
|
+
const removed = this.tables.get(entity)?.delete(id) ?? false;
|
|
102
|
+
this.recordTombstone(entity, id, tombstoneSeq);
|
|
103
|
+
return removed;
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
private isTombstoned(entity: string, id: string, at_seq?: number): boolean {
|
|
84
107
|
if (this.optimisticTombstones.get(entity)?.has(id)) return true;
|
|
85
108
|
const tombSeq = this.tombstones.get(entity)?.get(id);
|