@pylonsync/sync 0.3.193 → 0.3.195

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 +81 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.193",
6
+ "version": "0.3.195",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
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,56 @@ 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.reconcileRemove(entity, id, cursor.last_seq)`
1495
+ * to record a tombstone at the current cursor. A future server
1496
+ * issuing seqs strictly greater than the cursor can re-create
1497
+ * the row if policy is re-granted; replays older than the cursor
1498
+ * are filtered by the tombstone (closing the "WS update lands
1499
+ * between revoke and tombstone" race).
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.reconcileRemove(
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
+ if (this.persistence) {
1514
+ void this.persistence.deleteRow(entity, rowId).catch(() => {
1515
+ /* best-effort */
1516
+ });
1517
+ }
1518
+ this.store.notify();
1519
+ }
1520
+ for (const listener of this.rowEvictionListeners) {
1521
+ listener(entity, rowId);
1522
+ }
1523
+ }
1524
+
1525
+ /**
1526
+ * Register a listener invoked when the server signals a per-
1527
+ * subscriber row revocation. Used by `@pylonsync/loro` to evict
1528
+ * the LoroDoc registry entry for the row so collaborative doc
1529
+ * handles unmount cleanly. Returns an unsubscribe function.
1530
+ */
1531
+ addRowEvictionListener(
1532
+ listener: (entity: string, rowId: string) => void,
1533
+ ): () => void {
1534
+ this.rowEvictionListeners.add(listener);
1535
+ return () => {
1536
+ this.rowEvictionListeners.delete(listener);
1537
+ };
1538
+ }
1539
+
1459
1540
  /**
1460
1541
  * Switch the caller's active tenant (organization) and refresh the
1461
1542
  * resolved session in one shot. Membership is verified server-side