fix(calendar): write updated events to the EventStore so the cache is not left stale (#62) #63
No reviewers
Labels
No labels
bug
discussion
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
edufeed/edufeed-app!63
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/issue-62-stale-cache-after-update"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #62.
The bug
Editing a calendar event publishes the replacement to the relays but never offers it to the local cache, so every reload of the detail page renders the pre-edit version — indefinitely.
Measured by @TestOER: after a save, the relay held exactly one event at the coordinate carrying the new title, while the page rendered the old one. So the relay is fine; the client serves a stale read.
Mechanism
calendar/event/[naddr]/+page.js:5isssr = false;load()callsfetchEventById, which isfirstValueFrom(addressLoader(...))(nostrUtils.js:243-245).cacheRequest(loaders/base.js:41-43), andaddressPointerLoadingSequencedrops a pointer fromremainingas soon as any event at that address arrives. A cache hit ends the sequence — relay hints, additional relays and lookup relays are never tried.eventStore.insert$(event-cache.svelte.js:70-78), and 31922/31923 are explicitly cacheable.createEventusespublishEventOptimistic, which doeseventStore.add(publish-service.js:244).updateEventusespublishEvent, which does not — andcalendar-actions.svelte.jsdid not importeventStoreat all.That explains the apparent counter-evidence too: creation passes (nothing cached yet) and the prefill test passes (it asserts the original title, which is exactly what the stale read returns).
Updating
calendarStoreis not a substitute: that is the list view's state, not the detail page's read path.Why not just swap in
publishEventOptimisticIts failure path is
eventStore.remove(signedEvent)(publish-service.js:333), and the cache pipeline is insert-only — no code path deletes a single event from IndexedDB. For an addressable kind that is worse than a leak: nostr-idb keys bykind:pubkey:d, newest-wins, so a rejected publish leaves a phantom that overwrites the last good version, and by the cache-hit short-circuit above the relay is never asked to correct it.So:
await publishEvent(...), theneventStore.add(signed)only on success — the shapeEditProfileModal,pin-list-serviceandhelpers/comments.jsalready use.The add is best-effort.
eventStore.addvalidates and throws on a malformed event; by then the publish has already landed, so a cache-write failure must not surface asFailed to update calendar event. It degrades to the old stale-read behaviour, matching the "cache is ADDITIVE" contract inevent-cache.svelte.js.Tests
New
src/lib/__tests__/calendar-update-event-eventstore.test.js, 5 cases: the add happens and carries the new title at the same coordinate; the signed event is added rather than thedTag-decorated return value; no add whensuccessCountis 0; a throwingadddoes not fail the update; and an ordering control that publish precedes add.Negative control: with the source change reverted, 4 of the 5 fail. The fifth ("does NOT cache when the publish failed") passes vacuously without the fix — stated so nobody reads 5/5 as five independent guards.
Verification
5185/5185, 469 files, with.envexported (see #55 — without it the suite is red for unrelated reasons).pnpm check0 errors (4 pre-existing warnings in test fixtures),pnpm lintclean.Errors: NonGlobalFAB.test.jsis the known #55-family teardown race, not this change: 3 consecutive isolated runs in this worktree give 0 errors, and it appears on untouched worktrees too.calendar-editingtests green.Not fixed here, filed separately
Same missing-
eventStore.addshape found while in the area, all confirmed by grep rather than assumed:relay-settings-service.js:66(kind 10002)grep -c eventStorereturns 0, 10002 is cacheable, and it is the relay list itself: a stale read there mis-routes every subsequent querycalendar-actions.svelte.js:279(kind 31924)createEventviapublishEventOptimisticpin-list-service.js:102,135eventStore.addat 103 and 136calendarActions.deleteEvent(:234)eventStore.addand nocacheDeletion, but has zero callers (grep -rn '\.deleteEvent(' src/is empty; the UI goes throughhelpers/eventDeletion.js). Dead code encoding the wrong pattern — left in place because deleting it is outside this fixAlso noted: the all-day toggle flips 31922<->31923 while keeping the d-tag, so the coordinate changes and nothing is replaced. Documented on #62; not the cause of the three failing tests, none of which touch the toggle.
The EventStore write alone was necessary but not sufficient. A replacement that lands in the same wall-clock second as its predecessor is dropped by three independent layers, with two different tie-breaks: - relays (NIP-01): on equal created_at the LOWER id wins — a coin flip, so the edit survives on the relay only about half the time; - applesauce EventStore: same rule, `incomingBeatsWinner` in event-store/event-store.js; - nostr-idb: strict `event.created_at > existing` in database/insert.js, so on a tie the IDB write is ALWAYS rejected. Only the relay layer is a coin flip. The cache is deterministically stale, and because the IDB cache is the first step of applesauce's address-loader sequence and a cache hit ends that sequence, no relay ever corrects it. So a same-second edit reads to the user as "the save did nothing" even in the runs where the relay accepted the replacement. Stamp created_at as max(now, existing.created_at + 1). The bump only closes a tie: an edit made a second or more after the event it replaces still gets wall-clock time. Reachable outside the tests by a user editing straight after creating, and by two tabs replacing the same coordinate. Diagnosed by TestOER from a full relay recording of a failing e2e run: in the one attempt that passed, the create's d-tag was minted 23ms before a second boundary; in the five that failed there was 232-474ms of headroom, so create and update shared a second. Refs: #62