@pylonsync/sync 0.3.193 → 0.3.196
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 +84 -0
- package/src/local-store.ts +23 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -274,6 +274,15 @@ export class SyncEngine {
|
|
|
274
274
|
private reactiveHandlers: Map<string, (msg: ReactiveMessage) => void> =
|
|
275
275
|
new Map();
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Listeners notified when the server signals a per-subscriber row
|
|
279
|
+
* revocation (`row-revoked` envelope). Used by `@pylonsync/loro`
|
|
280
|
+
* to evict the LoroDoc registry entry for a row whose policy was
|
|
281
|
+
* revoked mid-session. Plain Set so identity is the dedup key.
|
|
282
|
+
*/
|
|
283
|
+
private rowEvictionListeners: Set<(entity: string, rowId: string) => void> =
|
|
284
|
+
new Set();
|
|
285
|
+
|
|
277
286
|
/**
|
|
278
287
|
* Register a binary-frame handler. Returns an unsubscribe fn that
|
|
279
288
|
* pulls the handler back out — call on hook unmount / module
|
|
@@ -731,6 +740,28 @@ export class SyncEngine {
|
|
|
731
740
|
return;
|
|
732
741
|
}
|
|
733
742
|
|
|
743
|
+
// Server-driven revocation: a subscriber whose read policy
|
|
744
|
+
// was revoked mid-session for a specific row. Drop the row
|
|
745
|
+
// from the local replica at the current cursor seq so the
|
|
746
|
+
// tombstone supersedes any racing late-arriving WS update
|
|
747
|
+
// for the same row, and notify any LoroDoc subscriber
|
|
748
|
+
// (registered via `addRowEvictionListener`) so collaborative
|
|
749
|
+
// doc handles unmount cleanly.
|
|
750
|
+
//
|
|
751
|
+
// Distinct from a regular Delete change event because this
|
|
752
|
+
// envelope has no global seq — the row's underlying data
|
|
753
|
+
// hasn't been deleted, only the recipient's visibility of
|
|
754
|
+
// it. Other subscribers (with matching policy) keep their
|
|
755
|
+
// row intact.
|
|
756
|
+
if (
|
|
757
|
+
msg.type === "row-revoked" &&
|
|
758
|
+
typeof msg.entity === "string" &&
|
|
759
|
+
typeof msg.row_id === "string"
|
|
760
|
+
) {
|
|
761
|
+
this.handleRowRevocation(msg.entity, msg.row_id);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
734
765
|
// Session mutated server-side. Fires for select-org / clear-org
|
|
735
766
|
// / session revoke — every tab connected as this user gets the
|
|
736
767
|
// envelope (cross-machine too via the cluster bus). Trigger
|
|
@@ -1456,6 +1487,59 @@ export class SyncEngine {
|
|
|
1456
1487
|
return this.refreshResolvedSession();
|
|
1457
1488
|
}
|
|
1458
1489
|
|
|
1490
|
+
/**
|
|
1491
|
+
* Drop a row from the local replica because the server signaled
|
|
1492
|
+
* that the current subscriber's read policy was revoked for it.
|
|
1493
|
+
*
|
|
1494
|
+
* Calls `LocalStore.revokeRow(entity, id, cursor.last_seq)` —
|
|
1495
|
+
* NOT `reconcileRemove` — because the latter early-returns when
|
|
1496
|
+
* the row isn't in memory (common for CRDT-only consumers using
|
|
1497
|
+
* `useLoroDoc` without a JSON row). `revokeRow` always records
|
|
1498
|
+
* the tombstone so a stale insert/update arriving after the
|
|
1499
|
+
* revocation can't resurrect the row.
|
|
1500
|
+
*
|
|
1501
|
+
* Also notifies row-eviction listeners so external row-bound
|
|
1502
|
+
* resources (LoroDoc registries, etc.) can unmount.
|
|
1503
|
+
*/
|
|
1504
|
+
private handleRowRevocation(entity: string, rowId: string): void {
|
|
1505
|
+
const removed = this.store.revokeRow(
|
|
1506
|
+
entity,
|
|
1507
|
+
rowId,
|
|
1508
|
+
this.cursor.last_seq,
|
|
1509
|
+
);
|
|
1510
|
+
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
|
+
// For CRDT-only consumers `removed` is false (the row was
|
|
1514
|
+
// never materialized into `tables`), but the tombstone is
|
|
1515
|
+
// still recorded above so future replays are filtered.
|
|
1516
|
+
if (this.persistence) {
|
|
1517
|
+
void this.persistence.deleteRow(entity, rowId).catch(() => {
|
|
1518
|
+
/* best-effort */
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
this.store.notify();
|
|
1522
|
+
}
|
|
1523
|
+
for (const listener of this.rowEvictionListeners) {
|
|
1524
|
+
listener(entity, rowId);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Register a listener invoked when the server signals a per-
|
|
1530
|
+
* subscriber row revocation. Used by `@pylonsync/loro` to evict
|
|
1531
|
+
* the LoroDoc registry entry for the row so collaborative doc
|
|
1532
|
+
* handles unmount cleanly. Returns an unsubscribe function.
|
|
1533
|
+
*/
|
|
1534
|
+
addRowEvictionListener(
|
|
1535
|
+
listener: (entity: string, rowId: string) => void,
|
|
1536
|
+
): () => void {
|
|
1537
|
+
this.rowEvictionListeners.add(listener);
|
|
1538
|
+
return () => {
|
|
1539
|
+
this.rowEvictionListeners.delete(listener);
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1459
1543
|
/**
|
|
1460
1544
|
* Switch the caller's active tenant (organization) and refresh the
|
|
1461
1545
|
* resolved session in one shot. Membership is verified server-side
|
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);
|