Skip to content

Email visitors who don't have the chat open almost straight away - #7

Merged
rowanseymour merged 2 commits into
mainfrom
rowan/webchat-email-offline-fast
Oct 29, 2026
Merged

rowanseymour merged 2 commits into
mainfrom
rowan/webchat-email-offline-fast

Conversation

@rowanseymour

@rowanseymour rowanseymour commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

For a visitor who doesn't have the chat open, email is the conversation, so holding their messages back for 3 minutes only delays the reply. Chip now emails them within about 15 seconds. Visitors with the chat open still get the 3-minute wait, since they may be reading along.

A chat's presence key has no unsubscribe callback. It lapses 150s after the last subscription refresh, so a visitor without it has already been gone a while. That signal carries its own delay, which is why chip doesn't need to wait much longer on top of it.

Changes

  • core/email/pending.go: the hold script checks the chat's presence key, the same one publishing checks. It does this atomically with the hold, so there's no extra round trip and no separate failure that could skip it.
    • The wait is 10s if the visitor isn't connected. That's long enough for a chat that's only starting to load a welcome message from history and report it read, and for back-to-back messages to go as one email. It's 3m if they're connected, and the 30m cap is unchanged.
    • The latest message now sets the due time. One held back after the visitor leaves brings forward anything held while they were there, and one held back after they return puts it off again.
    • While a failed send waits to retry, the due time still only moves later, so the backoff isn't cut short.
  • core/email/sweeper.go: the sweeper ticks every 5s instead of 30s, since the tick sets how late a fast email can be. Each tick is one ZRANGEBYSCORE when nothing is due.

Behavior

Visitor Before After
Doesn't have the chat open emailed after 3m quiet (+ up to 30s) emailed after 10s quiet (+ up to 5s)
Has the chat open, doesn't read after 3m quiet (+ up to 30s) after 3m quiet (+ up to 5s)
Leaves after a message was held that message waits out the 3m the next message brings it forward
Comes back before a fast email goes emailed after 10s the next message puts it back to 3m

Test plan

  • TestSendHoldsForEmail: due in 10s with no subscriber, 3m with one
  • TestSweeper:
    • an offline hold brings forward a connected one
    • a connected hold puts an offline one back to 3m
    • held messages go in one email
    • an offline hold during retry backoff doesn't shorten it
  • go test ./... passes in the devcontainer

Comment thread web/internal.go Resolved
@claude

claude Bot commented Oct 29, 2026

Copy link
Copy Markdown

I didn't find any correctness bugs. I traced the new due-time logic in holdScript against claim and requeue:

  • Hold while a send is in flight: claim has deleted the state, so the hold sees no attempts and does a plain ZADD. If that send then fails, requeue uses ZADD GT with the retry delay, so the backoff still wins. If the send succeeds, the new message gets its own short wait, which is what we want.
  • Hold while waiting to retry: the HEXISTS attempts guard keeps GT, which the new test covers.
  • Due time moving earlier: it's still capped by first + maxHold, so a chatty conversation can't push it out indefinitely.
  • Presence check: it uses the same key and the same 150s TTL as publishing. If the visitor is "offline" here, the publish was dropped too, so they never saw the message live. That makes the shorter wait consistent.

Minor (inline): a failed presence check now skips the hold entirely. It also costs an extra connection and round trip per send, which could go inside holdScript.

Test gap: nothing covers the isSubscribed error path, or a connected hold arriving after an offline one. The second case now pushes a nearly-due email back to 3m. That looks intended, but a test would pin it down.

The 5s sweep interval is fine: when nothing is due, each instance runs one ZRANGEBYSCORE per tick.

@rowanseymour
rowanseymour merged commit 47162d5 into main Oct 29, 2026
2 checks passed
@rowanseymour
rowanseymour deleted the rowan/webchat-email-offline-fast branch October 29, 2026 20:54
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant