fix(02-review): WR-04 require explicit persist on resources.ts entry()/optimistic()
entry(key, persist = true) and optimistic(key, update) silently defaulted
to persist:true after the first call for a key, and optimistic() didn't
accept a persist argument at all. Every current call site happened to be
safe, but the invariant was unenforced: a future caller invoking
store.optimistic() before any useCachedResource({persist:false}) has run
for that key in the same tick would silently start writing to
sessionStorage with no indication anything is wrong (T-02-01).
persist is now a required argument on both functions (no default), and the
per-key decision is recorded and asserted (dev-only warning) against any
later call that disagrees. useCachedResource's optimistic() wrapper now
threads its own already-resolved persist value through automatically, so
no existing composable caller changes behavior. The two call sites that
use the resources store directly (Cloud.vue/PeerFiles.vue's per-peer
browse cache) now pass persist:true explicitly, matching their existing
behavior exactly.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,11 @@ export const useResourcesStore = defineStore('resources', () => {
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const revalidators = new Map<string, Set<() => void>>()
|
||||
const invalidateTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
// Records each key's persist decision at creation time (WR-04) — reused to
|
||||
// detect (and warn on, in dev) a later call for the same key disagreeing
|
||||
// about persist, since that almost always means two call sites disagree
|
||||
// about whether a cache key is safe to write to sessionStorage (T-02-01).
|
||||
const entryPersist = new Map<string, boolean>()
|
||||
// Bumped by clearAll() so an in-flight fetch from a just-ended session that
|
||||
// resolves afterward can detect it and skip writing its result — without
|
||||
// this guard the resolving promise would still call writeSnapshot() and
|
||||
@@ -64,8 +69,12 @@ export const useResourcesStore = defineStore('resources', () => {
|
||||
|
||||
/** Get (or create) the reactive entry for a key, hydrating from the
|
||||
* sessionStorage snapshot on first sight so revisits after a reload paint
|
||||
* before any RPC completes. Pass `persist: false` to skip snapshots. */
|
||||
function entry<T>(key: string, persist = true): ResourceEntry<T> {
|
||||
* before any RPC completes. `persist` is REQUIRED (no default) so a call
|
||||
* site can never silently opt a cache key into sessionStorage by omission
|
||||
* (WR-04/T-02-01) — it only takes effect on the very first call for a
|
||||
* given key; every later call for the same key must keep passing the SAME
|
||||
* decision, asserted (dev-only warning) rather than silently reused. */
|
||||
function entry<T>(key: string, persist: boolean): ResourceEntry<T> {
|
||||
let e = entries.get(key)
|
||||
if (!e) {
|
||||
const snap = persist ? readSnapshot<T>(key) : null
|
||||
@@ -76,6 +85,12 @@ export const useResourcesStore = defineStore('resources', () => {
|
||||
error: null,
|
||||
})
|
||||
entries.set(key, e)
|
||||
entryPersist.set(key, persist)
|
||||
} else if (import.meta.env.DEV && entryPersist.get(key) !== persist) {
|
||||
console.warn(
|
||||
`[resources] entry("${key}") called with persist=${persist}, but this key was already created with persist=${entryPersist.get(key)}. `
|
||||
+ 'The original decision wins for the lifetime of this entry — persist is decided once per key, never per call (T-02-01).',
|
||||
)
|
||||
}
|
||||
return e as ResourceEntry<T>
|
||||
}
|
||||
@@ -149,9 +164,14 @@ export const useResourcesStore = defineStore('resources', () => {
|
||||
}
|
||||
|
||||
/** Optimistically apply `update` to the cached value; returns a rollback.
|
||||
* Pattern: rollback on RPC failure (generalized TransportPrefsCard). */
|
||||
function optimistic<T>(key: string, update: (current: T | null) => T): () => void {
|
||||
const e = entry<T>(key)
|
||||
* Pattern: rollback on RPC failure (generalized TransportPrefsCard).
|
||||
* `persist` is REQUIRED (no default) for the same reason as entry() —
|
||||
* silently falling back to persist:true here was the exact footgun WR-04
|
||||
* flagged: a caller that runs before any useCachedResource({persist:false})
|
||||
* has created the entry would otherwise start writing to sessionStorage
|
||||
* with no indication anything is wrong. */
|
||||
function optimistic<T>(key: string, update: (current: T | null) => T, persist: boolean): () => void {
|
||||
const e = entry<T>(key, persist)
|
||||
const before = e.data
|
||||
const beforeState = e.loadState
|
||||
e.data = update(before)
|
||||
|
||||
Reference in New Issue
Block a user