Skip to content

fix(thread): prune reply drafts already published after a mid-mine exit - #915

Merged
spe1020 merged 5 commits into
mainfrom
fix/stale-reply-draft-after-published-mine
Oct 30, 2026
Merged

spe1020 merged 5 commits into
mainfrom
fix/stale-reply-draft-after-published-mine

Conversation

@dmnyc

@dmnyc dmnyc commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

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):

  1. Parent note published, thread opened, reply typed in the inline composer — blur saved the draft to localStorage (verified present, keyed on the parent's id).
  2. Reply submitted with PoW 20 — mining indicator up, composer cleared, draft handed to the background job.
  3. Stepped out mid-publish (navigated away before the job's cleanup ran).
  4. Returned to the thread: reply visible as published, and the composer still prefilled with the published words — the bug, reproduced:

Bug: composer prefilled with already-published words

  1. With the fix restored: same stale-draft state, reload → the reconcile sweep drops the draft, composer comes back clean, published reply stays in the thread:

Fixed: composer clean after reconcile

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.
  • E2E as above, both states captured on a live relay with a fresh identity.

🧂 Seasoned by GLM 5.3

dmnyc added 2 commits October 25, 2026 14:17
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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
frontend 6082090 Oct 30 2026, 02:25 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
zapcooking-frontend 6082090 Oct 30 2026, 02:24 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Deploying frontend with  Cloudflare Pages  Cloudflare Pages

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

View logs

@spe1020
spe1020 merged commit bf73cac into main Oct 30, 2026
6 of 8 checks passed
@spe1020
spe1020 deleted the fix/stale-reply-draft-after-published-mine branch October 30, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants