Skip to main content

max / goingson

2.8 KB · 55 lines History Blame Raw
1 -- An outbox, and send-later with it.
2 --
3 -- Ruled by Max 2026-08-22 (task a3c76a24): "have GoingsOn use an outbox model
4 -- explicitly and leverage that to make send-later a first class feature."
5 --
6 -- WHAT LED HERE, so the columns are not mistaken for plumbing. `send_email` is
7 -- async: it opens an SMTP connection, and `commands/email/send.rs` has seven
8 -- `.await` sites. A described route handler is synchronous, by
9 -- `quasi_router`'s Decision 6, which exists so egui and a terminal do not need
10 -- a runtime. So compose could not be described at all while sending was
11 -- something a button did.
12 --
13 -- Queueing is a local write, so it can be. And an outbox is not the consolation
14 -- prize for that: it is a message you can see before it goes and stop, a
15 -- message you can schedule, and a send that survives being offline instead of
16 -- failing at the instant you pressed the button. Eudora shipped exactly this
17 -- and named the distinction Send versus Queue.
18 --
19 -- A QUEUED MESSAGE IS A DRAFT THAT HAS BEEN COMMITTED TO SEND, so it stays in
20 -- `emails` rather than moving to a table of its own. Eudora's Out mailbox is
21 -- the same idea. `save_draft`, the drafts list and the compose screen loading
22 -- one all keep working with no change, and the outbox is a query rather than a
23 -- second store that has to be kept in step:
24 --
25 -- is_draft = 1 AND queued_at IS NOT NULL
26 --
27 -- Every column is NULL for a message nobody queued, so this migration changes
28 -- nothing about an existing row and needs no backfill.
29 --
30 -- `emails` is per-device and not synced (see `syncstore::manifest`), so there
31 -- are no sync triggers to rewrite here. That is also the honest behaviour: a
32 -- queued message belongs to the machine that will send it, and two machines
33 -- draining one outbox would send twice.
34
35 ALTER TABLE emails ADD COLUMN queued_at TEXT;
36
37 -- The instant the message may leave. NULL means as soon as the drainer next
38 -- wakes, which is the ordinary case; a value is send-later, and it is the whole
39 -- of that feature. Compared against `now` by the drainer rather than by a
40 -- timer, so a message scheduled while the app was shut still goes when the app
41 -- comes back rather than being missed.
42 ALTER TABLE emails ADD COLUMN send_after TEXT;
43
44 -- How many times the drainer has tried. Kept so a message that cannot go says
45 -- so rather than retrying silently forever, and so the outbox can back off.
46 ALTER TABLE emails ADD COLUMN send_attempts INTEGER NOT NULL DEFAULT 0;
47
48 -- Why the last attempt failed, for the outbox to show. NULL once an attempt
49 -- succeeds, which never persists: a sent message stops being a draft.
50 ALTER TABLE emails ADD COLUMN send_error TEXT;
51
52 -- What the drainer asks for on every wake: queued, due, oldest first. Partial,
53 -- because the outbox is a handful of rows in a table of tens of thousands.
54 CREATE INDEX idx_emails_outbox ON emails(queued_at) WHERE queued_at IS NOT NULL;
55