add persisted.readLatest for cross-tab session reads

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-31 18:58:44 +03:00
parent 65faee2836
commit e79d8e8e52
3 changed files with 44 additions and 0 deletions
+12
View File
@@ -31,6 +31,18 @@ export function get<K extends keyof Schema>(key: K): Schema[K] {
}
get satisfies PersistedApi['get']
/**
* Native is single-instance: there is no other tab that could have written
* newer data behind our back, so the in-memory `_state` is already the truth
* and a synchronous fresh read is impossible anyway (AsyncStorage is async).
* This mirrors {@link get}; the web implementation is the one that actually
* re-reads the store.
*/
export function readLatest<K extends keyof Schema>(key: K): Schema[K] {
return _state[key]
}
readLatest satisfies PersistedApi['readLatest']
export async function write<K extends keyof Schema>(
key: K,
value: Schema[K],
+23
View File
@@ -39,6 +39,29 @@ export function get<K extends keyof Schema>(key: K): Schema[K] {
}
get satisfies PersistedApi['get']
/**
* Force a fresh synchronous re-read of localStorage and return the requested
* key from it, WITHOUT adopting it as `_state`.
*
* This exists for the cross-tab expiry-rescue path. A frozen tab may not have
* processed a queued broadcast yet, so {@link get} (and persisted's in-memory
* `_state`) can be stale even though another tab already wrote healthy tokens
* to storage. Reading through storage directly here is the only way to see the
* true cross-tab-latest tokens on web.
*
* Crucially we do NOT adopt into `_state`. {@link readFromStorage} memoizes by
* raw string and returns the same object reference for unchanged data, so
* adopting here would make the later queued broadcast/storage event for that
* same write see `next === _state` and suppress its listener notification -
* leaving non-current-account changes (removals, other tokens, metadata) stale
* indefinitely. Leaving `_state` alone lets that queued event still fire.
*/
export function readLatest<K extends keyof Schema>(key: K): Schema[K] {
const next = readFromStorage()
return next?.[key] ?? _state[key]
}
readLatest satisfies PersistedApi['readLatest']
// eslint-disable-next-line @typescript-eslint/require-await
export async function write<K extends keyof Schema>(
key: K,
+9
View File
@@ -3,6 +3,15 @@ import {type Schema} from './schema'
export type PersistedApi = {
init(): Promise<void>
get<K extends keyof Schema>(key: K): Schema[K]
/**
* Like {@link get}, but on web forces a fresh synchronous re-read of the
* backing store before returning (without adopting it as the in-memory
* state). This exists for the cross-tab expiry-rescue path: a frozen tab may
* not have processed a queued broadcast yet, so {@link get} can be stale
* while another tab has already written healthy tokens to storage. On native
* it is identical to {@link get} (single-instance, no other writer).
*/
readLatest<K extends keyof Schema>(key: K): Schema[K]
write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void>
onUpdate<K extends keyof Schema>(
key: K,