Repository navigation
fix(thread): prune reply drafts already published after a mid-mine exit - #915
Merged
Merged
Conversation
A proof-of-work reply publishes from a background job that outlives the composer. If the page unloads between the relay accepting the event and the job's own draft cleanup, the draft record survives the reload - and the thread page offers to continue words that are already on the wire (reported: reply published, still prompted to continue the same draft). The job's cleanup only runs inside its own page instance, so no in-page guard can close this. Instead the thread page reconciles: once replies have loaded, drop any stored draft of this account whose raw text exactly matches a published reply in the thread. Exact match only - a draft the member edited since, or fresh words for a new reply, never matches and always survives. - replyDraftStore: prunePublishedReplyDrafts(match) - sweep + persist, returns the dropped ids. - [nip19] page: pruneDraftsMatchingPublished() runs reactively once replies exist, matching draft.pubkey + draft.text === published r.content (no-mention replies are identical in both forms; media/poll URLs ride the wire content, so exact match holds there too). - replyDraftStore.test: 4 new cases - match dropped, edited draft kept, other-account draft kept, partial sweep persists the rest.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
frontend | 6082090 | Oct 30 2026, 02:25 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
zapcooking-frontend | 6082090 | Oct 30 2026, 02:24 AM |
Deploying frontend with
|
| Latest commit: |
6082090
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://fffef6f6.frontend-hvd.pages.dev |
| Branch Preview URL: | https://fix-stale-reply-draft-after.frontend-hvd.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A proof-of-work reply publishes from a background job (
startMinedReply) that outlives the composer. The job deletes the draft when the relays accept the reply — but that cleanup only runs inside the page instance that started the mine. Step out of the app mid-publish (navigate away, close the tab) and the job dies before it cleans up: the draft record survives the reload, and the thread page offers to continue words that are already on the wire.Reported from a 390px phone: reply visible in the thread as published, and the reply box still prefilled with "Continue your reply — ".
No in-page guard can close this — the page that would run the guard is gone. The fix has to reconcile on the way back in.
The fix
replyDraftStore:prunePublishedReplyDrafts(match)— sweeps stored drafts through a caller-supplied match predicate, deletes the matches, persists, returns the dropped ids. Account-scoped and exact-match by contract of the caller.Thread page (
[nip19]/+page.svelte):pruneDraftsMatchingPublished()— runs reactively once replies have loaded. Drops any stored draft of the viewing account whose raw text exactly equals a published reply's wire content in this thread.Exact match is the safety property: a draft the member edited since the publish, or fresh words for a new reply, never matches exactly and always survives the sweep. Replies without mentions (the common case) are identical in raw and wire form, which is the state this catches; media/poll URLs ride the wire content, so exact match holds for those too.
E2E evidence
Real flow against a live relay (
wss://nos.lol), fresh test identity, mobile viewport (390×844):Testing
replyDraftStore.test.ts: 4 new cases — exact-match dropped, edited draft kept, other-account draft kept, partial sweep persists the survivors.vitest run src/lib/replyDraftStore.test.ts src/lib/comments/minedReply.test.ts: 24/24 passed.🧂 Seasoned by GLM 5.3