Skip to main content

max / makenotwork

multithreaded: chat message storage and retention The storage half of MT's chat build (wiki livechat-design). No routes and no trait impls yet; this is the layer they sit on. 038 creates chat_messages. BIGINT identity rather than UUID because the reconnect cursor (?after=<id>) needs a total order and chat is log-shaped; this is livechat::MessageId. Three indexes, one per access path that exists: (community_id, id) for backlog replay, (expires_at) for the sweep, (community_id, author_id) for the ban purge, which runs on a hot path at the moment the room is busiest. 039 adds chat_retention_hours and chat_max_messages to communities. 037 gave a community a chat_policy but nothing to build a livechat::Retention from, and the owner settings screen needs somewhere to write. Both NOT NULL with defaults matching Retention::forum_default(), both CHECK-bounded against the crate ceilings, and deliberately no NULL: that would be exactly the "keep forever" sentinel Retention has no way to express. Expiry is computed in SQL as now() + make_interval rather than bound as a chrono timestamp. Not style: the server build unifies sqlx's time and chrono features, so the compile-time macro infers a TIMESTAMPTZ bind as time::OffsetDateTime, and unlike an output column a bind parameter's type cannot be overridden. Computing it server-side keeps all nine statements compile-time checked, which mutations/moderation.rs had to give up. Both halves of retention are one statement each. The age sweep is a global indexed delete, not per-room, since expiry is already on the row. The count cap is one window-function delete across the whole table rather than a query per community per tick, most of which would find nothing. Recompute restamps from created_at, not now(), or an owner touching the setting would silently extend every old message by a full window. 17 storage tests; 311 integration and 178 unit tests pass, clippy and fmt clean.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-08 18:23 UTC
Signed with PGP, not checked
Commit: 64233e8467e2eaf7c0c4ff16bb3b432623ec6abc
Parent: f3025d7
18 files changed, +1002 insertions, -8 deletions
@@ -4819,14 +4819,6 @@
4819 4819 source = "registry+https://github.com/rust-lang/crates.io-index"
4820 4820 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
4821 4821
4822 - [[patch.unused]]
4823 - name = "kberg"
4824 - version = "0.1.0"
4825 -
4826 - [[patch.unused]]
4827 - name = "painhours"
4828 - version = "0.1.0"
4829 -
4830 4822 [[patch.unused]]
4831 4823 name = "synckit-client"
4832 4824 version = "0.8.0"
@@ -4834,3 +4826,11 @@
4834 4826 [[patch.unused]]
4835 4827 name = "synckit-config"
4836 4828 version = "0.2.0"
4829 +
4830 + [[patch.unused]]
4831 + name = "kberg"
4832 + version = "0.1.0"
4833 +
4834 + [[patch.unused]]
4835 + name = "painhours"
4836 + version = "0.1.0"
@@ -3,6 +3,7 @@
3 3 mod admin_queries;
4 4 mod auth;
5 5 mod bans;
6 + mod chat_storage;
6 7 mod community_state;
7 8 mod crud;
8 9 mod csrf;
@@ -6,6 +6,7 @@
6 6 use uuid::Uuid;
7 7
8 8 mod category;
9 + mod chat;
9 10 mod community;
10 11 mod endorsement;
11 12 mod footnote;
@@ -21,6 +22,7 @@
21 22 mod user;
22 23
23 24 pub use category::*;
25 + pub use chat::*;
24 26 pub use community::*;
25 27 pub use endorsement::*;
26 28 pub use footnote::*;
@@ -46,6 +46,7 @@
46 46
47 47 mod admin;
48 48 mod category;
49 + mod chat;
49 50 mod community;
50 51 mod endorsement;
51 52 mod footnote;
@@ -63,6 +64,7 @@
63 64
64 65 pub use admin::*;
65 66 pub use category::*;
67 + pub use chat::*;
66 68 pub use community::*;
67 69 pub use endorsement::*;
68 70 pub use footnote::*;
@@ -1,0 +1,41 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT id,\n author_id,\n body_html,\n EXTRACT(EPOCH FROM created_at)::BIGINT AS \"created_at!\"\n FROM chat_messages\n WHERE community_id = $1\n ORDER BY id DESC\n LIMIT $2",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Int8"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "author_id",
14 + "type_info": "Uuid"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "body_html",
19 + "type_info": "Text"
20 + },
21 + {
22 + "ordinal": 3,
23 + "name": "created_at!",
24 + "type_info": "Int8"
25 + }
26 + ],
27 + "parameters": {
28 + "Left": [
29 + "Uuid",
30 + "Int8"
31 + ]
32 + },
33 + "nullable": [
34 + false,
35 + false,
36 + false,
37 + null
38 + ]
39 + },
40 + "hash": "0ef15820f2a3379772ffdd8f278714efba504a1e29c884862ef05f63dc8958b0"
41 + }
@@ -1,0 +1,15 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "DELETE FROM chat_messages WHERE community_id = $1 AND author_id = $2",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": [
8 + "Uuid",
9 + "Uuid"
10 + ]
11 + },
12 + "nullable": []
13 + },
14 + "hash": "1201fb0fac2f479e2bd4107aad514f5ee69c156dfc7be3abe8b5debf63e74583"
15 + }
@@ -1,0 +1,12 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "DELETE FROM chat_messages WHERE expires_at < now()",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": []
8 + },
9 + "nullable": []
10 + },
11 + "hash": "57cb472786f62a3768367b5f5b864e1645cf00227513edd85f77c5419ffabbbb"
12 + }
@@ -1,0 +1,23 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT author_id FROM chat_messages WHERE community_id = $1 AND id = $2",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "author_id",
9 + "type_info": "Uuid"
10 + }
11 + ],
12 + "parameters": {
13 + "Left": [
14 + "Uuid",
15 + "Int8"
16 + ]
17 + },
18 + "nullable": [
19 + false
20 + ]
21 + },
22 + "hash": "610ed2513b6cf027ba05411dd1c157b2da8c75cd025f93eaa448c47f6a981bbe"
23 + }
@@ -1,0 +1,15 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "DELETE FROM chat_messages WHERE community_id = $1 AND id = $2",
4 + "describe": {
5 + "columns": [],
6 + "parameters": {
7 + "Left": [
8 + "Uuid",
9 + "Int8"
10 + ]
11 + },
12 + "nullable": []
13 + },
14 + "hash": "79162206eec1d9bf6b9424aac8fcb3b6158a6f318a576acd01a17d053036eb13"
15 + }
@@ -1,0 +1,31 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "INSERT INTO chat_messages (community_id, author_id, body_html, expires_at)\n VALUES ($1, $2, $3, now() + make_interval(hours => $4))\n RETURNING id, EXTRACT(EPOCH FROM created_at)::BIGINT AS \"created_at!\"",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Int8"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "created_at!",
14 + "type_info": "Int8"
15 + }
16 + ],
17 + "parameters": {
18 + "Left": [
19 + "Uuid",
20 + "Uuid",
21 + "Text",
22 + "Int4"
23 + ]
24 + },
25 + "nullable": [
26 + false,
27 + null
28 + ]
29 + },
30 + "hash": "838adf82ee4c1e8c378626e84d3c7a6f7f3733d8cba64aa71e4991b654b49ff9"
31 + }