···292292293293### 4.1 Feed Fetching
294294295295-A background scheduler polls subscribed feeds on a fixed 5-minute tick. Feeds are fetched at most once per cycle regardless of how many users share them.
296296-297297-```
298298- ┌─────────────────────────┐
299299- │ Feed Scheduler │
300300- │ (background goroutine) │
301301- └────────┬────────────────┘
302302- │ every 5 min
303303- ┌────────▼────────────────┐
304304- │ Feed Fetcher │
305305- │ │
306306- │ 1. SELECT feeds where │
307307- │ subscriber_count > 0 │
308308- │ AND not fetched in │
309309- │ last 30 min │
310310- │ 2. Dedup in-flight: │
311311- │ skip if already │
312312- │ being fetched │
313313- │ 3. Respect ETag/If-None│
314314- │ Match / Last-Modified│
315315- │ 4. GET feed URL │
316316- │ 5. Parse XML/JSON │
317317- │ 6. Upsert articles │
318318- │ 7. Update feed metadata│
319319- └────────┬────────────────┘
320320- │
321321- ┌──────────────┼──────────────┐
322322- │ │ │
323323- ┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
324324- │RSS/XML │ │RSS1/RDF │ │Atom/XML │ │JSON Feed │
325325- │Parser │ │Parser │ │Parser │ │Parser │
326326- └─────────┘ └───────────┘ └───────────┘ └───────────┘
327327-```
295295+A background scheduler polls subscribed feeds on a configurable tick. Feeds are fetched at most once per cycle regardless of how many users share them.
328296329297### 4.2 Fetch Schedule
330298···334302- **Staleness threshold**: Feeds not fetched in the last 30 minutes are eligible
335303- **Subscriber filter**: Only feeds with `subscriber_count > 0` are fetched
336304- **In-flight dedup**: If a feed is already being fetched (e.g., manual refresh and background scheduler overlap), the second caller waits for the first to complete rather than fetching again
337337-- **HTTP cache**: Honor `ETag` and `Last-Modified` headers to skip parsing when nothing changed (304 Not Modified)
338305- **Error tracking**: `error_count` increments on failure, resets to 0 on success. Feeds with high error counts are surfaced as "dead feeds" to the user.
339306340307```sql
···534501 last_fetched_at DATETIME,
535502 last_error TEXT,
536503 subscriber_count INTEGER NOT NULL DEFAULT 0,
537537- etag TEXT,
538538- last_modified TEXT,
539504 consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0,
540505 error_count INTEGER NOT NULL DEFAULT 0,
541506 favicon_url TEXT
+1-2
internal/atproto/stream_handler.go
···7575 return err
76767777 case actionDelete:
7878- parsed, ok := ParseRecordURI(event.URI)
7878+ _, ok := ParseRecordURI(event.URI)
7979 if !ok {
8080 return nil
8181 }
···8383 if err == nil && sub != nil {
8484 return h.articles.DeleteSubscription(ctx, event.DID, sub.FeedURL)
8585 }
8686- _ = parsed
8786 }
8887 return nil
8988}
-2
internal/db/db.go
···173173 last_fetched_at DATETIME,
174174 last_error TEXT,
175175 subscriber_count INTEGER NOT NULL DEFAULT 0,
176176- etag TEXT,
177177- last_modified TEXT,
178176 consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0,
179177 error_count INTEGER NOT NULL DEFAULT 0,
180178 favicon_url TEXT