@rindle/optimistic 0.4.4 → 0.6.3

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/src/client-id.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { TicketPersistence } from "@rindle/remote";
2
+
1
3
  // The connection's stable clientID — kept in its own (wasm-free) module so it can be unit-tested
2
4
  // without pulling the engine in. A per-ORIGIN base (localStorage) shared by every tab and kept
3
5
  // across reloads, a per-TAB suffix (sessionStorage) that survives a reload within a tab but is
@@ -10,6 +12,12 @@
10
12
  const CLIENT_ID_KEY = "rindle-client-id";
11
13
  /** The per-TAB suffix (sessionStorage): survives a reload within a tab but is distinct per tab. */
12
14
  const TAB_ID_KEY = "rindle-tab-id";
15
+ /** The per-TAB follower-affinity ticket (sessionStorage): kept across a reload WITHIN a tab (so a
16
+ * reload lands back on the same follower — monotone reads survive it, FOLLOWER-AFFINITY-DESIGN.md
17
+ * §8), but DISTINCT per tab so two tabs can pin two regions (design §13; a shared localStorage
18
+ * ticket would force both onto one follower). The ticket is opaque and short-lived — the follower
19
+ * re-mints one on every connect — so losing it (no sessionStorage) merely re-anycasts. */
20
+ const AFFINITY_TICKET_KEY = "rindle-affinity-ticket";
13
21
 
14
22
  /** Read-or-mint a value in a web Storage area, tolerating its absence (SSR, privacy mode). Returns
15
23
  * `undefined` when the area is unavailable so the caller can pick a fallback. */
@@ -55,6 +63,42 @@ export function resetStableClientID(): void {
55
63
  tabInstance = 0;
56
64
  }
57
65
 
66
+ /** A sessionStorage-backed {@link TicketPersistence} for the affinity ticket — per-tab, survives a
67
+ * reload (see {@link AFFINITY_TICKET_KEY}). Every access tolerates web storage being absent (SSR,
68
+ * private mode): the store then behaves as pure in-memory, re-anycasting on each fresh load. */
69
+ export function sessionTicketPersistence(): TicketPersistence {
70
+ const area = () => {
71
+ try {
72
+ return (globalThis as unknown as Record<string, Storage | undefined>).sessionStorage;
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ };
77
+ return {
78
+ load: () => {
79
+ try {
80
+ return area()?.getItem(AFFINITY_TICKET_KEY) ?? undefined;
81
+ } catch {
82
+ return undefined;
83
+ }
84
+ },
85
+ save: (ticket) => {
86
+ try {
87
+ area()?.setItem(AFFINITY_TICKET_KEY, ticket);
88
+ } catch {
89
+ // Best-effort persistence; a full/blocked store just means we re-anycast on the next load.
90
+ }
91
+ },
92
+ clear: () => {
93
+ try {
94
+ area()?.removeItem(AFFINITY_TICKET_KEY);
95
+ } catch {
96
+ // Best-effort.
97
+ }
98
+ },
99
+ };
100
+ }
101
+
58
102
  /** The connection's stable clientID. A per-origin base (localStorage) keeps one logical identity
59
103
  * across reloads; a per-tab suffix (sessionStorage) gives each tab its OWN mid/lmid stream; a
60
104
  * per-load instance suffix keeps two clients constructed in ONE tab from sharing that stream. Falls