Skip to content

Commit b8a2800

Browse files
authored
feat(directory): add a per-project Lobby cell beside the directory (#335)
* feat(directory): add a per-project Lobby cell beside the directory The directory's Worker gains a Lobby Durable Object, one per project ID, at GET /lobbies/<project> behind the same ACE_SECRET. Hosts hold one socket per project: they replace their owner's presence, which lives only in hibernation attachments, and send channel lifecycle events, which the lobby numbers with one seq per project and keeps as a 30-day, 2,000-event resume buffer. A host resuming from a pruned or unknown seq is told to read listings again. Pings are auto-answered and an alarm evicts sockets silent for 90 s. The Directory object and its contract are unchanged. Refs #17, #14. Why: services/directory/src/lobby.ts#const MIGRATIONS | Durable Object SQLite refuses PRAGMA user_version, so the schema version lives in the kv `version` key; never edit a shipped migration Why: services/directory/src/lobby.ts#method webSocketMessage | seq lives in kv apart from the rows so pruning every event never reuses a seq a host resumes from Why: services/directory/src/lobby.ts#type Attachment | presence must never reach SQLite; it lives only in socket attachments * lane: sync lobby-cell memory * fix(directory): keep lobby sweeps, clocks, and attachments within bounds Re-arm the 60 s sweep when a host joins a lobby whose alarm waits on the oldest event, prune by the lobby's own receipt time, fit presence into the 2 KiB hibernation attachment, replace sockets only within one host process, and answer a malformed project ID with 400. Why: services/directory/src/lobby.ts#method fetch | a joining host must re-arm the sweep when the alarm is further than SWEEP away, or silent sockets stay here until the oldest event expires Why: services/directory/src/lobby.ts#function fit | serializeAttachment throws above 2 KiB, so presence must be trimmed to fit before every serialize Why: services/directory/src/lobby.ts#method webSocketMessage | the events.at column is the lobby's receipt time, never the host's clock, since retention and resync depend on it * lane: sync lobby-cell memory * lane: sync lobby-cell memory * fix(directory): sweep lobbies every 5 minutes to bound alarm writes Each sweep re-arms the alarm, which Durable Objects bill as one row write, so a 60 s sweep cost 1,440 writes a day per project with a host connected. Every 5 minutes costs 288. Silent sockets still close after 90 s without a ping, now within about 6.5 minutes. Why: services/directory/src/lobby.ts#const SWEEP | the alarm re-arm is the lobby's only steady storage write; pings and presence must never write, and the sweep must not run more often than the write budget allows * lane: sync lobby-cell memory
1 parent e65cd91 commit b8a2800

16 files changed

Lines changed: 460 additions & 6 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: 01M4YSA7BQDTQ2QGZ23V3TEQDW
3+
anchor: 'method #latest'
4+
created: 2026-10-15T03:22:24Z
5+
norm: '1'
6+
---
7+
8+
seq lives in kv apart from the rows so pruning every event never reuses a seq a host resumes from
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: 01M4YSABZQZVRXYFE3TQP1H1K8
3+
anchor: method webSocketMessage
4+
created: 2026-10-15T03:22:29Z
5+
norm: '1'
6+
---
7+
8+
seq lives in the kv 'seq' key apart from the event rows, so pruning every event never reuses a seq a host resumes from
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: 01M4YSWBMHMXS6T8GZWWX71686
3+
anchor: 'method #latest'
4+
created: 2026-10-15T03:32:19Z
5+
norm: '1'
6+
---
7+
8+
seq lives in kv apart from the rows so pruning every event never reuses a seq a host resumes from
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M4YSA7B2JKQ12MX3B6DA66G1
3+
anchor: const MIGRATIONS
4+
created: 2026-10-15T03:22:24Z
5+
norm: '1'
6+
sig: 106ab0bf742bac08
7+
body_hash: 22e53bc349d0c6f1
8+
raw_hash: 7a63f5f5162b5252
9+
lines: 18-25
10+
---
11+
12+
Durable Object SQLite refuses PRAGMA user_version, so the schema version lives in the kv `version` key; never edit a shipped migration
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
id: 01M4YSA7C9D4AD1GN5TQP2JYZ7
3+
anchor: type Attachment
4+
created: 2026-10-15T03:22:25Z
5+
norm: '1'
6+
sig: 1b055bbb637ab6c2
7+
body_hash: e3b0c44298fc1c14
8+
raw_hash: 0a365dc77261089b
9+
vouched: 2026-10-15T04:07:57Z
10+
lines: 28-28
11+
---
12+
13+
presence must never reach SQLite; it lives only in socket attachments
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
id: 01M4YSWEM54B8WRTBBHRP331GE
3+
anchor: method webSocketMessage
4+
created: 2026-10-15T03:32:22Z
5+
norm: '1'
6+
sig: ba8d2f921d2933e4
7+
body_hash: ed970d004b996e65
8+
raw_hash: e8f7fe94b6c60eb8
9+
vouched: 2026-10-15T04:07:57Z
10+
lines: 63-98
11+
---
12+
13+
seq lives in kv apart from the rows so pruning every event never reuses a seq a host resumes from
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M4YVXHY7HPE7NG8FJ3HY2291
3+
anchor: method fetch
4+
created: 2026-10-15T04:07:55Z
5+
norm: '1'
6+
sig: 34a9c2419f926c50
7+
body_hash: 640988774703d52e
8+
raw_hash: c970c77cfaff901e
9+
lines: 76-87
10+
---
11+
12+
a joining host must re-arm the sweep when the alarm is further than SWEEP away, or silent sockets stay here until the oldest event expires
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M4YVXHYES1JA1G03YKYBV47S
3+
anchor: function fit
4+
created: 2026-10-15T04:07:55Z
5+
norm: '1'
6+
sig: 1e227ead5527c329
7+
body_hash: c9ad6fbf34e09ff9
8+
raw_hash: e3e43b6c5e9c565b
9+
lines: 43-52
10+
---
11+
12+
serializeAttachment throws above 2 KiB, so presence must be trimmed to fit before every serialize
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M4YVXHYN15MPDM4TPR658TT7
3+
anchor: method webSocketMessage
4+
created: 2026-10-15T04:07:55Z
5+
norm: '1'
6+
sig: ba8d2f921d2933e4
7+
body_hash: ed970d004b996e65
8+
raw_hash: e8f7fe94b6c60eb8
9+
lines: 89-131
10+
---
11+
12+
the events.at column is the lobby's receipt time, never the host's clock, since retention and resync depend on it
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: 01M4ZGW2CM5CC9W0QZGSYF3R35
3+
anchor: const SWEEP
4+
created: 2026-10-15T10:14:06Z
5+
norm: '1'
6+
sig: b3fc1afe94e7106a
7+
body_hash: e3b0c44298fc1c14
8+
raw_hash: 5cffbee4659df889
9+
lines: 16-16
10+
---
11+
12+
the alarm re-arm is the lobby's only steady storage write; pings and presence must never write, and the sweep must not run more often than the write budget allows

0 commit comments

Comments
 (0)