fix(cache): make the failed-publish restore survive the from-cache filter (#64) #71

Merged
laoc merged 1 commit from fix/issue-64-restore-from-cache into dev 2026-07-30 22:20:21 +00:00
Owner

Follow-up to #69. Found by TestOER against merged dev (59b2de15).

The restore from #69 reaches memory but not IndexedDB whenever the predecessor came from the cache — which is every path with a page reload in it (open app, open resource, edit). So the 404 it was meant to prevent is still there on dev.

Cause — one line of applesauce

// applesauce-core/dist/helpers/event-cache.js:20
filter((e) => !isFromCache(e))

An event loaded through cacheRequest carries Symbol.for('from-cache'). publishEventOptimistic re-adds that same object, so persistEventsToCache skips it: eventStore.add restores the UI and the durable write silently never happens.

The marker's premise — it is already in the cache, no need to write it back — was true when it was set and is false by the time we restore, because uncacheEvent has just deleted that row.

TestOER's measurement:

same session            predecessor isFromCache=false  -> IDB=GOOD (restored)
RELOAD + addressLoader  predecessor isFromCache=true   -> IDB=EMPTY (never persisted)

insert$ fires in both cases ([GOOD, PHANTOM, GOOD]) and memory ends GOOD in both — the write is dropped one layer down.

Fix — direct write, not marker-clearing

recacheEvent puts the predecessor straight into IDB, after the un-cache and alongside the existing eventStore.add.

Chosen over stripping the symbol because it does not depend on an applesauce internal keeping its current shape — and because {...previous} does NOT strip it: object spread copies own enumerable symbol properties. There is a test asserting exactly that, so the tempting one-liner cannot be reintroduced by accident.

recacheEvent honours CACHEABLE_KINDS, which the insert$ writer applies too — a direct write must not smuggle in a kind we deliberately do not persist.

Verification

474 files / 5231 tests pass, exit 0    clean tree on 59b2de15
pnpm check 0 errors                    pnpm lint clean

Load-bearing checks confirmed by reverting what they cover:

no recacheEvent call     -> 2 fail
kind guard removed       -> 1 fail

The from-cache drop is also pinned directly against the real pipeline: an event carrying the marker is proved not to reach IDB through insert$, with a keeper event in the same batch so the absence cannot pass vacuously.

Not established

Not re-measured in a browser. TestOER's reload scenario at 59b2de15 is what found this, and the same scenario is what should confirm it.

Follow-up to #69. Found by TestOER against merged `dev` (`59b2de15`). The restore from #69 reaches memory but **not IndexedDB** whenever the predecessor came from the cache — which is every path with a page reload in it (open app, open resource, edit). So the 404 it was meant to prevent is still there on `dev`. ## Cause — one line of applesauce ```js // applesauce-core/dist/helpers/event-cache.js:20 filter((e) => !isFromCache(e)) ``` An event loaded through `cacheRequest` carries `Symbol.for('from-cache')`. `publishEventOptimistic` re-adds **that same object**, so `persistEventsToCache` skips it: `eventStore.add` restores the UI and the durable write silently never happens. The marker's premise — *it is already in the cache, no need to write it back* — was true when it was set and is **false** by the time we restore, because `uncacheEvent` has just deleted that row. TestOER's measurement: ``` same session predecessor isFromCache=false -> IDB=GOOD (restored) RELOAD + addressLoader predecessor isFromCache=true -> IDB=EMPTY (never persisted) ``` `insert$` fires in both cases (`[GOOD, PHANTOM, GOOD]`) and memory ends `GOOD` in both — the write is dropped one layer down. ## Fix — direct write, not marker-clearing `recacheEvent` puts the predecessor straight into IDB, after the un-cache and alongside the existing `eventStore.add`. Chosen over stripping the symbol because it does not depend on an applesauce internal keeping its current shape — and because **`{...previous}` does NOT strip it**: object spread copies own enumerable symbol properties. There is a test asserting exactly that, so the tempting one-liner cannot be reintroduced by accident. `recacheEvent` honours `CACHEABLE_KINDS`, which the `insert$` writer applies too — a direct write must not smuggle in a kind we deliberately do not persist. ## Verification ``` 474 files / 5231 tests pass, exit 0 clean tree on 59b2de15 pnpm check 0 errors pnpm lint clean ``` Load-bearing checks confirmed by reverting what they cover: ``` no recacheEvent call -> 2 fail kind guard removed -> 1 fail ``` The from-cache drop is also pinned directly against the real pipeline: an event carrying the marker is proved **not** to reach IDB through `insert$`, with a keeper event in the same batch so the absence cannot pass vacuously. ## Not established **Not re-measured in a browser.** TestOER's reload scenario at `59b2de15` is what found this, and the same scenario is what should confirm it.
The restore merged in #69 reaches memory but not IndexedDB whenever the
predecessor came from the cache — which is every path with a page reload
in it (open app, open resource, edit). So the 404 it was meant to prevent
is still there on dev, measured by TestOER at 59b2de15.

Cause is one line of applesauce:

    // applesauce-core/dist/helpers/event-cache.js:20
    filter((e) => !isFromCache(e))

An event loaded through `cacheRequest` carries `Symbol.for('from-cache')`.
`publishEventOptimistic` re-adds that same object, so `persistEventsToCache`
skips it: `eventStore.add` restores the UI and the durable write silently
never happens. The marker's premise — it is already in the cache, no need
to write it back — was true when it was set and is false by the time we
restore, because `uncacheEvent` has just deleted that row.

Fixed with a direct write rather than by clearing the marker. `recacheEvent`
puts the predecessor straight into IDB, after the un-cache and alongside the
existing `eventStore.add`. Chosen over stripping the symbol because it does
not depend on an applesauce internal keeping its current shape — and because
`{...previous}` does NOT strip it: object spread copies own enumerable
symbol properties. There is a test asserting exactly that, so the tempting
one-liner cannot be reintroduced by accident.

`recacheEvent` honours CACHEABLE_KINDS, which the insert$ writer applies
too — a direct write must not smuggle in a kind we deliberately do not
persist.

Verification: 474 files / 5231 tests pass, exit 0, at a clean tree on
59b2de15. lint clean, `pnpm check` 0 errors. Both new assertions confirmed
load-bearing by reverting what they cover: no `recacheEvent` call -> 2 fail;
kind guard removed -> 1 fail. The from-cache drop is also pinned directly
against the real pipeline — an event carrying the marker is proved NOT to
reach IDB through insert$, with a keeper event in the same batch so the
absence cannot pass vacuously.

NOT re-measured in a browser. TestOER's reload scenario at 59b2de15 is what
found this, and the same scenario is what should confirm it.

Refs #64
laoc merged commit e0aa0aba97 into dev 2026-07-30 22:20:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!71
No description provided.