feat: resource caps, Saved Messages, author walls, docs for node bring-up
Node flags (cmd/node/main.go):
--max-cpu / --max-ram-mb — Go runtime caps (GOMAXPROCS / GOMEMLIMIT)
--feed-disk-limit-mb — hard 507 refusal for new post bodies over quota
--chain-disk-limit-mb — advisory watcher (can't reject blocks without
breaking consensus; logs WARN every minute)
Client — Saved Messages (self-chat):
- Auto-created on sign-in, pinned top of chat list, blue bookmark avatar
- Send short-circuits the relay (no encrypt, no fee, no mailbox hop)
- Empty state rendered outside inverted FlatList — fixes the mirrored
"say hi…" on Android RTL-aware layout builds
- PostCard shows "You" for own posts instead of the self-contact alias
Client — user walls:
- New route /(app)/feed/author/[pub] with infinite-scroll via
`created_at` cursor and pull-to-refresh
- Profile screen gains "View posts" button (universal) next to
"Open chat" (contact-only)
Feed pipeline:
- Bump client JPEG quality 0.5 → 0.75 to match server scrubber (Q=75),
so a 60 KiB compose doesn't balloon past 256 KiB after server re-encode
- ErrPostTooLarge now wraps with the actual size vs cap, errors.Is
preserved in the HTTP layer
- FeedMailbox quota + DiskUsage surface — supports new CLI flag
README:
- Step-by-step "first node / joiner" section on the landing page,
full flag tables incl. the new resource-cap group, minimal
checklists for open/private/low-end deployments
This commit is contained in:
@@ -97,24 +97,35 @@ type FeedPost struct {
|
||||
// ErrPostTooLarge is returned by Store when the post body exceeds MaxPostBodySize.
|
||||
var ErrPostTooLarge = errors.New("post body exceeds maximum allowed size")
|
||||
|
||||
// ErrFeedQuotaExceeded is returned by Store when the on-disk footprint
|
||||
// (LSM + value log) plus the incoming post would exceed the operator-set
|
||||
// disk quota. Ops set this via --feed-disk-limit-mb. Zero = unlimited.
|
||||
var ErrFeedQuotaExceeded = errors.New("feed mailbox disk quota exceeded")
|
||||
|
||||
// FeedMailbox stores feed post bodies.
|
||||
type FeedMailbox struct {
|
||||
db *badger.DB
|
||||
ttl time.Duration
|
||||
db *badger.DB
|
||||
ttl time.Duration
|
||||
quotaBytes int64 // 0 = unlimited
|
||||
}
|
||||
|
||||
// NewFeedMailbox wraps an already-open Badger DB. TTL controls how long
|
||||
// post bodies live before auto-eviction (on-chain metadata persists
|
||||
// forever independently).
|
||||
func NewFeedMailbox(db *badger.DB, ttl time.Duration) *FeedMailbox {
|
||||
// forever independently). quotaBytes caps the on-disk footprint; 0 or
|
||||
// negative means unlimited.
|
||||
func NewFeedMailbox(db *badger.DB, ttl time.Duration, quotaBytes int64) *FeedMailbox {
|
||||
if ttl <= 0 {
|
||||
ttl = time.Duration(FeedPostDefaultTTLDays) * 24 * time.Hour
|
||||
}
|
||||
return &FeedMailbox{db: db, ttl: ttl}
|
||||
if quotaBytes < 0 {
|
||||
quotaBytes = 0
|
||||
}
|
||||
return &FeedMailbox{db: db, ttl: ttl, quotaBytes: quotaBytes}
|
||||
}
|
||||
|
||||
// OpenFeedMailbox opens (or creates) a dedicated BadgerDB at dbPath.
|
||||
func OpenFeedMailbox(dbPath string, ttl time.Duration) (*FeedMailbox, error) {
|
||||
// quotaBytes caps the total on-disk footprint (LSM + vlog); 0 = unlimited.
|
||||
func OpenFeedMailbox(dbPath string, ttl time.Duration, quotaBytes int64) (*FeedMailbox, error) {
|
||||
opts := badger.DefaultOptions(dbPath).
|
||||
WithLogger(nil).
|
||||
WithValueLogFileSize(128 << 20).
|
||||
@@ -124,9 +135,19 @@ func OpenFeedMailbox(dbPath string, ttl time.Duration) (*FeedMailbox, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open feed mailbox db: %w", err)
|
||||
}
|
||||
return NewFeedMailbox(db, ttl), nil
|
||||
return NewFeedMailbox(db, ttl, quotaBytes), nil
|
||||
}
|
||||
|
||||
// DiskUsage returns the current on-disk footprint (LSM + value log) in
|
||||
// bytes. Cheap — Badger tracks these counters internally.
|
||||
func (fm *FeedMailbox) DiskUsage() int64 {
|
||||
lsm, vlog := fm.db.Size()
|
||||
return lsm + vlog
|
||||
}
|
||||
|
||||
// Quota returns the configured disk quota in bytes. 0 = unlimited.
|
||||
func (fm *FeedMailbox) Quota() int64 { return fm.quotaBytes }
|
||||
|
||||
// Close releases the underlying Badger handle.
|
||||
func (fm *FeedMailbox) Close() error { return fm.db.Close() }
|
||||
|
||||
@@ -139,7 +160,23 @@ func (fm *FeedMailbox) Close() error { return fm.db.Close() }
|
||||
func (fm *FeedMailbox) Store(post *FeedPost, createdAt int64) ([]string, error) {
|
||||
size := estimatePostSize(post)
|
||||
if size > MaxPostBodySize {
|
||||
return nil, ErrPostTooLarge
|
||||
// Wrap the sentinel so the HTTP layer can still errors.Is() on it
|
||||
// while the operator / client sees the actual offending numbers.
|
||||
// This catches the common case where the client's pre-scrub
|
||||
// estimate is below the cap but the server re-encode (quality=75
|
||||
// JPEG) inflates past it.
|
||||
return nil, fmt.Errorf("%w: size %d > max %d (after server scrub)",
|
||||
ErrPostTooLarge, size, MaxPostBodySize)
|
||||
}
|
||||
// Disk quota: refuse new bodies once we're already over the cap.
|
||||
// `size` is a post-body estimate, not the exact BadgerDB write-amp
|
||||
// cost; we accept that slack — the goal is a coarse guard-rail so
|
||||
// an operator's disk doesn't blow up unnoticed. Exceeding nodes
|
||||
// still serve existing posts; only new Store() calls are refused.
|
||||
if fm.quotaBytes > 0 {
|
||||
if fm.DiskUsage()+int64(size) > fm.quotaBytes {
|
||||
return nil, ErrFeedQuotaExceeded
|
||||
}
|
||||
}
|
||||
|
||||
post.CreatedAt = createdAt
|
||||
|
||||
Reference in New Issue
Block a user